![Todd Poynor](/assets/img/avatar_default.png)
Debug logs for device-specific callback invocation aren't very useful, remove. Signed-off-by: Todd Poynor <toddpoynor@google.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1818 lines
50 KiB
C
1818 lines
50 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Gasket generic driver framework. This file contains the implementation
|
|
* for the Gasket generic driver framework - the functionality that is common
|
|
* across Gasket devices.
|
|
*
|
|
* Copyright (C) 2018 Google, Inc.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
|
|
#include "gasket_core.h"
|
|
|
|
#include "gasket_interrupt.h"
|
|
#include "gasket_ioctl.h"
|
|
#include "gasket_page_table.h"
|
|
#include "gasket_sysfs.h"
|
|
|
|
#include <linux/capability.h>
|
|
#include <linux/compiler.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/device.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/init.h>
|
|
#include <linux/of.h>
|
|
#include <linux/pid_namespace.h>
|
|
#include <linux/printk.h>
|
|
#include <linux/sched.h>
|
|
|
|
#ifdef GASKET_KERNEL_TRACE_SUPPORT
|
|
#define CREATE_TRACE_POINTS
|
|
#include <trace/events/gasket_mmap.h>
|
|
#else
|
|
#define trace_gasket_mmap_exit(x)
|
|
#define trace_gasket_mmap_entry(x, ...)
|
|
#endif
|
|
|
|
/*
|
|
* "Private" members of gasket_driver_desc.
|
|
*
|
|
* Contains internal per-device type tracking data, i.e., data not appropriate
|
|
* as part of the public interface for the generic framework.
|
|
*/
|
|
struct gasket_internal_desc {
|
|
/* Device-specific-driver-provided configuration information. */
|
|
const struct gasket_driver_desc *driver_desc;
|
|
|
|
/* Protects access to per-driver data (i.e. this structure). */
|
|
struct mutex mutex;
|
|
|
|
/* Kernel-internal device class. */
|
|
struct class *class;
|
|
|
|
/* Instantiated / present devices of this type. */
|
|
struct gasket_dev *devs[GASKET_DEV_MAX];
|
|
};
|
|
|
|
/* do_map_region() needs be able to return more than just true/false. */
|
|
enum do_map_region_status {
|
|
/* The region was successfully mapped. */
|
|
DO_MAP_REGION_SUCCESS,
|
|
|
|
/* Attempted to map region and failed. */
|
|
DO_MAP_REGION_FAILURE,
|
|
|
|
/* The requested region to map was not part of a mappable region. */
|
|
DO_MAP_REGION_INVALID,
|
|
};
|
|
|
|
/* Global data definitions. */
|
|
/* Mutex - only for framework-wide data. Other data should be protected by
|
|
* finer-grained locks.
|
|
*/
|
|
static DEFINE_MUTEX(g_mutex);
|
|
|
|
/* List of all registered device descriptions & their supporting data. */
|
|
static struct gasket_internal_desc g_descs[GASKET_FRAMEWORK_DESC_MAX];
|
|
|
|
/* Mapping of statuses to human-readable strings. Must end with {0,NULL}. */
|
|
static const struct gasket_num_name gasket_status_name_table[] = {
|
|
{ GASKET_STATUS_DEAD, "DEAD" },
|
|
{ GASKET_STATUS_ALIVE, "ALIVE" },
|
|
{ GASKET_STATUS_LAMED, "LAMED" },
|
|
{ GASKET_STATUS_DRIVER_EXIT, "DRIVER_EXITING" },
|
|
{ 0, NULL },
|
|
};
|
|
|
|
/* Enumeration of the automatic Gasket framework sysfs nodes. */
|
|
enum gasket_sysfs_attribute_type {
|
|
ATTR_BAR_OFFSETS,
|
|
ATTR_BAR_SIZES,
|
|
ATTR_DRIVER_VERSION,
|
|
ATTR_FRAMEWORK_VERSION,
|
|
ATTR_DEVICE_TYPE,
|
|
ATTR_HARDWARE_REVISION,
|
|
ATTR_PCI_ADDRESS,
|
|
ATTR_STATUS,
|
|
ATTR_IS_DEVICE_OWNED,
|
|
ATTR_DEVICE_OWNER,
|
|
ATTR_WRITE_OPEN_COUNT,
|
|
ATTR_RESET_COUNT,
|
|
ATTR_USER_MEM_RANGES
|
|
};
|
|
|
|
/* Perform a standard Gasket callback. */
|
|
static inline int
|
|
check_and_invoke_callback(struct gasket_dev *gasket_dev,
|
|
int (*cb_function)(struct gasket_dev *))
|
|
{
|
|
int ret = 0;
|
|
|
|
if (cb_function) {
|
|
mutex_lock(&gasket_dev->mutex);
|
|
ret = cb_function(gasket_dev);
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* Perform a standard Gasket callback without grabbing gasket_dev->mutex. */
|
|
static inline int
|
|
gasket_check_and_invoke_callback_nolock(struct gasket_dev *gasket_dev,
|
|
int (*cb_function)(struct gasket_dev *))
|
|
{
|
|
int ret = 0;
|
|
|
|
if (cb_function)
|
|
ret = cb_function(gasket_dev);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Return nonzero if the gasket_cdev_info is owned by the current thread group
|
|
* ID.
|
|
*/
|
|
static int gasket_owned_by_current_tgid(struct gasket_cdev_info *info)
|
|
{
|
|
return (info->ownership.is_owned &&
|
|
(info->ownership.owner == current->tgid));
|
|
}
|
|
|
|
/*
|
|
* Find the next free gasket_internal_dev slot.
|
|
*
|
|
* Returns the located slot number on success or a negative number on failure.
|
|
*/
|
|
static int gasket_find_dev_slot(struct gasket_internal_desc *internal_desc,
|
|
const char *kobj_name)
|
|
{
|
|
int i;
|
|
|
|
mutex_lock(&internal_desc->mutex);
|
|
|
|
/* Search for a previous instance of this device. */
|
|
for (i = 0; i < GASKET_DEV_MAX; i++) {
|
|
if (internal_desc->devs[i] &&
|
|
strcmp(internal_desc->devs[i]->kobj_name, kobj_name) == 0) {
|
|
pr_err("Duplicate device %s\n", kobj_name);
|
|
mutex_unlock(&internal_desc->mutex);
|
|
return -EBUSY;
|
|
}
|
|
}
|
|
|
|
/* Find a free device slot. */
|
|
for (i = 0; i < GASKET_DEV_MAX; i++) {
|
|
if (!internal_desc->devs[i])
|
|
break;
|
|
}
|
|
|
|
if (i == GASKET_DEV_MAX) {
|
|
pr_err("Too many registered devices; max %d\n", GASKET_DEV_MAX);
|
|
mutex_unlock(&internal_desc->mutex);
|
|
return -EBUSY;
|
|
}
|
|
|
|
mutex_unlock(&internal_desc->mutex);
|
|
return i;
|
|
}
|
|
|
|
/*
|
|
* Allocate and initialize a Gasket device structure, add the device to the
|
|
* device list.
|
|
*
|
|
* Returns 0 if successful, a negative error code otherwise.
|
|
*/
|
|
static int gasket_alloc_dev(struct gasket_internal_desc *internal_desc,
|
|
struct device *parent, struct gasket_dev **pdev)
|
|
{
|
|
int dev_idx;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
internal_desc->driver_desc;
|
|
struct gasket_dev *gasket_dev;
|
|
struct gasket_cdev_info *dev_info;
|
|
const char *parent_name = dev_name(parent);
|
|
|
|
pr_debug("Allocating a Gasket device, parent %s.\n", parent_name);
|
|
|
|
*pdev = NULL;
|
|
|
|
dev_idx = gasket_find_dev_slot(internal_desc, parent_name);
|
|
if (dev_idx < 0)
|
|
return dev_idx;
|
|
|
|
gasket_dev = *pdev = kzalloc(sizeof(*gasket_dev), GFP_KERNEL);
|
|
if (!gasket_dev) {
|
|
pr_err("no memory for device, parent %s\n", parent_name);
|
|
return -ENOMEM;
|
|
}
|
|
internal_desc->devs[dev_idx] = gasket_dev;
|
|
|
|
mutex_init(&gasket_dev->mutex);
|
|
|
|
gasket_dev->internal_desc = internal_desc;
|
|
gasket_dev->dev_idx = dev_idx;
|
|
snprintf(gasket_dev->kobj_name, GASKET_NAME_MAX, "%s", parent_name);
|
|
gasket_dev->dev = get_device(parent);
|
|
/* gasket_bar_data is uninitialized. */
|
|
gasket_dev->num_page_tables = driver_desc->num_page_tables;
|
|
/* max_page_table_size and *page table are uninit'ed */
|
|
/* interrupt_data is not initialized. */
|
|
/* status is 0, or GASKET_STATUS_DEAD */
|
|
|
|
dev_info = &gasket_dev->dev_info;
|
|
snprintf(dev_info->name, GASKET_NAME_MAX, "%s_%u", driver_desc->name,
|
|
gasket_dev->dev_idx);
|
|
dev_info->devt =
|
|
MKDEV(driver_desc->major, driver_desc->minor +
|
|
gasket_dev->dev_idx);
|
|
dev_info->device =
|
|
device_create(internal_desc->class, parent, dev_info->devt,
|
|
gasket_dev, dev_info->name);
|
|
|
|
/* cdev has not yet been added; cdev_added is 0 */
|
|
dev_info->gasket_dev_ptr = gasket_dev;
|
|
/* ownership is all 0, indicating no owner or opens. */
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Free a Gasket device. */
|
|
static void gasket_free_dev(struct gasket_dev *gasket_dev)
|
|
{
|
|
struct gasket_internal_desc *internal_desc = gasket_dev->internal_desc;
|
|
|
|
mutex_lock(&internal_desc->mutex);
|
|
internal_desc->devs[gasket_dev->dev_idx] = NULL;
|
|
mutex_unlock(&internal_desc->mutex);
|
|
put_device(gasket_dev->dev);
|
|
kfree(gasket_dev);
|
|
}
|
|
|
|
/*
|
|
* Maps the specified bar into kernel space.
|
|
*
|
|
* Returns 0 on success, a negative error code otherwise.
|
|
* A zero-sized BAR will not be mapped, but is not an error.
|
|
*/
|
|
static int gasket_map_pci_bar(struct gasket_dev *gasket_dev, int bar_num)
|
|
{
|
|
struct gasket_internal_desc *internal_desc = gasket_dev->internal_desc;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
internal_desc->driver_desc;
|
|
ulong desc_bytes = driver_desc->bar_descriptions[bar_num].size;
|
|
int ret;
|
|
|
|
if (desc_bytes == 0)
|
|
return 0;
|
|
|
|
if (driver_desc->bar_descriptions[bar_num].type != PCI_BAR) {
|
|
/* not PCI: skip this entry */
|
|
return 0;
|
|
}
|
|
/*
|
|
* pci_resource_start and pci_resource_len return a "resource_size_t",
|
|
* which is safely castable to ulong (which itself is the arg to
|
|
* request_mem_region).
|
|
*/
|
|
gasket_dev->bar_data[bar_num].phys_base =
|
|
(ulong)pci_resource_start(gasket_dev->pci_dev, bar_num);
|
|
if (!gasket_dev->bar_data[bar_num].phys_base) {
|
|
dev_err(gasket_dev->dev, "Cannot get BAR%u base address\n",
|
|
bar_num);
|
|
return -EINVAL;
|
|
}
|
|
|
|
gasket_dev->bar_data[bar_num].length_bytes =
|
|
(ulong)pci_resource_len(gasket_dev->pci_dev, bar_num);
|
|
if (gasket_dev->bar_data[bar_num].length_bytes < desc_bytes) {
|
|
dev_err(gasket_dev->dev,
|
|
"PCI BAR %u space is too small: %lu; expected >= %lu\n",
|
|
bar_num, gasket_dev->bar_data[bar_num].length_bytes,
|
|
desc_bytes);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (!request_mem_region(gasket_dev->bar_data[bar_num].phys_base,
|
|
gasket_dev->bar_data[bar_num].length_bytes,
|
|
gasket_dev->dev_info.name)) {
|
|
dev_err(gasket_dev->dev,
|
|
"Cannot get BAR %d memory region %p\n",
|
|
bar_num, &gasket_dev->pci_dev->resource[bar_num]);
|
|
return -EINVAL;
|
|
}
|
|
|
|
gasket_dev->bar_data[bar_num].virt_base =
|
|
ioremap_nocache(gasket_dev->bar_data[bar_num].phys_base,
|
|
gasket_dev->bar_data[bar_num].length_bytes);
|
|
if (!gasket_dev->bar_data[bar_num].virt_base) {
|
|
dev_err(gasket_dev->dev,
|
|
"Cannot remap BAR %d memory region %p\n",
|
|
bar_num, &gasket_dev->pci_dev->resource[bar_num]);
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
dma_set_mask(&gasket_dev->pci_dev->dev, DMA_BIT_MASK(64));
|
|
dma_set_coherent_mask(&gasket_dev->pci_dev->dev, DMA_BIT_MASK(64));
|
|
|
|
return 0;
|
|
|
|
fail:
|
|
iounmap(gasket_dev->bar_data[bar_num].virt_base);
|
|
release_mem_region(gasket_dev->bar_data[bar_num].phys_base,
|
|
gasket_dev->bar_data[bar_num].length_bytes);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Releases PCI BAR mapping.
|
|
*
|
|
* A zero-sized or not-mapped BAR will not be unmapped, but is not an error.
|
|
*/
|
|
static void gasket_unmap_pci_bar(struct gasket_dev *dev, int bar_num)
|
|
{
|
|
ulong base, bytes;
|
|
struct gasket_internal_desc *internal_desc = dev->internal_desc;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
internal_desc->driver_desc;
|
|
|
|
if (driver_desc->bar_descriptions[bar_num].size == 0 ||
|
|
!dev->bar_data[bar_num].virt_base)
|
|
return;
|
|
|
|
if (driver_desc->bar_descriptions[bar_num].type != PCI_BAR)
|
|
return;
|
|
|
|
iounmap(dev->bar_data[bar_num].virt_base);
|
|
dev->bar_data[bar_num].virt_base = NULL;
|
|
|
|
base = pci_resource_start(dev->pci_dev, bar_num);
|
|
if (!base) {
|
|
dev_err(dev->dev, "cannot get PCI BAR%u base address\n",
|
|
bar_num);
|
|
return;
|
|
}
|
|
|
|
bytes = pci_resource_len(dev->pci_dev, bar_num);
|
|
release_mem_region(base, bytes);
|
|
}
|
|
|
|
/*
|
|
* Setup PCI memory mapping for the specified device.
|
|
*
|
|
* Reads the BAR registers and sets up pointers to the device's memory mapped
|
|
* IO space.
|
|
*
|
|
* Returns 0 on success and a negative value otherwise.
|
|
*/
|
|
static int gasket_setup_pci(struct pci_dev *pci_dev,
|
|
struct gasket_dev *gasket_dev)
|
|
{
|
|
int i, mapped_bars, ret;
|
|
|
|
for (i = 0; i < GASKET_NUM_BARS; i++) {
|
|
ret = gasket_map_pci_bar(gasket_dev, i);
|
|
if (ret) {
|
|
mapped_bars = i;
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail:
|
|
for (i = 0; i < mapped_bars; i++)
|
|
gasket_unmap_pci_bar(gasket_dev, i);
|
|
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* Unmaps memory for the specified device. */
|
|
static void gasket_cleanup_pci(struct gasket_dev *gasket_dev)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < GASKET_NUM_BARS; i++)
|
|
gasket_unmap_pci_bar(gasket_dev, i);
|
|
}
|
|
|
|
/* Determine the health of the Gasket device. */
|
|
static int gasket_get_hw_status(struct gasket_dev *gasket_dev)
|
|
{
|
|
int status;
|
|
int i;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
|
|
status = gasket_check_and_invoke_callback_nolock(gasket_dev,
|
|
driver_desc->device_status_cb);
|
|
if (status != GASKET_STATUS_ALIVE) {
|
|
dev_dbg(gasket_dev->dev, "Hardware reported status %d.\n",
|
|
status);
|
|
return status;
|
|
}
|
|
|
|
status = gasket_interrupt_system_status(gasket_dev);
|
|
if (status != GASKET_STATUS_ALIVE) {
|
|
dev_dbg(gasket_dev->dev,
|
|
"Interrupt system reported status %d.\n", status);
|
|
return status;
|
|
}
|
|
|
|
for (i = 0; i < driver_desc->num_page_tables; ++i) {
|
|
status = gasket_page_table_system_status(gasket_dev->page_table[i]);
|
|
if (status != GASKET_STATUS_ALIVE) {
|
|
dev_dbg(gasket_dev->dev,
|
|
"Page table %d reported status %d.\n",
|
|
i, status);
|
|
return status;
|
|
}
|
|
}
|
|
|
|
return GASKET_STATUS_ALIVE;
|
|
}
|
|
|
|
static ssize_t
|
|
gasket_write_mappable_regions(char *buf,
|
|
const struct gasket_driver_desc *driver_desc,
|
|
int bar_index)
|
|
{
|
|
int i;
|
|
ssize_t written;
|
|
ssize_t total_written = 0;
|
|
ulong min_addr, max_addr;
|
|
struct gasket_bar_desc bar_desc =
|
|
driver_desc->bar_descriptions[bar_index];
|
|
|
|
if (bar_desc.permissions == GASKET_NOMAP)
|
|
return 0;
|
|
for (i = 0;
|
|
i < bar_desc.num_mappable_regions && total_written < PAGE_SIZE;
|
|
i++) {
|
|
min_addr = bar_desc.mappable_regions[i].start -
|
|
driver_desc->legacy_mmap_address_offset;
|
|
max_addr = bar_desc.mappable_regions[i].start -
|
|
driver_desc->legacy_mmap_address_offset +
|
|
bar_desc.mappable_regions[i].length_bytes;
|
|
written = scnprintf(buf, PAGE_SIZE - total_written,
|
|
"0x%08lx-0x%08lx\n", min_addr, max_addr);
|
|
total_written += written;
|
|
buf += written;
|
|
}
|
|
return total_written;
|
|
}
|
|
|
|
static ssize_t gasket_sysfs_data_show(struct device *device,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
int i, ret = 0;
|
|
ssize_t current_written = 0;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
struct gasket_dev *gasket_dev;
|
|
struct gasket_sysfs_attribute *gasket_attr;
|
|
const struct gasket_bar_desc *bar_desc;
|
|
enum gasket_sysfs_attribute_type sysfs_type;
|
|
|
|
gasket_dev = gasket_sysfs_get_device_data(device);
|
|
if (!gasket_dev) {
|
|
dev_err(device, "No sysfs mapping found for device\n");
|
|
return 0;
|
|
}
|
|
|
|
gasket_attr = gasket_sysfs_get_attr(device, attr);
|
|
if (!gasket_attr) {
|
|
dev_err(device, "No sysfs attr found for device\n");
|
|
gasket_sysfs_put_device_data(device, gasket_dev);
|
|
return 0;
|
|
}
|
|
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
|
|
sysfs_type =
|
|
(enum gasket_sysfs_attribute_type)gasket_attr->data.attr_type;
|
|
switch (sysfs_type) {
|
|
case ATTR_BAR_OFFSETS:
|
|
for (i = 0; i < GASKET_NUM_BARS; i++) {
|
|
bar_desc = &driver_desc->bar_descriptions[i];
|
|
if (bar_desc->size == 0)
|
|
continue;
|
|
current_written =
|
|
snprintf(buf, PAGE_SIZE - ret, "%d: 0x%lx\n", i,
|
|
(ulong)bar_desc->base);
|
|
buf += current_written;
|
|
ret += current_written;
|
|
}
|
|
break;
|
|
case ATTR_BAR_SIZES:
|
|
for (i = 0; i < GASKET_NUM_BARS; i++) {
|
|
bar_desc = &driver_desc->bar_descriptions[i];
|
|
if (bar_desc->size == 0)
|
|
continue;
|
|
current_written =
|
|
snprintf(buf, PAGE_SIZE - ret, "%d: 0x%lx\n", i,
|
|
(ulong)bar_desc->size);
|
|
buf += current_written;
|
|
ret += current_written;
|
|
}
|
|
break;
|
|
case ATTR_DRIVER_VERSION:
|
|
ret = snprintf(buf, PAGE_SIZE, "%s\n",
|
|
gasket_dev->internal_desc->driver_desc->driver_version);
|
|
break;
|
|
case ATTR_FRAMEWORK_VERSION:
|
|
ret = snprintf(buf, PAGE_SIZE, "%s\n",
|
|
GASKET_FRAMEWORK_VERSION);
|
|
break;
|
|
case ATTR_DEVICE_TYPE:
|
|
ret = snprintf(buf, PAGE_SIZE, "%s\n",
|
|
gasket_dev->internal_desc->driver_desc->name);
|
|
break;
|
|
case ATTR_HARDWARE_REVISION:
|
|
ret = snprintf(buf, PAGE_SIZE, "%d\n",
|
|
gasket_dev->hardware_revision);
|
|
break;
|
|
case ATTR_PCI_ADDRESS:
|
|
ret = snprintf(buf, PAGE_SIZE, "%s\n", gasket_dev->kobj_name);
|
|
break;
|
|
case ATTR_STATUS:
|
|
ret = snprintf(buf, PAGE_SIZE, "%s\n",
|
|
gasket_num_name_lookup(gasket_dev->status,
|
|
gasket_status_name_table));
|
|
break;
|
|
case ATTR_IS_DEVICE_OWNED:
|
|
ret = snprintf(buf, PAGE_SIZE, "%d\n",
|
|
gasket_dev->dev_info.ownership.is_owned);
|
|
break;
|
|
case ATTR_DEVICE_OWNER:
|
|
ret = snprintf(buf, PAGE_SIZE, "%d\n",
|
|
gasket_dev->dev_info.ownership.owner);
|
|
break;
|
|
case ATTR_WRITE_OPEN_COUNT:
|
|
ret = snprintf(buf, PAGE_SIZE, "%d\n",
|
|
gasket_dev->dev_info.ownership.write_open_count);
|
|
break;
|
|
case ATTR_RESET_COUNT:
|
|
ret = snprintf(buf, PAGE_SIZE, "%d\n", gasket_dev->reset_count);
|
|
break;
|
|
case ATTR_USER_MEM_RANGES:
|
|
for (i = 0; i < GASKET_NUM_BARS; ++i) {
|
|
current_written =
|
|
gasket_write_mappable_regions(buf, driver_desc,
|
|
i);
|
|
buf += current_written;
|
|
ret += current_written;
|
|
}
|
|
break;
|
|
default:
|
|
dev_dbg(gasket_dev->dev, "Unknown attribute: %s\n",
|
|
attr->attr.name);
|
|
ret = 0;
|
|
break;
|
|
}
|
|
|
|
gasket_sysfs_put_attr(device, gasket_attr);
|
|
gasket_sysfs_put_device_data(device, gasket_dev);
|
|
return ret;
|
|
}
|
|
|
|
/* These attributes apply to all Gasket driver instances. */
|
|
static const struct gasket_sysfs_attribute gasket_sysfs_generic_attrs[] = {
|
|
GASKET_SYSFS_RO(bar_offsets, gasket_sysfs_data_show, ATTR_BAR_OFFSETS),
|
|
GASKET_SYSFS_RO(bar_sizes, gasket_sysfs_data_show, ATTR_BAR_SIZES),
|
|
GASKET_SYSFS_RO(driver_version, gasket_sysfs_data_show,
|
|
ATTR_DRIVER_VERSION),
|
|
GASKET_SYSFS_RO(framework_version, gasket_sysfs_data_show,
|
|
ATTR_FRAMEWORK_VERSION),
|
|
GASKET_SYSFS_RO(device_type, gasket_sysfs_data_show, ATTR_DEVICE_TYPE),
|
|
GASKET_SYSFS_RO(revision, gasket_sysfs_data_show,
|
|
ATTR_HARDWARE_REVISION),
|
|
GASKET_SYSFS_RO(pci_address, gasket_sysfs_data_show, ATTR_PCI_ADDRESS),
|
|
GASKET_SYSFS_RO(status, gasket_sysfs_data_show, ATTR_STATUS),
|
|
GASKET_SYSFS_RO(is_device_owned, gasket_sysfs_data_show,
|
|
ATTR_IS_DEVICE_OWNED),
|
|
GASKET_SYSFS_RO(device_owner, gasket_sysfs_data_show,
|
|
ATTR_DEVICE_OWNER),
|
|
GASKET_SYSFS_RO(write_open_count, gasket_sysfs_data_show,
|
|
ATTR_WRITE_OPEN_COUNT),
|
|
GASKET_SYSFS_RO(reset_count, gasket_sysfs_data_show, ATTR_RESET_COUNT),
|
|
GASKET_SYSFS_RO(user_mem_ranges, gasket_sysfs_data_show,
|
|
ATTR_USER_MEM_RANGES),
|
|
GASKET_END_OF_ATTR_ARRAY
|
|
};
|
|
|
|
/* Add a char device and related info. */
|
|
static int gasket_add_cdev(struct gasket_cdev_info *dev_info,
|
|
const struct file_operations *file_ops,
|
|
struct module *owner)
|
|
{
|
|
int ret;
|
|
|
|
cdev_init(&dev_info->cdev, file_ops);
|
|
dev_info->cdev.owner = owner;
|
|
ret = cdev_add(&dev_info->cdev, dev_info->devt, 1);
|
|
if (ret) {
|
|
dev_err(dev_info->gasket_dev_ptr->dev,
|
|
"cannot add char device [ret=%d]\n", ret);
|
|
return ret;
|
|
}
|
|
dev_info->cdev_added = 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Disable device operations. */
|
|
void gasket_disable_device(struct gasket_dev *gasket_dev)
|
|
{
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
int i;
|
|
|
|
/* Only delete the device if it has been successfully added. */
|
|
if (gasket_dev->dev_info.cdev_added)
|
|
cdev_del(&gasket_dev->dev_info.cdev);
|
|
|
|
gasket_dev->status = GASKET_STATUS_DEAD;
|
|
|
|
gasket_interrupt_cleanup(gasket_dev);
|
|
|
|
for (i = 0; i < driver_desc->num_page_tables; ++i) {
|
|
if (gasket_dev->page_table[i]) {
|
|
gasket_page_table_reset(gasket_dev->page_table[i]);
|
|
gasket_page_table_cleanup(gasket_dev->page_table[i]);
|
|
}
|
|
}
|
|
}
|
|
EXPORT_SYMBOL(gasket_disable_device);
|
|
|
|
/*
|
|
* Registered driver descriptor lookup for PCI devices.
|
|
*
|
|
* Precondition: Called with g_mutex held (to avoid a race on return).
|
|
* Returns NULL if no matching device was found.
|
|
*/
|
|
static struct gasket_internal_desc *
|
|
lookup_pci_internal_desc(struct pci_dev *pci_dev)
|
|
{
|
|
int i;
|
|
|
|
__must_hold(&g_mutex);
|
|
for (i = 0; i < GASKET_FRAMEWORK_DESC_MAX; i++) {
|
|
if (g_descs[i].driver_desc &&
|
|
g_descs[i].driver_desc->pci_id_table &&
|
|
pci_match_id(g_descs[i].driver_desc->pci_id_table, pci_dev))
|
|
return &g_descs[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Verifies that the user has permissions to perform the requested mapping and
|
|
* that the provided descriptor/range is of adequate size to hold the range to
|
|
* be mapped.
|
|
*/
|
|
static bool gasket_mmap_has_permissions(struct gasket_dev *gasket_dev,
|
|
struct vm_area_struct *vma,
|
|
int bar_permissions)
|
|
{
|
|
int requested_permissions;
|
|
/* Always allow sysadmin to access. */
|
|
if (capable(CAP_SYS_ADMIN))
|
|
return true;
|
|
|
|
/* Never allow non-sysadmins to access to a dead device. */
|
|
if (gasket_dev->status != GASKET_STATUS_ALIVE) {
|
|
dev_dbg(gasket_dev->dev, "Device is dead.\n");
|
|
return false;
|
|
}
|
|
|
|
/* Make sure that no wrong flags are set. */
|
|
requested_permissions =
|
|
(vma->vm_flags & (VM_WRITE | VM_READ | VM_EXEC));
|
|
if (requested_permissions & ~(bar_permissions)) {
|
|
dev_dbg(gasket_dev->dev,
|
|
"Attempting to map a region with requested permissions "
|
|
"0x%x, but region has permissions 0x%x.\n",
|
|
requested_permissions, bar_permissions);
|
|
return false;
|
|
}
|
|
|
|
/* Do not allow a non-owner to write. */
|
|
if ((vma->vm_flags & VM_WRITE) &&
|
|
!gasket_owned_by_current_tgid(&gasket_dev->dev_info)) {
|
|
dev_dbg(gasket_dev->dev,
|
|
"Attempting to mmap a region for write without owning "
|
|
"device.\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Verifies that the input address is within the region allocated to coherent
|
|
* buffer.
|
|
*/
|
|
static bool
|
|
gasket_is_coherent_region(const struct gasket_driver_desc *driver_desc,
|
|
ulong address)
|
|
{
|
|
struct gasket_coherent_buffer_desc coh_buff_desc =
|
|
driver_desc->coherent_buffer_description;
|
|
|
|
if (coh_buff_desc.permissions != GASKET_NOMAP) {
|
|
if ((address >= coh_buff_desc.base) &&
|
|
(address < coh_buff_desc.base + coh_buff_desc.size)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int gasket_get_bar_index(const struct gasket_dev *gasket_dev,
|
|
ulong phys_addr)
|
|
{
|
|
int i;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
for (i = 0; i < GASKET_NUM_BARS; ++i) {
|
|
struct gasket_bar_desc bar_desc =
|
|
driver_desc->bar_descriptions[i];
|
|
|
|
if (bar_desc.permissions != GASKET_NOMAP) {
|
|
if (phys_addr >= bar_desc.base &&
|
|
phys_addr < (bar_desc.base + bar_desc.size)) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
/* If we haven't found the address by now, it is invalid. */
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* Sets the actual bounds to map, given the device's mappable region.
|
|
*
|
|
* Given the device's mappable region, along with the user-requested mapping
|
|
* start offset and length of the user region, determine how much of this
|
|
* mappable region can be mapped into the user's region (start/end offsets),
|
|
* and the physical offset (phys_offset) into the BAR where the mapping should
|
|
* begin (either the VMA's or region lower bound).
|
|
*
|
|
* In other words, this calculates the overlap between the VMA
|
|
* (bar_offset, requested_length) and the given gasket_mappable_region.
|
|
*
|
|
* Returns true if there's anything to map, and false otherwise.
|
|
*/
|
|
static bool
|
|
gasket_mm_get_mapping_addrs(const struct gasket_mappable_region *region,
|
|
ulong bar_offset, ulong requested_length,
|
|
struct gasket_mappable_region *mappable_region,
|
|
ulong *virt_offset)
|
|
{
|
|
ulong range_start = region->start;
|
|
ulong range_length = region->length_bytes;
|
|
ulong range_end = range_start + range_length;
|
|
|
|
*virt_offset = 0;
|
|
if (bar_offset + requested_length < range_start) {
|
|
/*
|
|
* If the requested region is completely below the range,
|
|
* there is nothing to map.
|
|
*/
|
|
return false;
|
|
} else if (bar_offset <= range_start) {
|
|
/* If the bar offset is below this range's start
|
|
* but the requested length continues into it:
|
|
* 1) Only map starting from the beginning of this
|
|
* range's phys. offset, so we don't map unmappable
|
|
* memory.
|
|
* 2) The length of the virtual memory to not map is the
|
|
* delta between the bar offset and the
|
|
* mappable start (and since the mappable start is
|
|
* bigger, start - req.)
|
|
* 3) The map length is the minimum of the mappable
|
|
* requested length (requested_length - virt_offset)
|
|
* and the actual mappable length of the range.
|
|
*/
|
|
mappable_region->start = range_start;
|
|
*virt_offset = range_start - bar_offset;
|
|
mappable_region->length_bytes =
|
|
min(requested_length - *virt_offset, range_length);
|
|
return true;
|
|
} else if (bar_offset > range_start &&
|
|
bar_offset < range_end) {
|
|
/*
|
|
* If the bar offset is within this range:
|
|
* 1) Map starting from the bar offset.
|
|
* 2) Because there is no forbidden memory between the
|
|
* bar offset and the range start,
|
|
* virt_offset is 0.
|
|
* 3) The map length is the minimum of the requested
|
|
* length and the remaining length in the buffer
|
|
* (range_end - bar_offset)
|
|
*/
|
|
mappable_region->start = bar_offset;
|
|
*virt_offset = 0;
|
|
mappable_region->length_bytes =
|
|
min(requested_length, range_end - bar_offset);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* If the requested [start] offset is above range_end,
|
|
* there's nothing to map.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Calculates the offset where the VMA range begins in its containing BAR.
|
|
* The offset is written into bar_offset on success.
|
|
* Returns zero on success, anything else on error.
|
|
*/
|
|
static int gasket_mm_vma_bar_offset(const struct gasket_dev *gasket_dev,
|
|
const struct vm_area_struct *vma,
|
|
ulong *bar_offset)
|
|
{
|
|
ulong raw_offset;
|
|
int bar_index;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
|
|
raw_offset = (vma->vm_pgoff << PAGE_SHIFT) +
|
|
driver_desc->legacy_mmap_address_offset;
|
|
bar_index = gasket_get_bar_index(gasket_dev, raw_offset);
|
|
if (bar_index < 0) {
|
|
dev_err(gasket_dev->dev,
|
|
"Unable to find matching bar for address 0x%lx\n",
|
|
raw_offset);
|
|
trace_gasket_mmap_exit(bar_index);
|
|
return bar_index;
|
|
}
|
|
*bar_offset =
|
|
raw_offset - driver_desc->bar_descriptions[bar_index].base;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int gasket_mm_unmap_region(const struct gasket_dev *gasket_dev,
|
|
struct vm_area_struct *vma,
|
|
const struct gasket_mappable_region *map_region)
|
|
{
|
|
ulong bar_offset;
|
|
ulong virt_offset;
|
|
struct gasket_mappable_region mappable_region;
|
|
int ret;
|
|
|
|
if (map_region->length_bytes == 0)
|
|
return 0;
|
|
|
|
ret = gasket_mm_vma_bar_offset(gasket_dev, vma, &bar_offset);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!gasket_mm_get_mapping_addrs(map_region, bar_offset,
|
|
vma->vm_end - vma->vm_start,
|
|
&mappable_region, &virt_offset))
|
|
return 1;
|
|
|
|
/*
|
|
* The length passed to zap_vma_ptes MUST BE A MULTIPLE OF
|
|
* PAGE_SIZE! Trust me. I have the scars.
|
|
*
|
|
* Next multiple of y: ceil_div(x, y) * y
|
|
*/
|
|
zap_vma_ptes(vma, vma->vm_start + virt_offset,
|
|
DIV_ROUND_UP(mappable_region.length_bytes, PAGE_SIZE) *
|
|
PAGE_SIZE);
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(gasket_mm_unmap_region);
|
|
|
|
/* Maps a virtual address + range to a physical offset of a BAR. */
|
|
static enum do_map_region_status
|
|
do_map_region(const struct gasket_dev *gasket_dev, struct vm_area_struct *vma,
|
|
struct gasket_mappable_region *mappable_region)
|
|
{
|
|
/* Maximum size of a single call to io_remap_pfn_range. */
|
|
/* I pulled this number out of thin air. */
|
|
const ulong max_chunk_size = 64 * 1024 * 1024;
|
|
ulong chunk_size, mapped_bytes = 0;
|
|
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
|
|
ulong bar_offset, virt_offset;
|
|
struct gasket_mappable_region region_to_map;
|
|
ulong phys_offset, map_length;
|
|
ulong virt_base, phys_base;
|
|
int bar_index, ret;
|
|
|
|
ret = gasket_mm_vma_bar_offset(gasket_dev, vma, &bar_offset);
|
|
if (ret)
|
|
return DO_MAP_REGION_INVALID;
|
|
|
|
if (!gasket_mm_get_mapping_addrs(mappable_region, bar_offset,
|
|
vma->vm_end - vma->vm_start,
|
|
®ion_to_map, &virt_offset))
|
|
return DO_MAP_REGION_INVALID;
|
|
phys_offset = region_to_map.start;
|
|
map_length = region_to_map.length_bytes;
|
|
|
|
virt_base = vma->vm_start + virt_offset;
|
|
bar_index =
|
|
gasket_get_bar_index(gasket_dev,
|
|
(vma->vm_pgoff << PAGE_SHIFT) +
|
|
driver_desc->legacy_mmap_address_offset);
|
|
phys_base = gasket_dev->bar_data[bar_index].phys_base + phys_offset;
|
|
while (mapped_bytes < map_length) {
|
|
/*
|
|
* io_remap_pfn_range can take a while, so we chunk its
|
|
* calls and call cond_resched between each.
|
|
*/
|
|
chunk_size = min(max_chunk_size, map_length - mapped_bytes);
|
|
|
|
cond_resched();
|
|
ret = io_remap_pfn_range(vma, virt_base + mapped_bytes,
|
|
(phys_base + mapped_bytes) >>
|
|
PAGE_SHIFT, chunk_size,
|
|
vma->vm_page_prot);
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev,
|
|
"Error remapping PFN range.\n");
|
|
goto fail;
|
|
}
|
|
mapped_bytes += chunk_size;
|
|
}
|
|
|
|
return DO_MAP_REGION_SUCCESS;
|
|
|
|
fail:
|
|
/* Unmap the partial chunk we mapped. */
|
|
mappable_region->length_bytes = mapped_bytes;
|
|
if (gasket_mm_unmap_region(gasket_dev, vma, mappable_region))
|
|
dev_err(gasket_dev->dev,
|
|
"Error unmapping partial region 0x%lx (0x%lx bytes)\n",
|
|
(ulong)virt_offset,
|
|
(ulong)mapped_bytes);
|
|
|
|
return DO_MAP_REGION_FAILURE;
|
|
}
|
|
|
|
/* Map a region of coherent memory. */
|
|
static int gasket_mmap_coherent(struct gasket_dev *gasket_dev,
|
|
struct vm_area_struct *vma)
|
|
{
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
const ulong requested_length = vma->vm_end - vma->vm_start;
|
|
int ret;
|
|
ulong permissions;
|
|
|
|
if (requested_length == 0 || requested_length >
|
|
gasket_dev->coherent_buffer.length_bytes) {
|
|
trace_gasket_mmap_exit(-EINVAL);
|
|
return -EINVAL;
|
|
}
|
|
|
|
permissions = driver_desc->coherent_buffer_description.permissions;
|
|
if (!gasket_mmap_has_permissions(gasket_dev, vma, permissions)) {
|
|
dev_err(gasket_dev->dev, "Permission checking failed.\n");
|
|
trace_gasket_mmap_exit(-EPERM);
|
|
return -EPERM;
|
|
}
|
|
|
|
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
|
|
|
|
ret = remap_pfn_range(vma, vma->vm_start,
|
|
(gasket_dev->coherent_buffer.phys_base) >>
|
|
PAGE_SHIFT, requested_length, vma->vm_page_prot);
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev, "Error remapping PFN range err=%d.\n",
|
|
ret);
|
|
trace_gasket_mmap_exit(ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Record the user virtual to dma_address mapping that was
|
|
* created by the kernel.
|
|
*/
|
|
gasket_set_user_virt(gasket_dev, requested_length,
|
|
gasket_dev->coherent_buffer.phys_base,
|
|
vma->vm_start);
|
|
return 0;
|
|
}
|
|
|
|
/* Map a device's BARs into user space. */
|
|
static int gasket_mmap(struct file *filp, struct vm_area_struct *vma)
|
|
{
|
|
int i, ret;
|
|
int bar_index;
|
|
int has_mapped_anything = 0;
|
|
ulong permissions;
|
|
ulong raw_offset, vma_size;
|
|
bool is_coherent_region;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
struct gasket_dev *gasket_dev = (struct gasket_dev *)filp->private_data;
|
|
const struct gasket_bar_desc *bar_desc;
|
|
struct gasket_mappable_region *map_regions = NULL;
|
|
int num_map_regions = 0;
|
|
enum do_map_region_status map_status;
|
|
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
|
|
if (vma->vm_start & ~PAGE_MASK) {
|
|
dev_err(gasket_dev->dev,
|
|
"Base address not page-aligned: 0x%lx\n",
|
|
vma->vm_start);
|
|
trace_gasket_mmap_exit(-EINVAL);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Calculate the offset of this range into physical mem. */
|
|
raw_offset = (vma->vm_pgoff << PAGE_SHIFT) +
|
|
driver_desc->legacy_mmap_address_offset;
|
|
vma_size = vma->vm_end - vma->vm_start;
|
|
trace_gasket_mmap_entry(gasket_dev->dev_info.name, raw_offset,
|
|
vma_size);
|
|
|
|
/*
|
|
* Check if the raw offset is within a bar region. If not, check if it
|
|
* is a coherent region.
|
|
*/
|
|
bar_index = gasket_get_bar_index(gasket_dev, raw_offset);
|
|
is_coherent_region = gasket_is_coherent_region(driver_desc, raw_offset);
|
|
if (bar_index < 0 && !is_coherent_region) {
|
|
dev_err(gasket_dev->dev,
|
|
"Unable to find matching bar for address 0x%lx\n",
|
|
raw_offset);
|
|
trace_gasket_mmap_exit(bar_index);
|
|
return bar_index;
|
|
}
|
|
if (bar_index > 0 && is_coherent_region) {
|
|
dev_err(gasket_dev->dev,
|
|
"double matching bar and coherent buffers for address "
|
|
"0x%lx\n",
|
|
raw_offset);
|
|
trace_gasket_mmap_exit(bar_index);
|
|
return -EINVAL;
|
|
}
|
|
|
|
vma->vm_private_data = gasket_dev;
|
|
|
|
if (is_coherent_region)
|
|
return gasket_mmap_coherent(gasket_dev, vma);
|
|
|
|
/* Everything in the rest of this function is for normal BAR mapping. */
|
|
|
|
/*
|
|
* Subtract the base of the bar from the raw offset to get the
|
|
* memory location within the bar to map.
|
|
*/
|
|
bar_desc = &driver_desc->bar_descriptions[bar_index];
|
|
permissions = bar_desc->permissions;
|
|
if (!gasket_mmap_has_permissions(gasket_dev, vma, permissions)) {
|
|
dev_err(gasket_dev->dev, "Permission checking failed.\n");
|
|
trace_gasket_mmap_exit(-EPERM);
|
|
return -EPERM;
|
|
}
|
|
|
|
if (driver_desc->get_mappable_regions_cb) {
|
|
ret = driver_desc->get_mappable_regions_cb(gasket_dev,
|
|
bar_index,
|
|
&map_regions,
|
|
&num_map_regions);
|
|
if (ret)
|
|
return ret;
|
|
} else {
|
|
if (!gasket_mmap_has_permissions(gasket_dev, vma,
|
|
bar_desc->permissions)) {
|
|
dev_err(gasket_dev->dev,
|
|
"Permission checking failed.\n");
|
|
trace_gasket_mmap_exit(-EPERM);
|
|
return -EPERM;
|
|
}
|
|
num_map_regions = bar_desc->num_mappable_regions;
|
|
map_regions = kcalloc(num_map_regions,
|
|
sizeof(*bar_desc->mappable_regions),
|
|
GFP_KERNEL);
|
|
if (map_regions) {
|
|
memcpy(map_regions, bar_desc->mappable_regions,
|
|
num_map_regions *
|
|
sizeof(*bar_desc->mappable_regions));
|
|
}
|
|
}
|
|
|
|
if (!map_regions || num_map_regions == 0) {
|
|
dev_err(gasket_dev->dev, "No mappable regions returned!\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Marks the VMA's pages as uncacheable. */
|
|
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
|
|
for (i = 0; i < num_map_regions; i++) {
|
|
map_status = do_map_region(gasket_dev, vma, &map_regions[i]);
|
|
/* Try the next region if this one was not mappable. */
|
|
if (map_status == DO_MAP_REGION_INVALID)
|
|
continue;
|
|
if (map_status == DO_MAP_REGION_FAILURE) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
has_mapped_anything = 1;
|
|
}
|
|
|
|
kfree(map_regions);
|
|
|
|
/* If we could not map any memory, the request was invalid. */
|
|
if (!has_mapped_anything) {
|
|
dev_err(gasket_dev->dev,
|
|
"Map request did not contain a valid region.\n");
|
|
trace_gasket_mmap_exit(-EINVAL);
|
|
return -EINVAL;
|
|
}
|
|
|
|
trace_gasket_mmap_exit(0);
|
|
return 0;
|
|
|
|
fail:
|
|
/* Need to unmap any mapped ranges. */
|
|
num_map_regions = i;
|
|
for (i = 0; i < num_map_regions; i++)
|
|
if (gasket_mm_unmap_region(gasket_dev, vma,
|
|
&bar_desc->mappable_regions[i]))
|
|
dev_err(gasket_dev->dev, "Error unmapping range %d.\n",
|
|
i);
|
|
kfree(map_regions);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Open the char device file.
|
|
*
|
|
* If the open is for writing, and the device is not owned, this process becomes
|
|
* the owner. If the open is for writing and the device is already owned by
|
|
* some other process, it is an error. If this process is the owner, increment
|
|
* the open count.
|
|
*
|
|
* Returns 0 if successful, a negative error number otherwise.
|
|
*/
|
|
static int gasket_open(struct inode *inode, struct file *filp)
|
|
{
|
|
int ret;
|
|
struct gasket_dev *gasket_dev;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
struct gasket_ownership *ownership;
|
|
char task_name[TASK_COMM_LEN];
|
|
struct gasket_cdev_info *dev_info =
|
|
container_of(inode->i_cdev, struct gasket_cdev_info, cdev);
|
|
struct pid_namespace *pid_ns = task_active_pid_ns(current);
|
|
bool is_root = ns_capable(pid_ns->user_ns, CAP_SYS_ADMIN);
|
|
|
|
gasket_dev = dev_info->gasket_dev_ptr;
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
ownership = &dev_info->ownership;
|
|
get_task_comm(task_name, current);
|
|
filp->private_data = gasket_dev;
|
|
inode->i_size = 0;
|
|
|
|
dev_dbg(gasket_dev->dev,
|
|
"Attempting to open with tgid %u (%s) (f_mode: 0%03o, "
|
|
"fmode_write: %d is_root: %u)\n",
|
|
current->tgid, task_name, filp->f_mode,
|
|
(filp->f_mode & FMODE_WRITE), is_root);
|
|
|
|
/* Always allow non-writing accesses. */
|
|
if (!(filp->f_mode & FMODE_WRITE)) {
|
|
dev_dbg(gasket_dev->dev, "Allowing read-only opening.\n");
|
|
return 0;
|
|
}
|
|
|
|
mutex_lock(&gasket_dev->mutex);
|
|
|
|
dev_dbg(gasket_dev->dev,
|
|
"Current owner open count (owning tgid %u): %d.\n",
|
|
ownership->owner, ownership->write_open_count);
|
|
|
|
/* Opening a node owned by another TGID is an error (unless root) */
|
|
if (ownership->is_owned && ownership->owner != current->tgid &&
|
|
!is_root) {
|
|
dev_err(gasket_dev->dev,
|
|
"Process %u is opening a node held by %u.\n",
|
|
current->tgid, ownership->owner);
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
return -EPERM;
|
|
}
|
|
|
|
/* If the node is not owned, assign it to the current TGID. */
|
|
if (!ownership->is_owned) {
|
|
ret = gasket_check_and_invoke_callback_nolock(gasket_dev,
|
|
driver_desc->device_open_cb);
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev,
|
|
"Error in device open cb: %d\n", ret);
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
return ret;
|
|
}
|
|
ownership->is_owned = 1;
|
|
ownership->owner = current->tgid;
|
|
dev_dbg(gasket_dev->dev, "Device owner is now tgid %u\n",
|
|
ownership->owner);
|
|
}
|
|
|
|
ownership->write_open_count++;
|
|
|
|
dev_dbg(gasket_dev->dev, "New open count (owning tgid %u): %d\n",
|
|
ownership->owner, ownership->write_open_count);
|
|
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Called on a close of the device file. If this process is the owner,
|
|
* decrement the open count. On last close by the owner, free up buffers and
|
|
* eventfd contexts, and release ownership.
|
|
*
|
|
* Returns 0 if successful, a negative error number otherwise.
|
|
*/
|
|
static int gasket_release(struct inode *inode, struct file *file)
|
|
{
|
|
int i;
|
|
struct gasket_dev *gasket_dev;
|
|
struct gasket_ownership *ownership;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
char task_name[TASK_COMM_LEN];
|
|
struct gasket_cdev_info *dev_info =
|
|
container_of(inode->i_cdev, struct gasket_cdev_info, cdev);
|
|
struct pid_namespace *pid_ns = task_active_pid_ns(current);
|
|
bool is_root = ns_capable(pid_ns->user_ns, CAP_SYS_ADMIN);
|
|
|
|
gasket_dev = dev_info->gasket_dev_ptr;
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
ownership = &dev_info->ownership;
|
|
get_task_comm(task_name, current);
|
|
mutex_lock(&gasket_dev->mutex);
|
|
|
|
dev_dbg(gasket_dev->dev,
|
|
"Releasing device node. Call origin: tgid %u (%s) "
|
|
"(f_mode: 0%03o, fmode_write: %d, is_root: %u)\n",
|
|
current->tgid, task_name, file->f_mode,
|
|
(file->f_mode & FMODE_WRITE), is_root);
|
|
dev_dbg(gasket_dev->dev, "Current open count (owning tgid %u): %d\n",
|
|
ownership->owner, ownership->write_open_count);
|
|
|
|
if (file->f_mode & FMODE_WRITE) {
|
|
ownership->write_open_count--;
|
|
if (ownership->write_open_count == 0) {
|
|
dev_dbg(gasket_dev->dev, "Device is now free\n");
|
|
ownership->is_owned = 0;
|
|
ownership->owner = 0;
|
|
|
|
/* Forces chip reset before we unmap the page tables. */
|
|
driver_desc->device_reset_cb(gasket_dev);
|
|
|
|
for (i = 0; i < driver_desc->num_page_tables; ++i) {
|
|
gasket_page_table_unmap_all(gasket_dev->page_table[i]);
|
|
gasket_page_table_garbage_collect(gasket_dev->page_table[i]);
|
|
gasket_free_coherent_memory_all(gasket_dev, i);
|
|
}
|
|
|
|
/* Closes device, enters power save. */
|
|
gasket_check_and_invoke_callback_nolock(gasket_dev,
|
|
driver_desc->device_close_cb);
|
|
}
|
|
}
|
|
|
|
dev_dbg(gasket_dev->dev, "New open count (owning tgid %u): %d\n",
|
|
ownership->owner, ownership->write_open_count);
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Gasket ioctl dispatch function.
|
|
*
|
|
* Check if the ioctl is a generic ioctl. If not, pass the ioctl to the
|
|
* ioctl_handler_cb registered in the driver description.
|
|
* If the ioctl is a generic ioctl, pass it to gasket_ioctl_handler.
|
|
*/
|
|
static long gasket_ioctl(struct file *filp, uint cmd, ulong arg)
|
|
{
|
|
struct gasket_dev *gasket_dev;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
void __user *argp = (void __user *)arg;
|
|
char path[256];
|
|
|
|
gasket_dev = (struct gasket_dev *)filp->private_data;
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
if (!driver_desc) {
|
|
dev_dbg(gasket_dev->dev,
|
|
"Unable to find device descriptor for file %s\n",
|
|
d_path(&filp->f_path, path, 256));
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (!gasket_is_supported_ioctl(cmd)) {
|
|
/*
|
|
* The ioctl handler is not a standard Gasket callback, since
|
|
* it requires different arguments. This means we can't use
|
|
* check_and_invoke_callback.
|
|
*/
|
|
if (driver_desc->ioctl_handler_cb)
|
|
return driver_desc->ioctl_handler_cb(filp, cmd, argp);
|
|
|
|
dev_dbg(gasket_dev->dev, "Received unknown ioctl 0x%x\n", cmd);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return gasket_handle_ioctl(filp, cmd, argp);
|
|
}
|
|
|
|
/* File operations for all Gasket devices. */
|
|
static const struct file_operations gasket_file_ops = {
|
|
.owner = THIS_MODULE,
|
|
.llseek = no_llseek,
|
|
.mmap = gasket_mmap,
|
|
.open = gasket_open,
|
|
.release = gasket_release,
|
|
.unlocked_ioctl = gasket_ioctl,
|
|
};
|
|
|
|
/* Perform final init and marks the device as active. */
|
|
int gasket_enable_device(struct gasket_dev *gasket_dev)
|
|
{
|
|
int tbl_idx;
|
|
int ret;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
gasket_dev->internal_desc->driver_desc;
|
|
|
|
ret = gasket_interrupt_init(gasket_dev);
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev,
|
|
"Critical failure to allocate interrupts: %d\n", ret);
|
|
gasket_interrupt_cleanup(gasket_dev);
|
|
return ret;
|
|
}
|
|
|
|
for (tbl_idx = 0; tbl_idx < driver_desc->num_page_tables; tbl_idx++) {
|
|
dev_dbg(gasket_dev->dev, "Initializing page table %d.\n",
|
|
tbl_idx);
|
|
ret = gasket_page_table_init(&gasket_dev->page_table[tbl_idx],
|
|
&gasket_dev->bar_data[driver_desc->page_table_bar_index],
|
|
&driver_desc->page_table_configs[tbl_idx],
|
|
gasket_dev->dev,
|
|
gasket_dev->pci_dev);
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev,
|
|
"Couldn't init page table %d: %d\n",
|
|
tbl_idx, ret);
|
|
return ret;
|
|
}
|
|
/*
|
|
* Make sure that the page table is clear and set to simple
|
|
* addresses.
|
|
*/
|
|
gasket_page_table_reset(gasket_dev->page_table[tbl_idx]);
|
|
}
|
|
|
|
/*
|
|
* hardware_revision_cb returns a positive integer (the rev) if
|
|
* successful.)
|
|
*/
|
|
ret = check_and_invoke_callback(gasket_dev,
|
|
driver_desc->hardware_revision_cb);
|
|
if (ret < 0) {
|
|
dev_err(gasket_dev->dev,
|
|
"Error getting hardware revision: %d\n", ret);
|
|
return ret;
|
|
}
|
|
gasket_dev->hardware_revision = ret;
|
|
|
|
/* device_status_cb returns a device status, not an error code. */
|
|
gasket_dev->status = gasket_get_hw_status(gasket_dev);
|
|
if (gasket_dev->status == GASKET_STATUS_DEAD)
|
|
dev_err(gasket_dev->dev, "Device reported as unhealthy.\n");
|
|
|
|
ret = gasket_add_cdev(&gasket_dev->dev_info, &gasket_file_ops,
|
|
driver_desc->module);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(gasket_enable_device);
|
|
|
|
static int __gasket_add_device(struct device *parent_dev,
|
|
struct gasket_internal_desc *internal_desc,
|
|
struct gasket_dev **gasket_devp)
|
|
{
|
|
int ret;
|
|
struct gasket_dev *gasket_dev;
|
|
const struct gasket_driver_desc *driver_desc =
|
|
internal_desc->driver_desc;
|
|
|
|
ret = gasket_alloc_dev(internal_desc, parent_dev, &gasket_dev);
|
|
if (ret)
|
|
return ret;
|
|
if (IS_ERR(gasket_dev->dev_info.device)) {
|
|
dev_err(parent_dev, "Cannot create %s device %s [ret = %ld]\n",
|
|
driver_desc->name, gasket_dev->dev_info.name,
|
|
PTR_ERR(gasket_dev->dev_info.device));
|
|
ret = -ENODEV;
|
|
goto free_gasket_dev;
|
|
}
|
|
|
|
ret = gasket_sysfs_create_mapping(gasket_dev->dev_info.device,
|
|
gasket_dev);
|
|
if (ret)
|
|
goto remove_device;
|
|
|
|
ret = gasket_sysfs_create_entries(gasket_dev->dev_info.device,
|
|
gasket_sysfs_generic_attrs);
|
|
if (ret)
|
|
goto remove_sysfs_mapping;
|
|
|
|
*gasket_devp = gasket_dev;
|
|
return 0;
|
|
|
|
remove_sysfs_mapping:
|
|
gasket_sysfs_remove_mapping(gasket_dev->dev_info.device);
|
|
remove_device:
|
|
device_destroy(internal_desc->class, gasket_dev->dev_info.devt);
|
|
free_gasket_dev:
|
|
gasket_free_dev(gasket_dev);
|
|
return ret;
|
|
}
|
|
|
|
static void __gasket_remove_device(struct gasket_internal_desc *internal_desc,
|
|
struct gasket_dev *gasket_dev)
|
|
{
|
|
gasket_sysfs_remove_mapping(gasket_dev->dev_info.device);
|
|
device_destroy(internal_desc->class, gasket_dev->dev_info.devt);
|
|
gasket_free_dev(gasket_dev);
|
|
}
|
|
|
|
/*
|
|
* Add PCI gasket device.
|
|
*
|
|
* Called by Gasket device probe function.
|
|
* Allocates device metadata and maps device memory. The device driver must
|
|
* call gasket_enable_device after driver init is complete to place the device
|
|
* in active use.
|
|
*/
|
|
int gasket_pci_add_device(struct pci_dev *pci_dev,
|
|
struct gasket_dev **gasket_devp)
|
|
{
|
|
int ret;
|
|
struct gasket_internal_desc *internal_desc;
|
|
struct gasket_dev *gasket_dev;
|
|
struct device *parent;
|
|
|
|
dev_dbg(&pci_dev->dev, "add PCI gasket device\n");
|
|
|
|
mutex_lock(&g_mutex);
|
|
internal_desc = lookup_pci_internal_desc(pci_dev);
|
|
mutex_unlock(&g_mutex);
|
|
if (!internal_desc) {
|
|
dev_err(&pci_dev->dev,
|
|
"PCI add device called for unknown driver type\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
parent = &pci_dev->dev;
|
|
ret = __gasket_add_device(parent, internal_desc, &gasket_dev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
gasket_dev->pci_dev = pci_dev;
|
|
ret = gasket_setup_pci(pci_dev, gasket_dev);
|
|
if (ret)
|
|
goto cleanup_pci;
|
|
|
|
/*
|
|
* Once we've created the mapping structures successfully, attempt to
|
|
* create a symlink to the pci directory of this object.
|
|
*/
|
|
ret = sysfs_create_link(&gasket_dev->dev_info.device->kobj,
|
|
&pci_dev->dev.kobj, dev_name(&pci_dev->dev));
|
|
if (ret) {
|
|
dev_err(gasket_dev->dev,
|
|
"Cannot create sysfs pci link: %d\n", ret);
|
|
goto cleanup_pci;
|
|
}
|
|
|
|
*gasket_devp = gasket_dev;
|
|
return 0;
|
|
|
|
cleanup_pci:
|
|
gasket_cleanup_pci(gasket_dev);
|
|
__gasket_remove_device(internal_desc, gasket_dev);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(gasket_pci_add_device);
|
|
|
|
/* Remove a PCI gasket device. */
|
|
void gasket_pci_remove_device(struct pci_dev *pci_dev)
|
|
{
|
|
int i;
|
|
struct gasket_internal_desc *internal_desc;
|
|
struct gasket_dev *gasket_dev = NULL;
|
|
/* Find the device desc. */
|
|
mutex_lock(&g_mutex);
|
|
internal_desc = lookup_pci_internal_desc(pci_dev);
|
|
if (!internal_desc) {
|
|
mutex_unlock(&g_mutex);
|
|
return;
|
|
}
|
|
mutex_unlock(&g_mutex);
|
|
|
|
/* Now find the specific device */
|
|
mutex_lock(&internal_desc->mutex);
|
|
for (i = 0; i < GASKET_DEV_MAX; i++) {
|
|
if (internal_desc->devs[i] &&
|
|
internal_desc->devs[i]->pci_dev == pci_dev) {
|
|
gasket_dev = internal_desc->devs[i];
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&internal_desc->mutex);
|
|
|
|
if (!gasket_dev)
|
|
return;
|
|
|
|
dev_dbg(gasket_dev->dev, "remove %s PCI gasket device\n",
|
|
internal_desc->driver_desc->name);
|
|
|
|
gasket_cleanup_pci(gasket_dev);
|
|
__gasket_remove_device(internal_desc, gasket_dev);
|
|
}
|
|
EXPORT_SYMBOL(gasket_pci_remove_device);
|
|
|
|
/**
|
|
* Lookup a name by number in a num_name table.
|
|
* @num: Number to lookup.
|
|
* @table: Array of num_name structures, the table for the lookup.
|
|
*
|
|
* Description: Searches for num in the table. If found, the
|
|
* corresponding name is returned; otherwise NULL
|
|
* is returned.
|
|
*
|
|
* The table must have a NULL name pointer at the end.
|
|
*/
|
|
const char *gasket_num_name_lookup(uint num,
|
|
const struct gasket_num_name *table)
|
|
{
|
|
uint i = 0;
|
|
|
|
while (table[i].snn_name) {
|
|
if (num == table[i].snn_num)
|
|
break;
|
|
++i;
|
|
}
|
|
|
|
return table[i].snn_name;
|
|
}
|
|
EXPORT_SYMBOL(gasket_num_name_lookup);
|
|
|
|
int gasket_reset(struct gasket_dev *gasket_dev)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&gasket_dev->mutex);
|
|
ret = gasket_reset_nolock(gasket_dev);
|
|
mutex_unlock(&gasket_dev->mutex);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(gasket_reset);
|
|
|
|
int gasket_reset_nolock(struct gasket_dev *gasket_dev)
|
|
{
|
|
int ret;
|
|
int i;
|
|
const struct gasket_driver_desc *driver_desc;
|
|
|
|
driver_desc = gasket_dev->internal_desc->driver_desc;
|
|
if (!driver_desc->device_reset_cb)
|
|
return 0;
|
|
|
|
ret = driver_desc->device_reset_cb(gasket_dev);
|
|
if (ret) {
|
|
dev_dbg(gasket_dev->dev, "Device reset cb returned %d.\n",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Reinitialize the page tables and interrupt framework. */
|
|
for (i = 0; i < driver_desc->num_page_tables; ++i)
|
|
gasket_page_table_reset(gasket_dev->page_table[i]);
|
|
|
|
ret = gasket_interrupt_reinit(gasket_dev);
|
|
if (ret) {
|
|
dev_dbg(gasket_dev->dev, "Unable to reinit interrupts: %d.\n",
|
|
ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Get current device health. */
|
|
gasket_dev->status = gasket_get_hw_status(gasket_dev);
|
|
if (gasket_dev->status == GASKET_STATUS_DEAD) {
|
|
dev_dbg(gasket_dev->dev, "Device reported as dead.\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL(gasket_reset_nolock);
|
|
|
|
gasket_ioctl_permissions_cb_t
|
|
gasket_get_ioctl_permissions_cb(struct gasket_dev *gasket_dev)
|
|
{
|
|
return gasket_dev->internal_desc->driver_desc->ioctl_permissions_cb;
|
|
}
|
|
EXPORT_SYMBOL(gasket_get_ioctl_permissions_cb);
|
|
|
|
/* Get the driver structure for a given gasket_dev.
|
|
* @dev: pointer to gasket_dev, implementing the requested driver.
|
|
*/
|
|
const struct gasket_driver_desc *gasket_get_driver_desc(struct gasket_dev *dev)
|
|
{
|
|
return dev->internal_desc->driver_desc;
|
|
}
|
|
|
|
/* Get the device structure for a given gasket_dev.
|
|
* @dev: pointer to gasket_dev, implementing the requested driver.
|
|
*/
|
|
struct device *gasket_get_device(struct gasket_dev *dev)
|
|
{
|
|
return dev->dev;
|
|
}
|
|
|
|
/**
|
|
* Asynchronously waits on device.
|
|
* @gasket_dev: Device struct.
|
|
* @bar: Bar
|
|
* @offset: Register offset
|
|
* @mask: Register mask
|
|
* @val: Expected value
|
|
* @max_retries: number of sleep periods
|
|
* @delay_ms: Timeout in milliseconds
|
|
*
|
|
* Description: Busy waits for a specific combination of bits to be set on a
|
|
* Gasket register.
|
|
**/
|
|
int gasket_wait_with_reschedule(struct gasket_dev *gasket_dev, int bar,
|
|
u64 offset, u64 mask, u64 val,
|
|
uint max_retries, u64 delay_ms)
|
|
{
|
|
uint retries = 0;
|
|
u64 tmp;
|
|
|
|
while (retries < max_retries) {
|
|
tmp = gasket_dev_read_64(gasket_dev, bar, offset);
|
|
if ((tmp & mask) == val)
|
|
return 0;
|
|
msleep(delay_ms);
|
|
retries++;
|
|
}
|
|
dev_dbg(gasket_dev->dev, "%s timeout: reg %llx timeout (%llu ms)\n",
|
|
__func__, offset, max_retries * delay_ms);
|
|
return -ETIMEDOUT;
|
|
}
|
|
EXPORT_SYMBOL(gasket_wait_with_reschedule);
|
|
|
|
/* See gasket_core.h for description. */
|
|
int gasket_register_device(const struct gasket_driver_desc *driver_desc)
|
|
{
|
|
int i, ret;
|
|
int desc_idx = -1;
|
|
struct gasket_internal_desc *internal;
|
|
|
|
pr_debug("Loading %s driver version %s\n", driver_desc->name,
|
|
driver_desc->driver_version);
|
|
/* Check for duplicates and find a free slot. */
|
|
mutex_lock(&g_mutex);
|
|
|
|
for (i = 0; i < GASKET_FRAMEWORK_DESC_MAX; i++) {
|
|
if (g_descs[i].driver_desc == driver_desc) {
|
|
pr_err("%s driver already loaded/registered\n",
|
|
driver_desc->name);
|
|
mutex_unlock(&g_mutex);
|
|
return -EBUSY;
|
|
}
|
|
}
|
|
|
|
/* This and the above loop could be combined, but this reads easier. */
|
|
for (i = 0; i < GASKET_FRAMEWORK_DESC_MAX; i++) {
|
|
if (!g_descs[i].driver_desc) {
|
|
g_descs[i].driver_desc = driver_desc;
|
|
desc_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&g_mutex);
|
|
|
|
if (desc_idx == -1) {
|
|
pr_err("too many drivers loaded, max %d\n",
|
|
GASKET_FRAMEWORK_DESC_MAX);
|
|
return -EBUSY;
|
|
}
|
|
|
|
internal = &g_descs[desc_idx];
|
|
mutex_init(&internal->mutex);
|
|
memset(internal->devs, 0, sizeof(struct gasket_dev *) * GASKET_DEV_MAX);
|
|
internal->class =
|
|
class_create(driver_desc->module, driver_desc->name);
|
|
|
|
if (IS_ERR(internal->class)) {
|
|
pr_err("Cannot register %s class [ret=%ld]\n",
|
|
driver_desc->name, PTR_ERR(internal->class));
|
|
ret = PTR_ERR(internal->class);
|
|
goto unregister_gasket_driver;
|
|
}
|
|
|
|
ret = register_chrdev_region(MKDEV(driver_desc->major,
|
|
driver_desc->minor), GASKET_DEV_MAX,
|
|
driver_desc->name);
|
|
if (ret) {
|
|
pr_err("cannot register %s char driver [ret=%d]\n",
|
|
driver_desc->name, ret);
|
|
goto destroy_class;
|
|
}
|
|
|
|
return 0;
|
|
|
|
destroy_class:
|
|
class_destroy(internal->class);
|
|
|
|
unregister_gasket_driver:
|
|
mutex_lock(&g_mutex);
|
|
g_descs[desc_idx].driver_desc = NULL;
|
|
mutex_unlock(&g_mutex);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL(gasket_register_device);
|
|
|
|
/* See gasket_core.h for description. */
|
|
void gasket_unregister_device(const struct gasket_driver_desc *driver_desc)
|
|
{
|
|
int i, desc_idx;
|
|
struct gasket_internal_desc *internal_desc = NULL;
|
|
|
|
mutex_lock(&g_mutex);
|
|
for (i = 0; i < GASKET_FRAMEWORK_DESC_MAX; i++) {
|
|
if (g_descs[i].driver_desc == driver_desc) {
|
|
internal_desc = &g_descs[i];
|
|
desc_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!internal_desc) {
|
|
mutex_unlock(&g_mutex);
|
|
pr_err("request to unregister unknown desc: %s, %d:%d\n",
|
|
driver_desc->name, driver_desc->major,
|
|
driver_desc->minor);
|
|
return;
|
|
}
|
|
|
|
unregister_chrdev_region(MKDEV(driver_desc->major, driver_desc->minor),
|
|
GASKET_DEV_MAX);
|
|
|
|
class_destroy(internal_desc->class);
|
|
|
|
/* Finally, effectively "remove" the driver. */
|
|
g_descs[desc_idx].driver_desc = NULL;
|
|
mutex_unlock(&g_mutex);
|
|
|
|
pr_debug("removed %s driver\n", driver_desc->name);
|
|
}
|
|
EXPORT_SYMBOL(gasket_unregister_device);
|
|
|
|
static int __init gasket_init(void)
|
|
{
|
|
int i;
|
|
|
|
mutex_lock(&g_mutex);
|
|
for (i = 0; i < GASKET_FRAMEWORK_DESC_MAX; i++) {
|
|
g_descs[i].driver_desc = NULL;
|
|
mutex_init(&g_descs[i].mutex);
|
|
}
|
|
|
|
gasket_sysfs_init();
|
|
|
|
mutex_unlock(&g_mutex);
|
|
return 0;
|
|
}
|
|
|
|
MODULE_DESCRIPTION("Google Gasket driver framework");
|
|
MODULE_VERSION(GASKET_FRAMEWORK_VERSION);
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_AUTHOR("Rob Springer <rspringer@google.com>");
|
|
module_init(gasket_init);
|