1:前置知识

1.1 BAR

        BAR(base address register) 基地址空间寄存器,查看数据手册,就可以发现其共有6个BAR,最后两个是保留状态,前四个bar投入使用,每个bar在网卡设备上都是4Bytes,只有高4到31位的位置才真正有用。


        下边的截图可以看到关于各个BAR description:

映射窗口 对应 BAR 功能描述
Memory BAR 0 BAR0 所有内部寄存器和内存都通过这个 BAR 访问,支持字节、字、双字访问。这是驱动唯一需要关心的 BAR
Flash BAR 1 BAR1 访问网卡外部的 Flash 闪存,用于固件更新和配置存储。普通网卡驱动不需要使用。
I/O BAR 2 BAR2 所有内部寄存器、内存和 Flash 都可以通过 I/O 端口访问。这是遗留接口,现代驱动完全不使用。
MSI-X BAR 3 BAR3 MSI-X 中断表和 PBA 数组所在的空间。内核会自动映射这个 BAR 用于 MSI-X 中断处理,驱动不需要手动映射


关于这个BAR的大小的描述:(BAR0的128KB是映射之后的物理内存的大小)

1.2 e1000e寄存器

        为了验证这个驱动是能够正常的访问到硬件设备的,可以通过对数据手册中的两个寄存器当中的数据进行读取来进行验证。

        关于这个偏移有一点比较容易搞混

第一种偏移:

特性 PCI 配置空间
空间大小 固定 256 字节(PCIe 扩展到 4KB)
访问方式 只能通过pci_read_config_*/pci_write_config_*函数访问
偏移范围 0x00 ~ 0xFF(基础部分)
谁定义的 PCI/PCIe 总线规范,所有 PCIe 设备都必须遵循
作用 设备枚举、资源分配、基本控制

第二种偏移:

        就是数据手册datasheet展示的register summary table

特性 MMIO 寄存器空间
空间大小 设备自定义(82574 是 128KB)
访问方式 只能通过ioread32/iowrite32函数访问
偏移范围 0x00000 ~ 0x1FFFF(82574)
谁定义的 设备厂商(Intel),每个设备都不一样
作用 设备的具体功能控制(发送、接收、中断等)

2:基本的驱动框架实现

2.0h文件的定义

/* 头文件保护宏的命名规则是文件名全大写_后缀全大写,用下划线代替点号 */
#ifndef E1000E_PCI_H
#define E1000E_PCI_H

/* QEMU e1000e 对应 Intel 82574L,PCI ID 为 8086:10d3。 */
#define E1000E_VENDOR_ID 0x8086
#define E1000E_DEVICE_ID 0x10d3

/* e1000e BAR0 是 128 KB MMIO 空间,本阶段只映射 BAR0。 */
#define E1000E_BAR 0
#define E1000E_MMIO_SIZE 0x20000

/* 先只读安全寄存器,验证 MMIO 路径已经打通。 */
#define E1000E_REG_CTRL 0x00000
#define E1000E_REG_STATUS 0x00008

#endif /* E1000E_PCI_H */

         关于条件编译:代码描述的是如果没有引用e1000e_pci.h的话,就自动引用一次,等到下次其他文件再次引用的时候,这个判断条件就为false,所以不用编译 条件编译之间的任何事情,这样可以避免重复编译。

        如果没有这个保护,当多个 C 文件同时包含e1000e_pci.h,或者一个 C 文件间接包含了它多次时,编译错误:redefinition of 'struct e1000_adapter',结构体、宏、函数声明被重复定义

2.1自定义的驱动的结构体:

struct e1000e_pci_dev {
	struct pci_dev *pdev;
	void __iomem *bar0;
	resource_size_t bar0_start;
	resource_size_t bar0_len;
};

2.2 注册表匹配

        关键点是在于Module_DEVICE_TABLE,他会把这个注册表交与pci子系统进行处理。子系统根据这个表与之前开机时候枚举到的设备进行匹配。

/* PCI 匹配表:告诉 PCI core 本驱动支持 8086:10d3 的设备。 */
static const struct pci_device_id e1000e_pci_ids[] = {
	{ PCI_DEVICE(E1000E_VENDOR_ID, E1000E_DEVICE_ID) },
	{ }
};
MODULE_DEVICE_TABLE(pci, e1000e_pci_ids);

2.3 probe函数

https://blog.csdn.net/qq_63125683/article/details/161117211?spm=1011.2124.3001.6209

基本的流程与之前讲的差不多。


补充一点关于:

    e1000e->bar0_start = pci_resource_start(pdev, E1000E_BAR);

    e1000e->bar0_len = pci_resource_len(pdev, E1000E_BAR);

        这两行的含义是,pci子系统查询早在上电开机之后,子系统枚举硬件的时候,就给这个网卡硬件分配好了这个物理地址,以及相应的这个物理内存的大小,这两行只是告诉子系统,把之前的分配好的数据,现在再告诉用户自定义的驱动一次。


再补充一点关于地址的内容:

地址名称 含义 类型 谁分配的
e1000e->bar0_start BAR0 在CPU 物理地址空间中的起始地址 物理地址 BIOS / 内核 PCI 子系统
e1000e->bar0 BAR0 在内核虚拟地址空间中的起始地址 虚拟地址 内核ioremap函数
寄存器偏移 寄存器在82574 内部 MMIO 空间中的偏移量 相对地址 Intel 硬件设计

CPU 物理地址空间是 CPU 能够直接寻址的所有地址的集合,RAM 只是这个空间中的一部分,CPU物理地址≠RAM地址

82574内部MMIO空间
┌─────────────────────┐
│ 0x00000: CTRL       │ ← 寄存器偏移
│ 0x00008: STATUS     │
│ ...                 │
│ 0x5400: RAL         │
│ 0x5404: RAH         │
│ ...                 │
│ 0x1FFFF: 最后一个寄存器 │
└─────────────────────┘
         ↓
         │ 硬件通过PCIe总线映射到
         ↓
CPU物理地址空间
┌─────────────────────┐
│ 0xfe280000: CTRL    │ ← e1000e->bar0_start = 0xfe280000
│ 0xfe280008: STATUS  │
│ ...                 │
│ 0xfe29ffff: 最后一个寄存器 │
└─────────────────────┘
         ↓
         │ 内核通过ioremap映射到
         ↓
内核虚拟地址空间
┌─────────────────────┐
│ 0xffffc90000c00000: CTRL │ ← e1000e->bar0 = 0xffffc90000c00000
│ 0xffffc90000c00008: STATUS │
│ ...                 │
│ 0xffffc90000c1ffff: 最后一个寄存器 │
└─────────────────────┘

关于这个CPU物理地址空间:

物理地址绝对不是虚拟的,它是CPU 芯片上真实存在的物理引脚输出的电信号。x86机器上有48根地址总线。

✅ 当 CPU 访问物理地址0xfe280000时,内存条根本不会响应,因为这个地址不在它的监听范围内。

✅ 响应这个请求的是82574 网卡芯片,它会把自己内部 CTRL 寄存器的值放到数据总线上返回给 CPU。

CPU物理地址空间 (48位)
┌─────────────────────────────────┐ 0xFFFFFFFFFFFFFFFF
│  PCIe MMIO区域                  │
│  ├─ 82574网卡BAR0: 0xfe280000  │ ← 网卡芯片监听这个地址
│  ├─ 显卡帧缓冲区: 0xf0000000    │ ← 显卡芯片监听这个地址
│  └─ APIC中断控制器: 0xfee00000  │ ← APIC芯片监听这个地址
├─────────────────────────────────┤ 0x100000000 (4GB)
│  BIOS ROM区域                   │ ← 主板Flash芯片监听这个地址
├─────────────────────────────────┤ 0x00100000 (1MB)
│  系统RAM区域                    │ ← 内存条监听这个地址
│  ├─ 内核代码段                  │
│  ├─ 内核数据段                  │
│  └─ 用户进程内存                │
├─────────────────────────────────┤ 0x00000000

关于这个内核虚拟地址空间:

如果没有虚拟地址,所有程序都直接使用物理地址,会有三个致命问题:

  1. 内存冲突:两个程序如果访问同一个物理地址,会互相覆盖数据
  2. 内存碎片化:程序频繁申请释放内存,会导致物理内存变得支离破碎
  3. 安全问题:恶意程序可以直接访问任意物理地址,修改内核数据

虚拟地址解决了所有这些问题:

  • 每个进程都有自己独立的虚拟地址空间,互相隔离
  • 操作系统可以把不连续的物理内存映射成连续的虚拟内存
  • 操作系统可以通过页表权限位,限制程序对内存的访问

所以;

1. e1000e->bar0_start = 0xfe280000

  • 这是一个物理地址
  • 它对应 CPU 地址引脚上输出0xfe280000这个电信号
  • 当这个信号出现在地址总线上时,82574 网卡芯片会响应
  • 内核不能直接用这个地址访问网卡,因为内核运行在虚拟地址模式下

2. e1000e->bar0 = 0xffffc90000c00000

  • 这是一个内核虚拟地址
  • 它是操作系统为了访问物理地址0xfe280000而创建的一个别名
  • 内核可以直接用这个地址访问网卡
  • 当内核访问这个虚拟地址时,MMU 会自动把它转换成物理地址0xfe280000

完整probe函数

static int e1000e_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
	struct e1000e_pci_dev *e1000e;
	u32 ctrl;
	u32 status;
	int ret;

	dev_info(&pdev->dev, "probe vendor=%04x device=%04x\n",
		 pdev->vendor, pdev->device);

	ret = pcim_enable_device(pdev);
	if (ret) {
		dev_err(&pdev->dev, "pcim_enable_device failed: %d\n", ret);
		return ret;
	}

	e1000e = devm_kzalloc(&pdev->dev, sizeof(*e1000e), GFP_KERNEL);
	if (!e1000e)
		return -ENOMEM;

	e1000e->pdev = pdev;
	e1000e->bar0_start = pci_resource_start(pdev, E1000E_BAR);
	e1000e->bar0_len = pci_resource_len(pdev, E1000E_BAR);

	if (e1000e->bar0_len < E1000E_MMIO_SIZE) {
		dev_err(&pdev->dev, "BAR%d too small: %pa, expected at least %#x\n",
			E1000E_BAR, &e1000e->bar0_len, E1000E_MMIO_SIZE);
		return -ENODEV;
	}

	ret = pcim_iomap_regions(pdev, BIT(E1000E_BAR), "e1000e_pci");
	if (ret) {
		dev_err(&pdev->dev, "pcim_iomap_regions BAR%d failed: %d\n",
			E1000E_BAR, ret);
		return ret;
	}

	e1000e->bar0 = pcim_iomap_table(pdev)[E1000E_BAR];
	if (!e1000e->bar0)
		return -ENOMEM;

	pci_set_drvdata(pdev, e1000e);
	pci_set_master(pdev);

	ctrl = e1000e_readl(e1000e, E1000E_REG_CTRL);
	status = e1000e_readl(e1000e, E1000E_REG_STATUS);

	dev_info(&pdev->dev, "BAR%d start=%pa len=%pa mapped=%p\n",
		 E1000E_BAR, &e1000e->bar0_start, &e1000e->bar0_len,
		 e1000e->bar0);
	dev_info(&pdev->dev, "CTRL=0x%08x STATUS=0x%08x\n", ctrl, status);

	return 0;
}

2.4 remove函数:

        之前申请内核资源的时候很多都是用的带m标志的函数,诸如pcim_enable_device、devm_kzalloc、pcim_iomap_regions这些都是可以在自定义的驱动的生命周期结束的时候,自动回收的,不需要手动去释放什么资源。

        同时需要补充的是devm_*pcim_*系列函数只能自动释放 "内核内存类资源",不能自动处理 "硬件状态类操作" 和 "内核子系统注册类操作"。

        而 我们在remove中,需要手动进行消除的这两个函数恰巧是关于这个硬件状态类的,pci_set_master(pdev); pci_clear_master(pdev);这一对说明的是这个PCI设备是否可以当作master,从而对其他设备发起读写的申请。允许这个 PCI 设备作为总线主设备,主动发起 DMA 传输,读写系统内存。

        第二个则是为了避免野指针的出现,pci_set_drvdata的本质,是把本地自定义的e1000e_pci_dev结构体的地址,给了这个内核维护的通用pci设备的一个特殊字段里边(特殊在可以接受任何类型的数据),要是不主动清零的话,会变成野指针,指不定啥时候出现问题(感觉这里是内核设计者没有考虑到,或者是由于其他的一些缘由故意设计的)

static void e1000e_pci_remove(struct pci_dev *pdev)
{
	pci_clear_master(pdev);
	pci_set_drvdata(pdev, NULL);
	dev_info(&pdev->dev, "remove\n");
}

2.5解读输出:

[   38.866971] e1000e_pci: loading out-of-tree module taints kernel.
[   38.867314] e1000e_pci: module verification failed: signature and/or required key missing - tainting kernel
[   38.875451] e1000e_pci 0000:02:00.0: probe vendor=8086 device=10d3
[   38.878110] e1000e_pci 0000:02:00.0: BAR0 start=0x00000000fe280000 len=0x0000000000020000 mapped=(____ptrval____)
[   38.878441] e1000e_pci 0000:02:00.0: CTRL=0x18140241 STATUS=0x00080283

正常打印vendor ID&device ID这个是在probe函数中定义的

输出关于BAR0 start与len也是符合预期的(len转换一下就是128Kb)

CTRL & STATUS可以通过转化一下进制解读出具体的含义

其BDF也可以表明 该网卡是挂载在bus2上的,也就是root complex引出的第二个root port上挂载的设备。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐