UNIX网络编程卷一 第六章 I/O复用 select 和 poll 函数

通常我们要等待某个IO事件的发生(比如套接字中有数据可读,标准输入中有用户输入的内容),一般都是采用阻塞读、写的方式,但是这样我们就只能等待一个IO事件的发生,就像上一章的例子中客户端出现的情况,无论是阻塞在fgets 还是 read 都不行,最好的办法是可以阻塞带多个描述符,任意一个发生了期待的事件内核就通知进程。select 和poll就是这个作用。


一、 IO模型

共有五钟IO模型

阻塞式I/O: 睡眠等待事件发生,出让CPU。

非阻塞式I/O: 尝试读或写数据,不管成功与否立刻返回,这种情况下一般要采用轮询的方式。轮询就是CPU不停的检查IO是否可读,是要占用CPU的。

I/O复用:同时等待几个事件的发生,只要任意一个发生内核就通知进程,在这期间也是阻塞式的。

信号驱动式I/O(SIGIO):事件发生时,发送信号给进程,这个就相当于软件中断,要首先安装信号捕获函数,这时程序可以继续执行后续步骤,不出让CPU。

异步I/O:比如发出一个读、写命令,暂时不管命令执行是否成功,此时进程就继续往下执行,在内核完成了读写操作后通知进程。与信号驱动式的区别是 这里当内核通知进程时,读写操作操作已经完成了,而信号驱动的方式只是通知进行可以去读写了,进程随后调用读写命令。



二、 select 函数

原型:

int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

返回值:返回就绪描述符数目,超时返回0, 出错返回-1

输入参数: maxfdp1 表示描述符集中最大的描述符 再加 1,注意不是描述符数量

readset:   读描述符集

writeset:  写描述符集

exceptset: 异常描述符集

以上三个描述符集我们只要设置我们关心的描述符就行,不用的置为NULL


timeout:  函数返回的超时时间,为 0 表示立即返回,为NULL表示永不超时


maxfdp1 的最大值有限制, 最大值被定义成FD_SETSIZE ,一般定义为256, 这个值也不能超过进程可使用的描述符最大值。


fd_set 结构体的定义通常是一个结构体,里面是一个整数数组,其中每个整数中的每一位对应一个描述符。比如使用32位整数,那么该数组的第一个元素对应于描述符0~31.

第二个元素对应32 ~63,依次类推。将数组放结构体的好处是可以直接复制整个数组。

操作这个结构体由四个宏:

void FD_ZERO(fd_set *fdset); 描述符集置0, 初始化的时候使用

void FD_SET( int fd, fd_set *fdset); 打开描述符fd, 在描述符集中对应的位

void FD_CLR( int fd, fd_set *fdset); 关闭描述符fd, 在描述符集中对应的位

int FD_ISSET( int fd, fd_set *fdset); 测试描述符fd, 在描述符集中对应的位是否打开


使用示例:

上一章中str_cli函数改用seclect实现,即同时监听标准输入和套接字。

#include "unp.h"


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 (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}


if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
if (Fgets(sendline, MAXLINE, fp) == NULL)
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}


上面的函数对于交互式输入来说可以正常工作,但是对于批量输入就不行,比如重定向输入为一个文件。

有2个问题:

1. 程序可能没有任何回显。

原因分析: 

当程序从标准输入读取到文本之后,写入到套接字,当数据量比较多时,数据在发送的过程中同时也会有服务器端处理完成的数据返回来,发送操作只是本地缓冲的一个拷贝,发送过程却需要经历客服端和服务器的往返时间以及服务器的处理时间,在本地写完EOF后,可能服务端的数据还没有返回,这是程序因为已经写完已经return了。

这个问题的解决办法是在写完以后,不要return, 而是半关闭套接字(关闭写端,不关闭读端)这样TCP连接就不会断开。调用shutdown 可以半关闭套接字。


2. 程序会一直阻塞在secletc, 即使已经读到了文件末尾。

原因分析:

这个问题根本原因是缓冲区问题, fgets函数是标准库中的函数,为了提高读写效率fgets会设置缓冲区,因此会尽可能多的在一次系统调用时读取更多内容(当然这个不是越大越好,可能是1K或4K等), 但是在返回时就只返回缓冲区中的一行。也就是说,在调用一次fgets时,可能吧整个输入文件都读到缓冲区了,但是只返回其中一行, fgets返回一行,并发送到服务器一切正常,但是select 却不知道整个文件都被读取了,EOF字符这是正在应用缓冲区中,因此再此进入阻塞状态。


解决办法可以不要调用fgets库函数,可以直接调用read。


三、 shutdown 函数


原型:int shutdown(int sockfd,int howto);

成功返回0, 失败返回-1

针对套接字描述符 sockfd, 可以有几种关闭方式,由howto指定

SHUT_RD :  关闭读端 

SHUT_WR:  关闭写端

SHUT_RDWR: 关闭读写


SHUT_RD  据说用来防止路由器套接字数据环回。

SHUT_WR 客户机和服务器通信时一般都是使用这个,在写完了以后发送这个表示不再发了,还保持接收。

SHUT_RDWR 和上面2个加起来的效果一样。

调用shutdown 后会发送一个FIN字节,不管还有没有该套接字描述符被打开着,这个是与close的区别。


代码示例:str_cli 改进版本:



void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;


stdineof = 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)) { /* socket is readable */
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* normal termination */
else
err_quit("str_cli: server terminated prematurely");
}


Write(fileno(stdout), buf, n);
}


if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}


Writen(sockfd, buf, n);
}
}
}

这个版本调用shutdown,并设置stdineof标识,发送完成后将该标志设为1,并继续循环,而不是直接退出程序,

也将库函数换成系统调用read ,解决缓冲的问题。

四、pselect 函数

pselect是posix在select基础上定义的。

原型:

int pselect (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);

这个函数与select的区别是,1,增加了sigmask参数,在阻塞过程中可以设置屏蔽一些信号。 2. timeout的结构体修改为struct timespec,具有更好的精度。


五、 poll函数

poll函数作用与select类似,使用频率没有select高,在处理流设备时,能提供额外信息 ?。

原型:

int poll(struct pollfd *fdarray, unsigned long nfds, int timeoutt);

返回值:返回就绪描述符数目,超时返回0, 出错返回-1

fdarray : 指向一个结构数组,每个数组元素用于测试一个描述符

nfds: 为数组元素的个数,通常也设为需要监听的最大描述符+1


struct pollfd{

int fd; /*descriptor to check*/

short events; /*events of interest on fd */

short revents; /*events that occured on fd */ 

};

要测试的条件由events 指定,返回结果放在revents中。

这些条件值定义为了9个宏值

分别为 :

读:

POLLIN 普通数据或优先级带数据

POLLRDNORM 普通数据

POLLRDBAND 优先级带数据

POLLPRI 高优先级带数据

写:

POLLOUT 同上

POLLWRNORM

POLLWRBAND

异常:

POLLERR 发生错误

POLLHUP 发生挂起

POLLNVAL 描述符不是一个打开的文件

其中表示异常的三个值只能作为返回值放在revents中,读写的宏值既可以作为输入也可以作为输出值,判断输出结果时可以将revents 与 某个宏值相与。

timeout  参数指定poll超时时间, INFTIM 表示永不超时 , 0 表示立即超时

如果我们不再关心某个描述符值,可以将pollfd结构中的fd设成一个负值, poll函数将忽略这样的描述符。

示例代码:

tcpservpoll01.c

/* include fig01 */
#include "unp.h"
#include <limits.h> /* for OPEN_MAX */


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;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */
maxi = 0; /* max index into client[] array */
/* end fig01 */


/* include fig02 */
for ( ; ; ) {
nready = Poll(client, maxi+1, INFTIM);


if (client[0].revents & POLLRDNORM) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif


for (i = 1; i < OPEN_MAX; i++)
if (client[i].fd < 0) {
client[i].fd = connfd; /* save descriptor */
break;
}
if (i == OPEN_MAX)
err_quit("too many clients");


client[i].events = POLLRDNORM;
if (i > maxi)
maxi = i; /* max index in client[] array */


if (--nready <= 0)
continue; /* no more readable descriptors */
}


for (i = 1; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) {
/*4connection reset by client */
#ifdef NOTDEF
printf("client[%d] aborted connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
err_sys("read error");
} else if (n == 0) {
/*4connection closed by client */
#ifdef NOTDEF
printf("client[%d] closed connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
Writen(sockfd, buf, n);


if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */

使用select 或者 poll 可以不用fork子进程来处理已经建立连接的客户, 不过个人认为fork的方法更简洁些。

client[0] 用来管理监听描述符。 其余的用来管理已建立连接的描述符。

Poll 返回之后 用 if (client[0].revents & POLLRDNORM) 测试是否有新客户连接;

然后逐个判断其余描述符是否有数据到来或者有异常 if (client[i].revents & (POLLRDNORM | POLLERR))


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值