Linux内核设计与实现学习笔记(九-十)

(九)内核同步介绍

9.1临界区和竞争条件

临界区:访问和操作共享数据的代码段。多个执行线程并发的访问同一资源通常是不安全的,为了避免在临界区中并发的访问,必须保证这些代码原子的执行。避免并发和防止竞争条件称为同步。

9.2加锁

以请求队列为例,可以使用一个单独的锁进行保护,每当有一个新请求要加入队列,线程会首先站住锁,这样就可以安全的将请求加入请求队列中,结束操作后在释放锁。可见锁机制可以防止并发执行,并且保护队列不受竞态条件的影响。

造成并发执行的原因:用户空间需要同步,因为用户程序会被调度程序抢占和重新调度。

可能造成并发执行的原因:

  • 中断——可以在任何时刻异步发生,也就可以随时打断正在执行的代码
  • 软中断和tasklet——内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
  • 内核抢占——内核具有抢占性,内核中任何可能会被另一任务抢占;
  • 睡眠及用户空间的同步——在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
  • 对称多处理——两个或多个处理器可以同时执行代码。

我们要保护的不是代码,而是数据资源。

9.3死锁

如果一个执行线程试图获得一个自己已经持有的锁,他将不得不等待该锁的释放,此时会形成死锁(自己的资源无法释放,有请求一个无法释放的资源)。有些内核提供递归锁防止自死锁现象,Linux不提供递归锁。

如何避免死锁?

  • 按顺序加锁。使用嵌套的锁时,必须保证以相同的顺序获得锁,这样可以阻止部分类型的死锁
  • 防止饥饿问题。如:“食堂“没有饭,“我”要一直等待下去么?
  • 不要重复请求同一个锁
  • 设计力求简单——越复杂的加锁方案越可能造成思索。

9.4争用和扩展性

锁的争用:是指锁在被占用时,其他线程试图获得该锁。被高度争用的锁会成为系统的瓶颈,严重降低系统性能。

扩展性:系统可扩展程度的一个度量。

(十)内核同步方法

10.1原子操作

原子操作:不能够被分割的指令。

10.2自旋锁(Spinlock)

自旋锁:自旋锁最多只能被一个可执行线程持有。

如果一个执行线程试图获得一个已经被持有的自旋锁,那么该县城就会一直进行忙循环-旋转-等待锁重新可用。在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区,同一个锁可以用在多个位置。

一个被征用的自旋锁使得请求他的执行线程在等待锁可用时自旋(特别浪费处理器时间)。因此对于长期持有锁的活动,不应该采用自旋锁。

另一种方式是,让请求线程睡眠,直至锁可用时唤醒。但是这样会带来一定的开销——有两次上下文切换,被阻塞的线程要换入和换出。因此持有自旋锁的时间最好小于完成两次上下文切换的耗时。

自旋锁的代码定义在文件<asm/spinlock.h>和<linnux/spinlock.h>中。

注意:如果禁止内核抢占,在编译时自旋锁会被完全剔除出内核。

此外自旋锁也可用于中断处理程序中(不能使用信号量,信号量会导致睡眠,此时可能会导致死锁)。因此,在中断处理程序中使用自旋锁时,一定要在获取锁之前禁止本地中断。

可以使用spin_lock_init()动态创建自旋锁;

spin_try_lock()视图获取某个特定的自旋锁,如果该锁已经被争用,那么该方法会立刻返回一个非0值;成功获得自旋锁则返回0。该函数只做判断,并不实际占用自旋锁。

自旋锁分为读写自旋锁:DEFINE_RWLOCK(mr_lock),read_lock(&mr_rwlock);write_lock(&mr_rwlock);读自旋锁不可以升级为写自旋锁,这样会导致死锁。

10.3信号量

Linux中的信号量是一种睡眠锁。如果一个任务试图获得一个不可用的信号量,信号量会将其放入等待队列睡眠。当信号量被释放时,会唤醒等待队列上的任务,并获得该信号量。

  • 信号量在等待信号量可用时会睡眠,因此适用于锁会被长时间持有的情况
  • 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中获取信号量,在中断上下文中是不可调度的。
  • 占用信号量的同时不能占用自旋锁,自旋锁禁止睡眠。

与自旋锁不同,信号量也分为读写锁,但区别是,读信号量可以转化为写信号量。

信号量是内核中唯一允许睡眠的锁。

10.4互斥体

静态定义mutex:DEFINE_MUTEX(name);

动态定义:mutex_init(&mutex);

mutex_lock(&mutex);

mutex_unlock(&mutex);

mutex_trylock(&mutex),获得指定的mutex,成功返回1,失败返回0;

与自旋锁相同,任意时刻只有一个任务可以持有mutex

  • 给mutex上锁者必须负责给其解锁——不能再一个上下文中锁定一个mutex,在另一个上下文中给其解锁。
  • 持有一个mutex时,进程不可以退出。
  • mutex不能再中断或者下半部中使用,mutex_trylock()也不行

自旋锁和互斥体:中断上下文只能使用自旋锁,任务睡眠只能使用mutex

10.5大内核锁(高版本内核已弃用)

BKL是一个全局自旋锁,与自旋锁不同:持有BKL的任务可以睡眠;BKL是一种递归锁,一个进程可以多次请求同一个锁不会造成死锁;BKL只可以用在进程上下文。

10.6顺序和屏障

在多处理器上编译器和处理器为了提高效率,可能会对读和写重新排序,这样就会导致输入的数据乱序。

rmb()方法提供了一个“读”内存屏障,他确保跨越rmb的载入动作不会发生重排序。也就是说,发生在rmb之前的载入操作不会在rmb之后重新调用。

wmb()方法提供了一个“写”内存屏障,功能和rmb相同。

mb()方法及提供了读屏障也提供写屏障。载入和存储动作都不会跨越屏障重新排序。

read_barrier_depends()是rmb的变种,在某些体系结构上,比rmb执行的要快,实际它仅仅是个空操作,实际并不需要。

barrier()方法可以防止编译器跨屏障对载入或存储操作进行优化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值