I/O多路复用
I/O为阻塞模式时,比如read()时,会一直等待直到数据的到来。I/O为非阻塞模式时,如果该次读操作并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型。那么当我们写服务器的时候,假如又有客户端连接,已连接的客户端又产生了IO请求,且都为阻塞。怎么解决这个问题呢?首先提出的select模型。下面先从select开始。
select多路复用
select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生(类似于这个函数监听这些事件,看它们有没有发生),并只在有一个或多个事件发生或经历一段指定时间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set); //将给定的描述符从文件中删除
int FD_ISSET(int fd, fd_set *set);//判断指定描述符是否在集合中
void FD_SET(int fd, fd_set *set);//将给定的描述符加入集合
void FD_ZERO(fd_set *set);//清空集合
- select函数的返回值是就绪描述符的数目,超时时返回0,出错返回-1;
- 第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1。Linux内核从0开始到max_fd-1扫描文件描述符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么Linux内核实际也要监测0~7,此时真 正带测试的文件描述符是0~10总共11个,即max(8,9,10)+1,所以第一个参数是所有要监听的文件描述符中最大的+1。
- 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的可以设置为 NULL。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。一个文件描述集保存在 fd_set 类型中;
- 最后一个参数是设置select的超时时间,如果设置为NULL则永不超时;
需要注意的是待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要 监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1
在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中,这也意味着select所用到的FD_SET是有限的,也正是这个 原因select()默认只能同时处理1024个客户端的连接请求。
fd_set类型变量每一位代表了一个描述符,FD_ZERO宏将一个 fd_set类型变量的所有位都设为 0,使用FD_SET将变量的某个位置位。清除某个位时可以使用 FD_CLR,我们可以使用 FD_SET来测试某个位是否被置位。整个过程like this:
首先将自己需要监听的文件描述符通过FD_SET将它添加到可写,可读,异常三个集合中的一个。相应当事件发生,select返回,fd_set集合相应的位被置位,可以通过FD_ISSET测试给定位是否置位,这样就能判断相应的文件描述符是否能进行I/O操作。select返回后会把以前加入的但并无事件发生的fd清空,这样每次就需要重新加入fd,所以需要一个和__FD_SETSIZE大小对应的数组来保存需要关心的事件,每次执行后就需要将数组中的重新拷贝到fd_set集合中。
poll多路复用
从select的过程就很容易发现问题,一次只能处理1024个请求(根据相应的系统而定),假如12306只能处理这么多个连接,那春节买票得买到下一个春节。poll就解决了这个大小限制的问题。
select()和poll()系统调用的本质一样,前者在BSD UNIX中引入的,后者在Sy