layout | title | category | description | tags |
---|---|---|---|---|
post |
内核抢占 |
内核同步 |
内核抢占... |
内核抢占 临界区 读写安全 |
我们可以把内核看作是不断对请求响应的一个服务器,这些请求可能来自在CPU上执行的进程,也可能来自发出中断请求的外部设备。内核的各个部分不是严格按照顺序依次自行的,而是采用交错执行的方式,这和进程切换的感觉是一样。所以,这些请求可能引起竞态条件,所以内核需要对这种情况进行适当的控制,这需要一些同步机制。
内核可以被看作是必须满足两种请求的服务器,一种请求来自中断,一种来自进程。
- 如果产生了一个中断,内核如果正在执行用户空间进程,那么必须响应中断,不过通常情况下这种情况比较少。
- 如果内核正在执行一个进程,无论是处于用户空间还是内核空间,一旦产生了中断,就立即停止服务,响应中断。
- 如果一个中断提出请求的时候内核正在处理另一个中断,那么内核就暂停中断,处理刚刚产生的中断,然后再处理之前的中断。有时候,中断也有优先级,如果处理的中断有最高的优先级,而新的中断可以被延迟,那么就先处理最高优先级的中断。
- 一个中断可以改变内核服务的对象,当内核正在执行处理一个进程A,中断可以改变内核服务对象,改变后,进程可能被切换到B。
内核抢占的概念比较复杂,无法精确的下一个定义,我们可以看做一件事情正在执行过程中,老板让你去左另外一件事情,那么可以说你是可以被抢占的,但内核抢占的概念实际上要复杂更多:
无论在抢占内核还是非抢占内核中,运行在内核态的进程都可以自动放弃CPU,比如,进程可能由于等待资源而不得不进入睡眠状态,如I/O请求,这个时候进程可以自己放弃CPU。我们把这种进程切换称为计划性进程切换。但是,抢占式内核在响应引起进程切换的异步时间的方式上与非抢占内核是有差别的,抢占式的进程切换可以被称作强制性进程切换。
所有的进程切换都是由switch_to来完成的,在抢占内核和非抢占内核中,当执行完某些具有内核功能的线程,而且调度程序被调用后,就发送进程切换,不过,在非抢占内核中,当前进程是无法被切换掉的。
因此抢占性内核的主要特点是,一个内核态运行的进程,可能在执行内核函数期间被另外一个进程取代。
让内核可抢占的目的是减少用户态进程的分派延迟(dispatch latency),即从进程变为可执行状态它实际开始运行之间的时间间隔。内核抢占对执行及时被调度的任务的进程确实是有好处的,因为它降低了这种进程被另一个运行在内核态的进程延迟的风险。
我们知道preempt_count字段大于0时,就禁止内核抢占。这个字段的编码对应三个不同的计数器1,因此它们在如下任何一种情况发生时,都会禁止内核抢占,而值会大于0。
- 内核正在执行中断服务例程。
- 可延迟函数被禁止,当内核正在执行软中断或者tasklet的时候。
- 通过把抢占计数器设为正数显式地禁止内核抢占。
所以,只有当内核正在执行异常处理程序,尤其是系统调用,而且内核抢占没有被指明显式地被禁用时,才可能抢占内核。另外,本地CPU必须打开本地中断,否则无法完成内核抢占。
下面列举了一些用于抢占计数器字段的宏:
宏 | 说明 |
---|---|
preempt_count() | 在thread_info描述符中选择preempt_count字段 |
preempt_disable() | 让抢占计数器的值加1 |
preempt_enable_no_resched() | 让抢占计数器的值减1 |
preempt_enable() | 让抢占计数器的值减1,并且进行相关的处理2 |
get_cpu() | 同preempt_disable,但要返回CPU的数量 |
put_cpu() | 同preempt_enable,但要返回CPU的数量 |
put_cpu_no_resched() | 同preempt_enable_no_resched() |
内核抢占会引起内核开销,并且不是可以视而不见的开销,所以Linux可以允许用户在编译内核时通过设置选项来禁用或者启用内核抢占。
虽然并不是所有的情况都需要内核同步,但对于重要的数据结构,内核需要保证读写安全。我们知道内核有竞争条件和进程临界区的概念,这些情况在内核控制路径中同样也是如此。
当计算的结果依赖于两个或两个以上的交叉内核控制路径的嵌套方式时,可能出现竞争,我们称作竞态条件。临界区是一段代码,在其它内核控制路径能够进入临界区之前,进入临界区的内核控制路径必须全部执行完这段代码。
交叉内核控制路径使内核的开发更加复杂,我们必须特别小心地识别异常处理程序,中断处理程序,可延迟函数和内核线程中的临界区。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任意时刻只有一个内核控制路径处于临界区。
假设两个不同的中断处理程序要访问同一个包含了几个相关变量的数据结构,比如一个缓冲区大小的整型变量,所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU系统,可以参去访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。
另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU。就可以非常简单的通过在访问共享数据结构中禁止内核抢占的功能来实现临界区。但是,在多处理器系统中这个情况要复杂的多。
由于许多CPU可能同时执行一个内核控制路径,所以不能假设只要禁用内核抢占功能,而且中断,异常和软中断处理程序都没有访问过数据结构,就能够保证这个数据结构可以安全的访问。这需要更多的判断条件。多CPU的内核同步是一个复杂的情况,我们可以通过使用禁止本地中断或者自旋锁的方式来实现,后面会一点一点的记录。
内核同步中有几个同步技术需要了解,这些都是内核中重要的同步技术:
技术 | 说明 | 适用范围 |
---|---|---|
per-CPU变量 | 在CPU之间复制数据结构 | 所有CPU |
原子操作 | 对一个指令原子地读写和修改的指令 | 所有CPU |
内存屏障 | 避免指令重新排序 | 本地或所有CPU |
自旋锁 | 加锁时忙等 | 所有CPU |
信号量 | 枷锁时阻塞等待 | 所有CPU |
顺序所 | 基于访问计数器的锁 | 所有CPU |
禁止本地中断 | 禁止单个CPU上的中断处理 | 本地CPU |
禁止本地软中断 | 禁止单个CPU上的可延迟函数处理 | 本地CPU |
读写拷贝的更新(RCU) | 用指针而不是锁来访问共享数据结构 | 所有CPU |
这些同步技术都会在后面单独记录笔记。