About me

Monday 10 April 2017

Linux poll 機制

語法
#須引入這個header
#include <poll.h>

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

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

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

  
描述

poll()的功能跟select很相似,都是等待某個檔案描述子(File Descriptior)(以下簡稱FD)的狀態已經準備好以後,然後去執行IO。講白一點,就是這個機制的底層(driver/kernel)會幫你檢查這個裝置能不能讀寫,而一系列被監聽的FD則被描述在參數『fds』裡面,『fds』的資料結構如下:
struct pollfd {
  int   fd;         /* file descriptor */
  short events;     /* requested events */
  short revents;    /* returned events */
};

呼叫者必須要具體的指定在nfds裡面的『fds』陣列的項目代號。


    fd

『fd』代表的是一個已經被打開的檔案描述子,如果這個欄位是負值的話,則相關的『events』欄位將會被省略並且『revents』欄位會回傳0。這樣子的設計方式是為了可以簡單的省略一個poll()呼叫的fd,但是如果你的df為0的話則無法被省略。


    events

『events』欄位是一個輸入參數,會有一個bit mask來描述這個軟體對於這個fd所監聽的events。如果你將這個欄位設定成0的話,則只有在你的『revents』為『POLLHUP』,『POLLERR』或是『POLLNVAL』之下的狀況才能回傳,也就是都是出狀況了才會回傳的意思。


    revents

『revents』欄位是一個output的參數,通常都是事件發生後由kernel所填入數值。在『revents』所回傳的bits通常包含了各種的『events』或者是『POLLHUP』,『POLLERR』,『POLLNVAL』(後面這三個bits在events欄位其實是沒什麼意義,只有在相關的狀況之下才會被設定)。


如果你在任何的fd上所監聽『events requested』都沒有發生的話,則『poll()』將會block直到某個相對應的事件發生。 


timeout

『timeout』欄位必須具體的定義『poll()』必須等待某個fd已經準備好的時間,單位為milliseconds ,通常底下個三種狀況會造成呼叫被block住:

    1. 一個fd已經是ready狀態。
    2. 被一個『signal handler』所中斷。
    3. timeout過期。

如果指定timeout為負值的話會就是沒有timeout,而指定0的話就代表poll馬上會return,就算fd還沒有準備好的話也會馬上return。這邊要特別注意的是,timeout的時間是由系統的時鐘所計時的,所以代表的是會因為kernel的scheduling delays的影響,所以可能會超出一點時間。


nfds_t

是一個unsigned integer類型,使用來代表fd的數量 。這個參數的實現必須支援跨平台的環境,確保不會會超過類型『long』的長度,如果有需要的話可以透過『confstr()』或是『getconf』來取得相關的資訊。

Event bits

在『events』和『revents』這兩個欄位可以被設定或是回傳的bits都定義在標頭檔『』裡面,解釋如下:


    POLLIN
    有資料可以讀了。
    POLLPRI
    有緊急的資料要讀,通常是在TCP socket上的out-of-band資料,或是master虛擬終端機偵測到slave在packet mode上狀態的改變。
    POLLOUT
    已經可以寫入,除非你有設定『O_NONBLOCK』,不然在你的寫入內容大於可寫入socket或是大於pipe空間的話還是會被block住。
    POLLRDHUP
    從kernel 2.6.17以後才有的bit,代表stream socker peer關閉連線,或是寫到一半突然關閉連線,如果想要獲得這項資訊的話,你就必須要在任何的標頭檔之前宣告『_GNU_SOURCE』這個test macro。
    POLLERR
    代表Error的狀況,只會在『revents』回傳,並且在『events』被忽略。
    POLLHUP
    連線關掉了,只會在『revents』回傳,並且在『events』被忽略。當從pipe或是stream socket這樣子的channel讀取資料時,這個event僅僅是代表peer關閉了channel端點。讀取端只有在channel上的資料被讀取完了才會回傳0 (EOF)。
    POLLNVAL
    不合法的request,所以fd沒有open,只會在『revents』回傳,並且在『events』被忽略。

當使用定義『_XOPEN_SOURCE』來編譯時,會多出下面四個bit,但是都沒什麼用,因為都被上面的幾個bits給涵蓋了:


    POLLRDNORM
    跟『POLLIN』一樣。


    POLLRDBAND
    Priority band data可以被讀取,但是通常在Linux裡面不會用。


    POLLWRNORM
    跟『POLLOUT』一樣。


    POLLWRBAND
    Priority data可以被寫入。


    POLLMSG
    這個bit也沒在使用。
  
  
ppoll()

poll()和ppoll()就像是select()和pselect一樣,ppoll()允許讓一個應用程式安全(何謂安全的??)的等待到一個fd變成ready的狀態或者是直到有接收到任何的訊號。

其他不一樣的地方主要就是『timeout』這個行為,像是底下這個範例:
ready = ppoll(&fds, nfds, tmo_p, &sigmask);


就像是自動的執行底下這段程式碼一樣:
sigset_t origmask;
int timeout;

timeout = (tmo_p == NULL) ? -1 :
(tmo_p->tv_sec * 1000 + tmo_p->tv_nsec / 1000000);
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = poll(&fds, nfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);


如果你的『sigmask』參數是NULL的話,則沒有signal mask manipulation,所以這樣poll跟ppoll就真的只是『timeout』的差別而已。

而參數『tmo_p』定義了ppoll()將會block的時間上限,這個參數指到了底下的這個結構:
struct timespec {
    long    tv_sec;         /* seconds */
    long    tv_nsec;        /* nanoseconds */
};

如果tmo_p設定為NULL,則ppoll()將會一直處於block的狀態。


回傳值

如果成功的話,將會回傳一個正整數。代表的是擁有非0值的revents欄位的結構數量(換句話說就是fd的events或是errors)。如果回傳值為0的話代表所有的呼叫都timeout而且沒有任何的fd準備好了。回傳-1的話代表error,相關的訊息可以參考errno。


Error

    EFAULT

    傳入的array參數並沒有包含在呼叫程式的記憶體合法空間裡。

    EINTR

    在任何requested event之前已經有一個訊號發生了。

    EINVAL

    nfds的值已經超過『RLIMIT_NOFILE』。

    ENOMEM

    已經沒有任何空間來連結fd tables。


poll_wait

poll_wait這個funciton是使用在driver/kernel裡的,會將你的裝置(也就是『file』這個結構)加到某個清單,而這個清單可以用來將你的process給喚醒。主要的概念是利用poll/epoll/select等等命令來將一系列的fds加到這個清單裡面,代表在這個清單的裝置都是要等待的,在每個driver的poll函數裡面會將他自己加到這個等待清單裡(經由poll_wait)。所以這個清單裡面的所有process會被kernel給統一管理並且block,然後跟這相關的裝置都可以利用他來喚醒這個process。如果回傳非零的mask bits的話,代表檔案已經處於ready的狀態了。


Example 1

上面是看了官方文件以後,我自己邊翻譯又講了一堆廢話,當然主要還是要給個例子會比較好懂,底下給了兩個例子。

程式碼位於:
https://github.com/hugh712/my_driver/tree/master/006_poll

例子一主要有三隻檔案:

  cdata.c
    是一個driver層的程式,是建立一個裝置檔driver叫做『cdata』,有一個全域變數叫做『wait』,代表的是是否可以讀檔,如果為0的話代表你不能寫,為1的話代表你可以寫入,預設為0代表不能寫。主要開放ioctl來改變這個『wait』的值。

  main-loop.c
    user space程式,主要是開啟『cdata』裝置以後,然後用『poll』去問driver能不能寫資料,不能寫就一直loop直到能寫為止。

你看程式碼的話,可以看到使用的結構像底下:

//宣告fds和fd
struct pollfd fds;
int fd;

//開啟檔案
fd=open("/dev/cdata", O_RDWR);

//將fd給assign給fds的fd欄位
//然後指定只監聽read event
fds.fd=fd;
fds.events=POLLIN;

//然後呼叫poll,timeout為兩秒
ret = poll(&fds,sizeof(fds),2000);

//do something below


上面我只有監聽一個fd的read event,如果你有多個event要監聽的話,底下是另一個例子:


//宣告fds Array和fd1, fd2
struct pollfd fds[2]={0};
int fd1, fd2;

//開啟檔案,裝置1和裝置2
fd1=open("/dev/device-1", O_RDWR);
fd2=socket(PF_INET, SOCK_STREAM, 0);

//將fd1和fd2給assign給fds的fd欄位
//設定fd1監聽read,
//設定fd2監聽read和out-of-band資料
fds[0].fd = fd1;
fds[0].events = POLLIN;
fds[1].fd = fd2;
fds[1].events = POLLIN | POLLPRI;

//然後呼叫poll
ret = poll(fds, sizeof(fds)/sizeof(fds[0]), -1);

//do something below



  control.c
user space程式,也是會開啟『cdata』裝置,主要是用命令『ioctl』去修改『wait』變數的值。


  作法與解釋

這邊假設你都有相關的Linux kernel header於相對應的位置 -『/lib/modules/$(Version)/』底下,並且是使用『out of tree』的方式建制,流程如底下所示:


    1. 使用底下預設的target建制driver:
make

    2. 建置user space 的test程式:
make test


    3. 掛載裝置檔:
make load


    4. 執行程式『main-loop』
./main-loop


    5. 執行程式『control』
./control



  Result

結果如下圖,使用『make』和『make test』建立相關driver和applications後,在用target『make load』建立裝置檔并『insmod』模組,此時可以看到右邊的視窗顯示出kernel log說裝置已經初始化好了,然後左上角視窗執行『main-loop』會一直持續的迴圈等待driver的poll function告訴它裝置已經ready了,timeout設定為2秒,所以你可以看到kernel log每兩秒有一次的『poll message』,但是因為此時『wait』的值還是0,所以還不會回來。

下一步就是左下角,執行了『./control』,將『wait』值給設定成1,可以看到main-loop馬上就收到裝置已經準備好了,所以就回來了。


Example 2

程式碼位於:
https://github.com/hugh712/my_driver/tree/master/007_poll_v2

例子二主要有兩隻檔案:
  cdata.c
    是一個driver層的程式,是建立一個裝置檔driver叫做『cdata』,ioctl有兩個cases -『IOCTL_ENABLE』和『IOCTL_DISABLE』,『IOCTL_DISABLE』會將這個process放進wait queue以後讓自己睡著,而相對而言『IOCTL_ENABLE』則是將這個process喚醒。這邊的『poll』file operation則是沒有什麼特殊的,就是進來直接就設定可讀然後回傳。

  test.c
    user space程式,主要是開啟『cdata』裝置以後,folk出另一個process,parent一開始讓自己睡著,然後child等待你輸入『pass』,輸入以後會將睡著的process給喚醒,然後直接詢問『poll』並且read』。


  作法與解釋

    這邊也是假設你都有相關的Linux kernel header於相對應的位置 -『/lib/modules/$(Version)/』底下,並且是使用『out of tree』的方式建制,流程如底下所示:

    1. 使用底下預設的target建制driver:
  make

    2. 建置user space 的test程式:
  make test


    3. 掛載裝置檔:
  make load


    4. 執行程式『main-loop』
  ./test


  Result

看到下圖,經由建制後,產生相關的『driver』和『application』,然後一樣用『make load』載入相關模組,下一步直接執行『test』,會發現prompt會等待你輸入『pass』,這時kernel log還是在顯示剛剛被『DISABLE』的部份,代表parent已經睡著了,沒有執行user space application的『poll』函數,所以這時候經由child的ioctl喚醒parent以後,就直接往下走,直接很順的經過問『poll』和『read』。執行完之後在使用『make unload』來卸載相關模組。

    
Ref http://www.cnblogs.com/alyssaCui/archive/2013/04/01/2993886.html http://beej-zhtw.netdpi.net/09-man-manual/9-17-poll http://man7.org/linux/man-pages/man2/poll.2.html http://pubs.opengroup.org/onlinepubs/009695399/basedefs/poll.h.html

No comments:

Post a Comment