解析 select 函数

解析 select 函数

select 函数是 Unix/Linux 系统中用于多路复用的系统调用,主要用于在多个文件描述符(file descriptors)上等待事件的发生。它允许程序同时监视多个 I/O 通道,并在任意一个通道准备好进行 I/O 操作时通知程序,从而实现高效的 I/O 处理。

函数原型
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds: 监视的文件描述符集合中最大的文件描述符值加1。
  • readfds: 指向文件描述符集合的指针,监视其中是否有可读事件。
  • writefds: 指向文件描述符集合的指针,监视其中是否有可写事件。
  • exceptfds: 指向文件描述符集合的指针,监视其中是否有异常事件。
  • timeout: 超时时间,指定 select 等待的最长时间。如果为 NULL,则 select 会一直阻塞直到有事件发生。
fd_set 结构

fd_set 是一个文件描述符集合,通常使用以下宏来操作:

  • FD_ZERO(fd_set *set): 清空文件描述符集合。
  • FD_SET(int fd, fd_set *set): 将文件描述符 fd 添加到集合中。
  • FD_CLR(int fd, fd_set *set): 从集合中移除文件描述符 fd
  • FD_ISSET(int fd, fd_set *set): 检查文件描述符 fd 是否在集合中。

工作原理

select 的工作流程如下:

  1. 初始化文件描述符集合: 使用 FD_ZERO 初始化 readfdswritefdsexceptfds
  2. 添加监视的文件描述符: 使用 FD_SET 将需要监视的文件描述符添加到相应的集合中。
  3. 调用 select: select 会阻塞,直到至少有一个文件描述符准备好进行 I/O 操作,或者超时。
  4. 检查结果: 使用 FD_ISSET 检查哪些文件描述符准备好了,并根据需要进行处理。

应用场景

1. 网络服务器

描述: 网络服务器需要同时处理多个客户端连接。select 可以监视所有客户端的套接字,检测哪些套接字有数据可读或可写。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define PORT 8080
#define MAX_CLIENTS 1024

int main() {
    int server_fd, new_socket, client_sockets[MAX_CLIENTS];
    fd_set read_fds;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    int max_sd;

    // 初始化客户端套接字数组
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_sockets[i] = 0;
    }

    // 创建服务器套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定套接字
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&read_fds);

        // 添加服务器套接字到集合中
        FD_SET(server_fd, &read_fds);
        max_sd = server_fd;

        // 添加客户端套接字到集合中
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (sd > 0) {
                FD_SET(sd, &read_fds);
            }
            if (sd > max_sd) {
                max_sd = sd;
            }
        }

        // 调用 select
        int activity = select(max_sd + 1, &read_fds, NULL, NULL, NULL);
        if (activity < 0) {
            perror("select error");
            exit(EXIT_FAILURE);
        }

        // 检查是否有新连接
        if (FD_ISSET(server_fd, &read_fds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            printf("New connection, socket fd is %d\n", new_socket);
            // 添加到客户端数组中
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    break;
                }
            }
        }

        // 检查客户端套接字是否有数据可读
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (FD_ISSET(sd, &read_fds)) {
                char buffer[1024] = {0};
                int valread = read(sd, buffer, 1024);
                if (valread == 0) {
                    // 客户端断开连接
                    printf("Connection closed, socket fd is %d\n", sd);
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    // 处理数据
                    printf("Received message: %s\n", buffer);
                    // 回显
                    send(sd, buffer, valread, 0);
                }
            }
        }
    }
    return 0;
}
2. 终端应用

描述: 终端应用需要同时处理用户输入和定时事件。select 可以监视标准输入和定时器文件描述符。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>

int main() {
    fd_set read_fds;
    struct timeval timeout;

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&read_fds);
        // 添加标准输入到集合中
        FD_SET(STDIN_FILENO, &read_fds);

        // 设置超时时间为10秒
        timeout.tv_sec = 10;
        timeout.tv_usec = 0;

        int activity = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);
        if (activity < 0) {
            perror("select");
            break;
        } else if (activity == 0) {
            printf("Timeout occurred!\n");
        } else {
            if (FD_ISSET(STDIN_FILENO, &read_fds)) {
                char buffer[100];
                int n = read(STDIN_FILENO, buffer, sizeof(buffer));
                if (n > 0) {
                    buffer[n] = '\0';
                    printf("You entered: %s\n", buffer);
                }
            }
        }
    }
    return 0;
}

优缺点

优点
  1. 简单易用: select 提供了一种简单的方法来监视多个文件描述符。
  2. 跨平台: 在大多数 Unix/Linux 系统上都有实现,具有良好的可移植性。
  3. 灵活性: 可以监视不同类型的 I/O 事件(读、写、异常)。
缺点
  1. 性能问题: select 的性能在处理大量文件描述符时较差,因为每次调用都需要遍历所有监视的文件描述符。
  2. 文件描述符数量限制: select 通常有一个上限(通常是 1024),对于需要监视大量连接的应用不适用。
  3. 可读性差: 使用 select 的代码通常较为复杂,难以维护。

替代方案

由于 select 的局限性,现代应用中更常使用以下替代方案:

  1. poll:

    • 描述: poll 提供了与 select 类似的功能,但使用不同的接口,支持更大的文件描述符集合。
    • 优点: 没有文件描述符数量限制,性能优于 select
    • 缺点: 仍然需要遍历所有文件描述符,效率提升有限。
  2. epoll (Linux):

    • 描述: epoll 是 Linux 特有的高效 I/O 多路复用机制,适用于处理大量并发连接。
    • 优点: 高效,支持边缘触发和水平触发,事件驱动。
    • 缺点: 仅适用于 Linux 系统。
  3. kqueue (BSD/macOS):

    • 描述: kqueue 是 BSD 系统(如 macOS)上的高效 I/O 多路复用机制。
    • 优点: 高效

select 函数在以下情况下会检测写就绪:

  1. 发送缓冲区有足够空间

    • 当发送缓冲区中有足够的空间来容纳要写入的数据时,select 会将对应的文件描述符标记为写就绪。这意味着对文件描述符执行写操作不会阻塞【5†source】【8†source】。
  2. 写操作被关闭

    • 当对文件描述符的写操作被关闭(例如,通过 closeshutdown 函数)时,对这个写操作被关闭的文件描述符进行写操作会触发 SIGPIPE 信号。在这种情况下,select 会将文件描述符标记为写就绪【5†source】【8†source】。
  3. 非阻塞 connect 操作完成

    • 对于非阻塞的 connect 操作,当连接成功或失败时,select 会将文件描述符标记为写就绪【8†source】。
  4. 异常事件

    • 虽然主要与写操作相关,但 select 也会检测异常事件。例如,当 socket 收到带外数据时,select 会将文件描述符标记为异常就绪【8†source】。

select监听写就绪

  • 发送缓冲区空间

    • 当发送缓冲区的空闲空间大于或等于低水位标记 SO_SENDLOWAT 时,select 会将文件描述符标记为写就绪。这意味着程序可以无阻塞地写入数据【5†source】。
  • 写操作关闭

    • 如果对文件描述符的写操作被关闭,尝试写入数据会导致 SIGPIPE 信号。在这种情况下,select 会将文件描述符标记为写就绪,以便程序可以处理这种情况【5†source】。
  • 非阻塞 connect

    • 对于非阻塞的 connect 操作,select 可以用来检测连接是否成功或失败。当连接操作完成时,select 会将文件描述符标记为写就绪【8†source】。

示例代码

以下是一个简单的示例,展示了如何使用 select 来检测写就绪:

#include <stdio.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 尝试连接
    int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) {
        perror("connect");
        close(sockfd);
        return 1;
    }

    fd_set writefds;
    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;

    FD_ZERO(&writefds);
    FD_SET(sockfd, &writefds);

    ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
    if (ret < 0) {
        perror("select");
        close(sockfd);
        return 1;
    } else if (ret == 0) {
        printf("Timeout occurred!\n");
    } else {
        if (FD_ISSET(sockfd, &writefds)) {
            printf("Socket is ready for writing!\n");
        }
    }

    close(sockfd);
    return 0;
}

在这个示例中,select 会等待最多5秒钟,直到 sockfd 准备好进行写操作。如果 sockfd 准备好,select 会返回,并通过 FD_ISSET 检查是否就绪。

总结

select 函数通过监视文件描述符的读写和异常状态,提供了一种有效的多路复用机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值