linux内核互斥机制之综述

这篇笔记并不打算具体介绍Linux内核提供的各种同步机制,而是对与同步机制相关的一些概念做个概述,然后概括性的描述一下Linux内核所提供的各种同步机制之间的特点以及它们之间的区别。

1. 概述

首先要谈的是为什么会需要同步机制?假设内存中有一个int型变量,那么通常,对该数据的写操作分为(1)从内存中读出数据(2)修改数据(3)然后写回内存,这三步是顺序执行的。如果只有一个执行流操作该数据,那么一切都是很顺利的,不会什么问题,但是Linux2.6内核是多进程并且还支持多CPU的系统,所以可能会发生多个执行流同时访问该变量的情况。假设目前有ProcessA和ProcessB两个执行流都要访问该变量(假设变量的初值为10),它们的访问时序刚好如下图所示:
在这里插入图片描述

显然,如果ProcessA和ProcessB按照图中所示时序执行完后,该变量的值为11,可是,理论上该值应该是12才对呀,像这种由于多个执行路径对共享数据进行操作时导致数据(更准确的讲叫资源,数据仅仅是资源的一种,也有可能是硬件资源)紊乱的现象我们称之为竞态。而把访问共享资源的代码段称为临界区。而把导致出现多个执行路径的因素称为并发源。这三个概念是同步机制(不仅仅是Linux内核,只要有并发的地方,这些概念都是适用的)的基本概念,一定要牢记。

1.1 linux内核中的并发源

下面我们看看linux 2.6内核中存在哪些并发源,理解这些并发源对我们在实际中使用哪一种互斥方法至关重要。

  1. 中断处理函数。准确的讲,这里包含两个部分,中断处理的上半部和下半部。即时在单CPU场景,一段内核代码也有可能被中断打断,如果这段代码和中断处理程序都会访问同一资源,那么也是需要进行互斥保护的;
  2. 调度器的抢占特性。2.6内核的一大特点就是支持内核态抢占,这意味着内核代码在执行过程也可能会随时被调度出去,然后运行另外一个进程的代码,如果该进程再次执行到了临界区,所以对于这种情况,即使在单CPU系统中,也需要对共享资源进行互斥访问;
  3. 多CPU之间的并发。这才是真正意义上的并发,系统中同时有多个进程在执行,这些进程可能会同时运行同一段内核代码,所以临界区必须要进行互斥;

需要说明的是,上面这三个并发源并不是每个Linux内核都会全部包含。如果我们的硬件只有一个CPU,那么就不存在并发源(3);如果内核在编译时指定不支持内核态抢占,那么并发源(2)也不会出现;不过,一般来说,并发源(1)总是存在的吧,现如今没有中断的系统还能用吗?

了解了并发源,那么就来谈谈如何解决这些并发源导致的竞态。简单来说,解决竞态的方法就是让所有临界区不要同时执行,保证它们对共享资源的顺序访问,这样就不会导致竞态。下面宏观的介绍一下Linux内核所提供的各种同步机制是如何实现对共享资源的互斥访问的。

2. 原子操作

所谓原子操作,就是说该操作一旦开始就不会被其它任何操作或事件打断。所以这种操作本身就决定了这种操作不会出现竞态。表面上看来,这东西好啊,用这种方式就可以解决所有的并发问题了。可是,原子操作只能操作一个整数或者是某一个位,它可以很好的解决像上面例子中的现象。不过,假如我们现在要保护一个链表,那么对该链表的插入操作肯定不会像例子中那样简单,首先要找到插入的地方,然后处理被插入节点与插入点前后的指针关系,这样的操作就比较复杂了,原子操作显然无能为力,所以对于复杂场景,我们需要寻求其它方法。

3. 自旋锁

因为其适用性强,所以自旋锁应该是内核同步机制中使用最多的一种。它有两种状态,锁状态和开锁状态,如果执行流在获取锁的时候锁处于开锁状态,那么它继续执行,否则就会一直忙等即自旋直到其能够获取到锁然后继续执行,这种自旋状态并不会释放CPU。需要同步的执行流在访问共享资源之前获取锁,然后进入临界区,离开临界区时释放锁,这样就可以实现对共享资源的互斥访问。自旋锁通过这样的机制来实现互斥:首先,所有的执行流在访问共享资源时都遵守上述的流程,那么解决了并发源(3);此外,持有自旋锁时,内核态抢占是被关闭的,所以解决了并发源(2);最后,如果需要和中断处理部分进行同步,则在获取锁时可以选择关闭中断,这样并发源(1)也就解决了。其中禁止内核态抢占内核帮忙完成的,另外两种需要我们调用不同的API函数自己完成,这些内容见自旋锁笔记。

4. 读写自旋锁

这是自旋锁的一种变体。自旋锁在加锁时并没有考虑持锁后是要对共享资源做读操作还是读写操作,但是有时候,如果在加锁后只是想要对共享资源进行读操作,那么这样的若干个执行流是可以并行执行的,在这种情况下,普通的自旋锁无疑会降低系统性能。但是上述的写操作还是需要进行互斥的。为了适用于这种情况,Linux内核引入了读写自旋锁:它允许任意数量的读操作同时进行,但是只允许同时有一个写操作,在写操作获取锁的同时,其它的读操作也会自旋。

5. 信号量

这里信号量的概念和用户空间信号量的概念是一致的,但是二者之间没有任何的联系。信号量的特点就是可能会使调用它的进程睡眠,可是在内核中,并不是任何时候都可以睡眠的。(1)持有自旋锁时就不可以睡眠;(2)持有信号量睡眠时要特别小心的睡眠(可能会死锁);(3)在中断上下文不能睡眠。正是睡眠限制了信号量的使用范围。信号量维护一个计数值,当进程获取信号量时,如果该值减1,如果仍大于等于0,则进程继续执行,否则睡眠。当进程释放信号量时,该值加1,如果结果大于0,则唤醒等待该信号量的进程。使用信号量保护共享资源的方式和自旋锁相同(获取—临界区—释放),不在赘述。

6. 读写信号量

概念上类似于读写自旋锁,只不过这里使用的是信号量,不在赘述。

7. 互斥锁

概念上就是信号量的初值为1的情况,只不过Linux对于互斥锁做了优化,不过对于驱动开发者,这些细节不重要了,这里不在赘述。

没有更多推荐了,返回首页