IO模型总结

263 阅读6分钟

# IO模型

I/O 分为等待数据到 io 设备和把数据复制到用户内存空间这两步

linux 的网络 IO 中是不存在异步 IO 的,linux 的网络 IO 处理的第二阶段总是阻塞等待数据 copy 完成的

1. 阻塞 IO (blocking IO)

  • 等待数据和读数据都是阻塞的,读完之后再返回(recvfrom 系统调用)

  • 读数据指内核内存中的数据复制到用户内存中

  • 在阻塞状态下,程序是不会浪费 CPU 的,cpu 只是不执行 io 操作了,还会去做别的(阻塞状态不耗 cpu 资源)

2. 非阻塞 IO (nonblocking IO)

  • 等待数据不阻塞,读数据阻塞,数据没有准备好返回 EWOULDBLOCK 错误(多次 recvfrom 系统调用)

  • 不断地主动询问

3. IO 多路复用 (IO multiplexing)

  • 等待数据和读数据都是阻塞的,等待结束返回数据已可读(select 系统调用),再调用读数据系统调用(recvfrom 系统调用)

  • 适用于针对大量的 io 请求的情况,对于服务器必须在同时处理来自客户端的大量的 io 操作的时候,就非常适合

  • 先到的数据先处理,而不管 fd 创建的顺序

4. 信号驱动 IO (signal driven IO)

  • 等待数据结束后,发出 notice 之后阻塞读数据

5. 异步 IO (asynchronous IO) (AIO)

  • 等待数据和读数据都不阻塞的,发出 aio_read 系统调用后立即返回,读数据结束之后给一个 notice

select & poll & epoll

select、poll、epoll 本质上都是 IO 多路复用

1. 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,函数就可以返回

2. I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作

3. select poll epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

1. select 函数监视的文件描述符分 3 类,分别是 writefds readfds lexceptfds

2. 使用过程

  • 从用户空间拷贝 fd 集合到内核空间

  • 进程调用 select 函数后会阻塞,直到有描述符就绪(有数据可读、可写、或者有 except),或者超时(timeout指定等待时间,如果立即返回设为 null 即可),select 函数返回

  • 当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符

3. select/polle 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接

  • 如果处理的连接数不是很高的话,blocking IO 的性能可能更好

4. 缺点

  • 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024

  • 文件描述符是通过顺序遍历的,遍历时间 O(n) 级别

  • 每次调用 select,都需要把 fd 集合从用户空间拷贝到内核空间,这个开销在 fd 很多时会很大

poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout);

1. poll 的实现和 select 非常相似,只是描述 fd 集合的方式不同,故没有 fd 数量的限制

3. 链式,和 select 相比没有最大描述符数量限制,触发方式是 LT 水平触发,没有处理下次处理

3. 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符,遍历时间 O(n) 级别

  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率线性下降

4. 不同于 select 使用三个位图来表示三个 fdset 的方式,poll 使用单个 pollfd 的指针实现

  • pollfd 结构包含了要监视的 events 和发生的 revents,不再使用 select 参数-值传递方式

epoll

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int pfd, struct epoll_event *events, int maxevents, int timeout);

1. epoll_create 创建一个 epfd 句柄,告诉内核监听的数目有多大(这个数据不是限制,只是初始分配的数量)

2. epoll_ctl 操纵 epfd 句柄,添加、删除、修改监听事件

3. epoll_wait 用来得到事件的集合

4. 当数据准备好之后,就会把就绪的 fd 加入一个就绪的队列中,通过内核和用户空间共享一块内存来实现消息传递

5. epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的 fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数

6. 触发方式 -- 水平触发 LT (level trigger) -- 缺省工作模式

  • epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件

  • 下次调用epoll_wait时,会再次响应应用程序并通知此事件

7. 触发方式 -- 边缘触发 ET (edge trigger)

  • epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件

  • 下次调用epoll_wait时,不会再次响应应用程序并通知此事件

  • 减少了 epoll 事件被重复触发的次数,如果有一些事件不想被响应,则它们只会在第二次准备好的时候进行通知

epoll 相对 select 优势

1. 每次注册新的事件到 epoll 句柄中时(epoll_ctl),会把所有的 fd 拷贝进内核,保证了每个 fd 在整个过程中只会拷贝一次

2. 只在epoll_ctl时为每个 fd 指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的 fd 加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的 fd

3. epoll 没有最多 fd 这个限制,它所支持的 fd 上限是最大可以打开文件的数目,只和可用内存有关

select & poll & epoll 区别

1. select/poll 中,进程只有在调用方法后,返回确定有 fd 已经就绪,之后就要轮询寻找准备好的 fd,而 epoll 准备好的 fd 主动加入队列中,调用epoll_wait查找就绪队列就知道哪个 fd 准备好了

  • epoll 先通过epoll_ctl注册一个文件描述符

2. select/poll 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次

3. select 支持的文件描述符个数有上限,而 poll/epoll 没有上限,只和系统资源有关

4. select/poll 需要遍历所有的文件描述符,监听的越多效率越低,而 epoll 不是基于轮询机制,监听的数量不会影响速度

5. 当有大量没有准备好的连接时,epoll 的效率较高,否则效率并不会高很多