IO多路復(fù)用的幾種實現(xiàn)機(jī)制的分析
服務(wù)器端編程經(jīng)常需要構(gòu)造高性能的IO模型,常見的IO模型有四種:
(1)同步阻塞IO(Blocking IO):即傳統(tǒng)的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默認(rèn)創(chuàng)建的socket都是阻塞的,非阻塞IO要求socket被設(shè)置為NONBLOCK。注意這里所說的NIO并非Java的NIO(New IO)庫。
(3)IO多路復(fù)用(IO Multiplexing):即經(jīng)典的Reactor設(shè)計模式,有時也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
(4)異步IO(Asynchronous IO):即經(jīng)典的Proactor設(shè)計模式,也稱為異步非阻塞IO。
同步和異步的概念描述的是用戶線程與內(nèi)核的交互方式:同步是指用戶線程發(fā)起IO請求后需要等待或者輪詢內(nèi)核IO操作完成后才能繼續(xù)執(zhí)行;而異步是指用戶線程發(fā)起IO請求后仍繼續(xù)執(zhí)行,當(dāng)內(nèi)核IO操作完成后會通知用戶線程,或者調(diào)用用戶線程注冊的回調(diào)函數(shù)。
阻塞和非阻塞的概念描述的是用戶線程調(diào)用內(nèi)核IO操作的方式:阻塞是指IO操作需要徹底完成后才返回到用戶空間;而非阻塞是指IO操作被調(diào)用后立即返回給用戶一個狀態(tài)值,無需等到IO操作徹底完成。
select和poll的實現(xiàn)比較相似,目前也有很多為人詬病的缺點,epoll可以說是select和poll的增強(qiáng)版。
一、select實現(xiàn)
1、使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間
2、注冊回調(diào)函數(shù)__pollwait
3、遍歷所有fd,調(diào)用其對應(yīng)的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據(jù)情況會調(diào)用到tcp_poll,udp_poll或者datagram_poll)
4、以tcp_poll為例,其核心實現(xiàn)就是__pollwait,也就是上面注冊的回調(diào)函數(shù)。
5、__pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊列中,不同的設(shè)備有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk-》sk_sleep(注意把進(jìn)程掛到等待隊列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù)據(jù)(磁盤設(shè)備)后,會喚醒設(shè)備等待隊列上睡眠的進(jìn)程,這時current便被喚醒了。
6、poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據(jù)這個mask掩碼給fd_set賦值。
7、如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是current)進(jìn)入睡眠。當(dāng)設(shè)備驅(qū)動發(fā)生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進(jìn)程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調(diào)用select的進(jìn)程會重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒有就緒的fd。
8、把fd_set從內(nèi)核空間拷貝到用戶空間。
總結(jié):
select的幾大缺點:
(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在fd很多時會很大
(2)同時每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個開銷在fd很多時也很大
(3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
二、poll實現(xiàn)
poll的實現(xiàn)和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結(jié)構(gòu)而不是select的fd_set結(jié)構(gòu)。其他的都差不多。
三、epoll實現(xiàn)
epoll既然是對select和poll的改進(jìn),就應(yīng)該能避免上述的三個缺點。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個函數(shù)——select或者poll函數(shù)。而epoll提供了三個函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。
對于第一個缺點,epoll的解決方案在epoll_ctl函數(shù)中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。
對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊列上的等待者時,就會調(diào)用這個回調(diào)函數(shù),而這個回調(diào)函數(shù)會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現(xiàn)睡一會,判斷一會的效果,和select實現(xiàn)中的第7步是類似的)。
說明一下這個回調(diào)機(jī)制的原理,其實很簡單,看一下select和epoll在把current加入fd對應(yīng)的設(shè)備等待隊列時使用的代碼:
select:
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_table_entry *entry = poll_get_entry(p);
if (!entry)
return;
get_file(filp);
entry-》filp = filp;
entry-》wait_address = wait_address;
init_waitqueue_entry(&entry-》wait, current);
add_wait_queue(wait_address, &entry-》wait);
}
其中init_waitqueue_entry實現(xiàn)如下:
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
q-》flags = 0;
q-》private = p;
q-》func = default_wake_function;
}
上面的代碼是說建立一個poll_table_entry結(jié)構(gòu)entry,首先把current設(shè)置為entry-》wait的private成員,同時把default_wake_function設(shè)為entry-》wait的func成員,然后把entry-》wait鏈入到wait_address中(這個wait_address就是設(shè)備的等待隊列,在tcp_poll中就是sk_sleep)。
再看一下epoll:
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi-》nwait 》= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq-》wait, ep_poll_callback);
pwq-》whead = whead;
pwq-》base = epi;
add_wait_queue(whead, &pwq-》wait);
list_add_tail(&pwq-》llink, &epi-》pwqlist);
epi-》nwait++;
} else {
/* We have to signal that an error occurred */
epi-》nwait = -1;
}
}
其中init_waitqueue_func_entry的實現(xiàn)如下:
static inline void init_waitqueue_func_entry(wait_queue_t *q,
wait_queue_func_t func)
{
q-》flags = 0;
q-》private = NULL;
q-》func = func;
}
可以看到,總體和select的實現(xiàn)是類似的,只不過它是創(chuàng)建了一個eppoll_entry結(jié)構(gòu)pwq,只不過pwq-》wait的func成員被設(shè)置成了回調(diào)函數(shù)ep_poll_callback(而不是default_wake_function,所以這里并不會有喚醒操作,而只是執(zhí)行回調(diào)函數(shù)),private成員被設(shè)置成了NULL。最后吧pwq-》wait鏈入到whead中(也就是設(shè)備等待隊列中)。這樣,當(dāng)設(shè)備等待隊列中的進(jìn)程被喚醒時,就會調(diào)用ep_poll_callback了。
再梳理一下,當(dāng)epoll_wait時,它會判斷就緒鏈表中有沒有就緒的fd,如果沒有,則把current進(jìn)程加入一個等待隊列(file-》private_data-》wq)中,并在一個while(1)循環(huán)中判斷就緒隊列是否為空,并結(jié)合schedule_timeout實現(xiàn)睡一會,判斷一會的效果。如果current進(jìn)程在睡眠中,設(shè)備就緒了,就會調(diào)用回調(diào)函數(shù)。在回調(diào)函數(shù)中,會把就緒的fd放入就緒鏈表,并喚醒等待隊列(file-》private_data-》wq)中的current進(jìn)程,這樣epoll_wait又能繼續(xù)執(zhí)行下去了。
對于第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
總結(jié):
1、select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機(jī)制帶來的性能提升。
2、select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。
非常好我支持^.^
(1) 100%
不好我反對
(0) 0%
相關(guān)閱讀:
- [電子說] 通用開關(guān)與矩陣開關(guān)卡及多路復(fù)用開關(guān)的對比 2023-10-23
- [電子說] 普源精電(RIGOL)為開環(huán)多路溫度測試方案提供產(chǎn)品支持 2023-10-13
- [電子說] 基于多路復(fù)用模型的Netty框架 2023-09-30
- [電子說] Coherent推出WaveMaker和WaveShaper系列產(chǎn)品 2023-09-22
- [電子說] 使用普通的多路復(fù)用器進(jìn)行切換時鐘會發(fā)生什么? 2023-09-20
- [電子說] mos管電流方向是單向 2023-09-07
- [電子說] 新一代多路復(fù)用ADC如何簡化復(fù)雜系統(tǒng)設(shè)計 2023-09-04
- [電子說] 觸發(fā)器的輸出是現(xiàn)態(tài)函數(shù) 2023-08-24
( 發(fā)表人:龔婷 )