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;
}