select
在五種IO模型中我們認識了select,用于實作多路複用輸入/輸出模型。
回憶一下其作用:
1. select系統調用是用來讓我們的程式監視多個檔案描述符的狀态變化的;
2. 程式會停在select這裡等待,直到被監視的檔案描述符有一個或多個發生了狀态改變。
函數原型

參數解釋:
- 參數nfds是需要監視的最大的檔案描述符值+1;
- readfds,writefds,exceptfds分别對應于需要檢測的可讀檔案描述符的集合,可寫檔案描述符的集合及異常檔案描述符的集合;
- 參數timeout為結構timeval,用來設定select()的等待時間
參數timeout取值:
- NULL:則表示select()沒有timeout,select将一直被阻塞,直到某個檔案描述符上發生了事件;
- 0:僅檢測描述符集合的狀态,然後立即傳回,并不等待外部事件的發生。
- 特定的時間值:如果在指定的時間段裡沒有事件發生,select将逾時傳回.
fd_set結構
注意到select函數中參數類型出現了fd_set結構,其實它是一個”位圖”,使用位圖中對應的位來表示要監視的檔案描述符。
它的大小由系統決定,是有上限的,是以表示的檔案描述符也是有上限的。
系統中提供了一組操作fd_set的接口, 來比較友善的操作位圖:
函數 | 作用 |
FD_CLR | 用來清除描述詞組set中相關fd的位 |
FD_ISSET | 用來測試描述詞組set中相關fd的位是否為真 |
FD_SET | 用來設定描述詞組set中相關fd的位 |
FD_ZERO | 用來清除描述詞組set的全部位 |
函數傳回值:
特定的數:執行成功則傳回檔案描述詞狀态已改變的個數
0:如果傳回0代表在描述詞狀态改變前已超過timeout時間,沒有傳回
-1:當有錯誤發生時則傳回-1,錯誤原因存于errno,此時參數readfds,writefds, exceptfds和timeout 的值變成不可預測。
舉例select執行過程
上文提到過fd_set為位圖,用來監視檔案描述符集。要了解select模型,我們首先要了解fd_set。
比如:fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd.
(1)執行fdset set; FDZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執行 FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變為0001,0011
(4)執行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發生可讀事件,則select傳回,此時set變為0000,0011。
注意:沒有事件發生的fd=5被清空。且fd_set的大小由系統決定,是有上限的,是以表示的檔案描述符也是有上限的
socket就緒條件
從select函數原型我們可知,select監視讀、寫、或者異常條件就緒。
那麼在什麼情況下,滿足就緒條件呢?
讀就緒:
- socket核心中,接收緩沖區中的位元組數,大于等于低水位标記SO_RCVLOWAT. 此時可以無阻塞的讀該檔案描述符, 并且傳回值大于0;
- socket TCP通信中, 對端關閉連接配接, 此時對該socket讀, 則傳回0;
- 監聽的socket上有新的連接配接請求;
- socket上有未處理的錯誤;
寫就緒:
- socket核心中, 發送緩沖區中的可用位元組數(發送緩沖區的空閑位置大小), 大于等于低水位标記 SO_SNDLOWAT, 此時可以無阻塞的寫, 并且傳回值大于0;
- socket的寫操作被關閉(close或者shutdown).對一個寫操作被關閉的socket進行寫操作, 會觸發 SIGPIPE信号;
- socket使用非阻塞connect連接配接成功或失敗之後;
- socket上有未讀取的錯誤
異常就緒
socket上收到帶外資料. 關于帶外資料, 和TCP緊急模式相關。
select的特點
- 可監控的檔案描述符個數取決與sizeof(fdset) 的值 ,是有上限的,每bit表示一個檔案描述符
- 将fd加入select監控集的同時,還要再使用一個資料結構array儲存放到select監控集中的fd,其作用為:
- 用于在select 傳回後,array作為源資料和fdset 進行FDISSET判斷,是否已經就緒。
- select傳回後會把以前加入的但并無事件發生的fd清空,則每次開始select前都要重新從array取得fd加入都fd_set(FD_ZERO最先);掃描array的同時取得fd最大值maxfd,用于select的第一個參數。
select的優缺點
優點
比較與多線程和多程序,效率較高。
缺點
- 每次調用select, 都需要手動設定fd集合, 從接口使用角度來說非常不便.
- 每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
- 同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
- select支援的檔案描述符數量太小
使用select編寫網絡伺服器
我們在代碼裡隻是為了示範select的用法,假設資料可以一次從用戶端取完,不用考慮資料粘包等問題,代碼如下:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<string.h>
5 #include<sys/socket.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8 #include<sys/select.h>
9 #define MAX_FDS sizeof(fd_set)*8
10 #define INIT_DATA -1
11
12 static void initArray(int arr[],int num){
13 int i=0;
14 for(;i<num;i++){
15 arr[i]=INIT_DATA;
16 }
17 }
18
19 static int addSockToArray(int arr[],int num,int fd){
20 int i=0;
21 for(;i<num;i++){
22 if(arr[i]<0){
23 arr[i]=fd;
24 return i;
25 }
26 }
27 return -1; //full
28 }
29
30 int setArrayToFdSet(int arr[],int num,fd_set *rfds){
31 int i=0;
32 int max_fd=INIT_DATA;
33 for(;i<num;i++){
34 if(arr[i]>=0){
35 FD_SET(arr[i],rfds);
36
37 if(max_fd<arr[i]){
38 max_fd=arr[i];
39 }
40 }
41 }
42 return max_fd;
43 }
44
45
46 static void serviceIO(int arr[],int num,fd_set *rfds){
47 int i=0;
48 for(;i<num;i++){
49 if(arr[i]>INIT_DATA){
50 int fd=arr[i];
51 if(i==0&&FD_ISSET(arr[i],rfds)){
52 //listen ready
53 struct sockaddr_in client;
54 socklen_t len=sizeof(client);
55 int sock=accept(fd,(struct sockaddr*)&client,&len);
56 if(sock<0){
57 perror("accept");
58 continue;
59 }
60 printf("get a new client [%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port) );
61 //addArray
62 if(addSockToArray(arr,num,sock)==-1)
63 {
64 close(sock);
65 }
66
67 }
68 else if(i!=0&&FD_ISSET(arr[i],rfds)){
69 //normal fd ready
70 char buf[1024];
71 ssize_t s=read(fd,buf,sizeof(buf)-1);
72 if(s>0){
73 buf[s]=0;
74 printf("client:> %s\n",buf);
75 }
76 else if(s==0){
77 close(fd);
78 arr[i]=INIT_DATA;
79 printf("client quit!\n");
80 }
81 else{
82 perror("read\n");
83 close(fd);
84 arr[i]=INIT_DATA;
85 }
86 }
87 else{
88 //do nothing
89 }
90 }
91 }
92 }
93
94
95 int startup(int port){
96 int sock=socket(AF_INET,SOCK_STREAM,0);
97 if(sock<0){
98 perror("sock");
99 exit(2);
100 }
101
102 int opt=1;
103 setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
104
105 struct sockaddr_in local;
106 local.sin_family=AF_INET;
107 local.sin_addr.s_addr=htonl(INADDR_ANY);
108 local.sin_port=htons(port);
109
110 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
111 perror("bind");
112 exit(3);
113 }
114 if(listen(sock,5)<0){
115 perror("listen");
116 exit(4);
117 }
118 return sock;
119 }
120 // ./select_server port
121 int main(int argc,char *argv[]){
122 if(argc!=2){
123 printf("Usage:%s [port]\n",argv[0]);
124 return 1;
125 }
126
127 int listen_sock=startup(atoi(argv[1]));
128
129 fd_set rfds;
130 int fd_array[MAX_FDS];
131 initArray(fd_array,MAX_FDS);
132 addSockToArray(fd_array,MAX_FDS,listen_sock);
133 for(;;){
134 FD_ZERO(&rfds);
135
136 int max_fd=setArrayToFdSet(fd_array,MAX_FDS,&rfds);
137
138 struct timeval timeout={3,0};
139 switch(select(max_fd+1,&rfds,NULL,NULL,NULL)){
140 case -1:
141 perror("select\n");
142 break;
143 case 0:
144 printf("timeout...\n");
145 break;
146 default:
147 serviceIO(fd_array,MAX_FDS,&rfds);
148 break;
149 }
150 }
151 }
測試:
用遠端登入工具telnet測試,需要安裝:
yum install telnet-server
yum install telnet
service xinetd restart