一、什么是内核抢占
1.如果一个进程正在内核态执行时,允许发生内核切换,这就是内核抢占。
如果一个进程正在用户态运行,就无所谓内核抢占
2.当满足以下任意一个条件时,不允许内核抢占
(1)内核正在执行中断处理程序
(2)内核正在执行软中断或tasklet
(3)内核抢占被显式地禁止
只有在执行异常处理程序,且内核抢占没有被显示禁止时,才允许内核抢占。
note:
内核抢占会导致进程切换,或进程从一个CPU移动另一个CPU,这个在(1)和(2)中是不允许的
3.Linux内核是抢占式的,进程无论是处于内核态还是用户态,都可能被抢占
4.被抢占的进程没有被挂起,因为它还处于TASK_RUNNING状态,只不过不再使用CPU
二、临界区、嵌套的内核控制路径
1.只有在开中断的情况下才可能发生内核控制路径的嵌套
2.临界区是一段代码,在任意时刻最多只有一个内核控制处理临界区中
内核控制路径的嵌套执行,导致临界区必须被识别并通过同步来保护
3.内核控制路径的有三种:中断处理程序、异常处理程序、可延迟函数,可以组成多种不同的嵌套组合,不同的嵌套组合要通过不同的同步方式来保护
4.为了简化内核控制路径的同步,系统做了以下规定
(1)中断处理程序结束之前,不允许产生相同类型的中断 -----> 中断处理程序不必是不可重入的
(2)中断处理程序、软中断和tasklet不可以被抢占也不可以被阻塞 ------> 在单CPU的情况下可以不考虑对自己的同步问题
(3)中断处理程序不允许被可延迟函数或系统调用中断
(4)软中断和tasklet不能在一个给定的CPU上交错执行 -----> 仅被软中断和tasklet访问的每CPU变量不需要同步
(5)同一个tasklet不可能同时在几个CPU上执行 ----->仅被tasklet访问的数据结构不需要同步
三、Linux中使用了同步技术
1.每CPU变量
(1)介绍:
一个每CPU变量是一个数据结构的数组,系统的每个CPU对应数组的一个元素。
一个CPU不能访问其它CPU对应的数组元素,但可以任意读写自己的元素
(2)优点:每CPU变量能为不同CPU的并发访问自己的数据提供保护
(3)局限:
对来自异步函数(中断处理程序、可延迟函数)的访问不提供保护,这种情况下需要另外的同步原语
在访问每CPU变量时必须禁用内核抢占
2.原子操作
使一些“读-修改-写”类型的的操作成为原子级的,这些操作称为原子操作
原子操作除了可以原子地执行“读-修改-写”操作,还可以建立在其他更灵活机制的基础之上以创建临界区
3.优化和内存屏障
(1)源代码转换为汇编后,汇编指令会重新排序。如果同步原语之后的指令被排到同步原语之前会出错,因此引入屏障。
(2)所有的同步原语都有优化和内存屏障的作用。
所有的原子操作都起内存屏障的作用。
(3)优化屏障:保证编译程序不会混淆放在原语之前的汇编指令后放在原语之后的汇编指令。
内存屏障:确保屏障之后的语句操作开始之前,屏障之前的操作已完成
4.自旋锁
如果内核控制路径发现自旋锁“开着”,就获取自旋锁并继续执行。
如果自旋锁“锁着”,就在周围“旋转”,反复执行一条紧凑的循环指令(忙等),直到锁被释放
单CPU时,自旋锁仅仅是禁止或启用内核抢占。
5读/写自旋锁
(1)作用:提高对数据结构并发读
(2)原理:
只有没有内核控制路径对数据结构进行修改,读/写自旋锁就允许多个内核控制路径同时读一数据结构
如果一个内核控制路径想对这个数据结构进行写操作,就必须先获得读/写自旋锁
6.顺序锁
(1)作用:与读/写自旋锁相似,为写赋予更高的优化级
(2)原理:即使读者正在读,也允许写操作
(3)优点:写者永远不会等待
(4)缺点:读者可能反复多次读相同的数据结构获得有效的复本
7.读-拷贝-更新(RCU)
(1)作用:允许多个读者和写者并发地执行
(2)特点:
不使用锁
只保护被动态分配并通指针引用的数据结构
在被RCU保护的临界区中,任何内核控制路径不能睡眠
(3)原理
读者:间接引用该数据结构指针所对应的内存单元并开始读这个数据结构,完成读操作之前不能睡眠
写者:间接引用指针并生成整个数据结构的复本,然后修改复本。修改完成后,写者改变这个指针,使它指向修改后的指针。此时不能释放旧的复本,直到所有旧复本的读者读完
(4)使用范围:RCU主要用于网络层和虚拟文件系统
8.信号量
原理:把申请资源和释放资源封装为原子操作,当内核控制路径试图获取被信号量保护的忙资源时,进程被挂起,直到资源被释放,才会被唤醒。
特点:只有可以睡眠的函数才可以获取内核信号量。中断处理函数和可延迟函数不能使用内核信号量
9.读/写信号量
(1)特点:与读/写自旋锁相似,只是等待时等待的进程被挂起而不忙等
(2)原理:
多个读者可以并发地获取读/写信号量
写者必须对被保护资源互斥访问
只有在没有读者或写者持有信号量时,才可以获得写信号量
10.补充原语
用于解决多处理器上一种特殊的竞争关系
11.禁止本地中断
保护中断处理程序访问的数据结构
在多CPU的情况下,需要与自旋锁相结合
12.禁止和激活可延迟函数
四、针对不同的内核控制路径嵌套方式,如何选择同步原语?
1.选择同步原语的原则
(1)同步原语的选择取决于访问数据结构的内核控制路径的种类
(2)为了使I/O吞吐量最大,尽量减少禁止中断的时间
(3)为了CPU效率最高,尽量少用自旋锁
(4)只要内核控制路径获得自旋锁(或读写锁、顺序锁、RCU读锁),就禁用本地中断或本地软中断,自动禁用内核抢占
2.根据内核控制路径的嵌套方式选择自旋锁
访问数据结构的内核控制路径 | 单处理器保护 | 多处理器进一步保护 |
异常 | 信号量 | 无(1) |
中断 | 本地中断禁止(2) | 自旋锁(3) |
可延迟函数 | 无(4) | 无或自旋锁(5) |
异常+中断 | 本地中断禁止(6) | 自旋锁 |
异常+可延迟函数 | 本地软中断禁止(7) | 自旋锁 |
中断+可延迟函数 | 本地中断禁止(8) | 自旋锁 |
异常+中断+可延迟函数 | 本地中断禁止 | 自旋锁 |
分析:
(1)信号量的工作方式在单处理器和多处理器上完全相同
(2)对于只被一个中断处理程序访问的数据结构,不需要同步原语,因为每个中断处理函数相对自己串行地执行
对于会被多个中断处理程序访问的数据结构,只能禁止本地中断,因为信号量不能用于中断处理程序,而自旋锁可能会使系统冻结
(3)在多CPU情况下,自旋锁不会使系统冻结
(4)可延迟函数在一个CPU有串行地执行,不需要同步原语
(5)不同的可延迟函数处理方式不同:
软中断:自旋锁
一个tasklet:无(不能并发地执行)
多个tasklet:自旋锁
(6)中断不会被异常中断,只要保证执行异常时不会发生中断就可以了
(7)可延迟处理函数执行时不会发生异常。
或延迟函数本质上是由中断的出现而激活的。没有必须禁止中断,只要禁止软中断,就能保证执行异常时不会被可延迟函数打断
(8)可延迟函数执行时可能会产生中断