select模型详解
1.select模型原理
使用select函數(shù)檢查文件描述符上是否有io事件發(fā)生,包括可讀,可寫以及異常
?select參數(shù)和返回值意義如下:
int select (
?IN int nfds,?? ????????????????????? ??//0,無意義
?IN OUT fd_set* readfds,????? //檢查可讀性
?IN OUT fd_set* writefds,?? ??//檢查可寫性
?IN OUT fd_set* exceptfds,? //例外數(shù)據(jù)
?IN const struct timeval* timeout);?? ?//函數(shù)的返回時(shí)間
?參數(shù)說明:
第一個(gè)參數(shù)nfds在linux表示要監(jiān)視的最大文件描述符+1,在windows下為0
第二個(gè)參數(shù)readfds檢查文件描述符集合可讀
第三個(gè)參數(shù)writefds檢查文件描述符集合可寫
第四個(gè)參數(shù)exceptfds檢查文件描述符集合異常
第五個(gè)參數(shù)timeout結(jié)構(gòu)如下
struct? timeval {
??????? long??? tv_sec;??????? //秒
??????? long??? tv_usec;???? //毫秒
};
設(shè)置select函數(shù)返回的等待時(shí)間
如果參數(shù)timeout設(shè)為:
NULL,則表示select()沒有timeout,select將一直被阻塞,直到某個(gè)文件描述符上發(fā)生了事件。
0:僅檢測描述符集合的狀態(tài),然后立即返回,并不等待外部事件的發(fā)生。
特定的時(shí)間值:如果在指定的時(shí)間段里沒有事件發(fā)生,select將超時(shí)返回。
返回值:執(zhí)行成功則返回文件描述符狀態(tài)已改變的個(gè)數(shù),如果返回0代表在描述詞狀態(tài)改變前已超過timeout時(shí)間,沒有返回;當(dāng)有錯(cuò)誤發(fā)生時(shí)則返回-1,錯(cuò)誤原因存于errno,此時(shí)參數(shù)readfds,writefds,exceptfds和timeout的值變成不可預(yù)測。錯(cuò)誤值可能為:
EBADF 文件描述詞為無效的或該文件已關(guān)閉
EINTR 此調(diào)用被信號(hào)所中斷
EINVAL 參數(shù)n 為負(fù)值。
ENOMEM 核心內(nèi)存不足
select返回fd_set中可用的套接字個(gè)數(shù)。
系統(tǒng)調(diào)用:
?fd_set是一個(gè)SOCKET集合(數(shù)組),以下宏可以對(duì)該集合進(jìn)行操作:
FD_CLR( s, *set) 從集合set刪除句柄s;
FD_ISSET( s, *set) 檢查句柄s是否存在與集合set中;
FD_SET( s, *set )把句柄s添加到集合set中;
FD_ZERO( *set ) 把set隊(duì)列初始化集合成空.
2.select工作流程
1:用FD_ZERO宏來初始化我們感興趣的fd_set。
也就是select函數(shù)的第二三四個(gè)參數(shù)。
2:用FD_SET宏來將套接字句柄分配給相應(yīng)的fd_set。
如果想要檢查一個(gè)套接字是否有數(shù)據(jù)需要接收,可以用FD_SET宏把套接接字句柄加入可讀性檢查集合中
3:調(diào)用select函數(shù)。
如果該套接字沒有數(shù)據(jù)需要接收,select函數(shù)會(huì)把該套接字從可讀性檢查集合中刪除掉,
4:用FD_ISSET對(duì)套接字句柄進(jìn)行檢查。
如果我們所關(guān)注的那個(gè)套接字句柄仍然在開始分配的那個(gè)fd_set里,那么說明馬上可以進(jìn)行相應(yīng)的IO操 作。比如一個(gè)分配給select第一個(gè)參數(shù)的套接字句柄在select返回后仍然在select第一個(gè)參數(shù)的fd_set里,那么說明當(dāng)前數(shù)據(jù)已經(jīng)來了, 馬上可以讀取成功而不會(huì)被阻塞。
3.使用實(shí)例
下面給出一個(gè)基于udp組播在windows下的實(shí)現(xiàn),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; } //執(zhí)行結(jié)果ret = 1.
4.總結(jié)討論
1.select為何效率低
通過select的代碼流程,我們發(fā)現(xiàn)首先要將文件描述符循環(huán)拷貝到select調(diào)用的臨時(shí)集合,內(nèi)核在調(diào)用select時(shí)要將用戶態(tài)數(shù)組拷貝到內(nèi)核態(tài)并執(zhí)行輪詢操作,輪詢完成后將有事件發(fā)送的文件描述符拷貝到select輪詢后的集合,并將內(nèi)核太數(shù)據(jù)拷貝到用戶態(tài),處理時(shí)在循環(huán)處理返回集合,這里至少3次的循環(huán)和數(shù)據(jù)拷貝,并有用戶態(tài)數(shù)據(jù)到內(nèi)核態(tài),內(nèi)核態(tài)到用戶態(tài)數(shù)據(jù)的拷貝,epoll為文件描述符建立一個(gè)以紅黑樹結(jié)構(gòu)的文件系統(tǒng),大大提高了搜索效率,另外在文件描述符通過epoll_ctl時(shí)已經(jīng)將文件描述符拷貝到內(nèi)核中,并沒有文件描述符的在內(nèi)核與用戶之間的來回拷貝,epoll_wait后之間返回事件節(jié)點(diǎn),不用輪詢集合了,iocp也是在套接字和完成端口關(guān)聯(lián)時(shí)拷貝到內(nèi)核態(tài),并在完成后放在完成隊(duì)列中,用戶直接取出處理,不用在循環(huán)集合,總結(jié)起來就兩點(diǎn),一是避免了大量文件描述符集合的拷貝及重復(fù)的處理流程,二是內(nèi)核在搜索文件描述符可用事件的方法高效
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下修改內(nèi)核FD_SETSIZE宏定義
方法2:分段輪詢
使用一個(gè)select輪詢多個(gè)文件描述符集合,完成一個(gè)集合輪詢后,切換到下一個(gè)集合,輪詢的數(shù)量可以達(dá)到n*64
方法3:多線程
使用多個(gè)線程select,每個(gè)線程中select輪詢64個(gè)文件描述符,輪詢的文件描述符數(shù)量可達(dá)到64*n
方法4:動(dòng)態(tài)數(shù)組
?linux內(nèi)核代碼中有很多零長度數(shù)組的運(yùn)用,只不過不是c/c++標(biāo)準(zhǔn),由于輪詢的是一個(gè)數(shù)組,那么也可以定義為動(dòng)態(tài)數(shù)組;
這也是libevent的做法,這是libevent在win32下的實(shí)現(xiàn)
struct win_fd_set { u_int fd_count; SOCKET fd_array[1]; }; 使用時(shí)win_fd_set * Set = (win_fd_set*)malloc(sizeof(win_fd_set) + sizoef(SCOEKT) * 10);
讓fd_array動(dòng)態(tài)變化
Set->fd_array 可以放11 個(gè) SOCKET,因?yàn)閯?dòng)態(tài)開辟的內(nèi)存控件足夠存放11個(gè)SOCKET。
總結(jié)
以上是生活随笔為你收集整理的select模型详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++常见面试题30道
- 下一篇: epoll模型详解