IO多路复用技术使得程序运行时可同时监听多个文件描述符,对于现实中出现的实际情况有重要意义。多路复用通过三中不同的系统调用——select、poll、epoll来实现,通常来说以下情况需要使用多路复用:
1、客户端同时处理多个客户端请求;
2、客户端程序需要同时处理用户操作和网络连接;
3、TCP服务器同时处理监听socket和连接socket;
4、服务器同时处理UDP和TCP请求;
5、服务器同时监听多个端口或处理多种服务(xinetd服务器)。
一、select API
select系统调用的主要用途是:在指定的一段时间内,轮询监听用户需要的文件描述符(用户添加到fd_set中的),当监听到的文件描述符传来可读、可写或异常事件发生时就会返回。
轮询:不断地循环监听0~记录文件描述符的最大值;
fd_set:是一个仅包含一个整型数组的结构体,该整型数组每个元素的每个位代表一个文件描述符,所以数组的总位数决定select可同时处理文件描述符最大数量,该最大值被定义在<sys/select.h>头文件中的FD_SETSIZE常量。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds指定被监听文件描述符的总数,因为文件描述符通常从0开始计数,因此nfds通常为readfd,writefd,exceptfd这三个描述符集中的最大描述符编号加1;
readfd,writefd,exceptfd分别指向可读、可写、异常事件对应的文件描述符集合。应用程序调用select时,通过这三个参数传入需要监听的文件描述符,轮询等待有事件产生。select调用返回时内核将修改他们来通知应用进程那些文件描述符对应事件产生,这三个参数是fd_set结构指针类型,其定义如下:
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Some versions of <linux/posix_typeshygfdfsdgfhjkl.h> define these macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace.*/
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
由定义我们可看到fd_set结构体中仅包含一个整型数组,该数组中每个元素的每个bit都标记一个文件描述符,且可容纳的文件描述符数量由FD_SETSIZE指定,这限制了select能同时处理文件描述符的数量。
系统抽象了几个函数来访问操作fd_set结构体中的位:
void FD_ZERO(fd_set *set); /* 清除fd_set的所有位 */
void FD_SET(int fd, fd_set *set); /* 设置fd_set的位fd */
void FD_CLR(int fd, fd_set *set); /* 清除fd_set的位fd */
int FD_ISSET(int fd, fd_set *set); /* 测试fd_set的位fd是否被设置 */
struct timeval {
long tv_sec; //秒数
long tv_usec; //微秒数
};
timeout参数来设置select函数得超时时间,表示select愿意等待多长时间,他是一个timeval类型的结构体指针,我们不能完全依赖select调用返回后timeout的值,如果调用失败时timeout的值不确定:
如果我们给timeval成员都赋值为0,则select立即返回;如果timeout为NULL,则select一直阻塞直到某个文件描述符就绪。
select返回值
select成功时返回就绪文件描述符的总数,若超时时间内没有文件描述符就绪则返回0,调用失败返回-1并设置errno,在select等待期间,若程序收到信号则立刻返回-1,此时设置errno为EINTR。
理解select模型:
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待---读取
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
二、文件描述符就绪条件
在网络编程中下列情况下socket可读:
①socket内核接收缓存区中字节数大于或等于其低水位标记SO_RCVLOWAT,此时可无阻塞读socket,并读操作返回字节数大于0;
②socket通信对方关闭连接,此时可对该socket读操作将返回0;
③监听socket有新的连接请求;
④socket有未处理的错误,我们可通过getsockopt读取和清除错误
下列情况socket可写:
①socket内核发送缓冲区可用字节数大于或等于其低水位标记SO_SNDLOWAT,我们可无阻塞写该socket,并写操作返回字节数大于0;
②socket写操作被关闭,写操作被关闭时socket再进行写操作会触发一个SIGPIPE信号。
③socket使用非阻塞connect连接成功或失败后;
④socket有未处理的错误我们可通过getsockopt读取和清除错误。
网络编程中,select可处理的异常情况只有:socket上接收到带外数据。
三、程序示例
1、标准输入读取键盘
该程序演示了select函数使用方法,程序读取标准输入,超时时间设置为3s,只有在输入就绪是才会读取,这样可通过添加其他文件描述符(管道、套接字)扩展。下列程序通过select调用来检查标准输入状态,程序通过设置超时时间每隔3秒打印timeout,通过select调用返回0判断,实现如下:
// select系统调用:读取键盘
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/select.h>
#define STDIN 0 /* 标准输入:文件描述符为0 */
int main()
{
/* fd为标准输入文件描述符 0 */
int fd = STDIN;
fd_set fdset;
while (1)
{
/* fdset初始化为空集合 */
FD_ZERO(&fdset);
/* 在集合中添加fd传递的描述符 */
FD_SET(fd, &fdset);
/* 超时时间设置为5s,即在标准输入stdin上最多等待5s */
struct timeval tv = { 3, 0 };
/* select返回值为n,表示有n个状态发生变化的描述符 */
int n = select(fd + 1, &fdset, NULL, NULL, &tv);
/* select失败返回-1并设置errno */
if (n == -1)
{
perror("select error");
}
/* select为0表示文件描述符都没有变化,那么打印出超时信息 */
else if (n == 0)
{
printf("time out\n");
}
else
{
/* 判断参数fd指向的文件描述符是否是由参数
** fdset指向的fd_set集合中的一个元素 */
if (FD_ISSET(fd, &fdset))
{
char buffer[128] = { 0 };
/* 从标准输入stdin读取数据到buffer中 */
int res = read(fd, buffer, 127);
/* 将buffer中的数据打印 */
printf("read(%d) = %s\n", res, buffer);
}
}
}
}
运行结果如下;
root@Ubuntu-14:~$ ./select
time out
time out
hello
read(6) = hello
time out
simon
read(6) = simon
2、改进多客户端并发访问服务器
// select系统调用:改进的多客户/服务器
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<sys/select.h>
#define MAXFD 10
/* 向集合fds中添加文件描述符fd */
void fds_add(int fds[], int fd)
{
int i = 0;
for (; i < MAXFD; i++)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
/* 在集合fds中删除文件描述符fd */
void fds_del(int fds[], int fd)
{
int i = 0;
for (; i < MAXFD; i++)
{
if (fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
int main()
{
/* 创建监听套接字(socket描述符),指定协议族ipv4,字节流服务传输 */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
/* socket专用地址信息设置 */
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* 命名套接字,将socket专用地址绑定到socket描述符上 */
int res = bind(sockfd, (struct sockaddr*) & saddr, sizeof(saddr));
assert(res != -1);
/* 创建监听队列 */
listen(sockfd, 5);
fd_set fdset;
/* 初始化文件描述符集合fds */
int fds[MAXFD];
int i = 0;
for (; i < MAXFD; i++)
{
fds[i] = -1;
}
/* 将sockfd添加到文件描述符集合fds中 */
fds_add(fds, sockfd);
while (1)
{
/* fdset初始化为空集合,清除fdset的所有位 */
FD_ZERO(&fdset);
int maxfd = -1;
int i = 0;
/* 循环遍历找到最大的文件描述符 */
for (; i < MAXFD; i++)
{
if (fds[i] == -1)
{
continue;
}
/* 设置fdset的fds[i]位 */
FD_SET(fds[i], &fdset);
if (fds[i] > maxfd)
{
maxfd = fds[i];
}
}
/* 设置超时时间为5秒 */
struct timeval tv = { 5,0 };
/* 用n来接收select成功时返回就绪文件描述符的总数 */
int n = select(maxfd + 1, &fdset, NULL, NULL, &tv);
/* select失败返回-1并设置errno */
if (n == -1)
{
perror("select error");
}
/* select为0表示文件描述符都没有变化,那么打印出超时信息 */
else if (n == 0)
{
printf("time out\n");
}
else
{
/* 遍历所有可能的文件描述符,以检查是哪个上面有活动发生。*/
for (i = 0; i < MAXFD; i++)
{
if (fds[i] == -1)
{
continue;
}
/* fdset的fds[i]位已经被标识,即有数据 */
if (FD_ISSET(fds[i], &fdset))
{
/*
** 此时有两种情况,若fds[i] == sockfd
** 说明监听队列中有连接待处理,则使用accept拿出一个连接
** 否则,没有新连接,我们直接使用recv接收客端数据,并打印
*/
if (fds[i] == sockfd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
/* 接收一个套接字已建立的连接,得到连接套接字connfd */
int connfd = accept(sockfd, (struct sockaddr*) & caddr, (socklen_t *)&len);
if (connfd < 0)
{
continue;
}
printf("accept connfd=%d\n", connfd);
/* 将连接套接字connfd,添加到fds文件描述集合中 */
fds_add(fds, connfd);
}
else
{
char buff[128] = { 0 };
/* recv用来接收客端数据*/
int res = recv(fds[i], buff, 127, 0);
/* 接收服务器端的数据是零,即res返回0,说明客户端已经关闭 */
if (res <= 0)
{
/* 关闭文件描述符fds[i] */
close(fds[i]);
/* 清除fds数组 */
fds_del(fds, fds[i]);
printf("one client over\n");
}
else
{
/* 输出客端发来的数据,并向客端发送一个OK的回复 */
printf("recv(%d)=%s\n", fds[i], buff);
send(fds[i], "ok", 2, 0);
}
}
}
}
}
}
}
运行结果如下:
accept connfd=4
recv(4)=hello world
accept connfd=5
recv(5)=hello world
time out
one client over
one client over
time out
通过对多个文件描述符的监听,可实现select多客户访问。
while (1)
{
printf("agent running!");
if ((ret = agent_check_socket(&state_server)) < 0)
{
printf("agent's network:%d\n", ret);
os_sock_close_server_ota(state_server.socket);
continue;
}
server_fd = os_sock_fd_get_by_sid(state_server.socket);
if (g_server_fd != server_fd) /*recard the server fd*/
{
printf("g_server_fd:%d,server_fd:%d",g_server_fd, server_fd);
FD_ZERO(&inset);
FD_SET(server_fd, &inset);
max_fd = server_fd;
g_server_fd = server_fd;
}
tmp_inset = inset;
if (!(select(max_fd + 1, &tmp_inset, NULL, NULL, NULL) > 0))
{
printf("select error");
close(server_fd);
g_server_fd = 0;
continue;
}
for (fd = 0; fd < max_fd + 1; fd++)
{
if (FD_ISSET(fd, &tmp_inset) > 0)
{
if (fd == server_fd)
{
if ((c_client_fd = accept(server_fd, (struct sockaddr *)&client_sockaddr, &sin_size))== -1)
{
printf("accept error!");
break;
}
FD_SET(c_client_fd, &inset);
max_fd = ((max_fd) > (c_client_fd) ? (max_fd) : (c_client_fd));
printf("(%d)New connection\n", c_client_fd);
}
else
{
memset(&recvBuffer, 0, sizeof(recvBuffer));
if ((count = recv(fd, &recvBuffer, sizeof(recvBuffer), 0)) > 0)
{
printf("[%d]Received a message from code-%d:%d bytes", fd, recvBuffer.opt_code,count);
agent_confirm_entry_fd(fd,(void *)&recvBuffer);
}
else
{
close(fd);
FD_CLR(fd, &inset);
printf("(%d)Client has left(%d) ", fd,count);
}
}
}
}
}