五种IO模型一文中介绍了五种IO模型。在数据通信过程中,分为两部分:一个是等待数据到达内核,一个是将数据从内核拷贝到用户区。在实际应用中,等待的时间往往比拷贝的时间多,所以要提高IO的效率,就要减少等的比重。在阻塞IO,非阻塞IO,信号驱动IO和异步IO中,虽然等待的方式或等待的主体不同,但是无论是谁在等,无论如何等,等待的时间总长是不变的。
五种IO模型一文中提到,在服务器与客户端进行通信时,服务器要处理多客户端的情形。所以服务器程序首先要使用selcet等系统调用一次等待多个文件描述符,当至少有一个文件描述符满足就绪条件时,select返回,然后进程调用read对满足就绪条件的文件描述符进行读写。将满足就绪条件的所有文件描述符处理完之后,之前的客户端可能还会再次发送消息,所以,此时就需要不断调用select来循环式的等待满足就绪条件的文件描述符,然后对其进行处理。
select
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds:表示一次等待的文件描述符的个数加1;用于限定操作系统遍历的区间
fd_set:该结构实际是一个位图,因为文件描述符其实是数组下标,也就是从0开始的整数,所以对于位图的每一个比特位表示的是一个文件描述符的状态,如果是1表示关心该文件描述符上的事件,如果是0,表示不关心该文件描述符上的事件。而具体关心文件描述符上的什么事件,则由中间三个参数决定。
readfds:可以理解为读事件位图。如果该变量某一个比特位为1,则表示关心该比特位所表示的文件描述符的读事件。如:该变量的第2个比特位为1,表示关心文件描述符为1上的读事件。
writefds:写事件位图
exceptfds:异常事件位图
注意:以上三个位图参数都是输入输出型参数,作为输入参数,表示的是关心的相应事件集合,作为输出参数,表示满足相应就绪条件的事件集合。
timeout:用于设置select阻塞等待的时间,取值如下:
struct timeval { long tv_sec; /* seconds :秒*/ long tv_usec; /* microseconds:微秒 */ };//头文件<sys/time.h>
如果函数调用成功,则返回满足就绪条件的事件个数(大于0)(包含以上三种情形);
如果在规定的时间内没有事件发生,则超时返回0(包含非阻塞和特定的时间值两种情形);
1. 首先要将关心的事件集合存放在上述的三个位图中:
void FD_ZERO(fd_set* set);//将set位图中的所有位设置为0 void FD_SET(int fd,fd_set* set);//将位图set中的相应fd位设置为1 void FD_CLR(int fd,fd_set* set);//将位图set中的相应fd为设置为0 int FD_ISSET(int fd,fd_set* set);//判断位图set中的相应位fd是否为1,为1返回1,不为1返回0
2. 知道如何设置参数之后,开始调用select函数,该函数的执行过程如下:
首先,清空位图集(这里只关心读事件集):FD_AERO(set);
再调用将关心的文件描述符添加到位图中:FD_SET(fd,set);
然后,调用select进行等待:select(nfds,set,NULL,NULL,NULL);(这里为阻塞等待)
最后,select返回后,遍历整个关心的文件描述符集,对输出型参数set进行判断哪些文件描述符上的事件发生了:FD_ISSET(fd,set);
3. 调用select时,要检查相应的事件有无发生,也就是相应的文件描述符上就绪条件是否满足。不同的事件对应不同的就绪条件:
(1)当对内核中的数据进行读取时,如果接收缓冲区中的字节数,大于等于低水位SO_RCVLOWAT,此时就说明读就绪,在对该文件描述符调用read等进行读取时,不会阻塞,且返回值大于0;
(2)在TCP通信中,如果服务器的socket上有新的连接到达时,客户端会发送SYN数据包给服务器,说明读就绪。此时在调用accept接收新连接时,不会阻塞。而且会返回新的文件描述符与客户端进行通信;
(3)TCP通信中,如果对端关闭连接,此时会发送FIN数据包。也说明读就绪,此时在调用read对文件描述符进行读取时,会返回0;
(4)当socket上有未处理的错误时,也说明读就绪,在对文件描述符进行read读取时,会返回-1;
写就绪:
(1)在socket内核中,如果发送缓冲区中的可用字节数大于等于低水位标记SO_SNDLOWAT时,说明写就绪,此时调用write进行写操作时,不会阻塞,且返回值大于0;
(2)当一方要进行写操作(即关心的是写事件),而对端将文件描述符关闭,此时写就绪。调用write时会触发SIGPIPE信号;
(3)当socket使用非阻塞connect连接成功或失败之后,写就绪;
(4)当socket上有未读取的错误时,写就绪,此时write进行写时,会返回-1;
异常就绪:
当socket上收到带外数据时,异常事件就绪。
1. 首先绑定设置监听套接字;对应于listen_server函数
2. 在本程序中,只关心读事件,所以首先要设置一个读事件集readfds。
4. 调用select开始等待事件就绪。
5. 当select返回时,如果返回值为0,说明超时返回,如果返回值为-1.说明select调用出错。如果返回值大于0,则进行如下处理(对应server_fd函数):根据数组中保存的关心的文件描述符集合,判断哪些文件描述符上有事件就绪。
(1)如果就绪的文件描述符是监听套接字,则进行接收连接处理(对应Accept_Server函数);
(2)如果就绪的文件描述符是普通套接字,则进行读取数据处理。(对应Read_Server函数)。
6. 在Accept_Server函数中,调用accept接收新连接。
7. 在Read_Server函数中,调用read来对文件描述符进行数据的读取。此时,read一定不会阻塞。
Select实现TCP服务器。客户端代码与之前的TCP客户端代码相同,完整的代码见:客户端程序
运行结果如下:
服务器端:
客户端:
select的特点:
1. 在select中关心的文件描述符被添加在fd_set位图结构中,所以,fd_set的大小决定了能够关心的最大文件描述符数量。可以通过sizeof(fd_set)来进行查看(fd_set的大小可以调整);
2. 上述也有提到,在调用select之前需要设置一个数组来保存关心的文件描述符集合。作用有如下两方面:
(1)select返回之后,根据数组中的元素来判断关心的文件描述符上的哪些事件就绪了;
(2)select返回之后,会清空fd_set就够,所以在下一次调用select时还需要根据数组来再次设置fd_set来添加关心的文件描述符。
select的缺点:
1. 每次在调用select之前,都要遍历数组将关心的文件描述符添加进fd_set集中;
2. 在调用select时,需要将fd_set集合将用户态拷贝到内核态;然后操作系统还要遍历fd_set集合来等待关心的文件描述符集上的事件发生;
3. 在select调用结束后,还有遍历数组来判定哪些文件描述符上的事件就绪;
4. 因为fd_set大小的限制,所以select所关心的文件描述符的个数有限。