linux5.05内核同步实现机制
1 临界区
2 内核同步原语
>> 每cpu变量、volatile关键字、屏障、atomic变量、禁中断、禁抢占、自旋锁、读写锁、顺序锁、信号量、互斥锁、cru
1 临界区
1.1 临界区定义
>> 临界资源是指同一时刻只允许一个进程使用的共享资源,可以是一个变量、一段程序或者是有限的设备。而每个进程中访问临界资源的那段代码就称为临界区。
1.2 由此带来的问题
>> 需要产生某种机制,用来决定某一时刻由哪个进程来使用临界资源,内核中采用各种同步原语来解决这一问题。
2 同步原语
>> 同步原语有很多类型,有各自的使用场景,理解它们原理和特点才能做出最好的选择。它们的根本目的都是为了使临界区互斥的访问临界资源。
2.1 每cpu变量
2.1.1 定义
>> 多核系统中,每个cpu保存一份只有自己能访问的变量。从逻辑上可以将其理解为数组,例如,使用数组A[3]保存三个cpu的变量b,A[0]保存cpu0的变量b,A[1]保存cpu1的变量b,A[2]保存cpu2的变量b,这三个元素有不同的内存地址。但每cpu变量的使用和数组是不同的。
2.1.2优点
>> 因为每cpu变量对每个cpu来说,有不同的内存地址,所以cpu之间对每cpu变量的访问,不用考虑同步的问题。
2.1.3 内核中的实现
(1)定义
静态定义:
在链接过程中被放置在指定secton,这些section起始于__per_cpu_start,结束与__per_cpu_end。内核启动时被复制到内存中,同时预留部分内存供动态申请每cpu变量。
描述 | 函数 |
---|---|
定义类型为type,名字为name的每cpu变量 | DEFINE_PER_CPU(type, name) |
声明为每cpu变量 | DECLARE_PER_CPU(type, name) |
动态定义:
程序执行过程中进行分配和回收。
描述 | 函数 |
---|---|
申请每cpu变量 | alloc_percpu(type) |
释放每cpu变量 | free_percpu(type) |
2.1.4 内核路径
include/linux/percpu.h
mm/percpu.c
2.2volatile关键字
2.2.1 防止编译器优化后删除变量操作
(1)不使用volatile时
char a;
a = 1;
a =0;
编译器优化后,汇编语句中可能会将这两句赋值语句优化掉,即不会产生汇编语句。如果a是对寄存器的操作,a=1将电平拉高,a=0将电平拉底,以产生某种时序,这种情况下,编译器优化后就达不到想要的效果。
(2)使用volatile时
volatile char a;
a = 1;
a = 0;
编译器不会将这两句赋值语句优化掉,会产生汇编语句。可以达到产生时序的目的
2.2.2防止编译器优化后读取变量值错误
(1)不使用volatile
int b,c;
int *a = (int *)0x300000000;
b = *a;
c = *c;
return c + b;
编译器优化后,产生汇编代码,将a的值存在r0寄存器中,b的值存在r1中,最后返回的结果会被汇编为r1中的值乘2。即获取的是变量a在寄存器r1中的缓存值。而r0中的实际值可能已经变化。
(2)使用volatile
int b, c;
volatile int *a = (int *)0x30000000;
b = *a;
c = *a;
return c + b;
b、c两次都从a的r0中读取。
2.3屏障
某些情况下,cpu需要按顺序访问内存
2.3.1barrier
>> 实际上不属于内存屏障,供编译器使用,不产生cpu代码。语句前后需要重新读写内存。
2.4atomic变量
>> 多线程环境中,进行原子性操作,atomic保护的临界范围只是一个变量。atomic相关的函数,大多与实现平台相关,各函数的操作负责实现它的原子性。单独的读、写操作都是原子性的,但涉及到读修改写的动作都需要加上lock。
typedef struct{
volatile int counter;
}atomic;
2.5禁中断
>> 中断发生以后,以下情况需要禁止中断:
(1)在必要的处理之前不能被打断
(2)处于某些临界状态时,不想被打断
(3)某设备处理自身中断,处理完毕前不能接受来自自身的新中断
>> 禁止中断分为两种情况:
(1)禁止本地cpu的可屏蔽中断(硬中断)
(2)禁止本设备对应的中断(经过操作系统映射的中断)
函数 | 描述 |
---|---|
local_irq_disable() | 禁止本地cpu的可屏蔽中断 |
local_irq_enable() | 使能本地cpu的可屏蔽中断 |
disable_irq(irq) | 禁止irq号对应的中断 |
enable_irq(irq) | 使能irq号对应的中断 |
2.6禁止抢占
>> 以下情况需要禁止抢占
(1)cpu正在处理硬中断,不能执行软中断,也不能随意切换进程
(2)当前进程正在执行关键程序,不允许在该过程中切换进程
>> 在x86平台中使用每cpu变量__preempt_count来决定是否可以抢占,当该值为0时,才可以抢占当前进程。在非抢占式内核中,进程只有在从内核返回用户空间和主动放弃执行时才会被调度。但在抢占式内核中,内核态的进程可以主动禁止被抢占或使能内核抢占,若当前处于中断、软中断等状态时不会发生内核抢占。
2.7自旋锁
>> 自旋锁相关函数一般需要配对使用。spin_lock_irq和spin_lock_irqsave都会禁止本地中断,区别在于spin_lock_irqsave会保存中断的原状态,在使用spin_unlock_irq和spin_unlock_irqrestore使能本地中断后,irqsave和irqrestore恢复中断原始状态,spin_unlock_irq会使能所有irq,之前没有被开启的irq也可能会被使能。
>> 所有的锁相关的函数,都会禁止内核抢占,在申请成功之前都会进行忙等,所以使用自旋锁保护的临界区应尽可能小,以提高cpu利用率。
2.8读写锁
>> 读写锁的目的是让读者可以一起读,写者依然要单独写。使用读写锁和自旋锁要注意的问题基本相同,需要说明的是读写锁有可能造成写饥饿,如果读者获得读锁后一直不释放,那么写无法获得锁。
2.9顺序锁
>> 可以解决读写锁中写饥饿的问题。
2.10信号量
>>== 自旋锁、读写锁、顺序锁在申请锁的过程中都是忙等的,即使此刻不能获取锁,也不会释放cpu,这在很多场景中是必须的,如中断服务例程中==。但在不需要忙等的情况下,需要让出cpu让其他进程进行,可以提高cpu利用率,信号量和互斥锁可以使用在这类场景中。
>> 信号量在内核中使用semaphore和semaphore_waiter两个结构体实现,二者是一对多的关系。
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
bool up;
};
>> 进程申请信号量时,如果count字段值大于0,则成功获取信号量,并将count减1。如果count值等于0,说明信号量被用光,进程需要排队,以进程本身的信息初始化一个waiter,加入到waiter队列尾部,当up字段值为true时,可以执行task,如果up字段为false,则需要执行schedule让出cpu。释放信号量时,如果waiter队列为空,则count加1,否则将waiter队列的第一个waiter的up字段置为ture,唤醒task字段表示的进程。
>> 信号量主要有三种使用场景
(1)实现互斥,count最大值为1时,可以保证同一时刻只能有一个进程获取信号量。
(2)第二种情况贴近现实场景。例如招聘面试中,四个面试官同时面试,在同一时刻最多只能有四名应聘者面试,当一个应聘者面试完毕后,如果还有人在排队,则叫下一个应聘者面试,如果没有应聘者了,则count+1,协调下一个新来的应聘者。
(3)实现事件的同步
2.11互斥锁
>> 互斥锁是专门实现互斥的,在内核中涉及到mutex和mutex_waiter,二者是一对多关系。和semaphore、semaphore_waiter类似。