IO复用之 select

Posted by KalosAner on January 21, 2025

一、引言

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 = 0select 会遍历 0 ~ nfds-1 的所有 FD,即使它们没有被 FD_SETselectFD_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 完全相同。