select系統調用的的用途是:在一段指定的時間內,監聽用戶感興趣的文件描述符上可讀、可寫和異常等事件。編程
爲何會出現select模型?api
先看一下下面的這句代碼:服務器
int iResult = recv(s, buffer,1024);
這是用來接收數據的,在默認的阻塞模式下的套接字裏,recv會阻塞在那裏,直到套接字鏈接上有數據可讀,把數據讀到buffer裏後recv函數纔會返回,否則就會一直阻塞在那裏。在單線程的程序裏出現這種狀況會致使主線程(單線程程序裏只有一個默認的主線程)被阻塞,這樣整個程序被鎖死在這裏,若是永 遠沒數據發送過來,那麼程序就會被永遠鎖死。這個問題能夠用多線程解決,可是在有多個套接字鏈接的狀況下,這不是一個好的選擇,擴展性不好。網絡
再看代碼:數據結構
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul); iResult = recv(s, buffer,1024);
這一次recv的調用無論套接字鏈接上有沒有數據能夠接收都會立刻返回。緣由就在於咱們用ioctlsocket把套接字設置爲非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的狀況下,recv確實是立刻返回了,可是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操做沒有成功完成。多線程
看到這裏不少人可能會說,那麼就重複調用recv並檢查返回值,直到成功爲止,可是這樣作效率很成問題,開銷太大。socket
select模型的出現就是爲了解決上述問題。
select模型的關鍵是使用一種有序的方式,對多個套接字進行統一管理與調度 。函數
如上所示,用戶首先將須要進行IO操做的socket添加到select中,而後阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據並繼續執行。oop
從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操做,效率更差。可是,使用select之後最大的優點是用戶能夠在一個線程內同時處理多個socket的IO請求。用戶能夠註冊多個socket,而後不斷地調用select讀取被激活的socket,便可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到這個目的。測試
select流程僞代碼以下:
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } }
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
參數說明:
maxfdp:被監聽的文件描述符的總數,它比全部文件描述符集合中的文件描述符的最大值大1,由於文件描述符是從0開始計數的;
readfds、writefds、exceptset:分別指向可讀、可寫和異常等事件對應的描述符集合。
timeout:用於設置select函數的超時時間,即告訴內核select等待多長時間以後就放棄等待。timeout == NULL 表示等待無限長的時間
timeval結構體定義以下:
struct timeval { long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
返回值:超時返回0;失敗返回-1;成功返回大於0的整數,這個整數表示就緒描述符的數目。
如下介紹與select函數相關的常見的幾個宏:
#include <sys/select.h> int FD_ZERO(int fd, fd_set *fdset); //一個 fd_set類型變量的全部位都設爲 0 int FD_CLR(int fd, fd_set *fdset); //清除某個位時可使用 int FD_SET(int fd, fd_set *fd_set); //設置變量的某個位置位 int FD_ISSET(int fd, fd_set *fdset); //測試某個位是否被置位
select使用範例:
當聲明瞭一個文件描述符集後,必須用FD_ZERO將全部位置零。以後將咱們所感興趣的描述符所對應的位置位,操做以下:
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);
而後調用select函數,擁塞等待文件描述符事件的到來;若是超過設定的時間,則再也不等待,繼續往下執行。
select(fd+1, &rset, NULL, NULL,NULL);
select返回後,用FD_ISSET測試給定位是否置位:
if(FD_ISSET(fd, &rset) { ... //do something }
下面是一個最簡單的select的使用例子:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { fd_set rd; struct timeval tv; int err; FD_ZERO(&rd); FD_SET(0,&rd); tv.tv_sec = 5; tv.tv_usec = 0; err = select(1,&rd,NULL,NULL,&tv); if(err == 0) //超時 { printf("select time out!\n"); } else if(err == -1) //失敗 { printf("fail to select!\n"); } else //成功 { printf("data is available!\n"); } return 0; }
咱們運行該程序而且隨便輸入一些數據,程序就提示收到數據了。
理解select模型的關鍵在於理解fd_set,爲說明方便,取fd_set長度爲1字節,fd_set中的每一bit能夠對應一個文件描述符fd。則1字節長的fd_set最大能夠對應8個fd。
(1)執行fd_set set; FD_ZERO(&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被清空。
基於上面的討論,能夠輕鬆得出select模型的特色:
(1)可監控的文件描述符個數取決與sizeof(fd_set)的值。我這邊服務器上sizeof(fd_set)=512,每bit表示一個文件描述符,則我服務器上支持的最大文件描述符是512*8=4096。聽說可調,另有說雖然可調,但調整上限受於編譯內核時的變量值。
(2)將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,一是用於再select返回後,array做爲源數據和fd_set進行FD_ISSET判斷。二是select返回後會把之前加入的但並沒有事件發生的fd清空,則每次開始select前都要從新從array取得fd逐一加入(FD_ZERO最早),掃描array的同時取得fd最大值maxfd,用於select的第一個參數。
(3)可見select模型必須在select前循環加fd,取maxfd,select返回後利用FD_ISSET判斷是否有事件發生。
網絡程序中,select能處理的異常狀況只有一種:socket上接收到帶外數據。
什麼是帶外數據?
帶外數據(out—of—band data),有時也稱爲加速數據(expedited data),
是指鏈接雙方中的一方發生重要事情,想要迅速地通知對方。
這種通知在已經排隊等待發送的任何「普通」(有時稱爲「帶內」)數據以前發送。
帶外數據設計爲比普通數據有更高的優先級。
帶外數據是映射到現有的鏈接中的,而不是在客戶機和服務器間再用一個鏈接。
咱們寫的select程序常常都是用於接收普通數據的,當咱們的服務器須要同時接收普通數據和帶外數據,咱們如何使用select進行處理兩者呢?
下面給出一個小demo:
#include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char* argv[]) { if(argc <= 2) { printf("usage: ip address + port numbers\n"); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); printf("ip: %s\n",ip); printf("port: %d\n",port); int ret = 0; struct sockaddr_in address; bzero(&address,sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET,ip,&address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET,SOCK_STREAM,0); if(listenfd < 0) { printf("Fail to create listen socket!\n"); return -1; } ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address)); if(ret == -1) { printf("Fail to bind socket!\n"); return -1; } ret = listen(listenfd,5); //監聽隊列最大排隊數設置爲5 if(ret == -1) { printf("Fail to listen socket!\n"); return -1; } struct sockaddr_in client_address; //記錄進行鏈接的客戶端的地址 socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength); if(connfd < 0) { printf("Fail to accept!\n"); close(listenfd); } char buff[1024]; //數據接收緩衝區 fd_set read_fds; //讀文件操做符 fd_set exception_fds; //異常文件操做符 FD_ZERO(&read_fds); FD_ZERO(&exception_fds); while(1) { memset(buff,0,sizeof(buff)); /*每次調用select以前都要從新在read_fds和exception_fds中設置文件描述符connfd,由於事件發生之後,文件描述符集合將被內核修改*/ FD_SET(connfd,&read_fds); FD_SET(connfd,&exception_fds); ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL); if(ret < 0) { printf("Fail to select!\n"); return -1; } if(FD_ISSET(connfd, &read_fds)) { ret = recv(connfd,buff,sizeof(buff)-1,0); if(ret <= 0) { break; } printf("get %d bytes of normal data: %s \n",ret,buff); } else if(FD_ISSET(connfd,&exception_fds)) //異常事件 { ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB); if(ret <= 0) { break; } printf("get %d bytes of exception data: %s \n",ret,buff); } } close(connfd); close(listenfd); return 0; }
上面提到過,,使用select之後最大的優點是用戶能夠在一個線程內同時處理多個socket的IO請求。在網絡編程中,當涉及到多客戶訪問服務器的狀況,咱們首先想到的辦法就是fork出多個進程來處理每一個客戶鏈接。如今,咱們一樣可使用select來處理多客戶問題,而不用fork。
服務器端
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; int result; fd_set readfds, testfds; server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//創建服務器端socket server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(INADDR_ANY); server_address.sin_port = htons(8888); server_len = sizeof(server_address); bind(server_sockfd, (struct sockaddr *)&server_address, server_len); listen(server_sockfd, 5); //監聽隊列最多容納5個 FD_ZERO(&readfds); FD_SET(server_sockfd, &readfds);//將服務器端socket加入到集合中 while(1) { char ch; int fd; int nread; testfds = readfds;//將須要監視的描述符集copy到select查詢隊列中,select會對其修改,因此必定要分開使用變量 printf("server waiting\n"); /*無限期阻塞,並測試文件描述符變更 */ result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系統默認的最大文件描述符 if(result < 1) { perror("server5"); exit(1); } /*掃描全部的文件描述符*/ for(fd = 0; fd < FD_SETSIZE; fd++) { /*找到相關文件描述符*/ if(FD_ISSET(fd,&testfds)) { /*判斷是否爲服務器套接字,是則表示爲客戶請求鏈接。*/ if(fd == server_sockfd) { client_len = sizeof(client_address); client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len); FD_SET(client_sockfd, &readfds);//將客戶端socket加入到集合中 printf("adding client on fd %d\n", client_sockfd); } /*客戶端socket中有數據請求時*/ else { ioctl(fd, FIONREAD, &nread);//取得數據量交給nread /*客戶數據請求完畢,關閉套接字,從集合中清除相應描述符 */ if(nread == 0) { close(fd); FD_CLR(fd, &readfds); //去掉關閉的fd printf("removing client on fd %d\n", fd); } /*處理客戶數據請求*/ else { read(fd, &ch, 1); sleep(5); printf("serving client on fd %d\n", fd); ch++; write(fd, &ch, 1); } } } } } return 0; }
客戶端
//客戶端 #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <sys/time.h> int main() { int client_sockfd; int len; struct sockaddr_in address;//服務器端網絡地址結構體 int result; char ch = 'A'; client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//創建客戶端socket address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); address.sin_port = htons(8888); len = sizeof(address); result = connect(client_sockfd, (struct sockaddr *)&address, len); if(result == -1) { perror("oops: client2"); exit(1); } //第一次讀寫 write(client_sockfd, &ch, 1); read(client_sockfd, &ch, 1); printf("the first time: char from server = %c\n", ch); sleep(5); //第二次讀寫 write(client_sockfd, &ch, 1); read(client_sockfd, &ch, 1); printf("the second time: char from server = %c\n", ch); close(client_sockfd); return 0; }
運行流程:
客戶端:啓動->鏈接服務器->發送A->等待服務器回覆->收到B->再發B給服務器->收到C->結束
服務器:啓動->select->收到A->發A+1回去->收到B->發B+1過去
測試:咱們先運行服務器,再運行客戶端
select本質上是經過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:
一、單個進程可監視的fd數量被限制,即能監聽端口的大小有限。通常來講這個數目和系統內存關係很大,具體數目能夠cat/proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
二、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:當套接字比較多的時候,每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。若是能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操做,那就避免了輪詢,這正是epoll與kqueue作的。
三、須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。