《網絡應用程序設計》課件第7章 阻塞式非阻塞式_第1頁
《網絡應用程序設計》課件第7章 阻塞式非阻塞式_第2頁
《網絡應用程序設計》課件第7章 阻塞式非阻塞式_第3頁
《網絡應用程序設計》課件第7章 阻塞式非阻塞式_第4頁
《網絡應用程序設計》課件第7章 阻塞式非阻塞式_第5頁
已閱讀5頁,還剩147頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第7章阻塞式/非阻塞式I/O

7.1I/O模

型7.2阻塞函數的編程7.3非阻塞函數的編程7.4信號驅動I/O7.5本章小結習題7.1I/O模型

7.1.1產生阻塞的原因 產生阻塞的原因是操作系統的進程結構和進程調度方式。通過前面章節的講述,我們知道,在Linux系統中,一個進程對應于系統進程向量表里某個指針所指的一個task_struct結構。這個結構表明了進程的運行狀態、進程占用CPU的時間、計時器的數值等信息,其中運行狀態包括以下幾種:

(1)運行態:進程正在運行,或者準備運行。 (2)等待態:進程在等待一個事件的發生或某種系統資源。這種狀態又分為兩個類型:可中斷型和不可中斷型。可中斷型等待進程可以被某一信號中斷,而不可中斷型等待進程將一直等待硬件狀態的改變。

(3)停止態:進程已經被停止,如正被調試的程序。

(4)死亡態:進程已經被終止,釋放掉了曾經占用的程序、數據及堆棧空間,只是還在進程向量表中占有一個task_struct結構。 Linux系統使用時間片的調度算法,每個當前進程在占用CPU一個時間片后就被掛起,另一個處于運行隊列的進程將占用CPU。如果當前進程需要等待其他的系統資源,而這個進程又不是非阻塞類型進程時,則它將轉入等待狀態;如果這個進程是非阻塞進程,則它將仍處于運行態。處在等待隊列的進程,如果發生信號中斷或硬件狀態改變,則這個進程將轉變為可運行狀態。 一個包含阻塞式套接字函數的進程被啟動后,它將處于可運行狀態,在成為當前進程時,如果調用了connect()、read()、write()等函數,進程需要等待足夠的緩存區或通信對方的響應,這些要求常常不能立刻得到滿足,于是進程轉換到等待狀態,產生阻塞。這種阻塞一直持續到函數需求得到滿足、得到通信對方的響應、被信號中斷或發生系統故障。 7.1.2產生阻塞的函數 進程總是一段時間運行于用戶方式下,另一段時間運行于內核方式下,這種切轉換是通過系統調用完成的。使用套接字的用戶進程是在調用了套接字函數,進入到內核運行狀態之后被阻塞的,那么具體有哪些套接字函數會產生阻塞呢?我們可以通過對Linux系統內核的網絡接口的層次結構和源代碼進行分析來找出這些函數。Linux的內核源代碼可以在網站,一些支持Linux系統開發的公司網站,或者支持開放源代碼的網站上下載。本節引用的源代碼來自于網站http://lxr.linux.no/,內核版本為Linux2.4.20,i386體系。

Linux的網絡接口可以分為四個層次:網絡設備層、網絡核心層、網絡協議層以及網絡應用接口(socket)層,如圖7-1所示。網絡接口中的網絡設備層主要負責從物理介質接收和發送數據,實現的文件在linux/driver/net目錄下。網絡核心層則是整個網絡接口的核心部分,它為網絡協議提供統一的接收、發送接口,屏蔽各種各樣的物理介質,同時也負責把來自下層的數據包向合適的協議配送,它的主要實現文件在linux/net/core目錄下。網絡協議層是各種具體協議實現的地方,Linux支持的協議很多,包括TCP/IP、ATM、AppleTalk、IPX、Bluetooth和X.25等,各種具體協議實現的源碼在linux/net/目錄下有相應的名稱,如linux/net/ipv4和linux/net/atm。網絡應用接口層是為用戶提供網絡服務的編程接口,也是我們分析研究網絡應用程序的主要考慮對象,socket主要的源碼在linux/net/ socket.c目錄下,主要的頭文件是linux/net/socket.h。

圖7-1網絡接口內核程序的層次結構示意圖 socket函數中能夠產生阻塞的有四類:

(1)數據發送:包括sendmsg()、sendto()、send()、write()和writev()。

(2)數據接收:包括revvmsg()、recvfrom()、recv()、read()和readv()。

(3)建立連接:connect()。

(4)接受連接:accept()。 這些函數有些是套接字所特有的,如sendto()和connect()等,有些是通用的文件操作函數,如write()和read()。在內核代碼socket.c中,套接字特有的函數對應的系統調用函數名稱是在套接字函數名稱前面加sys_,即存在表7-1的對應關系。表7-1套接字函數與內核函數名稱的對應關系

通用的文件操作函數在socket.c中也通過以下一個結構,被映射為套接字的操作函數,源代碼如下:通用的文件操作函數在socket.c中也通過以下一個結構,被映射為套接字的操作函數,源代碼如下:

114 staticstructfile_operations

socket_file_ops={

115

llseek: no_llseek,

116

read: sock_read,

117

write: sock_write,

118

poll: sock_poll,

119

ioctl: sock_ioctl,

120

mmap: sock_mmap,

121

open: sock_no_open,

122

release: sock_close,

123

fasync: sock_fasync,

124 readv: sock_readv,

125 writev: sock_writev,

126 sendpage: sock_sendpage

127 };

當我們在用戶程序中調用read()函數時,內核就調用sock_read()函數;當我們在用戶程序中調用sendto()函數時,內核就調用sys_sendto()函數。其他函數的調用情況可依此類推。 現在來看sys_sendmsg()函數,它的部分源代碼如下:

1343asmlinkagelongsys_sendmsg(intfd,

structmsghdr

*msg,

unsignedflags) 1344{ 1345structsocket

*sock; 1346charaddress[MAX_SOCK_ADDR];

120

mmap: sock_mmap,

121

open: sock_no_open

122

release: sock_close,

123

fasync: sock_fasync,

124 readv: sock_readv,

125 writev: sock_writev,

126 sendpage: sock_sendpage

127

};

當我們在用戶程序中調用read()函數時,內核就調用sock_read()函數;當我們在用戶程序中調用sendto()函數時,內核就調用sys_sendto()函數。其他函數的調用情況可依此類推。 現在來看sys_sendmsg()函數,它的部分源代碼如下:

1343asmlinkagelongsys_sendmsg(intfd,

structmsghdr

*msg,

unsignedflags) 1344{ 1345structsocket

*sock; 1346charaddress[MAX_SOCK_ADDR]; 1347structioveciovstack[UIO_FASTIOV],

*iov=iovstack; 1348unsignedcharctl[sizeof(structcmsghdr)+20]; 1349unsignedchar*ctl_buf=ctl; 1350structmsghdrmsg_sys; 1351interr,

ctl_len,

iov_size,

total_len;

… /*

一系列合法性檢查、數據拷貝等工作

*/

/*

檢查文件(socket)描述符否有非阻塞標志,如果有,則將欲發送信息包的標志設為

MSG_DONTWAIT類型

*/ 1399msg_sys.msg_flags=flags; 14001401if(sock->file->f_fla gs&O_NONBLOCK) 1402msg_sys.msg_flags|=MSG_DONTWAIT; 1403err=sock_sendmsg(sock,&msg_sys,total_len);/*

調用sock_sendmsg函數發出信息包

/* 1404…/*

一系列錯誤狀態輸出工作

*/ 1413 out: 1414returnerr; 1415 }

可以發現這個函數是在檢查了當前套接字是否為非阻塞型套接字后,調用sock_sendmsg函數,并完成數據發送工作的。

再來看sys_sendto()函數的源代碼:

1177

asmlinkagelongsys_sendto(intfd,

void*buff,

size_t

len,

unsignedflags,

1178structsockaddr

*addr,

intaddr_len)

1179 {

1180structsocket

*sock;

1181charaddress[MAX_SOCK_ADDR];

1182interr;

1183structmsghdr

msg;

1184structioveciov;

1185

sock=sockfd _lookup(fd,

&err); /*

檢查fd是否一個正確的套接字描述符,

并獲取相關信息

*/

1187if(!sock)

1188gotoout;

1189iov.iov_base=buff; /*

設置緩存區首地址

*/

1190iov.iov_len=len; /*

設置待發送的數據塊長度

*/

1191

msg.msg_name=NULL;

1192

msg.msg_iov=&iov; /*

將緩存區首地址賦給msg結構

*/

1193

msg.msg_iovlen=1; /*

只發送一個數組

*/

1194

msg.msg_control=NULL;

1195

msg.msg_controllen=0;

1196

msg.msg_namelen=0;

1197if(addr)

1198{

1199

err=move_addr_to_kernel(addr,

addr_len,

address);

1200if(err<0)

1201gotoout_put;

1202

msg.msg_name=address;

1203

msg.msg_namelen=addr_len;

1204}

1205if(sock->file->f_flags&O_NONBLOCK) /*

檢查是否為非阻塞式套接字

*/

1206

flags|=MSG_DONTWAIT;

1207

msg.msg_flags=flags;

1208

err=sock_sendmsg(sock,

&msg,

len); /*

發出數據

*/

1209

1210 out_put:

1211

sockfd_put(sock);

1212

out:

1213returnerr;

1214 }

這段源代碼告訴我們,sys_sendto()函數也是通過調用sock_sendmsg()完成數據發送工作的。當我們繼續考察有關發送數據的套接字函數的源代碼時,就可以看到sys_send()函數調用了sys_sendto(),而sock_write()是通過調用sock_sendmsg()完成數據發送的。對于接收數據的套接字函數,我們通過類似的分析,同樣可以發現sys_recv()函數調用了sys_recvfrom(),而sys_recvfrom()、sys_recvmsg()、sock_read()都是通過調用sock_recvmsg()完成數據接收工作的。

在套接字源代碼中,還有一個用于收/發數據的函數sock_readv_writev(),該函數既調用了sock_sendmsg()也調用了sock_recvmsg(),在這個函數的基礎上,套接字內核實現了sock_writev()函數和sock_readv()函數。我們將套接字內核中幾個主要的數據收/發函數之間的調用關系在圖7-2中示出。從圖7-2中可以發現,所有的接收和發送數據函數歸根結底都是在執行sock_sendmsg()和sock_recvmsg(),而在調用這兩個函數之前,接收、發送數據函數無一例外地都檢查了文件標識符是否有O_NONBLOCK標志,如果有,則設置MSG_DONTWAIT標志,即以非阻塞方式發送或接收數據包。圖7-2套接字內核中主要數據收/發函數之間的調用關系

同樣,還可以在socket.c程序清單中找到sys_connect()函數和sys_accept()函數的源代碼,如下所示:

1105asmlinkagelongsys_connect(intfd,

structsockaddr*uservaddr,

intaddrlen) 1106 { 1107structsocket*sock; 1108charaddress[MAX_SOCK_ADDR]; 1109interr;

1110 1111sock=sockfd_lookup(fd,

&err); 1112if(!sock) 1113gotoout; 1114err=move_addr_to_kernel(uservaddr,

addrlen,

address); 1115if(err<0) 1116gotoout_put; 1117err=sock->ops->connect(sock,(structsockaddr*)address,addrlen, 1118sock->file->f_flags); 1119 out_put: 1120sockfd_put(sock); 1121 out: 1122returnerr;112 3

} /*

*/1046 elongsys_accept(intfd,

structsockaddr*upeer_sockaddr,

int*upeer_addrlen) 1047 { 1048structsocket*sock,

*newsock; 1049interr,

len; 1050charaddress[MAX_SOCK_ADDR]; 1051 1052sock=sockfd_lookup(fd,

&err); 1053if(!sock) 1054gotoout; 1055 1056err=-EMFILE; 1057if(!(newsock=sock_alloc())) 1058gotoout_put; 10591060newsock->type=sock->type;1061newsock->ops=sock->ops;10621063err=sock->ops->accept(sock,

newsock,

sock->file->f_flags);1064if(err<0)1065gotoout_release;1066

1074gotoout_release;1075}10761077/*Fileflagsarenotinheritedviaaccept()unlikeanotherOSes.*/10781079if((err=sock_map_fd(newsock))<0)1080gotoout_release;10811082 out_put:1083sockfd_put(sock);1084 out:1085returnerr;10861087 out_release:1088sock_release(newsock);1089gotoout_put;1090 }

從代碼中可以看到,connect()和accept()直接使用了文件描述符的O_NONBLOCK標志來決定進程是否為非阻塞型。這一點與收/發數據的函數稍有不同。 現在歸納一下用戶程序被阻塞的四種操作過程: (1)數據發送:如圖7-3所示,應用程序調用數據發送函數后,進程進入內核態運行,內核程序先做一系列初始化工作,包括合法性檢查、將通信對方的地址結構從用戶空間向內核空間拷貝等,若在此過程中出現錯誤,則退出內核狀態,切換到用戶態運行,并返回錯誤代碼;若初始化工作未出現錯誤,則內核運行sock_sendmsg()函數,阻塞進程直到將待發送的數據從用戶空間拷貝到套接字的發送緩存區,然后進程切換回用戶態,繼續應用程序的運行。圖7-3數據發送的阻塞操作

(2)數據接收:如圖7-4所示,應用程序調用數據接收函數后,進程進入內核態運行,內核程序先做一系列初始化工作,包括合法性檢查、將通信對方的地址結構從用戶空間向內核空間拷貝等,若在此過程中出現錯誤,則退出內核狀態,切換到用戶態運行,并返回錯誤代碼;若初始化工作未出現錯誤,則內核運行sock_recvmsg()函數,阻塞進程直到有數據包到達,然后接收到的數據被從套接字的接收緩存區拷貝到用戶空間,接下來進程切換回用戶態,繼續應用程序的運行。圖7-4數據接收的阻塞操作

(3)建立連接:如圖7-5所示,應用程序調用數據連接函數后,進程進入內核態運行,內核程序先做一系列初始化工作,包括合法性檢查、將通信對方的地址結構從用戶空間向內核空間拷貝等,若在此過程中出現錯誤,則退出內核狀態,切換到用戶態運行,并返回錯誤代碼;若初始化工作未出現錯誤,則內核運行sock->ops->connect()函數,阻塞進程直到三次握手操作結束,然后進程切換回用戶態,繼續應用程序的運行。圖7-5請求建立連接的阻塞操作

(4)接受連接:如圖7-6所示,應用程序調用接受連接函數后,進程進入內核態運行,內核程序先做一系列初始化工作,包括合法性檢查、將通信對方的地址結構從用戶空間向內核空間拷貝等,若在此過程中出現錯誤,則退出內核狀態,切換到用戶態運行,并返回錯誤代碼;若初始化工作未出現錯誤,則內核運行sock->ops->accept()函數,阻塞進程直到有效的客戶連接請求被接受,然后建立一個連接套接字,并將進程切換回用戶態,繼續應用程序的運行。圖7-6接受連接的阻塞操作

7.2阻塞函數的編程 7.2.1阻塞式I/O的客戶機編程

1.基本的阻塞式程序 當客戶機進程只訪問一個服務器進程,或訪問多個服務器進程但這些進程之間有順序關系時,選擇阻塞式I/O是一個合適的方案。例如,我們需要從一個英語論文庫中獲取一篇論文“ResearchontheHairofDinosaurian”,然后把它交給一個翻譯服務程序將這篇論文譯成漢語,最后得到一篇漢語論文“恐龍毛發的研究”。我們需要做的工作如下:

(1)確定服務器地址:首先我們要知道提供英語論文的進程地址和翻譯服務的進程地址,將其分別命名為:

ADDR_PROVIDER:PORT1;

ADDR_TRASLATOR:PORT2。

(2)確定通信協議:一般來說,服務器會采用通用的應用層通信協議,如http、ftp等,但也很有可能服務器會采用特殊的規約來進行信息傳輸,那么客戶機就必須以符合這種規約的數據格式與服務器進行交互。假設我們遇到的這兩個服務器采用了圖7-7所示的數據規約,則從圖7-7中可以看出想要獲取論文的時候,我們應該組成這樣一個數據包:先是4個字節的0xee,接下來是一個整形數12,然后是一個字串Get_A_Paper,再下來又是一個整形數36,代表文章標題的長度,最后是一個字串ResearchontheHairofDinosaurian,代表文章標題。

我們能夠收到的一個正確的數據包的數據順序應該是:4個字節的0xee,一個整形數12,一個字串Correct_Ret,又一個整形數(如23686)代表文章數據的字節數,后面跟著一長串的文章內容數據。當我們接收到數據時,首先從數據中找到同步字,然后根據指令字長度取出指令字串,再根據數據長度取出所有數據,接下來判斷指令字的類型,如果是正常數據,則調用正常顯示函數,如果是錯誤數據,則根據錯誤類型進行出錯處理。圖7-7一種應用層的通信規約 (3)建立訪問函數:建立兩個函數GetAPaper()和TransADoc()。第一個函數用來獲取一篇論文,第二個函數用來得到這篇論文的翻譯稿。將這些具體的數據處理過程放在函數里執行,可以使主程序顯得比較簡捷,能夠保持清晰的邏輯結構,有利于軟件的更新。例如,應用層的通信規約發生變化時,只需要修改相應的函數,而不用改動主程序。

(4)建立主程序:完成了上述工作之后,就可以建立主程序了,主程序基本上只是對一系列函數的調用。

程序的源代碼片斷:

#include<…> #defineADDR_PROVIDER"" #defineADDR_TRASLATOR"" #definePORT18081 #definePORT28082 /*GetAPaper()向服務器ADDR_PROVIDER:PORT1索要一篇論文,論文題目在pTitle中,論文長度(字節數)放在*pLen中返回。因論文長度無法事先確定,所以采用雙指針參數,用*ppData根據實際需要申請空間,將*ppData指向論文內容首地址。*/ intGetAPaper(char*pTitle,int*pLen,char**ppData) { intsockfd; structsockaddr_inservaddr;

sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0)exit(1);bzero(&servaddr,sizeof(servaddr));servaddr.sin_family=AF_INET;servaddr.sin_port=htons(PORT1);if(inet_aton(ADDR_PROVIDER,&servaddr.sin_addr)==0) exit(1);if(connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr))<0) {close(sockfd);exit(1);}chartx[100];char*str="Get_A_Paper";intnCmd=htonl(sizeof(str)); /*

指令字長度

*/char*pCh=(char*)&nCmd;tx[0]=0xee;tx[1]=0xee;tx[2]=0xee;tx[3]=0xee; /*

添加同步字

*/ tx[4]=*pCh;pCh++; /*

把指令字長度添加到數據包中

*/tx[5]=*pCh;pCh++;tx[6]=*pCh;pCh++;tx[7]=*pCh;pCh++;

inti;pCh=str; /*

把指令字內容添加到數據包中

*/for(i=8;i<8+nCmd;i++) {tx[i]=*pCh;pCh++;} intnDat=htonl(pTitle); /*

數據(此處是標題)長度

*/ pCh=(char*)&nDat; for(intj=0;j<nDat;j++) {tx[i]=*pCh;pCh++;i++;} /*

把數據長度添加到數據包中

*/ pCh=pTitle; /*

把指令字內容添加到數據包中

*/ for(intk=0;k<nDat;k++) {tx[i]=*pCh;pCh++;i++;} intnbytes=write(sockfd,tx,i); /*

把數據包發送出去

*/if(nbytes<=0) {close(sockfd);exit(1);}char*pPaper=newint[65536]; /*

設置接收緩存區,在函數退出前必須刪除

*/nbytes=read(sockfd,(char*)pPaper,65536);if(nbytes<=0) {close(sockfd);deletepPaper;return-1;}

intnNum=0;

…. /*

解析收到的數據包,提取其中的數據長度放入nNum,并將數據起始下標賦i*/

*ppData=newchar[nNum]; /*

把數據放入*ppData開始的空間里,數據長度放入*pLen中

*/int*pData=*ppData;for(;i<nNum;i++) {*pData=pPaper[i];pData++;}

*pLen=nNum;close(sockfd);deletepPaperreturn0;} intTransADoc(intnlen_in,char*pData_in,int*nlen_out,char**ppData_out) /*TransADoc()向服務器ADDR_PROVIDER:PORT2要求翻譯一篇論文,其實現方法與GetAPaper()方法類似。nlen_in是輸入數據的長度,pData_in是輸入數據的首地址,nlen_out是輸出數據的長度,ppData_out是輸出數據的首地址

*/ {

… }

…/*

再添加一些錯誤處理函數、保存文件函數等

*/voidmain(){intn;chartitle[50];intnlen_en,nlen_ch;char**ppData_en;char**ppData_ch;

*ppData_en=NULL;*ppData_ch=NULL; nlen_en=0;nlen_ch=0; gets(title); n=GetAPaper(title,&nlen_en,ppData_en); if(!(*ppData_en)) exit(2); elseif(n!=0) {ShowErr(n);exit(1);}

n=TransADoc(nlen_en,*ppData,&nlen_ch,ppData_ch);if(!(*ppData_ch)) exit(3);elseif(n!=0) {ShowErr(n);exit(1);}SavePaper(nlen_ch,*ppData_ch);if(*ppnData_en!=NULL) delete*ppnData_en;if(*ppnData_ch!=NULL) delete*ppnData_ch;}

在這個例子中,雖然應用程序使用了兩個套接字,但這兩個套接字完成的任務具有相關性,只有先得到一篇論文之后才能把它拿去翻譯。即使將第一個套接字設置成非阻塞式套接字,程序的整體性能也并不會提高。因此在這種情況下,我們使用阻塞式I/O是一種合理的選擇。本例一方面是為了討論阻塞式I/O模型,另一方面也是想讓讀者對如何在TCP/IP之上設計應用層協議有所了解。

當我們使用多個套接字,而它們各自承擔的任務沒有必然的順序關系時,則可能出現的長期甚至永久性阻塞會嚴重影響應用程序的性能。假設我們想要完成的任務不是先找到一篇論文然后再翻譯它,而是從兩個服務器里拿到一篇論文即可。那么就可能出現這樣的情況:進程在與第一個服務器通信時被永久阻塞,這時我們也無法從第二個服務器中得到想要的論文。這種情況下,如果我們還鐘情于阻塞式模型,則可以考慮采用超時控制的方法對這種模型加以改良。 2.I/O超時控制 調用alarm()函數是最常用的超時控制方法,這在第3章已經簡單地介紹過了。下面介紹I/O超時控制方法,先給出如下一段代碼:

#include<…> #defineSERVER_PORT8989 voidsigalrm_handler(intsig) { }intmain(intargc,char*argv[]){charbuf[256];intsockfd;structsockaddr_inaddr1;structsockaddr_inaddr2;structsigactionact; for(inti=0;i<128;i++) buf[i]=i; sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) { fprintf(stderr,"Socketerror"); exit(1); }bzero(&addr1,sizeof(addr1));addr1.sin_family=AF_INET;addr1.sin_port=htons(SERVER_PORT);if(inet_aton(argv[1],&addr1.sin_addr)==0){ fprintf(stderr,"Inet_atonerror"); exit(1);}

… /*

設定addr2的地址

*/

act.sa_handler=sigalrm_handler;act.sa_mask=0;act.sa_flags=0;sigaction(SIGALRM,&act,NULL); alarm(5); sendto(sockfd,buf,128,0,(structsockaddr*)&addr1,sizeof(addr1));n=recvfrom(sockfd,buf,256,0,NULL,NULL);

sendto(sockfd,buf,128,0,(structsockaddr*)&addr2,sizeof(addr2));n=recvfrom(sockfd,buf,256,0,NULL,NULL);

alarm(0);close(sockfd);}

在這段程序中,客戶機設定了一次alarm(5)函數,然后向兩個服務器請求數據。假設在等待第一個服務器數據時發生阻塞,那么5s后進程被喚醒,繼續向下執行;如果在等待第二個服務器數據時,服務器進程已經終止并且不再重新啟動,則客戶程序又一次發生阻塞。這次,客戶進程就可能被長期阻塞,因為alarm()函數已經失效,不會再發出SIGALRM信號來喚醒進程。我們可以將程序改成如下形式,來保證每次阻塞均被超時機制喚醒,但是代碼顯得比較繁瑣:

… intmain(intargc,char*argv[]) {

… alarm(5);

sendto(sockfd,buf,128,0,(structsockaddr*)&addr1,sizeof(addr1));n=recvfrom(sockfd,buf,256,0,NULL,NULL);alarm(0);

alarm(5);sendto(sockfd,buf,128,0,(structsockaddr*)&addr2,sizeof(addr2));n=recvfrom(sockfd,buf,256,0,NULL,NULL);alarm(0);close(sockfd);}

調用函數setsockopt(),設置SO_RCVTIMEO和SO_SNDTIMEO的選項,是另一種常用的超時控制方法。SO_RCVTIMEO和SO_SNDTIMEO這兩個宏的定義可以在socket.h中找到。內核程序linux/net/socket.c在sock_setsockopt()函數中將這兩個宏轉換為sock_set_timeout()函數的不同超時參數,相應的代碼片斷如下: 162 intsock_setsockopt(structsocket*sock,

intlevel,

intoptname,

163char*optval,

intoptlen) 164 { ... 324caseSO_RCVTIMEO: 325ret=sock_set_timeout(&sk->rcvtimeo,

optval,

optlen); 326break;327328caseSO_SNDTIMEO:329ret=sock_set_timeout(&sk->sndtimeo,

optval,

optlen);330break; ...375 }

函數sock_set_timeout()處于網絡核心層,在linux/net/core/sock.c中實現,代碼如下:

140 staticintsock_set_timeout(long*timeo_p,

char*optval,

intoptlen) 141 { 142structtimevaltv; 143 144if(optlen<sizeof(tv)) 145return-EINVAL;

146if(copy_from_user(&tv,

optval,

sizeof(tv))) 147return-EFAULT; 148 149*timeo_p=MAX_SCHEDULE_TIMEOUT; 150if(tv.tv_sec==0&&tv.tv_usec==0) 151return0; 152if(tv.tv_sec<(MAX_SCHEDULE_TIMEOUT/HZ-1)) 153*timeo_p=tv.tv_sec*HZ+(tv.tv_usec+(1000000/HZ-1))/(1000000/HZ); 154return0;155

}

從這段代碼中可看到,當超時值設定為0時,*timeo_p沒有被賦值,也就是說套接字沒有設定超時值,不會從阻塞狀態中跳出。這一點與alarm(0)非常相似。

alarm()函數是與進程相關的,而setsockopt()函數則是與套接字相關的,對哪個套接字設置了這種超時選項,哪個套接字在執行讀、寫操作時,就可以從阻塞中跳出,但需要引起注意:SO_RCVTIMEO和SO_SNDTIMEO對connect()、accept()不起作用。

現在我們再來考察setsockopt()函數是不是周期性的函數。用一段圖形界面X-Windows下QtDesigner編制的程序來進行測試。Qt是由挪威TrollTech公司出品的跨平臺C++圖形用戶界面庫,具有優良的跨平臺特性,可以在多種操作系統使用,包括Windows、Linux、Solaris、SunOS、HP-UX、UNIX、FreeBSD等。使用Qt開發程序,能夠使程序高度模塊化,具有良好的封裝、繼承和重載特性。Qt原本并不是自由軟件,但到了2000年Trolltech公司宣布Qt/Embedded、Qtfreeedition開始使用

GPL。目前,許多程序員將Qt稱為Linux系統下的“MFC”。QtDesigner則是Qt的一個快速開發工具,使用QtDesigner編制下面這段程序的步驟如下: (1)創建工程:單擊主菜單“File”,選擇“New”,然后在彈出的對話框中選擇“C++Project”,單擊“OK”按鈕。在新彈出的“ProjectSettings”中選擇路徑,此處選為“/root/sockopt/”,再鍵入文件名sockopt,單擊“OK”按鈕。工程創建完畢,文件名為。

(2)編輯對話框:單擊主菜單“File”,選擇“New”,然后在彈出的對話框中選擇“Dialog”,單擊“OK”按鈕。這時將出現一個表單,在屬性欄中把表單的名字改為sockopt,表單的屬性Caption也改為sockopt,然后調整表單尺寸,使它比較小;再從Toolbox的CommonWidgets中選取PushButton、LineEdit和TextLabel各一個放入表單,將PushButton的名字改為pBtn1,其Text屬性改為recv;將TextLabel的Text屬性改為“阻塞次數:”。把這幾個控件放到合適的位置。 (3)建立Signal/Slot映射:信號/槽是Qt提供的一種替代

callback的機制,這種機制使得程序各個元件之間的協同工作變得十分簡單,當一個信號發出后,與該信號有連接(connection)關系的槽函數就會被啟動。在“recv”按鈕上單擊鼠標右鍵選擇“Connections…”,這時將彈出“ViewandEditConnections”對話框,單擊“New”按鈕來增加一個信號/槽的連接,Sender選為pBtn1,Signal選為clicked(),Receiver選為sockopt,然后單擊“EditSlots”按鈕創建一個新的槽函數,在新彈出的“EditFunction”中單擊“EditSlots”按鈕,把函數名改為recvData,選項Specifier選為nonvirtual,單擊“OK”按鈕退回到上一個對話框,將Slot的函數選為recvData(),單擊“OK”按鈕,完成這個步驟。 (4)編輯代碼:在“ProjectOverview”對話框中單擊sockopt.ui.h,代碼窗口將呈現出來,這時就可以把代碼修改成后面程序清單中的內容。 ●添加主程序:單擊主菜單“File”,選擇“New”,在彈出的對話框中選“C++Main-File(main.cpp)”,單擊“OK”按鈕,然后保存自動生成的主程序。接下來,單擊主菜單“File”,選擇“SaveAll”。 ●?Make:打開一個終端,進入文件所在的子目錄,鍵入qmake-oMakefile,然后按回車鍵,如果正常,則再鍵入make按下回車。接下來在文件夾里找到sockopt的二進制文件,雙擊即可運行。

程序清單如下: 用戶界面的頭文件sockopt.ui.h. /**************************************************************************** *ui.hextensionfile,

includedfromtheuic-generatedformimplementation. *Ifyouwishtoadd,

deleteorrenamefunctionsorslotsuse *QtDesignerwhichwillupdatethisfile,

preservingyourcode.Createan

*init()functioninplaceofaconstructor,

andadestroy()functionin *placeofadestructor. *****************************************************************************/ #include<sys/socket.h> #include<stdio.h> #include<syscall.h> #include<unistd.h> #include<signal.h> #defineSERVER_PORT8089 intsockfd; structsockaddr_inaddr;

voidsockopt::init() { structtimevalrx; sockfd=socket(AF_INET,SOCK_DGRAM,0); if(sockfd<0) exit(1); bzero(&addr,sizeof(addr)); addr.sin_family=AF_INET; addr.sin_port=htons(SERVER_PORT); rx.tv_sec=5; rx.tv_usec=0; setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&rx,sizeof(rx)); } voidsockopt::recvData() { staticintn=0; n++; socklen_tlen; charmsg[1024]; recvfrom(sockfd,msg,1024,0,(structsockaddr*)&addr,&len); this->lineEdit1->setText(QString::number(n,10)); } voidsockopt::destroy() { if(sockfd>0) close(sockfd); }

其中,init()是對話框的初始化函數,對話框創建后將首先執行這個函數;destroy()是對話框的解構函數,對話框被銷毀前調用這個函數,通常在解構函數里做一些諸如關閉打開的文件描述符、釋放程序運行過程中申請的空間等工作。雖然Qt有自己定義的套接字類QSocket,但在這里為了說明阻塞問題,也為了與先前的例子保持一致性,我們還是用了系統定義的套接字形式。

主程序main.cpp:#include<qapplication.h>#include"sockopt.h"intmain(intargc,

char**argv){QApplicationa(argc,

argv);sockoptw;w.show();a.connect(&a,SIGNAL(lastWindowClosed()),&a,SLOT(quit()));returna.exec();}

每次單擊“recv”按鈕時,就調用一次sockopt::recvData()函數,因為我們所在的網絡里沒有人使用8089端口,所以程序必定被阻塞在這個函數里。單擊“recv”按鈕,我們會發現按鈕5?s之后才彈起,同時編輯框里的數字加1。圖7-8是單擊“recv”按鈕后的顯示結果。因為我們只是在初始化的時候設定了延時參數,所以說明使用setsockopt()時,超時控制是周期性的。圖7-8sockopt程序的運行結果

7.2.2阻塞式I/O的服務器編程

一般來說,服務器總是在接收到客戶機的請求后,才開始處理客戶請求的內容,然后返回處理結果。當沒有客戶機請求時,服務器進程沒有必要去占用CPU時間,因此阻塞式I/O通常是合理的選擇。服務器的運行模式主要有循環服務器和并發服務器,無論是循環服務、并發服務,還是將兩者結合起來,通常阻塞式I/O模型都能夠為之提供良好的支持。7.3非阻塞函數的編程

采用兩種方法可以將套接字設為非阻塞式:

(1)函數fcntl(),設置O_NONBLOCK選項:

intflag=fcntl(sockfd,F_GETFL,0); fcntl(sockfd,F_SETFL,flag|O_NONBLOCK); (2)函數ioctl(),設置FIONBIO選項:

intnIO=1; ioctl(sockfd,FIONBIO,&nIO);

非阻塞式I/O模型可以避免進程被長期阻塞的問題,使得進程在沒有套接字描述符就緒的時候可以進行其他的工作,能夠提高系統的工作效率,但是編程相對于阻塞式I/O要復雜一些,邏輯結構不如阻塞式I/O清晰。另外,非阻塞式程序需要不斷地檢查是否有套接字描述符就緒,持續占用CPU的時間,因此也常常需要采用定時查詢等方法加以改進。下面將分別討論客戶機和服務器在這種I/O模型下的編程。 7.3.1非阻塞式I/O的客戶機編程 基本的非阻塞式程序有三種:一般、定時查詢和多連接。

1.一般 當用戶的一部分任務需要由服務器完成,而另一部分任務可以在本地執行時,可以采用非阻塞式模型,先向服務器發出請求,然后執行本地數據處理函數,執行完畢后,再從服務器讀取服務結果。這樣服務進程和本地進程可以同步進行,縮短了任務的完成時間。例如,我們要完成一個運算

y=f1(x)+f2(x)

其中,f1(x)需由遠方服務器運行,大約耗時10s;f2(x)在本地運行,大約耗時8s。如果不考慮網絡傳輸延遲,則采用阻塞式模型耗時18s,而采用非阻塞式模型,則時間可縮短為10s。這個例子的程序流程如圖7-9所示,代碼中考慮了連接不成功、讀數據過程中連接被對方斷開等情況,出現這些情況時需要重新建立連接。該程序不會因為等待服務器的運算結果而阻塞,但同時也可以看到,在非阻塞模型下,程序的復雜程度比阻塞模型復雜,程序結構也不像阻塞模型那樣清晰。圖7-9基本的非阻塞模型流程示例

示例程序代碼如下:

…/*

頭文件

*/

…/*

定義本地處理函數

*/ intmain(intx) { intsockfd; structsockaddr_inservaddr; intnLocalFin=0; intnFin=0; intnConnected=0; staticintnOut=0; staticintnFirst=0; intx1,x2; if((sockfd1=socket(AF_INET,SOCK_STREAM,0))<0) { fprintf(stderr,"Socketerror"); exit(1); } bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(SERVER_PORT); if(inet_aton(ADDR,&servaddr.sin_addr)==0) exit(1); intflag=fcntl(sockfd,F_GETFL,0); /*

設置非阻塞標志

*/ fcntl(sockfd,F_SETFL,flag|O_NONBLOCK); intn=0; while((nFin1==0||(nLocalFin==0)) { if(n<0&&errno==EINTR) /*

收到中斷信號,繼續運行程序

*/ continue; if(nFin==0) /*

讀服務器的返回值

*/ {if(nConnected==0) /*

未連接

*/{n=connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));if(n<0&&errno==EINPROGRESS) nConnect=1; /*

設定正在連接標志

*/ elseif(n>0) { nConnected=2;nOut=0; /*

連接成功

*/ fprintf("Connected."); }}elseif(nConnected==1) /*

正在連接

*/ {n=connect(sockfd,(structsockaddr*)&servaddr,sizeof(servaddr));if(n<0&&errno!=EINPROGRESS) { nConnected=0; /*

連接出錯,重新連接

*/ nOut++; if(nOut>9) /*

連續10次都連接出錯,終止程序,返回錯誤

*/ exit(1);

} elseif(n>=0) /*

連接成功

*/ { nConnected=2;nOut=0; fprintf("Connected."); }}else{if(nFirst==0) /*建立連接后,首先向服務器發出數據*/{n=write(sockfd,&x,sizeof(buf));nFirst=1;}elsen=read(sockfd,buf,128);

if(n==0) /*

連接被斷開,設置未連接標志,重新連接

*/ { fprintf("Socket1isdisconnected."); nConnected=0;nFirst=0; } elseif(n<0&&errno!=EINTR&&errno!=EWOULDBLOCK)/*發生錯誤,退出*/ { perror("AnError."); exit(2); } elseif(n>0) { x1=…

/*

從中讀出服務器的返回值,賦給x1*/

fprintf("Remotetaskisover."); nFin=1; }}} if(nLocalFin==0){x2=… /*

本地數據處理,結果數值賦給x2*/nLocalFin=1;fprintf("Localtaskisover.");} } close(sockfd); returnx1+x2; } 2.定時查詢 在上一個例子中當進程已執行完本地任務后,如果這時服務器的運算結果仍未返回,則進程就會不停地檢測套接字是否就緒,這時進程繼續處于可運行狀態,占用CPU時間。一個系統中如果有大量這種進程存在,系統的整體運行速度將大大降低。這時我們可以采取定時查詢的方法,在一定程度上克服上述缺點。如圖7-10所示,首先設置SIGALRM信號捕獲函數,然后進程在完成本地處理后,先調用alarm()函數,設置定時常數(圖中為1s),再調用sleep()函數,設置一個較大的時常數,將進程阻塞。進程將在alarm()定時到達時喚醒進程,檢查套接字是否有數據可以讀出。圖7-10定時查詢的非阻塞模型流程示例 3.多連接

常常有這樣的情況,用戶需要訪問多個服務器才能完成一項任務,例如,一位遺傳學家想要對比人和猩猩、熊貓、北極熊的某個基因片斷的差異,他可以在本地找到人的基因樣本,其他幾種研究對象的基因樣本數據需要分別從非洲、中國、加拿大的基因數據庫中去找。這時,他編制了一個客戶端程序,先與三個遠程數據服務器建立連接,然后采用非阻塞的方式向三個數據庫索要數據;接下來載入本地數據資料,裝載完畢后,定時查詢三個套接口是否有數據返回,一旦發現有數據就取出來;三種數據都到達時,這位遺傳學家就可以做對比運算了。這個過程中,從三個數據庫中獲取數據和載入本地資料這四個過程是并行工作的,理所當然地可以加快處理速度。

這個例子的主要程序代碼如下:

… /*

頭文件

*/ #defineSD16 #defineRE16000

…/*定義一些本地處理函數*/ voidsigalrm_handler(intsig) /*

信號處理函數

*/

{ staticintn=0; n++; if(n>1000) /*

如果長期不能完成全部任務,則進程退出并報錯

*/ exit(1); }

intmain() { structsigactionsigact; intsockfd1,sockfd2,sockfd3,flag; structsockaddr_inservaddr1,servaddr2,servaddr3; intnLocalFin=0; intnFin1,nFin2,nFin3; chartx1[SD],tx2[SD],tx3[SD]; /*發送緩存區*/ charrx1[RE],rx2[RE],rx3[RE],rx4[RE]; /*1~3:接收緩存區,4:本地數據緩存區

*/ tx1="Orangutan";tx2="Panda";tx3="Polarbear"; chartx1[SD],tx2[SD],tx3[SD]; /*發送緩存區*/ charrx1[RE],rx2[RE],rx3[RE],rx4[RE]; /*1~3:接收緩存區,4:本地數據緩存區

*/ tx1="Orangutan";tx2="Panda";tx3="Polarbear"; chartx1[SD],tx2[SD],tx3[SD]; /*發送緩存區*/ charrx1[RE],rx2[RE],rx3[RE],rx4[RE]; /

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論