LKD 笔记:内核同步方法

前一节介绍了内核同步,这一节介绍内核提供的同步方法。

原子操作

从 互斥锁与条件变量 这篇文章中我们知道:即使多个线程对同一个整数进行自增操作也会存在同步问题(因为整数的自增操作不是原子(性)的)。

因此内核提供了两类原子性的操作接口——一类接口操作一个整数,另一类接口操作整数中单独的某一位。这些接口的实现是和 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 位原子操作

可进行原子操作的 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

读写信号量

与信号量的关系和读写自旋锁与自旋锁的关系类似。使用方法略过、

互斥锁

完成变量

参考 互斥锁与条件变量。使用方法略过。

顺序锁

关掉内核抢占

顺序与屏障

你可能感兴趣的:(LKD,笔记)