做人呢,最紧要就系开心啦

linux源码解析16- Linux内核常用锁机制总结

1,998次阅读
没有评论

首先看看 Linux 内核中的并发场景;

单 CPU 多进程系统,产生并发访问因素有:

  • 中断处理程序可以打断软中断,tasklet 和进程上下文;
  • 软中断和 tasklet 之间不会并发,但可以打断进程上下文;
  • 在支持抢占的内核中,进程上下文之间会并发;
  • 在不支持抢占的内核中,进程上下文不会并发;

在多 CPU 和多进程系统中,产生并发访问因素:

  • 同一类型的中断处理程序不会并发,但不同类型的中断有可能分发到不同 CPU 上响应,可能产生并发;
  • 同一类型的软中断会在不同 CPU 上并发;
  • 同一类型的 tasklet 是串行执行的,不会在多个 CPU 并发;
  • 不同 CPU 上的进程上下文会并发;

使用锁的原则:

保护资源或者数据,而不是保护代码;

Linux 内核中的锁机制

1. 自旋锁

特点:临界区不允许调度和睡眠,禁止抢占;解锁恢复抢占,可以用于进程上下文,中断上下文;

自旋锁主要用途是多处理器之间的并发控制,适用于竞争不太激烈的场景。如果竞争激烈,大量时间浪费在自旋锁,导致整体性能下降。

内核提供 API:

spin_lock(lock);// 加锁,成功后返回,否则自旋等待

// 当锁进程和中断,有并发访问时,关闭中断
spin_lock_irqsave(lock, flags); // 加锁,并关闭中断
spin_lock_bh(lock);             // 加锁,并关闭软中断

spin_unlock(lock);// 解锁

spin_unlock_irqrestore(lock, flags);    // 解锁,打开硬中断
spin_unlock_bh(lock);                  // 解锁,打开软中断 

ps: 为什么有的代码用 spin_lock(), 有的代码使用 raw_spin_lock()?

实时补丁 RT-patch, spinlock 变成可抢占和睡眠的锁;
在绝对不允许被抢占和睡眠的临界区,使用 raw_spin_lock,否则使用 spinlock;

驱动工程师应该谨慎使用自旋锁:

  • 自旋锁实际上是忙等待,当临界区很大时,或有共享设备的时候,需要较长时间占用锁,会降低系统的性能;
  • 自旋锁可能导致系统死锁,一般是由递归使用一个自旋锁引起;
  • 持有自旋锁期间,不能调用可能引起进程调度的函数;
    copy+from/to_user(),kmalloc()msleep 等;
  • 在单核下编程,也要考虑多核场景。比如进程持有 spin_lock_irqsave(), 单核情况下中断不调用 spin_lock 也没问题,因为 spin_lock_irqsave() 保证了这个 CPU 中断服务程序不会执行。
    但是在多核环境,spin_lock_irqsave() 不能屏蔽另外一个核的中断,另外一个核就可能造成并发问题;

2. 读写自旋锁:rwlock_t

允许多个读者同时持有锁;
只允许一个写者同时持有锁;

读写锁适合读者多,写者少的应用场景;

内核支持 API:

DEFINE_RWLOCK(lock);

rwlock_init(lock);

read_lock(lock);
write_lock(lock);
read_unlock(lock);write_unlock(lock);

3. 信号量:

信号量是多值的,当其用作二值信号时,类似于锁,一个值代表未锁,另一个代表已锁;

原理:
获取锁的过程中,若不能立即得到,会发生调度,进入睡眠;

特点:
锁的竞争不是忙等,信号量临界区允许调度和睡眠而不会导致死锁;
锁的竞争者会转入睡眠,让出 CPU,因此锁的竞争不会影响系统整体性能;

内核执行路径释放锁时,唤醒等待该锁的执行路径;

自旋锁 vs 信号量:

  • 自旋锁问题:
    持有自旋锁的临界区不允许调度和睡眠,竞争激烈时整体性能不好;

  • 信号量缺点:
    中断上下文要求整体运行时间可预测 (不能太长),而信号量临界区允许睡眠,可能发生调度,因此不能用于中断上下文;

如果抢锁的过程很短,那么信号量不划算,因为进程睡眠加上唤醒代价太大,消耗 CPU 资源可能远大于短时间忙等待;

内核的 semaphore 实现:

struct semaphore{
    raw_spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
}

wait_list 字段当信号量为忙时,所有等待信号量的进程列表,而 lock 则是保护 wait_list 的自旋锁;

信号量的 API:

DEFINE_SEMPHORE(sem); // 静态定义 sem 信号量
void sema_init(struct semaphore *sem, int val);  /// 初始化一个信号量 sem,计数初值为 val
void down(struct semaphore *sem);               // 减少信号量 sem 计算器,如果失败 (count 已经为 0), 转入睡眠, 不能被信号唤醒 (TASK_UNINTERRUPTIBLE), 并把当前进程挂到 wait_list;被唤醒后继续尝试获取锁
void up(struct semaphore *sem);                 // 增加信号量 sem 计数器 (类似释放锁), 然后唤醒 wait_list 里的第一个进程 (如果有的话);int down_interruptible(struct semaphore *sem);// 可以被信号唤醒,驱动推荐使用
int down_trylock(struct semaphore *sem);// 立即返回
void down_timeout(struct semaphore *sem,long timeout);//timeout 超时返回 

4. 读写信号量

类似读写自旋锁,为了区分不同的竞争者,比如允许读者共享,而写者互斥

struct rw_semaphore{
    long count;
    struct list_head wait_list;
    raw_spinlock_t wait_lock;
}

DECLARE_RWSEM(sem);// 静态声明 sem 变量
init_rwsem(sem);// 初始化一个 sem

down_read(struct rw_semaphore *sem);     // 读者减少信号量 sem 计算器,类似获取锁
down_write(struct rw_semaphore *sem);   // 写者减少 sem 计数器
up_read(struct rw_semaphore *sem);     // 增加 sem 计数器
up_write(struct rw_semaphore *sem);   // 增加 sem 计数器 

5. 互斥体:

本质上是二值的信号量;

struct mutex{
    atomic_t count;
    spinlock_t wait_lock;
    struct list_head wait_list;
}

DEFINE_MUTEX(mutex);// 静态定义 mutex 互斥量
mutex_init(mutex);// 初始化 mutex,初始未锁;void mutex_lock(struct mutex *lock);    // 加锁,若失败,转入 D 状态 (TASK_UNINTERRUPTIBLE), 并把当前进程挂到 wait_list
void mutex_unlock(struct mutex *lock);  // 解锁,并唤醒 wait_list 里第一个进程 

mutex 特点:

  • 同一个时刻,只有一个线程可以持有 mutex;
  • 只有锁持有者可以解锁, 因此 mutex 不适合多进程负载同步场景;
  • 不允许递归加锁或解锁;
  • 当进程持有 mutex 时,进程不可以退出;
  • mutex 必须使用官方 API 初始化;
  • mutex 可以睡眠,所以不能用在中断处理程序,或中断下半部,比如 tasklet, 定时器等;

自旋锁和互斥体:
自旋锁属于更底层实现,互斥体基于自旋锁是实现;

  • 互斥体的开销是进程上下文的切换,自旋锁的开销是等待获取锁。若临界区较小,用自旋锁,若临界区较大,应该用互斥体;
  • 互斥体保护的临界区可能包含睡眠的代码,而自旋锁不允许阻塞,睡眠,因为睡眠意味着要进行进程切换,如果切换出去,另一个进程试图获取本自旋锁,就死锁了。
  • 互斥体存在于进程上下文,所以在中断,软中断环境,只能用自旋锁。如果一定要使用互斥体,应该使用 mutex_trylock(),不能获取锁立即返回,避免阻塞;

6.RCU 机制:

提高读锁的性能;
原理,没搞懂

正文完
 1
admin
版权声明:本站原创文章,由 admin 2022-05-10发表,共计3275字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)