select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述上的可读、可写和异常等事件。
select 系统调用的原型如下:
#include<sys/select.h> int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);参数解释: 1)nfds参数指定被监听的文件描述符的总数。通常设置为 select 监听的所有文件描述符中的最大值加1,这是因为文件描述符是从0开始计数的。
2)readfds、writefds 和 exceptfds 参数分别指可读、可写和异常等事件对应的文件描述符集合。select 调用返回时,内核将修改它们来通知应用程序那些文件描述符已经就绪。这3个参数是fd_set结构指针类型。fd_set 结构体的定义如下:
#include<typesizes.h> #define __FD_SETSIZE 1024 #include<sys/select.h> #define FD_SETSIZE __FD_SETSIZE typedef long int __fd_mask; #undef __NFDBITS #define __NFDBITS (8 * (int)sizeof(__fd_mask)) typedef struct { #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; #define __FDS_BITS(set) ((set)->fds_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; #define __FDS_BITS(set) ((set)->fds_bits) #endif }fd_set;fd_set 结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。
需要注意的一点是:
fd_set能容纳的文件描述符数量由 FD_SETSIZE 指定,其限制了select能同时处理的文件描述符的总量。
为了便于操作,我们使用下面的一系列宏(MACROS)来访问 fd_set 结构体中的位:
FD_ZERO(fd_set *fdset); /*清除 fdset 的所有位 */ FD_SET(int fd,fd_set *fdset); /*设置 fdset 的位 fd */ FD_CLR(int fd,fd_set *fdset); /*清除 fdset 的位 fd */ int FD_ISSET(int fd,fd_set *fdset); /*测试 fdset 的位 fd 是否被设置*/3)timeout 参数用来设置 select 函数的超时时间,它是一个 timeval 结构类型的指针。它的结构体定义如下:
struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微秒 */ };需要注意的是:select 调用失败时 timeout 的值是不确定的,所以我们不能完全相信 timeout 的值。
如果参数 timeout 的值为0,则 select 立即返回。 如果参数 timeout 的值为 NULL,则 select 一直阻塞,直到某个文件描述符就绪。
select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果超时,则返回 0 。失败返回 -1 并设置 errno。如果在 select 等待期间,程序接收到信号,则 select 立即返回 -1,并设置 errno 为 EINTR。
在网络编程中,下列情况下 socket 可读:
socket 内核接收缓存区中的字节数大于或等于其低水位标记 SO_RCVLOWAT。此时,我们可以无阻塞地读该 socket,并且读操作返回的字节数大于 0。socket 通信的对方关闭连接。此时对该 socket 的读操作将返回 0。监听 socket 上有新的连接请求。socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。下列情况下 socket 可写:
socket 内核发送缓冲区中的可用字节数大于或等于其低水位标记 SO_SNDLOWAT。此时,我们可以无阻塞地写该 socket,并且写操作返回的字节数大于 0。socket 的写操作被关闭。对于写操作被关闭的 socket 执行写操作将触发一个 SIGPIPE 信号。socket 使用非阻塞 connect 连接成功或者失败(超时)之后。socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。网络程序中, select 能处理的异常情况只有一种:socket 上接收到带外数据。
socket 上接收到普通数据和带外数据(点击查看)都将使 select 返回,但 socket 处于不同的就绪状态:前者处于可读状态,后者处于异常状态。下面是 socket 同时接收普通数据和带外数据的一个例子:
#include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<assert.h> #include<stdio.h> #include<unistd.h> #include<errno.h> #include<string.h> #include<fcntl.h> #include<stdlib.h> #define BACKLOG 5 int main(int argc,char *argv[]) { if(argc <= 2) { printf("usage: %s ip_address port_number\n",basename(argv[0])); return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in address; inet_pton(AF_INET,ip,&address.sin_addr); address.sin_port = htons(port); int listenfd = socket(AF_INET,SOCK_STREAM,0); assert(listenfd >= 0); ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address)); assert(ret != -1); ret = listen(listenfd,BACKLOG); assert(ret != -1); struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_ad drlength); if(connfd < 0) { printf("errno is : %d\n",errno); close(listenfd); } char buf[1024]; fd_set read_fds; fd_set exception_fds; FD_ZERO(&read_fds); FD_ZERO(&exception_fds); while(1) { memset(buf,'\0',sizeof(buf)); FD_SET(connfd,&read_fds); FD_SET(connfd,&exception_fds); ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL); if(ret < 0) { printf("selection failure\n"); break; } if(FD_ISSET(connfd,&read_fds)) { ret = recv(connfd,buf,sizeof(buf)-1,0); if(ret <= 0) break; printf("get %d bytes of normal data:%s\n",ret,buf); }else if(FD_ISSET(connfd,&exception_fds)) { ret = recv(connfd,buf,sizeof(buf)-1,MSG_OOB); if(ret <= 0) break; printf("get %d bytes of oob data: %s\n",ret,buf); } } close(connfd); close(listenfd); return 0; }