IO多路复用-select详细介绍

1.概念与原理

        定义:I/O 多路复用是一种同步 I/O 模型,它允许单个进程同时监视多个文件描述符(如套接字),以确定它们是否准备好进行 I/O 操作(可读、可写或出现异常)。select是一种在 Unix 和类 Unix 系统中实现 I/O 多路复用的系统调用。

        工作原理select函数会阻塞进程,直到被监视的文件描述符集合中的一个或多个满足指定的条件(可读、可写或异常)。它维护了三个文件描述符集合:readfds(可读)、writefds(可写)和exceptfds(异常)。当调用select时,内核会检查这些集合中的文件描述符状态,当有满足条件的文件描述符时,select返回,应用程序可以通过遍历集合来确定具体是哪些文件描述符就绪。

2.函数原型与参数

#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数解释

  • nfds:是所有集合(readfdswritefdsexceptfds)中最大文件描述符值加 1。这个参数限制了内核在检查文件描述符时的范围。
  • readfds:指向一个fd_set结构的指针,用于监视可读事件的文件描述符集合。如果一个文件描述符在这个集合中,并且对应的 I/O 设备(如套接字)有数据可读,那么select会返回并且这个文件描述符会在返回后的readfds集合中被标记为就绪。
  • writefds:指向一个fd_set结构的指针,用于监视可写事件的文件描述符集合。如果一个文件描述符在这个集合中,并且对应的 I/O 设备(如套接字)可以写入数据(例如,套接字的发送缓冲区有足够的空间),那么select会返回并且这个文件描述符会在返回后的writefds集合中被标记为就绪。
  • exceptfds:指向一个fd_set结构的指针,用于监视异常事件的文件描述符集合。异常情况可能包括带外数据到达等情况。
  • timeout:指向一个struct timeval结构的指针,用于指定select的超时时间。如果这个参数为NULLselect会一直阻塞,直到有文件描述符就绪。struct timeval结构有两个成员:tv_sec(秒)和tv_usec(微秒),用于指定超时的时间长度。

3.fd_set结构与操作函数

fd_set结构fd_set是一个用于存储文件描述符集合的结构。它在内部是一个位向量,每个位对应一个文件描述符。在实际使用中,通常不需要直接操作fd_set的内部结构,而是通过一组宏来操作。

操作宏

  • FD_ZERO(fd_set *set):将set所指向的fd_set清空,即把所有位都设置为 0,用于初始化一个文件描述符集合。
  • FD_SET(int fd, fd_set *set):将文件描述符fd添加到set所指向的fd_set中,即将对应的位置 1。
  • FD_CLR(int fd, fd_set *set):将文件描述符fdset所指向的fd_set中清除,即将对应的位置 0。
  • FD_ISSET(int fd, fd_set *set):检查文件描述符fd是否在set所指向的fd_set中,如果在则返回 1,否则返回 0。

4.使用步骤与示例

步骤

  1. 首先,使用FD_ZERO初始化readfdswritefdsexceptfds(根据需要)。
  2. 然后,使用FD_SET将需要监视的文件描述符添加到相应的集合中。
  3. 接着,设置timeout参数来指定超时时间(如果需要)。
  4. 调用select函数,等待文件描述符就绪。
  5. select返回后,使用FD_ISSET检查哪些文件描述符就绪,并进行相应的 I/O 操作。

示例(简单的 TCP 服务器使用select处理多个客户端连接)

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

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_socket, client_sockets[MAX_CLIENTS];
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;
    fd_set readfds;
    int max_fd;
    char buffer[BUFFER_SIZE];

    // 创建服务器套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        return 1;
    }

    // 初始化服务器地址结构
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    // 绑定服务器套接字到地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Binding failed");
        close(server_socket);
        return 1;
    }

    // 监听连接请求
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        perror("Listening failed");
        close(server_socket);
        return 1;
    }

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

    while (1) {
        // 每次循环开始时重新初始化文件描述符集合
        FD_ZERO(&readfds);
        // 将服务器套接字添加到可读集合中
        FD_SET(server_socket, &readfds);
        max_fd = server_socket;

        // 将客户端套接字添加到可读集合中
        for (int i = 0; i < MAX_CLIENTS; i++) {
            if (client_sockets[i]!= -1) {
                FD_SET(client_sockets[i], &readfds);
                if (client_sockets[i] > max_fd) {
                    max_fd = client_sockets[i];
                }
            }
        }

        // 调用select等待文件描述符就绪
        int activity = select(max_fd + 1, &readfds, NULL, NULL, NULL);
        if (activity == -1) {
            perror("Select error");
            return 1;
        } else if (activity == 0) {
            // 超时情况,这里没有设置超时,所以一般不会进入这个分支
            continue;
        } else {
            // 检查服务器套接字是否可读,即有新的连接请求
            if (FD_ISSET(server_socket, &readfds)) {
                client_addr_len = sizeof(client_addr);
                int new_client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
                if (new_client_socket == -1) {
                    perror("Accept failed");
                } else {
                    // 将新连接的客户端套接字添加到数组中
                    for (int i = 0; i < MAX_CLIENTS; i++) {
                        if (client_sockets[i] == -1) {
                            client_sockets[i] = new_client_socket;
                            break;
                        }
                    }
                }
            }

            // 检查客户端套接字是否可读,即有数据可读
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i]!= -1 && FD_ISSET(client_sockets[i], &readfds)) {
                    int bytes_read = read(client_sockets[i], buffer, BUFFER_SIZE);
                    if (bytes_read <= 0) {
                        // 客户端关闭连接或读取错误
                        close(client_sockets[i]);
                        client_sockets[i] = -1;
                    } else {
                        buffer[bytes_read] = '\0';
                        // 在这里可以对读取的数据进行处理,例如打印或转发等
                        printf("Received from client: %s", buffer);
                        // 简单地回显数据给客户端
                        write(client_sockets[i], buffer, strlen(buffer));
                    }
                }
            }
        }
    }

    // 关闭服务器套接字
    close(server_socket);
    return 0;
}

示例解释

1.首先创建一个 TCP 服务器套接字,绑定到指定端口并监听连接请求。

2.然后进入一个无限循环,在每次循环中:

  • 初始化readfds集合,将服务器套接字添加进去,并遍历客户端套接字数组,将非-1(表示有效的客户端连接)的客户端套接字也添加到readfds集合中,同时记录最大的文件描述符值。
  • 调用select函数等待文件描述符就绪。
  • 如果select返回后,首先检查服务器套接字是否在readfds集合中(即是否有新的连接请求),如果是,则接受新连接并将新的客户端套接字添加到客户端套接字数组中。
  • 接着遍历客户端套接字数组,检查每个客户端套接字是否在readfds集合中(即是否有数据可读),如果是,则读取数据,处理数据(这里是简单地回显),并根据读取结果判断是否关闭连接。

5.优缺点

优点

  • 可以同时处理多个文件描述符,提高服务器的并发处理能力,适用于处理大量的 I/O 操作,如在网络服务器中处理多个客户端连接。
  • 跨平台性较好,在许多 Unix 和类 Unix 系统中都有支持。

缺点

  • 每次调用select都需要将文件描述符集合从用户空间复制到内核空间,并且在返回时还要从内核空间复制回用户空间,当文件描述符数量很大时,这种复制操作会带来较大的开销。
  • 单个进程可监视的文件描述符数量有限,通常由FD_SETSIZE定义,虽然可以通过一些手段修改这个限制,但会增加系统的复杂性。
  • 每次调用select都需要遍历所有的文件描述符集合来确定哪些文件描述符就绪,当文件描述符数量很大时,效率较低。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值