1. 程式人生 > >I/O複用(I/O multiplexing): select, pselect, poll, ppoll, epoll

I/O複用(I/O multiplexing): select, pselect, poll, ppoll, epoll

I/O複用:select, pselect, poll, epoll.

  • 注意:本文主要介紹的是epoll相關知識,無法確保正確

1. 相關問題:

  • 1.1 什麼是I/O複用?
  • 1.2 四個I/O複用方法相關知識點?
  • 1.3 四個I/O複用方法的比較?
  • 1.4 epoll有哪些觸發模式?有何區別?
  • 1.5 select 什麼情況下返回?
  • 1.6 如果select返回可讀,結果只讀到0位元組,什麼情況?
  • 1.7 兩個epoll等待同一個檔案描述符會發生什麼?[事件發生時會同時返回給兩個epoll例項]
  • 1.8 如果epoll 把自己epoll_create()返回的描述符放入自己檔案描述符集裡面,會有發生什麼情況?
  • 1.9 如何設計大規模的併發模型?

[參見man epoll手冊後面的9個問題]

2.拓展問題:

  • 2.1 什麼是執行緒安全?

3. 解答

3.1 什麼是I/O複用?

  • I/O複用(I/O multiplexing): 單個執行緒通過記錄跟蹤每一個I/O流的狀態來同時管理多個I/O流.

3.2 四個I/O複用方法相關知識點?

  • poll 和 select的工作機制是:核心遍歷所有監聽中的檔案描述符,返回”準備好”的檔案描述符的個數.

3.2.1 select:

 1). 原型:

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct
timeval *timeout); void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);

 2). 說明:

  • 永遠等待: timeout == NULL;
  • 不等待: timeout->tv_sec == 0 && timeout->tv_usec == 0;
  • 等待指定時間: timeout->tv_sec != 0 || timeout->tv_usec != 0;
  • 宣告描述符集以後,必須用FD_ZERO將描述符集置0!
  • 當第2,3,4個引數都為NULL時,select 只作為定時器.
  • 返回:-1: 出錯;0: 沒有描述符準備好;>0: 準備好的描述符的個數.
  • 描述符阻塞與否不影響select是否阻塞.
  • select關注的最大描述符數是:FD_SETSIZE, 一般為1024.(對於一般程式來說太大了)

3.2.2 pselect:

 1). 原型:

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *timeout,
                   const sigset_t *sigmask);

 和pselect 的區別在於:
 timespec 結構是s+ns(秒+納秒)級別,且為const修飾的.(select 是s+ms)
 pselect 可使用可選訊號遮蔽字.

3.2.3 poll和ppoll:

 1). 原型:

       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <poll.h>

       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *timeout_ts, const sigset_t *sigmask);

 2). 說明:

  • struct pollfd結構:

    struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events,interest */
    short revents; /* returned events ,occurred */
    };
  • timeout == -1:永遠等待
  • timeout == 0: 不等待
  • timeout > 0: 等待timeout**毫秒**!
  • 結構中的events是感興趣的事件;revents是發生(返回)的事件.
  • poll關注的描述符數為nfds(第二個引數),一般為unsigned long 型.

3.2.4 epoll:

  • 由於select和poll的侷限性,linux 2.6 核心引入了event poll(epoll)機制.
  • epoll的工作原理是:建立一個epoll上下文->新增/刪除檔案描述符到epoll上下文(描述符集)->事件等待,記錄發生事件的檔案描述符.

1). 可以通過epoll_create()[不贊成使用]和epoll_create1()建立一個epoll上下文[開啟一個epoll檔案描述符].

  • 原型:
       #include <sys/epoll.h>

       int epoll_create(int size);
       int epoll_create1(int flags);
  • 引數說明:

    • 從linux 2.6.8後,size就被忽略了(但必須大於0)
    • 當flags==0時,epoll_create1()功能和epoll_create()功能一樣;
      flags==EPOLL_CLOEXEC時,新檔案描述符中會設定close-on-exec (FD_CLOEXEC)標誌.(見man 2 open)
  • 返回:

    • 返回值是一個檔案描述符,但是此檔案描述符和真實檔案沒有關係.當不用的時候,應當close().
    • 當返回-1時,代表出錯,errno被設定:
    EINVAL: 無效的flags;[size不是正數]
    EMFILE: 達到使用者能開啟最大檔案數;
    ENFILE: 達到系統能開啟的最大檔案數;
    ENOMEM: 記憶體不足.

2). 可以通過epoll_ctl()新增或刪除檔案描述符到epoll上下文.

  • 原型:
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 說明:

    • 引數op的值:

          EPOLL_CTL_ADD: 指定fd新增到epfd關聯的epoll上下文中,event定義事件;
          EPOLL_CTL_DEL: 刪除;
          EPOLL_CTL_MOD: 修改指定fd的event(監聽行為).
    • struct epoll_event:

         typedef union epoll_data {
             void        *ptr;
             int          fd;
             uint32_t     u32;
             uint64_t     u64;
         } epoll_data_t;
      
         struct epoll_event {
             uint32_t     events;      /* Epoll events */
             epoll_data_t data;        /* User data variable */
         };
    • events 是一個位集(bit set), 可用的值有:

         EPOLLIN: 可讀.
         EPOLLOUT: 可寫.
         EPOLLPRI: 高優先順序資料可讀.
         EPOLLERR: 錯誤條件發生在關聯的檔案描述符中.(epoll_wait總是等待這個事件,不需要把它設定在events中.)
         EPOLLHUP: 結束通話(hangup)發生.(epoll_wait總是等待這個事件,不需要把它設定在events中.)
         EPOLLET: 指定檔案描述符設定為邊緣觸發(預設動作是水平觸發).
                  需要用EPOLL_CTL_MOD呼叫epoll_ctl()重新設定事件才能再監聽.
         EPOLLRDHUP (since Linux 2.6.17): (Stream socket)關閉連線或半關閉寫連線.
                  (This flag is  especially  useful  for  writing simple code to detect peer shutdown when using Edge Triggered monitoring.)
    • 返回值: 成功0,失敗-1,errno唄設定:

         EBADF: epfd或fd不是有效的檔案描述符.
         EEXIST: op是 EPOLL_CTL_ADD,但fd已經註冊過了.
         EINVAL: epfd不是一個epoll檔案描述符或fd和epfd相同或op所請求的操作不被支援.
         ENOENT: op是EPOLL_CTL_MOD或EPOLL_CTL_DEL,但fd還沒有註冊.
         ENOMEM: 記憶體不足.
         ENOSPC: 達到最大監聽數目.[?,百度一下]
         EPERM: 目標fd不支援epoll.

3).等待一個I/O事件發生.

  • 原型:

       #include <sys/epoll.h>
    
       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
       int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);
  • 說明:

    • epoll_wait()等待/收集監聽事件中已經發生的事件.
    • events: 分配好記憶體的結構陣列.
    • maxevents: 使用者指定的events結構陣列的大小(events的最大數目).[這個引數不是很理解.]
    • timeout: -1未定義; 0立即返回,>0指定毫秒.
      返回: 0超時,>0發生事件數目,-1錯誤,errno被設定:
         EBADF: epfd不是有效檔案描述符
         EFAULT: 程序對events指向的記憶體沒有寫許可權.
         EINTR: 呼叫在事件發生或超時前被訊號中斷.
         EINVAL: epfd不是一個epoll檔案描述符,或maxevents<=0.
  • epoll_wait()和epoll_pwait()的關係:

           ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);

    等於

           sigset_t origmask;
    
           sigprocmask(SIG_SETMASK, &sigmask, &origmask);
           ready = epoll_wait(epfd, &events, maxevents, timeout);
           sigprocmask(SIG_SETMASK, &origmask, NULL);

    當sigmask==NULL的時候,兩個函式相等.

4). 邊緣觸發和水平觸發[摘自man epoll]
這裡寫圖片描述

  • 用於讀管道的檔案描述符rfd在epoll例項中註冊了.
  • writer在寫端寫2kB資料到管道.
  • 此時呼叫epoll_wait()會返回rfd,作為”準備好”讀的檔案描述符.
  • reader通過rfd從管道讀取1KB資料.
  • 然後再呼叫一次epoll_wait(). //邊緣觸發和水平觸發的區別在這裡體現.
    • 當在步驟1中註冊使用水平觸發(EPOLLLT)時,步驟5會和步驟3一樣返回rfd,因為此時管道中還有資料.
    • 當使用邊緣觸發(EPOLLET)時,步驟5將可能掛起,儘管有效的資料還在輸入緩衝區中,同時,資料傳送端(寫端)可能還在等待一個反饋.發生這種情況的原因是:邊緣觸發只在檔案描述符狀態發生改變的時候才遞交事件.所以,在步驟5中呼叫者可能會不再等待已經在輸入緩衝區中的資料.
    • 在上面的例子中,rfd上的事件發生後,步驟2寫資料,事件在步驟3銷燬(但輸入緩衝還有資料).因此如果步驟4讀資料但沒有全部讀完,那麼步驟5呼叫epoll_wait()可能未定義地阻塞.
    • 一個程式如果用了EPOLLET標誌的話,應該使用非阻塞檔案描述符來避免讀/寫阻塞把處理多個檔案描述符的任務餓死.
    • 使用邊緣觸發的epoll時,建議:
      • 使用非阻塞檔案描述符
      • 只在read()或write()返回EAGAIN後才等待一個事件(epoll_wait()).

3.3 四個I/O複用方法的比較?

3.3.1 select的問題:

  • select 會修改傳入的引數陣列,對於一個需要呼叫很多次的函式,是非常不友好的。
  • select 遍歷陣列,看哪個準備好,陣列越大,所需時間越長.
  • 描述符上(I/O stream)出現了資料(準備好可讀可寫異常),select 僅僅返回準備好的描述符個數,並不會告訴你是哪個描述符.
  • select 只能監視1024個描述符.
  • select 不是執行緒安全的
  • 核心 / 使用者空間記憶體拷貝問題,select需要複製大量的控制代碼資料結構,產生巨大的開銷

3.3.2 poll:

  • poll的個數限制為unsigned long.
  • poll 不修改引數陣列.
  • poll 仍然不是執行緒安全的.

3.3.3 epoll:

  • epoll把以上問題都解決了並且加入了一些新特性.(特性不知..)

3.4 epoll有哪些觸發模式?有何區別?

  • 見3.2.4節關於epoll的知識點說明.

3.5 select 什麼情況下返回?

  • 偵聽到檔案描述符可讀/可寫/異常時.

3.6 如果select返回可讀,結果只讀到0位元組, 為什麼?

  • 讀到了檔案尾.[EOF]
  • 如果在一個檔案描述符上碰到檔案尾端,則select會認為該描述符可讀.然後呼叫read()返回0,這是UNIX系統指示到檔案尾端的方法.[摘自<<unix環境高階程式設計(第3版)>>p407]

3.7 兩個epoll等待同一個檔案描述符會發生什麼?

  • 事件發生時會同時返回給兩個epoll例項

3.8 如果epoll 把自己epoll_create()返回的描述符放入自己檔案描述符集裡面,會有發生什麼情況?

  • epoll_ctl會失敗(EINVAL),但可以把自己的檔案描述符放到別的epoll描述符集裡面.

3. 9 如何設計大規模的併發模型?

  • ...

4. 拓展問題

4.1 什麼是執行緒安全?

  • 多執行緒訪問同一段程式碼,不會產生不確定的結果,就是執行緒安全的。

5. 參考資料

5.1 知乎答案
5.2 <<Unix 環境高階程式設計(第3版)>>
5.3 <<Linux系統程式設計>>