解决竞态引起异常的方法之信号量
信号量特点
内核中的信号量和用户态下的信号量是一模一样的。
信号量又称为睡眠锁,是基于自旋锁实现的。
信号量就是解决自旋锁保护的临界区不能休眠的问题,当遇到临界区中必须进行休眠操作,此时此刻只能用信号量来解决竞态引起的异常问题。
“休眠操作”仅仅存在于进程的世界中,进程休眠是指当前进程会释放占用的CPU资源给其他进程使用,信号量仅用于进程。
如果进程获取信号量,在访问临界区时是可以进行休眠操作的;当获取信号量失败时,那么进程将进行休眠操作。
Linux内核描述信号量的数据结构:struct semaphore
利用信号量来解决竞态引起异常的编程步骤
- 确定代码中哪些是共享资源。
- 确定代码中哪些是临界区。
- 明确临界区中是否有休眠。如果有,必须使用信号量;如果没有,可以考虑使用信号量或者之前提到的衍生自旋锁。
- 访问临界区之前,先获取信号量对象:
//定义初始化信号量对象
struct seamphore sema; //定义信号量对象
sema_init(&sema, 1); //初始化信号量对象
//获取信号量
down(&sema);
//说明:获取信号量,如果获取信号量成功,进程从此函数中立马返回
//然后可以踏踏实实的访问临界区,如果获取信号量失败,进程将进入此函数中进入不可中断的休眠状态(释放CPU资源,在休眠期间接收到信号不会立即处理信号),直到持有信号量的进程释放了信号量并且唤醒这个休眠的等待进程
//"不可中断的休眠状态":进程在休眠期间,如果接收到了一个kill信号,进程不会立即处理接收到的信号,而是获取信号量的任务释放信号量以后唤醒这个休眠的进程,进程一旦被唤醒以后会处理之前接收到的信号
//“可中断的休眠状态”:进程在休眠期间,如果接收到了一个信号进程会被立即唤醒并且处理接收到的信号。
down_interruptible(&sema);
//获取信号量,如果获取信号量成功,进程从此函数中立马返回,然后去访问临界区;如果获取信号量失败,进程将进入可中断的休眠状态。
//(休眠期间会立即处理接收到的信号)直到获取信号被唤醒或者持有信号量的任务,释放信号量唤醒之前休眠的进程。
- 一旦获取信号量成功,进程可以踏踏实实的访问临界区。
- 访问临界区之后,释放信号量并唤醒休眠的进程。
up(&sema);
- 获取信号量和释放信号量一定在逻辑上成对使用。
代码示例(修改之前的设备操作)
- led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <mach/platform.h> //PAD_GPIO_C
#include <linux/miscdevice.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define LED_ON 0x100001
#define LED_OFF 0x100002
//声明描述LED硬件相关的数据结构
struct led_resource {
int gpio; //GPIO软件编号
char *name; //LED的名称
};
//定义初始化四个LED灯的硬件信息对象
static struct led_resource led_info[] = {
{
.name = "LED1",
.gpio = PAD_GPIO_C+12
},
{
.name = "LED2",
.gpio = PAD_GPIO_C+7
},
{
.name = "LED3",
.gpio = PAD_GPIO_C+11
},
{
.name = "LED4",
.gpio = PAD_GPIO_B+26
}
};
//共享资源
//记录设备打开状态
static int open_cnt = 1;
//初始化定义信号量对象
static struct semaphore sema;
//打开设备操作接口
int led_open(struct inode *inode, struct file *file)
{
//unsigned long flags;
int i;
//获取信号量
down(&sema);
//临界区
if(--open_cnt != 0)
{
printk("device was opened!!!\n");
open_cnt++;
up(&sema);
return -EBUSY;//返回设备忙错误码
}
up(&sema);
printk("device open success.\n");
//1.先向内核申请GPIO硬件资源
//2.然后配置GPIO为输出功能,输出0,开灯
for(i = 0; i < ARRAY_SIZE(led_info); i++) {
gpio_request(led_info[i].gpio,
led_info[i].name);
gpio_direction_output(led_info[i].gpio, 0);
}
printk("led open %s...\n", __func__);
return 0;
}
//定义ioctl操作接口
static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int kindex;
copy_from_user(&kindex, (int*)arg, sizeof(kindex));
switch(cmd){
case LED_ON:
gpio_set_value(led_info[kindex -1].gpio, 0);
printk("%s: open led %d ...\n", __func__, kindex);
break;
case LED_OFF:
gpio_set_value(led_info[kindex -1].gpio, 1);
printk("%s: close led %d ...\n", __func__, kindex);
break;
default:
printk("no opts ! \n");
return -1;
}
return 0;
}
//关闭设备操作接口
int led_close(struct inode *inode, struct file *file)
{
int i;
//获取信号量
down(&sema);
open_cnt++;
up(&sema);
//1.输出1,关灯
//2.释放GPIO硬件资源
for(i = 0; i < ARRAY_SIZE(led_info); i++) {
gpio_set_value(led_info[i].gpio, 1);
gpio_free(led_info[i].gpio);
}
printk("led close %s...\n", __func__);
}
//定义初始化LED硬件操作接口
static struct file_operations led_fops ={
.owner = THIS_MODULE,
.open = led_open, //oepn led cdev
.release = led_close, //close led cdev_init
.unlocked_ioctl = led_ioctl
};
//定义初始化混杂设备对象
static struct miscdevice led_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "myled",
.fops = &led_fops
};
//入口:insmod
static int led_init(void)
{
int i;
//初始化信号量
sema_init(&sema, 1);
//1.先向内核申请GPIO硬件资源
//2.然后配置GPIO为输出功能,输出0,开灯
for(i = 0; i < ARRAY_SIZE(led_info); i++) {
gpio_request(led_info[i].gpio,
led_info[i].name);
gpio_direction_output(led_info[i].gpio, 1);
}
//注册混杂设备对象
misc_register(&led_misc);
printk("led init...\n");
return 0;
}
//出口:rmmod
static void led_exit(void)
{
int i;
//1.输出1,关灯
//2.释放GPIO硬件资源
for(i = 0; i < ARRAY_SIZE(led_info); i++) {
gpio_set_value(led_info[i].gpio, 1);
gpio_free(led_info[i].gpio);
}
//卸载混杂设备对象
misc_deregister(&led_misc);
printk("led exit...\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
- led_test.c
/*************************************************************************
> File Name: led_test.c
> Author:
> Mail:
> Created Time: 2019年12月23日 星期一 21时59分34秒
************************************************************************/
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
int i;
fd = open("/dev/myled", O_RDWR);
if(fd < 0)
{
printf("open myled fail.\n");
perror("open fail.\n");
return -1;
}
printf("open myled.\n");
for(i = 8; i > 0; i--)
{
sleep(1);
printf("wait %d s \n", i);
}
printf("close myled.\n");
close(fd);
return 0;
}
- Makefile
obj-m += led_drv.o
all:
make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
make -C /opt/kernel SUBDIRS=$(PWD) clean
- 执行结果:
总结
其实信号量不同于其他方式能够解决临界区内有休眠操作的问题,最主要的原因是信号量的获取和释放只会影响到需要访问临界区的进程任务,并且在获取访问临界区条件不成立时会进入休眠并释放CPU资源(也就是不会占用消耗CPU资源)。而屏蔽中断、自旋锁、衍生自旋锁则不一样。最主要的是几种方式在访问临界区条件不成立的时候的影响不一样(当前有任务在访问临界区,所以其他任务不能访问),具体如下:
- 屏蔽中断: 当有任务正在访问临界区导致其他任务访问临界区条件不成立,此时因为使用屏蔽中断,则导致当前所有的中断响应暂时无效,直接影响系统的任务响应。这种影响是全局的,所以必须时间短暂否则后果很严重,自然不能有休眠等耗时操作包含在临界区内。
- 自旋锁: 自旋锁的影响相对屏蔽中断要小点,当有任务正在访问临界区造成其他任务暂时不能访问临界区时会导致获取自旋锁失败从而进入原地空转,而原地空转是CPU在轮询是否可以获得自旋锁,也就是CPU资源并没有释放从而造成资源浪费。在这种情况下如果访问的临界区内有休眠操作,其他CPU中也有任务想要访问临界区肯定是失败的,但是又不会释放资源只能原地空转,最终导致多个CPU核资源在轮询等待一个CPU核释放自旋锁,这种现象造成资源的很大程度浪费,所以在自旋锁操作中也不能有休眠操作等耗时操作。
- 衍生自旋锁: 衍生自旋锁=屏蔽中断+自旋锁,所以更不能运行在被保护的临界区内有耗时操作,且临界区内的操作耗时要尽肯能小。
- 信号量: 信号量跟上面三种方式的最大不同就是当有任务访问临界区条件不成立时,当前任务会进入休眠并且释放CPU资源,当条件成立时会被唤醒从而进行被执行。这样既不会影响系统整体的任务响应也不会造成CPU资源的损耗。所以在信号量保护的临界区内能够允许有休眠等耗时操作。