五種典型的IO模型

首先,IO過程是什麼?-----等待+數據拷貝

  1. 發起IO調用
  2. 等待IO條件就緒
  3. 將數據拷貝到緩衝區中進行處理

阻塞IO

操作流程是順序的,一個完成後進行下一個
爲完成IO發起調用,若當前不具備IO條件則一直等待,這個過程不幹其他事情,條件滿足後才進行操作

大部分的IO都是阻塞IO
:流程控制特別簡單,清晰:一個IO完了纔會進入下一個IO
:對於資源沒有充分利用,大部分時間都在等待IO就緒。爲彌補缺點提出非阻塞IO

非阻塞IO

操作流程是順序的,利用了等待的時間
爲完成IO發起調用,若當前不具備IO條件則立即返回【常需要循環操作,利用等待時間完成其他操作後重新發起IO】,條件滿足後才進行操作

:對資源利用更加充分
:相對阻塞IO複雜了一點;IO操作不夠實時

信號驅動IO

操作流程是不定的,不用等待IO就緒
定義IO就緒信號,收到信號後在處理方式裏進行IO操作,IO就緒時通知進程,讓進程去進行IO

:IO更加實時,對資源的利用也更加充分
:流程更加複雜

異步IO

操作流程是不定的,由別人完成功能
定義IO完成信號,通過異步IO調用告訴OS:IO哪些數據拷貝到哪裏,IO的等待過程及數據拷貝都由OS完成

:對資源利用更加充分【能同時發起多個IO調用】
:流程更加複雜

多路轉接IO【多路複用】

對大量的描述符集中進行IO事件監控,告訴程序員/進程當前哪些描述符就緒了哪些事件,程序員/進程可以直接對就緒了對應事件的描述符進行相應的操作,避免對沒有就緒的描述符進行的IO操作導致的效率降低/程序流程阻塞
IO事件:

  • 可讀事件
  • 可寫事件
  • 異常事件

只要是需要監控描述符的場景都可以使用多路轉換IO,與服務端/客戶端、TCP/UDP這些無關

監控的好處?
讓進程可以只針對就緒了指定事件的描述符進行操作,避免因沒有就緒的描述符操作導致的阻塞

使用場景:只要對描述符有(可讀/可寫/異常)事件監控的需求就都可以使用多路轉接模型
適用場景:適用於對大量描述符進行監控,但是同-時間只有少量描述符活躍的場景
多路轉接模型的併發與多線程/多進程的併發並行不同之處?
多路轉接模型可以和線程池搭配適用

多路轉接IO模型

多路轉接IO的實現模型:select/poll/epoll
epoll和select【常考點】
多路轉接IO的模型都是對描述符事件進行監控
select模型:
操作流程:

  1. 程序員定義指定事件的描述符集合【可讀/可寫/異常事件的描述符集合】,初始化清空集合,對那個描述符關心什麼事件,就將該描述符添加到相應事件的描述符集合中去
    FD_ZERO(fd_set* set)–清空,FD_SET(int fd,fd_set *set)–將fd描述符添加到set集合
  2. 發起監控調用,將事件描述符集合拷貝到內核(在內核中才能看到是否就緒)中進行監控,監控的原理是輪詢遍歷判斷
    int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set exceptfds,struct timeval *timeout)
    可讀事件的就緒:接收緩衝區中的數據大小大於低水位標記【量化標準:常默認爲1字節】
    可寫事件的就緒:發送緩衝區中剩餘空間的大小大於低水位標記【量化標準:常默認爲1字節】
    異常事件的就緒:描述符是否產生了某個異常
  3. 監控調用的返回,表示監控出錯/描述符就緒/監控等待超時;調用返回時,將事件監控的描述符集合中未就緒的描述符從集合中移除【集合中只保留就緒的描述符
    由於返回時修改了集合,下次監控時要重新向集合中添加描述符
  4. 程序員輪詢判斷哪個描述符還在就緒的描述符集合中,從而確定描述符是否就緒了某個事件,然後進行對應事件的操作
    void FD_ISSET(int fd,fd_set*)–判斷fd描述符是否在集合中
  5. 若對那個描述符解除監控
    void FD_CLR(int fd,fd_set*)–從set集合中刪除描述符fd

select不會直接返回給用戶就緒的描述符直接操作,而是返回就緒的描述符集合,需要程序員自行判斷

缺點:
select所能監控的描述符數量有最大限制,取決於宏_FD_SETSIZE,默認1024
select監控原理:是輪詢遍歷,因此性能會隨着描述符的增多而下降
select需要進程進行遍歷判斷,才能得知哪個描述符就緒了哪個事件
select每次監控都需要重新向集合中添加描述符,並拷貝到內核中
優點:
跨平臺移植性較好

poll
接口:int poll(struct pollfd *arry_fds,nfds_t nfds,int timeout)
採用事件結構體的形式

  • struct pollfd{
    int fd;//要監控的描述符
    short events;//要監控的事件eg:POLLIN\POLLOUT
    short revents//調用返回時的就緒事件
    }
  • arry_fds:事件結構體數組,填充要監控的描述符以及事件信息
  • nfds:數組中有效節點個數【數組中目前要監控的節點】
  • timeout:監控的超時等待時間【單位:ms】
  • 返回值:
    大於0:標識就緒的描述符事件個數
    等於0:等待超時
    小於0:監控出錯

操作流程:

  1. 定義監控的描述符事件結構體數組,將要監控的描述符及事件標識信息添加到數組的各個節點----pollfd
  2. 發起調用開始監控,將描述符事件結構體數組拷貝到內核,進行輪詢遍歷判斷,若有就緒/等待 /等待超時則調用返回,並在每個描述符對應的事件結構體中標識當前就緒的事件----poll
  3. 進程輪詢遍歷數組、判斷數組中每個節點的就緒事件是那個事件,從而決定相應的操作

缺點:
poll依然在內核中式輪詢遍歷監控,性能隨描述符增多而下降
poll依然每次需要重新向內核拷貝數據
poll跨平臺移植性差
優點
用事件結構體形式對描述符進行監控,簡化了select中三種事件集合的操作流程
所能監控的描述符數量不做限制
不需要重複添加描述符

epoll
linux下最好用,性能最高的多路轉接模型
操作流程:

  1. 發起調用在內核中創建epoll句柄,epollevent結構體
    int epoll_creat(int size)—>size在linux2.6.2之後被忽略,大於0即可
    返回文件描述符【epoll操作句柄】
  2. 發起調用對內核中的epollevent結構添加/刪除/修改所監控的描述符監控信息
    int epoll_ctl(int epfd,int cmd,int _fd,struct epoll_event *ev)
    epfd:操作句柄
    cmd:針對fd描述法的監控信息進行的操作:添加-EPOLL_CTL_ADD/刪除-EPOLL_CTL_DEL/修改-EPOLL_CTL_MOD
    fd:要監控的描述符
    ev:fd描述符對應的事件結構體信息--------struct epoll event{ uint32_t events --要監控的事件; union{int fd; void *ptr}data --就緒後返回進行操作的數據;}
  3. 發起調用開始監控,在內核中採用異步阻塞操作實現監控,等待超時/有描述符就緒的事件調用返回
    int epoll_wait(int epfd,struct epoll_event *evs,int max_event,int timeout)
    epfd:epoll操作句柄
    evs:struct epoll_event結構體數組的首地址,用於接收就緒描述符對應的事件結構體信息
    max_ event:本次監控想要獲取的就緒事件的最大數量,不大於evs數組的節點個數
    timeout:等待超時時間
    返回值:大於0:–就緒的事件個數;等於0—等待超時;小於0—監控出錯
  4. 進程直接對就緒的事件結構體中的描述符成員進行操作

優點:
1.沒有描述符監控數量的上限
2.監控信息只需要向內核添加一次
3.監控使用異步阻塞操作完成,性能不會隨着描述符的增多而下降
4.直接向用戶返回就緒的事件信息(包含描述符在內) ,進程直接可以針對描述符以及事件進行操作,不需要判斷有沒有就緒了
缺點:
1.跨平臺移植性差

一旦epoll開始監控,描述符若就緒了進程關心的事件,就會給用戶返回所添加的對應事件結構體信息,通過事件結構體信息中包含的描述符進行操作,因此union中的fd與epoll_ctl第三個參數通常是同一個描述符

epoll監控原理:異步阻塞操作
監控由系統完成,用戶添加監控的描述符以及對應事件結構體會被添加到內核的eventpoll結構體中的紅黑樹中。一旦發起調用開始監控,OS會爲每個描述符的事件做一個回調函數,功能是當描述符就緒了關心的事件,就將描述符對應的事件結構體添加到雙向鏈表中。進程自己只需要每隔一段時間,判斷雙向鏈表是否爲空,決定是否已經就緒

流程:

epoll中就緒事件的觸發方式:

  • 水平觸發方式–LT
    可讀事件:接收緩衝區中數據大小大於低水位標記,就會觸發可讀事件
    可寫事件:發送緩衝區中剩餘空間大小大於低水位標記,就會觸發可寫事件
    低水位標記:基準衡量值,默認爲1個字節
  • 邊緣觸發方式–ET
    可讀事件:只有新數據到來的時候,纔會觸發一次事件
    可寫事件:發送緩衝區中剩餘空間從無到有的時候纔會觸發一次事件

邊緣觸發,因爲觸發方式的不同,要求進程中事件觸發進行數據接收的時候,要求最好能夠一次將所有數據全部讀取(因爲剩餘數據不會觸發第二次事件,只有新數據到來的時候纔會觸發) 。然而循環讀取能夠保證讀完緩衝區中的所有數據,但是在沒有數據的時候就會造成阻塞,因此邊緣觸發方式中,描述符的操作都採用非阻塞操作
非阻塞的描述符操作在沒有數據/超時的情況下會報錯返回: EAGAIN/EWOULDBLOCK

如何將描述符設置爲非阻塞(描述符的所有操作都爲非阻塞操作)?
int fcntl(int fd,int cmd…/arg/);
recv(fd, buf, len, flag) :flag–MSG_ DONTWAIT將本次接收操作設爲非阻塞
fd:指定的描述符
cmd: F_GETFL/F_SETFL --獲取/設置描述符的屬性信息:O_NONBLOCK【非阻塞屬性】
arg:要設置的屬性信息/獲取的屬性信息F_GETFL使用的時候,arg被忽略,默認設置即可

邊緣觸發:爲了防止一些事件不斷觸發(接收數據後(按條【指定長度】取 ),緩衝區中留有半條, 就會不斷觸發事件,這種情況要不然上層操作,將半條數據讀取出來,外部維護,要不然就使用邊緣觸發,等待新數據到來數據完整在觸發事件

阻塞、非阻塞、同步、異步

調用
阻塞:爲完成一功能發起調用,當前不具備完成條件,則一直等待
非阻塞:爲完成一功能發起調用,當前不具備完成條件,立即報錯返回
處理流程
同步:處理流程:順序處理【清晰明瞭】,因爲功能都由進程自己來完成
異步:處理流程:順序不定【無法確定什麼時間完成】,功能由OS來完成

注意區分:同步互斥/處理流程的同步異步/ajax的同步異步

  • 異步阻塞:調用等待着OS完成功能
  • 異步非阻塞:調用立即返回

對於同步而言,由於是順序調用的,大多認爲是阻塞的,因爲只有完成一個才能進行下一個

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章