select系統(tǒng)調(diào)用的的用途是:在一段指定的時(shí)間內(nèi),監(jiān)聽(tīng)用戶感興趣的文件描述符上可讀、可寫(xiě)和異常等事件。
select 機(jī)制的優(yōu)勢(shì)
為什么會(huì)出現(xiàn)select模型?
先看一下下面的這句代碼:
int iResult = recv(s, buffer,1024);
這是用來(lái)接收數(shù)據(jù)的,在默認(rèn)的阻塞模式下的套接字里,recv會(huì)阻塞在那里,直到套接字連接上有數(shù)據(jù)可讀,把數(shù)據(jù)讀到buffer里后recv函數(shù)才會(huì)返回,不然就會(huì)一直阻塞在那里。在單線程的程序里出現(xiàn)這種情況會(huì)導(dǎo)致主線程(單線程程序里只有一個(gè)默認(rèn)的主線程)被阻塞,這樣整個(gè)程序被鎖死在這里,如果永 遠(yuǎn)沒(méi)數(shù)據(jù)發(fā)送過(guò)來(lái),那么程序就會(huì)被永遠(yuǎn)鎖死。這個(gè)問(wèn)題可以用多線程解決,但是在有多個(gè)套接字連接的情況下,這不是一個(gè)好的選擇,擴(kuò)展性很差。
再看代碼:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);iResult = recv(s, buffer,1024);
這一次recv的調(diào)用不管套接字連接上有沒(méi)有數(shù)據(jù)可以接收都會(huì)馬上返回。原因就在于我們用ioctlsocket把套接字設(shè)置為非阻塞模式了。不過(guò)你跟蹤一下就會(huì)發(fā)現(xiàn),在沒(méi)有數(shù)據(jù)的情況下,recv確實(shí)是馬上返回了,但是也返回了一個(gè)錯(cuò)誤:WSAEWOULDBLOCK,意思就是請(qǐng)求的操作沒(méi)有成功完成。
看到這里很多人可能會(huì)說(shuō),那么就重復(fù)調(diào)用recv并檢查返回值,直到成功為止,但是這樣做效率很成問(wèn)題,開(kāi)銷太大。
select模型的出現(xiàn)就是為了解決上述問(wèn)題。
select模型的關(guān)鍵是使用一種有序的方式,對(duì)多個(gè)套接字進(jìn)行統(tǒng)一管理與調(diào)度 。
如上所示,用戶首先將需要進(jìn)行IO操作的socket添加到select中,然后阻塞等待select系統(tǒng)調(diào)用返回。當(dāng)數(shù)據(jù)到達(dá)時(shí),socket被激活,select函數(shù)返回。用戶線程正式發(fā)起read請(qǐng)求,讀取數(shù)據(jù)并繼續(xù)執(zhí)行。
從流程上來(lái)看,使用select函數(shù)進(jìn)行IO請(qǐng)求和同步阻塞模型沒(méi)有太大的區(qū)別,甚至還多了添加監(jiān)視socket,以及調(diào)用select函數(shù)的額外操作,效率更差。但是,使用select以后最大的優(yōu)勢(shì)是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè)socket的IO請(qǐng)求。用戶可以注冊(cè)多個(gè)socket,然后不斷地調(diào)用select讀取被激活的socket,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè)IO請(qǐng)求的目的。而在同步阻塞模型中,必須通過(guò)多線程的方式才能達(dá)到這個(gè)目的。
select流程偽代碼如下:
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } }}
select相關(guān)API介紹與使用
#include #include #include #include int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
參數(shù)說(shuō)明:
maxfdp:被監(jiān)聽(tīng)的文件描述符的總數(shù),它比所有文件描述符集合中的文件描述符的最大值大1,因?yàn)槲募枋龇菑?開(kāi)始計(jì)數(shù)的;
readfds、writefds、exceptset:分別指向可讀、可寫(xiě)和異常等事件對(duì)應(yīng)的描述符集合。
timeout:用于設(shè)置select函數(shù)的超時(shí)時(shí)間,即告訴內(nèi)核select等待多長(zhǎng)時(shí)間之后就放棄等待。timeout == NULL 表示等待無(wú)限長(zhǎng)的時(shí)間
timeval結(jié)構(gòu)體定義如下:
struct timeval{ long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
返回值:超時(shí)返回0;失敗返回-1;成功返回大于0的整數(shù),這個(gè)整數(shù)表示就緒描述符的數(shù)目。
以下介紹與select函數(shù)相關(guān)的常見(jiàn)的幾個(gè)宏:
#include int FD_ZERO(int fd, fd_set *fdset); //一個(gè) fd_set類型變量的所有位都設(shè)為 0int FD_CLR(int fd, fd_set *fdset); //清除某個(gè)位時(shí)可以使用int FD_SET(int fd, fd_set *fd_set); //設(shè)置變量的某個(gè)位置位int FD_ISSET(int fd, fd_set *fdset); //測(cè)試某個(gè)位是否被置位
select使用范例:
當(dāng)聲明了一個(gè)文件描述符集后,必須用FD_ZERO將所有位置零。之后將我們所感興趣的描述符所對(duì)應(yīng)的位置位,操作如下:
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);
然后調(diào)用select函數(shù),擁塞等待文件描述符事件的到來(lái);如果超過(guò)設(shè)定的時(shí)間,則不再等待,繼續(xù)往下執(zhí)行。
select(fd+1, &rset, NULL, NULL,NULL);
select返回后,用FD_ISSET測(cè)試給定位是否置位:
if(FD_ISSET(fd, &rset) { ... //do something }
下面是一個(gè)最簡(jiǎn)單的select的使用例子:
#include #include #include #include #include int main(){ fd_set rd; struct timeval tv; int err; FD_ZERO(&rd); FD_SET(0,&rd); tv.tv_sec = 5; tv.tv_usec = 0; err = select(1,&rd,NULL,NULL,&tv); if(err == 0) //超時(shí) { printf("select time out!\n"); } else if(err == -1) //失敗 { printf("fail to select!\n"); } else //成功 { printf("data is available!\n"); } return 0;}
我們運(yùn)行該程序并且隨便輸入一些數(shù)據(jù),程序就提示收到數(shù)據(jù)了。
深入理解select模型:
理解select模型的關(guān)鍵在于理解fd_set,為說(shuō)明方便,取fd_set長(zhǎng)度為1字節(jié),fd_set中的每一bit可以對(duì)應(yīng)一個(gè)文件描述符fd。則1字節(jié)長(zhǎng)的fd_set最大可以對(duì)應(yīng)8個(gè)fd。
(1)執(zhí)行fd_set set; FD_ZERO(&set); 則set用位表示是0000,0000。
(2)若fd=5,執(zhí)行FD_SET(fd,&set);后set變?yōu)?001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變?yōu)?001,0011
(4)執(zhí)行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發(fā)生可讀事件,則select返回,此時(shí)set變?yōu)?000,0011。注意:沒(méi)有事件發(fā)生的fd=5被清空。
基于上面的討論,可以輕松得出select模型的特點(diǎn):
(1)可監(jiān)控的文件描述符個(gè)數(shù)取決與sizeof(fd_set)的值。我這邊服務(wù)器上sizeof(fd_set)=512,每bit表示一個(gè)文件描述符,則我服務(wù)器上支持的最大文件描述符是512*8=4096。據(jù)說(shuō)可調(diào),另有說(shuō)雖然可調(diào),但調(diào)整上限受于編譯內(nèi)核時(shí)的變量值。
(2)將fd加入select監(jiān)控集的同時(shí),還要再使用一個(gè)數(shù)據(jù)結(jié)構(gòu)array保存放到select監(jiān)控集中的fd,一是用于再select返回后,array作為源數(shù)據(jù)和fd_set進(jìn)行FD_ISSET判斷。二是select返回后會(huì)把以前加入的但并無(wú)事件發(fā)生的fd清空,則每次開(kāi)始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時(shí)取得fd最大值maxfd,用于select的第一個(gè)參數(shù)。
(3)可見(jiàn)select模型必須在select前循環(huán)加fd,取maxfd,select返回后利用FD_ISSET判斷是否有事件發(fā)生。
用select處理帶外數(shù)據(jù)
網(wǎng)絡(luò)程序中,select能處理的異常情況只有一種:socket上接收到帶外數(shù)據(jù)。
什么是帶外數(shù)據(jù)?
帶外數(shù)據(jù)(out—of—band data),有時(shí)也稱為加速數(shù)據(jù)(expedited data),
是指連接雙方中的一方發(fā)生重要事情,想要迅速地通知對(duì)方。
這種通知在已經(jīng)排隊(duì)等待發(fā)送的任何“普通”(有時(shí)稱為“帶內(nèi)”)數(shù)據(jù)之前發(fā)送。
帶外數(shù)據(jù)設(shè)計(jì)為比普通數(shù)據(jù)有更高的優(yōu)先級(jí)。
帶外數(shù)據(jù)是映射到現(xiàn)有的連接中的,而不是在客戶機(jī)和服務(wù)器間再用一個(gè)連接。
我們寫(xiě)的select程序經(jīng)常都是用于接收普通數(shù)據(jù)的,當(dāng)我們的服務(wù)器需要同時(shí)接收普通數(shù)據(jù)和帶外數(shù)據(jù),我們?nèi)绾问褂胹elect進(jìn)行處理二者呢?
下面給出一個(gè)小demo:
#include #include #include #include #include #include #include #include #include #include int main(int argc, char* argv[]){ if(argc <= 2) { printf("usage: ip address + port numbers\n"); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); printf("ip: %s\n",ip); printf("port: %d\n",port); int ret = 0; struct sockaddr_in address; bzero(&address,sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET,ip,&address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET,SOCK_STREAM,0); if(listenfd < 0) { printf("Fail to create listen socket!\n"); return -1; } ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address)); if(ret == -1) { printf("Fail to bind socket!\n"); return -1; } ret = listen(listenfd,5); //監(jiān)聽(tīng)隊(duì)列最大排隊(duì)數(shù)設(shè)置為5 if(ret == -1) { printf("Fail to listen socket!\n"); return -1; } struct sockaddr_in client_address; //記錄進(jìn)行連接的客戶端的地址 socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength); if(connfd < 0) { printf("Fail to accept!\n"); close(listenfd); } char buff[1024]; //數(shù)據(jù)接收緩沖區(qū) fd_set read_fds; //讀文件操作符 fd_set exception_fds; //異常文件操作符 FD_ZERO(&read_fds); FD_ZERO(&exception_fds); while(1) { memset(buff,0,sizeof(buff)); /*每次調(diào)用select之前都要重新在read_fds和exception_fds中設(shè)置文件描述符connfd,因?yàn)槭录l(fā)生以后,文件描述符集合將被內(nèi)核修改*/ FD_SET(connfd,&read_fds); FD_SET(connfd,&exception_fds); ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL); if(ret < 0) { printf("Fail to select!\n"); return -1; } if(FD_ISSET(connfd, &read_fds)) { ret = recv(connfd,buff,sizeof(buff)-1,0); if(ret <= 0) { break; } printf("get %d bytes of normal data: %s \n",ret,buff); } else if(FD_ISSET(connfd,&exception_fds)) //異常事件 { ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB); if(ret <= 0) { break; } printf("get %d bytes of exception data: %s \n",ret,buff); } } close(connfd); close(listenfd); return 0;}
用select來(lái)解決socket中的多客戶問(wèn)題
上面提到過(guò),,使用select以后最大的優(yōu)勢(shì)是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè)socket的IO請(qǐng)求。在網(wǎng)絡(luò)編程中,當(dāng)涉及到多客戶訪問(wèn)服務(wù)器的情況,我們首先想到的辦法就是fork出多個(gè)進(jìn)程來(lái)處理每個(gè)客戶連接?,F(xiàn)在,我們同樣可以使用select來(lái)處理多客戶問(wèn)題,而不用fork。
服務(wù)器端
#include #include #include #include #include #include #include #include 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);//建立服務(wù)器端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); //監(jiān)聽(tīng)隊(duì)列最多容納5個(gè) FD_ZERO(&readfds); FD_SET(server_sockfd, &readfds);//將服務(wù)器端socket加入到集合中 while(1) { char ch; int fd; int nread; testfds = readfds;//將需要監(jiān)視的描述符集copy到select查詢隊(duì)列中,select會(huì)對(duì)其修改,所以一定要分開(kāi)使用變量 printf("server waiting\n"); /*無(wú)限期阻塞,并測(cè)試文件描述符變動(dòng) */ result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系統(tǒng)默認(rèn)的最大文件描述符 if(result < 1) { perror("server5"); exit(1); } /*掃描所有的文件描述符*/ for(fd = 0; fd < FD_SETSIZE; fd++) { /*找到相關(guān)文件描述符*/ if(FD_ISSET(fd,&testfds)) { /*判斷是否為服務(wù)器套接字,是則表示為客戶請(qǐng)求連接。*/ 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中有數(shù)據(jù)請(qǐng)求時(shí)*/ else { ioctl(fd, FIONREAD, &nread);//取得數(shù)據(jù)量交給nread /*客戶數(shù)據(jù)請(qǐng)求完畢,關(guān)閉套接字,從集合中清除相應(yīng)描述符 */ if(nread == 0) { close(fd); FD_CLR(fd, &readfds); //去掉關(guān)閉的fd printf("removing client on fd %d\n", fd); } /*處理客戶數(shù)據(jù)請(qǐng)求*/ else { read(fd, &ch, 1); sleep(5); printf("serving client on fd %d\n", fd); ch++; write(fd, &ch, 1); } } } } } return 0;}
客戶端
//客戶端#include #include #include #include #include #include #include #include int main() { int client_sockfd; int len; struct sockaddr_in address;//服務(wù)器端網(wǎng)絡(luò)地址結(jié)構(gòu)體 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); } //第一次讀寫(xiě) write(client_sockfd, &ch, 1); read(client_sockfd, &ch, 1); printf("the first time: char from server = %c\n", ch); sleep(5); //第二次讀寫(xiě) 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; }
運(yùn)行流程:
客戶端:?jiǎn)?dòng)->連接服務(wù)器->發(fā)送A->等待服務(wù)器回復(fù)->收到B->再發(fā)B給服務(wù)器->收到C->結(jié)束
服務(wù)器:?jiǎn)?dòng)->select->收到A->發(fā)A+1回去->收到B->發(fā)B+1過(guò)去
測(cè)試:我們先運(yùn)行服務(wù)器,再運(yùn)行客戶端
select總結(jié):
select本質(zhì)上是通過(guò)設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行下一步處理。這樣所帶來(lái)的缺點(diǎn)是:
1、單個(gè)進(jìn)程可監(jiān)視的fd數(shù)量被限制,即能監(jiān)聽(tīng)端口的大小有限。一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat/proc/sys/fs/file-max察看。32位機(jī)默認(rèn)是1024個(gè)。64位機(jī)默認(rèn)是2048.
2、 對(duì)socket進(jìn)行掃描時(shí)是線性掃描,即采用輪詢的方法,效率較低:當(dāng)套接字比較多的時(shí)候,每次select()都要通過(guò)遍歷FD_SETSIZE個(gè)Socket來(lái)完成調(diào)度,不管哪個(gè)Socket是活躍的,都遍歷一遍。這會(huì)浪費(fèi)很多CPU時(shí)間。如果能給套接字注冊(cè)某個(gè)回調(diào)函數(shù),當(dāng)他們活躍時(shí),自動(dòng)完成相關(guān)操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、需要維護(hù)一個(gè)用來(lái)存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開(kāi)銷大。
評(píng)論