OS | IO模型

727 阅读5分钟

1、一般的IO过程

  • 等待数据准备好,比如socket,等待数据到达内核(TCP协议栈是内核管理的啊!)
  • 将数据从内核缓冲区拷贝到应用进程缓冲区。

读写事件:

  • 有数据了,可以读了,是读事件。
  • 数据区满了就没法写了,当数据区有空间了,可以写了,是写事件。

IO种类:内存IO,磁盘IO,缓存IO,网络IO。

一般是网络IO才需要等待,才需要多路复用;

2、IO模型

同步(synchronous)IO:在IO事件就绪后自己负责进行IO,也就是说这个IO过程是阻塞的,直到IO完成; 阻塞、非阻塞、IO复用都是同步IO;

异步(asynchronous)IO:启动IO时,如果数据还没准备好则直接返回,当数据准备好时,内核将数据copy到应用进程,一切搞定后,再通知应用进程read操作完成了;

阻塞(blocking)IO:数据没有准备好就一直阻塞等待;

非阻塞(non-blocking)IO:数据没准备好时,会返回错误给你,不会阻塞你。所以你需要不断轮询,看数据是否准备好了;(浪费CPU) 当数据准备好了,调用system call将数据从内核copy到应用进程时,这时进程会被阻塞。可以看出,非阻塞不是异步,异步是完全不会被阻塞的;

IO多路复用(multiplexing):也叫event driven IO。单个进程监听多个描述符,当某个描述符数据准备好了,就通知应用进程;也是阻塞的,当一个或多个描述符数据准备好了,才被唤醒。 另一个表述:监听多个事件,当一个或多个事件发生时,才唤醒进程;

什么叫复用?重复使用,单个进程管理多个IO,也就是多个IO重复使用一个进程;

BIO(同步阻塞) NIO(同步非阻塞) AIO(异步非阻塞)

另一个表述:监听多个事件,当一个或多个事件发生时,才唤醒进程;

3、IO多路复用的优势

  • I/O多路复用的优势并不是对于单个连接能处理的更快,而是在于可以在单个线程/进程中处理更多的连接。 那多个连接要并发,还是需要多线程啊?
  • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,自然也就不需要线程切换了,从而大大减小了系统的开销。

4、select poll epoll的比较

这三个都是系统调用,内核帮你监听;

(1)select

原型:int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout); fd_set就是fd数组;返回值是就绪fd的个数;(fd_set虽然是指针,但还是会发生内存拷贝,用户态-->内核态)

内核不能信任任何用户空间的指针。必须对用户空间的指针指向的数据进行验证。如果只做验证不做拷贝的话,那么在随后的运行中要随时受到其它进/线程可能修改用户空间数据的威胁。所以必须做拷贝。

运行机制: 将fd_set拷贝进内核,内核帮你监听和遍历,遍历结束后,当有一个或多个fd就绪,内核就将 fd_set 从内核空间拷贝到用户空间,并通知你有fd就绪了,然后你自己再去轮询找哪个是就绪的。

缺点:

  • fd采用数组结构,数量有限制,最多是1024;
  • 每次调用select需要将所有fd从用户态copy到内核态,开销巨大;
  • 某个fd就绪了,会通知你,但是不会告诉你是那个就绪了,你需要遍历所有fd;
(2)poll

原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

运行机制: 和select一样,只是fd采用链表结构,数量不限制了;其他缺点都存在;

(3)epoll

运行机制: 监听一组fd,当有一个或多个fd就绪时,会通知你,并告诉你哪些fd是就绪的;

优点:

  • fd数量不限制,上限是进程的最大可以打开文件数;
  • 每个fd定义一个类似回调的函数,来实现只有就绪的fd才会执行回调函数。那么应用进程就不需要遍历所有fd了;
  • 省去不必要的内存拷贝:epoll 通过内核与用户空间 mmap 同一块内存实现。
for true{
	[就绪fd] = epoll_wait(epoll_fd) //阻塞
	
	//有fd就绪
	for i in [就绪fd]{
		读写操作
	}
}
(4)触发机制

水平触发(LT):默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件

边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

select/poll只有水平触发,epoll两者都有;