内核同步方法

来源:互联网 发布:在线询问医生软件 编辑:程序博客网 时间:2024/05/22 02:14

内核同步方法

原子操作

我们首先介绍同步方法中的原子操作,因为它是其他同步方法的基石.原子操作可以保证指令以原子的方式执行—执行过程不被打断。众所周知,原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令。

内核提供了两组原子操作接口—— 一组针对整数进行操作,另一组针对单独的位进行操作。

整数操作:

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) */

  • 原子性与顺序性的比较

    字长的读取总是原子地发生,绝不可能对同一个字交错地进行写;读总是返回一个完整的字,这或者发生在写操作之前,或者之后,绝不可能发生在写的过程中。例如,如果一个整数初始化为42,然后又置为365,那么读取这个整数肯定会返回42或者365,而绝不会是二者的混合。这就是我们所谓的原子性。

  • 也许代码比这有更多的要求。或许要求读必须在待定的写之前发生—这种需求其实不属于原子性要求,而是顺序要求。原子性确保指令执行期间不被打断,要么全部执行完,要么根本不执行。另一方面,顺序性确保即使两条或多条指令出现在独立的执行线程中,甚至独立的处理器上,它们本该的执行顺序却依然要保持。

原子操作保证原子性,顺序性通过屏障来保证(barrier).

原子位操作

自旋锁

Linux内核中最常见的锁就是自旋锁(spin lock).自旋锁最多被一个可执行线程持有。一个执行线程尝试持有一个已经被持有的自旋锁时,会一直循环,等待锁重新可用。

一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),所以自旋锁不应该被长时间持有。事实上,这点正是使用自旋锁的初衷:在短期间内进行轻量级加锁。还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它。这样处理器就不必循环等待,可以去执行其他代码。这也会带来一定的开销—这里有两次明显的上下文切换,被阻塞的线程要换出和换入,与实现自旋锁的少数几行代码相比,上下文切换当然有较多的代码。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时。

自旋锁的方法

DEFINE_SPINLOCK(mr_lock);spin_lock(&mr_lock);/* critical region ... */spin_unlock(&mr_lock);

Linux内核实现的自旋锁是不可递归的,这点不同于自旋锁在其他操作系统中的实现。所以如果你试图得到一个你正持有的锁,你必须自旋,等待你自己释放这个锁。但你处于自旋忙等待中,所以你永远没有机会释放锁,于是你被自己锁死了。千万小心自旋锁!

自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠)。一定要在获取锁之前,首先禁止本地中断.如果不禁止中断,另一个中断进入,而又不睡眠,就造了死锁。(关闭的是当前处理器的中断)

内核提供的禁止中断和请求锁的接口:

DEFINE_SPINLOCK(mr_lock);unsigned long flags;spin_lock_irqsave(&mr_lock, flags);/* critical region ... */spin_unlock_irqrestore(&mr_lock, flags);

spin_lock_irqsave()函数保存当前状态,并且加锁。spin_unlock_irqsave()恢复,解锁。

自旋锁和下半部

在与下半部配合使用时,必须小心地使用锁机制。函数spin_lock_bh()用于获取指定锁,同时它会禁止所有下半部的执行。相应的函数spin_unlock_bh()执行相反的操作。

由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。

同类的tasklet不能同时运行。数据被不同tasklet共享时,需要加锁。 不需要禁止下半部分,因为同一个处理器tasklet不会抢占。

读一写自旋锁

有时,锁的用途可以明确地分为读取和写入两个场景。这个时候读写应该是互斥的。但是读数据不改变数据的内容,所以应该是可以并发的。自旋锁提供读-写自旋锁。

DEFINE_RWLOCK(mr_rwlock);//DEFINE_RWLOCK(mr_rwlock);//Then, in the reader code path:read_lock(&mr_rwlock);/* critical section (read only) ... */read_unlock(&mr_rwlock);//Finally, in the writer code path:write_lock(&mr_rwlock);/* critical section (read and write) ... */write_unlock(&mr_lock);

读写锁之前不能相互转换:

read_lock(&mr_rwlock);write_lock(&mr_rwlock);

以上会导致死锁。

在使用Linux读一写自旋锁时,最后要考虑的一点是这种锁机制照顾读比照顾写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是,读者却可以继续成功地作占用锁。而自旋等待的写者在所有读者释放锁之前是无法获得锁的。所以,大量读者必定会使挂起的写者处于饥饿状态.

信号量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号且会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。

  • 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。

  • 相反,锁被短时间持有时,使用信号量就不太适宜了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长。

  • 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的。

  • 你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其他进程试图获得同一信号量时不会因此而死锁(因为该进程也只是去睡眠而已,而你最终会继续执行的)。

  • 在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。

在用户空间需要同步的时候需要休眠,只能使用信号量。如果在信号量和自旋锁之间选择,应该根据锁的时间来做判断。

信号量不同不同于自旋锁,不会禁止内核抢占。不会对调度产生影响。

信号量使用

struct semaphore name;sema_init(&name, count);//更为普通的方法static DECLARE_MUTEX(name);sema_init(sem, count);

down_interruptible()会尝试获取信号量,如果信号量不可用,设置TASK_INTERRUPTIBLE。down()会设置为TASK_UNINTERRUPTIBLE.

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);

读-写信号量

类似读-写spin lock。

static DECLARE_RWSEM(mr_rwsem);/* attempt to acquire the semaphore for reading ... */down_read(&mr_rwsem);/* critical region (read only) ... *//* release the semaphore */up_read(&mr_rwsem);/* ... *//* attempt to acquire the semaphore for writing ... */down_write(&mr_rwsem);/* critical region (read and write) ... *//* release the semaphore */up_write(&mr_sem);

互斥体

在信号量中可以设置值大于1,。但是更多的被应用于互斥的情形。所以有了后来的互斥体(mutex).它也是允许睡眠的锁。mutex在内核中对应数据结构mutex ,其行为和使用计数为1的信号量类似,但操作接口更简单,实现也更高效,而且使用限制更强。

DEFINE_MUTEX(name);mutex_init(&mutex);//Locking and unlocking the mutex is easy:mutex_lock(&mutex);/* critical region ... */mutex_unlock(&mutex);

  • 任何时刻只有一个任务可以持有mutex。

  • 在同一个上下文上锁,解锁。

  • 递归上锁,解锁不被允许。

  • 持有mutex的时候,进程不能退出。

  • 不能再中断或者下半部分使用。

  • 只能用官方API操作,不可以拷贝,手动初始化或者赋值。

信号量和互斥体

优先使用互斥体,特殊情形使用信号量。

自旋锁和互斥体

自旋锁多用于中断和下半部分,睡眠只能用互斥体。

顺序锁

seq锁。锁的实现依赖序列计数器,有疑问的数据写入时会得到锁,并且序列值增加。在读取前后,序列之都被读取。如果序列值相同说明读操作过程没有被打断过。(读的值为偶数则表明写操作没有发生。因为写会使得值为基数,因为初始值为0)

To define a seq lock:seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_seq_lock);The write path is thenwrite_seqlock(&mr_seq_lock);/* write lock is obtained... */write_sequnlock(&mr_seq_lock);do {  seq = read_seqbegin(&mr_seq_lock);  /* read data here ... */} while (read_seqretry(&mr_seq_lock, seq));

seq对写更加有利,只要其他写着没有获得锁,写就能成功。在以下情形使用:

  • 数据存在很多读者。

  • 写者少。

  • 你希望写者优先。

  • 数据简单。

jeffies,存储了Linux开启到现在的时间。eg:

u64 get_jiffies_64(void){unsigned long seq;u64 ret;do {seq = read_seqbegin(&xtime_lock);ret = jiffies_64;} while (read_seqretry(&xtime_lock, seq));return ret;}write_seqlock(&xtime_lock);jiffies_64 += 1;write_sequnlock(&xtime_lock);

禁止内核抢占

内核是抢占性的。内核抢占代码把自旋锁作为非抢占区的标记。如果一个自旋锁被持有,内核便不能进摘抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的(SMP-safe)所以,这种简单的变化使得内核也是抢占安全的(preempt-safe).

最频繁出现的情况就是每个处理器上的数据。如果数据对每个处理器是唯一的,那么,这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问。

如果数据是处理器唯一的,那么禁止了内核抢占,数据就安全了。

顺序和屏障

当处理多处理器之间或硬件设备之间的同步i司题时,有时需要在你的程序代码中以指定的顺序发出读内存〔读人)和写内存(存储)指令。在和硬件交互时,时常需要确保一个给定的读操作发生在其他读或写操作之前。另外,在多处理器上,可能需要按写数据的顺序读数据(通常确保后来以同样的顺序进行读取)。但是编译器和处理器为了提高效率,可能对读和写重新排序这样无疑使问题复杂化了。幸好,所有可能重新排序和写的处理器提供了机器指令来确保顺序要求。同样也可以指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称作屏障(barrirers).

rmb()方法提供r一个“读”内存屏障,它确保跨越rmb()的载人动作不会发生重排序。也就是说,在n”b()之前的载入操作不会被重新排在该调用之后,同理,在rmb()之后的载入操作不会被重新排在该调用之前。

wmb()方法提供了一个“写”内存屏障,这个函数的功能和rmb()类似,区别仅仅是它是针对存储而非载入—它确保跨越屏障的存储不发生重排序。

看看使用了mb()和rmb()的一个例子,其中a的初始值是1,b的初始值是2.

如果不使用内存屏障,在某些处理器上,c可能接收了b的新值,而d接收了a原来的值。比如c可能等于4(正是我们希望的),然而d可能等于1(不是我们希望的)口使用mb()能确保a和b按照预定的顺序写入,而rmb()确保c和d按照预定的顺序读取。