以前寫伺服器程式直接就都寫成多線程的了,沒考慮過其他方式,也沒考慮到底哪種方式好;
前些日子看些人說windows下面用完成連接埠、Linux下面用epoll,這些效率高。
其它環境一說就是select;似乎很多人不願意提多線程方式,也許被傳說中的線程同步嚇得吧;
我個人還是偏向多線程方式,這樣不但可以監視多連接埠,還可以分離商務邏輯,便於調試維護。
----------------------------------------------------------------------------------------------------
看明白這幾個東西,需要一個環境,描述問題;
就是我的程式需要同時處理兩個或兩個以上的檔案描述符;
這幾種方法都能解決,除了多線程方式外,都要依靠非阻塞I/O;
fd = open(filename , O_RDONLY|O_NONBLOCK); |
輪詢的方法就可以當個傳說中的笑話來聽,沒有人會這麼用。
#define BLKSIZE 1024 int nbytes; char buf[BLKSIZE]; int sign_done = 0; while(!sign_done){ nbytes = read(fd,buf,BLKSIZE); if( ( ( nbytes<0 ) && (errno != EAGAIN ) && (errno != EINTR) ) || !nbytes) do_something_process_error(); else do_something_process_data(); //if should end loop .set sign_done to 1; } |
fd是非阻塞的,讀不到東西的話就一直讀,一直佔用CPU資源。除了浪費,沒有價值了吧;
第二種方法是使用SIGPOLL訊號的非同步I/O;
SIGPOLL是SISTEM V的訊號,BSD系統用SIGIO;
當系統知道有東西需要你讀的時候,就發個SIGPOLL訊號來通知;
int sigpoll_received = 0; static void poll_handler(int signo){ //這個是SIGPOLL的訊號處理函數 sigpoll_received = 1; } |
int fd1, fd2 ; int fd1_done=0 , fd2_done = 0; sigset_t oldmask , newmask , zeromask; struct sigaction newact; fd1 = open (filename1 , O_RDONLY | O_NONBLOCK); fd2 = open (filename2 , O_RDONLY | O_NONBLOCK); sigemptyset ( &newmask ); sigaddset ( &newmask , SIGPOLL); sigprocmask (SIG_BLOCK , &newmask , &oldmask); //要在實際讀取之前阻塞SIGPOLL訊號 newact.sa_handler = poll_handler; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction ( SIGPOLL , &newact , NULL ); //給SIGPOLL訊號安裝處理器 ioctl(fd1 , I_SETSIG , S_INPUT | S_HANGUP ); //設定當有東西讀時產生 SIGPOLL訊號 ioctl(fd2 , I_SETSIG , S_INPUT | S_HANGUP ); sigemptyset ( &zeromask ) ; while ( !fd1_done || !fd2_done ){ if ( !fd1_done) deal_with_fd1_and_set_fd1done_sign; if ( !fd12done) deal_with_fd2_and_set_fd1done_sign; while ( !sigpoll_received && ( !fd1_done || !fd2_done ) ) sigsuspend ( &zeromask ) ; //沒東西讀的時候,阻塞在這裡一直等到SIGPOLL訊號來到 sigpoll_received = 0; } |
第三種方法是使用select系統調用;
select是BSD系統的,但大多數系統都支援,可能是因為spec1170的原因吧;
#include #include int select (int nfds , fd_set *readfds, fd_set *writefds, fd_set * exceptfds, struct timeval * timeout) ; |
第一個參數nfds是檔案描述符集中要檢測的掩碼位元,這是因為以前描述符集是作為一個整數位屏蔽碼實現的;
也就是每一位表示一個描述符,但那種方法沒辦法處理多於32個描述符(原來洋人也有不用大腦思考問題的時候^_^);
現在描述符一般用整數數組的位域表示;這個nfds的數值要比實際要檢測的描述符數多一,具體為啥要去看系統調用的源碼了(今天俺不看了);
後面三個(fd_set*)類型的就是實際的描述符集了,讀監控、寫監控、異常監控三個單獨的集;
最後是逾時時間,到時間函數就返回了,很多人說這個可以當成定時器用,比alarm好用;
select被訊號中斷時返回-1,並設errno 為 EINTR ;
fd_set readset; int maxfd, fd1 , fd2; maxfd = fd1; if (fd2 > maxfd ) maxfd = fd2; //找出最大的描述符 while (1) { FD_ZERO(&readset); /*由於select返回時會清除描述符集中無資料的描述符,所以每次select之前都要重設描述符集 */ FD_SET(fd1 , &readset ); FD_SET(fd2 , &readset ); if ( (select (maxfd+1, &readset , NULL, NULL, NULL) == -1 ) && (errno != EINTR) ) /* deal with error */ else{ if( FD_ISSET(fd1 , &readset ) ) /* FD_ISSET(2) 檢查指定的描述符是否被設定了 */ /* get and process data *//*這裡的問題是處理這些資料的時候,進程會阻塞在這裡,其他連接埠的資料的處理沒有辦法重疊操作*/ if( FD_ISSET(fd2 , &readset ) ) /* get and process */ } } |
Poll是SVR4的東西,是和select幾乎一樣的東西,僅僅是對描述符使用的方式不一樣;
#include #include int poll (struct pollfd *fds, size_t nfds , int timeout); struct pollfd { int fd; /* 檔案描述符 */ short events; /* 等待的事件 */ short revents; /* 實際發生了的事件 */ };
|
poll用pollfd結構數組提供描述符,並且分別使用輸入和輸出的訊息掩碼,這樣不用像select那樣每次調用之前都設定一次。
struct pollfd * fds; short errmsk; int idx; errmsk = POLLERR|POLLHUP; fds = (void *)calloc(num_fds , sizeof(struct pollfd)); for(idx = 0 ; idx (fds + idx)->fd = *****; /*設定描述符*/ (fds + idx)->events = POLLRDNORM; /* 設定要監聽的事件 */ (fds + idx)->revents = 0; } while (situation ){ if( ( num_ret = poll ( fds , num_fds , INFTIM ) ) == -1 ) && (errno != EINTR ) ) break; for ( idx = 0 ; idx < num_fds && num_ret >0 ; idx++){ if( (fds + idx )->events && (fds + idx )->revents ) { /* 確實有訊息來,有事件發生,而不是錯誤 */ if( (fds + idx)->revents & errmsk ) { /* 錯誤 */ (fds + idx)->revents = 0; /*清理掉 */ } else if( (fds +idx )->revents & POLLRDNORM ){ /* 正常處理資料 */ } } } } |