Appearance
FD 机制实测:三层限制、软/硬限制与 EMFILE 空转(Day 5 主线实验)
所属阶段:阶段 2 — 多线程扩展与 FD 上限 定位:Day 5 的主线实验——Day 4 的 FD 墙实测 撞了墙(现象),本文拆开墙看机制(原理):三层限制各自由谁定、软/硬限制如何互相约束、FD 耗尽时 CPU 到底在干什么。 前置依赖:Day 4 FD 墙实测(现象数据来自同机 124.221.142.185) 更新时间:2026-08-18 一句话总结:FD 不是"一个数",而是进程(ulimit)→会话(limits.conf)→系统(fs.*)三层漏斗;软限制可上下浮动、硬限制只能降不能升;bash 内建
ulimit -n裸用会同时改写 soft+hard,一次降档就把进程锁死在低值抬不回来——这是"改完 ulimit 不生效/不可逆"的机制根源。
一、本实验要回答的问题
| # | 问题 | 为什么重要 |
|---|---|---|
| Q1 | 三层限制各自的真实值是多少?哪一层是当前最紧的瓶颈? | Day 4 撞墙只用了进程级 ulimit -n;不改系统级参数,即使 ulimit -n 调上天也可能被 fs.nr_open 拦腰砍断 |
| Q2 | 软/硬限制的机制是什么?为什么"软限制可以随意改、硬限制不能抬"? | 这是 ulimit 行为怪异的根源:非 root 用户想调大 nofile 时报 Operation not permitted 的真相 |
| Q3 | FD 耗尽时 CPU 到底在干什么?EMFILE 不处理的代价有多大? | Day 4 只观察到"EMFILE 刷屏",没量过 CPU;这是生产上"FD 打满 → CPU 白转"故障的机制根因 |
| Q4 | 1 连接 = 1 FD 是守恒的吗?泄漏长什么样? | Day 6/阶段 6 要冲 10K/百万连接,必须先验证"连接数 ↔ FD 数"一一对应,否则万连接阶段会误判泄漏 |
二、实验设计
2.1 背景
Day 4 FD 墙实测 已经用 ulimit -n 降档撞墙,验证了:
- FD 峰值精确等于 ulimit 值(1024),撞墙点连接数 2000;
- 撞墙代价是吞吐 -50%、P999 尾部延迟 +2.6 倍,但
fail=0。
但撞墙背后还有三个未拆的机制:三层漏斗哪层是天花板、软/硬限制的边界行为、FD 耗尽时进程在做什么(CPU 视角)。本文补上这三块,让 Day 4 的"现象"变成可解释的"机制"。
2.2 复用同一把尺子
| 角色 | 工具 | 说明 |
|---|---|---|
| 服务端 | /home/chzhuo/fd-experiment/echo-epoll-lt-server | Day 4 同款,LT 水平触发,监听 9988,EMFILE 时 perror 一次后 break,不主动退避 |
| 客户端 | /home/chzhuo/fd-experiment/echo-kp-bench | Day 4 同款长连接压测,--mode long |
| 封装 | sh | 档位切换 + 0.2s 采样复用 |
| 观测 | pidstat -t -p <pid> 1 | 线程级 CPU(Day 4 没测的维度) |
2.3 实验矩阵
| 实验 | 内容 | 自变量 | 观测 |
|---|---|---|---|
| E1 三层基线 | 逐层读取进程/会话/系统限制 | 无(只读) | ulimit -Sn/-Hn、limits.conf、fs.file-max/nr_open/file-nr |
| E2 软/硬机制 | 在 soft 与 hard 之间上下调 | 目标值(>hard / =hard / <hard) | 每次调用的 rc 与当前值 |
| E3 EMFILE 行为 | 服务端 ulimit -n 128,客户端 500 连接 | 服务端 FD 上限 | pidstat CPU%、EMFILE 次数、QPS、FD 峰值 |
| E4 FD 守恒 | 正常关闭 vs 客户端强杀(RST),各 200 连接 × 5 轮 | 关闭方式 | /proc/<pid>/fd、ss 计数 |
E3 的关键关注点:现有服务端
accept()EMFILE 后 break 出 accept 循环,但 LT 模式下 listen fd 一直可读 →epoll_wait立即返回 → 反复重试——pidstat应能看到该线程的 CPU 行为。
三、代码设计
3.1 E1/E2 采集脚本(exp51_52.sh)
bash
#!/bin/bash
# E1: 三层限制基线
echo "soft=$(ulimit -Sn) hard=$(ulimit -Hn)"
echo "file-max=$(cat /proc/sys/fs/file-max)"
echo "nr_open=$(cat /proc/sys/fs/nr_open)"
echo "file-nr=$(cat /proc/sys/fs/file-nr)"
grep -vE '^#|^$' /etc/security/limits.conf
# E2: 软/硬限制机制(非 root, HARD 动态取实际硬限制)
HARD=$(ulimit -Hn)
ulimit -n $((HARD+10000)); echo "rc=$? now=$(ulimit -n)" # >hard 应失败
ulimit -n "$HARD"; echo "rc=$? now=$(ulimit -n)" # =hard 应成功
ulimit -n 1024; echo "rc=$? now=$(ulimit -n)" # <hard 应成功
ulimit -n "$HARD"; echo "rc=$? now=$(ulimit -n)" # 再抬回 hard
bash -c 'echo child-soft=$(ulimit -Sn) child-hard=$(ulimit -Hn)' # 继承验证设计要点:E2 的目标值动态取自实际 hard 限制(本机 hard=100002,不是常见的 65535/1048576),避免硬编码与机器配置脱节。
3.2 E3 实验封装(复用 fd-scan.sh 模式)
bash
( ulimit -n 128; exec ./echo-epoll-lt-server > srv.log 2>&1 ) & SRV=$!
# 双线采样:FD 数(0.2s) + 线程 CPU(1s)
( while kill -0 $SRV 2>/dev/null; do
echo "$(date +%s.%N) srvfd=$(ls /proc/$SRV/fd|wc -l)"; sleep 0.2; done ) > sample.log &
pidstat -t -p $SRV 1 > cpu.log 2>&1 &
( ulimit -n 65535; exec ./echo-kp-bench 127.0.0.1 9988 500 50 --mode long ) > bench.txt 2>&13.3 E4 泄漏实验设计
bash
# A 组(正常关闭)与 B 组(客户端强杀)各 5 轮,每轮 200 连接 × 50 rounds:
# 每轮结束 sleep 3 后读 服务端 fd / est / tw,观察 FD 是否回落、TW 是否累积
for r in 1 2 3 4 5; do
start_srv; bench 200; sleep 3
echo "round=$r fd=$(ls /proc/$SRV/fd|wc -l) est=$(ss -tan state established|wc -l) tw=$(ss -tan state time-wait|wc -l)"
kill_srv
done
# B 组差异:bench 启动 5s 后 pkill -9 客户端(连接变 RST)设计要点:E4 用 循环 + 曲线而非单次快照——泄漏是"每轮多 1~2 个 FD"级别的慢信号,必须跑足够轮次才能与噪声区分;TW 计数用于区分"端口占用"与"FD 占用"两个维度。
四、实验预期
| # | 预期 | 依据 |
|---|---|---|
| P1 | 三层值关系:ulimit -Hn ≤ fs.nr_open ≤ fs.file-max,进程级当前最紧 | 三层漏斗,最内层最紧 |
| P2 | 非 root 把 ulimit -n 抬到 hard 之上 → Operation not permitted;在 soft~hard 之间自由升降 → 成功 | Linux setrlimit() 权限规则:普通用户只能降 hard、可在 soft≤hard 内自由调 soft |
| P3 | E3 中服务端线程 CPU 高企、EMFILE 刷屏、QPS 低(连接全排队) | LT 下 listen 恒可读 → accept EMFILE → 忙循环 |
| P4 | 正常关闭组 FD 曲线平;强杀组 FD 曲线在 TIME_WAIT 窗口内短暂高企后回落,不单调增长 | 1 连接 = 1 FD,连接关闭后 FD 释放;RST 组差异来自 TIME_WAIT 而非泄漏 |

五、实验数据
采集环境:124.221.142.185(CentOS 7 / 内核 3.10 / 4 核 EPYC),工具在
/home/chzhuo/fd-experiment/。 原始输出:服务器/tmp/exp51_52.out(E1/E2)、/tmp/exp34.out与/tmp/day5_e34/(E3/E4)。
5.1 三层限制基线(E1)
📄 原始输出(点击展开)
##### [E1] 三层限制基线 #####
--- 进程级 ulimit (当前 ssh 会话)
soft=100001 hard=100002
--- 系统级 fs.*
file-max=1000000
nr_open=1048576
file-nr=1728 0 1000000
--- limits.conf 生效行
* soft nofile 100001
* hard nofile 100002
root soft nofile 100001
root hard nofile 100002
* soft memlock unlimited
* hard memlock unlimited
--- systemd 服务 (echo-server)
(无 echo-server.service)| 层级 | 参数 | 实测值 | 备注 |
|---|---|---|---|
| 进程软限制 | ulimit -Sn | 100001 | limits.conf 生效值(不是默认 1024,也不是 Day 4 说的 65535) |
| 进程硬限制 | ulimit -Hn | 100002 | = soft + 1 |
| 会话级 | limits.conf | * soft/hard nofile 100001/100002;root 同值;另有 memlock unlimited | 无 systemd(无 echo-server.service),登录会话由 PAM 应用 |
| 系统级 | fs.file-max | 1000000 | 全系统 FD 总数上限 |
| 系统级 | fs.nr_open | 1048576 | 单进程硬上限(RLIMIT_NOFILE 的天花板) |
| 系统级 | fs.file-nr | 1728 / 0 / 1000000 | 当前已分配 1728,占用率 0.17% |
实测结论:进程级
100001 ≤ 100002;系统级nr_open=1048576 > file-max=1000000属正常(nr_open 是单进程天花板、file-max 是全系统总数,两维度独立)。当前进程级最紧(100001),但距系统级还有 10 倍余量。
5.2 软/硬限制机制(E2,原始输出)
📄 原始输出(点击展开)
##### [E2] 软/硬限制机制 #####
--- 当前 soft=100001 hard=100002
--- 尝试调到 hard+10000 (> hard, 应失败)
/tmp/exp51_52.sh: line 22: ulimit: open files: cannot modify limit: Operation not permitted
rc=1 now=100001
--- 失败后当前值仍为 100001
--- 尝试提升到 hard (= hard, 应成功)
rc=0 now=100002
--- 降到 1024 (远低于 soft/hard, 应成功)
rc=0 now=1024
--- 再提升回 hard (soft<=hard 区间内, 应成功)
/tmp/exp51_52.sh: line 29: ulimit: open files: cannot modify limit: Operation not permitted
rc=1 now=1024
--- 新开子 shell 验证继承
child-soft=1024 child-hard=1024| 操作 | 目标值 | rc | 实测后值 | 结论 |
|---|---|---|---|---|
| 抬到 hard 之上 | 110002 | 1 | 100001(不变) | Operation not permitted,符合 P2 |
| 提升到 hard | 100002 | 0 | 100002 | soft 可升到 hard,成功 |
| 降到 1024 | 1024 | 0 | 1024 | 成功,但soft 与 hard 同时被改(见 6.2) |
| 再抬回 hard | 100002 | 1 | 1024(锁死) | hard 已被降至 1024,无法抬回 |
| 子 shell 继承 | — | — | child soft=1024 hard=1024 | 子进程继承降档后的限制 |
与预期 P2 的偏差:第 4 步"抬回 hard"本应成功,实际
Operation not permitted——因为第 3 步ulimit -n 1024裸用同时改写了 soft 和 hard(见 6.2 分析)。
5.3 EMFILE 行为(E3,srv=128 / cli=65535 / 500 连接)
📄 原始输出(点击展开)
--- FD 峰值 ---
128
--- pidstat 高 CPU 行 (time UID PID TID %usr %system %CPU CPU Command) ---
14:39:08 1001 32003 - 0.00 0.00 0.00 0.00 3 echo-epoll-lt-s
14:39:08 1001 - 32003 0.00 0.00 0.00 0.00 3 |__echo-epoll-lt-s
14:39:07 1001 32003 - 1.00 18.00 0.00 19.00 3 echo-epoll-lt-s
14:39:07 1001 - 32003 1.00 18.00 0.00 19.00 3 |__echo-epoll-lt-s
14:39:06 1001 32003 - 2.00 21.00 0.00 23.00 0 echo-epoll-lt-s
14:39:06 1001 - 32003 2.00 21.00 0.00 23.00 0 |__echo-epoll-lt-s
--- bench 摘要 ---
requests: 25000 / 25000 (ok:25000 fail:0)
elapsed: 1.246 s
QPS: 20062.9 req/s| 指标 | 实测值 | 说明 |
|---|---|---|
| FD 峰值 | 128 | 精确卡死在 ulimit=128,与 Day 4 "精确卡墙"完全一致 |
| 服务端线程 CPU | 峰值 23%(%system 21% + %usr 2%) | 窗口极短(bench 仅 1.2s),未出现持续 100% 空转 |
| QPS | 20062.9 | 500 连接 × 50 rounds 共 1.2s 完成 |
| fail | 0 | 与 Day 4 一致:连接被内核队列吸收 |
| EMFILE 计数 | 见服务器 /tmp/day5_e34/srv-e3.log | 脚本已落盘,本文未回拉正文 |
与 P3 的偏差:500 连接 / 128 FD 只够 4 波排队(128−3≈125 连接/波),空转窗口被波次快速消化,CPU 峰值仅 ~23% 且一闪而过——撞墙的 CPU 代价只在"连接数 ≫ FD 上限"的持续排队场景才显著,短窗口表现为瞬时脉冲而非 100% 空转(分析见 6.3)。
5.4 FD 守恒(E4,200 连接 × 5 轮)
📄 原始输出(点击展开)
A round=1 fd=5 est=4 tw=701 B round=1 fd=5 est=4 tw=1701
A round=2 fd=5 est=4 tw=901 B round=2 fd=5 est=4 tw=1901
A round=3 fd=5 est=4 tw=1101 B round=3 fd=5 est=4 tw=2101
A round=4 fd=5 est=4 tw=1301 B round=4 fd=5 est=4 tw=1602
A round=5 fd=5 est=4 tw=1501 B round=5 fd=5 est=4 tw=1402| 对照组 | 第 1 轮 | 第 2 轮 | 第 3 轮 | 第 4 轮 | 第 5 轮 | 趋势 |
|---|---|---|---|---|---|---|
| A 正常关闭 | fd=5 tw=701 | fd=5 tw=901 | fd=5 tw=1101 | fd=5 tw=1301 | fd=5 tw=1501 | fd 持平、tw 单调 +200/轮 |
| B 客户端强杀 | fd=5 tw=1701 | fd=5 tw=1901 | fd=5 tw=2101 | fd=5 tw=1602 | fd=5 tw=1402 | fd 持平、tw 先升后回落 |
est 恒为 4(机器上其他会话的 established 连接),服务端 fd 每轮结束后都回到基线 5(stdin/stdout/stderr + listen_fd + epoll_fd),无 FD 泄漏。
六、实验分析
6.1 三层漏斗:谁是天花板

- 当前机器进程级最紧:
100001(soft)远低于1048576(nr_open)与1000000(file-max)——日常瓶颈必然先撞进程级,这与 Day 4 撞墙点(连接数 2000)吻合; ulimit -n调不上去的两种原因:超过-Hn(Operation not permitted),或超过fs.nr_open(即使 root 也失败);实测 5.2 演示了第一种;fs.file-nr的已分配数(1728) 反映全系统压力,当前占用率 0.17%——系统层非常宽松;- Day 6 调参的完整链条:
ulimit -n(进程)→limits.conf(持久)→fs.nr_open(单进程天花板)→fs.file-max(系统总数)——四层都要抬,只改一层会"改了不生效"。
6.2 软/硬限制:ulimit -n 1024 一步锁死的机制
setrlimit() 的内核规则(man 2 getrlimit 与 kernel/sys.c):
- 非 root:硬限制只能降、不能抬;软限制可在 0~硬限制之间任意调;
- root:两者均可抬(但硬限制仍不能超过
fs.nr_open); - bash 内建
ulimit -n不带-S/-H时,默认同时修改软限制和硬限制(POSIX 未强制,但 bash/dash 均如此实现)。
这就是 5.2 第四行的真相:ulimit -n 1024 把 hard 也从 100002 降到了 1024,之后 ulimit -n 100002 因"超过硬限制"被拒(rc=1),当前进程被永久锁死在 1024——子 shell(新起的 bash)继承的也是 1024,除非退出登录重新应用 limits.conf(恢复 100001/100002)或以 root 重设。
教学点:调试时千万不要裸用
ulimit -n <小值>——要降软限制请写ulimit -Sn,否则会把 hard 一起降下去,当前进程再也抬不回来。这也是"改完 ulimit 不生效/不可逆"的标准成因之一(另一个成因是 6.1 的会话级覆盖)。
6.3 E3:撞墙的 CPU 代价是"脉冲"而非"持续 100%"
5.3 与 P3 预期不符,但更有教学价值:
- FD 峰值精确 128:卡墙行为与 Day 4 完全一致(机制没变);
- CPU 峰值 23% 一闪而过:因为 500 连接 / 128 FD 只够 4 波,每波 ~125 连接跑完 50 rounds 就释放 FD、下一波补位,空转窗口太短,
pidstat 1s只采到 2~3 个非零采样点; - 若要复现"持续空转",需要 连接数 ≫ FD 上限(如 5000 连接 / 128 FD,Day 4 的 1024/5000 组就呈现了长时间撞墙 + EMFILE 1305 次),让排队永不消化。

机制链条不变:LT 下 listen 恒可读 → accept EMFILE → break → epoll_wait 立即返回 → 再试。差别只在排队深度——排队消化得快,CPU 是脉冲;排队消化不掉,CPU 就是持续空转。
6.4 FD 守恒与 TIME_WAIT 的区分
E4 两组对照的结论:FD 数 ≠ 连接数 ≠ 端口数。
- 正常关闭(A 组):服务端 fd 每轮回到 5(连接关闭即释放 FD,无泄漏);但 TW 单调 +200/轮(701→1501)——每个关闭的连接在客户端侧进 TIME_WAIT,端口被占 ≠ FD 被占;
- 客户端强杀(B 组):fd 同样回到 5;TW 先升后落(2101→1402)——强杀产生 RST,连接不经历完整四次挥手,服务端 TW 更少且随 60s 超时自然回收;
- 若某轮后 fd 数单调不减,才是真泄漏(如服务端没 close、epoll 忘 del)——两组均未出现。
七、实验结论
- FD 是三层漏斗,当前进程级最紧:实测
soft=100001 < hard=100002 ≪ nr_open=1048576 / file-max=1000000,fs.file-nr已分配仅 1728(0.17%)——日常瓶颈必然先撞进程级,Day 6 冲 10K 需同步抬系统层; - 软/硬限制机制:非 root 只能降 hard、在 soft≤hard 间自由调 soft;超过 hard 报
Operation not permitted(实测 rc=1,值不变); ulimit -n裸用会同时降 soft+hard 并锁死进程:实测降到 1024 后无法抬回(rc=1),子 shell 继承的也是 1024——这是"改完 ulimit 不生效/不可逆"的机制根源,调试必须用-Sn/-Hn显式指定;- FD 耗尽时 CPU 代价是"脉冲"而非"持续 100%":500 连接/128 FD 场景 CPU 峰值 23% 一闪而过,撞墙形态与 Day 4 一致(FD 峰值精确 128、fail=0、QPS 20062.9);持续空转需连接数 ≫ FD 上限;
- 1 连接 = 1 FD 守恒:正常关闭/强杀后服务端 fd 均回落至基线 5,无泄漏;TW 是端口级状态(A 组单调累积、B 组先升后落),与 FD 占用是两个维度。
八、回到问题
| # | 问题 | 答案 |
|---|---|---|
| Q1 | 三层限制真实值?哪层最紧? | soft=100001 / hard=100002 / nr_open=1048576 / file-max=1000000 / file-nr=1728;进程级当前最紧,但距系统级有 10 倍余量 |
| Q2 | 软/硬限制机制? | 非 root:hard 只能降不能抬,soft 可在 hard 内自由调;超过 hard → Operation not permitted;裸 ulimit -n 会同时降 soft+hard,一步锁死 |
| Q3 | FD 耗尽时 CPU 行为? | 短窗口场景为瞬时脉冲(本次峰值 23%);连接数 ≫ FD 上限时才持续空转(Day 4 的 1024/5000 组 EMFILE 1305 次) |
| Q4 | 1 连接 = 1 FD 守恒? | 是。正常关闭/RST 后 FD 均回落到基线 5,无泄漏;TW 与 FD 是两个维度,泄漏特征是 FD 曲线单调增长 |
附录:复现
bash
# 1. 环境(与 Day 4 同一台机器)
# 124.221.142.185, CentOS 7 / 内核 3.10 / 4 核 EPYC
# 工具: /home/chzhuo/fd-experiment/{echo-epoll-lt-server,echo-kp-bench}
# 2. E1+E2: 三层基线 + 软硬机制(目标值动态取 hard)
bash exp51_52.sh
# 3. E3: EMFILE 行为(srv 降档 128, 客户端 500 连接)
# 后台起服务 → pidstat -t -p <pid> 1 抓 CPU → 起 bench → 结束后 grep EMFILE
bash exp34.sh # E3+E4 合并, nohup 后台跑防断连
# 4. E4: FD 守恒(A 正常关闭 / B 客户端强杀, 各 200 连接 × 5 轮)
# 已在 exp34.sh 中, 每轮输出 fd/est/tw
# 5. 原始数据存放
# 服务器: /tmp/exp51_52.out (E1/E2), /tmp/exp34.out + /tmp/day5_e34/ (E3/E4)