Linux设备驱动第七天(原子性:中断屏蔽、自旋锁、信号量)

回顾:

  1. linux内核底半部的工作队列
    明确:linux内核底半部的机制有哪些?
    tasklet:tasklet对应的延后执行函数工作中断上下文中,并且tasklet本身也是基于软中断实现,所以不能进行休眠操作
    工作队列:工作队列对应的延后执行函数工作在进程上下文中,所以可以进行休眠操作
    软中断:对应的延后执行函数以不能以模块的形式实现,必须静态编译到内核中;并且要求延后执行函数具备可重入性。
  2. 强调一点是底半部机制并不是发彩要和顶半部中断处理函数配合使用,底半部机制本质就是延后执行的一种手段而已!
  3. linux内核一此关于时间的概念
    系统定时器硬件:能够通过编程来设置它的工作输出频率,并且能够周期性,稳定的给CPU产生一个时钟中断信号。
    系统定时器硬件对应的时钟中断处理函数:
    更新jiffies
    更新实际时间
    检查时间片
    检查是否有超时的内核定时器

    HZ:常数,对于RMA来说,HZ=100,一秒钟100次时钟中断
    tick:
    jiffies:内核全局变量,没发生一次时钟中断,自动加1

    linux内核的软件定时器
    数据结构:struct timer_list
    成员:
    expires:
    function:
    data:
    操作方法:
    init_timer
    add_timer
    del_timer
    mod_timer
    切记:定时器本身基于软中断实现,要求对应的超时处理函数不能做休眠!

linux内核的延时方法:
忙延时:
ndelay
udelay
mdelay
“bogomips”:CPU一秒钟做多少百万条指令
休眠延时:
msleep()
ssleep()
schedule
shcedule_timeout

linux内核的并发和竞态:
案例:要求一个硬件设备只能被一个应用软件所打开访问操作!

概念:
并发:多个执行单元同时发生
“执行单元”:硬件中断、软中断、进程
竞态:多个执行单元同时访问共享资源产生竞态
产生竞态的条件:
1,必须有多个执行单元
2,必须有共享资源
3,必须同时访问
共享资源:硬件资源(驱动程序中但凡设计的寄存器都是共享资源)和软件上的全局变量
互斥访问:当有多个执行单元要访问共享资源的时候,只允许一个执行单元访问共享资源,其他执行单元禁止访问!
临界区:访问共享资源的代码区域

if(--open_cnt !=0){
   ...
   ++open_cnt;
   ...
}

互斥访问的本质目的:就是让临界区的代码执行路径具有原子性!

2,一定要明确在哪些场合会形成竞态?
多核:因为多核会共享内存,外存,IO,如果同时访问,势必形成竞态;
进程与进程的抢占;
中断和进程;
中断和中断;

3,linux内核解决竞态的方法
中断屏蔽:
能够解决哪些场合的竞态问题?
答:进程和进程的抢占(依赖中断来实现)
中断和进程
中断和中断
注意:中断屏蔽的是当前CPU的中断信号
中断屏蔽的相关使用方法:
1,在访问临界区之前屏蔽中断
unsigned long flags;
local_irq_disable();//仅仅屏蔽中断
或者
local_irq_save(flags);//屏蔽中断,保存中断状态到flags中断
2,执行临界区的代码
切记:临界区的代码要求执行速度要快,千万不能做休眠操作
3,在访问临界区之后恢复中断
local_irq_enable();//仅仅恢复中断
或者
local_irq_restore(flags);//恢复中断,从flags中恢复保存的中断状态

参考代码:
static int open_cnt = 1;
int led_open(struct inode *inode,struct file *file){
    unsigned long flags;
    //屏蔽中断
    local_irq_save(flags);
    if(--open_cnt!=0){
       ++open_cnt;
       //恢复中断
       local_irq_restore(flags);
       return -EBUSY;
    }
    //恢复中断
    local_irq_restore(flags);
    return 0;
}

注意:屏蔽中断与恢复中断一定要成对出现
切记:中断屏蔽保护临界区要求代码的执行速度要快,不能做休眠操作。

linux内核原子操作:
问:原子操作能够解决哪些场合的竞态问题?
答:原子操作能够解决所有的竞态问题!

原子操作分为两类操作:
位原子操作:位操作+操作过程具有原子性
使用场合:如果在设备驱动开发时,代码中有共享资源,并且对共享资源进行位操作,并且这个过程有竞态问题,此时可以选择位原子操作来解决竞态问题!

例如:

static int open_cnt = 1;//共享资源
int led_open(struct inode *inode,struct file *file){
    open_cnt &= ~0x1;//把bit1清0,这行代码不具体原子性
    return 0;
}

解决方法:
方法1:采用中断屏蔽

static int open_cnt = 1;//共享资源
int led_open(struct inode *inode,struct file *file){
    unsigned long flags;
    local_irq_save(flags);
    open_cnt &= ~0x1;//代码具体原子性
    local_irq_restore(flags);
    return 0;
}//这种方法无法解决多核引起的竞态

方法二:采用位原子操作:
linux内核提供了位原子操作的相关方法:
set_bit/clear_bit/change_bit/test_bit组合函数

位原子操作:

static int open_cnt = 1;//共享资源
int led_open(struct inode *inode,struct file *file){
    clear_bit(0,&open_cnt);//第一个bit位清0
    return 0;
}

整形原子操作:整形操作+操作过程具有原子性
使用场合:如果在设备驱动开发时,代码中有共享资源,并且对共享资源进行整形操作,并且这个过程有竞态问题,此时可以选择整形原子操作来解决竞态问题!
linux内核对于整形原子操作提供的相关方法:
整形原子变量的数据类型:atomic_t
1,分配整型原子变量
atomic_t v;
2,内核对整型原子变量提供的方法
atomic_set/atmoic_read/atmoic_add/atmoic_sub/atomic_inc/atmoic_dec组合函数
参考代码:

static int open_cnt = 1;//共享资源
int led_open(struct inode *inode,struct file *file){
    unsigned long flags;
    if(--open_cnt!=0){//临界区
       ++open_cnt;
       return -EBUSY;
    }
    return 0;
}

分析:
–open_cnt:这个整形操作不具备原子性
解决方法:
方法1:采用中断屏蔽,问题是不能解决多核问题!
方法2:采用整形原子操作,代码修改如下:

static atomic_t open_cnt = ATOMIC_INIT(1);//atomic_t,声明一个整形原子变量
int led_open(struct inode *inode,struct file *file){
    unsigned long flags;
    if(!atmoic_dec_and_test(&open_cnt)){//具有原子性
       ++open_cnt;
       return -EBUSY;
    }
    return 0;
}

注意:原子操作涉及的相关操作方法,如果对于多核情况,这些函数方法会使用ARM两条原子指令:ldrex,strex
如果一个CPU在处理ldrex,strex的代码时,其他CPU原地空转!等待前一个CPU执行完毕!

案例:要求LED设备只能被一个应用软件打开访问操作!(中断屏蔽(解决不了多核的竞态)、原子操作)
open->led_open->struct file_operation->struct cdev 或者 struct miscdevice

led_dev.c

#include <linux/init.h>
#include <linux/moudule.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

//共享资源
static int open_cnt = 1;

static int led_open(struct innode *node,struct file *file){
    unsigned long flags;
    //屏蔽中断
    local_irq_save(flags);
    if(--open_cnt != 0){
        printk("设备已经被打开了。。。\n");
        open_cnt ++;
         //恢复中断
        local_irq_restore(flags);
        return -EBUSY;
    }

    //恢复中断
    local_irq_restore(flags);
    printk("设备打开成功。。。");
    return 0;
}

static int led_close(struct innode *node,struct file *file){
    unsigned long flags;
    local_irq_save(flags);
    open_cnt ++;
    local_irq_restore(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_DYNAMDIC_MINOR,//动态分配次设备号
     .name = "myled",//dev/myled
     .fops = &led_fops
}

static int  led_init(void){
     //注册混杂设备
     misc_register(&led_misc);
     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/types.h>
#include <sys/stat.h>
#include <fcnt1.h>

int main(int argc,char *argv[]){
    int fd;
    fd = open("/dev/myled",o_RDWR);
    if(fd<0){
        printf("打开设备失败\n");
        return -1;
    }

    //打开设备成功后,进入休眠状态
    sleep(1000);
    close(fd);
    return 0;
}

实验的步骤:
inmod led_drv.ko
ls /dev/myled -lh
./led_test & //启动A进程
ps //查看进程的PID
top //查看进程的状态:休眠状态s
./led_test //启动B进程


linux内核互斥机制—–自旋锁

自旋锁的特点:自旋+锁
仅仅只有自旋锁是没有意义的,它必须附加在某个共享资源上。
自旋锁在同一时刻只能被一个执行单元进行持有!
如果一个执行单元获取不了自旋锁,将会原地打转!

linux内核自旋锁的数据类型:spinlock_t

问:自旋锁如何保护临界区呢?
答:
1,分配自旋锁
spinlock_t lock;
2,初始化自旋锁
spinlock_init(&lock);
3,访问临界区之前,获取自旋锁
spin_lock(&lock);//如果获取自旋锁,立即返回,执行临界区的代码;如果获取不了自旋锁,任务将会在此忙等待,直到持有自旋锁的任务释放自旋锁!
或者:
spin_try_lock(&lock);//尝试获取自旋锁,如果获取自旋锁返回true,才有资格访问临界区,否则返回false,就没有资格访问临界区,注意在使用的时候,一定要对其返回值进行判断
4,执行临界区代码
5,释放自旋锁
spin_unlock(&lock);//一旦释放自旋锁,等待自旋锁的任务也将会获取自旋锁
6,切记:
以上自旋锁的操作过程只能解决多核和进程与进程之间的抢占引起的竞态问题,无法解决中断引起的竞态问题!

问:如果利用自旋锁要解决中断引起的竞态问题,那么怎么办?
答:可以利用衍生自旋锁
衍生自旋锁的特点:其实就是在自旋锁的基础上加了中断屏幕的功能而已!

衍生自旋锁的使用
1,分配自旋锁
spinlock_t lock;
2,初始化自旋锁
spinlock_init(&lock);
3,访问临界区之前,获取自旋锁
unsigned long flags;
spin_lock_irq(&lock);//如果获取自旋锁,还会屏蔽中断,立即返回,执行临界区的代码;如果获取不了自旋锁,任务将会在此忙等待,直到持有自旋锁的任务释放自旋锁!
或者:
spin_lock_irqsave(&lock,flags);//尝试获取自旋锁,还会屏蔽中断,如果获取自旋锁返回true,才有资格访问临界区,否则返回false,就没有资格访问临界区,注意在使用的时候,一定要对其返回值进行判断
4,执行临界区代码
5,释放自旋锁
spin_unlock_irq(&lock);//一旦释放自旋锁,恢复中断,等待自旋锁的任务也将会获取自旋锁
或者
spin_unlock_irqrestore(&lock,flags);//释放自旋锁,恢复中断
6,切记:衍生自旋锁能够解决所有的竞态问题
总结:
使用普通的自旋锁,如果临界区的执行时间较长,会降低系统的性能!
使用衍生自旋锁,如果临界区的执行时间较长,不仅仅会降低系统的性能,甚至会造成数据丢失或者系统的崩溃!
所有使用自旋锁,要求临界区的代码执行速度要快,更不能做休眠动作!

案例:利用自旋锁实现一个设备只能被一个应用软件打开访问操作!

#include <linux/init.h>
#include <linux/moudule.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

//共享资源
static int open_cnt = 1;
//分配自旋锁
static spinlock_t lock;

static int led_open(struct innode *node,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;
}

static int led_close(struct innode *node,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_DYNAMDIC_MINOR,//动态分配次设备号
     .name = "myled",//dev/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");

linux内核信号量互斥机制:
进程的状态:
1,进程的准备就绪状态:TASK_READY
2,进程的运行状态:TASK_RUNNING
3,进程的休眠状态:
进程的可中断休眠状态:TASK_INTERRUPTIBLE,如果给进程发送信号,这个进程立即被唤醒并且处理这个信号。
进程的不可中断休眠状态:TASK_UNINTERRUPTIBLE,如果给进程发送信号,这个进程不会立即处理信号,一旦进程被唤醒,进程将会处理这个信号。

信号量特点:
有别名睡眠锁!
持有信号量的任务在执行临界区的代码可以进行休眠操作!获取信号量的任务在没有获取信号量也可以进入休眠状态!
信号量的数据结构:struct semaphore

问:如何使用信号量保护临界区呢?
1,分配信号量
struct semaphore sema;
2,初始化信号量为互斥信号量
sema_init(&sema,1);
3,访问临界区之前获取信号量
down(&sema);//如果获取信号量,立即返回;如果没有获取信号量,进程将进入不可中断的休眠状态!
或者
down_interruptible(&sema);//获取信号量,立即返回0,如果没有获取信号量,进程将进入可中断的休眠状态!一旦休眠的进程被唤醒(信号量可用),休眠的进程立即获取信号量,此函数返回0,如果返回非0,表明这个进程接收到信号引起的唤醒!
或者
down_trylock(&sema);//获取信号量返回0,否则返回非0,不会引起进程的休眠
4,执行临界区的代码
5,释放信号量,并且唤醒休眠的进程
up(&sema);

案例:利用信号量,实现一个设备只能被一个应用软件访问操作!

#include <linux/init.h>
#include <linux/moudule.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

//分配信号量
static struct semphore sema;

static int led_open(struct innode *node,struct file *file){
    if(down_trylock(&sema)){
        printk("设备打开失败 \n");
        return -EBUSY; 
    }
    printk("设备打开成功 \n");
    return 0;
}

static int led_close(struct innode *node,struct file *file){
    //释放信号量
    up(&sema);//释放信号量并且唤醒休眠的进程
    return 0;
}

//分配硬件操作接口
static struct file_operations led_fops ={
     .owner = THIS_MODULE,
     .open = led_open,
     .release = led_close
};

//分配混杂设备对象
static struct miscdevice led_misc = {
     .minor = MISC_DYNAMDIC_MINOR,//动态分配次设备号
     .name = "myled",//dev/myled
     .fops = &led_fops
}

static int  led_init(void){
     //注册混杂设备
     misc_register(&led_misc);
     //初始化信号量为互斥信号量
     sema_init(&sema,1);
     return 0;
}

static void led_exit(void){
      misc_deregister(&led_misc);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值