IO復用
我們首先來看看服務器編程的模型,客戶端發來的請求服務端會產生一個進程來對其進行服務,每當來一個客戶請求就產生一個進程來服務,然而進程不可能無限制的產生,因此為了解決大量客戶端訪問的問題,引入了IO復用技術。
即:一個進程可以同時對多個客戶請求進行服務。
也就是說IO復用的“介質”是進程(准確的說復用的是select和poll,因為進程也是靠調用select和poll來實現的),復用一個進程(select和poll)來對多個IO進行服務,雖然客戶端發來的IO是並發的但是IO所需的讀寫數據多數情況下是沒有准備好的,因此就可以利用一個函數(select和poll)來監聽IO所需的這些數據的狀態,一旦IO有數據可以進行讀寫了,進程就來對這樣的IO進行服務。
IO多路復用指內核一旦發現進程指定的一個或者多個IO條件准備讀取,它就通知該進程。
IO多路復用適用如下場合:
1.當客戶處理多個描述字時(一般是交互式輸入和網絡套接口),必須使用I/O復用。
2.當一個客戶同時處理多個套接口時,而這種情況是可能的,但很少出現。
3.如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O復用。
4.如果一個服務器即要處理TCP,又要處理UDP,一般要使用I/O復用。
5.如果一個服務器要處理多個服務或多個協議,一般要使用I/O復用。
與多進程和多線程技術相比,I/O多路復用技術的最大優勢是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。
select函數
該函數允許進程指示內核等待多個事件中的任何一個發生,並只在有一個或多個時間發生或經歷一段指定的時間后才喚醒他。
#include <sys/select.h> #include >sys/time.h> int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
返回值:
若有就緒描述符返回其數目,若超時則為0,若出錯則為-1
參數
第一個參數——int maxfdp1
第一個參數maxfdp1指定待測試的描述字個數。
它的值是待測試的最大描述字加1(因此把該參數命名為maxfdp1),描述字0、1、2…maxfdp1-1均將被測試。
因為文件描述符是從0開始的。
fd_set *readset
fd_set *writeset
fd_set *exceptset
中間的三個參數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); // 檢查集合中指定的文件描述符是否可以讀寫
const struct timeval *timeout
timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds };
這個參數有三種可能:
1.永遠等待下去:僅在有一個描述字准備好I/O時才返回。為此,把該參數設置為空指針NULL。
2.等待一段固定時間:在有一個描述字准備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
3.根本不等待:檢查描述字后立即返回,這稱為輪詢。為此,該參數必須指向一個timeval結構,而且其中的定時器值必須為0。
select函數的調用過程
(1)使用copy_from_user從用戶空間拷貝fd_set到內核空間
(2)注冊回調函數__pollwait
(3)遍歷所有fd
調用其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會調用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面注冊的回調函數。
(5)__pollwait的主要工作就是把current(當前進程)掛到設備的等待隊列中,不同的設備有不同的等待隊列,對於tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中並不代表進程已經睡眠了)。在設備收到一條消息(網絡設備)或填寫完文件數據(磁盤設備)后,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。
(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。
(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是current)進入睡眠。當設備驅動發生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調用select的進程會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。
(8)把fd_set從內核空間拷貝到用戶空間。
select睡眠和喚醒過程
select巧妙的利用等待隊列機制讓用戶進程適當在沒有資源可讀/寫時睡眠,有資源可讀/寫時喚醒。
select睡眠過程
select會循環遍歷它所監測的fd_ set內的所有文件描述符對應的驅動程序的poll函數。
驅動程序提供的poll函數首先會將調用select的用戶進程插入到該設備驅動對應資源的等待隊列(如讀/寫等待隊列),然后返回一個bitmask告訴select當前資源哪些可用。
當select循環遍歷完所有fd_set內指定的文件描述符對應的poll函數后,如果沒有一個資源可用(即沒有一個文件可供操作),則select讓該進程睡眠,一直等到有資源可用為止,進程被喚醒(或者timeout)繼續往下執行。
select喚醒過程
喚醒該進程的過程通常是在所監測文件的設備驅動內實現的。
驅動程序維護了針對自身資源讀寫的等待隊列。當設備驅動發現自身資源變為可讀寫並且有進程睡眠在該資源的等待隊列上時,就會喚醒這個資源等待隊列上的進程。
select的缺點
1.每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
2.同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
3.select支持的文件描述符數量太小了,默認是1024
使用select函數寫的服務器代碼如下:
chata
1 #include "func.h" 2 3 int main(int argc,char* argv[]) 4 { 5 if(argc!=3) 6 { 7 printf("error args\n"); 8 return -1; 9 } 10 int fdr,fdw; 11 fdr=open(argv[1],O_RDONLY); 12 fdw=open(argv[2],O_WRONLY); 13 printf("fdr=%d,fdw=%d\n",fdr,fdw); 14 char buf[128]={0}; 15 //當管道里沒有數據時,read會阻塞 16 int ret; 17 fd_set rdset; 18 while(1) 19 { 20 FD_ZERO(&rdset);//清空集合 21 FD_SET(0,&rdset); 22 FD_SET(fdr,&rdset); 23 ret=select(fdr+1,&rdset,NULL,NULL,NULL); 24 if(ret>0) 25 { 26 if(FD_ISSET(fdr,&rdset)) 27 { 28 memset(buf,0,sizeof(buf)); 29 ret=read(fdr,buf,sizeof(buf)); 30 if(0==ret) 31 { 32 printf("byebye\n"); 33 break; 34 } 35 printf("%s\n",buf); 36 } 37 if(FD_ISSET(0,&rdset)) 38 { 39 memset(buf,0,sizeof(buf)); 40 ret=read(0,buf,sizeof(buf)); 41 if(ret==0) 42 { 43 printf("byebye\n"); 44 break; 45 } 46 write(fdw,buf,strlen(buf)-1); 47 } 48 } 49 } 50 close(fdr); 51 close(fdw); 52 return 0; 53 }
chatb
1 #include "func.h" 2 3 int main(int argc,char* argv[]) 4 { 5 if(argc!=3) 6 { 7 printf("error args\n"); 8 return -1; 9 } 10 int fdw,fdr,ret; 11 fdw=open(argv[1],O_WRONLY); 12 fdr=open(argv[2],O_RDONLY); 13 printf("fdw=%d,fdr=%d\n",fdw,fdr); 14 char buf[128]={0}; 15 fd_set rdset; 16 while(1) 17 { 18 FD_ZERO(&rdset);//清空集合 19 FD_SET(0,&rdset); 20 FD_SET(fdr,&rdset); 21 ret=select(fdr+1,&rdset,NULL,NULL,NULL); 22 if(ret>0) 23 { 24 if(FD_ISSET(fdr,&rdset)) 25 { 26 memset(buf,0,sizeof(buf)); 27 ret=read(fdr,buf,sizeof(buf)); 28 if(0==ret) 29 { 30 printf("byebye\n"); 31 break; 32 } 33 printf("%s\n",buf); 34 } 35 if(FD_ISSET(0,&rdset)) 36 { 37 memset(buf,0,sizeof(buf)); 38 ret=read(0,buf,sizeof(buf)); 39 if(ret==0) 40 { 41 printf("byebye\n"); 42 break; 43 } 44 write(fdw,buf,strlen(buf)-1); 45 } 46 } 47 } 48 return 0; 49 }
當數據量足夠多時,slect模型的資源消耗會大幅提升,poll模型和slect差不多,因此多數時候選擇epoll模型,可參見下一篇……
原博客來源:https://blog.csdn.net/lixungogogo/article/details/52219951