目录

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

1. 定义

2. 作用

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

二、IO 模型分类

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

(1)select 详解

工作步骤

特性

(2)epoll 详解

工作步骤

特性

四、select vs epoll 对比

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

1. select 版 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 数量增加急剧下降
  • 用户态与内核态数据拷贝频繁

代码示例:

#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

代码示例:

#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 服务器、网关、聊天室等
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐