第五章--内核同步

简介:         内核的各个部分并不是严格按照顺序依次执行的,而是采用交错执行的方式。因此这些请求可能引起竞态条件,而我们必须采用适当的同步机制对这种情况进行控制。 一、内核如何为不同的请求提供服务         把内核看作必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板。
        内核的各个部分并不是严格按照顺序依次执行的,而是采用交错执行的方式。因此这些请求可能引起竞态条件,而我们必须采用适当的同步机制对这种情况进行控制。
一、内核如何为不同的请求提供服务
        把内核看作必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板。对不同的请求,侍者采用如下的策略:
        1、老板提出请求时,如果侍者正空闲,则侍者开始为老板服务。
        2、如果老板提出请求时侍者正在为顾客服务,那么侍者停止为顾客服务,开始为老板服务。
        3、如果一个老板提出请求时侍者正在为另一个老板服务,那么侍者停止为第一个老板提供服务,而开始为第二个老板服务,服务完毕再继续为第一个老板服务。
        4、一个老板可能命令侍者停止正在为顾客提供的服务。侍者在完成对老板最近请求的服务之后,可能会暂时不理会原来的顾客而去为新选中的顾客服务。(内核抢占)
        侍者提供的服务对应于CPU处于内核态时所执行的代码。如果CPU在用户态执行,则侍者被认为处于空闲状态。老板的请求相当于中断,而顾客的请求相当于用户态进程发出的系统调用或异常。
1.1、内核抢占
        如果进程正执行内核函数时,即它在内核态运行时,允许发生内核切换(被替换的进程是正执行内核函数的进程),这个内核就是抢占的。在Linux中,情况要复杂的多。
        1、无论在抢占内核还是非抢占内核中,运行在内核态的进程都可以自动放弃CPU。
        2、所有的进程切换都是由宏switch_to来完成。在抢占内核和非抢占内核中,当进程执行完某些具有内核功能的线程,而且调度程序被调用后,就发生进程切换。不过,在非抢占内核中,当前进程是不可能被替换的,除非它打算切换到用户态。
        抢占内核的主要特点:一个在内核态运行的进程,可能在执行内核函数期间被另外一个进程取代。
        使内核可抢占的目的是减少用户态进程的分派延迟(dispatch laency),即从进程变为可执行状态到它实际开始运行之间的时间间隔。内核抢占对执行及时被调度的任务的进程确实有好处的,因为它降低了这种进程被另一个运行在内核态的进程延迟的风险。
        使Linux2.6内核具有可抢占的特性无需对支持非抢占的旧内核在设计上做太大的改变,当被current_thread_info()宏所引用的thread_info描述符的preempt_count字段大于0时,就禁止内核抢占。该字段的编码对应三个不同的计数器,因此它在如下任何一种情况发生时,取值都大于0:
        1、内核正在执行中断服务例程。
        2、可延迟函数被禁止(当内核正在执行软中断或tasklet时经常如此)。
        3、通过把抢占计数器设置为正数而显示地禁用内核抢占。
        上面的原则告诉我们:只有当内核正在执行异常处理程序(尤其是系统调用),而且内核抢占没有被显式地禁用时,才可能抢占内核。此外,本地CPU必须打开本地中断,否则无法完成内核抢占。
        内核抢占可能在结束内核控制路径(通常是一个中断处理程序)时发生,也可能在异常处理程序调用preempt_enable()重新允许内核抢占时发生。内核抢占也可能发生在启用可延迟函数的时候。
1.2、什么时候同步是必须的
        当计算的结果依赖于两个或两个以上的交叉内核控制路径的嵌套方式时,可能出现竞争条件。临界区是一段代码,在其他的内核控制路径能够进入临界区前,进入临界区的内核控制路径必须全部执行完这段代码。
        交叉内核控制路径使内核开发者的工作变得复杂了:他们必须特别小心地识别出异常处理程序、中断处理程序、可延迟函数和内核线程中的临界区。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任意时刻只有一个内核控制路径处于临界区。
        例如,假设两个不同的中断处理程序要访问同一个包含了几个相关变量的数据结构,比如一个缓冲区和一个表示缓冲区大小的整型变量。所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU的系统,可以采取访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。
        另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU,就可以非常简单地通过在访问共享数据结构时禁用内核抢占功能来实现临界区。
        在多处理器系统中,情况要复杂得多。由于许多CPU可能同时执行内核路径,因此内核开发者不能假设只要禁用内核抢占功能,而且中断、异常和软中断处理程序都没有访问过该数据结构,就能保证这个数据结构能够安全地被访问。
1.3、什么时候同步是不必要的
        1、 所有的中断处理程序响应来自PIC的中断并禁用IRQ线。此外,在中断处理程序结束之前,不允许产生相同的中断事件。
        2、 中断处理程序、软中断和tasklet既不可以被抢占也不能被阻塞,所以它们不可能长时间处于挂起状态。在最坏的情况下,它们的执行将有轻微的延迟,因为在其执行的过程中可能发生其他的中断(内核控制路径的嵌套执行)。
        3、 执行中断处理的内核控制路径不能被执行可延迟函数或系统调用服务例程的内核控制路径中断。
        4、 软中断和tasklet不能在一个给定的CPU上交错执行。
        5、 同一个tasklet不可能同时在几个CPU上执行。
        简化的例子:
        1、 中断处理程序和tasklet不必编写成可重入的函数。
        2、 仅被软中断和tasklet访问的每CPU变量不需要同步。
        3、 仅被一种tasklet访问的数据结构不需要同步。
二、同步原语
        下表列出了Linux内核使用的同步技术。“适用范围”一栏表示同步技术是适用于系统中的所有CPU还是单个CPU。例如,本地中断的禁止只适用于一个CPU(系统中的其他CPU不受影响);相反,原子操作影响系统中的所有CPU(当访问同一个数据结构时,几个CPU上的原子操作不能交错)。
        表:内核使用的各种同步技术
            技术                                说明                                                            适用范围
        每CPU变量                    在CPU之间复制数据结构                                        所有CPU
        原子操作                对一个计数器原子地“读-修改-写”的指令                           所有CPU
        内存屏障                            避免指令重新排序                                    本地CPU或所有CPU
        自旋锁                            加锁时忙等                                                       所有CPU
        信号量                             加锁时阻塞等待(睡眠)                                        所有CPU
        顺序锁                            基于访问计数器的锁                                            所有CPU
        本地中断的禁止                 禁止单个CPU上的中断处理                                  本地CPU
        本地软中断的禁止              禁止单个CPU上的可延迟函数处理                         本地CPU
        读-拷贝-更新(RCU)             通过指针而不是锁来访问共享数据结构                   所有CPU
2.1、每CPU变量(per-cpu variable)
        最好的同步技术是把设计不需要同步的内核放在首位。
        内核控制路径应该在禁用抢占的情况下访问每CPU变量。
2.2、原子操作
        Linux内核提供了一个专门的atomic_t类型(一个原子访问计数器)和一些专门的函数和宏,这些函数和宏作用于atomic_t类型的变量,并当做单独的、原子的汇编语言指令来使用。在多处理器系统中,每条这样的指令都有一个lock字节的前缀。
        另一类原子函数操作作用于位掩码。
2.3、优化和内存屏障
        当处理同步时,必须避免指令重新排序。如果放在同步原语之后的一条指令在同步原语本身之前执行,事情很快就会变得失控。事实上,所有的同步原语起优化和内存屏障的作用。
        优化屏障(optimization barrier)原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令,这些汇编语言指令在C中都有对应的语句。在Linux中,优化屏障(optimization barrier)就是barrier宏。它展开为asm volatile("":::"memory")。指令asm告诉编译程序要插入汇编语言片段(这种情况下为空)。volatile关键字禁止编译器把asm指令与程序中的其他指令重新组合。memory关键字强制编译器假定RAM中的所有内存单元已经被汇编语言指令修改;因此,编译器不能使用存放在CPU寄存器中的内存单元的值来优化asm指令前的代码。注意,优化屏障并不保证不使当前CPU把汇编语言指令混在一起执行--这是内存屏障的工作。
        在多处理器系统上,在前一节“原子操作”中描述的所有原子操作都起内存屏障的作用,因为它们使用了lock字节。
2.4、自旋锁
        一种广泛应用的同步技术就是加锁(locking)。当内核控制路径必须访问共享数据结构或进入临界区时,就需要为自己获得一把“锁”。由锁机制保护的资源非常类似于限制于房间内的资源,当某人进入房间时,就把门锁上。如果内核控制路径希望访问资源,就试图获取钥匙“打开门”。当切仅当资源空闲时,它才能成功。然后,只要它还想使用这个资源,门就依然锁着。当内核控制路径释放了锁时,门就打开,另一个内核控制路径就可以进入房间。
        自旋锁(spin lock)是用来在多处理器环境中工作的一种特殊的锁。如果内核控制路径发现自旋锁“开着”,就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在周围“旋转”,反复执行一条紧凑的循环指令,直到锁被释放。
        自旋锁的循环指令表示“忙等”。即使等待的内核控制路径无事可做(除了浪费时间),它也在CPU上保持运行。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段;所以说,释放CPU和随后又获得CPU都不会消耗多少时间。
        一般来说,由自旋锁保护的每个临界区都是禁止内核抢占的。在单处理器系统上,这种锁本身并不起锁的作用,自旋锁原语仅仅是禁止或启用内核抢占。在自旋锁忙等期间,内核抢占还是有效的,因此,等待自旋锁释放的进程有可能被更高优先级的进程替代。
2.4.1、具有内核抢占的spin_lock宏

2.4.2、非抢占式内核中的spin_lock宏

2.4.3、spin_unlock宏

2.5、读/写自旋锁
        读/写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径对数据结构进行修改,读/写自旋锁就允许多个内核控制路径同时读同一数据结构。如果一个内核控制路径想对这个结构进行写操作,那么它必须首先获取读/写锁的写锁,写锁授权独占访问这个资源。当然,允许对数据结构并发读可以提高系统性能。
        每个读/写自旋锁都是一个rwlock_t结构,其lock字段是一个32位的字段,分为两个不同的部分:
        * 24位计数器,表示对受保护的数据结构并发地进行读操作的内核控制路径的数目。这个计数器的二进制补码存放在这个字段的0~23位。
        * “未锁”标志字段,当没有内核控制路径在读或写时设置该位,否则清0。这个“未锁”标志存放在lock字段的第24位。
2.5.1、为读获取和释放一个锁

2.5.2、为写获取和释放一个锁

2.6、顺序锁
        顺序锁(seqlock)与读/写自旋锁非常相似,只是它为写者赋予了较高的优先级。
        并不是每一种资源都可以使用顺序锁来保护。一般来说,必须在满足下述条件时才能使用顺序锁:
        * 被保护的数据结构不包括被写着修改和被读者间接引用的指针(否则,写着可能在读者的眼鼻下就修改指针)。
        * 读者的临界区代码没有副作用(否则,多个读者的操作会与单独的读操作有不同的结果)。
        此外,读者的临界区代码应该简短,而且写着应该不常获取顺序锁,否则,反复的读访问会引起严重的开销。在Linux2.6中,使用顺序锁的典型例子包括保护一些与系统时间处理相关的数据结构。
2.7、读--拷贝--更新(RCU)
        读-拷贝-更新(RCU)是为了保护在多数情况下被多个CPU读的数据结构而设计的另一种同步技术。RCU允许多个读者和写着并发执行(相对于只允许一个写者执行的顺序锁有了改进)。而且,RCU是不使用锁的,就是说,它不使用被所有CPU共享的锁或计数器。
        RCU关键的思想包括限制RCP的范围,如下:
        1、RCU只保护被动态分配并通过指针引用的数据结构。
        2、在被RCU保护的临界区中,任何内核控制路径都不能睡眠。
        当内核控制路径要读取被RCU保护的数据结构时,执行宏rcu_read_lock(),它等同于preempt_disable()。接下来,读者间接引用该数据结构指针所对应的内存单元并开始读这个数据结构。正如在前面强调的,读者在完成对数据结构的读操作之前,是不能睡眠的。用等同于preempt_enable()的宏rcu_read_unlock()标记临界区的结束。
        当写者要更新数据结构时,它间接引用指针并生成整个数据结构的副本。接下来,写者修改这个副本。一旦修改完毕,写者改变指向数据结构的指针,以使它指向被修改后的副本。由于修改指针值 的操作是一个原子操作,所以旧副本和新副本对每个读者和写者都是可见的,在数据结构中不会出现数据崩溃。尽管如此,还需要内存屏障来保证:只有在数据结构被修改之后,已更新的指针对其他CPU才是可见的。如果把自旋锁与RCU结合起来以禁止写者的并发执行,就隐含地引入了这样的内存屏障。
        然而,使用RCU技术的真正困难在于:写者修改指针时不能立即释放数据结构的旧副本。实际上,写者开始修改时,正在访问数据结构的读者可能还在读旧副本。只有在CPU上的所有(潜在的)读者都执行完宏rcu_read_unlock()之后,才可以释放旧副本。内核要求每个潜在的读者在下面的操作之前执行rcu_read_unlock()宏:
        * CPU执行进程切换。
        * CPU开始在用户态执行。
        * CPU执行空循环。
        对上述每种情况,我们说CPU已经过了静止状态(quiescent state)。
        写者调用call_rcu()来释放数据结构的旧副本。当所有的CPU都通过静止状态之后,call_rcu()接受rcu_head描述符(通常嵌在要被释放的数据结构中)的地址和将要调用的回调函数的地址作为参数。一旦回调函数被执行,它通常释放数据结构的旧副本。
        函数call_rcu()把回调函数和其参数的地址存放在rcu_head描述符中,然后把描述符插入回调函数的每CPU(per-CPU)链表中。内核每经过一个始终滴答就周期性地检查本地CPU是否经过了一个静止状态。如果所有的CPU都经过了静止状态,本地tasklet(它的描述符存放在每CPU变量rcu_tasklet中)就执行链表中的所有回调函数。
2.8、信号量
        内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量所保护的忙资源时,相应的进程被挂起。只有在资源被释放时,进程才再次变为可运行的。因此,只有可以睡眠的函数才能获取内核信号量。 中断处理程序和可延迟函数都不能使用内核信号量
2.8.1、获取和释放信号量

2.9、读/写信号量
        内核以严格的FIFO顺序处理等待读/写信号量的所有进程。如果读者或写者进程发现信号量关闭,这些进程就被插入到信号量等待队列链表的末尾。当信号量被释放时,就检查处于等待队列链表第一个位置的进程。第一个进程常被唤醒。如果是一个写者进程,等待队列上其他进程就继续睡眠。如果是一个读者进程,那么紧跟第一个进程的其他所有读者进程也被唤醒并获得锁。不过,在写者进程之后排队的读者进程继续睡眠。
2.10、补充原语
        类似于信号量的原语:补充(completion)。
        struct completion {
            unsigned int done;
            wait_queue_head_t wait;
        };
        与down()对应的函数叫做wait_for_completion()。wait_for_completion()接收completion数据结构的地址作为参数,并检查done标志的值。如果该标志的值大于0,wait_for_completion()就终止,因为这说明complete()已经在另一个CPU上运行。否则,wait_for_completion()把current作为一个互斥进程加到等待队列的末尾,并把current置为TASK_UNINTERRUPTIBLE状态让其睡眠。一旦current被唤醒,该函数就把current从等待队列中删除,然后,函数检查done标志的值:如果等于0函数就结束,否则,再次挂起当前进程。与complete()函数中的情形一样,wait_for_completion()使用补充等待队列中的自旋锁。
        补充原语和信号量之间的真正差别在于如何使用等待队列中包含的自旋锁。在补充原语中,自旋锁用来确保complete()和wait_for_completion()不会并发执行。在信号量中,自旋锁用于避免并发执行的done()函数弄乱信号量的数据结构。
2.11、禁止本地中断
        确保一组内核语句被当作一个临界区处理的主要机制之一就是中断禁止,即使当硬件设备产生一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保中断处理程序访问的数据结构也受到保护。然而,禁止本地中断并不保护运行在另一个CPU上的中断处理程序对数据结构的并发访问,因此,在多处理器系统上,禁止本地中断经常与自旋锁结合使用。
        宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断,宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断。汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disabled()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。
        保存和恢复eflags的内容是分别通过宏local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部变量中,随后用cli汇编语言指令把IF标志清0。在临界区末尾,宏local_irq_restore恢复eflags原来的内容。因此,只是在这个控制路径发出cli汇编语言指令之前,中断被激活的情况下,中断才处于打开状态。
2.12、禁止和激活可延迟函数
        禁止可延迟函数在一个CPU上执行的一种简单方式就是禁止在那个CPU上的中断,因为没有中断处理程序被激活,因此,软中断操作就不能异步地开始。
        如果软中断计数器是正数,do_softirq()函数就不会执行软中断,而且,因为tasklet在软中断之前被执行,把这个计数器设置为大于0的值,由此禁止了在给定CPU上的所有可延迟函数和软中断的执行。
        宏local_bh_disable给本地CPU的软中断计数器加1,而函数local_bh_enbale()从本地CPU的软中断计数器中减掉1。内核因此能使用几个嵌套的local_bh_disable调用,只有宏local_bh_enable与第一个local_bh_disable调用相匹配,可延迟函数才再次被激活。
        递减软中断计数器之后,local_bh_enable()执行两个重要的操作以有助于保证适时地执行长时间等待的线程:
        1、检查本地CPU的preempt_count字段中硬中断计数器和软中断计数器,如果这两个计数器的值都等于0而且有挂起的软中断要执行,就调用do_softirq()来激活这些软中断。
        2、检查本地CPU的TIF_NEED_RESCHED标志是否被设置,如果是,说明进程切换请求是挂起的,因此调用preempt_schedule()函数。
三、对内核数据结构的同步访问
        法则: 把系统中的并发度保持在尽可能高的程序
        系统中的并发度又取决于两个主要因素:
        1、同时运转的I/O设备数。
        2、进行有效工作的CPU数。
        为了使I/O吞吐量最大化,应该使中断禁止保持在很短的时间
        为了有效地利用CPU,应该尽可能避免使用基于自旋锁的同步原语。当一个CPU执行紧指令循环等待自旋锁打开时,是在浪费宝贵的机器周期。 由于自旋锁对硬件高速缓存的影响而使其对系统的整体性能产生不利影响
        既可以维持较高的并发度,也可以达到同步的两种方法:
        1、共享的数据结构是一个单独的整数值,可以把它声明为atomic_t类型并使用原子操作对其更新。原子操作比自旋锁和中断禁止都快,只有在几个内核控制路径同时访问这个数据结构时速度才会慢下来。
        2、把一个元素插入到共享链表的操作绝不是原子的,因为这至少涉及两个指针赋值。
3.1、在自旋锁、信号量及中断禁止之间选择
        同步原语的选取取决于访问数据结构的内核控制路径的种类。只要内核控制路径获得自旋锁(还有读/写锁、顺序锁或RCU“读锁”),就禁止本地中断或本地软中断,自动禁用内核抢占。
表:内核控制路径访问的数据结构所需要的保护
        访问数据结构的内核控制路径                单处理器保护                    多处理器进一步保护
        异常                                                      信号量                                无
        中断                                                本地中断禁止                            自旋锁
        可延迟函数                                            无                                    无或自旋锁
        异常与中断                                      本地中断禁止                            自旋锁
        异常与可延迟函数                            本地软中断禁止                          自旋锁
        中断与可延迟函数                            本地中断禁止                             自旋锁
        异常、中断与可延迟函数                   本地中断禁止                             自旋锁
3.1.1、保护异常所访问的数据结构
        竞争条件可以通过信号量避免,因为信号量原语允许进程睡眠到资源变为可用。注意,信号量工作方式在单处理器系统和多处理器系统上完全相同。
        只有在访问每CPU变量的情况下,必须显示地禁用内核抢占
3.1.2、保护中断所访问的数据结构
        假定一个数据结构仅被中断处理程序的“上半部分”访问,由于中断处理程序本身不能同时多次运行。因此,访问数据结构就无需任何同步原语
        如果多个中断处理程序访问一个数据结构,共享的数据就很容易被破坏
        在单处理器系统上,必须通过在中断处理程序的所有临界区上禁止中断来避免竞争条件。只能用这种方式进行同步,因为其他的同步原语都不能完成这件事。信号量能够阻塞进程,因此,不能用在中断处理程序上。另一个方面,自旋锁可能使系统冻结:如果访问数据结构的处理程序被中断,它就不能释放锁;因此,新的中断处理程序在自旋锁的紧循环上保持等待
        多处理器系统不能简单地通过禁止本地中断来避免竞争条件。因为即使在一个CPU上禁止了中断,中断处理程序还可以在其他CPU上执行,避免竞争条件最简单的方法是禁止本地中断(以便运行在同一个CPU上的其他中断处理程序不会造成干扰),并获取保护数据结构的自旋锁或读/写自旋锁。注意,这些附加的自旋锁不能冻结系统,因为即使中断处理程序发现锁被关闭,在另一个CPU上拥有锁的中断处理程序最终也会释放这个锁
3.1.3、保护可延迟函数所访问的数据结构
        在单处理器系统上不存在竞争条件。这是因为可延迟函数的执行总是在一个CPU上串行进行--也就是说,一个可延迟函数不会被另一个可延迟函数中断,因此,根本不需要同步原语
        在多处理器系统上,竞争条件的确存在,因为几个可延迟函数并发运行。
        软中断和多个tasklet使用自旋锁保护,一个tasklet不需要
        由软中断访问的数据结构必须受到保护,通常使用自旋锁进行保护,因为同一个软中断可以在两个或多个CPU上并发运行。相反,仅由一种tasklet访问的数据结构不需要保护,因为同种tasklet不能并发运行。但是,如果数据结构被几种tasklet访问,那么就必须对数据结构进行保护
3.1.4、保护由异常和中断访问的数据结构
        在单处理器系统上,竞争条件的防止是相当简单的,因为中断处理程序不是可重入的且不能被异常中断。只要内核以本地中断禁止访问数据结构,内核在访问数据结构的过程中就不会被中断。不过,如果数据结构正好是被一种中断处理程序访问,那么,中断处理程序不用禁止本地中断就可以自由地访问数据结构。
        在多处理器系统上,必须关注异常和中断在其他CPU上的并发执行。本地中断禁止还必须外加自旋锁,强制并发的内核控制路径进行等待,直到访问数据结构的处理程序完成自己的工作
        有时,用信号量代替自旋锁可能更好。因为中断处理程序不能被挂起,它们必须用紧循环和down_trylock()函数获得信号量;对这些中断处理程序来说,信号量起的作用本质上与自旋锁一样。另一方面,系统调用服务例程可以在信号量忙时挂起调用进程。对大部分系统调用而言,这是所期望的行为。在这种情况下,信号量比自旋锁更好,因为信号量使系统具有更高的并发度。
3.1.5、保护由异常和可延迟函数访问的数据结构
        异常和可延迟函数都访问的数据结构与异常和中断处理程序访问的数据结构处理方式类似。可延迟函数本质上是由中断的出现激活的,而可延迟函数执行时不可能产生异常。因此,把本地中断禁止与自旋锁结合起来就足够了
        异常处理程序可以通过使用local_bh_disable()宏简单地禁止可延迟函数,而不禁止本地中断。仅禁止可延迟函数比禁止中断更可取,因为中断还可以继续在CPU上得到服务。在每个CPU上可延迟函数的执行都被串行化,因此,不存在竞争条件。
        同样,在多处理器系统上,要用自旋锁确保任何时候只有一个内核控制路径访问数据结构。
3.1.6、保护由中断和可延迟函数访问的数据结构
        这种情况类似于中断和异常处理程序访问的数据结构。当可延迟函数运行时可能产生中断,但是,可延迟函数不能阻止中断处理程序。因此,必须通过在可延迟函数执行期间禁用本地中断来避免竞争条件。不过,中断处理程序可以随意访问被可延迟函数访问的数据结构而不用关中断,前提是没有其他的中断处理程序访问这个数据结构。
        在多处理器系统上,需要自旋锁禁止对多个CPU上数据结构的并发访问。
3.1.7、保护由异常、中断和可延迟函数访问的数据结构
        禁止本地中断和获取自旋锁几乎总是避免竞争条件所必须的。没有必要显示地禁止可延迟函数,因为当中断处理程序终止执行时,可延迟函数才能被实质激活,因此,禁止本地中断就足够了。
四、避免竞争条件的实例
4.1、引用计数器
        引用计数器广泛地用在内核以避免由于资源的并发分配和释放而产生的竞争条件。引用计数器是一个atomic_t计数器,与特定的资源,如内存页、模块或文件相关。当内核控制路径开始使用资源时就原子地减少计数器的值,当内核控制路径使用完资源时就原子地增加计数器。当计数器变为0时,说明该资源未被使用,如果必要,就释放该资源。
4.2、大内核锁
        在Linux2.6版本的内核中,用大内核锁来保护旧的代码(绝大多数是与VFS和几个文件系统相关的函数)。
4.3、内存描述符/写信号量

4.4、slab高速缓存链表的信号量

4.5、索引节点的信号量

目录
相关文章
|
10月前
|
Unix Linux 调度
Linux 多线程开发(附有案例代码)
Linux 多线程开发(附有案例代码)
玩转UDP用户原语,这篇文章就够了【Verilog高级教程】
玩转UDP用户原语,这篇文章就够了【Verilog高级教程】
玩转UDP用户原语,这篇文章就够了【Verilog高级教程】
|
Linux
第五章--内核同步
        内核的各个部分并不是严格按照顺序依次执行的,而是采用交错执行的方式。因此这些请求可能引起竞态条件,而我们必须采用适当的同步机制对这种情况进行控制。 一、内核如何为不同的请求提供服务         把内核看作必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板。
1383 0
|
算法 Linux 调度
第七章--进程调度
        本章讨论进程调度(schednling),主要关心什么时候进行进程切换及选择哪一个进程来运行。 一、调度策略         决定什么时候以怎样的方式选择一个新进程运行的这组规则就是所谓的调度策略(scheduling policy)。
770 0
|
算法 Linux
第八章--内存管理
        整个系统的性能取决于如何有效地管理动态内存。因此,现在所有多任务操作系统都在尽力优化对动态内存的使用,也就是说,尽可能做到当需要时分配,不需要时释放。 一、页框管理         Linux采用4KB页框大小作为标准的内存分配单元。
984 0
|
异构计算
《OpenACC并行程序设计:性能优化实践指南》一 3.7 释放主机进程
本节书摘来自华章出版社《OpenACC并行程序设计:性能优化实践指南》一 书中的第3章,第3.7节,作者:[美] 罗布·法伯(Rob Farber),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1180 0
|
JavaScript Linux
《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》——2.13 开启中断
本节书摘来自华章计算机《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理》一书中的第2章,第2.13节,作者:新设计团队著, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1107 0