典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)
Posted _BitterSweet
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)相关的知识,希望对你有一定的参考价值。
1.典型IO模型
1.1 什么是IO模型
-
我们在执行一个程序时,这个程序要求外设进行输入或者输出,内核在处理这个数据时,都在做什么?
-
第一步:等待IO就绪,已经准备好需要的资源了,可以开始操作
-
第二步:将数据拷贝到缓冲区当中(接受缓冲区&发送缓冲区)
1.2 阻塞IO模型
-
资源不可用的情况下,IO请求一直被阻塞,直到资源可以用
-
以钓鱼举例子:钓鱼的时候,将鱼钩抛入水中
(发起IO调用)
,一直盯着鱼漂(等待IO就绪)
,鱼儿咬钩(拷贝数据到缓冲区)
,将鱼儿钓上来(IO调用返回)
-
阻塞IO的特点:
1.发起IO调用之后,等待的时间取决于内核
2.在等待的过程中,执行流是被挂起的(对CPU的利用率低)
3.在IO就绪到拷贝数据之间,实时性很高(响应快)
1.3 非阻塞IO模型
- 我们在使用程序是轮询的方式,一直询问内核数据有没有准备好
- 资源不可用的时候,IO请求不会被阻塞,而是直接返回,返回当前资源不可用(EBUSY)
- 以钓鱼举例子:钓鱼的时候,将鱼钩抛入水中,看一眼鱼漂,鱼漂如果没动,则看一会手机,再看一眼鱼漂,鱼漂没动,再看一会手机,如此往复,直到鱼儿咬钩,将鱼钓上来
- 非阻塞IO的特点:
1.非阻塞IO对CPU的利用率比阻塞IO高 ``2.代码结构相对复杂
3.需要搭配循环使用,直到IO请求完成
4.IO准备就绪到数据拷贝之间,实时性不高(因为可能在你看手机的过程中,鱼儿咬钩了)
区别:
在资源不可用的情况下,就看系统的调用是否立即返回
- 立即返回:非阻塞IO
- 没有立即返回:阻塞IO
1.4 信号驱动IO模型
- 信号驱动IO:内核态中把数据准备完成之后,使用之前定义的SIGIO信号通知应用程序进行IO操作
流程:
1.自定义一个IO信号(SIGIO)的处理函数,再处理函数当中发起IO调用
2.程序收到一个IO信号(SIGIO),内核就会调用自定义的处理函数,在自定义的处理函数中发起IO调用
- 以钓鱼举例子:先在鱼杆上绑一个铃铛,将鱼钩抛入水中(接下来玩手机),鱼儿咬钩之后,铃铛就会响,将鱼儿钓上来
- 信号驱动IO的特点:
1.IO准备就绪到拷贝数据之间,实时性更强
2.代码更加复杂,流程控制更加困难(引入了信号)
3.不需要重复发起IO调用,但是需要在代码当中增加自定义信号的逻辑
1.5 异步IO模型
- 异步IO:当内核中把程序拷贝完成后,再通知程序(信号驱动就是告诉应用程序何时开始拷贝数据)
流程:
1.自定义信号(SIGIO)处理函数,用来通知数据拷贝完成
2.发起一个异步IO调用,并且异步IO调用直接返回
3.异步IO调用返回之后,执行流可以执行用户代码
(由操作系统内核来等待IO就绪和数据拷贝)
4.当数据拷贝完成之后,内核通过信号来告知调用者
- 异步IO调用:异步IO函数接口当中一般会设置一个函数指针,来保存IO调用完成之后,通知调用者的回调函数
小结:
1.同步IO:
当程序发出一个调用的时候,在没有得到结果之前,这个调用就不会返回。当调用返回的时候,就一定是得到了一个结果
2.异步IO:
当程序发出一个调用,这个调用就直接返回了,所以没有返回结果。等到内核中完成了拷贝的操作,再通过一些方式来通知调用者(信号),或者通过回调函数来处理这个调用
3.异步IO最大的特点:用户不需要拷贝数据了,拷贝数据由操作系统内核来完成,完成拷贝之后,通知调用
1.6 多路转接IO模型
- IO多路转接:可以完成大量文件描述符的监控,监控的事件:
可读事件
,可写事件
,异常事件
- 监控文件描述符:哪个文件描述符准备就绪,就处理哪一个文件描述符
- 好处:避免了其他进程对没有就绪的文件描述符进行操作,从而陷入阻塞状态
1.6.1 select
- 作用:用程序来对多个文件描述符的状态进行监控,如果有描述符准备就绪,就返回该描述符,让用户对这个描述符进行操作
- 流程:
Ⅰ
.将用于关心的文件描述符拷贝到内核当中,内核来进行监控Ⅱ
.如何内核监控到某个文件描述符就绪,则返回该描述符Ⅲ
.用户对返回的描述符进行操作
- 函数接口
1.nfds
:取值为监控最大的文件描述符数值+1(最大的数值为1024),轮询的范围由nfds
来决定
2.fd_set
:本质是一个结构体,结构体内部是一个fds_bits
数组,可以理解为一个1024位的位图,对应1024个文件描述符
_FD_SIZE:#define _FD_SETSIZE 1024
_NFDBITS:#define _NFDBITS (8*(int)sizeof(_fd_mask))
数组的元素个数:1024 / 8*(int)sizeof(_fd_mask)
_fd_mask:typedef long int_fd mask
数组当中比特位的个数:
(1024/8 * (int)sizeof(_fd_mask)) * 8 *(int)sizeof(_fd_mask)) = 1024
总结:select的事件集合当中总共有1024个比特位,取决于宏 _FD_SETSIZE 这个宏的大小
3.举个例子
4.fd_set集合中提供了4个函数
//从事件集合当中删除文件描述符fd,描述符对应的比特位置0
void FD_CLR(int fd, fd_set *set);
//判断fd描述符是否在set集合当中;
//返回值:0表示没有在集合当中,非0表示在集合当中
int FD_ISSET(int fd, fd_set *set);
//将文件描述符fd设置到set集合当中,描述符对应的比特位置1
void FD_SET(int fd, fd_set *set);
//清空事件集合,将所有的比特位置0
void FD_ZERO(fd_set *set);
5.readfds:
可读文件描述符的结合 ;writefds:
可写文件描述符的集合;exceptfds:
异常文件描述符的集合
6.timeout:
超时时间; tv_sec:秒
tv_usec:微秒
- timeout == NULL
阻塞监控
- timeout == 0
非阻塞监控
- timeout > 0
等待超时时间监控
7.函数返回值
1.6.1.1 select优缺点总结
- 优点
1.select遵循的是POSIX标准,可以
跨平台移植
2.select的超时时间
可以精确到微秒
- 缺点
1.select采用的是轮询遍历,监控的效率会随着文件描述符的增多而下降
2.select所能监控的文件描述符是有上限的(1024
),取决于内核FD_SETSIZE宏的值
3.select监控文件描述符的时候,需要将集合拷贝到内核当中,select发现有事件就绪之后,同时需要将事件集合从内核拷贝到用户空间,效率也会受影响
4.select在返回的时候,会将未就绪的文件描述符从集合当中去除掉
,导致下一次监控的时候,如果还需要监控去除掉的文件描述符,就得重新添加
5.select无法直接查看那个文件描述符已经就绪
,需要手动通过返回事件的集合去判断
6.select的超时机制
,如果在循环判断的情况下,每次调用之前都需要更新一下时间。因为在计时的时候,这个结构体中的时间是会变的
1.6.2 poll
poll和select相比,跨平台移植性不如select,poll函数只能在Linux环境下使用,也采用轮询遍历
与select相比,改进的点:
1.不限制监控的文件描述符的个数
2.select使用的是事件集合方式,poll采用的是事件结构
(文件描述符对应一个事件结构
,这个结构中有两个事件
,一个是要监控的文件描述符
,另一个是这个文件描述符所对应的事件
)
1.6.2.1 函数接口
fds:
事件结构数组
nfds:
事件结构数组中有效的元素个数
timeout:
超时时间
fd:
关心的文件描述符
是什么
events:关心的文件描述符产生的事件是什么,如果关心多个事件,可以将多个事件按照按位或的方式连接起来
revents:当关心的文件描述符产生对应的关心的事件时,返回给调用者发生的事件(每次监控的时候,就会被初始化为空)
1.6.2.2 编程实例
#include<stdio.h>
#include<unistd.h>
#include<poll.h>
#include<iostream>
int main()
{
//创建一个struct pollfd结构体,关心0号文件描述符,标准输入
//监视可读事件
struct pollfd fd_arr[10];
fd_arr[0].fd = 0;
fd_arr[0].events = POLLIN;
//轮询遍历
while(1)
{
int ret = poll(fd_arr, 1 , 1000);
if(ret < 0) //poll出错
{
perror("poll error");
return -1;
}
else if(ret == 0)//监控超时
{
printf("poll timeout\\n");
continue;
}
for(int i = 0; i < ret; i++)
{
if(fd_arr[i].revents == POLLIN)//当发生的事件可读
{
//读取数据
char buf[1024] = {0};
read(fd_arr[i].fd, buf, sizeof(buf) - 1);
printf("buf: %s\\n",buf);
}
}
}
return 0;
}
运行结果:
1.6.2.3 poll的优缺点
优点:
- poll采用了事件结构的方式,简化了代码的编写
- poll不限制文件描述符的个数
- 不需要再二次监控的时候重新添加文件描述符
缺点:
- poll采用轮询遍历事件结构数组的方式,随着文件描述符增多,性能下降
- poll不支持平台
- poll也没有告诉用户哪一个具体的文件描述符就绪了,需要自己判断
- poll也需要将事件结构拷贝到内核,从内核再拷贝到用户空间
1.6.3 epoll(目前公认的在Linux系统下监控性能最高)
1.6.3.1 epoll(创建句柄)接口
- 创建
epoll
的操作句柄
#include <sys/epoll.h>
int epoll_create(int size);
size:本来的含义是定义epoll最大能够监控文件描述符的个数,内核在2.6.8之后就启弃用了,现在采用动态内存开辟的方式,来进行扩容,size的值大于0,在使用完之后,必须使用close进行关闭
返回值:返回epoll的操作句柄
内核观点:在内核当中就是创建一个struct eventpoll结构体,在这个结构体当中有两个重要的变量,一个是红黑树,一个是双向链表
epoll的操作句柄其实就是用来找到struct eventpoll结构体,从而对结构体当中的变量进行操作
1.6.3.2 epoll(添加/删除/修改事件结构)接口
向内核维护的红黑树当中添加/删除/修改事件结构
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
epfd:epoll_create的返回值,epoll的操作句柄
op:想让epoll_ctl做的事
fd:告诉epoll函数,我们关心的文件描述符
event:epoll的事件结构,类型是 struct epoll_event结构体
typedef union epoll_data
{
*ptr和fd两者只能选一个*
void *ptr; --->如果使用ptr,需要包含fd的内容
int fd; --->用户关心的文件描述符,可以当做文件描述符中的事件就绪之后,返回给我们时查看;其取值为文件描述符的数值
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
想让文件描述符关心的事件集合:EPOLLIN可读事件 EPOLLOUT可写事件
uint32_t events; /* Epoll events */
epoll_data类型的联合结构体
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
1.6.3.3 epoll监控接口
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epfd:epoll操作句柄
events:事件结构数组,作为出参,返回就绪的事件结构,一个文件描述符对应一个事件结构
maxevents:表示最大能接受多少个事件结构
timeout:超时时间
1.6.3.4 epoll的工作原理
1.6.3.5 epoll模拟代码
#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<string.h>
#include<stdlib.h>
/*
* 1.创建epoll操作句柄
* 2.添加事件结构
* 2.1准备事件结构
* 2.2关心的事件及文件描述符
* 3.监控
* 3.1阻塞
* 3.2非阻塞
* 3.3带有超时事件
* 4.判断epoll_wait的返回值
* 5.执行相应的操作
* */
int main()
{
int epfd = epoll_create(5);
if(epfd < 0)
{
perror("create error");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
while(1)
{
struct epoll_event arr[10];
memset(arr, '\\0', sizeof(struct epoll_event) * 10);
int ret = epoll_wait(epfd, arr, 10, 0);
if(ret < 0)
{
perror("epoll_wait error");
return 0;
}
else if(ret == 0)
{
sleep(1);
printf("timeout..\\n");
continue;
}
for(int i = 0; i < 10; i++)
{
if(arr[i].events == EPOLLIN)
{
char buf[1024] = {0};
read(arr[i].data.fd, buf,sizeof(buf) - 1);
printf("buf:%s", buf);
}
}
}
return 0;
}
1.6.3.6 工作方式:水平触发 LT模式(EPOLLLT)
- epoll的默认工作方式,
select和poll
都是水平触发方式 - 在LT模式当中,当epoll中检测到了等待触发事件就绪后,可以不立即进行处理,而是只处理一部分。等到第二次调用epoll_wait函数的时候,可以接着刚才没有处理完的数据进行操作(
支持阻塞读写和非阻塞读写)
- 可读事件:只要发送缓冲区当中数据大于低水位标记(1字节),就会一直触发可读事件就绪,直到接收缓冲区当中没有数据可读(接收缓冲区当中的数据低于低水位标记)
- 可写事件:只要发送缓冲区当中的空间大小大于低水位标记(1字节),就会一直触发可写事件就绪,直到缓冲区当中没有空间可写
1.6.3.7 工作方式:边缘触发 ET模式(EPOLLET)
- EPOLLET只有epoll才有
- 可读事件:只有新的数据到来的时候,才会触发可读,否则通知一次就不通知了(
每次到来一个新的数据,只会通知一次,如果应用程序没有将接收缓冲区当中的数据读走或者读完,也不会通知,直到新的数据到来,才会触发可读事件。如果出发可读事件,尽量将数据读完
) - 对于ET模式而言,如果就绪事件产生,一定要把握好机会,对于可读事件,将数据读完,对于可写事件,将数据写完
如何在代码中体现ET
设置文件描述符对应的事件结构的时候,只需要在事件结构当中关心的事件变量按位或上 EPOLLET 就可以了
struct epoll_enent ev;
ev.events = EPOLLIN | EPOLLET;
以上是关于典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)的主要内容,如果未能解决你的问题,请参考以下文章