Linux 基础体系 · 第 76/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux 资源限制:ulimit、RLIMIT、systemd Limit、文件句柄和进程数
Linux 中的“资源限制”不是一个单一开关,而是几层机制共同作用的结果:
登录会话 / systemd 服务配置
│
▼
进程继承的 RLIMIT(soft / hard)
│
├── 文件描述符上限:RLIMIT_NOFILE
├── 进程/线程数量上限:RLIMIT_NPROC
└── 地址空间、锁定内存、CPU 时间等其他限制
同时还可能受到:
├── 内核全局资源上限
└── cgroup v2 组级限制,例如 pids.max
因此,“把 ulimit -n 调大了”并不意味着服务一定能打开更多文件;“把 LimitNPROC 调大了”也不意味着服务一定能创建更多线程。必须明确限制属于哪个对象、由谁设置、如何继承,以及触发失败时返回什么错误。
1. 先区分四个概念
1.1 ulimit 是用户态接口,不是内核限制本身
ulimit 通常是 shell 的内建命令,例如 Bash:
ulimit -n
ulimit -a
它通过系统调用修改或查询当前 shell 进程的资源限制。Linux 内核真正保存和执行的是每个进程的 RLIMIT_* 值。
某些系统上不存在独立的 /bin/ulimit:
command -v ulimit
type ulimit
常见结果是:
ulimit is a shell builtin
这解释了一个常见现象:
ulimit -n 65536
some-command
这里的限制先作用于当前 shell,然后由 shell 启动的 some-command 继承。如果在脚本中执行:
#!/bin/sh
ulimit -n 65536
exec /opt/example/server
则 server 会继承新的限制;但如果从另一个已经运行的终端启动服务,那个服务不会自动获得此设置。
1.2 RLIMIT 是内核对单个进程的资源限制
Linux 通过 getrlimit(2)、setrlimit(2) 和 prlimit(2) 管理资源限制。每种资源有一组 RLIMIT_* 常量,例如:
| 资源 | 含义 |
|---|---|
RLIMIT_NOFILE |
一个进程能够持有的文件描述符数量上限 |
RLIMIT_NPROC |
同一个真实用户能够拥有的进程/线程数量限制 |
RLIMIT_AS |
进程可用虚拟地址空间上限 |
RLIMIT_STACK |
主线程栈或进程栈限制 |
RLIMIT_CPU |
进程可消耗的 CPU 时间 |
RLIMIT_FSIZE |
进程能够创建的文件大小上限 |
RLIMIT_CORE |
core dump 文件大小上限 |
RLIMIT_MEMLOCK |
能够锁定在内存中的内存量 |
RLIMIT_MSGQUEUE |
POSIX 消息队列资源限制 |
RLIMIT_SIGPENDING |
用户能够排队的信号数量限制 |
文章重点讨论 RLIMIT_NOFILE 和 RLIMIT_NPROC,但必须记住:ulimit 并不是只管理文件和进程。
每个 RLIMIT 通常包含两个值:
- soft limit,软限制:当前实际执行检查的值。
- hard limit,硬限制:软限制能够被提高到的最大值。
查看 Bash 当前限制:
ulimit -Sn # soft nofile
ulimit -Hn # hard nofile
ulimit -Su # soft nproc
ulimit -Hu # hard nproc
例如:
$ ulimit -Sn
1024
$ ulimit -Hn
1048576
表示当前进程最多按 1024 检查,但该进程可以自行把软限制提高到 1048576,因为它没有超过硬限制。
普通进程不能把硬限制提高到当前硬限制以上。降低硬限制通常是不可逆的:进程没有足够权限时,不能再把它恢复到原来的值。具有相应特权的进程可以提高硬限制,但这不是普通应用应依赖的行为。
1.3 systemd 的 Limit* 是启动时设置进程 RLIMIT 的配置
systemd 服务单元中可以写:
[Service]
LimitNOFILE=65536
LimitNPROC=4096
LimitCORE=0
这些 Limit* 指令通常会在 systemd 启动服务进程前,为该服务设置对应的 RLIMIT_*。它们不是另一套独立的“文件句柄系统”,而是 systemd 对进程资源限制的配置入口。
例如:
LimitNOFILE=65536:131072
表示软限制为 65536,硬限制为 131072。具体语法和可用指令应以目标发行版的 systemd.exec(5) 为准;现代主流 systemd 支持以 soft:hard 形式分别指定两个值,单值通常同时设置软、硬限制。
常见映射关系包括:
LimitNOFILE -> RLIMIT_NOFILE
LimitNPROC -> RLIMIT_NPROC
LimitAS -> RLIMIT_AS
LimitSTACK -> RLIMIT_STACK
LimitCPU -> RLIMIT_CPU
LimitFSIZE -> RLIMIT_FSIZE
LimitCORE -> RLIMIT_CORE
LimitMEMLOCK -> RLIMIT_MEMLOCK
但并非所有 systemd 资源指令都对应 RLIMIT。比如:
TasksMax=4096
对应的是 cgroup 的进程数控制,现代 systemd 通常通过 cgroup v2 的 pids.max 实现,而不是 RLIMIT_NPROC。
1.4 文件描述符、文件句柄和进程数不是同一个对象
“文件句柄”在工程讨论中经常混用,至少要区分三层:
- 文件描述符 fd:进程中的整数,例如
0、1、2、57。 - 内核 open file description:描述打开文件的偏移量、状态标志等,多个 fd 可以引用同一个对象。
- 系统全局文件对象资源:内核整体能够分配的文件对象数量。
例如:
int fd2 = dup(fd1);
fd1 和 fd2 是两个不同的文件描述符,但通常引用同一个 open file description,因此共享文件偏移量。
执行:
int child = fork();
子进程会继承父进程的文件描述符引用。父子进程各自拥有 fd 表中的条目,但这些条目可能引用相同的内核打开文件对象。
所以:
RLIMIT_NOFILE主要限制一个进程的 fd 表;fs.file-max影响系统整体可分配的文件对象;- socket、epoll、eventfd、管道等也会消耗文件描述符;
- 进程数限制和文件描述符限制彼此独立。
2. RLIMIT 的生命周期:设置、继承和执行
2.1 资源限制属于进程的执行上下文
可以把进程的 RLIMIT 看作进程属性的一部分。一个典型流程如下:
父进程设置 RLIMIT
│
▼
fork()
│
├── 子进程继承相同限制
│
▼
execve()
│
└── 新程序继续使用这些限制
execve() 只替换进程的程序映像,不会因为换了可执行文件就自动重置 RLIMIT。因此,下面的 shell 脚本有效:
#!/bin/bash
ulimit -n 65536
exec /opt/app/server
server 启动后仍然会看到 RLIMIT_NOFILE=65536。
反过来,如果先启动服务,再在另一个 shell 中执行:
ulimit -n 65536
已经运行的服务不会受到影响。限制修改作用于执行修改操作的进程及其后续继承者,而不是按进程名、端口或服务名称全局匹配。
2.2 查看当前 shell 和目标进程必须使用不同方法
查看当前 shell:
ulimit -a
查看任意进程:
cat /proc/$PID/limits
示例:
$ cat /proc/1234/limits
Limit Soft Limit Hard Limit Units
Max open files 65536 65536 files
Max processes 4096 4096 processes
Max locked memory 65536 65536 bytes
Max stack size 8388608 unlimited bytes
也可以使用 prlimit:
prlimit --pid 1234
prlimit --pid 1234 --nofile=65536:65536
修改其他进程的限制需要目标进程权限以及相应的特权检查,通常只能由 root 或具备相关 capability 的进程完成。生产环境中不应把“能否执行命令”与“目标进程是否接受限制修改”混为一谈。
2.3 PAM、shell 和 systemd 是不同配置路径
登录用户常见配置文件是:
/etc/security/limits.conf
/etc/security/limits.d/*.conf
它们通常由 PAM 的 pam_limits.so 在登录会话建立时应用。例如:
appuser soft nofile 65536
appuser hard nofile 131072
但是这并不保证 systemd 系统服务会读取这些文件。系统服务由 PID 1 启动时,通常不经过普通 SSH 登录会话的 PAM shell 路径。因此,服务的 RLIMIT 应在 unit 中显式写:
[Service]
LimitNOFILE=65536:131072
而不是只修改 /etc/security/limits.conf。
sudo 也会造成误判:
ulimit -n
sudo sh -c 'ulimit -n'
第二个命令是在 sudo 启动的新进程中执行,其环境和限制可能与当前 shell 不同。诊断时应直接检查最终服务进程的 /proc/$PID/limits。
3. 文件描述符限制:RLIMIT_NOFILE 的完整链路
3.1 一个打开请求至少要通过多个条件
当进程调用:
open()
socket()
pipe()
epoll_create1()
dup()
accept()
并需要分配一个新的文件描述符时,至少涉及以下条件:
进程 fd 数量未达到 RLIMIT_NOFILE 的 soft limit
AND
请求的 fd 不超过内核允许的描述符范围
AND
系统全局文件对象资源仍然足够
AND
调用自身的对象资源、内存和权限条件满足
可以形式化为:
其中:
- :进程当前持有的文件描述符数量;
- :该进程的
RLIMIT_NOFILE软限制; - :内核允许的单进程 fd 数量上界,常与
/proc/sys/fs/nr_open有关; - :系统当前已分配的全局文件对象数量;
- :系统全局文件对象上限。
如果进程自己的限制先达到,通常得到:
EMFILE: Too many open files
如果系统整体文件对象耗尽,常见错误是:
ENFILE: Too many open files in system
这两个错误都可能被应用日志简化为“打开文件失败”,但处理方向不同。
3.2 “打开文件数”不只包括普通文件
以下对象通常都占用文件描述符:
ls /proc/$$/fd
可看到:
0 -> /dev/pts/0
1 -> /dev/pts/0
2 -> /dev/pts/0
3 -> socket:[...]
4 -> anon_inode:[eventpoll]
5 -> pipe:[...]
因此,一个网络服务的 fd 消耗可能包括:
- 监听 socket;
- 每个客户端连接;
- epoll fd;
- 日志文件;
- 配置文件;
- 管道;
- eventfd、timerfd、signalfd;
- TLS、数据库或监控库间接创建的 fd。
假设服务有:
监听 socket 2
epoll、eventfd、timerfd 5
日志和配置文件 20
客户端连接 50000
则粗略 fd 数量为:
如果 RLIMIT_NOFILE=50000,即使只差 27 个 fd,新的连接也可能失败。实际服务还会有连接关闭延迟、重启连接、临时文件和库内部 fd,因此不能只把业务连接数原样写成 LimitNOFILE。
3.3 fd 上限与 fd 数量不是完全相同的数字
RLIMIT_NOFILE 的语义与“可分配的最大 fd 编号”紧密相关。传统 Linux 文档通常将它描述为进程能够打开的最大文件描述符数量,内核实现中也会检查待分配 fd 是否超出该限制。
工程上应把它当作“该进程可同时持有的 fd 数量上限”使用,而不要依赖 fd 编号恰好从 0 连续增长。因为:
close(10);
open(...);
新 fd 可能复用 10;而多个 dup()、关闭和并发操作会使编号与数量关系不直观。
3.4 系统级参数:fs.nr_open 和 fs.file-max
检查相关参数:
cat /proc/sys/fs/nr_open
cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr
常见含义:
fs.nr_open:单个进程允许的 fd 数量上界,RLIMIT_NOFILE不能任意超过它;fs.file-max:系统级可分配文件句柄资源的上限;fs.file-nr:当前系统文件句柄资源使用情况,具体显示格式和内核版本有关。
fs.nr_open 与 LimitNOFILE 的关系是:
LimitNOFILE <= fs.nr_open
如果 unit 请求超过内核允许值,systemd 可能无法启动服务,日志中会出现设置资源限制失败的错误。此时单纯修改应用配置没有意义。
修改全局参数会影响整台机器,不应因为一个服务报 EMFILE 就直接大幅提高 fs.file-max。先确定是:
- 应用确实需要更多 fd;
- 应用存在 fd 泄漏;
- 服务的 RLIMIT 太低;
- 系统全局资源耗尽;
- 实际失败来自 socket 内存、端口、连接跟踪或 cgroup,而不是 fd。
3.5 RLIMIT_NOFILE 的生产风险
提高 fd 上限不会预先分配同等数量的物理内存,但会允许程序创建更多内核对象。大量 socket、epoll 条目、管道和文件对象仍会消耗内核内存与应用内存。
因此下面的设置不是“越大越好”:
LimitNOFILE=infinity
它可能掩盖 fd 泄漏,使故障从快速、可诊断的 EMFILE,变成更晚出现的内存压力、调度异常或系统级资源枯竭。
4. 进程数限制:RLIMIT_NPROC 与线程的关系
4.1 RLIMIT_NPROC 不是“这个进程最多 fork 几次”
RLIMIT_NPROC 按真实用户 ID 统计资源。它限制的是该真实用户可以拥有的进程数量;在 Linux 中,线程也会计入相关任务数量。
因此假设用户 app 已经拥有:
主服务进程 1
工作进程 8
线程 200
其他同 UID 进程 50
合计 259
如果该用户的 RLIMIT_NPROC 软限制是 300,服务再创建大量线程时,失败点取决于内核当前计数和其他并发创建者。它不是只统计当前服务的子进程。
典型失败表现:
fork(): Resource temporarily unavailable
pthread_create(): Resource temporarily unavailable
常见错误码是:
EAGAIN
这个错误不一定只表示 RLIMIT_NPROC。也可能是:
- cgroup
pids.max达到上限; - 系统 PID 数量接近
kernel.pid_max; - 内存不足,无法创建新任务;
- 内核无法分配任务结构。
所以不能看到 EAGAIN 就直接修改 LimitNPROC。
4.2 一个完整的进程创建条件
把一次 fork() 或线程创建抽象为:
其中:
- :该真实 UID 当前拥有的进程/线程计数;
- :
RLIMIT_NPROC软限制; - :当前 cgroup 的任务计数;
- :该 cgroup 的
pids.max; - :系统整体任务数量;
- :系统级 PID 范围等约束。
只要任意一层不满足,创建就可能失败。提高其中一层不会绕过其他层。
4.3 LimitNPROC 的隔离边界经常被误解
systemd unit:
[Service]
User=app
LimitNPROC=1024
设置的是服务进程的 RLIMIT_NPROC。但该限制按真实 UID 生效,因此同样以 app 身份运行的其他服务或用户进程也可能消耗同一个计数。
这会导致两个服务相互影响:
service-a 使用 User=app
service-b 使用 User=app
│
└── 共享同一个 UID 的 RLIMIT_NPROC 计数
如果目标是限制“这个服务单元及其所有后代任务的总数”,通常应优先考虑:
[Service]
TasksMax=1024
TasksMax 是 cgroup 级限制,边界是服务的 cgroup,而不是 UID。它更适合服务隔离。
不过 TasksMax 与 LimitNPROC 可以同时存在:
RLIMIT_NPROC:限制 UID 维度
pids.max:限制 cgroup 维度
实际允许的任务数量大致受二者较小值支配。
5. systemd 的 Limit*、TasksMax 和 cgroup 不是一回事
5.1 systemd 启动服务的关键时序
一个简化的 systemd 服务启动过程是:
读取 unit 文件
│
▼
创建或加入服务 cgroup
│
▼
准备 User=、组、能力、命名空间等执行环境
│
▼
为即将执行的进程设置 RLIMIT
│
▼
fork/exec 启动 ExecStart
服务进程最终同时处于两套控制中:
进程属性:
RLIMIT_NOFILE
RLIMIT_NPROC
RLIMIT_AS
...
cgroup 属性:
pids.max
memory.max
cpu.max
io.max
...
LimitNOFILE 只设置进程级 RLIMIT;TasksMax 设置 cgroup 任务数量限制。不能用 LimitNOFILE 替代 cgroup 内存限制,也不能用 TasksMax 替代单进程 fd 限制。
5.2 一个可验证的 systemd 配置
创建 /etc/systemd/system/limit-demo.service:
[Unit]
Description=Resource limit demonstration
[Service]
Type=simple
ExecStart=/usr/bin/sleep infinity
User=app
LimitNOFILE=65536:65536
LimitNPROC=4096
TasksMax=1024
前置条件是系统存在 app 用户:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin app
加载并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now limit-demo.service
这里:
daemon-reload让 systemd 重新读取 unit 文件;enable创建开机启动关系;--now立即启动;LimitNOFILE设置服务进程的 fd RLIMIT;LimitNPROC设置该服务进程继承的 UID 维度限制;TasksMax限制该 unit cgroup 中的任务数量。
查看 systemd 记录的配置:
systemctl show limit-demo.service \
-p MainPID \
-p LimitNOFILE \
-p LimitNPROC \
-p TasksMax
再查看实际进程:
PID=$(systemctl show -p MainPID --value limit-demo.service)
sudo cat /proc/$PID/limits | grep -E \
'Max open files|Max processes'
sudo ls /proc/$PID/fd | wc -l
systemctl show 展示的是 unit 管理器中的属性;/proc/$PID/limits 展示的是实际进程当前生效的 RLIMIT。生产诊断时两者都应检查。
检查 cgroup 的实际状态:
CGROUP=$(systemctl show -p ControlGroup --value limit-demo.service)
sudo cat /sys/fs/cgroup"$CGROUP"/pids.max
sudo cat /sys/fs/cgroup"$CGROUP"/pids.current
sudo cat /sys/fs/cgroup"$CGROUP"/pids.events
在 cgroup v2 中,pids.events 可能包含:
max 0
当 pids.max 阻止过任务创建时,max 会增加。不同发行版的 cgroup 挂载路径和 systemd 默认层级可能不同,因此应以 ControlGroup 返回的路径为准。
修改 unit 后:
sudo systemctl daemon-reload
sudo systemctl restart limit-demo.service
只执行 daemon-reload 不会改变已经运行进程的 RLIMIT;通常需要重启服务,让新进程在启动时获得新限制。
5.3 LimitNPROC 与 TasksMax 的失败日志不同
如果服务创建任务失败,可以同时观察:
journalctl -u limit-demo.service
cat /proc/$PID/limits
cat /sys/fs/cgroup"$CGROUP"/pids.current
cat /sys/fs/cgroup"$CGROUP"/pids.max
cat /sys/fs/cgroup"$CGROUP"/pids.events
诊断逻辑如下:
/proc/$PID/limits中Max processes很低,且 UID 下已有大量任务:怀疑RLIMIT_NPROC。pids.current接近pids.max,pids.events中max增加:怀疑 cgroup PID 限制。- 两者都未达到,但仍然
EAGAIN:继续检查系统 PID、内存、内核日志和应用自身错误处理。 - 如果服务以 root 或带有相关特权运行,
RLIMIT_NPROC的实际强制行为可能与普通用户不同;这也是不应只依赖LimitNPROC做隔离的原因。
6. ulimit 的常用语法和边界
6.1 查询软限制和硬限制
Bash 中:
ulimit -Sn # 最大打开文件数的 soft limit
ulimit -Hn # 最大打开文件数的 hard limit
ulimit -Su # 最大进程数的 soft limit
ulimit -Hu # 最大进程数的 hard limit
常用选项:
-n RLIMIT_NOFILE
-u RLIMIT_NPROC
-c RLIMIT_CORE
-s RLIMIT_STACK
-v RLIMIT_AS
-t RLIMIT_CPU
-f RLIMIT_FSIZE
-l RLIMIT_MEMLOCK
完整列表以目标 shell 和 help ulimit 为准:
help ulimit
设置当前 shell 的文件描述符限制:
ulimit -n 65536
设置软、硬值:
ulimit -S -n 65536
ulimit -H -n 131072
也可以写成:
ulimit -Sn 65536
ulimit -Hn 131072
如果当前软限制已经高于目标值,降低它通常可行;如果目标值超过硬限制,普通用户会收到类似:
ulimit: open files: cannot modify limit: Operation not permitted
6.2 管道和子 shell 会影响验证
以下命令中的 ulimit 可能运行在管道创建的子 shell 中:
some-command | while read line; do
ulimit -n 65536
done
即使修改成功,也不一定影响外层 shell。验证资源限制时,应避免把修改放在不确定的子 shell 中。
同样,下面的设置只影响当前 shell 及其后代:
ulimit -n 65536
./server
它不会影响已经启动的兄弟进程:
terminal shell A ── set ulimit ── server
terminal shell B ── old limits
7. 典型故障:EMFILE、ENFILE、EAGAIN 如何定位
7.1 文件描述符耗尽
应用日志:
accept4: Too many open files
open: Too many open files
第一步不是立即修改配置,而是找出实际服务进程:
PID=$(systemctl show -p MainPID --value example.service)
sudo cat /proc/$PID/limits | grep 'Max open files'
sudo ls /proc/$PID/fd | wc -l
按 fd 类型聚合:
sudo ls -l /proc/$PID/fd 2>/dev/null \
| sed -E 's/.*-> //' \
| sed -E 's/\[[0-9]+\].*/[socket-or-pipe]/' \
| sort | uniq -c | sort -nr | head
检查网络连接:
sudo ss -tanp | grep "pid=$PID,"
如果实际 fd 数量接近软限制,继续判断:
- 是正常连接规模超过配置;
- 是连接关闭不及时;
- 是文件、socket 或 epoll fd 泄漏;
- 是重载期间新旧 worker 同时存在;
- 是 systemd unit 的限制低于交互 shell;
- 是全局
file-max耗尽。
若是 EMFILE,提高服务的 LimitNOFILE 可能有效;若是 ENFILE,应检查系统全局文件对象与其他服务,不能只改当前 unit。
7.2 进程或线程创建失败
先记录应用层的 errno:
fork failed: EAGAIN
pthread_create failed: EAGAIN
然后检查:
sudo cat /proc/$PID/limits | grep 'Max processes'
ps -eLf | awk '$1 == "app" { n++ } END { print n+0 }'
按真实 UID 统计线程时,ps -eLf 的列格式可能因实现不同而变化;可以使用:
ps -u app -L --no-headers | wc -l
这类统计存在瞬时竞争,只适合作为诊断近似。更重要的是检查 cgroup:
CGROUP=$(systemctl show -p ControlGroup --value example.service)
sudo cat /sys/fs/cgroup"$CGROUP"/pids.current
sudo cat /sys/fs/cgroup"$CGROUP"/pids.max
sudo cat /sys/fs/cgroup"$CGROUP"/pids.events
如果目标服务与其他程序共享 UID,ps -u app 统计到的任务不一定都属于该服务。此时使用 cgroup 维度更容易建立因果关系。
8. 常见误解和反例
8.1 误解:ulimit -n 调大后,systemd 服务也会变大
反例:
ulimit -n 65536
systemctl restart example.service
systemctl 只是向 PID 1 发送请求。服务进程由 systemd 创建,不是由当前 shell 直接 fork/exec。当前 shell 的 RLIMIT_NOFILE 不会自动传递给 systemd 启动的服务。
正确做法是在 unit 中配置:
[Service]
LimitNOFILE=65536
然后:
sudo systemctl daemon-reload
sudo systemctl restart example.service
最后检查:
PID=$(systemctl show -p MainPID --value example.service)
sudo grep 'Max open files' /proc/$PID/limits
8.2 误解:LimitNPROC 就是该服务最多有多少线程
反例:
[Service]
User=app
LimitNPROC=1000
另一个服务也使用 User=app,并且已经消耗 900 个进程/线程。当前服务只能使用剩余空间,实际还会受到系统内存和 cgroup pids.max 约束。
如果需求是:
example.service 的所有后代任务总和不超过 1000
应使用:
[Service]
TasksMax=1000
如果还需要限制 UID 层面的整体任务数量,才考虑同时使用 LimitNPROC。
8.3 误解:线程数与进程数是完全独立的两种额度
Linux 内核调度的是 task。pthread_create() 创建的线程通常也会创建内核 task,因此会受到 cgroup pids.max 和相关用户任务计数影响。
一个进程有 1 个主线程和 999 个工作线程,不应简单理解为“只有一个进程”。在进程数保护和 fork bomb 防护中,必须把线程纳入统计。
8.4 误解:提高所有上限就能修复资源不足
如果应用因为 fd 泄漏最终持有几十万个 socket,把 LimitNOFILE 调到更大只会推迟故障。
类似地,把 TasksMax 设置为无限,会使错误的线程创建策略更容易拖垮服务 cgroup,甚至加剧调度和内存压力。
限制的作用包括:
防止单个服务无限消耗资源
尽早暴露泄漏或错误并发模型
缩小故障影响范围
它不是性能优化开关。正确值应由业务并发模型、连接生命周期、worker 模型、重启行为和机器容量共同决定。
9. 一个可计算的配置示例
假设某网络服务的并发设计如下:
最大客户端连接数 20,000
每个连接平均占用 fd 1
监听和管理 fd 32
日志、配置、数据库等固定 fd 128
优雅重启期间旧连接重叠系数 1.5
安全余量 20%
先计算正常峰值:
考虑滚动重启或 worker 重叠:
再加入 20% 余量:
可以向上取整为:
[Service]
LimitNOFILE=40000:40000
这个算例成立的前提是“每个客户端连接确实平均只占用一个 fd”。如果 TLS 库、代理层、每连接临时文件或辅助管道额外消耗 fd,模型必须修改。
反例是直接配置:
LimitNOFILE=20000
它把业务连接数误认为总 fd 数量,连固定 fd 和重启重叠都没有计算,服务可能在连接数尚未达到 20000 时就返回 EMFILE。
10. systemd 配置验证、恢复与变更风险
10.1 查看 unit 文件和最终属性
查看完整合并配置:
systemctl cat example.service
查看 systemd 解析后的关键值:
systemctl show example.service \
-p LimitNOFILE \
-p LimitNPROC \
-p TasksMax \
-p User \
-p MainPID
如果 unit 使用了 drop-in:
systemctl edit example.service
通常会生成:
/etc/systemd/system/example.service.d/override.conf
避免直接修改发行版提供的 /usr/lib/systemd/system/*.service 或 /lib/systemd/system/*.service,因为包升级可能覆盖修改。
10.2 修改后必须同时验证配置和运行态
推荐顺序:
sudo systemctl daemon-reload
sudo systemctl restart example.service
systemctl is-active example.service
systemctl show example.service -p MainPID -p LimitNOFILE -p TasksMax
PID=$(systemctl show -p MainPID --value example.service)
sudo cat /proc/$PID/limits
如果服务未启动:
systemctl status example.service
journalctl -u example.service -b --no-pager
如果出现类似资源限制设置失败,应检查:
- 指令拼写是否正确;
- 单位是否正确;
- soft limit 是否超过 hard limit;
- 值是否超过
fs.nr_open; - systemd 版本是否支持该指令;
- 服务是否实际重新启动;
User=、特权和 capability 是否改变了权限行为。
回滚时删除 drop-in 或恢复旧值,再执行:
sudo systemctl daemon-reload
sudo systemctl restart example.service
不能只删除文件而不 reload,也不能只 reload 而期待旧进程自动改变限制。
11. 与 cgroups v2 的分工
systemd 的服务资源控制逐渐以 cgroups v2 为核心。可以按控制对象理解:
| 需求 | 更适合的机制 |
|---|---|
| 限制单个进程可持有的 fd | RLIMIT_NOFILE / LimitNOFILE |
| 限制 UID 维度的进程/线程数量 | RLIMIT_NPROC / LimitNPROC |
| 限制一个服务 cgroup 的任务总数 | pids.max / TasksMax |
| 限制服务组内存 | memory.max / MemoryMax |
| 限制 CPU 带宽 | cpu.max / CPUQuota |
| 限制块设备 I/O | io.max / IOReadBandwidthMax 等 |
这种分工很重要:
RLIMIT_NOFILE:一个进程的 fd 边界
TasksMax:一个 cgroup 的 task 边界
MemoryMax:一个 cgroup 的内存边界
例如,一个服务可以有 100 个进程,每个进程拥有 1000 个 fd:
TasksMax=100
LimitNOFILE=1000
这并不等价于“服务最多拥有 1000 个 fd”。LimitNOFILE 是逐进程限制,服务总体 fd 数量可能接近:
还要加上 cgroup 中其他进程的 fd。因此,如果要控制服务的总内存、总任务数或总 I/O,应使用对应的 cgroup 控制器,而不是把进程级 RLIMIT 当作组级总量。
12. 权限、发行版差异和特殊值
12.1 infinity 不是无条件的无限资源
systemd 中常见:
LimitNOFILE=infinity
它通常表示尽可能设置为无限值,但内核仍可能受:
cat /proc/sys/fs/nr_open
以及实现、权限和系统架构约束。它不意味着系统能无限创建 fd。
对于 TasksMax=infinity,也不能绕过系统 PID 范围、内存和其他 cgroup 层级的父级限制。
12.2 root 不等于绕过所有限制
Linux 的资源限制与 capability 有关。部分 RLIMIT 的强制检查对具有特定特权的进程存在例外,但不同资源的规则不同,不能简单概括为“root 永远不受限制”。
此外,即使 root 进程不受某个 RLIMIT_NPROC 检查,也仍可能受:
- cgroup
pids.max; - 内存限制;
- 系统 PID 上限;
- 内核资源;
- 安全策略;
- systemd sandbox 设置。
生产配置不应依赖“以 root 运行”来获得稳定性。
12.3 systemd 默认值是版本和发行版相关的
以下内容可能变化:
- systemd manager 的默认
LimitNOFILE; TasksMax默认值;- cgroup v1 或 v2 的使用方式;
- PAM 是否加载
pam_limits.so; - 发行版提供的服务 unit drop-in;
- shell 的
ulimit选项和默认行为。
不要仅凭另一台发行版机器上的:
ulimit -a
推断当前 systemd 服务的限制。应检查目标服务的:
systemctl show
/proc/$PID/limits
/sys/fs/cgroup/...
13. 一套从现象到根因的诊断流程
遇到“连接建立失败”“线程创建失败”或“服务启动后行为异常”时,可以按以下顺序建立证据链。
第一步:确定真实服务进程
systemctl show example.service -p MainPID -p ControlGroup
不要只检查 systemctl 客户端或登录 shell。
第二步:检查实际 RLIMIT
PID=$(systemctl show -p MainPID --value example.service)
sudo cat /proc/$PID/limits
重点看:
Max open files
Max processes
Max address space
Max locked memory
Max stack size
第三步:检查 fd 当前使用量
sudo ls /proc/$PID/fd | wc -l
sudo ls -l /proc/$PID/fd | head -50
计数是瞬时近似,因为进程可能并发打开和关闭 fd;必要时应使用应用自身指标或短时间采样。
第四步:检查 UID 和 cgroup 任务数
ps -u app -L --no-headers | wc -l
CGROUP=$(systemctl show -p ControlGroup --value example.service)
sudo cat /sys/fs/cgroup"$CGROUP"/pids.current
sudo cat /sys/fs/cgroup"$CGROUP"/pids.max
sudo cat /sys/fs/cgroup"$CGROUP"/pids.events
第五步:检查全局文件资源和内核日志
cat /proc/sys/fs/nr_open
cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr
dmesg -T | tail -100
使用容器、权限受限环境或发行版日志服务时,dmesg 可能不可读,应结合:
journalctl -k
第六步:根据 errno 和边界选择修复
EMFILE
-> 优先检查该进程 RLIMIT_NOFILE、fd 泄漏和连接模型
ENFILE
-> 检查系统全局文件对象耗尽及其他服务
EAGAIN(fork/pthread_create)
-> 同时检查 RLIMIT_NPROC、pids.max、系统 PID 和内存
服务启动失败且限制设置报错
-> 检查 systemd 语法、权限、fs.nr_open 和版本支持
只有当运行态证据表明限制确实是瓶颈时,才提高对应限制。否则,修改配置可能只是把故障推迟或扩大故障半径。
14. 核心结论
ulimit 是 shell 提供的操作入口,RLIMIT 是内核执行的进程级限制,systemd 的 Limit* 是为服务进程设置 RLIMIT 的声明式配置,而 TasksMax 等 systemd 资源控制通常属于 cgroup 层级。
文件描述符限制至少要同时考虑:
进程 RLIMIT_NOFILE
内核单进程 fd 上界 fs.nr_open
系统全局文件对象上限 fs.file-max
应用实际 fd 生命周期和泄漏
进程和线程数量限制至少要同时考虑:
RLIMIT_NPROC 的真实 UID 维度
cgroup pids.max 的服务组维度
系统 PID 范围
内存和内核任务资源
因此,可靠的判断方式不是问“ulimit 是多少”,而是明确回答:
哪个进程?
哪个资源?
哪个 soft/hard 值?
哪个 UID 或 cgroup?
失败时返回了什么 errno?
系统还有没有更外层的全局限制?
这些问题分别对应 Linux 的进程模型、systemd 服务模型和 cgroups 资源模型。只有把三者放在同一条数据流中,文件句柄耗尽、线程创建失败和服务启动限制错误才不会被误判为同一种问题。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux auditd 审计:规则、事件、性能、检索和合规留存
- 下一篇:Linux 时间同步:Clocksource、NTP、chrony、漂移和分布式系统影响
- 延伸:systemd 服务管理:Unit、依赖、重启、资源限制和安全沙箱
- 延伸:Linux cgroups v2 深入:CPU、Memory、IO、PID、Pressure 和委派
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论