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资源的损耗。所以在信号量保护的临界区内能够允许有休眠等耗时操作。


相关文章
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
4234 77
|
12月前
|
Ubuntu 搜索推荐 Linux
详解Ubuntu的strings与grep命令:Linux开发的实用工具。
这就是Ubuntu中的strings和grep命令,透明且强大。我希望你喜欢这个神奇的世界,并能在你的Linux开发旅程上,通过它们找到你的方向。记住,你的电脑是你的舞台,在上面你可以做任何你想做的事,只要你敢于尝试。
512 32
|
消息中间件 Linux
Linux中的System V通信标准--共享内存、消息队列以及信号量
希望本文能帮助您更好地理解和应用System V IPC机制,构建高效的Linux应用程序。
547 48
|
消息中间件 Linux
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
1008 20
|
Linux
【Linux】System V信号量详解以及semget()、semctl()和semop()函数讲解
System V信号量的概念及其在Linux中的使用,包括 `semget()`、`semctl()`和 `semop()`函数的具体使用方法。通过实际代码示例,演示了如何创建、初始化和使用信号量进行进程间同步。掌握这些知识,可以有效解决多进程编程中的同步问题,提高程序的可靠性和稳定性。
980 19
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
341 5
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
427 6
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
832 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
332 6
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
492 5