第六章筆記
1. IO複用:
(1)需要IO複用的原因:
echo客戶端同時需要讀終端和套接字會遇到這樣的問題:如果客戶端面向兩個文件描述符,控制和連接套接字,那麼就不能同時收到終端和套接字的數據,如果客戶端阻塞在終端,那即使服務器發來了close也不能立即處理。
這樣的進程需要一種預先告知內核的能力,使得內核一旦發現進程指定的一個或多個IO條件就緒(也就是說輸入已準備好被讀取,或者描述符已能承擔更多的輸入),它就通知進程。這個能力成爲IO複用。
(2)IO複用的特點:
使用IO複用,使進程不再阻塞在真正的IO系統調用上,而是阻塞在select poll epoll這其中某一個系統調用之上,而它們的優勢在於它們可以幫助我們同時等待多個描述符就緒。
(3)IO複用適用的場景:
- 客戶端處理多種描述符(交互性輸入和網絡套接字)必須使用IO複用
- 客戶端需要同時處理多個套接字
- 服務器既要處理監聽套接字又要處理已連接套接字,一般就要使用IO複用
2. IO模型
IO複用是IO模型的一種,共有五種IO模型:阻塞IO、非阻塞IO、IO複用、信號驅動式IO、異步IO
以套接字recvfrom爲例:recvfrom操作包括兩個階段(1)等待內核緩衝區的數據是否準備好。(2)從內核向進程複製數據。而這幾種IO模型的主要區別就在於當內核緩衝區的數據已經準備好之後以何方方式通知用戶進程
2.1 阻塞IO
默認情況下,所有套接字都是阻塞的。這種模型表現爲:如果數據沒有準備好,調用recvfrom的當前線程進入阻塞狀態,直到數據準備好並複製到應用程序緩衝區才返回。它的缺點是:當應用程序遇到多個文件描述符時,前面的文件描述符會阻塞後面的文件描述符,即:即使有某個文件描述符可讀,也要等到前面的文件描述符變成可讀才能處理。
2.2 多線程/多進程+阻塞IO
每個線程處理一個IO,這樣即使有多個文件描述符也可同時處理。這種方式可以處理上面那種非阻塞IO不能有效處理多個文件描述的情況。《unix網絡編程筆記(三)》服務器代碼就是這種模型。但缺點是:當需要成千上萬設置更多文件描述符,多進程非常耗費資源。多線程的切換會影響性能。
2.3 非阻塞IO
如果數據沒有準備好,調用recvfrom會返回錯誤,使用返回值告訴應用程序數據是否可讀且已經複製到應用程序緩衝區中,因此應用程序需要不斷輪詢查看數據是否準備好。缺點爲:輪詢往往耗費大量CPU時間
2.4 IO複用
使進程不再阻塞在真正的IO系統調用上,而是阻塞在select poll epoll這其中某一個系統調用之上,雖然與阻塞IO一樣如果數據沒有準備好還是阻塞,但是其優勢爲可以同時等待多個文件描述符,只要有任何一個準備好就返回。之後應用進程調用read讀取數據。特點:當同時處理大量描述符時性能由於多線程+阻塞IO。
2.5 信號驅動式IO
使用信號SIGIO來通知應用程序數據已準備好,用戶需要捕獲信號並在信號處理程序中讀取數據
2.5 異步IO
支持POSIX異步IO模型的系統較罕見。工作機制爲:告知內核啓動某個操作,並讓內核在整個操作(包括將數據從內核複製到我們自己的緩衝區)完成後通知我們。與信號驅動模型的主要區別是:信號驅動IO是由內核通知我們何時可以啓動一個IO操作,而異步IO模型是由內核通知我們IO操作何時完成,即通知我們時數據已經複製到應用程序緩衝區內了。
2. 6 總結:
阻塞IO、非阻塞IO、IO複用、信號驅動式IO前四種IO模型有一個共同點:對於recvfrom的第二個階段即把數據從內核緩衝區複製到調用緩衝區期間,進程都阻塞與recvfrom調用。即都有阻塞。而異步IO模型複製操作由異步IO函數完成,不需要因爲調用recvfrom而阻塞。即:全程沒有阻塞,異步IO參與了recvfrom的兩個階段。
3. select 函數
linux支持三種IO複用,select poll epoll
int select(int maxfdpl,fd_set* readset,fd_set* writeset,fd_set* exceptset,const struct timeval* timeout);
3.1 參數解釋:
- timeout:NULL,永遠等待下去;正常時間:等待一段固定時間; 0:檢查描述符後立即返回,可用於輪詢查看是否有描述符可讀
- readset writeset exceptset:是三個文件描述符集,用於存儲檢查用的文件描述符。可使用函數
FD_ZERO、FD_SET、FD_CLR、FD_ISSET
來清空文件描述符集、添加描述符、刪除描述符、判斷描述符是否位於文件描述符集中。也可用賦值語句將某個描述符集的值賦值給另一個描述符集。注意:在設置描述集時,必須給描述集初始化,否則可能發生不可預期的結果。
readset writeset exceptset中,如果我們對其中某一個條件不感興趣,就可以把它設爲空指針。這三個參數爲輸入輸出型參數。輸入時表示檢查哪些文件描述符,當函數返回時,指示哪些描述符已就緒,可用FD_ISSET來測試文件描述符集中哪些描述符已就緒。因爲未就緒的文件描述符都被清爲0,所以再次調用select時需要再次把所有關心的描述符置爲1。
select檢測是否就緒的描述符不侷限於套接字,任何描述符都可以使用select - maxfdp1: 指定待測試的描述符個數,它的值是待測試的最大描述符+1,即調用select時描述符0 1 2 …maxfdp1-1都將檢查。因爲readset writeset exceptset文件描述符集最多存放的描述符個數爲FD_SETSIZE,所以maxfdp1不能大於FD_SETSIZE。FD_SETSIZE定義在/usr/include/sys/select.h中。
- 返回值:表示所有描述符已就緒的總位。如果爲0表示定時器到時無描述符就緒。爲-1表示出錯,如果錯誤爲EINTR,忽略該錯誤。注意:當某個描述符即準備好讀又準備好寫,則返回次數爲2。
select會被信號中斷,並從信號處理函數返回。需要做好返回EINTR的準備
3.2 使用select常見的兩個錯誤:
maxfdp1爲檢測的最大描述值+1,不要忘加1;描述符集是輸入輸出參數再次調用select時忘記再次設置描述符。
3.3 select返回的條件:
(1)select返回的條件爲:等待的文件描述符有一個或多個就緒,或者指定的時間到達。
(2)套接字讀描述符就緒的條件:
- 該套接字接收緩衝區的數據字節數>=套接字接收緩衝區低水位標記,可以使用SO_RCVLOWAT套接字選項設置套接字低水位標記。對於Tcp和Udp套接字,默認值爲1。
- 該連接的讀半部關閉(接收到了FIN),這時read返回0。
- 捕獲了某個信號且從響應信號處理函數返回時,read中斷
- 其他錯誤 3)對於監聽套接字,已完成連接數>0
(3)套接字寫描述符就緒的條件:
- 該套接字發送緩衝區的可用空間字節數>=套接字發送緩衝區低水位標記,可以使用SO_SNDLOWAT套接字選項設置套接字低水位標記。對於Tcp和Udp套接字,默認值爲2048。注意:對於Udp並沒有發送緩衝區,因爲不像Tcp一樣因爲超時重傳而爲應用程序傳遞給它的數據報保留副本。只有發送緩衝區大小這一屬性,所以只要Udp套接字的發送緩衝區大小大於該套接字的低水位標記(默認應該總是這種關係),該Upd套接字就總是可寫,即Udp套接字總是可寫的 7.5.9)
- 連接的寫半部關閉(接收了RST)再次write,這時返回-1併產生SIGPIPE信號
- 捕獲了某個信號且從響應信號處理函數返回時,read中斷
- 其他錯誤
- 使用非阻塞式的connect已建立連接或者connect已經以失敗告終
4. 使用select實現echo客戶端
4.1 使用select實現echo客戶端注意兩個問題
(1)不能在讀輸入讀到eof時就認爲程序處理完畢
問題描述:
當實現echo客戶端,如果從終端讀數據,當讀數據讀到eof時,我們認爲數據傳送完畢close套接字是沒有問題的。但如果把從終端讀數據換成從文件讀數據,當我們read到eof後close套接字就會發現從服務器傳回來的數據少於我們發送的數據。
問題原因:
這是因爲從終端讀數據,發送數據和讀取數據是停等模式的,我們只有等待對端發來了數據才向對端發送數據,所以當從終端輸入了eof時我們可以確認之前發送對端的數據對端已經全部收到,而且對端發來的數據我們也全部收到。但把終端換成文件,那麼向服務器寫完一行數據後會立即寫一行,兩個寫動作之間幾乎沒有時間延遲,這樣當讀文件讀到了eof,可能連接通道中還有數據連接通道中可能還有其他的請求和應答,或者是去往服務器的請求數據,或者是返回客戶端的應答數據。
也就是說當從文件讀到了eof,只意味着我們已經寫完了數據,但我們並不知道全部數據有沒有全部發往對端,或者對端是否還有數據發給我們。
解決方案:我們需要一種只關閉Tcp寫的方式,也就是說我們告訴服務器我們已經完成了數據發送,但還可以繼續接受對端發送的數據,只有讀到對端發來了eof才認爲數據傳送結束。而因爲本端發送的eof和對端發送的eof是本端和對端最後發送的消息,如果兩端都收到了對端發送的eof,則可以保證兩端之前發送的消息肯定都收到了。這可以調用shutdown完成。
(2)不要讓select和stdio和其他帶有緩衝區的函數混合使用
**原因:**stdio具有自己的緩衝區,雖然fgets只返回一行,但是其他輸入行的數據仍然在stdio緩衝區中。這樣再次調用select等待新的輸入時,由於select不知道stdio的緩衝區數據,它只單純從read系統調用的角度判斷是否有數據可讀,而不是從fget角度考慮。這樣可能就會產生雖然fgets能讀到數據,但read卻發現數據已經讀完,而select返回後發現無數據可讀的問題。
4.2 shutdown
int shutdown(int sockfd,int howto);
shutdown與close的區別:close只有當文件描述符引用計數爲0才發送FIN,而shutdown不管引用計數就發送FIN。close終止讀和寫,而shutdown可用於只終止讀半步或寫半步。當告知對端我們已經完成了數據發送,但還可以讀取對端發送的數據,可讓howto設置爲SHUT_WR。調用shutdown(fd,SHUT_WR)後,當前留在套接字發送緩衝區中的數據將被髮送掉,後跟Tcp的FIN終止序列。
4.3 select echo客戶端代碼
#include "unp.h"
void str_cli(FILE* fp,int sockfd);
int main(int argc,char* argv[])
{
if(argc != 3)
{
err_quit("usage: ip port\n");
}
int sockfd = Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
Inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
servaddr.sin_port = htons(atoi(argv[2]));
Connect(sockfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
str_cli(stdin,sockfd);
exit(0);
}
void str_cli(FILE* fp,int sockfd)
{
int fd = fileno(fp);
char buf[MAXLINE] = {0};
fd_set rds;
FD_ZERO(&rds);
int eof = 0;
int maxfds = -1;
ssize_t size = 0;
for(;;)
{
FD_SET(sockfd,&rds);
if(!eof)
{
FD_SET(fd,&rds);
maxfds = (fd > sockfd ? fd : sockfd) + 1;
}
else
{
maxfds = sockfd + 1;
}
Select(maxfds,&rds,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&rds))
{
if( (size = Read(sockfd,buf,MAXLINE)) == 0)
{
if(eof)
{
break;
}
else
{
err_quit("str_cli:server terminate prematurely");
}
}
else
{
Write(fd,buf,size);
}
}
else if(FD_ISSET(fd,&rds))
{
if( (size = Read(fd,buf,MAXLINE)) == 0)
{
eof = 1;
Shutdown(sockfd,SHUT_WR);
FD_CLR(fd,&rds); //不要忘記清除fd
}
else
{
writen(sockfd,buf,size);
}
}
}
}
(1)select只能檢測文件描述符,fileno用於把標準IO文件指針轉換爲對應的描述符
(2)使用eof標記是否標準輸入了eof,當eof爲1時不再把它放入文件描述符集中
5. 使用select實現echo客戶端
#include "unp.h"
int main(int argc,char* argv[])
{
if(argc != 3)
{
err_quit("usage:port listen_backlog");
}
int listenfd = Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(atoi(argv[1]));
Bind(listenfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
Listen(listenfd,atoi(argv[2]));
char buf[MAXLINE] = {0};
int clientfd[FD_SETSIZE];
for(int i = 0;i < FD_SETSIZE; i++)
{
clientfd[i] = -1;
}
int maxfd = listenfd;
int maxi = -1;
ssize_t size;
fd_set rds,allds;
FD_ZERO(&allds);
FD_SET(listenfd,&allds);
for(;;)
{
rds = allds;
int n = select(maxfd + 1,&rds,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&rds))
{
struct sockaddr_in clientaddr;
socklen_t addrlen;
int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&addrlen);
int i;
for(i = 0;i<FD_SETSIZE;i++)
{
if(clientfd[i] < 0)
{
clientfd[i] = connfd;
break;
}
}
if( i >= FD_SETSIZE)
{
err_msg("too many client");
continue;
}
FD_SET(connfd,&allds);
if(maxfd < clientfd[i])
maxfd = clientfd[i];
if(maxi < i)
maxi = i;
n--;
if(n <= 0)
continue;
}
for(int i = 0; i<= maxi;i++)
{
if(clientfd[i] < 0)
continue;
if(FD_ISSET(clientfd[i],&rds))
{
ssize_t size = Read(clientfd[i],buf,MAXLINE);
if(size == 0)
{
close(clientfd[i]);
FD_CLR(clientfd[i],&allds);
clientfd[i] = -1;
}
else if(size > 0)
{
writen(clientfd[i],buf,size);
}
n--;
if(n == 0)
break;
}
}
}
}
使用clientfd數組來存儲已連接的客戶端fd,使用maxi標記clientfd數組中有效fd最小index,這樣select後,就不用從0到maxfd開始遍歷,而是從0到maxi遍歷,減少遍歷時間。
6. 總結:
(1)select文件描述符集:
- 使用函數
FD_ZERO、FD_SET、FD_CLR、FD_ISSET
來清空文件描述符集、添加描述符、刪除描述符、判斷描述符是否位於文件描述符集中。 - 可用賦值語句將某個描述符集的值賦值給另一個描述符集。
- 在設置描述集時,必須給描述集初始化,否則可能發生不可預期的結果。
- 如果我們對其中某一個條件不感興趣,就可以把它設爲空指針。
- readset writeset exceptset是輸入輸出參數,當select返回時描述符集變成就緒的文件描述符,當再次調用select時不要忘記再次設置需要檢測的描述符。
- select檢測是否就緒的描述符不侷限於套接字,任何描述符都可以使用select
(2)maxfd:
- maxfd:表示select檢測的最大描述值+1,調用select時會檢測即調用select時描述符0 1 2 …maxfdp1-1都將檢查。不要忘加1。
- maxfdp1不能大於FD_SETSIZE
(3) select會被信號中斷,需要做好返回EINTR的準備
(4) 套接字讀描述符就緒條件之一:
該套接字接收緩衝區的數據字節數>=套接字接收緩衝區低水位標記,可以使用SO_RCVLOWAT套接字選項設置套接字低水位標記。對於Tcp和Udp套接字,默認值爲1。
(5)套接字寫描述符就緒的條件之一:
該套接字發送緩衝區的可用空間字節數>=套接字發送緩衝區低水位標記,可以使用SO_SNDLOWAT套接字選項設置套接字低水位標記。對於Tcp和Udp套接字,默認值爲2048。
(6)使用select實現echo客戶端注意兩個問題:
- 不能在讀輸入讀到eof時就認爲程序處理完畢
- 不要讓select和stdio和其他帶有緩衝區的函數混合使用
(7)fileno用於把標準IO文件指針轉換爲對應的描述符