前一节介绍了内核同步,这一节介绍内核提供的同步方法。
从 互斥锁与条件变量 这篇文章中我们知道:即使多个线程对同一个整数进行自增操作也会存在同步问题(因为整数的自增操作不是原子(性)的)。
因此内核提供了两类原子性的操作接口——一类接口操作一个整数,另一类接口操作整数中单独的某一位。这些接口的实现是和 CPU 架构相关的。大部分 CPU 架构都提供了简单算术操作的原子化版本。
可进行原子操作的 32 位整数用 atomic_t
结构体表示:
typedef struct {
volatile int counter;
} atomic_t;
定义并初始化一个整数原子变量如下:
atomic_t v; /* define v */
atomic_t u = ATOMIC_INIT(0); /* define u and initialize it to zero */
对它的操作都很简单:
atomic_set(&v, 4); /* v = 4 (atomically) */
atomic_add(2, &v); /* v = v + 2 = 6 (atomically) */
atomic_inc(&v); /* v = v + 1 = 7 (atomically) */
如果你想将它转换成一个 int
,用atomic_read()
:
printk(“%d\n”, atomic_read(&v)); /* will print “7” */
一个通常使用原子整数操作的场景是用来实现计数器。
另一个使用场景是原子地执行一个操作并测试结果。一个常见的例子是原子地递减并测试:
int atomic_dec_and_test(atomic_t *v)
这个函数将原子变量的值减一。如果结果是 0 那么返回 true,否则返回 false 。
完整的原子整数操作列表如下:
ATOMIC_INIT(int i) //At declaration, initialize to i.
int atomic_read(atomic_t *v) //Atomically read the integer value of v.
void atomic_set(atomic_t *v, int i) //Atomically set v equal to i.
void atomic_add(int i, atomic_t *v) //Atomically add i to v.
void atomic_sub(int i, atomic_t *v) //Atomically subtract i from v.
void atomic_inc(atomic_t *v) //Atomically add one to v.
void atomic_dec(atomic_t *v) //Atomically subtract one from v.
int atomic_sub_and_test(int i, atomic_t *v) //Atomically subtract i from v and return true if the result is zero; otherwise false.
int atomic_add_negative(int i, atomic_t *v) //Atomically add i to v and return true if the result is negative; otherwise false.
int atomic_add_return(int i, atomic_t *v) //Atomically add i to v and return the result.
int atomic_sub_return(int i, atomic_t *v) //Atomically subtract i from v and return the result.
int atomic_inc_return(int i, atomic_t *v) //Atomically increment v by one and return the result.
int atomic_dec_return(int i, atomic_t *v) //Atomically decrement v by one and return the result.
int atomic_dec_and_test(atomic_t *v) //Atomically decrement v by one and return true if zero; false otherwise.
int atomic_inc_and_test(atomic_t *v) //Atomically increment v by one and return true if the result is zero; false otherwise.
可进行原子操作的 64 位整数用 atomic64_t
结构体表示:
typedef struct {
volatile long counter;
} atomic64_t;
将 32 位原子操作函数前缀 atomic 改成 atomic64 就得到 64 位原子操作函数。
内核也提供了原子的位操作接口。一个可能令人惊讶的地方是这些位操作函数操作一般的内存地址(而不是一个整数)。函数的参数是一个指针和一个表示位的数。直接看一个例子:
unsigned long word = 0;
set_bit(0, &word); /* bit zero is now set (atomically) */
set_bit(1, &word); /* bit one is now set (atomically) */
printk(“%ul\n”, word); /* will print “3” */
clear_bit(1, &word); /* bit one is now unset (atomically) */
change_bit(0, &word); /* bit zero is flipped; now it is unset (atomically) */
/* atomically sets bit zero and returns the previous value (zero) */
if (test_and_set_bit(0, &word)) {
/* never true ... */
}
/* the following is legal; you can mix atomic bit instructions with normal C */
word = 7;
完整的位操作列表如下:
void set_bit(int nr, void *addr) //Atomically set the nr-th bit starting from addr.
void clear_bit(int nr, void *addr) //Atomically clear the nr-th bit starting from addr.
void change_bit(int nr, void *addr) //Atomically flip the value of the nr-th bit starting from addr.
int test_and_set_bit(int nr, void *addr) //Atomically set the nr-th bit starting from addr and return the previous value.
int test_and_clear_bit(int nr, void *addr) //Atomically clear the nr-th bit starting from addr and return the previous value.
int test_and_change_bit(int nr, void *addr) //Atomically flip the nr-th bit starting from addr and return the previous value.
int test_bit(int nr, void *addr) //Atomically return the value of the nrth bit starting from addr.
内核也提供了从某个地址开始查找第一个被置上位(或未被置上位)的位数的函数:
int find_first_bit(unsigned long *addr, unsigned int size)
int find_first_zero_bit(unsigned long *addr, unsigned int size)
如果临界区只包含简单的变量操作,那么使用上述原子操作就可以了。但现实中临界区中的操作要更复杂,比如要对链表这类数据结构操作。在这样的场景中就需要用到一种最基本的锁——自旋锁。基本使用方法如下:
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* critical region ... */
spin_unlock(&mr_lock);
自旋锁只能同时被一个线程持有。也就是,一次只能允许一个线程进入临界区执行。这提供了在多处理器中需要的并行访问保护。在单处理器中,自旋锁在编译完后并不存在。它们仅仅是用作关闭或开启内核抢占的标记。如果内核抢占在编译时被关闭了,那么自旋锁在编译完后就完全不存在。
注意:自旋锁不是递归的。如果你尝试获取一个自己已经持有的锁,你会自旋等待自己释放锁。但是由于你在自旋,所以你永远不会释放锁,这样就造成死锁。
自旋锁可以在中断处理函数中使用,而信号量会导致睡眠所以不能使用。如果在中断处理函数中使用锁,你必须在获取锁前要关掉本地中断。否则,中断处理函数可能在锁被持有的时候中断内核代码然后尝试获取锁。这时中断处理函数就在自旋等待锁释放。然而,锁的持有者只能在中断处理函数完成后才有机会释放锁。这样就会造成死锁。
注意你只需要关掉本地中断,如果一个中断发生在另一个处理器上,它不会阻止锁的持有者最终释放锁。
内核提供了一个接口方便地关掉本地中断然后获取锁。使用方法如下:
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags);
/* critical region ... */
spin_unlock_irqrestore(&mr_lock, flags);
如果你确定中断在进入临界区前是开着的,那么就不需要恢复中断状态。你可以无条件地在释放锁后再打开中断。在这种情形下,使用spin_lock_irq()
和spin_unlock_irq()
是最优的:
DEFINE_SPINLOCK(mr_lock);
spin_lock_irq(&mr_lock);
/* critical section ... */
spin_unlock_irq(&mr_lock);
但是由于内核代码量太庞大,你很难确保中断在某个代码路径是开着的,所以不推荐使用这种方法。
完整的自旋锁方法列表如下:
spin_lock() //Acquires given lock
spin_lock_irq() //Disables local interrupts and acquires given lock
spin_lock_irqsave() //Saves current state of local interrupts, disables local interrupts, and acquires given lock
spin_unlock() //Releases given lock
spin_unlock_irq() //Releases given lock and enables local interrupts
spin_unlock_irqrestore() //Releases given lock and restores local interrupts to given previous state
spin_lock_init() //Dynamically initializes given spinlock_t
spin_trylock() //Tries to acquire given lock; if unavailable, returns nonzero
spin_is_locked() //Returns nonzero if the given lock is currently acquired, otherwise it returns zero
因为一个底半部可能抢占进程上下文代码,如果数据在底半部和进程上下文之间共享,你必须使用锁并且关掉底半部来保护进程上下文中的数据。类似地,因为中断处理函数可能抢占一个底半部,如果数据在中断处理函数和底半部之间共享,你必须获取合适的锁并且关掉中断。
回忆一下,两个相同类型的 tasklets 不可能同时运行。因此,在相同类型 tasklets 之间共享的数据不需要保护。然而,如果数据在不同类型的 tasklets 之间共享,在访问数据前你需要获取一个自旋锁。你不需要关掉底半部,因为一个处理器上的 tasklet 不会被另一个 tasklet 抢占。
如果数据在软中断之间共享,那么必须要使用锁保护。因为两个相同类型的软中断可以同时在多个处理器上运行。一个软中断不会抢占另一个在相同处理器上运行的软中断,所以也不需要关掉底半部。
有时候,锁的使用能够被清楚地分为读和写路径。比如,考虑一个能同时被更新和被搜索的链表。当链表在被更新(写)的时候,很重要的一点是要确保没有其他线程正在执行写或者读链表。也就是写操作要求访问是互斥的。另一方面,当链表被搜索(读)的时候,只需确保没有其他线程在写链表。也就是说只要没有其他线程在写链表,多个线程同时读链表是安全的。像这种情形,就可以使用读写自旋锁来保护这个链表。
读写自旋锁是自旋锁的一个变种,可以理解为它包括一个读锁和写锁。一个或多个读者能够同时持有读锁。写锁,相反地,只能被最多一个写者持有(同时也没有其他读者)。读写锁有时也被成为共享/独占锁。
读写锁使用方法如下:
DEFINE_RWLOCK(mr_rwlock);
//读者的代码路径
read_lock(&mr_rwlock);
/* critical section (read only) ... */
read_unlock(&mr_rwlock);
//写者的代码路径
write_lock(&mr_rwlock);
/* critical section (read and write) ... */
write_unlock(&mr_lock);
完整的读写自旋锁方法如下:
read_lock() //Acquires given lock for reading
read_lock_irq() //Disables local interrupts and acquires given lock for reading
read_lock_irqsave() //Saves the current state of local interrupts, disables local interrupts, and acquires the given lock for reading
read_unlock() //Releases given lock for reading
read_unlock_irq() //Releases given lock and enables local interrupts
read_unlock_irqrestore() // Releases given lock and restores local interrupts to the given previous state
write_lock() //Acquires given lock for writing
write_lock_irq() //Disables local interrupts and acquires the given lock for writing
write_lock_irqsave() //Saves current state of local interrupts, disables local interrupts, and acquires the given lock for writing
write_unlock() //Releases given lock
write_unlock_irq() //Releases given lock and enables local interrupts
write_unlock_irqrestore() //Releases given lock and restores local interrupts to given previous state
write_trylock() //Tries to acquire given lock for writing; if unavailable, returns nonzero
rwlock_init() //Initializes given rwlock_t
关于 Linux 读写自旋锁一个很重要的一点是读者的优先级比写者高。如果读锁被一个读者持有了,一个写者正在自旋等待互斥的访问,那么接下来的读者获取读锁还是会成功。正在自旋的写者只有等到所有的读者都释放读锁才能持有写锁。因此,大量的读者会导致等待的写者遭受饥饿。在使用时要考虑这点。
Linux 中的信号量是一种睡眠锁。当一个任务尝试去获取一个信号量而失败时,任务会被放到信号量的等待队列中,然后进入睡眠,处理器就被释放出来去执行其他代码。当信号量变得可获取时,等待队列中的一个任务会被唤醒以获取信号量。
你可以从信号量的睡眠行为中发现一些有趣的结论:
参考 Posix信号量。一个是应用层的信号量,一个是内核中的信号量,两者很相似。
静态声明的信号量以如下的方式创建:
struct semaphore name;
sema_init(&name, count);
静态创建二值信号量的方法如下:
static DECLARE_MUTEX(name);
动态创建的二值信号量初始化方法如下:
init_MUTEX(sem);
函数 down_interruptible()
尝试获取一个信号量。如果信号量不可获取,进程进入 TASK_INTERRUPTIBLE
睡眠状态。如果任务在睡眠时,接收到一个信号,那么任务会被唤醒, down_interruptible()
会返回 -EINTR
。
函数 down()
会将睡眠的任务设置成 TASK_UNINTERRUPTIBLE
状态,这时候睡眠的任务就会忽略信号。你通常不想这样,所以 down_interruptible()
函数更常用。
你可以使用 down_trylock()
无阻塞的获取一个信号量。
释放一个信号量,调用 up()
。一个例子如下:
/* define and declare a semaphore, named mr_sem, with a count of one */
static DECLARE_MUTEX(mr_sem);
/* attempt to acquire the semaphore ... */
if (down_interruptible(&mr_sem)) {
/* signal received, semaphore not acquired ... */
}
/* critical region ... */
/* release the given semaphore */
up(&mr_sem);
完整的信号量方法如下:
sema_init(struct semaphore *, int) //Initializes the dynamically created semaphore to the given count
init_MUTEX(struct semaphore *) //Initializes the dynamically created semaphore with a count of one
init_MUTEX_LOCKED(struct semaphore *) //Initializes the dynamically created semaphore with a count of zero (so it is initially locked)
down_interruptible (struct semaphore *) //Tries to acquire the given semaphore and enter interruptible sleep if it is contended
down(struct semaphore *) //Tries to acquire the given semaphore and enter uninterruptible sleep if it is contended
down_trylock(struct semaphore *) //Tries to acquire the given semaphore and immediately return nonzero if it is contended
up(struct semaphore *) //Releases the given semaphore and wakes a waiting task, if any
与信号量的关系和读写自旋锁与自旋锁的关系类似。使用方法略过、
参考 互斥锁与条件变量。使用方法略过。