Linux驱动开发——并发和竞态(自旋锁方式的使用③)

简介: Linux驱动开发——并发和竞态(自旋锁方式的使用③)

文章目录

解决竞态引起异常的方法之自旋锁

自旋锁特点:

自旋锁解决竞态引起异常的编程步骤

示例:利用自旋锁来解决之前案例中的代码漏洞(之前按键触发的问题)

示例二:同时间只能一个进程操作LED设备(同前一篇使用屏蔽中断类似需求)

进阶(衍生自旋锁)

衍生自旋锁的特点

利用衍生自旋锁解决竞态引起异常的编程步骤

修改LED设备同一时间仅一个进程能够操作的示例:

总结


解决竞态引起异常的方法之自旋锁

自旋锁特点:

自旋锁:


  • 能够解决多核引起的竞态问题.
  • 能够解决进程与进程之间的抢占引起的竞态问题,
  • 自旋锁无法解决中断引起的竞态问题,自旋锁保护的临界区的代码执行速度要快,更不能进行进行休眠操作,没有获取自旋锁的任务将会原地忙等待(原地空转)。

Linux内核描述自旋锁的数据类型:spinlock_t


自旋锁解决竞态引起异常的编程步骤

  • 明确驱动代码中哪些是临界区。
  • 明确驱动代码中哪些是共享资源。
  • 明确临界区中是否有休眠操作,如果有,势必不考虑此方法;如果没有,还要考虑是否有中断参与;如果有中断参与,势必不考虑,如果没有中断参与,可以考虑使用。
  • 访问临界区之前先获取自旋锁。
//定义初始化一个自旋锁对象
spinlock_t lock; //定义
spin_lock_init(&lock); //初始化
spin_lock(&lock);
//获取自旋锁,如果获取成功,立马返回即可访问临界区。
//如果获取失败,任务将在此函数中进行循环忙等待,
//直到持有自旋锁的任务释放自旋锁。


  • 任务获取自旋锁以后,即可踏踏实实的访问临界区。
  • 访问临界区之后,记得要释放自旋锁。
spin_unlock(&lock);


  • 注意:获取自旋锁和释放自旋锁务必在逻辑上成对使用。

示例:利用自旋锁来解决之前案例中的代码漏洞(之前按键触发的问题)

  • mytimer_drv.c
/*************************************************************************
  > File Name: btn_drv.c
  > Author: 
  > Mail: 
  > Created Time: 2019年12月29日 星期日 09时08分30秒
 ************************************************************************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h>
struct btn_resource{
    int gpio; //按键对应的GPIO
    char *name;//gpio Name
    int code;//按键值
};
struct led_resource{
    int gpio;
    char *name;
};
//定义吃时候按键对应的硬件信息
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A + 28,
        .name = "KEY_UP",
        .code = KEY_UP
    },
    {
        .gpio = PAD_GPIO_B + 9,
        .name = "KEY_DOWN",
        .code = KEY_DOWN
    }
};
static struct led_resource led_info[] ={
    {
        .gpio = PAD_GPIO_C + 12,
        .name = "LED1"
    },
    {
        .gpio = PAD_GPIO_C + 7,
        .name = "LED2"
    },
    {
        .gpio = PAD_GPIO_C + 11,
        .name = "LED3"
    },
    {
        .gpio = PAD_GPIO_B + 26,
        .name = "LED4"
    }
};
//记录按键的硬件信息
static struct btn_resource *pdata;
//定义定时器对象
static struct timer_list mytimer;
//定义定时器时间
static int mytimer_value = 5;
static int led_cmd_flag = 0;
//tasklet process mytimer_function led opts
static void timer_tasklet_function(unsigned long data)
{
    //open led or close led
    int i = 0;
    int *p_data = data;
    printk("%s : current timer value = %d, state = %d\n", __func__, mytimer_value, *p_data);
    if(*p_data)
    {
        *p_data = 0;
        for(i = 0; i < ARRAY_SIZE(led_info); i++)
        {
            gpio_set_value(led_info[i].gpio, 0);
        }
    }
    else
    {
        *p_data = 1;
        for(i = 0; i < ARRAY_SIZE(led_info); i++)
        {
            gpio_set_value(led_info[i].gpio, 1);
        }
    }
    printk("tasklet process : %s \n", __func__);
}
//定义初始化一个tasklet对象
static DECLARE_TASKLET(mytimer_tasklet, timer_tasklet_function, (unsigned long)&led_cmd_flag);
//定义初始化一个自旋锁对象
static spinlock_t mylock;
//定时器处理函数
//data = (unsigned long)&g_data
static void mytimer_function(unsigned long data)
{
    //登记底半部tasklet处理函数
    tasklet_schedule(&mytimer_tasklet);
    //add_timer(&mytimer);
    mod_timer(&mytimer, jiffies + mytimer_value * HZ);
    printk("schedule timer top process : %s \n", __func__);
}
//工作队列方式处理定时器延长或缩短
//work = &btn_work
static void btn_work_function(struct work_struct *work)
{
    switch(pdata->code)
    {
        case KEY_UP:
            //add expir timeout
            spin_lock(&mylock);
            if(mytimer_value <= 10)
                mytimer_value++;
    spin_unlock(&mylock);
            break;
        case KEY_DOWN:
            //submit expir timeout
            spin_lock(&mylock);
            if(mytimer_value > 0)
                mytimer_value--;
    spin_unlock(&mylock);
            break;
        default:
            printk("dev->code is not impire.\n");
    }
    printk("btn work function : %s \n", __func__);
}
//定义工作队列
static struct work_struct btn_work;
//中断处理函数
//不同的按键触发irq不同,KEY_UP对应irq = gpio_to_irq(GPIOA28)
//响应的参数不同,dev = &btn_info[0]或者dev = &btn_info[1]
static irqreturn_t button_isr(int irq, void *dev)
{
    //获取当前硬件信息
    pdata = (struct btn_resource *)dev;
    //登记工作队列
    schedule_work(&btn_work);
    //验证顶半部先执行
    printk("top func : %s mytimer_value = %d\n", __func__, mytimer_value);
    return IRQ_HANDLED;//返回执行成功
}
static int mytimer_init(void)
{
    int i;
  //初始化自旋锁
  spin_lock_init(&mylock);
    //吃时候定时器对象
    init_timer(&mytimer);
    //指定定时器超时时间
    mytimer.expires = jiffies + 5 * HZ;
    mytimer.function = mytimer_function;
    //指定参数
    mytimer.data = (unsigned long)&led_cmd_flag;
    add_timer(&mytimer);
    for(i = 0; i < ARRAY_SIZE(btn_info); i++)
    {
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_request(btn_info[i].gpio, btn_info[i].name);
        //IRQF_TRIGGER_FALLING
        //IRQF_TRIGGER_RISING
        request_irq(irq, button_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, btn_info[i].name, &btn_info[i]);
    }
    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 btn timer init...\n");
    //初始化工作队列
    INIT_WORK(&btn_work, btn_work_function);
    return 0;
}
static void mytimer_exit(void)
{
    int i;
    del_timer(&mytimer);
    for(i =0; i < ARRAY_SIZE(btn_info); i++)
    {
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_free(btn_info[i].gpio);
        free_irq(irq, &btn_info[i]);
    }
    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 btn timer exit...\n");
}
module_init(mytimer_init);
module_exit(mytimer_exit);
MODULE_LICENSE("GPL");


  • Makefile
obj-m += mytimer_drv.o
all:
  make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
  make -C /opt/kernel SUBDIRS=$(PWD) clean


  • 执行结果


2019123122184129.png

2019123122184129.png

示例二:同时间只能一个进程操作LED设备(同前一篇使用屏蔽中断类似需求)

  • led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
//共享资源
static int open_cnt = 1; //记录LED打开的状态开关
//定义自旋锁对象
static spinlock_t lock;
static int led_open(struct inode *inode, 
                        struct file *file)
{
    //获取自旋锁
    spin_lock(&lock);
    //临界区
    if(--open_cnt != 0) {
        printk("设备已被打开!\n");
        open_cnt++;
        //释放自旋锁
        spin_unlock(&lock);
        return -EBUSY;//返回设备忙错误码
    }
    //释放自旋锁
    spin_unlock(&lock);
    printk("设备打开成功!\n");
    return 0; //open返回成功
}
static int led_close(struct inode *inode, 
                        struct file *file)
{
    //获取自旋锁
    spin_lock(&lock);
    //临界区
    open_cnt++;
    //释放自旋锁
    spin_unlock(&lock);
    return 0;
}
//定义初始化硬件操作接口对象
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_close
};
//定义初始化混杂设备对象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};
static int led_init(void)
{
    misc_register(&led_misc);
    //初始化自旋锁对象
    spin_lock_init(&lock);
    return 0;
}
static void led_exit(void)
{
    misc_deregister(&led_misc);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


  • led_test.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main(void)
{
    int fd;
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0) {
        printf("打开设备失败!\n");
        return -1;
    }
    sleep(100000000);
    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


  • 执行结果

20191231222752352.png

进阶(衍生自旋锁)

衍生自旋锁的特点

衍生自旋锁其实就是结合目前介绍的两种解决竞态问题的方法同时使用,即衍生自旋锁 = 屏蔽中断 + 自旋锁。

所以衍生自旋锁能够解决所有的竞态引起的异常问题。

衍生自旋锁保护的临界区的代码执行速度也必须要快且不能有休眠操作,因为在这段被保护的代码不仅屏蔽了中断而且还上了锁。当然同自旋锁一样,没有获取到自旋锁的任务将会原地忙等待(原地空转)。


Linux内核描述衍生自旋锁的数据类型其实也是:spinlock_t,区别就是在上锁的地方使用的不是之前的spin_lock接口而是上锁屏蔽中断接口:spin_lock_irqsave.


利用衍生自旋锁解决竞态引起异常的编程步骤

  1. 明确驱动代码中哪些是临界区。
  2. 明确驱动代码中哪些是共享资源。
  3. 明确临界区中是否有休眠操作,如果有,则不考虑使用衍生自旋锁,如果没有则可以考虑使用。
  4. 访问临界区之前需要获取自旋锁,方法如下:
//定义初始化一个衍生自旋锁对象
spinlock_t lock; //定义
spin_lock_init(&lock); //初始化
unsigned long flags;
spin_lock_irqsave(&lock, flags);
//屏蔽中断,获取衍生自旋锁,如果获取成功,立马返回即可访问临界区,如果获取失败,任务将在此函数中进行循环忙等待直到持有自旋锁的任务释放自旋锁

  1. 任务获取衍生自旋锁后,即可以踏踏实实的访问临界区。
  2. 访问临界区之后,记得要释放衍生自旋锁,恢复中断。
spin_unlock_irqrestore(&lock);

  1. 注意:获取衍生自旋锁和释放衍生自旋锁务必在逻辑上成对使用。

修改LED设备同一时间仅一个进程能够操作的示例:

  • led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
//共享资源
static int open_cnt = 1; //记录LED打开的状态开关
//定义自旋锁对象
static spinlock_t lock;
static int led_open(struct inode *inode, 
                        struct file *file)
{
    unsigned long flags;
    //屏蔽中断,获取衍生自旋锁
    spin_lock_irqsave(&lock, flags);
    //临界区
    if(--open_cnt != 0) {
        printk("设备已被打开!\n");
        open_cnt++;
        //释放自旋锁,恢复中断
        spin_unlock_irqrestore(&lock, flags);
        return -EBUSY;//返回设备忙错误码
    }
    //释放自旋锁,恢复中断
    spin_unlock_irqrestore(&lock, flags);
    printk("设备打开成功!\n");
    return 0; //open返回成功
}
static int led_close(struct inode *inode, 
                        struct file *file)
{
    unsigned long flags;
    //屏蔽中断,获取衍生自旋锁
    spin_lock_irqsave(&lock, flags);
    //临界区
    open_cnt++;
    //释放自旋锁,恢复中断
    spin_unlock_irqrestore(&lock, flags);
    return 0;
}
//定义初始化硬件操作接口对象
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_close
};
//定义初始化混杂设备对象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};
static int led_init(void)
{
    misc_register(&led_misc);
    //初始化自旋锁对象
    spin_lock_init(&lock);
    return 0;
}
static void led_exit(void)
{
    misc_deregister(&led_misc);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


  • led_test.c(同上,不详写了)

  • Makefile(同上,不详写了)

  • 执行结果:


20191231225017443.png

20191231225017443.png

总结

本篇介绍了Linux内核解决竞态引起的异常处理中的自旋锁方法,但是由于自旋锁不能够避免中断引起的竞态问题,所以内核给出了衍生自旋锁的解决方法,也就是将屏蔽中断和自旋锁组合起来,这样就可以解决多核、中断、多任务等各种情况下的竞态问题了。(在被保护的临界区内还是不能有休眠操作),所以当我们在面对全局变量、共享资源等情况下首先应该优先采取的方案也就是目前看到的最好的方法——衍生自旋锁。


  • 当然,后面还会提到剩下的两种方法——信号量和原子操作。


相关文章
|
13天前
|
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开发知识可参考相关书籍。
54 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
3月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
41 6
|
3月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
46 5
|
3月前
|
移动开发 监控 网络协议
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
|
3月前
|
缓存 负载均衡 网络协议
Linux的TCP连接数量与百万千万并发应对策略
【8月更文挑战第15天】在Linux系统中,关于TCP连接数量的一个常见误解是认为其最大不能超过65535个。这一数字实际上是TCP端口号的上限,而非TCP连接数的直接限制。实际上,Linux服务器能够处理的TCP连接数远远超过这一数字,关键在于理解TCP连接的标识方式、系统配置优化以及应用架构设计。
430 2
|
3月前
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
|
3月前
|
网络协议 Linux
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
在Linux中,如何查看 http 的并发请求数与其 TCP 连接状态?
|
4月前
|
Ubuntu Linux Docker
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
Java演进问题之Alpine Linux创建更小的Docker镜像如何解决
|
Linux Shell
linux驱动开发--内核定时器
<p>1、内核定时器</p> <p>时钟中断:由系统的定时硬件以周期性的时间间隔发生,这个间隔(也就是频率)由内核根据常数HZ来确定。<br> HZ常数:她是一个与体系结构无关的常数,可以配置50-1200之间,可以在内核中配置<br> tick:她是HZ的倒数,也就是每发生一次硬件定时器中断的事件间隔。如HZ为200,tick为5毫秒。<br> jiffies核心变数:它是lin
1872 0