請注意這是 libev 而不是 libevent 的文章!
這篇文章是第三篇,主要講 libev 裡基本集中的 watcher。
本文地址:https://segmentfault.com/a/1190000006679929
ev_io:直接操作fd
這個 watcher 負責檢測檔案描述符(以下簡稱fd)是否可寫入資料或者是讀出資料。最好是將fd設定為非阻塞的。
注意有時候在呼叫read
時是沒有資料的(返回0),此時一個一個非阻塞的read
會得到EAGAIN
錯誤。
(以下兩個特殊問題,是 libev 文件中特別提到的,但是我看不太懂……)
失蹤的 fd 的特殊問題
部分系統需要顯式地呼叫close
(如kqueue
、epoll
),否則當一個 fd 消失、而新的 fd 進入,佔用同一個 fd 號時,libev
不知道這是一個新的fd。
libev 一側解決的辦法是每次呼叫ev_io_set
時,都假定這是一個新的 fd。
使用dup
操作 fd 的特殊問題
一些後端(backend)不能註冊普通的 fd 事件,只能註冊underlying file descriptions
,這意味著使用dup()
或其他奇怪操作的fd,只能由其中一個被接收到。
這沒有有效的解決辦法,除非將後端設定為BACKEND_SELECT
或EVBACKEND_POLL
關於檔案的特殊問題
ev_io
對於檔案淚說沒有什麼用,只要檔案存在,就立即會有時間。對於stdin
和stdout
,請謹慎使用,確保這兩者沒有被重定向至檔案。
關於 fork 的特殊問題
記得使用ev_loop_fork
,並且使用EVFLAG_FORKCHECK
。不過對於epoll
和kqueue
之外的無需擔心。
關於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_WRITE
和EV_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_suspend
和ev_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)
以下是幾種不同應用場景的設定方法:
- 絕對計時器:offset 等於絕對時間,interval 為0,reschedule_cb 為 NULL。在這種設定下,時鐘只執行一次,不重複
- 重複內部時鐘:offset 小於等於 interval 值,interval 大於0,reschedule_cb 為 NULL。這種設定下,watcher 永遠在每一個(offset + N * interval)超時。
- 手動排程模式: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_statdata
和struct 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 響應伺服器的簡單流程