Libev 官方文件學習筆記 – 03:常用 watcher 介面

amc發表於2019-05-12

請注意這是 libev 而不是 libevent 的文章!

這篇文章是第三篇,主要講 libev 裡基本集中的 watcher。

本文地址:https://segmentfault.com/a/1190000006679929


ev_io:直接操作fd

這個 watcher 負責檢測檔案描述符(以下簡稱fd)是否可寫入資料或者是讀出資料。最好是將fd設定為非阻塞的。
  注意有時候在呼叫read時是沒有資料的(返回0),此時一個一個非阻塞的read會得到EAGAIN錯誤。

(以下兩個特殊問題,是 libev 文件中特別提到的,但是我看不太懂……)

失蹤的 fd 的特殊問題

部分系統需要顯式地呼叫close(如kqueueepoll),否則當一個 fd 消失、而新的 fd 進入,佔用同一個 fd 號時,libev不知道這是一個新的fd。
  libev 一側解決的辦法是每次呼叫ev_io_set時,都假定這是一個新的 fd。

使用dup操作 fd 的特殊問題

一些後端(backend)不能註冊普通的 fd 事件,只能註冊underlying file descriptions,這意味著使用dup()或其他奇怪操作的fd,只能由其中一個被接收到。
  這沒有有效的解決辦法,除非將後端設定為BACKEND_SELECTEVBACKEND_POLL

關於檔案的特殊問題

ev_io對於檔案淚說沒有什麼用,只要檔案存在,就立即會有時間。對於stdinstdout,請謹慎使用,確保這兩者沒有被重定向至檔案。

關於 fork 的特殊問題

記得使用ev_loop_fork,並且使用EVFLAG_FORKCHECK。不過對於epollkqueue之外的無需擔心。

關於SIGPIPE的問題

只是提醒一下:記得處理SIGPIPE事件。

關於accept一個無法接受的連線

大多數 POSIX accpet 實現中在刪除因為錯誤而導致的連線時(如 fd 到達上限)都回產生一個錯誤的操作,比如使 accept 失敗但不拒絕連線,只產生ENFILE錯誤。但這個會導致 libev 還是將其標記為 ready 狀態。
  推薦方法是列出所有的錯誤並記錄下來,或者是暫時關閉 watchers。

相關函式

void ev_io_init (ev_io *, callback, int fd, int events)
void ev_io_set (ev)io *, int fd, int events)

其中 events 可以是EV_WRITEEV_READ的組合。

示例

static void stdin_readable_db (struct ev_loop *loop,
                               ev_io *w,
                               int revents)
{
    ev_io_stop (loop, w)
    ......    // 從 w->fd 中進行read
}

......

some_init_func ()
{
    ......
    struct ev_loop *loop = ev_default_init (0);
    ev_io stdin_readable;
    ev_io_init (&stdin_readable, stdin_readable_db , STDIN_FILENO, EV_READ);
    ev_io_start (loop, &stdin_readable);
    ev_run (loop, 0);
    ...
}

ev_timer:相對超時機制

Libev 提供了一個相對超時機制的定時器。所謂的“相對”,就是說這個定時器的引數是:指定以當前時間為基準,延遲多久出發事件。這個定時器與基於萬年曆的日期/時間是無關的,只基於系統單調時間。

迴圈定時器設計

下面列出一個以60秒為單位的迴圈定時器作為例子,來說明使用ev_timer的不同策略

1. 使用標準的初始化和停止 API 來重設

ev_timer_init (timer, callback, 60.0, 6.0);
ev_timer_start (loop, timer)

標準設定。或——

ev_timer_stop (loop, timer);
ev_timer_set (timer, 60.0, 0.0);
ev_timer_start (loop, timer)

這樣的設定,當每次有活躍時間時,停止timer,並且重啟它。第一個引數是首次超時,第二個引數是第二次開始的固定超時時間。
  但是這樣的方法雖然比較簡易,但是時間不穩定,而且開銷較大

2. 使用ev_timer_again重設

使用ev_timer_again,可以忽略ev_timer_start

ev_init (timer, callback);
timer->repeat = 60.0;
ev_timer_again (loop, start);

上面的初始化完成後,在 callback 裡呼叫:

timer->repeat = 60.0;
ev_timer_again (loop, timer);

可以改變 timeout 值,不管 timer 是否 active

3. 讓 timer 超時,但視情況重新配置

這個方式的基本思路是因為許多 timeout 時間都比 interval 大很多,此時要記住上一次活躍的時間,然後再 callback 中檢查真正的 timeout

ev_tstamp g_timeout = 60.0;
ev_tstamp g_last_activity;
ev_timer  g_timer;

static void callback (EV_P_ev_timer *w, int revents)
{
    ev_tstamp after = g_last_activity - ev_now(EV_A) + g_timeout;
    
    // 如果小於零,表示時間已經發生了,已超時
    if (after < 0.0) {
        ......    // 執行 timeout 操作
    }
    else {
        // callback 被呼叫了,但是卻有一些最近的活躍操作,說明未超時
        // 此時就按照需要設定的新超時事件來處理
        ev_timer_set (w, after, 0.0);
        ev_timer_start (loop, g_timer);
    }
}

啟用這種模式,記得初始化時將g_last_activity設定為ev_now,並且呼叫一次callback (loop, &g_timer, 0);當活躍時間到來時,只需修改全域性的 timeout 變數即可,然後再呼叫一次 callback

g_timeout = new_value
ev_timer_stop (loop, &timer)
callback (loop, &g_timer, 0)

4. 為 timer 使用雙向連結串列

使用場景:有成千上萬個請求,並且都需要 timeouts
  當 timeout 開始前,計算 timeout 的值,並且將 timeout 放在連結串列末尾。然後當連結串列前面的項需要 fire 時。使用ev_timer來將其 fire 掉。
  當有 activity 時,將 timer 從 list 中一處,重算 timeout,並且再附到 list 末尾,確保如果ev_timer已經被 list 的第一項取出時,更新它

“太早”的問題

假設在500.9秒的時候請求延時1秒,那麼當501秒到來時,可能導致 timeout,這就是“太早”問題。Libev的策略是對於這種情況,在502秒時才執行 timeout。但是這又有“太晚”的問題,請程式設計師注意.

“假死”問題

Suspenged animation,也稱為休眠,指的是將機子置於休眠狀態。注意不同的機子不同的系統這個行為可能不一樣。
  其中一種休眠是使得所有程式感覺只是經過了很小的一段時間一般(時間跳躍)
  推薦在SIGTSTP處理中呼叫ev_suspendev_resume

其他注意點

ev_now_update()的開銷很大,請謹慎使用
  Libev使用的時一個內部的單調時鐘而不是系統時鐘,而ev_timer則是基於系統時鐘的,所以在做比較的時候兩者不同步。

相關函式

void ev_timer_init (ev_timer *, callback, ev_tstamp after, ev_tstamp repeat);
void ev_timer_set (ev_timer *, ev_tstamp after, ev_tstamp repeat);

如果repeat為正,這個timer會重複觸發,否則只觸發一次。

void ev_timer_again (loop, ev_timer *)
ev_tstamp ev_timer_remaining (loop, ev_timer *)

ev_periodic:基於日曆的定時器

相關函式

void ev_periodic_init (ev_periodic *, callback, ev_tstamp offset, 
                       ev_tstamp interval, reschedule_cb)
void ev_periodic_set (ev_periodic *, ev_tstamp offset,
                      ev_tstamp interval, reschedule_cb)

以下是幾種不同應用場景的設定方法:

  1. 絕對計時器:offset 等於絕對時間,interval 為0,reschedule_cb 為 NULL。在這種設定下,時鐘只執行一次,不重複
  2. 重複內部時鐘:offset 小於等於 interval 值,interval 大於0,reschedule_cb 為 NULL。這種設定下,watcher 永遠在每一個(offset + N * interval)超時。
  3. 手動排程模式:offset 忽略,reschedule_cb 設定。使用 callback 來返回下次的 trigger 時間。callback 原型為:
ev_tstamp (*reschedule_cb)(ev_periodic *w, ev_tstamp now);

例程是:

static ev_tstamp my_scheduler (...)
{
    return now + 60.0;
}

類似於 Linux 核心的jiffies,返回下一個時間點。

這個timer非常便於用來提供諸如“下一個正午12點”之類的定時器。

void ev_periodic_again (loop, ev_periodic *)

關閉並重啟 watcher,參見前文。

ev_tstamp ev_periodic_at (ev_periodic *)

返回下一次觸發的絕對時間。

ev_signal:捕獲 signal 事件

在哦你跟一個 loop 可以多次觀測同一個 signal,但是無法在多個 loop 中觀測同一個 signal。此外,SIGCHILD只能在 default loop 中監聽。

注意點

關於繼承 fork / execve / ptherad_create 的問題

在子程式呼叫 exec 之前,應當將 signal mask 重設為你所需的預設值。最簡單的方法就是子程式做一個pthread_atfork()來重設。

關於執行緒訊號處理

POSIX 的不少功能(如sigwait)只有在程式中的所有執行緒遮蔽了 signal 時才真正生效
  為了解決這個問題,如果真的要使用這些功能的話,建議在建立執行緒之前遮蔽所有的 signal,並且在建立 loops 的時候指定EVFLAG_NOSIGMASK,然後制定一個 thread 用來接收 signals。

相關函式

void _ev_signal_init (ev_signal *, callback, int signum)
void ev_signal_set (ev_signal *, int signum)

ev_child:子程式退出事件

當接收到SIGCHILD事件時,child watcher 觸發。大部分情況下,子程式退出或被殺掉。只要這個 watcher 的 loop 未開始,你甚至可以在 shild 被 fork 之後才加入 child watcher。
  Ev_child 的優先順序固定是EV_MAXPRI

void ev_chile_init (ev_child *, callback, int pid, int trace)
void ev_child_set (ev_child *, int pid, int trace)

Pid 如果指定0的話,表示任意子程式。可以在 ev_child 中觀察rstatus成員來了解子程式狀態。

int pid;

表示監控中的 pid,只讀。

int rpid;

可讀寫,表示檢測到狀態變化的 pid

int tstatus;

可讀寫,表示由 rpid 導致的程式的 exit/trace 狀態值。

ev_stat:監控檔案屬性變化

使用 ev_stat 時,監控目標位置上無需存在檔案,因為檔案從“不存在”變為存在也是一種狀態變化。
  檔案路徑必須是絕對路徑,不能存在“./”或“../”。
  Ev_stat 的實現其實只是定期呼叫stat()來判斷檔案屬性的變化,所以可以指定檢查週期。指定0的話會使用預設事件週期。
  正因為這是輪詢操作,所以這個功能不適合做大資料量或者是大併發檢測;同時,ev_stat 是非同步的。

大檔案支援

預設關閉大檔案支援(使用32位的stat)。如果要使用大檔案支援(ABI),libev 的作者在這裡吐槽,說你要遊說作業系統的釋出方去支援……囧rz

關於檔案時間

有些系統的檔案時間僅精確到秒,這就意味著 ev_stat 無法區分秒以下的變動。

相關函式和資料成員

void ev_stat_init (ev_stat *, callback, const char *path, ev_tstamp interval);
void ev_stat_set (ev_stat *, const char *path, ev_tstamp interval);
void ev_stat_stat (loop, ev_stat *);

第三個函式使用新的檔案 stat 值去更新 stat buffer,使用此函式來使得你做的一些配置更改不會被觸發。

ev_statdata attr

只讀,代表檔案最近一次的狀態。ev_statdatastruct stat基本是相通的。

ev_statdata prev

檔案上一次的狀態

ev_tstamp interval
const char *path

都是隻讀,字面意義上的意思。

ev_idle:無事可做時的事件

void ev_idle_init (ev_idle *, callback)

這個功能沒有研究過,暫記著把。

其他事件(僅記錄)

ev_prepare 和 ev_check

ev_embed

ev_fork

ev_cleanup

ev_asunc

其他函式

void ev_once (loop, int fd, int events, ev_tstamp timeout, callback)

從指定的f fd 中指定一個超時事件,這個函式的方便之處在於無需做 alloc/conf/start/stop/free
  Fd 可以小於0,這樣就沒有 I/O 監控,並且“events”會被忽略。

void ev_feed_event (loop, int fd, int revents);

向一個 fd 傳送事件。需要注意的是,這個功能貌似是隻能在 loop 內呼叫才有效,非同步地在 loop 的另一個執行緒直接呼叫是無效的。

void ev_feed_signal_event (loop, signum)

向一個 loop 模擬 signal。參見 ev_feed_signal

系列篇

Libev 官方文件學習筆記(1)——概述和 ev_loop
Libev 官方文件學習筆記(2)——watcher 基礎
Libev 官方文件學習筆記(3)——常用 watcher 介面(本文)
使用 libev 構建 TCP 響應伺服器的簡單流程

相關文章