MST

星途 面试题库

面试题:C语言中Linux多路复用实现事件监听基础

在Linux环境下,使用C语言通过select函数实现多路复用进行事件监听时,描述fd_set数据结构的作用以及如何正确初始化和使用它。
50.1万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

fd_set数据结构的作用

fd_set是在多路复用I/O中用于表示一组文件描述符的结构体。它允许程序在一个或多个文件描述符上等待I/O事件,如可读、可写或异常情况。通过fd_set,可以方便地管理和检查多个文件描述符的状态,避免逐个轮询文件描述符,从而提高效率。

初始化fd_set

在使用fd_set之前,需要先进行初始化。在POSIX系统中,可以使用FD_ZERO宏来清空一个fd_set集合,将所有位都设置为0。示例代码如下:

#include <sys/select.h>

fd_set read_fds;
FD_ZERO(&read_fds);

添加文件描述符到fd_set

要将文件描述符添加到fd_set集合中,可以使用FD_SET宏。这个宏会将指定文件描述符对应的位设置为1。例如,将标准输入(文件描述符为0)添加到read_fds集合中:

FD_SET(0, &read_fds);

从fd_set中移除文件描述符

若要从fd_set集合中移除文件描述符,可以使用FD_CLR宏。它会将指定文件描述符对应的位设置为0。示例:

FD_CLR(0, &read_fds);

检查fd_set中的文件描述符状态

select函数返回后,可以使用FD_ISSET宏来检查某个文件描述符是否在事件发生的集合中。如果指定的文件描述符在集合中,FD_ISSET返回非零值,否则返回0。示例:

if (FD_ISSET(0, &read_fds)) {
    // 标准输入有数据可读
}

在select函数中使用fd_set

select函数使用fd_set来监听多个文件描述符的事件。函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要检查的文件描述符集合中最大文件描述符值加1。
  • readfds:指向一个fd_set结构体,用于检查可读性的文件描述符集合。
  • writefds:指向一个fd_set结构体,用于检查可写性的文件描述符集合。
  • exceptfds:指向一个fd_set结构体,用于检查异常情况的文件描述符集合。
  • timeout:设置select等待的最长时间。如果设置为NULLselect将一直阻塞,直到有事件发生。

示例代码,监听标准输入和一个socket描述符的可读事件:

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

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_fd, new_socket, valread;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    fd_set read_fds;
    fd_set tmp_fds;
    int activity;

    // 创建socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &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);

    // 绑定socket到指定地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

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

    // 初始化文件描述符集合
    FD_ZERO(&read_fds);
    FD_ZERO(&tmp_fds);

    // 添加标准输入和server_fd到集合
    FD_SET(0, &read_fds);
    FD_SET(server_fd, &read_fds);

    while (1) {
        tmp_fds = read_fds;
        activity = select(server_fd + 1, &tmp_fds, NULL, NULL, NULL);

        if ((activity < 0) && (errno!= EINTR)) {
            printf("select error");
        } else if (activity) {
            if (FD_ISSET(server_fd, &tmp_fds)) {
                if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                FD_SET(new_socket, &read_fds);
            }
            if (FD_ISSET(0, &tmp_fds)) {
                fgets(buffer, 1024, stdin);
                printf("Input: %s", buffer);
            }
            // 处理其他客户端socket的可读事件
            for (int i = 0; i < server_fd + 1; i++) {
                if (FD_ISSET(i, &tmp_fds) && i!= server_fd && i!= 0) {
                    valread = read(i, buffer, 1024);
                    buffer[valread] = '\0';
                    printf("Message from client: %s\n", buffer);
                }
            }
        }
    }
    return 0;
}

在上述代码中,通过select函数使用fd_set同时监听标准输入和socket连接的可读事件,实现了多路复用I/O。