多路选择I/O提供另一种处理I/O的方法,相比于传统的I/O方法,这种方法更好,更具有效率。多路选择是一种充分利用系统时间的典型。
1、多路选择I/O的概念
当用户需要从网络设备上读数据时,会发生的读操作一般分为两步。
(1)等待数据准备好,等待数据的到达,并且将其复制到内核的缓冲区,该缓冲区在系统态。
(2)复制数据,将数据从内核缓冲区中复制到用户指定的缓冲区中。
一般的读操作形式为:
Int nbytes = read(sfd, buf, MAX);
如果需要的数据没有准备好,例如,数据尚未到达时,read函数发生阻塞,直到所有的数据到达,read函数才将其复制到用户指定的缓冲区,并且返回。如果数据一直未到达,那么read函数将一直阻塞下去,该进程会陷入僵尸状态、这种I/O模型称为阻塞I/O。
为了防止I/O阻塞使进程进入僵死状态,可以使用多路选择I/O。
这种方法的思想是先构造一张需要读取文件描述符的表,调用一个函数轮循这个表中的文件描述符,知道有一个文件描述符可以读写该函数才返回,多路选择I/O需要使用两个系统调用,一个负责检查并返回可用的文件描述符;另一个负责对该文件描述符进行读写。
2、实现多路选择I/O
Linux环境下使用select函数实现多路选择I/O,函数原型如下:
Int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);
头文件: #include <sys/select.h>
参数说明:
第一个参数maxfdp1表示所关心状态的描述符的个数,正确解释是最大描述符加1。如果maxfdp1的值是2,表示用户关心的描述符数为2;最大的文件描述符为1时,描述符0,和1都会被该函数茶韵,大于1则不关心。一个进程最多可以用于1024个文件描述符,因此maxfdp1的值为(0~1023);
第2、3、4这3个参数是readfds、writefds和exceptfds,分别表示用户关心的可读、可写和异常的各个描述符,这3个参数是3个位向量,每一位对应一个文件描述符的状态,每一位对应一个文件描述符的状态。
第5个参数表示用户期望等待的时间。如果tvptr==NULL表示一直等。
返回值:出错返回-1,返回0表示没有设备准备好,返回值大于0表示准备好的设备数目。
在这个函数中第2、3、4这三个参数是特殊的参数,这三个参数是fd_set数据类型的,fd_set本质上市一个位向量,是一个无符号的整型。其中每一位代表一个设备的状态,如果是1表示被设置,如果是0表示没有被设置。上边的的三个参数分别代表的是可读、可写和异常三种状态,这三个位向量中的每一位代表一个状态。比如readfds是“111000”表示前3个文件可读,后三个文件不可读。
最后通过一个实例来演示使用select函数同时处理多个连接请求的服务器端程序。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <ctype.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include "iolib.h" #define MAX_LINE 80 int port = 8000; void my_fun(char *p) { if(p == NULL) exit(1); for(; *p != ‘\0‘; p++) if(*p >= ‘A‘ && *p <= ‘Z‘) *p = *p - ‘A‘ + ‘a‘; } int main(void) { struct sockaddr_in sin; struct sockaddr_in cin; int lfd; int cfd; int sfd; int rdy; int client[FD_SETSIZE]; ///客户端连接的套接字描述符数组 int maxi; int maxfd; ///最大连接数 fd_set rest; fd_set allset; socklen_t addr_len; ///地址结构的长度 char buf[MAX_LINE]; char addr_p[INET_ADDRSTRLEN]; int i, n; int len; int opt = 1; ///套接字选项 bzero(&sin, sizeof(sin)); ///填充地址结构 sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = hton(port); /*创建一个面向连接的套接字*/ lfd = socket(AF_INET, SOCK_STREAM, 0); if(lfd == -1) { perror("call to sock"); exit(1); } /*设置套接字选项,使用默认选项*/ setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); /*绑定套接字到地址结构*/ n = bind(lfd, (struct sockaddr_in *) &sin, sizeof(opt)); if(n == -1) { perror("call to bind"); exit(1); } /*开始监听连续请求*/ n = listern(lfd, 20); if(n == -1) { perror("call to listern"); exit(1); } printf("Accepting connecting ...\n"); maxfd = lfd; ///对最大文件描述符进行初始化 maxi = -1; for(i = 0; i < FD_SETSIZE; i++) ///初始化客户端连接描述符集合 client[i] = -1; FD_ZERO(&allset); ///清空文件描述符集合 FD_SET(lfd, &allset); ///将监听接字设置在集合内 /*开始服务器程序的死循环*/ while(1) { rset = allset; /*得到当前可以读的文件描述符*/ rdy = select(maxfd + 1, &rset, NULL, NULL, NULL); if(FD_ISSET(lfd, &rest)) { addr_len = sizeof(cin); /*创建一个链接描述符*/ cfd = accept(lfd, (struct sockaddr_in *) &cin, &addr_len); if(cfd == -1) { perror("fail to accept"); exit(1); } /*查找一个空闲的位置*/ for(i = 0; i < FD_SETSIZE; i++) { if(client[i] < 0) { client[i] = cfd; break; } } /*太多的客户端连接,服务器拒绝连接,跳出循环*/ if(i == FD_SETSIZE) { printf("too many clients\n"); exit(1); } FD_SET(cfd, &allset); ///设置连接集合 if(cfd > max_fd) maxfd = cfd; if(i > maxi) maxi = i; if(--rdy <= 0) continue; } for(i = 0; i < maxi; i++) ///对每一个连接描述符做处理 { if((sfd = client[i] < 0)) continue; if(FD_ISSET(sfd, &rset)) { n = my_read(sfd, buf, MAX_LINE); ///读取数据 if(n == 0) { printf("the other side has been closed\n"); fflush(stdout); ///刷新到输出终端 close(sfd); FD_CLR(sfd, &allset); ///清空连接描述符数组 client[i] = -1; } else { inet_ntop(AF_INET, &cin.sin_addr, addr_p, sizeof(addr_p)); printf("client IP is %s, port is %d\n", addr_p, ntohs(cin.sin_port)); my_fun(buf); n = my_write(sfd, buf, len + 1); if(n == -1) exit(1); } if(--rdy <= 0) break; } } } close(lfd); return 0; }