内核(驱动)编程中的并发控制
在为操作系统编写驱动设备时,因为涉及到中断、多任务和多处理器SMP的处理,所以内核提供了诸如原子操作、信号量、完成量等几种并发控制机制,对公用资源进行保护。下文将分别予以阐述。
1、原子变量
原子变量就是,在对其进行操作时不会被其它任务或中断打断。而原子操作需要硬件的支持,因此时架构相关的,其API和原子类型的定义在内核include/asm/atomic.h
文件中,都是用汇编语言实现的。它的优点是使用简单,但缺点是功能单一,只能做计数操作,保护的东西太少。在Linux中,原子变量的定义如下:
typedef struct{
volatile int counter;
}atomic_t;
在Linux中定义了两种原子变量操作方法,一是原子整型操作,二是原子位操作。
1.1 原子整型操作
-
定义并初始化atomic_t变量
atomic_t counter = ATOMIC_INIT(0); //定义并初始化原子变量counter为0.
-
设置atomic_t变量的值
atomic_set(counter, 2); //设置原子变量counter的值为2.
-
读atomic_t变量的值
var = atomic_read(counter); // 读原子变量counter的值到var中。
-
原子变量的加减法
atomic_add(2, &counter); //将原子变量counter加2.
atomic_sub(2, &counter); //将原子变量counter减2.
-
原子变量的自增/自减
atomic_inc(&counter); //将原子变量counter自增1
atomic_dec(&counter); //将原子变量counter自减1
-
原子变量的加减测试
atomic_inc_and_test(&counter); //将原子变量counter自增1,若结果为0返回真,否则返回假。
atomic_dec_and_test(&counter); //将原子变量counter自减1,若结果为0返回真,否则返回假。
1.2 原子位操作
函数原型:static inline void set_bit(int nr, volatile unsigned long *addr)
-
设置atomic_t变量的某一位
set_bit(nr, &addr); //设置原子变量addr的第nr位.
-
清除atomic_t变量的某一位
clear_bit(nr, &addr); //清除原子变量addr的第nr位.
-
取反atomic_t变量的某一位
change_bit(nr, &addr); //取反原子变量addr的第nr位.
-
测试及设置atomic_t变量的某一位
test_and_set_bit(nr, &addr); //返回原子变量addr的第nr位,然后设置该位.
-
测试及清除atomic_t变量的某一位
test_and_clear_bit(nr, &addr); //返回原子变量addr的第nr位,然后清除该位.
-
测试及取反atomic_t变量的某一位
test_and_change_bit(nr, &addr); //返回原子变量addr的第nr位,然后取反该位.
- 在linux中还定义了一组与原子位操作函数功能相同的函数,其函数名是在原子位操作函数名前加两个下划线。区别在于他们不会保证是一个原子操作。
2、自旋锁
自旋锁是一种阻塞结构(忙等待),这样会对系统的性能有所影响,所以不应该长时间持有,他是一种适合短时间锁定的轻量级的加锁机制,另外,自旋锁不能递归使用。
自旋锁的类型也是一个结构体,即struct spinlock_t。下面对它的操作函数进行介绍:
2.1 定义和初始化自旋锁
-
定义时初始化
spinlock_t lock = SPIN_LOCK_UNLOCKED; //大写字母表示的是一个初始化宏
- 动态初始化
spinlock_t lock;
spin_lock_init(lock);
-
锁定自旋锁
spin_lock(lock); //这个宏一直等待,直到获得自旋锁
-
释放自旋锁
spin_unlock(lock); //这个宏立刻释放自旋锁
-
自旋锁的使用举例
在驱动程序中,有些设备只允许打开一次,那么就需要一个自旋锁保护表示设备打开次数的count变量。如果不对count变量进行保护,当该设备被频繁打开的话,容易出现错误的count计数。
int count = 0;
spinlock_t lock;
int xxx_init(void)
{
...
spin_lock_init(&lock);
...
}
/* 设备打开函数 */
int xxx_open(struct inode *inode, struct file* filp)
{
...
spin_lock(&lock);
/* 临界代码 */
if(count) /*已经被其它程序打开过了*/
{
spin_unlock(&lock);
return -EBUSY;
}
count++;
spin_unlock(&lock);
...
}
/* 设备释放函数 */
int xxx_open(struct inode *inode, struct file* filp)
{
...
spin_lock(&lock);
count--;
spin_unlock(&lock);
...
}
3、信号量
Linux中实现了两种信号量,一种用于内核程序中,另一种应用于应用程序中,本文仅介绍内核中的信号量。信号量与自旋锁的不同点在于:当一个进程或线程试图去获取一个已经被锁定的信号量时,它不会向自旋锁一样在原地忙等待,而是将自身加入到系统的一个等待队列中去睡眠,直到拥有信号量的进程释放该信号量后,才会被系统唤醒并再次尝试获取该信号量。此处也提醒我们,只有能够睡眠的进程(函数)才能使用信号量,向中断处理函数那样需要立刻执行的函数是不能使用信号量的。
3.1 信号量的定义
在不同的实现中,信号量的实现可能不同。在Linux中其定义如下:
struct semaphore {
spinlock_t lock; //用来对count起保护作用
unsigned int count;
struct list_head wait_list;
};
- 关于count:等于0时,表示信号量正在被一个进程使用,现在不可以获取,且等待队列wait_list中没有进程在等待信号量;小于0时,代表wait_list中还有-count个进程在等待信号量;大于0时,表示可以获取该信号量。count初始化的值代表该信号量可以同时被多少进程持有。
- 关于wait_list:它是一个链表,将所有等待该信号量的正在睡眠的进程组成一个链表结构。
3.2信号量的使用
-
定义一个信号量
struct semaphore sema;
-
初始化一个信号量
sema_init(sema, val); // 初始化信号量sema为val
当sema中的count为1时,我们称为互斥体(同一时间仅有一个进程持有该信号量),他有专门的宏来进行初始化:
init_MUTEX(sema); //初始化sema信号量为1。
init_MUTEX_LOCKED(sema); //初始化sema信号量为0。
-
锁定信号量
down(&sema);
//如果请求不到会导致进程睡眠,且不会被其它信号唤醒,故不能用于中断上下文中;
down_interruptible(&sema); //
如果请求不到会导致进程睡眠,但可以被其它信号唤醒。所以在调用该函数时,要检查返回值,以判断被唤醒的原因。 -
释放信号量
up(&sema);
- 信号量的使用
struct semaphore sema;
int xxx_init(void)
{
...
init_MUTEX(&sema);
...
}
int xxx_open(struct inode *inode, struct file *filp)
{
...
down(&sema);
不允许其它进程访问该临界资源区
...
return 0;
}
int xxx_release(struct inode *inode, struct file *filp)
{
...
up(&sema);
...
return 0;
}
-
信号量用于同步
当信号量被初始化为0时,可以用来保证两个进程的执行顺序,如下图所示:
信号量用于同步操作 - 总结:信号了用于在多个进程之间互斥,但信号量的执行会引起进程的睡眠,而睡眠需要进程上下文的切换,是很耗费时间的。所以,只有在一个进程对其保护的资源占用时间要比进程切换的时间长很多时,才划算!
4、完成量
上节中讲的进程间的同步(一个线程等待另一个线程完成某操作后才能继续执行),在Linux中有专门的机制(虽然使用信号量也可以实现)叫完成量,即一个线程发送一个信号通知另一个线程开始完成某个任务。
4.1完成量的定义
struct completion{
unsigned int done;
wait_queue_head_t wait;
};
done:维护一个计数,其被初始化为1。当done为0时,会将拥有完成量的线程置于等待状态;当其值大于1时,表示等待完成量的函数可以立刻执行!
wait:存放所有等待该完成量的正在睡眠的进程组成的链表
4.2完成量的使用
-
定义一个完成量
struct completion com;
-
初始化一个完成量
init_completion(&com);
//将done设置为0 -
定义并初始化一个完成量
DECLARE_COMPLETION(com);
-
等待完成量
wait_for_completion(&com);
// 线程将一直等待,且不会被中断打断。 -
释放完成量
complete(&com);
//只唤醒一个等待的进程
complete_all(&com);
//唤醒所有等待的进程 -
完成量的使用
struct completion com;
int xxx_init(void)
{
...
init_completion(&com);
...
}
int xxx_A(void)
{
...
/* 代码1 */
wait_for_completion(&com);
/* 代码3 */
return 0;
}
int xxx_B(void)
{
...
/* 代码2 */
complete(&com);
...
return 0;
}
初始化后com中的done值为0,此时若xxx_A先执行,则当执行完成代码1 后便会进入睡眠,等待进程xxx_B执行完代码2,并释放完成量(使done加1),此时系统会唤醒处于完成量com中的等待队列里正在睡觉的进程A,然后进程A继续执行完代码3.