select模型详解
1.select模型原理
使用select函數檢查文件描述符上是否有io事件發生,包括可讀,可寫以及異常
?select參數和返回值意義如下:
int select (
?IN int nfds,?? ????????????????????? ??//0,無意義
?IN OUT fd_set* readfds,????? //檢查可讀性
?IN OUT fd_set* writefds,?? ??//檢查可寫性
?IN OUT fd_set* exceptfds,? //例外數據
?IN const struct timeval* timeout);?? ?//函數的返回時間
?參數說明:
第一個參數nfds在linux表示要監視的最大文件描述符+1,在windows下為0
第二個參數readfds檢查文件描述符集合可讀
第三個參數writefds檢查文件描述符集合可寫
第四個參數exceptfds檢查文件描述符集合異常
第五個參數timeout結構如下
struct? timeval {
??????? long??? tv_sec;??????? //秒
??????? long??? tv_usec;???? //毫秒
};
設置select函數返回的等待時間
如果參數timeout設為:
NULL,則表示select()沒有timeout,select將一直被阻塞,直到某個文件描述符上發生了事件。
0:僅檢測描述符集合的狀態,然后立即返回,并不等待外部事件的發生。
特定的時間值:如果在指定的時間段里沒有事件發生,select將超時返回。
返回值:執行成功則返回文件描述符狀態已改變的個數,如果返回0代表在描述詞狀態改變前已超過timeout時間,沒有返回;當有錯誤發生時則返回-1,錯誤原因存于errno,此時參數readfds,writefds,exceptfds和timeout的值變成不可預測。錯誤值可能為:
EBADF 文件描述詞為無效的或該文件已關閉
EINTR 此調用被信號所中斷
EINVAL 參數n 為負值。
ENOMEM 核心內存不足
select返回fd_set中可用的套接字個數。
系統調用:
?fd_set是一個SOCKET集合(數組),以下宏可以對該集合進行操作:
FD_CLR( s, *set) 從集合set刪除句柄s;
FD_ISSET( s, *set) 檢查句柄s是否存在與集合set中;
FD_SET( s, *set )把句柄s添加到集合set中;
FD_ZERO( *set ) 把set隊列初始化集合成空.
2.select工作流程
1:用FD_ZERO宏來初始化我們感興趣的fd_set。
也就是select函數的第二三四個參數。
2:用FD_SET宏來將套接字句柄分配給相應的fd_set。
如果想要檢查一個套接字是否有數據需要接收,可以用FD_SET宏把套接接字句柄加入可讀性檢查集合中
3:調用select函數。
如果該套接字沒有數據需要接收,select函數會把該套接字從可讀性檢查集合中刪除掉,
4:用FD_ISSET對套接字句柄進行檢查。
如果我們所關注的那個套接字句柄仍然在開始分配的那個fd_set里,那么說明馬上可以進行相應的IO操 作。比如一個分配給select第一個參數的套接字句柄在select返回后仍然在select第一個參數的fd_set里,那么說明當前數據已經來了, 馬上可以讀取成功而不會被阻塞。
3.使用實例
下面給出一個基于udp組播在windows下的實現,linux下可能略有不同
#include <winsock2.h> #include <mswsock.h> #include <MSTcpIP.h> #include <errno.h> #include <ws2ipdef.h> #include <process.h>#pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "mswsock.lib") #define MAX_FD 16 fd_set fdread; SOCKET fd_arr[MAX_FD] = {0}; unsigned int __stdcall threadfunc(void *) {while (1){fd_set fd_tmp = fdread;timeval tmv;tmv.tv_sec = 1;tmv.tv_usec = 0;int ret = select(0, &fd_tmp, NULL, NULL, &tmv);if (ret < 0){printf("select failed:%d\n",ret);break;}else{for (unsigned int i=0;i<fd_tmp.fd_count;i++){SOCKET fd = fd_tmp.fd_array[i];if (fd > 0){SOCKADDR_IN client_addr;int nlen_addr = sizeof(SOCKADDR_IN);char buff[40960]={0};int nrecv = recvfrom(fd, buff, 40960, 0, (SOCKADDR *)&client_addr, &nlen_addr);if (nrecv < 0){printf("recvfrom error:%d\n", WSAGetLastError());continue;}printf("sock:%d src addr:%s:%d recv data:%s\n", fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),buff);}}/*for (int i=0;i<MAX_FD;i++){SOCKET fd = fd_arr[i];if (FD_ISSET(fd, &fd_tmp)){}}*/}}return 0; };#define MULTI_IP "239.0.0.37"#define RECV_START_PORT 16000#define LOCAL_BIND_PORT 20260int _tmain(int argc, _TCHAR* argv[]) {WSADATA wsaData;if (WSAStartup(MAKEWORD(2,2), &wsaData)!=0){printf("init socket failed\n");return -1;}FD_ZERO(&fdread);for (int i=0;i<MAX_FD;i++){SOCKADDR_IN udplocal;udplocal.sin_family = AF_INET;udplocal.sin_addr.s_addr = inet_addr("0.0.0.0");//htonl(ADDR_ANY)udplocal.sin_port = htons(RECV_START_PORT+2*i);SOCKET fd_local= socket(PF_INET, SOCK_DGRAM, IPPROTO_IP | IPPROTO_UDP);if (fd_local == INVALID_SOCKET){printf("create socket() failed\n");return -1;}//int nbroadcast = 1;//setsockopt(fd_local, SOL_SOCKET, SO_BROADCAST, (char *)&nbroadcast, sizeof(int));int err = bind(fd_local, (SOCKADDR *)&udplocal, sizeof SOCKADDR_IN);if (err != 0){printf("bind() socket failed\n");return -1;}//char buff[1024]={0};//memset(buff, 0x0, 1024);//add multicast group IP_MREQ mreq;mreq.imr_interface.s_addr = htonl(ADDR_ANY);mreq.imr_multiaddr.s_addr = inet_addr(MULTI_IP);setsockopt(fd_local, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char*)&mreq, sizeof mreq);//set client socket ttlint ttl_value = 4;if (setsockopt(fd_local, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&ttl_value, sizeof(ttl_value)) != 0){printf("setsockopt multicast_ttl failed\n");return -1;}fd_arr[i] = fd_local;FD_SET(fd_local,&fdread);}::_beginthreadex(NULL,0,&threadfunc,NULL,0,NULL);printf("select io model server thread start running\n");getchar();for (int i=0;i<MAX_FD;i++){//shutdown(fd_arr[i], 1);closesocket(fd_arr[i]);}WSACleanup();return 0; } linux下檢查鍵盤stdio輸入#include<sys/time.h> #include<sys/types.h> #include<unistd.h> #include<string.h> #include<stdlib.h> #include<stdio.h> int main() { char buf[10]=""; fd_set rdfds; struct timeval tv; int ret; FD_ZERO(&rdfds); FD_SET(0,&rdfds); //文件描述符0表示stdin鍵盤輸入 tv.tv_sec = 3; tv.tv_usec = 500; ret = select(1,&rdfds,NULL,NULL,&tv); if(ret<0) printf("\n selcet error"); else if(ret == 0) printf("\n select timeout"); else printf("\n ret = %d",ret); if(FD_ISSET(1,&rdfds)) //如果有輸入,從stdin中獲取輸入字符 { printf("\n reading"); fread(buf,9,1,stdin); } write(1,buf,strlen(buf)); printf("\n %d \n",strlen(buf)); return 0; } //執行結果ret = 1.
4.總結討論
1.select為何效率低
通過select的代碼流程,我們發現首先要將文件描述符循環拷貝到select調用的臨時集合,內核在調用select時要將用戶態數組拷貝到內核態并執行輪詢操作,輪詢完成后將有事件發送的文件描述符拷貝到select輪詢后的集合,并將內核太數據拷貝到用戶態,處理時在循環處理返回集合,這里至少3次的循環和數據拷貝,并有用戶態數據到內核態,內核態到用戶態數據的拷貝,epoll為文件描述符建立一個以紅黑樹結構的文件系統,大大提高了搜索效率,另外在文件描述符通過epoll_ctl時已經將文件描述符拷貝到內核中,并沒有文件描述符的在內核與用戶之間的來回拷貝,epoll_wait后之間返回事件節點,不用輪詢集合了,iocp也是在套接字和完成端口關聯時拷貝到內核態,并在完成后放在完成隊列中,用戶直接取出處理,不用在循環集合,總結起來就兩點,一是避免了大量文件描述符集合的拷貝及重復的處理流程,二是內核在搜索文件描述符可用事件的方法高效
2.如何突破64的限制
#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;方法1:修改宏定義
linux和windows下重定義FD_SETSIZE宏的大小,linux下修改內核FD_SETSIZE宏定義
方法2:分段輪詢
使用一個select輪詢多個文件描述符集合,完成一個集合輪詢后,切換到下一個集合,輪詢的數量可以達到n*64
方法3:多線程
使用多個線程select,每個線程中select輪詢64個文件描述符,輪詢的文件描述符數量可達到64*n
方法4:動態數組
?linux內核代碼中有很多零長度數組的運用,只不過不是c/c++標準,由于輪詢的是一個數組,那么也可以定義為動態數組;
這也是libevent的做法,這是libevent在win32下的實現
struct win_fd_set { u_int fd_count; SOCKET fd_array[1]; }; 使用時win_fd_set * Set = (win_fd_set*)malloc(sizeof(win_fd_set) + sizoef(SCOEKT) * 10);
讓fd_array動態變化
Set->fd_array 可以放11 個 SOCKET,因為動態開辟的內存控件足夠存放11個SOCKET。
總結
以上是生活随笔為你收集整理的select模型详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++常见面试题30道
- 下一篇: epoll模型详解