《网络应用程序设计》课件第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

提交评论