select函數的作用:
在編程的過程中,經常會遇到許多阻塞的函數,好像read和網絡編程時使用的recv, recvfrom函數都是阻塞的函數,當函數不能成功執行的時候,程序就會一直阻塞在這里,無法執行下面的代碼。這是就需要用到非阻塞的編程方式,使用selcet函數就可以實現非阻塞編程。
selcet函數是一個輪循函數,即當循環詢問文件節點,可設置超時時間,超時時間到了就跳過代碼繼續往下執行。
select()在SOCKET編程中還是比較重要的,可是對於初學SOCKET的人來說都不太愛用select()寫程序,他們只是習慣寫諸如 conncet()、accept()、recv()或recvfrom這樣的阻塞程序(所謂阻塞方式block,顧名思義,就是進程或是線程執行到這些函數時必須等待某個事件發生,如果事件沒有發生,進程或線程就被阻塞,函數不能立即返回)。可是使用select()就可以完成非阻塞(所謂非阻塞方式non-block,就是進程或線程執行此函數時不必非要等待事件的發生,一旦執行肯定返回,以返回值的不同來反映函數的執行情況。如果事件發生則與阻塞方式相同,若事件沒有發生則返回一個代碼來告知事件未發生,而進程或線程繼續執行,所以效率高)方式工作的程序,它能夠監視我們需要監視的文件描述符的變化情況——讀寫或是異常。
select函數格式:
select()函數的格式(所說的是Unix系統下的Berkeley Socket編程,和Windows下的有區別,一會兒說明):
Unix系統下解釋:
int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
先說明兩個結構體:
第一:struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄,這可以是我們所說的普通意義的文件,當然Unix下任何設備、管道、FIFO等都是文件形式,全部包括在內,所以,毫無疑問,一個socket就是一個文件,socket句柄就是一個文件描述符。fd_set集合可以通過一些宏由人為來操作,比如清空集合:FD_ZERO(fd_set*),將一個給定的文件描述符加入集合之中FD_SET(int, fd_set*),將一個給定的文件描述符從集合中刪除FD_CLR(int, fd_set*),檢查集合中指定的文件描述符是否可以讀寫FD_ISSET(int, fd_set*)。一會兒舉例說明。
第二:struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個毫秒數。
具體解釋select的參數:
int maxfdp是一個整數值,是指集合中所有文件描述符的范圍,即所有文件描述符的最大值加1,不能錯!在Windows中這個參數值無所謂,可以設置不正確。
fd_set* readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀,如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。
fd_set* writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。
fe_set* errorfds同上面兩個參數的意圖,用來監視文件錯誤異常。
struct timeval* timeout是select的超時時間,這個參數至關重要,它可以使select處於三種狀態。
第一:若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化為止;
第二:若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;
第三:timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時后不管怎樣一定返回,返回值同上述。
select函數返回值:
負值:select錯誤
正值:某些文件可讀寫或出錯
0:等待超時,沒有可讀寫或錯誤的文件
Windows平台下解釋:
1,函數原型:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout);
2,參數:
writefds: (可選)指針,指向一組等待可寫性檢查的套接口;
exceptfds:(可選)指針,指向一組等待錯誤檢查的套接口;
timeout: 本函數最多等待時間,對阻塞操作則為NULL。
(1)select()調用返回處於就緒狀態並且已經包含在fd_set結構中的描述字總數;
(2)如果超時則返回0;
(3)否則的話,返回SOCKET_ERROR錯誤,應用程序可通過WSAGetLastError()獲取相應錯誤代碼。
看下源碼:
#ifndef FD_SETSIZE #define FD_SETSIZE 64 #endif /* FD_SETSIZE */ typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set; extern int PASCAL FAR __WSAFDIsSet(SOCKET, fd_set FAR *); #define FD_CLR(fd, set) do { / u_int __i; / for (__i = 0; __i < ((fd_set FAR *)(set))->;fd_count ; __i++) { / if (((fd_set FAR *)(set))->;fd_array[__i] == fd) { / while (__i < ((fd_set FAR *)(set))->;fd_count-1) { / ((fd_set FAR *)(set))->;fd_array[__i] = / ((fd_set FAR *)(set))->;fd_array[__i+1]; / __i++; / } / ((fd_set FAR *)(set))->;fd_count--; / break; / } / } / } while(0) #define FD_SET(fd, set) do { / u_int __i; / for (__i = 0; __i < ((fd_set FAR *)(set))->;fd_count; __i++) { / if (((fd_set FAR *)(set))->;fd_array[__i] == (fd)) { / break; / } / } / if (__i == ((fd_set FAR *)(set))->;fd_count) { / if (((fd_set FAR *)(set))->;fd_count < FD_SETSIZE) { / ((fd_set FAR *)(set))->;fd_array[__i] = (fd); / ((fd_set FAR *)(set))->;fd_count++; / } / } / } while(0) #define FD_ZERO(set) (((fd_set FAR *)(set))->;fd_count=0) #define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set)) typedef int32_t __fd_mask; #define _NFDBITS (sizeof(__fd_mask) * 8) /* 8 bits per byte */ #define __howmany(x,y) (((x)+((y)-1))/(y)) #ifndef _FD_SET # define _FD_SET typedef struct __fd_set { long fds_bits[__howmany(FD_SETSIZE, (sizeof(long) * 8))]; } fd_set; # ifndef _KERNEL # ifdef __cplusplus extern "C" { # endif /* __cplusplus */ #ifdef _INCLUDE_HPUX_SOURCE # define FD_SET(n,p) (((__fd_mask *)((p)->;fds_bits))[(n)/_NFDBITS] |= (1 << ((n) % _NFDBITS))) # define FD_CLR(n,p) (((__fd_mask *)((p)->;fds_bits))[(n)/_NFDBITS] &= ~(1 << ((n) % _NFDBITS))) # define FD_ISSET(n,p) (((__fd_mask *)((p)->;fds_bits))[(n)/_NFDBITS] & (1 << ((n) % _NFDBITS))) # define FD_ZERO(p) memset((void *)(p), (int) 0, sizeof(*(p))) #else # define FD_SET(n,p) (__fd_set1(n, p)) # define FD_CLR(n,p) (__fd_clr(n, p)) # define FD_ISSET(n,p) (__fd_isset(n, p)) # define FD_ZERO(p) memset((void *)(p), (int) 0, sizeof(fd_set))
本函數用於確定一個或多個套接口的狀態。對每一個套接口,調用者可查詢它的可讀性、可寫性及錯誤狀態信息。用fd_set結構來表示一組等待檢查的套接口。在調用返回時,這個結構存有滿足一定條件的套接口組的子集,並且select()返回滿足條件的套接口的數目。有一組宏可用於對fd_set的操作,這些宏與Berkeley Unix軟件中的兼容,但內部的表達是完全不同的。
readfds參數標識等待可讀性檢查的套接口。如果該套接口正處於監聽listen()狀態,則若有連接請求到達,該套接口便被標識為可讀,這樣一個accept()調用保證可以無阻塞完成。對其他套接口而言,可讀性意味着有排隊數據供讀取。或者對於SOCK_STREAM類型套接口來說,相對於該套接口的虛套接口已關閉,於是recv()或recvfrom()操作均能無阻塞完成。如果虛電路被“優雅地”中止,則recv()不讀取數據立即返回;如果虛電路被強制復位,則recv()將以WSAECONNRESET錯誤立即返回。如果SO_OOBINLINE選項被設置,則將檢查帶外數據是否存在(參見setsockopt())。
writefds參數標識等待可寫性檢查的套接口。如果一個套接口正在connect()連接(非阻塞),可寫性意味着連接順利建立。如果套接口並未處於connect()調用中,可寫性意味着send()和sendto()調用將無阻塞完成。〔但並未指出這個保證在多長時間內有效,特別是在多線程環境中〕。
exceptfds參數標識等待帶外數據存在性或意味錯誤條件檢查的套接口。請注意如果設置了SO_OOBINLINE選項為假FALSE,則只能用這種方法來檢查帶外數據的存在與否。對於SO_STREAM類型套接口,遠端造成的連接中止和KEEPALIVE錯誤都將被作為意味出錯。如果套接口正在進行連接connect()(非阻塞方式),則連接試圖的失敗將會表現在exceptfds參數中。
如果對readfds、writefds或exceptfds中任一個組類不感興趣,可將它置為空NULL。
在winsock2.h頭文件中共定義了四個宏來操作描述字集。FD_SETSIZE變量用於確定一個集合中最多有多少描述字(FD_SETSIZE缺省值為64,可在包含winsock.h前用#define FD_SETSIZE來改變該值)。對於內部表示,fd_set被表示成一個套接口的隊列,最后一個有效元素的后續元素為INVAL_SOCKET。宏為:
FD_CLR(s,*set): 從集合set中刪除描述字s。
FD_ISSET(s,*set): 若s為集合中一員,非零;否則為零。
FD_SET(s,*set): 向集合添加描述字s。
FD_ZERO(*set): 將set初始化為空集NULL。
timeout參數控制select()完成的時間。若timeout參數為空指針,則select()將一直阻塞到有一個描述字滿足條件。否則的話,timeout指向一個timeval結構,其中指定了select()調用在返回前等待多長時間。如果timeval為{0,0},則select()立即返回,這可用於探詢所選套接口的狀態。如果處於這種狀態,則select()調用可認為是非阻塞的,且一切適用於非阻塞調用的假設都適用於它。
#define MAXIMUM_WAIT_OBJECTS 1024 //要等待的對象數
select系統調用是用來讓我們的程序監視多個文件句柄(file descriptor)的狀態變化的。程序會停在select這里等待,直到被監視的文件句柄有某一個或多個發生了狀態改變。
文件在句柄在Linux里很多,如果你man某個函數,在函數返回值部分說到成功后有一個文件句柄被創建的都是的,如man socket可以看到“On success, a file descriptor for the new socket is returned.”而man 2 open可以看到“open() and creat() return the new file descriptor”,其實文件句柄就是一個整數,看socket函數的聲明就明白了:
int socket(int domain, int type, int protocol);
當然,我們最熟悉的句柄是0、1、2三個,0是標准輸入,1是標准輸出,2是標准錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr,0就是stdin,1就是stdout,2就是stderr。
比如下面這兩段代碼都是從標准輸入讀入9個字節字符:
#include <stdio.h> #include <unistd.h> #include <string.h> int main(int argc, char ** argv) { char buf[10] = ""; read(0, buf, 9); /* 從標准輸入 0 讀入字符 */ fprintf(stdout, "%s\n", buf); /* 向標准輸出 stdout 寫字符 */ return 0; } /* **上面和下面的代碼都可以用來從標准輸入讀用戶輸入的9個字符** */ #include <stdio.h> #include <unistd.h> #include <string.h> int main(int argc, char ** argv) { char buf[10] = ""; fread(buf, 9, 1, stdin); /* 從標准輸入 stdin 讀入字符 */ write(1, buf, strlen(buf)); return 0; } |
繼續上面說的select,就是用來監視某個或某些句柄的狀態變化的。select函數原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函數的最后一個參數timeout顯然是一個超時時間值,其類型是struct timeval *,即一個struct timeval結構的變量的指針,所以我們在程序里要申明一個struct timeval tv;然后把變量tv的地址&tv傳遞給select函數。struct timeval結構如下:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; |
第2、3、4三個參數是一樣的類型: fd_set *,即我們在程序里要申明幾個fd_set類型的變量,比如rdfds, wtfds, exfds,然后把這個變量的地址&rdfds, &wtfds, &exfds 傳遞給select函數。這三個參數都是一個句柄的集合,第一個rdfds是用來保存這樣的句柄的:當句柄的狀態變成可讀的時系統就會告訴select函數返回,同理第二個wtfds是指有句柄狀態變成可寫的時系統就會告訴select函數返回,同理第三個參數exfds是特殊情況,即句柄上有特殊情況發生時系統會告訴select函數返回。特殊情況比如對方通過一個socket句柄發來了緊急數據。如果我們程序里只想檢測某個socket是否有數據可讀,我們可以這樣:
fd_set rdfds; /* 先申明一個 fd_set 集合來保存我們要檢測的 socket句柄 */ struct timeval tv; /* 申明一個時間變量來保存時間 */ int ret; /* 保存返回值 */ FD_ZERO(&rdfds); /* 用select函數之前先把集合清零 */ FD_SET(socket, &rdfds); /* 把要檢測的句柄socket加入到集合里 */ tv.tv_sec = 1; tv.tv_usec = 500; /* 設置select等待的最大時間為1秒加500毫秒 */ ret = select(socket + 1, &rdfds, NULL, NULL, &tv); /* 檢測我們上面設置到集合rdfds里的句柄是否有可讀信息 */ if(ret < 0) perror("select");/* 這說明select函數出錯 */ else if(ret == 0) printf("超時\n"); /* 說明在我們設定的時間值1秒加500毫秒的時間內,socket的狀態沒有發生變化 */ else { /* 說明等待時間還未到1秒加500毫秒,socket的狀態發生了變化 */ printf("ret=%d\n", ret); /* ret這個返回值記錄了發生狀態變化的句柄的數目,由於我們只監視了socket這一個句柄,所以這里一定ret=1,如果同時有多個句柄發生變化返回的就是句柄的總和了 */ /* 這里我們就應該從socket這個句柄里讀取數據了,因為select函數已經告訴我們這個句柄里有數據可讀 */ if(FD_ISSET(socket, &rdfds)) { /* 先判斷一下socket這外被監視的句柄是否真的變成可讀的了 */ /* 讀取socket句柄里的數據 */ recv(...); } } |
注意select函數的第一個參數,是所有加入集合的句柄值的最大那個值還要加1。比如我們創建了3個句柄:
/************關於本文檔********************************************
*filename: Linux網絡編程一步一步學-select詳解
*purpose: 詳細說明select的用法
*wrote by: zhoulifa(zhoulifa@163.com) 周立發(http://zhoulifa.bokee.com)
Linux愛好者 Linux知識傳播者 SOHO族 開發者 最擅長C語言
*date time:2007-02-03 19:40
*Note: 任何人可以任意復制代碼並運用這些文檔,當然包括你的商業用途
* 但請遵循GPL
*Thanks to:Google
*Hope:希望越來越多的人貢獻自己的力量,為科學技術發展出力
* 科技站在巨人的肩膀上進步更快!感謝有開源前輩的貢獻!
*********************************************************************/
int sa, sb, sc; sa = socket(...); /* 分別創建3個句柄並連接到服務器上 */ connect(sa,...); sb = socket(...); connect(sb,...); sc = socket(...); connect(sc,...); FD_SET(sa, &rdfds);/* 分別把3個句柄加入讀監視集合里去 */ FD_SET(sb, &rdfds); FD_SET(sc, &rdfds); |
在使用select函數之前,一定要找到3個句柄中的最大值是哪個,我們一般定義一個變量來保存最大值,取得最大socket值如下:
int maxfd = 0; if(sa > maxfd) maxfd = sa; if(sb > maxfd) maxfd = sb; if(sc > maxfd) maxfd = sc; |
然后調用select函數:
ret = select(maxfd + 1, &rdfds, NULL, NULL, &tv); /* 注意是最大值還要加1 */ |
同樣的道理,如果我們要檢測用戶是否按了鍵盤進行輸入,我們就應該把標准輸入0這個句柄放到select里來檢測,如下:
FD_ZERO(&rdfds); FD_SET(0, &rdfds); tv.tv_sec = 1; tv.tv_usec = 0; ret = select(1, &rdfds, NULL, NULL, &tv); /* 注意是最大值還要加1 */ if(ret < 0) perror("select");/* 出錯 */ else if(ret == 0) printf("超時\n"); /* 在我們設定的時間tv內,用戶沒有按鍵盤 */ else { /* 用戶有按鍵盤,要讀取用戶的輸入 */ scanf("%s", buf); } |