知识梳理之互斥与同步(适用于面试)

互斥是指对资源的排他性访问,而同步是对进程执行的先后顺序作出妥善的安排。

所谓竞态,就是多个执行路径有可能对同一资源进行操作时可能导致的资源数据紊乱的行为。把对共享的资源进行访问的代码片段成为临界区。

并发的来源:中断处理路径(中断处理函数与被中断的进程之间形成的并发)、调度器的可抢占性(调度器被抢占,形成进程间的并发)、多处理器的并发执行(进程之间严格意义上的并发)。

1、 local_irq_enablelocal_irq_disable

local_irq_enable宏用来打开本地处理器的中断,而local_irq_disable是相反操作。它们是通过关中断的方式进行互斥操作,必须确保处于两者之间的代码效率高,否则影响性能。

2、 自旋锁

目的是实现在多处理器系统中提供对共享数据的保护,核心思想是:设置一个多处理器之间共享的全局变量锁V,并定义当V=1时为上锁状态,当V=0是为解锁状态。如果V0表明有其他处理器上的代码正在对共享数据进行访问,此时访问处理器进入忙等待即自旋状态。如果V=0表明当前没有其他处理器上代码进入临界区,访问处理器可以访问该资源。

1)、spin_lock

函数定义:#define raw_spin_lock(lock)     _raw_spin_lock(lock),从定可看出是一个宏定义,因此进入_raw_spin_lock(lock)的定义,如下:

static inline void __raw_spin_lock(raw_spinlock_t *lock)

{

preempt_disable();

spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);

LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);

}

说明:preempt_disable()宏是用来定义内核是否支持可抢占的操作,如果定义,则关闭调度器的可抢占性;如果没有定义,此语句不做任何操作。此定义用于支持SMP系统。

2spin_lock的变体

情景:当进程A通过调用spin_lock进入临界区后,正处于临界区中时,进程A所在的处理器上发生了一个外部硬件中断,此时系统暂停当前进程A的操作进而处理外部中断。假设该中断也恰好要操作进程A的资源,因为资源是一个全局变量,所以操作之前也要调用spin_lock试图去获得自旋锁,因为该锁已被进程A拥有,所以中断处理例程要进入自旋状态。这是非常致命的状态。

为解决上述场景,出现了spin_lock_irq spin_lock_irqsavespin_lock_irq首先调用raw_spin_lock_irq,代码如下:

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)

{

    local_irq_disable();

    preempt_disable();

    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);

    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);

}

从代码中可以看出,在调用preempt_disable()之前调用了local_irq_disable(),即先关闭本地处理器响应外部中断的能力,这样在获取一个锁时就可以确保不会发生中断,从而避免场景中出现的死锁问题。

使用自旋锁的一条规则:任何拥有自旋锁的代码必须是原子的,不能睡眠。

当知道一个自旋锁在中断处理的上下文中有可能被使用时,应该使用spin_lock_irq函数,而不是spin_lock,后者只有在能确定中断上下文中不会使用到自旋锁的情况下才能使用。

另一个spinlock版本是:spin_lock_bh函数,该函数用来处理进程与延迟处理导致的并发中的互斥问题。

3、 信号量(semaphone

信号量的最大特点是允许调用它的进程进入睡眠状态。这也会导致对处理器拥有权的丧失,也即出现进程的切换。

在驱动程序程序中定义了一个struct semaphone型的信号量变量,需要注意的是不要直接对该变量进行赋值,而应该使用sema_init函数来初始化该信号量。

信号量上的主要操作是DOWNUPlinux内核中对信号量的DOWN操作有:

void down(struct semaphore *sem);

void down_interruptible(struct semaphore *sem);

void down_killable(struct semaphore *sem);

void down_trylock(struct semaphore *sem);

void down_timeout(struct semaphore *sem, long jiffies);

驱动中最常使用的是down_interruptible函数,

(1)      DOWN操作

代码如下:

static noinline void __sched __down(struct semaphore *sem)

{__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);}

static noinline int __sched __down_interruptible(struct semaphore *sem)

{return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT}

static noinline int __sched __down_killable(struct semaphore *sem)

{return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);}

static noinline int __sched __down_timeout(struct semaphore *sem, long jiffies)

{return __down_common(sem, TASK_UNINTERRUPTIBLE, jiffies);}

从以上可以看出,四类函数最终调用的均是__down_common函数。

首先把当前进程放到信号量sem的成员变量管理队列中,接着把当前进程的状态设置为TASK_INTERRUPTIBLE,在调用schedule_timeout使得当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。当进程被再一次调度执行时,schedule_timeout开始返回,接着根据进程被再次调度的原因进行处理:如果waiter.up不为0,说明进程在信号量sem的队列中被该信号量的UP操作所唤醒,进程可以获得信号量,返回0。如果进程是因为被用户空间发送的信号所中断或者超时引起的唤醒,则返回相应的错误代码。

所以,对down_interruptible的调用总是应该检查其返回值,以确定函数是已经获得了信号量还是因为被中断因而需要特别处理。

(2)      UP操作

Linux下只有一个UP版本。

即使不是信号量的拥有者,也可以调用UP函数来释放一个信号量。信号量的常用用途是实现互斥机制,任意一个时刻只允许一个进程进入临界区。

4、 互斥锁mutex

Linux内核针对count=1的信号量重新定义了一个新的数据结构struct mutex,称之为互斥锁。

互斥锁的DOWN操作,在linux内核中为mutex_lock函数,函数定义如下:

void __sched mutex_lock(struct mutex *lock)

{

     might_sleep();

     __mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath);

     mutex_set_owner(lock);

}

__mutex_fastpath_lock用来快速判断当前可否获得互斥锁,如果成功获得锁,则函数直接返回,否则进入到__mutex_lock_slowpath函数中。在进程进入__mutex_lock_slowpath之前,会多次检查是否有互斥锁被释放。这基于这样一个事实:拥有互斥锁的进程总是会在尽可能短的时间里释放该锁。

互斥锁的UP操作,在linux内核中为mutex_unlock函数,函数定义如下:

void __sched mutex_unlock(struct mutex *lock)

{

#ifndef CONFIG_DEBUG_MUTEXES

     mutex_clear_owner(lock);

#endif

     __mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath);

}

__mutex_fastpath_unlock__mutex_unlock_slowpath分别对应互斥锁的快速和慢速解锁操作。__mutex_fastpath_unlock函数完成的工作是把count->counter的值加1,然后返回。如果有别的进程在竞争该互斥锁,那么函数进入__mutex_unlock_slowpath,此函数主要用来唤醒在当前mutexwait_list中休眠的进程。

5、 顺序锁seqlock

顺序锁的设计思想是:对某一共享数据读取时不加锁,写的时候加锁。为保证读取过程中不会因为写入者的出现导致该共享数据的更新,在写入者与读取者之间引入一整型变量,称为顺序值。读取者在开始读取之前读取该sequence,在读取后再重新读该值,如果与之前读取的不一致,则说明本次读取操作过程中发生了数据更新,读取操作无效。因此要求写入者在写入的时候更新sequence的值。

6、 RCU

RCU全称Read-Copy-Update,意即读/-复制-更新,在linux提供的所有内核互斥设施当中属于一种免锁机制。RCU的适用模型也是读取者与写入者共存的系统。不同的是,rcu中不需要考虑读取者与写入者之间的互斥问题。

RCU的原理:将读取者和写入者要访问的共享数据放在一个指针P中,读取者通过P来访问其中的数据,而写入者则通过修改P来更新数据。具体实现上,读取者一方并没有太多的事情要做,大量的工作集中在写入者一方。

1)、读取者RCU临界区

调用rcu_read_lockrcu_read_unlock函数构建读取者自己的临界区,在临界区中获得指向共享数据区的指针,实际的读取操作就是对该指针的引用。对指针的引用必须在临界区中完成,离开临界区之后不应该出现任何形式的对该指针的引用。

2)、写入者RCU的操作

RCU操作中写入者要完成的工作是重新分配一个被保护的共享数据区,将老数据区的数据复制到新数据区,然后根据需要修改新数据区,最后用新数据区指针替换掉老的指针,替换指针的操作是一个原子操作,不需要与读取者进行互斥操作。

7、 等待队列

等待队列不是互斥机制中的一种方案。在内核中是一种常用数据结构。

等待队列本质上是一双向链表,有等待队列头和队列节点构成,当运行的进程要获得某一资源而得不到时,进程有时候需要等待,此时它可以进入睡眠状态,内核为此生成一个新的等待队列节点将睡眠的进程挂载到等待队列中。

定义等待队列的两种方法:

其一、通过DECLARE_WAIT_QUEUE_HEAD宏来完成等待队列头对象的静态定义与初始化;

其二、通过init_waitqueue_head宏在程序运行期间初始化一个头结点对象;

内核中对等待队列的核心操作是等待(wait)与唤醒(wake up)。

8、 完成接口completion

该机制主要用来在多个执行路径间作同步使用,也即协调多个执行路径的执行顺序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章