Linux 驱动研究 —— I2C (5)

3.2 数据交互

  在前期的研究中,我们已经梳理了 I2C 控制器(Adapter)与从机外设(Client)的设备节点创建流程。在内核核心层,我们对数据交互的核心结构体 struct i2c_msg 以及统一的传输总入口 i2c_transfer() 进行了分析。

  在此基础上,本文将进一步深入探讨 Linux I2C 子系统的接口适配层,解析其如何将来自不同维度的多轨读写请求,统一收敛并转化为底层的 i2c_transfer() 控制流程。

重点阐述以下两大核心接入路径:

1. 用户层(User Space)系统调用接入路径

 解析应用层程序通过标准的文件系统接口(如 /dev/i2c-x 设备节点)发起的 I2C 访问请求。本部分将重点分析应用层如何利用 write()、read() 以及 ioctl()(如 I2C_RDWR 复合传输)等系统调用跨越内核边界。

 我们将深入剖析 drivers/i2c/i2c-dev.c 中的协议转换机制,探讨内核如何通过 copy_from_user() 和 copy_to_user() 实现用户态与内核态数据缓冲区的安全搬运,并解析内核如何解析用户空间传入的控制参数,从而动态构建起底层的 i2c_msg 消息体结构。

2. 内核层(Kernel Space)自发驱动接入路径

 剖析在纯内核态运行的外设驱动程序(如音频 Codec、各种传感器,或系统后台的电源管理机制)如何直接与 I2C 核心层进行交互。

 我们将基于 drivers/i2c/i2c-core-base.c 源码,重点分析内核层驱动在直接调用 i2c_master_send() 与 i2c_master_recv() 等标准内核 API 时,物理缓冲区的封装流程。同时,本文还将深入探讨内核层数据传输的同步阻塞与等待机制(如通过 completion 或信号量等待硬件中断释放),以及子系统框架(如 Regmap)在其中所起到的抽象与桥梁作用。


3.2.1 用户层

这里我们直接用 i2c-dev 进行研究:

  • 为什么用它研究接口层?
    它是研究的"白板":
    因为 i2c-dev 不做任何高层逻辑加工,它是最简单、最纯粹的 I2C 使用者。如果你用它来追踪 write() 或 ioctl(),你看到的就是最原始的内核 I2C 数据流,不会被音频 Codec、触摸屏或传感器驱动内部那复杂的寄存器逻辑和业务状态机所干扰。
    解耦硬件与逻辑:
    特定驱动: 它"知道"芯片内部有功耗控制寄存器、时钟寄存器、音频接口控制寄存器,它在调用写入时,需要关心复杂的硬件初始化序列和偏置电压切换逻辑。
    i2c-dev: 它完全"不知道"硬件对端挂载的是什么芯片。它只管把用户传过来的字节流,原封不动地通过 I2C 总线发送出去。它就像是一个纯净的、不带任何修饰的硬件访问管道。

步骤一:埋点

既然我们选择用 i2c-dev,那么应用程序执行 write() 时自然会调用 i2cdev_write()。因此,我们在这个函数内部进行日志打印并 dump 堆栈,来追踪完整的用户态到内核态的调用链。

请在内核源码的 drivers/i2c/i2c-dev.c 中找到 i2cdev_write 函数,修改如下:

c 复制代码
static ssize_t i2cdev_write(struct file *file, const char __user *buf,
			    size_t count, loff_t *offset)
{
	int ret;
	unsigned char *tmp;
	struct i2c_client *client = file->private_data;

	/* 插入埋点:打印当前进程的 PID 并 dump 堆栈 */
	pr_info("KD_LOGO:here is the function:i2cdev_write,belong to pid: %d", current->pid);
	dump_stack();

	if (count > 8192)
		count = 8192;

	/* 核心机制:跨越内核边界,搬运用户态数据 */
	tmp = memdup_user(buf, count);
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

	ret = i2c_master_send(client, tmp, count);
	kfree(tmp);
	return ret;
}

步骤二:编写程序

编写程序的目的是为了验证埋点是否能捕获到真实的写操作,并观察 I2C 消息体在内核中如何一层层传递。

先来看我们有哪些 spidev 设备:

复制代码
[root@100ask:/dev]# ls
adxl345          gpiochip5         mmcblk0             ram0    spidevx  tty23  tty42  tty61
autofs           hwrng             mmcblk0p1           ram1    stderr   tty24  tty43  tty62
block            i2c-0             mmcblk1             ram10   stdin    tty25  tty44  tty63
bus              i2c-1             mmcblk1boot0        ram11   stdout   tty26  tty45  tty7
char             iio:device0       mmcblk1boot1        ram12   tty      tty27  tty46  tty8
console          iio:device1       mmcblk1p1           ram13   tty0     tty28  tty47  tty9
cpu_dma_latency  input             mmcblk1p2           ram14   tty1     tty29  tty48  ttymxc0
dht11            irda              mmcblk1p3           ram15   tty10    tty3   tty49  ttymxc2
disk             kmsg              mmcblk1rpmb         ram2    tty11    tty30  tty5   ttymxc5
dri              log               mxc_asrc            ram3    tty12    tty31  tty50  ubi_ctrl
ds18b20          loop-control      network_latency     ram4    tty13    tty32  tty51  urandom
fb0              loop0             network_throughput  ram5    tty14    tty33  tty52  usb-ffs
fb1              loop1             null                ram6    tty15    tty34  tty53  v4l
fd               loop2             ppp                 ram7    tty16    tty35  tty54  vcs
full             loop3             pps0                ram8    tty17    tty36  tty55  vcs1
fuse             loop4             pps1                ram9    tty18    tty37  tty56  vcsa
gpiochip0        loop5             ptmx                random  tty19    tty38  tty57  vcsa1
gpiochip1        loop6             ptp0                rtc     tty2     tty39  tty58  video0
gpiochip2        loop7             ptp1                rtc0    tty20    tty4   tty59  watchdog
gpiochip3        mem               pts                 shm     tty21    tty40  tty6   watchdog0
gpiochip4        memory_bandwidth  pxp_device          snd     tty22    tty41  tty60  zero

编写测试程序 (test_i2cdev.c)

这个程序将演示如何打开 I2C 节点、通过 ioctl 绑定目标从机地址,并发送数据,从而触发内核中的 i2cdev_write。

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>

int main() {
    // 1. 打开设备节点 (对应你的 spidev0.0,这里选择 i2c-1)
    int fd = open("/dev/i2c-1", O_RDWR);
    if (fd < 0) {
        perror("打开 /dev/i2c-1 失败");
        return -1;
    }

    // 2. 指定从机地址 (SPI 靠片选,I2C 必须通过 ioctl 指定寻址地址)
    // 0x1a 是你之前内核堆栈中 WM8960 音频芯片的从机地址
    int addr = 0x50;
    if (ioctl(fd, I2C_SLAVE, addr) < 0) {
        perror("设置从机地址失败");
        close(fd);
        return -1;
    }

    // 3. 构造数据 (0xAA, 0x55 也是典型的测试数据,电平翻转明显)
    unsigned char tx[] = {0xAA, 0x55};
    
    printf("正在通过 i2c-1 向地址 0x1a 发送数据...\n");

    // 4. 执行 write(),这将触发你在 i2c-dev.c 中埋点的 pr_info 和 dump_stack
    ssize_t ret = write(fd, tx, sizeof(tx));
    
    if (ret < 0) {
        perror("写入失败");
    } else {
        printf("成功写入 %zd 字节\n", ret);
    }

    close(fd);
    return 0;
}

之所以 write() 会调用 i2cdev_write(),是因为你打开的 /dev/i2c-1 设备节点在内核字符设备初始化时,其 file_operations 结构体已经预先绑定了 i2cdev_write。

交叉编译后把可执行文件传到开发板中执行:

root@100ask:/# ./test_i2cdev

正在通过 i2c-1 向地址 0x50 456.495896 KD_LOGO:here is the function:i2cdev_write,belong to pid: 363 发送数据...

456.508996 i2c-core: KD_LOG:i2c_transfer -> test_i2cdev Bus:i2c-1, Addr:0x50, N_msg:1

456.521897 KD_LOG:i2c_imx_xfer---adapter_id=1, msg_num=1, dir=WRITE, len=2

写入失败: No such device or address

最后的报错 写入失败: No such device or address(在 C 语言中对应错误码 -ENXIO)其实是一个正常的底层物理反馈:因为 0x50 只是我们为了绕开内核保护而伪造的一个"虚无"地址,物理总线上根本没有这个器件。当 I2C 控制器发出地址和写信号后,没有收到硬件的 ACK 应答,于是硬件报错返回。

但最关键的是,在报错之前,数据已经成功走完了从用户空间到内核物理层的完整生命周期。

写入成功,日志成功打印,接下来查看调用链:

root@100ask:/# dmesg | grep "KD_LOGO:here is the function:i2cdev_write,belong to pid: 363" -A 50

456.495896 KD_LOGO:here is the function:i2cdev_write,belong to pid: 363

456.508383 CPU: 0 PID: 363 Comm: test_i2cdev Not tainted 4.9.88 #26

456.508448 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

456.508549 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

456.508614 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

456.508682 \<80469964\> (dump_stack) from \<807752e4\> (i2cdev_write+0x48/0xc8)

456.508751 \<807752e4\> (i2cdev_write) from \<80254938\> (__vfs_write+0x38/0x120)

456.508817 \<80254938\> (__vfs_write) from \<802557fc\> (vfs_write+0xb4/0x178)

456.508881 \<802557fc\> (vfs_write) from \<802566b8\> (SyS_write+0x4c/0xa0)

456.508947 \<802566b8\> (SyS_write) from \<80109280\> (ret_fast_syscall+0x0/0x48)

456.508996 i2c-core: KD_LOG:i2c_transfer -> test_i2cdev Bus:i2c-1, Addr:0x50, N_msg:1

456.521209 CPU: 0 PID: 363 Comm: test_i2cdev Not tainted 4.9.88 #26

456.521239 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

456.521341 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

456.521405 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

456.521470 \<80469964\> (dump_stack) from \<80772360\> (i2c_transfer+0x58/0x12c)

456.521530 \<80772360\> (i2c_transfer) from \<80772480\> (i2c_master_send+0x4c/0x5c)

456.521590 \<80772480\> (i2c_master_send) from \<8077532c\> (i2cdev_write+0x90/0xc8)

456.521655 \<8077532c\> (i2cdev_write) from \<80254938\> (__vfs_write+0x38/0x120)

456.521720 \<80254938\> (__vfs_write) from \<802557fc\> (vfs_write+0xb4/0x178)

456.521785 \<802557fc\> (vfs_write) from \<802566b8\> (SyS_write+0x4c/0xa0)

456.521854 \<802566b8\> (SyS_write) from \<80109280\> (ret_fast_syscall+0x0/0x48)

456.521897 KD_LOG:i2c_imx_xfer---adapter_id=1, msg_num=1, dir=WRITE, len=2

456.532983 CPU: 0 PID: 363 Comm: test_i2cdev Not tainted 4.9.88 #26

456.533014 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

456.533116 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

456.533187 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

456.533261 \<80469964\> (dump_stack) from \<8077833c\> (i2c_imx_xfer+0x70/0xf80)

456.533329 \<8077833c\> (i2c_imx_xfer) from \<80771ddc\> (__i2c_transfer+0x15c/0x688)

456.533389 \<80771ddc\> (__i2c_transfer) from \<807723b0\> (i2c_transfer+0xa8/0x12c)

456.533448 \<807723b0\> (i2c_transfer) from \<80772480\> (i2c_master_send+0x4c/0x5c)

456.533505 \<80772480\> (i2c_master_send) from \<8077532c\> (i2cdev_write+0x90/0xc8)

456.533570 \<8077532c\> (i2cdev_write) from \<80254938\> (__vfs_write+0x38/0x120)

456.533634 \<80254938\> (__vfs_write) from \<802557fc\> (vfs_write+0xb4/0x178)

456.533698 \<802557fc\> (vfs_write) from \<802566b8\> (SyS_write+0x4c/0xa0)

456.533761 \<802566b8\> (SyS_write) from \<80109280\> (ret_fast_syscall+0x0/0x48)

参考 1.2.2.2 数据交互分析 的分析过程。

3.2.2 内核层

在 i2c_transfer 函数中添加打印信息:

c 复制代码
	pr_info("KD_LOG:i2c_transfer -> [%s] Bus:i2c-%d, Addr:0x%02x, N_msg:%d\n",
				current->comm, adap->nr, msgs[0].addr, num);
	dump_stack();

选取其中一个查看调用链:

41.972646 i2c-core: KD_LOG:i2c_transfer -> kworker/u2:3 Bus:i2c-1, Addr:0x1a, N_msg:1

41.984228 CPU: 0 PID: 349 Comm: kworker/u2:3 Not tainted 4.9.88 #25

41.984238 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

41.984265 Workqueue: events_unbound async_run_entry_fn

41.984307 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

41.984326 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

41.984348 \<80469964\> (dump_stack) from \<80772360\> (i2c_transfer+0x58/0x12c)

41.984365 \<80772360\> (i2c_transfer) from \<80772480\> (i2c_master_send+0x4c/0x5c)

41.984383 \<80772480\> (i2c_master_send) from \<805c8c30\> (regmap_i2c_write+0x20/0x3c)

41.984400 \<805c8c30\> (regmap_i2c_write) from \<805c2dd4\> (_regmap_bus_formatted_write+0xb0/0x268)

41.984416 \<805c2dd4\> (_regmap_bus_formatted_write) from \<805c2a78\> (_regmap_write+0x70/0x164)

41.984430 \<805c2a78\> (_regmap_write) from \<805c2c3c\> (_regmap_update_bits+0xd0/0xd4)

41.984444 \<805c2c3c\> (_regmap_update_bits) from \<805c42c8\> (regmap_update_bits_base+0x5c/0x80)

41.984464 \<805c42c8\> (regmap_update_bits_base) from \<809059c0\> (snd_soc_component_update_bits+0x40/0x6c)

41.984483 \<809059c0\> (snd_soc_component_update_bits) from \<80905a08\> (snd_soc_update_bits+0x1c/0x20)

41.984503 \<80905a08\> (snd_soc_update_bits) from \<8090d560\> (wm8960_set_bias_level_out3+0xe8/0x1e0)

41.984518 \<8090d560\> (wm8960_set_bias_level_out3) from \<8090bcac\> (wm8960_set_bias_level+0x24/0x28)

41.984538 \<8090bcac\> (wm8960_set_bias_level) from \<808f13c8\> (snd_soc_codec_set_bias_level+0x28/0x2c)

41.984556 \<808f13c8\> (snd_soc_codec_set_bias_level) from \<808f896c\> (snd_soc_dapm_set_bias_level+0x84/0x244)

41.984572 \<808f896c\> (snd_soc_dapm_set_bias_level) from \<808f9148\> (dapm_post_sequence_async+0xb8/0xf8)

41.984589 \<808f9148\> (dapm_post_sequence_async) from \<80159178\> (async_run_entry_fn+0x80/0x1c8)

41.984608 \<80159178\> (async_run_entry_fn) from \<8014f2e0\> (process_one_work+0x1f8/0x570)

41.984623 \<8014f2e0\> (process_one_work) from \<801503bc\> (worker_thread+0x70/0x5c8)

41.984640 \<801503bc\> (worker_thread) from \<80155aa8\> (kthread+0x118/0x120)

41.984659 \<80155aa8\> (kthread) from \<80109350\> (ret_from_fork+0x14/0x24)

这里发生的事情是:内核正在通过一个异步后台工作队列,调用音频 Codec 芯片(WM8960)的电源管理函数,试图通过 Regmap 去修改芯片内部的寄存器位(Update Bits)。

我们从下往上(按照时间线,从最初触发到最终执行)来拆解这个过程:

  1. 触发源:异步工作队列机制(底部)

    复制代码
    [ 41.984640] (worker_thread+0x70/0x5c8)
    [ 41.984608] (async_run_entry_fn+0x80/0x1c8)
    [ 41.984589] (dapm_post_sequence_async+0xb8/0xf8)

    发生了什么: 内核的系统工作队列(Workqueue)创建了一个异步任务。

    为什么异步: 音频开关(比如播完歌需要休眠,或者刚准备放歌)涉及硬件电容充电和消爆音(Pop-noise)延迟,比较耗时。为了不卡住系统主线程,ASoC 把它放到了后台异步执行(async_run_entry_fn)。

  2. ASoC DAPM 电源管理决策(中部)

    复制代码
    [ 41.984572] (snd_soc_dapm_set_bias_level+0x84/0x244)
    [ 41.984556] (snd_soc_codec_set_bias_level+0x28/0x2c)

    发生了什么: DAPM(动态音频电源管理)子系统正在改变芯片的 Bias Level(偏置电压级别)。

    背景知识: 音频芯片为了省电,通常有四个电源状态:OFF(彻底关闭) -> STANDBY(待机) -> PREPARE(准备) -> ON(完全打开)。这里 DAPM 决定要让芯片切换状态。

  3. 执行者:WM8960 驱动的具体实现(中上部)

    复制代码
    [ 41.984538] (wm8960_set_bias_level+0x24/0x28)
    [ 41.984503] (snd_soc_update_bits)

    发生了什么: DAPM 回调了 WM8960 语音芯片驱动 专有的电源设置函数 wm8960_set_bias_level_out3。

    wm8960 是一颗非常经典的音频 Codec 芯片,通常通过 I2C 控配置寄存器。

    驱动为了切换电源状态,需要修改 WM8960 内部的控制寄存器,于是它调用了 ASoC 核心层提供的公共函数 snd_soc_update_bits(更新寄存器的某些特定位)。

  4. 落地:Regmap 框架改写寄存器(顶部)

    复制代码
    [ 41.984464] (regmap_update_bits_base+0x5c/0x80)
    [ 41.984444] (_regmap_update_bits)

    发生了什么: snd_soc_update_bits 底层正是由 Regmap 框架 支撑的。调用来到了 _regmap_update_bits。

    具体动作: Regmap 接下来会执行标准的 "读-改-写"(Read-Modify-Write) 流程:

    先把 WM8960 对应的电源控制寄存器读出来。

    配合掩码(Mask)修改特定的 Bit(比如使能输出、开启偏置电流等)。

    再通过底层的 I2C 总线把新值写回到 WM8960 芯片里。

进入 _regmap_write 后可以参考 1.2.2 通过设备创建过程分析 的后续分析过程。

3.3 控制流

 在 Linux 内核中,字符设备节点的引入(/dev/i2c-x)为用户空间程序提供了直接控制硬件总线的通路。用户空间对 I2C 外设的所有配置改变(如地址绑定、超时设置)以及数据交互,在系统调用层均统一收拢于 ioctl 通道。因此,研究接口层控制流的核心,在于剖析内核如何对各种非对称的控制指令进行识别、合规性检查与多路径分流。

 作为整个 i2c-dev 接口层控制流的核心总调度室,i2cdev_ioctl 函数承担了这一关键角色。该函数通过内部的条件分支路由,将应用层抽象的命令字(Command)转化为具体的内核变量修改或深层函数下沉。深入分析该函数的控制流逻辑,不仅能理清参数配置与物理传输的解耦机制,更能为后续进一步探讨高频多线程并发下、由具体数据分支(I2C_RDWR)下沉触发的内核临界区排队与底层总线锁互斥隔离机制,奠定架构层面的理论基础。

c 复制代码
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct i2c_client *client = file->private_data; // 从文件私有数据中提取出当前打开的 i2c_client
	unsigned long funcs;

	/* 【控制流观测埋点】 */
	pr_info("KD_LOGO: i2cdev_ioctl | pid: %d, cmd: 0x%x\n", current->pid, cmd);
	dump_stack();

	/* 控制流的核心分水岭:根据用户空间传入的 cmd 命令字,路由到不同的处理分支 */
	switch (cmd) {
	
	/* 1. 从机地址绑定控制流(我们在实验一中遇到 busy 报错的分支) */
	case I2C_SLAVE:
	case I2C_SLAVE_FORCE:
		/* 位数合规性检查:如果是7位地址模式,传入地址不能大于 0x7f */
		if ((arg > 0x3ff) ||
		    (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))
			return -EINVAL;
		
		/* 核心隔离机制:如果使用普通的 I2C_SLAVE,且该地址已被内核驱动占用,则控制流直接报错返回 -EBUSY */
		if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))
			return -EBUSY;
		
		/* 绑定生效:将经过合规和安全检查的地址,写入当前文件描述符对应的 client 结构体中 */
		client->addr = arg;
		return 0;

	/* 2. 传输模式配置控制流(设置 10 位地址模式) */
	case I2C_TENBIT:
		if (arg)
			client->flags |= I2C_M_TEN;   // 开启 10 位地址寻址
		else
			client->flags &= ~I2C_M_TEN;  // 恢复 7 位地址寻址
		return 0;

	/* 3. 校验机制配置控制流(SMBus PEC 错误校验) */
	case I2C_PEC:
		if (arg)
			client->flags |= I2C_CLIENT_PEC;
		else
			client->flags &= ~I2C_CLIENT_PEC;
		return 0;

	/* 4. 硬件能力查询控制流 */
	case I2C_FUNCS:
		/* 从底层适配器(Adapter)中获取该 I2C 控制器硬件支持的功能集(如是否支持SMBus,是否支持I2C_RDWR) */
		funcs = i2c_get_functionality(client->adapter);
		/* 安全拷回:将获取的硬件能力集数据,安全地写入应用层 arg 指针指向的用户空间内存 */
		return put_user(funcs, (unsigned long __user *)arg);

	/* 5. 数据交互控制流核心入口(复合读写流) */
	case I2C_RDWR:
		/* 控制流跳转:负责多包深拷贝与下沉发送的终极处理器 */
		return i2cdev_ioctl_rdwr(client, arg);

	/* 6. SMBus 协议兼容控制流 */
	case I2C_SMBUS:
		/* 控制流跳转:进入 SMBus 规范的标准协议交互流 */
		return i2cdev_ioctl_smbus(client, arg);

	/* 7. 传输容错参数控制流(设置重试次数) */
	case I2C_RETRIES:
		client->adapter->retries = arg; // 直接修改底层适配器结构体中的重试配置
		break;

	/* 8. 物理超时参数控制流 */
	case I2C_TIMEOUT:
		/* 用户空间传入的单位是 10ms,在此处统一转化为内核标准时间单位 jiffies 并写入适配器 */
		client->adapter->timeout = msecs_to_jiffies(arg * 10);
		break;

	/* 9. 异常处理控制流 */
	default:
		/* 如果用户传入了内核不认识的控制命令,控制流走向未知分支,统一拒绝并返回不合法的 ioctl 命令错误码 */
		return -ENOTTY;
	}
	return 0;
}

核心控制流分支的分类与机制分析

通过对 i2cdev_ioctl 源码的执行路径进行解构,该总调度室的控制流表现出明显的"非对称性",依据其对内核及硬件产生的影响,可前向归纳为以下三类控制路径:

  1. 静态参数配置流(解耦机制的实现)

    • 涉及分支:I2C_SLAVE、I2C_SLAVE_FORCE、I2C_TENBIT、I2C_PEC、I2C_RETRIES、I2C_TIMEOUT。
    • 控制行为:此类控制路径在执行时,内核完全不触发物理总线的电信号传输。
    • 运行机制:内核仅对用户空间传入的参数(如 arg 携带的从机地址或超时数值)进行合规性边界检查(例如,非10位地址模式下地址值不得大于 0x7f)。通过检查后,控制流直接将数据写入当前文件描述符对应的 struct i2c_client 变量或底层 i2c_adapter 结构体中。
    • 架构价值:这证实了接口层将"参数配置"与"物理传输"进行了全解耦。应用层在此流中建立的配置信息(如绑定的从机地址 client->addr),将作为静态上下文暂存于内核,直到后续数据交互流启动时才被提取使用。
  2. 硬件能力探测流(跨平台兼容保证)

    • 涉及分支:I2C_FUNCS。
    • 控制行为:完成从内核到用户空间的数据单向安全反向拷贝。
    • 运行机制:控制流通过调用 i2c_get_functionality,直接读取物理硬件驱动在适配器(adapter)中注册的特性掩码。最后利用 put_user 函数,将该掩码穿透拷贝回用户空间指针 arg 所指向的物理内存。
    • 架构价值:由于不同硬件平台的 I2C 控制器(如 SoC 原生 I2C 控制器与通过 GPIO 模拟的 I2C 接口)在硬件能力上存在差异(例如某些控制器不支持复合读写),此控制路径为应用层提供了在不破坏通用性的前提下,探测底层硬件边界的物理沙箱接口。
  3. 数据下沉交互流(临界区锁竞争的策源地)

    • 涉及分支:I2C_RDWR、I2C_SMBUS。
    • 控制行为:终止当前接口层的控制分流逻辑,控制权向下递交,激活真正的总线数据传输流。
    • 运行机制:一旦应用层发起 I2C_RDWR 宏命令,switch 控制流将立刻跳出参数维护逻辑,通过 return i2cdev_ioctl_rdwr(client, arg); 将整个系统调用栈压入数据交互的深层网络。

由控制流向并发冲突流演进的必然性

 分析 i2cdev_ioctl 的控制流分水岭可以发现,前两类控制路径(参数配置与能力探测)由于只涉及当前进程私有文件描述符或内核静态变量的读写,在宏观上属于无状态或弱关联操作,多线程并发执行时彼此相互隔离。

 然而,一旦控制流收拢并下沉至 I2C_RDWR(数据下沉交互流),其执行逻辑将发生本质转变:

  1. 解除空间隔离:控制流将触发多层用户态到内核态的内存深拷贝,将数据在内核堆区中集中平铺。

  2. 触及共享硬件实体:所有通过不同文件描述符进入 I2C_RDWR 的多线程控制流,最终目的地都是同一个物理 I2C 控制器(client->adapter)以及同一对物理漏极开路信号线(SDA/SCL)。

 由于物理总线在同一时钟周期内仅能容纳单组时序交互,这种控制流上的收拢,必然导致多个独立进程/线程在内核深层触发对独占性硬件资源的刚性争夺。由此,整个研究的重心正式从"上层单线程的控制流路由"转入"底层多线程高并发下的临界区排队、同步、与总线互斥锁竞争"。

3.4 同步、并发与总线锁

 在现代嵌入式 Linux 系统中,随着多核处理器的普及与工业智能化需求的提升,应用层采用多线程或多进程高频并发访问外设已成为常态。以基于智能相机的机器状态监测系统为例,上层软件通常需要在一个线程中以高帧率搬运大块的红外热成像图像数据,同时在其它线程中实时下发传感器配置、音频微调或硬件状态查询等短控制指令。

 然而,作为连接这些外设的底层纽带,I2C 总线在物理本质上是一条低速且高度共享的串行拓扑结构。普通的快速 I2C 模式波特率仅为 400 kbps 400\text{ kbps} 400 kbps,这意味着在硬件层面,同一时刻只能有一个设备独占总线。此时,一个严峻的系统级矛盾便浮出水面:

  • 多线程并发的无序性:上层应用线程的调度是随机且并发的,若缺乏有效的隔离机制,多个线程的读写时序将在总线上交织错乱,导致硬件传输彻底崩溃。
  • 物理总线的单一性:低速传输导致大块数据(如数百字节的图像帧碎片)会长时间霸占总线,从而将底层的同步阻塞压力逆向传导至应用层,引发严重的控制时延抖动。

 为了在机制上绝对隔离多线程冲突,并确保高频并发下的数据安全性,Linux 内核必须在用户空间与硬件驱动之间筑起一道坚固的防御工事。这条链路的源头,并非直接指向底层的硬件寄存器操作,而是收敛于两态交界处的系统调用。

 在 I2C 子系统中,负责将用户层"多包并发请求"安全地转化为内核态原子操作的核心海关关口与协议翻译器,正是 drivers/i2c/i2c-dev.c 中的 i2cdev_ioctl_rdwr 函数。

 将以该函数为切入点,深度剖析 Linux 内核在接口层如何通过内存深拷贝隔离线程冲突,以及如何将并发流下沉收敛至底层 bus_lock 总线锁的刚性互斥占有中,并定量分析由此带来的时延开销。

c 复制代码
static noinline int i2cdev_ioctl_rdwr(struct i2c_client *client,
		unsigned long arg)
{
	struct i2c_rdwr_ioctl_data rdwr_arg; // 用于承载用户态传入的控制包头
	struct i2c_msg *rdwr_pa;             // 内核态的 i2c_msg 数组指针
	u8 __user **data_ptrs;               // 临时阵列,用来备份用户态的数据缓冲区指针
	int i, res;

	/* 【第一步:搬运控制包头】
	 * 将用户空间构建的 packets 结构体(包含消息包数量 nmsgs 和 msgs 指针)
	 * 拷贝到内核栈空间的 rdwr_arg 中。*/
	if (copy_from_user(&rdwr_arg,
			   (struct i2c_rdwr_ioctl_data __user *)arg,
			   sizeof(rdwr_arg)))
		return -EFAULT;

	/* 【安全边界检查】
	 * 限制单次 ioctl 复合传输的消息包数量,防止应用层恶意或误操作耗尽内核内存 */
	if (rdwr_arg.nmsgs > I2C_RDWR_IOCTL_MAX_MSGS)
		return -EINVAL;

	/* 【第二步:搬运消息体外壳】
	 * 此时 rdwr_arg.msgs 指向的是用户态的 struct i2c_msg 数组。
	 * 使用 memdup_user 一次性将这一组消息头(包含各包的地址、读写标志、长度)
	 * 复制到内核新分配的 rdwr_pa 空间中。*/
	rdwr_pa = memdup_user(rdwr_arg.msgs,
			      rdwr_arg.nmsgs * sizeof(struct i2c_msg));
	if (IS_ERR(rdwr_pa))
		return PTR_ERR(rdwr_pa);

	/* 分配一个指针数组,专门用来记录用户态原始的数据指针(buf),后面释放或拷回数据时要用 */
	data_ptrs = kmalloc(rdwr_arg.nmsgs * sizeof(u8 __user *), GFP_KERNEL);
	if (data_ptrs == NULL) {
		kfree(rdwr_pa);
		return -ENOMEM;
	}

	res = 0;
	/* 【第三步:深层嵌套拷贝 - 循环处理每一个消息包的数据物理缓冲区】 */
	for (i = 0; i < rdwr_arg.nmsgs; i++) {
		/* 限制单个 I2C 消息包的数据长度不能超过 8192 字节 */
		if (rdwr_pa[i].len > 8192) {
			res = -EINVAL;
			break;
		}

		/* 核心痛点:此时 rdwr_pa[i].buf 里面存的是一个【用户态虚拟地址】。
		 * 内核无法直接硬件寻址这个地址,所以必须做全套的数据剥离与重组:*/
		data_ptrs[i] = (u8 __user *)rdwr_pa[i].buf; // 1. 先把这个用户态指针备份到 data_ptrs

		/* 2. 使用 memdup_user 申请一块【内核态物理内存】,
		 *    并将用户态 buf 里的实际数据(如你的测试数据 0xAA, 0x55)拷贝进来,
		 *    最后用这块安全的内核态内存地址,覆盖掉原来的用户态地址! */
		rdwr_pa[i].buf = memdup_user(data_ptrs[i], rdwr_pa[i].len);
		if (IS_ERR(rdwr_pa[i].buf)) {
			res = PTR_ERR(rdwr_pa[i].buf);
			break;
		}

		/* (此处省略关于 SMBus 动态长度接收的兼容性检查逻辑...) */
		if (rdwr_pa[i].flags & I2C_M_RECV_LEN) {
			// ... 动态长度校验
		}
	}
	
	/* 如果在循环搬运某一个包的数据时失败了,进行倒序释放,防止内存泄漏 */
	if (res < 0) {
		int j;
		for (j = 0; j < i; ++j)
			kfree(rdwr_pa[j].buf);
		kfree(data_ptrs);
		kfree(rdwr_pa);
		return res;
	}

	/* 【第四步:下沉收敛】
	 * 此时,rdwr_pa 数组里的消息头是内核态的,msgs[i].buf 指向的缓冲区也是内核态的!
	 * 终于满足了安全要求,直接送入你最熟悉的 I2C 核心总线驱动接口:i2c_transfer()。
	 * 在这里它会去拿 bus_lock 总线锁,并最终触发 i2c_imx_xfer。 */
	res = i2c_transfer(client->adapter, rdwr_pa, rdwr_arg.nmsgs);

	/* 【第五步:读数据倒贴回用户空间 & 善后清理】 */
	while (i-- > 0) {
		/* 如果 i2c_transfer 成功执行,并且当前这个包是【读包 (I2C_M_RD)】*/
		if (res >= 0 && (rdwr_pa[i].flags & I2C_M_RD)) {
			/* 硬件读到的数据现在躺在内核态的 rdwr_pa[i].buf 中。
			 * 顺着刚才备份的原始用户态指针 data_ptrs[i],
			 * 调用 copy_to_user 把硬件数据逆向跨越边界送回应用层。 */
			if (copy_to_user(data_ptrs[i], rdwr_pa[i].buf,
					 rdwr_pa[i].len))
				res = -EFAULT;
		}
		/* 释放为该包数据申请的内核态临时缓冲区 */
		kfree(rdwr_pa[i].buf);
	}
	
	/* 释放指针备份阵列,释放内核态 i2c_msg 数组外壳,结束生命周期 */
	kfree(data_ptrs);
	kfree(rdwr_pa);
	return res;
}

① 它是什么?

它是用户层复合传输(Multiple Messages Transfer)的终极处理器。

当你操作复杂的 I2C 设备(例如:读取红外相机的一帧图像、或者读取 MPU6050 传感器)时,通常需要"先写寄存器地址,再读数据"。普通的 write() 和 read() 是分离开的,会造成两次系统调用,甚至在中途总线可能被其他进程抢走。

而应用层通过 ioctl(fd, I2C_RDWR, &packets) 就会调用到这个函数。它允许你把"写包"和"读包"打包成一个原子操作,一次性丢给内核。

② 它有什么用?

核心价值 1:它是"两态数据搬运(内存拷贝开销)"的教科书标本

你在论文里写"用户态和内核态存在隔离,需要拷贝数据",如果没有源码支持,会显得很空洞。

这个函数展现了 Linux 极其经典的 "深拷贝(Deep Copy)" 流程:

  1. 先拷控制外壳(copy_from_user 得到 rdwr_arg)。
  2. 再拷数组外壳(memdup_user 得到 rdwr_pa)。
  3. 循环拷物理数据缓冲区(memdup_user 得到内核态 buf)。
  4. 硬件执行完,再倒贴回去(copy_to_user)。

可以利用这个流程,解释为什么高频大块数据(红外相机图像)走用户层会有额外的 CPU 拷贝开销和内存抖动。


假设用户程序发起了一次 I2C 复合传输:先向外设(地址 0x50)写入 2 字节的寄存器配置指令,随后读取 4 字节的数据。

1. 传输前:用户空间的初始数据状态

在用户空间(User Space)中,定义了如下数据并分配了虚拟地址(假设值):

  • 写缓冲区 (write_buf) :内核分配的用户态虚拟地址为 0x7fff1000,内容为 [0xAA, 0x55]

  • 读缓冲区 (read_buf) :内核分配的用户态虚拟地址为 0x7fff1008,内容为空 [0x00, 0x00, 0x00, 0x00]

  • 消息数组 (msgs) :包含两个 struct i2c_msg 结构体,虚拟地址为 0x7fff2000

    • msgs[0] (写): .addr = 0x50, .flags = 0, .len = 2, .buf = 0x7fff1000
    • msgs[1] (读): .addr = 0x50, .flags = I2C_M_RD, .len = 4, .buf = 0x7fff1008
  • 控制包装 (packets) :虚拟地址为 0x7fff3000

    • .msgs = 0x7fff2000, .nmsgs = 2

用户程序调用 ioctl(fd, I2C_RDWR, 0x7fff3000),正式进入内核态的 i2cdev_ioctl_rdwr 函数。

2. 进入内核态:三层深拷贝的数据流向

① 第一层:拷贝控制外壳

  • 执行代码copy_from_user(&rdwr_arg, (void *)0x7fff3000, sizeof(rdwr_arg));
  • 内存变化 :内核在自身的栈内存(Kernel Stack)中开辟了一块空间给 rdwr_arg。此时,rdwr_arg.nmsgs 变为了 2rdwr_arg.msgs 存储的值为用户态指针 0x7fff2000
  • 当前状态:内核仅获取了消息的数量,但尚未获取具体的消息内容。

② 第二层:拷贝数组外壳

  • 执行代码rdwr_pa = memdup_user((void *)0x7fff2000, 2 * sizeof(struct i2c_msg));
  • 内存变化 :内核在内核堆区(Kernel Heap,通过 kmalloc)分配了一块全新的内存(假设内核虚拟地址为 0xffff8000),并将用户空间 0x7fff2000 处的两个 i2c_msg 结构体完整复制进来。
  • 当前状态 :内核现在拥有了消息数组的副本 rdwr_pa
    • rdwr_pa[0].buf 的值依然是用户态地址 0x7fff1000
    • rdwr_pa[1].buf 的值依然是用户态地址 0x7fff1008
    • 注:此时内核如果直接操作 rdwr_pa[0].buf,硬件 DMA 或内核低层驱动无法直接对其进行物理寻址,且面临用户多线程并发篡改该地址数据的安全风险。

③ 第三层:循环拷贝物理数据缓冲区

代码进入 for 循环,对数组中的每个消息进行物理数据隔离:

  • 处理 rdwr_pa[0](写消息)

    1. 备份用户态指针:data_ptrs[0] = 0x7fff1000;
    2. 申请内核内存并拷贝数据:rdwr_pa[0].buf = memdup_user((void *)0x7fff1000, 2);
    3. 结果 :内核在堆区分配了一块 2 字节的内存(假设地址为 0xffff9000),将用户空间里的 [0xAA, 0x55] 复制到了 0xffff9000。随后,rdwr_pa[0].buf 的值被更新为 0xffff9000
  • 处理 rdwr_pa[1](读消息)

    1. 备份用户态指针:data_ptrs[1] = 0x7fff1008;
    2. 申请内核内存:rdwr_pa[1].buf = memdup_user((void *)0x7fff1008, 4);
    3. 结果 :内核在堆区分配了一块 4 字节的内存(假设地址为 0xffff9004),用于暂存即将读出的数据。rdwr_pa[1].buf 的值被更新为 0xffff9004

3. 物理传输与数据倒贴(Copy back)

① 物理总线传输

当三层深拷贝完成后,内核调用 i2c_transfer(..., rdwr_pa, 2)。此时所有的指针(rdwr_pardwr_pa[0].bufrdwr_pa[1].buf)全部指向内核空间地址(0xffffXXXX)。

  1. 驱动程序获取 bus_lock 总线锁。
  2. 驱动控制硬件向外设发出 [0xAA, 0x55]
  3. 硬件从外设读取 4 字节的数据,直接写入到内核态地址 0xffff9004 中。假设读到的真实硬件数据为 [0x11, 0x22, 0x33, 0x44]
  4. 驱动程序释放 bus_lock 总线锁。

② 数据倒贴回用户空间

传输成功后,函数开始执行善后清理工作,并在 while 循环中识别出 rdwr_pa[1] 带有 I2C_M_RD(读标志):

  • 执行代码copy_to_user((void *)0x7fff1008, (void *)0xffff9004, 4);
  • 内存变化 :内核将暂存在内核堆内存 0xffff9004 处的硬件数据 [0x11, 0x22, 0x33, 0x44],强行复制回应用层最初指定的虚拟地址 0x7fff1008(即用户空间的 read_buf)。
  • 释放内存 :内核调用 kfree 依次释放 0xffff90000xffff9004 以及消息外壳 0xffff8000

ioctl 系统调用返回,应用层在自己的 read_buf 中成功读取到了 [0x11, 0x22, 0x33, 0x44]


核心价值 2:它揭示了"多线程并发安全"的前置代价

你之前知道 i2c_transfer 内部有 bus_lock 总线锁。

但试想:如果两个用户层线程同时调用 I2C_RDWR,它们是在什么时候开始排队的?

通过代码可以看到,内核是先执行完所有 copy_from_user 和内存分配(kmalloc)之后,才调用 i2c_transfer 去拿总线锁。

深层思考点: 这意味着,如果总线正在被红外相机大块数据霸占(锁死 6ms),第二个线程进来时,它虽然拿不到总线,但它已经消耗了内核的进程上下文、分配了内核内存并执行了数据拷贝。这种"先拷贝、后等锁"的机制,在极高频的并发冲突下,对内核资源的浪费和时延的放大是显而易见的。

相关推荐
独自归家的兔2 小时前
2026年7月2日 Docker 容器化部署 PaddleOCR-VL 实战(OpenAI 协议兼容 HTTP 调用)
运维·docker·容器
会周易的程序员2 小时前
从零构建多核CPU负载自适应控制系统
linux·c++·笔记·物联网·测试工具
ZXXstarstar2 小时前
2026年电池安全使用年限的量化决策:什么时间到期最需要替换?
linux·运维·服务器·电池
网络小白不怕黑2 小时前
httpd监控
linux·运维·服务器
2501_915716723 小时前
Nginx网关讲解_小白版
运维·nginx
茯苓gao3 小时前
机器人产业的三级火箭:卖电机、卖模组与卖整机,三种商业模式的终局推演
笔记·学习·机器人
Amy187021118233 小时前
告别“爬楼抄表”时代:电梯计量自动化改造,让每一度电都“开口说话”
运维·自动化
mm9954203 小时前
PMI三大证书:PMP、PgMP、PfMP证书对比
学习·职场和发展·项目管理·学习方法·pmp
dadaobusi3 小时前
在 Buildroot 中为 RISC-V 64 架构集成 numactl
运维·服务器