LLM-ReAct 搜索 Agent 实验

实验元数据 (Meta Data) 实验编号/标题:例如:LLM-ReAct 搜索 Agent 实验 ...

LLM-智能翻译助手-实验

实验元数据 (Meta Data) 实验编号/标题:例如:LLM-智能翻译助手-实验 ...

Transformer 实验

实验元数据 (Meta Data) 实验编号/标题:Transformer 实验 ...

LLM 复习

预备知识 - 预备知识 - 线性代数 - 向量与矩阵乘法 - 向量与向量乘法 - 内积 - 计算:对应元素相乘再相加,结果是一个标量 - $a\cdot b=\sum_{i=1}^n a_ib_i$ - 几何意义:衡量两个向量的相似度 - 如果内积为 0,向量正交 - 内积也可以表示为 $||a||||b||cos(\theta)$,即 a 在 b 方向上的投影长度乘以 b 的长度 - 外积 - 计算:一个列向量乘以一个行向量,结果是一个矩阵 - 意义:构造一个秩为 1 的矩阵。在 SVD 中,复杂的举证就是由多个这样的秩 1 矩阵加权累加的 - 列空间 - 定义 - 想象矩阵 A 的每一列都是一个导航箭头(向量) - 如果 A 有两列 a1,a2,那么这两根箭头张开所能到达的所有地方,就是一个平面 - 这个平面就是矩阵 A 的列空间 - 列空间就是利用矩阵里的列向量,通过加减、缩放所能拼凑出的所有可能的结果向量的集合 - 矩阵与向量的乘法 - 假设有矩阵 A 和向量 x - 线性组合 - 矩阵 A 乘以向量 x,等价于将 A 的列向量按照 x 中的元素进行加权求和 - 结果向量 b 必然落在矩阵 A 的列空间内 - 线性变换 - 将矩阵 A 看作一个函数或算子,它把输入向量 x 旋转、伸缩或投影到了一个新的位置 - 特征值/特征向量的学习,本质就是在寻找这个变换中方向不变的特殊向量 - 矩阵与向量的乘法 - 行乘列(传统定义) - C 中第 i 行第 j 列的元素,是 A 的第 i 行与 B 的第 j 列的内积 - 列变换视角 - 把 B 看作一组列向量[b1,b2,...,bn],那么 AB 的结果就是[Ab1,Ab2,...,Abn] - 意义:矩阵 A 同时对 B 的每一列进行了相同的线性变换 - 分块矩阵乘法 - 将大矩阵划分为子矩阵进行运算 - 是计算机高性能计算和处理大数据时的核心原理 - 关键运算性质 - 不满足交换律:一般情况下 AB!=BA,矩阵乘法的顺序至关重要 - 满足结合律:A(BC)=(AB)C,意味着在计算长链乘法时,可以通过改变计算顺序来优化计算量 - 转置性质:$(AB)^T=B^TA^T$ - 范数 - 定义:范数是一个将向量映射到非负实数的函数,直观可以理解为衡量向量的大小或长度 - 没有范数,无法定义距离,也无法优化模型 - 性质 - 非负性:||x||>=0,且只有当 x 是零向量时,范数才是 0 - 齐次性:$||kx||=|k|\cdot ||x||$,向量放大 k 倍,长度也放大 k 倍 - 三角不等式:||x+y||<=||x||+||y||,两边之和大于第三边 - 常见的向量范数 - 公式 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222152155557.png,192,81) - L1 范数 - 计算:所有元素绝对值之和。$||x||_1=\sum|x_i|$ - 几何:只能沿格子线走 - 特性:会倾向于让向量中的许多元素变为 0,从而产生稀疏性 - 应用:L1正则化,用于特征选择,剔除不重要的变量 - L2 范数 - 计算:元素平方和再开方。$||x||_2=\sqrt{\sum x_i^2}$ - 几何:计算两点之间的直线距离(最直观的距离) - 特性:对大数值非常敏感,处处可导,计算方便 - 应用:L2 正则化,防止模型过拟合;深度学习中的权重衰减。 - $L\infty$ 范数 - 计算:向量中绝对值最大的那个元素的值 - 应用:用于衡量最坏情况下的误差 - 矩阵范数 - Frobenius 范数 - 计算:把矩阵看成一个大向量,所有元素的平方和再开方 - 用途:衡量两个矩阵之间的距离,用于矩阵分解(SVD 或推荐系统)的损失函数。 - 基 - 定义 - 在向量空间里,基就是坐标系 - 在二维平面上,习惯用 x 轴 (1,0) 和 y 轴(0,1) 作为基。 - 任何一个点 (3,4) 都可以看作是:在 x 轴方向走 3 步,在 y 轴方向走 4 步。 - 基决定了观察和描述向量的视角。 - 特征值与特征向量 - 定义 - 对于一个方阵 Ai,如果存在一个非零向量 v 和一个标量 $\lambda$,满足如下等式 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222172925249.png,88,37) - 那么 v 就是特征向量,$\lambda$ 就是对应的特征值 - 特征值分解 - 如果一个 nxn 矩阵 A 有 n 个线性无关的特征向量,它可以被分解为 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222173125585.png,119,42) - Q:由特征向量组成的矩阵 - $\Lambda$ (Lambda):对角矩阵,对角线上是对应的特征值 - 奇异值分解 - 数学定义 - 对于任何 m*n 的矩阵 A,都可以分解为 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222153210370.png,107,40) - U(m x n 阶):左奇异值向量矩阵,它是正交矩阵(各列互相垂直且长度为 1),代表了变换后的输出空间的基。 - $\sum$ (m x n 阶):奇异值矩阵,只有对角线有值,称为奇异值($\sigma_1,\sigma_2, \dots$$),按从小到大排列,代表了每个方向上的权重或重要性。 - $V^T$ (n x n 阶):右奇异向量矩阵的转置,也是正交矩阵,代表了输入空间的基。 - 几何直观 - 如果把矩阵 A 看作一个变换,SVD 告诉我们这个变换可以拆为 3 步: - $V^T$(旋转):将输入向量旋转到特定的方向,使其与奇异向量对齐。 - $\sum$(缩放):在这些特定的方向上进行拉伸或压缩,奇异值越大,拉伸幅度越大。 - U (旋转):再次旋转,将结果映射到最终的输出空间。 - 概率统计 - 最大似然 - 有一组数据 $D={x_i}^n_{i=1}$,选择了一个参数化模型 p(x|θ) - 似然:把数据当成已发生的事实,把 θ 当成变量,问在这个 θ 下,数据出现的可能性有多大 - 最大似然估计:选一个 $\hat\theta$ 让 L(θ) 最大

Redis List 底层数据结构实验

实验元数据 (Meta Data) 实验编号/标题:Redis List 底层数据结构实验 ...

Redis Set 底层数据结构实验

实验元数据 (Meta Data) 实验编号/标题:Redis set 底层数据结构实验 ...

Redis String 底层数据结构实验

实验元数据 (Meta Data) 实验编号/标题:Redis String 底层数据结构实验 ...

Redis ZSet 底层数据结构实验

实验元数据 (Meta Data) 实验编号/标题:Redis set 底层数据结构实验 ...

冥想 SOP

目标 得到充分的休息 步骤 环境与姿态准备 安全屋:不会有人打扰 物理隔绝:手机开启免打扰 舒适坐姿:脊柱挺直,不要僵硬,想象有一根绳子在头顶轻轻拽,手部自然垂放在大腿上,掌心朝上 开启仪式 定个闹钟:选一个温和的铃声 深呼吸 3 次:用鼻子吸气,感觉腹部隆起,用嘴呼气,想象身体像一个漏气的皮球 轻轻闭眼:低垂眼帘,视线虚焦 寻找锚点 感受气息:注意空气经过鼻尖时的清凉感,或是胸腔起伏的节奏 不要控制呼吸:观察它 默念计数:吸气时心里默念 1,呼气时默念 2,数到 10 再循环 捕捉并回归 察觉:发现自己在想某个工作时,不要沮丧 标记:心里轻轻对自己说:“在想事情“。 温柔回归:把注意力重新带回到呼吸 温和收尾 重拾感知:慢慢感受身体接触椅子的压力,听听周围声音 微动:动动手指、脚趾、扭脖子 睁眼:给自己 30s 适应光线,带着平静进入下一项活动 备注 不要强求平静: 有时候坐下来发现脑子更乱了,这很正常。看到乱,本身就是进步。 时间点的选择: 晨起(定调一天)或睡前(辅助入睡)效果最好。 利用工具: 如果初期觉得静不下心,可以尝试使用 Headspace、Calm 或国内的 潮汐 等 App 进行引导式冥想。

Java IO

IO 模型与概念框架 - IO 模型与概念框架 - 四个基本概念 - 阻塞 vs 非阻塞 - 定义:一次调用在数据未就绪时是否立即返回 - 阻塞:调用线程进入等待,直到条件满足才返回,例如:InputStream.read(),数据没到就卡住 - 非阻塞:调用立即返回,不等数据 - 读:没数据时返回 0/-1 或抛特定异常 - 写:写不完就写一部分,返回已写字节数 - JavaNIO - SocketChannel.configureBlocking(false) 之后,read() 不会阻塞,可能返回 0 - 阻塞非阻塞指的就是线程是否在这次调用上被卡主 - 同步 vs 异步 - 定义:结果是由谁来完成并通知 - 同步:调用负责发起+等待/轮询+拿结果,即使是非阻塞轮询,仍然是同步,因为结果获取的责任方在调用方 - 异步:调用方发起后立即返回,后续由系统/框架在操作完成时回调/事件通知取结果,调用方不需要自己轮询等待结果 - JavaNIO - AsynchronousSocketChannel + CompletionHandler 是典型的 AIO 异步模型:完成时回调你 - 四象限组合 - 同步阻塞:BIO(经典 read() 卡住直到有数据) - 同步非阻塞:NIO 非阻塞 read() + 你自己轮询/配合 Selector 处理就绪事件,本质仍然是调用方驱动流程 - 异步非阻塞:AIO(回调通知你“读完了/写完了“) - 异步阻塞:理论上存在,比如发起异步后又 get() 阻塞等待,但是工程上不这么用 - BIO/NIO/AIO 的准确定义 - BIO(Blocking IO) - API 代表:ServerSocket/Socket、InputStream/OutputStream - 特征:accept()/read()/write() 默认阻塞 - 典型服务端模型:1 连接 1 线程(或线程池+阻塞读写) - 痛点: - 连接数上来后线程数膨胀:上下文切换、栈内存、调度开销 - 大量线程处于阻塞态,浪费资源 - NIO(Non-blocking I/O + Multiplexing) - API 代表:Channel/Buffer/Selector - 关键点:非阻塞 Channel + Selector 多路复用 - 本质:少量线程监听大量连接的就绪事件,再去执行非阻塞读写 - 价值: - 连接多时不需要等比例增加线程 - 事件驱动,提升并发连接处理能力 - NIO 三件套 - Channel:数据容器,可读可写 - Buffer:数据容器 - Selector:事件分发器 - AIO(Asynchronous I/O) - API 代表 AsynchronousServerSocketChannel/AsynchronousSocketChannel - 特征:发起读写后立即返回,完成后通过 CompletionHandler 回调通知结果 - 工程事实:Java AIO 在不同 OS 上底层支持与效果不同;很多高性能网络主流仍然是 Netty(基于 NIO 的 Reactor + 工程化) - 多路复用(IO Multiplexing) - 核心:一个线程可以等待多个 fd 的事件 - 没有多路复用:要等待某个 socket 可读,只能在这个 socket 上阻塞 read - 有多路复用:线程阻塞在 selector/poll/epoll_wait 上,内核告诉你那些 fd 可读/可写,再去处理 - Java NIO 的 Selector.select()=在做等事件,底层映射到 OS 的 select/poll/epoll/kqueue - Reactor 模式(NIO 常见线程模型) - Reactor:事件循环(event loop) + 分发(dispatch) - 流程 - selector.select() 事件 - 拿到 selected keys - 根据事件类型调用对于 handler - 单 Reactor 单线程:简单,适合低负载 - 主从 Reactor(boss/worker):boss 负责 accept,worker 负责读写 Java BIO - Java BIO - IO 流体系 - 两大抽象 - 字节流:InputStream/OutputStream - 面向原始字节 - 字符流:Reader/Writer - 面向字符,内部会涉及字符集解码/编码 - 涉及文本语义/字符集,就优先用 Reader/Writer 或者用 InputStream + 明确 Charset 解码;处理二进制就用 InputStream/OutputStream - 节点流 vs 处理流 - 节点流:直接连接数据源/目的地 - FileInputStream、FileOutputStream、Socket.getInputStream - 处理流:包装节点流,增强能力(缓冲、转码、数据类型、对象序列化、压缩、校验) - BufferedInputStream、InputStreamReader、DataInputStream、ObjectInputStream - 装饰器模式:不改原类,通过包装叠加功能 - Buffered 缓冲为什么快 - 不加缓冲为什么慢 - 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用,系统开销不小 - 不缓冲:每读 1KB 就要系统调用一次,次数巨大 - 加缓冲:一次系统调用读 8KB/64KB 放到用户态缓冲区,后续小读都在内存中完成,降低系统调用次数 - BufferedInputStream/BufferedOutputStream - BufferedInputStream:内部有 byte[] buffer(默认 8KB),先批量从底层读,再按需给你 - BufferedOutputStream:先写入 buffer,满了再 flush 到底层,减少了 write 的系统调用次数 - flush 是什么 - 把用户缓冲区里的数据推下去 - flush() != 一定落盘 - 字符集与桥接流 - InputStreamReader/OutputStreamWriter - InputStreamReader:字节 -> 字符(解码),需要 Charset - OutputStreamWriter:字符 -> 字节(编码),需要 Charset - 典型用法 - 读文本 - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8)) - 写文本 - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8)) - 为什么不能随便 new String(byte[]) - 不指定 charset,会走平台默认编码,导致乱码 - 常见功能处理流 - DataInputStream/DataOutputStream(理解二进制协议) - 能按固定类型读写:writeInt/readInt,writeLong/readLong - 用途:自定义二进制协议、文件格式 - DataInputStream 读写的是 Java 规定格式,跨语言要慎重,更常见的跨语言协议是 protobuf、msgpack 等 - ObjectInputStream/ObjectOutputStream - Java 原生序列化:实现 Serializable - 问题:性能一般、版本兼容差、安全风险 - 不建议用于 RPC/外部输入,内部常见简单可用,生产用 protobuf/kryo/json 等 - PrintWriter/PrintStream(输出遍历但注意编码/flush) - System.out 是 PrintStream - PrintWriter 可选 autoFlush(遇到 println/printf/format 自动 flush) - PrintWriter 默认吞异常 - 资源管理与异常 - try-with-resources - 自动 close,且 close 顺序与声明顺序相反 - close 过程中异常会作为 suppressed 异常附加到主屏幕 - close vs flush - close vs flush - close() 会隐式 flush(),不能依赖进程崩溃前的 close - 网络场景:写完重要数据要主动 flush 或者使用协议层 ack。 - BIO 与网络 IO:Socket 阻塞读写的关键点 - 阻塞读到集中返回情况 - InputStream.read(byte[]) - 返回 >0:读到的字节数 - 返回 -1:对端关闭输出 - 可能一直阻塞:对端不发数据也不关 - 半包/粘包在 BIO 也存在 - BIO 只是阻塞,TCP 仍然是字节流,read() 不保证一条业务消息完整到达 NIO 基础 Buffer/Channel - NIO 基础 - NIO 的核心心智模型:Buffer+Channel - 在 NIO 中,数据永远在 Buffer 里转 - 读:Channel.read(ByteBuffer) 把数据写进 buffer - 写:Channel.write(ByteBuffer) 从 buffer 读出数据写到 channel - Channel 是管道,Buffer 是货箱,NIO 必须管理 buffer 状态 - ByteBuffer 属性与状态 - 关键属性 - capacity:容量,创建后固定 - position:当前位置(下次读/写从这里开始) - limit:边界(读/写不能超过它) - mark:标记位置(可选) - remaing():limit - position - 两种模式 - 写模式:准备往 buffer 里放数据(例如从 channel read 进来) - position 向后走,limit 通常等于 capacity - 读模式:准备从 buffer 里取数据(例如写到 channel 或解析协议) - position 向后走,limit 表示可读数据的末尾 - 三个最重要的方法 - flip:写模式 -> 读模式 - 把 limit 设置为当前 position,再把 position 设置为 0 - 我写完了,现在从头读刚写的数据 - clear:读完后重置为写模式(不清数据,只重置指针) - position = 0; limit = capacity; mark = -1 - 不关心旧数据了,直接当成空 buffer 继续写 - compact:读了一部分,保留剩余未读数据,并切换为写模式 - 把未读数据 [position,limit) 移动到开头 - position=remaining;limit=capacity - 我还剩一点没处理,先挪到前面,下次继续在后面写新数据 - HeapBuffer vs DirectBuffer - Heap ByteBuffer(堆内) - ByteBuffer.allocate(n) - 好处:分配快,受 JVM GC 管理,排查简单 - 坏处:做网络 IO 时需要把数据从堆复制到内核缓冲区 - Direct ByteBuffer(堆外) - ByteBuffer.allocate(n) - 好处:减少一次拷贝,适合大块网络 IO,频繁 IO。 - 坏处 - 分配/释放更贵 - 受 MaxDirectMemorySize 限制 - 回收依赖 Cleaner/GC 时机,不是=null 就立刻释放 - Channel:FileChannel 与 SocketChannel - FileChannel - 来源 - FileInputStream.getChannel() - FileOutputStream.getChannel() - 常用能力 - read(ByteBuffer)/write(ByteBuffer) - position()/position(long):随机读写位置 - size() 文件大小 - force(boolean metaData):把修改刷到存储设备 - transferTo/transferFrom:为后续零拷贝铺垫 - SocketChannel - 支持 configureBlocking(false) NIO 非阻塞基石 - NIO 文件读写的标准流程 - FileChannel 读取 - buffer.clear() 进入写模式 - int n = channel.read(buffer) 把数据写进 buffer - buffer.flip() 进入读模式 - 从 buffer 读取处理 - 循环直到 n == -1 NIO 进阶 Selector/多路复用 - NIO 进阶 Selector/多路复用 - Selector/SelectionKey/Channel:三者关系与生命周期 - 三件套职责 - Selector:事件复用器,阻塞在 select() 等待就绪事件,把就绪的 key 放进 selectedKeys 集合 - SelectableChannel:可注册到 Selector 的通道 - SelectionKey:一次注册的凭证/句柄,包含 - channel():对于 channel - selector():归属 selector - interestOps():你关心那些事件 - readyOps():当前就绪了哪些事件 - attachment():绑定业务状态 - 注册与事件的基本规则 - channel.register(selector, OP_READ, attachment):把 channel 注册给 selector,并声明兴趣事件 - interestOps 是订阅,可以动态改 - readyOps 是本轮 select 的结果,不能直接改 - key 的取消与资源释放 - key.cancel():从 selector 的 key 集合中移除 - 移动要同时 close channel:channel.close(),否则 fd 泄露 - 被 cancel 的 key 不会立刻从所有集合消失,通常在下一次 select 时清理 - 多路复用:就绪事件语义 - 就绪不是完成 - OP_READ 就绪:表示现在读不阻塞,不保证读到一条完整的业务消息 - OP_WRITE 就绪:表示现在写更可能成功,不保证一次 write 写完你要写的所有数据 - 各个事件含义 - OP_ACCEPT:ServerSocketChannel 有新连接可以 accept - OP_CONNECT:客户端连接建立过程完成(非阻塞 connect) - OP_READ:SocketChannel 可读 - OP_WRITE:SocketChannel 可写(注意:很多时候“总是可写”) - Reactor 模式:从单线程到主从 Reactor - 单线程 Reactor - 一个线程做三件事 - selector.select() 等待事件 - 遍历 selectedKeys - 按事件类型分发处理:accept/read/write - 优点:简单 - 缺点:业务处理慢会阻塞整个事件循环 - 主从 Reactor(boss/worker) - boss:只负责 accept,把新 SocketChannel 分发到 worker 的 selector - worker:负责读写与协议解析、业务派发 Netty - Netty - 解决什么问题 - 原生 NIO 的痛点 - 需要自己管理:Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关 - Buffer 管理复杂:flip/compact、输入输出缓冲复用、DirectBuffer 回收时机 - 性能与稳定性:Selector 空轮询、内存膨胀、背压、线程隔离 - Netty 提供的工程化能力 - 线程模型(EventLoopGroup + Reactor)成熟 - Pipeline/Handler 体系:解码、业务、编码职责分离 - ByteBuf:读写指针友好、池化、零拷贝视图、引用计数 - 编解码器、背压、写队列、水位线、内存泄露检测等工具链 - 线程模型 - EventLoop - EventLoop = 一个线程 + 一个 Selector + 一组 Channel - EventLoop 负责 - 处理 IO 事件 - 执行任务队列 - 同一个 Channel 的所有 Handler 回调默认都在通一个 EventLoop 线程执行,一致性,减少锁 - EventLoopGroup - bossGroup:负责 accept(ServerSocketChannel) - workerGroup:负责已建立连接的读写(SocketChannel) - 为什么少量线程能抗大量连接 - 事件驱动:线程主要阻塞在 epoll/kqueue 上,事件就绪才处理 - 连接多不会导致线程线性增长 - Channel、Future、Promise:异步编程语义 - ChannelFuture - Netty 大量 API 返回 ChannelFuture,比如 bind、connect、writeAndFlush,表示操作已发起,完成会通知 - future.addListener(...):完成回调(推荐) - future.sync():阻塞等待 - Promise - 可写的 future,用于自己控制完成/失败 - Pipeline/Handler:职责分离 - Pipeline 是什么 - 每个 Channel 都有一条 ChannelPipeline - Inbound(入站):数据从 socket 进来 -> 解码 -> 业务处理 - Outbound(出站):业务写出 -> 编码 -> flush 到 socket - Handler 分类 - ChannelInboundHandler:处理入站事件(channelRead、channelActive…) - ChannelOutboundHandler:处理出站事件(write、flush…) - ChannelDuplexHandler:双向 - SimpleChannelInboundHandler<T>:自动释放入站消息(配合 ByteBuf 引用计数要理解) - ByteBuf - 为什么 ByteBuf 比 ByteBuffer 友好 - 两个指针:readerIndex/writerIndex - 不需要 flip:写完读直接通过读指针移动 - 支持切片/复制语义清晰 - 三种常见 ByteBuf - Heap:堆内 - Direct:堆外 - CompositeByteBuf:多个 buffer 组合成一个逻辑 buffer(减少拷贝) select/poll/epoll - select/poll/epoll - 简介 - 都是操作系统提供的 I/O 多路复用机制 - 让一个线程同时等待很多文件描述符的可读/可写/异常事件,然后再去对就绪的 fd 做 read/write - 术语 - fd:内核里对打开的文件/套接字等资源的编号。网络 socket 也是 fd。 - 就绪:表示现在读/写大概率不会阻塞,不等于你一次就能读到完整业务包/一次写完所有数据 - select:最老牌的多路复用 - 工作方式:把三组 fd(读/写/异常) 集合传给内核,内核扫描那些就绪,然后返回就绪集合。 - 特点: - fd 数量上限:select 受到 FD_SETSIZE 限制,常见 1024 - 每次调用要拷贝与扫描:用户态要把位图/集合传入内核,内核要线性扫描 - 集合会被修改:返回时就绪集合是被改过的,下次调用要重新准备集合 - 适用: - fd 少,跨平台,简单场景。 - poll:对 select 的数组版改良 - 工作方式:把 fd 列表放在一个 pollfd[] 数组传递给内核,内核扫描并在数组里标记那些就绪。 - 相比 select 改进 - 不收 FD_SETSIZE 位图限制 - 表达更灵活 - 缺点 - 仍然是 O(n) 扫描 - 每次调用仍然需要把数组从用户态传给内核 - epoll:为海量连接而生 - epoll 是 Linux 的高性能事件通知机制,核心思想是:把关注列表留在内核里,避免每次把大集合传来传去,返回就绪列表而不是让自己全量扫描 - 关键点 - 关注列表在内核维护:避免每轮 select/poll 大集合拷贝+扫描 - epoll_wait 返回的是就绪事件列表,只处理这些 fd,更接近 O(就绪数) - LT vs ET 触发模式 - LT (Level-Triggered 水平触发,默认) - 只要还可读/可写,就会持续通知你 - 编程更简单:读不完下次还会通知你 - ET (Edge-Triggered,边缘触发) - 只有从不可读 -> 可读的边缘变化才通知一次 - 必须把数据一次性读到不能再读,否则可能丢通知导致卡住 - 性能更高,但对实现要求更严格(非阻塞+循环读/写)