
IO模型
1. 阻塞式IO
等待数据时应用程序被阻塞,等待数据到来。直到数据被复制到用户态缓冲区才返回。只是应用程序被阻塞,不占用CPU时间,阻塞期间还可以执行其他进程。
2. 非阻塞式IO
程序执行系统调用后,如recvfrom, 内核返回一个状态码,应用继续执行,内核负责等待数据,程序需要不断执行系统调用来轮询内核IO是否完成。
3. IO复用
调用select、poll等系统调用等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞。当某一套接字可读时,调用recvfrom将数据从内核复制到进程中。单进程处理多IO,省去进程创建和切换的开销。
4. 信号驱动
应用进程调用sigaction,内核立即返回,等待数据到达,等待阶段是非阻塞的。当内核发现数据到来时,向进程发一个SIGIO信号,进程收到后,调用recvfrom拷贝数据。
5. 异步IO
执行aio_read调用后立即返回,不等待。进程继续执行,不阻塞。内核完成所有操作后向进程发信号。与信号驱动不同在于,异步IO发送信号告诉进程IO已经完成,信号驱动发信号是说可以开始IO。
总结:14属于同步IO,即第二阶段(从内核拷贝到用户控件)应用程序会阻塞,其中24在第一阶段(等待数据到来)进程不会阻塞。异步IO有点像甩手掌柜,把事情交给内核打理。
IO复用:select,poll,epoll
select
select把事件分类,用三个数组来分别监听读,写和异常情况。调用时把数组全盘拷贝进内核,有事件到来时轮询数组看看哪个socket上发生。
它的缺点在于:
- 单个进程能监听的文件描述符存在最大限制,
FD_SIZE通常是1024。由于select采用轮询方式扫描文件描述符,文件描述符越多,性能越差。 - 内核/用户态切换开销大。select需要切换大量的句柄到内核态。
- select返回整个数组,进程需要遍历整个数据才知道哪个socket上有事件。
- select使用水平触发,select如果没有处理已就绪的事件,后续select仍会将该文件操作符返回给进程。
poll
poll使用链表存储文件操作符,打破了select最大描述符的限制,但其他不足仍存在
epoll
epoll的实现:在内核中申请一个简易的文件系统(B+树)。
epoll调用分3步:
-
epoll_create:创建一个epoll对象 -
epoll_ctl:向对象中添加高并发socket -
epoll_wait: 收集有事件发生的socket,放到返回双链表中。
因此,只需创建一个epoll对象,适时地向该对象添加或删除socket。
epoll对象数据结构
红黑树: 保存所有需要监控的句柄,高效去重
双链表:保存事件发生所在的句柄,待返回给进程
回调:epoll中事件与设备驱动建立回调关系,一旦事件发生回调函数就把事件添加到双链表中。
2种触发方式
水平触发:只要有fd就绪就向�内核发出通知,如果不处理,下次调用依然是就绪状态。
边缘触发:fd就绪,内核只发一次通知,如果不处理下次调用不再通知。