并发和竞态
Linux设备驱动中必须解决的一个问题:多个进程对共享资源的并发访问导致的竞态。
并发指多个执行单元同时、并行被执行,而并发的执行单元对共享资源的访问容易导致竞态。
Linux内核中,主要的竞态发生于以下几种情况:
- 对称多处理器(SMP)的多个CPU:SMP的特点是多个CPU使用同一条系统总线,共享外设和存储器。在SMP的情况下,两个核的竞态可能发生于:① CPU0和CPU1的进程之间;② CPU0的进程和CPU1的中断之间; ③ CPU0的中断和CPU1的中断之间。
- 抢占式并发访问,单CPU内进程与抢占它的进程:一个进程可能被另一个高优先级进程打断,进程与抢占它的进程访问共享资源存在竞态,类似于SMP的多个CPU。
- 中断程序并发访问,中断与进程之间:中断指硬中断、软中断、Tasklet、底半部。中断可以打断正在执行的进程,如果中断服务程序访问进程正在访问的资源,则会发生竞态。
- 多线程并发访问
解决竞态问题的方法:保证对共享资源的互斥访问,即一个执行单元访问共享资源的时候,其他执行单元不能访问该共享资源。
访问共享资源的代码区域称为临界区,需要以某种互斥机制保护临界区。Linux设备驱动中可用的互斥机制有:中断屏蔽、原子操作、自旋锁、信号量、互斥体、完成量、RCU等。
中断屏蔽
在进入临界区前屏蔽系统的中断可避免竞态问题,中断屏蔽使得中断与进程之间、进程与进程之间的并发访问临界区的情况不再发生。
中断屏蔽只能屏蔽本CPU的中断,需要与自旋锁一起使用才能解决SMP多CPU引发的竞态。
异步I/O
、进程调度等都依赖中断,长时间屏蔽中断可能会导致数据丢失甚至系统崩溃等问题,因此要求临界区代码要简短、处理要快(后引入顶半部、底半部机制处理既想要中断多干活又想要中断干的快的问题)。
local_irq_disable(); /* 禁止中断 */
local_irq_enable(); /* 开启中断 */
local_irq_save(flags); /* 保存中断标志位,禁止中断 */
local_irq_restore(flags); /* 恢复中断标志位,开启中断 */
local_bh_disable(); /* 只禁止中断的底半部 */
local_bh_enable(); /* 只开启中断的底半部 */
原子操作
原子操作指不能再进一步分割的操作,可以保证对一个整型数据的修改是排他性的,即修改操作一步到位,不存在修改过程中被其他东西打断。
内核中的原子操作函数分为两类:针对位和针对整型变量。这些操作都依赖于底层CPU的原子操作指令。
32位原子整型操作:
/* 原子变量类型,位于include/linux/types.h */
typedef struct {
int counter;
} atomic_t;
/* 定义原子变量a */
atomic_t a;
/* 定义原子变量v并初始化为0 */
atomic_t v = ATOMIC_INIT(0);
/* 定义原子变量时对其初始化 */
ATOMIC_INIT(int i);
/* 设置原子变量的值 */
void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */
/* 读取原子变量的值 */
int atomic_read(atomic_t *v); /* 返回原子变量的值 */
/* 原子变量加/减 */
void atomic_add(int i, atomic_t *v); /* 原子变量增加i */
void atomic_sub(int i, atomic_t *v); /* 原子变量减少i */
/* 原子变量自增/自减 */
void atomic_inc(atomic_t *v); /* 原子变量增加1 */
void atomic_dec(atomic_t *v); /* 原子变量减少1 */
/* 先修改值再测试值是否为0 */
/* 先对原子变量执行自增,然后测试其是否为0。为0则返回true,否则返回false */
int atomic_inc_and_test(atomic_t *v);
/* 先对原子变量执行自减,然后测试其是否为0。为0则返回true,否则返回false */
int atomic_dec_and_test(atomic_t *v);
/* 先对原子变量执行减i,然后测试其是否为0。为0则返回true,否则返回false */
int atomic_sub_and_test(int i, atomic_t *v);
/* 先对原子变量执行加i,然后测试其是否为负数。为负数则返回true,否则返回false */
int atomic_add_negative(int i, atomic_t *v);
/* 操作并返回 */
int atomic_add_return(int i, atomic_t *v); /* 对原子变量加i,并返回新值 */
int atomic_sub_return(int i, atomic_t *v); /* 对原子变量减i,并返回新值 */
int atomic_inc_return(atomic_t *v); /* 对原子变量自加,并返回新值 */
int atomic_dec_return(atomic_t *v); /* 对原子变量自减,并返回新值 */
64位原子整型操作:
64位原子整型操作函数与32位原子整型操作函数一致,只是将atomic
改为atomic64
,将int
改为long long
。
typedef struct {
long long counter;
} atomic64_t;
原子位操作:
原子位操作是直接对内存进行操作,没有像原子整型那样的atomic_t
的数据结构。
/* 设置位 */
void set_bit(int nr, void *addr); /* addr地址的第nr位置1 */
/* 清除位 */
void clear_bit(int nr, void *addr); /* addr地址的第nr位置0 */
/* 改变位 */
void change_bit(int nr, void *addr); /* 翻转addr地址的第nr位 */
/* 测试位 */
int test_bit(int nr, void *addr); /* 返回addr地址的第nr位的值 */
/* 测试并操作位 */
int test_and_set_bit(int nr, void *addr); /* addr地址的第nr位置1,并返回nr位原来的值 */
int test_and_clear_bit(int nr, void *addr); /* addr地址的第nr位置0,并返回nr位原来的值 */
int test_and_change_bit(int nr, void *addr); /* 翻转addr地址的第nr位,并返回nr位原来的值 */
实例:
1、使用原子操作实现多线程互斥访问共享资源
/* 全局变量 */
atomic_t g_atomic_val = ATOMIC_INIT(1); /* 原子整型变量 */
int g_shared_resource = 0; /* 共享资源 */
/* 线程A访问共享资源 */
if (atomic_dec_and_test(&g_atomic_val)) /* 原子变量先减1,若为0则说明目前没有其他线程在访问共享资源 */
{
/* 操作g_shared_resource */
atomic_inc(&g_atomic_val); /* 再将原子整型变量值设为1,相当释放操作,以便其他线程可访问共享资源 */
}
/* 线程B访问共享资源 */
if (atomic_dec_and_test(&g_atomic_val))
{
/* 操作g_shared_resource */
atomic_inc(&g_atomic_val); /* 再将原子整型变量值设为1 */
}
自旋锁
自旋锁基本原理:在某CPU上运行的代码先执行一个原子操作,该操作测试并设置某个内存变量,在该操作完成之前其他执行单元不可能访问该内存变量。当测试结果表明锁已经空闲时,程序获得这个自旋锁并继续执行,如果测试结果表明锁仍被占用,程序将在一个小的循环内重复执行“测试并设置”操作,直至锁空闲。
自旋,即CPU在原地打转,处于忙等待状态,会浪费CPU资源,降低系统性能。因此,自旋锁适用于短时期的轻量级加锁。
普通自旋锁
普通自旋锁相关操作:
/* 自旋锁的类型 */
typedef struct spinlock {
union {
struct raw_spinlock rlock;
/* 去掉条件编译 */
};
} spinlock_t;
/* 头文件位于include/linux/spinlock.h */
/* 定义自旋锁 */
spinlock_t lock;
/* 定义并初始化自旋锁x */
DEFINE_SPINLOCK(x);
/* 初始化自旋锁 */
spin_lock_init(spinlock_t *lock);
/* 获得自旋锁 */
spin_lock(spinlock_t *lock); /* 若能立即获得锁,则马上返回;否则自旋直至锁空闲 */
int spin_trylock(spinlock_t *lock); /* 尝试获得锁,若锁可得,则获得锁并返回;否则立即返回false */
/* 释放自旋锁 */
spin_unlock(spinlock_t *lock);
/* 检查指定的锁是否被获取 */
int spin_is_locked(spinlock_t *lock);/* 锁未被获取则返回非0,否则返回0 */
自旋锁主要针对SMP或者支持抢占的单CPU下线程之间的并发访问,即用于线程与线程之间,不适用于单CPU且内核不支持抢占的系统。
自旋锁保护的临界区中一定不能调用任何会引起睡眠和阻塞的函数,否则会导致死锁。自旋锁会自动禁止抢占,即当线程A获取锁后会暂时禁止内核抢占。若线程A持有锁后进入了睡眠状态,线程A会主动放弃CPU。线程B开始运行并想获取锁,但此时锁已被线程A持有且内核抢占还被禁止了,线程B就无法被调度出去,线程A也无法运行,锁就不能释放,从而导致死锁。
自旋锁可保证临界区不受其他CPU和本CPU内的抢占进程的影响,但仍可能受中断和底半部的影响。此外,中断服务函数中可以使用自旋锁。使用自旋锁时,为了避免新中断对当前进程或当前中断的影响,在获取锁之前应先禁止本地中断(即本CPU的中断),以避免可能导致的死锁现象的发生。例如:线程A获取自旋锁lock后进入临界区,此时中断发生,在中断服务程序中也去获取锁lock,这就导致了死锁。
为了解决上述问题,处理中断和底半部对自旋锁的影响,获取锁之前最好关闭本地中断,如下:
spin_lock_irq(&lock); /* 禁止本地中断,并获取自旋锁 */
spin_unlock_irq(&lock); /* 激活本地中断,并释放自旋锁 */
spin_lock_irqsave(&lock, flags); /* 保存中断状态至flags,禁止本地中断,并获取自旋锁 */
spin_unlock_irqrestore(&lock, flags); /* 根据flags将中断状态恢复到以前的状态,并激活本地中断,释放自旋锁 */
spin_lock_bh(&lock); /* 关底半部+获得锁 */
spin_unlock_bh(&lock); /* 开底半部+释放锁 */
使用spin_lock_irq()/spin_unlock_irq()
时,用户可自己确定加锁之前的中断状态,但实际上运行时中断状态千变万化,用户很难手动确定某个时刻的中断状态,因此不推荐使用这一组函数。推荐使用spin_lock_irqsave()/spin_unlock_irqrestore()
函数,这组函数在加锁时可自行保存中断状态,释放锁时自行恢复中断状态。
一般情况下,推荐:
- 在进程/线程上下文中调用
spin_lock_irqsave()/spin_unlock_irqrestore()
。 - 在中断上下文中调用
spin_lock()/spin_unlock()
。 - 在底半部调用
spin_lock_bh()/spin_unlock_bh()
。
示例:
#include <linux/spinlock.h>
DEFINE_SPINLOCK(lock); /* 定义并初始化自旋锁lock */
/* 线程A */
void functionA (){
unsigned long flags; /* 用于保存中断状态 */
spin_lock_irqsave(&lock, flags); /* 保存中断状态,禁止本地中断,获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags); /* 恢复中断状态,激活本地中断,释放锁 */
}
/* 中断服务程序 */
void irq()
{
spin_lock(&lock); /* 获取锁 */
/* 临界区 */
spin_unlock(&lock); /* 释放锁 */
}
读写自旋锁
普通自旋锁不关心锁定的临界区的具体操作,无论读还是写都会锁住临界区。实际上,对临界区并发访问时,多个执行单元同时读取是不存在问题的。
作为普通自旋锁的衍生,读写自旋锁是对普通自旋锁在读操作上的优化。
读写自旋锁允许多个执行单元同时获得读锁来读共享资源,当有执行单元获得写锁来写共享资源时,只允许一个执行单元获得写锁,其他执行单元不能获得读锁或写锁来读或写共享资源。
读写自旋锁的操作:
#include <linux/rwlock.h>
typedef struct{
arch_rwlock_t raw_lock;
} rwlock_t;
/* 定义和初始化读写自旋锁 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);
/* 定义并初始化读写自旋锁x */
DEFINE_RWLOCK(x);
/* 读锁定和读解锁 */
void read_lock(rwlock_t *lock); /* 读锁定 */
void read_unlock(rwlock_t *lock); /* 读解锁 */
void read_lock_irqsave(rwlock_t *lock, unsigned long flags); /* 读锁定+关本地中断+保存状态位 */
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); /* 读解锁+开本地中断+恢复状态位 */
void read_lock_irq(rwlock_t *lock); /* 读锁定+关本地中断 */
void read_unlock_irq(rwlock_t *lock); /* 读解锁+开本地中断 */
void read_lock_bh(rwlock_t *lock); /* 读锁定+关底半部 */
void read_unlock_bh(rwlock_t *lock); /* 读解锁+开底半部 */
/* 写锁定和写解锁 */
int write_trylock(rwlock_t *lock); /* 尝试获取写锁,获取失败立即返回false */
void write_lock(rwlock_t *lock); /* 写锁定 */
void write_unlock(rwlock_t *lock); /* 写解锁 */
void write_lock_irqsave(rwlock_t *lock, unsigned long flags); /* 写锁定+关本地中断+保存状态位 */
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); /* 写解锁+开本地中断+恢复状态位 */
void write_lock_irq(rwlock_t *lock); /* 写锁定+关本地中断 */
void write_unlock_irq(rwlock_t *lock); /* 写解锁+开本地中断 */
void write_lock_bh(rwlock_t *lock); /* 写锁定+关底半部 */
void write_unlock_bh(rwlock_t *lock); /* 写解锁+开底半部 */
顺序自旋锁
作为读写自旋锁的衍生,顺序锁是对读写锁的一种优化,允许读执行单元和写执行单元同时访问共享资源,即在写执行单元对被顺序锁保护的共享资源进行写操作时,读执行单元仍可以继续读,不必等待写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进行写操作。当然,多个写执行单元之间仍然互斥访问。
虽然读执行单元和写执行单元可以同时访问共享资源,但是如果在读执行单元读操作期间,写执行单元发生了写操作,就要求读执行单元必须重新读取数据,以便保证读取的数据是有效的。
顺序锁保护的资源不能是指针。若在写操作时导致指针无效,恰巧此时有读操作在访问指针,就会导致系统崩溃。
顺序锁操作函数:
#include <linux/seqlock.h>
typedef struct{
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
/* 定义和初始化顺序锁 */
seqlock_t seqlock;
seqlock_init(seqlock_t *lock);
/* 定义并初始化顺序锁x */
DEFINE_SEQLOCK(x)
/* 写执行单元的顺序锁操作 */
void write_seqlock(seqlock_t *sl); /* 获取写锁 */
int write_tryseqlock(seqlock_t *sl); /* 尝试获取写锁,获取不到立即返回false */
void write_sequnlock(seqlock_t *sl); /* 释放写锁 */
write_seqlock_irqsave(seqlock_t *sl, flags); /* 获取写锁+关本地中断+保存状态位 */
void write_sequnlock_irqrestore(seqlock_t *sl, flags); /* 释放写锁+开本地中断+恢复状态位 */
write_seqlock_irq(lock); /* 获取写锁+关本地中断 */
void write_sequnlock_irq(seqlock_t *sl); /* 释放写锁+开本地中断 */
write_seqlock_bh(lock); /* 获取写锁+关底半部 */
void write_sequnlock_bh(seqlock_t *sl); /* 释放写锁+开底半部 */
/* 读执行单元的顺序锁操作 */
/* 读开始:读执行单元进行读操作前,需先调用读开始函数,该函数会返回顺序锁的当前顺序号 */
unsigned read_seqbegin(const seqlock_t *sl);
unsigned read_seqbegin_irqsave(lock, flags); /* 关本地中断+保存状态位+read_seqbegin */
/* 重读:读执行单元在访问完被顺序锁保护的共享资源后需调用该函数来检查读操作期间是否有写操作,有就需要重新读 */
int read_seqretry(const seqlock_t *sl, unsigned iv); /* iv为读开始函数返回的顺序号,读操作期间有写操作则返回1,否则返回0 */
int read_seqretry_irqrestore(lock, iv, flags); /* 开本地中断+恢复状态位+read_seqretry */
读执行单元使用顺序锁的模式:
do {
seqnum = read_seqbegin(&seqlock_a);
/* 读操作代码块 */
...
} while (read_seqretry(&seqlock_a, seqnum));
使用自旋锁时的注意事项:
- 自旋锁实际上是CPU忙等待,只有在占用锁的时间极短(临界区很短)的情况下使用才合理。
- 不能递归使用一个自旋锁,会导致死锁。
- 自旋锁锁定期间,临界区内不能有会引起进程调度(线程睡眠)的函数,如
copy_from_user
、kmalloc
、msleep
等,否则会导致死锁。 - 编写驱动时必须考虑驱动的可移植性。无论单核还是多核的SOC,都当作多核SOC编写驱动程序,以便驱动有很好的可移植性。
RCU
RCU,read-copy-update,即读-复制-更新。
使用RCU机制的读端没有锁、内存屏障、原子指令类的开销,几乎可以认为是直接读。
而RCU的写执行单元在访问它的共享资源前首先复制一个副本,然后对副本进行修改,最后通过一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。
这个时机就是所有引用该数据的CPU都退出对共享数据读操作的时候。等待适当时机的这一时期称为宽限期。
RCU可以看作读写锁的高性能版本,其优点在于:既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。
RCU不能代替读写锁,当写执行单元比较多时,写执行单元之间的同步开销会比较大,需要延迟数据结构的释放,复制被修改的数据结构,还必须使用某种锁机制来同步并发的其他写执行单元的修改操作。虽然提高了读执行单元,但不能弥补写执行单元同步导致的损失。
RCU相关的操作:
读锁定和读解锁:
仅用于声明一个读临界区,总是会得到满足。
rcu_read_lock();
rcu_read_unlock();
rcu_read_lock_bh(); /* 获取读锁+关闭底半部 */
rcu_read_unlock_bh(); /* 释放锁+开启底半部 */
同步RCU:
synchronize_rcu();
由RCU写执行单元调用该函数,该函数会阻塞写执行单元,直至当前CPU上所有的已经存在的读执行单元完成读临界区并释放读锁(即等待一个宽限期),写执行单元才可以继续下一步操作。注意,当前已存在的读执行单元指调用该函数之前时刻的所有获取读锁的读执行单元,该函数不需要等待后续获得锁的读执行单元完成临界区的读操作。
挂接回调:
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
该函数由RCU写执行单元调用,不会使写执行单元阻塞,可在中断上下文或软中断中使用。
该函数把函数func
挂接到RCU回调函数链表上就立即返回。挂接的回调函数会在一个宽限期结束(即所有已存在的RCU读执行单元读临界区完成)后被执行。
赋值引用:
写端给RCU保护的指针p
赋一个新值v
,相当于发布了数据结构地址p
。
rcu_assign_pointer(p, v);
读端使用该函数获取一个RCU保护的指针,相当于订阅了数据结构地址p
。之后便可以安全的引用它,访问它指向的区域。一般在rcu_read_lock/rcu_read_unlock
保护的区间引用这个指针。
rcu_dereference(p);/* 返回p的值,即v */
读端使用该函数获取一个RCU保护的指针,但不引用它,即不通过它访问指向的区域。而是只关心指针本身的值,如是否为NULL,而不关心指针指向的内容。
rcu_access_pointer(p);
信号量
信号量是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或n。
信号量可使线程进入睡眠状态。当线程获取不到信号量时,就进入睡眠状态,该线程会被调度出去。待其他线程释放信号量时,会唤醒睡眠线程。这样就提高了CPU的使用效率,但由于涉及线程切换,会导致信号量的开销比自旋锁大。
信号量的特点:
- 信号量可使等待资源线程进入睡眠状态,适用于占用资源比较久的场合
- 信号量不能用于中断中,因为它会引起睡眠,而中断中不能睡眠
- 共享资源的持有时间较短时,不适合使用信号量,因为频繁睡眠、切换线程引起的开销要大于信号量带来的优势
信号量可分为计数型信号量和二值型信号量。
计数型信号量是指信号量初始值大于1,用于控制访问资源的线程数,信号量初始值就表示允许多少个线程同时访问共享资源。
二值型信号量是指信号量初始值不大于1,当初始值为1时可用于多线程互斥访问共享资源;当初始值为0时,可用于线程间数据同步。
信号量的PV操作:
P(S):
- 将信号量S的值减1,即S=S-1
- 如果S≥0,则该进程继续执行,否则该进程置为等待状态,排入等待队列
V(S):
- 将信号量S的值加1,即S=S+1
- 如果S>0,唤醒队列中等待信号量的进程
信号量相关的操作:
#include <linux/semaphore.h>
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
/* 定义信号量 */
struct semaphore sem;
/* 初始化信号量 */
void sema_init(struct semaphore *sem, int val);
/* 定义并初始化信号量x */
DEFINE_SEMAPHORE(x)
/* 获得信号量,即P操作 */
void down(struct semaphore *sem); /* 会导致深度睡眠,不能被信号唤醒,不能在中断上下文使用 */
int down_interruptible(struct semaphore *sem); /* 会导致浅度睡眠,能被信号唤醒(此时返回非0) */
int down_trylock(struct semaphore *sem); /* 尝试获得信号量,获得就返回0,否则返回非0,不会导致睡眠,可在中断上下文使用 */
/* 释放信号量,即V操作 */
void up(struct semaphore *sem); /* 释放信号量,唤醒等待者 */
目前信号量主要用于同步,如生产者/消费者问题,而互斥可使用专门的互斥量。
示例:
1、互斥访问临界区
struct semaphore sem;
sema_init(&sem, 1);
/* 线程A */
down(&sem);
/* 临界区 */
up(&sem);
/* 线程B */
down(&sem);
/* 临界区 */
up(&sem);
2、同步
struct semaphore sem;
sema_init(&sem, 0);
/* 生产者线程A */
/* 生产资源 */
up(&sem);
/* 消费者线程B */
down(&sem);
/* 消费资源 */
互斥体
虽然用信号量也可以实现共享资源的互斥访问,但互斥体机制更专业。
使用互斥体时需注意:
- 互斥体会导致睡眠,不能在中断中使用
- 互斥体保护的临界区可以调用引起阻塞的函数
- 一次只有一个线程可以持有
mutex
,必须由持有者释放mutex
,且不能递归上锁和解锁
互斥体相关的操作:
#include <linux/mutex.h>
struct mutex{
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
/* 定义和初始化互斥体 */
struct mutex my_mutex;
mutex_init(&my_mutex);
/* 定义并初始化互斥体x */
DEFINE_MUTEX(x);
/* 获取互斥体 */
void mutex_lock(struct mutex *lock); /* 获取不到会导致调用者深度睡眠,无法被信号唤醒 */
int mutex_lock_interruptible(struct mutex *lock); /* 获取不到会导致调用者浅度睡眠,可以被信号唤醒 */
int mutex_trylock(struct mutex *lock); /* 尝试获取互斥体 */
/* 释放互斥体 */
void mutex_unlock(struct mutex *lock);
int mutex_is_locked(struct mutex *lock); /* 判断mutex是否已被获取,是则返回1,否则返回0 */
互斥体和自旋锁的区别:
互斥体和自旋锁都可以解决互斥问题。但自旋锁属于更底层的手段,互斥体的实现需要依赖自旋锁。
互斥体和自旋锁的使用时机:
- 临界区
- 互斥体是进程级别的,用于多个进程之间对资源的互斥,会发生进程上下文切换,开销大,更适用于进程占用资源时间较长的情况。
- 自旋锁的开销主要是等待获取自旋锁,更适用于临界区很短的情况。
- 阻塞
- 互斥体保护的临界区可包含会引起阻塞的代码。
- 自旋锁保护的临界区不能包含引起阻塞的代码,否则阻塞的进程被切换出去,另一个进程再获取该自旋锁,就会导致死锁。
- 中断或软中断
- 互斥体用于进程上下文,不能用于中断或软中断(除非使用
mutex_trylock
)。因为其会引起调用者阻塞。 - 自旋锁可用于中断或软中断
- 互斥体用于进程上下文,不能用于中断或软中断(除非使用
完成量
完成量用于一个执行单元等待另一个执行单元执行完某事,即多个执行单元之间的同步。
完成量相关的操作:
/* 定义完成量 */
struct completion my_completion;
/* 初始化完成量 */
init_completion(&my_completion);
/* 去初始化完成量 */
reinit_completion(&my_completion);
/* 等待完成量 */
void wait_for_completion(struct completion *c); /* 等待一个完成量被唤醒 */
/* 唤醒完成量 */
void complete(struct completion *c); /* 唤醒一个等待的执行单元 */
void complete_all(struct completion *c); /* 唤醒所有等待该完成量的执行单元 */