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


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

自旋锁特点:

自旋锁:

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

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

  • 执行结果 在这里插入图片描述

示例二:同时间只能一个进程操作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

  • 执行结果
    在这里插入图片描述

进阶(衍生自旋锁)

衍生自旋锁的特点

衍生自旋锁其实就是结合目前介绍的两种解决竞态问题的方法同时使用,即衍生自旋锁 = 屏蔽中断 + 自旋锁。
所以衍生自旋锁能够解决所有的竞态引起的异常问题。
衍生自旋锁保护的临界区的代码执行速度也必须要快且不能有休眠操作,因为在这段被保护的代码不仅屏蔽了中断而且还上了锁。当然同自旋锁一样,没有获取到自旋锁的任务将会原地忙等待(原地空转)。

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(同上,不详写了)

  • 执行结果:
    在这里插入图片描述

总结

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

  • 当然,后面还会提到剩下的两种方法——信号量和原子操作。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值