IO多路复用原理分析

『AI先锋杯·14天征文挑战第9期』 10w+人浏览 170人参与

介绍

什么是IO多路复用?

IO多路复用是一种高效的IO处理技术,它允许单个线程同时监控多个文件描述符(如网络连接、文件、管道等),当其中任何一个准备好读写操作时,系统会立即通知程序进行处理。这种机制避免了传统阻塞IO中每个连接都需要独立线程等待的问题,极大提升了并发处理能力。具体来说,当程序调用select、poll或epoll等系统调用时,内核会同时检查所有被监控的文件描述符状态,只返回那些已经就绪的IO操作,这样程序就能在非阻塞的情况下高效处理成千上万个并发连接,是现代高并发服务器的核心技术基础。

  • 传统阻塞IO的问题
// 每个连接需要一个线程
func handleConn(conn net.Conn) {
    buf := make([]byte, 1024)
    // 这里会阻塞等待数据
    n, err := conn.Read(buf)  // 线程挂起,直到有数据
    // 处理数据...
}
  • IO多路复用:一个线程监控所有连接
// 一个线程监控所有连接
for {
    // 询问内核:哪些连接有数据了?
    readyConns := poll()  // 返回有数据的连接列表
    
    for _, conn := range readyConns {
        go handleData(conn)  // 只处理有数据的连接
    }
}

类型

select

  • 在传统阻塞IO模型下,每个网络连接都需要独立线程阻塞等待数据,造成大量线程资源浪费;而select机制通过IO多路复用技术,实现了单线程对最多1024个连接的高效批量监控,将分散的阻塞等待转化为统一的事件驱动处理,显著提升了网络IO的并发处理能力和系统吞吐量。下面是一个select的使用示例
// 用户空间:告诉内核要监控哪些fd
FD_SET(fd1, &readfds);   // 监控fd1读
FD_SET(fd2, &readfds);   // 监控fd2读  
FD_SET(fd3, &writefds);  // 监控fd3写

// 进入内核:阻塞等待事件
select(nfds, &readfds, &writefds, &exceptfds, timeout);

// 返回后:内核告诉你哪些fd就绪了
if (FD_ISSET(fd1, &readfds)) {
    // fd1有数据了,可以读了
}
if (FD_ISSET(fd3, &writefds)) {
    // fd3可以写了
}
  • 内核层发生了什么?
// 简化版内核逻辑
int do_select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {
    while (1) {
        int retval = 0;
        
        // 遍历所有fd(0到n-1)
        for (int i = 0; i < n; i++) {
            struct file *file = fget(i);
            if (!file) continue;
            
            // 检查这个fd是否有读事件
            if (readfds && FD_ISSET(i, readfds)) {
                if (file->f_op->poll(file) & POLLIN) {
                    FD_SET(i, readfds);  // 标记为就绪
                    retval++;
                }
            }
            
            // 检查写事件...
            // 检查异常事件...
        }
        
        if (retval > 0) return retval;  // 有fd就绪了
        if (timeout && time_after(timeout)) return 0;  // 超时
        schedule();  // 让出CPU,睡眠等待
    }
}
  • 可以看出,内核层每次需要遍历所有的fd。为什么最多只能等待1024个连接?因为select使用bitmap来存储文件描述符,且这个长度由于历史包袱,被限制死1024了。不好改,所以情况就是这么个情况~
// POSIX标准规定:
#define FD_SETSIZE 1024
#include <sys/select.h>
#include <stdio.h>

int main() {
    fd_set readfds;
    
    // 尝试设置第1024个fd
    FD_SET(1023, &readfds);  // ✅ 成功
    
    // 尝试设置第1025个fd
    FD_SET(1024, &readfds);  // ❌ 数组越界!
    
    printf("FD_SETSIZE = %d\n", FD_SETSIZE);  // 输出:1024
    return 0;
}

poll

  • poll机制针对select的1024连接限制进行了关键改进,摒弃了固定大小的位图结构,转而采用动态数组来存储文件描述符信息,这种设计不仅突破了连接数量的硬性上限,使其能够轻松支持成千上万的并发连接,还提供了更灵活的事件管理能力,为大规模网络应用奠定了基础架构优势。像下面这样
#include <poll.h>

int main() {
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    bind(listen_sock, ...);
    listen(listen_sock, 5);
    
    // poll用数组,不是位图
    struct pollfd fds[2000];  // ✅ 可以轻松超过1024
    int nfds = 0;
    
    // 添加监听socket
    fds[0].fd = listen_sock;
    fds[0].events = POLLIN;   // 监听读事件
    fds[0].revents = 0;       // 内核返回的事件
    nfds = 1;
    
    while (1) {
        // 调用poll
        int ret = poll(fds, nfds, -1);
        
        if (ret > 0) {
            // 检查监听socket
            if (fds[0].revents & POLLIN) {
                // 有新连接
                int new_client = accept(listen_sock, ...);
                
                // 添加到poll数组
                fds[nfds].fd = new_client;
                fds[nfds].events = POLLIN;
                fds[nfds].revents = 0;
                nfds++;
            }
            
            // 检查客户端socket
            for (int i = 1; i < nfds; i++) {
                if (fds[i].revents & POLLIN) {
                    // 有数据
                    char buffer[1024];
                    int n = read(fds[i].fd, buffer, sizeof(buffer));
                    if (n <= 0) {
                        // 客户端断开,从数组移除
                        close(fds[i].fd);
                        // 用最后一个元素覆盖当前元素
                        fds[i] = fds[nfds-1];
                        nfds--;
                        i--; // 重新检查当前位置
                    } else {
                        handle_data(buffer, n);
                    }
                }
            }
        }
    }
}
  • 但是轮询所有的fd的问题,依然存在呢~~时间复杂度还是O(n)的

epoll

  • epoll在poll基础上实现了革命性优化,其命名中的"e"代表事件(event),彰显其事件驱动的核心设计理念。与poll的主动轮询机制不同,epoll采用被动事件通知模式,通过内核维护的就绪队列和回调机制,仅当文件描述符状态发生变化时才触发处理,这种设计不仅避免了无效遍历,更将时间复杂度从O(n)降至O(1),使单线程能够高效管理数十万并发连接,真正实现了高性能的IO多路复用。下面是一段示例代码
// ep_poll - epoll的核心函数
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, 
                  int maxevents, struct timespec *timeout) {
    int res, avail;
    struct epitem *epi;
    struct epoll_event __user *eventpoll_buf;
    unsigned long flags;
    
    // 关键:直接获取就绪队列!O(1)复杂度!
    mutex_lock(&ep->mtx);
    avail = ep->ovflist != EP_UNACTIVE_PTR;
    if (!avail)
        avail = ep->rdllink.next != &ep->rdllist;  // 检查就绪队列是否为空
    mutex_unlock(&ep->mtx);
    
    if (avail) {
        // 有就绪的fd,直接拷贝给用户空间
        res = ep_send_events(ep, events, maxevents);
        return res;
    }
    
    // 没有就绪的fd,睡眠等待
    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        
        // 再次检查就绪队列
        mutex_lock(&ep->mtx);
        avail = ep->ovflist != EP_UNACTIVE_PTR;
        if (!avail)
            avail = !list_empty(&ep->rdllist);
        mutex_unlock(&ep->mtx);
        
        if (avail || timed_out)
            break;
            
        // 睡眠等待事件
        if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
            timed_out = 1;
    }
    
    __set_current_state(TASK_RUNNING);
    return ep_send_events(ep, events, maxevents);
}

// ep_send_events - 发送就绪事件
static int ep_send_events(struct eventpoll *ep, struct epoll_event __user *events, 
                         int maxevents) {
    struct epitem *epi, *tmp;
    struct epoll_event event;
    unsigned long flags;
    int cnt = 0;
    
    // 关键:只遍历就绪队列!不是遍历所有fd!
    list_for_each_entry_safe(epi, tmp, &ep->rdllist, rdllink) {
        // 检查事件是否仍然有效
        if (ep_item_poll(epi, &pt)) {
            // 拷贝到用户空间
            __put_user(epi->event.events, &event.events);
            __put_user(epi->event.data, &event.data);
            __copy_to_user(&events[cnt++], &event, sizeof(event));
            
            if (cnt >= maxevents)
                break;
        }
    }
    
    return cnt;
}
// 每个epoll实例
struct eventpoll {
    spinlock_t lock;           // 自旋锁
    struct mutex mtx;          // 互斥锁
    wait_queue_head_t wq;      // 等待队列
    struct list_head rdllist;  // 关键:就绪fd链表!
    struct rb_root_cached rbr; // 红黑树,存储所有监听的fd
    struct epitem *ovflist;    // 溢出链表
};

// 每个监听的fd
struct epitem {
    union {
        struct rb_node rbn;    // 红黑树节点
        struct rcu_head rcu;
    };
    struct list_head rdllink;  // 就绪链表节点
    struct epitem *next;       // 溢出链表
    struct epoll_filefd ffd;   // 文件描述符信息
    int nwait;                 // 等待队列数量
    struct list_head pwqlist;  // 等待队列
    struct eventpoll *ep;      // 所属的epoll实例
    struct list_head fllink;   // 文件链表
    struct epoll_event event;  // 事件类型
};
  • 下面是添加一个fd到epoll的例子。当fd就绪的时候,它会触发一个回调,添加到就绪队列里面,使用这种事件驱动的方式,把O(n)优化到了O(1)。这样就使得效率大大提升了
// ep_insert - 添加fd到epoll
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
                    struct file *tfile, int fd) {
    struct epitem *epi;
    struct ep_pqueue epq;
    
    // 分配epitem
    epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);
    
    // 初始化epitem
    epi->ep = ep;
    epi->event = *event;
    epi->ffd.file = tfile;
    epi->ffd.fd = fd;
    
    // 添加到红黑树(O(log n))
    ep_rbtree_insert(ep, epi);
    
    // 设置回调函数 - 关键!
    epq.epi = epi;
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    
    // 调用文件的poll函数,注册回调
    revents = tfile->f_op->poll(tfile, &epq.pt);
    
    // 如果已经有事件,直接添加到就绪队列
    if (revents & event->events) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
    }
    
    return 0;
}

// 回调函数 - fd就绪时被调用
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                                 poll_table *pt) {
    struct ep_pqueue *epq = container_of(pt, struct ep_pqueue, pt);
    struct epitem *epi = epq->epi;
    
    // 添加到等待队列,设置回调
    add_wait_queue(whead, &epi->wait);
}
  • golang的网络轮询器,就是基于epoll的实现,这使得golang的io效率非常高,让用户的同步代码能够享受到异步的性能
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Clarence Liu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值