Linux驱动开发——内核延时操作和内核等待队列


Linux内核延时

概念

延时又称为等待,延时分为两类:忙延时和休眠延时。
忙延时: 当任务进行忙延时时,任务将会导致所占用的CPU资源进行白白消耗,类似原地空转。
休眠延时: 进程进入休眠状态,进程会释放所占用的CPU资源给其他进程使用。

应用场景

忙延时应用在等待时间极短的场合,进程和中断都可以使用忙延时。
休眠延时应用在等待时间较长的场合,只能用于进程,不能用于中断。
注意:CPU资源在进程之间的切换也是需要时间的消耗的 。

Linux内核相关延时函数

忙延时相关的延时函数:

  • 如果忙等待时间超过10毫秒,建议还是使用休眠延时。
ndelay(int n) //纳秒级延时
ndelay(10);//忙延时10纳秒
  		
udelay(int n) //微秒级延时
udelay(10);//忙延时10微秒
  		
mdelay(int n) //毫秒级延时
mdelay(5); //忙延时5毫秒,CPU空转5秒

休眠延时相关的延时函数:

  • 执行休眠延时,进程会释放所占用的CPU资源给其他进程。
msleep(int n); //毫秒级休眠延时
msleep(100);//休眠延时100毫秒

ssleep(int n); //秒级休眠延时 	 
ssleep(10);//休眠延时10秒

schedule();//永久休眠
schedule_timeout(5 * HZ);//休眠延时5秒钟,
  • 补充:如果想让进程能够随时随地休眠和唤醒,必须采用Linux内核的等待队列机制。msleep和ssleep本身都是基于等待队列实现的。
    • 消息队列:IPC通信的一种方式。
    • 工作队列:底半部的延后执行的一种方法。
    • 等待队列:进程在内核空间随时随地休眠,随时随地被唤醒的一种机制。

Linux内核等待队列机制

概述

问:进程在内核空间虽然可以调用msleep / ssleep / schedule / schedule_timeout,能够进行随时随地的进入休眠等待状态,但是不能被随时随地被唤醒。如何才能让进程在内核控件进行随时随地的休眠和被唤醒呢?

  • 答:使用等待队列机制,信号量能够使进程休眠本身也是基于等待队列实现的。

示例应用场景:
一个进程能够获取按键的操作状态(按下或者松开)的需求分析:进程调用read系统调用函数来获取按键的操作状态(按下或者松开)。由于用户操作按键(按下或者松开)本身就是一个随机过程(开心了按两下,不开心不操作),read读进程为了能够及时获取到用户的按键操作,首先想到采用轮训方式(死等,while(1)),但是这种方式会大量消耗CPU资源,大大降低了CPU的利用率(CPU永远只做一件事),于是乎想到轮训的死对头中断机制也就是说进程调用read系统调用函数来获取按键的操作状态,最终进程调用到底层驱动的read接口,如果进程在驱动的read接口中发现按键无操作(既没有按下也没有松开),干脆让read进程在驱动的read接口中进行休眠等待,等待着按键有操作,一旦read进程在驱动的read接口中进行休眠等待,read进程会释放掉占用的CPU资源给其他进程使用(中断不需要给,它会直接抢占),一旦将来用户对按键进行按下或者松开操作,势必产生按键中断,表示按键有操作,此时只需在按键的中断处理函数中去唤醒休眠的read进程一旦read进程被唤醒,read进程再去读取按键的操作状态即可返回到用户空间,此时此刻,CPU至少做2件事(一个是执行read进程,另一个是其他进程),大大提高了CPU的利用率。

  • 问:在这个过程中,如何让read进程随时随地休眠并且在中断到来时让read进程随时随地被唤醒呢?
    • 答:同样利用等待队列机制,等待队列诞生的根本原因:外设的处理速度远远慢于CPU,所以外设没有准备好数据的时候操作外设的进程就需要进行休眠等待。

所以,只要用户进程操作外设,外设的数据处理速度远远慢于CPU,将来驱动势必利用等待队列来实现休眠等待外设准备好数据。

等待队列的功能

等带队列能让用户进程在内核空间随时随地休眠、随时随地被唤醒。
等待队列中操作的对象就是进程。
等待队列=等待+队列 = 要休眠的进程排成一队,形成等待队列,这些休眠的进程要等待某个事件到来,事件没有发生就休眠去等待。

驱动编程实施步骤

这里举个类似下图的例子:
小鸡作为要休眠的进程(驱动完成)、鸡妈妈作为等待队列头(驱动完成)、老鹰作为进程调度器(由内核完成)。
在这里插入图片描述具体编程步骤:

  1. 定义初始化一个等待队列头(构造一个鸡妈妈)
wait_queue_head_t wq; //定义等待队列头对象
init_waitqueue_head(&wq);//初始化等待队列头
  1. 定义初始化装载要休眠进程的容器(给每个休眠的进程构造一个小鸡)。注意:一个要休眠的进程对应一个容器wait(小鸡),其中current是一个内核全局指针变量,对应的数据类型为struct task_struct,此数据结构用来描述进程的信息,只要fork一个进程,内核就会用此数据结构定义初始化一个对象来描述fork出来的进程信息,current指针永远指向当前进程对应的struct task_struct对象。“当前进程”指正在获取CPU资源投入运行的进程。
wait_queue_t wait; //定义装载休眠进程的容器(造小鸡)
init_waitqueue_entry(&wait, current); //将当前要休眠的进程添加到容器wait中
  1. 将休眠的当前进程添加到等待队列中去(将小鸡放在鸡妈妈的后面),注意此时此刻进程还没有正式休眠。
add_wait_queue(&wq, &wait); 
  1. 设置要休眠的当前进程的休眠状态,进程休眠状态的类型分为两类:
    • 可中断的休眠类型(TASK_INTERRUPTIBLE):休眠器件可以立即处理接收的信号,此类休眠进程被唤醒的方式有两种:接收信号唤醒,驱动主动唤醒。
    • 不可中断的休眠类型(TASK_UNINTERRUPTIBLE):休眠器件如果接收到信号,不会立即处理而是在被唤醒以后处理信号,此类休眠进程被唤醒的方法只有一种:驱动主动唤醒。
      注意:此时此刻当前进程还没有正式休眠。
set_current_state(TASK_INTERRUPTIBLE);//设置为可中断的休眠类型

set_current_state(TASK_UNINTERRUPTIBLE);//设置为不可中断的休眠类型
  1. 此时此刻当前进程正式进入休眠状态,此时此刻当前进程会释放所占用的CPU资源给其他进程,此时此刻代码停止不前等待被唤醒,一旦被唤醒,代码继续往下执行。
schedule();
//注意:不能单独调用此函数,否则要休眠的当前进程
//会默认放到内核的默认等待队列中,将来如果要唤醒,做不到随时随地了
  1. 一旦休眠的进程被唤醒,设置休眠的进程状态位运行态。
set_current_state(TASK_RUNNING);
  1. 最后将唤醒的休眠进程从等待队列中移除
remove_wait_queue(&wq, &wait);
  1. 一般最好建议判断一下进程被唤醒的原因
if(signal_pending(current)) 
{
	printk("进程由于接收到了信号引起的唤醒!\n");
	return -ERESTARTSYS;
} 
else 
{
	printk("进程由于驱动主动引起唤醒!\n");
	//接下里被唤醒的进程就可以访问硬件
	//说明硬件数据准备就绪了
}
  1. 驱动主动唤醒休眠进程的方法
wake_up(&wq);//唤醒wq等待队列中所有的休眠进程

wake_up_interruptible(&wq);//唤醒wq等待队列中所有的休眠类型为可中断的进程

示例代码(一)

实现一个使read操作的进程休眠,另一个进程操作write唤醒被休眠的进程的驱动。

  • wake_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/sched.h> //TASK_INTERRUPTIBLE等

//定义一个等待队列头对象(造鸡妈妈)
static wait_queue_head_t rwq;
static ssize_t wake_read(struct file *file,
                        char __user *buf,
                        size_t count,
                        loff_t *ppos)
{
    //1.定义初始化装载休眠进程的容器(构造小鸡)
    //将当前进程添加到容器中
    //一个进程一个容器
    wait_queue_t wait;
    init_waitqueue_entry(&wait, current);

    //2.将当前进程添加到等待队列中去
    add_wait_queue(&rwq, &wait);

    //3.设置当前进程休眠的状态类型为可中断
    set_current_state(TASK_INTERRUPTIBLE);

    //4.当前进程正式进入休眠状态
    //此时代码停止不前
    //当前进程释放CPU资源
    //一旦被唤醒,进程继续往下执行
    printk("读进程[%s][%d]将进入休眠状态...\n",
                    current->comm, current->pid);
    schedule();

    //5.一旦被唤醒,设置进程的状态为运行
    set_current_state(TASK_RUNNING);
    
    //6.将被唤醒的进程从队列中移除
    remove_wait_queue(&rwq, &wait);

    //7.判断进程被唤醒的原因
    if(signal_pending(current)) {
        printk("读进程[%s][%d]由于接收到了信号引起唤醒\n",
                            current->comm, current->pid);
        return -ERESTARTSYS;
    } else {
        printk("读进程[%s][%d]由于驱动主动引起唤醒.\n",
                            current->comm, current->pid);
    }
    return count;
}

static ssize_t wake_write(struct file *file,
                        const char __user *buf,
                        size_t count,
                        loff_t *ppos)
{
    //1.唤醒休眠的读进程
    printk("写进程[%s][%d]唤醒读进程\n",
                        current->comm, current->pid);
    wake_up(&rwq);
    return count;
}

//定义初始化硬件操作接口对象
static struct file_operations wake_fops = {
    .owner = THIS_MODULE,
    .read = wake_read,
    .write = wake_write
};

//定义初始化混杂设备对象
static struct miscdevice wake_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "mywake",
    .fops = &btn_fops
};

static int wake_init(void)
{
    //注册混杂设备对象
    misc_register(&wake_misc);
    //初始化等待队列头(武装鸡妈妈)
    init_waitqueue_head(&rwq);
    return 0;
}

static void wake_exit(void)
{
    //卸载混杂设备对象
    misc_deregister(&wake_misc);
}
module_init(wake_init);
module_exit(wake_exit);
MODULE_LICENSE("GPL");

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

  • wake_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;

    fd = open("/dev/mywake", O_RDWR);
    if (fd < 0) 
        return -1;

    if(argc != 2) {
        printf("用法:%s <r|w>\n", argv[0]);
        return -1;
    }

    if(!strcmp(argv[1], "r"))
        read(fd, NULL, 0); //启动一个读进程
    else if(!strcmp(argv[1], "w"))
        write(fd, NULL, 0); //启动一个写进程

    close(fd);
    return 0;
}

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

示例代码(二)

实现一个休眠等待读取按键值的驱动,进程任务休眠等待按键,按键按下并唤醒进程读取当前按键的键值。

  • btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/uaccess.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/sched.h> //TASK_INTERRUPTIBLE等
#include <linux/input.h>

//声明描述按键信息的数据结构
struct btn_resource {
    int gpio; //按键对应的GPIO编号
    char *name;//按键名称
    int code;//按键值
};

//声明上报按键信息的数据结构
struct btn_event {
    int state; //上报按键的状态:1:按下;0:松开
    int code;  //上报按键值
};

//定义初始化四个按键的硬件信息对象
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A + 28,
        .name = "KEY_UP",
        .code = KEY_UP
    }
};

//分配内核缓冲区,记录当前操作的按键信息
static struct btn_event kbtn;

//定义一个等待队列头对象(造鸡妈妈)
static wait_queue_head_t rwq;
static ssize_t btn_read(struct file *file,
                        char __user *buf,
                        size_t count,
                        loff_t *ppos)
{
    //1.定义初始化装载休眠进程的容器(构造小鸡)
    //将当前进程添加到容器中
    //一个进程一个容器
    wait_queue_t wait;
    init_waitqueue_entry(&wait, current);

    //2.将当前进程添加到等待队列中去
    add_wait_queue(&rwq, &wait);

    //3.设置当前进程休眠的状态类型为可中断
    set_current_state(TASK_INTERRUPTIBLE);

    //4.当前进程正式进入休眠状态
    //此时代码停止不前
    //当前进程释放CPU资源
    //一旦被唤醒,进程继续往下执行
    schedule();

    //5.一旦被唤醒,设置进程的状态为运行
    set_current_state(TASK_RUNNING);
    
    //6.将被唤醒的进程从队列中移除
    remove_wait_queue(&rwq, &wait);

    //7.判断进程被唤醒的原因
    if(signal_pending(current)) {
        printk("读进程[%s][%d]由于接收到了信号引起唤醒\n",
                            current->comm, current->pid);
        return -ERESTARTSYS;
    } else {
        //此时的kbtn已经被中断处理函数进行赋值操作
        copy_to_user(buf, &kbtn, sizeof(kbtn));
    }
    return count;
}

//中断处理函数
static irqreturn_t button_isr(int irq, void *dev)
{
    //1.获取当前操作的按键的硬件信息
    struct btn_resource *pdata = dev;
    
    //2.获取按键的状态和按键值保存在全局变量中
    kbtn.state = gpio_get_value(pdata->gpio);
    kbtn.code = pdata->code;

    //3.一旦有按键操作,硬件上势必产生中断
    //也就说明按键有操作,应该唤醒read进程读取按键的信息
    wake_up(&rwq);
    return IRQ_HANDLED; //中断返回有可能才轮到read进程执行
}

//定义初始化硬件操作接口对象
static struct file_operations btn_fops = {
    .owner = THIS_MODULE,
    .read = btn_read,
};

//定义初始化混杂设备对象
static struct miscdevice btn_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "mybtn",
    .fops = &btn_fops
};

static int btn_init(void)
{
    int i;
    //注册混杂设备对象
    misc_register(&btn_misc);
    //初始化等待队列头(武装鸡妈妈)
    init_waitqueue_head(&rwq);
    //申请GPIO资源
    //申请中断资源,注册中断处理函数
    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);
        request_irq(irq, button_isr,
            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
            btn_info[i].name, &btn_info[i]);
    }
    return 0;
}

static void btn_exit(void)
{
    int i;
    //各种释放
    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]);
    }
    //卸载混杂设备对象
    misc_deregister(&btn_misc);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");

  • btn_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

//声明按键信息数据结构
struct btn_event {
    int state; //按键状态:1:松开;0:按下
    int code; //按键值
};

int main(int argc, char *argv[])
{
    int fd;
    struct btn_event btn; //分配用户缓冲区,记录按键的信息

    //打开设备
    fd = open("/dev/mybtn", O_RDWR);
    if (fd < 0) {
        printf("打开设备失败!\n");
        return -1;
    }

    while(1) {
        read(fd, &btn, sizeof(btn));
        printf("按键[%d]的状态为[%s]\n",
                btn.code, btn.state ?"松开":"按下");
    }

    //关闭设备
    close(fd);
    return 0;
}
  • Makefile
obj-m += btn_drv.o
all:
	make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
	make -C /opt/kernel SUBDIRS=$(PWD) clean

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

总结

使用等待队列的方式能够很好的解决应用层的进程调用外设却需要等待外设准备就绪的问题。

发布了78 篇原创文章 · 获赞 25 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_37596943/article/details/103795769