Linux IO多路複用之select
Linux IO多路複用之select
首先,我我們來介紹一下什麼是IO多路複用:
IO多路複用是指核心一旦發現程序指定的一個或者多個IO條件準備讀取,它就通知該程序。
IO多路複用適用如下場合:
- 當客戶處理多個描述符時(一般是互動式輸入和網路套介面),必須使用I/O複用。
- 當一個客戶同時處理多個套介面時,而這種情況是可能的,但很少出現。
- 如果一個TCP伺服器既要處理監聽套介面,又要處理已連線套介面,一般也要用到I/O複用。
- 如果一個伺服器即要處理TCP,又要處理UDP,一般要使用I/O複用。
- 如果一個伺服器要處理多個服務或多個協議,一般要使用I/O複用。
目前支援I/O多路複用的系統呼叫有 select,pselect,poll,epoll
,I/O多路複用就是通過一種機制,一個程序可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作
。但select,pselect,poll,epoll本質上都是同步I/O
,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。
接下來,我們來看看select這個機制。
基本原理
select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述符就緒(有資料 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。
基本流程
我們用一張圖來表示:
在select中,有一個缺陷就是單個程序能夠監視的檔案描述符的數量存在最大限制 ,單個程序可監視的fd數量被限制,即能監聽埠的大小有限。一般來說這個數目和系統記憶體關係很大,具體數目可cat/proc/sys/fs/file-max察看。32位機預設是1024個。64位機預設是2048.
select本質上是通過設定或者檢查存放fd標誌位的資料結構來進行下一步處理 ,當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間,這是select的弊端。
我們再來看一下select函式:
#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); //測試某個位是否被置位
當聲明瞭一個檔案描述符集後,必須用FD_ZERO將所有位置零。
例子:使用select進行通訊。
使用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; }