
PCI/PCIe 是嵌入式与服务器平台的核心高速总线,网卡、SSD、显卡、FPGA 等高速外设均依赖它完成数据交互,是 Linux 内核开发的必备核心知识点。很多开发者熟悉常规字符设备、外设驱动,却对 PCI 驱动体系一知半解,看不懂设备枚举流程、摸不透内核 PCI 子系统工作逻辑,面对设备匹配、资源映射、DMA 配置、中断注册等问题时常无从下手,排查设备识别失败、总线初始化异常等问题更是毫无思路。
不同于简单外设驱动,Linux PCI 驱动依托内核标准子系统运行,拥有固定的注册、探测、初始化流程,有专属的开发规范与调用逻辑。本文摒弃晦涩理论与零散代码,从 PCI 总线基础原理入手,通俗拆解设备枚举、资源申请、地址映射、驱动匹配等核心流程,手把手梳理标准开发范式,规避常见适配坑点,零基础也能快速吃透 Linux PCI 驱动核心逻辑,轻松搞定各类 PCI 设备开发与调试工作。
一、初识 Linux PCI 总线
面试题写作模版PCI(Peripheral Component Interconnect)即外设部件互连标准,是一种由英特尔公司在 1991 年下半年首先提出的局部总线标准,用于规范计算机主板与外部设备之间的连接和数据通信,实现CPU、内存与各类外设扩展卡之间的数据交互。PCI 总线具备诸多特点。它支持即插即用功能,系统启动时会自动扫描、配置 PCI 设备,统一为设备分配内存地址、中断号等系统资源,无需用户手动调整硬件跳线,大幅简化了硬件设备的安装与使用流程。
在数据传输方面,PCI 总线支持 32 位或 64 位数据宽度,常规时钟频率为 33MHz 或 66MHz。以 33MHz、32 位的 PCI 总线为例,其理论传输带宽可达 133MB/s(33MHz×32bit÷8),可以满足同期绝大多数计算机外设的数据传输需求。
同时,PCI 总线具备良好的通用性与兼容性,总线运行机制独立于处理器,不受特定 CPU 型号的限制。除此之外,PCI 总线采用地址/数据复用技术,通过同一组信号线分时传输地址信息和数据信息,有效减少了硬件引脚数量,降低了主板与外设的硬件设计难度。
无论是服务器场景还是嵌入式设备场景,硬件外设都无法直接和 CPU、内存完成高速可靠通信,Linux PCI 子系统正是打通软硬件交互的核心桥梁,是各类高速外设正常工作的必备支撑。
在服务器领域,高性能网卡、RAID 卡等核心业务硬件均依托 PCI 总线接入主板,离不开 Linux PCI 子系统统一调度管理。以数据中心万兆网卡为例,PCI 子系统全权负责网卡与 CPU、内存间的数据交互,依托 PCI 高速传输特性,网卡可以逼近理论带宽收发海量网络报文,承载数据中心大规模流量交换业务;RAID 卡同样依靠 PCI 子系统完成系统对接,实现多块磁盘统一管控、数据冗余备份,在保障存储数据安全的同时提升读写速率,为企业关键业务提供稳定存储底座。
在嵌入式工业场景下,Linux PCI 子系统同样不可替代。工控嵌入式主机外接各类传感器、执行控制器件时,依靠 PCI 子系统统筹外设和主控系统的通信链路。自动化产线上的温度、压力传感器采集的现场数据,经由 PCI 总线传输至嵌入式系统解析运算,系统据此精准调控生产线运行状态,实现工业流程自动化闭环控制。
如果缺少 Linux PCI 子系统,操作系统无法统一枚举硬件、分配 IO 与内存资源、管理中断,高速外设无法正常寻址,多设备并发通信会产生资源冲突,网卡、RAID 卡、工控外设等硬件都难以稳定发挥硬件性能,服务器和嵌入式设备的业务功能也就无法落地实现。
二、Linux PCI 驱动核心数据结构
面试题写作模版struct pci_dev 是 Linux 内核中用于表示 PCI 设备的数据结构,它包含了丰富的设备信息,这些信息对于内核识别、管理和操作 PCI 设备至关重要。该结构体定义在 include/linux/pci.h 头文件中 ,以下是对其部分重要成员的详细介绍。
vendor 和 device 成员分别存储了设备制造商的供应商 ID 和设备被探测时用于标识该特定设备的 ID。供应商 ID 由 PCI 特殊兴趣小组维护的全球注册表进行统一分配,每个制造商都有唯一的供应商 ID,例如 Intel 的供应商 ID 通常为 0x8086。设备 ID 则由设备制造商自行设定,用于区分同一家制造商生产的不同设备型号。这两个 ID 组合在一起,形成了设备的唯一标识,驱动程序可以通过它们来准确识别设备。
// 在内核驱动的 probe 函数中,读取 PCI 设备的 vendor 和 device IDstatic int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id){ // 打印设备的供应商 ID 和设备 ID pr_info("PCI Vendor ID: 0x%04x, Device ID: 0x%04x\n", pdev->vendor, pdev->device); return 0;}subsystem_vendor 和 subsystem_device 用于指定 PCI 子系统供应商和子系统设备 ID,在某些情况下,当设备具有多个子系统或同一设备在不同子系统中有不同配置时,这些 ID 可以帮助驱动进一步识别设备的具体类型和配置。
// 读取并打印 PCI 子系统 IDpr_info("Subsystem Vendor ID: 0x%04x, Subsystem Device ID: 0x%04x\n", pdev->subsystem_vendor, pdev->subsystem_device);class 成员标识设备所属的类别,它存储在一个 16 位寄存器中,高 8 位标识基类或组,例如网络设备通常属于 PCI_CLASS_NETWORK 类别,通过这个成员,驱动可以快速判断设备的大致类型,以便进行相应的初始化和操作。
// 判断设备是否为网络设备if ((pdev->class >> 8) == PCI_CLASS_NETWORK_ETHERNET) { pr_info("该 PCI 设备为以太网控制器\n");}pin 成员记录了在传统基于 INTx 的中断情况下,设备使用的中断引脚,当设备需要产生中断信号时,会通过该引脚向系统发送中断请求。irq 字段用于存储设备的中断号,在设备启动时,默认使用预分配的非 MSI(Message Signaled Interrupts)中断。当设备需要启用 MSI 或 MSI-X 中断模式时,会通过 pci_alloc_irq_vectors () 函数进行配置,在 MSI 中断模式下,该字段的值会被新的 MSI 向量替换,而在 MSI-X 中断模式下,该字段的预分配值不变,但此时 irq 字段在该模式下无效。
// 打印传统中断引脚与中断号pr_info("中断引脚 INTx: %d, 分配的中断号 irq: %d\n", pdev->pin, pdev->irq);// 申请 MSI 中断向量(标准用法)pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);struct pci_driver 代表了 PCI 驱动,它是驱动程序在内核中的抽象表示,它包含了驱动的关键信息和操作函数,用于驱动的注册以及与设备的匹配过程。该结构体同样定义在 include/linux/pci.h 头文件中。
name 成员是驱动的名称,这个名称在内核中所有 PCI 驱动中必须是唯一的,通常会将其设置为驱动模块的名称。在驱动注册时,如果内核中已经存在同名的驱动,注册操作将会失败。id_table 是一个指向 struct pci_device_id 表的指针,这个表中包含了驱动所支持的设备的相关信息,如供应商 ID、设备 ID 等,它是驱动识别和匹配设备的关键依据,只有当 id_table 不为空时,驱动的 probe 函数才会被调用。
probe 函数是 PCI 驱动的核心函数之一,当 PCI 设备与驱动的 id_table 中的条目匹配时,PCI 核心会调用该函数。在 probe 函数中,驱动会对设备进行初始化操作,包括分配系统资源、设置设备寄存器、初始化设备的工作状态等。如果 probe 函数成功完成设备初始化,它会返回 0;如果在初始化过程中出现错误,如资源分配失败、设备硬件故障等,则会返回一个负的错误码,告知内核初始化失败。
remove 函数则在设备被移除时被调用,它的主要作用是释放设备占用的系统资源,如内存、I/O 端口、中断等,并进行一些必要的清理工作,确保设备移除后系统的状态保持正常。例如,当用户卸载一个 PCI 设备驱动模块时,remove 函数会被执行,它会将之前在 probe 函数中分配的资源归还给系统,避免资源泄漏。
// 完整定义一个 struct pci_driver 结构体实例staticstruct pci_driver my_pci_driver = { .name = "my_pci_demo", // 唯一驱动名称 .id_table = my_pci_ids, // 支持的设备 ID 表 .probe = my_pci_probe, // 设备匹配成功时调用 .remove = my_pci_remove, // 设备移除时调用};// 模块加载时注册 PCI 驱动module_pci_driver(my_pci_driver);struct pci_device_id 主要用于识别设备,它通过一系列的设备标识信息,帮助驱动准确地找到对应的设备。该结构体定义如下:
struct pci_device_id { u32 vendor, device; u32 subvendor, subdevice; u32class, class_mask; kernel_ulong_t driver_data;};vendor 和 device 分别代表设备的供应商 ID 和设备 ID,这两个 ID 组合起来形成了设备的唯一 32 位标识符,驱动程序主要依靠这个标识符来识别它所支持的设备。例如,一个特定型号的网卡,其供应商 ID 和设备 ID 是固定的,网卡驱动在初始化时,会遍历系统中的 PCI 设备,将每个设备的 vendor 和 device ID 与自己支持的 ID 列表进行比对,当找到匹配的设备时,就可以确定该设备是自己需要驱动的网卡。
subvendor 和 subdevice 代表子系统 ID,在一些复杂的设备系统中,可能存在多个子系统,这些 ID 可以帮助驱动进一步区分不同子系统中的设备。class 和 class_mask 主要用于与类相关的 PCI 驱动,当驱动需要处理给定类别的所有设备时,可以使用这两个成员。对于此类驱动,vendor 和 device 通常会设置为 PCI_ANY_ID,表示不关心具体的供应商和设备 ID,只关注设备的类别。例如,一个通用的存储设备驱动,可能会将 class 设置为存储设备的类别 ID,class_mask 设置为相应的掩码,这样它就可以匹配所有符合该类别的存储设备。
driver_data 是驱动的私有数据,它并不用于设备识别,而是用于驱动在处理设备时传递一些自定义的数据,以便区分不同的设备或保存设备相关的特定信息。例如,驱动可以在这个字段中存储设备的一些配置参数或状态信息,方便在后续的操作中使用。
为了方便创建 struct pci_device_id 的实例,内核提供了三个宏:PCI_DEVICE 用于描述特定的 PCI 设备,通过给定供应商和设备 ID 来匹配特定设备;PCI_DEVICE_CLASS 用于描述特定的 PCI 设备类,通过给定类和类掩码来匹配特定类别的设备;PCI_DEVICE_SUB 用于描述带有子系统的特定 PCI 设备,通过给定子系统信息来匹配特定设备 。这些宏简化了设备 ID 的定义和使用,提高了驱动开发的效率。
// 1. 使用 PCI_DEVICE 宏:匹配指定供应商+设备 IDstatic conststruct pci_device_id my_pci_ids[] = { // 匹配 Intel 某一特定设备(示例 ID) PCI_DEVICE(0x8086, 0x1234), { 0 } // 表必须以全 0 结尾};// 2. 使用 PCI_DEVICE_SUB 宏:匹配带子系统 ID 的设备PCI_DEVICE_SUB(0x8086, 0x1234, 0x8086, 0x5678),// 3. 使用 PCI_DEVICE_CLASS 宏:匹配某一类设备(如所有以太网控制器)PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET << 8, 0xffff00),// 4. 带私有数据 driver_data 的用法{ PCI_DEVICE(0x8086, 0x1234), .driver_data = (kernel_ulong_t)"设备私有数据" },三、Linux PCI 驱动工作原理
面试题写作模版在 Linux 系统启动过程中,PCI 设备的枚举与识别是系统识别和管理硬件设备的关键步骤。当 Linux 内核启动时,会执行一系列操作来扫描和识别连接到 PCI 总线上的设备。
内核会初始化 PCI 子系统,这涉及到设置 PCI 总线的基本参数,包括总线时钟频率、地址空间映射等。初始化完成后,内核会开始遍历 PCI 总线。PCI 总线采用树形结构,内核会从根总线开始,递归地扫描每一条总线以及连接在总线上的设备。
在扫描过程中,内核会读取每个 PCI 设备的配置空间。每个 PCI 设备都有一个 256 字节的配置空间,其中前 64 字节是标准化的,包含了设备的重要信息。内核会读取配置空间中的 “Vendor ID”(供应商 ID)和 “Device ID”(设备 ID),这两个 ID 是设备的唯一标识。例如,Intel 公司的网卡设备,其 “Vendor ID” 通常为 0x8086,而不同型号的网卡会有不同的 “Device ID”。内核通过将读取到的 “Vendor ID” 和 “Device ID” 与系统中已注册的设备驱动列表进行比对,来确定设备的类型和对应的驱动程序。
如果遇到 PCI-PCI 桥设备,内核会将其视为一个新的总线起点,并继续在其下游总线上进行设备扫描。在扫描过程中,内核会为每个发现的 PCI 设备创建一个 struct pci_dev 结构体,这个结构体包含了设备的各种信息,如设备的供应商 ID、设备 ID、中断号、资源分配情况等。通过这个结构体,内核可以对设备进行统一的管理和操作。例如,对于一个新发现的 PCI 设备,内核会将其 “Vendor ID” 和 “Device ID” 存储在 struct pci_dev 结构体的相应字段中,以便后续驱动程序匹配和设备管理使用。
在 PCI 设备被枚举和识别后,接下来的关键步骤是驱动的匹配与加载,这一步骤确保每个 PCI 设备都能找到合适的驱动程序来实现其功能。
当 PCI 设备的相关信息被内核获取并存储在 struct pci_dev 结构体中后,PCI 核心会开始尝试为该设备找到对应的驱动程序。每个 PCI 驱动在注册时,都会向内核提供一个 struct pci_driver 结构体,其中包含了驱动所支持的设备 ID 列表,即 id_table。这个列表中记录了驱动能够支持的设备的 “Vendor ID” 和 “Device ID” 组合。
PCI 核心会遍历系统中已注册的所有 PCI 驱动的 id_table,将设备的 “Vendor ID” 和 “Device ID” 与 id_table 中的每一项进行匹配。如果找到匹配的项,就意味着找到了能够驱动该设备的驱动程序。例如,假设系统中有一个 NVIDIA 的显卡设备,其 “Vendor ID” 为 0x10DE,“Device ID” 为 0x2684,而 NVIDIA 显卡驱动的 id_table 中包含了这一 ID 组合,那么 PCI 核心就会认定该驱动与该显卡设备匹配。
一旦驱动与设备匹配成功,PCI 核心就会调用驱动的 probe 函数。probe 函数是驱动程序的核心初始化函数,在这个函数中,驱动会对设备进行一系列的初始化操作。它会检查设备的硬件状态,确保设备正常工作。然后,驱动会为设备分配必要的系统资源,如内存空间、I/O 端口和中断请求线(IRQ)等。以内存分配为例,驱动可能会通过 pci_request_regions 函数向系统申请一段内存区域,用于设备的数据存储和操作。
驱动还会初始化设备的寄存器,设置设备的工作模式和参数,使其能够正常运行。如果 probe 函数执行成功,设备就被成功初始化,驱动与设备之间的连接建立完成,设备就可以开始正常工作;如果 probe 函数执行过程中出现错误,如资源分配失败、设备硬件故障等,驱动会返回一个错误代码,告知内核初始化失败,设备将无法正常工作。
PCI 设备在系统中运行需要占用一定的系统资源,包括内存、中断等,Linux 系统通过 PCI 核心来对这些资源进行分配与管理,以确保设备能够正常工作,同时避免资源冲突。
当 PCI 设备与驱动匹配成功并调用 probe 函数进行初始化时,PCI 核心会首先为设备分配内存资源。PCI 设备通常需要内存来存储数据和代码,内存分配方式主要有两种:I/O 映射内存(I/O-mapped memory)和内存映射内存(Memory-mapped memory)。
对于 I/O 映射内存,设备通过特定的 I/O 端口来访问内存,这种方式需要使用 inb、outb 等 I/O 指令进行数据读写。而内存映射内存则是将设备的内存空间直接映射到系统的内存地址空间中,这样设备可以像访问普通内存一样访问自己的内存,数据读写速度更快。在分配内存时,PCI 核心会根据设备的需求和系统内存的使用情况,为设备分配合适的内存区域。例如,对于一个高速网卡设备,它可能需要大量的内存来缓存网络数据包,PCI 核心会为其分配足够的连续内存空间,以满足其数据处理需求。
中断资源的分配也是 PCI 设备资源管理的重要部分。PCI 设备通过中断来通知系统有事件发生,如数据到达、设备状态改变等。PCI 核心会为每个 PCI 设备分配一个中断请求线(IRQ),当设备需要产生中断时,会通过该 IRQ 向系统发送中断信号。在分配 IRQ 时,PCI 核心会尽量避免中断冲突,确保每个设备的中断能够被系统正确接收和处理。例如,系统中可能同时存在多个 PCI 设备,如网卡、显卡、声卡等,PCI 核心会为它们分配不同的 IRQ,使得每个设备的中断请求都能独立地被系统响应。
设备的地址空间管理同样关键。PCI 设备的地址空间包括配置空间、I/O 空间和内存空间。配置空间用于存储设备的基本信息,如 “Vendor ID”、“Device ID” 等,内核可以通过 pci_read_config_byte、pci_write_config_byte 等函数来访问配置空间中的信息。
I/O 空间和内存空间则用于设备与系统之间的数据传输和存储。驱动程序可以通过 pci_resource_start、pci_resource_end 等函数来获取设备所分配的 I/O 空间和内存空间的地址范围,然后使用 ioremap 函数将这些物理地址映射到内核虚拟地址空间,以便驱动程序能够直接访问设备的地址空间。
例如,驱动程序在访问设备的寄存器时,就需要先通过这些函数获取寄存器的地址,并进行地址映射,然后才能对寄存器进行读写操作,实现对设备的控制和数据传输。
四、Linux PCI 驱动开发实战
面试题写作模版设备 ID 表用于告诉内核,这个驱动可以支持哪些 PCI 设备。内核通过对比设备的供应商 ID、设备 ID 等信息,判断驱动与设备是否匹配。在 Linux 内核中,设备 ID 表通常使用 struct pci_device_id 类型的数组定义。比如:
static conststruct pci_device_id my_pci_ids[] = { {PCI_DEVICE(0x1234, 0x5678)}, // 支持 Vendor ID 为 0x1234,Device ID 为 0x5678 的设备 {0, } // 必须以全 0 条目结束};MODULE_DEVICE_TABLE(pci, my_pci_ids);代码使用 PCI_DEVICE 宏初始化 struct pci_device_id 结构体,自动填入供应商 ID 和设备 ID。MODULE_DEVICE_TABLE 宏用于生成设备表,让内核可以读取该驱动支持的设备列表。系统检测到新 PCI 设备时,会遍历该表,查找匹配的设备 ID,如果匹配成功,就会使用该驱动管理设备。
注册 PCI 驱动是把驱动加入内核管理,让内核可以识别并使用该驱动。Linux 内核使用 pci_register_driver 函数完成驱动注册。注册前,需要定义 struct pci_driver 结构体,指定驱动名称、设备 ID 表和回调函数。例如:
staticstruct pci_driver my_pci_driver = { .name = "my_pci_driver", // 驱动名称,可自定义,但要保证唯一性 .id_table = my_pci_ids, // 指向上面定义的设备 ID 表 .probe = my_pci_probe, // 探测函数,设备匹配时调用 .remove = my_pci_remove, // 移除函数,设备移除或驱动卸载时调用};在驱动初始化函数中调用 pci_register_driver 完成注册:
static int __init my_pci_init(void) { return pci_register_driver(&my_pci_driver);}module_init(my_pci_init);my_pci_init 是驱动的初始化入口函数,module_init 宏标记该函数为模块加载函数,驱动模块加载时自动执行。pci_register_driver 函数把 struct pci_driver 结构体注册到内核的 PCI 驱动列表中,内核后续会根据设备 ID 匹配并使用该驱动。驱动名称 name 必须唯一,方便系统识别和管理驱动。例如网卡驱动可以命名为 my_ethernet_pci_driver。
probe 函数是 PCI 驱动的核心函数,内核匹配到设备与驱动后,会自动调用该驱动的 probe 函数。probe 函数用于初始化设备,让设备具备工作条件。一般需要执行以下操作:
(1)启用设备:使用 pci_enable_device 函数启用 PCI 设备,让设备可以响应 I/O 和内存访问。例如:
err = pci_enable_device(pdev);if (err) { return err;}pdev 是 struct pci_dev 结构体指针,代表当前匹配的 PCI 设备。如果启用失败,函数返回错误码,probe 函数直接返回错误,停止初始化。
(2)资源分配与映射:使用 pci_request_regions 函数申请设备的 I/O 和内存资源,避免被其他程序占用。例如:
err = pci_request_regions(pdev, "my_pci_dev");if (err) { goto err_request;}申请成功后,使用 pci_iomap 函数将设备的 BAR(基地址寄存器)映射到内核虚拟地址,方便驱动访问设备寄存器和内存。示例代码:
void __iomem *bar0_addr = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));if (!bar0_addr) { err = -EIO; goto err_iomap;}这段代码将设备的第一个 BAR 映射到内核虚拟地址 bar0_addr,pci_resource_len 函数用于获取 BAR 的长度。
(3)中断处理准备:如果设备使用中断,需要申请中断号并注册中断处理函数。示例代码:
err = request_irq(pdev->irq, my_interrupt_handler, IRQF_SHARED, "my_pci_dev", pdev);if (err) { goto err_irq;}request_irq 函数用于申请中断号,pdev->irq 是设备中断号,my_interrupt_handler 是中断处理函数,IRQF_SHARED 表示中断可共享,my_pci_dev 是中断名称,pdev 是设备结构体指针。
完整的 probe 函数示例:
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { int err; // 启用设备 err = pci_enable_device(pdev); if (err) { return err; } // 请求资源 err = pci_request_regions(pdev, "my_pci_dev"); if (err) { goto err_request; } // 映射 BAR void __iomem *bar0_addr = pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!bar0_addr) { err = -EIO; goto err_iomap; } // 申请中断 err = request_irq(pdev->irq, my_interrupt_handler, IRQF_SHARED, "my_pci_dev", pdev); if (err) { goto err_irq; } // 其他初始化操作,如初始化设备队列、设置设备寄存器等 return 0;err_irq: pci_iounmap(pdev, bar0_addr);err_iomap: pci_release_regions(pdev);err_request: pci_disable_device(pdev); return err;}remove 函数在设备移除或驱动卸载时被调用,主要作用是释放 probe 函数中申请的所有资源,保证系统正常运行。例如释放内存、中断号、取消地址映射等。示例代码:
static void my_pci_remove(struct pci_dev *pdev) { // 释放中断 free_irq(pdev->irq, pdev); // 取消 BAR 映射 pci_iounmap(pdev, pci_resource_start(pdev, 0)); // 释放资源 pci_release_regions(pdev); // 禁用设备 pci_disable_device(pdev);}释放顺序与 probe 函数中申请资源的顺序相反。先使用 free_irq 释放中断号,再用 pci_iounmap 取消 BAR 映射,接着用 pci_release_regions 释放资源,最后用 pci_disable_device 禁用设备。如果 probe 函数中分配了私有数据、设备队列等资源,remove 函数中也必须对应释放。例如 probe 中使用 pci_set_drvdata 绑定了私有数据,卸载时需要先用 pci_get_drvdata 获取数据指针,再释放内存。
完成以上步骤后,驱动可以正常识别和初始化设备,接下来实现设备的具体功能,并与用户空间程序通信。
(1)设备操作:设备初始化完成后,根据设备功能编写操作代码。例如网卡驱动需要实现数据发送和接收函数。以发送数据为例,my_pci_send_data 函数会访问映射后的设备寄存器,配置发送参数,通过 DMA 将数据写入发送缓冲区,最后启动发送。
(2)与用户空间交互:要让用户空间访问 PCI 设备,需要创建设备节点。字符设备使用 cdev_init、cdev_add 初始化并添加设备,使用 device_create 在 sysfs 中创建设备节点。示例代码:
// 定义字符设备结构体struct cdev my_cdev;// 定义设备类struct class *my_class;// 定义设备结构体指针struct device *my_device;// 初始化字符设备cdev_init(&my_cdev, &my_fops);my_cdev.owner = THIS_MODULE;// 添加字符设备err = cdev_add(&my_cdev, MKDEV(major, minor), 1);if (err) { printk(KERN_ERR "Failed to add cdev\n"); return err;}// 创建设备类my_class = class_create(THIS_MODULE, "my_pci_class");if (IS_ERR(my_class)) { cdev_del(&my_cdev); printk(KERN_ERR "Failed to create class\n"); return PTR_ERR(my_class);}// 创建设备节点my_device = device_create(my_class, NULL, MKDEV(major, minor), NULL, "my_pci_device");if (IS_ERR(my_device)) { class_destroy(my_class); cdev_del(&my_cdev); printk(KERN_ERR "Failed to create device\n"); return PTR_ERR(my_device);}执行后,系统 /dev 目录下会生成 my_pci_device 设备节点,用户空间程序可以使用 open、read、write、ioctl 等系统调用与设备通信。驱动中需要实现 my_fops 结构体里的 open、read、write、ioctl 函数,处理用户空间请求。例如 read 函数从设备接收缓冲区读取数据并返回给用户空间;ioctl 函数用于配置设备工作模式、查询设备状态等。
end
如果这篇文章对你有所启发,欢迎点赞、在看,转发三连。星标⭐账号,还可以第一时间收到推送,感谢你的收看,我们下期再见~
往期干货推荐