Utensil按:这应该是最实用,最接近日常编程的一章了。
同步机制用于避免对共享数据的不安全访问而导致的数据崩溃。下面按从轻到重讲述内核同步机制。
最好的同步
同步是一件烦人、容易出错,最重要的是拖慢并行的事情,所以最好的同步就是不用同步——这不是废话,而是在内核设计时的重要考虑。对不同的任务,量体裁衣,以不同的机制来处理;对每种机制,加以不同程度的限制,从而不同程度地简化用这个机制完成任务的编码难度,其中就包括减少对同步机制的需要。以下是一些书中举出的“设计简化同步”的例子:
-
Interrupt handlers and tasklets need not to be coded as
reentrant functions.
-
Per-CPU variables accessed by softirqs and tasklets only do not
require synchronization.
-
A data structure accessed by only one kind of tasklet does not
require synchronization.
每CPU变量(Per-CPU variables)
第二好的同步技术,是不共享。因此我们有了每CPU变量。但注意:内核抢占可能使每CPU变量产生竞争条件,因此内核控制路径应该在禁用抢占的情况下访问每CPU变量。
原子操作(Atomic operation)
具有“读-修改-写”特征的指令,如果不是原子的,就会出现竞争条件。
非对齐的内存访问不是原子的;
单处理器中,inc、dec这样的操作是原子的;
多处理器中,由于会发生内存总线被其它CPU窃用,所以这些操作要加上lock前缀(0xf0),这样可以锁定内存总线,保证一条指令的原子性;
有rep前缀(0xf2、0xf3)的指令不是原子的,每一循环控制单元都会检查挂起的中断。
Linux提供了atomic_t和一系列的宏来进行原子操作。
优化屏障(Optimization barrier)、内存屏障(Memory barrier)
编译器喜欢在优化代码时重新安排代码的执行顺序,由于它对某些代码顺序执行的意义没有感知,所以可能对一些必须顺序执行的代码构成致命伤,比如把同步原语之后的指令放到同步原语之前去执行——顺便带一句,C++0x中对并行的改进正是努力使编译器能感知这些顺序的意义。
优化屏障barrier()宏,展开来是asm volatile("":::"memory")
。这是一段空汇编,但volatile关键字禁止它与程序中的其它指令重新组合,而
memory则强迫编译器认为RAM的所有内存单元都给这段汇编改过了,因此编译器不能因为懒惰和优化直接使用之前放在寄存器里的内存变量值。但
优化屏障只阻止指令组合,不足以阻止指令重新排序。
内存屏障原语mb()保证,在原语之后的操作开始执行之前,原语之前的已经完成,任何汇编语言指令都不能穿过内存屏障。
80x86处理器中,I/O操作指令,有lock前缀的指令,写控制、系统、调试寄存器的指令,自动起内存屏障的作用。Pentium 4还引入了lfence、sfence和mfence这些指令,专门实现内存屏障。
rmb()在Pentium 4之后使用lfence,之前则使用带lock的无意义指令来实现。wmb()直接展开为barrier(),因为Intel处理器不会对写内存访问重新排序。
自旋锁(Spin Locks)
自旋锁是一种忙等的锁,当获取锁失败,进程不会休眠,而是一直在那里自旋(spin)或者说忙等(busy waiting),不断循环执行cpu_relax()——它等价于pause指令或者rep; nop指令。
自旋锁用spinlock_t表示,其中两个字段,slock代表锁的状态(1为未锁),break_lock代表有无其它进程在忙等这个锁,这两个字段都受到原子操作的保护。
我们详细讨论一下spin_lock(slp)宏(slp代表要获取的spinlock_t):
首先禁用内核抢占(preempt_disable()),然后调用平台相关的_raw_spin_trylock(),其中用xchg原子性地交换了8位寄存器%al(存着0)和slp->slock,如果交换出来的是正数(说明原先未锁),那么锁已经获得(0已经写入了slp->slock,上好了锁)。
否则,获锁失败,执行下列步骤:
1)执行preempt_enable(),这样其它进程就有可能取代正在等待自旋锁的进程。注意preempt_enable()本质上仅仅是将显式禁用抢占的次数减一,并不意味着就一定可以抢占了,能否抢占还取决于本次禁用之前有否禁用抢占、是否正在中断处理中、是否禁用了软中断以及PREEMPT_ACTIVE标志等等因素。就像,领导说:“我这里没问题了,你问问别的领导的意见吧。”。
2)如果break_lock==0,就置为1.这样,持有锁的进程就能感知有没人在等锁,如果它觉得自己占着太长时间了,可以提前释放。
3)执行等待循环:while (spin_is_locked(slp) && slp->break_lock)
cpu_relax();
4)跳转回到“首先”,再次试图获取自旋锁。
奇怪的是,我未能在LXR中找到这段描述对应的源代码,也无从验证我由while (spin_is_locked(slp) && slp->break_lock)
产生的的一个疑问:当锁易手之后,怎么处理break_lock这个字段?
读/写自旋锁(Read/Write Spin Locks)与顺序锁(Seqlock)
读/写自旋锁允许并发读,写锁则独占。注意:在已有读者加读锁的情况下,写者不能获得写锁。读/写自旋锁rwlock_t的32位字段lock使用了25位,拆分为两部分,24位被设置则表示未锁,0-23位是读者计数器的补码,有读者时,0-23位不为0,有写者时,0-23位为0(写时无读者)。
顺序锁则允许在读者正在读的时候,写者写入。这样做的优点是:写者无需等待读锁,缺点是有时读者不得不重复读取直到获得有效的副本。顺序锁seqlock_t有两个字段:一个是spinlock_t,写者需要获取,一个是顺序计数器,写者写时其值为奇数,写完时为偶数。读者每次读,前后都会检查顺序计数器。
顺序锁的适用场合:读者的临界区代码没有副作用,写者不常写,而且,被保护的数据结构不包括写者会改而读者会解引用(dereference, *)的指针。
RCU(Read-Copy Update)
锁还是少用的好:使用被所有CPU共享的锁,由于高速缓存行侦听(原书译为窃用)和失效而有很高的开销(a high overhead due to cache line-snooping and invalidation)。
RCU允许多个读者和写者并发运行,它不使用锁,但它仅能保护被动态分配并通过指针引用的数据结构,而且在被RCU保护的临界区,任何内核控制路径都不能睡眠。
读者读时执行rcu_read_lock()(仅相当于preempt_disable()),读完执行rcu_read_unlock()(仅相当于
preempt_enable( )
)。这很轻松,但是,内核要求每个读者在执行进程切换、返回用户态执行或执行idle循环之前,必须结束读并执行
rcu_read_unlock(),原因在写者这边:
写者要更新一个数据结构的时候,会读取并制作一份拷贝,更新拷贝里的值然后修改指向旧数据的指针指向拷贝,这里会使用一个内存屏障来保证只有修改完成,指针才进行更新。但难点是,指针更新完之后不能马上释放旧数据,因为读者可能还在读,所以,写者调用call_rcu()。
call_rcu()接受rcu_head描述符(通常嵌入在要释放的数据结构中——它自己知道自己是注定要受RCU保护的)的指针和回调函数(通常用来“析构”...)作为参数,把它们放在一个rcu_head描述符里,然后插入到一个每CPU的链表中。
每一个时钟中断,内核都会检查是否已经经过了静止状态(gone through quiescent state,即已发生进程切换、返回用户态执行或执行idle循环)
——如果已经经过了静止状态,加上每个读者都遵循了内核的要求,自然所有的读者也都读完了旧拷贝。如果所有的CPU都经过了静止状态,那么就可以大开杀戒,让本地tasklet去执行链表中的回调函数来释放旧的数据结构。
RCU是2.6的新功能,用在网络层和虚拟文件系统中。
(按:RCU描述起来可累了,尤其是原书和源代码中对静止状态都语焉不详,很难理解其确切含义,暂时只能整理成上面这种理解,以后在研究下usage,弄清实际上应该如何理解。疑问所在:因为静止状态从字面上感觉,应该指旧数据结构仍需“静止地”残余的状态,但是由于内核后来还需要检查是否否度过了静止阶段,那么如何检查这种“仍需”?显然更为容易的是检查进程切换什么的,所以只好把静止状态理解为还未发生进程切换、返回用户态执行或执行idle循环的状态,然后再“
经过了
”。怎么想怎么别扭。)
信号量(Semaphores)
这个可不是System V的IPC信号量,仅仅是供内核路径使用的信号量。信号量对于内核而言太重了,因为获取不到锁的时候需要进程睡眠!所以中断处理程序不能用,可延迟函数也不能用...
信号量struct semaphore包含3个字段,一个是atomic_t的count,也就是我们在IPC信号量那里已经熟知的表示可用资源的一个计数器;一个是一个互斥的等待队列,因为这里涉及了睡眠,信号量的up()原语在释放资源的同时需要唤醒一个之前心里堵得慌睡着了的进程;最后一个是sleepers,表示是否有进程堵在那里,用于在down()里面进行细节得恐怖而又非常有效的优化(为此,作者感叹:Much of the complexity of the semaphore implementation is precisely due to the
effort of avoiding costly instructions in the main branch of the execution flow.)
自然还有读/写信号量,这里不再敷述。
完成原语(Completion)
原书将之非常不准确地翻译为补充原语。
Completion是一种类似信号量的原语,其数据结构如下:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
它拥有类似于up()的函数complete()和类似于down()的wait_for_completion()。
它和信号量的真正区别是如何使用等待队列中包含的自旋锁。在完成原语这边,自旋锁用来确保complete()和wait_for_completion()之间不会相互竞争(并发执行),而在信号量那边,自旋锁用于避免down()与down()的相互竞争。
那么在什么情况下up()和down()可能出现竞争呢?
其实do_fork()的源代码中就包含一个活生生的例子,用于实现vfork(),下面略去了与vfork()无关的代码:
long do_fork(...)
{
struct task_struct *p;
/* ... */
long pid = alloc_pidmap();
/* ... */
/* p是复制出来的新进程 */
p = copy_process(...);
if (!IS_ERR(p)) {
/* 声明一个叫做vfork的完成原语 */
struct completion vfork;
if (clone_flags & CLONE_VFORK) {
/* 把vfork这个完成原语传递给新进程 */
p->vfork_done = &vfork;
/* 初始化:未完成状态;
这相当于一个一开始就为0的信号量——初始关闭,获取必睡的锁 */
init_completion(&vfork);
}
/* ... */
if (!(clone_flags & CLONE_STOPPED))
/* 此时新进程运行 */
wake_up_new_task(p, clone_flags);
else
p->state = TASK_STOPPED;
/* ... */
if (clone_flags & CLONE_VFORK) {
/* 等待:新进程执行完会调用complete()标志done——相当于up()。
这里相当于一个down(),所以老进程睡了 */
wait_for_completion(&vfork);
/* 接下来的代码继续执行的时候,老进程醒了,这并不一定说明新进程结束了。新进程可能仅仅是正在另外一个CPU上执行complete()函数,这时就出现了竞争条件。 */
/* ... */
}/* 完成原语vfork出作用域,消失了。如果使用的是信号量而非完成原语,相当于该信号量被销毁了,而这时新进程可能还在另外一个CPU执行up()/complete() */
} else {
free_pidmap(pid);
pid = PTR_ERR(p);
}
return pid;
}
禁止本地中断
local_irq_disable()宏使用了cli汇编指令,通过清除IF标志,关闭了本地CPU上的中断。离开临界区时,则会恢复IF标志原先的值。
禁止中断,在单CPU情形可以确保一组内核语句被当作一个临界区处理,因为这样不会受到新的中断的打扰。然而多CPU的情形中,禁止的仅是本地CPU的中断,因此,要和自旋锁配合使用,Linux提供了一组宏来把中断激活/禁止与自旋锁结合起来,例如spin_lock_irq()、spin_lock_bh()等。
禁止可延迟函数
可延迟函数禁止是中断禁止的一种弱化的形式,它通过前一篇笔记描述过的preempt_count字段来进行,具体的调用函数是local_bh_disable()。这里不再重复。
系统的并发度
为了性能,系统的并发度应该尽可能高。它取决于同时运转的I/O设备数(这需要尽可能减短中断禁止的时间),也取决于进行有效工作的CPU数(这需要尽可能避免使用基于自旋锁的同步原语,因为它对硬件高速缓存有不良影响)。
有两种情况,既可以维持较高的并发度,也可以达到同步:
共享的数据结构是一个单独的整数值,这样原子操作就足以保护它,这是在内核中广泛使用的引用计数器;
类似将元素插入链表中这样的操作设计两次指针赋值,虽然不是原子的,但只要两次赋值依序进行,单一的一次操作仍能保证数据的一致性和完整性,因此,需要在两个指针赋值中间加入一个写内存屏障原语。
大内核锁(Big Kernel Lock,BKL)
大内核锁从前被广泛使用,现在用于保护旧的代码,从前它的实现是自旋锁,2.6.11之后则变成了一种特殊的信号量kernel_sem。kernel_sem中有一个lock_depth的字段,允许一个进程多次获得BKL。
改变实现的目的是使得在被大内核锁保护的临界区内允许内核抢占或自愿切换。在自愿进程切换的情形(进程在持有BKL的情况下调用schedule()),schedule()会为之释放锁,切换回来的时候又为之获取锁,非常周到的服务。在抢占的情形,preempt_schedule_irq()会通过篡改lock_depth欺骗schedule()这个进程没有持有BKL,因此被抢占的进程得以继续持有这个锁。
分享到:
相关推荐
到了Linux 2.6内核,热插拔支持得到了显著增强,内核能够自动检测并响应设备的插入和移除事件,同时自动加载或卸载相应的驱动程序。 #### 2. USB设备的热插拔事件的产生 ##### 2.1 物理层检测 USB采用主从架构,...
Linux内核配置是定制操作系统...总的来说,Linux 2.6内核配置是一个复杂而细致的过程,需要根据系统的需求和硬件特性谨慎选择。每个选项都有其特定的作用,理解这些选项的含义有助于创建一个高效、稳定且定制化的内核。
Linux 2.6.x系列标志着Linux内核的一次重大飞跃,引入了许多改进和新特性,为后续的稳定性和性能提升奠定了坚实基础。Linux 2.6.11内核源码,结合中文笔记注释,为开发者提供了一个极好的学习平台,帮助我们深入理解...
Linux 2.6内核对电源管理的休眠支持主要涉及两种电源管理标准:APM(Advanced Power Management)和ACPI(Advanced Configuration and Power Interface)。APM是早期的电源管理技术,由BIOS控制,虽然提供了CPU和...
在Linux 2.6内核版本中,设备驱动的编写和管理有了更高效和灵活的方法。这篇笔记主要针对的是Linux 2.6.10及以后内核版本的设备驱动开发,特别是基于《Linux Device Driver3》这本书中的scull设备驱动示例。 首先,...
《2.6内核配置手册》是针对软件开发人员的重要参考资料,主要涵盖了Linux内核配置的各种选项和设置,以满足不同需求和环境。内核配置是定制化内核的关键步骤,直接影响系统的性能、稳定性和功能。 1. **代码成熟度...
另外,作者根据自己反复阅读linux2.6内核源代码和linux内核参考书的笔记与心得,用很大篇幅深入剖析了linux内核的组成结构以及各组件的实现原理,在阐述理论的同时对内核源代码进行详细注释,这样既加深了对linux...
另外,作者根据自己反复阅读linux2.6内核源代码和linux内核参考书的笔记与心得,用很大篇幅深入剖析了linux内核的组成结构以及各组件的实现原理,在阐述理论的同时对内核源代码进行详细注释,这样既加深了对linux...
1. **下载内核源码**:可以从官方网址(`http://www.kernel.org/pub/linux/kernel/v2.6/`)下载所需的内核版本。例如,如果选择的是2.6.22版本,则下载链接为:`...
《升级,感受Linux新时空》这篇文档主要讨论的是Linux操作系统中的一个重要更新——2.6内核的升级及其带来的新特性。Linux 2.6内核的发布标志着Linux系统的一个重大进步,尤其在服务器领域表现出色,但对于桌面电脑...
本文旨在详细介绍如何将Linux 2.6.22.2内核移植到友善之臂SBC2440V4开发板上,特别关注双网卡的支持。友善之臂SBC2440V4是一款基于ARM920T内核的S3C2440处理器的开发板,具有较高的性价比和广泛的用途。本篇文档不仅...
Linux 2.6.24 内核注解 增加了mtd层的注解,顺带引出了block文件夹中的注解 若代码和头文件中注解有不一致的,以头文件的注解为准, 因为都是一边看一边加注的,经常会出现第一次理解不对的情况,而后的修改,再...
/sys 是 linux2.6 内核的一个大的变化,该目录下安装了 2.6 内核中新出现的一个文件系统 sysfs /temp 存放一些临时文件 /dev 类似于 windows 的设备管理器,把所有的硬件用文件的形式存储 /media linux 系统会自动...
- **移植多媒体播放器MPlayer**:提供了移植MPlayer到Linux 2.6内核的具体步骤。 - **移植YAFFS文件系统**:介绍了移植YAFFS文件系统的尝试过程。 - **处理NAND ECC问题**:探讨了与NAND ECC相关的常见问题及其解决...
Linux 2.6内核学习笔记本仓库已经开始作为gitbook仓库,访问地址 GitHub访问地址 Something I hope you know before go into the coding~First, please watch or star this repo, I'll be more happy if you follow ...
此外,文档“移植linux-2.6.32.2到mini2440开发板(实录)——杨学鹏.doc”可能是作者在移植过程中的详细笔记,包含了具体的操作步骤、遇到的问题及解决方案,对于进行类似移植工作的人来说是一份宝贵的参考资料。...
arm部分: s3c2440的linux2.6内核移植所需修改的代码,代码都是直接从内核源代码修改 的。所以请遵守GNU通用公共许可证 一些随手记录的东西 还有一些技术名词和概念的解释 网上收集: 一些我在网上收集的一些...
Linux2.6 内核支持两种格式的 initrd,一种是前面第 3 部分介绍的 Linux2.4 内核那种传统格式的文件系统镜像-image-initrd,它的制作方法同 Linux2.4 内核的 initrd 一样,其核心文件就是 /linuxrc。另外一种格式的...