Linux阻塞IO(高级字符设备二)

阻塞IO属于同步 IO,阻塞IO在Linux内核中是非常常用的 IO 模型,所依赖的机制是等待队列。

一、等待队列介绍

在 Linux 驱动程序中,阻塞进程可以使用等待队列来实现。等待队列是内核实现阻塞和唤醒的内核机制,以双循环链表为基础结构,由链表头和链表项两部分组成,分别表示等待队列头和等待队列元素

等待队列头使用结构体 wait_queue_head_t 来表示,等待队列头是一个等待队列的头部,这个结构体定义在文件 include/linux/wait.h 里面,结构体内容如下所示:

c 复制代码
struct _wait_queue_head
{
	spinlock_t lock; //自旋锁
	struct list_head task_list //链表头
};
typefef struct _wait_queue_head wait_queue_head_t;

等待队列项使用结构体 wait_queue_t 来表示,等待队列项是等待队列元素,该结构体同样定义在文件 include/linux/wait.h 里面,结构体内容如下所示:

c 复制代码
struct _wait_queue
{
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct _wait_queue wait_queue_t;

二、等待队列 API

2.1、定义并初始化等待队列头

等待队列要想被使用,第一步就是对等待队列头进行初始化,有俩种办法如下所示:

2.1.1、方法一

使用 DECLARE_WAIT_QUEUE_HEAD 宏静态创建等待队列头,宏定义如下:

c 复制代码
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

参数 name 表示要定义的队列头名字。通常以全局变量的方式定义,如下所示:

c 复制代码
DECLARE_WAIT_QUEUE_HEAD(head);
2.1.1、方法二

使用 init_waitqueue_head 宏动态初始化等待队列头,宏定义如下:

c 复制代码
#define init_waitqueue_head(q) \
do { \
static struct lock_class_key __key; \
 __init_waitqueue_head((q), #q, &__key); \
 } while (0)

参数 q 表示需要初始化的队列头指针。使用宏定义如下所示:

c 复制代码
wait_queue_head_t head; //等待队列头
init_waitqueue_head(&head); //初始化等待队列头指针

2.2、创建等待队列项

一般使用宏 DECLARE_WAITQUEUE(name,tsk)给当前正在运行的进程创建并初始化一个等待队列项,宏定义如下:

c 复制代码
#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

第一个参数 name 是等待队列项的名字,第二个参数 tsk 表示此等待队列项属于哪个任务(进程),一般设置为 current。在 Linux 内核中 current 相当于一个全局变量,表示当前进程。

创建等待队列项如下所示:

c 复制代码
DECLARE_WAITQUEUE(wait,current); //给当前正在运行的进程创建一个名为wait的等待队列项。
add_wait_queue(wq,&wait); //将 wait 这个等待队列项加到wq 这个等待队列当中

2.3、添加/删除队列

当设备没有准备就绪(如没有可读数据)而需要进程阻塞的时候,就需要将进程对应的等待队列项添加到前面创建的等待队列中,只有添加到等待队列中以后进程才能进入休眠态。当设备可以访问时(如有可读数据),再将进程对应的等待队列项从等待队列中移除即可。

等待队列项添加队列函数如下所示

函数原型:

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

函数功能:

(通过等待队列头)向等待队列中添加队列项

参数含义:

wq_head 表示等待队列项要加入等待队列的等待队列头

wq_entry 表示要加入的等待队列项函数

返回值

等待队列项移除队列函数如下所示

函数原型:

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

函数功能:

要删除的等待队列项所处的等待队列头

参数含义:

第一个参数 q 表示等待队列项要加入等待队列的等待队列头

第二个参数 wait 表示要加入的等待队列项函数

返回值:

2.4、等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,使用如下所示的宏,是不可中断的阻塞等待。

c 复制代码
#define __wait_event(wq_head, condition) (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())

宏定义功能:

不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition 变成真,被内核唤醒。

参数含义:

第一个参数 wq: wait_queue_head_t 类型变量

第二个参数 condition : 等待条件,为假时才可以进入休眠。如果condition 为真,则不会休眠

除此之外,wait_event_interruptible 的宏是可中断的阻塞等待。

c 复制代码
#define __wait_event_interruptible(wq_head, condition) ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, schedule())

宏含义功能:

可中断的阻塞等待,让调用进程进入可中断的睡眠状态,直到condition 变成真被内核唤醒或信号打断唤醒。

参数含义:

第一个参数 wq :wait_queue_head_t 类型变量

第二个参数 condition :等待条件。为假时才可以进入休眠。如果condition 为真,则不会休眠。

wait_event_timeout() 宏也与 wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且 condition 为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回 0。

wait_event_interruptible_timeout() 宏与 wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回 ERESTARTSYS 错误码。

wait_event_interruptible_exclusive() 宏同样和   wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程

注意:调用的时要确认 condition 值是真还是假,如果调用 condition 为真,则不会休眠。

2.5、等待队列唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下俩个函数

c 复制代码
函数原型:
	wake_up(wait_queue_head_t *q)
函数功能:
	唤醒所有休眠进程
参数含义:
	q 表示要唤醒的等待队列的等待队列头
c 复制代码
函数原型:
	wake_up_interruptible(wait_queue_head_t *q)
函数功能:
	唤醒可中断的休眠进程
参数含义:
	q 表示要唤醒的等待队列的等待队列头

三、等待队列使用方法

步骤一:初始化等待队列头,并将条件置成假(condition=0)。

步骤二:在需要阻塞的地方调用 wait_event(),使进程进入休眠状态。

步骤三:当条件满足时,需要解除休眠,先将条件(condition=1),然后调用wake_up函数唤醒等待队列中的休眠进程。

四、阻塞IO驱动程序示例

4.1、阻塞IO驱动程序

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include  <linux/wait.h>


struct device_test{
   
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
};


struct  device_test dev1;  

DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");

    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;//将条件置1
    wake_up_interruptible(&read_wq); //并使用wake_up_interruptible唤醒等待队列中的休眠进程

    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    struct device_test *test_dev=(struct device_test *)file->private_data;

    wait_event_interruptible(read_wq,test_dev->flag); //可中断的阻塞等待,使进程进入休眠态

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

  
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

return 0;

 err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}




static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);

4.2、阻塞IO使用API要点

c 复制代码
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头
wake_up_interruptible(&read_wq); //使用wake_up_interruptible唤醒等待队列中的休眠进程
wait_event_interruptible(read_wq,test_dev->flag); //可中断的阻塞等待,使进程进入休眠态
相关推荐
hello_ world.21 分钟前
RHCA10NUMA
linux
神秘人X7071 小时前
Linux高效备份:rsync + inotify实时同步
linux·服务器·rsync
轻松Ai享生活1 小时前
一步步学习Linux initrd/initramfs
linux
轻松Ai享生活1 小时前
一步步深入学习Linux Process Scheduling
linux
绵绵细雨中的乡音3 小时前
网络基础知识
linux·网络
Peter·Pan爱编程3 小时前
Docker在Linux中安装与使用教程
linux·docker·eureka
kunge20134 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5494 小时前
DVWA中级
linux
Sadsvit5 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok5 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux