轉載:http://blog.csdn.net/michael_kong_nju/article/details/44887411
I/O復用技術
本文將討論網絡編程中的高級I/O復用技術,將從下面幾個方面進行展開:
a. 什么是復用技術呢?
b. 什么情況下需要使用復用技術呢?
c. I/O的復用技術的工作原理是什么?
d. select, poll and epoll的實現機制,以及他們之間的區別。
下面我們以一個背景問題來開始:
包括在以前的文章中我們討論的案例都是阻塞式的I/O包括(fgetc/getc, fgets/gets),即當輸入條件未滿足時進程會阻塞直到滿足之后進行讀取,但是這樣導致的一個
問題是如果此時進程還有別的I/O信息需要讀取那么這些信息將會被進程忽略掉。如何解決這個問題呢??
可能這么說還是有點抽象,我們針對之前的回射程序舉個例子。程序在:http://blog.csdn.net/michael_kong_nju/article/details/43457393
在正常連接的情況下,客戶端阻塞于fgets函數,此時如果服務器終止我們發現客戶端沒有得到消息,仍然阻塞:
這個時候看下網絡狀態,發現服務器已經結束,而客戶端處于CLOSE_WAIT狀態。
而客戶端此時得不到這個FIN的消息,一直阻塞。而這是我們不希望看到,那么這個問題該怎么解決呢?
可以從下面幾個條件來考慮:
1.使用多進程或者多線程,讓不同的線程或者進程阻塞在不同的描述符上。
但是這種方法會造成程序的復雜,而且進程和線程也需要OS資源的消耗,如果訪問請求過大的話,那么很可能造成服務器的崩潰。Apache服務器是用的子進程的方式,
其中的優點是在于不同的線程服務于不同的用戶可以隔離用戶。
2.用一個進程,但是使用的是非阻塞的I/O讀取數據,
當一個I/O不可讀的時候立刻返回,檢查下一個是否可讀,這種形式的循環為輪詢(polling),這種方法比較浪費CPU時間,因為大多數時間是不可讀,但是仍花費時間不斷反復執行read系統調用。
使用這種方法,進程不會阻塞,而是設置一個信號處理函數,當I/O條件滿足時由內核通知進程進行數據讀取。但是這也會有一個問題,如果請求很多的話,那么需要的信號也很多。
4. 使用異步I/O(asynchronous I/O)技術
和信號驅動式類似,異步I/O技術也是使用信號進行通知進程,但是不同的是這里只有一個階段,即當內核完成i/o操作之后會通知進程而不是就緒的時候。
關于2,3,4是另外的幾種高級I/O技術,我們將在后面的文章分別進行詳細的討論。
還有一種方法就是我們即將討論的I/O多路復用技術,下面先回答第一個問題
什么是復用技術呢?
I/O復用技術是一種預先告知內核此進程需要進行哪些I/O,并且當任何指定一個或多個I/O條件就緒時內核通知進程去進行處理的一種技術。他使得一個進程在不阻塞的
情況下處理多個描述符I/O.
針對上面的背景問題,我們可以回答我們開始的第二個疑問,
什么情況下需要使用復用技術呢?
(1)當客戶處理多個描述符時,即上面的這種情況,同時處理交互式輸入和網絡套接字。
(2)當客戶需要處理多個套接字時。
(3)當服務器需要處理多個套接字時,即并發服務器模型。
(4)當服務器需要同時處理TCP和UDP等不同的傳輸協議時也需要使用多路復用技術。
上面大概是幾種需要使用多路復用技術的場景,下面我們來討論復用技術的實現原理。回答開頭的第三個問題:
I/O的復用技術的工作原理是什么?
下面這幅圖是復用技術的工作模型,可以看到這里是使用select來實現的,當然也可以用epoll和poll來實現只是其中的具體細節不一樣罷了。
下面我們開始回答最后一個問題:
select, poll and epoll的實現機制,以及他們之間的區別。
select函數:
該函數準許進程指示內核等待多個事件中的任何一個發送,并只在有一個或多個事件發生或經歷一段指定的時間后才喚醒。函數原型如下:
#include <sys/select.h> #include <sys/time.h>int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就緒描述符的數目,超時返回0,出錯返回-1
函數參數介紹如下:
(1)第一個參數maxfdp1指定待測試的描述字個數,注意這里是個數是實際的最大的描述符加1,和數組下標類似從0到maxfdp 共maxfd + 1個,所以這里是maxfd plus 1. 描述字0、1、2...maxfdp1-1均將被測試。在linux中,頭文件<sys/select.h>定義了最大的描述符是1024,所以這里最大的maxfdp1也就是1025,在互聯網沒有快速發展的時候這個值可能已經很大了,但是在現在看來很容易就會實現這么大的并發,所以select在很多條件下已經不能滿足服務器的要求了,所以出現了epoll這種無限制的機制。
(2)中間的三個參數readset、writeset和exceptset指定我們要讓內核測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指針。struct fd_set可以理解為一個集合,這個集合中存放的是文件描述符,可通過以下四個宏進行設置:
????????? void FD_ZERO(fd_set *fdset);?????????? //清空集合 ? 例如: fd_set rset;?FD_ZERO(&set) ; 初始化,將所有位置0
????????? void FD_SET(int fd, fd_set *fdset);?? //將一個給定的文件描述符加入集合之中 ??FD_SET(1, &rset); 1bit開啟。
????????? void FD_CLR(int fd, fd_set *fdset);?? //將一個給定的文件描述符從集合中刪除
????????? int FD_ISSET(int fd, fd_set *fdset);?? // 檢查集合中指定的文件描述符是否可以讀寫。
在select中集合是使用整數數組實現的,數組中的每一個位都是一個int可以表示32bit,即fd_set[0]可以用來表示0-31號描述符,下面依次。
(3)timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用于指定這段時間的秒數和微秒數。
???????? struct timeval{
?????????????????? long tv_sec;?? //seconds
?????????????????? long tv_usec;? //microseconds
?????? };
這個參數有三種可能:
(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。為此,把該參數設置為空指針NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
(3)根本不等待:檢查描述字后立即返回,這稱為輪詢。為此,該參數必須指向一個timeval結構,而且其中的定時器值必須為0。
下面看看描述有哪些就緒條件;
準備好讀:
1,套接字接收緩沖區的數據字節數大于等于,套接字接收緩沖區低水位線,可以用SO_RCVLOWAT套接選項來設置低水位線,對于TCP和UDP套按字,默認值為1
2,該連接的讀半部分關閉(接收到了FIN的TCP連接).對這樣的套接字讀操作,返回0(EOF)
3,該套接字是一個監聽套接字且已經完成的連接數不為0.對這樣的套按字的accept通常不會阻塞
4,其上有一個套接字錯誤街處理.對這樣的套按字的讀操作將不阻塞并返回-1(錯誤),同時把errno設置成錯誤條件,這些待處理錯誤也可以通過指定SO_ERROR套接字選項調用getsockopt獲取.
準備好寫:
1,該套接字發送緩沖區的可用字節數大于等于套接字發送緩沖區低水位線的當前大小.并且或者該套接已經連接,或者套按字不需要連接(UDP),如果我們把這套接字設置成非阻塞,寫操作將不阻塞并返回一個正值.可以使用SO_SNDLOWAT設置一個該套接字的低水位標記.對于TCP和UDP默認值通常為2048.
2,該連接的寫半部關閉.對這樣的套接寫的寫操作將產生SIGPIPE信號.
3使用非阻塞式的connect的套按字已經建立連接,或者connect已經失敗.
4,其上有一個套接字錯誤等處理,對這樣的套接字進行寫操作會返回-,且,把ERROR設置成錯誤條件,可以通過指定SO_ERROR套按選項調用getsockopt獲取并清除.
上面都是理論的知識,我們現在來看一個例子,我們用select重寫http://blog.csdn.net/michael_kong_nju/article/details/43457393?中的echo_tcp_client.c中的str_cli函數:
限于篇幅的原因這里不給出客戶端的main函數,但是我們建議你去我的github中下載這個我已經展開過的源碼去調試運行一下:
https://github.com/michaelnju/UNPV-Relaxing-Code/blob/master/Chaper6_Select_Test/select_echo_tcp_cli.c
服務器程序還是用上一個連接中的。
這時候你會看到在客戶端和服務器正常連接的過程中,如果這時候服務器斷開了,那么客戶端會立馬被告知,而不像我們剛開始的時候那樣會阻塞。
所以我們看到了select的作用。下篇文章我們將看到select在并發服務器中的作用。
完整的客戶端代碼:
//#include "unp.h" | |
? | /* |
? | Lingtao relax this code in 2015 |
? | */ |
? | #include <stdio.h> |
? | #include <sys/types.h> |
? | #include <sys/socket.h> |
? | #include <netinet/in.h> |
? | #include <sys/select.h> |
? | #include <sys/time.h> |
? | ? |
? | #define LISTENQ 5 |
? | #define MAXLINE 2048 |
? | #define SERV_PORT 9877 |
? | #define max(a,b) ((a) > (b) ? (a) : (b)) |
? | ? |
? | typedef struct sockaddr SA; |
? | ? |
? | void |
? | str_cli(FILE *fp,int sockfd) |
? | { |
? | int maxfdp1; |
? | fd_set rset; |
? | char sendline[MAXLINE], recvline[MAXLINE]; |
? | ? |
? | FD_ZERO(&rset); |
? | for ( ; ; ) { |
? | FD_SET(fileno(fp), &rset); |
? | FD_SET(sockfd, &rset); |
? | maxfdp1 = max(fileno(fp), sockfd) +1; |
? | select(maxfdp1, &rset, NULL, NULL, NULL); |
? | ? |
? | if (FD_ISSET(sockfd, &rset)) {/* socket is readable*/ |
? | if (read(sockfd, recvline, MAXLINE) ==0) |
? | { |
? | perror("str_cli: server terminated prematurely"); |
? | exit(1); |
? | } |
? | fputs(recvline, stdout); |
? | } |
? | ? |
? | if (FD_ISSET(fileno(fp), &rset)) {/* input is readable*/ |
? | if (fgets(sendline, MAXLINE, fp) ==NULL) |
? | return; /* all done */ |
? | write(sockfd, sendline, strlen(sendline)); |
? | } |
? | } |
? | } |
? | ? |
? | int |
? | main(int argc,char **argv) |
? | { |
? | int sockfd; |
? | struct sockaddr_in servaddr; |
? | ? |
? | if (argc != 2) |
? | { |
? | perror("usage: tcpcli <IPaddress>"); |
? | exit(1); |
? | } |
? | sockfd = socket(AF_INET, SOCK_STREAM,0); |
? | ? |
? | bzero(&servaddr, sizeof(servaddr)); |
? | servaddr.sin_family = AF_INET; |
? | servaddr.sin_port = htons(SERV_PORT); |
? | inet_pton(AF_INET, argv[1], &servaddr.sin_addr); |
? | ? |
? | connect(sockfd, (SA *) &servaddr,sizeof(servaddr)); |
? | str_cli(stdin, sockfd); /* do it all */ |
? | ? |
? | exit(0); |
? | } |