第六章 I/O复用: select和poll函数 - ㊣来者犹可追

来源:百度文库 编辑:神马文学网 时间:2024/04/27 15:01:27
第六章 I/O复用: select和poll函数 6.1 Introduction
前面的例子了客户端要同时处理标准输入和TCP socket  当输入阻塞时不能立即处理socket  所以需要采用I/O复用技术
以下几个方面需要用到I/O复用技术:
1.一个客户同时处理多个描述符(如前所述)
2.客户同时处理多个socket
3.TCP服务器同时处理监听端口和连接端口
4.服务器同时处理TCP和UDP
5.服务器同时处理多个服务或多个协议
6.2 I/O Models
以UDP为例说明
阻塞式I/O模型

recvfrom()直到收到数据并复制给进程缓冲后返回  或者返程错误
非阻塞式I/O模型

当I/O操作请求不能立即完成时  不使进程进入休眠  而是立即返回错误信息
前4个recvfrom()因数据未到而直接返回了EWOULDBLOCK
应用程序在一个循环中反复这样调用  称为polling  这很占CPU时间
I/O复用模型
   需要用到select()或poll()
如select()会在数据准备好之后返回而后调用recvfrom()
这个过程看似与阻塞式区别不大  但是在有多个描述符存在的情况下是很有利的!
信号驱动I/O模型

sigaction()建立一个信号处理函数  当即得到返回  直到数据到达后内核发送SIGIO信号给进程
随后recvfrom()
异步I/O模型

由POSIX定义  但一般系统都未将之实现  其与信号驱动I/O相比是在数据复制完成后再返回调用
前4个模型都可以成为是同步I/O
6.3 select Function
高知内核等待某一或某些事件发生  而后唤醒进程  或超时返回
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *excepset,
const struct timeval *timeout);    返回准备好的描述符数量 超时返0错误-1
struct timeval{
long tv_sec;    秒
long tv_usec;   毫秒
};
1.该参数用以指明等待的时间:
a.永远等待  设置NULL指针
b.不等待立即返回  2个元素都设为0
c.等待一定时间  具体设置之
2.并不十分精确  最大精度10ms  但强于sleep()的1s
3.因为有const修饰符  所以该值不会被修改  若要计算时间2次调用时间函数求差时
readset writeset excepset用以标示我们需要内核进行检测读写错误的描述符
1.excepset只有两种情况  一个是out-of-band  另一个书上没详细说
2.用以组整数数组来标识  每一位对应一个描述符  如果array[0]指示0~31个描述符  常用4个宏进行操作
void FD_ZERO(fd_set *fdset);           清除所有位
void FD_SET(int fd, fd_set *fdset);    设置位
void FD_CLR(int fd, fd_set *fdset);    关闭位
void FD_ISSET(int fd, fd_set *fdset);  查询该位是否设置
3.任何一个XXXset设为NULL表示对该状态不关心  全NULL可以代替sleep()使用
4.value-result参数  送时表明哪些描述符需要检测  返回时表明哪些描述符准备完毕
maxfdp1意思是最大描述符号加1
FD_SETSIZE定义fd_set描述符数量  但通常过大所以用这个参数减小系统负担
何时描述符准备完毕
书上列举很详细  比较容易想到  不记了
6.4 str_cli Function (Revisited)
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);    设置描述符  fileno()将标准I/O指针转为对应描述符
FD_SET(sockfd, &rset);
maxfdp1=max(fileno(fp), sockfd)) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset) {        socket可读
if(Readline(sockfd, recvline, MAXLINE) ==0)
err_quit(...);
Fputs(recvline, stdout);
}
if(FD_ISSET(fileno(fp), &rset) {    标准I/O可读
if(Fgets(sendline, MAXLINE, fp) == NULL)
return;
Writen(sockfd, sendline, strlen(sendline);
}
}
}
该改进程序处理如下情况:
1.对方TCP发数据  本方socket可读  read()返回大于0值
2.对方TCP发FIN  本方socket可读  read()返回0即EOF
3.对方TCP发RST  本方socket可读  read()返回-1并由errno指明错误
6.5 Batch Input and Buffering

时刻7和8时候在网络上的报文流向
在前一节改进的str_cli程序中  当输入数据都完成后系统得到EOF并一路返回到main()  而这时并没有完成程序的所有功能  没有接受到返回消息
所以在大批输入和缓冲的情况下  仍要改进程序  方法是半关闭TCP连接shutdown()
发送FIN告知对方  本方已没有数据要发送  但socket仍开放等待读取
6.6 shutdown Function
函数功能上一节已经描述了

int shutdown(int sockfd, int howto);    OK返0错误-1
howto有三个选择(括号里表示定义的数值):
SHUT_RD(0)  :  连接的读功能关闭-socket不会收到任何数据  当前接受缓冲数据亦被丢弃  读函数无效  再接受到的数据会被ACK然后直接丢弃
SHUT_WR(1)  :  连接的写功能关闭-当前发送缓冲数据会被发送  然后进入半关闭流程  写函数无效
SHUT_RDWR(2):  如同RD WR同时使用
6.7 str_cli Function (Revisited Again)
void str_cli(FILE *fp, int sockfd)
{
**    int    maxfdp1, stdineof;
fd_set rset;
**    char buf[MAXLINE];
**    int    n;
stdioof = 0;
FD_ZERO(&rset);
for(;;) {
if(stdineof == 0)
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) {
if(
if(stdioeof == 1)     仅当输入EOF关闭后才可能退出程序
return;
else
err_quit(...);
}
Writen(fileno(stdout), buf, n);
}
if(FD_ISSET(fileno(fp), &rset) {
if((n=Read(fileno(fp), buf, MAXLINE) ==0) {
stdioeof=1;
Shutdown(sockfd, SHUT_WR);    半关闭
FD_CLR(fileno(fp), &ret);     输入设备关闭描述符
continue;
}
Writen(sockfd, buf, n);
}
}
stdineof的作用  仅当输入确认完成后置1  随后才可能退出socket
注意程序中只用到read() write()  因为select()可以代替readn writen的作用
6.8 TCP Echo Server (Revisited)
第五章中的服务器程序对每一个客户开启一个进程  这一节的程序仅使用一个进程  用select()处理不同的客户
举一个实例:
Figure 6.15
1.服务器仅有一个监听socket时候的TCP数据结构
当服务器在前台运行  rset中描述符0 1 2分别针对标准输入 输出 错误设备  所以第一个可分配给监听socket的描述符号是3
另外还有一个数值型client[]  标明分配给每个客户的连接socket描述符  初始化为-1

2.新连接accept()后返回描述符4  后一个连接返回5
rset相应位置置1  client[]置描述符号

3.第一个连接关闭  rset相应位置置0  client[]置-1
注意!  maxfd值没有改变
int main(int argc, char **argv)
{
**    int       i, maxi, maxfd, listenfd, connfd, sockfd;
**    int       nready, client[FD_SETSIZE];
ssize_t   n;
**    fd_set    rset, allset;
char      buf[MAXLINE];
pid_t     childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd=Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr);
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s.addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
**    maxfd=listenfd;              记录最大的描述名号
maxi =-1;                    记录client[]用到的最大数目
for(i=0; i< FD_SETSIZE; i++)
client=-1;              初始-1
FD_ZERO(&allset);
FD_SET(listenfd, &allset);   仅关注listenfd操作
for(;;) {
**      rset=allset;
nready=Select(maxfd+1, &rset, NULL, NULL, NULL);
if(FD_ISSET(listenfd, &rset) {    有新连接接收
clilen=sizeof(cliaddr);
connfd=Accept(listenfd, (SA*)&cliaddr, &clilen);
for(i=0; iif(client<0) {
client=connfd;
break;
}
if(i==FD_SETSIZE)               连接过多退出
err_quit(...)
FD_SET(connfd, &allset);        新增关注的描述符
if(connfd>maxfd)                是否增加一个新单元
maxfd=connfd;
if(i>maxi)
maxi=i;
if(--nready<=0)                 当没有可读描述符了
continue;
}
for(i=0; i<=maxi; i++) {          检查所以客户的数据请求
if((sockfd=client < 0)
continue;
if(FD_ISSET(sockfd, &rset) {
if((n=Read(sockfd,buf, MAXLINE)) == 0) {
Close(sockfd);
FD_CLR(sockfd, &allset);
client=-1;
} else
Writen(sockfd, buf, n);
if(--nready<=0)               当没有可读描述符了
break;
}
}
}
}
这个版本还存在的问题是DOS拒绝服务
某客户发送一个字节后进入休眠  服务器在下次进入Read()会被阻塞     (不是很明白!!!)
所以结论是  在处理多客户时  服务器绝不能调用只于一个客户相关的函数  否则被被阻塞而不能提供其他客户服务
6.9 pselect Function
以后会有例子  见到再说
6.10 poll Funtion
poll()功能同select()  但能更好处理流设备
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);  返回值同select()
1.struct pollfd {
int   fd;        需要检查的描述符名
short events;    检测的情况
short revents;   检测后返回的情况
}
Figure 6.23
2.poll()定义了三个类别:  普通 优先带 高优先  这些词汇来自于基于流的系统实现  以后有更详细解释
3.nfds记录了pollfd{}里的元素数
4.timeout的设置
INFTIM  永远等待
0       立即返回
>0      一段时间  以ms为单位(没这么精确而以)
5.不需要处理某个描述符的时候  只要将fd置负数events就会被忽略而revents会被填0
6.11 TCP Echo Server (Revisited Again)
poll()的一个实例  不需要waitpid()了
int main(int argc, char **argv)
{
int       i, maxi, listenfd, connfd, sockfd;
int       nready;
ssize_t   n;
char      buf[MAXLINE];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd=Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr);
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s.addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
client[0].fd=listenfd;            第一个元素赋给listenfd
client[0].events=POLLRDNORM;
for(i=1; iclient[i].fd=-1;
maxi=0;
for(;;) {
nready=Poll(client, maxi+1, INFTIM);
if(client[0].revents & POLLRDNORM) {    新连接
clilen=sizeof(cliaddr);
connfd=Accept(listenfd, (SA*)&cliaddr, &clilen);
for(i=1; iif(client[i].fd < 0) {
client[i].fd=connfd;
break;
}
if(i == OPEN_MAX)             连接过多
err_quit(...)
client[i].events=POLLRDNORM;  等待poll()检测
if(i>maxi)
maxi=i;
if(--nready<=0)
continue;
}
for(i=1; i<=maxi; i++) {        检测所有客户
if((sockfd=client[i].fd) < 0)
continue;
if(client[i].revents & (POLLRDNORM | POLLERR) {
if((n=read(sockfd, buf, MAXLINE)) < 0) {
if(errno==ECONNRESET) {   连接RST
Close(sockfd);
client[i].fd=-1;
}else
err_sys(...)            读错误
}else if(n==0) {            客户关闭连接
Close(sockfd);
client[i].fd=-1;
}else
Writen(sockfd, buf, n);
if(--nready <= 0)
break;
}
}
}
}