linux设备驱动五(并发和竞态)

本文深入探讨了操作系统中并发控制的原理与实践,包括竞态条件的产生与避免,信号量、互斥体、自旋锁等并发控制原语的使用,以及读写锁、RCU等高级锁机制。通过理解这些机制,开发者可以有效避免多线程环境下的数据不一致性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发及管理

竞态的产生:

用户空间多线程访问
SMP在不同处理器上同时执行
内核代码抢占,驱动程序可能在任何时候丢失对处理器的独占
设备中断
内核延迟机制
热插拔设备可能会在正在使用时消失

避免竞态,使用内核的并发控制原语

竞态通胀作为对资源共享访问结果而产生。

设计驱动程序第一个原则,只要可能就应该避免资源的共享,最明显的应用时避免使用全局变量。但在实际过程中,共享是难以避免的,共享就是现实的生活。

资源共享的硬原则:

在单个执行线程之外共享硬件或者软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须现实的管理对该资源的访问。访问管理的常见技术称为锁定或者互斥—确保一次只有一个执行线程可操作共享资源。
另外一个原则,当内核代码创建一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在。也就是引用计数。

信号量和互斥体

信号量的PV操作,进入临界区调用P操作,如果信号量的值大于0,则该值会减一,进程继续,相反值小于等于0,进程等待。V操作,增加信号量的值,并在必要时唤醒等待进程。
互斥体,信号量用于互斥,初始值设置为1.

linux信号量的实现

sema_init初始化一个信号量,需要指定初值
用于互斥的信号量,也就是互斥提,通过宏DECLARE_MUTEX 和DECLARE_MUTEX_LOCKED

init_MUTEX,和init_MUTEX_LOCKED,传入的都是struct semaphore* 类型
down操作(P操作)
down 不可中断
down_interruptible 可中断,通常规则,我们不应该使用非中断操作。除非没有其他可变通的方法。
down_trylock 永远不会休眠,不可获得返回非零值
up操作(V操作)
up

donw_interruptible中断时的处理

这个函数返回非零值,表示操作被中断,可以返回的错误有:-ERESTARTSYS和-EINTER。
如果返回的是-ERESTRARTSYS,内核高层代码可能会从头重新启动该调用,也可能是把错误返回给用户,因此,需要在返回这个错误码之前,必须撤销已经做出的任何用户可见的修改。如果无法撤销,则只能返回-EINTER

读取者/写入者信号量

struct rw_semaphore
down_read
dow_read_trylock 比较特别的是,授予访问时返回非零
up_read

down_write
dow_write_trylock
up_write
downgrade_write 当某个快速修改改变获得了写入者锁,而其后是更长时间的只读访问的话,我们可以在结束修改之后调用downgrade_write来允许其他读者访问。

问题:写入者优先级高于读取者,如果大量写入请求,会导致读取饿死
适用场景:很少需要写访问且写入这只会短期拥有信号量时使用rwsem

completion

使用场景:在当前线程之外初始化某个活动,然后等待该活动结束。

一种错误的方式:
struct semaphore sem;
init_MUTEX_LOCKED(&sem);
start_external_task(&sem);
down(&sem);
在external_task任务结束后调用up(&sem);

这种方式有一些问题,
通常使用中,试图锁定某个信号量的代码会发现信号量机会总是可用(什么意思?)
如果存在对信号量的严重竞争,性能会受到影响。

completion是一种轻量级的机制。
DECLARE_COMPLETION

init_completion
wait_for_completion 非中断的等待。

complete 唤醒一个
complete_all 唤醒所有

completion通常是一个单次设备,使用一次然后丢弃。
如果需要复用,没有调用complete_all的情况下,可以直接再次调用wait_for_completion,如果调用过了complete_all,那么就必须使用INIT_COMPLETION宏来重新初始化

completion的典型使用场景,模块退出时等待内核线程终止。

自旋锁

自旋锁的使用场景:通常是用在不能休眠的代码中,可以提供比信号量更高的性能。
自旋锁的实现,通过原子操作,测试并设置相关的位

超线程处理器,可以实现多个虚拟的CPU,他们共享单个处理器核心及缓存。当存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。
非抢占式单个处理器系统进入自旋状态,则会永远自旋下去。
spinlock_t
spin_lock_init
spin_lock
spin_unlock

自旋锁和原子上下文

避免自旋锁等待很长时间,核心原则是:任何拥有自旋锁的代码都必须是原子的,他不能休眠,事实上他不鞥因为任何原因放弃处理器。
内核抢占由自旋锁代码本身处理。

避免休眠有时候很难做到,所以谨慎对待自旋锁下调用的函数。

中断的处理,如果设备驱动程序在运行过程中拥有锁,此时产生了中断,中断函数中要申请锁,此时无法获得。因此在拥有自旋锁时禁止中断(仅在本地CPU上)

使占用锁的时间最短,避免影响CPU的调度。

自旋锁函数
spin_lock
spin_lock_irqsave 禁止中断,保存中断标记
spin_lock_irq 禁止中断,不跟踪标志,
spin_lock_bh 禁止软件中断,硬件中断会打开, 以tasklet运行的代码可能产生软件中断

如果锁会被运行在中断上下文中,则必须使用某个禁止中的形式,软件中断(例如,以tasklet运行的代码)

spin_unlock
spin_unlock_irqstore
spin_unlock_irq
spin_unlock_bh

非阻塞
spin_trylock
spin_trylock_bh (意义是啥?都非阻塞了,为什么要)

读取者/写入者自旋锁

和上面的API差不多,但是没有try形式。和rwsem类似有读取者饥饿的问题,另外竞争会导致性能变得很低

锁陷阱

不明确的规则

如果某个获得锁的函数需要调用的其他函数同样试图获取这个锁,那么系统会挂起。无论是信号量还是自旋锁,都不允许锁拥有者第二次获取这个锁。

设计决策是:提供给外部调用的函数必须显示地处理锁,内部函数假定锁和信号量已经被正确获得。

锁的顺序原则

最好是避免出现多个锁的情况。
避免死锁,始终以相同顺序来处理。

局部锁,和一个属于内核中心位置的锁,应该先获取局部锁
信号量和自旋锁,应该先获取信号量

细粒度锁和粗粒度锁

粗粒度锁的问题是扩展性
细粒度锁的问题是复杂性,维护性

设备驱动程序中通常的做法,应该最初使用粗粒度锁(整个驱动程序或者每个设备)在确认是锁导致的原因下,再使用细粒度锁。 通过lockmeter工具来度量内核话费在锁上的时间。

除了锁之外的办法

无锁算法

生产者/消费者模式下,如果只有一个生产者,可以使用无锁循环缓冲区(具体实现?)

原子变量

某些处理器可以以原子的方式执行++操作,但不是可移植的。

位操作

内核提供了一组可原子地修改和测试单个位的函数

函数使用的参数是依赖具体架构的。nr(int 或者unsigned long)

使用位操作来管理一个锁变量以控制对某个共享变量的访问,则相对复杂并值得讨论。大多数现代代码不会以这种方式使用位操作(尽管内核中因为历史原因还有这种写法)。而应该使用自旋锁。

seqlock

适用的场景:保护的资源很小、很简单、会频繁被访问而且写入访问很少发生且必须快速时。通常不能用于保护含有指针的数据结构,因为在写入者修改该数据结构的同时,读取者可能会追随一个无效的指针。

场景样例
unsigned int seq;
do {
seq = read_seqbegin(&this_lock);
/完成要保护的工作/
} while read_seqretry(&this_loc, seq);

write_seqlock_irqsave
write_seqlock_irq
write_seqlock_bh

write_unseqlock_irqrestore
write_unseqlock_irq
write_unseqlock_bh

RCU(read-copy-update)

很少在驱动程序中使用

RCU使用时的一些限定:
多读少写
被保护的资源通过指针访问
对这些资源的引用必须仅由原子代码拥有。

rcv_read_lock 只是禁止内核抢占,不会等待

在需要修改该数据结构时,写入线程首先复制,最后修改副本,之后用新的版本替换相关指针。当内核确信老的版本没有其他引用时,就可以释放老的版本。

设置回调函数,受rcu保护的结构,必须分配一个struct rcu_head结构

void call_rcu(struct rcu_head *head, void(*func)(void *arg), void *arg);
修改完资源后,应该调用该函数,在可安全释放资源时,func会被调用,func唯一工作就是kfree

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值