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

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

文章目录

Linux内核延时

概念

应用场景

Linux内核相关延时函数

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,将来驱动势必利用等待队列来实现休眠等待外设准备好数据。


等待队列的功能

等带队列能让用户进程在内核空间随时随地休眠、随时随地被唤醒。

等待队列中操作的对象就是进程。

等待队列=等待+队列 = 要休眠的进程排成一队,形成等待队列,这些休眠的进程要等待某个事件到来,事件没有发生就休眠去等待。


驱动编程实施步骤

这里举个类似下图的例子:

小鸡作为要休眠的进程(驱动完成)、鸡妈妈作为等待队列头(驱动完成)、老鹰作为进程调度器(由内核完成)。


20200101210020723.png

20200101210020723.png

具体编程步骤:


  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. 设置要休眠的当前进程的休眠状态,进程休眠状态的类型分为两类:
  2. 可中断的休眠类型(TASK_INTERRUPTIBLE):休眠器件可以立即处理接收的信号,此类休眠进程被唤醒的方式有两种:接收信号唤醒,驱动主动唤醒。
  3. 不可中断的休眠类型(TASK_UNINTERRUPTIBLE):休眠器件如果接收到信号,不会立即处理而是在被唤醒以后处理信号,此类休眠进程被唤醒的方法只有一种:驱动主动唤醒。
  4. 注意:此时此刻当前进程还没有正式休眠。
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;
}


  • 执行结果:

20200101220015662.png

示例代码(二)

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


  • 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


  • 执行结果:

20200101222006916.png

总结

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

相关文章
|
3天前
|
Linux API 调度
技术笔记:Linux内核跟踪和性能分析
技术笔记:Linux内核跟踪和性能分析
|
15天前
|
编解码 Linux
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
XviD是开源的MPEG-4视频编解码器,曾与DivX一起用于早期MP4视频编码,但现在已被H.264取代。要集成XviD到Linux上的FFmpeg,首先下载源码,解压后配置并编译安装libxvid。接着,在FFmpeg源码目录中,重新配置FFmpeg以启用libxvid,然后编译并安装。成功后,通过`ffmpeg -version`检查是否启用libxvid。详细步骤包括下载、解压libxvid,使用`configure`和`make`命令安装,以及更新FFmpeg配置并安装。
33 2
FFmpeg开发笔记(二十八)Linux环境给FFmpeg集成libxvid
|
21天前
|
Web App开发 安全 Linux
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
《FFmpeg开发实战》书中介绍轻量级流媒体服务器MediaMTX,但其功能有限,不适合生产环境。推荐使用国产开源的ZLMediaKit,它支持多种流媒体协议和音视频编码标准。以下是华为欧拉系统下编译安装ZLMediaKit和FFmpeg的步骤,包括更新依赖、下载源码、配置、编译、安装以及启动MediaServer服务。此外,还提供了通过FFmpeg进行RTSP和RTMP推流,并使用VLC播放器拉流的示例。
35 3
FFmpeg开发笔记(二十六)Linux环境安装ZLMediaKit实现视频推流
|
11天前
|
Linux
查看linux内核版本
在Linux中查看内核版本可使用`uname -r`、`cat /proc/version`、`lsb_release -a`(若安装LSB)、`/etc/*release`或`/etc/*version`文件、`dmesg | grep Linux`、`cat /sys/class/dmi/id/product_name`、`hostnamectl`、`kernrelease`(如果支持)、`rpm -q kernel`(RPM系统)和`dpkg -l linux-image-*`(Debian系统)。
25 4
|
12天前
|
安全 Linux 数据处理
探索Linux的kmod命令:管理内核模块的利器
`kmod`是Linux下管理内核模块的工具,用于加载、卸载和管理模块及其依赖。使用`kmod load`来加载模块,`kmod remove`卸载模块,`kmod list`查看已加载模块,`kmod alias`显示模块别名。注意需有root权限,且要考虑依赖关系和版本兼容性。最佳实践包括备份、查阅文档和使用额外的管理工具。
|
16天前
|
Linux Windows 虚拟化
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
|
3天前
|
移动开发 程序员 Linux
老程序员分享:linux驱动开发笔记_ioctl函数
老程序员分享:linux驱动开发笔记_ioctl函数
|
18天前
|
Linux 编译器 C语言
编译Linux内核:基础、重要性和交叉编译方法
Linux内核作为操作系统的心脏,负责管理计算机的硬件资源,同时也是运行所有其他程序的基础。理解如何编译Linux内核对于系统管理员、开发者乃至高级用户来说都是一项极其宝贵的技能。本文将介绍编译Linux内核的基本知识、编译的重要性、具体步骤以及交叉编译的概念。
28 0
|
1天前
|
Linux 数据处理
探索Linux下的readelf命令:深入了解ELF文件
`readelf`是Linux下分析ELF文件的命令行工具,用于查看文件头、节区、符号表等信息。支持可执行文件、共享库等多种类型。常用选项有`-h`(文件头)、`-l`(程序头)、`-S`(节区)、`-s`(符号表)、`-r`(重定位)和`-d`(动态节区)。结合其他工具如`objdump`,能深入理解二进制文件,助力开发和调试。