
1. 板子跑着跑着报 Too many open files先把 ulimit 管的那几个开关认全先说一个我在 RK3588 边缘盒子上遇到的真实场景一台带四路网络摄像头接入 NPU 推理 本地录像的板子跑起来前两个小时一切正常第二天早上日志里开始刷Too many open files随后推流线程一个个退掉进程还在但业务已经死了。第一反应当然是去改/etc/security/limits.conf把nofile从 1024 拉到 65535reboot之后发现ulimit -n还是 1024。这个问题我在不同板子上重复遇到过好几次每次卡住的点都不完全一样所以这篇不讲改哪个文件这种一句话答案而是把 RK3588 这类 ARM64 板卡上 ulimit 的生效链条完整拆一遍。ulimit是 shell 的内建命令它本身不设置系统限制只是在调用setrlimit(2)去修改当前进程的 rlimit 结构这个结构会被 fork 出的子进程继承。也就是说ulimit的作用域是进程树不是系统。理解这一点非常关键因为绝大多数改了不生效的问题本质都是改的地方和出问题的进程不在同一棵进程树上。RK3588 这类板子常见于边缘计算盒子、NVR、车载终端、工控一体机特点是要在有限的内存和单一 rootfs 上同时跑多媒体、网络、AI 推理几套东西。这些业务对资源限制的消耗类型完全不同先看一张对照表把常见资源项和它们在这类设备上的典型触发点对应起来资源项shell 键内核对应RK3588 上的典型触发场景RLIMIT_NOFILE-nstruct rlimit的nofile多路 RTSP 拉流、大量 MQTT 长连接、文件句柄泄漏RLIMIT_NPROC-u按 UID 统计的进程/线程数GStreamer pipeline 开大量线程、线程池未复用RLIMIT_MEMLOCK-l可mlock的物理页NPU 推理池、实时音频、mlockall预分配RLIMIT_STACK-s主线程栈大小大数组放栈上、深递归的解析代码RLIMIT_CORE-ccore dump 文件大小崩溃现场保留、调试板子上的段错误RLIMIT_RTPRIO-r实时优先级上限视频采集线程要设SCHED_FIFO1.1 soft limit 与 hard limit能自己抬和只能求 root 抬每个资源项在rlimit里是一对值rlim_cursoft和rlim_maxhard。普通进程只能把 soft 提高到不超过 hard 的值也可以把 hard 往下降——但一旦降下去在同一个进程树里就再也升不回来了因为提升 hard 需要CAP_SYS_RESOURCE能力。这个设计是不可逆降级我踩过的最难受的一个坑就是在启动脚本里写了ulimit -Hn 4096做临时保护然后在同一个 shell 里起服务结果后面所有子进程的 hard limit 全被钉死在 4096再改limits.conf也没用必须重新登录或者重启服务单元。所以日常操作里有一条铁律要降 hard limit一定要放在最末端、最局部的地方要做全局调优只动 soft或者只在系统级配置里统一抬高 hard。1.2 为什么 RK3588 上这类报错来得特别早在 x86 服务器上nofile的发行版默认值往往已经是 1024:524288 甚至更高而很多板卡的 rootfs 镜像里/etc/security/limits.conf还是空的模板systemd的服务默认LimitNOFILE是 1024:4096老版本或 1024:524288新版本但只在部分配置下生效。更麻烦的是嵌入式场景下 rootfs 常常是精简构建出来的PAM 模块可能压根没装limits.conf就是个摆设文件——写了也不报错但一点作用都没有。另一个放大器是业务模型一台 RK3588 盒子接四路 4K 摄像头每路 RTSP 拉流至少 2 个 socketRTP RTCP加上解码器的 buffer 文件、录像的 mp4 分片、日志、Unix domain socket、epoll 注册的 fd一路轻松吃掉 30~50 个 fd再来个 NPU 推理服务对每路做 detour、一个 Web 管理面板、一个 MQTT 上报几百个 fd 是起步价。1024 这个默认值在这种负载下撑不过半天。2. 限制是分层的内核总量、PAM 会话、systemd 单元各管一段很多人以为 ulimit 是一层配置实际上在 RK3588 这种 systemd PAM 的 rootfs 上至少有三层而且它们的优先级和覆盖关系是从内到外层层收紧的。2.1 内核层的三个总量开关决定了天花板内核并不直接管某个进程能开多少 fd它管的是系统总共能有多少个文件。相关的 sysctl 有三个fs.file-max整个系统能同时打开的文件数量上限内核按 CPU 和内存自动算一般是内存页数的 10% 左右。这是全局总量不是单进程上限。fs.nr_open单个进程能打开的文件描述符最大数量默认 1048576。这是很多人的盲区——你把LimitNOFILE设成 2000000setrlimit会直接失败返回EPERMsystemd 单元会起不来日志里只给一句很含糊的 Failed to set RLIMIT。kernel.pid_max和kernel.threads-max进程和线程的总量约束RLIMIT_NPROC的实际效果会被threads-max二次限制。这三者是物理天花板上面两层的任何配置都只能在它们之下活动。所以调优顺序永远是先抬天花板再配会话最后配服务反过来做会出现配置写了但被静默截断的现象。2.2 PAM 这一路只覆盖登录会话且必须装了 pam_limits/etc/security/limits.conf和/etc/security/limits.d/*.conf不是内核读的是pam_limits.so在会话建立时读的。也就是说rootfs 里必须存在pam_limits.so/etc/pam.d/里对应的服务sshd、login、common-session必须引用它只有通过会话建立这条路径启动的进程才会被应用。嵌入式板子上最常见的翻车就是第 1 条——用精简方式构建出来的 rootfs 根本没有 PAM/etc/pam.d目录都不全limits.conf写满了也是白写。判断方法很简单# 看 pam_limits 是否存在 find /lib /usr/lib -name pam_limits.so* 2/dev/null # 看 sshd 是否引用Debian 系通常是 include common-session grep -r pam_limits /etc/pam.d/两条命令都没输出说明这条路对你无效直接跳到 systemd 层去配。2.3 systemd 这一路优先级最高而且很多人不知道它存在在 systemd 系统上所有服务都是 systemd 的子进程它们的初始 rlimit 继承自 systemdPID 1跟limits.conf完全没关系。systemd 里控制这个的地方有三处/etc/systemd/system.conf的DefaultLimitNOFILE作用于系统服务/etc/systemd/user.conf的DefaultLimitNOFILE作用于用户服务单元文件里的LimitNOFILE覆盖上面两者只作用于该单元三者的覆盖关系是就近原则。另外还有/etc/systemd/logind.conf里的UserTasksMax它限制的是每个用户能创建的任务数cgroup pids 控制器跟RLIMIT_NPROC是两个东西但报错现象很像容易混。我见过一个 caseulimit -u明明设成了 65535但服务一开线程池就报 Resource temporarily unavailable最后查到是UserTasksMax卡在 4096。配置位置生效对象是否需要重启常见失效原因fs.nr_open/fs.file-max全系统sysctl -p即时生效没写进/etc/sysctl.d/重启丢失limits.conf/limits.dPAM 会话ssh、login、su重新登录没装 pam_limits、通配符不匹配 rootsystem.conf/user.conf所有 systemd 单元systemctl daemon-reexec只改了system.conf用户服务不受影响单元LimitNOFILE单个服务systemctl daemon-reload restart单元被 drop-in 覆盖容器--ulimit容器内进程重建容器宿主机 hard limit 不够3. 改完不生效的七个断点把排查链路走一遍这一节是我自己的排查清单顺序是从下往上因为越靠下的越容易被忽略。每次遇到改了没效果按顺序走一遍基本十分钟内能定位。3.1 断点一先确认你看的是哪个进程的限制这是最容易犯的错。在终端里敲ulimit -n看到 65535就以为配好了但报错的进程根本不是这个终端起的。正确的做法是直接看目标进程# 看某个正在跑的进程的真实限制 cat /proc/$(pidof your_app)/limits # 输出长这样注意 Max open files 那行 # Limit Soft Limit Hard Limit Units # Max open files 1024 4096 files/proc/PID/limits是唯一可信的真相来源。同时可以看它当前开了多少ls /proc/$(pidof your_app)/fd | wc -l如果这个数字已经贴着 Soft Limit那没什么可怀疑的就是撞上限了。3.2 断点二跑的是服务改的是会话这是最高频的原因。limits.conf只对登录会话生效你的业务进程如果是systemd拉起来的那它继承的是 systemd 的默认值。验证方法systemctl show your-app.service -p LimitNOFILE -p LimitNPROC -p LimitMEMLOCK输出如果还是LimitNOFILE1024:524288说明你改的limits.conf对它无效需要去写单元文件或者system.conf。3.3 断点三通配符*不包括 root/etc/security/limits.conf里的*是个用户名通配但 pam_limits 对 root 有特殊处理——*不匹配 root必须显式写一行root。嵌入式板子上业务基本都跑在 root 下所以这一条几乎必踩。正确的写法是* soft nofile 65535 * hard nofile 65535 root soft nofile 65535 root hard nofile 65535顺手提一句limits.conf的格式是三段式加一个可选的第四段写成* - nofile 65535表示 soft 和 hard 一起设。很多人写* soft nofile 65535只设了 soft然后发现 hard 还是 4096后面想再抬就抬不上去了。3.4 断点四fs.nr_open把上限锁死了如果你在system.conf里写了DefaultLimitNOFILE2000000而fs.nr_open还是默认的 1048576那 systemd 会直接忽略或者截断服务可能起不来。查一下sysctl fs.nr_open fs.file-max kernel.pid_max经验值是fs.nr_open设成你需要的最大nofile的 1.5~2 倍fs.file-max再高一个量级。比如目标nofile65535fs.nr_open1048576完全够用不需要动目标nofile1000000那就得把fs.nr_open抬到 2000000。3.5 断点五登录方式不同结果完全不同同一台板子SSH 登录进去ulimit -n是 65535插串口终端登录进去还是 1024。原因很直接SSH 走sshd/etc/pam.d/sshd里 include 了common-sessionpam_limits生效串口getty走的是另一套 PAM 配置甚至可能是 busybox 自带的简化登录压根不走 PAM。另一个变种是su和sudo。su -带横杠会重建完整登录会话PAM 生效su不带横杠在很多发行版上不重建会话PAM 的 session 阶段可能被跳过。sudo则取决于/etc/pam.d/sudo的配置。所以我用 sudo 试了是好的这句话不能作为配置成功的证据。3.6 断点六systemd-logind 的用户级默认值较新版本的 systemd 在logind.conf里提供了UserTasksMax在user.conf里提供了DefaultLimitNOFILE。如果你的服务是 user unitsystemctl --user那它读的是user.conf而不是system.conf。板子上跑桌面环境的场景下这个坑特别常见。3.7 断点七容器里是另一套继承链板上用 Docker 跑业务的话容器内进程的 rlimit 来自dockerd的继承再叠加docker run --ulimit参数。默认情况下 Docker 会把nofile设成1024:524288或者继承 daemon 的设置。容器内改limits.conf是无效的因为容器里通常没有完整的 PAM就算有进程也不是通过登录会话启动的。# 容器运行时指定 docker run --ulimit nofile65535:65535 --ulimit memlock-1 your-image # 已经在跑的容器看实际值 docker exec container cat /proc/1/limits排查链路走完一般会落在服务层或者容器层上因为这两层是默认生效的。4. RK3588 上把配置真正钉住的四步操作知道原理之后落地其实很快。我习惯按量、内核、会话、服务的顺序做每一步都能单独验证出问题好回滚。4.1 第一步先量别拍脑袋定数字先把现有负载的真实需求测出来不然后面设多少都是猜。我一般这么做# 业务跑满负载后看目标进程的实际 fd 消耗 PID$(pidof your_app) ls /proc/$PID/fd | wc -l # 看 fd 的类型分布判断是 socket 多还是文件多 ls -l /proc/$PID/fd 2/dev/null | awk {print $NF} | sed s/.*\[//;s/\]// | sort | uniq -c | sort -rn | head # 看系统全局的文件使用情况 cat /proc/sys/fs/file-nr # 三个数字已分配、已使用、上限测出来的峰值乘以 2~3 作为目标值是比较稳妥的做法。RK3588 的典型业务我一般会落在nofile65535这个量级——够用又不至于让 fd 表占用太多内存。4.2 第二步抬内核总量写进 sysctl.d不要直接改/etc/sysctl.conf写独立文件更好管理也方便回滚cat /etc/sysctl.d/99-rk3588-limits.conf EOF # 单进程 fd 硬天花板必须 任何 LimitNOFILE fs.nr_open 1048576 # 系统全局文件句柄总量 fs.file-max 2097152 # 进程/线程总量 kernel.pid_max 4194304 # 高并发网络场景顺手带上 net.core.somaxconn 1024 net.ipv4.tcp_max_syn_backlog 2048 EOF sysctl --systemsysctl --system会按文件名顺序加载/etc/sysctl.d/下所有配置99-前缀保证它排在发行版默认值之后。验证sysctl fs.nr_open fs.file-max kernel.pid_max注意fs.nr_open改大不需要重启但改小的时候如果有进程已经超过了新值会失败得先把进程收掉。4.3 第三步会话层用 limits.d 而不是改 limits.conflimits.conf是发行版模板升级时可能被覆盖我习惯放独立文件cat /etc/security/limits.d/99-rk3588.conf EOF * soft nofile 65535 * hard nofile 65535 root soft nofile 65535 root hard nofile 65535 * soft nproc 32768 * hard nproc 32768 root soft memlock unlimited root hard memlock unlimited EOFmemlock unlimited这一条在 RK3588 上很值得加NPU 推理框架和一些实时音频库会调mlockall预分配内存默认 64KB 的 memlock 会直接失败。加完之后重新登录验证。这里有个顺序细节如果 hard 值只写在limits.d里而 systemd 的默认 hard 是 4096那么通过 SSH 登录进来时pam_limits 想设 65535 的 hard 会被拒绝因为它自己也被继承的 hard 卡着。所以第三步之前第四步的 systemd 全局值必须已经抬上去两层要配套。4.4 第四步服务层system.conf 加单元覆盖双保险先改全局默认让所有新起的单元都有足够的空间mkdir -p /etc/systemd/system.conf.d cat /etc/systemd/system.conf.d/99-rk3588-limits.conf EOF [Manager] DefaultLimitNOFILE65535:65535 DefaultLimitNPROC32768:32768 DefaultLimitMEMLOCKinfinity EOF # 用户级服务也要配否则 systemctl --user 起的服务还是老值 mkdir -p /etc/systemd/user.conf.d cat /etc/systemd/user.conf.d/99-rk3588-limits.conf EOF [Manager] DefaultLimitNOFILE65535:65535 EOF然后针对个别重负载服务做单元级覆盖用 drop-in 的方式而不是直接编辑原单元文件mkdir -p /etc/systemd/system/your-app.service.d cat /etc/systemd/system/your-app.service.d/limits.conf EOF [Service] LimitNOFILE65535 LimitNPROC32768 LimitMEMLOCKinfinity LimitCOREinfinity EOF systemctl daemon-reload systemctl restart your-appdaemon-reexec和daemon-reload的区别值得记一下改system.conf这类全局配置需要systemctl daemon-reexec让 PID 1 重新读配置改单元文件只需要daemon-reload。两个都执行一遍最保险systemctl daemon-reexec systemctl daemon-reload4.5 验证清单四个地方全对才算真的成了验证点命令期望结果内核天花板sysctl fs.nr_open≥ 你设的最大 nofile会话层重新 SSH 登录后ulimit -n65535服务层systemctl show your-app -p LimitNOFILE65535进程实际cat /proc/$(pidof your-app)/limitsMax open files 65535四个都对才算收工。只对前三个说明服务重启没做或者被 drop-in 覆盖了。5. NOFILE 之外的几个 limit板子上更隐蔽的坑调完nofile就完事的人不少但 RK3588 这类板机上还有几个限制会以完全不同的报错形式出现看着不像 ulimit实际上就是。5.1 RLIMIT_MEMLOCKNPU 推理和实时音频的隐形天花板默认 64KB这个值在服务器上基本没人管因为业务很少mlock。但在 RK3588 上NPU 驱动、某些推理 runtime、实时音频ALSA 的memlock、DPDK 风格的零拷贝网络都会用到。表现是mlock failed: Cannot allocate memory或者推理框架初始化时报一段看不懂的 DMA 分配错误。这时候先查ulimit -l grep -i memlock /proc/$(pidof your_app)/limits改成unlimited就行。需要注意的是memlock unlimited在部分内核配置下需要CAP_IPC_LOCKroot 默认有普通用户没有。另外如果板子开了systemd的MemoryMax或者 cgroup v2 的memory.maxmlock的页也会计入超了照样失败。5.2 RLIMIT_NPROC不是进程数是任务数ulimit -u限制的是某个 UID 下的任务数进程 线程。这个很反直觉因为 Linux 里线程也是 task。RK3588 是 4×A76 4×A55 的八核业务代码里开个 32 线程的线程池很常见几路视频一叠加轻松上千个 task。默认 1024 或者 4096 的nproc很容易被打满。更隐蔽的是RLIMIT_NPROC对 root 用户其实是不生效的内核里对CAP_SYS_RESOURCE或CAP_SYS_ADMIN有豁免所以你会看到 root 跑的服务明明超了nproc也没报错而切到普通用户就立刻失败。如果业务里有降权运行的子进程这在安全加固过的镜像里很常见这个坑就会出现。还有一种情况是报错信息误导人fork: Resource temporarily unavailable看着像内存不够其实是nproc或者 cgroup 的pids.max打满了。查一下cat /sys/fs/cgroup/pids.max # cgroup v2 cat /sys/fs/cgroup/pids/pids.max # cgroup v15.3 RLIMIT_STACK主线程栈和线程栈是两码事ulimit -s设的是主线程栈默认 8MB。pthread_create创建的子线程栈由pthread_attr_setstacksize决定默认也是 8MB但不受ulimit -s约束实际上 glibc 会参考但不是强约束。板子上内存紧张的时候很多团队会把ulimit -s调到 1024省点虚拟内存结果主线程里一个大数组或者一次深度递归的 JSON 解析直接段错误。RK3588 上内存通常是 4/8/16GB栈其实不缺那几 MB除非你在跑几百个线程。真要省去调线程栈大小别动主线程栈。另外 8MB 的栈在嵌入式场景下对alloca和变长数组是很危险的见到-Wvla警告建议直接开-Werrorvla掐掉。5.4 RLIMIT_CORE调试期一定要开量产镜像经常把core设成 0 省空间但调试阶段这是自断后路。板子上的崩溃现场本来就难复现core关了等于什么都不剩。建议调试版镜像里ulimit -c unlimited echo /var/crash/core.%e.%p /proc/sys/kernel/core_pattern配合coredumpctlsystemd 系统或者直接分析 core 文件比看日志有用得多。注意core_pattern写到/var/crash时要确认那个分区够大RK3588 的 eMMC 容量通常不大一个几 GB 的 core 写下去可能把分区撑爆——这也是量产版关掉它的原因。6. 调大之后的反噬几个我真实踩过的坑把限制调大是看起来只有好处的操作实际上坑不少而且往往在上线几周后才暴露。6.1 硬上限的继承陷阱前面提过一次这里再强调。任何脚本里出现ulimit -Hn、ulimit -Hu这类降 hard 的写法都要极其小心。我见过一个启动脚本为了兼容老环境先执行ulimit -Hn 4096再exec业务进程结果这棵进程树里的 hard limit 永远升不回去后面怎么改配置都没用只能重启。排查的时候看/proc/PID/limits的 Hard Limit 是 4096Soft Limit 也是 4096一眼就能看出来是被脚本钉死了。6.2 fd 不是免费的每个 fd 都占内核内存nofile65535意味着理论上一个进程可以开 6 万多个文件内核会为每个打开的文件维护struct file大概 200~300 字节加上 inode、dentry 缓存。真跑到这个量级几 MB 到几十 MB 的内核内存就没了。板子只有几个 G 内存的时候这个开销是要算进去的。更需要注意的是fd limit 调大之后fd 泄漏变得更隐蔽了。原来 1024 的时候泄漏半天就报错你能立刻发现调到 65535 之后泄漏可能要跑一周才暴露而那时代码迭代了好几版排查成本高得多。所以调大限制的同时一定要加监控# 简单的 fd 使用率监控超过 70% 告警 PID$(pidof your_app) CUR$(ls /proc/$PID/fd 2/dev/null | wc -l) MAX$(awk /Max open files/{print $4} /proc/$PID/limits) [ $((CUR * 10 / MAX)) -ge 7 ] echo fd warning: $CUR/$MAX这个脚本挂到 cron 或者塞进你的监控 agent 里比事后 debug 便宜太多。6.3 调大 nofile 反而触发了 select() 崩溃这个坑很经典值得单独说。select()的fd_set在内核和 glibc 里都有个FD_SETSIZE1024的硬编码限制如果你往select()里传了大于 1023 的 fd行为是未定义的——在 ARM64 上通常是栈越界写然后以极难定位的方式崩溃。原来 fd limit 是 1024 的时候fd 编号天然不会超过 1023所以select()一直没问题。你把 limit 调到 65535 之后程序跑得久了fd 编号爬到 2000 多这时候再调select()就炸了。表现是随机段错误或者数据错乱backtrace 完全指不到业务代码。解决办法只有两个改用poll()或epoll或者对大 fd 做重映射不推荐太脏。RK3588 上跑多路网络的业务本来就该用epoll我一般建议在代码评审阶段就把select()直接列为禁用 API。6.4 固化与回滚别让配置只活在当前会话最后说一个流程上的事。板子上调试时ulimit -n 65535敲一下很方便但重启就没。所有验证有效的改动必须落到文件里内核参数 →/etc/sysctl.d/99-*.conf会话参数 →/etc/security/limits.d/99-*.conf服务参数 →/etc/systemd/system.conf.d/99-*.conf 单元 drop-in同时留一个回滚脚本把这三个目录下的 99 文件删掉systemctl daemon-reexecsysctl --system一分钟回到出厂状态。RK3588 的板子经常要发到现场出问题时现场人员只能执行脚本提前准备好这个比什么都值。还有一个细节如果板子用的是只读 rootfsoverlay 或者 squashfs tmpfs你改的这些文件重启就没了。这种情况一定要在构建镜像的阶段就写进去而不是在板子上手改。我见过在现场改完limits.d开机就还原排查了两天以为配置写错了最后发现是 rootfs 只读。配置这件事本身不难难的是链路太长、生效点太多每一层都有办法悄悄把你的设置吃掉。把/proc/PID/limits当成唯一的验收标准剩下的都只是过程。