IO多路複用編程
是同步IO的一種,用一個進程一次等待多個IO就緒事件的發生,加大機率,儘量高效的等。數組
適用場景服務器
(1)當客戶處理多個描述字時(通常是交互式輸入和網絡套接口),必須使用I/O複用。網絡
(2)當一個客戶同時處理多個套接口時,而這種狀況是可能的,但不多出現。多線程
(3)若是一個TCP服務器既要處理監聽套接口,又要處理已鏈接套接口,通常也要用到I/O複用。併發
(4)若是一個服務器即要處理TCP,又要處理UDP,通常要使用I/O複用。socket
(5)若是一個服務器要處理多個服務或多個協議,通常要使用I/O複用。ide
與多進程和多線程技術相比,I/O多路複用技術的最大優點是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。函數
select函數測試
該函數准許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。函數原型以下:
#include <sys/.h>
<sys/>
( maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,返回值:就緒描述符的數目,超時返回0,出錯返回-1
函數參數介紹以下:
(1)第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(所以把該參數命名爲maxfdp1),描述字0、一、2...maxfdp1-1均將被測試。由於文件描述符是從0開始的。
(2)中間的三個參數readset、writeset和exceptset指定咱們要讓內核測試讀、寫和異常條件的描述字。若是對某一個的條件不感興趣,就能夠把它設爲空指針。struct fd_set能夠理解爲一個集合,這個集合中存放的是文件描述符,可經過如下四個宏進行設置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的文件描述符是否能夠讀寫
fd_set結構體是文件描述符集,該結構體其實是一個整型數組,數組中的每一個元素的每一位標記一個文件描述符。fd_set能容納的文件描述符 數量由FD_SETSIZE指定,通常狀況下,FD_SETSIZE等 於1024,這就限制了select能同時處理的文件描述符的總量。
(3)timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
這個參數有三種可能:
1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。爲此,把該參數設置爲空指針NULL。
2)等待一段固定時間:在有一個描述字準備好I/O時返回,可是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
3)根本不等待:檢查描述字後當即返回,這稱爲輪詢。爲此,該參數必須指向一個timeval結構,並且其中的定時器值必須爲0。
(4)返回值狀況:
a)超時時間內,若是文件描述符就緒,select返回就緒的文件描述符總數(包括可讀、可寫和異常),若是沒有文件描述符就緒,select返回0;
b)select調用失敗時,返回 -1並設置errno,若是收到信號,select返回 -1並設置errno爲EINTR。
(5)文件描述符的就緒條件:
在網絡編程中,
1)下列狀況下socket可讀:
a) socket內核接收緩衝區的字節數大於或等於其低水位標記SO_RCVLOWAT;
b) socket通訊的對方關閉鏈接,此時該socket可讀,可是一旦讀該socket,會當即返回0(能夠用這個方法判斷client端是否斷開鏈接);
c) 監聽socket上有新的鏈接請求;
d) socket上有未處理的錯誤。
2)下列狀況下socket可寫:
a) socket內核發送緩衝區的可用字節數大於或等於其低水位標記SO_SNDLOWAT;
b) socket的讀端關閉,此時該socket可寫,一旦對該socket進行操做,該進程會收到SIGPIPE信號;
c) socket使用connect鏈接成功以後;
d) socket上有未處理的錯誤。
selelct原理圖
說明:
一、select只負責等待IO,不負責對IO進行操做,由recv/send等函數進行
二、select一共有兩次系統調用:1)select系統調用 2)recvfrom系統調用
調用select時,會發生如下事情:
從用戶空間拷貝fd_set到內核空間;
註冊回調函數__pollwait;
遍歷全部fd,對所有指定設備作一次poll(這裏的poll是一個文件操做,它有兩個參數,一個是文件fd自己,一個是當設備還沒有就緒時調用的回調函數__pollwait,這個函數把設備本身特有的等待隊列傳給內核,讓內核把當前的進程掛載到其中);
當設備就緒時,設備就會喚醒在本身特有等待隊列中的【全部】節點,因而當前進程就獲取到了完成的信號。poll文件操做返回的是一組標準的掩碼,其中的各個位指示當前的不一樣的就緒狀態(全0爲沒有任何事件觸發),根據mask可對fd_set賦值;
若是全部設備返回的掩碼都沒有顯示任何的事件觸發,就去掉回調函數的函數指針,進入有限時的睡眠狀態,再恢復和不斷作poll,再做有限時的睡眠,直到其中一個設備有事件觸發爲止。
只要有事件觸發,系統調用返回,將fd_set從內核空間拷貝到用戶空間,回到用戶態,用戶就能夠對相關的fd做進一步的讀或者寫操做了。
select優勢
select模型是Windows sockets中最多見的IO模型。它利用select函數實現IO 管理。經過對select函數的調用,應用程序能夠判斷套接字是否存在數據、可否向該套接字寫入據。
如:在調用recv函數以前,先調用select函數,若是系統沒有可讀數據那麼select函數就會阻塞在這裏。當系統存在可讀或可寫數據時,select函數返回,就能夠調用recv函數接 收數據了。
能夠看出使用select模型,須要兩次調用函數。第一次調用select函數第二次socket API。使用該模式的好處是:能夠等待多個套接字。
select缺點
最大併發數限制:使用32個整數的32位,即32*32=1024來標識fd;
效率低:每次都會線性掃描整個fd_set,集合越大速度越慢;
內核/用戶空間內存拷貝問題。
代碼:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int fds[64]; const fds_nums=sizeof(fds)/sizeof(fds[0]); static int startup(const char *ip,int port) { int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("socket"); exit(2); } int opt = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=inet_addr(ip); if(bind(sock,(struct sockaddr *)&local,sizeof(local))<0) { perror("bind"); exit(3); } if(listen(sock,5)<0) { perror("listen"); exit(4); } return sock; } static void usage(const char *proc) { printf("%s [ip] [port]\n",proc); } int main(int argc,char *argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } int listen_sock=startup(argv[1],atoi(argv[2])); fd_set rset; int i=0; FD_ZERO(&rset); FD_SET(listen_sock,&rset); //initial fds for(;i<fds_nums;i++) { fds[i]= -1; } fds[0]=listen_sock; int done=0; while(!done) { //reset current rset int max_fd= -1; for(i=0;i<fds_nums;i++) { if(fds[i]>0) { FD_SET(fds[i],&rset); max_fd=max_fd<fds[i]?fds[i]:max_fd; } } //struct timeval _ti={5,0}; switch(select(max_fd+1,&rset,NULL,NULL,NULL)) { case 0: printf("time out...\n"); break; case -1: perror("select"); break; default: for(i=0;i<fds_nums;i++) { //listen_fd if(i==0&&FD_ISSET(listen_sock,&rset)) { //printf("there\n"); struct sockaddr_in peer; socklen_t len=sizeof(peer); int newfd=accept(listen_sock,(struct sockaddr *)&peer,&len); if(newfd>0) { printf("get a new client$ socket->%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port)); } int j=0; for(j;j<fds_nums;j++) { if(fds[j]== -1) { fds[j]=newfd; break; } } //mfull of queue if(j==fds_nums) { close(newfd); } } else//normal accept_fd { // printf("there\n"); if(FD_ISSET(fds[i],&rset)) { char buf[1024]; memset(buf,'\0',sizeof(buf)); ssize_t _s=read(fds[i],buf,sizeof(buf)-1); if(_s>0) { buf[_s-1]='\0'; printf("client$ %s\n",buf); } else if(_s==0) { printf("%d is read done..\n",fds[i]); close(fds[i]); fds[i]= -1; } else{ perror("read"); } } } } break; } } return 0; }