前言

众所周知,Linux 是一个多用户多任务操作系统。在多处理器(SMP)情况下存在真正的并行运算,因为线程是同时执行的。而在单处理器(uniprocessor,UP)情形中,并行是通过抢占实现的,即通过临时中断一个线程以执行另一个线程的方式来实现。当存在并发访问的可能时,必须使用有效的机制来保证同步和保护资源。另外对中断的处理也会打断正在运行的任务。Linux 操作系统中包含众多的同步机制,包括信号量(semaphore)、自旋锁(spinlock)、原子操作(atomic operation)、读写锁(rwlock)、RCU 和seqlock,每种机制应用在不同场合。随着Linux 从单处理器内核发展到对称多处理器内核、从非抢占内核发展到抢占内核,这些机制越来越高效,也越来越复杂。本章主要介绍Linux 内核的各种同步机制。

1. 原子操作

原子操作是一系列不可中断的操作的集合,它的执行过程是封闭的,不可打断的。在单处理器系统中,能够在单条指令中完成的操作都可以认为是原子操作。在对称多处理器结构中,即使能在单条指令中完成的操作也有可能被打断。原子性不可能由软件单独保证,必须有硬件的支持,因此是和平台相关的,而且通常使用汇编语言实现。原子操作保护的资源通常被定义成原子型整数(atomic_t)类型:

typedef struct {volatile int counter; } atomic_t;

volatile 修饰符告诉编译器不要对该类型的数据做优化处理。
原子操作包含整数型和比特型.
整数型:

  • atomic_read(atomic_t *v); // 原子读操作
  • atomic_set(atomic_t *v, int i); // 设置原子类型的变量v 的值为i
  • void atomic_add(int i, atomic_t *v); // 给原子类型的变量v 增加值i
  • void atomic_sub(int i, atomic_t *v); // 从原子类型的变量v 中减去i
    比特型:
  • int set_bit(int nr,void *addr);// 对给定地址addr 的第nr bit 进行置位
  • int clear_bit(int nr, void *addr);// 对给定地址addr 的第nr bit 进行清位
  • int test_bit(int nr,void *add); // 检测给定地址addr 的第nr bit 的值
  • int change_bit(int nr, void *addr); // 使地址addr 的第nr bit 发生跳转(0 变为1,1 变为0)

2. 锁机制

2.1 自旋锁

自旋的意思就是一直循环直到条件满足。自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元占有,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁。如果不能在很短的时间内获得锁,这无疑会导致CPU 效率降低。ARM 体系下的自旋锁相关的结构如下:

typedef struct
{
    volatile unsigned int lock;
} arch_spinlock_t;
typedef struct raw_spinlock
{
    arch_spinlock_t raw_lock;
    …
} raw_spinlock_t;
typedef struct
{
    raw_spinlock_t raw_lock;
    …
} spinlock_t;

与自旋锁相关的函数主要包括以下几个:

spin_lock_init(lock);//初始化自旋锁
spin_lock(lock);//获得自旋锁
spin_trylock(lock);//尝试获得自旋锁
spin_unlock(lock);//释放自旋锁

spin_lock 函数在获得锁后将立即返回,否则在原地等待,直到获得锁。spin_trylock 函数尝试获得自旋锁lock,如果能立即获得锁,则返回真,否则立即返回假。

spin_lock_irq 函数获得自旋锁的同时会禁止本地CPU 上的中断与内核抢占。当自旋锁需要用于中断上下文时,必须使用spin_lock_irq 函数。spin_lock_irq 与spin_unlock_irq 成对使用,spin_lock_irqsave 与spin_unlock_irqsave 成对使用。spin_lock_irqsave 函数会保存本地中断的状态,在解锁时需用spin_unlock_irqrestore 函数恢复中断的状态。

2.2 读写锁

读写锁(rwlock)实际是一种特殊的自旋锁。在实际应用中如果需要对某个数据的读操作和写操作进行有区别的加锁时,可以使用读写锁。读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。读写锁相对于普通自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源。写者是排他性的,一个读写锁同时只能有一个写者或多个读者,但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋,直到写者释放该读写锁。在ARM 平台上,内核用32bit 的数据来记录读写锁中读写线程的数量,最高位为写者的数量,只允许一个;0~30bit 记录读者的数量(具体见内核arch_read_lock 与arch_write_lock 代码)。

读写锁的类型是rwlock_t。初始化读写锁的方法有两种,一种是动态初始化,一种是静态初始化。

rwlock_t   x;
rwlock_init(x);          //动态初始化读写锁x
rwlock_t   x = RW_LOCK_UNLOCKED  //静态初始化

读锁最基本的函数组合如下:

read_lock(lock);
read_unlock(lock);

写锁最基本的函数组合如下:

write_lock(lock);
write_unlock(lock);

与自旋锁一样,读写锁中分别为读写提供了尝试获取锁,这两个函数立即返回,成功获得锁返回真,否则返回假:

read_trylock(lock);;
write_trylock(lock);

2.3 RCU

RCU:read-copy-update,它是一种高性能的锁机制。RCU 读者最基本的函数组合如下:

#define rcu_read_lock()     preempt_disable()   //进入读操作临界区标记
#define rcu_read_unlock()  preempt_enable()     //退出读操作临界区

2.4 信号量

信号量是一种睡眠锁。假如有一个任务想要获得已被占用的信号量,信号量就会将其放入一个等待队列然后让其睡眠,这样CPU 可以去处理其他事情。持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒并获得信号量。自旋锁与信号量的第一个区别是前者不会引起调用者睡眠。自旋锁与信号量的选用应该取决于锁被持有的时间长短。如果锁的持有时间较短,使用自旋锁是更好的选择。自旋锁与信号量的第二个区别是信号量允许有多个持有者,而自旋锁只能有一个持有者。
信号量(semaphore)的实现也是和平台相关的。信号量使用semaphore 结构描述,它的结构成员count 会被初始化为最多的信号量持有者数量。

struct semaphore {
    raw_spinlock_t          lock;
    unsigned int  count;
    struct list_head     wait_list;//等待列表
};

信号量最基本的函数组合如下:

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

获取信号量的函数包括down、down_trylock、down_interruptible。down 函数会一直等待信号量。down_trylock 函数会尝试获取信号量,假如信号量被占有则立即返回。down函数会导致睡眠,因此不能在中断上下文中使用。在中断上下文中应该选用down_trylock 函数:

int down_trylock(struct semaphore * sem);

down_interruptible 函数能被信号打断,它的返回值如果是0,表示获得信号量正常返回;如果是-EINTR,表示被信号打断。函数原型如下:

int down_interruptible(struct semaphore * sem);

2.5 读写信号量

信号量也衍生了一种区分读写操作的同步机制,即读写信号量(rw_semaphore)。它的原理与读写锁差不多,读写信号量相关的函数如下,相信读者不难看出它们的用法。

void init_rwsem(struct rw_semaphore *sem);
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

2.6 互斥量

互斥量用mutex 结构描述,同一时间只允许一个访问者。互斥量加锁失败会进入睡眠等待唤醒,不能用于中断上下文。

struct mutex {
    atomic_t             count;//1=未加锁;0=已加锁;负数=已加锁,表示可能的等待者数目
    spinlock_t           wait_lock;
    struct list_head     wait_list;
…
};

互斥量相关的函数如下:

void mutex_init(mutex);
void mutex_lock(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
void mutex_unlock(struct mutex *lock);

3. 等待队列

3.1 等待队列原理

等待队列常用于异步通知和阻塞式访问。如果进程需要等待某些条件发生才能继续,则可以使用等待队列机制。在Linux 内核中通常使用等待队列来实现阻塞式访问。

// 初始化一个等待队列头
void init_waitqueue_head(wait_queue_head_t *q);
// 等待事件发生的函数
wait_event(wq, condition)//不可中断的等待
wait_event_interruptible(wq, condition)//可中断的等待
wait_event_timeout(wq, condition, timeout)//带超时返回的等待
wait_event_interruptible_timeout(wq, condition, timeout)//可中断并超时返回的等待
// 唤醒等待队列
wake_up(wait_queue_head_t *q);//唤醒所有等待q 的进程
wake_up_interruptible(wait_queue_head_t *q);//只唤醒执行可中断休眠的进程
// 加入或退出等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

加入等待队列的线程将等待唤醒(wake_up)。阻塞式字符驱动程序一般在读函数中等待,并在中断或内核线程中使用wake_up 函数唤醒等待队列。

3.2 完成事件

完成事件(completion)是一种轻量级的同步机制,它允许一个进程告诉另一个进程某种工作已经完成。完成事件是基于等待队列的。完成事件使用下面的结构描述:

struct completion {
    unsigned int done;//等待完成的事件数量
    wait_queue_head_t wait;
};

完成事件初始化函数如下:

DECLARE_COMPLETION(my_comp);    //静态初始化
void init_completion(struct completion *x);动态初始化完成事件

等待完成事件的函数如下:

wait_for_completion(struct completion *x);
//可中断的wait_for_completion
wait_for_completion_interruptible(struct completion *x);
//带超时处理的wait_for_completion
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout);

唤醒等待进程的函数包括complete 和complete_all:

complete(struct completion *x);    //唤醒一个等待完成事件的进程
complete_all(struct completion *);   //唤醒所有等待的进程。

3.3 通知链

Linux 内核分为多个子系统,但这些子系统之间并非相互独立。当某个子系统中的某个状态发生变化时,有时候需要通知其他子系统做出相应的处理。为满足这种需求,内核出现了通知链(Notifier Chain)机制。notifier_block 结构描述一个通知单元:

typedef      int(*notifier_fn_t)(struct notifier_block *nb,unsigned long action, void *data);
struct notifier_block {
notifier_fn_t notifier_call;
struct notifier_block __rcu *next;
int priority;
};

通知链有四种类型,分别是原子通知链、可阻塞通知链、原始通知链、SRCU 通知链:

struct atomic_notifier_head
{
    spinlock_t lock;
    struct notifier_block __rcu *head;
};
struct blocking_notifier_head
{
    struct rw_semaphore rwsem;
    struct notifier_block __rcu *head;
};
struct raw_notifier_head
{
    struct notifier_block __rcu *head;
};
struct srcu_notifier_head
{
    struct mutex mutex;
    struct srcu_struct srcu;
    struct notifier_block __rcu *head;
};
  • 原子通知链的回调函数在中断或原子上下文中运行,不允许阻塞。
  • 可阻塞通知链的回调函数在进程上下文中运行,允许阻塞。
  • 原始通知链不限制回调运行环境,保护措施由调用者自己维护。
  • SRCU 通知链是可阻塞通知链的变种,它使用可睡眠的RCU机制(SleepableRead-Copy Update)而不是rw-semaphores 来保护通知链。

这四种通知链对应的通知单元注册函数如下:

int atomic_notifier_chain_register(struct atomic_notifier_head *nh,struct notifier_block *nb);
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,struct notifier_block *nb);
int raw_notifier_chain_register(struct raw_notifier_head *nh,struct notifier_block *nb);
int srcu_notifier_chain_register(struct srcu_notifier_head *nh,struct notifier_block *nb);

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐