IO多路複用之select、poll以及epoll

進程的阻塞

處理運行態的進程(獲得CPU資源),由於需要等待一些事件的發送而不能繼續執行時,就會祖東的轉爲阻塞狀態,這是,他是不佔用CPU資源的。因此也只有處於運行態的進程,才能夠轉爲阻塞狀態。這時候調度器會切換到其他進程,一旦這個進程等待的事件發生了,那麼就會重新喚醒這個進程,重新等待被調度。一旦被調度器調度,那麼就會恢復到切換之前的狀態,繼續執行,由阻塞狀態切換到運行態。

文件描述符

一個用於對文件引用的抽象化概念。形式上就是一個非負整數,一個索引值,指向內核爲每個進程維護的進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件的時候,內核就會向進程返回一個文件描述符。在linux中,將所有的外圍設備也抽象爲文件,所以在驅動程序開發中,打開一個設備,首先就需要獲得這個設備對應的文件描述符,然後持有這個描述符,就可以打開並操作這個設備了。

緩存IO

其實就是標準IO,在linux中,當進程需要訪問文件中數據時,會先將數據從磁盤文件拷貝到內核的緩衝區,然後再從內核緩衝區拷貝到進程的地址空間中。這樣做的好處是實現了數據的共享,和保證數據在緩衝區中的時間儘可能的長。爲什麼這麼說呢,因爲從磁盤中讀取一個文件的速度和從內存中拷貝一個數據的速度相差是100倍,一旦發生了一次數據的共享,那麼這麼一次週轉的開銷就是非常值得的。如果緩衝區中沒有,再從磁盤中去拷貝,這個過程進行了多次數據的拷貝操作,帶來的CPU和內存的開銷是非常大的。

IO模式

當一個read操作發生時,會經歷兩個階段:
1.數據從磁盤中拷貝到內核緩衝區
2.數據從內核緩衝區拷貝到進程地址空間。
正是由於這兩個階段,linux產生了下面五種網絡模式的方案。

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路複用
  • 異步I/O
  • 信號驅動I/O

阻塞I/O(blocking IO)

默認情況中所有的socket都是阻塞的,當用戶進程調用了recvfrom這個系統調用,然後kernel就會進入上述的兩個階段,這時候進程都是被阻塞的。當kernel一直等到數據準備好了,並拷貝到了用戶進程地址空間,然後kernel返回結果,用戶進程纔會解除block的狀態,重新運行起來。

非阻塞I/O

可以將socket設置爲非阻塞的。當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶內存,然後返回。
所以,特點就是用戶進程需要不斷的主動詢問kernel數據是否準備好。

IO多路複用(IO multiplexing)

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。
當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。

所以,I/O 多路複用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回。在IO multiplexing Model中,實際中,對於每一個socket,一般都設置成爲non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

異步I/O

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

I/O 多路複用之select、poll、epoll詳解

select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select

select函數監視的文件描述符有3類,writefd,readfd,exceptfd.調用select函數會阻塞,直到有描述符就緒,或者超時,函數會返回。返回之後,可以通過遍歷fdset,找出就緒的描述符,然後再調用recvfrom函數將數據從緩衝區拷貝到進程地址空間。
缺點在於單個進程能夠監測的文件描述符的數量存在最大限制,linux上一般爲1024。通過重新編譯內核或者修改宏定義可以提升這個限制,但是會造成效率的降低。

poll

不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。從上面看,select和poll都需要在返回後,通過遍歷文件描述符來獲取已經就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

int epoll_create(int size);//創建一個epoll的句柄,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);
  1. int epoll_create(int size);
    創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大,這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值,參數size並不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。
    當創建好epoll句柄後,它就會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    函數是對指定描述符fd執行op操作。

    • epfd:是epoll_create()的返回值。
    • op:表示op操作,用三個宏來表示:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對fd的監聽事件。
    • fd:是需要監聽的fd(文件描述符)
    • epoll_event:是告訴內核需要監聽什麼事,struct epoll_event結構如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏
  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待epfd上的io事件,最多返回maxevents個事件。
    參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。

     epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
      LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
      ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

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