多路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 服务器、网关、聊天室等
相关推荐
上海云盾第一敬业销售14 分钟前
服务器遭受攻击的应对策略及快速防护实践
运维·服务器·web安全·ddos
AI人工智能+电脑小能手6 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
yyuuuzz6 小时前
独立站的技术基础与常见运维问题
大数据·运维·服务器·网络·数据库·aws
键盘上的猫头鹰9 小时前
【MySQL 教程(八)】索引、事务、用户管理、导入导出与分页查询
数据库·python·mysql
Royzst9 小时前
数据库知识点
数据库
雪的季节10 小时前
企业级 Qt 全功能项目
开发语言·数据库·qt
宋浮檀s10 小时前
应急响应——Web漏洞:命令执行+SSRF+弱口令
运维·数据库·sql·网络安全·oracle·应急响应
日取其半万世不竭10 小时前
iftop、nethogs 和 nload:Linux 服务器网络流量实时监控工具介绍
linux·运维·服务器
mounter62510 小时前
Linux 内核资源管理:控制组(cgroup)的演进与“策略组”新提案
linux·运维·服务器·cgroup·kernel
bksczm10 小时前
文件在磁盘中的存储方式
linux·运维·服务器