多路IO学习笔记

目录

[一、多路 IO(IO 多路复用)](#一、多路 IO(IO 多路复用))

[1. 定义](#1. 定义)

[2. 作用](#2. 作用)

[3. 与进程 / 线程并发对比](#3. 与进程 / 线程并发对比)

[二、IO 模型分类](#二、IO 模型分类)

[三、IO 多路复用核心:select & epoll](#三、IO 多路复用核心:select & epoll)

[(1)select 详解](#(1)select 详解)

工作步骤

特性

[(2)epoll 详解](#(2)epoll 详解)

工作步骤

特性

[四、select vs epoll 对比](#四、select vs epoll 对比)

[五、代码实现:TCP 服务器(select + epoll)](#五、代码实现:TCP 服务器(select + epoll))

[1. select 版 TCP 服务器(多客户端)](#1. select 版 TCP 服务器(多客户端))

[2. epoll 版 TCP 服务器(多客户端,高性能)](#2. epoll 版 TCP 服务器(多客户端,高性能))

笔记核心总结


一、多路 IO(IO 多路复用)

1. 定义

一种 IO 处理机制,单个进程 / 线程同时监听多个文件描述符(socket、fd),内核监控 IO 事件,当某个描述符就绪(可读 / 可写)时,立即通知程序处理,避免阻塞等待。

2. 作用

  • 单线程 / 进程实现高并发 IO 处理,无需为每个连接创建进程 / 线程
  • 减少资源消耗,提升系统并发处理能力
  • 解决传统 IO 阻塞导致的效率低下问题

3. 与进程 / 线程并发对比

  • 多进程 / 线程:每个连接独立进程 / 线程,资源占用高、切换开销大、并发量有限
  • 多路 IO 复用:单线程管理所有连接,资源占用低、无切换开销、支持高并发

二、IO 模型分类

  1. 阻塞 IO:进程发起 IO 后一直阻塞,直到 IO 完成(最简单,效率低)
  2. 非阻塞 IO:进程轮询检查 IO 状态,不阻塞但 CPU 占用高
  3. 信号驱动 IO:IO 就绪后内核发送信号通知进程,异步性弱
  4. 并行模型:多进程 / 线程处理 IO,并发能力差
  5. IO 多路复用重点,单线程监听多个 IO,select/poll/epoll 是实现方式

三、IO 多路复用核心:select & epoll

(1)select 详解

工作步骤
  1. 定义并初始化文件描述符集合(fd_set),添加需要监听的 fd
  2. 循环中重置监听集合(select 会修改集合,每次循环必须重新赋值)
  3. 调用 select (),阻塞等待内核监控 IO 事件,筛选就绪 fd
  4. 遍历所有 fd,检查是否就绪
  5. 处理就绪的 IO 事件(读 / 写 / 接受连接)
特性
  • 支持跨平台,兼容性最好
  • 有最大文件描述符限制(默认 1024)
  • 每次都需要遍历全部 fd,效率随 fd 数量增加急剧下降
  • 用户态与内核态数据拷贝频繁

代码示例:

cs 复制代码
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{
  //创建有名管道
  int ret = mkfifo("myfifo", 0666);
  if (ret == -1)
  {
    if (EEXIST == errno)  //管道已经存在的错误
    {
      //程序继续
    }
    else
    {
      perror("mkfifo fail");
      return 1;
    }
  }
  //打开有名管道
  int fd = open("myfifo", O_RDONLY);
  if (fd == -1)
  {
    perror("open myfifo");
    return 1;
  }

  //创建集合(存放fd)、标志位集合
  fd_set rd_set, tmp_set;

  //添加fd到标志位集合
  FD_ZERO(&rd_set);  //集合清空
  FD_ZERO(&tmp_set);
  FD_SET(0, &tmp_set);   //把标准输入放入集合
  FD_SET(fd, &tmp_set);  //把fd放入集合

  //读管道
  while (1)
  {
    char buf[100] = {0};
    //每次循环先清除标志位
    rd_set = tmp_set;
    //等待读事件
    select(fd + 1, &rd_set, NULL, NULL, NULL);

    //判断哪个事件触发了
    int i = 0;
    for (i = 0; i < fd + 1; i++)  // i表示文件描述符
    {
      //触发事件fd
      if (FD_ISSET(i, &rd_set) && i == fd)
      {
        read(fd, buf, sizeof(buf));
        printf("fifo : %s\n", buf);
      }
      //触发事件0
      if (FD_ISSET(i, &rd_set) && i == 0)
      {
        bzero(buf, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        printf("terminal:%s\n", buf);
        fflush(stdout);
      }
    }
  }
  //关闭管道
  close(fd);
  return 0;
}

(2)epoll 详解

工作步骤
  1. epoll_create() 创建 epoll 实例
  2. epoll_ctl() 向内核添加 / 删除 / 修改需要监听的 fd
  3. epoll_wait() 阻塞等待 IO 事件,仅返回就绪 fd
  4. 遍历就绪 fd 列表,直接处理事件
  5. 循环持续监听
特性
  • Linux 专属,无最大连接数限制
  • 内核事件驱动,只返回就绪 fd,无需全量遍历
  • 用户态与内核态共享内存,减少数据拷贝
  • 高并发场景性能远超 select/poll

代码示例:

cs 复制代码
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int add_fd(int epfd, int fd)
{
  struct epoll_event ev = {0};
  //监听事件类型:可读事件
  ev.events = EPOLLIN;
  //把要监听的文件描述符存入
  ev.data.fd = fd;
  //将事件fd存入epfd
  int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
  if (ret == -1)
  {
    perror("epoll_ctl");
    return ret;
  }
  return 0;
}

int main(int argc, char **argv)
{
  //创建有名管道
  int ret = mkfifo("myfifo", 0666);
  if (ret == -1)
  {
    if (EEXIST == errno)  //管道已经存在的错误
    {
      //程序继续
    }
    else
    {
      perror("mkfifo fail");
      return 1;
    }
  }
  //打开有名管道
  int fd = open("myfifo", O_RDONLY);
  if (fd == -1)
  {
    perror("open myfifo");
    return 1;
  }

  //创建集合-二叉树
  struct epoll_event rev[2];  //用来放准备好的文件
  int epfd = epoll_create(2);
  if (epfd == -1)
  {
    perror("epoll_create");
    return -1;
  }

  //添加fd到epfd集合中
  add_fd(epfd, 0);
  add_fd(epfd, fd);

  //读管道
  while (1)
  {
    char buf[100] = {0};
    //把监听到的文件描述符存入rev数组中
    int ep_ret = epoll_wait(epfd, rev, 2, -1);
    int i = 0;
    for (i = 0; i < ep_ret; i++)
    {
      if (rev[i].data.fd == fd)  // fifo可读
      {
        read(fd, buf, sizeof(buf));
        //打印
        printf("fifo : %s\n", buf);
      }
      if (rev[i].data.fd == 0)  //标准输入 可读
      {
        bzero(buf, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        printf("terminal:%s\n", buf);
        fflush(stdout);
      }
    }
  }
  //关闭管道
  close(fd);
  return 0;
}

四、select vs epoll 对比

特性 select epoll
连接限制 有(1024) 无,受系统文件数限制
遍历方式 全量遍历所有 fd 仅遍历就绪 fd
效率 低,连接越多越慢 高,O (1) 复杂度
数据拷贝 每次都拷贝 共享内存,零拷贝
平台支持 全平台 Linux 专用
使用场景 少量连接、跨平台 高并发服务器(百万连接)

总结:select 兼容性强、性能差;epoll 性能极致、Linux 专属,是高并发首选。


五、代码实现:TCP 服务器(select + epoll)

1. select 版 TCP 服务器(多客户端)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>

#define PORT 8888
#define MAX_FD 1024

int main() {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(lfd, 128);

    fd_set read_fds, tmp_fds;
    FD_ZERO(&read_fds);
    FD_SET(lfd, &read_fds);
    int maxfd = lfd;

    while(1) {
        tmp_fds = read_fds;
        int nready = select(maxfd+1, &tmp_fds, NULL, NULL, NULL);

        if(FD_ISSET(lfd, &tmp_fds)) {
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(cliaddr);
            int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
            FD_SET(cfd, &read_fds);
            if(cfd > maxfd) maxfd = cfd;
            if(--nready == 0) continue;
        }

        for(int i = lfd+1; i <= maxfd; i++) {
            if(FD_ISSET(i, &tmp_fds)) {
                char buf[1024] = {0};
                int ret = read(i, buf, sizeof(buf));
                if(ret <= 0) {
                    close(i);
                    FD_CLR(i, &read_fds);
                } else {
                    printf("client %d: %s\n", i, buf);
                    write(i, buf, ret);
                }
                if(--nready == 0) break;
            }
        }
    }
    close(lfd);
    return 0;
}

2. epoll 版 TCP 服务器(多客户端,高性能)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

#define PORT 8888
#define MAX_EVENTS 1024

int main() {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(lfd, 128);

    int epfd = epoll_create(1);
    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    while(1) {
        int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for(int i = 0; i < nready; i++) {
            int fd = events[i].data.fd;
            if(fd == lfd) {
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
            } else {
                char buf[1024] = {0};
                int ret = read(fd, buf, sizeof(buf));
                if(ret <= 0) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                } else {
                    printf("client %d: %s\n", fd, buf);
                    write(fd, buf, ret);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

笔记核心总结

  1. IO 多路复用:单线程管理多 IO,高并发核心技术
  2. select:全平台、有限连接、全量遍历、性能低
  3. epoll:Linux 专属、无连接限制、仅返回就绪、高性能
  4. 工作流程:监听 → 等待事件 → 处理就绪 IO → 循环
  5. 应用场景:高并发 TCP 服务器、网关、聊天室等
相关推荐
码上上班42 分钟前
Mongodb课程
数据库·mongodb
农村小镇哥1 小时前
Oracle中的锁和10704对高级队列锁
数据库·oracle
峥无1 小时前
SQL性能调优:从执行计划看懂索引的使用与失效
数据库·sql
studytosky1 小时前
OpenClaw 入门与 Skill 开发
linux·服务器·ai编程
kaoa0001 小时前
Linux入门攻坚——82、kvm虚拟化-2
linux·运维·服务器
xyj29175964111 小时前
在Trae中配置数据库MCP
数据库·ai·mcp
廋到被风吹走1 小时前
【AI】【Claude】从 Prompt 到 Agent 的完整进阶地图
数据库·人工智能·prompt
数智化管理手记1 小时前
智能财务能替代人工核算吗?智能财务核心功能包含哪些?
大数据·网络·数据库·数据挖掘·精益工程
金智维科技官方2 小时前
DeepSeek接入企业系统:流程自动化新玩法
大数据·数据库·人工智能
小小龙学IT2 小时前
Tigris:下一代无服务器数据库
数据库·云原生·serverless