驱动学习笔记7 内核软件定时器,内核延时,等待队列机制

1.linux内核软件定时器 


1.1.了解硬件定时器特性 


    硬件定时器本质就是一个硬件电路(可以是处理器内部集成的定时器控制器或者外置的硬件电路)
    一旦启动硬件定时器,硬件定时器按照一定的频率周期性的给CPU核发送中断信号
    此中断又称定时器中断
    发送中断信号的频率(周期)在软件上是可以配置的 
    STM32定时器中断触发的周期公式:(PSC + 1) * (ARR + 1) / 时钟频率
    


1.2.硬件定时器对应的中断处理函数所作的工作(了解)


    此函数已经由linux内核默认完成,此函数完成的工作如下:
    1.不断的更新系统的运行时间,jiffies_64加1
    2.不断更新系统的实际时间(wall-time,时分秒)
    3.检查当前进程的时间片是否用完,如果用完,让进程调度器重新分配CPU资源给其他进程
    4.检查当前内核中是否有超时的软件定时器,如果有超时的软件定时器,内核调用软件定时器的
    处理函数  
    5.统计系统资源,例如:执行top命令,可以看到CPU的利用率,内存使用信息等


    
1.3.掌握linux内核中跟时间相关的三个概念:


a)HZ:它是linux内核的全局常量


    ARM架构:HZ=100
    X86架构:HZ=1000
    以ARM架构为例,HZ=100,表示硬件定会器一秒钟给CPU核发送100次定时器中断信号
    每发生一次中断的时间间隔为10ms
    例如:5*HZ=5*100=500(次硬件定时器中断)=5秒钟
              HZ/2=500ms    


b)jiffies_64:它是linux内核的全局变量,它的数据类型是unsigned long long(64位)


    它记录系统自开机以来,硬件定时器给CPU发送的定时器中断次数,硬件定时器每发生一次中断
    jiffies_64加1  
    例如:对于ARM架构,每个10ms,jiffies_64加1  


c)jiffies:它也是linux内核全局变量,它的数据类型是unsigned long(32位)


    它的值取jiffies_64的低32位,也就是每发生一次定时器中断,jiffies也会加1
    一般它用于计算时间间隔!
    切记:将来只要在程序中看到jiffies,就是表示当前时刻的时间!
    例如:
    unsigned long timeout  = jiffies + 2*HZ;
    说明:
    jiffies:就是表示当前时刻的时间
    2*HZ:2秒
    timeout:    表示2秒以后那个时刻的时间
    
    案例:分析以下代码存在的漏洞
    unsigned long timeout = jiffies + 5*HZ;
    //后面有一堆的代码,CPU执行这些代码需要消耗时间
    ...
    ...
    //执行完以上代码之后,CPU核判断是否发生了超时现象
    if(jiffies > timeout) 
        超时;
    else
        没有超时; 
    问:如何解决此问题呢?
    答:利用内核大神提供的宏函数来解决回滚溢出的问题
           time_after或者time_before
    解决以后的代码:
    unsigned long timeout = jiffies + 5*HZ;
    //后面有一堆的代码,CPU执行这些代码需要消耗时间
    ...
    ...
    //执行完以上代码之后,CPU核判断是否发生了超时现象
    if(time_after(jiffies, timeout)) 
        超时;
    else
        没有超时;
    作业:研究内核大神的宏函数:time_after或者time_before的C语言实现

d)linux内核软件定时器  


    特点:
    1.内核软件定时器可以指定一个超时时间,一旦超时时间到期,内核自动调用其超时处理函数
    并且内核自动将超时的定时器删除,所以内核的软件定时器的超时处理函数只执行一次
    2.基于软中断实现,所以其超时处理函数不能进行休眠操作!
    
    内核描述软件定时器属性的结构体:
    struct timer_list {
        unsigned long expires;
        void (*function)(unsigned long data);
        unsigned long data;
        ...
    };    
    expires:指定软件定时器超时时刻的时间
        例如:expires = jiffies + 5*HZ;
    function:指定超时处理函数,基于软中断实现,不能休眠
          形参data:保存给超时处理函数传递的参数
    data:给超时处理函数传递的参数
配套函数:
    //初始化软件定时器对象
    init_timer(&定时器对象);
    //但是还需要额外自己初始化:expires,function,data
    定时器对象.expires = ....; //指定超时时刻的时间
    定时器对象.function = ...; //指定超时处理函数
    定时器对象.data = ...; //指定给超时处理函数传递的参数,不传参不用初始化
    
    //向内核注册添加定时器对象,一旦注册成功就开始倒计时
    //一旦倒计时为0,内核调用其超时处理函数并且删除定时器对象
    add_timer(&定时器对象);

    //从内核中删除定时器对象    
    del_timer(&定时器对象);

    //修改定时器的超时时间
    mod_timer(&定时器对象, 新的超时时间);
    注意:
    此函数等价于调用三步骤:
    1.先删除之前的定时器:del_timer
    2.重新修改定时器的超时时间:expires = jiffies + xxxx;
    3.重新向内核添加定时器:add_timer
案例1:利用内核软件定时器,不要利用字符设备或者混杂设备驱动编程框架
        实现内核程序每隔2秒打印一句话:参考代码:day07/1.0

只打印一次

疯狂打印,不是2s,(内核将定时器超时时间减为0)

重新修改定时器超时时间

但比较危险,若被别的中断修改了定时器超时时间就出错了


案例2:利用内核软件定时器,不要利用字符设备或者混杂设备驱动编程框架
        实现内核程序每隔2秒开关灯


案例3:利用内核软件定时器,不要利用字符设备或者混杂设备驱动编程框架
    可以实现动态调整灯的闪烁频率:1s,2s,3s,4s等
    提示:module_param(time, int, 0664);
    echo 1 > /sys/..../time
    echo 2 > /sys/.../time
    ...
    mod_timer(&led_timer, jiffies + time*HZ);
下位机测试:
cd /home/drivers
insmod led_drv.ko  time=1
cat /sys/module/led_drv/parameters/time
echo 4 > /sys/module/led_drv/parameters/time //修改时间间隔为4秒

2.linux内核延时的方法


2.1.延时定义:又称等待,等待某个事件满足要求,不满足则让程序停一停,等一等
          如果满足要求则继续执行


2.2.等待分两种:忙等待和休眠等待


2.3.忙等待(忙延时)特点和相关函数:
    特点:
    1.CPU原地空转,死等某个事件满足要求
    2.忙等待用于等待时间极短的场合:ns,us,ms(10ms以内)
    3.中断和进程都可以使用(硬件中断处理函数,tasklet,工作队列,普通的进程)
    
    涉及的函数:
    void ndelay(int ns) //纳秒级忙等待
    例如:ndelay(10) //cpu原地空转10纳秒
    void udelay(int us) //微秒级忙等待
    例如:udelay(10) //cpu原地空转10微秒
    void mdelay(int ms) //毫秒级忙等待
    例如:mdelay(5) //cpu原地空转5毫秒


2.4.休眠等待(休眠延时)特点和相关函数
    特点:
    1.休眠等待只能用于进程,不能用于中断,进程休眠是指进程会释放掉占用的CPU资源给其他进程
    2.应用于等待时间较长或者随机场合
    
    涉及的函数:
    回顾应用程序休眠的函数:sleep(10)

    void msleep(int ms) //毫秒级休眠等待

    
    例如:msleep(500) //进程休眠等待500毫秒
    说明:应用程序也就是进程利用系统调用函数可以陷入内核空间,然后进程由用户空间切换到
    内核空间继续运行,一旦进程在内核空间调用此函数立马释放占用的CPU资源给其他进程
    而这个进程进入休眠状态,等待被唤醒,唤醒的方法有两种:
    1.进程休眠的时间到期(500毫秒到期),内核主动来唤醒休眠的进程,进程继续运行
    2.进程在休眠期间接收到了kill信号,进程会被立即唤醒,但是结果很悲催:进程被唤醒之后立刻死去

    void ssleep(int s) //秒级休眠等待    


    例如:ssleep(500) //进程休眠等待500秒
    说明:应用程序也就是进程利用系统调用函数可以陷入内核空间,然后进程由用户空间切换到
    内核空间继续运行,一旦进程在内核空间调用此函数立马释放占用的CPU资源给其他进程
    而这个进程进入休眠状态,等待被唤醒,唤醒的方法有两种:
    1.进程休眠的时间到期(500秒到期),内核主动来唤醒休眠的进程,进程继续运行
    2.进程在休眠期间接收到了kill信号,进程会被立即唤醒,但是结果很悲催:进程被唤醒之后立刻死去

    schedule() //永久性休眠


    应用程序也就是进程利用系统调用函数可以陷入内核空间,然后进程由用户空间切换到
    内核空间继续运行,一旦进程在内核空间调用此函数立马释放占用的CPU资源给其他进程
    而这个进程进入休眠状态,等待被唤醒,唤醒的方法就一种:
    1.进程在休眠期间接收到了kill信号,进程会被立即唤醒,但是结果很悲催:进程被唤醒之后立刻死去

    schedule_timeout(5*HZ); //休眠等待5秒钟,时间单位是硬件定时器中断触发的次数
    schedule_timeout(5); //休眠等待50毫秒钟,时间单位是硬件定时器中断触发的次数


    说明:应用程序也就是进程利用系统调用函数可以陷入内核空间,然后进程由用户空间切换到
    内核空间继续运行,一旦进程在内核空间调用此函数立马释放占用的CPU资源给其他进程
    而这个进程进入休眠状态,等待被唤醒,唤醒的方法有两种:
    1.进程休眠的时间到期(5秒或者50毫秒到期),内核主动来唤醒休眠的进程,进程继续运行
    2.进程在休眠期间接收到了kill信号,进程会被立即唤醒,但是结果很悲催:进程被唤醒之后立刻死去

    总结:以上休眠函数有个致命的缺陷:
    进程调用这些函数可以做到随时随地休眠,但是做不到随时随地被唤醒还能正常运行(而不是kill去死)
    因为有些场合,等待某个事件满足要求,这个事件满足要求它可能是随机的,可能随时就能够满足
    你的要求,那么此时就需要立刻唤醒休眠的进程并且让他正常运行处理到来的事件!
    问:如何解决这种致命的缺陷呢?如何做到让进程随时随地休眠并且随时随地被唤醒还能正常运行呢?
    答:利用大名鼎鼎的等待队列机制!
    
--------------------------------------------------------------------------------


3.linux内核等待队列机制  


3.1.谈谈linux内核等待队列在什么时候,什么场合会使用呢?


      举例子阐述说明:以CPU读取UART接收缓冲区数据为例:
      CPU读取数据的流程如下:
      1.首先启动一个应用程序(进程),进程调用read或者ioctl来从UART接收缓冲区读取数据(read(fd, buf, 1024))
      2.然后进程由于调用了read或者ioctl,进程立刻陷入内核空间调用底层驱动的read或者ioctl接口(uart_read)
      3.底层驱动的read或者ioctl立刻去UART接收缓冲区获取数据,但是由于UART接收移位器接收数据的速度很慢
         此时数据还没有准备继续,然后首先想到轮询方式,但是此方法会让CPU做大量无用功,降低了CPU的利用率
         所以想到轮询的死对头中断方式
         问:此时进程在底层驱动的read或者ioctl接口函数中干嘛呢?
         答:有两种选择


     1.不等待:


    如果发现数据没有准备就绪,不进行等待延时操作,进程立刻返回,回到用户空间
    这种操作方式就是系统编程课程所讲的非阻塞(open("a.txt", O_RDWR|O_NONBLOCK))
    底层驱动代码:
    xxx_read(....) {
        if(如果数据没有准备就绪并且采用非阻塞方式读取数)
            return -EAGAIN; //直接返回到应用程序
        例如: 
        if(file->f_flags & O_NONBLOCK && uart接收缓冲区没有数据)
            return -EAGAIN;
    }
    如果返回到应用程序,如果应用还想读取数据,应用程序只需重复调用read或者ioctl来读取数据即可
    依次重复下去    


    2.等待(阻塞):


    如果发现数据没有准备就绪,那么可以让进程在底层驱动的read或者ioctl接口函数中进行等待操作
    此等待必然用休眠等待,如果让进程休眠等待,又不能调用msleep/ssleep/schedule/schedule_timeout
    这些函数,因为这些函数可以让进程进行休眠等待,但是将来一旦数据准备继续了不能让进程随时随地唤醒
    并且正常运行(时间没有到期来数据了,怎么办?总不能kill杀死吧?)
    对于此种情况只能采用等待队列机制让进程进行休眠,并且将来数据一旦准备就绪可以随时唤醒休眠的
    进程并且让进程正常运行读取数据即可
    问:什么时候才能随时随地唤醒休眠的进程呢?
    答:如果UART接收缓冲区数据准备就绪,UART控制器势必给CPU发送一个中断信号
    内核势必调用其中断处理函数,那咱们只需在中断处理函数中唤醒休眠的进程即可
    中断到来也是表示数准备就绪了,那么就可以唤醒休眠的进程了
    一旦进程被唤醒,进程就可以读取接收到的数据并且将数据拷贝到用户缓冲区然后返回即可
    至此应用程序的read或者ioctl函数完成数据的一次读取操作
    结论:等待队列实现进程在内核空间休眠并且随时被唤醒这个操作就是传说中的阻塞方式
    应用程序open时,默认采用的就是阻塞方式!

3.2.结论:


    1.有中断的地方必然有等待队列  
       如果事件不满足,利用等待队列让进程休眠,如果事件一旦满足,产生中断,利用中断来唤醒休眠的进程
    2.有等待队列的地方,不一定有中断
    3.等待队列可以让进程随时随地休眠并且随时随地唤醒休眠的进程!

3.3.linux内核利用等待队列实现进程休眠和唤醒的编程步骤(方法1):


a)类比:
    鸡妈妈<-------->等待队列头
    小鸡<---------->要休眠的进程,一个小鸡对应一个进程
    老鹰<---------->进程调度器,负责进程CPU资源的获取,释放,进程之间的调度,进程之间的抢占等
    结论:老鹰进程调度器已经由内核帮咱们完成,咱们只需搞定鸡妈妈等待队列头和小鸡休眠的进程即可!


1)定义初始化等待队列头对象(构造武装一个鸡妈妈)
    wait_queue_head_t  wq; //定义等待队列对象
    init_waitqueue_head(&wq); //初始化等待队列头对象


2)定义初始化装载要休眠进程的容器(盒子,构造小鸡)
    wait_queue_t  wait; //定义容器
    init_waitqueue_entry(&wait,  current);//把当前进程添加到wait容器中,构造一个小鸡
    说明:
    "当前进程":正在获取CPU资源并且运行中的进程
    current:它是linux内核的全局指针变量:struct task_struct  *current;
        对应的结构体类型:
        struct task_struct {
            volatile long state;    //记录进程的状态
            pid_t pid;//记录进程的PID号
            char comm[TASK_COMM_LEN];//进程的名字
            ...
        };
        功能:此结构体用来描述linux系统进程的各种属性信息
        每当创建一个进程时(./helloworld或者fork或者pthread_create等),linux内核就会自动
        用这个结构体创建一个对象并且初始化对象来描述你新创建的进程的各种属性信息
        结论:current指针就是指向当前进程对应的task_struct结构体对象,将来底层驱动利用
        current就能够获取到当前进程的各种属性了:
        printf("进程{%s}{%d}\n", current->comm, current->pid);
    注意:由于一个小鸡对应一个进程,所以一个wait容器也对应一个进程
    所以wait对象的定义初始化代码一定是局部变量,不能是全局变量    
    例如:底层驱动参考代码
    xxx_read(...) {
        wait_queue_t  wait; //定义容器
        init_waitqueue_entry(&wait,  current);//把当前进程添加到wait容器中,构造一个小鸡    
        ...
    }
    只要一个应用程序调用read,最终都会调用到底层驱动的唯一的xxx_read函数
    而xxx_read函数上来就给这个进程分配一个容器并且添加到这个容器中!
    一夫当关万夫莫开!


3)将要休眠的进程添加到等待队列中(关联鸡妈妈和小鸡)
    add_wait_queue(&wq, &wait);
    注意:此时进程还没有休眠


4)设置进程休眠的类型
    明确:linux系统中,进程休眠的类型有两种:
    1.不可中断的休眠类型:进程在休眠期间,如果接收到了kill信号,不会被立刻唤醒,而是等内核来主动
    唤醒休眠的进程之后再去处理之前接收到的kill信号
    结论:不可中断的休眠进程被唤醒的方法只有一种:内核主动来唤醒
    问:何为内核主动来唤醒呢?
    答:就是驱动程序调用一个唤醒函数来唤醒休眠的进程
    2.可中断的休眠类型:进程在休眠期间,如果接收到了kill信号,进程会被立刻唤醒然后处理接收到的kill信号
    结论:可中断的休眠进程被唤醒的方法有两种:
        a)内核来主动唤醒
        b)接收到kill信号来唤醒
    设置进程休眠类型的方法:
    set_current_state(TASK_INTERRUPTIBLE); //可中断类型
    或者
    set_current_state(TASK_UNINTERRUPTIBLE);//不可中断类型
    注意:此时进程还没有休眠


5)然后当前要休眠的进程调用以下函数即可完成最终的休眠:
    schedule();
    进程一旦调用此函数,立刻进入休眠状态,此时释放占用的CPU资源,并且代码停止不前
    静静等待被唤醒,一旦被唤醒,进程立刻继续向下运行


6)一旦进程被唤醒,进程立马从schedule函数返回继续向下运行,首先设置进程的状态由休眠状态改为运行状态:
    set_current_state(TASK_RUNNING);


7)然后将唤醒的进程从等待队列中移除
     remove_wait_queue(&wq, &wait);


8)如果之前休眠的类型是可中断的休眠类型,最后要判断唤醒的原因
    是因为内核主动来唤醒?还是接收到了kill信号引起的唤醒?
    if(signal_pending(current)) {
        printk("进程由于接收到了kill信号引起的唤醒,待会儿就要死去!");
        return -ERESTARTSYS; //重启应用,18年后又是一个好汉
    } else {
        printk("进程由内核主动来唤醒.\n");
        //那么进程就可以正常的继续运行
    }


9)将来一旦事件满足,数据准备就绪则唤醒休眠的进程
例如:数据准备就绪产生中断,由中断处理函数来唤醒休眠的进程
此过程又称内核主动唤醒或者驱动主动唤醒,则进程继续正常运行
唤醒函数两个:
    wake_up(&wq); //唤醒wq队列中所有的进程
    或者
    wake_up_interruptible(&wq);//只唤醒休眠类型是可中断的休眠进程

案例:编写内核程序,实现写write进程来唤醒读read进程


参考代码:day07/2.0

  1 #include<linux/init.h>
  2 #include<linux/module.h>
  3 #include<linux/fs.h>
  4 #include<linux/miscdevice.h>
  5 #include<linux/uaccess.h>
  6 #include<linux/gpio.h>
  7 #include<mach/platform.h>
  8 #include<linux/irq.h>
  9 #include<linux/interrupt.h>
 10 #include<linux/sched.h>//等待队列相关头文件
 11 
 12 //定义等待队列头对象(鸡妈妈)
 13 static wait_queue_head_t rwq;
 14 
 15 //读接口
 16 static ssize_t btn_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
 17     //1.定义初始化装载休眠进程的容器,武装构造小鸡,一个小鸡对应一个休眠的进程
 18     wait_queue_t wait;
 19     init_waitqueue_entry(&wait, current);
 20     //2.将休眠的进程添加到队列中
 21     add_wait_queue(&rwq, &wait);
 22     //3.设置进程休眠的类型
 23     set_current_state(TASK_INTERRUPTIBLE);//可中断的休眠类型
 24     //4.让进程进入真正休眠状态,释放CPU资源,代码停止不前,等待被唤醒
 25     printk("读进程[%s][%d]将进入休眠.\n", current->comm, current->pid);
 26     schedule();
 27     //5.进程一旦被唤醒,从schedule函数返回继续向下运行,首先设置为运行态
 28     set_current_state(TASK_RUNNING);
 29     //6.将唤醒的进程从队列中移除
 30     remove_wait_queue(&rwq, &wait);
 31     //7.判断进程唤醒的原因
 32     if(signal_pending(current)) {
 33         printk("读进程[%s][%d]由于接收到了kill信号引起唤醒.\n", current->comm, current->pid);
 34         return -ERESTARTSYS;
 35     }else{
 36         printk("读进程[%s][%d]由驱动主动唤醒,可以继续正常运行.\n", current->comm, current->pid);
 37     }
 38     return count;
 39 }
 40 
 41 static ssize_t btn_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
 42     //主动唤醒读进程
 43     printk("写进程[%s][%d]来唤醒读进程.\n", current->comm, current->pid);
 44     wake_up(&rwq);
 45     return count;
 46 }
 47 
 48 //定义硬件操作接口对象
 49 static struct file_operations btn_fops = {
 50     .read = btn_read,
 51     .write = btn_write
 52 };
 53 //定义初始化混杂设备对象
 54 static struct miscdevice btn_misc = {
 55     .name = "mybtn",
 56     .minor = MISC_DYNAMIC_MINOR,
 57     .fops = &btn_fops
 58 };
 59 
 60 static int btn_init(void){
 61     misc_register(&btn_misc);
 62     //初始化等待队列头,武装鸡妈妈
 63     init_waitqueue_head(&rwq);
 64     return 0;
 65 }
 66 
 67 static void btn_exit(void){
 68     misc_deregister(&btn_misc);
 69 }
 70 
 71 module_init(btn_init);
 72 module_exit(btn_exit);
 73 MODULE_LICENSE("GPL");


下位机测试:
cd /home/drivers
insmod btn_drv.ko

./btn_test r & //启动读进程,并且利用等待队列休眠


ps
top //查看读进程的休眠类型
    看第四列:STAT
    S:可中断的休眠类型
    D:不可中断的休眠类型


按Q键退出
./btn_test w  //启动写进程来唤醒读进程

 

./btn_test r & //启动读进程
ps
kill  读进程的PID //给读进程发送kill信号来唤醒它

./btn_test r & //启动A读进程
./btn_test r & //启动B读进程 
./btn_test r & //启动C读进程
./btn_test r & //启动D读进程
ps
top
./btn_test  w //唤醒所有的读进程

 往前添加,所以是栈的形式,先休眠的进程后唤醒

./btn_test r & //启动A读进程
./btn_test r & //启动B读进程 
./btn_test r & //启动C读进程
./btn_test r & //启动D读进程
ps
kill 其中某个读进程

案例:基于以上代码,改造成一个真实的按键驱动,采用混杂设备,提供read接口  


    应用程序能够打印按键的状态:按下或者松开
    思路:应用read发现按键无操作,立马进入休眠,如果按键有按下或者松开操作,调用中断处理函数
    中断处理函数来唤醒休眠的read进程,然后read进程将按键的状态拷贝到用户空间
参考代码:day07/3.0

  1 #include<linux/init.h>
  2 #include<linux/module.h>
  3 #include<linux/fs.h>
  4 #include<linux/miscdevice.h>
  5 #include<linux/uaccess.h>
  6 #include<linux/gpio.h>
  7 #include<mach/platform.h>
  8 #include<linux/irq.h>
  9 #include<linux/interrupt.h>
 10 #include<linux/sched.h>//等待队列相关头文件
 11 
 12 //声明描述按键硬件信息的结构体
 13 struct btn_resource {
 14     char *name;
 15     int gpio;
 16 };
 17 static struct btn_resource btn_info[] = {
 18     {
 19         .name = "KEY_UP",
 20         .gpio = PAD_GPIO_A + 28
 21     }
 22 };
 23 //定义等待队列头对象(鸡妈妈)
 24 static wait_queue_head_t rwq;
 25 
 26 //暂存按键的状态
 27 static int g_state;
 28 
 29 //读接口
 30 static ssize_t btn_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
 31     //1.定义初始化装载休眠进程的容器,武装构造小鸡,一个小鸡对应一个休眠的进程
 32     wait_queue_t wait;
 33     init_waitqueue_entry(&wait, current);
 34     //2.将休眠的进程添加到队列中
 35     add_wait_queue(&rwq, &wait);
 36     //3.设置进程休眠的类型
 37     set_current_state(TASK_INTERRUPTIBLE);//可中断的休眠类型
 38     //4.让进程进入真正休眠状态,释放CPU资源,代码停止不前,等待被唤醒
 39     printk("读进程[%s][%d]将进入休眠.\n", current->comm, current->pid);
 40     schedule();
 41     //5.进程一旦被唤醒,从schedule函数返回继续向下运行,首先设置为运行态
 42     set_current_state(TASK_RUNNING);
 43     //6.将唤醒的进程从队列中移除
 44     remove_wait_queue(&rwq, &wait);
 45     //7.判断进程唤醒的原因
 46     if(signal_pending(current)) {
 47         printk("读进程[%s][%d]由于接收到了kill信号引起唤醒.\n", current->comm, current->pid);
 48         return -ERESTARTSYS;
 49     }else{
 50         //如果按键有操作产生中断,中断处理函数获取按键状态,此时读进程也被唤醒,然后将状态拷贝到用户内存
 51         copy_to_user(buf, &g_state, count);
 52         printk("读进程[%s][%d]由驱动主动唤醒,可以继续正常运行.\n", current->comm, current->pid);
 53     }
 54     return count;
 55 }
 56 
 57 static ssize_t btn_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
 58     //主动唤醒读进程
 59     printk("写进程[%s][%d]来唤醒读进程.\n", current->comm, current->pid);
 60     wake_up(&rwq);
 61     return count;
 62 }
 63 
 64 //中断处理函数
 65 static irqreturn_t button_isr(int irq, void *dev){
 66     //获取当前触发中断的按键硬件信息
 67     struct btn_resource *pdata = (struct btn_resource *)dev;
 68 
 69     //获取按键状态保存到全局变量g_state中
 70     g_state = gpio_get_value(pdata->gpio);
 71 
 72     //唤醒休眠的读进程
 73     wake_up(&rwq);
 74     return IRQ_HANDLED;
 75 };
 76 
 77 //定义硬件操作接口对象
 78 static struct file_operations btn_fops = {
 79     .read = btn_read,
 80     .write = btn_write
 81 };
 82 //定义初始化混杂设备对象
 83 static struct miscdevice btn_misc = {
 84     .name = "mybtn",
 85     .minor = MISC_DYNAMIC_MINOR,
 86     .fops = &btn_fops
 87 };
 88 
 89 static int btn_init(void){
 90     int i;
 91     for(i=0;i<ARRAY_SIZE(btn_info);i++){
 92         int irq = gpio_to_irq(btn_info[i].gpio);//中断号
 93         gpio_request(btn_info[i].gpio,btn_info[i].name);//申请GPIO
 94         request_irq(irq, button_isr, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
 95                     btn_info[i].name, &btn_info[i]);//注册中断处理函数
 96 
 97     }
 98     misc_register(&btn_misc);
 99     //初始化等待队列头,武装鸡妈妈
100     init_waitqueue_head(&rwq);
101     return 0;
102 }
103 
104 static void btn_exit(void){
105     int i;
106     for(i=0;i<ARRAY_SIZE(btn_info);i++){
107         int irq = gpio_to_irq(btn_info[i].gpio);
108         gpio_free(btn_info[i].gpio);
109         free_irq(irq, &btn_info[i]);
110 
111     }
112     misc_deregister(&btn_misc);
113 }
114 
115 module_init(btn_init);
116 module_exit(btn_exit);
117 MODULE_LICENSE("GPL");


下位机测试:
cd /home/drivers/

 

cat /proc/interrupt 只注册了一个中断


insmod btn_drv.ko
./btn_test & 

ps
top
操作按键观察打印信息并且观察btn_test的CPU占用率


原先轮询方式基本100%,非常糟糕!

 

 

总结:以按键驱动为例,整个代码执行流程如下:
    应用程序read->驱动btn_read->立刻进入休眠,等待被唤醒
    某个时刻,按下按键->产生中断->调用中断处理函数->获取按键状态kstate=0,按下,并且唤醒休眠的读进程
    读进程从schedule函数返回继续向下运行->将按键的状态kstate=0拷贝到用户缓冲区state->然后调用read函数
    返回,然后打印按键的状态---->按下,至此第一次read操作结束
    
    应用程序第一次read完毕并且打印完毕之后立刻再次read->驱动btn_read->立刻进入休眠,等待被唤醒
    某个时刻,松开按键->再次产生中断->调用中断处理函数->获取按键状态kstate=1,松开,并且唤醒休眠的读进程
    读进程从schedule函数返回继续向下运行->将按键的状态kstate=1拷贝到用户缓冲区state->然后read函数
    返回,然后打印按键的状态---->松开,至此第二次read操作结束,至此整个一个按键的按下松开操作完毕!

    往后就是各种重复循环读取->休眠->唤醒

3.4.等待队列编程方法2


    只需三步骤即可(只是将方法1的九个步骤进行高度封装而已)
具体实施步骤如下:
a)定义初始化等待队列头对象(武装构造鸡妈妈)
    wait_queue_head_t  wq;
    init_waitqueue_head(&wq);
b)进程直接调用以下宏函数完成休眠等工作:
    wait_event(wq, condition);
    说明:
    wq:等待队列头对象,代表整个休眠队列
    condition:
        如果为真,进程调用此宏函数是不会进行休眠操作,立即返回继续向下运行
        如果为假,进程调用此宏函数立刻进入不可中断的休眠状态,代码停止不前,等待被唤醒
        当然此种休眠唤醒的方法只有一种:驱动主动来唤醒
    或者
    wait_event_interruptible(wq, condition);
    说明:
    wq:等待队列头对象,代表整个休眠队列
    condition:
        如果为真,进程调用此宏函数是不会进行休眠操作,立即返回继续向下运行
        如果为假,进程调用此宏函数立刻进入可中断的休眠状态,代码停止不前,等待被唤醒
        当然此种休眠唤醒的方法有两种:驱动主动来唤醒和接收到kill信号来唤醒
    此步骤完成了方法1的第2,3,4,5,6,7,8,七大步骤!
c)事件满足或者外设数据准备继续产生中断来唤醒休闲的进程
    wake_up(&wq); //唤醒所有的休眠进程
    或者
    wake_up_interruptible(&wq);    //唤醒可中断休眠类型的进程
            
d)切记:利用编程方法2实现进程休眠和唤醒的编程框架
    //休眠的代码位置
    int condition = 0; //初始值为假
    ... xxx(....) {
        ...
        wait_even_interruptible(wq, condition); //根据condition决定让进程是否休眠
        condition = 0; //重新置假,为了下一次能够休眠
        ...
    }
    //唤醒的代码位置
    ... yyy(....) {
        ...
        condition = 1; //必须先置真,为了能够从wait_event_interrupitble中返回,否则又休眠了
        wake_up_interruptible(&wq); //唤醒休眠进程
        ...
    }
案例2:利用编程方法2实现按键驱动
参考代码:day07/4.0


实验步骤同上!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值