一、五种典型的IO模型
- 阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路转接模型。
- IO的过程:
发起IO调用,等待IO条件就绪,然后将数据拷贝到缓冲区中进行处理——等待/拷贝。
1. 阻塞IO:
- 为了完成IO,发起IO调用,此时若不具备调用条件,则一直等待,直到条件满足,完成IO调用。
- 流程很简单,一个IO完毕后才能进行下一个IO调用,对于资源未能完全充分利用,大部分时间处于等待状态。
2.非堵塞IO:
- 为了完成IO,发起调用,若当前不具备IO条件,则立即报错返回。(通常返回进行其他操作后再次发起IO调用)。
- 相对于阻塞IO来说,资源利用更加充分,但IO操作不具备实时性。
3.信号驱动IO:
- 定义信号IO处理方式,在处理方式中进行IO操作,IO就绪信号去通知进程,在IO就绪时去进行IO调用。
- 对于信号驱动IO来说,IO更加实时性,对资源利用率更加充分,但调用过程很复杂。
4. 异步IO:
- 通过异步IO调用告诉操作系统,IO哪些数据拷贝到哪里,等待过程于拷贝过程都有操作系统来完成。
- 对资源利用更大家充分,流程更加复杂。
注意:
①阻塞:为了完成一个功能,发起调用,若当前不具备完成条件则一直等待。
②非阻塞:为了完成一个功能,发起调用,若当前不具备完成条件则直接报错返回。
③阻塞与非阻塞区别:发起调用在不具备完成调用的情况下是否等待。
④同步:处理流程,顺序处理,一个完成了之后再完成另一个,所有功能都有进程自己进行完成。
⑤异步:处理流程中,顺序不定,因为功能都由操作系统完成。
⑥异步阻塞:功能由别人完成,调用中等待别人完成。
⑦异步非阻塞:功能有别人完成,调用是立即返回的。
二、多路转接IO
- 对大量的描述符集中进行IO事件监控,可以告诉程序员/进程现在有哪些描述符就绪了那些事件,此时程序员/进程就可以直接对就绪的对应事件的描述符进行响应即可,避免对没有就绪的操作符进行IO操作而导致效率降低/程序流程堵塞。
- IO事件:可读事件,可写事件,异常事件;
- 多路转接IO模型:select/poll/epoll;用于对描述符进行监控。
1.select模型:
(1)操作流程:
①程序员定义某个事件的描述符集合(可读事件的描述符集合/可写事件的描述符集合/异常事件的描述符集合),初始化清空集合,对哪个描述符关心什么事件,就将这个描述符添加到对应事件的描述符集合中。
②将集合拷贝到内核中进行监控,监控的原理是轮询遍历判断。可读事件的就绪:接收缓冲区中数据的大小大于低水位标记(量化标准–通常默认为一个字节);可写事件的就绪:发送缓冲区剩余的空间大小大于低水位标记(量化标准–通常默认为一个字节);异常事件的就绪:描述符是否产生来了某个异常。
③监控的调用返回,表示监控出错/有描述符就绪/监控等待超时;并且调用返回的时候,将事件监控的描述符集合中的未就绪描述符从集合中移除—(集合中仅仅保留就绪的描述符)。因为返回的时候修改了集合,所以在下一次监控的时候,就需要重新向集合中添加操作符。
④程序员轮询判断那个描述符还在哪个集合中,就确定这个描述符是否就绪了某个事件,然后进行对应事件的操作即可;select并不直接返回给用户就绪描述符直接操作,而是返回了就虚的描述符集合,因此需要程序员进行判断。
(2)代码操作:
①定义集合—struct fd_set — 成员是一个数组,当作二进制位图使用–添加描述符就是将描述符的值对应的比特位置1,因此seclect能够监听的描述符数量,取决于二进制位图的比特位多少—而比特位多少取决于宏(_FD_SETSIZE,默认为1024);
-
void FD_ZERO(fd_set* set);—初始化清空集合。
-
void FD_SET(int fd, fd_set* set);—将操作符fd增加到set集合中。
②发起调用接口:
-
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout); nfds:当前监控的集合中最大的描述符+1;减少遍历次数。 readfds/writefd/exceptfds:可读/可写/异常三种事件的集合。 timeout:时间的结构体。struct{tv_sec;tv_usec;},通过这个事件决定select阻塞/非阻塞/限制超时阻塞; -若timeout为NULL,则表示阻塞监控,直到描述符就绪,或者监控出错才会返回。 -若timeout中的成员数据为0,则表示非阻塞,监控的时候若没有操作符就绪,则就立即超时返回。 -若timeout中成员数据不为0,则在指定时间内,没有就虚则超时返回。 返回值:返回值大于0表示就绪的描述符个数;返回值等于0表示没有棉袄舒服就绪,超时返回;返回值小于0表示监控出错。
③调用返回,返回给程序员,就绪的描述符集合,程序员偏离判断那个描述符还在集合中,就是就绪了哪个事件。
-
int FD_ISSET(int fd, fd_set* set);—判断fd描述符是否在集合中;
- 因为select返回时会修改集合,因此每次监控的时候都要重新添加描述符
④若对描述符不想进行监控了,则从集合中移除描述符fd
-
void FD_CLR(int fd, fd_set* set);--从set集合中删除描述符fd;
(3)select对标准输入进行监控:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/select.h>
int main()
{
// 对标准输入进行监控
// 1.定义指定事件的集合
fd_set rfds;
while(1)
{
printf("开始监控\n");
//selent(maxfsd+1,可读集合,可写集合,异常集合,超时时间)
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
FD_ZERO(&rfds);//初始化清空集合
FD_SET(0,&rfds);// 将0号描述符添加到集合中
int res = select(0+1,&rfds,NULL,NULL,&tv);
if(res < 0){
perror("select error");
return -1;
}else if(res == 0){
printf("wait timeout\n");
continue;
}
if(FD_ISSET(0,&rfds)){
//判断描述符是否在集合中判断是否就绪了事件
printf("准备从标准输入读取数据:...\n");
char buf[1024] = {
0};
int ret = read(0,buf,1023);
if(ret<0)
{
perror("read error");
FD_CLR(0,&rfds);//移除描述符从集合中
return -1;
}
printf("read buf:[%s]\n",buf);
}
}
return 0;
}
(4)优缺点分析:
- 缺点:
①select对描述符进行监控有最大数量限制,上限取决于宏-_FD——SETSIZE,默认大小1024;
②在内核中进行监控,是通过轮询遍历判断实现的,性能会随着描述符的增多而下降
③只能返回就绪的集合,需要进程对集合遍历才知道哪个描述符就绪哪个事件。
④每次监控都需要重新添加描述符到集合中,每次监控都需要将集合重新拷贝到内核中。 - 优点:
跨平台移植性比较好,遵循posix标准。
2.poll模型:
(1)操作流程:
① 定义监控的描述符事件结构体数组,将需要监控的描述符以及事件标识信息,添加到数组的各个节点中;
②发起调用开始监控,将描述符事件结构体数组,拷贝到内核中进行轮询遍历判断,若就绪/等待超时则调用返回,并在每个就绪事件结构体中,表示当前就绪事件;
③进行轮询遍历数组,判断数组中的每个节点中的就绪事件是哪个事件,决定是否就绪了以及如何对描述符进行操作。
(2)代码操作:
-
int poll(struct pollfd* arry_fds,nfds_t nfds,int timeout) poll---监控采用时间结构体的形式; struct pollfd { int fd ;---要监控的描述符; short events; --- 要监控的事件POLLIN/POLLOUT; short revents; --- 调用返回是填充的就绪事件 arry_fds---事件结构体数组,填充要监控的描述符以及事件信息; nfds --- 数组中的有效节点数个数; timeout---监控事件超时等待事件; 返回值:返回值大于0表示就绪描述符事件的个数;返回值等于0就表示等待超时,返回值小于0表示监控出错。
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct pollfd poll_fd;//定义事件结构体
poll_fd.fd = 0;
poll_fd.events = POLLIN;//输入事件
for (;;) {
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0) {
perror("poll");
continue;
}
if (ret == 0) {
printf("poll timeout\n");
continue;
}
if (poll_fd.revents == POLLIN) {
char buf[1024] = {
0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}
}
}
(3)优缺点分析:
- 优点:
①使用事件结构体进行监控,简化了select中三种事件的操作流程。
②监控的描述符数量不做最大数量限制。
③不需要每次重新定义事件节点。 - 缺点:
①跨平台移植性差。
②每次监控依然需要向内核中拷贝监控数据。
③在内核中监控依然采用轮询遍历,性能会随着描述符增多而下降。
3.eppol模型:
- Linux下最好用的,性能最好的多路转接模型。
(1)操作流程:
①发起调用在内核中创建epoll句柄epollevent结构体(这个结构体包含很多信息,红黑树+双向链表);
②发起调用对内核中的epollevent结构添加/删除/修改所监控的描述符监控信息;
③发起开始监控,在内核中采用异步阻塞操作实现监控,等待超时/有描述符就绪则就会返回,返回给用户就绪描述符的事件结构信息;
④进程直接对就绪的事件结构体中的描述符成员进行操作即可。
(2)接口信息:
① 在内核中创建epoll句柄eventpoll,返回描述符
-
int epoll _create(int size); :返回epoll操作句柄 -size : 在linux2.6.2以后被忽略,只要大于0即可。 返回值:文件描述符--epoll的操作句柄
②epoll时间注册函数:
-
int epoll_ctl(int epfd,int cmd, int fd, struct epoll_event* ev); -epfd : epoll_creat 返回的操作句柄; -cmd : 针对fd描述符的监控信息要进行的操作--添加/删除/修改 EPOLL_CTL_ADD/EPOLL_CTL_DEL/EPOLL_CTL_MOD; -fd : 要监控操作的描述符; -ev : fd描述符对应的事件结构体信息; struct epoll_event{ uint32_t events; //对fd描述符要监控的事件--EPOLLIN/EPOLLOUT; union{ int fd; // 监控操作的描述符; void* ptr;//要填充的描述符信息; } }
③收集在epoll监控事件中已经发送的事件:
-
int epoll_wait(int epfd,struct epoll_event *evs,int max_event,int timeout) -epfd : epoll操作句柄; -evs : struct epoll_event 结构体的首地址; -max_event : 本次监控想要获取的就绪事件的最大数量,不大于evens数组的最大节点数量,禁止越界访问。 -timeout : 等待超时时间--单位:毫秒。 返回值:返回值<0 表示监控出错 ,返回值 == 0 表示超时返回 , 返回值 > 0 表示就绪的时间的个数。
(3)epoll监控原理:异步阻塞操作:
- 监控由系统完成,用户添加监控描述符以及对应事件结构体会被添加到内核的eventpoll结构体中的红黑树中,一旦发起调用开始监控,则操作系统为每个描述符的事件做了一个回调函数,功能是当描述符就绪了关心的事件,就将该结构体添加到双向链表中。
- 进程自身只是每过一段时间,判断双向链表是否为NULL,决定是否有就绪。
①创建句柄
②添加监控的描述符信息,以及对应事件的结构体信息到内核;
③开始异步阻塞监控,系统将就绪描述符的对应事件结构体信息添加到双向链表中;
④通过判断双向链表的事件结构体返回给进程。
⑤进程只需要更具就绪事件结构体中的时间信息决定对时间结构体中的发的描述符进行相应操作即可。
(4)优缺点:
- 优点:
①没有没有描述符监控数量的上限;
②监控信息只需要向内核中添加一次;
③监控使用异步堵塞操作,性能不会随着操作符的增加而下降;
④直接向用户返回就绪的时间信息,进程直接对返回的描述符和事件进行操作,不需要判断有没有就绪了。 - 缺点:
跨平台移植性差。
(5)epoll中的触发方式:
- 假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait
①水平触发方式:
- epoll默认状态下就是LT工作模式:
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分;
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪;
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回;
即:支持阻塞读写和非阻塞读写
②边缘触发方式:
- 如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式:
当epoll检测到socket上事件就绪时, 必须立刻处理;
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了;
也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会;
ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
即:只支持非阻塞的读写 - select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET