linux串口驱动之五一级接收缓存同步

目录

buffer的commit字段

赋值部分之分配新内存

应用

总结

同步作用

最后tty_buffer数据的提交

buffer的lock字段

同步作用

总结


buffer的commit字段

commit字段的含义即为提交,即提交给消费者处理的数据。

赋值部分之分配新内存

buffer的commit字段,是used字段的影子,即每次申请一个新buffer时,会将此前已经满了的buffer的commit字段赋值为used的值

复制代码
b = buf->tail;
		/* This is the slow path - looking for new buffers to use */
		n = tty_buffer_alloc(port, size);
		if (n != NULL) {
			n->flags = flags;
			buf->tail = n;
			/* paired w/ acquire in flush_to_ldisc(); ensures
			 * flush_to_ldisc() sees buffer data.
			 */
			smp_store_release(&b->commit, b->used);
			/* paired w/ acquire in flush_to_ldisc(); ensures the
			 * latest commit value can be read before the head is
			 * advanced to the next buffer
			 */
			smp_store_release(&b->next, n);

如上图绿色部分,为分配新buffer时相关结构体字段的变化。commit仅仅作为used的一个影子值,需要注意: 此处仍然处于中断上下文中。

1) buffer tail指向绿色新buffer

2) commit记录used的数值

应用

应用则在work上下文中,此处注释与赋值时的注释遥相呼应。当中断中写入的数据,即commit字段与read字段相同时,即表示此buffer中数据已经被取走了,则

复制代码
next = smp_load_acquire(&head->next);
/* paired w/ release in __tty_buffer_request_room() or in
		 * tty_buffer_flush(); ensures we see the committed buffer data
		 */
		count = smp_load_acquire(&head->commit) - head->read;
		if (!count) {
			if (next == NULL)
				break;
			buf->head = next;
			tty_buffer_free(port, head);
			continue;
		}

1) 移动head 到绿色buffer

2) 将原先buffer放入free链表中

如果commit 大于read,则继续通知用户读取。

总结

基于上述两节的过程,看起来

1)work每次从红色tty_buffer读取数据

2) 中断往绿色tty_buffer中写入数据。

假设中断往绿色的写入了100个字节,而后再没有数据从串口进来了,此时绿色tty_buffer的commit依然是0,read依然为0,最后这个buffer中的数据不能被接收到了!

同步作用

1) 假设没有commit字段

复制代码
// 假设初始状态:b->used = 0, b->read = 0

// 生产者代码(编译后可能被重排):
void producer() {
    // 写入数据
    b->data[0] = 'A';    // (1) 写入数据A
    b->data[1] = 'B';    // (2) 写入数据B  
    b->used = 2;         // (3) 更新used计数器
}

// 消费者代码:
void consumer() {
    if (b->used > b->read) {           // (4) 检查有新数据
        char data1 = b->data[0];       // (5) 读取数据A
        char data2 = b->data[1];       // (6) 读取数据B
        process_data(data1, data2);
    }
}

上述代码存在以下可能性,由于重排

复制代码
CPU 1 (生产者)        CPU 2 (消费者)
b->used = 2;         if (b->used > 0) → true! (看到used=2)
                     char data1 = b->data[0]; → 垃圾值! (数据还没写入)
b->data[0] = 'A';    
b->data[1] = 'B';

2) 增加commit字段,并且采用smp_strore和smp_load函数

复制代码
// 生产者代码:
void producer() {
    // 1. 写入数据
    b->data[0] = 'A';                    // (1)
    b->data[1] = 'B';                    // (2)
    b->used = 2;                         // (3)
    
    // 2. 发布提交 - 关键!
    smp_store_release(&b->commit, b->used);  // (4)
}

// 消费者代码:
void consumer() {
    // 1. 获取提交点 - 关键!
    size_t commit = smp_load_acquire(&b->commit);  // (5)
    
    if (commit > b->read) {
        // 2. 安全读取数据
        char data1 = b->data[0];                   // (6)
        char data2 = b->data[1];                   // (7)
        process_data(b->read, commit, data1, data2);
    }
}

smp_store_release(&b->commit, value) 保证:

  1. 存储commit之前的所有内存写入都对其他CPU可见

  2. 存储commit操作本身不会被重排到之前的写入之前

smp_load_acquire(&b->commit) 保证:

  1. 在加载commit之后的所有内存读取都不会被重排到加载之前

  2. 会看到smp_store_release发布的最新值

    CPU 1 (生产者) CPU 2 (消费者)

    b->data[0] = 'A'
    b->data[1] = 'B'
    b->used = 2
    commit = smp_load_acquire(&b->commit)
    smp_store_release(&b->commit, 2)
    ↓ (看到 commit == 2)
    data1 = b->data[0] // 保证看到 'A'
    data2 = b->data[1] // 保证看到 'B'

commit字段通过内存屏障解决了以下问题:

  1. 编译器重排序:防止编译器优化破坏写入顺序

  2. CPU重排序:防止CPU乱序执行导致数据不一致

  3. 缓存一致性:确保写入数据对其他CPU核心可见

  4. 可见性时序:建立明确的"数据就绪"时间点

核心原理smp_store_releasesmp_load_acquire创建了一个同步点,确保在这个点之前的所有写入,在这个点之后的所有读取都能看到完整的结果。

同时,这里采用commit一次性提交了一个tty_buffer,而非单字节提交

最后tty_buffer数据的提交

buffer的lock字段

复制代码
static void flush_to_ldisc(struct work_struct *work)
{
	struct tty_port *port = container_of(work, struct tty_port, buf.work);
	struct tty_bufhead *buf = &port->buf;

	mutex_lock(&buf->lock);

在所有代码实现中,只看到了此接口中有lock,作为一个消费者,它用于和谁的并发控制呢?

同步作用

与工作队列调度器同步

代码中调度工作队列的地方示例:

1) 中断接收完字节后

2) 读取数据时。通过接口 n_tty_kick_worker

/没有这个锁,flush_to_ldisc可能并发执行,示例如下:

上图的情况具体描述如下:

复制代码
// 假设没有mutex_lock的情况:

// CPU 1                          CPU 2
// flush_to_ldisc()               flush_to_ldisc()
// head = buf->head               head = buf->head (相同值)
// process data                   process data (重复处理!)
// buf->head = next               buf->head = next (竞争条件!)
// free(head)                     free(head) (双重释放!)

mutex_lock(&buf->lock)的作用是:

  1. 确保flush_to_ldisc的单实例执行 - 防止工作队列重入

  2. 保护线路规程的单线程要求 - receive_buf不是线程安全的

  3. 保护缓冲区链表操作 - 防止并发修改链表结构

  4. 提供消费进度的一致性 - 确保head->read的更新是原子的

总结

通过lock保证多个work的并发执行;通过commit字段保证生产者和消费者对数据访问的并发。

而非简单的对链表加一个锁。

相关推荐
Bruce_Liuxiaowei36 分钟前
Linux 内存获取方法详解
linux·运维·网络安全
源梦想37 分钟前
英雄无敌动作冒险网页小游戏Linux部署演示
linux·运维·服务器
XH-hui37 分钟前
【打靶日记】群内靶机 Creds
linux·网络安全
liulilittle41 分钟前
C++ 计算当前时区偏移量秒数(GMT/UNIX偏移量)
linux·c++·unix
Ghost Face...42 分钟前
Linux V4L2驱动架构与实现详解
linux·运维·架构
再睡一夏就好43 分钟前
深入理解Linux程序加载:从ELF文件到进程地址空间的完整旅程
linux·运维·服务器·c++·学习·elf
执笔者5481 小时前
网络编程:socket编程与两个简单的UdpServer练习
linux·服务器·网络·学习
The_cute_cat1 小时前
Ubuntu指令的初步学习
linux·运维·ubuntu
python百炼成钢1 小时前
40.linux自带LED驱动
linux·运维·服务器