文章目录
- IO的概述
- IO模型的实现
- 阻塞IO
- 非阻塞IO
- IO多路复用
- 信号驱动
- 异步IO
- 编译与测试说明
IO的概述
io,英文名称为inoput
与output
,表示输入与输出。
在冯诺依曼结构计算机中,计算机由 运算器、控制器、存储器、输入、输出五部分组成,各个部分的数据流、指令流、控制流的大概流向如图所示:
在上图中,输入就是指鼠标键盘等设备通过计算机的输入设备向计算机内部输入信息,而输出设备是指控制器将计算机内部需要传递到计算机外部的数据通过输出设备传出,比如传出到显示器中。
一个完整的IO过程需要包含以下三步:
- 系统调用:用户空间的应用程序向内核发起IO请求;
- 数据准备:内核准备需要传递的数据,并且将IO设备的数据加载到内核缓冲区中;
- 拷贝数据:操作系统拷贝数据,将内核缓冲区数据拷贝到用户进程缓冲区中。
IO模型根据实现的功能可划分为:
IO模型的实现
阻塞IO
阻塞IO,是指用户程序发起一个系统调用后,如果内核中数据未准备好,那么用户程序就会一直阻塞,直到内核数据准备完成。
以用户程序发起read
为例,在用户发起读取系统数据后,如果内核数据未准备好,那么用户程序就会一直阻塞;反之,程序就可继续运行。如图示:
阻塞IO的实现
在linux驱动程序中,阻塞IO可使用等待队列实现。等待队列是linux内核实现阻塞与唤醒的内核机制,其以双向循环链表为基础结构,可借助下图来理解:
等待队列的使用方法
- 步骤一:初始化等待队列队头,并将唤醒条件设置成假。
初始化可使用宏定义DECLARE_WAIT_QUEUE_HEAD
静态初始化等待队列,其宏定义原型为:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
使用时直接传入队列名字即可。
初始化也可使用init_waitqueue_head
宏定义动态初始化等待队列,其宏定义原型为:
#define init_waitqueue_head(wq_head) \
do { \
static struct lock_class_key __key; \
\
__init_waitqueue_head((wq_head), #wq_head, &__key); \
} while (0)
使用时,先定义一个struct wait_queue_head
类型的变量,然后传入该宏定义用于初始化。
- 步骤二 :在需要阻塞的地方调用设置等待事件
wait_event
,使进程进入休眠。
不可中断等待wait_event
,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition 变成真,被内核唤醒。
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
第一个参数 wq: wait_queue_head_t 类型变量。第二个参数 condition : 等待条件,为假时才可以进入休眠。如果 condition 为真,则不会休眠
可中断等待wait_event_interruptible
,,让调用进程进入可中断的睡眠状态,直到 ondition 变成真被内核唤醒或信号打断唤醒。
#define wait_event_interruptible(wq_head, condition) \
({ \
int __ret = 0; \
might_sleep(); \
if (!(condition)) \
__ret = __wait_event_interruptible(wq_head, condition); \
__ret; \
})
参数wq :是指等待队列,是wait_queue_head_t 类型变量。参数condition :是等待条件。为假时才可以进入休眠。如果 condition 为真,则不会休眠。
- 步骤三:当条件满足时,需要解除休眠,先将条件(condition=1),然后调用
wake_up
或wake_up_interruptible
函数唤醒等待队列中的休眠进程。
使用方法为:直接向wake_up
或wake_up_interruptible
传入需要唤醒的等待队列即可。
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
x 表示要唤醒的等待队列的等待队列头。两者的区别就是wake_up
唤醒所有休眠进程,而wake_up_interruptible
只唤醒可中断的休眠进程。
其他函数
- 创建等待队列项
一般使用宏DECLARE_WAITQUEUE(name,tsk)
给当前正在运行的进程创建并初始化一个等待队列项,宏定义如下:
#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)
第一个参数 name 是等待队列项的名字,第二个参数 tsk 表示此等待队列项属于哪个任务(进程),一般设置为 current。在 Linux 内核中 current相当于一个表示当前进程的全局变量。
- 添加/删除队列
当设备没有准备就绪(如没有可读数据)而需要进程阻塞的时候,就需要将进程对应的等
待队列项添加到前面创建的等待队列中,只有添加到等待队列中以后进程才能进入休眠态。当
设备可以访问时(如有可读数据),再将进程对应的等待队列项从等待队列中移除即可。
添加队列项函数add_wait_queue
:
- 函数原型
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry
- **函数功能
向等待队列中添加队列项。- 参数含义
wq_head 表示等待队列项要加入等待队列的等待队列头。
wq_entry 表示要加入的等待队列项
移除队列项函数add_wait_queue
:
- 函数原型
void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
- **函数功能
向等待队列中删除队列项。- 参数含义
q表示等待队列项要加入等待队列的等待队列头。
wait表示要函数的等待队列项。
驱动编写
编写驱动完成:当应用程序读取数据时,若内核数据未准备完成,需要阻塞等待内核数据准备完成。
- 第一步:使用宏定义
DECLARE_WAIT_QUEUE_HEAD
静态初始化等待队列;
DECLARE_WAIT_QUEUE_HEAD(wqueue)
定义变量char myFlag
保存唤醒条件,并且初始化为唤醒条件为假。
- 第二步:在
read
中添加等待事件。
wait_event_interruptible(wqueue,test_dev->myFlag);
- 第三步:在
write
中设置唤醒条件,并且发出唤醒信号。
test_dev->myFlag = 1;
wake_up_interruptible(&wqueue);
完整驱动:
#include <linux/kernel.h>
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h> //注册设备节点的文件结构体
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h> // 系统错误文件
#include <linux/wait.h>
#define CREATE_DEVICE_NUM 1
#define KBUFFSIZE 32 // 缓冲区大小
// 设备结构体
struct device_test{
dev_t dev_num; //设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev cdev_test; // cdev
struct class *class; // 类
struct device *device; // 设备
char kbuff[KBUFFSIZE]; //缓冲区
char myFlag; // 标志位
};
struct device_test dev[CREATE_DEVICE_NUM]; // 定义设备
char *deviceName[] = {"mydevice1","mydevice2","mydevice3","mydevice4"}; // 设备名
DECLARE_WAIT_QUEUE_HEAD(wqueue); // 定义一个等待队列
// 打开设备函数
static int cdev_test_open(struct inode*inode,struct file*file)
{
// 设置次设备号
int i;
for(i = 0;i<CREATE_DEVICE_NUM;++i)
dev[i].minor = i;
// 将访问的设备设置成私有数据
file->private_data = container_of(inode->i_cdev,struct device_test,cdev_test);
printk("cdev_test_open is ok\n");
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(wqueue,test_dev->myFlag); // 等到标志位
if(copy_to_user(buf,test_dev->kbuff,strlen(test_dev->kbuff)) != 0)
{
printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
return -1;
}
printk("read data from kernel:%s\r\n",test_dev->kbuff);
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->kbuff,buf,size) != 0)
{
printk("copy_from_user error\r\n"); // 应用数据传输到内核错误
return -1;
}
printk("write data to kernel: %s\r\n",test_dev->kbuff);
// 唤醒等待队列
test_dev->myFlag = 1;
wake_up_interruptible(&wqueue);
return 0;
}
// 释放设备(关闭设备)
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release\r\n");
return 0;
}
/*设备操作函数,定义 file_operations 结构体类型的变量 cdev_test_fops*/
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,i,num;
printk("------------------------------------\r\n");
/*1 创建设备号*/
//动态分配设备号
ret = alloc_chrdev_region(&dev[0].dev_num, 0, CREATE_DEVICE_NUM, "alloc_name");
if (ret < 0)
{
printk("alloc_chrdev_region error\r\n");
goto errpr_chrdev;
}
for(i=0;i<CREATE_DEVICE_NUM;++i)
{
// 获取主从设备号
if(i == 0)
num=dev[i].dev_num;
else
num=dev[i-1].dev_num+1;
dev[i].major = MAJOR(num);
dev[i].minor = MINOR(num);
printk("number:%d major:%d minor:%d\r\n",num,dev[i].major,dev[i].minor);
// 初始化cdev
dev[i].cdev_test.owner = THIS_MODULE;
cdev_init(&dev[i].cdev_test,&cdev_test_fops);
// 添加cdev设备到内核
ret = cdev_add(&dev[i].cdev_test,num,1);
if(ret < 0)
{
printk("cdev_add error\r\n");
goto error_cdev_add;
}
// 创建类
dev[i].class = class_create(THIS_MODULE,deviceName[i]);
if(IS_ERR(dev[i].class))
{
printk("class_create error\r\n");
ret = PTR_ERR(dev[i].class);
goto error_class_create;
}
// 创建设备
dev[i].device = device_create(dev[i].class,NULL,num,NULL,deviceName[i]);
if(IS_ERR(dev[i].device))
{
printk("device_create error\r\n");
ret = PTR_ERR(dev[i].device);
goto error_device_create;
}
// 设置标志位
dev[i].myFlag = 0;
}
return 0;
// 创建设备失败
error_device_create:
device_destroy(dev[i].class, num);
// 创建类失败
error_class_create:
class_destroy(dev[i].class);
// 添加设备失败
error_cdev_add:
unregister_chrdev_region(num, 1);
// 字符设备添加出错
errpr_chrdev:
return ret;
}
// 注销字符设备
static void __exit chr_fops_exit(void) //驱动出口函数
{
int i,num;
for(i=0;i<CREATE_DEVICE_NUM;++i)
{
if(i == 0)
num=dev[i].dev_num;
else
num=dev[i-1].dev_num+1;
printk("number:%d \r\n",num);
//注销设备号
unregister_chrdev_region(num, 1);
//删除 cdev
cdev_del(&dev[i].cdev_test);
//删除设备
device_destroy(dev[i].class, num);
//删除类
class_destroy(dev[i].class);
}
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");
非阻塞IO
非阻塞IO,恰好是阻塞IO的对立面。使用非阻塞IO后,如果内核数据为准备我那成,内核不会阻塞并会返回一个err错误;若内核数据准备完成,就将该数据返回给用户程序。
这里还是以用户程序发起read
操作为例,在发起read
操作后,如果内核数据未准备好,那么用户程序不会阻塞,转而执行后面的程序,但是用户程序如果还想获取数据就需要间隔一定时间再次“询问”内核数据是否准备完成。这个过程用图表示如下:
使用非阻塞IO时,需要现在应用程序中以非阻塞 O_NONBLOCK
模式打开文件,即使用open(“/dev/xxx_dev”, O_RDWR | O_NONBLOCK)
打开设备文件,这样就可与内核交换数据时以非阻塞模式实现。
驱动编写
编写驱动完成:当应用程序读取数据时,若内核数据未准备完成,则立刻返回;反之就读取数据。
与阻塞IO相比,这里需要改动的就是read
函数,需要判断文件打开模式,若是O_NONBLOCK
模式,并且数据还未准备好,就立刻返回。判断语句可这样写:
if((file->f_flags & O_NONBLOCK) && (test_dev->myFlag == 0))
return -EAGAIN;
判断语句中的变量file
是 struct file
类型,其f_flags
域表示文件打开的模式,而变量test_dev
是自定义的 struct device_test
类型,其myFlag
域表示文件是否可被唤醒。
具体定义如下:
// 设备结构体
struct device_test{
dev_t dev_num; //设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev cdev_test; // cdev
struct class *class; // 类
struct device *device; // 设备
char kbuff[KBUFFSIZE]; //缓冲区
char myFlag; // 标志位
};
而后驱动的其他部分与阻塞IO一样即可。
IO多路复用
IO多路复用,是指同时监测若干个文件描述符是否可以执行IO操作的能力。
通常情况下使用 select()、poll()、epoll()函数实现 IO 多路复用。这里以 select 函数为例进行讲解,使用时可以对 select 传入多个描述符,并设置超时时间。当执行 select 的时候,系统会发起一个系统调用,内核会遍历检查传入的描述符是否有事件发生(如可读、可写事件)。如有,立即返回,否则进入睡眠状态,使进程进入阻塞状态,直到任何一个描述符事件产生后(或者等待超时)立刻返回。此时用户空间需要对全部描述符进行遍历,以确认具体是哪个发生了事件,这样就能使用一个进程对多个 IO 进行管理,如下图所示:
信号驱动
信号驱动,是指当信号与处理函数绑定后,一旦系统发出特定信号就会触发处理函数。
例如,在用户进程中,可将某个信号与处理函数绑定,而处理函数的主要功能是read
,一旦系统发出特定信号,处理函数就可读取数据,如图:
异步IO
异步IO,是指用户进程访问内核数据时,如果内核数据未准备就绪,用户进程不会阻塞;反之,就直接将数据从内核空间拷贝到用户空间缓冲区中,然后再执行定义好的回调函数接收数据。如图所示:
编译与测试说明
由于本文操作未涉及任何硬件,因此我们可尝试直接使用客户机ubuntu来测试。
为了实现在客户机ubuntu中测试,首先我们需要更改Makefile
文件,因为之前都是使用的交叉编译环境,如果不改就无法在客户机ubuntu中运行,只能在对应开发板中运行。
更改Makefile
需要注意:
- 首先,更改
ARCH
所指定的平台;
- 其次,更改
CROSS_COMPILE
指定的编译工具,这里可直接接空,后续编译时会加上gcc
。 - 最后,需要更改内核源码位置。
可先试用命令uname -a
查看内核版本。
然后再进入/lib/modules
下指定版本的linux内核中。
最后需要将内核路径指定到上步骤选中的内核版本下的built
目录下。
#!/bin/bash
# 环境变量
export ARCH=x86
export CROSS_COMPILE=
# 目标文件 此处
obj-m += file.o
# ubuntu中内核源码所在的位置
KDIR := /lib/modules/5.4.0-150-generic/build
PWD ?= $(shell pwd)
# 执行编译操作
all: file.c
make -C $(KDIR) M=$(PWD) modules
rm -rf *.cmd *.mod.c *.o *.symvers *.order *.mod
# 执行删除操作
clean:
make -C $(KDIR) M=$(PWD) clean
# 编译测试文件
test1:test.c
gcc test.c -o target
为了编译方便,在上述Makefile
文件中,小编特意将测试文件test.c
的编译命令加入Makefile
文件中。