Linux系统编程 / triggerhappy 源码分析(3.select 的应用)
哈嘍,我是老吳,繼續記錄我的學習心得。
一、進步的滯后性
我們期望進步是線性:
每一個人付出一些努力后,都希望它有立竿見影的效果。
現實是:
做出努力后,結果的顯現往往滯后。
只有在幾個月或幾年后,我們才意識到以前學習/工作的真正價值。
“失望谷地” 的出現:
人們在投入數周或數月的辛勤工作后,卻沒有任何看得見的效果,于是進入深感沮喪的時期。
如何改變:
1> 改變意識。功夫并沒有白費,它只是蓄積起來了。直到很久以后,以前努力的全部價值才會顯露出來。
2> 尋找一些可以幫助自己對抗滯后進步所帶來的低落情緒的技巧。例如培養習慣的四大定律,具體的如即時獎勵、習慣追蹤等。
二、triggerhappy 源碼分析 / 3.select() 的應用
正文目錄:
1.?thd.c?/?process_events()?源碼分析 2.?I/O?多路復用?(?Multiplexing?)?是為了解決什么問題 3.?I/O?多路復用設計思路 4.?select()?的用法 5.?和?poll/epoll?對比(補充知識,非重點) 6. triggerhappy:thd.c / process_devices()?源碼分析 7.?相關參考寫作目的:
通過閱讀 triggerhappy 的源碼,學習 I/O 多路復用的方法之一 select() 的使用方法。
測試環境:
Ubuntu 16.04
Gcc 5.4.0
1. thd.c / process_events() 源碼分析
1.1 process_events() 的作用
1) 作用:
監控 input 設備;
讀取 input 事件,并執行相應的 action;
檢查 socket 是否有接受到命令。
2) 調用流程:
thd.cmain()start_readers()process_events()1.2 process_events() 的內容:4 個步驟
1) 建立主循環:
while?(?count_devices()?>?0?||?cmd_fd?!=?-1?)?{[...] }2) 在主循環內,使用 select 監測多個 input 設備文件 (重點):
while(...)?{[...]???????//?some?init?for?selectretval?=?select(max_fd+1,?&rfds,?NULL,?NULL,?&tv); }本文的重點就是了解 select 相關的知識點。
3) select 返回后,調用 process_devices() 讀取數據 (重點):
while(...)?{[...]???????//?some?init?for?selectretval?=?select(max_fd+1,?&rfds,?NULL,?NULL,?&tv);if?(retval)?{process_devices();} }process_devices() 是 triggerhappy 的重點函數。
先大致了解一下它的作用:
對于每一個有待讀數據的 input 設備文件,都調用 read() 函數讀取數據,然后解析數據,根據解析結果去執行相應的 action,action 在 example.conf 中定義:
/usr/bin/amixer set Master 5%+ 就是一個 action。
暫時不用太深入,后續會專門寫一篇文章來詳細分析 process_devices()。
4) 是否有接收到用戶命令 (th-cmd)?
while(...)?{[...]if?(retval)?{process_devices();if?(?cmd_fd?!=?-1?&&?FD_ISSET(?cmd_fd,?&rfds?)?)?{struct?command?*cmd?=?read_command(?cmd_fd?);obey_command(?cmd?);free(cmd);}} }cmd_fd 是一個 socket:
main()start_readers()cmd_fd?=?bind_cmdsocket(cmd_file);cmd_fd?=?socket(AF_UNIX,?SOCK_DGRAM,?0);triggerhappy 支持使用 socket 通信以動態增加和刪除輸入設備,例如動態地添加要監測的輸入設備 /的 /dev/input/event0:
$?thd?--socket?/var/run/triggerhappy.socket $?th-cmd?--socket?/var/run/triggerhappy.socket?--add?/dev/input/event0read_command() 和 obey_command() 負責讀取和解析用戶通過 socket 發送過來的命令,后面找時間寫一些關于 domain socket 的文章。
接下來,我們先學習一下 select 的機制、使用方法和注意事項。
注意:本文的寫作目的不是為了描述 select() 完整的使用方法和內部實現,而是在有限的篇幅內盡量整理一下 I/O 多路復用相關的知識點,即僅僅是引子,更完整的內容仍需要小伙伴自行閱讀相關書籍。
2. I/O 多路復用 ( Multiplexing ) 是為了解決什么問題: 多文件阻塞
應用通常需要在多個文件描述符上阻塞,例如在鍵盤輸入、進程間通信以及其他文件之間協調 I/O。
很多情況下,一個文件描述符依賴另一個文件描述符。只要是有 1 個文件描述符數據還沒有準備好,比如發送了 read() 調用,但是還沒有任何數據,就進程會阻塞在此 I/O 操作上,此時無法對其他的文件描述符提供服務。這導致應用效率變低,影響用戶體驗。
尤其是對于網絡應用而言,可能會同時打開多個 socket,如果應用阻塞在某個 socket上,將引發很多問題。
設計 I/O 多路復用就是為了解決上述問題:
支持應用同時在多個文件描述符 (普通文件、管道、套接字...) 上阻塞,并在其中某個文件描述符可以讀寫時收到通知。
3. I/O 多路復用設計思路
1> 無 I/O 就緒:在有可用的文件描述符之前一直處于睡眠狀態;
2> 任意文件描述符 I/O 就緒:喚醒應用,應用檢查是哪個文件描述符可用了;
3> 執行 I/O 操作:應用處理所有 I/O 就緒的文件描述符,此時沒有阻塞;
4> 返回第 1> 步:重新開始;
Linux 提供了三種 I/O 多路復用方案:
-select()、poll() 和 epoll()。
本文先將學習重點放在 select() 上,同時也稍微了解一下 poll() 和 epoll(),明確三者的優缺點。
4. select() 的用法
4.1 函數概述
$?man?2?select/*?According?to?POSIX.1-2001,?POSIX.1-2008?*/ #include?<sys/select.h>/*?According?to?earlier?standards?*/ #include?<sys/time.h> #include?<sys/types.h> #include?<unistd.h>int?select(int?nfds,?fd_set?*readfds,?fd_set?*writefds,?fd_set?*exceptfds,?struct?timeval?*timeout);select() 會監測 3 個 文件描述符集,直到有 1 個或多個文件描述符集成為就緒態。
在給定的文件描述符 I/O 就緒之前并且還沒有超出指定的時間限制,select() 會阻塞。
4.2 參數說明
1) 參數 fd_set * readfds、writefds、exceptfds:
select() 監測 3 種 I/O 事件(讀、寫、異常),每 1 種事件對應 1 個文件描述符集。
每個描述符集存儲在一個 fd_set 數據類型中,可以認為它是一個很大的位數組 (array of bits):
select() 中間 3 個文件描述符集參數 readfds、writefds 和 exceptfds 分別對應了我們關心的可讀、可寫或處于異常條件的文件描述符集。
2) 操作文件描述符集的幾個宏:
FD_ZERO:從指定集合中刪除所有的文件描述符;
FD_SET:向指定集中添加一個文件描述符,而FD_CLR則從指定集中刪除一個文件描述符;
FD_ISSET:檢查一個文件描述符是否在給定集合中;
由于文件描述符集是靜態建立的,所以文件描述符數存在上限值,而且存在最大文件描述符值,這兩個值都是由 FD_SETSIZE 設置。在 Linux 中,該值是1024;
3) 參數 int nfds:最大文件描述符 + 1
參數 nfds 必須設為比 3 個文件描述符集合中所包含的最大文件描述符 + 1 的值。
傳遞該參數是為了讓內核不用去檢查大于這個值的文件描述符是否屬于上述 3 個文件描述符集合,這也從側面說明了 select() 在內核里實現是會遍歷 0~nfds 文件描述符。
select() 的設計天生就沒考慮到用于監測大量文件 I/O 就緒的情景。但是在 triggerhappy 里使用 select() 則是合理的,這是因為 一個系統上的 input 設備并不會太多,本文末尾還會對 select() 的優缺點進行總結。
4) 參數 struct timeval *timeout:
timeout == NULL:永遠等待。當所指定的描述符中的之一已就緒或捕捉到一個信號則返回。如果捕捉到一個信號,則返回 -1,errno 設置為 EINTR。
timeout->tv_sec == 0 && timeout->tv_usec == 0:完全不等待。測試所有指定的描述符并立即返回。
timeout->tv_sec != 0 || timeout->tv_usec != 0:等待指定的秒數和微秒數。當指定的描述符之一已就緒,或超過指定的時間值時立即返回。如果在超時到期時還沒有如何描述符準備好,則返回 0。
4.3 返回說明
1) return = -1:表示有錯誤發生。典型的錯誤碼 (errno) 包括 EBADF 和 EINTR 等。
EBADF: 表示 readfds、writefds、exceptfds 中有非法文件描述符。
EINTR: 表示該調用被某個信號中斷了。
2) return = 0:表示在有文件描述符成為就緒態之前 select() 已經超時。
在這種情況下,每個返回的文件描述符集合將被清空。
3) return > 0:表示有 1 個或多個文件描述符已就緒。
返回值表示處于就緒態的文件描述符個數。
每個集合都修改成只包含相應類型的 I/O 就緒的文件描述符。
每個集合都需要檢查通過 FD_ISSET() 以此找出發生的 I/O 事件是什么。
如果同一個文件描述符在 readfds、writefds 和 exceptfds 中同時被指定,且它對于多個 I/O 事件都處于就緒態的話,那么就會被統計多次。。例如同一描述符已準備好讀和寫,那么在返回值中會對其計 2 次。
4.4 注意事項
每次調用 select() 之前,都要對所有參數進行初始化;
在 Linux 上,如果 select() 被信號中斷的話,struct timeval 會被修改以表示剩余的超時時間;
exceptfds 常常被誤解為在文件描述符上出現了異常情況,這是錯誤的。它其實與偽終端和流式套接字相關;
如果在一個描述符上碰到了文件尾端,select() 會認為該描述符是可讀的。此時調用 read(),會返回 0,這是 UNIX 系統指示到達文件尾端的方法。很多人錯誤地認為,當到達文件尾端時, select() 會指示一個異常條件。
如果想更詳細地了解 I/O 多路復用的使用方法,可以學習一下開源軟件 libevent。
4.5 最簡單的 demo 程序
1) 初始化select() 的參數:
#define?TIMEOUT?5 #define?BUF_LEN?1024int?main?(void) {struct?timeval?tv;fd_set?readfds;int?ret;/*?Wait?on?stdin?for?input.?*/FD_ZERO(&readfds);FD_SET(STDIN_FILENO,?&readfds);/*?Wait?up?to?five?seconds.?*/tv.tv_sec?=?TIMEOUT;tv.tv_usec?=?0;[...] }2) select () 監測是否有 I/O 就緒:
int?main?(void) {[...]???//?1)?init?select?param/*?All?right,?now?block!?*/ret?=?select?(STDIN_FILENO?+?1,?&readfds,?NULL,?NULL,?&tv);if?(ret?==?-1)?{perror?("select");return?1;}?else?if?(!ret)?{printf?("%d?seconds?elapsed.\n",?TIMEOUT);return?0;}[...] }3) 進行 I/O 操作:
int?main?(void) {[...]???//?1)?init?select?param[...]???//?2)?do?select()//?3)?do?I/Oif?(FD_ISSET(STDIN_FILENO,?&readfds))?{char?buf[BUF_LEN+1];int?len;len?=?read?(STDIN_FILENO,?buf,?BUF_LEN);if?(len?==?-1)?{perror?("read");return?1;}if?(len)?{buf[len]?=?'\0';printf?("read:?%s\n",?buf);}}return?0; }4) 運行效果:
$?gcc?simple_select.c?-o?simple_select $?./simple_select? a read:?a啟動程序時,程序會阻塞,這時從鍵盤敲入 'a',程序會被喚醒。
5. 和 poll / epoll 進行簡單對比(補充知識,非重點)
5.1 對比 poll()
$?man?2?poll#include?<poll.h> int?poll(struct?pollfd?*fds,?nfds_t?nfds,?int?timeout);struct?pollfd?{int???fd;?????????/*?file?descriptor?*/short?events;?????/*?requested?events?*/short?revents;????/*?returned?events?*/ };poll()系統調用是 System V 的 I/O 多路復用解決方案。它解決了一些select()的不足,不過出于習慣或可移植性的考慮,select() 還是被頻繁使用。
poll 的監測對象是 struct pollfd 數組,它可以精準的指定要監測的文件集合。而 select() 的文件描述符集合是靜態的,當文件集合是稀疏的時,例如要監測文件描述符 0 和 1000 時,select() 的效率比 poll() 低很多。
select() 返回時會重新創建文件描述符集,因此每次調用都必須重新初始化。poll() 系統調用會把輸入 (events 成員) 和輸出 (revents 成員) 分離開,無需重新初始化數組就可以重新使用。
5.2 對比 epoll
epoll 是 Linux 特有的 I/O 多路復用解決方案。
epoll 的實現比 poll() 和 select() 要復雜得多,epoll 解決了前兩個都存在的基本性能問題,并增加了一些新的特性。
poll() 和 select() 每次調用時,內核必須遍歷所有被監視的文件描述符。當文件描述符列表變得很大時,例如包含幾百個甚至幾千個文件描述符時,每次調用都要遍歷列表就變成規模上的瓶頸。而 epoll 監測的對象是事件,無需遍歷文件列表,所以在性能上有很大的提升。
select、poll、epoll 的后端都是 struct file_operations 里的 unsigned int (*poll) (struct file *, struct poll_table_struct *);。以后有機會的話,會繼續跟進它們三者在內核里的實現。
6. 繼續分析 triggerhappy:thd.c / process_devices():讀取 input 數據
static?void?process_devices(void)?{for_each_device(?&check_device?); }devices.c / for_each_device() 會遍歷所有 input 設備(內含 input 文件描述符),對每一個設備都調用 check_device() 函數:
static?void?check_device(?device?*d?)?{int?fd?=?d->fd;if?(FD_ISSET(?fd,?&rfds?))?{if?(read_event(?d?))?{/*?read?error??Remove?the?device!?*/remove_device(?d->devname?);}} }thd.c / read_event() 執行input 數據的 read 操作:
static?int?read_event(?device?*dev?)?{[...]???????????//?some?initstruct?input_event?ev;int?n?=?read(?fd,?&ev,?sizeof(ev)?);[...]???????????//?parse?and?do?actionrun_triggers(?ev.type,?ev.code,?ev.value,?*keystate,?dev?); }1 個input_event 事件相當于產生了一個 trigger,接下來的重點就是調用 trigger.c / run_triggers() 來觸發用戶自定義的 action 了。
鑒于大多數人的注意力無法在一篇文章里上集中太久,更多的內容將放在后面的文章里。建議大家可以先自行閱讀相關書籍,不是自己理解到的東西是消化不了的。
7. 相關參考
《UNIX 環境高級編程》,14,16,17.6 章節
《Linux 系統編程》,2.10,4.2,8.7,11.7 章節
《Linux/UNIX系統編程手冊》,63 章節
Libevent
?推薦閱讀:
? ??專輯|Linux文章匯總
? ??專輯|程序人生
? ??專輯|C語言
嵌入式Linux
微信掃描二維碼,關注我的公眾號
總結
以上是生活随笔為你收集整理的Linux系统编程 / triggerhappy 源码分析(3.select 的应用)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java制作验证码的完整代码
- 下一篇: 富勒wms系统里的定时器id_仓储、运输