Linux下select使用

目录

概述

代码示例

示例1

示例2

select 的优缺点

优点

缺点

适用场景


概述

在Linux中,select 是一个经典的IO多路复用函数,用于监控多个文件描述符(file descriptors)的状态变化(如可读、可写或异常)。它允许程序同时等待多个文件描述符,并在其中任何一个文件描述符就绪时返回。

函数原型

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:

  1. nfds

    • 需要监控的文件描述符的最大值加 1。

    • 例如,监控的文件描述符是 0、1、2,则 nfds 应为 3。

  2. readfds

    • 指向一个 fd_set 类型的文件描述符集合,用于监控是否有数据可读。

    • 如果为 NULL,则表示不监控读事件。

  3. writefds

    • 指向一个 fd_set 类型的文件描述符集合,用于监控是否可写。

    • 如果为 NULL,则表示不监控写事件。

  4. exceptfds

    • 指向一个 fd_set 类型的文件描述符集合,用于监控是否有异常事件。

    • 如果为 NULL,则表示不监控异常事件。

  5. timeout

    • 指向 struct timeval 结构体,用于设置超时时间。

    • 如果为 NULL,则表示阻塞等待,直到有文件描述符就绪。

    • 如果设置为 0,则表示非阻塞,立即返回。

返回值:

  • 成功:返回就绪的文件描述符的总数。

  • 超时:返回 0。

  • 错误:返回 -1,并设置 errno

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 是否在集合中。

struct timeval 结构体

timeout 参数是一个指向 struct timeval 的指针,定义如下:

struct timeval {
    time_t      tv_sec;   // 秒
    suseconds_t tv_usec;  // 微秒
};

select 的工作流程

  1. 初始化 fd_set 集合,并使用 FD_SET 添加需要监控的文件描述符。

  2. 调用 select 函数,阻塞等待文件描述符就绪。

  3. 当 select 返回时,使用 FD_ISSET 检查哪些文件描述符就绪。

  4. 处理就绪的文件描述符。

代码示例

示例1

以下是一个简单的 select 示例,监控标准输入(stdin)的可读事件:

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

int main() {
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(STDIN_FILENO, &read_fds); // 监控标准输入

    while (1) {
        printf("Waiting for input...\n");
        fd_set tmp_fds = read_fds; // 复制一份,因为 select 会修改 fd_set
        int ret = select(STDIN_FILENO + 1, &tmp_fds, NULL, NULL, NULL);
        if (ret == -1) {
            perror("select");
            exit(EXIT_FAILURE);
        }

        if (FD_ISSET(STDIN_FILENO, &tmp_fds)) {
            printf("Data available on stdin\n");
            char buf[1024];
            int n = read(STDIN_FILENO, buf, sizeof(buf));
            if (n > 0) {
                printf("Read: %.*s", n, buf);
            } else if (n == 0) {
                printf("EOF reached\n");
                break;
            } else {
                perror("read");
                exit(EXIT_FAILURE);
            }
        }
    }

    return 0;
}

代码说明:

  1. 使用 FD_SET 将标准输入(STDIN_FILENO)添加到 read_fds 集合中。

  2. 调用 select 阻塞等待标准输入的可读事件。

  3. 当标准输入有数据可读时,select 返回,程序读取数据并打印。

示例2

下面是一个使用select函数的简单示例,展示如何在一个TCP服务器中监听新的连接请求以及已建立连接上的数据接收。
这个例子中,服务器会创建一个TCP套接字并绑定到指定端口,然后使用select来监听新的连接请求以及已建立连接上的数据接收。

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

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_sock, client_socks[MAX_CLIENTS], max_sd, sd;
    int activity, valread, new_socket;
    fd_set readfds;
    struct sockaddr_in address;
    char buffer[BUFFER_SIZE];
    int addrlen = sizeof(address);

    // 初始化所有客户端套接字为0
    for (int i = 0; i < MAX_CLIENTS; i++) {
        client_socks[i] = 0;
    }

    // 创建socket
    if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // 绑定socket
    if (bind(server_sock, (struct sockaddr *)&address, sizeof(address)) == -1) {
        perror("Bind failed");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    // 监听socket
    if (listen(server_sock, 5) == -1) {
        perror("Listen failed");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    printf("Server is running and listening on port 8080...\n");

    while (1) {
        FD_ZERO(&readfds); // 清空文件描述符集合
        FD_SET(server_sock, &readfds); // 将服务器套接字加入集合
        max_sd = server_sock;

        // 将所有客户端套接字加入集合
        for (int i = 0; i < MAX_CLIENTS; i++) {
            sd = client_socks[i];
            if (sd > 0)
                FD_SET(sd, &readfds);
            if (sd > max_sd)
                max_sd = sd;
        }

        // 使用select等待活动
        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            perror("Select error");
        }

        // 如果是新连接请求
        if (FD_ISSET(server_sock, &readfds)) {
            if ((new_socket = accept(server_sock, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("Accept failed");
                continue;
            }
            printf("New connection, socket fd is %d , ip is : %s , port : %d \n", new_socket,
                   inet_ntoa(address.sin_addr), ntohs(address.sin_port));

            // 将新连接添加到数组中
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_socks[i] == 0) {
                    client_socks[i] = new_socket;
                    break;
                }
            }
        }

        // 检查现有连接是否有数据可读
        for (int i = 0; i < MAX_CLIENTS; i++) {
            sd = client_socks[i];
            if (FD_ISSET(sd, &readfds)) {
                // 读取数据
                if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
                    // 客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected , ip %s , port %d \n",
                           inet_ntoa(address.sin_addr), ntohs(address.sin_port));

                    // 关闭套接字并清除数组中的条目
                    close(sd);
                    client_socks[i] = 0;
                } else {
                    // 回显数据给客户端
                    send(sd, buffer, strlen(buffer), 0);
                    printf("Sent echoed message to client\n");
                }
            }
        }
    }

    close(server_sock);
    return 0;
}



代码说明:
1. 初始化:

  • 创建一个服务器套接字,并将其绑定到本地地址和端口。
  • 设置监听队列长度为5。

2. 主循环:

  • 使用FD_ZERO清空文件描述符集合并使用FD_SET将服务器套接字添加到集合中。
  • 遍历客户端套接字数组,将有效套接字添加到集合中,并更新最大文件描述符编号max_sd。
  • 调用select函数等待任何套接字有活动(即有新的连接请求或者已有连接上有数据可读)。

3. 处理新连接:

  • 当检测到服务器套接字有活动时,表示有新的连接请求到达。调用accept接受连接并将新套接字添加到客户端套接字数组中。

4. 处理已有连接的数据:

  • 遍历客户端套接字数组,检查每个套接字是否在select返回的集合中有活动。
  • 如果某个套接字有活动,则读取数据。如果读取到的数据长度为0,表示客户端已经关闭连接;否则,回显数据给客户端。

这个示例展示了如何使用select函数来管理多个连接,适用于小型服务器场景。对于更大的应用,可能需要考虑更高效的I/O多路复用机制,比如poll或epoll。

select 的优缺点

优点

  • 跨平台支持,几乎所有操作系统都实现了 select

  • 简单易用,适合监控少量文件描述符。

缺点

  • 文件描述符数量有限(通常为 1024)。

  • 每次调用需要重新设置 fd_set,效率较低。

  • 时间复杂度为 O(n),性能较差,不适合高并发场景。


适用场景

  • 监控少量文件描述符。

  • 需要跨平台兼容性的场景。

  • 对性能要求不高的场景。

如果需要高性能或监控大量文件描述符,建议使用 epoll(Linux)或 kqueue(BSD/macOS)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

W说编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值