第10章 内核同步方法

10.6 互斥体

内核中唯一允许睡眠的锁是信号量。多数用户使用信号量只使用计数1,是把其作为一个互斥的排他锁使用——好比允许睡眠的自旋锁。不幸的是,信号量用途更通用,没多少使用限制。这使得信号量适合用于较复杂的、未明情况下的互斥访问,比如内核于用户空间复杂的交互行为。但这也意味着简单的锁定而使用信号量并不方便,并且信号量也缺乏强制的规则来行使任何形式的自动调试,即便受限的调试也不可能。为了找到一个更简单睡眠锁,内核引入互斥体。互斥体指的是任何可以睡眠的强制互斥锁,比如使用计数是1的信号量。在最新的Linux内核中,互斥体也用于一种实现互斥的特定睡眠锁。互斥体是一种互斥信号。

mutex在内核中对应数据结构mutex,行为和使用计数为1的信号量类似,但操作接口更简单,实现更高效,而且使用限制更强。静态地定义mutex,需要做:

DEFINE_MUTEX(name);

动态初始化mutex,需要做:

mutex_init(&mutex);

对互斥锁定和解锁:

mutex_lock(&mutex);

//临界区

mutex_unlock(&mutex);

表10-7是基本的mutex操作列表

mutex_lock:为指定的mutex上锁,如果锁不可用则睡眠

mutex_unlock:为指定的mutex解锁

mutex_trylock:试图获取指定的mutex,如果成功则返回1;否则锁被获取,返回值是0

mutex_is_locked:如果锁已被争用,则返回1;否则返回0

备注:

任何时刻中只有一个任务可以持有mutex,mutex的使用计数是1。

给mutex上锁者必须负责给其再解锁——不能在一个上下文中锁定一个mutex,而在另一个上下文中解锁。最常用的方式是:在同一上下文中上锁和解锁。

递归地上锁和解锁是不允许的。不能递归地持有同一个锁,同样也不能再去解锁一个已经被解开的mutex。

当持有一个mutex时,进程不可以退出。

mutex不能在中断或者下半部中使用,即使使用mutex_trylock()也不行。

mutex只能通过官方API管理:只能使用上节中描述的方法初始化,不可被拷贝、手动初始化或者重复初始化。

也许mutex结构最有用的特色是:通过一个特殊的调试模式,内核可以采用编程方式检查和警告任何践踏其约束法则的不老实行为。当打开内核配置选项CONFIG_DEBUG_MUTEXES后,会有多种检测来确保这些约束得以遵守。这些调试手段能帮助你和其他mutex使用者们都能以规范式的、简单化的使用模式对其使用。

1、信号量和互斥体

当写新代码时,只有碰到特殊场合(一般是很底层代码)才会需要使用信号量。建议首选mutex。如果发现不能满足其约束条件,且没有其他别的选择时,再考虑选择信号量。

2、自旋锁和互斥体

何时使用自旋锁,何时使用互斥体(或信号量)对编写代码很重要,但是多数情况下,并不需要太多的考虑,因为在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体。

自旋锁与信号量的比较

持有锁需要睡眠——使用互斥体

中断上下文中加锁——使用自旋锁

长期加锁——优先使用互斥体

短期锁定——优先使用自旋锁

低开销加锁——优先使用自旋锁

10.7 完成变量

如果在内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。事实上,完成变量仅仅提供了代替信号量的一个简单的解决方法。例如,当子进程执行或者退出时,vfork()系统调用使用完成变量唤醒父进程。

完成变量由结构completion表示,定义在<linux/completion.h>中。通过以下宏静态地创建完成变量并初始化它:

#define COMPLETION_INITIALIZER(work) \
        { 0, __WAIT_QUEUE_HEAD_INITIALIZER((work).wait) }
/**
 * DECLARE_COMPLETION - declare and initialize a completion structure
 * @work:  identifier for the completion structure
 *
 * This macro declares and initializes a completion structure. Generally used
 * for static declarations. You should use the _ONSTACK variant for automatic
 * variables.
 */
#define DECLARE_COMPLETION(work) \
        struct completion work = COMPLETION_INITIALIZER(work)

通过init_completion()动态创建并初始化完成变量。
/**
 * init_completion - Initialize a dynamically allocated completion
 * @x:  pointer to completion structure that is to be initialized
 *
 * This inline function will initialize a dynamically created completion
 * structure.
 */
static inline void init_completion(struct completion *x)
{
        x->done = 0;
        init_waitqueue_head(&x->wait);
}

在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。

表10-9列出完成变量的方法

完成变量的通常用法是,将完成变量作为数据结构中的一项动态创建,而完成数据结构初始化工作的内核代码将调用wait_for_completion()进行等待。初始化完成后,初始化函数调用completion()唤醒在等待的内核任务。

10.8 BKL:大内核锁

BKL(大内核锁)是一个全局自旋锁,使用BKL主要是为了方便实现从Linux最初的SMP过渡到细粒度加锁机制。下面介绍BKL的特性:

持有BKL的任务可以睡眠。因为当任务无法被调度时,所加锁会自动被丢弃;当任务被调度时,锁又会被重新获得。这并不是说,当任务持有BKL时,睡眠是安全的,仅仅是可以这样做,因为睡眠不会造成任务死锁。

BKL是一种递归锁。一个进程可以多次请求一个锁,并不会像自旋锁产生死锁现象。

BKL只可以用在进程上下文中。和自旋锁不同,不能在中断上下文中申请BKL。

新的用户不允许使用BKL。随着内核版本的不断前进,越来越少的驱动和子系统再依赖于BKL了。

这些特性有助于2.0版本的内核向2.2版本过渡。在SMP支持被引入到2.0版本时,内核中一个时刻上只能有一个任务运行。2.2版本的目标是允许多处理器在内核中并发执行程序。引入BKL是为了使到细粒度加锁机制的过渡更容易些,虽然当时BKL对内核过渡很有帮助,但目前已成为内核可扩展性的障碍了。

在内核中不鼓励使用BKL。新代码中不再使用BKL,但是这种锁仍然在部分内核代码中得到沿用,所以仍然需要理解BKL以及它的接口。

请求锁:lock_kernel()

释放锁:unlock_kernel()

一个执行线程可以递归的请求锁,但是,释放锁时也必须调用同样次数的unlock_kernel()操作,在最后一个解锁操作完成后,锁才会被释放。kernel_locked()检测锁当前是否被持有,如果被持有,返回一个非0值,否则返回0。这些接口被声明在<linux/smp_lock.h>中,简单用法如下:

lock_kernel();

//临界区

unlock_kernel();

BKL在被持有时同样会禁止内核抢占。在单一处理器内核中,BKL并不执行实际的加锁操作。

表10-10 BKL函数列表

lock_kernel():获得BKL

unlock_kernel():释放BKL

kernel_locked():如果锁被持有返回非0值,否则返回0

对于BKL最主要的问题是确定BKL保护的到底是什么。多数情况下,BKL更像是保护代码而不是保护数据。这个问题给利用自旋锁取代BKL造成了很大困难,因为难以判断BKL到底锁的是什么,更难的是,发现所有使用BKL的用户之间的关系。

10.9 顺序锁

顺序锁,是在2.6版本内核中引入的一种新型锁。这种锁提供了一种很简单的机制,用于读写共享数据。实现这种锁主要依靠一个序列计数器。当数据被写入时,会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取。如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。如果读取的值是偶数,那么就表明写操作没有发生。因为锁的初值为0,所以写锁会使值成奇数,释放的时候变成偶数。

定义一个顺序锁

seqlock_t seq_lock = DEFINE_SEQLOCK(seq_lock);

写锁方法如下:

write_seqlock(&seq_lock);

//写锁被获取

write_sequnlock(&seq_lock);

不同的情况发生在读时,并且与自旋锁有很大不同:

unsigned long seq;

do {

seq=read_seqbegin(&seq_lock);

//读这里的数据

}while(read_seqretry(&seq_lock, seq));

在多个读者和少数写者共享一把锁时,seq锁有助于提供一种非常轻量级和具有可扩展性的外观。但是seq锁对写者更有利。只要没有其他写者,写锁总是能够被成功获得。读者不会影响写锁。另外,挂起的写者会不断地使得读操作循环,直到不再有任何写者持有锁为止。

seq锁在遇到如下需求时是最理想的选择:

数据存在很多读者。

数据写者很少。

虽然写者很少,但是希望写优先于读,而且不允许读者让写者饥饿。

数据很简单,如简单结构,甚至是简单的整型——在某些场合,是不能使用原子量的。

使用seq锁中最有说服力的是jiffies。jiffies变量存储了Linux机器启动到当前时间。jiffies是使用一个64位的变量,记录了自系统启动以来的时钟节拍累加数。对于那些能自动读取全部64位jiffies_64变量的机器来说,需要用get_jiffies_64()方法完成,而该方法的实现是用了seq锁:

 kernel/time.c

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

定时器中断会更新jiffies的值,此刻,也需要使用seq锁变量:

write_seqlock(&xtime_lock);

jiffies_64 += 1;

write_sequnlock(&xtime_lock);

10.10 禁止抢占

由于内核是抢占性的,内核中的进程在任何时刻都可能停下来以便另一个具有更高优先权的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行。为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的,所以,这种简单的变化使得内核也是抢占安全的。

实际中,某些情况并不需要自旋锁,但是仍然需要关闭内核抢占。最频繁出现的情况就是每个处理器上的数据。如果数据对每个处理器是唯一的,那么,这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问。如果自旋锁没有被持有,内核又是抢占式的,那么一个新调度的任务就可能访问同一个变量。

为了解决这个问题,可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。

preempt_disable();

//抢占被禁止

preempt_enable();

抢占计数存放着被持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占;如果为1或更大的值,那么,内核就不会进行抢占。这个计数非常有用——它是一种对原子操作和睡眠很有效的调试方法。preempt_count()返回这个值。

表10-11 内核抢占的相关函数

preempt_disable():增加抢占计数值,从而禁止内核抢占

preempt_enable():减少抢占计数,并当该值降为0时检查和执行被挂起的需调度的任务

preempt_enable_no_resched():激活内核抢占但不再检查任何被挂起的需调度任务

preempt_count():返回抢占计数

为了用更简洁的方法解决每个处理器上的数据访问问题,可以通过get_cpu()获得处理器编号。这个函数在返回当前处理器号前首先会关闭内核抢占。

int cpu;

//禁止内核抢占,并将CPU设置为当前处理器

cpu = get_cpu();

//对每个处理器的数据进行操作...

// 再给予内核抢占性,CPU可改变故它不再有效

put_cpu();

linux/smp.h

#define get_cpu()               ({ preempt_disable(); smp_processor_id(); })
#define put_cpu()               preempt_enable()

 

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/83008409