内核的并发和竞态

并发是指多个执行的单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。

1.临界区

访问共享资源的代码区称为临界区,这段代码区就是需要各种方法和机制去保护的,以防止由于竞态时发生的资源读写错误。

2.临界区保护

解决竞态问题的途径时保证对共享资源的互斥访问,所谓的互斥访问是指一个执行单元访问共享资源的时候,其它的执行单元被禁止访问。其实造成竞态的情况总的来说就三种:对称多处理器(SMP)的多个CPU、内核抢占以及中断。所以临界保护就从这三个方便防护,主要有中断屏蔽、原子操作、自旋锁、信号量、互斥体等。

3、中断屏蔽

CPU一般都具备屏蔽中断和打开中断的功能。这可以保证正在执行路径不被中断处理程序所抢占。中断屏蔽使得中断和进程之间的并发不再发生,而且由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免。

中断屏蔽API

1local_irq_disable()//屏蔽中断

2local_irq_enable() //开启中断

3local_irq_save(flags)  //除了屏蔽中断还能保存当前CPU的中断位信息

4local_irq_restore(flags) //开启中断

5local_bh_disable() //只是屏蔽中断底半部的中断

6local_bh_enable() //开启底半部的中断

以上的API都只能禁止和使能本CPU的中断,不能解决SMP多CPU引发的竞态,需要配合自旋锁配合使用。另外,Linux的异步IO、进程调度等依赖于中断,所以临界区的执行时间不宜过长。

4、原子操作

原子操作可以保证对一个整数的修改是排他性的,原子操作分为位和整数的操作。

原子整型操作API


1void atomic_set(atomic_t  *v, int i)   //设置原子变量的值为i

2atomic_t v = ATOMIC_INIT(0)  //定义原子变量v并初始化为0

3void atomic_add(int i, atomic_t *v)* //原子变量加1

4void atomic_sub(int i, atomic_t *v) //原子变量减1

5void atomic_inc(atomic_t *v)  //原子变量自增1

6void atomic_dec(atomic_t *v)  //原子变量自减1

操作并测试(注意并没有加)


1int atomic_inc_and_test(atomic_t *v)

2int atomic_dec_and_test(atomic_t *v)

3int atomic_sub_and_test(atomic_t *v)

以上函数对原子变量执行自增、自减和减1后测试其是否为0,若为0则返回true,否则返回false

操作并返回


1int atomic_add_return(int 1, atomic * v);

2int atomic_sub_return(int 1, atomic * v);

3int atomic_inc_return(atomic * v);

4int atomic_dec_return(atomic * v);

以上函数对原子变量加减/自增减后返回原子变量新的值

原子位操作


1void set_bit(nr, void *addr); //设置位,设置addr地址的第nr位,就是将位写1

2void clear_bit(nr, void *addr); //清除位,清除addr地址的第nr位,就是将位写0

3void change_bit(nr, void *addr); //改变位,将addrd地址的第nr位进行反转

4int test_bit(nr, void *addr); //测试位,返回addr地址的第nr位

5int test_and_set_bit(nr, *addr) //设置位后返回新的值

6int test_and_clear_bit(nr, *addr) //清除位后返回新的值

7int test_and_change_bit(nr, *addr) //反转位后返回新的值

5、自旋锁

自旋锁(Spin Lock)是一种典型的对临街资源进行互斥访问的方法。为了获得一个自旋锁,在某CPU上运行的代码必须先执行一个原子操作,该操作测试并设置某个内存变量,如果测试结果表明锁已经空闲,则程序获得这个自选锁;如果测试结果表明锁仍被占用,则程序在一个小循环内重复测试并设置操作,就是自旋。

自旋锁API

1spinlock_t lock; //自定义自选锁

2spin_lock_init(lock); //初始化自旋锁

3spin_lock(spinlock_t *lock); //获取自旋锁,如果能够立即获得锁,就立马返回,否则就一直等待直到锁释放

4int spin_trylock(spinlock_t *lock); //尝试获取自旋锁,能立即过去锁就返回true,若不能则返回false,也就是不会原地打转

5spin_unlock(spinlock_t *lock)//释放锁,与spin_lock、spin_trylock配合使用

自旋锁一般这样使用

spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
.....//临界区
spin_unlock(&lock);

自旋锁主要针对的是SMP和单CPU但内核可抢占的情况,对于单CPU不支持内核抢占则自旋锁实际上就是空操作。因为单CPU内核可抢占的的情况和SMP的情况类似,所以单CPU编程也需要用自旋锁。另外虽然自旋锁可以保证临界区不受本CPU和其它CPU的抢占经常打扰,但是还可能收到中断和底半部的进程打扰,所以得屏蔽掉中断和底半部,这就有了一下升级版的API:


1spin_lock_irq() = spin_lock() + local_irq_disable()
2spin_unlock_irq() = spin_unlock() + local_irq_enable()
3spin_lock_irqsave() = spin_lock() + local_irq_save()
1spin_unlock_irqrestore() = spin_lock() + local_irq_restore()
1spin_lock_irq_bh() = spin_lock() + local_bh_disable()
1spin_unlock_irq_bh() = spin_lock() + local_irq_enable()

特别注意,以上的API都是对本CPU的中断做相应的屏蔽,不能屏蔽其它CPU的中断。在单CPU下,进程和中断可能会访问同一个临界区,在进程调用以上API没有问题,因为可以屏蔽中断,但是在多核的时候,本CPU的中断虽然屏蔽了,但是其它的CPU的中断并没有屏蔽,所以不论如何,都需要在中断服务程序加入屏蔽中断的自旋锁spin_lock()

读写自旋锁

顺序锁

6、读-复制-更新

7、信号量

信号量(Semaphore)是操作系统最经典的用于同步于互斥的方式,信号量的值可以是0、1或是n。信号量与操作系统中的经典概念PV操作对应。当信号量S的值大于零时,改进程继续执行,如果S的值为零,则该进程置为等待的状态,排入信号量大的等待队列,直到V操作唤醒。

信号量的API

1struct semaphore sem; //定义一个信号量

2void sema_init(struct semaphore *sem, ini val); //初始化信号量

3void down(struct semaphore *sem); //获取信号量,会导致睡眠,进入休眠后信号不能打断因此不能在中断上下文使用

4int down_interruptible(struct semaphore *sem);  //进入休眠的进程能别信号打断,返回非零

5int down_trylock(struct semaphore *sem);
//尝试获取信号量,如果能立马获取信号量则返回0,否则返回非0,该函数不会导致休眠,故可以用于中断上下文

6void up(struct semaphore *sem);//释放信号量,唤醒等待者

信号量也可以用于同步,一个进程A执行down()等待信号量,另外一个进程B执行up()释放信号量,这样进程A就同步地等待了进程B。

8、互斥体

虽然信号量已经实现了互斥功能,但是新的Linux内核更倾向于直接使用mutex作为互斥手段

互斥体API

1struct mutex my_mutex; //定义一个互斥体

2mutex_init(&my_mutex); //初始化互斥体

3void mutex_lock(struct mutex *lock); //获取互斥体

4int mutex_lock_interruptible(struct mutex *lock); //进入休眠的进程能别信号打断,返回非零

5int mutex_trylock(struct metex *lock);//获取不到互斥体不会进入睡眠

6void mutex_unlock(struct mutex *lock); //释放互斥体

互斥体一般如下使用:

struct mutex my_mutex;
mutex_init(&my_mutex);
metex_lock(&my_mutex);
....... //临界区
mutex_unlock(my_mutex);

9、完成量

Linux提供了完成量(Cpmpletion),用于一个执行单元等待另一个执行单元执行完某事

1、定义完成量

struct compleetion my_completion;

2、初始化完成量

init_completion(&my_completion) ;//初始化完成量值为0

reinit_completion(&my_completion); //重新初始化完成量值为0

3、等待完成量

void wait_for_completion(struct cpmpletion *my_completion);

4、唤醒完成量

void complete(struct complete *my_completion);//只唤醒一个等待的执行单元

void complete_all(struct complete *my_completion);//唤醒所有的等待同一个完成量的执行单元

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值