Linux驱动开发——并发和竞态(信号量方式的使用④)

简介: Linux驱动开发——并发和竞态(信号量方式的使用④)

文章目录

解决竞态引起异常的方法之信号量

信号量特点

利用信号量来解决竞态引起异常的编程步骤

代码示例(修改之前的设备操作)

总结


解决竞态引起异常的方法之信号量

信号量特点

内核中的信号量和用户态下的信号量是一模一样的。

信号量又称为睡眠锁,是基于自旋锁实现的。

信号量就是解决自旋锁保护的临界区不能休眠的问题,当遇到临界区中必须进行休眠操作,此时此刻只能用信号量来解决竞态引起的异常问题。

“休眠操作”仅仅存在于进程的世界中,进程休眠是指当前进程会释放占用的CPU资源给其他进程使用,信号量仅用于进程。

如果进程获取信号量,在访问临界区时是可以进行休眠操作的;当获取信号量失败时,那么进程将进行休眠操作。


Linux内核描述信号量的数据结构:struct semaphore


利用信号量来解决竞态引起异常的编程步骤

  1. 确定代码中哪些是共享资源。
  2. 确定代码中哪些是临界区。
  3. 明确临界区中是否有休眠。如果有,必须使用信号量;如果没有,可以考虑使用信号量或者之前提到的衍生自旋锁。
  4. 访问临界区之前,先获取信号量对象:
//定义初始化信号量对象
struct seamphore sema; //定义信号量对象
sema_init(&sema, 1); //初始化信号量对象
//获取信号量
down(&sema);
//说明:获取信号量,如果获取信号量成功,进程从此函数中立马返回
  //然后可以踏踏实实的访问临界区,如果获取信号量失败,进程将进入此函数中进入不可中断的休眠状态(释放CPU资源,在休眠期间接收到信号不会立即处理信号),直到持有信号量的进程释放了信号量并且唤醒这个休眠的等待进程
//"不可中断的休眠状态":进程在休眠期间,如果接收到了一个kill信号,进程不会立即处理接收到的信号,而是获取信号量的任务释放信号量以后唤醒这个休眠的进程,进程一旦被唤醒以后会处理之前接收到的信号
//“可中断的休眠状态”:进程在休眠期间,如果接收到了一个信号进程会被立即唤醒并且处理接收到的信号。
down_interruptible(&sema);
//获取信号量,如果获取信号量成功,进程从此函数中立马返回,然后去访问临界区;如果获取信号量失败,进程将进入可中断的休眠状态。
//(休眠期间会立即处理接收到的信号)直到获取信号被唤醒或者持有信号量的任务,释放信号量唤醒之前休眠的进程。

  1. 一旦获取信号量成功,进程可以踏踏实实的访问临界区。
  2. 访问临界区之后,释放信号量并唤醒休眠的进程。
up(&sema);

  1. 获取信号量和释放信号量一定在逻辑上成对使用。

代码示例(修改之前的设备操作)

  • 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


  • 执行结果:

20200101075514243.png

总结

其实信号量不同于其他方式能够解决临界区内有休眠操作的问题,最主要的原因是信号量的获取和释放只会影响到需要访问临界区的进程任务,并且在获取访问临界区条件不成立时会进入休眠并释放CPU资源(也就是不会占用消耗CPU资源)。而屏蔽中断、自旋锁、衍生自旋锁则不一样。最主要的是几种方式在访问临界区条件不成立的时候的影响不一样(当前有任务在访问临界区,所以其他任务不能访问),具体如下:


  • 屏蔽中断: 当有任务正在访问临界区导致其他任务访问临界区条件不成立,此时因为使用屏蔽中断,则导致当前所有的中断响应暂时无效,直接影响系统的任务响应。这种影响是全局的,所以必须时间短暂否则后果很严重,自然不能有休眠等耗时操作包含在临界区内。
  • 自旋锁: 自旋锁的影响相对屏蔽中断要小点,当有任务正在访问临界区造成其他任务暂时不能访问临界区时会导致获取自旋锁失败从而进入原地空转,而原地空转是CPU在轮询是否可以获得自旋锁,也就是CPU资源并没有释放从而造成资源浪费。在这种情况下如果访问的临界区内有休眠操作,其他CPU中也有任务想要访问临界区肯定是失败的,但是又不会释放资源只能原地空转,最终导致多个CPU核资源在轮询等待一个CPU核释放自旋锁,这种现象造成资源的很大程度浪费,所以在自旋锁操作中也不能有休眠操作等耗时操作。
  • 衍生自旋锁: 衍生自旋锁=屏蔽中断+自旋锁,所以更不能运行在被保护的临界区内有耗时操作,且临界区内的操作耗时要尽肯能小。
  • 信号量: 信号量跟上面三种方式的最大不同就是当有任务访问临界区条件不成立时,当前任务会进入休眠并且释放CPU资源,当条件成立时会被唤醒从而进行被执行。这样既不会影响系统整体的任务响应也不会造成CPU资源的损耗。所以在信号量保护的临界区内能够允许有休眠等耗时操作。


相关文章
|
22天前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
1月前
|
安全 Linux API
Linux设备模型统一:桥接硬件多样性与应用程序开发的关键
在Linux的宏大世界中,各种各样的硬件设备如星辰般繁多。从常见的USB设备到复杂的网络接口卡,从嵌入式设备到强大的服务器,Linux需要在这些差异极大的硬件上运行。这就引出了一个问题:Linux是如何统一这些不同硬件的设备模型的呢?本文将探讨Linux是如何针对不同的硬件统一设备模型的,这一统一的设备模型对于应用程序开发人员来说又有何意义。让我们一探究竟🕵️‍♂️。
Linux设备模型统一:桥接硬件多样性与应用程序开发的关键
|
2月前
|
算法 Linux 测试技术
Linux C++开发中的代码优化之道:把握时机与策略
Linux C++开发中的代码优化之道:把握时机与策略
50 0
|
23天前
|
Unix Linux Shell
FFmpeg开发笔记(八)Linux交叉编译Android的FFmpeg库
在Linux环境下交叉编译Android所需的FFmpeg so库,首先下载`android-ndk-r21e`,然后解压。接着,上传FFmpeg及相关库(如x264、freetype、lame)源码,修改相关sh文件,将`SYSTEM=windows-x86_64`改为`SYSTEM=linux-x86_64`并删除回车符。对x264的configure文件进行修改,然后编译x264。同样编译其他第三方库。设置环境变量`PKG_CONFIG_PATH`,最后在FFmpeg源码目录执行配置、编译和安装命令,生成的so文件复制到App工程指定目录。
FFmpeg开发笔记(八)Linux交叉编译Android的FFmpeg库
|
2天前
|
Linux C语言
|
2天前
|
存储 Linux C++
linux信号量与PV操作知识点总结
linux信号量与PV操作知识点总结
|
3天前
|
存储 安全 Linux
【Linux】详解进程通信中信号量的本质&&同步和互斥的概念&&临界资源和临界区的概念
【Linux】详解进程通信中信号量的本质&&同步和互斥的概念&&临界资源和临界区的概念
|
6天前
|
算法 Linux API
【探索Linux】 P.22(POSIX信号量)
【探索Linux】 P.22(POSIX信号量)
16 0
|
8天前
|
安全 Linux Android开发
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
该文介绍了如何在Linux服务器上交叉编译Android的FFmpeg库以支持HTTPS视频播放。首先,从GitHub下载openssl源码,解压后通过编译脚本`build_openssl.sh`生成64位静态库。接着,更新环境变量加载openssl,并编辑FFmpeg配置脚本`config_ffmpeg_openssl.sh`启用openssl支持。然后,编译安装FFmpeg。最后,将编译好的库文件导入App工程的相应目录,修改视频链接为HTTPS,App即可播放HTTPS在线视频。
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
|
13天前
|
前端开发 Linux iOS开发
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践
【4月更文挑战第30天】Flutter扩展至桌面应用开发,允许开发者用同一代码库构建Windows、macOS和Linux应用,提高效率并保持平台一致性。创建桌面应用需指定目标平台,如`flutter create -t windows my_desktop_app`。开发中注意UI适配、性能优化、系统交互及测试部署。UI适配利用布局组件和`MediaQuery`,性能优化借助`PerformanceLogging`、`Isolate`和`compute`。
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践