网站网页设计公司,家庭做网站,wordpress 上传安装,网站关键词格式文章目录 IO的概述IO模型的实现阻塞IO非阻塞IOIO多路复用信号驱动异步IO 编译与测试说明 IO的概述
io#xff0c;英文名称为inoput与output#xff0c;表示输入与输出。
在冯诺依曼结构计算机中#xff0c;计算机由 运算器、控制器、存储器、输入、输出五部分组成#xf… 文章目录 IO的概述IO模型的实现阻塞IO非阻塞IOIO多路复用信号驱动异步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 为真则不会休眠。
步骤三当条件满足时需要解除休眠先将条件(condition1),然后调用 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; // cdevstruct 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;iCREATE_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(i0;iCREATE_DEVICE_NUM;i){// 获取主从设备号if(i 0)numdev[i].dev_num;elsenumdev[i-1].dev_num1;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);// 初始化cdevdev[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(i0;iCREATE_DEVICE_NUM;i){if(i 0)numdev[i].dev_num;elsenumdev[i-1].dev_num1;printk(number:%d \r\n,num);//注销设备号unregister_chrdev_region(num, 1);//删除 cdevcdev_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; // cdevstruct 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 ARCHx86
export CROSS_COMPILE# 目标文件 此处
obj-m file.o
# ubuntu中内核源码所在的位置
KDIR : /lib/modules/5.4.0-150-generic/build
PWD ? $(shell pwd)# 执行编译操作
all: file.cmake -C $(KDIR) M$(PWD) modulesrm -rf *.cmd *.mod.c *.o *.symvers *.order *.mod# 执行删除操作
clean:make -C $(KDIR) M$(PWD) clean# 编译测试文件
test1:test.cgcc test.c -o target为了编译方便在上述Makefile文件中小编特意将测试文件test.c的编译命令加入Makefile文件中。