多路選擇I/O提供另外一種處理I/O的方法,相比於傳統的I/O方法,這種方法更好,更具備效率。多路選擇是一種充分利用系統時間的典型。數組
一、多路選擇I/O的概念服務器
當用戶須要從網絡設備上讀數據時,會發生的讀操做通常分爲兩步。網絡
(1)等待數據準備好,等待數據的到達,而且將其複製到內核的緩衝區,該緩衝區在系統態。socket
(2)複製數據,將數據從內核緩衝區中複製到用戶指定的緩衝區中。ide
通常的讀操做形式爲:函數
Int nbytes = read(sfd, buf, MAX); spa
若是須要的數據沒有準備好,例如,數據還沒有到達時,read函數發生阻塞,直到全部的數據到達,read函數纔將其複製到用戶指定的緩衝區,而且返回。若是數據一直未到達,那麼read函數將一直阻塞下去,該進程會陷入殭屍狀態、這種I/O模型稱爲阻塞I/O。rest
爲了防止I/O阻塞使進程進入僵死狀態,可使用多路選擇I/O。code
這種方法的思想是先構造一張須要讀取文件描述符的表,調用一個函數輪循這個表中的文件描述符,知道有一個文件描述符能夠讀寫該函數才返回,多路選擇I/O須要使用兩個系統調用,一個負責檢查並返回可用的文件描述符;另外一個負責對該文件描述符進行讀寫。blog
二、實現多路選擇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);
第二、三、4這3個參數是readfds、writefds和exceptfds,分別表示用戶關心的可讀、可寫和異常的各個描述符,這3個參數是3個位向量,每一位對應一個文件描述符的狀態,每一位對應一個文件描述符的狀態。
第5個參數表示用戶指望等待的時間。若是tvptr==NULL表示一直等。
返回值:出錯返回-1,返回0表示沒有設備準備好,返回值大於0表示準備好的設備數目。
在這個函數中第二、三、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; }