多路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 服务器、网关、聊天室等
相关推荐
Nturmoils7 小时前
订单列表慢查询,先看 WHERE、ORDER BY 和 LIMIT
数据库
渣波11 小时前
拒绝 SQL 焦虑!手把手带你用 NestJS + Prisma + DTO 写出“防弹”级后端代码
javascript·数据库·后端
倔强的石头_2 天前
KingbaseES 新版MySQL 兼容版体验:旧版迁移 + 功能实测
数据库
zzzzzz3102 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
倔强的石头_4 天前
《Kingbase护城河》——数据库存储空间全景探测与精细化瘦身实战
数据库
冬奇Lab5 天前
每日一个开源项目(第134篇):Zvec - 阿里开源的嵌入式向量数据库,向量搜索界的 SQLite
数据库·人工智能·llm
ClouGence5 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
无响应de神5 天前
三、用户与权限管理
数据库·mysql
大树886 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
小宇宙Zz6 天前
Maven依赖冲突
java·服务器·maven