1:字符设备驱动
整体结构:
1. PCI 驱动骨架 → module_pci_driver → pci_device_id(匹配 1234:11e8) → probe / remove 2. 设备私有数据 struct edu_pci_dev → 保存 pci_dev、bar0 映射地址、misc 设备 3. 字符设备接口(/dev/edu0) → open/read/write/ioctl → misc_register 4. 硬件操作封装 → edu_readl / edu_writel(ioread32/iowrite32)1.1注册流程说明
用
miscdevice提供/dev/edu0用
ioctl/read/write让用户态操作硬件寄存器
1.1.1 PCI驱动骨架
1.1.1.1厂商ID+设备ID(Vendor ID + Device ID)
/* QEMU edu 设备固定的 PCI Vendor ID / Device ID。 */ #define EDU_VENDOR_ID 0x1234 #define EDU_DEVICE_ID 0x11e81.1.1.2 结构体标明
成员变量有:
①pci_dev这个说明该驱动是搞这个pci设备对象的
②_iomem则是标明这个bar0是要映射到内核的虚拟地址上的
③resource_size_t 该变量能够反应 bar 究竟是想要多大内核空间
④instance:该成员变量的作用是 当前pc要是有不同的edu设备的话 每个设备都会有不同的instance实例。
⑤miscdevice:则是内核提供专门供给字符设备注册使用的结构体
/* * 每一个 edu PCI function 对应一个 edu_pci_dev。 * * pdev:Linux PCI 子系统创建的 PCI 设备对象。 * bar0:BAR0 ioremap 后的内核虚拟地址,只能用 ioread/iowrite 访问。 * miscdev:misc 字符设备对象,注册成功后生成 /dev/eduN。 */ struct edu_pci_dev { struct pci_dev *pdev; void __iomem *bar0; resource_size_t bar0_len; int instance; struct miscdevice miscdev; char misc_name[32]; };1.1.1.3 设定硬件设备匹配表
通过该表可实现 同一驱动程序适用不同的硬件设备(本质上这个表也是要逐渐上交给pci子系统,在子系统内部进行匹配,配成了,再经由子系统决定之心probe函数)
/* PCI 匹配表:告诉 PCI core 本驱动支持 QEMU edu 设备 1234:11e8。 */ static const struct pci_device_id edu_pci_ids[] = { { PCI_DEVICE(EDU_VENDOR_ID, EDU_DEVICE_ID) }, { } }; MODULE_DEVICE_TABLE(pci, edu_pci_ids);安装驱动之后也可以动态的修改
new_id 和 remove_id
这两个接口允许你动态添加和删除驱动支持的设备 ID,不需要重新编译驱动。
# 让 edu_pci 驱动支持 Vendor ID 0x1234,Device ID 0x11e9 的设备 echo "1234 11e9" > new_id # 移除刚才添加的设备 ID echo "1234 11e9" > remove_id调试新设备时,可以快速测试驱动是否能支持新的设备 ID
不需要修改驱动代码和重新编译
可以临时让一个通用驱动支持特定的设备
1.1.1.3 probe函数(主要就是向pci子系统申请一些资源+自定义结构体与子系统结构体绑定)
①pci_dev,pci_device_id:
probe函数的两个入口参数是由pci子系统(pci core)下发的,就是告诉probe函数,欸你(用户自己写的驱动)提交的表格,和我(pci core)枚举的硬件设备,匹配中了,其pci_dev与pci_device_id我交给你(用户写的probe函数),以下是内核维护的pci_dev结构体
- include/linux/pci.h, line 322 (as a struct)
/* The pci_dev structure describes PCI devices */ struct pci_dev { struct list_head bus_list; /* Node in per-bus list */ struct pci_bus *bus; /* Bus this device is on */ struct pci_bus *subordinate; /* Bus this device bridges to */ void *sysdata; /* Hook for sys-specific extension */ struct proc_dir_entry *procent; /* Device entry in /proc/bus/pci */ struct pci_slot *slot; /* Physical slot this device is in */ unsigned int devfn; /* Encoded device & function index */ unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ u8 revision; /* PCI revision, low byte of class word */ u8 hdr_type; /* PCI header type (`multi' flag masked out) */ #ifdef CONFIG_PCIEAER u16 aer_cap; /* AER capability offset */ struct aer_stats *aer_stats; /* AER stats for this device */ #endif #ifdef CONFIG_PCIEPORTBUS struct rcec_ea *rcec_ea; /* RCEC cached endpoint association */ struct pci_dev *rcec; /* Associated RCEC device */ #endif u32 devcap; /* PCIe Device Capabilities */ u8 pcie_cap; /* PCIe capability offset */ u8 msi_cap; /* MSI capability offset */ u8 msix_cap; /* MSI-X capability offset */ u8 pcie_mpss:3; /* PCIe Max Payload Size Supported */ u8 rom_base_reg; /* Config register controlling ROM */ u8 pin; /* Interrupt pin this device uses */ u16 pcie_flags_reg; /* Cached PCIe Capabilities Register */ unsigned long *dma_alias_mask;/* Mask of enabled devfn aliases */ struct pci_driver *driver; /* Driver bound to this device */ u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */ struct device_dma_parameters dma_parms; pci_power_t current_state; /* Current operating state. In ACPI, this is D0-D3, D0 being fully functional, and D3 being off. */ u8 pm_cap; /* PM capability offset */ unsigned int imm_ready:1; /* Supports Immediate Readiness */ unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */ unsigned int pme_poll:1; /* Poll device's PME status bit */ unsigned int d1_support:1; /* Low power state D1 is supported */ unsigned int d2_support:1; /* Low power state D2 is supported */ unsigned int no_d1d2:1; /* D1 and D2 are forbidden */ unsigned int no_d3cold:1; /* D3cold is forbidden */ unsigned int bridge_d3:1; /* Allow D3 for bridge */ unsigned int d3cold_allowed:1; /* D3cold is allowed by user */ unsigned int mmio_always_on:1; /* Disallow turning off io/mem decoding during BAR sizing */ unsigned int wakeup_prepared:1; unsigned int skip_bus_pm:1; /* Internal: Skip bus-level PM */ unsigned int ignore_hotplug:1; /* Ignore hotplug events */ unsigned int hotplug_user_indicators:1; /* SlotCtl indicators controlled exclusively by user sysfs */ unsigned int clear_retrain_link:1; /* Need to clear Retrain Link bit manually */ unsigned int d3hot_delay; /* D3hot->D0 transition time in ms */ unsigned int d3cold_delay; /* D3cold->D0 transition time in ms */ #ifdef CONFIG_PCIEASPM struct pcie_link_state *link_state; /* ASPM link state */ u16 l1ss; /* L1SS Capability pointer */ unsigned int ltr_path:1; /* Latency Tolerance Reporting supported from root to here */ #endif unsigned int pasid_no_tlp:1; /* PASID works without TLP Prefix */ unsigned int eetlp_prefix_path:1; /* End-to-End TLP Prefix */ pci_channel_state_t error_state; /* Current connectivity state */ struct device dev; /* Generic device interface */ int cfg_size; /* Size of config space */ /* * Instead of touching interrupt line and base address registers * directly, use the values stored here. They might be different! */ unsigned int irq; struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */ struct resource driver_exclusive_resource; /* driver exclusive resource ranges */ bool match_driver; /* Skip attaching driver */ unsigned int transparent:1; /* Subtractive decode bridge */ unsigned int io_window:1; /* Bridge has I/O window */ unsigned int pref_window:1; /* Bridge has pref mem window */ unsigned int pref_64_window:1; /* Pref mem window is 64-bit */ unsigned int multifunction:1; /* Multi-function device */ unsigned int is_busmaster:1; /* Is busmaster */ unsigned int no_msi:1; /* May not use MSI */ unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */ unsigned int block_cfg_access:1; /* Config space access blocked */ unsigned int broken_parity_status:1; /* Generates false positive parity */ unsigned int irq_reroute_variant:2; /* Needs IRQ rerouting variant */ unsigned int msi_enabled:1; unsigned int msix_enabled:1; unsigned int ari_enabled:1; /* ARI forwarding */ unsigned int ats_enabled:1; /* Address Translation Svc */ unsigned int pasid_enabled:1; /* Process Address Space ID */ unsigned int pri_enabled:1; /* Page Request Interface */ unsigned int is_managed:1; /* Managed via devres */ unsigned int is_msi_managed:1; /* MSI release via devres installed */ unsigned int needs_freset:1; /* Requires fundamental reset */ unsigned int state_saved:1; unsigned int is_physfn:1; unsigned int is_virtfn:1; unsigned int is_hotplug_bridge:1; unsigned int shpc_managed:1; /* SHPC owned by shpchp */ unsigned int is_thunderbolt:1; /* Thunderbolt controller */ /* * Devices marked being untrusted are the ones that can potentially * execute DMA attacks and similar. They are typically connected * through external ports such as Thunderbolt but not limited to * that. When an IOMMU is enabled they should be getting full * mappings to make sure they cannot access arbitrary memory. */ unsigned int untrusted:1; /* * Info from the platform, e.g., ACPI or device tree, may mark a * device as "external-facing". An external-facing device is * itself internal but devices downstream from it are external. */ unsigned int external_facing:1; unsigned int broken_intx_masking:1; /* INTx masking can't be used */ unsigned int io_window_1k:1; /* Intel bridge 1K I/O windows */ unsigned int irq_managed:1; unsigned int non_compliant_bars:1; /* Broken BARs; ignore them */ unsigned int is_probed:1; /* Device probing in progress */ unsigned int link_active_reporting:1;/* Device capable of reporting link active */ unsigned int no_vf_scan:1; /* Don't scan for VFs after IOV enablement */ unsigned int no_command_memory:1; /* No PCI_COMMAND_MEMORY */ unsigned int rom_bar_overlap:1; /* ROM BAR disable broken */ unsigned int rom_attr_enabled:1; /* Display of ROM attribute enabled? */ pci_dev_flags_t dev_flags; atomic_t enable_cnt; /* pci_enable_device has been called */ spinlock_t pcie_cap_lock; /* Protects RMW ops in capability accessors */ u32 saved_config_space[16]; /* Config space saved at suspend time */ struct hlist_head saved_cap_space; struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */ struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */ #ifdef CONFIG_HOTPLUG_PCI_PCIE unsigned int broken_cmd_compl:1; /* No compl for some cmds */ #endif #ifdef CONFIG_PCIE_PTM u16 ptm_cap; /* PTM Capability */ unsigned int ptm_root:1; unsigned int ptm_enabled:1; u8 ptm_granularity; #endif #ifdef CONFIG_PCI_MSI void __iomem *msix_base; raw_spinlock_t msi_lock; #endif struct pci_vpd vpd; #ifdef CONFIG_PCIE_DPC u16 dpc_cap; unsigned int dpc_rp_extensions:1; u8 dpc_rp_log_size; #endif #ifdef CONFIG_PCI_ATS union { struct pci_sriov *sriov; /* PF: SR-IOV info */ struct pci_dev *physfn; /* VF: related PF */ }; u16 ats_cap; /* ATS Capability offset */ u8 ats_stu; /* ATS Smallest Translation Unit */ #endif #ifdef CONFIG_PCI_PRI u16 pri_cap; /* PRI Capability offset */ u32 pri_reqs_alloc; /* Number of PRI requests allocated */ unsigned int pasid_required:1; /* PRG Response PASID Required */ #endif #ifdef CONFIG_PCI_PASID u16 pasid_cap; /* PASID Capability offset */ u16 pasid_features; #endif #ifdef CONFIG_PCI_P2PDMA struct pci_p2pdma __rcu *p2pdma; #endif #ifdef CONFIG_PCI_DOE struct xarray doe_mbs; /* Data Object Exchange mailboxes */ #endif u16 acs_cap; /* ACS Capability offset */ phys_addr_t rom; /* Physical address if not from BAR */ size_t romlen; /* Length if not from BAR */ /* * Driver name to force a match. Do not set directly, because core * frees it. Use driver_set_override() to set or clear it. */ const char *driver_override; unsigned long priv_flags; /* Private flags for the PCI driver */ /* These methods index pci_reset_fn_methods[] */ u8 reset_methods[PCI_NUM_RESET_METHODS]; /* In priority order */ };②ret =pcim_enable_device(pdev);
启动匹配中的pci设备(以下说明是关于启动的具体描述)
/** * pci_enable_device - Initialize device before it's used by a driver. * @dev: PCI device to be initialized * * Initialize device before it's used by a driver. Ask low-level code * to enable I/O and memory. Wake up the device if it was suspended. * Beware, this function can fail. * * Note we don't actually enable the device many times if we call * this function repeatedly (we just increment the count). */
③ret =pcim_iomap_regions(pdev, BIT(EDU_BAR), "edu_pci");
请求并映射 BAR0(就是把这个BAR0映射到内核的虚拟空间里边),后续通过 edu->bar0 访问 MMIO 寄存器。
/** * pcim_iomap_table - access iomap allocation table * @pdev: PCI device to access iomap table for * * Access iomap allocation table for @dev. If iomap table doesn't * exist and @pdev is managed, it will be allocated. All iomaps * recorded in the iomap table are automatically unmapped on driver * detach. * * This function might sleep when the table is first allocated but can * be safely called without context and guaranteed to succeed once * allocated. */
④ edu =devm_kzalloc(&pdev->dev, sizeof(*edu), GFP_KERNEL);
申请一段内存,用来存放我们自己定义的私有结构体struct edu_pci_dev,这段内存的生命周期 绑定 到 &pdev->dev 这个设备上, 函数返回新分配结构体的指针,保存到edu,这部分只是让内核帮我们托管这块内存,设备销毁时自动释放。
也就是开辟一块空白的内存地址,私有结构体edu指向这块空地,并且这个空地(内存)的生命周期和内核维护的&pdev->dev 这个设备是一样,这样的话,当pci core释放这个&pdev->dev 时候,内存也会被回收。
/** * devm_kmalloc - Resource-managed kmalloc * @dev: Device to allocate memory for * @size: Allocation size * @gfp: Allocation gfp flags * * Managed kmalloc. Memory allocated with this function is * automatically freed on driver detach. Like all other devres * resources, guaranteed alignment is unsigned long long. * * RETURNS: * Pointer to allocated memory on success, NULL on failure. */
⑤edu->bar0 =pcim_iomap_table(pdev)[EDU_BAR];
edu->bar0_len =pci_resource_len(pdev, EDU_BAR);就是把pci core发现的 匹配的硬件设备的相关信息(信息存放在内核)给我们用户自定义的驱动部分
⑥pci_set_drvdata(pdev, edu);
实现用户定义的私有结构体与内核通用的pci结构体的绑定,
- 内核负责通用部分:设备发现、资源分配、总线管理、生命周期管理
- 驱动负责私有部分:设备寄存器操作、功能实现、用户接口
这块也是Linux内核哲学的一部分
本来一个简单的思路是,自定义的驱动程序,直接把结构暴露出来(比如基地址+offset之类的,用户直接读写该位置上的数据,即可完成对外设的交互访问),
但是真实的情况,往往更复杂,要考虑到诸如并发,生命周期管理,热插拔等等,这些不是说用户自定义搞不了,只是自己单独搞很复杂,也容易乱。
所以内核设计了一套规范,自己写的驱动只是在这套规范上,删删改改,这样,只用专注实现自己相加的功能,借助于这个pci子系统,可以很方便的完成设备发现,热插拔,并发等等问题,代价是所有的这些个非事务层的处理(内存空间申请、probe函数执行、remove函数执行等等),都要向内核进程报告,最终的执行命令也是由子系统去下发的。并且还需要把自己的变量与子系统的那边的变量进行绑定。
也就是
由 “用户”《==》“自定义驱动”
变成(插入了第三者,第三者pci负责一些宏观上的调控,诸如资源分配,并发冲突等)
“用户”《==》"PCI子系统"《==》“自定义驱动”
/* * probe 路径: * PCI core 发现 1234:11e8 与 edu_pci_ids 匹配后调用这里。 */ static int edu_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct edu_pci_dev *edu; resource_size_t start; int ret; u32 id_value; dev_info(&pdev->dev, "probe vendor=%04x device=%04x\n", pdev->vendor, pdev->device); /* 1. 启用 PCI 设备,允许访问 BAR 资源。 */ ret = pcim_enable_device(pdev); if (ret) { dev_err(&pdev->dev, "pcim_enable_device failed: %d\n", ret); return ret; } /* 2. 请求并映射 BAR0,后续通过 edu->bar0 访问 MMIO 寄存器。 */ ret = pcim_iomap_regions(pdev, BIT(EDU_BAR), "edu_pci"); if (ret) { dev_err(&pdev->dev, "pcim_iomap_regions BAR%d failed: %d\n", EDU_BAR, ret); return ret; } /* 3. 分配本设备私有结构,生命周期绑定到 pdev->dev。 */ edu = devm_kzalloc(&pdev->dev, sizeof(*edu), GFP_KERNEL); if (!edu) return -ENOMEM; edu->pdev = pdev; /* 4. 从 pcim_iomap_table 中取出 BAR0 的映射地址。 */ edu->bar0 = pcim_iomap_table(pdev)[EDU_BAR]; if (!edu->bar0) return -ENOMEM; /* 5. 保存 BAR0 长度,供 ioctl offset 合法性检查使用。 */ edu->bar0_len = pci_resource_len(pdev, EDU_BAR); if (edu->bar0_len < EDU_MMIO_SIZE) { dev_err(&pdev->dev, "BAR%d too small: %pa\n", EDU_BAR, &edu->bar0_len); return -ENODEV; } /* 6. 关联 pdev 和 edu,remove/debug 等路径可通过 pci_get_drvdata 取回。 */ pci_set_drvdata(pdev, edu); return 0; }1.1.1.4 剩下的就是remove函数
用户在shell中输入这个rmmod edu_pci时候,这个是移除驱动的指令,会先通知到pci子系统,这个时候pci子系统就会直接执行删除驱动的能力(包括一些之前probe函数申请的内存空间)都可以很方便的交给内核去释放,然后子系统还会下放执行用户自定义的移除驱动函数,
在这里定义成了直接打印一个remove就行
static void edu_pci_remove(struct pci_dev *pdev) { dev_info(&pdev->dev, "remove\n"); }1.1.2 字符设备驱动骨架
1.1.2.1 宏定义
/* 当前先暴露两个最基础寄存器:ID 寄存器和 liveness 测试寄存器。 */ #define EDU_REG_ID 0x00 #define EDU_REG_LIVENESS 0x04 /* 最多支持 256 个 edu 设备实例,对应 /dev/edu0 ~ /dev/edu255。 */ #define EDU_MAX_DEVICES 256Linux中的ioctl是个组合字段 ,bit 31-30: 数据传输方向(dir) bit 29-16: 数据大小(size) bit 15-8: 魔数(type/magic) bit 7-0: 命令序号(nr)
| 字段 | 含义 | 作用 |
|---|---|---|
| dir | 数据传输方向 | 告诉内核是用户态写数据到内核,还是内核读数据给用户态 |
| size | 数据大小 | 告诉内核这次 ioctl 传输的数据块大小(字节) |
| type | 魔数(Magic Number) | 用来区分不同设备的命令集,避免冲突 |
| nr | 命令序号 | 同一个设备的不同命令的编号 |
①魔数定义:EDU_IOC_MAGIC 'e
#define EDU_IOC_MAGIC 'e'作用:这是 EDU 设备 ioctl 命令集的唯一标识。
- 用一个字符(ASCII 码)作为魔数,是 Linux 驱动的标准做法
- 每个设备的魔数应该是唯一的,避免和其他设备的 ioctl 命令冲突
- 这里用
'e'是因为 "edu" 的首字母,非常直观
为什么需要魔数?如果没有魔数,你可能会不小心给两个不同的设备定义了相同的命令号,导致内核把发给 A 设备的命令错误地交给 B 设备处理,引发严重问题。魔数就是用来解决这个冲突的。
② 读 ID 命令:EDU_IOC_GET_ID
#define EDU_IOC_GET_ID _IOR(EDU_IOC_MAGIC, 0x00, __u32)拆解:
_IOR:表示这是一个读命令,数据从内核流向用户态EDU_IOC_MAGIC:魔数'e'0x00:命令序号 0__u32:数据大小是 4 字节(32 位无符号整数)
功能:用户态调用这个 ioctl,内核会把 EDU 设备的 ID 寄存器值(4 字节)返回给用户态。
③读寄存器命令:EDU_IOC_REG_READ
#define EDU_IOC_REG_READ _IOWR(EDU_IOC_MAGIC, 0x01, struct edu_ioc_reg)拆解:
_IOWR:表示这是一个双向传输命令- 先写:用户态把要读的寄存器偏移量传给内核
- 后读:内核把寄存器的值返回给用户态
EDU_IOC_MAGIC:魔数'e'0x01:命令序号 1struct edu_ioc_reg:数据大小是这个结构体的大小(8 字节)
为什么用 _IOWR?因为读寄存器需要两个步骤:
- 用户态告诉内核:"我要读偏移量为 X 的寄存器"
- 内核告诉用户态:"偏移量 X 的寄存器值是 Y"
这是一个先写后读的双向过程,所以必须用_IOWR。
④写寄存器命令:EDU_IOC_REG_WRITE
#define EDU_IOC_REG_WRITE _IOW(EDU_IOC_MAGIC, 0x02, struct edu_ioc_reg)拆解:
_IOW:表示这是一个写命令,数据从用户态流向内核EDU_IOC_MAGIC:魔数'e'0x02:命令序号 2struct edu_ioc_reg:数据大小是 8 字节
功能:用户态把要写的寄存器偏移量和值传给内核,内核把值写入对应的寄存器。
/* * 字符设备 ioctl 命令定义。 * * 目前只做最小可用接口: * - EDU_IOC_GET_ID:读取 edu 设备 ID 寄存器。 * - EDU_IOC_REG_READ:读取任意 32-bit 对齐的 BAR0 寄存器。 * - EDU_IOC_REG_WRITE:写入任意 32-bit 对齐的 BAR0 寄存器。 */ #define EDU_IOC_MAGIC 'e' #define EDU_IOC_GET_ID _IOR(EDU_IOC_MAGIC, 0x00, __u32) #define EDU_IOC_REG_READ _IOWR(EDU_IOC_MAGIC, 0x01, struct edu_ioc_reg) #define EDU_IOC_REG_WRITE _IOW(EDU_IOC_MAGIC, 0x02, struct edu_ioc_reg) /* 用户态通过 ioctl 传入/传出的寄存器访问参数。 */ struct edu_ioc_reg { __u32 offset; __u32 value; };1.1.2.2 成员变量的声明
instance的作用是在多个edu设备下进行区分,当插入多个 EDU 设备时,内核会为每个设备调用一次edu_pci_probe函数。如果没有instance,所有设备都会尝试创建/dev/edu0,导致冲突,只有第一个设备能成功注册。
使用内核提供的IDA(ID Allocator)机制来分配instance
misc_name[32]是个char类型的数组,用来存储设备节点的名字,比如有的叫edu0,有的叫edu22,32的尺寸大小是一个冗余设计。
struct edu_pci_dev { struct pci_dev *pdev; void __iomem *bar0; resource_size_t bar0_len; int instance; struct miscdevice miscdev; char misc_name[32]; }; /* ida 用来给多个 edu 设备分配稳定的小编号:edu0、edu1、... */ static DEFINE_IDA(edu_ida);1.1.2.3对字符设备的open\read\write\ioctl
对之前暴露出来的寄存器 进行读写以及ioctl都是差不多的
①应该先open的,然后再进行其余三者的操作
用户态:open("/dev/edu0", O_RDWR) ↓ 内核VFS层:创建 struct file 对象 ↓ VFS层调用 misc 驱动的 open 函数 ↓ 进入你的 edu_open 函数依旧是用户态发指令,指令会先上传到字符设备驱动的子系统中,经系统的处理(此处子系统会创建一个file类型的通用变量),再下发执行open的指令到自定义的驱动,进行具体事务的处理,在这里我们的具体事务是将自定义的edu_pci_dev类型的变量edu与file中private_data进行绑定处理。
绑定完之后VFS(虚拟文件系统)可以对edu直接进行操作的
static int edu_open(struct inode *inode, struct file *file) { // 1. 从 file->private_data 拿到 miscdevice 指针 // (这是 misc 驱动框架提前帮你设置好的) struct miscdevice *miscdev = file->private_data; // 2. 用 container_of 从 miscdev 找回我们的 edu_pci_dev struct edu_pci_dev *edu = container_of(miscdev, struct edu_pci_dev, miscdev); // 3. ✅ 关键!把 edu 指针赋值给 file->private_data file->private_data = edu; return 0; }②read write ioctl
整体都是差不多逻辑的,都是先通过file->private_data获取edu设备的,然后对edu设备上,指定位置的数据进行读取或者是写入,当然也有一些越界写入,边界检查等处理逻辑。
/* ida 用来给多个 edu 设备分配稳定的小编号:edu0、edu1、... */ static DEFINE_IDA(edu_ida); /* MMIO 读写小封装,避免到处直接写 edu->bar0 + offset。 */ static inline u32 edu_readl(struct edu_pci_dev *edu, u32 offset) { return ioread32(edu->bar0 + offset); } static inline void edu_writel(struct edu_pci_dev *edu, u32 offset, u32 value) { iowrite32(value, edu->bar0 + offset); } /* ioctl 允许用户指定 offset,因此这里统一检查越界和 32-bit 对齐。 */ static bool edu_reg_offset_valid(struct edu_pci_dev *edu, u32 offset) { return offset <= edu->bar0_len - sizeof(u32) && IS_ALIGNED(offset, sizeof(u32)); } /* * open 路径: * misc 框架进入 open 时,file->private_data 默认指向 struct miscdevice。 * 这里用 container_of 找回外层 edu_pci_dev,后续 read/write/ioctl 直接使用。 */ static int edu_open(struct inode *inode, struct file *file) { struct miscdevice *miscdev = file->private_data; struct edu_pci_dev *edu = container_of(miscdev, struct edu_pci_dev, miscdev); file->private_data = edu; return 0; } /* * read 路径: * 让用户可以直接 cat /dev/edu0,看到 ID 和 liveness 寄存器当前值。 */ static ssize_t edu_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { struct edu_pci_dev *edu = file->private_data; char text[96]; int len; len = scnprintf(text, sizeof(text), "id=0x%08x liveness=0x%08x\n", edu_readl(edu, EDU_REG_ID), edu_readl(edu, EDU_REG_LIVENESS)); return simple_read_from_buffer(buf, count, ppos, text, len); } /* * write 路径: * 用户 echo 一个数字到 /dev/edu0 时,把这个值写入 liveness 寄存器。 */ static ssize_t edu_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { struct edu_pci_dev *edu = file->private_data; char text[32]; unsigned long value; int ret; if (count >= sizeof(text)) return -EINVAL; if (copy_from_user(text, buf, count)) return -EFAULT; text[count] = '\0'; ret = kstrtoul(text, 0, &value); if (ret) return ret; edu_writel(edu, EDU_REG_LIVENESS, (u32)value); return count; } /* * ioctl 路径: * 给 C 测试程序使用,比 read/write 更适合表达“读/写某个寄存器”。 */ static long edu_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct edu_pci_dev *edu = file->private_data; struct edu_ioc_reg reg; __u32 value; if (_IOC_TYPE(cmd) != EDU_IOC_MAGIC) return -ENOTTY; switch (cmd) { case EDU_IOC_GET_ID: value = edu_readl(edu, EDU_REG_ID); if (copy_to_user((void __user *)arg, &value, sizeof(value))) return -EFAULT; return 0; case EDU_IOC_REG_READ: if (copy_from_user(®, (void __user *)arg, sizeof(reg))) return -EFAULT; if (!edu_reg_offset_valid(edu, reg.offset)) return -EINVAL; reg.value = edu_readl(edu, reg.offset); if (copy_to_user((void __user *)arg, ®, sizeof(reg))) return -EFAULT; return 0; case EDU_IOC_REG_WRITE: if (copy_from_user(®, (void __user *)arg, sizeof(reg))) return -EFAULT; if (!edu_reg_offset_valid(edu, reg.offset)) return -EINVAL; edu_writel(edu, reg.offset, reg.value); return 0; default: return -ENOTTY; } }1.1.2.4 probe函数中对字符设备的注册
/* /dev/eduN 支持的文件操作集合。 */ static const struct file_operations edu_fops = { .owner = THIS_MODULE, .open = edu_open, .read = edu_read, .write = edu_write, .unlocked_ioctl = edu_ioctl, .llseek = no_llseek, };start = pci_resource_start(pdev, EDU_BAR); dev_info(&pdev->dev, "BAR%d start=%pa len=%pa mapped=%p\n", EDU_BAR, &start, &edu->bar0_len, edu->bar0); id_value = edu_readl(edu, EDU_REG_ID); dev_info(&pdev->dev, "MMIO[0x00]=0x%08x\n", id_value); edu_writel(edu, EDU_REG_LIVENESS, 0xa5a5a5a5); dev_info(&pdev->dev, "MMIO[0x04]=0x%08x\n", edu_readl(edu, EDU_REG_LIVENESS)); /* 8. 分配字符设备实例编号,单设备时通常就是 0,对应 /dev/edu0。 */ ret = ida_alloc_max(&edu_ida, EDU_MAX_DEVICES - 1, GFP_KERNEL); if (ret < 0) return ret; edu->instance = ret; /* 9. 注册 misc 字符设备;成功后 devtmpfs 会创建 /dev/eduN。 */ snprintf(edu->misc_name, sizeof(edu->misc_name), "edu%d", edu->instance); edu->miscdev.minor = MISC_DYNAMIC_MINOR; edu->miscdev.name = edu->misc_name; edu->miscdev.fops = &edu_fops; edu->miscdev.parent = &pdev->dev; ret = misc_register(&edu->miscdev); if (ret) { //注册失败时释放实例编号,避免下次 probe 时分配到同一个编号导致 /dev/eduN 冲突。 dev_err(&pdev->dev, "misc_register failed: %d\n", ret); ida_free(&edu_ida, edu->instance); return ret; } dev_info(&pdev->dev, "registered /dev/%s\n", edu->misc_name);1.2 字符设备驱动成功效果
字符设备注册:
读写测试:
补充:QEMU edu 设备的 liveness 寄存器行为:写入一个值后,读回来会返回按位取反的结果,所以 0x0000ffff变成0xffff0000,这正好说明 MMIO 写入确实到达了虚拟设备。