您的位置:首页 > 其它

I/O复用系统调用之select()和poll()

2016-08-09 00:13 302 查看
I/O复用是一种让进程预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(如可以读/写了),内核就通知进程。主要有select、poll和epoll三种函数支持。调用这几个函数时,不会阻塞在真正的I/O函数上(如read、write),而是阻塞在这几个系统调用上,直到指定的I/O条件就绪。

下边看看select系统调用的应用场景。


I/O复用系统调用之select()——I/O复用场景

以下情况需要使用I/O复用技术:
客户端需要同时处理多个socket;
客户端需要同时处理用户输入和网络连接;
服务器需要同时处理监听socket和连接socket;
服务器需要同时处理TCP请求和UDP请求;
服务器需要同时监听多个端口;

select()函数原型:
#include <sys/select.h>
int select(int nfds,        //被监听描述符总数,通常被设置为select监听的最大描述符号+1
fd_set* readfds,     //接下来三个参数指向可读、可写、异常所对应描述符的集合
fd_set* writefds,
fd_set* exceptfds,
struct timeval* timeout);   //设置为NULL,则阻塞直到指定描述符事件发生,或设置超时时间没有任何描述符事件就绪就返回0
 
timeval结构体如下:
struct timeval {  //timeval的两个域都为0,则select不会阻塞,只是简单的轮询指定的描述符集合
long tv_sec;  //秒数
long tv_usec;  //微秒数
 
返回值: select成功返回就绪描述符的个数;
失败返回-1(信号中断返回-1,设置error为EINTR)


select函数的第四个参数说的“异常”并不是说文件描述符出现了错误,在Linux上一个异常情况只会在下面两种情况下发生:
连接到处于信包模式下的伪终端主设备上的从设备状态发生了改变;
流式套接字上接收到了带外数据。
FD_ZERO(fd_set *fds): 清空fds与所有文件句柄的联系。
FD_SET(int fd, fd_set *fds):建立文件句柄fd与fds的联系。
FD_CLR(int fd, fd_set *fds):清除文件句柄fd与fds的联系。
FD_ISSET(int fd, fd_set *fds):检查fds联系的文件句柄fd是否


readfds、writefds和exceptions指向的结构体都是保存结果的值。由于这些结构体会在调用中被修改,如果要在循环中重复调用select(0,必须保证每次都要重新初始化它们。


可读的条件:

socket内核接收缓冲区中字节大于等于低水位(低水位为套接字选项中的内容)
socket对方关闭连接
监听socket上有新的连接
socket上有待处理的错误


可写的条件

socket中发送缓冲区字节大于低水位
socket对方写操作关闭
socket上使用非阻塞connect连接成功或者失败之后
socket上有未处理的错误


select()调用的缺点:

单个进程可监视的fd数量被限制(FD_SETSIZE限定了select能容纳的最大描述符数,在Linux上通常为1024);
对socket采用轮询的方法;
需要维护一个用来存放大量fd的数据结构


select实例:

select监听的描述符上有普通数据,标志着这个select上有可读事件;如果上边有带外数据标志着select上有异常发生。

先看一个简单的客户/服务器回射程序(未使用select),客户从标准输入stdin读入一行数据,发送给服务器,然后又从服务器读取该行数据,回显至客户的标准输出stdout。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/mman.h>
 
void sig_chld(int signo)
{
pid_t pid;
int stat;
 
while((pid = waitpid(-1,&stat,WNOHANG)) > 0)
printf("child %d terminated\n",pid);
return;
}
 
void str_echo(int sockfd)
{
ssize_t n;
char buf[BUFSIZ];
 
again:
while((n = read(sockfd, buf, BUFSIZ)) > 0)
write(sockfd,buf,n);
 
if(n < 0 && errno == EINTR)
goto again;
else if(n < 0)
printf("str_echo:%m\n"),exit(0);
}
 
int main(int argc, char **argv)
{
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
void sig_chld(int);
 
listenfd = socket(AF_INET,SOCK_STREAM, 0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("192.168.234.129");
servaddr.sin_port = htons(13000);
 
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,3);
signal(SIGCHLD,sig_chld);
 
for(;;) {
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen)) < 0) {
if(errno == EINTR)
continue;
else
printf("accept: %m\n"),exit(1);
}
if((childpid = fork()) == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}

#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
 
void str_cli(FILE *fp, int sockfd)
{
char sendline[BUFSIZ],recvline[BUFSIZ];
 
while(fgets(sendline,BUFSIZ,fp) != NULL) {
write(sockfd,sendline,strlen(sendline));
if(read(sockfd,recvline,BUFSIZ) == 0)
printf("readline:%m\n"),exit(1);
fputs(recvline,stdout);
}
}
 
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
 
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13000);
inet_aton("192.168.234.129",&servaddr.sin_addr);
 
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
 
str_cli(stdin,sockfd);
 
exit(0);
}


服务器程序运行,客户端启动,输入test,回显test正常: 
 
 

客户端进程阻塞于stdin,等待标准输入(example行),此时关闭服务器端程序,客户端仍然阻塞于stdin。 
 
 

该客户端程序中存在一个问题:当客户程序阻塞于从标准输入fgets读数据时,服务器程序退出, 

客户端无法立刻收到通知。为了解决这个问题可以通过使用select重写该客户端程序的str_cli函数,使得服务器进程一终止,客户就能立刻得到通知。
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[BUFSIZ];
int n;
 
stdineof = 0;
FD_ZERO(&rset);
for(;;) {
if(stdineof == 0)
FD_SET(fileno(fp),&rset);   //fileno函数把标准I/O文件指针转换为对应的
描述符
FD_SET(sockfd,&rset);
if(fileno(fp) > sockfd)
maxfdp1 = fileno(fp) + 1;
else
maxfdp1 = sockfd + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
 
if(FD_ISSET(sockfd,&rset)) {
if((n = read(sockfd, buf, BUFSIZ)) == 0) {
if(stdineof == 1){
printf("stdineof == 1\n");
                                return; }
        else
printf("read:%m\n"),exit(1);
}
write(fileno(stdout), buf, n);
}
if(FD_ISSET(fileno(fp),&rset)) {
if((n = read(fileno(fp), buf, BUFSIZ)) == 0) {
stdineof = 1;
shutdown(sockfd, SHUT_WR);
FD_CLR(fileno(fp),&rset);
continue;
}
write(sockfd, buf, n);
}
}
}



poll()系统调用

和select()函数功能类似,
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
 
fds为pollfd结构体数组,定义如下:
struct pollfd {
int fd;           //文件描述符
short events;  //注册的事件,一系列事件按位或实现
short revents; //实际发生的事件,由内核填充,通知应用程序发生了哪些事件,若对某个描述符不感兴趣可将该字段设置为0
}
 
nfds指定了数组fds中元素的个数(nfds_t 为无符号整型);
timeout参数同select()函数;


poll()返回值: 

-1:表示发生错误,可能是EINTR信号中断; 

0:表示该调用在任意一个文件描述符称为就绪态之前超时; 

正整数:表示数组fds中拥有的非零revents字段的pollfd结构体数量。

select和poll返回正整数有一些差别。若一个文件描述符在返回的描述符集合中出现了不止一次,系统调用select()会将同一个文件描述符计数多次;而poll()系统调用返回的是就绪态的文件描述符个数,且一个文件描述符只会统计一次,就算在相应的revents字段中设定了多个位掩码也是如此。
poll事件类型: 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  select函数 io