多路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 服务器、网关、聊天室等
相关推荐
u01091476033 分钟前
CSS组件库如何快速扩展_通过Sass @extend继承基础布局
jvm·数据库·python
baidu_3409988237 分钟前
Golang怎么用go-noescape优化性能_Golang如何使用编译器指令控制逃逸分析行为【进阶】
jvm·数据库·python
m0_6784854538 分钟前
如何利用虚拟 DOM 实现无痕刷新?基于 VNode 对比的状态保持技巧
jvm·数据库·python
qq_3422958241 分钟前
CSS如何实现透明背景效果_通过RGBA色彩模式控制透明度
jvm·数据库·python
panzer_maus1 小时前
MySQL 索引介绍与索引优化的简单介绍
数据库·mysql
Greyson11 小时前
CSS如何处理超长文本换行问题_结合word-wrap属性
jvm·数据库·python
captain3761 小时前
事务___
java·数据库·mysql
justjinji1 小时前
如何批量更新SQL数据表_使用UPDATE JOIN语法提升效率
jvm·数据库·python
郝亚军1 小时前
ubuntu通过samba,让win11可以访问其共享文件夹
linux·服务器·ubuntu
农村小镇哥2 小时前
nginx服务器的介绍
运维·服务器·nginx