Linux 驱动研究 —— SPI (2)

1. 内核层引言

 在上一篇文章中,我们从系统架构维度梳理了 Linux SPI 子系统的框架图,对整体的数据流向与分层机制建立了宏观认知。站在架构基础上,对研究路线进行了梳理:


匹配(总线) → \rightarrow → 传输(标准化API) → \rightarrow → 解耦(接口/主控层) → \rightarrow → 物理控制(硬件) → \rightarrow → 用户交互

内核层(抽象层 -> 接口/主控层 -> 打通):这是最难也最核心的部分。通过 spi_register_controller 和 spi_register_driver 将这三者打通,掌握 Linux 驱动模型的解耦哲学(即控制器驱动只管传输,设备驱动只管协议,核心层负责撮合)。

硬件层(内核控制硬件):这是验证你理解的环节。研究 spi_transfer_one_message 或 transfer_one 回调函数,看看数据是如何从内核缓冲区流向硬件寄存器的,这会让你对"SPI 传输"不再有疑惑。

用户层(顶层控制):这是终点。研究 spidev.c 字符驱动,看用户空间的 ioctl 是如何通过抽象层到达控制器驱动的。


本篇将深入代码内核层,拆解 SPI 子系统各模块的执行逻辑。

 驱动的开发往往遵循'总线---设备---驱动'的模型,而一切的起点,在于如何在内核中确立 SPI 的专属协议总线。今天,我们从 SPI 子系统的初始化入口开始。"

 在 /drivers/spi/spi.c 中,我们找到创建总线的代码,搜索 'bus_register' 并且在其之后执行 dump_stack 跟踪其调用流程。

c 复制代码
status = bus_register(&spi_bus_type);
pr_info("KD_LOG: test the function: dump_stack");
pr_info("KD_LOG: here is dump_stack");
dump_stack();

2. 总线创建流程(抽象层)

启动开发板输入:

dmesg | grep "KD_LOG: test*" -A 20

截取 bus_register 相关的部分:

复制代码
[    0.308448] KD_LOG: test the function: dump_stack
[    0.308467] KD_LOG: here is dump_stack
[    0.308500] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.9.88 #4
[    0.308512] Hardware name: Freescale i.MX6 UltraLite (Device Tree)
[    0.308578] [<80112a34>] (unwind_backtrace) from [<8010dc2c>] (show_stack+0x20/0x24)
[    0.308616] [<8010dc2c>] (show_stack) from [<80469964>] (dump_stack+0x80/0x94)
[    0.308653] [<80469964>] (dump_stack) from [<8115edd8>] (spi_init+0x70/0xc4)
[    0.308686] [<8115edd8>] (spi_init) from [<80101cc4>] (do_one_initcall+0x54/0x180)
[    0.308721] [<80101cc4>] (do_one_initcall) from [<81100e9c>] (kernel_init_freeable+0x16c/0x204)
[    0.308759] [<81100e9c>] (kernel_init_freeable) from [<80b7e868>] (kernel_init+0x18/0x120)
[    0.308796] [<80b7e868>] (kernel_init) from [<80109350>] (ret_from_fork+0x14/0x24)

通过这部分打印信息,我们可以分析得到 SPI 注册总线的调用链,通过倒序调用分析可以得到如下关系:

ret_from_fork → kernel_init → kernel_init_freeable → do_one_initcall → spi_init

通过 dump_stack() 的输出,我们清晰地看到整个 SPI 子系统的注册逻辑起始于 spi_init,深入 spi_init 函数内部进行分析。

在 /drivers/spi/spi.c 搜索 ' spi_init ' ,在文件末尾可以发现如下代码:

c 复制代码
postcore_initcall(spi_init);

postcore_initcall 是 Linux 内核中用于控制模块初始化顺序的宏。它是内核初始化机制(Initcall 机制)的一部分。

c 复制代码
static int __init spi_init(void)
{
	int	status;

	buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
	if (!buf) {
		status = -ENOMEM;
		goto err0;
	}

	status = bus_register(&spi_bus_type);
	pr_info("KD_LOG: test the function: dump_stack");
	pr_info("KD_LOG: here is dump_stack");
	dump_stack();
	pr_info("KD_LOG: here is fstrace");
	if (status < 0)
		goto err1;

	status = class_register(&spi_master_class);
	if (status < 0)
		goto err2;

	if (IS_ENABLED(CONFIG_OF_DYNAMIC))
		WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));
	if (IS_ENABLED(CONFIG_ACPI))
		WARN_ON(acpi_reconfig_notifier_register(&spi_acpi_notifier));

	return 0;

err2:
	bus_unregister(&spi_bus_type);
err1:
	kfree(buf);
	buf = NULL;
err0:
	return status;
}

从这段代码中我们可以看到 spi_init 内容的执行过程:

复制代码
spi_init (SPI 子系统初始化入口)
|
|__ 内存准备: kmalloc (为核心层分配公共传输缓冲区)
|
|__ 构建总线骨架: bus_register (注册 SPI 总线,定义匹配与管理规则)
|
|__ 建立设备类: class_register (在 /sys/class 下创建节点,实现控制器可视化)

既然注册了总线,那么我们来看一下总线类型都定义了些什么内容:


c 复制代码
struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};

2.1 .name = "spi"

当内核启动并加载 SPI 核心驱动后,会看到这个目录 /sys/bus/spi/。所有的 SPI 设备和驱动都会挂载在这个目录下。可以通过 ls /sys/bus/spi/devices 和 ls /sys/bus/spi/drivers 验证这一点。

复制代码
[root@100ask:/sys/bus]# ls
clockevents  event_source  iio       platform  serio  usb-serial
clocksource  gpio          mdio_bus  rpmsg     soc    virtio
container    hid           mmc       scsi      spi    workqueue
cpu          i2c           nvmem     sdio      usb

2.2 .dev_groups = spi_dev_groups

它定义了在 /sys/bus/spi/devices/xxx/ 下会显示哪些属性文件(如 modalias、bus 等)。这些属性文件是用户空间调试驱动的"窗口"。

2.3 .match = spi_match_device

这是 bus_register 之后,内核最频繁调用的函数。每当有新的驱动被加载(spi_register_driver)或新的设备被发现(spi_add_device)时,总线就会调用这个函数来寻找是否有对应的设备/驱动相匹配。

接下来分析这个匹配过程:

c 复制代码
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
	const struct spi_device	*spi = to_spi_device(dev);
	const struct spi_driver	*sdrv = to_spi_driver(drv);

	/* Attempt an OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then try ACPI */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	if (sdrv->id_table)
		return !!spi_match_id(sdrv->id_table, spi);

	return strcmp(spi->modalias, drv->name) == 0;
}

2.3.1 通用类型到专有类型的转化

我们先观察这个函数传进来的参数,和内部的前两行代码。

参数为: struct device *dev 与 struct device_driver *drv

前两行代码:to_spi_device(dev) 与 to_spi_driver(drv)

我们可以得知,这个函数先将传递进来的通用设备和驱动类型转化为专用的SPI类型

c 复制代码
static inline struct spi_device *to_spi_device(struct device *dev)
{
	return dev ? container_of(dev, struct spi_device, dev) : NULL;
}

这行代码使用了 C 语言的三元运算符

条件 ? 结果为真 : 结果为假

条件 dev ?:检查传入的指针 dev 是否为 NULL。这是内核开发中的"防御性编程",防止空指针解引用导致内核崩溃(Panic)。

结果为真 container_of(...):如果 dev 有效,执行核心转换宏 container_of。

结果为假 : NULL:如果 dev 本身就是空,直接返回 NULL。

所以现在我们要分析 container_of 具体做了什么事情,返回的值是什么。

以下是 struct spi_device 的内存布局:

c 复制代码
struct spi_device {
	struct device		dev;
	struct spi_master	*master;
	u32			max_speed_hz;
	u8			chip_select;
	u8			bits_per_word;
	u16			mode;
	...
	...
	...
	...
};

我们发现通用的设备类型包含在SPI设备类型之中,且是SPI设备类型的第一个成员


内存地址的重叠与语义的叠加

内存是"死的":

内存里仅仅是一块连续的字节序列。

结构体是"活的":

结构体定义告诉编译器如何去"解读"这块内存。

当你把 struct device 放在 struct spi_device 的第一个成员位置时,这块内存的起始地址(比如 0x1000)既是 struct device 的开头,也是 struct spi_device 的开头。

属性的叠加:对于 CPU 来说,它看到的是 0x1000 开始的字节流。但对于 C 编译器来说:

如果你用 struct device * 指针指向它,你只能看到 dev 的属性(比如 parent,kobj)。

如果你用 struct spi_device * 指针指向它,你不仅能看到 dev 的属性,还能顺着往下看,看到 chip_select 等 SPI 特有属性。

结论:这块地址本身并没有变,变的是你"看它的视角"。


接下来分析 container_of(dev, struct spi_device, dev) :

你可能会疑惑,既然地址是一样的,为什么还要用 container_of ?这是一个极其深刻的问题。

情况 A:地址是一样的(当 device 是第一个成员时)

如果 struct device 是 struct spi_device 的第一个成员,偏移量为 0,那么 container_of 做的减法运算其实就是 地址 - 0。在这种情况下,container_of 看起来确实有点多余,直接强制转换 (struct spi_device *)dev 也能达到目的。
情况 B:地址是不一样的(核心价值所在)

这是 container_of 真正存在的意义。在 Linux 内核中,一个结构体里可能会内嵌多个不同类型的 device 或其他基类成员,或者 device 不在第一个位置。

例如,假设你的代码逻辑变了:

c 复制代码
struct my_spi_controller {
  int id;
  struct device dev; // 此时 dev 不在第一个位置,偏移量不是 0
};

如果你只有 struct device *dev 这个指针,你如何找回 my_spi_controller 的起始地址?

你不能简单地做强制转换,因为 dev 的起始地址比结构体的起始地址大 sizeof(int) 个字节。

你必须执行:

地址 - 偏移量

container_of 的最终使命:

它保证了无论你的基类成员在结构体的哪个位置,只要你知道它是"哪种类型"里的"哪个成员",它就能帮你算出这个结构体真正的、最原始的起始地址。


2.3.2 匹配规则

从 spi_match_device 可知,匹配的优先级如下:

  1. OF 匹配(设备树匹配)
  2. ACPI 匹配
  3. ID 表 匹配
  4. 名字 匹配 (strcmp)

这里主要介绍第一优先级的匹配流程:

c 复制代码
if (of_driver_match_device(dev, drv))
		return 1;

进入of_driver_match_device

c 复制代码
static inline int of_driver_match_device(struct device *dev,
					 const struct device_driver *drv)
{
	return of_match_device(drv->of_match_table, dev) != NULL;
}

进入of_match_device

c 复制代码
/**
 * of_match_device - 判断设备是否与驱动的支持列表匹配
 * @matches: 指向驱动程序定义的 of_device_id 数组(支持列表)
 * @dev: 指向正在寻找驱动的设备结构体(硬件实体)
 */
const struct of_device_id *of_match_device(const struct of_device_id *matches,
					   const struct device *dev)
{
	/* * 第一步:安全性与前提检查
	 * 1. !matches: 如果驱动没有定义 of_match_table,则无法进行设备树匹配。
	 * 2. !dev->of_node: 如果设备不是从设备树中解析出来的(不含 DTS 节点信息),
	 * 则无法通过设备树路径进行匹配。
	 */
	if ((!matches) || (!dev->of_node))
		return NULL;

	/* * 第二步:执行核心匹配逻辑
	 * 剥离外部的 struct device,取出核心的 dev->of_node(设备树节点指针),
	 * 交给更底层的 of_match_node 函数进行详细的字符串比对。
	 */
	return of_match_node(matches, dev->of_node);
}

进入of_match_node

c 复制代码
/**
 * of_match_node - 判断一个设备树节点(device_node)是否在匹配表中有对应项
 *	@matches:  驱动提供的支持列表数组
 *	@node:     要进行匹配的设备树节点实体
 *
 *	这是用于设备匹配的底层工具函数。
 */
const struct of_device_id *of_match_node(const struct of_device_id *matches,
					 const struct device_node *node)
{
	const struct of_device_id *match;
	unsigned long flags;

	/* 1. 锁住整个设备树,并保存当前中断状态,防止匹配时数据被篡改 */
	raw_spin_lock_irqsave(&devtree_lock, flags);

	/* 2. 进入真正的"核心逻辑圈",进行循环比对 */
	match = __of_match_node(matches, node);

	/* 3. 解锁并恢复中断 */
	raw_spin_unlock_irqrestore(&devtree_lock, flags);

	return match;
}

进入__of_match_node

c 复制代码
/**
 * __of_match_node - (内部函数) 遍历匹配表,寻找与节点最匹配的项
 * @matches: 指向驱动支持列表数组的指针(即 ids 数组)
 * @node:    指向设备树节点的指针(硬件信息)
 * * 注意:调用此函数前必须已经获取了 devtree_lock 锁。
 */
static const struct of_device_id *__of_match_node(const struct of_device_id *matches,
						  const struct device_node *node)
{
	const struct of_device_id *best_match = NULL;
	int score, best_score = 0;

	/* 基础安全检查:如果驱动根本没写匹配表,直接返回 NULL */
	if (!matches)
		return NULL;

	/**
	 * 核心循环:遍历 matches 数组
	 * 判断条件:只要 name、type 或 compatible 任意一个不为空,说明还没到数组末尾。
	 * 这里的迭代(matches++)利用了 C 语言指针自增,每次向后移动一个结构体大小。
	 */
	for (; matches->name[0] || matches->type[0] || matches->compatible[0]; matches++) {
		
		/* 调用底层函数,计算当前项与硬件节点的"匹配得分" */
		score = __of_device_is_compatible(node, matches->compatible,
						  matches->type, matches->name);
		
		/**
		 * 优胜劣汰逻辑:
		 * 如果当前这一项的匹配得分比之前记录的最高分还要高,
		 * 则更新"最佳匹配"指针,并记录新的最高分。
		 */
		if (score > best_score) {
			best_match = matches;
			best_score = score;
		}
	}

	/* 返回得分最高的那一项(如果一项都没匹配上,则返回 NULL) */
	return best_match;
}
  1. 为什么 compatible 的顺序很重要?
    设备树节点通常会列出多个 compatible 字符串,例如:
    compatible = "fsl,imx6q-uart", "arm,pl011";
    第一个是具体型号(得分高)。
    第二个是通用型号(得分低)。
    __of_device_is_compatible 会根据字符串在 DTS 列表中的位置来打分。越靠前的字符串,得分越高。这个 for 循环确保了即使驱动能支持这两个,它也会因为分值竞争而选中更具体、更靠前的那个。
  2. 为什么数组末尾一定要加 { }?
    如果没有那个空的 { },这个 for 循环的判断条件 matches->compatible0 就会在内存中一直往后跑,直到触碰到非法的内存地址导致内核崩溃(Kernel Panic)。

进入__of_device_is_compatible

c 复制代码
/**
 * 优先级顺序(1 为最高):
 * 1. 特定兼容名 && 类型 && 名称  (全中)
 * 2. 特定兼容名 && 类型
 * 3. 特定兼容名 && 名称
 * 4. 特定兼容名                  (最常用,只要型号对上就行)
 * ...
 * 11. 仅名称匹配                 (最无奈的匹配)
 */
static int __of_device_is_compatible(const struct device_node *device,
				     const char *compat, const char *type, const char *name)
{
	/* ... 变量定义 ... */

	/* 1. 兼容性匹配(最高优先级) */
	if (compat && compat[0]) {
		/* 寻找硬件节点里的 "compatible" 属性 */
		prop = __of_find_property(device, "compatible", NULL);
		/* 遍历硬件节点里列出的所有兼容性字符串 */
		for (cp = of_prop_next_string(prop, NULL); cp;
		     cp = of_prop_next_string(prop, cp), index++) {
			/* 如果匹配成功 */
			if (of_compat_cmp(cp, compat, strlen(compat)) == 0) {
				/* 给出一个巨大的基础分,并根据位置 index 减去一点微小的差值 */
				score = INT_MAX/2 - (index << 2);
				break;
			}
		}
		/* 如果驱动要求匹配 compat 但遍历完都没找到,直接判死刑返回 0 */
		if (!score) return 0;
	}

	/* 2. 设备类型匹配(次优先级) */
	if (type && type[0]) {
		if (!device->type || of_node_cmp(type, device->type))
			return 0; // 类型不符,判死刑
		score += 2;   // 类型对上了,加 2 分
	}

	/* 3. 名称匹配(最低优先级) */
	if (name && name[0]) {
		if (!device->name || of_node_cmp(name, device->name))
			return 0; // 名称不符,判死刑
		score++;      // 名称对上了,加 1 分
	}

	return score;
}
复制代码
__of_find_property
of_prop_next_string
of_compat_cmp
of_node_cmp

这几个匹配函数的本质基本都是利用宏实现strcmp或者strcasecmp

宏定义

#define of_compat_cmp(s1, s2, l) strcasecmp((s1), (s2))

内核对 compatible 字符串很宽容,忽略大小写 。这意味着你在驱动里写 "OVTI,ov5640",而设备树里写 "OVTI,OV5640",它们依然能匹配成功,为了代码可读性和专业感,业界标准做法是一律使用小写和中划线.

#define of_prop_cmp(s1, s2) strcmp((s1), (s2))

属性名(如 clock-frequency、interrupts)必须一字不差,大小写错了内核就完全认不出来。这是为了保证底层硬件描述的严谨性。

#define of_node_cmp(s1, s2) strcasecmp((s1), (s2))

例如:

c 复制代码
static struct property *__of_find_property(const struct device_node *np,
					   const char *name, int *lenp)
{
	struct property *pp;

	if (!np) return NULL; // 如果节点不存在,直接返回

	  /*A. 初始化:pp = np->properties
        动作:将指针 pp 指向该设备节点的第一个属性。
		B. 循环条件:pp
		动作:只要 pp 不是 NULL,循环就继续。
		C. 步进:pp = pp->next
		动作:将 pp 移向下一个属性。*/
	for (pp = np->properties; pp; pp = pp->next) {
		
		/* 核心:使用 strcmp 比较属性名是否一模一样 */
		if (of_prop_cmp(pp->name, name) == 0) {
			
			/* 如果找到了,且调用者关心数据长度 */
			if (lenp)
				*lenp = pp->length; // 把长度(如 4 字节或字符串长度)写回去
			
			break; // 找到了就不再继续翻了
		}
	}

	return pp; // 返回找到的"标签"指针,没找到则返回 NULL
}
复制代码
举例:

1. 场景模拟:摄像头的"参数清单"
假设你的设备树(DTS)里有这样一个摄像头节点:

代码段
ov5640: camera@3c {
    compatible = "ovti,ov5640";  // 属性 1
    reg = <0x3c>;                // 属性 2
    clocks = <&clk_cam>;         // 属性 3
};

当内核解析这段 DTS 后,在内存中会生成一个 struct device_node(即参数 np)。这个节点内部维护了一个单向链表,把这些属性一个个串起来。

2. 匹配过程:模拟调用 __of_find_property(np, "reg", &len)
当你调用这个函数寻找 "reg" 属性时,内核会像翻书一样:

第一步:进入函数
np 指向我们的摄像头节点。
name 是我们要找的目标 "reg"。
lenp 是一个用来接收数据长度的"空袋子"。

第二步:开始 for 循环(链表遍历)
第一次迭代:指针 pp 指向第一个属性 compatible。
执行 of_prop_cmp("compatible", "reg")。
结果:不匹配。

第二次迭代:指针 pp 移动到下一个(pp = pp->next),指向 reg。
执行 of_prop_cmp("reg", "reg")。
结果:匹配成功 (== 0)!

第三步:收尾与返回
记录长度:如果传了 lenp,就把 reg 属性的长度(这里是 4 字节)填进去:*lenp = 4。
跳出循环:执行 break。
返回指针:把包含 0x3c 这个数值的 property 结构体地址返回给调用者。

2.4 .uevent = spi_uevent

事件通知机制,当设备被添加或移除时,该函数负责向用户空间(udev/mdev)发送信号。比如当陀螺仪 ICM-20948 被识别时,内核通过这个函数告诉用户空间:"有一个新设备出现了,请为它创建设备节点 /dev/spidevX.Y。"


3. 主程序控制器创建流程(抽象层)

同样的,截取 bus_register 的 dump_stack 调用链日志进行分析:

复制代码
[    0.308927] KD_LOG: here is class_register
[    0.308958] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.9.88 #5
[    0.308972] Hardware name: Freescale i.MX6 UltraLite (Device Tree)
[    0.309011] [<80112a34>] (unwind_backtrace) from [<8010dc2c>] (show_stack+0x20/0x24)
[    0.309043] [<8010dc2c>] (show_stack) from [<80469964>] (dump_stack+0x80/0x94)
[    0.309075] [<80469964>] (dump_stack) from [<8115edf4>] (spi_init+0x8c/0xc0)
[    0.309107] [<8115edf4>] (spi_init) from [<80101cc4>] (do_one_initcall+0x54/0x180)
[    0.309137] [<80101cc4>] (do_one_initcall) from [<81100e9c>] (kernel_init_freeable+0x16c/0x204)
[    0.309169] [<81100e9c>] (kernel_init_freeable) from [<80b7e868>] (kernel_init+0x18/0x120)
[    0.309201] [<80b7e868>] (kernel_init) from [<80109350>] (ret_from_fork+0x14/0x24)

发现它的调用链和 bus_register 是相同的,都是由 spi_init 调用,所以我们直接分析这个 class_register 即可。

class 是为了向用户空间抽象出统一的设备操作接口(如/sys/class/spi_master/)

class_register 核心职能解析:

  1. 对象归类:通过分配 subsys_private 空间,初始化该类的内部管理数据结构(账本)。
  2. sysfs 挂载:调用 kset_register 将该类注册为内核对象容器,使其在 /sys/class/ 下可见。
  3. 设备接口桥梁:它是后续 device_create 函数的基石。没有 class 注册,内核就不知道新增加的 SPI 控制器应该被分类到哪里,也就无法在 /dev/ 下生成对应的节点。

3.1 对象归类

c 复制代码
struct subsys_private *cp;
cp = kzalloc(sizeof(*cp), GFP_KERNEL);

 在 Linux 内核中,struct class(也就是 cls)是一个公开的结构体。它定义在头文件中,所有的驱动代码都可以访问它。如果内核把所有管理数据(比如设备链表、互斥锁、属性列表)都放在 struct class 里,结构会变得极其臃肿且难以维护,一旦内部管理数据结构改变,所有包含该头文件的驱动代码都必须重新编译。为了解决这个问题,内核设计了"私有结构体"模式:

struct class:给外界看的"名片"(包含类名、销毁回调等)。

struct subsys_private:内核内部使用的"后台账本"(隐藏在驱动开发者视野之外的复杂管理数据)

c 复制代码
struct subsys_private {
	struct kset subsys;
	struct kset *devices_kset;
	struct list_head interfaces;
	struct mutex mutex;

	struct kset *drivers_kset;
	struct klist klist_devices;
	struct klist klist_drivers;
	struct blocking_notifier_head bus_notifier;
	unsigned int drivers_autoprobe:1;
	struct bus_type *bus;

	struct kset glue_dirs;
	struct class *class;
};
  1. struct subsys_private *cp; 的本质:定义内部管理上下文

    这一行的作用是在函数栈上声明一个指针,用于操作该类的私有管理上下文(Private Context)。

    技术层面:在 Linux 内核中,struct class 是一个公开暴露的结构体,内核倾向于将其设计为"只读"或"配置型"的属性容器。而 struct subsys_private 则包含了该类在运行时必须维护的复杂状态数据,例如:

    klist_devices:该类下所有已注册设备的链表。

    interfaces:该类支持的各种接口链表。

    mutex:保护该类数据结构的互斥锁。

    架构意义:通过将这些运行时状态变量移出 struct class,内核实现了接口与实现的分离。这确保了驱动开发者在操作 struct class 时,不会无意中破坏底层的设备管理链表。

  2. cp = kzalloc(sizeof(*cp), GFP_KERNEL); 的本质:初始化内存空间

    这一行的作用是动态分配并清零该类的私有数据内存区

    动态分配:内核使用 kzalloc 从堆(Heap)中分配一块连续的物理内存。之所以选择动态分配,是因为该类在系统中存在的数量是可变的(可以在运行时注册多个类),且其生命周期由 class_register 到 class_unregister 决定。

    内存清零(Zeroing):kzalloc 中的 z 代表 zero。这在内核编程中至关重要,它确保了所有未显式初始化的成员(如链表头、互斥锁指针)在起始状态下均为 NULL 或 0,避免了使用未初始化内存带来的安全风险。

    上下文绑定:分配后的内存起始地址被赋值给指针 cp。这块内存区域随后将成为该类生命周期内所有管理数据的载体。

3.2 sysfs 挂载

c 复制代码
error = kobject_set_name(&cp->subsys.kobj, "%s", cls->name);
...
cp->class = cls;
cls->p = cp;

kobject_set_name:设定在 /sys/class/ 下显示的文件夹名字(即 cls->name)。

双向指针绑定:cp->class = cls 与 cls->p = cp。这是实现"公有结构"与"私有结构"互访的关键,之后无论拿到 class 还是 subsys_private,都能通过指针回溯找到对方。

3.3 设备接口桥梁

c 复制代码
error = kset_register(&cp->subsys);

这是将该类正式接入内核设备模型的关键函数。kset_register 会将该类的 kobject 放入内核的全局对象管理系统,并在 /sys/class/ 目录下实际创建对应的目录节点。一旦执行成功,用户空间(如 udev)就可以通过 sysfs 看到这个分类了。

3.4 kobject

c 复制代码
struct kobject {
	const char		*name;
	struct list_head	entry;
	struct kobject		*parent;
	struct kset		*kset;
	struct kobj_type	*ktype;
	struct kernfs_node	*sd; /* sysfs directory entry */
	struct kref		kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
	struct delayed_work	release;
#endif
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};

如果把 Linux 内核的设备管理系统比作一座大厦,kobject 就是那块最基础的"砖头"。没有它,所有的驱动、总线、类(Class)都无法在内核中被统一管理。

对象的抽象基类:

在 C 语言中没有 class(类)的概念。内核开发者使用 kobject 来模拟面向对象中的"基类"。任何一个需要被系统统一管理的实体(如设备、驱动、总线、类),内部都会包含一个 kobject。

类比:你可以把 kobject 看作所有内核对象的"身份证"。只要一个对象拥有 kobject,内核就能识别它、统计它、并在 /sys/ 目录下为它创建节点。

引用计数:

这是 kobject 最核心的功能。内核中很多对象(如一个设备)被多个地方引用(例如总线引用它、驱动引用它、用户空间进程也在使用它)。

职能:kobject 负责记录引用计数。当计数归零时,kobject 会自动触发清理函数,把这块内存释放掉。这完美解决了内核中极其复杂的内存泄漏问题。

Sysfs 的映射点:

你刚才看到的 /sys/bus/、/sys/class/ 等目录,其背后的每一个条目,本质上都是一个 kobject 对应的结果。

职能:当内核注册一个 kobject 时,它会负责在 sysfs 对应的虚拟文件系统中创建一个目录,从而实现"内核对象在用户空间的透明可视化"。

4. 标准化 API(抽象层)

 在前面两章中,我们介绍了抽象层中的总线匹配,明白了内核是如何注册总线并且创建主程序控制器的,接下来我们要研究标准化API:

核心职能:

标准化 API 的存在意义在于解耦。驱动开发者不需要知道底层是 GPIO 模拟的 SPI 还是高速的 DMA 控制器,只需要调用这些函数即可。

  • 数据封装:将驱动程序提供的 struct spi_transfer(单次传输描述)封装进 struct spi_message(完整的消息对象)。

  • 队列化调度:将生成的 spi_message 挂入控制器的传输队列。

  • 同步与异步的交付:

    spi_sync (同步):阻塞调用。驱动必须等待传输彻底完成(调用返回即代表数据已发送/接收完毕)。

    spi_async (异步):非阻塞调用。驱动发出请求后立即返回,底层传输完成后通过回调函数通知驱动。

参考笔者的这篇文章 数据传输

5. 传输序列化 (抽象层)

在 Linux SPI 中,pump(泵)的意思是"推动任务流转"。

它的职责是:不管驱动往队列里塞了多少个消息,它都负责按顺序取出一个、丢给硬件驱动执行,等执行完了,再接着取下一个。

这种设计将复杂的"异步并发"变成了"线性的串行执行",极大地简化了底层硬件驱动的编写难度(底层驱动不需要处理复杂的重入问题)。

参考笔者的这篇文章 数据传输链路

相关推荐
Chris _data1 小时前
# WPF 学习记录( 第二天)
学习·wpf
難釋懷1 小时前
Nginx-UpStream工作流程
运维·nginx
梦071 小时前
Trae Friends福州线下活动收获一二-vibeCoding现状
经验分享·学习
星恒随风1 小时前
C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念
开发语言·c++·笔记·学习
delishcomcn1 小时前
AI赋能的薄膜分切机:从自动化到自优化
运维·人工智能·自动化·薄膜分切机
踏着七彩祥云的小丑1 小时前
嵌入式测试学习第35 天:蓝牙、WiFi嵌入式设备测试基础概念
单片机·嵌入式硬件·学习
极客先躯1 小时前
高级java每日一道面试题-2026年02月03日-实战篇[Docker]-如何备份和恢复 Docker Volume?
运维·docker·容器·自动化·备份·持久化·恢复
艾莉丝努力练剑1 小时前
【Qt】界面优化:绘图API
linux·运维·开发语言·网络·qt·tcp/ip·udp
方便面不加香菜1 小时前
Linux--基础IO(二)
linux·运维·服务器