文章目录
一、IO多路复用简介
允许单个线程或者进程管理多个网络连接或者文件描述符
可以通过以下几种机制实现:
- select(表,本质就是一个结构体,成员只有一个数组)
① select监听的最大文件描述符个数是1024
② select会有清空表的过程,需要反复构造表,反复拷贝表,效率比较低
③ select对应的进程从休眠态被唤醒后,需要再次遍历文件描述符表找到就绪的文件描述符,效率比较低 - poll(链表)
① poll监听的文件描述符没有个数限制
② poll没有清空表的过程,不需要反复构造表,反复拷贝表,效率比较高
③ poll对应的进程从休眠态被唤醒后,需要再次遍历文件描述符表,效率比较低 - epoll(红黑树+双向链表:红黑树保存要监听的文件描述符,双向链表保存就绪的文件描述符)
① epoll监听的文件描述符没有个数限制
② epoll没有清空表的过程,不需要反复构造表,反复拷贝表,效率比较高
③ epoll对应的进程从休眠态被唤醒后,将就绪的文件描述符直接放到双向链表中,不需要再次遍历文件描述符表,能直接获得就绪的文件描述符,效率比较高
二、应用层相关API
(一)select
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:
允许一个进程或线程管理多个文件描述符
参数:
nfds:监听的文件描述符数量,即最大的文件描述符+1
readfds:监听读表; 没有可以传NULL
writefds:监听写表; 没有可以传NULL
exceptfds:监听其他表; 没有可以传NULL
timeout:超时时间; NULL表示会一直阻塞,直到有文件描述符就绪
返回值:
成功返回三个集合一共就绪的文件描述符数量
失败返回-1,重置错误码
//判断fd是否存在于表中
int FD_ISSET(int fd, fd_set *set);
//向表中添加成员
void FD_SET(int fd, fd_set *set);
//删除表中的成员
void FD_CLR(int fd, fd_set *set);
//清空表
void FD_ZERO(fd_set *set);
补充:fd_set
该表本质就是一个结构体,结构体中只有一个unsigned long类型(4个字节,即32位)的数组成员,该数组有32个元素,即4*8*32=1024
- 注:进程会记录自己打开的文件描述符个数,每个进程维护自己的fd,进程与进程之间的fd互不影响
(二)poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
允许程序同时监视多个文件描述符,查看其是否发生读、写或者错误等事件
参数:
fds:指向pollfd结构数组的指针。每个pollfd结构代表一个要监视的文件描述符及其感兴趣的事件。
nfds:监视的文件描述符数量,即最大的文件描述符加1
timeout:超时时间
返回值:
成功返回revents为非0的结构体数量
超时没有就绪文件描述符,返回0
失败返回-1,重置错误码
struct pollfd {
int fd; // 文件描述符
short events; // 感兴趣的事件
short revents; // 返回的事件
};
成员:
fd:要监视的文件描述符。
events:通过位掩码指定的感兴趣的事件类型,如POLLIN(有数据可读)、POLLOUT(可以写入数据)等。
revents:在调用poll后,由系统填充,表示实际发生的事件。
nfds:fds数组中的元素数量,即要监视的文件描述符的数量。这个值通常是fds数组中最后一个元素的索引加1。
timeout:指定等待事件的超时时间(毫秒)。
如果timeout为-1,则poll会无限期地等待,直到某个事件发生。
如果timeout为0,则poll会立即返回,不阻塞。
如果timeout为正数,则poll会等待指定的毫秒数,如果在这段时间内没有事件发生,则返回
(三)epoll
更适用于高并发场景,epoll使用红黑树作为底层数据结构来维护注册的文件描述符集合,实现高效的检索和管理
#include <sys/epoll.h>
int epoll_create(int size);
功能:
创建一个epoll实例
参数:
@size:这是一个提示性的参数,表示epoll实例可以监视的文件描述符的数量上限。Linux 2.6.8及以后的版本中,这个参数被忽略,但仍然需要传递一个大于0的值。
返回值:
成功返回epfd;
失败返回-1,重置错误码
备注:
这个函数本质就是创建一个红黑树,用户通过epfd拿到红黑树,这个epfd也会占用一个文件描述符fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
epoll的控制操作
参数:
@epfd:由epoll_create返回的文件描述符,表示epoll实例
@op:操作方式
EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 删除
@fd:被操作的文件描述符
@event:事件结构体,包含事件类型
struct epoll_event{
uin32_t events; //events字段表示注册的事件类型(如EPOLLIN、EPOLLOUT等)
epoll_data_t data; //data字段用于存储用户数据
}
返回值:
成功返回0;
失败返回-1,置位错误码
int epoll_wait(int epfd, struct epoll_event *revents, int maxevents, int timeout);
功能:
阻塞等待文件描述符就绪
参数:
@epfd epoll实例
@revents 用于存放事件信息的数组,每个数组元素都是epoll_event类型的结构体。这个数组用于接收就绪的文件描述符及其对应的事件信息。
@maxevents events数组的大小,即最多可以等待多少个事件
@timeout 超时时间
= 0 :立即返回
> 0 :毫秒超时
=-1 :忽略超时,永久阻塞,直到有fd就绪
返回值:
成功,返回就绪的文件描述符的个数;
超时,返回0,代表超时,没有文件描述符就绪
失败,返回-1,重置错误码
三、内核中IO多路复用的实现机制
US:
---
select(fd2 + 1, &rfds, NULL, NULL, NULL)
-----------------------------------------------------------
KS: VFS:
---
进入到内核层之后会调用到syscall函数,进行宏定义替换后会得到sys_select函数
long sys_select(int n, fd_set __user * inp, fd_set __user * outp, fd_set __user * exp, struct __kernel_old_timeval __user * tvp))
↓
kern_select(n, inp, outp, exp, tvp);
↓
ret = core_sys_select(n, inp, outp, exp, to);
//n--文件描述符的数量;inp--读表;outp--写表;exp--其他表;
1.检查n(文件描述符的值)是否合法
2.在内核空间申请6张表的内存,前三张表用于保存用户的读,写,其他的表(copy_from_user)
后三张表用于保存就绪的文件描述符(初值都是0)
3.调用do_select检查文件描述符是否就绪
3.1遍历文件描述符:
mask1 = fds.in==>fd1==>fd_array[fd1]==>file*==>f_op==>poll(file,wait);
mask2 = fds.in==>fd2==>fd_array[fd2]==>file*==>f_op==>poll(file,wait);
//......
3.2上述的mask如果都为0,说明所有的数据都没就绪,进程会继续休眠。
3.3如果有一个或者多个文件描述符就绪,可以将等待队列唤醒,但是需要再次遍历文件描述符
4.将就绪的文件描述符拷贝到用户空间
------------------------------------------------------------------------
KS:DRIVER 驱动层:
__poll_t mycdev_poll(struct file *file, struct poll_table_struct *wait)
{
// 1.该函数本身不会阻塞,作用是向上提交等待队列头,构造等待队列;是在虚拟文件层实现阻塞
poll_wait(file,&wq_head,wait);
// 2.根据条件返回结果
//如果条件成立的话返回EPOLLOUT或者EPOLLIN,否则就返回0,表示没有就绪
if(condition){
return EPOLLIN;
}
return 0;
}
- 注:current 是一个在内核中定义的宏,指当前进程的结构体task_struct
- 注:
EPOLLIN ---- 读数据就绪,可读;包括普通数据和带优先级的数据
EPOLLOUT ---- 写数据就绪,可写
四、使用示例
(一)功能分析
实现一个进程同时监听mycdev输入和键盘输入,当任一文件描述符就绪时,将数据输出
补充: 如何找到键盘对应的input/eventX
第一步:输入以下命令:
sudo hexdump event1
- 注:hexdump 是linux中使用的命令行工具,用于以十六进制和可打印字符的形式显示文件内容或标准输入流的数据。
第二步:敲击键盘,查看是否会输出数据。
如果使用的虚拟机,此时需要切换到虚拟机下按按键,否则是使用了windows的键盘,敲击键盘没有反应
输入类设备上报的都是 input_event 结构体:
因此打印输入的键盘类事件的数据时,需要以input_event结构体去接收输入数据
//以键盘为例
#include <linux/input.h>
struct input_event{
struct timeval time; //事件发生的时间戳
__u16 type; //事件类型 EV_KEY 键盘类事件
__u16 code; //事件的代码 代表哪个键 KEY_Q 16; KEY_ENTER 28
__s32 value //事件的状态,按下1,松开0,长按2
}
(二)代码示例
此处仅展示部分代码
1. 应用层 test.c
#include <my_head.h>
#include <sys/epoll.h>
#include <linux/input.h>
int main(int argc, char const *argv[])
{
int ret,i;
char buf[128];
struct input_event in_ev;
//打开键盘J
int fd_key = open("/dev/input/event1",O_RDWR);
if(fd_key < 0)
ERR_LOG("open key error\n");
//打开cdev
int fd_cdev = open("/dev/mycdev0",O_RDWR);
if(fd_cdev < 0)
ERR_LOG("open cdev error\n");
/******epoll******/
struct epoll_event ep_event;
struct epoll_event revents[5];
//1. 创建epoll实例
int epfd = epoll_create(10);
if(epfd < 0)
ERR_LOG("epfd create error\n");
//2. 将两个文件描述符添加到epoll实例中
//2.1 填充epoll_event结构体
ep_event.events = EPOLLIN;
ep_event.data.fd = fd_key;
//2.2 向epoll实例中添加文件描述符
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd_key,&ep_event);
if(ret)
ERR_LOG("epoll_ctl key error\n");
ep_event.data.fd = fd_cdev;
ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fd_cdev,&ep_event);
if(ret)
ERR_LOG("epoll_ctl key error\n");
while(1){
//3. 等待文件描述符就绪
ret = epoll_wait(epfd,revents,5,-1);
for(i=0;i<ret;i++){
if(revents[i].events & EPOLLIN){
if(revents[i].data.fd == fd_cdev){
read(revents[i].data.fd,buf,sizeof(buf));
printf("buf = [%s]\n",buf);
}
if(revents[i].data.fd == fd_key){
read(revents[i].data.fd,&in_ev,sizeof(in_ev));
printf("buf:type = [%d],code = [%d],value = [%d]\n",in_ev.type,in_ev.code,in_ev.value);
read(revents[i].data.fd,&in_ev,sizeof(in_ev));
printf("buf:type = [%d],code = [%d],value = [%d]\n",in_ev.type,in_ev.code,in_ev.value);
read(revents[i].data.fd,&in_ev,sizeof(in_ev));
printf("buf:type = [%d],code = [%d],value = [%d]\n",in_ev.type,in_ev.code,in_ev.value);
}
}
}
}
return 0;
}
2. 内核驱动层 mycdev.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
/***需要包含此头文件***/
#include <linux/poll.h>
#include "mychr.h"
#define MYCNAME "mcdev"
int major=0; //主设备号
int minor=0; //次设备号
char kbuf[128]; //内核缓冲区
struct class *mycls; //提交的目录
struct device *mydev; //提交的设备信息
struct cdev* mycdev; //注册的字符设备
//定义等待队列头
wait_queue_head_t myqueue;
int condition=0; //唤醒条件
int myled_open(struct inode *inode, struct file *file){
pr_info("%s:%d\n",__FUNCTION__,__LINE__);
return 0;
}
int myled_close(struct inode *inode, struct file *file){
pr_info("%s:%d\n",__FUNCTION__,__LINE__);
return 0;
}
ssize_t myled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset){
int ret;
//检查size是否合法
if(size > sizeof(kbuf))
size = sizeof(kbuf);
/***read函数中无需继续判断是否为阻塞,
* 将依赖于poll来实现监听数据是否就绪***/
ret = copy_to_user(ubuf, kbuf, sizeof(kbuf));
if(ret){
pr_err("copy_to_user error\n");
return -EIO;
}
condition = 0; //条件置0
return size;
}
ssize_t myled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset){
int ret;
//检查size是否合法
if(size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_from_user(kbuf,ubuf,size);
if(ret){
pr_err("copy_from_user error\n");
return -EIO;
}
/***仍然采用write写入数据后将条件置1的方式来唤醒自己的设备文件***/
condition = 1; //条件置1
wake_up_interruptible(&myqueue); //唤醒等待队列
return size;
}
unsigned int myled_poll(struct file *file, struct poll_table_struct *wait){
poll_wait(file,&myqueue,wait);
if(condition){
return EPOLLIN;
}
return 0;
}
const struct file_operations myfops = {
.open=myled_open,
.read=myled_read,
.write=myled_write,
.poll=myled_poll,
.release=myled_close,
};
static int __init mychr_init(void){
int ret,i;
dev_t mydevno;
pr_info("%s:%d\n",__FUNCTION__,__LINE__);
//1.分配cdev对象
mycdev = cdev_alloc();
if(mycdev == NULL){
pr_err("cdev_alloc error\n");
ret = -ENOMEM;
goto err0;
}
pr_info("cdev_alloc success\n");
//2.填充cdev结构体的成员
cdev_init(mycdev,&myfops);
pr_info("cdev_init success\n");
//3.申请设备号
if(!major){//动态申请
ret = alloc_chrdev_region(&mydevno,0,3,MYCNAME);
if(ret){
pr_err("alloc_chrdev_region error\n");
goto err1;
}
major=MAJOR(mydevno);
minor=MINOR(mydevno);
}else{//静态分配
ret = register_chrdev_region(MKDEV(major,minor),3,MYCNAME);
if(ret){
pr_err("register_chrdev_region error\n");
goto err1;
}
}
pr_info("alloc_chrdev_region success\n");
//4.注册
ret = cdev_add(mycdev,MKDEV(major,minor),3);
if(ret){
pr_err("cdev_add error\n");
goto err2;
}
pr_info("cdev_alloc success\n");
/***自动创建设备节点***/
mycls = class_create(THIS_MODULE,MYCNAME);
if(IS_ERR(mycls)){
pr_err("class_create error\n");
ret = -ENOMEM;
goto err3;
}
for(i=0;i<3;i++){
mydev = device_create(mycls,NULL,MKDEV(major,minor+i),NULL,"%s%d",MYCNAME,i);
if(IS_ERR(mydev)){
pr_err("device_create error\n");
ret = -ENOMEM;
goto err4;
}
}
pr_info("devices create success\n");
//初始化等待队列头
init_waitqueue_head(&myqueue);
return 0;
err4:
for(--i;i>=0;i--){
device_destroy(mycls,MKDEV(major,minor+i));
}
class_destroy(mycls);
err3:
cdev_del(mycdev);
err2:
unregister_chrdev_region(MKDEV(major,minor),3);
err1:
kfree(mycdev);
err0:
return ret;
}
static void __exit mychr_exit(void){
int i;
pr_info("%s:%d\n",__FUNCTION__,__LINE__);
for(i=0;i<3;i++){
device_destroy(mycls,MKDEV(major,minor+i));
} //删除设备节点
class_destroy(mycls); //删除目录
cdev_del(mycdev); //注销字符设备
unregister_chrdev_region(MKDEV(major,minor),3); //注销设备号
kfree(mycdev); //释放mycdev的内存空间
}
module_init(mychr_init);
module_exit(mychr_exit);
MODULE_LICENSE("GPL");
(三)现象
安装驱动后,在ubuntu中按下键盘会打印出以下数据,如果此时向/dev/myled0中写入数据,写入的数据就会打印
type: