本文轉(zhuǎn)自該處瘾带,由于這篇文章寫的非常好就沒有再單獨(dú)總結(jié)旭旭。感謝作者V惺!;闼摹!
作者:涼拌姨媽好吃
鏈接:http://www.reibang.com/p/6a6845464770
來源:簡書
簡書著作權(quán)歸作者所有液兽,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處换况。
首先引用levin的回答讓我們理清楚五種IO模型
1.阻塞I/O模型(同步阻塞)
老李去火車站買票琅摩,排隊三天買到一張退票此迅。
耗費(fèi):在車站吃喝拉撒睡 3天汽畴,其他事一件沒干旧巾。
2.非阻塞I/O模型(同步非阻塞)
老李去火車站買票耸序,隔12小時去火車站問有沒有退票,三天后買到一張票鲁猩。耗費(fèi):往返車站6次坎怪,路上6小時,其他時間做了好多事廓握。
3.I/O復(fù)用模型
1.select/poll(同步非阻塞)
老李去火車站買票搅窿,委托黃牛,然后每隔6小時電話黃牛詢問隙券,黃牛三天內(nèi)買到票男应,然后老李去火車站交錢領(lǐng)票。
耗費(fèi):往返車站2次娱仔,路上2小時沐飘,黃牛手續(xù)費(fèi)100元,打電話17次
2.epoll(異步非阻塞)
老李去火車站買票牲迫,委托黃牛耐朴,黃牛買到后即通知老李去領(lǐng),然后老李去火車站交錢領(lǐng)票盹憎。
耗費(fèi):往返車站2次筛峭,路上2小時,黃牛手續(xù)費(fèi)100元陪每,無需打電話
4.信號驅(qū)動I/O模型(異步非阻塞)
老李去火車站買票影晓,給售票員留下電話镰吵,有票后,售票員電話通知老李俯艰,然后老李去火車站交錢領(lǐng)票捡遍。
耗費(fèi):往返車站2次,路上2小時竹握,免黃牛費(fèi)100元画株,無需打電話
5.異步I/O模型(異步非阻塞)
老李去火車站買票,給售票員留下電話啦辐,有票后谓传,售票員電話通知老李并快遞送票上門。
耗費(fèi):往返車站1次芹关,路上1小時续挟,免黃牛費(fèi)100元,無需打電話
1.I/O多路復(fù)用
1.1 它的形成原因
如果一個I/O流進(jìn)來侥衬,我們就開啟一個進(jìn)程處理這個I/O流诗祸。那么假設(shè)現(xiàn)在有一百萬個I/O流進(jìn)來,那我們就需要開啟一百萬個進(jìn)程一一對應(yīng)處理這些I/O流(——這就是傳統(tǒng)意義下的多進(jìn)程并發(fā)處理)轴总。思考一下直颅,一百萬個進(jìn)程,你的CPU占有率會多高怀樟,這個實(shí)現(xiàn)方式及其的不合理功偿。所以人們提出了I/O多路復(fù)用這個模型,一個線程往堡,通過記錄I/O流的狀態(tài)來同時管理多個I/O械荷,可以提高服務(wù)器的吞吐能力。
1.2 通過它的英文單詞來理解一下I/O多路復(fù)用
I/O multiplexing 也就是我們所說的I/O多路復(fù)用虑灰,但是這個翻譯真的很不生動吨瞎,所以我更喜歡將它拆開,變成 I/O multi plexing
multi意味著多穆咐,而plex意味著叢(叢:聚集颤诀,許多事物湊在一起。)庸娱,那么字面上來看I/O multiplexing 就是將多個I/O湊在一起着绊。就像下面這張圖的前半部分一樣,中間的那條線就是我們的單個線程熟尉,它通過記錄傳入的每一個I/O流的狀態(tài)來同時管理多個IO归露。
1.3 I/O多路復(fù)用的實(shí)現(xiàn)
- 當(dāng)進(jìn)程調(diào)用select,進(jìn)程就會被阻塞
- 此時內(nèi)核會監(jiān)視所有select負(fù)責(zé)的的socket斤儿,當(dāng)socket的數(shù)據(jù)準(zhǔn)備好后剧包,就立即返回恐锦。
- 進(jìn)程再調(diào)用read操作,數(shù)據(jù)就會從內(nèi)核拷貝到進(jìn)程疆液。
其實(shí)多路復(fù)用的實(shí)現(xiàn)有多種方式:select一铅、poll、epoll
1.3.1 select實(shí)現(xiàn)方式
先理解一下select這個函數(shù)的形參都是什么
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- nfds:指定待測試的描述子個數(shù)
- readfds,writefds,exceptfds:指定了我們讓內(nèi)核測試讀堕油、寫和異常條件的描述字
- fd_set:為一個存放文件描述符的信息的結(jié)構(gòu)體潘飘,可以通過下面的宏進(jìn)行設(shè)置。
void FD_ZERO(fd_set *fdset);
//清空集合
void FD_SET(int fd, fd_set *fdset);
//將一個給定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);
//將一個給定的文件描述符從集合中刪除
int FD_ISSET(int fd, fd_set *fdset);
// 檢查集合中指定的文件描述符是否可以讀寫
- timeout:內(nèi)核等待指定的描述字中就緒的時間長度
- 返回值:失敗-1 超時0 成功>0
#define FILE "/dev/input/mouse0"
int main(void)
{
int fd = -1;
int sele_ret = -1;
fd_set Fd_set;
struct timeval time = {0};
char buf[10] = {0};
//打開設(shè)備文件
fd = open(FILE, O_RDONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
//構(gòu)建多路復(fù)用IO
FD_ZERO(&Fd_set); //清除全部fd
FD_SET(0, &Fd_set); //添加標(biāo)準(zhǔn)輸入
FD_SET(fd, &Fd_set); //添加鼠標(biāo)
time.tv_sec = 10; //設(shè)置阻塞超時時間為10秒鐘
time.tv_usec = 0;
sele_ret = select(fd+1, &Fd_set, NULL, NULL, &time);
if (0 > sele_ret)
{
perror("select error");
exit(-1);
}
else if (0 == sele_ret)
{
printf("無數(shù)據(jù)輸入掉缺,等待超時.\n");
}
else
{
if (FD_ISSET(0, &Fd_set)) //監(jiān)聽得到得到的結(jié)果若是鍵盤,則讓去讀取鍵盤的數(shù)據(jù)
{
memset(buf, 0, sizeof(buf));
read(0, buf, sizeof(buf)/2);
printf("讀取鍵盤的內(nèi)容是: %s.\n", buf);
}
if (FD_ISSET(fd, &Fd_set)) //監(jiān)聽得到得到的結(jié)果若是鼠標(biāo),則去讀取鼠標(biāo)的數(shù)據(jù)
{
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf)/2);
printf("讀取鼠標(biāo)的內(nèi)容是: %s.\n", buf);
}
}
//關(guān)閉鼠標(biāo)設(shè)備文件
close(fd);
return 0;
}
1.3.2 poll實(shí)現(xiàn)方式
先理解一下poll這個函數(shù)的形參是什么
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- pollfd:又是一個結(jié)構(gòu)體
struct pollfd {
int fd; //文件描述符
short events; //請求的事件(請求哪種操作)
short revents; //返回的事件
};
- nfds:指定待測試的描述子個數(shù)
- timeout:內(nèi)核等待指定的描述字中就緒的時間長度
- timeout:內(nèi)核等待指定的描述字中就緒的時間長度
#define FILE "/dev/input/mouse0"
int main(void)
{
int fd = -1;
int poll_ret = 0;
struct pollfd poll_fd[2] = {0};
char buf[100] = {0};
//打開設(shè)備文件
fd = open(FILE, O_RDONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
//構(gòu)建多路復(fù)用IO
poll_fd[0].fd = 0; //鍵盤
poll_fd[0].events = POLLIN; //定義請求的事件為讀數(shù)據(jù)
poll_fd[1].fd = fd; //鼠標(biāo)
poll_fd[1].events = POLLIN; //定義請求的事件為讀數(shù)據(jù)
int time = 10000; //定義超時時間為10秒鐘
poll_ret = poll(poll_fd, fd+1, time);
if (0 > poll_ret)
{
perror("poll error");
exit(-1);
}
else if (0 == poll_ret)
{
printf("阻塞超時.\n");
}
else
{
if (poll_fd[0].revents == poll_fd[0].events)
//監(jiān)聽得到得到的結(jié)果若是鍵盤,則讓去讀取鍵盤的數(shù)據(jù)
{
memset(buf, 0, sizeof(buf));
read(0, buf, sizeof(buf)/2);
printf("讀取鍵盤的內(nèi)容是: %s.\n", buf);
}
if (poll_fd[1].revents == poll_fd[1].events)
//監(jiān)聽得到得到的結(jié)果若是鼠標(biāo),則去讀取鼠標(biāo)的數(shù)據(jù)
{
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf)/2);
printf("讀取鼠標(biāo)的內(nèi)容是: %s.\n", buf);
}
}
//關(guān)閉文件
close(fd);
return 0;
}
1.3.3 epoll實(shí)現(xiàn)方式
epoll操作過程中會用到的重要函數(shù)
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- int epoll_create(int size):創(chuàng)建一個epoll的句柄卜录,size表示監(jiān)聽數(shù)目的大小。創(chuàng)建完句柄它會自動占用一個fd值眶明,使用完epoll一定要記得close艰毒,不然fd會被消耗完。
- int epoll_ctl:這是epoll的事件注冊函數(shù)搜囱,和select不同的是select在監(jiān)聽的時候會告訴內(nèi)核監(jiān)聽什么樣的事件丑瞧,而epoll必須在epoll_ctl先注冊要監(jiān)聽的事件類型。
它的第一個參數(shù)返回epoll_creat的執(zhí)行結(jié)果
第二個參數(shù)表示動作蜀肘,用下面幾個宏表示
EPOLL_CTL_ADD:注冊新的fd到epfd中绊汹;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd幌缝;
第三參數(shù)為監(jiān)聽的fd,第四個參數(shù)是告訴內(nèi)核要監(jiān)聽什么事
- int epoll_wait:等待事件的發(fā)生灸促,類似于select的調(diào)用
2. select
2.1 select函數(shù)的調(diào)用過程
a. 從用戶空間將fd_set拷貝到內(nèi)核空間
b. 注冊回調(diào)函數(shù)
c. 調(diào)用其對應(yīng)的poll方法
d. poll方法會返回一個描述讀寫是否就緒的mask掩碼诫欠,根據(jù)這個mask掩碼給fd_set賦值涵卵。
e. 如果遍歷完所有的fd都沒有返回一個可讀寫的mask掩碼,就會讓select的進(jìn)程進(jìn)入休眠模式荒叼,直到發(fā)現(xiàn)可讀寫的資源后轿偎,重新喚醒等待隊列上休眠的進(jìn)程。如果在規(guī)定時間內(nèi)都沒有喚醒休眠進(jìn)程被廓,那么進(jìn)程會被喚醒重新獲得CPU坏晦,再去遍歷一次fd。
f. 將fd_set從內(nèi)核空間拷貝到用戶空間
2.2 select函數(shù)優(yōu)缺點(diǎn)
缺點(diǎn):兩次拷貝耗時嫁乘、輪詢所有fd耗時昆婿,支持的文件描述符太小
優(yōu)點(diǎn):跨平臺支持
3. poll
3.1 poll函數(shù)的調(diào)用過程(與select完全一致)
3.2 poll函數(shù)優(yōu)缺點(diǎn)
優(yōu)點(diǎn):連接數(shù)(也就是文件描述符)沒有限制(鏈表存儲)
缺點(diǎn):大量拷貝,水平觸發(fā)(當(dāng)報告了fd沒有被處理蜓斧,會重復(fù)報告仓蛆,很耗性能)
4. epoll
4.1 epoll的ET與LT模式
LT:延遲處理,當(dāng)檢測到描述符事件通知應(yīng)用程序挎春,應(yīng)用程序不立即處理該事件看疙。那么下次會再次通知應(yīng)用程序此事件豆拨。
ET:立即處理,當(dāng)檢測到描述符事件通知應(yīng)用程序能庆,應(yīng)用程序會立即處理施禾。
ET模式減少了epoll被重復(fù)觸發(fā)的次數(shù),效率比LT高搁胆。我們在使用ET的時候弥搞,必須采用非阻塞套接口,避免某文件句柄在阻塞讀或阻塞寫的時候?qū)⑵渌募枋龇娜蝿?wù)餓死
4.2 epoll的函數(shù)調(diào)用流程
a. 當(dāng)調(diào)用epoll_wait函數(shù)的時候渠旁,系統(tǒng)會創(chuàng)建一個epoll對象拓巧,每個對象有一個evenpoll類型的結(jié)構(gòu)體與之對應(yīng),結(jié)構(gòu)體成員結(jié)構(gòu)如下一死。
rbn,代表將要通過epoll_ctl向epll對象中添加的事件肛度。這些事情都是掛載在紅黑樹中。
rdlist投慈,里面存放的是將要發(fā)生的事件
b. 文件的fd狀態(tài)發(fā)生改變承耿,就會觸發(fā)fd上的回調(diào)函數(shù)
c. 回調(diào)函數(shù)將相應(yīng)的fd加入到rdlist,導(dǎo)致rdlist不空伪煤,進(jìn)程被喚醒加袋,epoll_wait繼續(xù)執(zhí)行。
d. 有一個事件轉(zhuǎn)移函數(shù)——ep_events_transfer抱既,它會將rdlist的數(shù)據(jù)拷貝到txlist上职烧,并將rdlist的數(shù)據(jù)清空。
e. ep_send_events函數(shù)防泵,它掃描txlist的每個數(shù)據(jù)蚀之,調(diào)用關(guān)聯(lián)fd對應(yīng)的poll方法去取fd中較新的事件,將取得的事件和對應(yīng)的fd發(fā)送到用戶空間捷泞。如果fd是LT模式的話足删,會被txlist的該數(shù)據(jù)重新放回rdlist,等待下一次繼續(xù)觸發(fā)調(diào)用锁右。
4.3 epoll的優(yōu)點(diǎn)
1.沒有最大并發(fā)連接的限制
2.只有活躍可用的fd才會調(diào)用callback函數(shù)
3.內(nèi)存拷貝是利用mmap()文件映射內(nèi)存的方式加速與內(nèi)核空間的消息傳遞失受,減少復(fù)制開銷。(內(nèi)核與用戶空間共享一塊內(nèi)存)
只有存在大量的空閑連接和不活躍的連接的時候咏瑟,使用epoll的效率才會比select/poll高