Linux(14)(上)

一.poll

1.1 poll函数介绍
cpp 复制代码
// I/O 多路复用:监视多个文件描述符的事件(比 select 更高效、无 fd 数量限制)
// 使用 pollfd 结构数组 避免每次重建位图 适合大量连接
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// 参数:
//   fds      - 指向 pollfd 结构数组(每个元素描述一个 fd 及关注事件)
//   nfds     - 数组中元素个数
//   timeout  - 超时时间(毫秒):
//              -1 表示永久阻塞
//               0 表示立即返回(非阻塞)
//              >0 表示最多等待 timeout 毫秒

// 返回值:
//   成功:返回就绪的文件描述符数量(> 0)
//   超时:返回 0
//   失败:返回 -1 并设置 errno

// struct pollfd 成员:
//   int   fd       - 要监视的文件描述符(<0 则忽略)
//   short events   - 关注的事件(输入,如 POLLIN、POLLOUT)
//   short revents  - 实际发生的事件(输出,由 poll 填写)

// 常见事件标志:
//   POLLIN    - 数据可读(包括对端关闭)
//   POLLOUT   - 可写(缓冲区有空)
//   POLLERR   - 发生错误(自动置位,无需在 events 中设置)
//   POLLHUP   - 对端挂起(如 TCP FIN)
//   POLLNVAL  - fd 无效

// 案例:
struct pollfd pfd;
pfd.fd = sockfd;
pfd.events = POLLIN;

int ready = poll(&pfd, 1, 5000); // 等待 5 秒
if (ready > 0 && (pfd.revents & POLLIN)) 
{
   // sockfd 可读
}
1.2 poll的优点
  1. 接口更清晰

    • 使用 struct pollfd数组,每个元素(结构体)明确包含:

      • fd:要监视的文件描述符;

      • events:关注的事件(输入);

      • revents:实际发生的事件(输出);

    • 避免了 select 的"输入/输出混用同一参数"的设计,使用更方便。

  2. 无硬编码 fd 数量限制

    • 不受 FD_SETSIZE(如 1024)限制,可监视更多 fd;

    • (但 fd 数量过大时,因仍需线性遍历,性能仍会下降。)

poll的缺点

  • select 一样,poll 返回后,必须遍历整个 pollfd 数组,才能找出哪些 fd 就绪。

  • 每次调用 poll 都需将整个 pollfd 数组从用户态拷贝到内核态,开销随 fd 数量线性增长。

  • 当监视大量 fd 但只有少数活跃时 ,效率低下------时间复杂度为 O(n)

二.CMake

引入:

引入 CMake 可以解决手写 Makefile 繁琐的问题:它通过高级配置文件(CMakeLists.txt自动生成平台适配的构建文件 ,从而提升开发效率、增强跨平台能力。如何编译:首先需要创建一个CMakeLists.txt,然后在里面编写这三条代码即可完成

bash 复制代码
# 1. 指定使用的 CMake 版本
cmake_minimum_required(VERSION 3.10)

# 2. 定义项目名称
project(MyFirstProject)

# 3. 指定生成可执行文件,以及对应的源文件
# 格式:add_executable(生成的文件名 源文件1 源文件2 ...)
add_executable(hello Main.cc)

//还可以设置C++的标准
# 4. 设置 C++ 标准(例如 C++11)
set(CMAKE_CXX_STANDARD 11)

使用:

bash 复制代码
cmake .#这样就会生成一堆配置文件,然后后面就生成了Makefile文件
       #然后就是Makefile文件的使用了

三.epoll

引入:

  • epoll 的引入是为了解决 poll(以及 select)在高并发场景下的性能瓶颈。虽然 poll 已解决了 select 的 fd 数量限制等问题,但其线性遍历和用户/内核间重复拷贝的缺陷无法通过用户态改进。
  • 因此,epoll 在内核中重新设计了事件通知机制 ,不再基于"每次传全量 fd 集合",而是采用注册-回调+就绪列表 的方式,使得它与 select/poll实现模型和扩展性上本质不同
3.1 系统接口
3.1.1 epoll_create1
cpp 复制代码
// 创建 epoll 实例(epoll_create 的增强版 支持标志控制)
// 推荐替代 epoll_create 以获得更安全的文件描述符行为
#include <sys/epoll.h>

int epoll_create1(int flags);

// 参数:
//   flags - 控制创建行为(通常为 0 或 EPOLL_CLOEXEC)
//           EPOLL_CLOEXEC: 设置 close-on-exec 标志 防止子进程继承

// 返回值:
//   成功:返回 epoll 文件描述符(非负整数)
//   失败:返回 -1 并设置 errno

// 优势:
//   - 支持原子地设置 FD_CLOEXEC 避免多线程中 fd 泄露到子进程
//   - 无需指定无用的 size 参数

// 案例:
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == -1) 
{
   // 错误处理
}
3.1.2 epoll_ctl
cpp 复制代码
// 控制 epoll 实例:向其中添加、修改或删除要监视的文件描述符
#include <sys/epoll.h>

//1
int epoll_ctl(
int epfd,
int op,
int fd,
struct epoll_event *event
);

// 参数:
//   epfd   - 由 epoll_create1() 返回的 epoll 文件描述符
//   op     - 操作类型:
//              EPOLL_CTL_ADD  添加 fd 到 epoll 实例
//              EPOLL_CTL_MOD  修改已注册 fd 的事件
//              EPOLL_CTL_DEL  从 epoll 实例中删除 fd
//   fd     - 要操作的目标文件描述符(如 socket)
//   event  - 指向 epoll_event 结构(EPOLL_CTL_DEL 时可为 NULL)

// 返回值:
//   成功:返回 0
//   失败:返回 -1 并设置 errno

struct epoll_event类型介绍

cpp 复制代码
// 表示 epoll 监视的事件及其关联数据
// 用于 epoll_ctl 注册事件 和 epoll_wait 返回就绪事件
#include <sys/epoll.h>

struct epoll_event 
{
   uint32_t     events;  // 监听或发生的事件标志(如 EPOLLIN)采用位图形式
   epoll_data_t data;    // 用户数据(union,可存 fd、指针等)
};

// 成员说明:
//   events - 位掩码 指定关注或已触发的事件(输入/输出)
//            常用值:
//              EPOLLIN    数据可读
//              EPOLLOUT   可写
//              EPOLLET    边缘触发模式(仅在 epoll_ctl 时设置)
//              EPOLLERR   错误(自动触发)
//              EPOLLHUP   对端关闭(自动触发)
//
//   data   - 用户自定义数据 内核原样返回(见 epoll_data_t)

// 使用场景:
//   - 调用 epoll_ctl 时:events 为要监听的事件,data 为用户绑定的数据
//   - 调用 epoll_wait 后:events 为实际发生的事件,data 为之前绑定的数据

epoll_data_t类型介绍

cpp 复制代码
// epoll_data_t 是 union 类型 用于在 epoll_event 中携带用户自定义数据
// 允许将文件描述符、指针或其他整数与事件关联 便于回调时识别来源
#include <sys/epoll.h>

typedef union epoll_data 
{
   void        *ptr;     // 通用指针(常用)
   int          fd;      // 文件描述符(最常见用法)
   uint32_t     u32;
   uint64_t     u64;
} epoll_data_t;

// 说明:
//   - 四个成员共享同一块内存 只能使用其中一个
//   - 最常用的是 .fd(直接存 socket fd)或 .ptr(指向自定义结构体)
//   - 内核不解释该字段 原样返回给用户

// 典型用法:
struct epoll_event ev;
ev.events = EPOLLIN;

// 方式1:存 fd
ev.data.fd = client_sockfd;
3.1.3 epoll_wait
cpp 复制代码
// 等待 epoll 实例中注册的文件描述符上的事件发生
// 阻塞(或限时阻塞)直到有事件就绪或超时
#include <sys/epoll.h>

int epoll_wait(
int epfd,
struct epoll_event *events,
int maxevents,
int timeout
);

// 参数:
//   epfd       - epoll 实例的文件描述符(由 epoll_create1 返回)
//   events     - 指向 epoll_event 数组 用于接收就绪事件
//   maxevents  - events 数组的最大容量(必须 > 0)
//   timeout    - 超时时间(毫秒):
//                 -1:永久阻塞
//                  0:立即返回(非阻塞)
//                >0:最多等待 timeout 毫秒

// 返回值:
//   成功:返回就绪事件的数量(>= 0)
//   失败:返回 -1 并设置 errno(如被信号中断)

// 说明:
//   - 内核将就绪事件填充到 events 数组中
//   - 每个事件包含触发的事件类型(events)和用户绑定的数据(data)
//   - 在边缘触发(ET)模式下 必须一次性读完所有数据

// 案例:
struct epoll_event events[64];
int nready = epoll_wait(epfd, events, 64, -1);
if (nready > 0) {
   for (int i = 0; i < nready; ++i) 
   {
       if (events[i].events & EPOLLIN) 
       {
           int fd = events[i].data.fd;
           // 读取 fd 上的数据
       }
   }
}
3.2 epoll原理
  1. 硬件中断通知数据到达 网卡收到数据帧后,通过 硬件中断 通知 CPU,触发内核的网络协议栈处理。

  2. 驱动与协议栈触发回调 数据经网卡驱动和 TCP/IP 协议栈处理后,若对应 socket 有数据可读, 内核会调用该 socket 关联的 ep_poll_callback 回调函数 (由 epoll 注册)。

  3. 内核维护两个核心数据结构

    • 红黑树 (rb-tree):存储所有通过 epoll_ctl(EPOLL_CTL_ADD) 注册的 fd 及其关注的事件,用于快速查找和管理。EPOLL_CTL_ADD 添加 fd 到 epoll 实例

    • 就绪队列 (ready list):当 fd 就绪时,其对应的 epitem 被加入此链表,epoll_wait 直接从此队列返回就绪事件,无需遍历全部 fd 。epitem是红黑树和就绪队列共用的节点结构,其具有红黑树和就绪队列所需的属性字段

  4. epoll和文件描述符怎么扯上关系的

    • 每次调用 epoll_create()(或 epoll_create1())都会在内核中创建一个独立的 struct eventpoll 对象 ;每个 struct eventpoll 包含自己的一棵红黑树和一个就绪队列

    • 这个 struct eventpoll 会被封装在一个 struct file 中,并在当前进程的文件描述符表 中分配一个 fd;因此,epoll_create1() 返回的是一个 int 类型的文件描述符(fd),用户通过它操作对应的 epoll 实例。

3.3 epoll的优势
  1. 接口更高效

    • 拆分为 epoll_createepoll_ctlepoll_wait 三个函数;

    • 关注的 fd 只需注册一次EPOLL_CTL_ADD),无需每次循环重设,且由内核进行管理fd

    • 输入与输出参数分离 ,避免 select/poll 的覆盖问题。

  2. 数据拷贝轻量

    • epoll_ctl 仅在增删 fd 时将描述符信息拷贝到内核;

    • epoll_wait 调用无需重复拷贝全量 fd 集合,大幅减少用户态 ↔ 内核态开销。

  3. 事件驱动,无遍历

    • 基于回调机制ep_poll_callback):fd 就绪时自动加入就绪队列;

    • epoll_wait 直接返回就绪列表,时间复杂度 O(1) 每个活跃 fd,不随总 fd 数增长。

  4. 无硬性数量限制

    • 仅受限于系统内存和进程 fd 上限
  5. 要点:

    selectpoll 采用轮询驱动,因此每次调用需要内核跑一遍所监视的文件描述符

    selectpoll 需要用户自己管理文件描述符集合

    epoll能完美解决这些问题,因此它是最常用的,这两个只是用于没有epoll的环境

四.epoll的工作方式

1.epoll的两种工作方式:

  1. 水平触发(LT)

  2. 边缘触发(ET)

2.两种工作用生活方式讲解:

假设你有 5 个快递放在驿站(相当于 socket 接收缓冲区中有数据)。

  • 张三(LT 模式) : 只要还有快递没取完 (缓冲区非空),他就会一直打电话催你 (每次 epoll_wait 都返回该 fd 可读),直到你把所有快递拿光。

  • 李四(ET 模式) : 他只在快递数量发生变化时打一次电话 (比如从 0 → 5,或 3 → 6)。 如果你第一次只取了 3 个(没读完),剩下的 2 个他不会再通知你 ! 除非又有新快递到来(缓冲区状态再次变化),他才会再打一次电话。

3.例子

  • 我们已经把一个tcp socket添加到epoll描述符

  • 这个时候socket的另一端被写入了2KB的数据

  • 调用epoll_wait,并且它会返回. 说明它已经准备好读取操作

  • 然后调用read, 只读取了1KB的数据

4.水平触发

  • epoll 检测到 socket 上有数据可读(如缓冲区非空),就会通知;

  • 即使只处理了部分数据(如 2KB 中读了 1KB);

  • 下次调用 epoll_wait 时,会立即再次返回该 fd 的可读事件

  • 只有当缓冲区中所有数据都被读完epoll_wait 才会阻塞或等待新数据;

  • 支持阻塞和非阻塞 I/O,编程更简单、容错性高。

5.边缘触发

  • epoll 仅在 socket 状态发生变化时通知一次(如从无数据 → 有数据);

  • 如例子中:收到 2KB 数据后,epoll_wait 返回一次;

  • 若只读取 1KB,剩余 1KB 不会再触发通知

  • 后续调用 epoll_wait 不会返回该 fd ,除非又有新数据到达(状态再次变化);

  • 因此,必须在收到通知后一次性读完所有可用数据 (通常用循环 read 直到返回 EAGAIN);

  • 必须使用非阻塞 I/O ,否则最后一次 read 可能阻塞;

  • 性能更高 (减少 epoll_wait 返回次数),Nginx、Redis 等高性能服务默认使用 ET 模式。

6.对比LT和ET

  • LT(水平触发)

    • epoll默认模式(select和poll同样也是水平触发);

    • 只要 fd 处于就绪状态(如接收缓冲区非空),每次调用 epoll_wait 都会返回该事件

    • 允许分多次处理数据,编程简单、容错性强。

  • ET(边缘触发)

    • 仅在 fd 状态发生变化时通知一次(如从不可读 → 可读);

    • 强制要求程序在一次就绪通知中将所有可用数据处理完 (通常需循环读写直到 EAGAIN);

    • 必须使用非阻塞 I/O,否则可能永久阻塞;

    • 减少了 epoll_wait 的唤醒次数,在高并发、大量连接但低活跃度场景下理论性能更高

cpp 复制代码
 //在 Linux 的 epoll 中,边缘触发(Edge-Triggered, ET)
 //模式是通过在 epoll_event.events 中设置 EPOLLET 标志来启用的。
 struct epoll_event ev;
 ev.events = EPOLLIN | EPOLLET;   // 关键:加上 EPOLLET
 ev.data.fd = sockfd;

 // 注册到 epoll 实例
 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

五.从实际出发理解ET

  • 使用 ET 模式的 epoll,需要将文件描述符设置为非阻塞。这个不是接口上的要求,而是 "工程实践"上的要求
  • **假设这样的场景:**服务器接收到一个 10KB 的请求,会向客户端返回一个应答数据。如果客户端收不到应答,就不会发送第二个 10KB 请求。
  • 如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,参考 man 手册的说明, 可能被信号打断),剩下的9k数据就会待在缓冲区中
  • 此时由于 epoll 是ET模式,并不会认为文件描述符读就绪。epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中。直到下一次客户端再给服务器写数据。epoll_wait 才能返回

问题:

  • 服务器只读到1KB个数据, 要10KB读完才会给客户端返回响应数据。

  • 客户端要读到服务器的响应才会发送下一个请求

  • 客户端发送了下一个请求,epoll_wait 才会返回, 服务器才能去读缓冲区中剩余的数据.

  • 因此,系统会陷入"客户端等服务器响应,服务器等客户端新数据"的死锁状态。为避免此问题,在 ET(边缘触发)模式下,必须将文件描述符设为非阻塞 ,并在事件触发后循环读取,直到内核缓冲区中无剩余数据。

  • 具体做法是:不断调用 read,直到它返回 -1errnoEAGAINEWOULDBLOCK------这表示本次可读数据已全部读完,而非发生真正的错误。

  • 需要特别注意:这个错误码判断不是为了确认文件描述符是否就绪 (因为 epoll/select 等 I/O 多路复用机制只在 fd 就绪时才通知),而是为了区分"正常读空"和"真实网络错误read-1errno 是其他值(如 ECONNRESETEPIPE 等),则说明发生了连接异常,需关闭连接。

那关于poll和epoll的知识就讲到这里了,希望大家好好学习,关于epoll的难点还是比较多的,首先需要理解它的系统接口,又要理解epoll的原理以及它的优势,最后就是它的工作方式!

相关推荐
小同志002 小时前
简单了解 JVM
网络·jvm
海盗猫鸥2 小时前
Linux基础指令2
linux·c语言
白太岁2 小时前
通信:(2) TCP/UDP、流量/拥塞控制、ARP 与 Socket 应用
网络·c++·tcp/ip·udp
小义_2 小时前
【RH134知识点问答题】第11章 管理网络安全
linux·安全·web安全·云原生
nnbulls12 小时前
Linux环境下Tomcat的安装与配置详细指南
linux·运维·tomcat
unirst19850072 小时前
Mysql官网下载Windows、Linux各个版本
linux·数据库·mysql
嵌入小生0072 小时前
线程 --- 嵌入式(Linux)
linux·vscode·vim·嵌入式·线程·进程
wsad05322 小时前
Linux Shell脚本执行方式全解析:source、点号、路径、bash与exec的区别
linux·运维·bash·shell
Whoami!2 小时前
⓬⁄₅ ⟦ OSCP ⬖ 研记 ⟧ Linux权限提升 ➱ 寻觅暴露的机密信息实现提权
linux·网络安全·信息安全·权限提升