在编程中我们用到最多的是阻塞式I/O模型。例如,进程调用recvfrom,它直到数据报到达后从内核缓冲区复制到进程缓冲区才返回。若没有数据报到达,或者没有复制到进程缓冲区,recvfrom将一直阻塞。也有可能被信号中断,而返回。例如进程调用fgets,直到用户输入回车后才后返回。否则将一直阻塞。
而I/O复用模型在一定程度上可以解决这样的问题。什么是I/O复用呢?
编程时要同时处理多个文件描述符,而一旦有一个描述符可读或可写,函数就返回,而不等待某个特定的函数返回。UNPv1给出了定义。I/O复用是一种让进程预先“警告”内核能力,使得内核一旦发现进程预先告知时指定的一个或多个I/O条件(就是描述符)就绪(可以读/写了),内核就通知进程。linux有4个调用可实现I/O复用:select、poll继承自Unix系统。pselect是select到Posix版。epoll是linux2.6内核特有的。
select函数
原型:
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
参数:
nfds1: 等待最大套接字+1
readfds: 检查读事件的容器
writefds: 检查写事件的容器
exceptfds:检查异常事件的容器
timeout: 超时时间
timeout设置为空指针:永远等待,直到有描述符准备好I/O
timeout指定timeval结构的秒数和微秒数:等待固定时间
timeout指定timeval结构的秒数和微秒数为0,不等待,检查后立即返回
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
4个操作宏
select使用的描述符集合一般为一个整数数组。使用fd_set类型表示描述符集合。头文件<sys/select.h>中定义的常值FD_SETSIZE,是fd_set类型的描述符数量,其值通常是1024。返回时,我们用宏FD_ISSET来测试fd_set中的描述符,没有准备好的描述符对应的位返回时清成0.为此,每次调用select时,我们都得将所有描述集中关心的设置为1.
void FD_CLR(int fd, fd_set *set); //从集合中删除一个描述符
int FD_ISSET(int fd, fd_set *set); //描述符是否在集合中
void FD_SET(int fd, fd_set *set); //添加一个描述符到集合
void FD_ZERO(fd_set *set); //清空描述符集合
select返回的条件:
套接口准备好写
套接口发送缓冲区中的可用空间字节数大于等于套接口发送缓冲区低潮限度的当前值。
连接写的一端被关闭。继续对该套接字写会产生SIGPIPE信号
有一个套接口产生错误待处理。继续写会返回一个错误(-1)
套接口准备好读
套接口接收缓冲区中的数据字节数大于等于套接口接收缓冲区低潮限度的当前值。
连接读的一端关闭,继续读会返回0.
套接口是一个监听套接口,且已完成的连接数为非0
有一个套接口错误待处理,继续读会返回错误(-1)
下面展示了一个阻塞式的,不考虑系统调用被信号中断的I/O的示例,服务端程序读取三次后会关闭套接字描述符,客户端由于每次阻塞到gets,所以无法及时获取服务端断开的消息。
头文件
#ifndef MY_NET_H
#define MY_NET_H
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#define MAXLINE 4096
#define SA struct sockaddr
#define LISTENEQ 10
/*err_quit*/
void err_quit(const char* err_string)
{
printf("%s\n", err_string);
exit(-1);
}
/*err_sys*/
void err_sys(const char* err_string)
{
perror(err_string);
exit(-1);
}
/*Socket*/
int Socket(int domain, int type, int protocol)
{
int sockfd = socket(domain, type, protocol);
if (sockfd == -1)
err_sys("socket error");
return sockfd;
}
/*Inet_pton*/
int Inet_pton(int af, const char *src, void *dst)
{
int r;
if ((r = inet_pton(af, src, dst)) <= 0)
err_sys("inet_pton error");
return r;
}
/*Connect*/
int Connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
int r;
if ((r = connect(sockfd, addr, addrlen)) == -1)
err_sys("connect error");
return r;
}
/*Bind*/
int Bind(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
int r;
r = bind(sockfd, addr, addrlen);
if (r == -1)
err_sys("bind error");
return r;
}
/*Listen*/
int Listen(int sockfd, int backlog)
{
int r;
r = listen(sockfd, backlog);
if (r == -1)
err_sys("listen error");
return r;
}
/*Accept*/
int Accept(int sockfd,
struct sockaddr *addr,
socklen_t *addrlen)
{
int r;
r = accept(sockfd, addr, addrlen);
if (r == -1)
err_sys("accept error");
return r;
}
/*Close*/
int Close(int fd)
{
int r = close(fd);
if (r == -1)
err_sys("close error");
return r;
}
/*Read*/
int Read(int fd, void *buf, size_t count)
{
int r;
r = read(fd, buf, count);
if (r == -1)
err_sys("read error");
return r;
}
/*Write*/
int Write(int fd, const void *buf, size_t count)
{
int r;
r = write(fd, buf, count);
if (r == -1)
err_sys("write error");
return r;
}
/*Writen*/
//返回剩余的字符
int Writen(int fd, const void *buf, int len)
{
int r;
const char* ptr;
int nleft = len;
if(len <= 0)
return -1;
ptr = buf;
while (1)
{
r = Write(fd, ptr, len);
nleft -= r;
ptr += r;
len -= r;
if (nleft == 0)
return 0;
}
}
/*Readn*/
int Readn(int fd, void *buf, int len)
{
int r;
char* ptr;
ptr = buf;
while (1)
{
r = Read(fd, ptr, len);
if (r == 0)
return 0;
if (len-r > 0)
{
len -= r;
ptr += r;
}
else if (len-r == 0)
return len;
}
}
#endif
客户端程序
#include "net.h"
main()
{
int sockfd, r, len;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(22222);
//servaddr.sin_addr.s_addr = htonl(127.0.0.1);
Inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
printf("连接成功\n");
while(gets(recvline) != NULL)
{
len = strlen(recvline);
r = Writen(sockfd, recvline, len);
r = Readn(sockfd, recvline, len);
if (r == 0)
{
printf("服务器断开连接\n");
break;
}
recvline[r] = 0;
printf("%s\n", recvline);
}
exit(0);
}
服务端程序
#include "net.h"
main(int argc, char **argv)
{
int listenfd, connfd, r;
struct sockaddr_in servaddr;
char buf[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(22222);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENEQ);
connfd = accept(listenfd, (SA*)NULL, NULL);
int num = 0;
while(1)
{
if (num == 3)
exit(0);
r = read(connfd, buf, MAXLINE);
if (r == 0)
{
printf("客户端断开连接\n");
break;
}
buf[r] = 0;
Writen(connfd, buf, r);
num++;
}
exit(0);
}
执行结果:客户端需要第四次输入才能获得服务器断开的消息,而不能在服务器断开的时候就及时得到
用select重写,客户端就能马上得到通知
#include "net.h"
main()
{
int sockfd, r, len;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
fd_set rset;
int maxfd, nfds;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(22222);
//servaddr.sin_addr.s_addr = htonl(127.0.0.1);
Inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
printf("连接成功\n");
maxfd = sockfd;
nfds = maxfd + 1;
FD_ZERO(&rset);
while (1)
{
FD_SET(fileno(stdin), &rset);
FD_SET(sockfd, &rset);
r = select(nfds, &rset, NULL,
NULL, NULL);
if (r == -1)
{
perror("select error");
exit(-1);
}
//标注输入可读
if (FD_ISSET(fileno(stdin), &rset))
{
if(gets(recvline) != NULL)
{
len = strlen(recvline);
r = Writen(sockfd, recvline, len);
}
}
//套接字描述符可读
if (FD_ISSET(sockfd, &rset))
{
r = Readn(sockfd, recvline, len);
if (r == 0)
{
printf("服务器断开连接\n");
break;
}
recvline[r] = 0;
printf("%s\n", recvline);
}
}
exit(0);
}
执行结果:客户端在发送三次数据后,也会获得服务器断开的消息
pselect函数
POSIX定义的pselect函数与select函数的功能类似。pselect相对与select有两个变化:
1.pselect使用timespec结构,而select使用timeval结构。这两个结构的区别在于timespec结构第的二个成员tv_nsec指定纳秒,timeval结构的第二个成员为微秒。
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
2.pselect增加了一个参数,一个指向信号掩码的指针。当pselect被调用时会将这个参数指向的掩码作为替代进程的掩码,在返回时恢复进程设置的掩码。
下面的示例展示了pselect的这种特性,不在终端中输入,会使进程阻塞于select和pselect,这时向进程发SIGALRM信号,调用select的进程由于不捕获信号会退出,而调用pselect的进程会屏蔽SIGALRM信号,会继续阻塞,直到标准输入可读才返回。
调用select
#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
main()
{
int nfds, r;
fd_set rset;
FD_ZERO(&rset);
FD_SET(fileno(stdin), &rset);
nfds = fileno(stdin) + 1;
printf("pid:%d\n", getpid());
r = select(nfds, &rset, NULL, NULL, NULL);
printf("select 返回\n");
}
调用pselect
#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
main()
{
int nfds, r;
fd_set rset;
sigset_t sig_masks, pselect_masks;
sigemptyset(&sig_masks);
sigemptyset(&pselect_masks);
sigaddset(&pselect_masks, SIGALRM);
sigprocmask(SIG_SETMASK, &sig_masks, NULL);
FD_ZERO(&rset);
FD_SET(fileno(stdin), &rset);
nfds = fileno(stdin) + 1;
printf("pid:%d\n", getpid());
r = pselect(nfds, &rset, NULL,
NULL, NULL, &pselect_masks);
printf("select 返回\n");
while(1);
}