多路復用
1.函數說明
前面的fcntl()函數解決了文件的共享問題,接下來該處理I/O復用的情況了。
總的來說,I/O處理的模型有5種。
● 阻塞I/O模型:在這種模型下,若所調用的I/O函數沒有完成相關的功能,則會使進程掛起,直到相關數據到達才會返回。如常見對管道設備、終端設備和網絡設備進行讀寫時經常會出現這種情況。
● 非阻塞I/O模型:在這種模型下,當請求的I/O操作不能完成時,則不讓進程睡眠,而且立即返回。非阻塞I/O使用戶可以調用不會阻塞的I/O操作,如open()、write()和read()。如果該操作不能完成,則會立即返回出錯(如打不開文件)或者返回0(如在緩沖區中沒有數據可以讀取或者沒空間可以寫入數據)。
● I/O多路轉接模型:在這種模型下,如果請求的I/O操作阻塞,且它不是真正阻塞I/O,而是讓其中的一個函數等待,在此期間,I/O還能進行其他操作。如本小節要介紹的select()和poll()函數,就是屬于這種模型。
● 信號驅動I/O模型:在這種模型下,進程要定義一個信號處理程序,系統可以自動捕獲特定信號的到來,從而啟動I/O。這是由內核通知用戶何時可以啟動一個I/O操作決定的。
它是非阻塞的。當有就緒的數據時,內核就向該進程發送SIGIO信號。 無論我們如何處理SIGIO信號,這種模型的好處是當等待數據到達時,可以不阻塞。主程序繼續執行,只有收到SIGIO信號時才去處理數據即可。
● 異步I/O模型:在這種模型下,進程先讓內核啟動I/O操作,并在整個操作完成后通知該進程。這種模型與信號驅動模型的主要區別在于:信號驅動I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知進程I/O操作何時完成的。現在,并不是所有的系統都支持這種模型。
可以看到,select()和poll()的I/O多路轉接模型是處理I/O復用的一個高效的方法。它可以具體設置程序中每一個所關心的文件描述符的條件、希望等待的時間等,從select()和poll()函數返回時,內核會通知用戶已準備好的文件描述符的數量、已準備好的條件(或事件)等。通過使用select()和poll()函數的返回結果(可能是檢測到某個文件描述符的注冊事件或是超時,或是調用出錯),就可以調用相應的I/O處理函數了。
2.函數格式
select()函數的語法要點如表2.8所示。
表2.8 select()函數語法要點
所需頭文件 |
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
|
函數原型 |
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exeptfds, struct timeval *timeout)
|
函數傳入值 |
numfds:該參數值為需要監視的文件描述符的大值加1 |
readfds:由select()監視的讀文件描述符集合 |
writefds:由select()監視的寫文件描述符集合 |
exeptfds:由select()監視的異常處理文件描述符集合 |
timeout |
NULL:永遠等待,直到捕捉到信號或文件描述符已準備好為止 |
具體值:struct timeval類型的指針,若等待了timeout時間還沒有檢測到任何文件描符準備好,就立即返回 |
0:從不等待,測試所有指定的描述符并立即返回 |
函數返回值 |
成功:準備好的文件描述符
0:超時; 1:出錯
|
可以看到,select()函數根據希望進行的文件操作對文件描述符進行了分類處理,這里對文件描述符的處理主要涉及4個宏函數,如表2.9所示。
表2.9 select()文件描述符處理函數
FD_ZERO(fd_set *set) |
清除一個文件描述符集 |
FD_SET(int fd, fd_set *set) |
將一個文件描述符加入文件描述符集中 |
FD_CLR(int fd, fd_set *set) |
將一個文件描述符從文件描述符集中清除 |
FD_ISSET(int fd, fd_set *set) |
如果文件描述符fd為fd_set集中的一個元素,則返回非零值,可以用于調用select()后測試文件描述符集中的哪個文件描述符是否有變化 |
一般來說,在每次使用select()函數之前,首先使用FD_ZERO()和FD_SET()來初始化文件描述符集(在需要重復調用select()函數時,先把一次初始化好的文件描述符集備份下來,每次讀取它即可)。在select()函數返回后,可循環使用FD_ISSET()來測試描述符集,在執行完對相關文件描述符的操作后,使用FD_CLR()來清除描述符集。
另外,select()函數中的timeout是一個struct timeval類型的指針,該結構體如下所示:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微秒 */
}
可以看到,這個時間結構體的精確度可以設置到微秒級,這對于大多數的應用而言已經足夠了。
poll()函數的語法要點如表2.10所示。
表2.10 poll()函數語法要點
所需頭文件 |
#include <sys/types.h>
#include <poll.h>
|
函數原型 |
int poll(struct pollfd *fds, int numfds, int timeout) |
函數傳入值 |
fds:struct pollfd結構的指針,用于描述需要對哪些文件的哪種類型的操作進行監控
struct pollfd
{
int fd; /* 需要監聽的文件描述符 */
short events; /* 需要監聽的事件 */
short revents; /* 已發生的事件 */
}
events成員描述需要監聽哪些類型的事件,可以用以下幾種標志來描述。
POLLIN:文件中有數據可讀,下面實例中使用到了這個標志
POLLPRI::文件中有緊急數據可讀
POLLOUT:可以向文件寫入數據
POLLERR:文件中出現錯誤,只限于輸出
POLLHUP:與文件的連接被斷開,只限于輸出
POLLNVAL:文件描述符是不合法的,即它并沒有指向一個成功打開的文件
|
numfds:需要監聽的文件個數,即第一個參數所指向的數組中的元素數目 |
timeout:表示poll阻塞的超時時間(毫秒)。如果該值小于等于0,則表示無限等待 |
函數返回值 |
成功:返回大于0的值,表示事件發生的pollfd結構的個數
0:超時; 1:出錯
|
3.使用實例
當使用select()函數時,存在一系列的問題,例如,內核必須檢查多余的文件描述符,每次調用select()之后必須重置被監聽的文件描述符集,而且可監聽的文件個數受限制(使用FD_SETSIZE宏來表示fd_set結構能夠容納的文件描述符的大數目)等。實際上,poll機制與select機制相比效率更高,使用范圍更廣。下面以poll()函數為例實現某種功能。
本實例中主要實現通過調用poll()函數來監聽三個終端的輸入(分別重定向到兩個管道文件的虛擬終端及主程序所運行的虛擬終端)并分別進行相應的處理。在這里我們建立了一個poll()函數監視的讀文件描述符集,其中包含三個文件描述符,分別為標準輸入文件描述符和兩個管道文件描述符。通過監視主程序的虛擬終端標準輸入來實現程序的控制(如程序結束);以兩個管道作為數據輸入,主程序將從兩個管道讀取的輸入字符串寫入到標準輸出文件(屏幕)。
為了充分表現poll()函數的功能,在運行主程序時,需要打開3個虛擬終端:首先用mknod命令創建兩個管道in1和in2。接下來,在兩個虛擬終端上分別運行cat>in1和cat>in2。同時在第三個虛擬終端上運行主程序。
在程序運行后,如果在兩個管道終端上輸入字符串,則可以觀察到同樣的內容將在主程序的虛擬終端上逐行顯示。
如果想結束主程序,只要在主程序的虛擬終端下輸入以“q”或“Q”字符開頭的字符串即可。如果三個文件一直在無輸入狀態中,則主程序一直處于阻塞狀態。為了防止無限期的阻塞,在程序中設置超時值(本實例中設置為60s),當無輸入狀態持續到超時值時,主程序主動結束運行并退出。該程序的流程圖如圖2.3所示。
 圖2.3 多路復用實例流程圖
/* multiplex_poll.c */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <poll.h>
#define MAX_BUFFER_SIZE 1024 /* 緩沖區大小 */
#define IN_FILES 3 /* 多路復用輸入文件數目 */
#define TIME_DELAY 60000 /* 超時時間秒數:60s */
#define MAX(a, b) ((a > b)?(a):(b))
int main(void)
{
struct pollfd fds[IN_FILES];
char buf[MAX_BUFFER_SIZE];
int i, res, real_read, maxfd;
/* 首先按一定的權限打開兩個源文件 */
fds[0].fd = 0;
if((fds[1].fd = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in1 error\n");
return 1;
}
if((fds[2].fd = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in2 error\n");
return 1;
}
/* 取出兩個文件描述符中的較大者 */
for (i = 0; i < IN_FILES; i++)
{
fds[i].events = POLLIN;
}
/* 循環測試是否存在正在監聽的文件描述符 */
while(fds[0].events || fds[1].events || fds[2].events)
{
if (poll(fds, IN_FILES, 0) < 0)
{
printf("Poll error or Time out\n");
return 1;
}
for (i = 0; i< IN_FILES; i++)
{
if (fds[i].revents) /* 判斷在哪個文件上發生了事件 */
{
memset(buf, 0, MAX_BUFFER_SIZE);
real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
{
return 1; /* 系統錯誤,結束運行 */
}
}
else if (!real_read)
{
close(fds[i].fd);
fds[i].events = 0; /* 取消對該文件的監聽 */
}
else
{
if (i == 0) /* 如果在標準輸入上有數據輸入時 */
{
if ((buf[0] == 'q') || (buf[0] == 'Q'))
{
return 1; /* 輸入“q”或“Q”則會退出 */
}
}
else
{ /* 將讀取的數據先傳送到終端上 */
buf[real_read] = '\0';
printf("%s", buf);
}
} /* end of if real_read*/
} /* end of if revents */
} /* end of for */
} /*end of while */
exit(0);
}
讀者可以將以上程序交叉編譯,并下載到開發板上運行,以下是運行結果:
$ mknod in1 p
$ mknod in2 p
$ cat > in1 /* 在第一個虛擬終端 */
SELECT CALL
TEST PROGRAMME
END
$ cat > in2 /* 在第二個虛擬終端 */
select call
test programme
end
$ ./multiplex_select /* 在第三個虛擬終端 */
SELECT CALL /* 管道1的輸入數據 */
select call /* 管道2的輸入數據 */
TEST PROGRAMME /* 管道1的輸入數據 */
test programme /* 管道2的輸入數據 */
END /* 管道1的輸入數據 */
end /* 管道2的輸入數據 */
q /* 在第三個終端上輸入“q”或“Q”則立刻結束程序運行 */
程序的超時結束結果如下:
$ ./multiplex_select
…(在60s之內沒有任何監聽文件的輸入)
Poll error or Time out
本文選自華清遠見嵌入式培訓教材《從實踐中學嵌入式Linux應用程序開發》
熱點鏈接:
1、linux 文件鎖的實現及其應用
2、底層文件I/O操作的系統調用
3、Linux中的文件及文件描述符
4、Linux文件系統之虛擬文件系統(VFS)
5、Linux系統調用及用戶編程接口(API)
更多新聞>> |