diff --git a/docs/reference/kernel-abi/index.rst b/docs/reference/kernel-abi/index.rst index 0a610c5a..93852bc9 100644 --- a/docs/reference/kernel-abi/index.rst +++ b/docs/reference/kernel-abi/index.rst @@ -17,35 +17,43 @@ usage guide and a formal reference for each ioctl operation. Every ioctl entry f structure: a top-level description, the C interface definition, the ioctl direction, preconditions on inputs, postconditions on outputs, and return values. -The module uses the Linux ``miscdevice`` framework to create the following device files, which -allocates dynamic minor numbers under major 10. Userspace discovers device nodes by path, not by -major/minor number. +The module allocates one dynamic character-device major and creates a dedicated +``/sys/class/slash`` class. Device numbers within that major have a fixed layout: -``/dev/slash_ctl`` / ``/sys/class/misc/slash_ctl_/device`` +- minor 0 is the global ``slash_hotplug`` device; +- board ``N`` uses minor ``2*N+1`` for ``slash_ctl`` and minor ``2*N+2`` for + ``slash_qdma_ctl``; +- ``N`` is in the range 0–15, so one loaded module supports at most 16 cards. + +PF1 and PF2 are matched by their board BDF (the ``DDDD:BB:SS`` portion) and share the same ``N``. +The board-to-``N`` assignment is retained while the module is loaded, including across PCI +remove/rescan cycles. Consequently the class entry name, ``/dev`` path, and device number +(``dev_t``) return unchanged after a remove/rescan. The major itself is dynamically allocated and +may change when the module is unloaded and loaded again. + +``/dev/slash_ctl`` / ``/sys/class/slash/slash_ctl_/device`` Provides BAR enumeration, MMIO access, and PCI device identity. Associated with PF2 (device ID ``10EE:50B6``). Examples: ``/dev/slash_ctl0``, ``/dev/slash_ctl1``, - ``/sys/class/misc/slash_ctl_0000:61:00.2/device``. + ``/sys/class/slash/slash_ctl_0000:61:00.2/device``. -``/dev/slash_qdma_ctl`` / ``/sys/class/misc/slash_qdma_ctl_/device`` +``/dev/slash_qdma_ctl`` / ``/sys/class/slash/slash_qdma_ctl_/device`` Manages DMA queue pairs for bulk data movement between host and card memory, as well as reconfiguration. Associated with PF1 (device ID ``10EE:50B5``). Examples: ``/dev/slash_qdma_ctl0``, - ``/dev/slash_qdma_ctl1``, ``/sys/class/misc/slash_qdma_ctl_0000:61:00.0/device``. + ``/dev/slash_qdma_ctl1``, ``/sys/class/slash/slash_qdma_ctl_0000:61:00.1/device``. -``/dev/slash_hotplug`` +``/dev/slash_hotplug`` / ``/sys/class/slash/slash_hotplug`` A single global instance created at module load. Provides privileged control over the PCIe lifecycle of SLASH cards (remove, rescan, secondary bus reset). -The kernel module creates one ``slash_ctl`` and ``slash_qdma_ctl`` file for each card during discovery, -which persist across reconfiguration, but will be removed and readded during a remove+rescan cycle. -The mapping of one file path to a physical card is therefore not guaranteed across remove+rescan cycles -and userspace should always verify the BDF identity of the accessed card. Also, suffixes for one card are -not guaranteed to be identical for ``/dev/slash_ctl`` and ``/dev/slash_qdma_ctl``. For example, -the device files ``/dev/slash_ctl0`` and ``/dev/slash_qdma_ctl1`` may reference the same physical card. +Class entry names use the full function-level BDF, while ``DEVNAME`` in each entry's ``uevent`` +file names the numeric ``/dev`` node. The entry's ``dev`` attribute and the ``st_rdev`` returned +by ``stat(2)`` on that node contain the same major and minor. Class entries are sysfs device +objects, not symlinks to ``/dev``. -The files in the ``/sys/class/misc/`` directory are symlinks to the respective files in ``/dev``, -and the placeholder ```` equates to the full, function-level BDF identifier of the physical -function. For example, the physical function 2 of board ``0000:61:00`` may be available as -``/sys/class/misc/slash_ctl_0000:61:00.2``. +During removal the affected class entry and ``/dev`` node disappear. An fd opened before removal +continues to refer to the old device instance and never rebinds to the rescanned instance; +device-specific ioctls on that old fd return ``-ENODEV``. After rescan, new opens through the stable +path reach the new instance. Data Conventions ================ @@ -58,7 +66,9 @@ Every ioctl argument struct carries a leading ``__u32 size`` field. Callers must copies ``min(user_size, kernel_size)`` bytes in. Fields the kernel knows about but the caller's older struct does not include are zero-filled. The response is written back for ``min(user_size, kernel_size)`` bytes; if ``user_size > kernel_size``, the kernel zero-fills the -extra tail via ``clear_user()``. This allows the driver and library to evolve independently. +extra tail via ``clear_user()``. This supports append-only struct extension within a coordinated +release; it is not a promise that arbitrary kernel-module and userspace-library releases can be +mixed. ``slash.ko``, libslash, vrtd/VRT, and v80-smi must come from the same SLASH release. Error Handling -------------- @@ -100,11 +110,11 @@ BDF string and vendor/device IDs to correlate the control device with a physical matching QDMA control device. - **Device file name:** ``/dev/slash_ctl`` (e.g. ``/dev/slash_ctl0``) -- **Sysfs name:** ``slash_ctl_`` (e.g., ``/sys/class/misc/slash_ctl_slash_ctl_0000:61:00.2``) +- **Sysfs name:** ``slash_ctl_`` (e.g., ``/sys/class/slash/slash_ctl_0000:61:00.2``) - **Associated PCI function:** PF2, device ID ``10EE:50B6`` - **Permissions:** ``0600`` (owner read/write) - **Creation:** one per card, created when PF2 is probed during module load or PCI rescan -- **File operations:** ``ioctl`` only — no ``open`` hook, no ``read``, ``write``, or ``mmap`` +- **File operations:** ``open``, ``release``, and ``ioctl`` — no ``read``, ``write``, or ``mmap`` on this fd itself. MMIO access is through a dma-buf fd returned by an ioctl. Usage @@ -340,14 +350,15 @@ the transfer channel: host buffers are registered once, and transfer ioctls name buffer, buffer offset, device-side physical address, length, and direction. - **Device file name:** ``/dev/slash_qdma_ctl`` (e.g. ``/dev/slash_qdma_ctl0``) -- **Sysfs name:** ``slash_qdma_ctl_`` (e.g. ``/sys/class/misc/slash_qdma_ctl_0000:61:00.1``) +- **Sysfs name:** ``slash_qdma_ctl_`` (e.g. ``/sys/class/slash/slash_qdma_ctl_0000:61:00.1``) - **Associated PCI function:** PF1, device ID ``10EE:50B5`` - **Permissions:** ``0600`` - **Creation:** one per card, created when PF1 is probed - **File operations:** ``open``, ``release``, ``ioctl`` on the control fd. DMA I/O is done on per-qpair anon-inode fds returned by an ioctl. -Same stable-``N`` mapping scheme as the control device, using a separate BDF-to-number map. +The QDMA and control functions use the same board-to-``N`` map, so +``slash_qdma_ctl`` is always paired with ``slash_ctl``. Usage ----- @@ -510,9 +521,9 @@ flag set) or the QDMA handle is not open. ``SLASH_QDMA_IOCTL_INFO`` ~~~~~~~~~~~~~~~~~~~~~~~~~ -Queries QDMA device capabilities. All output fields are currently zero; this ioctl is a placeholder -for future capability reporting. Callers should issue it during initialization but make no decisions -based on the returned values in the current implementation. +Queries the QDMA device's PCI identity and capabilities. ``bdf`` is always the full PF1 BDF and can +be matched with a control device by comparing the ``DDDD:BB:SS`` board portion. The capability +fields are reserved for future reporting and are currently zero. **Interface:** @@ -521,11 +532,12 @@ based on the returned values in the current implementation. #define SLASH_QDMA_IOCTL_INFO _IOWR('v', 0x50, struct slash_qdma_info) struct slash_qdma_info { - __u32 size; /* [in/out] ABI version */ - __u32 qsets_max; /* [out] Max queue sets (currently always 0) */ - __u32 msix_qvecs; /* [out] MSI-X vectors for queues (currently always 0) */ - __u32 vf_max; /* [out] Max VFs (currently always 0) */ - __u32 caps; /* [out] Capability bitmask (currently always 0) */ + __u32 size; /* [in/out] ABI version */ + char bdf[SLASH_PCI_BDF_LEN]; /* [out] Full PF1 BDF, e.g. "0000:61:00.1" */ + __u32 qsets_max; /* [out] Max queue sets (currently 0) */ + __u32 msix_qvecs; /* [out] Queue MSI-X vectors (currently 0) */ + __u32 vf_max; /* [out] Max VFs (currently 0) */ + __u32 caps; /* [out] Capability bitmask (currently 0) */ }; **Direction:** ``_IOWR`` — userspace writes ``size``; the kernel writes back all output fields. @@ -538,7 +550,9 @@ based on the returned values in the current implementation. **Postconditions:** -- All output fields are set to 0 in the current implementation. +- ``bdf`` is a NUL-terminated ``DDDD:BB:SS.1`` string with the full PCI domain. +- ``qsets_max``, ``msix_qvecs``, ``vf_max``, and ``caps`` are set to 0 in the current + implementation. - The output is truncated to ``min(size, sizeof(struct))`` bytes. Fields whose tail lies beyond the user-supplied ``size`` are not written; the corresponding bytes in the user buffer are left untouched. @@ -910,6 +924,8 @@ remove-and-rescan operation. These operations are used after loading a new FPGA performing a full board reset. - **Device file name:** ``/dev/slash_hotplug`` +- **Sysfs name:** ``/sys/class/slash/slash_hotplug`` +- **Device number:** the shared SLASH major, minor 0 - **Permissions:** ``0600`` - **Creation:** exactly one instance, created at module load, destroyed at module unload - **File operations:** ``ioctl`` only (includes 32-bit compat path). No ``open``, ``release``, @@ -1036,6 +1052,7 @@ callback. The corresponding ``/dev/slash_ctl`` or ``/dev/slash_qdma_ctl`` - Bus mastering is disabled on the device (``pci_clear_master()``). - The device is removed from the PCI hierarchy (``pci_stop_and_remove_bus_device()``). - The driver's ``.remove`` callback is invoked; associated device nodes disappear. +- A later rescan recreates the node with the same class name, ``/dev`` path, and ``dev_t``. **Return values:** diff --git a/driver/Makefile b/driver/Makefile index ac28900e..4cb74965 100644 --- a/driver/Makefile +++ b/driver/Makefile @@ -64,6 +64,8 @@ SLASH_HAVE_VM_FLAGS_SET ?= n SLASH_HAVE_MODULE_IMPORT_NS_TOKEN ?= n SLASH_HAVE_URING_CMD ?= n SLASH_HAVE_URING_SQE_CMD ?= n +SLASH_HAVE_CLASS_CREATE_ONE_ARG ?= n +SLASH_HAVE_CLASS_DEVNODE_CONST ?= n # Set GCOV=1 to instrument the module for kernel gcov coverage. # Not set by default — never enable this in production builds. @@ -72,7 +74,7 @@ GCOV_PROFILE := y endif obj-m := $(MODULE).o -$(MODULE)-objs := $(MODULE)_main.o $(MODULE)_ctldev.o $(MODULE)_pcie.o $(MODULE)_dmabuf.o $(MODULE)_hotplug.o $(MODULE)_qdma.o +$(MODULE)-objs := $(MODULE)_main.o $(MODULE)_chrdev.o $(MODULE)_ctldev.o $(MODULE)_pcie.o $(MODULE)_dmabuf.o $(MODULE)_hotplug.o $(MODULE)_qdma.o ccflags-y += \ -I$(src)/libslash/include \ -I$(src)/libslash/include/slash/uapi \ @@ -96,6 +98,14 @@ ifeq ($(SLASH_HAVE_MODULE_IMPORT_NS_TOKEN),y) ccflags-y += -DSLASH_HAVE_MODULE_IMPORT_NS_TOKEN endif +ifeq ($(SLASH_HAVE_CLASS_CREATE_ONE_ARG),y) +ccflags-y += -DSLASH_HAVE_CLASS_CREATE_ONE_ARG +endif + +ifeq ($(SLASH_HAVE_CLASS_DEVNODE_CONST),y) +ccflags-y += -DSLASH_HAVE_CLASS_DEVNODE_CONST +endif + # Optional io_uring uring_cmd async transfer path. Probed by kcompat; absent on # kernels without CONFIG_IO_URING or uring_cmd support (e.g. RHEL 9, Ubuntu # 22.04 GA), where the synchronous transfer ioctl remains the only path. diff --git a/driver/kcompat/class_create_one_arg.c b/driver/kcompat/class_create_one_arg.c new file mode 100644 index 00000000..bf4f4c3f --- /dev/null +++ b/driver/kcompat/class_create_one_arg.c @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include +#include + +static int __init conftest_init(void) +{ + struct class *class = class_create("slash-conftest"); + + return PTR_ERR_OR_ZERO(class); +} + +module_init(conftest_init); +MODULE_LICENSE("GPL"); diff --git a/driver/kcompat/class_devnode_const.c b/driver/kcompat/class_devnode_const.c new file mode 100644 index 00000000..eba6ae16 --- /dev/null +++ b/driver/kcompat/class_devnode_const.c @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include +#include + +static char *conftest_devnode(const struct device *device, umode_t *mode) +{ + return NULL; +} + +static struct class conftest_class = { + .name = "slash-conftest", + .devnode = conftest_devnode, +}; + +static int __init conftest_init(void) +{ + return conftest_class.name ? 0 : -EINVAL; +} + +module_init(conftest_init); +MODULE_LICENSE("GPL"); diff --git a/driver/libslash/include/slash/qdma.h b/driver/libslash/include/slash/qdma.h index 6f097288..e853c1e5 100644 --- a/driver/libslash/include/slash/qdma.h +++ b/driver/libslash/include/slash/qdma.h @@ -17,7 +17,7 @@ * * Userspace API for slash QDMA (Queue-based DMA) devices. * - * A QDMA device is a separate misc character device created for PF1, + * A QDMA device is a separate character device created for PF1, * while the control device (ctldev) is created for PF2. Each PCI * function gets at most one of each. Device nodes appear at * /dev/slash_qdma_ctl0, /dev/slash_qdma_ctl1, etc. diff --git a/driver/libslash/include/slash/uapi/slash_interface.h b/driver/libslash/include/slash/uapi/slash_interface.h index 501c8623..52bb7868 100644 --- a/driver/libslash/include/slash/uapi/slash_interface.h +++ b/driver/libslash/include/slash/uapi/slash_interface.h @@ -147,6 +147,7 @@ struct slash_qdma_info { __u32 size; /**< Struct size for ABI versioning. */ /* Kernel to userspace */ + char bdf[SLASH_PCI_BDF_LEN]; /**< [out] Full PF1 PCI BDF string, NUL-terminated. */ __u32 qsets_max; /**< [out] Maximum number of queue sets the hardware supports. */ __u32 msix_qvecs; /**< [out] Number of MSI-X vectors available for queues. */ __u32 vf_max; /**< [out] Maximum number of virtual functions. */ diff --git a/driver/libslash/src/qdma_mock.c b/driver/libslash/src/qdma_mock.c index d72762bb..f2709ddb 100644 --- a/driver/libslash/src/qdma_mock.c +++ b/driver/libslash/src/qdma_mock.c @@ -115,6 +115,7 @@ int slash_qdma_mock_info_read(struct slash_qdma *qdma, struct slash_qdma_info *i memset(info, 0, sizeof(*info)); info->size = sizeof(*info); + memcpy(info->bdf, "0000:00:00.1", sizeof("0000:00:00.1")); info->qsets_max = QDMA_MOCK_MAX_QUEUES; info->msix_qvecs = 1; diff --git a/driver/libslash/tests/qdma_test.cpp b/driver/libslash/tests/qdma_test.cpp index 23a70f7e..5bf172b6 100644 --- a/driver/libslash/tests/qdma_test.cpp +++ b/driver/libslash/tests/qdma_test.cpp @@ -20,6 +20,7 @@ #include +#include #include #include #include @@ -32,6 +33,22 @@ extern "C" { static constexpr const char *REAL_QDMA_PATH = "/dev/slash_qdma_ctl0"; static constexpr uint64_t DDR_BASE_ADDRESS = 0x60000000000ULL; +static bool isPf1Bdf(const char *bdf) { + if (bdf == nullptr || std::strlen(bdf) != 12 || bdf[4] != ':' || bdf[7] != ':' || + bdf[10] != '.' || bdf[11] != '1') { + return false; + } + for (size_t i = 0; i < 12; ++i) { + if (i == 4 || i == 7 || i == 10) { + continue; + } + if (!std::isxdigit(static_cast(bdf[i]))) { + return false; + } + } + return true; +} + // ─── Null / invalid argument tests (no hardware needed) ────────────────────── TEST(QdmaNullTest, Open) { @@ -167,7 +184,12 @@ TEST_P(ParametrizedQdmaTest, OpenSucceeds) { TEST_P(ParametrizedQdmaTest, InfoRead) { struct slash_qdma_info info{}; - EXPECT_EQ(slash_qdma_info_read(qdma_, &info), 0); + ASSERT_EQ(slash_qdma_info_read(qdma_, &info), 0); + if (mock) { + EXPECT_STREQ(info.bdf, "0000:00:00.1"); + } else { + EXPECT_TRUE(isPf1Bdf(info.bdf)) << "invalid PF1 BDF: " << info.bdf; + } } TEST_P(ParametrizedQdmaTest, QueueDmaTransfer) { diff --git a/driver/slash_chrdev.c b/driver/slash_chrdev.c new file mode 100644 index 00000000..a988a34f --- /dev/null +++ b/driver/slash_chrdev.c @@ -0,0 +1,222 @@ +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * @file slash_chrdev.c + * + * Shared SLASH character-device registration and board numbering. + */ + +#include "slash_chrdev.h" + +#include + +#include +#include +#include +#include +#include + +#include "slash.h" +#include "slash_compat.h" + +/** + * struct slash_board_slot - Stable board-BDF to device-number mapping. + * @node: Link in the module-lifetime mapping list. + * @domain: PCI domain of the board endpoints. + * @bus: PCI bus of the board endpoints. + * @slot: PCI device number shared by PF1 and PF2. + * @number: Stable card number used in the character-device minors. + * @active_pfs: Function bits currently bound to a SLASH PCI driver. + */ +struct slash_board_slot { + struct list_head node; + unsigned int domain; + unsigned int bus; + unsigned int slot; + unsigned int number; + unsigned long active_pfs; +}; + +static dev_t slash_chrdev_base; +static struct class *slash_chrdev_class; + +static LIST_HEAD(slash_board_slots); +static DEFINE_MUTEX(slash_board_slots_lock); +static unsigned int slash_board_slot_count; + +#if defined(SLASH_HAVE_CLASS_DEVNODE_CONST) +static char *slash_chrdev_devnode(const struct device *device, umode_t *mode) +#else +static char *slash_chrdev_devnode(struct device *device, umode_t *mode) +#endif +{ + unsigned int minor = MINOR(device->devt); + unsigned int card; + + if (mode) + *mode = SLASH_CHRDEV_MODE; + + if (minor == SLASH_HOTPLUG_MINOR) + return kstrdup(SLASH_HOTPLUG_DEVICE_NAME, GFP_KERNEL); + card = SLASH_CARD_FROM_MINOR(minor); + if (minor & 1) + return kasprintf(GFP_KERNEL, SLASH_CTLDEV_NODENAME_FMT, card); + return kasprintf(GFP_KERNEL, SLASH_QDMA_CTLDEV_NODENAME_FMT, card); +} + +int __init slash_chrdev_init(void) +{ + int err; + + err = alloc_chrdev_region(&slash_chrdev_base, 0, SLASH_CHRDEV_MINORS, + SLASH_NAME); + if (err) + return err; + + slash_chrdev_class = slash_class_create(SLASH_NAME); + if (IS_ERR(slash_chrdev_class)) { + err = PTR_ERR(slash_chrdev_class); + slash_chrdev_class = NULL; + unregister_chrdev_region(slash_chrdev_base, SLASH_CHRDEV_MINORS); + return err; + } + + slash_chrdev_class->devnode = slash_chrdev_devnode; + return 0; +} + +void __exit slash_chrdev_exit(void) +{ + struct slash_board_slot *entry; + struct slash_board_slot *tmp; + + class_destroy(slash_chrdev_class); + slash_chrdev_class = NULL; + unregister_chrdev_region(slash_chrdev_base, SLASH_CHRDEV_MINORS); + + mutex_lock(&slash_board_slots_lock); + list_for_each_entry_safe(entry, tmp, &slash_board_slots, node) { + list_del(&entry->node); + kfree(entry); + } + slash_board_slot_count = 0; + mutex_unlock(&slash_board_slots_lock); +} + +struct device *slash_chrdev_add(struct cdev *cdev, + const struct file_operations *fops, + unsigned int minor, + struct device *parent, + void *drvdata, + const char *name) +{ + struct device *device; + dev_t devt = MKDEV(MAJOR(slash_chrdev_base), minor); + int err; + + cdev_init(cdev, fops); + cdev->owner = fops->owner; + + err = cdev_add(cdev, devt, 1); + if (err) + return ERR_PTR(err); + + device = device_create(slash_chrdev_class, parent, devt, drvdata, + "%s", name); + if (IS_ERR(device)) + cdev_del(cdev); + + return device; +} + +void slash_chrdev_del(struct cdev *cdev, struct device *device) +{ + device_destroy(slash_chrdev_class, device->devt); + cdev_del(cdev); +} + +int slash_chrdev_board_get(struct pci_dev *pdev) +{ + struct slash_board_slot *entry; + unsigned int domain = pci_domain_nr(pdev->bus); + unsigned int bus = pdev->bus->number; + unsigned int slot = PCI_SLOT(pdev->devfn); + unsigned int function = PCI_FUNC(pdev->devfn); + int number; + + mutex_lock(&slash_board_slots_lock); + + list_for_each_entry(entry, &slash_board_slots, node) { + if (entry->domain != domain || entry->bus != bus || + entry->slot != slot) + continue; + + if (entry->active_pfs & BIT(function)) { + mutex_unlock(&slash_board_slots_lock); + return -EBUSY; + } + + entry->active_pfs |= BIT(function); + number = entry->number; + mutex_unlock(&slash_board_slots_lock); + return number; + } + + if (slash_board_slot_count == SLASH_MAX_CARDS) { + mutex_unlock(&slash_board_slots_lock); + return -ENOSPC; + } + + entry = kzalloc(sizeof(*entry), GFP_KERNEL); + if (!entry) { + mutex_unlock(&slash_board_slots_lock); + return -ENOMEM; + } + + entry->domain = domain; + entry->bus = bus; + entry->slot = slot; + entry->number = slash_board_slot_count++; + entry->active_pfs = BIT(function); + list_add_tail(&entry->node, &slash_board_slots); + + number = entry->number; + mutex_unlock(&slash_board_slots_lock); + return number; +} + +void slash_chrdev_board_put(struct pci_dev *pdev) +{ + struct slash_board_slot *entry; + unsigned int domain = pci_domain_nr(pdev->bus); + unsigned int bus = pdev->bus->number; + unsigned int slot = PCI_SLOT(pdev->devfn); + unsigned int function = PCI_FUNC(pdev->devfn); + + mutex_lock(&slash_board_slots_lock); + + list_for_each_entry(entry, &slash_board_slots, node) { + if (entry->domain != domain || entry->bus != bus || + entry->slot != slot) + continue; + + entry->active_pfs &= ~BIT(function); + mutex_unlock(&slash_board_slots_lock); + return; + } + + mutex_unlock(&slash_board_slots_lock); + dev_warn(&pdev->dev, "character-device board slot was not allocated\n"); +} diff --git a/driver/slash_chrdev.h b/driver/slash_chrdev.h new file mode 100644 index 00000000..5d510ba1 --- /dev/null +++ b/driver/slash_chrdev.h @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; if + * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * @file slash_chrdev.h + * + * Shared character-device range, class, and stable board-slot allocator. + */ + +#ifndef SLASH_CHRDEV_H +#define SLASH_CHRDEV_H + +#include +#include +#include +#include +#include + +#include "slash_config.h" + +/** Reserve the shared device-number range and create /sys/class/slash. */ +int __init slash_chrdev_init(void); +/** Destroy the shared class, range, and module-lifetime board map. */ +void __exit slash_chrdev_exit(void); + +/** + * slash_chrdev_add() - Add one cdev and its BDF-specific class device. + * + * Return: Class device pointer, or ERR_PTR on failure. + */ +struct device *slash_chrdev_add(struct cdev *cdev, + const struct file_operations *fops, + unsigned int minor, + struct device *parent, + void *drvdata, + const char *name); +/** Remove a class device and its cdev. */ +void slash_chrdev_del(struct cdev *cdev, struct device *device); + +/** + * slash_chrdev_board_get() - Get the stable card number for a PCI endpoint. + * + * PF1 and PF2 at the same domain:bus:slot share a number. The mapping remains + * allocated until module exit. + * + * Return: Card number in [0, SLASH_MAX_CARDS), or negative errno. + */ +int slash_chrdev_board_get(struct pci_dev *pdev); +/** Mark one endpoint inactive without discarding its stable mapping. */ +void slash_chrdev_board_put(struct pci_dev *pdev); + +#endif /* SLASH_CHRDEV_H */ diff --git a/driver/slash_compat.h b/driver/slash_compat.h index e6719487..087325dd 100644 --- a/driver/slash_compat.h +++ b/driver/slash_compat.h @@ -15,6 +15,7 @@ #ifndef SLASH_COMPAT_H #define SLASH_COMPAT_H +#include #include #include #include @@ -35,6 +36,15 @@ static inline void slash_vm_flags_set(struct vm_area_struct *vma, vm_flags_t fla #endif } +static inline struct class *slash_class_create(const char *name) +{ +#if defined(SLASH_HAVE_CLASS_CREATE_ONE_ARG) + return class_create(name); +#else + return class_create(THIS_MODULE, name); +#endif +} + /* * MODULE_IMPORT_NS argument form. * diff --git a/driver/slash_config.h b/driver/slash_config.h index acebe253..05f52bca 100644 --- a/driver/slash_config.h +++ b/driver/slash_config.h @@ -66,20 +66,31 @@ /** PCI driver name for the PF1 QDMA function. */ #define SLASH_QDMA_DRV_NAME SLASH_NAME "_qdma" +/** Maximum number of distinct board BDFs tracked until module unload. */ +#define SLASH_MAX_CARDS 16 +/** Shared character-device range: hotplug plus CTL and QDMA per card. */ +#define SLASH_CHRDEV_MINORS (1 + 2 * SLASH_MAX_CARDS) + +#define SLASH_HOTPLUG_MINOR 0 +#define SLASH_CTLDEV_MINOR(card) (2 * (card) + 1) +#define SLASH_QDMA_MINOR(card) (2 * (card) + 2) +/** Recover the board slot from any non-hotplug CTL or QDMA minor. */ +#define SLASH_CARD_FROM_MINOR(minor) (((minor) - 1) / 2) + /** - * Name format for control misc devices. - * Uses pci_name() (e.g. "0000:03:00.2") — appears in /sys/class/misc. + * Sysfs name format for control character devices. + * Uses pci_name() (e.g. "0000:03:00.2") — appears in /sys/class/slash. */ #define SLASH_CTLDEV_NAME_FMT "slash_ctl_%s" /** - * Node name format for control misc devices. - * Uses an incrementing counter — appears as /dev/slash_ctl0, etc. + * Node name format for control character devices. + * Uses the board's stable slot — appears as /dev/slash_ctl0, etc. */ #define SLASH_CTLDEV_NODENAME_FMT "slash_ctl%d" -/** Name format for QDMA control misc devices (/sys/class/misc). */ +/** Sysfs name format for QDMA control devices (/sys/class/slash). */ #define SLASH_QDMA_CTLDEV_NAME_FMT "slash_qdma_ctl_%s" -/** Node name format for QDMA control misc devices (/dev/). */ +/** Stable board-slot node name format for QDMA control devices (/dev/). */ #define SLASH_QDMA_CTLDEV_NODENAME_FMT "slash_qdma_ctl%d" /* @@ -87,8 +98,7 @@ * For production, prefer a udev rule to set permissions instead of * changing these constants. */ -#define SLASH_CTLDEV_MODE 0600 -#define SLASH_CTLDEV_QDMA_MODE 0600 +#define SLASH_CHRDEV_MODE 0600 /* * Override the kernel's pr_fmt to prefix every pr_info/pr_err/pr_dbg diff --git a/driver/slash_ctldev.c b/driver/slash_ctldev.c index 857b8050..613efce8 100644 --- a/driver/slash_ctldev.c +++ b/driver/slash_ctldev.c @@ -17,7 +17,7 @@ * * Control device implementation for the SLASH kernel module. * - * Creates a per-device misc character device (/dev/slash_ctl) that + * Creates a per-device character device (/dev/slash_ctl) that * exposes device identity, BAR properties, and dma-buf-backed BAR * mappings to userspace via ioctl. This is an ioctl-only interface — * no read/write/mmap file operations are provided on the control @@ -34,7 +34,7 @@ #include "slash_ctldev.h" -#include +#include #include #include #include @@ -43,6 +43,7 @@ #include #include "slash.h" +#include "slash_chrdev.h" #include "slash_dmabuf.h" /** Compute the size of a struct member without needing an instance. */ @@ -78,42 +79,21 @@ static int slash_ctldev_set_bar_info(struct pci_dev *pdev, struct slash_ctldev *ctldev); static int slash_ctldev_create_bar_dmabufs(struct slash_ctldev *ctldev); -static int slash_ctldev_create_misc(struct slash_ctldev *ctldev); +static int slash_ctldev_create_chrdev(struct slash_ctldev *ctldev); -static void slash_ctldev_destroy_misc(struct slash_ctldev *ctldev); static void slash_ctldev_destroy_dmabufs(struct slash_ctldev *ctldev); +static void slash_ctldev_release(struct kref *ref); +static int slash_ctldev_fop_open(struct inode *, struct file *); +static int slash_ctldev_fop_release(struct inode *, struct file *); static long slash_ctldev_fop_ioctl(struct file *, unsigned int, unsigned long); - -/** - * struct slash_ctldev_id_entry - Stable BDF-to-number mapping entry. - * @node: Intrusive list linkage for @slash_ctldev_id_map. - * @bdf: Full PCI BDF string including function (e.g. "0000:61:00.2"). - * @number: The /dev/slash_ctl suffix permanently assigned to this BDF. - * @in_use: True while the device is bound to the driver. Cleared on remove, - * set on probe. A probe that finds @in_use already true indicates - * the kernel handed us a device that was never properly unbound — - * this should never happen under normal operation. - * - * Entries are allocated in probe and intentionally never freed. They survive - * hotplug remove+rescan cycles so that a device always gets back the same N. - */ -struct slash_ctldev_id_entry { - struct list_head node; - char bdf[32]; /* "DDDD:BB:SS.F\0" fits comfortably in 32 bytes */ - int number; - bool in_use; -}; - -/** Persistent BDF-to-number map; entries live for the module's lifetime. */ -static LIST_HEAD(slash_ctldev_id_map); -/** Serialises all accesses to @slash_ctldev_id_map and @in_use fields. */ -static DEFINE_MUTEX(slash_ctldev_id_map_lock); -/** Source of new numbers; only incremented when a BDF is seen for the first time. */ -static atomic_t slash_ctldev_devcount = ATOMIC_INIT(0); +static long slash_ctldev_ioctl(struct slash_ctldev *, unsigned int, + unsigned long); static struct file_operations slash_ctldev_fops = { .owner = THIS_MODULE, + .open = slash_ctldev_fop_open, + .release = slash_ctldev_fop_release, .unlocked_ioctl = slash_ctldev_fop_ioctl, }; @@ -122,27 +102,29 @@ static struct file_operations slash_ctldev_fops = { * @pdev: PCI device to create the control device for. * * Allocates the control device state, probes all PCI BARs, creates - * dma-buf exporters for MMIO BARs, and registers a misc device. + * dma-buf exporters for MMIO BARs, and registers a character device. * The state is stored as PCI driver data on @pdev. * * Return: 0 on success, negative errno on failure. */ int slash_ctldev_create(struct pci_dev *pdev) { + struct slash_ctldev *ctldev; int err; - struct slash_ctldev *ctldev = kzalloc(sizeof(*ctldev), GFP_KERNEL); + pci_set_drvdata(pdev, NULL); + ctldev = kzalloc(sizeof(*ctldev), GFP_KERNEL); if (!ctldev) { dev_err(&pdev->dev, "ctldev: kzalloc failed\n"); return -ENOMEM; } ctldev->pdev = pdev; + ctldev->slot = -1; + kref_init(&ctldev->ref); + mutex_init(&ctldev->lock); dev_info(&pdev->dev, "ctldev: creating control device\n"); - /* Store early so that the ioctl handler can find us via pci_get_drvdata(). */ - pci_set_drvdata(pdev, ctldev); - err = slash_ctldev_set_bar_info(pdev, ctldev); if (err) { dev_err(&pdev->dev, "ctldev: set_bar_info failed: %d\n", err); @@ -159,12 +141,14 @@ int slash_ctldev_create(struct pci_dev *pdev) goto err_destroy_dmabufs; } - err = slash_ctldev_create_misc(ctldev); + err = slash_ctldev_create_chrdev(ctldev); if (err) { - dev_err(&pdev->dev, "ctldev: creating misc ctldev failed: %d\n", err); + dev_err(&pdev->dev, "ctldev: creating character device failed: %d\n", + err); goto err_destroy_dmabufs; } + pci_set_drvdata(pdev, ctldev); dev_info(&pdev->dev, "ctldev: device created successfully\n"); return 0; @@ -173,7 +157,8 @@ int slash_ctldev_create(struct pci_dev *pdev) slash_ctldev_destroy_dmabufs(ctldev); err_free_ctldev: - kfree(ctldev); + pci_set_drvdata(pdev, NULL); + kref_put(&ctldev->ref, slash_ctldev_release); return err; } @@ -255,205 +240,132 @@ static int slash_ctldev_create_bar_dmabufs(struct slash_ctldev *ctldev) } /** - * slash_ctldev_id_get() - Look up or allocate a stable number for a BDF. - * @bdf: Full PCI BDF string (e.g. "0000:61:00.2") from pci_name(). - * - * Called from probe. Returns the number permanently associated with @bdf, - * allocating a new one if this BDF is seen for the first time. Also marks - * the entry as in_use = true. + * slash_ctldev_create_chrdev() - Register the control character device. + * @ctldev: Control device to register. * - * If an existing entry is found with in_use already set, the device was - * never properly unbound before probe was called again — this indicates a - * kernel PCI driver bug. The function logs a loud error and returns - * -EBUSY so that probe aborts without touching the device. + * The shared board allocator gives matching PF1/PF2 endpoints the same stable + * card number. Sysfs keeps the full PF2 BDF while the class devnode callback + * names the node /dev/slash_ctl. * - * Return: non-negative stable device number on success, negative errno on - * failure (-ENOMEM if allocation fails, -EBUSY if already in use). + * Return: 0 on success, negative errno on failure. */ -static int slash_ctldev_id_get(const char *bdf) +static int slash_ctldev_create_chrdev(struct slash_ctldev *ctldev) { - struct slash_ctldev_id_entry *entry; - int number; - - mutex_lock(&slash_ctldev_id_map_lock); + char name[64]; + + ctldev->slot = slash_chrdev_board_get(ctldev->pdev); + if (ctldev->slot < 0) + return ctldev->slot; + + snprintf(name, sizeof(name), SLASH_CTLDEV_NAME_FMT, + pci_name(ctldev->pdev)); + ctldev->device = + slash_chrdev_add(&ctldev->cdev, &slash_ctldev_fops, + SLASH_CTLDEV_MINOR(ctldev->slot), + &ctldev->pdev->dev, ctldev, name); + if (!IS_ERR(ctldev->device)) + return 0; - list_for_each_entry(entry, &slash_ctldev_id_map, node) { - if (strcmp(entry->bdf, bdf) != 0) - continue; + slash_chrdev_board_put(ctldev->pdev); + ctldev->slot = -1; + return PTR_ERR(ctldev->device); +} - if (entry->in_use) { - /* - * This BDF is already marked in_use. The kernel should - * never call probe for a device that is still bound — - * if this fires, something has gone badly wrong in the - * PCI driver infrastructure. - */ - pr_err("slash_ctldev: BUG: probe called for %s but entry is already in_use " - "(number=%d); refusing to bind\n", bdf, entry->number); - mutex_unlock(&slash_ctldev_id_map_lock); - return -EBUSY; - } +void slash_ctldev_destroy(struct pci_dev *pdev) +{ + struct slash_ctldev *ctldev = pci_get_drvdata(pdev); - entry->in_use = true; - number = entry->number; - mutex_unlock(&slash_ctldev_id_map_lock); - pr_info("slash_ctldev: reusing number %d for %s\n", number, bdf); - return number; - } + if (!ctldev) + return; - /* First time we've seen this BDF — allocate a fresh entry. */ - entry = kzalloc(sizeof(*entry), GFP_KERNEL); - if (!entry) { - mutex_unlock(&slash_ctldev_id_map_lock); - return -ENOMEM; - } + dev_info(&pdev->dev, "ctldev: destroying control device\n"); + pci_set_drvdata(pdev, NULL); - strscpy(entry->bdf, bdf, sizeof(entry->bdf)); - entry->number = atomic_inc_return(&slash_ctldev_devcount) - 1; - entry->in_use = true; - list_add_tail(&entry->node, &slash_ctldev_id_map); + mutex_lock(&ctldev->lock); + ctldev->dead = true; + mutex_unlock(&ctldev->lock); - number = entry->number; - mutex_unlock(&slash_ctldev_id_map_lock); + slash_chrdev_del(&ctldev->cdev, ctldev->device); + ctldev->device = NULL; + slash_chrdev_board_put(ctldev->pdev); + ctldev->slot = -1; + slash_ctldev_destroy_dmabufs(ctldev); - pr_info("slash_ctldev: assigned number %d to %s\n", number, bdf); - return number; + kref_put(&ctldev->ref, slash_ctldev_release); } -/** - * slash_ctldev_id_release() - Mark a BDF's entry as no longer in use. - * @bdf: Full PCI BDF string passed to the matching slash_ctldev_id_get() call. - * - * Called from remove. Clears in_use so that the next probe for the same - * BDF can reuse the stored number. The entry itself is not freed — it must - * persist so the number remains stable across hotplug cycles. - * - * If no entry exists for @bdf (should never happen after a successful probe), - * the call is a no-op and a warning is logged. - */ -static void slash_ctldev_id_release(const char *bdf) +static void slash_ctldev_release(struct kref *ref) { - struct slash_ctldev_id_entry *entry; - - mutex_lock(&slash_ctldev_id_map_lock); - - list_for_each_entry(entry, &slash_ctldev_id_map, node) { - if (strcmp(entry->bdf, bdf) != 0) - continue; - - entry->in_use = false; - mutex_unlock(&slash_ctldev_id_map_lock); - pr_info("slash_ctldev: released number %d for %s\n", entry->number, bdf); - return; - } + struct slash_ctldev *ctldev = + container_of(ref, struct slash_ctldev, ref); - /* Should be unreachable: remove without a prior successful probe. */ - pr_warn("slash_ctldev: WARNING: release called for %s but no entry found\n", bdf); - mutex_unlock(&slash_ctldev_id_map_lock); + mutex_destroy(&ctldev->lock); + kfree(ctldev); } -/** - * slash_ctldev_create_misc() - Register the misc character device. - * @ctldev: Control device to register. - * - * Creates /dev/slash_ctl with a stable index derived from the BDF-to-number - * map. The sysfs name includes the PCI BDF for identification; the /dev node - * uses a numeric suffix that is stable across hotplug remove+rescan cycles. - * - * Return: 0 on success, negative errno on failure. - */ -static int slash_ctldev_create_misc(struct slash_ctldev *ctldev) +static void slash_ctldev_destroy_dmabufs(struct slash_ctldev *ctldev) { - int err, id; - const char *name, *nodename; - - /* sysfs name: includes PCI BDF (e.g. "slash_ctl_0000:03:00.2"). */ - name = kasprintf(GFP_KERNEL, SLASH_CTLDEV_NAME_FMT, pci_name(ctldev->pdev)); - if (!name) { - dev_err(&ctldev->pdev->dev, "ctldev: kasprintf(name) failed\n"); - return -ENOMEM; - } - - /* /dev node name: stable numeric index from BDF-to-number map. */ - id = slash_ctldev_id_get(pci_name(ctldev->pdev)); - if (id < 0) { - dev_err(&ctldev->pdev->dev, "ctldev: id_get failed: %d\n", id); - err = id; - goto err_free_name; - } + int i; - nodename = kasprintf(GFP_KERNEL, SLASH_CTLDEV_NODENAME_FMT, id); - if (!nodename) { - dev_err(&ctldev->pdev->dev, "ctldev: kasprintf(nodename) failed\n"); - err = -ENOMEM; - goto err_release_id; + for (i = 0; i < PCI_STD_NUM_BARS; i++) { + if (ctldev->bars[i].dmabuf) { + dev_dbg(&ctldev->pdev->dev, "ctldev: destroying BAR%d dmabuf\n", i); + slash_bar_dmabuf_destroy(ctldev->bars[i].dmabuf); + } } +} - ctldev->misc.minor = MISC_DYNAMIC_MINOR; - ctldev->misc.name = name; - ctldev->misc.fops = &slash_ctldev_fops; - ctldev->misc.parent = &ctldev->pdev->dev; - ctldev->misc.nodename = nodename; - ctldev->misc.mode = SLASH_CTLDEV_MODE; +static int slash_ctldev_fop_open(struct inode *inode, struct file *file) +{ + struct slash_ctldev *ctldev = + container_of(inode->i_cdev, struct slash_ctldev, cdev); - err = misc_register(&ctldev->misc); - if (err) { - dev_err(&ctldev->pdev->dev, "ctldev: misc_register failed: %d\n", err); - goto err_free_nodename; + mutex_lock(&ctldev->lock); + if (ctldev->dead) { + mutex_unlock(&ctldev->lock); + return -ENODEV; } + kref_get(&ctldev->ref); + mutex_unlock(&ctldev->lock); + file->private_data = ctldev; return 0; - -err_free_nodename: - kfree(nodename); - -err_release_id: - /* id_get succeeded and set in_use; undo that. */ - slash_ctldev_id_release(pci_name(ctldev->pdev)); - -err_free_name: - kfree(name); - - return err; } -void slash_ctldev_destroy(struct pci_dev *pdev) +static int slash_ctldev_fop_release(struct inode *inode, struct file *file) { - struct slash_ctldev *ctldev = pci_get_drvdata(pdev); + struct slash_ctldev *ctldev = file->private_data; - dev_info(&pdev->dev, "ctldev: destroying control device\n"); - slash_ctldev_destroy_misc(ctldev); - slash_ctldev_destroy_dmabufs(ctldev); + (void)inode; - kfree(ctldev); + if (ctldev) + kref_put(&ctldev->ref, slash_ctldev_release); + file->private_data = NULL; + return 0; } -static void slash_ctldev_destroy_misc(struct slash_ctldev *ctldev) +static long slash_ctldev_fop_ioctl(struct file *file, unsigned int op, + unsigned long arg) { - dev_dbg(&ctldev->pdev->dev, "ctldev: deregistering misc device\n"); - misc_deregister(&ctldev->misc); - slash_ctldev_id_release(pci_name(ctldev->pdev)); - kfree(ctldev->misc.name); - kfree(ctldev->misc.nodename); - ctldev->misc.name = NULL; - ctldev->misc.nodename = NULL; -} + struct slash_ctldev *ctldev = file->private_data; + long ret; -static void slash_ctldev_destroy_dmabufs(struct slash_ctldev *ctldev) -{ - int i; + if (!ctldev) + return -ENODEV; - for (i = 0; i < PCI_STD_NUM_BARS; i++) { - if (ctldev->bars[i].dmabuf) { - dev_dbg(&ctldev->pdev->dev, "ctldev: destroying BAR%d dmabuf\n", i); - slash_bar_dmabuf_destroy(ctldev->bars[i].dmabuf); - } + mutex_lock(&ctldev->lock); + if (ctldev->dead) { + mutex_unlock(&ctldev->lock); + return -ENODEV; } + ret = slash_ctldev_ioctl(ctldev, op, arg); + mutex_unlock(&ctldev->lock); + return ret; } /** - * slash_ctldev_fop_ioctl() - Handle control device ioctls. - * @file: Open file for the misc device. + * slash_ctldev_ioctl() - Handle control device ioctls. + * @ctldev: Control device for the open file. * @op: ioctl command number. * @arg: Pointer to the user-space ioctl struct. * @@ -467,11 +379,10 @@ static void slash_ctldev_destroy_dmabufs(struct slash_ctldev *ctldev) * * Return: 0 (or positive fd for GET_BAR_FD) on success, negative errno on failure. */ -static long slash_ctldev_fop_ioctl(struct file *file, unsigned int op, unsigned long arg) +static long slash_ctldev_ioctl(struct slash_ctldev *ctldev, unsigned int op, + unsigned long arg) { - struct miscdevice *misc = file->private_data; - struct pci_dev *pdev = to_pci_dev(misc->parent); - struct slash_ctldev *ctldev = pci_get_drvdata(pdev); + struct pci_dev *pdev = ctldev->pdev; dev_dbg(&pdev->dev, "ctldev: ioctl op=0x%x\n", op); switch(op) { diff --git a/driver/slash_ctldev.h b/driver/slash_ctldev.h index efa72579..506e8257 100644 --- a/driver/slash_ctldev.h +++ b/driver/slash_ctldev.h @@ -17,7 +17,7 @@ * * Control device interface for the SLASH kernel module. * - * Each probed PF2 device gets a control misc device (/dev/slash_ctl) + * Each probed PF2 device gets a control character device (/dev/slash_ctl) * that exposes device identity, BAR properties, and dma-buf-backed BAR * mappings to userspace via ioctl. */ @@ -25,8 +25,11 @@ #ifndef SLASH_CTLDEV_H #define SLASH_CTLDEV_H +#include +#include #include -#include +#include +#include #include /** @@ -52,16 +55,26 @@ struct slash_ctldev_bar { /** * struct slash_ctldev - Per-device control device state. - * @pdev: Back-pointer to the PCI device this control device manages. - * @misc: Kernel misc device registered as /dev/slash_ctl. - * @bars: Cached BAR metadata for all standard PCI BARs (0-5). + * @pdev: Back-pointer to the PCI device this control device manages. + * @cdev: Character device embedded for inode-to-device lookup. + * @device: Class device with the BDF-specific sysfs name. + * @ref: Lifetime reference held by the PCI binding and each open fd. + * @lock: Serializes remove against open and ioctl. + * @slot: Stable board number shared with the matching PF1 endpoint. + * @dead: Set during remove; all subsequent opens/ioctls return -ENODEV. + * @bars: Cached BAR metadata for all standard PCI BARs (0-5). * * Allocated during probe, stored via pci_set_drvdata(), and freed * during remove. */ struct slash_ctldev { struct pci_dev *pdev; - struct miscdevice misc; + struct cdev cdev; + struct device *device; + struct kref ref; + struct mutex lock; + int slot; + bool dead; struct slash_ctldev_bar bars[PCI_STD_NUM_BARS]; }; @@ -70,7 +83,7 @@ struct slash_ctldev { * @pdev: PCI device to create the control device for. * * Probes BARs, creates dma-buf exporters for MMIO BARs, and registers - * a misc device. The control device state is stored as PCI driver + * a character device. The control device state is stored as PCI driver * data on @pdev. * * Return: 0 on success, negative errno on failure. @@ -81,7 +94,7 @@ int slash_ctldev_create(struct pci_dev *pdev); * slash_ctldev_destroy() - Destroy a control device. * @pdev: PCI device whose control device should be torn down. * - * Deregisters the misc device, destroys dma-buf exporters, and frees + * Deregisters the character device, destroys dma-buf exporters, and frees * the control device state. */ void slash_ctldev_destroy(struct pci_dev *pdev); diff --git a/driver/slash_hotplug.c b/driver/slash_hotplug.c index ac527979..2b514cbe 100644 --- a/driver/slash_hotplug.c +++ b/driver/slash_hotplug.c @@ -45,6 +45,7 @@ #include "slash_hotplug_driver.h" #include "slash.h" +#include "slash_chrdev.h" #include @@ -52,13 +53,17 @@ #include #include #include -#include #include #include #include #include -#define SLASH_HOTPLUG_MODE 0600 +struct slash_hotplug_device { + struct cdev cdev; + struct device *device; +}; + +static struct slash_hotplug_device slash_hotplug_device; /** * slash_hotplug_copy_request() - Copy and sanitize a hotplug request from userspace. @@ -371,7 +376,7 @@ static int slash_hotplug_handle_hotplug(const char *bdf) /** * slash_hotplug_ioctl() - Dispatch hotplug ioctl commands. - * @file: Open file for the hotplug misc device. + * @file: Open file for the hotplug character device. * @cmd: ioctl command number. * @arg: Userspace pointer to the request struct (for commands that need one). * @@ -455,41 +460,47 @@ static long slash_hotplug_compat_ioctl(struct file *file, unsigned int cmd, unsi } #endif +static int slash_hotplug_open(struct inode *inode, struct file *file) +{ + struct slash_hotplug_device *hotplug = + container_of(inode->i_cdev, struct slash_hotplug_device, cdev); + + file->private_data = hotplug; + return 0; +} + static const struct file_operations slash_hotplug_fops = { .owner = THIS_MODULE, + .open = slash_hotplug_open, .unlocked_ioctl = slash_hotplug_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = slash_hotplug_compat_ioctl, #endif }; -static struct miscdevice slash_hotplug_misc = { - .minor = MISC_DYNAMIC_MINOR, - .name = SLASH_HOTPLUG_DEVICE_NAME, - .fops = &slash_hotplug_fops, - .mode = SLASH_HOTPLUG_MODE, -}; - int slash_hotplug_init(void) { - int ret; - - pr_info("slash_hotplug: registering misc device\n"); - - ret = misc_register(&slash_hotplug_misc); - if (ret) { - pr_err("slash_hotplug: misc_register failed: %d\n", ret); - return ret; + slash_hotplug_device.device = + slash_chrdev_add(&slash_hotplug_device.cdev, &slash_hotplug_fops, + SLASH_HOTPLUG_MINOR, NULL, &slash_hotplug_device, + SLASH_HOTPLUG_DEVICE_NAME); + if (IS_ERR(slash_hotplug_device.device)) { + int err = PTR_ERR(slash_hotplug_device.device); + + slash_hotplug_device.device = NULL; + pr_err("slash_hotplug: character-device registration failed: %d\n", + err); + return err; } - pr_info("slash_hotplug: misc device registered as /dev/%s (minor %d)\n", - slash_hotplug_misc.name, slash_hotplug_misc.minor); + pr_info("slash_hotplug: registered /dev/%s\n", + SLASH_HOTPLUG_DEVICE_NAME); return 0; } void slash_hotplug_exit(void) { - pr_info("slash_hotplug: deregistering misc device\n"); - misc_deregister(&slash_hotplug_misc); - pr_info("slash_hotplug: misc device unregistered\n"); + slash_chrdev_del(&slash_hotplug_device.cdev, + slash_hotplug_device.device); + slash_hotplug_device.device = NULL; } diff --git a/driver/slash_hotplug_driver.h b/driver/slash_hotplug_driver.h index fe3cf0d4..3b256919 100644 --- a/driver/slash_hotplug_driver.h +++ b/driver/slash_hotplug_driver.h @@ -22,7 +22,7 @@ * and rescanning. This is essential for FPGA reconfiguration workflows * where loading a new bitstream requires re-enumerating the device. * - * A single misc device (/dev/slash_hotplug) handles ioctls. All + * A single character device (/dev/slash_hotplug) handles ioctls. All * operations that target a specific device require an explicit BDF. */ @@ -30,7 +30,7 @@ #define SLASH_HOTPLUG_DRIVER_H /** - * slash_hotplug_init() - Register the hotplug misc device. + * slash_hotplug_init() - Register the hotplug character device. * * Creates /dev/slash_hotplug. * @@ -39,7 +39,7 @@ int slash_hotplug_init(void); /** - * slash_hotplug_exit() - Unregister the hotplug misc device. + * slash_hotplug_exit() - Unregister the hotplug character device. */ void slash_hotplug_exit(void); diff --git a/driver/slash_main.c b/driver/slash_main.c index 5296e0e3..9b545959 100644 --- a/driver/slash_main.c +++ b/driver/slash_main.c @@ -29,18 +29,16 @@ * * Initialization order matters: * - * 1. **QDMA** — libqdma must be initialized and its PCI driver - * registered before any PCI probe runs, because PF1 and PF2 - * may probe concurrently once drivers are registered. + * 1. **Character devices** — reserve the shared major/minor range and + * create the slash class before any endpoint is exposed. * - * 2. **Hotplug** — the /dev/slash_hotplug misc device must exist - * before PCI probe registers devices into the tracking list. + * 2. **Hotplug** — /dev/slash_hotplug must exist before either PCI + * driver is registered. * - * 3. **PCIe** — registering the PF2 PCI driver triggers probe for - * any devices already present on the bus. + * 3. **QDMA / PCIe** — register PF1, then PF2. Registering either + * driver may immediately probe devices already present on the bus. * - * Teardown is the reverse: PCIe first (unbinds devices), then hotplug - * (frees tracking list), then QDMA (shuts down libqdma). + * Teardown is the strict reverse order. */ #include "slash.h" @@ -51,6 +49,7 @@ #include #include +#include "slash_chrdev.h" #include "slash_pcie.h" #include "slash_hotplug_driver.h" #include "slash_qdma.h" @@ -78,31 +77,44 @@ static int __init slash_init(void) pr_info("slash: module init\n"); - /* 1. QDMA first — libqdma + PF1 PCI driver. */ - err = slash_qdma_init(qdma_num_threads, NULL); + /* Reserve all stable minors and create the shared class first. */ + err = slash_chrdev_init(); if (err) { - pr_err("slash: libqdma init failed: %d\n", err); + pr_err("slash: character-device init failed: %d\n", err); return err; } - /* 2. Hotplug — /dev/slash_hotplug misc device. */ + /* The hotplug endpoint must exist before either PCI probe can run. */ err = slash_hotplug_init(); if (err) { pr_err("slash: hotplug init failed: %d\n", err); - slash_qdma_exit(); - return err; + goto err_chrdev; + } + + /* libqdma initialization precedes PF1 PCI-driver registration. */ + err = slash_qdma_init(qdma_num_threads, NULL); + if (err) { + pr_err("slash: libqdma init failed: %d\n", err); + goto err_hotplug; } - /* 3. PCIe — PF2 PCI driver (triggers probe for present devices). */ + /* Register PF2 last; both PCI drivers can now create shared-class nodes. */ err = slash_pcie_init(); if (err) { pr_err("slash: PCIe init failed: %d\n", err); - slash_hotplug_exit(); - return err; + goto err_qdma; } pr_info("slash: module init complete\n"); return 0; + +err_qdma: + slash_qdma_exit(); +err_hotplug: + slash_hotplug_exit(); +err_chrdev: + slash_chrdev_exit(); + return err; } /** @@ -114,8 +126,9 @@ static void __exit slash_exit(void) { pr_info("slash: module exit\n"); slash_pcie_exit(); - slash_hotplug_exit(); slash_qdma_exit(); + slash_hotplug_exit(); + slash_chrdev_exit(); pr_info("slash: module exit complete\n"); } diff --git a/driver/slash_qdma.c b/driver/slash_qdma.c index d6c036ff..4344a9f2 100644 --- a/driver/slash_qdma.c +++ b/driver/slash_qdma.c @@ -48,6 +48,7 @@ #include "libqdma_export.h" #include "slash.h" +#include "slash_chrdev.h" #include #include @@ -61,7 +62,6 @@ #include #include #include -#include #include #include #include @@ -267,9 +267,10 @@ struct slash_qdma_qpair_entry { * @pdev: The PCI device (PF1) this instance is bound to. * @qdma_handle: Opaque handle returned by qdma_device_open(); * passed to every subsequent libqdma call. - * @misc: Miscdevice registered under /dev/slash_qdma_ctlN. - * Userspace opens this to issue queue management ioctls. - * @ref: Device-level reference count. The miscdevice open + * @cdev: Character device embedded for inode-to-device lookup. + * @device: Class device with the BDF-specific sysfs name. + * @slot: Stable board number shared with the matching PF2. + * @ref: Device-level reference count. The control-device open * path and each anon_inode fd hold a ref; the device * structure is freed when the last ref drops. * @lock: Serialises ioctl paths and protects @qpairs, @@ -281,13 +282,14 @@ struct slash_qdma_qpair_entry { * teardown. * @have_qdma_handle: True once qdma_device_open() succeeds; false after * qdma_device_close(). Guards against use-after-close. - * @is_misc_registered: True while the miscdevice is live. Prevents double - * deregistration on error paths. + * @have_board_slot: True after the shared board allocator is acquired. + * @is_cdev_registered: True while the character device is live. * @hw_shutdown: Set to true during destroy to signal that the HW is * going away. Any ioctl arriving after this flag is * set returns -ENODEV immediately. * - * The three booleans (@have_qdma_handle, @is_misc_registered, + * The state booleans (@have_qdma_handle, @have_board_slot, + * @is_cdev_registered, * @hw_shutdown) track partially-constructed state during probe/remove * error paths; outside of create/destroy they should always reflect a * fully initialised device. @@ -296,7 +298,9 @@ struct slash_qdma_dev { struct pci_dev *pdev; unsigned long qdma_handle; - struct miscdevice misc; + struct cdev cdev; + struct device *device; + int slot; struct kref ref; struct mutex lock; struct xarray qpairs; @@ -306,7 +310,8 @@ struct slash_qdma_dev { * Assume these are always true outside of create/destroy. */ bool have_qdma_handle; - bool is_misc_registered; + bool have_board_slot; + bool is_cdev_registered; bool hw_shutdown; }; @@ -649,29 +654,22 @@ static int slash_qdma_create_qdma_device(struct pci_dev *pdev, struct slash_qdma static void slash_qdma_destroy_qdma_device(struct slash_qdma_dev *device); static void slash_qdma_dev_release(struct kref *ref); static void slash_qdma_conf_options(struct qdma_dev_conf *conf, struct pci_dev *pdev); -static int slash_qdma_ioctl_info_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_info_w(struct slash_qdma_dev *qdma_dev, void __user *uarg); -static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add_w(struct slash_qdma_dev *qdma_dev, void __user *uarg); -static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_add *req); -static int slash_qdma_ioctl_qpair_add_q(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add_q(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_add *req, struct slash_qdma_qpair_entry *entry, enum queue_type_t qtype); -static void slash_qdma_ioctl_qpair_rm_q(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static void slash_qdma_ioctl_qpair_rm_q(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_entry *entry, enum queue_type_t qtype); -static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_op_w(struct slash_qdma_dev *qdma_dev, void __user *uarg); -static int slash_qdma_ioctl_qpair_op(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_op(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_op *req); static int slash_qdma_ioctl_qpair_op_apply(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_entry *entry, @@ -679,11 +677,9 @@ static int slash_qdma_ioctl_qpair_op_apply(struct slash_qdma_dev *qdma_dev, slash_qdma_queue_cmd_fn fn, const char *op_name, bool stop_on_err); -static int slash_qdma_ioctl_qpair_get_fd_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_get_fd_w(struct slash_qdma_dev *qdma_dev, void __user *uarg); -static int slash_qdma_ioctl_buf_create_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_buf_create_w(struct slash_qdma_dev *qdma_dev, void __user *uarg); static void slash_qdma_buf_release(struct kref *ref); static void slash_qdma_buf_put(struct slash_qdma_buf *buf); @@ -719,7 +715,8 @@ static const struct file_operations slash_qdma_qpair_fops = { static int slash_qdma_fop_open(struct inode *inode, struct file *file); static int slash_qdma_fop_release(struct inode *inode, struct file *file); static long slash_qdma_fop_ioctl(struct file *file, unsigned int op, unsigned long arg); -static void slash_qdma_ioctl_info(struct miscdevice *misc, struct slash_qdma_dev *qdma_dev, struct slash_qdma_info *qdma_info); +static void slash_qdma_ioctl_info(struct slash_qdma_dev *qdma_dev, + struct slash_qdma_info *qdma_info); @@ -750,9 +747,9 @@ static struct pci_driver slash_qdma_driver = { }; /** - * slash_qdma_fops - File operations for the QDMA control miscdevice. + * slash_qdma_fops - File operations for the QDMA control device. * - * The miscdevice (/dev/slash_qdma_ctlN) is the management interface: + * /dev/slash_qdma_ctlN is the management interface: * userspace opens it and issues ioctls to add/start/stop/delete queue * pairs and to obtain per-qpair I/O fds. */ @@ -763,129 +760,6 @@ static struct file_operations slash_qdma_fops = { .unlocked_ioctl = slash_qdma_fop_ioctl, }; -/* ───────────────────────────────────────────────────────────────────── - * BDF-to-device-number map (stable /dev/slash_qdma_ctlN across hotplug) - * ───────────────────────────────────────────────────────────────────── */ - -/** - * struct slash_qdma_id_entry - Stable BDF-to-number mapping entry. - * @node: Intrusive list linkage for @slash_qdma_id_map. - * @bdf: Full PCI BDF string including function (e.g. "0000:61:00.1"). - * @number: The /dev/slash_qdma_ctl suffix permanently assigned to this BDF. - * @in_use: True while the device is bound to the driver. Cleared on remove, - * set on probe. A probe that finds @in_use already true indicates - * the kernel handed us a device that was never properly unbound — - * this should never happen under normal operation. - * - * Entries are allocated in probe and intentionally never freed. They survive - * hotplug remove+rescan cycles so that a device always gets back the same N. - */ -struct slash_qdma_id_entry { - struct list_head node; - char bdf[32]; /* "DDDD:BB:SS.F\0" fits comfortably in 32 bytes */ - int number; - bool in_use; -}; - -/** Persistent BDF-to-number map; entries live for the module's lifetime. */ -static LIST_HEAD(slash_qdma_id_map); -/** Serialises all accesses to @slash_qdma_id_map and @in_use fields. */ -static DEFINE_MUTEX(slash_qdma_id_map_lock); -/** Source of new numbers; only incremented when a BDF is seen for the first time. */ -static atomic_t slash_qdma_devcount = ATOMIC_INIT(0); - -/** - * slash_qdma_id_get() - Look up or allocate a stable number for a BDF. - * @bdf: Full PCI BDF string (e.g. "0000:61:00.1") from pci_name(). - * - * Called from probe. Returns the number permanently associated with @bdf, - * allocating a new one if this BDF is seen for the first time. Also marks - * the entry as in_use = true. - * - * If an existing entry is found with in_use already set, the device was - * never properly unbound before probe was called again — this indicates a - * kernel PCI driver bug. The function logs a loud error and returns - * -EBUSY so that probe aborts without touching the device. - * - * Return: non-negative stable device number on success, negative errno on - * failure (-ENOMEM if allocation fails, -EBUSY if already in use). - */ -static int slash_qdma_id_get(const char *bdf) -{ - struct slash_qdma_id_entry *entry; - int number; - - mutex_lock(&slash_qdma_id_map_lock); - - list_for_each_entry(entry, &slash_qdma_id_map, node) { - if (strcmp(entry->bdf, bdf) != 0) - continue; - - if (entry->in_use) { - pr_err("slash_qdma: BUG: probe called for %s but entry is already in_use " - "(number=%d); refusing to bind\n", bdf, entry->number); - mutex_unlock(&slash_qdma_id_map_lock); - return -EBUSY; - } - - entry->in_use = true; - number = entry->number; - mutex_unlock(&slash_qdma_id_map_lock); - pr_info("slash_qdma: reusing number %d for %s\n", number, bdf); - return number; - } - - /* First time we've seen this BDF — allocate a fresh entry. */ - entry = kzalloc(sizeof(*entry), GFP_KERNEL); - if (!entry) { - mutex_unlock(&slash_qdma_id_map_lock); - return -ENOMEM; - } - - strscpy(entry->bdf, bdf, sizeof(entry->bdf)); - entry->number = atomic_inc_return(&slash_qdma_devcount) - 1; - entry->in_use = true; - list_add_tail(&entry->node, &slash_qdma_id_map); - - number = entry->number; - mutex_unlock(&slash_qdma_id_map_lock); - - pr_info("slash_qdma: assigned number %d to %s\n", number, bdf); - return number; -} - -/** - * slash_qdma_id_release() - Mark a BDF's entry as no longer in use. - * @bdf: Full PCI BDF string passed to the matching slash_qdma_id_get() call. - * - * Called when the misc device is deregistered (remove path, or probe error - * unwind after misc_register succeeds). Clears in_use so that the next probe - * for the same BDF can reuse the stored number. The entry itself is not freed. - * - * If no entry exists for @bdf (should never happen after a successful probe), - * the call is a no-op and a warning is logged. - */ -static void slash_qdma_id_release(const char *bdf) -{ - struct slash_qdma_id_entry *entry; - - mutex_lock(&slash_qdma_id_map_lock); - - list_for_each_entry(entry, &slash_qdma_id_map, node) { - if (strcmp(entry->bdf, bdf) != 0) - continue; - - entry->in_use = false; - mutex_unlock(&slash_qdma_id_map_lock); - pr_info("slash_qdma: released number %d for %s\n", entry->number, bdf); - return; - } - - /* Should be unreachable: release without a prior successful id_get. */ - pr_warn("slash_qdma: WARNING: release called for %s but no entry found\n", bdf); - mutex_unlock(&slash_qdma_id_map_lock); -} - /* ───────────────────────────────────────────────────────────────────── * Module init / exit * ───────────────────────────────────────────────────────────────────── */ @@ -1247,7 +1121,7 @@ static int slash_qdma_program_host_profiles(struct slash_qdma_dev *device) * the control function handled by slash_ctldev). Then: * 1. Allocates and initialises a slash_qdma_dev structure. * 2. Configures and opens the libqdma device via qdma_device_open(). - * 3. Registers the management miscdevice (/dev/slash_qdma_ctlN). + * 3. Registers the management character device (/dev/slash_qdma_ctlN). * * On any failure, the partially-constructed device is torn down and * the probe returns the error. @@ -1259,6 +1133,7 @@ static int slash_qdma_probe(struct pci_dev *pdev, const struct pci_device_id *id int err; struct qdma_dev_conf conf; struct slash_qdma_dev *device = NULL; + char name[64]; memset(&conf, 0, sizeof(conf)); @@ -1297,7 +1172,7 @@ static int slash_qdma_probe(struct pci_dev *pdev, const struct pci_device_id *id device->have_qdma_handle = true; /* - * Program the CPM5 Host Profiles before exposing the miscdevice, so + * Program the CPM5 Host Profiles before exposing the character device, so * they exist before userspace can add any queue. Host ID 0 steers * AXI4-MM traffic to NoC Channel 0 and Host ID 1 to NoC Channel 1; * the per-queue SW-context host_id (mirrored from mm_channel = qid & 1) @@ -1310,18 +1185,20 @@ static int slash_qdma_probe(struct pci_dev *pdev, const struct pci_device_id *id goto err_free; } - /* Register the management miscdevice so userspace can issue ioctls. */ - err = misc_register(&device->misc); - if (err) { - dev_err(&pdev->dev, "slash: qdma: could not register misc device: %d", err); - /* - * is_misc_registered is still false here, so slash_qdma_destroy_qdma_device - * will not call misc_deregister or id_release. Release the id explicitly. - */ - slash_qdma_id_release(pci_name(pdev)); + snprintf(name, sizeof(name), SLASH_QDMA_CTLDEV_NAME_FMT, pci_name(pdev)); + device->device = + slash_chrdev_add(&device->cdev, &slash_qdma_fops, + SLASH_QDMA_MINOR(device->slot), &pdev->dev, + device, name); + if (IS_ERR(device->device)) { + err = PTR_ERR(device->device); + device->device = NULL; + dev_err(&pdev->dev, + "slash: qdma: could not register character device: %d", + err); goto err_free; } - device->is_misc_registered = true; + device->is_cdev_registered = true; return 0; @@ -1339,7 +1216,7 @@ static int slash_qdma_probe(struct pci_dev *pdev, const struct pci_device_id *id * @pdev: The PCI device being removed. * * Tears down all HW queues, closes the libqdma device, deregisters the - * miscdevice, and drops the device reference. + * character device, and drops the device reference. */ static void slash_qdma_remove(struct pci_dev *pdev) { @@ -1361,71 +1238,44 @@ static void slash_qdma_remove(struct pci_dev *pdev) * @pdev: PCI device to bind to. * @pdevice: [out] Receives a pointer to the new device on success. * - * Allocates the slash_qdma_dev, initialises its mutex, xarray, kref, - * and miscdevice fields, and stores it in the PCI drvdata. A static - * atomic counter provides unique /dev node numbering across devices. + * Allocates the slash_qdma_dev, initialises its mutex, xarray, kref, stable + * board slot, and stores it in the PCI drvdata. * * Return: 0 on success, negative errno on failure. */ static int slash_qdma_create_qdma_device(struct pci_dev *pdev, struct slash_qdma_dev **pdevice) { - int err; struct slash_qdma_dev *device; - int id; + int err; + pci_set_drvdata(pdev, NULL); device = kzalloc(sizeof(*device), GFP_KERNEL); - if (!device) { + if (!device) return -ENOMEM; - } - device->pdev = pdev; + + device->pdev = pci_dev_get(pdev); + device->slot = -1; kref_init(&device->ref); mutex_init(&device->lock); xa_init_flags(&device->qpairs, XA_FLAGS_ALLOC); device->hw_shutdown = false; pci_set_drvdata(pdev, device); - { /* Miscdevice setup */ - device->misc.minor = MISC_DYNAMIC_MINOR; - device->misc.fops = &slash_qdma_fops; - device->misc.parent = &pdev->dev; - device->misc.mode = SLASH_CTLDEV_QDMA_MODE; - - /* Name visible in /sys/class/misc, includes PCI BDF for uniqueness. */ - device->misc.name = kasprintf(GFP_KERNEL, SLASH_QDMA_CTLDEV_NAME_FMT, pci_name(device->pdev)); - if (!device->misc.name) { - dev_err(&device->pdev->dev, "qdma: kasprintf(name) failed\n"); - err = -ENOMEM; - goto err_free; - } - - /* /dev node name: stable numeric index from BDF-to-number map. */ - id = slash_qdma_id_get(pci_name(device->pdev)); - if (id < 0) { - dev_err(&device->pdev->dev, "qdma: id_get failed: %d\n", id); - err = id; - goto err_free_name; - } - - device->misc.nodename = kasprintf(GFP_KERNEL, SLASH_QDMA_CTLDEV_NODENAME_FMT, id); - if (!device->misc.nodename) { - dev_err(&device->pdev->dev, "qdma: kasprintf(nodename) failed\n"); - err = -ENOMEM; - goto err_release_id; - } + err = slash_chrdev_board_get(pdev); + if (err < 0) { + dev_err(&pdev->dev, "qdma: board-slot allocation failed: %d\n", + err); + goto err_free; } + device->slot = err; + device->have_board_slot = true; *pdevice = device; return 0; -err_release_id: - slash_qdma_id_release(pci_name(device->pdev)); - -err_free_name: - kfree(device->misc.name); - device->misc.name = NULL; - err_free: slash_qdma_destroy_qdma_device(device); + kref_put(&device->ref, slash_qdma_dev_release); *pdevice = NULL; return err; @@ -1441,7 +1291,7 @@ static int slash_qdma_create_qdma_device(struct pci_dev *pdev, struct slash_qdma * * Teardown order: * 1. Set @hw_shutdown = true (prevents new ioctls). - * 2. Deregister the miscdevice (prevents new opens). + * 2. Deregister the character device (prevents new opens). * 3. Iterate all queue pairs: stop, remove each HW queue, erase from * xarray, and drop the xarray's ref. * 4. Destroy the xarray. @@ -1469,11 +1319,16 @@ static void slash_qdma_destroy_qdma_device(struct slash_qdma_dev *device) /* Detach from PCI drvdata so no new lookups can find us. */ pci_set_drvdata(device->pdev, NULL); - /* Deregister miscdevice to prevent new file opens. */ - if (device->is_misc_registered) { - misc_deregister(&device->misc); - slash_qdma_id_release(pci_name(device->pdev)); - device->is_misc_registered = false; + /* Remove the character device before tearing down hardware state. */ + if (device->is_cdev_registered) { + slash_chrdev_del(&device->cdev, device->device); + device->device = NULL; + device->is_cdev_registered = false; + } + if (device->have_board_slot) { + slash_chrdev_board_put(device->pdev); + device->slot = -1; + device->have_board_slot = false; } mutex_lock(&device->lock); @@ -1496,7 +1351,7 @@ static void slash_qdma_destroy_qdma_device(struct slash_qdma_dev *device) if (!(entry->dir_mask & dir_bit)) continue; - slash_qdma_ioctl_qpair_rm_q(&device->misc, device, entry, qtype); + slash_qdma_ioctl_qpair_rm_q(device, entry, qtype); } xa_erase(&device->qpairs, index); slash_qdma_qpair_put(entry); @@ -1528,9 +1383,9 @@ static void slash_qdma_destroy_qdma_device(struct slash_qdma_dev *device) * slash_qdma_dev_release() - kref release callback for the QDMA device. * @ref: kref embedded in the slash_qdma_dev being released. * - * Called when the last reference drops (after both the miscdevice is - * closed and all anon_inode fds are released). Frees the dynamically - * allocated miscdevice name/nodename strings and the device structure. + * Called when the last reference drops (after both the control device is + * closed and all anon_inode fds are released). Drops the retained PCI + * device reference and frees the device structure. */ static void slash_qdma_dev_release(struct kref *ref) { @@ -1538,15 +1393,7 @@ static void slash_qdma_dev_release(struct kref *ref) container_of(ref, struct slash_qdma_dev, ref); mutex_destroy(&device->lock); - - if (device->misc.name) { - kfree(device->misc.name); - } - - if (device->misc.nodename) { - kfree(device->misc.nodename); - } - + pci_dev_put(device->pdev); kfree(device); } @@ -1611,12 +1458,12 @@ static void slash_qdma_conf_options(struct qdma_dev_conf *conf, struct pci_dev * } /* ───────────────────────────────────────────────────────────────────── - * Miscdevice file operations (management interface) + * Character-device file operations (management interface) * ───────────────────────────────────────────────────────────────────── */ /** - * slash_qdma_fop_ioctl() - Dispatch ioctls on the QDMA control miscdevice. - * @file: Open file for the miscdevice. + * slash_qdma_fop_ioctl() - Dispatch ioctls on the QDMA control device. + * @file: Open file for the character device. * @op: Ioctl command number. * @arg: User-space argument pointer. * @@ -1634,15 +1481,12 @@ static void slash_qdma_conf_options(struct qdma_dev_conf *conf, struct pci_dev * static long slash_qdma_fop_ioctl(struct file *file, unsigned int op, unsigned long arg) { struct slash_qdma_dev *qdma_dev = file->private_data; - struct miscdevice *misc; void __user *uarg = (void __user *)arg; long ret = 0; if (!qdma_dev) return -ENODEV; - misc = &qdma_dev->misc; - SLASH_QDMA_OP_DEV_LOG(&qdma_dev->pdev->dev, "ioctl op=0x%x\n", op); /* Early rejection if the device is shutting down. */ @@ -1655,23 +1499,23 @@ static long slash_qdma_fop_ioctl(struct file *file, unsigned int op, unsigned lo switch (op) { case SLASH_QDMA_IOCTL_INFO: - ret = slash_qdma_ioctl_info_w(misc, qdma_dev, uarg); + ret = slash_qdma_ioctl_info_w(qdma_dev, uarg); break; case SLASH_QDMA_IOCTL_QPAIR_ADD: - ret = slash_qdma_ioctl_qpair_add_w(misc, qdma_dev, uarg); + ret = slash_qdma_ioctl_qpair_add_w(qdma_dev, uarg); break; case SLASH_QDMA_IOCTL_Q_OP: - ret = slash_qdma_ioctl_qpair_op_w(misc, qdma_dev, uarg); + ret = slash_qdma_ioctl_qpair_op_w(qdma_dev, uarg); break; case SLASH_QDMA_IOCTL_QPAIR_GET_FD: - ret = slash_qdma_ioctl_qpair_get_fd_w(misc, qdma_dev, uarg); + ret = slash_qdma_ioctl_qpair_get_fd_w(qdma_dev, uarg); break; case SLASH_QDMA_IOCTL_BUF_CREATE: - ret = slash_qdma_ioctl_buf_create_w(misc, qdma_dev, uarg); + ret = slash_qdma_ioctl_buf_create_w(qdma_dev, uarg); break; default: @@ -1683,22 +1527,19 @@ static long slash_qdma_fop_ioctl(struct file *file, unsigned int op, unsigned lo } /** - * slash_qdma_fop_open() - Open handler for the QDMA control miscdevice. + * slash_qdma_fop_open() - Open handler for the QDMA control device. * @inode: Inode of the device node. * @file: File being opened. * - * The misc framework sets file->private_data to the miscdevice before - * calling open. We use container_of to recover the slash_qdma_dev, - * take a device reference, and stash the device pointer in private_data - * so that subsequent ioctl/release calls can find it directly. + * Uses inode->i_cdev to recover the slash_qdma_dev, takes a device reference, + * and stores it in file->private_data for ioctl/release. * * Return: 0 on success, -ENODEV if the device is shutting down. */ static int slash_qdma_fop_open(struct inode *inode, struct file *file) { - struct miscdevice *misc = file->private_data; struct slash_qdma_dev *qdma_dev = - container_of(misc, struct slash_qdma_dev, misc); + container_of(inode->i_cdev, struct slash_qdma_dev, cdev); mutex_lock(&qdma_dev->lock); if (qdma_dev->hw_shutdown || !qdma_dev->have_qdma_handle) { @@ -1714,7 +1555,7 @@ static int slash_qdma_fop_open(struct inode *inode, struct file *file) } /** - * slash_qdma_fop_release() - Release handler for the QDMA control miscdevice. + * slash_qdma_fop_release() - Release handler for the QDMA control device. * @inode: Inode of the device node. * @file: File being closed. * @@ -1743,7 +1584,6 @@ static int slash_qdma_fop_release(struct inode *inode, struct file *file) /** * slash_qdma_ioctl_info_w() - Wrapper for the QDMA info ioctl. - * @misc: Miscdevice handle. * @qdma_dev: QDMA device. * @uarg: User-space pointer to a slash_qdma_info struct. * @@ -1755,8 +1595,7 @@ static int slash_qdma_fop_release(struct inode *inode, struct file *file) * * Return: 0 on success, -EFAULT on copy failure, -ENODEV if shutting down. */ -static int slash_qdma_ioctl_info_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_info_w(struct slash_qdma_dev *qdma_dev, void __user *uarg) { struct slash_qdma_info info; @@ -1767,7 +1606,7 @@ static int slash_qdma_ioctl_info_w(struct miscdevice *misc, return -EFAULT; if (user_size < SLASH_QDMA_INFO_MIN_SIZE) { - dev_warn(misc->this_device, + dev_warn(&qdma_dev->pdev->dev, "qdma: INFO size too small (%u)\n", user_size); return -EINVAL; } @@ -1780,7 +1619,7 @@ static int slash_qdma_ioctl_info_w(struct miscdevice *misc, mutex_unlock(&qdma_dev->lock); return -ENODEV; } - slash_qdma_ioctl_info(misc, qdma_dev, &info); + slash_qdma_ioctl_info(qdma_dev, &info); mutex_unlock(&qdma_dev->lock); copy_size = min_t(size_t, user_size, sizeof(info)); @@ -1797,20 +1636,17 @@ static int slash_qdma_ioctl_info_w(struct miscdevice *misc, /** * slash_qdma_ioctl_info() - Populate QDMA capability information. - * @misc: Miscdevice handle (unused). - * @qdma_dev: QDMA device (unused for now). + * @qdma_dev: QDMA device whose PF1 identity is reported. * @qdma_info: [out] Structure to fill with capability data. * * Currently returns zeroes for all fields. This is a placeholder for * future capability reporting (e.g., querying qdma_device_capabilities). */ -static void slash_qdma_ioctl_info(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static void slash_qdma_ioctl_info(struct slash_qdma_dev *qdma_dev, struct slash_qdma_info *qdma_info) { - (void) misc; - (void) qdma_dev; - + strscpy(qdma_info->bdf, pci_name(qdma_dev->pdev), + sizeof(qdma_info->bdf)); qdma_info->qsets_max = 0; qdma_info->msix_qvecs = 0; qdma_info->vf_max = 0; @@ -1823,7 +1659,6 @@ static void slash_qdma_ioctl_info(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_add_w() - Wrapper for the qpair-add ioctl. - * @misc: Miscdevice handle. * @qdma_dev: QDMA device. * @uarg: User-space pointer to a slash_qdma_qpair_add struct. * @@ -1839,8 +1674,7 @@ static void slash_qdma_ioctl_info(struct miscdevice *misc, * * Return: 0 on success, negative errno on failure. */ -static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add_w(struct slash_qdma_dev *qdma_dev, void __user *uarg) { struct slash_qdma_qpair_add req; @@ -1858,7 +1692,7 @@ static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, return -EFAULT; if (user_size < SLASH_QDMA_QPAIR_ADD_MIN_SIZE) { - dev_warn(misc->this_device, + dev_warn(&qdma_dev->pdev->dev, "qdma: QPAIR_ADD size too small (%u)\n", user_size); return -EINVAL; } @@ -1906,7 +1740,7 @@ static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, mutex_unlock(&qdma_dev->lock); return -ENODEV; } - err = slash_qdma_ioctl_qpair_add(misc, qdma_dev, &req); + err = slash_qdma_ioctl_qpair_add(qdma_dev, &req); mutex_unlock(&qdma_dev->lock); if (err) @@ -1932,7 +1766,6 @@ static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_add() - Allocate a qpair and add its constituent queues. - * @misc: Miscdevice handle. * @qdma_dev: QDMA device. * @req: Add request (dir_mask, mode, ring sizes); @qid is set on success. * @@ -1948,8 +1781,7 @@ static int slash_qdma_ioctl_qpair_add_w(struct miscdevice *misc, * * Return: 0 on success, negative errno on failure. */ -static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_add *req) { struct slash_qdma_qpair_entry *entry = kzalloc(sizeof(*entry), GFP_KERNEL); @@ -1989,7 +1821,7 @@ static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, if (!(req->dir_mask & dir_bit)) continue; - ret = slash_qdma_ioctl_qpair_add_q(misc, qdma_dev, req, entry, qtype); + ret = slash_qdma_ioctl_qpair_add_q(qdma_dev, req, entry, qtype); if (ret) goto rollback; @@ -2002,7 +1834,7 @@ static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, /* Undo any queues that were successfully added before the failure. */ for (idx = 0; idx < SLASH_QDMA_QTYPE_COUNT; idx++) { if (added[idx]) - slash_qdma_ioctl_qpair_rm_q(misc, qdma_dev, entry, idx); + slash_qdma_ioctl_qpair_rm_q(qdma_dev, entry, idx); } slash_qdma_qpair_remove(qdma_dev, req->qid); @@ -2012,7 +1844,6 @@ static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_add_q() - Add a single HW queue to a queue pair. - * @misc: Miscdevice handle (for error logging context). * @qdma_dev: QDMA device. * @req: The add request (provides queue index, mode, and ring sizes). * @entry: The qpair entry to attach the new queue to. @@ -2056,8 +1887,7 @@ static int slash_qdma_ioctl_qpair_add(struct miscdevice *misc, * * Return: 0 on success, negative errno on failure. */ -static int slash_qdma_ioctl_qpair_add_q(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_add_q(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_add *req, struct slash_qdma_qpair_entry *entry, enum queue_type_t qtype) @@ -2189,7 +2019,6 @@ static int slash_qdma_ioctl_qpair_add_q(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_rm_q() - Remove a single HW queue from a queue pair. - * @misc: Miscdevice handle (for logging context). * @qdma_dev: QDMA device. * @entry: The qpair entry to remove the queue from. * @qtype: Which queue type to remove (Q_H2C, Q_C2H, or Q_CMPT). @@ -2202,8 +2031,7 @@ static int slash_qdma_ioctl_qpair_add_q(struct miscdevice *misc, * Errors are logged but not propagated — this is best-effort cleanup * used during teardown. */ -static void slash_qdma_ioctl_qpair_rm_q(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static void slash_qdma_ioctl_qpair_rm_q(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_entry *entry, enum queue_type_t qtype) { @@ -2247,7 +2075,6 @@ static void slash_qdma_ioctl_qpair_rm_q(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_op_w() - Wrapper for the qpair operation ioctl. - * @misc: Miscdevice handle. * @qdma_dev: QDMA device. * @uarg: User-space pointer to a slash_qdma_qpair_op struct. * @@ -2256,8 +2083,7 @@ static void slash_qdma_ioctl_qpair_rm_q(struct miscdevice *misc, * * Return: 0 on success, negative errno on failure. */ -static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_op_w(struct slash_qdma_dev *qdma_dev, void __user *uarg) { struct slash_qdma_qpair_op req; @@ -2274,7 +2100,7 @@ static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, return -EFAULT; if (user_size < SLASH_QDMA_QPAIR_OP_MIN_SIZE) { - dev_warn(misc->this_device, + dev_warn(&qdma_dev->pdev->dev, "qdma: Q_OP size too small (%u)\n", user_size); return -EINVAL; } @@ -2292,7 +2118,7 @@ static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, mutex_unlock(&qdma_dev->lock); return -ENODEV; } - ret = slash_qdma_ioctl_qpair_op(misc, qdma_dev, &req); + ret = slash_qdma_ioctl_qpair_op(qdma_dev, &req); mutex_unlock(&qdma_dev->lock); if (ret) @@ -2318,7 +2144,6 @@ static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, /** * slash_qdma_ioctl_qpair_op() - Dispatch a lifecycle operation on a queue pair. - * @misc: Miscdevice handle (unused, present for API consistency). * @qdma_dev: QDMA device. * @req: Operation request (@qid identifies the target, @op selects * the action). @@ -2335,15 +2160,12 @@ static int slash_qdma_ioctl_qpair_op_w(struct miscdevice *misc, * Return: 0 on success, -ENOENT if qpair not found, other negative errno * from the underlying libqdma call. */ -static int slash_qdma_ioctl_qpair_op(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_op(struct slash_qdma_dev *qdma_dev, struct slash_qdma_qpair_op *req) { struct slash_qdma_qpair_entry *entry; int ret = 0; - (void) misc; - if (!qdma_dev->have_qdma_handle) return -ENODEV; @@ -2826,7 +2648,6 @@ static struct slash_qdma_buf *slash_qdma_buf_from_file(struct file *file) /** * slash_qdma_ioctl_buf_create_w() - Allocate a kernel buffer and return its fd. - * @misc: Miscdevice handle (for logging). * @qdma_dev: QDMA device the buffer is bound to (for DMA mapping). * @uarg: User pointer to a struct slash_qdma_buf_create. * @@ -2838,8 +2659,7 @@ static struct slash_qdma_buf *slash_qdma_buf_from_file(struct file *file) * * Return: The new buffer fd (>= 0) on success, negative errno on failure. */ -static int slash_qdma_ioctl_buf_create_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_buf_create_w(struct slash_qdma_dev *qdma_dev, void __user *uarg) { struct slash_qdma_buf_create req; @@ -2854,7 +2674,7 @@ static int slash_qdma_ioctl_buf_create_w(struct miscdevice *misc, return -EFAULT; if (user_size < SLASH_QDMA_BUF_CREATE_MIN_SIZE) { - dev_warn(misc->this_device, + dev_warn(&qdma_dev->pdev->dev, "qdma: BUF_CREATE size too small (%u)\n", user_size); return -EINVAL; } @@ -2879,7 +2699,15 @@ static int slash_qdma_ioctl_buf_create_w(struct miscdevice *misc, buf->qdma_dev = qdma_dev; buf->length = req.length; + mutex_lock(&qdma_dev->lock); + if (qdma_dev->hw_shutdown || !qdma_dev->have_qdma_handle) { + mutex_unlock(&qdma_dev->lock); + kref_put(&qdma_dev->ref, slash_qdma_dev_release); + kfree(buf); + return -ENODEV; + } rv = slash_qdma_buf_alloc(buf); + mutex_unlock(&qdma_dev->lock); if (rv < 0) { kref_put(&qdma_dev->ref, slash_qdma_dev_release); kfree(buf); @@ -3539,8 +3367,7 @@ static long slash_qdma_qpair_ioctl(struct file *file, switch (cmd) { case SLASH_QDMA_IOCTL_BUF_CREATE: - return slash_qdma_ioctl_buf_create_w(&ctx->qdma_dev->misc, - ctx->qdma_dev, + return slash_qdma_ioctl_buf_create_w(ctx->qdma_dev, (void __user *)arg); case SLASH_QDMA_QPAIR_IOCTL_TRANSFER: return slash_qdma_qpair_transfer(file, (void __user *)arg); @@ -3591,7 +3418,6 @@ static int slash_qdma_qpair_release(struct inode *inode, struct file *file) /** * slash_qdma_ioctl_qpair_get_fd_w() - Create an anon_inode fd for queue I/O. - * @misc: Miscdevice handle (unused). * @qdma_dev: QDMA device. * @uarg: User-space pointer to a slash_qdma_qpair_fd_request struct. * @@ -3613,8 +3439,7 @@ static int slash_qdma_qpair_release(struct inode *inode, struct file *file) * * Return: The new fd (>= 0) on success, negative errno on failure. */ -static int slash_qdma_ioctl_qpair_get_fd_w(struct miscdevice *misc, - struct slash_qdma_dev *qdma_dev, +static int slash_qdma_ioctl_qpair_get_fd_w(struct slash_qdma_dev *qdma_dev, void __user *uarg) { struct slash_qdma_qpair_fd_request req; @@ -3632,7 +3457,7 @@ static int slash_qdma_ioctl_qpair_get_fd_w(struct miscdevice *misc, return -EFAULT; if (user_size < SLASH_QDMA_QPAIR_GET_FD_MIN_SIZE) { - dev_warn(misc->this_device, + dev_warn(&qdma_dev->pdev->dev, "qdma: QPAIR_GET_FD size too small (%u)\n", user_size); return -EINVAL; } diff --git a/driver/tests/slash_test_helpers.h b/driver/tests/slash_test_helpers.h index fee08e89..871467a4 100644 --- a/driver/tests/slash_test_helpers.h +++ b/driver/tests/slash_test_helpers.h @@ -3,20 +3,19 @@ * Shared helpers for the SLASH kernel-module kselftests. * * Header-only. Each test binary includes this file and inlines what it - * needs. Device paths are hardcoded to /dev/slash_ctl0 and - * /dev/slash_qdma_ctl0 — the spec at docs/reference/kernel-abi/index.rst - * warns that those suffixes are independent across the two device - * categories, so any test that opens both fds calls - * slash_assert_same_card() to verify they describe the same physical card. + * needs. Device paths are hardcoded to the board-zero nodes; the stable + * chrdev ABI guarantees that control and QDMA nodes for one board share N. */ #ifndef SLASH_TEST_HELPERS_H #define SLASH_TEST_HELPERS_H +#include #include #include #include #include +#include #include #include #include @@ -24,6 +23,8 @@ #include #include #include +#include +#include #include @@ -31,6 +32,12 @@ #define SLASH_TEST_QDMA_DEV "/dev/slash_qdma_ctl0" #define SLASH_TEST_HOTPLUG_DEV "/dev/slash_hotplug" +#define SLASH_TEST_SYSFS_CLASS_DIR "/sys/class/slash" +#define SLASH_TEST_CTL_SYSFS_PREFIX "slash_ctl_" +#define SLASH_TEST_QDMA_SYSFS_PREFIX "slash_qdma_ctl_" +#define SLASH_TEST_HOTPLUG_SYSFS_NAME "slash_hotplug" +#define SLASH_TEST_MAX_CARDS 16 + /* Documented PCI identity for SLASH cards. */ #define SLASH_TEST_VENDOR_ID 0x10EE #define SLASH_TEST_PF1_DEV_ID 0x50B5 /* QDMA */ @@ -47,12 +54,172 @@ #define SLASH_TEST_BITSTREAM_BASE 0x0000000102100000ULL #define SLASH_TEST_BITSTREAM_END 0x0000000142100000ULL -/* Length (incl. NUL) of the "DDDD:BB:SS" bus prefix shared by all PFs of a card. */ +/* Length of the "DDDD:BB:SS" prefix shared by all PFs of a card. */ #define SLASH_TEST_BUS_PREFIX_LEN 10 +struct slash_test_node_identity +{ + char sysfs_name[64]; + char devname[64]; + char devpath[128]; + dev_t dev; + unsigned int major; + unsigned int minor; +}; + +/** + * slash_looks_like_bdf() - Validate a full DDDD:BB:SS.F PCI BDF. + */ +static inline int slash_looks_like_bdf(const char *s) +{ + static const int hex_positions[] = {0, 1, 2, 3, 5, 6, 8, 9, 11}; + static const int colon_positions[] = {4, 7}; + int i; + + if (!s || strnlen(s, SLASH_PCI_BDF_LEN) != 12) + return 0; + for (i = 0; i < (int)(sizeof(hex_positions) / sizeof(*hex_positions)); i++) + if (!isxdigit((unsigned char)s[hex_positions[i]])) + return 0; + for (i = 0; i < (int)(sizeof(colon_positions) / sizeof(*colon_positions)); i++) + if (s[colon_positions[i]] != ':') + return 0; + return s[10] == '.'; +} + +/** + * slash_test_read_string() - Read and trim one small sysfs file. + */ +static inline int slash_test_read_string(const char *path, char *buf, + size_t buf_sz) +{ + int fd; + int saved_errno; + ssize_t n; + + if (buf_sz == 0) + return -EINVAL; + fd = open(path, O_RDONLY); + if (fd < 0) + return -errno; + n = read(fd, buf, buf_sz - 1); + saved_errno = errno; + close(fd); + if (n < 0) + return -saved_errno; + buf[n] = '\0'; + while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) + buf[--n] = '\0'; + return 0; +} + +/** + * slash_test_read_node_identity() - Resolve one slash class entry. + * + * Verifies that the sysfs dev attribute, uevent DEVNAME, and /dev character + * node all describe the same dev_t. + */ +static inline int slash_test_read_node_identity( + const char *sysfs_name, struct slash_test_node_identity *out) +{ + char path[256]; + char buf[1024]; + char *line; + char *saveptr; + struct stat st; + int err; + + memset(out, 0, sizeof(*out)); + if (snprintf(out->sysfs_name, sizeof(out->sysfs_name), "%s", + sysfs_name) >= (int)sizeof(out->sysfs_name)) + return -ENAMETOOLONG; + + snprintf(path, sizeof(path), SLASH_TEST_SYSFS_CLASS_DIR "/%s/dev", + sysfs_name); + err = slash_test_read_string(path, buf, sizeof(buf)); + if (err) + return err; + if (sscanf(buf, "%u:%u", &out->major, &out->minor) != 2) + return -EINVAL; + + snprintf(path, sizeof(path), SLASH_TEST_SYSFS_CLASS_DIR "/%s/uevent", + sysfs_name); + err = slash_test_read_string(path, buf, sizeof(buf)); + if (err) + return err; + for (line = strtok_r(buf, "\n", &saveptr); line; + line = strtok_r(NULL, "\n", &saveptr)) + { + if (strncmp(line, "DEVNAME=", 8) == 0) + { + if (snprintf(out->devname, sizeof(out->devname), "%s", + line + 8) >= (int)sizeof(out->devname)) + return -ENAMETOOLONG; + break; + } + } + if (out->devname[0] == '\0') + return -ENOENT; + if (snprintf(out->devpath, sizeof(out->devpath), "/dev/%s", + out->devname) >= (int)sizeof(out->devpath)) + return -ENAMETOOLONG; + + if (stat(out->devpath, &st) < 0) + return -errno; + if (!S_ISCHR(st.st_mode)) + return -ENOTTY; + out->dev = st.st_rdev; + if (major(out->dev) != out->major || minor(out->dev) != out->minor) + return -EIO; + return 0; +} + +static inline int slash_test_node_identity_equal( + const struct slash_test_node_identity *a, + const struct slash_test_node_identity *b) +{ + return a->dev == b->dev && + strcmp(a->sysfs_name, b->sysfs_name) == 0 && + strcmp(a->devname, b->devname) == 0 && + strcmp(a->devpath, b->devpath) == 0; +} + +static inline int slash_test_validate_hotplug_node( + const struct slash_test_node_identity *id) +{ + return id->minor == 0 && + strcmp(id->devname, "slash_hotplug") == 0; +} + +static inline int slash_test_validate_ctl_node( + const struct slash_test_node_identity *id) +{ + char expected[64]; + + if (id->minor == 0 || id->minor > 2 * SLASH_TEST_MAX_CARDS - 1 || + (id->minor & 1) == 0) + return 0; + snprintf(expected, sizeof(expected), "slash_ctl%u", + (id->minor - 1) / 2); + return strcmp(id->devname, expected) == 0; +} + +static inline int slash_test_validate_qdma_node( + const struct slash_test_node_identity *id) +{ + char expected[64]; + + if (id->minor < 2 || id->minor > 2 * SLASH_TEST_MAX_CARDS || + (id->minor & 1) != 0) + return 0; + snprintf(expected, sizeof(expected), "slash_qdma_ctl%u", + (id->minor - 2) / 2); + return strcmp(id->devname, expected) == 0; +} + /** * slash_get_device_info() - Issue GET_DEVICE_INFO on a control fd. - * @fd: An open /dev/slash_ctl or /dev/slash_qdma_ctl fd. + * @fd: An open /dev/slash_ctl fd. * @out: Caller-owned struct; size field is set by this helper. * * Return: 0 on success, -errno on failure. @@ -92,7 +259,7 @@ static inline int slash_get_bdf(int fd, char bdf_out[SLASH_PCI_BDF_LEN]) static inline int slash_same_card(const char *bdf_a, const char *bdf_b) { /* "DDDD:BB:SS" is 10 chars, excluding the trailing ".F" and NUL. */ - return strncmp(bdf_a, bdf_b, SLASH_TEST_BUS_PREFIX_LEN - 1) == 0; + return strncmp(bdf_a, bdf_b, SLASH_TEST_BUS_PREFIX_LEN) == 0; } /** diff --git a/driver/tests/test_slash_ctldev.c b/driver/tests/test_slash_ctldev.c index c5f61f56..63c131a5 100644 --- a/driver/tests/test_slash_ctldev.c +++ b/driver/tests/test_slash_ctldev.c @@ -9,13 +9,10 @@ #include "kselftest_harness.h" #include "slash_test_helpers.h" -#include #include #include #include -#define SYSFS_CTL_FMT "/sys/class/misc/slash_ctl_%s" - /* ---------- fixture ---------- */ FIXTURE(ctldev) @@ -42,29 +39,6 @@ FIXTURE_TEARDOWN(ctldev) close(self->ctl_fd); } -/* ---------- helpers local to this file ---------- */ - -static int looks_like_bdf(const char *s) -{ - /* "DDDD:BB:DD.F" — 12 chars: 4 hex, ':', 2 hex, ':', 2 hex, '.', 1 hex */ - int i; - const int hex_positions[] = {0, 1, 2, 3, 5, 6, 8, 9, 11}; - const int colon_positions[] = {4, 7}; - const int dot_position = 10; - - if (strnlen(s, SLASH_PCI_BDF_LEN) < 12) - return 0; - for (i = 0; i < (int)(sizeof(hex_positions) / sizeof(*hex_positions)); i++) - if (!isxdigit((unsigned char)s[hex_positions[i]])) - return 0; - for (i = 0; i < (int)(sizeof(colon_positions) / sizeof(*colon_positions)); i++) - if (s[colon_positions[i]] != ':') - return 0; - if (s[dot_position] != '.') - return 0; - return 1; -} - /* ---------- tests ---------- */ TEST_F(ctldev, get_device_info_happy) @@ -74,7 +48,7 @@ TEST_F(ctldev, get_device_info_happy) ASSERT_EQ(0, slash_get_device_info(self->ctl_fd, &info)); EXPECT_NE('\0', info.bdf[0]); - EXPECT_TRUE(looks_like_bdf(info.bdf)) + EXPECT_TRUE(slash_looks_like_bdf(info.bdf)) TH_LOG("bad BDF '%s'", info.bdf); EXPECT_EQ(SLASH_TEST_VENDOR_ID, info.vendor_id); EXPECT_EQ(SLASH_TEST_PF2_DEV_ID, info.device_id); @@ -82,18 +56,28 @@ TEST_F(ctldev, get_device_info_happy) EXPECT_EQ('2', info.bdf[11]); } -TEST_F(ctldev, get_device_info_bdf_matches_sysfs) +TEST_F(ctldev, device_node_identity_matches_sysfs_and_fd) { struct slash_ioctl_device_info info; - char sysfs_path[256]; - struct stat st; + struct slash_test_node_identity identity; + char sysfs_name[64]; + struct stat fd_stat; ASSERT_EQ(0, slash_get_device_info(self->ctl_fd, &info)); - snprintf(sysfs_path, sizeof(sysfs_path), SYSFS_CTL_FMT, info.bdf); - EXPECT_EQ(0, stat(sysfs_path, &st)) - TH_LOG("expected sysfs entry %s for BDF reported by ioctl", - sysfs_path); + snprintf(sysfs_name, sizeof(sysfs_name), + SLASH_TEST_CTL_SYSFS_PREFIX "%s", info.bdf); + ASSERT_EQ(0, slash_test_read_node_identity(sysfs_name, &identity)) + TH_LOG("sysfs dev/uevent or /dev node disagree for %s/%s", + SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); + EXPECT_TRUE(slash_test_validate_ctl_node(&identity)) + TH_LOG("control node %s has invalid minor/name (%u:%u)", + identity.devpath, identity.major, identity.minor); + + ASSERT_EQ(0, fstat(self->ctl_fd, &fd_stat)); + EXPECT_EQ(identity.dev, fd_stat.st_rdev) + TH_LOG("opened fd %s does not match %s/%s", + SLASH_TEST_CTL_DEV, SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); } TEST_F(ctldev, bar_info_all_indices_succeed) @@ -430,7 +414,7 @@ TEST_F(ctldev, get_device_info_undersized_truncates_output) SLASH_CTLDEV_IOCTL_GET_DEVICE_INFO, &info)); /* bdf is inside the user-claimed window — the kernel must populate it. */ - EXPECT_TRUE(looks_like_bdf(info.bdf)) + EXPECT_TRUE(slash_looks_like_bdf(info.bdf)) TH_LOG("bdf was not populated within the user-claimed window"); /* vendor_id and the trailing IDs are beyond user_size — the kernel diff --git a/driver/tests/test_slash_hotplug.c b/driver/tests/test_slash_hotplug.c index c7afdb3e..dd3e5064 100644 --- a/driver/tests/test_slash_hotplug.c +++ b/driver/tests/test_slash_hotplug.c @@ -9,18 +9,17 @@ * against the live device(s). * * Discovery: the fixture enumerates every SLASH accelerator on the - * system at setup time by scanning /sys/class/misc/ for slash_ctl_ + * system at setup time by scanning /sys/class/slash/ for slash_ctl_ * (PF2) and slash_qdma_ctl_ (PF1), then pairs them by the * "DDDD:BB:SS" bus prefix. Operations target the first accelerator; * the teardown polls until *every* discovered accelerator has both its - * ctl and qdma sysfs entries back and pointing at a /dev node whose - * major:minor match the sysfs `dev` attribute. This catches both + * ctl and qdma sysfs entries back with the same paths and device numbers + * recorded before removal. This catches both * cross-card damage (touching the wrong accelerator) and stale /dev * state (sysfs back but udev missed the node, or wrong minor). * - * Two tests perform a full board reset (TOGGLE_SBR) or accept ~10 s of - * PCIe downtime; these are gated by SLASH_TEST_DESTRUCTIVE=1. All other - * tests run on every invocation. + * Tests that retain open resources across removal or perform a full board + * reset are gated by SLASH_TEST_DESTRUCTIVE=1. * * The accelerator-discovery helpers live in this file rather than in * slash_test_helpers.h because no other test binary needs them. @@ -35,18 +34,10 @@ #include #include -#include -#include -#include #define NODE_RECOVERY_TIMEOUT_S 10 #define SBR_SETTLE_SECONDS 7 -#define SLASH_TEST_MAX_ACCELERATORS 16 -#define SYSFS_MISC_DIR "/sys/class/misc" -#define CTL_SYSFS_PREFIX "slash_ctl_" -#define QDMA_SYSFS_PREFIX "slash_qdma_ctl_" - /* ==================================================================== * Accelerator discovery + verification * ==================================================================== */ @@ -54,138 +45,51 @@ struct accelerator { char pf0_bdf[SLASH_PCI_BDF_LEN]; /* derived: ".0" */ - char pf1_bdf[SLASH_PCI_BDF_LEN]; /* from slash_qdma_ctl_ */ - char pf2_bdf[SLASH_PCI_BDF_LEN]; /* from slash_ctl_ */ + char pf1_bdf[SLASH_PCI_BDF_LEN]; /* from slash_qdma_ctl_ */ + char pf2_bdf[SLASH_PCI_BDF_LEN]; /* from slash_ctl_ */ + struct slash_test_node_identity ctl_identity; + struct slash_test_node_identity qdma_identity; }; -/* Slurp a sysfs file into a NUL-terminated buffer, trimming a trailing newline. */ -static int read_sysfs_string(const char *path, char *buf, size_t buf_sz) -{ - int fd = open(path, O_RDONLY); - ssize_t n; - - if (fd < 0) - return -errno; - n = read(fd, buf, buf_sz - 1); - close(fd); - if (n < 0) - return -errno; - buf[n] = '\0'; - while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) - buf[--n] = '\0'; - return 0; -} - -/* Read DEVNAME=... from /sys/class/misc//uevent. */ -static int get_misc_devname(const char *sysfs_basename, char *out, size_t out_sz) +/* Both nodes must reappear with their recorded dev_t and path identity. */ +static int verify_accelerator_present(const struct accelerator *a) { - char path[256]; - char buf[1024]; - char *line, *saveptr; + struct slash_test_node_identity current; int err; - snprintf(path, sizeof(path), SYSFS_MISC_DIR "/%s/uevent", sysfs_basename); - err = read_sysfs_string(path, buf, sizeof(buf)); + err = slash_test_read_node_identity(a->ctl_identity.sysfs_name, ¤t); if (err) return err; + if (!slash_test_node_identity_equal(&a->ctl_identity, ¤t)) + return -ESTALE; - for (line = strtok_r(buf, "\n", &saveptr); line; - line = strtok_r(NULL, "\n", &saveptr)) - { - if (strncmp(line, "DEVNAME=", 8) == 0) - { - strncpy(out, line + 8, out_sz - 1); - out[out_sz - 1] = '\0'; - return 0; - } - } - return -ENOENT; -} - -/* Read MAJOR:MINOR from /sys/class/misc//dev. */ -static int get_misc_devnum(const char *sysfs_basename, - unsigned int *major_out, unsigned int *minor_out) -{ - char path[256]; - char buf[64]; - int err; - - snprintf(path, sizeof(path), SYSFS_MISC_DIR "/%s/dev", sysfs_basename); - err = read_sysfs_string(path, buf, sizeof(buf)); + err = slash_test_read_node_identity(a->qdma_identity.sysfs_name, ¤t); if (err) return err; - if (sscanf(buf, "%u:%u", major_out, minor_out) != 2) - return -EINVAL; + if (!slash_test_node_identity_equal(&a->qdma_identity, ¤t)) + return -ESTALE; return 0; } /* - * Verify a misc-class sysfs entry exists and points at a real /dev node. - * - * 0 — sysfs entry exists, /dev node exists, major:minor match - * -ENOENT — sysfs entry or /dev node missing - * -EIO — /dev node isn't a char dev, or its rdev doesn't match sysfs - * other — read/stat failure - */ -static int verify_misc_node(const char *sysfs_basename) -{ - char devname[64]; - char devpath[128]; - unsigned int sysfs_major = 0, sysfs_minor = 0; - struct stat st; - int err; - - err = get_misc_devnum(sysfs_basename, &sysfs_major, &sysfs_minor); - if (err) - return err; - err = get_misc_devname(sysfs_basename, devname, sizeof(devname)); - if (err) - return err; - - snprintf(devpath, sizeof(devpath), "/dev/%s", devname); - if (stat(devpath, &st) < 0) - return -errno; - if (!S_ISCHR(st.st_mode)) - return -EIO; - if (major(st.st_rdev) != sysfs_major || minor(st.st_rdev) != sysfs_minor) - return -EIO; - return 0; -} - -/* Both ctl and qdma nodes for one accelerator verify cleanly. */ -static int verify_accelerator_present(const struct accelerator *a) -{ - char name[64]; - int err; - - snprintf(name, sizeof(name), CTL_SYSFS_PREFIX "%s", a->pf2_bdf); - err = verify_misc_node(name); - if (err) - return err; - - snprintf(name, sizeof(name), QDMA_SYSFS_PREFIX "%s", a->pf1_bdf); - return verify_misc_node(name); -} - -/* - * Discover every SLASH accelerator by scanning /sys/class/misc/. + * Discover every SLASH accelerator by scanning /sys/class/slash/. * * Unpaired entries (PF2 without a matching PF1, or vice versa) indicate * a partial probe failure — log a warning and skip the orphan. */ static int discover_accelerators(struct accelerator *out, int max, int *n_out) { - char pf2_bdfs[SLASH_TEST_MAX_ACCELERATORS][SLASH_PCI_BDF_LEN] = {{0}}; - char pf1_bdfs[SLASH_TEST_MAX_ACCELERATORS][SLASH_PCI_BDF_LEN] = {{0}}; - int pf2_paired[SLASH_TEST_MAX_ACCELERATORS] = {0}; - int pf1_paired[SLASH_TEST_MAX_ACCELERATORS] = {0}; + char pf2_bdfs[SLASH_TEST_MAX_CARDS][SLASH_PCI_BDF_LEN] = {{0}}; + char pf1_bdfs[SLASH_TEST_MAX_CARDS][SLASH_PCI_BDF_LEN] = {{0}}; + int pf2_paired[SLASH_TEST_MAX_CARDS] = {0}; + int pf1_paired[SLASH_TEST_MAX_CARDS] = {0}; int n_pf2 = 0, n_pf1 = 0; int n_accels = 0; int i, j; DIR *d; struct dirent *de; - d = opendir(SYSFS_MISC_DIR); + d = opendir(SLASH_TEST_SYSFS_CLASS_DIR); if (!d) return -errno; @@ -193,24 +97,24 @@ static int discover_accelerators(struct accelerator *out, int max, int *n_out) { /* Match QDMA prefix first — neither prefix is a prefix of * the other, but the ordering keeps intent obvious. */ - if (strncmp(de->d_name, QDMA_SYSFS_PREFIX, - strlen(QDMA_SYSFS_PREFIX)) == 0) + if (strncmp(de->d_name, SLASH_TEST_QDMA_SYSFS_PREFIX, + strlen(SLASH_TEST_QDMA_SYSFS_PREFIX)) == 0) { - if (n_pf1 < SLASH_TEST_MAX_ACCELERATORS) + if (n_pf1 < SLASH_TEST_MAX_CARDS) { strncpy(pf1_bdfs[n_pf1], - de->d_name + strlen(QDMA_SYSFS_PREFIX), + de->d_name + strlen(SLASH_TEST_QDMA_SYSFS_PREFIX), SLASH_PCI_BDF_LEN - 1); n_pf1++; } } - else if (strncmp(de->d_name, CTL_SYSFS_PREFIX, - strlen(CTL_SYSFS_PREFIX)) == 0) + else if (strncmp(de->d_name, SLASH_TEST_CTL_SYSFS_PREFIX, + strlen(SLASH_TEST_CTL_SYSFS_PREFIX)) == 0) { - if (n_pf2 < SLASH_TEST_MAX_ACCELERATORS) + if (n_pf2 < SLASH_TEST_MAX_CARDS) { strncpy(pf2_bdfs[n_pf2], - de->d_name + strlen(CTL_SYSFS_PREFIX), + de->d_name + strlen(SLASH_TEST_CTL_SYSFS_PREFIX), SLASH_PCI_BDF_LEN - 1); n_pf2++; } @@ -238,6 +142,27 @@ static int discover_accelerators(struct accelerator *out, int max, int *n_out) out[n_accels].pf0_bdf[SLASH_PCI_BDF_LEN - 1] = '\0'; out[n_accels].pf0_bdf[11] = '0'; + { + char sysfs_name[64]; + int err; + + snprintf(sysfs_name, sizeof(sysfs_name), + SLASH_TEST_CTL_SYSFS_PREFIX "%s", + out[n_accels].pf2_bdf); + err = slash_test_read_node_identity( + sysfs_name, &out[n_accels].ctl_identity); + if (err) + return err; + + snprintf(sysfs_name, sizeof(sysfs_name), + SLASH_TEST_QDMA_SYSFS_PREFIX "%s", + out[n_accels].pf1_bdf); + err = slash_test_read_node_identity( + sysfs_name, &out[n_accels].qdma_identity); + if (err) + return err; + } + pf2_paired[i] = 1; pf1_paired[j] = 1; n_accels++; @@ -248,17 +173,19 @@ static int discover_accelerators(struct accelerator *out, int max, int *n_out) for (i = 0; i < n_pf2; i++) if (!pf2_paired[i]) fprintf(stderr, - "# WARNING: unpaired " CTL_SYSFS_PREFIX - "%s (no matching " QDMA_SYSFS_PREFIX "<%.*s.x>)\n", + "# WARNING: unpaired " SLASH_TEST_CTL_SYSFS_PREFIX + "%s (no matching " SLASH_TEST_QDMA_SYSFS_PREFIX + "<%.*s.x>)\n", pf2_bdfs[i], - SLASH_TEST_BUS_PREFIX_LEN - 1, pf2_bdfs[i]); + SLASH_TEST_BUS_PREFIX_LEN, pf2_bdfs[i]); for (j = 0; j < n_pf1; j++) if (!pf1_paired[j]) fprintf(stderr, - "# WARNING: unpaired " QDMA_SYSFS_PREFIX - "%s (no matching " CTL_SYSFS_PREFIX "<%.*s.x>)\n", + "# WARNING: unpaired " SLASH_TEST_QDMA_SYSFS_PREFIX + "%s (no matching " SLASH_TEST_CTL_SYSFS_PREFIX + "<%.*s.x>)\n", pf1_bdfs[j], - SLASH_TEST_BUS_PREFIX_LEN - 1, pf1_bdfs[j]); + SLASH_TEST_BUS_PREFIX_LEN, pf1_bdfs[j]); *n_out = n_accels; return 0; @@ -305,13 +232,14 @@ static int poll_accelerators_present(const struct accelerator *accels, return -ETIMEDOUT; } -/* Wait until /sys/class/misc/ is gone. */ -static int poll_misc_absent(const char *sysfs_basename, int timeout_s) +/* Wait until /sys/class/slash/ is gone. */ +static int poll_slash_absent(const char *sysfs_basename, int timeout_s) { char path[256]; int i; - snprintf(path, sizeof(path), SYSFS_MISC_DIR "/%s", sysfs_basename); + snprintf(path, sizeof(path), SLASH_TEST_SYSFS_CLASS_DIR "/%s", + sysfs_basename); for (i = 0; i < timeout_s * 10; i++) { if (access(path, F_OK) != 0) @@ -328,7 +256,8 @@ static int poll_misc_absent(const char *sysfs_basename, int timeout_s) FIXTURE(hotplug) { int hp_fd; - struct accelerator accels[SLASH_TEST_MAX_ACCELERATORS]; + struct slash_test_node_identity hotplug_identity; + struct accelerator accels[SLASH_TEST_MAX_CARDS]; int n_accels; }; @@ -346,11 +275,17 @@ FIXTURE_SETUP(hotplug) TH_LOG("open(%s) failed: %s", SLASH_TEST_HOTPLUG_DEV, strerror(errno)); + ASSERT_EQ(0, slash_test_read_node_identity( + SLASH_TEST_HOTPLUG_SYSFS_NAME, + &self->hotplug_identity)) + TH_LOG("hotplug sysfs dev/uevent or /dev node disagree"); + ASSERT_EQ(0, discover_accelerators(self->accels, - SLASH_TEST_MAX_ACCELERATORS, + SLASH_TEST_MAX_CARDS, &self->n_accels)); if (self->n_accels == 0) - SKIP(return, "no SLASH accelerators discovered in " SYSFS_MISC_DIR); + SKIP(return, "no SLASH accelerators discovered in " + SLASH_TEST_SYSFS_CLASS_DIR); /* Starting state must be sane: every discovered accelerator's nodes * must already pass verification. If not, a previous run left the @@ -399,11 +334,45 @@ static int hp_ioctl_bdf(int hp_fd, unsigned long cmd, const char *bdf) * Tests * ==================================================================== */ +TEST_F(hotplug, stable_chrdev_layout) +{ + struct stat hp_stat; + int i; + + ASSERT_TRUE(slash_test_validate_hotplug_node(&self->hotplug_identity)) + TH_LOG("hotplug node has invalid minor/name (%u:%u, %s)", + self->hotplug_identity.major, self->hotplug_identity.minor, + self->hotplug_identity.devname); + ASSERT_EQ(0, fstat(self->hp_fd, &hp_stat)); + EXPECT_EQ(self->hotplug_identity.dev, hp_stat.st_rdev); + EXPECT_GT(self->hotplug_identity.major, 0); + + for (i = 0; i < self->n_accels; i++) + { + const struct accelerator *a = &self->accels[i]; + + EXPECT_TRUE(slash_test_validate_ctl_node(&a->ctl_identity)) + TH_LOG("invalid control node %s (%u:%u)", + a->ctl_identity.devpath, + a->ctl_identity.major, a->ctl_identity.minor); + EXPECT_TRUE(slash_test_validate_qdma_node(&a->qdma_identity)) + TH_LOG("invalid QDMA node %s (%u:%u)", + a->qdma_identity.devpath, + a->qdma_identity.major, a->qdma_identity.minor); + EXPECT_EQ(self->hotplug_identity.major, a->ctl_identity.major); + EXPECT_EQ(self->hotplug_identity.major, a->qdma_identity.major); + EXPECT_EQ(a->ctl_identity.minor + 1, a->qdma_identity.minor) + TH_LOG("PF2 %s and PF1 %s do not have paired minors", + a->pf2_bdf, a->pf1_bdf); + EXPECT_TRUE(slash_same_card(a->pf2_bdf, a->pf1_bdf)); + } +} + TEST_F(hotplug, rescan_smoke) { EXPECT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); - /* All accelerators that were present before must still be present. */ + /* Presence includes unchanged class name, /dev path, and dev_t. */ EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, NODE_RECOVERY_TIMEOUT_S)); } @@ -436,92 +405,175 @@ TEST_F(hotplug, remove_unknown_bdf) "ffff:ff:1f.7")); } -TEST_F(hotplug, remove_then_rescan_recovers_pf2) +TEST_F(hotplug, remove_then_rescan_preserves_pf2_identity) { char sysfs_name[64]; snprintf(sysfs_name, sizeof(sysfs_name), - CTL_SYSFS_PREFIX "%s", self->accels[0].pf2_bdf); + SLASH_TEST_CTL_SYSFS_PREFIX "%s", self->accels[0].pf2_bdf); ASSERT_EQ(0, hp_ioctl_bdf(self->hp_fd, SLASH_HOTPLUG_IOCTL_REMOVE, self->accels[0].pf2_bdf)); - EXPECT_EQ(0, poll_misc_absent(sysfs_name, NODE_RECOVERY_TIMEOUT_S)) + EXPECT_EQ(0, poll_slash_absent(sysfs_name, NODE_RECOVERY_TIMEOUT_S)) TH_LOG("%s/%s did not disappear after REMOVE", - SYSFS_MISC_DIR, sysfs_name); + SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); ASSERT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, NODE_RECOVERY_TIMEOUT_S)) - TH_LOG("not all accelerators reappeared after RESCAN"); + TH_LOG("node identity changed after PF2 REMOVE + RESCAN"); } -TEST_F(hotplug, remove_then_rescan_recovers_pf1) +TEST_F(hotplug, remove_then_rescan_preserves_pf1_identity) { char sysfs_name[64]; snprintf(sysfs_name, sizeof(sysfs_name), - QDMA_SYSFS_PREFIX "%s", self->accels[0].pf1_bdf); + SLASH_TEST_QDMA_SYSFS_PREFIX "%s", self->accels[0].pf1_bdf); ASSERT_EQ(0, hp_ioctl_bdf(self->hp_fd, SLASH_HOTPLUG_IOCTL_REMOVE, self->accels[0].pf1_bdf)); - EXPECT_EQ(0, poll_misc_absent(sysfs_name, NODE_RECOVERY_TIMEOUT_S)) + EXPECT_EQ(0, poll_slash_absent(sysfs_name, NODE_RECOVERY_TIMEOUT_S)) TH_LOG("%s/%s did not disappear after REMOVE", - SYSFS_MISC_DIR, sysfs_name); + SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); ASSERT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, NODE_RECOVERY_TIMEOUT_S)) - TH_LOG("not all accelerators reappeared after RESCAN"); + TH_LOG("node identity changed after PF1 REMOVE + RESCAN"); +} + +/* ---------- Open-resource lifetime (destructive, env-gated) ---------- */ + +TEST_F(hotplug, open_pf2_fd_survives_remove_but_stays_offline) +{ + struct slash_ioctl_device_info info; + int ctl_fd; + + if (getenv("SLASH_TEST_DESTRUCTIVE") == NULL) + SKIP(return, "remove PF2 while its fd is open; " + "set SLASH_TEST_DESTRUCTIVE=1 to run"); + + ctl_fd = open(self->accels[0].ctl_identity.devpath, O_RDWR); + ASSERT_GE(ctl_fd, 0); + + ASSERT_EQ(0, hp_ioctl_bdf(self->hp_fd, SLASH_HOTPLUG_IOCTL_REMOVE, + self->accels[0].pf2_bdf)); + EXPECT_EQ(0, poll_slash_absent( + self->accels[0].ctl_identity.sysfs_name, + NODE_RECOVERY_TIMEOUT_S)); + + memset(&info, 0, sizeof(info)); + info.size = sizeof(info); + errno = 0; + EXPECT_EQ(-1, ioctl(ctl_fd, SLASH_CTLDEV_IOCTL_GET_DEVICE_INFO, &info)); + EXPECT_EQ(ENODEV, errno); + + EXPECT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); + EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, + NODE_RECOVERY_TIMEOUT_S)); + + errno = 0; + EXPECT_EQ(-1, ioctl(ctl_fd, SLASH_CTLDEV_IOCTL_GET_DEVICE_INFO, &info)); + EXPECT_EQ(ENODEV, errno) + TH_LOG("old PF2 fd rebound to the rescanned device"); + EXPECT_EQ(0, close(ctl_fd)); +} + +TEST_F(hotplug, open_pf1_fd_survives_remove_but_stays_offline) +{ + struct slash_qdma_info info; + int qdma_fd; + + if (getenv("SLASH_TEST_DESTRUCTIVE") == NULL) + SKIP(return, "remove PF1 while its fd is open; " + "set SLASH_TEST_DESTRUCTIVE=1 to run"); + + qdma_fd = open(self->accels[0].qdma_identity.devpath, O_RDWR); + ASSERT_GE(qdma_fd, 0); + + ASSERT_EQ(0, hp_ioctl_bdf(self->hp_fd, SLASH_HOTPLUG_IOCTL_REMOVE, + self->accels[0].pf1_bdf)); + EXPECT_EQ(0, poll_slash_absent( + self->accels[0].qdma_identity.sysfs_name, + NODE_RECOVERY_TIMEOUT_S)); + + memset(&info, 0, sizeof(info)); + info.size = sizeof(info); + errno = 0; + EXPECT_EQ(-1, ioctl(qdma_fd, SLASH_QDMA_IOCTL_INFO, &info)); + EXPECT_EQ(ENODEV, errno); + + EXPECT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); + EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, + NODE_RECOVERY_TIMEOUT_S)); + + errno = 0; + EXPECT_EQ(-1, ioctl(qdma_fd, SLASH_QDMA_IOCTL_INFO, &info)); + EXPECT_EQ(ENODEV, errno) + TH_LOG("old PF1 fd rebound to the rescanned device"); + EXPECT_EQ(0, close(qdma_fd)); } TEST_F(hotplug, remove_pf1_with_live_qpair_cleans_up) { /* - * Closing the qdma_ctl fd does not release qpairs — they live on - * the device, not the fd. If userspace leaks a qpair and the device - * is then removed, slash_qdma_destroy_qdma_device's xa_for_each - * teardown loop must stop and reclaim the leaked HW queues via - * slash_qdma_ioctl_qpair_rm_q. This test exercises that path: add a - * qpair, close the fd without DEL, REMOVE the PF1, then RESCAN. - * Recovery is verified via poll_accelerators_present. + * Keep both the management fd and an anon transfer fd open while PF1 + * is removed. The driver must reclaim the live HW queues, leave both + * old fds safely offline, and permit a fresh instance after rescan. */ - char qdma_sysfs[64]; - char qdma_devname[64]; - char qdma_path[128]; + struct slash_qdma_buf_create create; int qdma_fd; - uint32_t qid; + int io_fd = -1; + uint32_t qid = 0; if (getenv("SLASH_TEST_DESTRUCTIVE") == NULL) SKIP(return, "remove PF1 with live queue pairs." "set SLASH_TEST_DESTRUCTIVE=1 to run"); - snprintf(qdma_sysfs, sizeof(qdma_sysfs), - QDMA_SYSFS_PREFIX "%s", self->accels[0].pf1_bdf); - ASSERT_EQ(0, get_misc_devname(qdma_sysfs, qdma_devname, - sizeof(qdma_devname))); - snprintf(qdma_path, sizeof(qdma_path), "/dev/%s", qdma_devname); - - qdma_fd = open(qdma_path, O_RDWR); + qdma_fd = open(self->accels[0].qdma_identity.devpath, O_RDWR); ASSERT_GE(qdma_fd, 0) - TH_LOG("open(%s) failed: %s", qdma_path, strerror(errno)); + TH_LOG("open(%s) failed: %s", + self->accels[0].qdma_identity.devpath, strerror(errno)); /* Add a qpair and intentionally skip DEL — the teardown loop must * reclaim it when the device disappears. Bidirectional so both * H2C and C2H queue handles need cleanup. */ ASSERT_EQ(0, slash_qpair_add(qdma_fd, 0 /* MM */, 0x3, &qid)); - - close(qdma_fd); + ASSERT_EQ(0, slash_qpair_op(qdma_fd, qid, SLASH_QDMA_QUEUE_OP_START)); + io_fd = slash_qpair_get_fd(qdma_fd, qid, O_CLOEXEC); + ASSERT_GE(io_fd, 0); ASSERT_EQ(0, hp_ioctl_bdf(self->hp_fd, SLASH_HOTPLUG_IOCTL_REMOVE, self->accels[0].pf1_bdf)); - EXPECT_EQ(0, poll_misc_absent(qdma_sysfs, NODE_RECOVERY_TIMEOUT_S)) + EXPECT_EQ(0, poll_slash_absent( + self->accels[0].qdma_identity.sysfs_name, + NODE_RECOVERY_TIMEOUT_S)) TH_LOG("%s/%s did not disappear after REMOVE", - SYSFS_MISC_DIR, qdma_sysfs); + SLASH_TEST_SYSFS_CLASS_DIR, + self->accels[0].qdma_identity.sysfs_name); + + memset(&create, 0, sizeof(create)); + create.size = sizeof(create); + create.flags = O_CLOEXEC; + create.length = 4096; + errno = 0; + EXPECT_EQ(-1, ioctl(io_fd, SLASH_QDMA_IOCTL_BUF_CREATE, &create)); + EXPECT_EQ(ENODEV, errno) + TH_LOG("old QDMA anon fd remained usable after PF1 removal"); ASSERT_EQ(0, ioctl(self->hp_fd, SLASH_HOTPLUG_IOCTL_RESCAN)); EXPECT_EQ(0, poll_accelerators_present(self->accels, self->n_accels, NODE_RECOVERY_TIMEOUT_S)) TH_LOG("not all accelerators reappeared after RESCAN"); + + errno = 0; + EXPECT_EQ(-1, ioctl(io_fd, SLASH_QDMA_IOCTL_BUF_CREATE, &create)); + EXPECT_EQ(ENODEV, errno) + TH_LOG("old QDMA anon fd rebound to the rescanned device"); + + EXPECT_EQ(0, close(io_fd)); + EXPECT_EQ(0, close(qdma_fd)); } TEST_F(hotplug, hotplug_atomic_pf2) @@ -565,10 +617,6 @@ TEST_F(hotplug, toggle_sbr_no_upstream_bridge) "ffff:ff:00.0")); } -/* ==================================================================== - * Destructive (env-gated) - * ==================================================================== */ - /* ==================================================================== * ABI size-versioning tests * diff --git a/driver/tests/test_slash_qdma.c b/driver/tests/test_slash_qdma.c index 802f4d3b..e2f94aff 100644 --- a/driver/tests/test_slash_qdma.c +++ b/driver/tests/test_slash_qdma.c @@ -155,7 +155,37 @@ TEST_F(qdma, query_info) memset(&info, 0, sizeof(info)); info.size = sizeof(info); - EXPECT_GE(ioctl(self->ctl_fd, SLASH_QDMA_IOCTL_INFO, &info), 0); + ASSERT_EQ(0, ioctl(self->ctl_fd, SLASH_QDMA_IOCTL_INFO, &info)); + EXPECT_TRUE(slash_looks_like_bdf(info.bdf)) + TH_LOG("bad QDMA BDF '%s'", info.bdf); + EXPECT_EQ('1', info.bdf[11]) + TH_LOG("QDMA INFO must report the full PF1 BDF, got '%s'", info.bdf); +} + +TEST_F(qdma, device_node_identity_matches_sysfs_and_fd) +{ + struct slash_test_node_identity identity; + struct slash_qdma_info info; + char sysfs_name[64]; + struct stat fd_stat; + + memset(&info, 0, sizeof(info)); + info.size = sizeof(info); + ASSERT_EQ(0, ioctl(self->ctl_fd, SLASH_QDMA_IOCTL_INFO, &info)); + + snprintf(sysfs_name, sizeof(sysfs_name), + SLASH_TEST_QDMA_SYSFS_PREFIX "%s", info.bdf); + ASSERT_EQ(0, slash_test_read_node_identity(sysfs_name, &identity)) + TH_LOG("sysfs dev/uevent or /dev node disagree for %s/%s", + SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); + EXPECT_TRUE(slash_test_validate_qdma_node(&identity)) + TH_LOG("QDMA node %s has invalid minor/name (%u:%u)", + identity.devpath, identity.major, identity.minor); + + ASSERT_EQ(0, fstat(self->ctl_fd, &fd_stat)); + EXPECT_EQ(identity.dev, fd_stat.st_rdev) + TH_LOG("opened fd %s does not match %s/%s", + SLASH_TEST_QDMA_DEV, SLASH_TEST_SYSFS_CLASS_DIR, sysfs_name); } TEST_F(qdma, qpair_lifecycle) diff --git a/smi/src/validate.cpp b/smi/src/validate.cpp index 60f28159..baa0d0ea 100644 --- a/smi/src/validate.cpp +++ b/smi/src/validate.cpp @@ -48,8 +48,6 @@ #include #include #include -#include -#include #include #include #include @@ -61,6 +59,7 @@ #include #include +#include #include #include @@ -472,62 +471,6 @@ static void warnIfNotRoot(const char* mode) { } } -std::string readDevNameFromUevent(const std::filesystem::path& miscPath) { - std::ifstream uevent(miscPath / "uevent"); - if (!uevent.is_open()) { - throw std::runtime_error("Failed to open " + (miscPath / "uevent").string()); - } - - std::string line; - while (std::getline(uevent, line)) { - static constexpr std::string_view key{"DEVNAME="}; - if (!line.starts_with(key)) { - continue; - } - - std::string devName = line.substr(key.size()); - while (!devName.empty() && (devName.back() == '\n' || devName.back() == '\r')) { - devName.pop_back(); - } - return "/dev/" + devName; - } - - throw std::runtime_error("No DEVNAME entry found in " + (miscPath / "uevent").string()); -} - -std::string resolveQdmaDevicePath(const std::string& boardBdf) { - static const std::filesystem::path MISC_PATH{"/sys/class/misc"}; - - const std::string exactName = "slash_qdma_ctl_" + boardBdf + ".1"; - const auto exactPath = MISC_PATH / exactName; - if (std::filesystem::exists(exactPath)) { - return readDevNameFromUevent(exactPath); - } - - const std::string prefix = "slash_qdma_ctl_" + boardBdf + "."; - std::vector matches; - for (const auto& entry : std::filesystem::directory_iterator(MISC_PATH)) { - const std::string name = entry.path().filename().string(); - if (name.starts_with(prefix)) { - matches.push_back(entry.path()); - } - } - - if (matches.empty()) { - throw std::runtime_error( - "No QDMA misc device found for board " + boardBdf + - " (looked for /sys/class/misc/" + prefix + "*)"); - } - - std::sort(matches.begin(), matches.end()); - if (matches.size() > 1) { - std::cerr << "Warning: multiple QDMA devices found for " << boardBdf - << "; using " << matches.front().filename().string() << std::endl; - } - - return readDevNameFromUevent(matches.front()); -} - class RawQdmaDevice { public: explicit RawQdmaDevice(const std::string& path) : qdma_{slash_qdma_open(path.c_str())} { @@ -571,6 +514,37 @@ class RawQdmaDevice { slash_qdma* qdma_ = nullptr; }; +std::string resolveQdmaDevicePath(const std::string& boardBdf) { + struct GlobResult { + glob_t paths{}; + ~GlobResult() { globfree(&paths); } + } devices; + + const int ret = glob("/dev/slash_qdma_ctl*", GLOB_ERR, nullptr, &devices.paths); + if (ret == GLOB_NOMATCH) { + throw std::runtime_error("No QDMA devices found matching /dev/slash_qdma_ctl*"); + } + if (ret != 0) { + throw std::runtime_error("Failed to enumerate /dev/slash_qdma_ctl*"); + } + + const std::string expectedBdf = boardBdf + ".1"; + for (size_t i = 0; i < devices.paths.gl_pathc; ++i) { + const std::string path = devices.paths.gl_pathv[i]; + RawQdmaDevice qdma(path); + + struct slash_qdma_info info{}; + if (slash_qdma_info_read(qdma.get(), &info) != 0) { + throwSystemError("Failed to read QDMA info from " + path); + } + if (std::strncmp(info.bdf, expectedBdf.c_str(), sizeof(info.bdf)) == 0) { + return path; + } + } + + throw std::runtime_error("No QDMA device found for PF1 " + expectedBdf); +} + class RawTransferBuffer { public: RawTransferBuffer(slash_qdma* qdma, uint64_t physAddr, uint64_t size, diff --git a/vrt/vrtd/README.md b/vrt/vrtd/README.md index 0c17ed63..78c50ab9 100644 --- a/vrt/vrtd/README.md +++ b/vrt/vrtd/README.md @@ -22,7 +22,7 @@ AMD Alveo V80 hardware Key responsibilities: -- Sysfs-based automatic device discovery at startup +- Character-device discovery with stable CTL/QDMA number pairing - Per-client DMA buffer management (HBM and DDR) with automatic cleanup on disconnect - FPGA bitstream programming via the design writer subsystem diff --git a/vrt/vrtd/src/device.c b/vrt/vrtd/src/device.c index 22c4f6df..d56284c5 100644 --- a/vrt/vrtd/src/device.c +++ b/vrt/vrtd/src/device.c @@ -20,15 +20,14 @@ /** * @file device.c - * @brief Sysfs-based device discovery and initialization for SLASH FPGA devices. + * @brief Character-device discovery and initialization for SLASH FPGA devices. * * This module implements the device lifecycle for the vrtd daemon. It discovers * AMD Alveo V80 (SLASH) devices by globbing /dev/slash_ctl* character device * nodes exposed by the kernel driver, then opens each device by: * * 1. Opening the control device via libslash (slash_ctldev_open). - * 2. Locating the matching QDMA control device by PCI BDF (bus:device prefix) - * via sysfs enumeration under /sys/class/misc/. + * 2. Opening the matching QDMA control device with the same stable number. * 3. Probing all six PCI BARs and memory-mapping the usable ones. * 4. Initializing subsystem drivers: clock driver, design writer, memory map. * @@ -47,11 +46,11 @@ #include #include +#include #include +#include #include #include -#include -#include #include #include @@ -61,148 +60,52 @@ static bool devices_contains_path(const struct device_ptr_array *devices, const static int device_read_pci_info(struct device *d, struct vrtd_pci_info *out); /** - * Find the /dev/ path of the qdma_ctl device sharing the same PCI bus:device - * as the given BDF. Returns 0 on success (path written to out_path), -1 on - * failure or no match (out_path set to NULL). + * Open the QDMA control device paired with a control-device path. * - * The lookup works by: - * 1. Extracting the bus:device prefix from ctl_bdf (e.g., "0000:65:00" from - * "0000:65:00.1"). - * 2. Globbing /sys/class/misc/slash_qdma_ctl_.* to find any QDMA - * misc device nodes registered by the kernel driver on the same PCI slot. - * 3. Reading the uevent file under the matched sysfs entry to extract the - * DEVNAME, and prepending "/dev/" to form the full device path. + * The kernel assigns matching PF1/PF2 endpoints the same stable board number, + * so /dev/slash_ctlN always pairs with /dev/slash_qdma_ctlN. * - * @param ctl_bdf PCI BDF string of the control device (e.g., "0000:65:00.1"). - * @param out_path On success, receives a heap-allocated string with the /dev/ - * path of the QDMA device. Set to NULL if no match is found - * (which is not an error). Caller must free. - * @return 0 on success (match found or no match), -1 on I/O or allocation error. + * @param ctl_path Control-device path (e.g., "/dev/slash_ctl0"). + * @param out On success, receives the open QDMA handle, or NULL if the + * paired node is absent. Caller must close. + * @return 0 on success (including absence), -1 on malformed path or open error. */ -static int find_qdma_dev_path_by_bdf(const char *ctl_bdf, char **out_path) +static int open_paired_qdma(const char *ctl_path, struct slash_qdma **out) { - *out_path = NULL; + static const char ctl_prefix[] = "/dev/slash_ctl"; + const char *card; - char prefix[VRTD_PCI_BDF_LEN]; - if (pci_bdf_prefix(ctl_bdf, prefix) != 0) { + if (ctl_path == NULL || out == NULL) { + errno = EINVAL; return -1; } - /* Build a glob pattern to match any QDMA misc device on the same PCI slot. */ - _cleanup_(cleanup_free) - char *pattern = NULL; - if (asprintf(&pattern, "/sys/class/misc/slash_qdma_ctl_%s.*", prefix) < 0) { - return -1; - } - - _cleanup_(globfree) - glob_t g = {0}; - int ret = glob(pattern, GLOB_ERR, NULL, &g); - if (ret != 0) { - return (ret == GLOB_NOMATCH) ? 0 : -1; - } - - if (g.gl_pathc > 1) { - LOG( - LOG_WARNING, - "Multiple QDMA devices found for BDF prefix %s; using first match", - prefix - ); - } - - const char *entry = g.gl_pathv[0]; - - /* Read the uevent file to extract the kernel-assigned DEVNAME. */ - _cleanup_(cleanup_free) - char *uevent_path = NULL; - if (asprintf(&uevent_path, "%s/uevent", entry) < 0) { + *out = NULL; + if (strncmp(ctl_path, ctl_prefix, sizeof(ctl_prefix) - 1) != 0) { + errno = EINVAL; return -1; } - FILE *f = fopen(uevent_path, "r"); - if (f == NULL) { + card = ctl_path + sizeof(ctl_prefix) - 1; + if (*card == '\0' || strspn(card, "0123456789") != strlen(card)) { + errno = EINVAL; return -1; } - /* Parse uevent line-by-line looking for DEVNAME=. */ - char line[256]; - while (fgets(line, sizeof(line), f) != NULL) { - static const char devname_key[] = "DEVNAME="; - if (strncmp(line, devname_key, sizeof(devname_key) - 1) != 0) { - continue; - } - const char *devname = line + sizeof(devname_key) - 1; - size_t len = strlen(devname); - /* Strip trailing newline/carriage return characters. */ - while (len > 0 && (devname[len - 1] == '\n' || devname[len - 1] == '\r')) { - len--; - } - if (asprintf(out_path, "/dev/%.*s", (int)len, devname) < 0) { - *out_path = NULL; - fclose(f); - return -1; - } - fclose(f); - return 0; - } - fclose(f); - - return 0; -} - -/** - * Find the /dev/ path of a slash_ctl device by its full PCI BDF string. - * - * The slash_ctl misc device is registered by the kernel driver under a - * stable sysfs name derived from the full PCI BDF (including function number), - * e.g. /sys/class/misc/slash_ctl_0000:61:00.2. The corresponding /dev/ node - * uses an incrementing counter (slash_ctlN) that changes after each hotplug - * remove+rescan cycle. This function resolves the current /dev/ path by - * reading the DEVNAME entry from the stable sysfs uevent file. - * - * @param bdf Full PCI BDF string including function (e.g. "0000:61:00.2"). - * @param out_path On success, receives a heap-allocated string with the /dev/ - * path of the slash_ctl device. Set to NULL if the device is - * not yet registered (which is not an error). Caller must free. - * @return 0 on success (device found or not yet present), -1 on I/O or - * allocation error. - */ -int find_slash_ctl_dev_path_by_bdf(const char *bdf, char **out_path) -{ - *out_path = NULL; - _cleanup_(cleanup_free) - char *uevent_path = NULL; - if (asprintf(&uevent_path, "/sys/class/misc/slash_ctl_%s/uevent", bdf) < 0) { + char *qdma_path = NULL; + if (asprintf(&qdma_path, "/dev/slash_qdma_ctl%s", card) < 0) return -1; - } - FILE *f = fopen(uevent_path, "r"); - if (f == NULL) { - /* Device not yet registered in sysfs — treat as no-match, not an error. */ - return 0; + *out = slash_qdma_open(qdma_path); + if (*out == NULL) { + if (errno == ENOENT || errno == ENODEV) + return 0; + return -1; } - char line[256]; - while (fgets(line, sizeof(line), f) != NULL) { - static const char devname_key[] = "DEVNAME="; - if (strncmp(line, devname_key, sizeof(devname_key) - 1) != 0) { - continue; - } - const char *devname = line + sizeof(devname_key) - 1; - size_t len = strlen(devname); - while (len > 0 && (devname[len - 1] == '\n' || devname[len - 1] == '\r')) { - len--; - } - if (asprintf(out_path, "/dev/%.*s", (int)len, devname) < 0) { - *out_path = NULL; - fclose(f); - return -1; - } - fclose(f); - return 0; - } - fclose(f); + LOG(LOG_INFO, "Paired control device %s with QDMA device %s", + ctl_path, qdma_path); return 0; } @@ -291,8 +194,8 @@ static int devices_open(struct device_ptr_array *devices, size_t pathc, char **p * 3. Initialize the buffer tracking array. * 4. Create the device memory map (for BAR-based address translation). * 5. Create the clock driver (Xilinx clock wizard access via BAR4). - * 6. Read PCI info (BDF, vendor/device IDs) via ioctl, then locate and open - * the matching QDMA device by PCI BDF prefix. + * 6. Read PCI info (BDF, vendor/device IDs) via ioctl, then construct and + * open the matching numbered QDMA path. * 7. If QDMA is available, create the design writer (bitstream programming). * 8. Probe all six PCI BARs: read bar_info and mmap usable BARs. * @@ -331,51 +234,28 @@ static int device_open(struct device **out, const char *path) d->clock_driver = clock_driver_create(d->ctl); PROPAGATE_ERROR_NULL_STDC_LOG(d->clock_driver, LOG_ERR, "Error creating clock driver for %s", path); - /* Step 3: Match the QDMA ctl device by PCI BDF (bus:device prefix). */ + /* Step 3: Read PCI identity and open the same-numbered QDMA node. */ { struct vrtd_pci_info pci_info = {0}; int pci_ret = device_read_pci_info(d, &pci_info); if (pci_ret != 0) { LOG( LOG_WARNING, - "Could not read PCI info for %s; skipping QDMA lookup", + "Could not read PCI info for %s", d->path ); + } + + int qdma_ret = open_paired_qdma(path, &d->qdma); + if (qdma_ret != 0) { + LOG(LOG_WARNING, "Error opening QDMA device paired with %s: %m", + d->path); + } else if (d->qdma != NULL) { + /* QDMA available -- create the design writer for bitstream programming. */ + d->design_writer = design_writer_create(d->qdma); + PROPAGATE_ERROR_NULL_STDC_LOG(d->design_writer, LOG_ERR, "Error creating design writer for %s", d->path); } else { - _cleanup_(cleanup_free) - char *qdma_path = NULL; - int find_ret = find_qdma_dev_path_by_bdf(pci_info.bdf, &qdma_path); - if (find_ret != 0) { - LOG( - LOG_WARNING, - "Error searching for QDMA device for BDF %s (%s)", - pci_info.bdf, d->path - ); - } else if (qdma_path != NULL) { - d->qdma = slash_qdma_open(qdma_path); - if (d->qdma == NULL) { - LOG( - LOG_WARNING, - "Error opening QDMA device %s (for %s): %m", - qdma_path, d->path - ); - } else { - LOG( - LOG_INFO, - "Matched QDMA device %s for ctldev %s (BDF %s)", - qdma_path, d->path, pci_info.bdf - ); - /* QDMA available -- create the design writer for bitstream programming. */ - d->design_writer = design_writer_create(d->qdma); - PROPAGATE_ERROR_NULL_STDC_LOG(d->design_writer, LOG_ERR, "Error creating design writer for %s", d->path); - } - } else { - LOG( - LOG_WARNING, - "No QDMA device found for BDF %s (%s)", - pci_info.bdf, d->path - ); - } + LOG(LOG_WARNING, "No QDMA device paired with %s", d->path); } } diff --git a/vrt/vrtd/src/device.h b/vrt/vrtd/src/device.h index abad8c5d..4865a47e 100644 --- a/vrt/vrtd/src/device.h +++ b/vrt/vrtd/src/device.h @@ -53,7 +53,7 @@ struct device_memory_map; * drivers, DMA buffers, and the HBM/DDR memory map allocator. */ struct device { - /** @brief Sysfs path of this device (e.g. "/sys/bus/pci/devices/0000:03:00.0"). + /** @brief Stable control character-device path (e.g. "/dev/slash_ctl0"). * Heap-allocated, owning. */ char *path; /* owning */ /** @brief libslash control device handle for ioctl operations (owning). */ @@ -101,9 +101,9 @@ DECLARE_OWNING_PTR_ARRAY(device_ptr_array, struct device *, cleanup_device); /** * @brief Discover all SLASH FPGA devices on the system and open them. * - * Enumerates PCI devices via sysfs, opens libslash control and QDMA handles, - * maps BARs, and initializes per-device subsystems (design writer, clock - * driver, memory allocator). + * Enumerates SLASH control character devices, opens libslash control and QDMA + * handles, maps BARs, and initializes per-device subsystems (design writer, + * clock driver, memory allocator). * * @param[out] devices Array to populate with discovered device pointers. * The array takes ownership of all allocated devices. @@ -111,19 +111,4 @@ DECLARE_OWNING_PTR_ARRAY(device_ptr_array, struct device *, cleanup_device); */ int devices_discover_and_open(struct device_ptr_array *devices); -/** - * @brief Find the current /dev/slash_ctlN path for a PF2 device by its full BDF. - * - * The /dev node suffix is assigned by an incrementing kernel counter and changes - * after each hotplug remove+rescan cycle. This function resolves the current - * path by reading the stable sysfs uevent file at - * /sys/class/misc/slash_ctl_/uevent. - * - * @param bdf Full PCI BDF including function number (e.g. "0000:61:00.2"). - * @param out_path On success, receives a heap-allocated /dev/ path string, or - * NULL if the device is not yet registered. Caller must free. - * @return 0 on success (device found or absent), -1 on I/O or allocation error. - */ -int find_slash_ctl_dev_path_by_bdf(const char *bdf, char **out_path); - #endif // VRTD_DEVICE_H diff --git a/vrt/vrtd/src/serve.c b/vrt/vrtd/src/serve.c index bd39840c..be05e645 100644 --- a/vrt/vrtd/src/serve.c +++ b/vrt/vrtd/src/serve.c @@ -426,26 +426,12 @@ static uint16_t device_refresh_pf2_after_design_write(struct device *d) } /* - * The /dev/slash_ctlN suffix is assigned by an incrementing kernel counter - * and changes after each hotplug remove+rescan. d->path still holds the - * path from daemon startup (e.g. /dev/slash_ctl0); that node no longer - * exists. Resolve the new path via the stable sysfs name - * /sys/class/misc/slash_ctl_/uevent and update d->path in-place so - * that subsequent GET_BAR_FD and devices_discover_and_open deduplication - * both see the current path. + * The stable character-device path survives PF2 remove+rescan, but the + * existing handle still refers to the pre-PDI device. Close it and reopen + * the same path so subsequent BAR operations use the freshly-probed PF2. */ - _cleanup_(cleanup_free) char *new_ctl_path = NULL; - if (find_slash_ctl_dev_path_by_bdf(pf2_bdf, &new_ctl_path) != 0 || new_ctl_path == NULL) { - LOG(LOG_ERR, "device_refresh_pf2: cannot find slash_ctl device for %s in sysfs", pf2_bdf); - return VRTD_RET_INTERNAL_ERROR; - } - - LOG(LOG_INFO, "device_refresh_pf2: new slash_ctl path for %s is %s", pf2_bdf, new_ctl_path); - - slash_ctldev_close(d->ctl); - free(d->path); - d->path = new_ctl_path; - new_ctl_path = NULL; /* ownership transferred — prevent cleanup_free from freeing */ + (void) slash_ctldev_close(d->ctl); + d->ctl = NULL; /* * After a hotplug rescan the kernel creates the device node immediately @@ -3167,7 +3153,7 @@ static uint16_t client_handle_request_clock_op( * device. * * Populates a vrtd_device_info structure containing: - * - name: the basename of the device's sysfs path (e.g. "0000:65:00.0"). + * - name: the basename of the device's /dev path (e.g. "slash_ctl0"). * - pci: BDF string, vendor/device/subsystem IDs. * * Auth: auth_request_get_device_info. diff --git a/vrt/vrtd/tests/device_test.cpp b/vrt/vrtd/tests/device_test.cpp index c66fd9f6..cc43c92c 100644 --- a/vrt/vrtd/tests/device_test.cpp +++ b/vrt/vrtd/tests/device_test.cpp @@ -175,10 +175,23 @@ TEST(DeviceDiscoveryTest, DiscoverAndOpen) { EXPECT_EQ(ret, 0); EXPECT_GT(devices.len, 0u); - /* Each discovered device must have at least a control handle. */ + /* Each discovered device must have at least a control handle. When PF1 + * is available, its QDMA INFO BDF must identify the same board. */ for (size_t i = 0; i < devices.len; ++i) { EXPECT_NE(devices.d[i], nullptr); EXPECT_NE(devices.d[i]->ctl, nullptr); + if (devices.d[i]->qdma != nullptr) { + struct slash_qdma_info info{}; + int info_ret = slash_qdma_info_read(devices.d[i]->qdma, &info); + + EXPECT_EQ(info_ret, 0); + if (info_ret == 0) { + EXPECT_EQ(info.bdf[11], '1'); + EXPECT_EQ(std::strncmp(info.bdf, devices.d[i]->pci_info.bdf, + std::strlen(devices.d[i]->pci_info.bdf)), + 0); + } + } } device_ptr_array_free(&devices);