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


相关文章
|
6天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
23 5
|
9天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
26 6
|
30天前
|
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开发知识可参考相关书籍。
82 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
3月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
43 6
|
3月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
49 5
|
3月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
158 3
|
3月前
|
移动开发 监控 网络协议
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
|
3月前
|
缓存 负载均衡 网络协议
Linux的TCP连接数量与百万千万并发应对策略
【8月更文挑战第15天】在Linux系统中,关于TCP连接数量的一个常见误解是认为其最大不能超过65535个。这一数字实际上是TCP端口号的上限,而非TCP连接数的直接限制。实际上,Linux服务器能够处理的TCP连接数远远超过这一数字,关键在于理解TCP连接的标识方式、系统配置优化以及应用架构设计。
474 2
|
2月前
|
Linux
linux内核 —— 读写信号量实验
linux内核 —— 读写信号量实验