I/O复用 select系统调用
select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
select API
select 系统调用的定义如下:
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct ti
meval *timeout);
select 成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select 将返回 0。select 失败是返回-1.如果在 select 等待期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。
maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所有文件描述符中的最大值+1。
readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
fd_set 结构如下:
#define __FD_SETSIZE 1024
typedef long int __fd_mask;
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
typedef struct
{
#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 结构中的位:
void FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
void FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
void FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置
timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval结构的定义如下:
struct timeval
{
long tv_sec; //秒数
long tv_usec; // 微秒数
};
如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递NULL,则 select 将一直阻塞,直到某个文件描述符就绪。
文件描述符的就绪条件
哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。在网络编程中,下列情况下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 上接收到带外数据(紧急数据)。
select 原理分析
由上面的定义可见,fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
要把关注的文件描述符集合添加到fd_set集合中可以这样处理:
int SetFdToFdset(fd_set *fdset, int fds[], int maxfd)
{
FD_ZERO(fdset);
int i = 0, n = fds[0];
for (; i < maxfd; ++i)
{
if (fds[i] != -1)
{
FD_SET(fds[i], fdset);
if (fds[i] > n)
{
n = fds[i];
}
}
}
return n;
}
fdset参数是要添加到达fdset集合的指针,fds是要添加的文件描述符数组,n是数组的长度。
用户通过readfds、writefds 和 exceptfds 三个参数分别传入感兴趣的可读、可写和异常事件,内核通过对这三个参数的在线修改来反馈其中的就绪事件。
select采用轮询方式来检测fd_set结构体的数据位,当数据位由0变1时,所在位注册的文件描述符关注的事件就绪。
每次调用select时都要把这三个参数传给内核,且每次循环都可能修改这三个参数,所以在每次调用select之前都要重置这三个参数。
select的处理过程如下:
select的代码示例
使用select实现的TCP服务器代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#define MAX_FD 128
#define DATALEN 1024
// 初始化服务器端的 sockfd 套接字
int InitSocket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1) return -1;
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");
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(res == -1) return -1;
res = listen(sockfd, 5);
if(res == -1) return -1;
return sockfd;
}
// 初始化记录服务器套接字的数组
void InitFds(int fds[], int n)
{
int i = 0;
for (; i < n; ++i)
{
fds[i] = -1;
}
}
// 将套接字描述符添加到数组中
void AddFdToFds(int fds[], int fd, int n)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
// 删除数组中的套接字描述符
void DelFdFromFds(int fds[], int fd, int n)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
//将数组中的套接字描述符设置到 fd_set 变量中,并返回当前最大的文件描述符值
int SetFdToFdset(fd_set *fdset, int fds[], int n)
{
FD_ZERO(fdset);
int i = 0, maxfd = fds[0];
for (; i < n; ++i)
{
if (fds[i] != -1)
{
FD_SET(fds[i], fdset);
if (fds[i] > maxfd)
{
maxfd = fds[i];
}
}
}
return maxfd;
}
void GetClientLink(int sockfd, int fds[], int n)
{
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if (c < 0)
{
return;
}
printf("A client connection was successful\n");
AddFdToFds(fds, c, n);
}
// 处理客户端数据
void DealClientData(int fds[], int n, int clifd)
{
char data[DATALEN] = { 0 };
int num = recv(clifd, data, DATALEN - 1, 0);
if (num <= 0)
{
DelFdFromFds(fds, clifd, n);
close(clifd);
printf("A client disconnected\n");
}
else
{
printf("%d: %s\n", clifd, data);
send(clifd, "OK", 2, 0);
}
}
// 处理 select 返回的就绪事件
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] != -1 && FD_ISSET(fds[i],fdset))
{
if (fds[i] == sockfd)
{
GetClientLink(sockfd, fds, n);
}
else
{
DealClientData(fds, n, fds[i]);
}
}
}
}
int main()
{
int sockfd = InitSocket();
assert(sockfd != -1);
fd_set readfds;
int fds[MAX_FD];
InitFds(fds, MAX_FD);
AddFdToFds(fds, sockfd, MAX_FD);
while ( 1 )
{
int maxfd = SetFdToFdset(&readfds, fds,MAX_FD);
struct timeval timeout;
timeout.tv_sec = 2; // 秒数
timeout.tv_usec = 0; //微秒数
int n = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (n < 0)
{
printf("select error\n");
break;
}
else if (n == 0)
{
printf("time out\n");
continue;
}
DealReadyEvent(fds, MAX_FD, &readfds, sockfd);
}
exit(0);
}
测试结果: