Linux Select 使用-sinbingzoo-ChinaUnix博客
在Linux中,我们可以使用select函数实现I/O端口的复用,同时监视多个文件描述符变化,同时具备超时返回特点。
传递给 select函数的参数会告诉内核:
* 我们所关心的文件描述符
* 对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)
* 我们要等待多长时间。(我们可以等待无限长的时间,等待固定的一段时间,或者根本就不等待)
从 select函数返回后,内核告诉我们一下信息:
* 对我们的要求已经做好准备的描述符的个数
* 对于三种条件哪些描述符已经做好准备.(读,写,异常)
有了这些返回信息,我们可以调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞.
#include int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout); |
返回:
>0:就绪描述字的正数目
0:超时
-1:出错
首先我们先看一下最后一个参数。它指明我们要等待的时间:
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}
有三种情况:
timeout == NULL 等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。
timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。
timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。
中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。如下图所示:
对于 fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型变量的值,或者使用以下几个宏来控制它
void FD_ZERO (fd_set * void FD_SET (int fd,fd_set *fdset); // turn on the bit for fd in fdset
void FD_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset
FD_ZERO宏将一个 fd_set类型变量的所有位都设为 0,使用FD_SET将变量的某个位置位。清除某个位时可以使用 FD_CLR,我们可以使用 FD_ISSET来测试某个位是否被置位。
(1)intmaxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。
- int sa, sb, sc;
- sa = socket(...); /* 分别创建3个句柄并连接到服务器上 */
- connect(sa,...);
- sb = socket(...);
- connect(sb,...);
- sc = socket(...);
- connect(sc,...);
- FD_SET(sa, &rdfds);/* 分别把3个句柄加入读监视集合里去 */
- FD_SET(sb, &rdfds);
- FD_SET(sc, &rdfds);
(4)fd_set*errorfds同上面两个参数的意图,用来监视文件错误异常文件。
(1)当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数。
(2)当没有满足条件的文件描述符,且设置的timeval监控时间超时时,select函数会返回一个为0的值。
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。
利用select而不是fork来解决socket中的多客户问题,例程如下。服务器端
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <stdio.h>
- #include <netinet/in.h>
- #include <sys/time.h>
- #include <sys/ioctl.h>
- #include <unistd.h>
- int main()
- {
- int server_sockfd, client_sockfd;
- int server_len, client_len;
- struct sockaddr_in server_address;
- struct sockaddr_in client_address;
- int result;
- fd_set readfds, testfds;
- server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
- server_address.sin_family = AF_INET;
- server_address.sin_addr.s_addr = htonl(INADDR_ANY);
- server_address.sin_port = htons(9734);
- server_len = sizeof(server_address);
- bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
- listen(server_sockfd, 5);
- FD_ZERO(&readfds);
- FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
- while(1)
- {
- char ch;
- int fd;
- int nread;
- testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
- printf("server waiting/n");
- /*无限期阻塞,并测试文件描述符变动 */
- result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0);
- if(result < 1)
- {
- perror("server5");
- exit(1);
- }
- /*扫描所有的文件描述符*/
- for(fd = 0; fd < FD_SETSIZE; fd++)
- {
- /*找到相关文件描述符*/
- if(FD_ISSET(fd,&testfds))
- {
- /*判断是否为服务器套接字,是则表示为客户请求连接。*/
- if(fd == server_sockfd)
- {
- client_len = sizeof(client_address);
- client_sockfd = accept(server_sockfd,
- (struct sockaddr *)&client_address, &client_len);
- FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
- printf("adding client on fd %d/n", client_sockfd);
- }
- /*客户端socket中有数据请求时*/
- else
- {
- ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
- /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
- if(nread == 0)
- {
- close(fd);
- FD_CLR(fd, &readfds); //去掉关闭的fd
- printf("removing client on fd %d/n", fd);
- }
- /*处理客户数据请求*/
- else
- {
- read(fd, &ch, 1);
- sleep(5);
- printf("serving client on fd %d/n", fd);
- ch++;
- write(fd, &ch, 1);
- }
- }
- }
- }
- }
- }
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <stdio.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- int main()
- {
- int client_sockfd;
- int len;
- struct sockaddr_in address;//服务器端网络地址结构体
- int result;
- char ch = 'A';
- client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket
- address.sin_family = AF_INET;
- address.sin_addr.s_addr = inet_addr(“127.0.0.1”);
- address.sin_port = 9734;
- len = sizeof(address);
- result = connect(client_sockfd, (struct sockaddr *)&address, len);
- if(result == -1)
- {
- perror("oops: client2");
- exit(1);
- }
- write(client_sockfd, &ch, 1);
- read(client_sockfd, &ch, 1);
- printf("char from server = %c/n", ch);
- close(client_sockfd);
- zexit(0);
- }
Linux下select使用陷阱
select基本可以满足大部分的应用需求,如果连接数很大(几万或者几十万的连接数) select 将不再适合了;
Select函数使用简单,其工作原理大家通常也知道,但是在实际的使用过程中可能并没有严格遵守,而且确实也比较难以完全遵守,除非不使用它。
Select采用一个bit表,每个fd对应表中的一个bit位,宏FD_SETSIZE为表的大小,添加到fd_set中的fd值必须小于FD_SETSIZE,否则就会越界,假设有如下一段代码:
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
那么,这里的fd必须满足:fd < FD_SETSIZE,否则即会发生越界,使用valgrind和purify等内存检测工具能够检测到这个问题,但通常很少人去注意,会认为是一个可以忽略的warning,其后果是导致某个不能理解的crash问题。
通过ulimit命令和setrlimit函数来修改进程内句柄数的限制,并不会影响FD_SETSIZE的值,所以即使通过ulimit命令或setrlimit函数将进程允许的句柄改成很大了,但如果FD_SETSIZE值没有修改,则仍可能发生crash。
在什么情况下最容易遇到这个问题?
较容易发生在服务端程序中,因为服务端程序同一时刻的连接数很容易超过默认的FD_SETSIZE值,而服务端的代码可能是使用epoll使用的,所以它本身并不会存在问题,但是程序中可能还有个客户端,比如使用了select来实现超时连接,这个时候问题就来了,当连接数超过FD_SETSIZE时,超时连接处的select调用就发生了越界,进程就会在某个可能完全不相干的地方crash,要定位这个问题的成本是很高的,不具备一定经验,很难在短时间内定位出来。
如何去避免这个问题了?那就是尽量不使用select,而应当使用更安全的poll函数来替代,因为poll使用的数组是调用者自己维护的,完全可以保证不越界。