I/O模式

320 阅读7分钟

缓存I/O

大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存中。也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

也就是说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备
  2. 将数据从内核拷贝到进程中

根据这两个阶段,linux系统产生了五种网络模式:

  • 阻塞 I/O(blocking IO)

  • 非阻塞 I/O(nonblocking IO)

  • I/O 多路复用( IO multiplexing)

  • 信号驱动 I/O( signal driven IO)

  • 异步 I/O(asynchronous IO)

本节不会讲述第4中模式。

阻塞I/O

当用户进程调用recvfrom系统调用来从套接字中接收数据时:

(1)内核开始准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候内核就要等待足够的数据到来),也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程会被阻塞

(2)当内核等到数据准备好后,它会将数据从内核拷贝到用户内存,然后内核return ok,用户进程才解除阻塞状态。

非阻塞I/O

当用户进程发出read操作时,如果内核中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。用户进程判断结果是一个error后,可以选择再次发送read操作。一旦内核的数据准备好,并且收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后return ok。

因此NIO的特点是用户进程需要不断的主动询问内核数据好了没有

I/O多路复用

由于阻塞模型在没有收到数据时会阻塞,如果一次需要接受多个socket fd的时候,就会导致必须处理完前面的fd,才能处理后面的fd,即使可能后面的fd比前面的fd还要先准备好,所以这样就会造成客户端的严重延迟。也许你会想到使用多线程来处理socket fd,但为了避免启动大量线程造成的资源连给,我们可以使用I/O多路复用技术,即一个进程(线程)来处理多个fd请求。

fd 即fild descriptor,一个套接字描述器。在Unix/Linux系统下,一个socket,可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句通常也用表示文件句柄的fd来表示。

select

它的工作流程是:进程发出select并被阻塞,然后内核就会轮询检查所有select负责的fd,当找到一个client中的数据准备好了,select就会返回,此时程序就会系统调用,将数据从内核复制到进程缓冲区。

缺点

(1)单个进程最大只能同时监视1024个fd

(2)主动轮询的成本很高:每一次呼叫select()都需要从用户空间把把 FD_SET复制到内核里,然后内核还需要轮询每个fd。但实际上同时连接的客户端在一时刻很少处于就绪状态。

poll

poll的原理与select相似,差别在于:

(1)描述fd集合的方式不同:select使用的是一个long类型数组fd_set,而poll使用的是链式结构pollfd,因此没有最大连接数限制

(2)poll的一个特点是水平触发(LT):只要内核缓冲区中还有未读数据,就会一直返回描述符的就绪状态,即不断地唤醒应用进程。

(3)select需要为读,写,异常事件分别创建一个描述符集合,最后轮询时分别轮询这三个集合。而poll只需要一个集合,在每个描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,可以同时检查三种事件。

epoll

针对于select/poll的缺点,引入的一个新技术:epoll。

epoll 提供了三个函数

  • int epoll_create(int size); 建立一個 epoll 对象,并传回它的id
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 等待注册的事件被触发或者timeout发生

epoll的使用步骤

  1. 调用epoll_create()创建一个epoll描述符
  2. 调用epoll_ctl()给描述符设置所关注的事件,并把它添加到内核的事件列表中
  3. 等待内核通知事件发生,得到发生事件的描述符的结构列表,该过程由epoll_wait()完成。

epoll解决了什么问题?

  • epoll监控的fd没有数量限制
  • epoll不需要每次都从用户空间将fd复制到内核。这是因为epoll在使用epoll_ctl函数进行函数注册时,已经将fd复制到内核中。
  • select,poll都是主动轮询机制,需要拜访每一个fd;而epoll是被动触发方式,给fd注册相应时间时,我们为每一个fd指定回调函数,数据准备好之后,就会把就绪的fd加入一个就绪的队列中。epoll_wait函数实际是在就绪队列里查看有无就绪的fd,若有则唤醒就绪队列上的等待着,然后调用回调函数。

epoll的LT模式和ET模式

(1)水平模式LT:内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

(2)边缘触发ET:当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)

举例说明两模式:

  1. 把一个用来从管道中读取数据的文件句柄RFD添加到epoll描述符
  2. 从管道另一端被写入2KB的数据
  3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
  4. 我们读取1KB的数据
  5. 调用epoll_wait(2)...

如果是LT模式,那么在第5步调用后,仍能收到通知;而如果是ET模式(也就是在第1步时使用了EPOLLET标志),则在第5步调用后可能被挂起,因为剩余数据还存在于文件的输入缓冲区内,且数据发出端还在等待一个针对已经发出数据的反馈信息。只有当监视的文件描述符发送某个事件后,才会汇报时间。因此在第5步,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

总结

select, poll是为了解決同时大量IO的情況,但是随着连接数越多,性能越差

poll是select和poll的改进方案,在 linux 上可以取代 select 和 poll,可以处理大量连接的性能问题

参考资料

Linux IO模式及select poll epolll

细谈select, poll, epoll