• 正文
    • 1. 什么是IO多路轉(zhuǎn)接
    • 2. IO多路轉(zhuǎn)接技術(shù)——select詳解
  • 相關(guān)推薦
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

IO多路轉(zhuǎn)接技術(shù) | select詳解

21小時(shí)前
164
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

1. 什么是IO多路轉(zhuǎn)接

IO操作方式有兩種

阻塞等待

- 優(yōu)點(diǎn):不占用CPU時(shí)間片

- 缺點(diǎn):同一時(shí)刻只能處理一個(gè)操作,效率低下

非阻塞(忙輪詢)

- 優(yōu)點(diǎn)是提高了程序的執(zhí)行效率,缺點(diǎn)是需要占用更多的CPU和系統(tǒng)資源

- 只有一個(gè)任務(wù)時(shí)

- 多個(gè)任務(wù)

對(duì)于非阻塞方式多任務(wù)的場(chǎng)景,也就是上圖中的情況,解決方法是使用IO多路轉(zhuǎn)接技術(shù),常用的IO多路轉(zhuǎn)接技術(shù)包括select/poll/epoll。

select/poll ? ?—— ? ? 實(shí)現(xiàn)方式為線性表遍歷

通信的時(shí)候,委托內(nèi)核去檢測(cè)連接到server的client,有哪些client是在通信的,比如說(shuō)有10個(gè)client連接,但是只有6個(gè)發(fā)送了數(shù)據(jù),要把這6個(gè)client找出來(lái),這個(gè)工作由內(nèi)核去做。但是內(nèi)核只能給出發(fā)送數(shù)據(jù)的client的個(gè)數(shù)6,至于是哪6個(gè)client,需要進(jìn)程自己去遍歷。

在這兩種方式下,可以這么理解,select 代收員比較懶, 她只會(huì)告訴你有幾個(gè)快遞到了,但是具體是哪個(gè)快遞,你需要挨個(gè)遍歷一遍。

實(shí)際上,多路轉(zhuǎn)接就是進(jìn)程委托內(nèi)核去做一些事情,在進(jìn)程中只要調(diào)用select/poll/epoll就可以了,這樣就實(shí)現(xiàn)了多任務(wù)的處理。

epoll ? ?—— ? ?通過(guò)紅黑樹實(shí)現(xiàn)

epoll代收快遞員很勤快,她不僅會(huì)告訴你有幾個(gè)快遞到了,還會(huì)告訴你是哪個(gè)快遞公司的快遞。

通過(guò)上面介紹已經(jīng)大體了解了多路轉(zhuǎn)接是什么,那么多路轉(zhuǎn)接技術(shù)是怎么工作的呢?

先構(gòu)造一張有關(guān)文件描述符的列表,將要監(jiān)聽的文件描述符添加到該表中。(類似于阻塞信號(hào)集)

然后調(diào)用一個(gè)函數(shù),監(jiān)聽該表中的文件描述符,直到這些描述符表中的一個(gè)進(jìn)行I/O操作時(shí),該函數(shù)才返回。(select/poll/epoll).

該函數(shù)為阻塞函數(shù)

- 函數(shù)對(duì)文件描述符的檢測(cè)操作是由內(nèi)核完成的

- 在返回時(shí),它告訴進(jìn)程有多少(哪些)描述符要進(jìn)行I/O操作。

- 文件描述符對(duì)應(yīng)的是內(nèi)核緩沖區(qū),監(jiān)聽文件描述符,實(shí)際上就是監(jiān)聽內(nèi)核緩沖區(qū)的read區(qū),因?yàn)閞ead區(qū)有數(shù)據(jù)就說(shuō)明有進(jìn)程給我發(fā)送數(shù)據(jù)。

- select/poll會(huì)返回發(fā)生IO操作的進(jìn)程個(gè)數(shù);

- epoll返回發(fā)生IO操作的進(jìn)程個(gè)數(shù),以及是哪些進(jìn)程。

2. IO多路轉(zhuǎn)接技術(shù)——select詳解

(1)select()函數(shù)詳解

- 函數(shù)原型

?int?select(?int?nfds,?? ? ? ? fd_set *readfds, ??/*傳入傳出參數(shù) | 傳入傳出參數(shù):傳入函數(shù)之前,指針指向的內(nèi)存就已經(jīng)有值了,函數(shù)執(zhí)行完畢后,這個(gè)內(nèi)存的值可能發(fā)生變化,并通過(guò)指針傳遞出來(lái)。*/? ? ? ? fd_set *writefds,? ? ? ? ? ? ? fd_set *exceptfds,?struct?timeval *timeout );

- 函數(shù)參數(shù)

nfds:要檢測(cè)的文件描述符中最大的fd+1 —— 可以直接傳1024(文件描述符最大是1023,+1就是1024),因?yàn)閮?nèi)核要做遍歷,所以它需要一個(gè)最大值來(lái)作為遍歷的終點(diǎn)。

readfds:讀集合,重點(diǎn)關(guān)注,因?yàn)榕袛嗥渌M(jìn)程有沒(méi)有給當(dāng)前發(fā)送數(shù)據(jù)就是看讀緩沖區(qū)有沒(méi)有數(shù)據(jù),讀緩沖區(qū)有數(shù)據(jù)說(shuō)明有進(jìn)程連接并發(fā)送數(shù)據(jù)通信,這是被動(dòng)的,是當(dāng)前進(jìn)程無(wú)法預(yù)知的,所以要把文件描述符放入到讀集合中,讓內(nèi)核檢測(cè)讀緩沖區(qū)什么時(shí)候有數(shù)據(jù)。也就是告訴內(nèi)核,只檢測(cè)文件描述符對(duì)應(yīng)的讀緩沖區(qū)。

——我們想知道對(duì)方有沒(méi)有發(fā)數(shù)據(jù),所以讓內(nèi)核檢測(cè)文件描述符對(duì)應(yīng)的讀緩沖區(qū)是否有數(shù)據(jù),所以要把文件描述符放到讀集合中。讀集合的類型是一個(gè)fd_set(fd_set數(shù)據(jù)類型在內(nèi)核中是用一個(gè)數(shù)組實(shí)現(xiàn)的,數(shù)組大小是1024),這個(gè)集合所能存放的文件描述符的個(gè)數(shù)最大是1024個(gè)。內(nèi)核檢測(cè)的方式,是把這些文件描述符放到一個(gè)線性表中,然后遍歷線性表。

文件描述符集類型:fd_set readfds;fd_set數(shù)據(jù)類型的內(nèi)核代碼如下,通過(guò)下面的內(nèi)核代碼可以看出,使用select多路轉(zhuǎn)接的時(shí)候,最多只能委托內(nèi)核檢測(cè)1024個(gè)文件描述符,這是內(nèi)核決定的。

writefds: 寫集合,寫是進(jìn)程主動(dòng)動(dòng)作,不需要去檢測(cè),一般傳NULL。(寫集合作用:讓內(nèi)核只檢測(cè)文件描述符對(duì)應(yīng)的寫緩沖區(qū))

exceptfds: 異常集合,不關(guān)心異常傳NULL(讓內(nèi)核只檢測(cè)文件描述符是否發(fā)生異常),如果想要捕捉對(duì)文件描述符的異常操作就要把它加到異常集合中。

timeout: ?設(shè)置select是否阻塞

NULL: 永久阻塞

當(dāng)檢測(cè)到fd變化的時(shí)候返回(緩沖區(qū)數(shù)據(jù)變化)

struct timeval timeout;

timeout.tv_sec = 10,阻塞10s,10s后不管fd是否變換,都會(huì)返回,也就是說(shuō),只有到達(dá)指定時(shí)間才會(huì)返回。

timeout.tv_usec = 0;

?settitimer()struct?{long? ? tv_sec; ? ? ? ? ? ? ? ? ? ?long? ? tv_usec; ? ? ? ? ? ?? ? ? };/*賦值的時(shí)候,秒和微秒都要賦值,因?yàn)樽罱K結(jié)果是二者之和,否則得到的就是一個(gè)隨機(jī)數(shù)。*/

- 函數(shù)返回值

檢測(cè)的文件描述符集合中,只要有一個(gè)fd變化了,select函數(shù)就返回。

有幾個(gè)文件描述符發(fā)生變化,就返回幾,然后再通過(guò)遍歷,把變化的fd找出來(lái)。

(2)文件描述符操作函數(shù)

- 全部清空

void FD_ZERO(fd_set ? ? ?*set); //所有標(biāo)志位清0

- 從集合中刪除某一項(xiàng)

void FD_CLR(int fd, ? ? ?fd_set *set); //在set中清除fd

- 將某個(gè)文件描述符添加到集合

void FD_SET(int fd, fd_set *set);

判斷某個(gè)文件描述符是否在集合中

int FD_ISSET(int fd, fd_set *set); //fd對(duì)應(yīng)集合中的標(biāo)志位是0則返回0,是1就返回1

(3)使用select函的優(yōu)缺點(diǎn)

- 優(yōu)點(diǎn):跨平臺(tái)

- 缺點(diǎn):

每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會(huì)很大(內(nèi)核態(tài)到用戶態(tài)的頻繁切換,以及fd集合從用戶態(tài)和內(nèi)核態(tài)之間的復(fù)制)。

同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,這個(gè)開銷在fd很多時(shí)也很大,客戶端越多select的效率越低,并且隨著進(jìn)程的增多,效率下降的越來(lái)越快。——對(duì)于前兩個(gè)缺點(diǎn),poll和select都有這兩個(gè)缺點(diǎn),但是epoll沒(méi)有,因?yàn)閟elect/poll在用戶和內(nèi)核有兩塊內(nèi)存,所以需要來(lái)回復(fù)制,而epoll是內(nèi)核和用戶使用同一塊共享內(nèi)存。

select支持的文件描述符數(shù)量太小了,默認(rèn)是1024。poll不受1024的影響,但是poll不可以跨平臺(tái),其他方面二者差不多。(select中的fd_set是用數(shù)組實(shí)現(xiàn)的,而poll用的是鏈表實(shí)現(xiàn)的,所以不受限制。epoll就更厲害了,用的是樹來(lái)實(shí)現(xiàn)的)?!獙?shí)際上進(jìn)程中文件描述符最多是1024個(gè),這個(gè)數(shù)字是可以修改的,只要修改相應(yīng)的配置文件,重啟電腦就好了。

(4)select工作過(guò)程分析

首先假設(shè)客戶端A、B、C、D、E、 F連接到服務(wù)器,分別對(duì)應(yīng)文件描述符 3、4、100、101、102、103(fd都是server端的,每有一個(gè)client連接到server,都會(huì)產(chǎn)生一個(gè)用于通信的fd)。

現(xiàn)在,server通過(guò)select函數(shù)來(lái)委托內(nèi)核去檢測(cè)客戶端ABCDEF是否給server發(fā)數(shù)據(jù)了。

fd_set reads, temp; —— 文件描述符表reads,存放在用戶空間;內(nèi)核會(huì)拷貝一份,復(fù)制到內(nèi)核區(qū)。因?yàn)樵趦?nèi)核中會(huì)修改這個(gè)表并覆蓋原來(lái)的reads,所以我們需要提前備份一下原始表temp。

FD_SET(3, &reads); —— 調(diào)用6次把3、4、100、101、102、103依次加入reads集合。

select(103+1, &reads, NULL, NULL, NULL);

103+1表示要檢測(cè)的文件描述符中數(shù)字最大的fd+1,來(lái)指定遍歷的終點(diǎn)。

reads是傳入傳出參數(shù),內(nèi)核會(huì)對(duì)拿到的初始表進(jìn)行修改,根據(jù)讀緩沖區(qū)是否有數(shù)據(jù)將相應(yīng)的位分別置1或者清0,然后用修改后的表覆蓋傳入的初始表reads,并作為傳出參數(shù)傳出。

在上面的圖中

文件描述符0、1、2分別是標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤,所以供我們使用的文件描述符是從數(shù)字3開始的。

被修改后的表在內(nèi)核中,它會(huì)再一次拷貝,并放到用戶區(qū),且覆蓋原來(lái)的reads,這時(shí)候的reads是內(nèi)核處理后的(fd變化則保留1,否則清0),所以只要遍歷reads,就可以找出發(fā)送數(shù)據(jù)的client,reads相應(yīng)位值為1的文件描述符對(duì)應(yīng)的client發(fā)送了數(shù)據(jù)。那么我們就對(duì)應(yīng)的執(zhí)行read操作,去讀數(shù)據(jù)。

select中傳入的參數(shù)nfds是104,所以內(nèi)核會(huì)遍歷檢測(cè)0-103文件描述符,先檢測(cè)文件描述符標(biāo)志位是不是1,如果是1再去檢測(cè)fd對(duì)應(yīng)的讀緩沖區(qū)有沒(méi)有數(shù)據(jù),有數(shù)據(jù)說(shuō)明和該fd通信的client發(fā)送數(shù)據(jù)了。

client連接server的時(shí)候會(huì)進(jìn)行三次握手,發(fā)送FIN數(shù)據(jù)包到server的監(jiān)聽文件描述符lfd對(duì)應(yīng)的讀緩沖區(qū)中。所以,要想知道有沒(méi)有client發(fā)出連接請(qǐng)求,就要把lfd放到讀集合中,讓內(nèi)核去檢測(cè)。也就是說(shuō),有沒(méi)有連接請(qǐng)求也是委托內(nèi)核去檢測(cè)。

(5)select多路轉(zhuǎn)接代碼實(shí)現(xiàn)

#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<string.h>#include<sys/socket.h>#include<arpa/inet.h>#include<ctype.h>

intmain(int?argc, constchar* argv[]){if(argc <?2)? ? {printf("eg: ./a.out portn");exit(1);? ? }structsockaddr_inserv_addr;socklen_t?serv_len =?sizeof(serv_addr);int?port =?atoi(argv[1]);
// 創(chuàng)建套接字int?lfd =?socket(AF_INET, SOCK_STREAM,?0);// 初始化服務(wù)器 sockaddr_in?memset(&serv_addr,?0, serv_len);? ? serv_addr.sin_family = AF_INET;?// 地址族?? ? serv_addr.sin_addr.s_addr =?htonl(INADDR_ANY);?// 監(jiān)聽本機(jī)所有的IP? ? serv_addr.sin_port =?htons(port);?// 設(shè)置端口?// 綁定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設(shè)置同時(shí)監(jiān)聽的最大個(gè)數(shù)? ??listen(lfd,?36);printf("Start accept ......n");
structsockaddr_inclient_addr;socklen_t?cli_len =?sizeof(client_addr);
// 最大的文件描述符int?maxfd = lfd;// 文件描述符讀集合? ? fd_set reads, temp;// init 初始化? ??FD_ZERO(&reads);? ??FD_SET(lfd, &reads);
while(1)? ? {// 委托內(nèi)核做IO檢測(cè)? ? ? ? temp = reads;//在Linux下maxfd必須寫正確,要及時(shí)更新;在Windows下可以隨便寫int?ret =?select(maxfd+1, &temp,?NULL,?NULL,?NULL);if(ret ==?-1)? ? ? ? {? ? ? ? ? ??perror("select error");exit(1);? ? ? ? }// 客戶端發(fā)起了新的連接?// 用于監(jiān)聽的文件描述符有且只有1個(gè)lfd,lfd對(duì)應(yīng)位為1,說(shuō)明有新的連接請(qǐng)求if(FD_ISSET(lfd, &temp))? ? ? ? {// 接受新連接,返回一個(gè)用于通信的cfd,并加入到原始的讀集合reads(備份)
// 接受連接請(qǐng)求 - accept不阻塞 //因?yàn)橹灰M(jìn)入if語(yǔ)句,就說(shuō)明有新連接int?cfd =?accept(lfd, (struct?sockaddr*)&client_addr, &cli_len);if(cfd ==?-1)? ? ? ? ? ? {? ? ? ? ? ? ? ??perror("accept error");exit(1);? ? ? ? ? ? }char?ip[64];printf("new client IP: %s, Port: %dn",?? ? ? ? ? ? ? ? ? ?inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,?sizeof(ip)),? ? ? ? ? ? ? ? ? ?ntohs(client_addr.sin_port));// 將cfd加入到待檢測(cè)的讀集合中 - 下一次就可以檢測(cè)到了// 下次循環(huán)的時(shí)候,如果cfd發(fā)生變化就可以檢測(cè)到,當(dāng)前循環(huán)是檢測(cè)不到的,這也說(shuō)明select是異步的。? ? ? ? ? ??FD_SET(cfd, &reads);// 更新最大的文件描述符//maxfd決定了內(nèi)核遍歷檢測(cè)的范圍? ? ? ? ? ? maxfd = maxfd < cfd ? cfd : maxfd;? ? ? ? }// 已經(jīng)連接的客戶端有數(shù)據(jù)到達(dá)// 需要遍歷去判斷哪個(gè)client通信的cfd發(fā)生了變化(說(shuō)明通信了),變化則read讀取數(shù)據(jù)。// i為啥是從lfd+1開始的?// 因?yàn)閘fd是第一個(gè)創(chuàng)建的文件描述符,而文件描述符創(chuàng)建的規(guī)則是當(dāng)前最小空閑,所以lfd+1應(yīng)該就是第一個(gè)用于通信的文件描述符cfd。for(int?i=lfd+1; i<=maxfd; ++i)? ? ? ? {if(FD_ISSET(i, &temp))? ? ? ? ? ? {char?buf[1024] = {0};int?len =?recv(i, buf,?sizeof(buf),?0);if(len ==?-1)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ??perror("recv error");exit(1);? ? ? ? ? ? ? ? }elseif(len ==?0)? ? ? ? ? ? ? ? {printf("客戶端已經(jīng)斷開了連接n");? ? ? ? ? ? ? ? ? ??close(i);// 從讀集合中刪除? ? ? ? ? ? ? ? ? ??FD_CLR(i, &reads);? ? ? ? ? ? ? ? }else? ? ? ? ? ? ? ? {printf("recv buf: %sn", buf);? ? ? ? ? ? ? ? ? ??send(i, buf,?strlen(buf)+1,?0);//strlen(buf)不包括'',所以需要+1,并且前提是buf已經(jīng)被初始化為0//必須把''發(fā)出去來(lái)表示字符串結(jié)束,否則數(shù)據(jù)可能出錯(cuò)(比實(shí)際數(shù)據(jù)長(zhǎng)),出現(xiàn)亂碼? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

 

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設(shè)計(jì)資源下載
  • 產(chǎn)業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄

Linux、C、C++、Python、Matlab,機(jī)器人運(yùn)動(dòng)控制、多機(jī)器人協(xié)作,智能優(yōu)化算法,貝葉斯濾波與卡爾曼濾波估計(jì)、多傳感器信息融合,機(jī)器學(xué)習(xí),人工智能。