一、前言
目前为止,我们很少关注并发问题——亦即,当系统试图一次完成多个任务时会产生什么结果。但是,对并发的管理是操作系统编程中核心的一个问题。为了响应现代硬件和应用程序的需求,Linux内核已经发展到同时发展更多事情的年代了。设备驱动程序开发者必须在开始设计时考虑并发因素,并且还必须对内核提供的并发管理设施有坚实的理解。
竞态会导致共享数据的非控制访问。发生错误的访问模式,会产生非预期的结果。因为竟态是一种极端低可能性的事件,因此程序员往往会忽视竟态。但是在计算机世界中,百分之一的事件可能没几秒就会发生,而其结果是灾难性的。
二、访问共享数据手段
那么linux内核中如何做到对对共享资源的互斥访问呢?在linux驱动编程中,常用的解决并发与竟态的手段有信号量与互斥锁,Completions 机制,自旋锁(spin lock),以及一些其他的不使用锁的实现方式。下面一一介绍。
1、信号量和互斥体
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临届区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。
/*初始化函数*/
void sema_init(struct semaphore *sem, int val);``
/*方法一、声明+初始化宏*/
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
/*方法二、初始化函数*/
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
/*带有“_LOCKED”的是将信号量初始化为0,即锁定,允许任何线程访问时必须先解锁。没带的为1。*/
P函数为:
void down(struct semaphore *sem); /*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore *sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore *sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/
函数为:
void up(struct semaphore *sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/
2、读取者/写入者信号量
信号量对所有的调用者执行互斥。在一些任务中只需要读取受保护的数据结构,而其他的任务必须做出修改。这样做可以大大提高性能,因为读取任务可并行完成他们的任务,而不需要等待其他读取者退出临界区。
/*初始化*/
void init_rwsem(struct rw_semaphore *sem);
/*只读接口*/
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
/*写入接口*/
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);/*该函数用于把写者降级为读者,这
3、Completions 机制
完成量(completion)提供了一种比信号量更好的同步机制,它用于一个执行单元执行完某事后,通知另一个执行单元或多个执行单元。
</pre></div><div><pre name="code" class="cpp">// 定义完成量
struct completion my_completion;
// 初始化completion
init_completion(&my_completion);
// 定义和初始化快捷方式:
DECLEAR_COMPLETION(my_completion);
// 等待一个completion被唤醒
void wait_for_completion(struct completion *c);
// 唤醒完成量
void cmplete(struct completion *c);
void cmplete_all(struct completion *c);
4、自旋锁
若一个进程要访问临界资源,测试锁空闲,则进程获得这个锁并继续执行;若测试结果表明锁扔被占用,进程将在一个小的循环内重复“测试并设置”操作,进行所谓的“自旋”,等待自旋锁持有者释放这个锁。自旋锁与互斥锁类似,但是互斥锁不能用在可能睡眠的代码中,而自旋锁可以用在可睡眠的代码中,典型的应用是可以用在中断处理函数中。
// 定义自旋锁
spinlock_t spin;
// 初始化自旋锁
spin_lock_init(lock);
// 获得自旋锁:若能立即获得锁,它获得锁并返回,否则,自旋,直到该锁持有者释放
spin_lock(lock);
// 尝试获得自旋锁:若能立即获得锁,它获得并返回真,否则立即返回假,不再自旋
spin_trylock(lock);
// 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用
spin_unlock(lock);
自旋锁的使用:
// 定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); // 获取自旋锁,保护临界区
... // 临界区
spin_unlock(); // 解锁
自旋锁持有期间内核的抢占将被禁止。自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为防止这种影响,需要用到自旋锁的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
5、原子变量与位操作
完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在。原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。
void atomic_set(atomic_t *v, int i); /*设置原子变量 v 为整数值 i.*/
atomic_t v = ATOMIC_INIT(0); /*编译时使用宏定义 ATOMIC_INIT 初始化原子值.*/
int atomic_read(atomic_t *v); /*返回 v 的当前值.*/
void atomic_add(int i, atomic_t *v);/*由 v 指向的原子变量加 i. 返回值是 void*/
void atomic_sub(int i, atomic_t *v); /*从 *v 减去 i.*/
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v); /*递增或递减一个原子变量.*/
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
/*进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.*/
int atomic_add_negative(int i, atomic_t *v);
/*加整数变量 i 到 v. 如果结果是负值返回值是真, 否则为假.*/
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
/*像 atomic_add 和其类似函数, 除了它们返回原子变量的新值给调用者.*/
/*位操作*/
void set_bit(nr, void *addr); /*设置第 nr 位在 addr 指向的数据项中。*/
void clear_bit(nr, void *addr); /*清除指定位在 addr 处的无符号长型数据.*/
void change_bit(nr, void *addr);/*翻转nr位.*/
test_bit(nr, void *addr); /*这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.*/
/*以下原子操作如同前面列出的, 除了它们还返回这个位以前的值.*/
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
6、seqlock(顺序锁)
使用seqlock锁,读执行单元不会被写执行单元阻塞,即读执行单元可以在写执行单元对被seqlock锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。写执行单元之间仍是互斥的。
// 获得顺序锁
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh()
// 释放顺序锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh()
// 写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a);
... // 写操作代码块
write_sequnlock(&seqlock_a);
读执行单元操作:
// 读开始:返回顺序锁sl当前顺序号
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags)
// 重读:读执行单元在访问完被顺序锁sl保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。若有写操作,重读
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
// 读执行单元使用顺序锁的模式如下:
do{
seqnum = read_seqbegin(&seqlock_a);
// 读操作代码块
...
}while(read_seqretry(&seqlock_a, seqnum));
7、读取-复制-更新
读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率. 它在驱动中的使用很少。
三、实例——原子操作
在上一篇博客基础上添加例程,博客地址:
https://blog.csdn.net/Feng_8071/article/details/84726338
在scull_sem.c(上一例程的scull.c)
module_param(scull_major,int,S_IRUGO);
module_param(scull_minor,int,S_IRUGO);
module_param(scull_nr_devs,int,S_IRUGO);
struct scull_dev *scull_devices;
/*添加*/
**static atomic_t scull_available = ATOMIC_INIT(1);**
/*若设备以读写方式打开,它的长度截零,未锁定信号量*/
int scull_open(struct inode *inode,struct file *filep)
{
struct scull_dev *dev;
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filep->private_data = dev;
if((filep->f_flags & O_ACCMODE) == O_WRONLY){
/*if(down_interruptible(&dev->sem))
* return -ERESTARTSYS;
* */
scull_trim(dev);
/*up(&dev->sem);*/
}
/*添加*/
**if(!atomic_dec_and_test(&scull_available)){
atomic_inc(&scull_available);
return -EBUSY;
}**
return 0;
}
int scull_release(struct inode *inode,struct file *filep)
{
/*添加*/
**atomic_inc(&scull_available);**
printk(KERN_EMERG "Entering scull_release_module!\n");
return 0;
}
用户测试程序app_sem.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
void main(void)
{
int fd;
time_t timer;//time_t就是long int 类型
struct tm *tblock;
timer = time(NULL);
tblock = localtime(&timer);
printf("Local time is: %s\n", asctime(tblock));
fd = open("/dev/scull_sem0",O_RDWR);
if( fd < 0 )
{
perror("open");
return;
}
sleep(10);
close(fd);
timer = time(NULL);
tblock = localtime(&timer);
printf("Local time is: %s\n", asctime(tblock));
exit(0);
}
四、实验步骤
1、首先先在主目录下make,编译模块,生成.ko文件。
出现如下:
make[1]: 正在进入目录 `/usr/src/linux-2.6.35.3'
Building modules, stage 2.
MODPOST 1 modules
LD [M] /root/Cdev_Driver/Ldd-3/scull.ko
make[1]:正在离开目录 `/usr/src/linux-2.6.35.3'
2、2、使用chmod +x 命令将scull_load.sh和scull_unload.sh属性更改为可执行,脚本才能执行
3、打开另一个界面2,输入cat /proc/kmsg,查看printk打印结果
4、在第一个界面1运行脚本scull_load.sh,装载设备;
5、将用户程序app_sem.c编译,生产可执行文件app_sem.exe
gcc app_sem.c -o app_sem.exe
(1)如果直接执行,显示如下:
./app_sem.exe
Local time is: Sat Dec 8 19:30:17 2018
Local time is: Sat Dec 8 19:30:27 2018
间隔10s后退出
(2)后台执行或者两个界面,显示如下:
./app_sem.exe
Local time is: Sat Dec 8 19:32:31 2018
open: Device or resource busy
root# ./app_sem.exe
Local time is: Sat Dec 8 19:32:32 2018
open: Device or resource busy
root# ./app_sem.exe
Local time is: Sat Dec 8 19:32:34 2018
open: Device or resource busy
root# ./app_sem.exe
Local time is: Sat Dec 8 19:32:35 2018
若时间未到,显示“busy”,只能一个进程拥有;若前一个进程退出,另一个进程才能执行
五、总结
总的来说,信号量操作相对简单,避免不同进程同时操作同一共享区,以免发生竞态现象。再接再厉!