MST

星途 面试题库

面试题:C语言中复杂的非阻塞I/O事件驱动架构设计

设计一个高并发、高性能的应用程序架构,使用C语言在Linux系统上,基于非阻塞I/O的事件驱动模型,要求处理多种类型的I/O事件(如网络套接字、文件读写、信号等),同时要考虑资源管理、错误处理以及可扩展性,详细描述架构设计思路及关键代码实现要点。
48.9万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

架构设计思路

  1. 事件驱动模型:采用基于非阻塞I/O的事件驱动模型,使用如epoll(Linux系统下)这样的多路复用技术。epoll能高效处理大量并发连接,它将I/O事件通知给应用程序,应用程序只需处理发生事件的文件描述符。
  2. I/O事件处理
    • 网络套接字:使用非阻塞套接字进行网络通信。当有新连接到达时,接受连接并将新的套接字加入到事件监听列表;当有数据可读或可写时,进行相应的数据收发操作。
    • 文件读写:同样将文件描述符设置为非阻塞模式。在事件驱动下,当文件有数据可读或可写时,进行文件的读写操作。
    • 信号处理:注册信号处理函数,并将信号处理纳入事件驱动机制。例如,使用sigaction函数来捕获信号,然后将信号处理逻辑集成到事件循环中。
  3. 资源管理
    • 文件描述符管理:使用一个数据结构(如数组或链表)来管理所有注册到事件监听机制中的文件描述符。当某个文件描述符不再需要时,及时关闭并从管理结构中移除。
    • 内存管理:对于动态分配的内存,如用于接收网络数据或文件数据的缓冲区,在使用完毕后及时释放,避免内存泄漏。可以使用内存池技术来提高内存分配和释放的效率。
  4. 错误处理
    • I/O操作错误:在每次I/O操作(如readwriteaccept等)后检查返回值,根据错误码进行相应处理。例如,如果read操作返回-1errnoEAGAINEWOULDBLOCK,表示当前没有数据可读,这是正常的非阻塞I/O情况,继续等待下一次事件通知;如果是其他错误码,如EBADF表示文件描述符无效,需要进行适当的错误处理,如关闭文件描述符并从管理结构中移除。
    • 事件监听错误:在epoll相关操作(如epoll_createepoll_ctlepoll_wait)中,同样检查返回值。例如,epoll_create返回-1表示创建epoll实例失败,需要进行错误处理,如记录错误日志并退出程序。
  5. 可扩展性
    • 模块化设计:将不同功能模块(如网络模块、文件模块、信号处理模块)进行分离设计,每个模块有明确的接口。这样便于对单个模块进行扩展和维护,例如增加新的网络协议支持时,只需在网络模块中进行修改。
    • 动态资源分配:采用动态资源分配策略,如根据系统负载动态调整内存池大小或增加事件监听队列的容量,以适应不同规模的并发请求。

关键代码实现要点

  1. 初始化epoll实例
#include <sys/epoll.h>
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
    perror("epoll_create1");
    // 错误处理,如记录日志并退出
    exit(EXIT_FAILURE);
}
  1. 添加文件描述符到epoll监听列表
struct epoll_event event;
event.data.fd = sockfd; // 以网络套接字为例
event.events = EPOLLIN | EPOLLOUT | EPOLLET; // 监听读、写事件,采用边缘触发模式
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
    perror("epoll_ctl: add");
    // 错误处理,如关闭套接字并从管理结构中移除
}
  1. 事件循环
struct epoll_event events[max_events];
while (1) {
    int num_events = epoll_wait(epoll_fd, events, max_events, -1);
    if (num_events == -1) {
        perror("epoll_wait");
        // 错误处理,如记录日志并退出
        break;
    }
    for (int i = 0; i < num_events; ++i) {
        int fd = events[i].data.fd;
        if (events[i].events & EPOLLIN) {
            // 处理读事件,如网络套接字读数据或文件读数据
            char buffer[BUFFER_SIZE];
            ssize_t read_bytes = recv(fd, buffer, sizeof(buffer), 0);
            if (read_bytes == -1) {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;
                } else {
                    // 其他错误处理
                }
            } else if (read_bytes == 0) {
                // 连接关闭处理
            } else {
                // 处理接收到的数据
            }
        } else if (events[i].events & EPOLLOUT) {
            // 处理写事件,如网络套接字写数据或文件写数据
        }
    }
}
  1. 非阻塞套接字设置
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int flags = fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
    perror("fcntl: set non - blocking");
    // 错误处理,如关闭套接字
}
  1. 信号处理集成
#include <signal.h>
void signal_handler(int signum) {
    // 信号处理逻辑
}
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
    perror("sigaction");
    // 错误处理
}