linux内核编程之并发控制

版权声明:转载请保留出处:blog.csdn.net/gentleliu。邮箱:shallnew*163.com】

只要并发的多个执行单元存在对共享资源的访问,竞态就可能发生。在 Linux 内核中,主要的竞态发生于如下几种情况

1. 多CPU共享资源,如存储器。
2. 内核支持抢占调度,一个进程在内核执行时被另外一个高优先级进行打断。
3. 中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。访问共享资源的代码区域称为临界区(critical sections),临界区需要被以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是 Linux 设备驱动中可采用的互斥途径。

1.中断屏蔽(很少单独使用)
在单 CPU 范围内避免竞态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断。中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于 Linux 内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免了。
中断屏蔽的使用方法为:
local_irq_disable() /* 屏蔽中断 */
. . .
critical section /* 临界区*/
. . .
local_irq_enable() /* 开中断*/
由于 Linux 的异步 I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失乃至系统崩溃等后果。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 内的中断,因此,并不能解决 SMP 多CPU 引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与下文将要介绍的自旋锁联合使用。与 local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前的 CPU 的中断位信息,local_irq_restore(flags)进行的是与 local_irq_save(flags)相反的操作。
如果只是想禁止中断的底半部,应使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部应该调用 local_bh_enable()。
2.自旋锁
自旋锁原语要求的包含文件是 。一个实际的锁有类型spinlock_t。
自旋锁初始化可以在编译时完成, 如下:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
或者在运行时使用:
void spin_lock_init(spinlock_t *lock);
在进入一个临界区前, 你的代码必须获得需要的 lock , 用:
void spin_lock(spinlock_t *lock);
注意所有的自旋锁等待是不可中断的。一旦你调用spin_lock, 你将自旋直到锁变为可用。为释放一个你已获得的锁, 传递它给:
void spin_unlock(spinlock_t *lock);
有很多其他的自旋锁函数, 我们将在后面看到。但是没有一个背离上面列出的函数所展示的核心概念。除了加锁和释放, 没有什么可对一个锁所作的。但是, 有几个规则关于你必须如何使用自旋锁:
(1)、在拥有锁时应该关中断。
(2)、编写需要在自旋锁下执行的代码时,注意调用的函数是否会休眠,任何拥有自旋锁代码必须是原子的。
(3)、自旋锁必须在最短时间拥有。

自旋锁主要针对 SMP 或单 CPU 但内核可抢占的情况
,对于单 CPU 和内核不支持抢占的系统,自旋锁退化为空操作。在单 CPU 和内核可抢占的系统中,自旋锁持有期间内核的抢占将被禁止。尽管用了自旋锁可以保证临界区不受别的 CPU 和本 CPU 内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部的影响。为了防止这种影响,就需要用到自旋锁的衍生。
实际上有 4 个函数可以加锁一个自旋锁:
void spin_lock(spinlock_t *lock);
void spin_lock_irq(spinlock_t *lock);    //相当于spin_lock() + local_irq_disable()。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);//禁止中断(只在本地处理器)在获得自旋锁之前; 之前的中断状态保存在 flags里。相当于spin_lock() + local_irq_save()。
void spin_lock_bh(spinlock_t *lock);    //获取锁之前禁止软件中断, 但是硬件中断留作打开的,相当于spin_lock() + local_bh_disable()。
也有 4 个方法来释放一个自旋锁; 你用的那个必须对应你用来获取锁的函数.
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

读写自旋锁一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有1个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。读者写者锁有一个类型 rwlock_t, 在 中定义. 它们可以以 2 种方式被声明和被初始化:
静态方式:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;
rwlock_t my_rwlock;
动态方式:
rwlock_init(&my_rwlock);
可用函数的列表现在应当看来相当类似。 对于读者, 有下列函数可用:
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
对于写存取的函数是类似的:
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);

3.信号量
信号量(semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。使用前需要包含头文件
定义和初始化信号量:
struct semaphore sem;
void sema_init(struct semaphore *sem, int val);//初始化信号量,并设置信号量 sem 的值为 val。
#define init_MUTEX(sem) sema_init(sem, 1)//该宏用于初始化一个用于互斥的信号量,它把信号量 sem 的值设置为 1;
     下面两个宏是定义并初始化信号量的“快捷方式”:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
前者定义一个名为 name 的信号量并初始化为1;后者定义一个名为 name 的信号量并初始化为0。
    void down(struct semaphore * sem);//该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用;
    int down_interruptible(struct semaphore * sem);//该函数功能与 down 类似,不同之处为,因为 down()而进入睡眠状态的进程不能被信号打断,但因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0;
    int down_trylock(struct semaphore * sem);//该函数尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文使用。
     在使用 down_interruptible()获取信号量时,对返回值一般会进行检查,如果非 0,通常立即返回-ERESTARTSYS,如:
  
if (down_interruptible(&sem))
       return - ERESTARTSYS;
void up(struct semaphore * sem);//该函数释放信号量 sem,唤醒等待者。
信号量一般这样被使用:
/* 定义信号量
DECLARE_MUTEX(mount_sem);
down(&mount_sem);/* 获取信号量,保护临界区
. . .
critical section /* 临界区
. . .
up(&mount_sem);/* 释放信号量
4.互斥体
下面代码定义名为 my_mutex 的互斥体并初始化它:
struct mutex my_mutex;
mutex_init(&my_mutex);
下面的两个函数用于获取互斥体:
void inline _ _sched mutex_lock(struct mutex *lock);
int _ _sched mutex_lock_interruptible(struct mutex *lock);
int _ _sched mutex_trylock(struct mutex *lock);
mutex_lock()与 mutex_lock_interruptible()的区别和 down()与 down_trylock()的区别完全一致,前者引起的睡眠不能被信号打断,而后者可以。mutex_trylock()用于尝试获得 mutex,获取不到mutex 时不会引起进程睡眠。
下列函数用于释放互斥体:
void __sched mutex_unlock(struct mutex *lock);
mutex 的使用方法和信号量用于互斥的场合完全一样:
struct mutex my_mutex; /* 定义 mutex */
mutex_init(&my_mutex); /* 初始化 mutex */
mutex_lock(&my_mutex); /* 获取 mutex */
* 临界资源*/
mutex_unlock(&my_mutex); /* 释放 mutex */

你可能感兴趣的:(linux,内核编程系列,内核,并发,异步)