Linux驱动IO篇——阻塞/非阻塞IO

简介: Linux驱动IO篇——阻塞/非阻塞IO

非阻塞IO

在应用程序中,使用open函数打开一个/dev目录下的一个设备文件时,默认是以阻塞的方式打开。

所谓阻塞,就是当我们请求的资源不可用时(资源被占用,没有数据到达等等),会使得进程休眠,从现象看就是卡在那里。

应用层

如果我们希望以非阻塞方式打开设备文件,则应该在open设备文件时,添加一个O_NONBLOCK的flag参数,例如:

fd = open("/dev/vser0", O_RDWR | O_NONBLOCK);

驱动层

应用层以非阻塞方式打开设备文件,则驱动层也要有对应的处理操作才行。

应用层传入的O_NONBLOCK标志,会保存在struct file结构体的f_flags成员中。当资源不可用时,同时判断f_flags变量是否为O_NONBLOCK,有则代表以非阻塞方式打开,然后返回一个-EAGAIN错误,提示应用层资源暂时不可用。

下面是驱动中read函数非阻塞处理的伪代码,例如:

static ssize_t vser_read(struct file *flip, char __user *buf, size_t count, loff_t *pos)
{
 ......
 if (资源不可用)
  if (filp->f_flags & O_NONBLOCK)
   return -EAGAIN;
 ......
}

当出现资源不可用时,会出现以下提示:

read: Resource temporarily unavailable

阻塞IO

上述非阻塞方式打开设备文件,虽然可以防止进程休眠,无论结果如何都会立即返回,但缺点是必须要定期查询资源是否可以获得,例如上述代码中,每次调用read函数都要去查询一下资源是否可用。这种操作效率非常低。

但是用阻塞IO的话,进程休眠期间就再也不能做其他的事情了

所以不论是非阻塞IO还是阻塞IO,都有缺点,有没有好的办法呢?

正确做法应该是:使用阻塞IO,驱动中添加唤醒操作

什么意思呢?既然有休眠,就应该有对应的唤醒操作,否则进程将会一直休眠下去。驱动程序应该在资源可用时负责执行唤醒操作。

要实现既有休眠,又有唤醒的阻塞IO模型,应该使用等待队列

等待队列

我们以一个虚拟串口设备为例:

如图是一个虚拟串口设备示例图,这是一个功能弱化之后的只具备内回环作用的串口。

主要功能:在驱动中实现一个FIFO,驱动接收用户层传来的数据,然后将之放入FIFO,当应用层要获取数据时,驱动将FIFO中的数据读出,然后复制给应用层。

我们以这个虚拟串口设备为例,讲解等待队列的使用。

为了方便理解,简化了不必要的代码,下面是驱动代码:

//定义内核fifo
DEFINE_KFIFO(vsfifo, char ,32)
//定义两个等待队列头
wait_queue_head_t rwqh;//读等待队列
wait_queue_head_t wwqh;//写等待队列
static ssize_t vser_read(struct file *flip, char __user *buf, size_t count, loff_t *pos)
{
......
    /* fifo为空,没有数据可读,进入休眠*/
 if(kfifo_is_empty(&vsfifo)) {
  if(flip->f_flags & O_NONBLOCK)//非阻塞方式,直接返回
   return -EAGAIN;
        /* 阻塞方式,没有数据可读,将进程放入读等待队列rwqh,进程休眠;唤醒条件是fifo不为空 */
  if (wait_event_interruptible(rwqh, !kfifo_is_empty(vsfifo)))
   return -ERESTARTSYS;
 }
    //将fifo中的数据返回给应用层
    ret = kfifo_to_user(&vsfifo, buf, count, &copied);
    /* fifo未满,还有空间,代表可以往fifo写数据,唤醒写等待队列 */
 if (!kfifo_is_full(&vsfifo))
  wake_up_interruptible(&wwqh);
......
}
static ssize_t vser_write(struct file *flip, const char __user *buf, size_t count, loff_t *pos)
{
......   
    /* fifo已满,不可写 */
    if (kfifo_is_full(&vsfifo)) {
        if (flip->f_flags & O_NONBLOCK)//非阻塞方式,直接返回
            return -EAGAIN;
        /* 阻塞方式,进程休眠,放入写等待队列,唤醒条件是fifo未满时 */
        if (wati_event_interruptible(wwqh, !kfifo_is_full(&vsfifo)))
            return -ERESTARTSYS;
    }
    //从应用层获取数据,写入fifo
    ret = kfifo_from_user(&vsfifo, buf, count, &copied);
    /* fifo不为空,唤醒读等待队列rwqh */
    if (!kfifo_is_empty(&vsfifo))
         wake_up_interruptible(&rwqh);
......
}
/* 驱动入口函数 */
static int __init vser_init(void)
{
......
    /* 初始化等待队列头 */
 init_waitqueue_head(rwqh);
    init_waitqueue_head(wwqh);
......
}

首先用wait_queue_head_t定义两个等待队列头rwqhwwqh,分表代表读等待队列和写等待队列,然后在驱动入口函数vser_init中调用init_waitqueue_head初始化等待队列头。

读操作处理

当应用程序想要从驱动中读取数据,而fifo为空时,此时代表没有数据可读。如果是非阻塞方式则直接返回,如果是阻塞方式,调用wait_event_interruptible函数将进程放入读等待队列rwqh,进程休眠。当fifo不为空时,即fifo有数据时,才会被唤醒。

写操作处理

当应用层想往驱动中写数据,而fifo已经满了,此时代表不可写。如果是非阻塞方式直接返回,如果是阻塞方式,则调用wati_event_interruptible函数将进程放入写等待队列,进程休眠。当fifo未满时,才会被唤醒。

唤醒处理

调用wake_up_interruptible函数唤醒对应的等待队列

通过在上述加入等待队列的操作,当以阻塞方式打开设备文件时,资源可用时会被唤醒,而不至于一直休眠。

等待队列变体

上述只是使用了等待队列最常用的函数,等待队列还有其他很多的变体,下面列举常见的:

DECLARE_WAIT_QUEUE_HEAD(name)
init_waitqueue_head(q)
wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wake_up(x)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeour)
wait_event_interruptible_exclusive(wq, condition)
wake_up_interruptible(x)
wait_event_interruptible_locked(wq, condition)
wait_event_interruptible_locked_irq(wq, condition)
wake_up_locked(x)

虽然有很多的变体,但简单的说,使进程休眠调用wait_event_xxx函数,唤醒进程调用wake_up_xxx函数。其余的后缀一一对应即可。

关于这些宏或函数的更多信息请参考“include/linux/wait.h”

end

猜你喜欢

Linux驱动IO篇——ioctl设备操作

一个Linux驱动工程师必知的内核模块知识

Linux内核中常用的数据结构和算法

Linux内核中常用的C语言技巧

Linux内核基础篇——常用调试技巧汇总

Linux内核基础篇——动态输出调试

Linux内核基础篇——printk调试

Linux内核基础篇——initcall

相关文章
|
安全 Linux
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
436 84
|
9月前
|
监控 Linux 开发者
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
485 0
|
10月前
|
Linux C语言 网络架构
Linux的基础IO内容补充-FILE
而当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fwrite函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和fwrite函数打印的数据就有两份。此时我们就可以知道,
240 0
|
10月前
|
存储 Linux Shell
Linux的基础IO
那么,这里我们温习一下操作系统的概念我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。这就是因为在根本上操作系统确实像银行一样,并不完全信任用户程序,因为直接开放底层资源(如内存、磁盘、硬件访问权限)给用户程序会带来巨大的风险。所以就向银行一样他的服务是由工作人员隔着一层玻璃,然后对顾客进行服务的。
138 0
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
608 34
|
Linux API C语言
Linux基础IO
Linux基础IO操作是系统管理和开发的基本技能。通过掌握文件描述符、重定向与管道、性能分析工具、文件系统操作以及网络IO命令等内容,可以更高效地进行系统操作和脚本编写。希望本文提供的知识和示例能帮助读者更深入地理解和运用Linux IO操作。
303 14
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】

热门文章

最新文章