天天看點

116-poll 函數

poll 完成的功能和 select 幾乎是一模一樣的,是以在你學會了 select 後,你發現學 poll 會非常容易。在英文中 poll 表示“投票”的意思,這非常形象,有事件發生的描述符,就為其投票。

1. poll 原型

int poll(struct pollfd *fds, nfds_t nfds, int      

有幾個參數類型可能我們不認識,實際上 poll 函數的第一個參數是一個類型為 struct pollfd 的數組,第二個參數 nfds (一般是個無符号的整型)是這個數組的大小,第三個參數就是逾時時間了。接下來一個一個說明。

1.1 struct pollfd 類型

struct pollfd {
  int   fd;         /* 檔案描述符 */
  short events;     /* 監聽的事件,比如可讀事件,可寫事件 */
  short revents;    /* poll 函數的傳回結果,是可讀還是可寫 */      

和 select 不同的是,poll 函數沒有将可讀事件、可寫事件描述符單獨放進兩個不同的集合,而是配置設定了一個結構體,一次性進行監聽。

成員 fd 表示要監聽哪個描述符,如果該值是負數,poll 函數會忽略掉它。

對 events 來說,它使用 bit 位來儲存你要監聽什麼事件,通常用“位或”操作為其指派,在 linux 中,events 有下面的可選值:

  • POLLIN: 監聽是描述符是否可讀,相當于 select 中的讀集合參數。
  • POLLPRI:監聽是否有緊急資料可讀(比如 TCP 套接字上的 out-of-band(OOB) 資料,這種我們還沒學,是以不考慮監聽它了)
  • POLLOUT:監聽是描述符是否可寫。
  • POLLRDHUP:這種事件隻有 Linux 2.6.17 核心後才支援,它監聽流式套接字對端是否關閉右半關閉,因為我們還沒學套接字,是以這種的也先不考慮了。

如果有監聽的事件到來,它會将事件類型儲存到 revents 成員中,比如監聽到了有資料可讀,則 revents 的 bit 位中會儲存 POLLIN。如果監聽到了有資料可寫,那 revents 的 bit 位中會儲存 POLLOUT。

還有一些事件,即使你不主動監聽它也會發生,并儲存到 revents 中,這些事件一般都是異常事件:

  • POLLERR:這種情況極少見,很抱歉後面的實驗沒示範。一般是硬體上的問題吧,可以參考這個文章​​傳送門​​。
  • POLLHUP:對端挂斷,比如對于有名管道,其中一端關閉了。
  • POLLNVAL:使用了未打開的描述符

1.2 timeout 參數

  • timeout = -1,表示永遠等待,直到有事件發生。
  • timeout = 0,不等待,立即傳回。
  • timeout > 0,等待 timeout 毫秒。

2. poll 與 select

這兩個函數所做的事件是一樣的,但是它們也有差別:

  • select 使用 fd_set 來存放描述符,poll 使用結構體數組。
  • select 能夠一次監聽的描述符數量是受 fd_set 集合的限制的,通常這個集合最多隻能放 1024 個描述符。而 poll 一次能夠監聽的描述符個數是根據數組大小來決定的,這要看 nfds_t 被定義成什麼類型了,如果是 unsigned long,4 位元組寬度的情況下,poll 能監聽 232−1

    2

    32

    1

3. 實驗

程式 poll.c 隻是對前面的 select.c 做了一點點修改,将 select 替換成了 poll 函數。另外,為了能示範異常事件,程式使用了一個未打開的描述符 fd3。

writepipe 函數做了一點點小小的修改,就是輸入字母 ‘q’ 的時候會主動 close 掉管道描述符,同時退出。

poll 函數同樣會被信号打斷,所有

3.1 代碼

// poll.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

void handler(int sig) {
  if (sig == SIGINT) {
    puts("hello SIGINT");
  }
}

int process(char* prompt, int fd) {
  int n;
  char buf[64];
  char line[64];
  n = read(fd, buf, 64);
  if (n < 0) {
    // error
    PERR("read");
  }
  else if (n == 0) {
    // peer close
    sprintf(line, "%s closed\n", prompt);
    puts(line);
    return 0;
  }
  else if (n > 0) {
    buf[n] = 0;
    sprintf(line, "%s say: %s", prompt, buf);
    puts(line);
  }
  return n;
}

int main () {
  int i, n, res;
  char buf[64];

  struct pollfd fds[4];

  if (SIG_ERR == signal(SIGINT, handler)) {
    PERR("signal");
  }

  int fd0 = STDIN_FILENO;
  int fd1 = open("a.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fd1);
  int fd2 = open("b.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fd2);
  int fd3 = 100;

  fds[0].fd = fd0;
  fds[1].fd = fd1;
  fds[2].fd = fd2;
  fds[3].fd = fd3;

  for (i = 0; i < 4; ++i) {
    fds[i].events = POLL_IN;
  }


  while(1) {
    res = poll(fds, 4, -1);

    if (res < 0) {
      // error
      if (errno == EINTR) {
        perror("poll");
        continue;
      }
      PERR("poll");
    }
    else if (res == 0) {
      // timeout
      continue;
    }

    for (i = 0; i < 4; ++i) {
      if (fds[i].revents & POLLIN) {
        sprintf(buf, "fd%d", i);
        n = process(buf, fds[i].fd);
        if (n == 0) fds[i].fd = -1;
      }
      if (fds[i].revents & POLLERR) {
        printf("fd%d Error\n", i);
        fds[i].fd = -1;
      }
      if (fds[i].revents & POLLHUP) {
        printf("fd%d Hang up\n", i);
        fds[i].fd = -1;
      if (fds[i].revents & POLLNVAL) {
        printf("fd%d Invalid request\n", i);
        fds[i].fd = -1;
      }
    }
  }
}      

3.2 編譯與運作

  • 編譯
$ gcc poll.c -o poll      
  • 運作
116-poll 函數

圖1 運作結果

從圖 1 中可以看到,因為傳入了一個未打開的描述符,導緻 poll 函數監聽到了異常事件,即 POLLNVAL,是以螢幕列印了 fd3 Invalid request.

4. 總結

  • 掌握 poll 的用法
  • 知道 poll 和 select 的差別