-
Unix 网络IO模型介绍
同步和异步、阻塞和非阻塞
同步和异步
广义上讲同步异步描述的是事件中发送方和接收方之间的关系。
- 当发送方必须依赖接收方的响应结果(无论正确与否)才能进行下一步操作,则两者间的关系是同步的。
- 当发送方不必依赖接收方的响应即可继续执行,则两者间的关系是异步的。在异步关系中,发送方可能根本不在意接收方的返回信息,也可能接收方通过事件、回调的形式来通知发送方结果。
即在同步关系中,发送方和接收方的步调是一致的,而异步关系中则没有必要。
快递员派送一件必须当面签收的贵重物品,快递员必须在客户签字后才能确认送达,快递员和客户的关系就是同步。
快递员派送普通物件,直接放到快递柜里,客户取出后系统自动确认送达,快递员和客户的关系就是异步。
阻塞和非阻塞
阻塞和非阻塞形容的是事件单个参与者的状态。
- 当参与者因为某些条件没有满足而无法执行下一步动作,只能原地等待,那么该参与者就陷入了阻塞。
假设有一条单行车道,有一天道路中央由于暴雨积水严重无法通过,那么经过这条路的车辆便陷入了阻塞状态。
四种组合
-
同步阻塞:发送方发起调用后,必须等待接收方的完成响应,且在此期间发送方不能执行任何动作。
顾客去银行柜台存钱,在柜员存入流程完成之前,顾客必须在柜台前等候流程结束。
-
同步非阻塞:发送方发起调用后,如接收方不能马上完成,可先返回给发送方一个未完成状态,发送方收到后可自行判断继续等待还是先执行其他动作再做轮询查看。
顾客去买奶茶,奶茶不能马上做好,就给了顾客一张单号。顾客可以在附近逛逛,每隔一会儿主动过来询问好了没有。
-
异步阻塞:发送方发起调用后无需等待接收方任何响应,但由于接收方的动作影响发送方的状态,发送方无法执行其他动作。(实践中通常没有该应用场景)
-
异步非阻塞:发送方发起调用后无需等待接收方任何响应,自由执行其他动作。
顾客去吃饭排队,小程序扫码以后就可以去别处逛。当排到该顾客时,小程序推送就餐消息给顾客。
UNIX IO模型
通常所说IO模型为网络IO模型,一个网络IO主要包含几个阶段:应用进程监听某个端口,等待数据从网络中到达网卡缓冲区,数据到达后CPU收到信号将数据转移到内核缓冲区,然后将数据从缓冲区复制到应用进程缓冲区。依据监听方式和数据复制方式的不同,UNIX IO主要分为5种IO模型。
阻塞IO
阻塞IO是最基础的IO模型,应用进程监听端口后就一直陷入阻塞状态,直到有数据到达。如下图所示,应用程序调用recvfrom后即陷入阻塞状态,直到CPU将数据拷贝到用户空间后,应用程序才能继续执行。
非阻塞IO
非阻塞IO允许应用程序调用recvfrom时立即返回,在数据没有就绪时,返回状态为EWOULDBLOCK,这时应用程序可继续执行,但需要不断发起轮询(polling)判断数据是否就绪。
非阻塞IO仅针对数据未就绪时是非阻塞的,在数据拷贝过程还是阻塞的。
IO多路复用
通过使用select/poll/epoll,应用进程可以同时等待多个设备的数据状态。应用程序在发起select/poll/epoll调用时会进入阻塞状态,但当其监听的任一个文件描述符数据就绪即可返回,应用程序即可对对应描述符发起recvfrom调用,拷贝应用数据。
IO多路复用带来的好处:
在上述的阻塞和非阻塞IO,如果要对多个描述符进行监听,则需要同时开启多个进程/线程。
通过select/poll/epoll可以使单个进程/线程具备监听多个连接的能力。
只要当IO事件发生时处理相应描述符即可,因此也称为事件驱动IO。
信号驱动IO
应用程序通过为SIGIO信号注册一个信号关联函数监听文件描述符,调用注册后应用程序可立即返回继续执行。当描述符数据就绪时,通过产生SIGIO信号发起对应用程序信号关联函数的调用,应用程序可通过recvfrom进行数据拷贝。
异步IO
异步IO模式下,应用程序触发系统调用后可立即返回,内核在数据拷贝完成后再对应用程序发出信号,触发应用程序逻辑。
异步IO与信号驱动IO的区别是:
- 信号驱动IO产生信号后,应用程序仍然需要阻塞读取数据到应用程序空间。
- 异步IO数据拷贝的过程也是由CPU进行的,直到拷贝完成才通知应用程序,做到全程非阻塞。
模型比较
通过前面的描述我们可以看出,前四种IO模型只有在等待数据阶段有区别,在拷贝数据时都会进入阻塞状态。而异步IO在应用程序的整个阶段都是非阻塞的。前四种IO都属于同步IO,最后一种属于异步IO。
POSIX把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO。
select/poll/epoll
-
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select的缺点是
- select对于单个进程能够见识的文件描述符数量存在限制,32位环境为1024,64位为2048。
- select返回后仅直到有IO事件产生,具体到哪个描述符只能进行O(n)级别的轮询。
-
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现,同时poll没有最大数量限制。
-
epoll
int epoll_create(int size);//创建一个epoll的句柄epfd,size用来告诉内核这个监听的数目一共有多大 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//对指定fd添加删除监听事件 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//等待句柄上的事件
epoll操作需要三个函数才完成创建,epoll可以直接返回哪些描述符产生了事件,因此复杂度时O(1)的。
epoll有两种工作方式:
- LT(level trigger)模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET(edge trigger)模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
为什么要有两种模式:
如果采用LT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。而采用ET模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。
__EOF__