I/O复用技术能使程序同时监听多个文件描述符,这对提高程序性能至关重要。Linux下实现I/O复用的系统调用主要有select、poll、epoll。本节的内容是select。
对I/O复用最直观的理解是小时候使用的纸杯电话。
它的优点是减少了连线长度与纸杯个数。多进程服务器端模型与I/O复用服务器端模型的对比如下图所示,
可以看出复用技术可以减少进程数,提供服务的进程只有一个。
select函数是最具有代表性的实现复用技术服务器端的方法
使用select函数时可以将多个文件描述符集中到一起统一监视,监视的项目如下:
我们把监视项称为事件,发生监视项对应的情况时,称“事件发生”。下图介绍select函数的调用方法和顺序,
select函数使用和普通函数区别较大,需要一些准备工作,而且在还需要查看调用结果,接下来我们按照步骤逐一讲解。
select函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。首先需要将要监视的文件描述符集中到一起,集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类。
使用fd_set位数组执行此操作,最左端的位表示文件描述符0的位置,该位为1表示该文件描述符是监视对象。
使用如下的宏来对fd_set进行注册和更改
我们先介绍下select函数
#include
#include
int select(int maxfd,fd_set * readset,fd_set * writeset,fd_set * exceptset,const struct timeval *timeout);
select函数用来验证三种监视项的变化情况,根据监视项声明三个fd_set型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在调用select函数之前,需要决定下面两件事:
文件描述符的监视范围与select函数的第一个参数有关。实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会加一,故只需将最大的文件描述符值加一再传递到select函数即可,加一是因为文件描述符的值从0开始。
select函数的超时时间与select函数的最后一个参数相关,其中timeval结构体定义如下。
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
select函数在监视的文件描述符发生变化时才返回,如果未发生变化,就会进入阻塞状态,指定超时时间就是为了防止这种情况发生。通过声明上述结构体变量,将秒数填入tv_sec成员,将毫秒数填入tv_usec成员,然后将结构体的地址值传递到select函数的最后一个参数。超时select函数返回0,不设置超时,可以传递NULL。
select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍然为1的位置上的文件描述符发生了变化。
select函数使用示例伪代码如下
fd_set reads, temps;
int result, str_len;
struct timeval timeout;
//①设置文件描述符
FD_ZERO(&reads);//初始化fd_set变量都为0
//②设置监视范围
FD_SET(0, &reads); //监视文件描述符0位置
while (1)
{
temps = reads;//每次调用select剩余位会被初始化为零,所以需要保存初始值
//③设置超时
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//④调用select函数
result = select(1, &temps, 0, 0, &timeout);
//⑤查看结果
if (result == 0)//超时返回0
{
//超时
}
else if(FD_ISSET(0, &temps)//验证变化的文件描述符是否是标准输入
{
//有输入则输出
}
}
完整示例如下
#include
#include
#include
#include
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
//初始化fd_set变量都为0
FD_ZERO(&reads);
//监视文件描述符0位置
FD_SET(0, &reads); // 0 is standard input(console)
/*
timeout.tv_sec=5;
timeout.tv_usec=5000;
*/
while (1)
{
//每次调用select剩余位会被初始化为零,所以需要保存初始值
temps = reads;
//初始化超时
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//select
result = select(1, &temps, 0, 0, &timeout);
if (result == -1)
{
puts("select() error!");
break;
}
else if (result == 0)//超时返回0
{
puts("Time-out!");
}
else//验证变化的文件描述符是否是标准输入
{
if (FD_ISSET(0, &temps))
{
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
运行无任何输入每5秒会提示超时,也可以输入字符串,select函数监视后会读取并输出。
I/O复用服务器端的实现是在之前服务器端的基础上进行更改。关于select的伪代码如下,如果服务端套接字中有变化,将受理连接请求并注册与客户端连接的套接字文件描述符。发生变化的套接字并非服务端套接字时,接收的数据为EOF时需关闭套接字,并从reads中删除相应信息,而接收的数据为字符串时,执行回声服务。
int main(int argc, char *argv[])
{
//初始化fd_set变量都为0
FD_ZERO(&reads);
//监视文件描述符serv_sock
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while (1)
{
//每次调用select剩余位会被初始化为零,所以需要保存初始值
cpy_reads = reads;
//初始化超时
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//select
fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout);
for //查找状态变化的文件描述符
{
if (FD_ISSET(i, &cpy_reads)) //有变化
{
//服务器端套接字有变化
if (i == serv_sock) // connection request!
{
accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);//受理客户端连接请求
FD_SET(clnt_sock, &reads); //注册客户端套接字文件描述符
}
else //接受字符串还是断开连接
{
if (str_len == 0) // close request!
{
FD_CLR(i, &reads); //clear
}
else // read message!
{
// echo!
}
}
}
}
}
close(serv_sock);
return 0;
}
完整代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
void error_handling(char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
//初始化fd_set变量都为0
FD_ZERO(&reads);
//监视文件描述符serv_sock
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while (1)
{
//每次调用select剩余位会被初始化为零,所以需要保存初始值
cpy_reads = reads;
//初始化超时
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//select
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if (fd_num == 0)
continue;
//查找状态变化的文件描述符
for (i = 0; i < fd_max + 1; i++)
{
if (FD_ISSET(i, &cpy_reads))//有变化
{
//服务器端套接字有变化
if (i == serv_sock) // connection request!
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads); //注册客户端套接字文件描述符
if (fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else //接受字符串还是断开连接
{
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) // close request!
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else // read message!
{
write(i, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
使用前几节所用的I/O分割的客户端
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
pid = fork();
if (pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
return;
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}