博客搬家,原地址:https://langzi989.github.io/2017/10/08/Unix中SelectPollEpoll详解/
网络应用需要处理的问题无非两类,网络I/O和数据计算问题。
在处理计算密集型任务的时候,期间会有一些网络IO操作(如写数据库的操作,非本机),此时若使用同步IO,则会造成大量的IO等待,造成CPU使用率较低。所以此时考虑其他IO模型如异步模型。
Unix下网络I/O模型包括五类:
- 阻塞式IO
- 非阻塞式IO
- 多路复用IO
- 信号驱动IO(边缘触发)
- 异步IO
其中多路复用I/O机制是通过select,poll以及epoll进行监视。这里暂时只介绍多路复用IO,若想了解其他IO模型,参考《Unix网络编程》第六章
多路复用I/O模型
网络I/O的本质是socket的读取,socket在linux系统中被抽象为流,所以I/O操作可以理解为对流的操作。这个操作包括两个阶段:
- 等待流数据准备就绪(wait for data be ready)
- 从内核相进程复制数据
由于非阻塞调用的过程中,轮训占据了大部分的过程,所以轮训会占据大量的CPU时间。如果轮训不是进程的用户态,而是有人帮忙就好了。多路复用正好处理这样的问题。
多路复用的过程:多路复用有两个特别的系统调用select和poll。select调用是内核级别的,select轮训相对于非阻塞的轮训区别在于:前者可以等待多个socket,当其中一个socket数据准备好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到进程中,拷贝的过程是阻塞的。
多路复用有两种阻塞,select或poll调用之后,会阻塞进程,与第一种阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为”非阻塞”吧。
类比钓鱼过程:在钓鱼的时候,我们雇了一个帮手,他可以同时抛下多个鱼竿,任何一个鱼竿的鱼一上钩,他就会拉杆。他只负责帮我们钓鱼,并不处理,所以我们在一旁等着,等他收杆之后,我们再进行处理。
多路复用既然可以处理多个IO,也就带来了新的问题:多个IO的顺序不能保证
多路复用的特点多路复用通过一种机制一个进程能同时等待多个IO文件描述符,内核监视这些文件描述符(socket描述符),其中任意一个进入读就绪状态时,select,poll.epoll函数就可以返回。对于监视的方式,有可以分为select,poll,和epoll三种方式。
select函数详解
### 函数原型
#include <select.h>
#include <time.h>
int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* except_set, const struct timeval* timeout);
函数功能
该函数允许进程指示内核等待多个事件中的其中一个发生,并只在有一个或多个事件发生或者经历了一段时间之后才唤醒它。
其中等待的事件类型包括三种:指定集合中的描述符处于可读状态,执行集合中的描述符处于可写状态,指定集合中的描述符有异常未处理。
描述符就绪的条件如下:
可读就绪
当描述符满足下列四个条件中的其中一个,表示该描述符已经准备好读
- 该套接字接收缓冲区的字节数大于等于套接字接收缓冲区的低水位标记的大小。一般对于TCP和UDP该值默认为1,我们也可以通过SO_RCVLOWAT套接字选项设置该套接字的低水位标记。
- 该连接读半部关闭(接受了FIN的TCP连接),此时函数返回0。
- 该套接字是一个监听套接字且完成的连接数不为0。对于这种套接字,accept通常不会阻塞。
- 其上有一个套接字错误待处理.对这种套接字的读操作将不阻塞病返回-1。
可写就绪
当描述符满足下列四个条件中的其中一个,表示该描述符已经准备好写
- 该套接字的发送缓冲区的可用空间字节大于等于套接字发送缓冲区的低水位标记大小。TCP和UDP的默认大小一般为2048。可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记
- 该链接的写半部关闭
- 使用非阻塞式connect的套接字建立连接,或者connect以失败告终
- 其上有一个套接字错误未处理
函数参数
- maxfdp1 : fd_set中最大的描述符+1(特别注意不要忘了+1