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

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

8小時前
122
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

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

IO操作方式有兩種

阻塞等待

- 優(yōu)點:不占用CPU時間片

- 缺點:同一時刻只能處理一個操作,效率低下

非阻塞(忙輪詢)

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

- 只有一個任務時

- 多個任務

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

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

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

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

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

epoll ? ?—— ? ?通過紅黑樹實現(xiàn)

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

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

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

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

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

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

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

- 文件描述符對應的是內(nèi)核緩沖區(qū),監(jiān)聽文件描述符,實際上就是監(jiān)聽內(nèi)核緩沖區(qū)的read區(qū),因為read區(qū)有數(shù)據(jù)就說明有進程給我發(fā)送數(shù)據(jù)。

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

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

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í)行完畢后,這個內(nèi)存的值可能發(fā)生變化,并通過指針傳遞出來。*/? ? ? ? fd_set *writefds,? ? ? ? ? ? ? fd_set *exceptfds,?struct?timeval *timeout );

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

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

readfds:讀集合,重點關注,因為判斷其他進程有沒有給當前發(fā)送數(shù)據(jù)就是看讀緩沖區(qū)有沒有數(shù)據(jù),讀緩沖區(qū)有數(shù)據(jù)說明有進程連接并發(fā)送數(shù)據(jù)通信,這是被動的,是當前進程無法預知的,所以要把文件描述符放入到讀集合中,讓內(nèi)核檢測讀緩沖區(qū)什么時候有數(shù)據(jù)。也就是告訴內(nèi)核,只檢測文件描述符對應的讀緩沖區(qū)。

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

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

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

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

timeout: ?設置select是否阻塞

NULL: 永久阻塞

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

struct timeval timeout;

timeout.tv_sec = 10,阻塞10s,10s后不管fd是否變換,都會返回,也就是說,只有到達指定時間才會返回。

timeout.tv_usec = 0;

?settitimer()struct?{long? ? tv_sec; ? ? ? ? ? ? ? ? ? ?long? ? tv_usec; ? ? ? ? ? ?? ? ? };/*賦值的時候,秒和微秒都要賦值,因為最終結(jié)果是二者之和,否則得到的就是一個隨機數(shù)。*/

- 函數(shù)返回值

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

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

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

- 全部清空

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

- 從集合中刪除某一項

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

- 將某個文件描述符添加到集合

void FD_SET(int fd, fd_set *set);

判斷某個文件描述符是否在集合中

int FD_ISSET(int fd, fd_set *set); //fd對應集合中的標志位是0則返回0,是1就返回1

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

- 優(yōu)點:跨平臺

- 缺點:

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

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

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

(4)select工作過程分析

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

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

fd_set reads, temp; —— 文件描述符表reads,存放在用戶空間;內(nèi)核會拷貝一份,復制到內(nèi)核區(qū)。因為在內(nè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表示要檢測的文件描述符中數(shù)字最大的fd+1,來指定遍歷的終點。

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

在上面的圖中

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

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

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

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

(5)select多路轉(zhuǎn)接代碼實現(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);// 初始化服務器 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)聽本機所有的IP? ? serv_addr.sin_port =?htons(port);?// 設置端口?// 綁定IP和端口? ??bind(lfd, (struct?sockaddr*)&serv_addr, serv_len);
// 設置同時監(jiān)聽的最大個數(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檢測? ? ? ? temp = reads;//在Linux下maxfd必須寫正確,要及時更新;在Windows下可以隨便寫int?ret =?select(maxfd+1, &temp,?NULL,?NULL,?NULL);if(ret ==?-1)? ? ? ? {? ? ? ? ? ??perror("select error");exit(1);? ? ? ? }// 客戶端發(fā)起了新的連接?// 用于監(jiān)聽的文件描述符有且只有1個lfd,lfd對應位為1,說明有新的連接請求if(FD_ISSET(lfd, &temp))? ? ? ? {// 接受新連接,返回一個用于通信的cfd,并加入到原始的讀集合reads(備份)
// 接受連接請求 - accept不阻塞 //因為只要進入if語句,就說明有新連接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加入到待檢測的讀集合中 - 下一次就可以檢測到了// 下次循環(huán)的時候,如果cfd發(fā)生變化就可以檢測到,當前循環(huán)是檢測不到的,這也說明select是異步的。? ? ? ? ? ??FD_SET(cfd, &reads);// 更新最大的文件描述符//maxfd決定了內(nèi)核遍歷檢測的范圍? ? ? ? ? ? maxfd = maxfd < cfd ? cfd : maxfd;? ? ? ? }// 已經(jīng)連接的客戶端有數(shù)據(jù)到達// 需要遍歷去判斷哪個client通信的cfd發(fā)生了變化(說明通信了),變化則read讀取數(shù)據(jù)。// i為啥是從lfd+1開始的?// 因為lfd是第一個創(chuàng)建的文件描述符,而文件描述符創(chuàng)建的規(guī)則是當前最小空閑,所以lfd+1應該就是第一個用于通信的文件描述符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ā)出去來表示字符串結(jié)束,否則數(shù)據(jù)可能出錯(比實際數(shù)據(jù)長),出現(xiàn)亂碼? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }
? ??close(lfd);return0;}

 

相關推薦

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

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