Driver:内核的竞态和并发:中断屏蔽、原子操作、自旋锁、信号量

Linux内核的并发与竞态 《《linux设备驱动 - 第3版》.chm》第5章。

    函数的可重入性:
        判断一个函数是否有可重入性,关键在于该函数是否使用了全局变量。
        如果该函数使用了全局变量,那么该函数不具有可重入性。
    
    多进程访问同一个文件:
        文件锁机制实现
    共享内存:可以多进程访问,可以使用信号量集。

    共享资源:文件、共享内存、硬件设备(uart控制器)、多线程间可见的全局变量。

    内核态提供解决竞争的策略:
        1)中断屏蔽
        2)原子操作
        3)自旋锁
        4)信号量​

1、内核的竞态和并发

【竞态】竞争共享资源的状态。
    共享资源硬件(如uart)、文件、共享内存、线程见可见的全局变量
    临界区:访问共享资源的代码

竞态产生的原因
    1)SMP-对称多处理器的多个cpu之间,使用共同的系统总线,会抢占;
    2)抢占式内核中,多进程之间的竞争;
    3)进程和中断之间的抢占;
    4)中断和中断之间的抢占;


【内核汇总提供的解决竞态的策略】
    1)中断屏蔽
    2)原子操作
    3)自旋锁
    4)信号量

对于串口硬件,设计为同时只能有1个进程访问该设备。
    该功能的实现是在'驱动程序'中完成的。

/** 代码演示 - 设备驱动框架 **/
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE ("GPL");

dev_t dev;
struct cdev btn_cdev;
struct class *cls = NULL;

int __init btn_drv_init (void)
{
    /* 1.申请设备号 */
    alloc_chrdev_region (&dev, 0, 1, "mybtns");
    /* 2.初始化cdev */
    cdev_init (&btn_cdev, &btn_fops); // btn_fops ()操作函数
    /* 3.注册cdev */
    cdev_add (&btn_cdev, dev, 1); 
    /* 4.创建设备文件 */
    cls = class_create (THIS_MODULE, "mybtns");
    device_create (cls, NULL, dev, NULL, "btns");
    return 0;
}

void __exit btn_drv_exit (void)
{
    /* 销毁设备文件 */
    device_destroy (cls, dev);
    class_destroy (cls);
    /* 注销cdev */
    cdev_del (btn_cdev);
    /* 释放设备号 */
    unregister_chrdev_region (dev, 1); 
}

module_init (btn_drv_init);
module_exit (btn_drv_exit);

① 中断屏蔽 <linux/irqflags.h>
    local_irq_enable()  // 使能中断
    local_irq_disable () // 屏蔽中断

    在单核cpu最为有效一个解决竞态的策略,特别需要注意的是关中断的实践要特别的短,如果'关中断时间过长,有可能造成内核的崩溃'。
    不建议在驱动代码中使用中断屏蔽策略。

    local_irq_save()    // 屏蔽前保存中断状态,原有的I值,中断屏蔽 I=1
    local_irq_restore () // 恢复原有I值,关闭中断时保存的中断状态并打开中断。

    一般使用方法:
        local_irq_disable(); // 关闭中断
        critical section code; // 执行临界区代码
        local_irq_enable (); // 打开中断

② 原子操作
    该操作绝不会在执行完毕前被任何其他任务或事件打断。
    原子操作依赖底层CPU的原子操作来实现,所有这些函数都与CPU架构密切相关。
    主要分为两类:

②.1 位原子操作
    arch/arm/include/asm/bitops.h

#define set_bit(nr,p)	ATOMIC_BITOP(set_bit,nr,p)
// 【设置位】将addr地址内数据的第nr位设置为1
#define clear_bit(nr,p)	ATOMIC_BITOP(clear_bit,nr,p)
// 【清除位】将addr地址内数据的第nr位设置为0
#define change_bit(nr,p)	ATOMIC_BITOP(change_bit,nr,p)
// 【改变位】将addr地址内数据的第nr位反置
#define test_bit(nr,p)	ATOMIC_BITOP(nr, void* addr)
// 【测试位】获取addr地址内数据的第nr位的值
/* 测试并操作 */
#define test_and_set_bit(nr,p)	ATOMIC_BITOP(test_and_set_bit,nr,p)
#define test_and_clear_bit(nr,p)	ATOMIC_BITOP(test_and_clear_bit,nr,p)
#define test_and_change_bit(nr,p)	ATOMIC_BITOP(test_and_change_bit,nr,p)

②.2 整型原子操作
    arch/arm/include/asm/atomic.h
    typedef struct {
        int counter;
    } atomic_t;
    使用步骤:
        1) 定义变量
            atomic_t btn_at;
        2)初始化
            atomic_set (&btn_at, i);
        3)使用
            int atomic_read(atomic_t *v); // 获取原子变量的值
            void atomic_add (int i, atomic_t *v); // 原子变量的值 +1
            void atomic_sub (int i, atomic_t *v); // 原子变量的值 -1
            atomic_inc (atomic_t *v); // 原子变量的值增加1
            atomic_dec (atomic_t *v); // 原子变量的值减少1
            /* 测试并操作 */
            int atomic_inc_and_test(atomic_t *v);
            int atomic_dec_and_test (atomic_t *v);
            int atomic_sub_and_test (int i, atomic_t *v);
            返组操作对原子变量执行自增、自减或减操作(没有加操作)后测试其是否为0,如果为0返回true,否则返回false

    如果就是一个整型变量,它的执行不希望被打断,可以考虑使用原子操作的方式来代替。

/** 代码演示 **/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/atomic.h>

MODULE_LICENSE ("GPL");

dev_t dev;
struct cdev btn_cdev;
struct class *cls = NULL;

/* 定义原子变量 */
atomic_t btn_at;

int btn_open (struct inode* inode, struct file* filp)
{
    // if (--cnt != 0) {
    if (! atomic_dec_and_test (&btn_at)) { // 为0-true说明有人占用
        atomic_inc (&btn_at);
        return -EBUSY;
    }   
    return 0;
}

int btn_release (struct inode* inode, struct file* filp)
{
    atomic_inc (&btn_at);
    return 0;
}

struct file_operations btn_fops = 
{
    .owner   = THIS_MODULE,
    .open    = btn_open,
    .release = btn_release,
};

int __init btn_drv_init (void)
{
    /* 1.申请设备号 */
    alloc_chrdev_region (&dev, 0, 1, "mybtns");
    /* 2.初始化cdev */
    cdev_init (&btn_cdev, &btn_fops); // btn_fops ()操作函数
    /* 3.注册cdev */
    cdev_add (&btn_cdev, dev, 1); 
    /* 4.创建设备文件 */
    cls = class_create (THIS_MODULE, "mybtns");
    device_create (cls, NULL, dev, NULL, "btns");

    /* 给原子变量赋初值 */
    atomic_set (&btn_at, 1); 

    return 0;
}

void __exit btn_drv_exit (void)
{
    /* 销毁设备文件 */
    device_destroy (cls, dev);
    class_destroy (cls);
    /* 注销cdev */
    cdev_del (&btn_cdev);
    /* 释放设备号 */
    unregister_chrdev_region (dev, 1); 
}

module_init (btn_drv_init);
module_exit (btn_drv_exit);

/** test.c 同上 **/
#include <stdio.h>
#include <fcntl.h>
int main (void)
{
    int fd = 0;
    fd = open ("/dev/btns", O_RDWR);
    if (fd < 0) {
        perror ("open /dev/btns failed");
        return -1; 
    }   
    printf ("open /dev/btns success...\n");
    sleep (20);
    close (fd);
    return 0;
}

③ 自旋锁

    ---> 获取锁
    ---> 执行临界区代码,访问共享资源
    ---> 释放锁

    自旋锁最多只能被一个执行单元持有;
    如果一个执行单元,试图获取已经被其他单元持有的自旋锁,那该执行单元会一直自旋等待,直到获取锁成功为止。

    核心数据结构:
        typedef struct spinlock{
            /* 结构体内容不需要关注 */
        } spinlock_t;
    使用步骤:
        1) 定义一把自旋锁
            spinlock_t btn_lock;
        2) 初始化自旋锁
            spin_lock_init (&btn_lock); // #define
        3) 获取锁
            spin_lock (&btn_lock);
            // 不成功,原地等待自旋,直到锁被释放
            void spin_lock(spinlock_t *lock); // 原型
            spin_trylock (&btn_lock);
            // 不成功,直接返回一个锁
        4) 临界区代码
        5) 释放锁

            spin_unlock (&btn_lock);
    注意事项:
        1) 自旋锁使用的时候获取锁和释放锁必须成对出现
        2) 持有自旋锁时间过长,产生抢锁的几率就大大提高,在持有自旋锁期间内核几乎不做任务调度;系统性能严重下降
        总结:'自旋锁保护的临界区执行速度越快越好'
        3) 在临界区代码中不能调用引起阻塞或者睡眠的函数
        4) 注意避免死锁
           建议可以将spin_lock替换为spin_trylock。以进程一为例,如果在获取B不成功时,同时将A锁释放,过一段时间重新获取A锁...B锁。

            【死锁举例】
            进程一              进程二
            spin_lock (A)       spin_lock (B)
            ...                 ...
                spin_lock (B)       spin_lock (A)  // <--- 此行容易造成死锁
                ...                 ...
                spin_unlock (B)     spin_unlock (A)
            ...                 ...
            spin_unlock (A)     spin_unlock (B)

        自旋锁还有很多衍生自旋锁:读锁、写锁、顺序锁、内核的大锁。

/** 代码演示 - btn_drv.c **/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

MODULE_LICENSE ("GPL");

dev_t dev;
struct cdev btn_cdev;
struct class *cls = NULL;

int cnt = 1; // 共享资源

/* 定义自旋锁 */
spinlock_t btn_lock;

int btn_open (struct inode* inode, struct file* filp)
{
    /* 获取自旋锁 */
    spin_lock (&btn_lock);

    if (--cnt != 0) {
        cnt++;
        /* 释放自旋锁 */
        spin_unlock (&btn_lock);
        return -EBUSY;
    }

    /* 释放自旋锁 */
    spin_unlock (&btn_lock);

    return 0;
}

int btn_release (struct inode* inode, struct file* filp)
{
    /* 获取自旋锁 */
    spin_lock (&btn_lock);
    cnt++;
    /* 释放自旋锁 */
    spin_unlock (&btn_lock);
    return 0;
}

struct file_operations btn_fops =
{
    .owner   = THIS_MODULE,
    .open    = btn_open,
    .release = btn_release,
};

int __init btn_drv_init (void)
{
    /* 1.申请设备号 */
    alloc_chrdev_region (&dev, 0, 1, "mybtns");
    /* 2.初始化cdev */
    cdev_init (&btn_cdev, &btn_fops); // btn_fops ()操作函数
    /* 3.注册cdev */
    cdev_add (&btn_cdev, dev, 1);
    /* 4.创建设备文件 */
    cls = class_create (THIS_MODULE, "mybtns");
    device_create (cls, NULL, dev, NULL, "btns");

    /* 初始化自旋锁 */
    spin_lock_init (&btn_lock);

    return 0;
}

void __exit btn_drv_exit (void)
{
    /* 销毁设备文件 */
    device_destroy (cls, dev);
    class_destroy (cls);
    /* 注销cdev */
    cdev_del (&btn_cdev);
    /* 释放设备号 */
    unregister_chrdev_region (dev, 1);
}

module_init (btn_drv_init);
module_exit (btn_drv_exit);

/** test.c 同上 **/

④ 信号量


    如果临界区必须调用可能导致睡眠或者阻塞的函数,'信号量是保护临近区的常用方法'。
    内核信号量实际上是一种'睡眠锁'。
    1) 可以调用引起阻塞或睡眠的函数;
    2) 对临界区代码执行速度的快慢无要求;

       // 本质上是基于自旋锁实现的
    3) 可以有多个持有单元,获取信号量不成功则睡眠等待;
       // 和unix C信号量集作用是相似的,unix C的信号量集只能在用户空间编程使用。
    
    核心数据结构:
        struct semaphore;  // 内核不允许访问该结构体的任何成员。
    使用步骤:
        1) 定义一个信号量;
            struct semaphore btn_sem;
        2) 初始化信号量;
            void sema_init(struct semaphore *sem, int val);
            /* 定义并初始化信号量为1 */
            DEFINE_SEMAPHORE (btn_sem);
        3) 获取信号量
            本质就是信号量-1, 减到0再减不成功(获取信号量失败!)
            void down (struct semaphore* sem);
            // 成功 - 立即返回,失败 - 使调用者进程进入睡眠状态,直到获取信号量成功才返回。【该函数可能导致进程睡眠,因此不能再中断上下文中使用】
            int __must_check down_interruptible(struct semaphore *sem);
            // 成功 - 立即返回,失败 - 使调用者进程进入可中断的睡眠状态。该函数由返回值来区分正常返回还是被信号中断返回;如果返回0,代表获取信号量正常返回;如果返回非0,代表被信号打断【使用时必加判断 → 实现并发访问】
            int __must_check down_trylock(struct semaphore *sem);
            // 成功 - 立即返回,失败 - 立即返回1个出错信息【可在中断上下文使用】
            int __must_check down_timeout(struct semaphore *sem, long jiffies);
            // 成功 - 立即返回,失败 - 睡眠等待指定时间,如果还不成功返回出错信息
        4) 执行临界区代码,访问共享资源
        5) 释放信号量

            本质就是信号量计数+1
            void up(struct semaphore *sem);

/** 代码演示 - btn_drv.c **/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/delay.h>

MODULE_LICENSE ("GPL");

dev_t dev;
struct cdev btn_cdev;
struct class *cls = NULL;

int cnt = 1; // 共享资源

/* 定义信号量 */
struct semaphore btn_sem;

int btn_open (struct inode* inode, struct file* filp)
{
    /* 获取信号量 */
    down (&btn_sem);
    if (--cnt != 0) {
        cnt++;
        /* 释放信号量 */
        up (&btn_sem);
        return -EBUSY;
    }   
    /* 释放信号量 */
    up (&btn_sem);
    return 0;
}

int btn_release (struct inode* inode, struct file* filp)
{
    /* 获取信号量 */
    down (&btn_sem);
    cnt++;
    /* 释放信号量 */
    up (&btn_sem);
    return 0;
}

struct file_operations btn_fops = 
{
    .owner   = THIS_MODULE,
    .open    = btn_open,
    .release = btn_release,
};

int __init btn_drv_init (void)
{
    /* 1.申请设备号 */
    alloc_chrdev_region (&dev, 0, 1, "mybtns");
    /* 2.初始化cdev */
    cdev_init (&btn_cdev, &btn_fops); // btn_fops ()操作函数
    /* 3.注册cdev */
    cdev_add (&btn_cdev, dev, 1); 
    /* 4.创建设备文件 */
    cls = class_create (THIS_MODULE, "mybtns");
    device_create (cls, NULL, dev, NULL, "btns");

    /* 初始化信号量 */
    sema_init (&btn_sem, 1); 

    return 0;
}

void __exit btn_drv_exit (void)
{
    /* 销毁设备文件 */
    device_destroy (cls, dev);
    class_destroy (cls);
    /* 注销cdev */
    cdev_del (&btn_cdev);
    /* 释放设备号 */
    unregister_chrdev_region (dev, 1); 
}

module_init (btn_drv_init);
module_exit (btn_drv_exit);

/** test.c 同上 **/


【一种特殊需求】
./test 打开该设备
./test 第二个进程也打开该设备,进入睡眠等待,等到第一个进程使用设备结束
/** btn_drv.c 修改代码 **/
/* 定义信号量 */
struct semaphore btn_sem;
int btn_open (struct inode* inode, struct file* filp)
{
    /* 获取信号量 */
    if (down_interrupter (&btn_sem)){
        return -EAGAIN;
    }
    return 0;
}

int btn_release (struct inode* inode, struct file* filp)
{
    /* 释放信号量 */
    up (&btn_sem);
    return 0;
}

int __init btn_drv_init (void)
{
    ...
    /* 初始化信号量 - 只允许1个持有者 */
    sema_init (&btn_sem, 1);
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姜源Jerry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值