https://www.yuque.com/chgkn/notebook/ls6foy

1. I/O模式


对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  • 等待数据准备 (Waiting for the data to be ready)
  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

**image.png**
因为这两个阶段,Linux系统产生了下面五种网络模式的方案。

  1. 阻塞 I/O(blocking IO)
  2. 非阻塞 I/O(nonblocking IO)
  3. I/O 多路复用( IO multiplexing)
  4. 信号驱动 I/O( signal driven IO)不常用
  5. 异步 I/O(asynchronous IO)

1.2 阻塞I/O blocking I/O

在Linux中,默认情况下所有的socket都是blocking的,一个典型的读操作流程大概是:
1 select poll 和 epoll - 图2
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:

  • wait for data —> 准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。
  • copy data from kernel to user —>kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,直到 copy complete ,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

    所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

1.2 非阻塞I/O noblocking I/O

Linux下可以通过设置socket使其变为non-blocking**当对一个non-blocking socket执行操作时,轮询(polling)**流程是这个样子:

1 select poll 和 epoll - 图3
需要注意,copy data from kernel to user —> 拷贝数据整个过程,进程仍然是属于阻塞的状态。

  • 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。
  • 从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。
  • 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
  • 一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

1.3 I/O多路复用 I/O Multiplexing

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IOselect/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。( process blocks in call to select, waiting for one of possibly many sockets to become readable)

1 select poll 和 epoll - 图4
当用户进程调用了**select**,那么整个进程会被**block**,而同时,kernel会“监视” 所有 select负责的 socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

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

1.4 异步I/O asynchronous IO

1 select poll 和 epoll - 图5

2 select、poll与epoll之间的区别总结

select、poll与epoll都是IO 多路复用的机制。
IO多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(写或者读就绪),能够通知程序进行响应的读写操作。
但select,poll,epoll本质上都是**同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞**的,
而异步IO则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

2.1 select 实现

1 select poll 和 epoll - 图6

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

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds调用后select函数会阻塞直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

select缺点

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时会很大
  3. select支持的fd数量太小,默认1024

2.2 poll 实现

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

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

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

  1. struct pollfd {
  2. int fd; /* file descriptor */
  3. short events; /* requested events to watch */
  4. short revents; /* returned events witnessed */
  5. };

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符

poll 缺点

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

2.3 epoll 实现

oll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll是对select和poll的改进,应该可以避免上述三个缺点,那么epoll都是怎么解决的呢?
poll操作过程需要三个接口,分别如下:

  1. int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

select和poll都只提供了一个函数——select和poll函数,而epoll提供了三个函数

  1. epoll_create创建一个epoll句柄;
  2. epoll_ctl是注册需要监听的事件类型
  3. epoll_wait则是等待事件的发生

epoll_ctl解决fd集合拷贝到内核态的大开销的缺点

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

epoll_ctl/wait 解决内核遍历fd集合的大开销的缺点

epoll不像slelect和poll一样每次都把current轮流加入fd对应的设备等待队列中,而只是在epoll_ctl时把current挂一遍并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待fd时,就会调用回调函数把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd。

epoll没有fd数量限制

对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结

1. select,poll实现需要不断自己轮询所有的fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在醒着的时候要遍历整个fd集合,而epoll在醒着的时候只要判断以下就绪链表是否为空就行了,这就节省了大量cpu时间。

2. select、poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,而是一个epoll内部定义的等待队列)这样也能节省不少开销。