.html
.編程
.數組
.網絡
.併發
目錄app
(一) 一塊兒學 Unix 環境高級編程 (APUE) 之 標準IO框架
(二) 一塊兒學 Unix 環境高級編程 (APUE) 之 文件 IO異步
(三) 一塊兒學 Unix 環境高級編程 (APUE) 之 文件和目錄函數
(四) 一塊兒學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息測試
(五) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程環境
(六) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程控制
(七) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程關係 和 守護進程
(八) 一塊兒學 Unix 環境高級編程 (APUE) 之 信號
(九) 一塊兒學 Unix 環境高級編程 (APUE) 之 線程
(十) 一塊兒學 Unix 環境高級編程 (APUE) 之 線程控制
(十一) 一塊兒學 Unix 環境高級編程 (APUE) 之 高級 IO
(十二) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程間通訊(IPC)
(十三) [終篇] 一塊兒學 Unix 環境高級編程 (APUE) 之 網絡 IPC:套接字
1.非阻塞 I/O
高級 IO 部分有個很重要的概念是:非阻塞 I/O
在14章以前,咱們討論的全部函數都是阻塞的函數,例如 read(2) 函數讀取設備時,設備中若是沒有充足的數據,那麼 read(2) 函數就會阻塞等待,直到有數據可讀再返回。
當 IO 操做時出現了錯誤的時候,咱們以前在討論信號的博文中提到過會出現假錯的狀況。
那麼從學了非阻塞 I/O 爲止咱們一共遇到了兩種假錯的狀況:
EINTR:被信號打斷,阻塞時會遇到。
EAGAIN:非阻塞形式操做失敗。
遇到這兩種假錯的時候咱們須要從新再操做一次,因此一般對假錯的判斷是放在循環中的。
例如 read(2) 函數使用非阻塞方式讀取數據時,若是沒有讀取到數據,errno 爲 EAGAIN,此時並非說設備有問題或讀取失敗,只是代表採用的是非阻塞方式讀取而已。
阻塞與非阻塞是使用的同一套函數,flags 特殊要求指定爲 O_NONBLOCK 就能夠了。
下面咱們舉個小栗子:(僞代碼)
1 fd = open("/etc/service", O_RDONLY | O_NONBLOCK); 2 /* if error */ 3 4 while (1) { 5 size = read(fd, buf, BUFSIZE); 6 if (size < 0) { 7 if (EAGAIN == errno) { 8 continue; 9 } 10 perror("read()"); 11 exit(1); 12 } 13 14 // do sth... 15 16 }
上面的小栗子, 首先在 open(2) 的時候使用特殊要求 O_NONBLOCK 指定以非阻塞形式打開文件。
當 read(2) 發生錯誤時要判斷是否爲假錯,若是發生了假錯就再試一次,若是是真錯就作相應的異常處理。
2.有限狀態機
你們先考慮一個問題:把大象放到冰箱裏須要幾步?
1)打開冰箱門;
2)把大象放進去;
3)關閉冰箱門;
這就是解決這個問題的天然流程。
圖1 簡單流程與複雜流程
把一個問題的解決步驟(天然流程)擺出來發現是結構化的流程就是簡單流程,若是不是結構化的流程就是複雜流程。全部的網絡應用和須要與人交互的流程都是複雜流程。
結構化的流程就是做爲人類的本能解決問題的思路。
在以前的博文中 LZ 提到過一個「口令隨機校驗」的策略你們還記得嗎?就是要求用戶必須連續兩次輸入正確的密碼才認爲校驗經過。就算是這樣小的模塊也不會用一個單純的順序選擇流程把它完成,它必定是一個非結構化的流程。
有限狀態機就是程序設計的一種思路而已,你們剛開始接觸以爲難以理解,那是由於尚未習慣這種設計思路。咱們爲何以爲像原先那種流程化的程序設計思路好用?那是由於被虐慣了,你曾經被迫習慣用計算機的思路來考慮問題而不是用做爲人解決問題的本能步驟來考慮問題。有限狀態機就是讓你以做爲人的本能的解決問題的方式來解決問題,當你習慣了有限狀態機的設計思想以後就不以爲這是什麼難以理解的東西了。
有限狀態機被設計出來的目的就是爲了解決複雜流程的問題,因此更況且是簡單流程的問題也同樣可以輕鬆的解決。
做爲程序猿最怕的是什麼?
恐怕最怕的就是需求變動了吧。
爲何要使用有限狀態機的設計思路呢?由於它能幫助咱們從容的應對需求變動。
使用有限狀態機編程的程序在面對需求變動的時候每每僅須要修改幾條 case 語句就能夠了,而沒有使用有限狀態機編程的程序面對需求變動每每要把大段的代碼推倒重來。
因此若是你掌握了有限狀態機的編程思想,那麼在不少狀況下均可以相對輕鬆的解決問題,並且程序具備較好強的健壯性。
說了這麼多廢話,有限狀態機究竟是什麼呢?
使用有限狀態機首先要把程序的需求分析出來(廢話,用什麼編程都得先分析需求),而後把程序中出現的各類狀態抽象出來製做成一張狀態機流程圖,而後根據這個流程圖把程序的框架搭建出來,接下來就是添枝加葉了。
下面咱們經過一個栗子來講明有限狀態機的設計思想。
假若有以下需求:從設備 tty11 讀取輸入並輸出到 tty12 上,一樣從 tyy12 讀取輸入並輸出到 tty11 上。
首先咱們把它的各類狀態抽象出來畫成一幅圖。
圖2 有限狀態機
每一個狀態畫成一個圓形節點,每一個節點延伸出來有多少條線就表示有多少種可能性。
這些節點拿到咱們的程序中就變成了一條條 case 語句,下面咱們看看使用代碼如何實現。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <fcntl.h> 7 #include <errno.h> 8 9 #define BUFSIZE 1024 10 #define TTY1 "/dev/tty11" 11 #define TTY2 "/dev/tty12" 12 13 /* 狀態機的各類狀態 */ 14 enum 15 { 16 STATE_R=1, 17 STATE_W, 18 STATE_Ex, 19 STATE_T 20 }; 21 22 /* 狀態機,根據不一樣的需求設計不一樣的成員 */ 23 struct fsm_st 24 { 25 int state; // 狀態機當前的狀態 26 int sfd; // 讀取的來源文件描述符 27 int dfd; // 寫入的目標文件描述符 28 char buf[BUFSIZE]; // 緩衝 29 int len; // 一次讀取到的實際數據量 30 int pos; // buf 的偏移量,用於記錄堅持寫夠 n 個字節時每次循環寫到了哪裏 31 char *errstr; // 錯誤消息 32 }; 33 34 /* 狀態機驅動 */ 35 static void fsm_driver(struct fsm_st *fsm) 36 { 37 int ret; 38 39 switch(fsm->state) 40 { 41 case STATE_R: // 讀態 42 fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE); 43 if(fsm->len == 0) // 讀到了文件末尾,將狀態機推向 T態 44 fsm->state = STATE_T; 45 else if(fsm->len < 0) // 讀取出現異常 46 { 47 if(errno == EAGAIN) // 若是是假錯就推到 讀態,從新讀一次 48 fsm->state = STATE_R; 49 else // 若是是真錯就推到 異常態 50 { 51 fsm->errstr = "read()"; 52 fsm->state = STATE_Ex; 53 } 54 } 55 else // 成功讀取到了數據,將狀態機推到 寫態 56 { 57 fsm->pos = 0; 58 fsm->state = STATE_W; 59 } 60 break; 61 62 case STATE_W: // 寫態 63 ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len); 64 if(ret < 0) // 寫入出現異常 65 { 66 if(errno == EAGAIN) // 若是是假錯就再次推到 寫態,從新再寫入一次 67 fsm->state = STATE_W; 68 else // 若是是真錯就推到 異常態 69 { 70 fsm->errstr = "write()"; 71 fsm->state = STATE_Ex; 72 } 73 } 74 else // 成功寫入了數據 75 { 76 fsm->pos += ret; 77 fsm->len -= ret; 78 if(fsm->len == 0) // 若是將讀到的數據徹底寫出去了就將狀態機推向 讀態,開始下一輪讀取 79 fsm->state = STATE_R; 80 else // 若是沒有將讀到的數據徹底寫出去,那麼狀態機依然推到 寫態,下次繼續寫入沒寫完的數據,實現「堅持寫夠 n 個字節」 81 fsm->state = STATE_W; 82 } 83 84 break; 85 86 case STATE_Ex: // 異常態,打印異常並將狀態機推到 T態 87 perror(fsm->errstr); 88 fsm->state = STATE_T; 89 break; 90 91 case STATE_T: // 結束態,在這個例子中結束態沒有什麼須要作的事情,因此空着 92 /*do sth */ 93 break; 94 default: // 程序極可能發生了溢出等不可預料的狀況,爲了不異常擴大直接自殺 95 abort(); 96 } 97 98 } 99 100 /* 推進狀態機 */ 101 static void relay(int fd1,int fd2) 102 { 103 int fd1_save,fd2_save; 104 // 由於是讀 tty1 寫 tty2;讀 tty2 寫 tty1,因此這裏的兩個狀態機直接取名爲 fsm12 和 fsm21 105 struct fsm_st fsm12,fsm21; 106 107 fd1_save = fcntl(fd1,F_GETFL); 108 // 使用狀態機操做 IO 通常都採用非阻塞的形式,避免狀態機被阻塞 109 fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK); 110 fd2_save = fcntl(fd2,F_GETFL); 111 fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK); 112 113 // 在啓動狀態機以前將狀態機推向 讀態 114 fsm12.state = STATE_R; 115 // 設置狀態機中讀寫的來源和目標,這樣狀態機的讀寫接口就統一了。在狀態機裏面不用管究竟是 讀tty1 寫tty2 仍是 讀tty2 寫tty1 了,它只須要知道是 讀src 寫des 就能夠了。 116 fsm12.sfd = fd1; 117 fsm12.dfd = fd2; 118 119 // 同上 120 fsm21.state = STATE_R; 121 fsm21.sfd = fd2; 122 fsm21.dfd = fd1; 123 124 125 // 開始推狀態機,只要不是 T態 就一直推 126 while(fsm12.state != STATE_T || fsm21.state != STATE_T) 127 { 128 // 調用狀態機驅動函數,狀態機開始工做 129 fsm_driver(&fsm12); 130 fsm_driver(&fsm21); 131 } 132 133 fcntl(fd1,F_SETFL,fd1_save); 134 fcntl(fd2,F_SETFL,fd2_save); 135 136 } 137 138 int main() 139 { 140 int fd1,fd2; 141 142 // 假設這裏忘記將設備 tty1 以非阻塞的形式打開也不要緊,由於推進狀態機以前會從新設定文件描述符爲非阻塞形式 143 fd1 = open(TTY1,O_RDWR); 144 if(fd1 < 0) 145 { 146 perror("open()"); 147 exit(1); 148 } 149 write(fd1,"TTY1\n",5); 150 151 fd2 = open(TTY2,O_RDWR|O_NONBLOCK); 152 if(fd2 < 0) 153 { 154 perror("open()"); 155 exit(1); 156 } 157 write(fd2,"TTY2\n",5); 158 159 160 relay(fd1,fd2); 161 162 163 close(fd1); 164 close(fd2); 165 166 167 exit(0); 168 }
你們先把這段代碼讀明白,下面咱們還要用這段代碼來修改示例。
若是隻看上面的代碼是很難理解程序是作什麼的,由於都是一組一組的 case 語句,不容易理解。因此通常使用有限狀態機開發的程序都會與圖或相關的文檔配套發行,看了圖再結合代碼就很容易看出來代碼的目的了。
你們要對比着上面的圖來看代碼,這樣思路就很清晰了。
使用狀態機以前須要使兩個待進行數據中繼的文件描述符必須都是 O_NONBLOCK 的。
整個狀態機中都沒有使用循環來讀寫數據,由於狀態機能確保每一種狀態都是職責單一的,出現其它的任何情況的時候只要推進狀態機問題就能夠解決了。
因此這樣的程序可維護性是否是高了不少?若是出現了需求變動,只須要簡單的修改幾條 case 語句就能夠了,而不須要大段大段的修改代碼了。
你們要多使用狀態機的設計思想來寫程序才能加深對這種設計思想的掌握程度。
3. I/O 多路轉接
上面那個 讀tty11 寫tty12,讀tty12 寫tty11 的栗子是採用忙等的方式實現的,I/O 多路轉接這個小節討論的就是怎麼把上面那個栗子修改成非忙等的模式。
有些時候就是這樣的,讀取多個文件(通常是設備)的時候不能使用阻塞方式,由於一個阻塞了其它的就無法讀了;而非阻塞方式若是採用忙等的形式又得不償失。你想一想好比 telnet 服務在接收用戶的命令的時候是否是這種狀況呢?
對於處理這樣的需求,Linux 系統爲咱們提供了 3 種方案:select(2)、poll(2) 和 epoll(7),這些方案提供的函數能夠同時監視多個文件描述符,當它們的狀態沒有變化時阻塞等待,當它們的狀態發生變化時會給咱們一個通知讓咱們繼續處理任務,下面咱們一個一個的介紹它們。
先來看第一個函數:select(2)
1 select, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing 2 3 /* According to POSIX.1-2001 */ 4 #include <sys/select.h> 5 6 /* According to earlier standards */ 7 #include <sys/time.h> 8 #include <sys/types.h> 9 #include <unistd.h> 10 11 int select(int nfds, fd_set *readfds, fd_set *writefds, 12 fd_set *exceptfds, struct timeval *timeout); 13 14 void FD_CLR(int fd, fd_set *set); 15 int FD_ISSET(int fd, fd_set *set); 16 void FD_SET(int fd, fd_set *set); 17 void FD_ZERO(fd_set *set);
select(2) 的優勢是足夠老,各個平臺都支持它,這也是它相對於 poll(2) 惟一的優勢。
參數列表:
nfds:最大的文件描述符 + 1;
readfds:須要監視的輸入文件描述符集合;
writefds:須要監視的輸出文件描述符集合;
exceptfds:須要監視的會發生異常的文件描述符集合;
timeout:等待的超時時間,若是時間超時依然沒有文件描述符狀態發生變化那麼就返回。設置爲 0 會當即返回,設置爲 NULL 則一直阻塞等待,不會超時。
還記得咱們以前提到過使用 select(2) 函數替代 sleep(3) 函數嗎?記不起來的童鞋本身回去翻看前面的博文吧,這裏再也不贅述了。
咱們看到參數中的文件描述符集合是 fd_set 類型的,那麼怎麼把咱們的 int 類型的文件描述符添加到 fd_set 當中去呢?
經過帶參數的宏 FD_SET 就能夠將文件描述符 fd 添加到 set 中了,而 FD_CLR 能夠刪除 set 中的給定的文件描述符。
帶參數的宏 FD_ZERO 的做用是清空 set 中的文件描述符。
帶參數的宏 FD_ISSET 的做用是測試文件描述符 fd 是否在 set 集合中。
下面咱們重構上面的栗子,經過把它修改爲非忙等的形式來看看 select 是如何使用的。代碼沒有太大的區別,因此只貼出有差別的部分。
1 enum 2 { 3 STATE_R=1, 4 STATE_W, 5 STATE_AUTO, // 添加這個值是爲了起到分水嶺的做用,小於這個值的時候才須要使用 select(2) 監視 6 STATE_Ex, 7 STATE_T 8 }; 9 10 static int max(int a,int b) 11 { 12 if(a < b) 13 return b; 14 return a; 15 } 16 17 static void relay(int fd1,int fd2) 18 { 19 int fd1_save,fd2_save; 20 struct fsm_st fsm12,fsm21; 21 fd_set rset,wset; // 讀寫文件描述符集合 22 23 fd1_save = fcntl(fd1,F_GETFL); 24 fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK); 25 fd2_save = fcntl(fd2,F_GETFL); 26 fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK); 27 28 fsm12.state = STATE_R; 29 fsm12.sfd = fd1; 30 fsm12.dfd = fd2; 31 32 fsm21.state = STATE_R; 33 fsm21.sfd = fd2; 34 fsm21.dfd = fd1; 35 36 37 while(fsm12.state != STATE_T || fsm21.state != STATE_T) 38 { 39 //佈置監視任務 40 FD_ZERO(&rset); 41 FD_ZERO(&wset); 42 43 // 讀態監視輸入文件描述符;寫態監視輸出文件描述符 44 if(fsm12.state == STATE_R) 45 FD_SET(fsm12.sfd,&rset); 46 if(fsm12.state == STATE_W) 47 FD_SET(fsm12.dfd,&wset); 48 if(fsm21.state == STATE_R) 49 FD_SET(fsm21.sfd,&rset); 50 if(fsm21.state == STATE_W) 51 FD_SET(fsm21.dfd,&wset); 52 53 if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO) 54 { 55 // 以阻塞形式監視 56 if(select(max(fd1,fd2)+1,&rset,&wset,NULL,NULL) < 0) 57 { 58 if(errno == EINTR) 59 continue; 60 perror("select()"); 61 exit(1); 62 } 63 } 64 65 //查看監視結果 66 if( FD_ISSET(fd1,&rset) || FD_ISSET(fd2,&wset) || fsm12.state > STATE_AUTO) 67 fsm_driver(&fsm12); 68 if( FD_ISSET(fd2,&rset) || FD_ISSET(fd1,&wset) || fsm21.state > STATE_AUTO) 69 fsm_driver(&fsm21); 70 } 71 72 fcntl(fd1,F_SETFL,fd1_save); 73 fcntl(fd2,F_SETFL,fd2_save); 74 75 }
在上面的栗子中,不管設備中是否有數據供咱們讀取咱們都不停的推進狀態機,因此致使出現了忙等的現象。
而在這個栗子中,咱們在推狀態機以前使用 select(2) 函數對文件描述符進行監視,若是文件描述狀態沒有發生變化就阻塞等待;而哪一個狀態機的文件描述符發生了變化就推進哪一個狀態機,這樣就將查詢法的實現改成通知法的實現了。是否是很簡單呢?
poll(2) 出現的時間沒有 select(2) 那麼悠久,因此在可移植性上來講沒有 select(2) 函數那麼好,可是絕大多數主流 *nix 平臺都支持 poll(2) 函數,它比 select(2) 要優秀不少,下面咱們來了解下它。
1 poll - wait for some event on a file descriptor 2 3 #include <poll.h> 4 5 int poll(struct pollfd *fds, nfds_t nfds, int timeout); 6 7 struct pollfd { 8 int fd; /* 須要監視的文件描述符 */ 9 short events; /* 要監視的事件 */ 10 short revents; /* 該文件描述符發生了的事件 */ 11 };
參數列表:
fds:其實是一個數組的首地址,由於 poll(2) 能夠幫助咱們監視多個文件描述符,而一個文件描述放到一個 struct pollfd 結構體中,多個文件描述符就須要一個數組來存儲了。
nfds:fds 這個數組的長度。在參數列表中使用數組首地址 + 長度的作法仍是比較常見的。
timeout:阻塞等待的超時時間。傳入 -1 則始終阻塞,不超時。
結構體中的事件能夠指定下面七種事件,同時監視多個事件可使用按位或(|)添加:
事件 | 描述 |
POLLIN | 文件描述符可讀 |
POLLPRI | 能夠非阻塞的讀高優先級的數據 |
POLLOUT | 文件描述符可寫 |
POLLRDHUP | 流式套接字鏈接點關閉,或者關閉寫半鏈接。 |
POLLERR | 已出錯 |
POLLHUP | 已掛斷(通常指設備) |
POLLNVAL | 參數非法 |
表1 poll(2) 能夠監視的 7 種事件
使用 poll(2) 的步驟也很簡單:
1)首先經過 struct pollfd 結構體中的 events 成員佈置監視任務;
2)而後使用 poll(2) 函數進行阻塞的監視;
3)當從 poll(2) 函數返回時就能夠經過 struct polfd 結構體中的 revents 成員與上面的 7 個宏中被咱們選出來監視的宏進行按位與(&)操做了,只要結果不爲 1 就認爲觸發了該事件。
好了,這 3 步就是 poll(2) 函數的使用方法,簡單吧。
下面咱們修改一下上面的栗子,把上面用 select(2) 實現的部分修改成用 poll(2) 來實現。沒有改過的地方就不貼出來了,其實也只有 relay() 函數被修改了。
1 static void relay(int fd1,int fd2) 2 { 3 int fd1_save,fd2_save; 4 struct fsm_st fsm12,fsm21; 5 struct pollfd pfd[2]; // 一共監視兩個文件描述符 6 7 8 fd1_save = fcntl(fd1,F_GETFL); 9 fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK); 10 fd2_save = fcntl(fd2,F_GETFL); 11 fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK); 12 13 fsm12.state = STATE_R; 14 fsm12.sfd = fd1; 15 fsm12.dfd = fd2; 16 17 fsm21.state = STATE_R; 18 fsm21.sfd = fd2; 19 fsm21.dfd = fd1; 20 21 pfd[0].fd = fd1; 22 pfd[1].fd = fd2; 23 24 while(fsm12.state != STATE_T || fsm21.state != STATE_T) 25 { 26 // 佈置監視任務 27 pfd[0].events = 0; 28 if(fsm12.state == STATE_R) 29 pfd[0].events |= POLLIN; // 第一個文件描述符可讀 30 if(fsm21.state == STATE_W) 31 pfd[0].events |= POLLOUT; // 第一個文件描述符可寫 32 33 pfd[1].events = 0; 34 if(fsm12.state == STATE_W) 35 pfd[1].events |= POLLOUT; // 第二個文件描述符可讀 36 if(fsm21.state == STATE_R) 37 pfd[1].events |= POLLIN; // 第二個文件描述符可寫 38 39 // 只要是可讀寫狀態就進行監視 40 if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO) 41 { 42 // 阻塞監視 43 while(poll(pfd,2,-1) < 0) 44 { 45 if(errno == EINTR) 46 continue; 47 perror("poll()"); 48 exit(1); 49 } 50 } 51 52 // 查看監視結果 53 if( pfd[0].revents & POLLIN || \ 54 pfd[1].revents & POLLOUT || \ 55 fsm12.state > STATE_AUTO) 56 fsm_driver(&fsm12); // 推狀態機 57 if( pfd[1].revents & POLLIN || \ 58 pfd[0].revents & POLLOUT || \ 59 fsm21.state > STATE_AUTO) 60 fsm_driver(&fsm21); // 推狀態機 61 } 62 63 fcntl(fd1,F_SETFL,fd1_save); 64 fcntl(fd2,F_SETFL,fd2_save); 65 66 }
代碼中註釋寫得很明確了,相信不須要 LZ 再解釋什麼了。
epoll(7) 不是一個函數,它在 man 手冊的第 7 章裏,它是 Linux 爲咱們提供的「增強版 poll(2)」,既然是增強版,那麼必定有超越 poll(2) 的地方,下面就聊一聊 epoll(7)。
在使用 poll(2) 的時候用戶須要管理一個 struct pollfd 結構體或它的結構體數組,epoll(7) 則使內核爲咱們管理了這個結構體數組,咱們只須要經過 epoll_create(2) 返回的標識引用這個結構體便可。
1 epoll_create - open an epoll file descriptor 2 3 #include <sys/epoll.h> 4 5 int epoll_create(int size);
調用 epoll_create(2) 時最初 size 參數給傳入多少,kernel 在創建數組的時候就是多少個元素。可是這種方式很差用,因此後來改進了,只要 size 隨便傳入一個正整數就能夠了,內核不會再根據你們傳入的 size 直接做爲數組的長度了,由於內核是使用 hash 來管理要監視的文件描述符的。
返回值是 epfd,從這裏也能夠體現出 Linux 一切皆文件的設計思想。失敗時返回 -1 並設置 errno。
獲得了內核爲咱們管理的結構體數組標識以後,接下來就能夠用 epoll_ctl(2) 函數佈置監視任務了。
1 epoll_ctl - control interface for an epoll descriptor 2 3 #include <sys/epoll.h> 4 5 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 6 7 8 struct epoll_event { 9 uint32_t events; /* Epoll 監視的事件,這些事件與 poll(2) 能監視的事件差很少,只是宏名前面加了個E */ 10 epoll_data_t data; /* 用戶數據,除了能保存文件描述符之外,還能讓你保存一些其它有關數據,好比你這個文件描述符是嵌在一棵樹上的,你在使用它的時候不知道它是樹的哪一個節點,則能夠在佈置監視任務的時候將相關的位置都保存下來。這個聯合體成員就是 epoll 設計的精髓。 */ 11 };
epoll_ctl(2) 的做用是要對 fd 增長或減小(op) 什麼行爲的監視(event)。成功返回0,失敗返回 -1 並設置 errno。
op 參數可使用下面三個宏來指定操做:
宏 | 描述 |
EPOLL_CTL_ADD | 增長要監視的文件描述符 |
EPOLL_CTL_MOD | 更改目標文件描述符的事件 |
EPOLL_CTL_DEL | 刪除要監視的文件描述符,event 參數會被忽略,能夠傳入 NULL。 |
表2 epoll_ctl(2) 函數 op 參數的選項
與 select(2) 和 poll(2) 同樣, 佈置完監視任務以後須要取監視結果,epoll(7) 策略使用 epoll_wait(2) 函數進行阻塞監視並返回監視結果。
1 epoll_wait - wait for an I/O event on an epoll file descriptor 2 3 #include <sys/epoll.h> 4 5 int epoll_wait(int epfd, struct epoll_event *events, 6 int maxevents, int timeout);
參數列表:
epfd:要操做的 epoll 實例;
events + maxevents:共同指定了一個結構體數組,數組的起始位置和長度。其實每次使用 epoll_ctl(2) 函數添加一個文件描述符時至關於向內核爲咱們管理的數組中添加了一個成員,因此當咱們使用同一個 struct epoll_event 變量操做多個文件描述符時,只需傳入該變量的地址和操做了多少個文件描述符便可,你們看看下面的栗子就明白了。
timeout:超時等待的時間,設置爲 -1 則始終阻塞監視,不超時。
跟上面的栗子同樣,LZ 只貼出來被修改了的 relay() 函數,其它部分不變。
1 static void relay(int fd1,int fd2) 2 { 3 int fd1_save,fd2_save; 4 struct fsm_st fsm12,fsm21; 5 int epfd; 6 struct epoll_event ev; 7 8 epfd = epoll_create(10); 9 if(epfd < 0) 10 { 11 perror("epfd()"); 12 exit(1); 13 } 14 15 fd1_save = fcntl(fd1,F_GETFL); 16 fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK); 17 fd2_save = fcntl(fd2,F_GETFL); 18 fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK); 19 20 fsm12.state = STATE_R; 21 fsm12.sfd = fd1; 22 fsm12.dfd = fd2; 23 24 fsm21.state = STATE_R; 25 fsm21.sfd = fd2; 26 fsm21.dfd = fd1; 27 28 ev.events = 0; 29 ev.data.fd = fd1; 30 epoll_ctl(epfd,EPOLL_CTL_ADD,fd1,&ev); 31 32 ev.events = 0; 33 ev.data.fd = fd2; 34 epoll_ctl(epfd,EPOLL_CTL_ADD,fd2,&ev); 35 36 37 while(fsm12.state != STATE_T || fsm21.state != STATE_T) 38 { 39 // 佈置監視任務 40 41 ev.events = 0; 42 ev.data.fd = fd1; 43 if(fsm12.state == STATE_R) 44 ev.events |= EPOLLIN; 45 if(fsm21.state == STATE_W) 46 ev.events |= EPOLLOUT; 47 epoll_ctl(epfd,EPOLL_CTL_MOD,fd1,&ev); 48 49 ev.events = 0; 50 ev.data.fd = fd2; 51 if(fsm12.state == STATE_W) 52 ev.events |= EPOLLOUT; 53 if(fsm21.state == STATE_R) 54 ev.events |= EPOLLIN; 55 epoll_ctl(epfd,EPOLL_CTL_MOD,fd2,&ev); 56 57 // 監視 58 if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO) 59 { 60 while(epoll_wait(epfd,&ev,1,-1) < 0) 61 { 62 if(errno == EINTR) 63 continue; 64 perror("epoll_wait()"); 65 exit(1); 66 } 67 } 68 69 // 查看監視結果 70 if( ev.data.fd == fd1 && ev.events & EPOLLIN || \ 71 ev.data.fd == fd2 && ev.events & EPOLLOUT || \ 72 fsm12.state > STATE_AUTO) 73 fsm_driver(&fsm12); 74 if( ev.data.fd == fd2 && ev.events & EPOLLIN || \ 75 ev.data.fd == fd1 && ev.events & EPOLLOUT || \ 76 fsm21.state > STATE_AUTO) 77 fsm_driver(&fsm21); 78 } 79 80 fcntl(fd1,F_SETFL,fd1_save); 81 fcntl(fd2,F_SETFL,fd2_save); 82 83 close(epfd); 84 85 }
4.記錄鎖
記錄鎖就是用 fcntl(2) 函數建立一個鎖文件,比較麻煩,感興趣的童鞋能夠本身看看書上的介紹,在這裏 LZ 就不作介紹了,咱們在最後會討論兩個方便的文件鎖和鎖文件。
5.異步 I/O
這部分主要是說信號驅動 IO,不是真正意義上的異步 IO。
異步 I/O 分爲 System V 異步 I/O 和 BSD 異步 I/O,Linux 模仿的是後者,這裏咱們不過多討論了,後面 LZ 在討論內核的博文中會繼續討論異步。
6. readv(2) 和 write(2)
1 readv, writev - read or write data into multiple buffers 2 3 #include <sys/uio.h> 4 5 ssize_t readv(int fd, const struct iovec *iov, int iovcnt); 6 7 ssize_t writev(int fd, const struct iovec *iov, int iovcnt); 8 9 struct iovec { 10 void *iov_base; /* 起始地址 */ 11 size_t iov_len; /* Number of bytes to transfer */ 12 };
這兩個函數的做用就是對多個碎片的讀寫操做,將全部的小碎片寫到文件中。
readv(2) 當沒有連續的空間存儲從 fd 讀取或寫入的數據時,將其存儲在 iovcnt 個 iov 結構體中,writev(2) 的做用相同。iov 是結構體數組起始位置,iovcnt 是數組長度。
7. readn() 和 writen()
這兩個函數能夠從本書(《APUE》第三版)的光盤中找,它們並非什麼標準庫的函數,也不是系統調用,只是本書做者本身封裝的函數,算是方言中的方言,做用是堅持寫夠 n 個字節,以前咱們在討論 IO 的博文中實現過相似的效果。
對了,天朝在引入這本書的時候貌似沒有引入配套光盤,須要的童鞋能夠本身去網上搜索一下。
7.存儲映射 I/O
存儲映射 I/O 是十四章的小重點。
在 *nix 系統中分配內存的方法有好幾種,不必定非得使用 free(3) 函數。
經過 mmap(2) 和 unmap(2) 函數能夠實現一個實時的相似於 malloc(3) 和 free(3) 函數的效果,咱們在前面的博文中提到過,malloc(3) 和 free(3) 其實是以打白條的形式實現的,就是在你調用函數的時候並無當即分配內存給你,而是在你真正使用內存的時候才分配給你的。
存儲映射I/O說的就是將一個文件的一部分或所有映射到內存中,用戶拿到的就是這段內存的起始位置,訪問這個文件就至關於訪問一個大字符串同樣。
1 mmap, munmap - map or unmap files or devices into memory 2 3 #include <sys/mman.h> 4 5 void *mmap(void *addr, size_t length, int prot, int flags, 6 int fd, off_t offset); 7 int munmap(void *addr, size_t length);
mmap(2) 函數的做用是把 fd 這個文件從 offset 偏移位置開始把 length 字節個長度映射到 addr 這個內存位置上,若是 addr 參數傳入 NULL 則由 kernel 幫咱們選擇一塊空間並使用返回值返回這段內存的首地址。
prot 參數是操做權限,可使用下表中的宏經過按位或(|)來組合指定。
宏 | 含義 |
PROT_READ | 映射區可讀 |
PROT_WRITE | 映射區可寫 |
PROT_EXEC | 映射區可執行 |
PROT_NONE | 映射區不可訪問 |
表3 mmap(2) 函數的 prot 參數可選項
映射區不可訪問(PROT_NONE)的含義是若是我映射的內存中有一塊已經有某些數據了,絕對不能讓個人程序越界覆蓋了,就能夠把這段空間設置爲映射區不可訪問。
flags 參數是特殊要求,如下兩者必選其一:
宏 | 含義 |
MAP_SHARED | 對映射區進行存儲操做至關於對原來的文件進行寫入,會改變原來文件的內容。 |
MAP_PRIVATE | 當對映射區域進行存儲操做時會建立一個私有副本,全部後來再對映射區的操做都至關於操做這個副本,而不影響原來的文件。 |
表4 mmap(2) 函數的 flags 參數可選項
其它經常使用選項:
MAP_ANONYMOUS:不依賴於任何文件,映射出來的內存空間會被清 0,而且 fd 和 offset 參數會被忽略,一般咱們在使用的時候會把 fd 設置爲 -1。
用這個參數能夠很容易的作出一個最簡單最好用的在具備親緣關係的進程之間的共享內存,比後面第15章咱們要討論的共享內存還好用。後面 LZ 會給出一個小栗子讓你們看看這種方式如何使用。
mmap(2) 在成功的時候返回一個指針,會指向映射的內存區域的起始地址。失敗時返回 MAP_FAILED 宏定義,實際上是這樣定義的:(void *) -1。
首先咱們寫一個栗子看看如何把一個文件映射到內存中訪問。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/mman.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <unistd.h> 7 #include <fcntl.h> 8 9 10 #define FNAME "/etc/services" 11 12 int main(void) 13 { 14 int fd,i; 15 char *str; 16 struct stat statres; 17 int count = 0; 18 19 fd = open(FNAME,O_RDONLY); 20 if(fd < 0) 21 { 22 perror("open()"); 23 exit(1); 24 } 25 26 // 經過 stat(2) 得到文件大小 27 if(fstat(fd,&statres) < 0) 28 { 29 perror("fstat()"); 30 exit(1); 31 } 32 33 str = mmap(NULL,statres.st_size,PROT_READ,MAP_SHARED,fd,0); 34 if(str == MAP_FAILED) 35 { 36 perror("mmap()"); 37 exit(1); 38 } 39 40 // 將文件映射到內存以後文件描述符就能夠關閉了,直接訪問映射的內存就至關於訪問文件了。 41 close(fd); 42 43 for(i = 0 ; i < statres.st_size; i++) { 44 // 由於訪問的是文本文件,因此能夠把映射的內存看做是一個大字符串處理 45 if(str[i] == 'a') { 46 count++; 47 } 48 } 49 50 printf("count = %d\n",count); 51 52 // 用完了別忘了解除映射,否則會形成內存泄漏! 53 munmap(str,statres.st_size); 54 55 exit(0); 56 }
這段代碼會統計 /etc/services 文件中包含多少個字符 'a'。
mmap(2) 的返回值是 void* 類型的,這是一種百搭的類型,在映射了不一樣的東西的狀況下咱們可使用不一樣的指針來接收,這樣就能用不一樣的方式訪問這段內存空間了。上面這個文件是文本文件,因此咱們可使用 char* 來接收它的返回值,這樣就將整個文件看做是一個大字符串來訪問了。
這個仍是比較常規的用法,下面咱們看一下如何使用 mmap(2) 函數製做一個好用的共享內存。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/mman.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <unistd.h> 7 #include <fcntl.h> 8 #include <string.h> 9 #include <wait.h> 10 11 #define MEMSIZE 1024 12 13 int main(void) 14 { 15 char *str; 16 pid_t pid; 17 18 // 這裏在 flags 中添加 MAP_ANONYMOUS,爲製做共享內存作準備 19 str = mmap(NULL,MEMSIZE,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); 20 if(str == MAP_FAILED) 21 { 22 perror("mmap()"); 23 exit(1); 24 } 25 26 // 建立子進程,父子進程使用共享內存進行通訊 27 pid = fork(); 28 if(pid < 0) 29 { 30 perror("fork()"); 31 exit(1); 32 } 33 if(pid == 0) // 子進程向共享內存中寫入數據 34 { 35 strcpy(str,"Hello!"); 36 munmap(str,MEMSIZE); // 注意,雖然共享內存是在 fork(2) 以前建立的,可是 fork(2) 的時候子進程也拷貝了一份,因此子進程使用完畢以後也要解除映射 37 exit(0); 38 } 39 else // 父進程從共享內存中讀取子進程寫入的數據 40 { 41 wait(NULL); // 保障子進程先運行起來,由於就算父進程先運行了也會在這裏阻塞等待 42 puts(str); // 把從共享內存中讀取出來的數據打印出來 43 munmap(str,MEMSIZE); // 不要忘記解除映射 44 exit(0); 45 } 46 47 48 exit(0); 49 }
共享內存是進程間通訊的一種手段,就是在內存中開闢一塊空間讓多個進程之間能夠共同訪問這段空間,從而實現進程之間的數據交換。在後面討論 IPC 的博文中咱們還會詳細介紹共享內存,不過用 mmap(2) 製做的共享內存比後面介紹的共享內存使用起來更簡便一些。
你們本身運行一下這段代碼,能夠看到父進程打印出了子進程寫入的「Hello」字符串,說明這段內存確實是在父子進程之間共享的。
你們在使用的時候不要忘記父子進程最後都要作解除映射的動做。
從這個栗子中咱們也能夠看出來,這種共享內存的方式只適合在具備親緣關係的進程之間使用,沒有親緣關係的進程是沒法得到指向同一個映射內存空間的指針的。
8. flock(2) 和 lockf(3) 函數
1 lockf - apply, test or remove a POSIX lock on an open file 2 3 #include <unistd.h> 4 5 int lockf(int fd, int cmd, off_t len); 6 7 flock - apply or remove an advisory lock on an open file 8 9 #include <sys/file.h> 10 11 int flock(int fd, int operation);
這兩個函數能夠實現好用的文件加鎖。
咱們這裏只介紹 lockf(2) 函數,flock(2) 函數也差很少,都很簡單,因此你們能夠本身去查閱 man 手冊。
lockf(3) 能夠給文件進行局部加鎖,簡單來講就是從當前位置鎖住 len 個字節。
參數列表:
fd:要加鎖的文件描述符;
cmd:具體的命令見下表;
宏 | 說明 |
F_LOCK | 爲文件的一段加鎖,若是已經被加鎖就阻塞等待,若是兩個鎖要鎖定的部分有交集就會被合併 ,文件關閉時或進程退出時會自動釋放,不會被子進程繼承。 |
F_TLOCK | 與 F_LOCK 差很少,不過是嘗試加鎖,非阻塞。 |
F_ULOCK | 解鎖,若是是被合併的鎖會分裂。 |
F_TEST | 測試鎖,若是文件中被測試的部分沒有鎖定或者是調用進程持有鎖就返回 0; 若是是其它進程持有鎖就返回 -1,而且 errno 設置爲 EAGAIN 或 EACCES。 |
圖5 lockf(3) 函數的 cmd 參數可選值
len:要鎖定的長度,若是爲 0 表示文件有多長鎖多長,從當前位置一直鎖到文件結尾。
下面咱們使用 lockf(3) 函數寫一個栗子。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <wait.h> 6 #include <sys/types.h> 7 8 9 #define PROCNUM 20 10 #define FNAME "/tmp/out" 11 #define BUFSIZE 1024 12 13 14 static void func_add() 15 { 16 FILE *fp; 17 int fd; 18 char buf[BUFSIZE]; 19 20 fp = fopen(FNAME,"r+"); 21 if(fp == NULL) 22 { 23 perror("fopen()"); 24 exit(1); 25 } 26 27 fd = fileno(fp); 28 if(fd < 0) 29 { 30 perror("fd"); 31 exit(1); 32 } 33 34 // 使用以前先鎖定 35 lockf(fd,F_LOCK,0); 36 37 fgets(buf,BUFSIZE,fp); 38 rewind(fp); // 把文件位置指針定位到文件首 39 sleep(1); // 放大競爭 40 fprintf(fp,"%d\n",atoi(buf)+1); 41 fflush(fp); 42 43 // 使用以後釋放鎖 44 lockf(fd,F_ULOCK,0); 45 46 fclose(fp); 47 48 return ; 49 } 50 51 int main(void) 52 { 53 int i; 54 pid_t pid; 55 56 for(i = 0 ; i < PROCNUM ; i++) 57 { 58 pid = fork(); 59 if(pid < 0) 60 { 61 perror("fork()"); 62 exit(1); 63 } 64 if(pid == 0) // child 65 { 66 func_add(); 67 exit(0); 68 } 69 } 70 71 for(i = 0 ; i < PROCNUM ; i++) 72 wait(NULL); 73 74 75 exit(0); 76 }
仍是用我麼之前的栗子改的,你們還記得之前寫過一個栗子,讓 20 個進程同時向 1 個文件中累加數字嗎。
在這裏每一個進程在讀寫文件以前先加鎖,若是加不上就等待別人釋放鎖再加。若是加上了鎖就讀出文件中當前的值,+1 以後再寫回到文件中。
得到鎖以後 sleep(1) 是爲了放大競爭,讓進程之間必定要出現競爭的現象,便於咱們分析調試。
在調試併發的程序時,若是有些問題很難復現,那麼能夠經過加長每個併發單位的執行時間來強制它們出現競爭的狀況,這樣可讓咱們更容易的分析問題。
圖3 flock(2) 和 lockf(3) 的缺點
文件鎖還有一個機制是把一個文件看成鎖,好比要操做的是 /tmp/out 文件,那麼父進程能夠先建立一個 /tmp/lcok文件,而後再建立 20 個子進程同時對 /tmp/out 文件進行讀寫,可是子進程必須先鎖定 /tmp/lock 文件才能操做 /tmp/out 文件,沒搶到鎖文件的須要等待其它進程解鎖再搶鎖,等父進程爲全部的子進程收屍以後再關閉/tmp/lock,/tmp/lock 這個文件就被稱爲鎖文件。
高級 IO 部分大概就這些內容了,有什麼疑問歡迎你們在評論中討論。