15.4.1 select系统调用
在编写linux应用程序时,经常会 遇到需要检查好几个输入的状态才能确定下一步行动的情况.例如,像终端仿真器这样的通信程序,需要有效地同时读取键盘和串行口.如果是在一个单用户系统中,运行一个"忙等待"循环还是可以接受的,它不停地扫描输入设置看是否有数据,如果有数据到达就读取它,但这种做法很消耗CPU的时间.select系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或输出的完成).这意味着终端仿真程序可以一直阻塞到有事情可做为止.类似地,服务器也可以通过同时在多个打开的套接字上等待请求到来的方法来处理多个客户.
select函数对数据结构fd_set进行操作,它是由打开的文件描述符构成的集合,有一组定义好的宏可以用来控制这些集合:
#include <sys/types.h>
#include <sys/time.h>
void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ISSET(int fd, fd_set *fdset);
FD_ZERO用于将fd_set初始化为空集合.
FD_SET和FD_CLR分别用于在集合中设置和清除由参数fd传递的文件描述符.
如果FD_ISSET宏中由参数fd指向的文件描述符是由参数fdset指向的fd_set集合中的一个元素,FD_ISSET将返回非零值.
fd_set结构中可以容纳的文件描述符的最大数目由常量FD_SETSIZE指定.
select函数还可以用一个超时值来防止无限期的阻塞,这个超时值是由一个timeval结构给出,这个结构定义在文件件sys/time.h中,它由一下几个成员组成:
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
类型time_t在头文件sys/types.h中被定义为一个整数类型.
select系统调用的原型如下所示:
#include <sys/types.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
select调用用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态,它将阻塞以等待某个文件描述符进入上述这些状态.
参数nfds指定需要测试的文件描述符的数目,测试的描述符访问从0到nfds-1.3个描述符集合都可以被设为空指针,着表示不执行相应的测试.
select函数会发生以下情况时返回:readfds集合中描述符可读,writefds集合中有描述符可写或errorfds集合中有描述符遇到错误条件,如果这3种情况都没有发生,select将在timeout指定的超时时间经过后返回. 如果timeout参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下去.
当select返回时,描述符集合将被修改以指示哪些描述符正处于可读,可写或者错误的状态.可以用FD_ISSET对描述符进行测试,来找到需要注意的描述符.可以修改timeout值来表明剩余的超时时间,但这并不是在X/Open规范中定义的行为.如果select是因为超时而返回的话,所有描述符集合都将被清空.
select调用返回状态发生变化的描述符总数.失败时它将返回-1并设置errno来描述错误.可能出现的错误有:EBADF(无效的描述符),EINTR(因中断而返回),EINVAL(nfs或timeout取值错误)
程序 select系统调用
下面这个程序select.c演示了select函数的使用方法,它读取键盘(即标准输入--文件描述符为0),超时时间设为2.5秒,它只有在输入就绪时才读取键盘.它可以很容易地通过添加其他描述符(如串行线,管道,套接字等)进行扩展,具体做法取决于应用程序的需要.编写程序select.c
/*************************************************************************
> File Name: select.c
> Description: select.c
> Author: Liubingbing
> Created Time: 2015年07月26日 星期日 10时11分41秒
> Other: select.c
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main()
{
char buffer[128];
int result, nread;
fd_set inputs, testfds;
struct timeval timeout;
/* FD_ZERO用于将fd_set初始化空集合 */
FD_ZERO(&inputs);
/* FD_SET用于在集合中设置由第一个参数传递的文件描述符,参数0为标准输入 */
FD_SET(0, &inputs);
/* 在标准stdin上最多等待输入2.5秒 */
while (1) {
testfds = inputs;
/* select函数可以用一个超时值来防止无限期的阻塞,这个超时值由一个timeval结构给出
* tv_sec指出秒值
* tv_usec指出微秒值 */
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
/* select函数用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态
* 它将阻塞以等待某个文件描述符进入这些状态.
* 第一个参数nfds指定需要测试的文件描述符数目,测试的描述符范围从0到nfds-1.
* 第二个,第三个,第四个参数分别为3个描述符集合readfds,writefds,errorfds.
* 3个描述符集合都可以被设置为空指针,这表示不执行相应的测试.
* 如果select调用进入这3种状态之一,则它立刻返回.如果没有进入这些状态,select调用在timeout指定的超时时间经过后返回.
* 第五个参数指定select调用的超时时间.
* 如果timeout参数是一个空指针并且套接字上也没有任何活动,这个调用将一直阻塞下去.
* 如果成功,select调用返回状态发生变化的描述符总数.如果失败,它将返回-1 */
result = select(FD_SETSIZE, &testfds, (fd_set *)NULL, (fd_set *)NULL, &timeout);
switch (result) {
case 0:
printf("timeout\n");
break;
case -1:
perror("select");
exit(1);
default:
/* FD_ISSET由第一个参数0指向的文件描述符是由第二个参数&testfds指向的fd_set集合中的一个元素,FD_ISSET将返回非零值
* fd_set结构中可以容纳的文件描述符的最大数目由常量FD_SETSIZE指定 */
if (FD_ISSET(0, &testfds)) {
ioctl(0, FIONREAD, &nread);
if (nread == 0) {
printf("keyboard done\n");
exit(0);
}
/* read函数从0(标准输入中)读入nread个字节的数据到buffer指向的内存中
* 返回实际读入数据的字节数 */
nread = read(0, buffer, nread);
buffer[nread] = 0;
printf("read %d from keyboard: %s", nread, buffer);
}
break;
}
}
}
运行这个程序时,它会每隔2.5秒打印出一个timeout.如果在键盘上敲入字符,它就会从标准输入读取数据并报告输入的内容.对大多数shell来说,输入会在用户按下回车键或某个控制序列时被发送给程序,所以这个程序将在按下回车键时把输入内容显示出来.注意,回车键本身也像其他字符一样被读取和处理.
程序解析
这个程序用select调用来检查标准输入的状态,程序通过事先安排的超时时间每隔2.5秒打印出一个timeout信息,这是通过select调用返回0来判断的.在文件的结尾,标准输入描述符被标记为可读,但没有字符可以读取.15.4.2 多客户
服务器程序可以从select调用中获取益处,通过用select调用来同时处理多个客户就无需再依赖于子进程了.但在把这个技巧应用到实际的应用程序中时,必须要注意,不能在处理第一个连接的客户时让其他客户等太长的时间.服务器可以让select调用同时检查监听套接字和客户的连接套接字.一旦select调用指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个上面有活动发生.
如果是监听套接字可读,这说明正有一个客户试图建立连接,此时就可以调用accept而不用担心发生阻塞的可能.如果是某个客户描述符准备好,这说明该描述符上有一个客户请求需要读取和处理.如果读操作返回零字节,这表示有一个客户进程已结束,可以关闭该套接字并把它从描述符集合中删除.
编写程序server5.c,使用头文件sys/time.h和sys/ioctl.h替换上一个程序中的signal.h,并且为select调用定义了一些变量.
/*************************************************************************
> File Name: server5.c
> Description: server5.c
> Author: Liubingbing
> Created Time: 2015年07月26日 星期日 13时12分24秒
> Other: server5.c
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.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;
/* socket函数创建一个套接字,返回一个套接字描述符
* 第一个参数指定协议族,套接字通信中使用的网络介质
* 第二个参数指定用于新套接字的通信类型,SOCK_STREAM是一个有序,可靠,面向连接的双向字节流
* 第三个参数指定协议,一般默认为0 */
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* 每个套接字都有自己的地址格式,AF_INET域中,套接字地址由结构sockaddr_in指定.
* 一个AF_INET套接字由它的域,IP地址和端口号完全确定 */
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函数命名套接字,以使AF_INET套接字关联到一个IP端口号
* bind调用需要将一个特定的结构指针转换为指向通用地址类型(struct sockaddr *)
* bind调用实际上就是把创建的套接字和套接字地址联系起来 */
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
/* listen函数创建一个队列来保存未处理的请求. */
listen(server_sockfd, 5);
/* FD_ZERO用于将readfds初始化为空集合 */
FD_ZERO(&readfds);
/* FD_SET用于在集合中设置由参数server_sockfd传递的文件描述符 */
FD_SET(server_sockfd, &readfds);
/* 现在开始等待客户和请求的到来.因为给timeout参数传递了一个空指针,所以select调用将不会发生超时.
* 如果select调用的返回值小于1,程序将退出并报告出现的错误 */
while (1) {
char ch;
int fd;
int nread;
testfds = readfds;
printf("server waiting\n");
/* select函数用于测试文件描述符集合中,是否有一个文件描述符已处于可读状态或可写状态或错误状态
* select调用将阻塞以等待某个文件描述符进入上述这些状态.
* 第一个参数指定需要测试的文件描述符数目
* 第二个,第三个,第四个参数分别为描述符集合readfds,writefds,errorfds.
* 3个描述符集合可以被设置为空指针,表示不执行相应的测试
* 第五个参数是timeval指针,指定select调用的超时时间
* 返回状态发生变化的描述符总数,失败时返回-1.
* FD_ISSERT对发生变化的描述符进行测试 */
result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);
if (result < 1) {
perror("server5");
exit(1);
}
/* 一旦得知有活动发生,可以用FD_ISSET来依次检查每个描述符,以发现活动发生在哪个描述符上 */
for (fd = 0; fd < FD_SETSIZE; fd++) {
/* 如果FD_ISSET中由第一个参数fd指向的文件描述符是由第二个参数&testfds指向的fd_set集合中的一个元素,FD_ISSET将返回非零值 */
if (FD_ISSET(fd, &testfds)) {
/* 如果活动是发生在套接字server_sockfd上,它肯定是一个新的连接请求
* 因此就把相关的client_sockfd添加到描述符集合中 */
if (fd == server_sockfd) {
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len);
/* FD_SET用于在集合中设置由第一个参数client_sockfd传递的文件描述符 */
FD_SET(client_sockfd, &readfds);
printf("adding client on fd %d\n", client_sockfd);
}
/* 如果活动不是发生在服务器套接字上,那肯定是客户的活动. */
else {
ioctl(fd, FIONREAD, &nread);
/* 如果接收到的活动是close,就说明客户已经立刻,可以把客户的套接字从描述符集合中删除 */
if (nread == 0) {
close(fd);
/* FD_CLR用于在集合中清除由第一个参数fd传递的文件描述符 */
FD_CLR(fd, &readfds);
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);
}
}
}
}
}
}
运行服务器的这个版本,它将在一个进程中对多个客户一次进行处理.如下所示:
下面对套接字连接和电话接入进行对比:
电话 套接字
给公司打电话,号码是021-888 连接到IP地址为127.0.0.1
接线员接听电话 建立起到远程主机的连接
要求转到财务处 转到指定端口(9734)
财务转换接听电话 服务器从select调用返回
电话转给免费账号经理 服务器调用accept,在456编号上创建新的套接字