自旋锁:
1、中断处理程序与下半部之间共享数据时,应该如何加锁?
答:由于中断处理程序会异步于其他任何程序执行,所以在下半部中对共享数据进行操作前,必须要禁止本地中断,然后获取锁。
可用的锁接口推荐使用spin_lock_irqsave(保存中断状态->禁止本地中断->获取锁)和spin_unlock_irqrestore(释放锁并将中断恢复到以前的状态)。当然,可以根据实际需要使用相应的读写自旋锁(由于读写自旋锁更照顾读操作,所以在写操作试图获取写锁时,总是等待所有的读者执行完读操作,当存在大量读操作时,可能会造成写着处于饥饿状态)。
问题:这里为什么没有考虑中断处理程序的并发导致出现函数重入造成的数据错误呢?
答:因为中断处理程序执行前,总是先禁止该中断线,然后才开始执行中断处理程序,保证该中断处理程序是可重入的。
2、下半部与进程上下文之间共享数据,应该如何加锁?
答:
因为下半部程序能够抢占进程上下文中的程序,所以当下半部与进程上下文间共享数据时,在进程上下文程序中(如系统调用--软件引起的中断,异常出现陷入内
核等)应该在获取锁之前禁止下半部,可用的锁的接口有:spin_lock_bh(禁止所有下半部的执行,获取锁)和spin_unlock_bh(释放
锁并恢复下半部的执行) 。
问题:下半部程序之间共享数据时,如何进行加锁,是否需要禁止下半部?
答:下半部程序中,1)对于tasklet,由于同类的tasklet不可能同时运行,即使是在不同的处理器上,所以不需要加锁,当然也不需要禁止下半
部;不同类型的tasklet可以同时运行,当不同类型的tasklet之间共享数据时,就需要在对共享数据操作前加锁(普通的自旋锁即可)了,但不需要
禁用下半部,因为tasklet之间并不会出现抢占。2)对于软中断来说,虽然同种类型的软中断在同一处理器上不会同时运行(软中断处理程序执行时,当前
处理器上的软中断会被禁止),但在同一系统的不同处理器上仍会同时运行,所以软中断间的共享数据操作,必须进行加锁(普通的spin_lock即可),不
需要禁用下半部,因为同一系统上软中断间也不会互相抢占。当然,如果仅仅是通过加锁防止软中断程序自身被并发执行,那么使用软中断就没意义了,tasklet会是更好的选择
自旋锁提供了一种快速简单的锁实现方法。如果加锁时间不长且代码不能睡眠,利用自旋锁比较方便;如果加锁时间长,或者可能会睡眠,则应考虑使用信号量。
信号量:
信号量是一种睡眠锁,比自旋锁有更大的开销。
由于信号量的睡眠特性,所以:
1)由于争用信号量的进程在等待锁变为可用时会进入睡眠状态,所以信号量适用于锁被长时间持有的情况。
2)相反,如果持有锁的时间较短,使用信号量就不合适了。因为睡眠、维护等待队列以及唤醒进程所花费的时间可能会多于锁被占用的时间。
3)由于信号量在持有时,执行线程可能会睡眠,所以信号量只能用在进程上下文中。因为中断上下文中是不能CPU进行调度的。
4)你可以在持有信号量时睡眠,因为其他试图获取该信号量的线程也只是去睡眠而已,并不会造成死锁。
5)当你占用信号量时,就不能占有自旋锁,因为自旋锁是不允许进程睡眠的。
信号量分类:
信号量分为二值信号量和计数信号量,二值信号量是特殊的计数信号量,只是计数初值设置为1,主要用来进程间的互斥。
信号量的主要操作有:down和up,down对信号量进行P操作(即减1操作),up对信号量进行V操作(即加1操作)。
其中与down具有相同功能的还有down_interruptible,这两个函数的主要区别:
当信号量已经被占用时:
1)执行down操作,会把调用进程置为TASK_UNINTERRUPTIBLE状态后,进入睡眠状态。进程睡眠时,不会被信号打断,只有当其他进程执行up操作释放信号量,才能将睡眠在该信号量上的进程唤醒。
2)
执行down_interruptible操作,会把调用进程置为TASK_INTERRUPTIBLE状态后,进入睡眠状态。跟down操作不同的是,
该进程不光会被up操作唤醒,也会被信号唤醒,被信号唤醒时,down_interruptible一般会返回-EINTR。
如果调用down_interruptible()则可以有这样一个好处:如果你觉得当前进程等待的资源一直不可用,以至于你希望给当前进程发送一个信号(signal,不是信号量),以便结束当前进程的执行。
同样的,对于信号量,也存在读写信号量,主要操作有down_read、up_read和down_write、up_write。不过,读写信号量的睡眠都是不可以被信号打断的,所以它只有一个版本。
互斥体mutex(可以睡眠的强制互斥锁)
mutex作用相当于二值信号量,不过mutex的操作接口更简单,实现更高效,而且使用的限制性更强。
主要操作有:mutex_lock和mutex_unlock。
使用mutex的场景及特点:
1)任何时刻,只有一个任务可以持有mutex。
2)给mutex上锁者同时也是对mutex的解锁者。所以,不适合内核同用户空间复杂的同步场景,经常是在同一上下文中使用mutex。
3)不支持递归。
4)持有mutex锁的进程不可以退出。
5)mutex不能在中断上下文中使用,即使是mutex_trylock也不行。
6)只能使用官方API对其进行操作,不可以被拷贝、手动初始化和重复初始化。
使用mutex时,可以打开CONFIG_DEBUG_MUTEXES宏进行测试,使程序强制检测是否遵守了上述约束。
RCU锁:
RCU--Read Copy Update, 是根据其原理进行命名的锁机制。RCU锁是在linux内核的互斥机制当中,属于一种免锁机制。RCU中的读取和写入无需考虑互斥的问题。
RCU的工作原理:将读取者和写入者要访问的共享数据放在一个指针中,读者通过该指针访问其中的数据,写者通过修改该指针来更新数据。
读取者的RCU临界区:
对于读者来说,如果要访问共享数据,所要做的工作首先是调用rcu_read_lock和rcu_read_unlock函数构建自己所谓的读取者一侧的临界区,然后在临界区中获得指向共享数据区的指针。离开临界区后不应该有任何形式的对该指针的引用。
在临界区中,关闭内核的可抢占性意味着在临界区中不会因为中断的发生导致进程的切换,而且作为确定的规则,在临界区中的代码不能发生 睡眠(因为已经关闭了内核抢占)。简而言之,临界区中的代码不应该导致任何形式的进程切换。
注意:虽然从名称上来看,rcu操作函数中含有lock字样,但实际上rcu_read_lock和rcu_read_unlock实际要做的工作仅仅是分别关闭和打开内核的可抢占性而已。
写入者的RCU操作:
RCU
操作中写入者要完成的工作是重新分配一个被保护的共享数据区,(视情况决定是否)将老数据区的数据复制到新数据区,然后再根据需要修改新的数据区,最后用
新数据区的指针替换掉老的指针(替换指针的操作是原子操作,不需要与读操作互斥)。写入者完成这些工作后,后续的所有读操作都将读取新数据区中的数据。
---备注----将新指针替换老指针
注意:写入者将新数据区指针替换老指针后,还不能够马上就释放指针指向的数据空间(从这里也可以看出,当写操作较多时,会大大降低性能),因为此时可能系统中还会有对老指针的引用---存在两种情况:
1)因为rcu_read_lock操作只是禁止内核的可抢占性,并没有关闭中断,所以如果读者在进程上下文中刚获取到临界区数据的指针,此时发生了中
断,而恰好中断处理程序中执行的是rcu的写操作。如果写操作将新的数据区指针替换老指针后,将老指针指向的空间释放了,当中断返回时,rcu读者仍然存
有的是老指针,此时就会出现野指针问题。
2)对于多处理器系统,当处理器A上的读者刚获取到临界区的指针时,处理器B上执行了写操作,替换掉了老指针,同时释放了老指针的空间,也会出现处理器A上的读者对老指针的错误引用。
综
上所述,写者在将新数据区指针替换掉老指针后,不能马上释放老指针所在的空间。写者需要同内核协作,只有在确定对所有老指针的引用都结束后,才能释放老指
针的空间。内核提供了call_rcu的接口,该函数向内核注册一个回调函数,内核在确定系统中对所有老指针的引用都结束后,就会调用已注册的回调函数,
释放掉老指针引用的空间。
问题:内核如何确保没有读取者对老指针的引用的?
答:
所有可能对共享数据区指针的不一致引用一定发生在读取者的RCU临界区内,所以就单处理器而言,在临界区中一定不会发生进程间切换,所以一旦某一CPU中
发生过进程切换,则对老指针的引用就会结束。因此,内核确定没有对老指针的引用的条件是:系统中所有处理器上都至少发生过一次进程间切换。注
意:如果写着调用call_rcu_bh进行注册回调函数时,意味着内核将把系统中的软中断的完成也当成是类似进程间切换执行了一次,因为读操作可能是在
软中断中,而软中断是可能被中断打断的,所以如果当rcu读操作是在软中断中,那么RCU的写操作就必须使用call_rcu_bh注册老指针销毁函数;
同时,由于写者调用的是call_rcu_bh,那么读者也必须通过调用rcu_read_lock_bh和rcu_read_unlock_bh(禁止
和使能中断下半部)函数构建临界区。
以上内容参考了和<深入Linux设备驱动程序内核机制>以及网上一些资料作的一些笔记,如有错误,欢迎大家zh
阅读(2321) | 评论(2) | 转发(0) |