0%

Linux IO

Linux内核把所有管理的资源,如网卡、磁盘驱动器、打印机、输入输出设备、普通文件或目录都当作一个文件。对文件的读写操作会调用内核提供的命令,返回一个文件描述符。文件描述符是一个非负整数索引值,指向内核为每个进程所维护的该进程打开的文件记录表,表中的项是一个结构体(包含文件路径、数据区等属性)。套接字是通信端点的抽象,套接字描述符是一种用来访问套接字的文件描述符,很多处理文件描述符的函数(如read和write)都可以用来处理套接字描述符。

一次IO过程可以分为两个阶段:

  1. 等待数据准备(数据先读到内核的缓冲区)
  2. 将数据从内核拷贝到进程中

Unix提供了五种IO模型,分别是阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。IO编程中,当需要处理多个客户端接入时,可以利用多线程或IO多路复用技术。IO多路复用是把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。模型使用一个进程监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

五种IO模型

Linux支持IO多路复用的系统调用有select、pselect、poll和epoll,IO多路复用需要使用两个系统调用(select/pselect/poll/epoll和recvfrom),而阻塞IO只需要一个系统调用(recvfrom)。但多路复用的优势是可以同时处理多个connection,系统开销小,不需要创建和维护新的额外进程或线程。适用于需要同时处理多个(可以是不同状态或协议)套接字。如果处理连接数不是很高的情况下,IO多路复用不一定比阻塞IO+多线程性能好。这两种IO模型都是进程主动等待并向内核检查状态,所以都是同步阻塞模型。

select

调用select后进程阻塞,直到有fd就绪(可读、可写或except)或超时后返回。select几乎支持所有的平台,具有良好的跨平台性,但最大的缺陷是单个进程对监控的fd有数量限制。这个限制由FD_SETSIZE定义,32位默认值是1024,64位默认值2048.

poll

poll和select没有本质区别,它基于链表存储fd,没有最大数限制。与select一样,监控到描述符就绪并返回后,通过遍历文件描述符获取已经就绪的scoket。

epoll

epoll在Linux 2.6内核提出,基于事件驱动的IO方式,没有描述符个数限制(1G内存能监听10W端口),使用一个文件描述符管理多个描述符(使用红黑树管理),它将用户关心的文件描述符事件存放在内核的一个事件表中,这样在用户空间和内核空间的copy只有一次。而select和poll每次调用都需要把文件描述符集合从用户态copy到内核态。

epoll采用回调方式,只有活跃可用的fd才会调用callback函数,即只关注活跃的连接,与连接总数无关,效率远高于select和poll。

select/poll 都需要内核把 fd 消息通知给用户空间,而 epoll 是通过内核和用户空间 mmap 同一块内存实现。

epoll对文件描述符的操作有两种模式:水平触发LT(level trigger)和边缘触发ET(Edge trigger),LT是默认模式,两者区别如下:

LT模式——当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式——当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知该事件。

假如有这样一个例子:

  1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

  2. 这个时候从管道的另一端被写入了2KB的数据

  3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

  4. 然后我们读取了1KB的数据

  5. 调用epoll_wait(2)……

Edge Triggered 工作模式:

如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

i 基于非阻塞文件句柄

ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

Level Triggered 工作模式

相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。

因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

图示说明:

img

Nginx默认采用ET模式来使用epoll。

参考资料

  1. 《Netty权威指南(第二版)》
  2. 《Unix环境高级编程》
  3. 聊聊Linux五种IO模型
  4. Java网络编程与NIO详解8:浅析mmap和Direct Buffer
  5. Linux下I/O多路复用系统调用(select, poll, epoll)介绍
  6. Socket通信网络模型 ——Epoll、IOCP模型详解以及与select、kqueue等常见模型的区别特点