select 函数是一种常见的 I/O 多路复用技术,使用 select 函数,通知内核挂起进程,当一个或多个 I/O 事件发生后,控制权返还给应用程序,由应用程序进行 I/O 事件的处理。
这些 I/O 事件的类型非常多,比如:
标准输入文件描述符准备好可以读。
监听套接字准备好,新的连接已经建立成功。
已连接套接字准备好可以写。
如果一个 I/O 事件等待超过了 10 秒,发生了超时事件。
select函数的声明如下:
int select(int nfds, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
参数说明:
- nfds:监听的所有文件描述符中,最大文件描述符+1,表待监听文件符的数量,文件描述符下标从0开始。*
- readset:读文件描述符监听集合,传入、传出参数
- writeset:写文件描述符监听集合,传入、传出参数
- exceptset:异常文件描述符监听集合,传入、传出参数
- timeout :定时器
- 大于0:设置监听超时时长
- NULL:阻塞监听
- 0:非阻塞监听,轮询
- 返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
那么如何设置这些描述符集合呢?以下的宏可以帮助到我们。
void FD_ZERO(fd_set *fdset); //清空一个文件描述符集合
void FD_SET(int fd, fd_set *fdset); //将待监听的文件描述符,添加进监听集合中
void FD_CLR(int fd, fd_set *fdset); //将一个文件描述符从监听集合中移除
int FD_ISSET(int fd, fd_set *fdset); //判断一个文件描述符是否在监听集合中
怎么理解了,举例说明一下,下面一个向量代表了一个描述符集合,其中,这个向量的每个元素都是二进制数中的 0 或者 1。
a[maxfd-1], ..., a[1], a[0]
可以按照下面的思路来理解这些宏:
- FD_ZERO 用来将这个向量的所有元素都设置成 0;
- FD_SET 用来把对应套接字 fd 的元素,a[fd]设置成 1;
- FD_CLR 用来把对应套接字 fd 的元素,a[fd]设置成 0;
- FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd]是 0 还是 1。
其中 0 代表不需要处理,1 代表需要处理。
下面是一个具体的程序例子:
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: select01 ");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
char recv_line[MAXLINE], send_line[MAXLINE];
int n;
fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads); //一开始全为空
FD_SET(0, &allreads); //标准输入置为1
FD_SET(socket_fd, &allreads); //连接套接字3置为1
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
if (rc <= 0) {
error(1, errno, "select failed");
}
if (FD_ISSET(socket_fd, &readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read error");
} else if (n == 0) {
error(1, 0, "server terminated \n");
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
if (FD_ISSET(STDIN_FILENO, &readmask)) {
if (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt < 0) {
error(1, errno, "write failed ");
}
printf("send bytes: %zu \n", rt);
}
}
}
}
这里需要注意的是17-18行:
第 17 行是每次测试完之后,重新设置待测试的描述符集合。你可以看到上面的例子,在 select 测试之前的数据是{0,3},select 测试之后就变成了{0}。
这是因为 select 调用每次完成测试之后,内核都会修改描述符集合,通过修改完的描述符集合来和应用程序交互,应用程序使用 FD_ISSET 来对每个描述符进行判断,从而知道什么样的事件发生。
第 18 行则是使用 socket_fd+1 来表示待测试的描述符基数。切记需要 +1。
当我们说 select 测试返回,某个套接字准备好可读,表示什么样的事件发生呢?
总结成一句话就是,内核通知我们套接字有数据可以读了,使用 read 函数不会阻塞。
说完了套接字可读,再来看套接字可写。select 检测套接字可写,完全是基于套接字本身的特性来说的,具体来说有以下几种情况。
这里估计,很多人有些疑惑,跟我一样,我之前一直是从应用程序的角度去理解套接字可写的:当程序完成了相应的计算,有数据准备发送给对端时,可以往套接字写,对应的就是套接字可写。
总结成一句话就是,内核通知我们套接字可以往里写了,使用 write 函数就不会阻塞。
主要知识点:
select的优缺点:
缺点:
- 监听上限受文件描述符限制,最大为1024
- 检测满足条件的fd,自己添加业务逻辑提高小,提高了编码难度
优点:
- 跨平台,win、linux、macOS、类unix等