一、引言
I/O 复用是指一个进程维护多个 I/O 套接字。传统的通信可能每个套接字都需要一个进程维护,这样会占用更多的资源(进程需要分配内存),由于在服务过程中等待通信会占用大量的时候导致进程闲置,所以可以使用一个进程维护多个套接字。I/O 复用有多种方法:select、poll、epoll(Linux 专用)、kqueue(macOS/FreeBSD专用)、IOCP(Windows专用)。
二、select
select 函数是典型的实现复用的方法,Windows 系统有同名函数,移植性较好。select 函数主要功能就是监视,它可以监视多个套接字的事件,这些事件包括:可读(有数据来了),可写(之前写入的数据被读了),错误(发生了错误)。select 函数目前使用的较少,但是它可以作为其他复用技术的基础来学习。
函数原型:
1
2
3
4
5
#include <sys/select.h>
#include <sys/time.h>
// 成功返回大于 0 的值,失败返回 -1
int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);
maxfd
:监听的文件描述符数量,文件描述符(FD)集合中最大 FD 加 1。Linux 下可以传 nfds = 0
,select
会遍历 0 ~ nfds-1
的所有 FD,即使它们没有被 FD_SET
。select
受 FD_SETSIZE
限制(通常 1024)。
2.1 设置监听的套接字集合
设置套接字集合有 4 个相关的宏:
FD_ZERO(fd_set * fdset)
:将 fdset 指向的变量的所有位初始化为 0。
FD_SET(int fd, fd_set * fdset)
:在参数 fdset 指向的变量中注册文件描述符 fd 的信息。
FD_CLR(int fd, fd_set * fdset)
:从参数 fdset 指向的变量中清除文件描述符 fd 的信息。
FD_ISSET(int fd, fd_set * fdset)
:若参数 fdset 指向的变量中包含文件描述符 fd 的信息,则返回”真”。
2.2 设置超时时间
select 函数的最后一个参数用来设置该函数的超时时间。
timeval 结构体的原型如下:
1
2
3
4
struct timeval {
long tv_sec; // seconds
long tv_usec; //microseconds
}
传入 NULL 则不设置超时。
不设置超时的情况下,select 函数只有在监视到文件描述符发生变化时才返回,如果未发生变化就会进入阻塞状态。
2.3 查看结果
当监测到事件发生时,select 函数会返回一个大于 0 的整数表示产生事件的文件描述符数量。
select 函数调用完成后,向其传递的 fd_set 变量中将发生变化。原来为 1 的所有位均变为 0,但发生变化的文件描述符对应位还是 1。因此,可以认为值仍为 1 的位置上的文件描述符产生了事件。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char * argv[]) {
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads); //standard input
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
while (1) {
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
result = select(1, &temps, 0, 0, &timeout);
if (result == -1) {
puts("select() error!");
break;
} else if (result == 0) {
puts("Time-out!");
} else {
if (FD_ISSET(0, &temps)) {
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
三、Windows 上的使用
Windows 同样提供 select 函数,并且所有参数与 Linux 的 select 函数完全相同。只不过 Windows 上 select 函数的第一个参数知识为了保持兼容性而添加的,没有特殊意义。
函数原型:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <winsock2.h>
int select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * excepfds, const struct timeval * timeout);
typedef struct timeval {
long tv_sec;
long tv_usec;
} TIMEVAL;
typedef struct fd_set {
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set;
fd_set 结构体的 FD_XXX 的 4 个宏的名称、功能和使用方法与 Linux 完全相同。