内核实验(九):添加IO驱动的阻塞读写功能

简介: 本文通过修改内核模块代码,介绍了如何在Linux内核中为IO驱动添加阻塞读写功能,使用等待队列和条件唤醒机制来实现读写操作的阻塞和非阻塞模式,并在Qemu虚拟机上进行了编译、部署和测试。

一、篇头

继续使用qemu调试内核的实验。本章复习阻塞与非阻塞IO的概念和机制,然后对之前实验(八)的代码做少许修改,添加阻塞的IO读写。

二、系列文章

略……

三、实验环境

  • 编译服务器+NFS:ubuntu 22.04
  • Qemu 虚拟机:Linux version 5.15.102 + Buysbox 1.3.36 + ARM_32bit

Qemu 启动命令:qemu-system-arm -nographic -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -initrd …/busybox/rootfs.ext4.img.gz -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb

四、源码

4.1 概念及接口介绍

非阻塞IO

进程发起I/O系统调用后,如果设备驱动的缓冲区没有数据,那么进程返回一个错误而不会被阻塞。如果驱动缓冲区中有数据,那么设备驱动把数据直接返回给用户进程。

阻塞

进程发起I/O系统调用后,如果设备的缓冲区没有数据,那么需要到硬件I/O中重新获取新数据,进程会被阻塞,也就是睡眠等待。直到数据准备好,进程才会被唤醒,并重新把数据返回给用户空间。

睡眠等待

Linux内核提供了简单的睡眠方式,并封装成wait_event()的宏以及其他几个扩展宏,主要功能是在让进程睡眠时也检查进程的唤醒条件。

wait_event(wq, condition)
wait_event_interruptible(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible_timeout(wq, condition, timeout)

wq表示等待队列头。condition是一个布尔表达式,在condition变为真之前,进程会保持睡眠状态。timeout表示当timeout时间到达之后,进程会被唤醒,因此它只会等待限定的时间。当给定的时间到了之后,wait_event_timeout()和wait_event_interruptible_timeout()这两个宏无论condition是否为真,都会返回0。
wait_event_interruptible()会让进程进入可中断睡眠状态,而wait_event()会让进程进入不可中断睡眠态,也就是说不受干扰,对信号不做任何反应,不可能发送 SIGKILL 信号使它停止,因为它们不响应信号。因此,一般驱动程序不会采用这个睡眠模式。

唤醒
wake_up(x)
wake_up_interruptible(x)

wake_up()会唤醒等待队列中所有的进程;
wake_up()应该和 wait_event()或者wait_event_timeout()配对使用;
wake_up_interruptible()应该和 wait_event_interruptible()wait_event_interruptible_timeout()配对使用。

4.2 驱动源码

  • 文件名:linux-stable\my_kmodules\test_9.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/kfifo.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/slab.h> 



#define MY_DEV_NAME "my_dev"


struct my_device_st {
   
   
    const char *name;
    struct device *dev;
    struct miscdevice *miscdev;
    wait_queue_head_t read_queue;
    wait_queue_head_t write_queue;    
};

static struct my_device_st *my_device; 



DEFINE_KFIFO(my_kfifo, char, 128);

static int test_9_open(struct inode *inode, struct file *file)
{
   
   
    int major = MAJOR(inode->i_rdev);
    int minor = MINOR(inode->i_rdev);

    pr_info("%s: major=%d, minor=%d\n", __func__, major, minor);

    file->private_data = my_device;
    pr_info("%s: file->private_data=0x%x \n", __func__, (unsigned int)file->private_data);    

    return 0;
}

static int test_9_release(struct inode *inode, struct file *file)
{
   
       
    struct my_device_st *data = file->private_data;
    pr_info("%s: file->private_data=0x%x \n", __func__, (unsigned int)file->private_data);    
    kfree(data);
    file->private_data = NULL;
    return 0;
}

static ssize_t test_9_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
   
   
    int ret;
    unsigned int copied_count=0;
    struct my_device_st *my_device = file->private_data;

    pr_info("%s \n", __func__);

     /*
     * (1)add for O_NONBLOCK reading
     */
    if(kfifo_is_empty(&my_kfifo)){
   
   
        if(file->f_flags & O_NONBLOCK)
            return  -EAGAIN;

        // (2)若用户发起的是非BLOCK读,则因为空间为空,调度进程到等等队列,进程睡眠
        pr_info("%s: pid=%d, going to sleep\n", __func__, current->pid);
        ret = wait_event_interruptible(my_device->read_queue,
                    !kfifo_is_empty(&my_kfifo));
        if (ret)
            return ret;
    }

    ret = kfifo_to_user(&my_kfifo, buf,  count, &copied_count);
    if(ret != 0) 
        return -EFAULT;

    // (3)如果FIFI有空余,唤醒等待写入的其他进程
    if (!kfifo_is_full(&my_kfifo))
        wake_up_interruptible(&my_device->write_queue);

    pr_info("%s, pid=%d, actual_readed=%d, pos=%lld\n",__func__,
            current->pid, copied_count, *ppos);

    return copied_count;
}

static ssize_t test_9_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
   
   
    int ret;
    unsigned int copied_count=0;
    struct my_device_st *data = file->private_data;

    pr_info("%s \n", __func__);

     /*
     * (1)add for O_NONBLOCK writing
     */
    if(kfifo_is_full(&my_kfifo)){
   
   
        if(file->f_flags & O_NONBLOCK){
   
   
            return  -EAGAIN;
        }

        // (2)若用户发起的是非BLOCK写,则因为空间已满,调度进程到等等队列,进程睡眠
        pr_info("%s: pid=%d, going to sleep\n", __func__, current->pid);
        ret = wait_event_interruptible(data->write_queue,
                    !kfifo_is_empty(&my_kfifo));
        if (ret)
            return ret;
    }

    ret = kfifo_from_user(&my_kfifo, buf, count, &copied_count);
    if(ret != 0) 
        return -EFAULT;

    // (3)如果数据非空,唤醒等待读取的其他进程
    if (!kfifo_is_empty(&my_kfifo))
        wake_up_interruptible(&data->read_queue);

    pr_info("%s, pid=%d, actual_readed=%d, pos=%lld\n",__func__,
            current->pid, copied_count, *f_pos);


    return copied_count;


}

static const struct file_operations test_fops = {
   
   
    .owner = THIS_MODULE,
    .open = test_9_open,
    .release = test_9_release,
    .read = test_9_read,
    .write = test_9_write
};

static struct miscdevice test_9_misc_device ={
   
   

    .minor = MISC_DYNAMIC_MINOR,
    .name = MY_DEV_NAME,
    .fops = &test_fops,
};


static int __init test_9_init(void)
{
   
   
    int ret;

    pr_info("test_9_init\n");

    //(1) 创建共享的数据结构,包含IO读和写的等待队列
    my_device = kmalloc(sizeof(struct my_device_st), GFP_KERNEL);
    if (!my_device)
        return -ENOMEM;

    my_device->miscdev = &test_9_misc_device;
    init_waitqueue_head(&my_device->read_queue);
    init_waitqueue_head(&my_device->write_queue);


    ret = misc_register(&test_9_misc_device);
    if (ret != 0 ) {
   
   
        pr_err("failed to misc_register");
        return ret;
    }

    pr_err("Minor number = %d\n", test_9_misc_device.minor);
    return 0;
}

static void __exit test_9_exit(void)
{
   
   
    pr_info("test_9_exit\n");
    if(my_device)
        kfree(my_device);
    misc_deregister(&test_9_misc_device);
}

module_init(test_9_init);
module_exit(test_9_exit);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("szhou <66176468@qq.com>");
MODULE_DESCRIPTION("test_9, 使用misc、KFIFO开发设备驱动非阻塞+阻塞的设备驱动。");

4.3 APP源码

  • 简单起见,用echo和cat分别启2个进程进行读写。

4.4 Makefile

  • 文件名:linux-stable\my_kmodules\Makefile
  • 如下,继续追加 test_9.o 即可编译出test_9.ko
KDIR := /home/szhou/works/qemu_linux/linux-stable

obj-m := test_1.o test_2.o test_3.o test_4.o  test_5.o test_6.o test_7.o test_9.o
all :
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
    rm -f *.ko

五、编译及部署

1)执行驱动KO编译
szhou@bc01:~/works/qemu_linux/linux-stable/my_kmodules$ make
make -C /home/szhou/works/qemu_linux/linux-stable M=/home/szhou/works/qemu_linux/linux-stable/my_kmodules modules
make[1]: Entering directory '/home/szhou/works/qemu_linux/linux-stable'
  CC [M]  /home/szhou/works/qemu_linux/linux-stable/my_kmodules/test_9.o
  MODPOST /home/szhou/works/qemu_linux/linux-stable/my_kmodules/Module.symvers
  CC [M]  /home/szhou/works/qemu_linux/linux-stable/my_kmodules/test_9.mod.o
  LD [M]  /home/szhou/works/qemu_linux/linux-stable/my_kmodules/test_9.ko
make[1]: Leaving directory '/home/szhou/works/qemu_linux/linux-stable'2)执行app编译
szhou@bc01:~/works/qemu_linux/linux-stable/my_kmodules$ arm-linux-gnueabi-gcc app_test_9.c -o app_test_9 --static3)将KO和APP存放到NFS共享目录
szhou@bc01:~/works/qemu_linux/linux-stable/my_kmodules$ cp test_9.ko app_test_9  ~/works/nfs_share/
szhou@bc01:~/works/qemu_linux/linux-stable/my_kmodules$

六、运行及测试

1)启动之前编译组建的QEMU虚拟机
----------------------------------------
Welcome to szhou's tiny Linux
----------------------------------------

Please press Enter to activate this console. 
~2)挂载NFS共享目录
~ #  mount -t nfs -o nolock 192.168.3.67:/home/szhou/works/nfs_share /mnt

(3) 加载ko
~ # cd /mnt/
/mnt # 
/mnt # insmod test_9.ko 
test_9: loading out-of-tree module taints kernel.
test_9_init
Minor number = 125
/mnt # 
/mnt # mdev -s

(4) 运行app测试
/mnt # cat /dev/my_dev  &
/mnt # test_9_open: major=10, minor=125
test_9_open: file->private_data=0x81b8bb00 
test_9_read 
test_9_read: pid=489, going to sleep

/mnt # echo "Hellow, do block reading ."  >  /dev/my_dev 
test_9_open: major=10, minor=125
test_9_open: file->private_data=0x81b8bb00 
test_9_write 
test_9_write, pid=457, actual_readed=27, pos=0
test_9_read, pid=489, actual_readed=27, pos=0
Hellow, do block reading .
test_9_read 
test_9_read: pid=489, going to sleep
test_9_release: file->private_data=0x81b8bb00 
/mnt #

操作画面如下图:

图片

相关文章
|
3月前
|
Java Unix Linux
什么是阻塞IO和非阻塞IO
什么是阻塞IO和非阻塞IO
|
20天前
|
存储 Java 数据库连接
BIO阻塞IO流与数据存储大揭秘:性能与资源消耗,一文让你彻底解锁!
【8月更文挑战第25天】本文探讨了Java中BIO阻塞IO流与数据存储的概念及其实现。BIO作为一种传统IO模型,在处理每个客户端请求时需创建新线程并等待响应,这在并发量大时会导致性能下降和高资源消耗。示例代码展示了如何利用`ServerSocket`实现基于BIO的简单服务器。此外,文章还介绍了数据存储的基本方法,例如通过`BufferedWriter`向文件写入数据。两者对比显示,BIO适合连接数稳定的场景,而数据存储则适用于需要持久化保存信息的情况。通过这些分析和实例,希望能帮助读者更好地掌握这两种技术的应用场景及其优缺点。
26 0
|
2月前
|
Linux 开发工具
CPU-IO-网络-内核参数的调优
CPU-IO-网络-内核参数的调优
64 7
|
2月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
115 1
|
2月前
|
Java
什么是阻塞IO?
**阻塞IO是一种IO操作模式,使得调用线程在IO未完成时会暂停,等待操作完成。简单但可能导致线程阻塞,适用于低并发、长处理场景。Java示例中,`ServerSocket`和`Socket`展示了这种模式。服务端接收到客户端连接后读取数据,回应&quot;Echo&quot;,每个连接需单独线程处理。高并发时可考虑非阻塞IO(NIO)或异步IO来优化。**
|
2月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
2月前
|
缓存 网络协议 算法
【Linux系统编程】深入剖析:四大IO模型机制与应用(阻塞、非阻塞、多路复用、信号驱动IO 全解读)
在Linux环境下,主要存在四种IO模型,它们分别是阻塞IO(Blocking IO)、非阻塞IO(Non-blocking IO)、IO多路复用(I/O Multiplexing)和异步IO(Asynchronous IO)。下面我将逐一介绍这些模型的定义:
118 1
|
4月前
|
Java API
文件IO (File对象, 文件读写)
文件IO (File对象, 文件读写)
39 2
|
4月前
|
移动开发 前端开发 JavaScript
uniapp中IO模块(管理本地文件系统)的常用功能封装
uniapp中IO模块(管理本地文件系统)的常用功能封装
339 1
|
3月前
|
Java
文件操作与IO(3) 文件内容的读写——数据流
文件操作与IO(3) 文件内容的读写——数据流
28 0