ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

dup工具实战指南:文件描述符复制、重定向与FD泄漏排查

dup工具实战指南:文件描述符复制、重定向与FD泄漏排查 简介柯尼卡美能达DPUDriver Packaging Utility驱动打包工具使用说明面向需要批量部署打印机驱动的IT管理员和技术支持人员解决多台电脑重复安装驱动效率低、配置不一致的问题。文档以实际打包流程为主线从启动EXE程序、在本地正常安装驱动开始逐步讲解添加驱动、设置驱动名并勾选默认打印机、复制现有打印机设置作为默认首选项、修改为单面黑白打印并使用位图字体、通过“添加驱动名”补充32位与64位驱动、配置IP地址等操作细节最后保存生成可分发安装包。内容还涵盖了安装界面的自定义选项及包的部署方式按步骤操作即可完成标准化的驱动安装显著降低维护成本。资源为1个PDF文件压缩包大小约356KB内容精炼便于随时查阅。目前已有212人浏览学习适合管理多台柯尼卡美能达打印机的企业环境有助于统一驱动版本、简化分发流程、提升整个打印系统的稳定性。1. dup 工具是什么一份 PDF 说明背后是文件描述符复制的刚需dup 工具这个名词一出现就常配着一份 PDF 使用说明但它从来不是某个厂商的图形软件——它指的是 dup、dup2、dup3 这一族文件描述符复制系统调用以及围绕它们封装起来的命令行小工具。你每天敲的21、日志守护进程里的重定向、Nginx 和 Redis 的 daemon 化底层全是它在干活。这份说明要讲透三件事这组调用能解决什么、参数怎么设、出问题时怎么从/proc和 strace 里把真相挖出来。很多后端开发和网络运维把 FD 泄漏当成黑匣子其实只要把 dup 工具用对了现场一目了然。适合所有需要跟服务端进程、shell 重定向和文件描述符打交道的人。2. 从 dup 到 dup3 的选型三个系统调用决定重定向的底层行为2.1 dup、dup2、dup3 的签名差异一张参数表讲清楚先把三个调用放到同一个文件里看#include unistd.h int dup(int oldfd); int dup2(int oldfd, int newfd); #define _GNU_SOURCE #include unistd.h int dup3(int oldfd, int newfd, int flags);注意#define _GNU_SOURCE必须放在所有#include之前否则编译时看不到 dup3 的声明。三个调用的共同点是让两个文件描述符指向同一个「打开文件描述」也就是共享同一个文件偏移和打开模式区别在于你有多大的控制权。调用新 fd 怎么选老 fd 被占用怎么办附加能力适用场景dup自动选当前最小可用编号不涉及永远分配新号无快速拿到一个副本dup2指定 newfd先静默关闭再复用原子无标准重定向 0/1/2dup3指定 newfd同上flags 传 O_CLOEXEC多线程 exec 场景我一般会优先用 dup2 而不是 dup因为依赖 dup 的「最小可用编号」在进程运行一段时间后会变得不可预测很容易在某个瞬间踩中一个刚被 close 的低编号 fd。dup2 把目标编号写死行为是确定的。先看一个最直接的最小复现。这个 C 程序做的事情是打开一个日志文件用 dup 复制出一个副本关掉原 fd再用副本继续写#include stdio.h #include fcntl.h #include unistd.h int main(void) { int fd open(/tmp/dup.log, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd 0) { perror(open); return 1; } int copy dup(fd); /* 复制 fd返回一个新的描述符 */ write(copy, hello dup\n, 10); close(fd); /* 关掉原 fd */ write(copy, still alive\n, 12); /* 副本不受影响继续可写 */ close(copy); return 0; }运行完后cat /tmp/dup.log可以看到两行内容按顺序出现。这里的重点是「共享文件偏移」dup 出来的副本不是把文件内容重新打开一遍而是两个 fd 共同引用同一个内核对象所以 write 会接着上一次的偏移继续写。新手最容易误以为 copy 是「另开一个文件」然后惊讶为什么不是从 0 开始写。参数层面的第二个要点是 errno。三个调用失败时都返回 -1最常见的三个错误码是 EBADFoldfd 不是打开的 fd、EMFILE进程 fd 数达到ulimit -n上限、EINVALdup3 传了 O_CLOEXEC 之外的 flags。看到 EMFILE 时不要急着调ulimit -n先确认是不是有 fd 泄漏——这个判断方法在第 4 章展开。2.2 21 为什么是 dup2 的功劳从 shell 重定向到守护进程你每天都在敲的cmd file 21shell 在背后做的事就是一次dup2(file_fd, 1)和一次dup2(file_fd, 2)。把这条命令拆开看更直观# 三步分开做效果等价于 cmd file 21 bash -c exec 3 /tmp/a.log; exec 23; exec 13; exec 3-; echo err 2; echo out这里的exec 23、exec 13就是 dup2 的语法糖把 fd 2 和 fd 1 复制成 fd 3 的副本三者指向同一个打开文件描述。最后的exec 3-关闭 fd 3但 stdout 和 stderr 仍然引用这个文件所以echo err和echo out都能写进/tmp/a.log。这个例子顺带验证了 2.1 的结论关闭原 fd 不影响副本。真正容易翻车的地方是顺序。cmd 21 file和cmd file 21的结果完全不同# stderr 留在终端上因为 21 先把 stderr 指到了旧的 stdout终端 cmd 21 file # stderr 进 file因为先改了 stdout再把 stderr 指过去 cmd file 21原因很简单dup2 复制的是「当前时刻」目标 fd 指向的东西而不记录任何「以后 stdout 会变」的关系。从右往左读这条命令每一步都在复制当时的现场。这也是我每次讲解重定向顺序时必举的例子。守护进程场景里 dup2 的地位更直观。经典的 daemon 化流程是先 fork、再 setsid、再 fork然后把 0/1/2 全部重定向到 /dev/nullint nullfd open(/dev/null, O_RDWR); if (dup2(nullfd, 0) 0) perror(dup2 stdin); if (dup2(nullfd, 1) 0) perror(dup2 stdout); if (dup2(nullfd, 2) 0) perror(dup2 stderr); if (nullfd 2) close(nullfd);注意这里的写法先把 /dev/null 开到一个临时 fd再 dup2 到 0/1/2最后关闭临时 fd。为什么要多绕这一步如果直接dup2(open(...), 1)极端情况 open 返回的恰好是 1行为会变得很绕。先开临时 fd 再逐个 dup2撤场干净也避免把没有被 dup2 覆盖的 fd 留在进程里。到这里你应该能回答一个常见面试连环题为什么 dup 之后关闭原 fd副本还能用因为关闭只是减少引用计数只要还有一个 fd 指着这个打开文件描述文件就不会真正释放。这个特性也是后文「文件删了但磁盘空间不释放」的根源。3. 把 PDF 里的使用说明落到命令最小可复现的 dup 工具用法3.1 自建一个 50 行的 dup 命令行工具源文件、编译与运行直接背系统调用签名和真正有一把顺手的工具是两回事。我见到的大多数自用方案是把 dup/dup2/dup3 封装成一个几十行的命令行小程序日常用来验证复制行为和排查 fd 指向。这里给你一个最小版本源文件我习惯叫 dupfd.c#define _GNU_SOURCE #include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include getopt.h int main(int argc, char **argv) { int src 3, dst 4, flags 0, opt; while ((opt getopt(argc, argv, s:d:c)) ! -1) { switch (opt) { case s: src atoi(optarg); break; case d: dst atoi(optarg); break; case c: flags O_CLOEXEC; break; default: fprintf(stderr, usage: dupfd -s src -d dst [-c]\n); return 2; } } if (dup3(src, dst, flags) 0) { perror(dup3); return 1; } /* 通过 /proc/self/fd 验证复制结果指向同一个打开文件 */ char link[64], buf[256]; snprintf(link, sizeof link, /proc/self/fd/%d, dst); ssize_t n readlink(link, buf, sizeof buf - 1); if (n 0) { buf[n] \0; printf(dup3(%d, %d) done, fd %d - %s\n, src, dst, dst, buf); } return 0; }编译只有一个注意点dup3 要用到_GNU_SOURCE并且这个宏必须出现在第一个#include之前否则编译器会按隐式声明处理运行起来行为不可控这是 C 里最容易踩的坑之一。gcc -Wall -Wextra -o dupfd dupfd.c # 把 /etc/hostname 作为 fd 3 传给 dupfd复制到 fd 5程序自己验证指向 ./dupfd -s 3 -d 5 3/etc/hostname预期输出是dup3(3, 5) done, fd 5 - /etc/hostname。3/etc/hostname是 shell 重定向语法它把 hostname 以只读方式打开成 fd 3 并传给 dupfd 进程dupfd 继承这个 fd执行 dup3(3, 5) 完成复制然后 readlink/proc/self/fd/5确认 fd 5 和 fd 3 指向同一个文件。-s指定源 fd-d指定目标 fd-c给复制出的 fd 打上 close-on-exec 标记。不用-c时fork 之后这个 fd 会跟着子进程走加上之后任何 exec 都会自动关闭它这是多线程编程里防止 fd 泄漏的关键。需要留意的是dupfd 进程退出后 fd 5 就被释放了你不会在父 shell 的/proc/$$/fd里看到它——命令行工具没法给父进程新增 fd这是内核隔离决定的。所以它真正的价值是验证「调用能否成功、复制后指向是否一致」以及观察从外部传进来的 fd 是否能被正确继承和复制。这个工具在嵌入式环境里尤其好用因为那些设备上通常没有 gdb 和 strace想确认一段重定向逻辑是否生效编译一个静态版 dupfd 丢进去配合3重定向就能看到结果。3.2 用 pdftotext 从 PDF 说明里抠参数-layout 避免被分页拆断拿到《dup工具使用说明.pdf》这类文档我的第一个动作永远是先用 pdftotext 把 PDF 解析成纯文本再检索而不是打开阅读器一页页翻。原因很简单PDF 手册的正文和命令往往跨页眼睛看容易漏搜索也不方便。pdftotext -layout dup工具使用说明.pdf dup.txt grep -n -E dup3|O_CLOEXEC|EMFILE|dup2 dup.txt-layout这个参数非常关键它尽量保留原版面的行列结构命令参数表不会被拆成一坨。如果手头这份 PDF 是扫描版里面全是图片pdftotext 解析出来是空文本那就先用pdfimages -list看一下页数把关键页导出成图片再人工读不要浪费时间在调编码上。从 PDF 里复制命令到终端时还有一个隐藏杀手行尾的连字符。PDF 排版经常把长命令在-后面断行复制出来就多了一个无意义的换行shell 直接报 command not found。我一般会先cat -A dup.txt检查隐藏符号或者用下面这条 sed 把「行尾连字符 换行」合并回一行pdftotext -layout dup工具使用说明.pdf dup.txt sed -i :a;N;$!ba;s/-\n//g dup.txt这个 sed 的:a;N;$!ba是把整个文件读进模式空间后再做全局替换只处理以连字符结尾的行不会动正文里的短横线。文件特别大时这个写法会占内存但一份使用说明通常只有几页到几十页完全够用。也提醒一句不要一上来就想着把 PDF 转成 markdown 或 word。这类格式转换对表格和命令行的还原度并不高转完经常出现参数被塞进同一行、21的排印宽度被改掉之类的问题。最稳的路线是先-layout转纯文本、保住版面再把真正要用的命令段手工贴回终端验证一遍。命令这种东西验证过才信不要信转换结果。4. 排查文件描述符泄漏用 /proc、strace 给 dup 工具做体检4.1 ls /proc/PID/fd 的三个必看指标数量、目标、deleteddup 工具用得多了你会发现它绝大部分时间不是在复制 fd而是在帮你诊断 fd 泄漏。泄漏的起点千奇百怪但现场都集中在同一个地方/proc/PID/fd。先看数量。一个健康的长驻进程fd 数量应该在几十以内且保持稳定如果它随时间单调增长基本就是泄漏。把观察做成循环比看一眼准得多PID$(pgrep -f myservice | head -1) for i in {1..120}; do echo $i $(ls /proc/$PID/fd | wc -l) sleep 1 done | awk $2 200 {print WARN: count$2; exit}ls /proc/$PID/fd | wc -l数出来的就是当前 fd 数量因为 ls 不会计入.和..。如果 120 秒内数量一路上涨不用再猜先把ulimit -n和/proc/$PID/limits里的 open files 软硬限制记下来然后回代码里查谁在反复 open/dup 而不 close。再看目标。ls -l /proc/$PID/fd每一行会告诉你这个 fd 指向哪里最常见的几个目标类型值得记一下/dev/pts/*终端、/dev/null、/path/to/file、socket:[编号]、pipe:[编号]。如果某个 fd 指向了不该出现的文件或者大量 fd 指向同一个文件那基本都是复制后忘关的迹象。第三看 deleted。当一行显示/var/log/nginx/access.log (deleted)时说明有人把这个文件删了但进程还握着这个 fd 继续写。这在 dup 工具场景里尤其容易发生你复制了一个日志 fd主 fd 关了副本还开着日志轮转脚本 unlink 文件后空间一直不释放df 显示磁盘满但 du 把所有目录加起来又是对的——这个矛盾几乎是 deleted 文件的独有特征。配合 lsof 可以看得更细lsof -p $PID | grep -c pipe lsof L1 | grep myservicelsof L1只列出链接数小于 1 的文件也就是那些被删除但还被打开的 inode。看到结果后正确做法是让进程重新打开日志文件或者重启进程释放 fd而不是再去删一次别的文件——删再多也腾不出你要的空间。4.2 strace 跟踪 dup/dup2/dup3 调用确认谁在复制、谁在泄漏/proc 告诉你「有泄漏」strace 告诉你「哪一行代码干的」。对 dup 工具来说最常用的过滤参数是-e tracedup,dup2,dup3只看这一类调用避免被海量 read/write 刷屏。# 跟踪已有进程的所有分支重点看 dup 系列 strace -f -e tracedup,dup2,dup3 -p $PID -o /tmp/dup_trace.log-f让 strace 跟随子进程很多泄漏发生在 fork 出来的子进程里不加会错过最关键的一帧。-o输出到文件而不是终端因为终端回显会进一步拖慢这个本来就有性能损耗的进程。跑一段时间后直接看日志里的模式。正常情况应该是稀疏的一个长驻进程每分钟可能只有几次 dup2且参数大多是你认识的标准重定向。异常情况是高频 dup 同名 fd像下面这样单调递增dup(11) 12 dup(12) 13 dup(13) 14这种模式几乎就是泄漏现场某段逻辑每个请求都复制 fd但复制后没有 close。如果还看不出是谁把 trace 日志和代码里的 open/dup 调用点一一对应重点排查那些接受了 fd 参数却「默认不负责关闭」的封装函数。如果想观察整体而不是单点用统计模式更省事strace -f -c -e tracedup,dup2,dup3 -p $PID-c会在进程结束或你按 Ctrl-C 时输出一张汇总表包含调用次数、出错次数和总耗时。出错次数里如果 EMFILE 占比高说明进程撞到了 fd 上限下一步就回到 4.1 的/proc/PID/limits核对到底是谁把上限吃满。最后提醒一条生产环境经验strace 是有开销的特别是-f跟随多线程进程时吞吐可能明显下降。我一般的节奏是先在 /proc 里确认异常再挂 strace 采集 10 到 20 秒就摘掉。不要让 strace 常驻在一个在线的核心服务上那是把临时放大镜当眼镜戴早晚出问题。5. dup 工具实战排查5 条高频踩坑记录与对应解法5.1 三个程序行为坑dup 后 close(0)、重定向顺序、fork 继承第一坑dup 后随手 close(0)程序把日志写进了标准输入。现象是程序行为整体错乱读不到网络数据反而读到一堆日志文本。原因是 dup 永远返回「当前最小可用编号」你刚把 0 关掉下一次任何 open 或 dup 就有可能拿回 0于是本该读输入的 fd 变得指向日志文件。解决方法是不要依赖 dup 的「最小可用」特性做业务逻辑要固定目标 fd 就用 dup2 或 dup3 显式指定如果程序里确实有close(0)检查它前后 5 行代码看是不是为了重定向 stdin。第二坑cmd 21 file写成这个顺序stderr 死活不进文件。现象是 app 日志在终端刷刷出文件里却是空的。原因是 shell 从左往右执行重定向21发生时 stdout 还指向终端它复制到的是旧现场后面的 file只改了 stdout 本身。解决方法是记住这个口诀右侧代表当前状态把1写在整条重定向的最后。这个坑在写 cron 脚本时尤其多因为脚本里的命令往往被组装成字符串顺序一眼看不出来。第三坑fork 之后子进程继承父进程所有 fd导致端口关不掉。现象是父进程 close 了监听 socket但lsof -i仍然显示端口被占用重启服务一直报 Address already in use。原因是 fork 会完整复制 fd 表只要子进程手上还有一个 fd 指向这个 socket内核就认为 socket 仍在用。解决方法是 fork 后立即在子进程关闭无关 fd或者从一开始就给所有可能跨 exec 的 fd 设置 close-on-exec。但要注意close-on-exec 只对 exec 生效对 fork 的复制无效——如果你的程序 fork 后不 exec就必须显式 close。一个常见的最小防御是封装一个close_all_fds_except函数在 fork 后调用比逐个核对靠谱得多。5.2 两个现场排查坑deleted 占空间、PDF 参数断行第四坑磁盘 df 满但 du 不大日志轮转后空间不释放。现象是 df 显示/用了 98%但逐目录 du 加起来只有 60%怎么都找不到那 38% 在哪。原因是某个进程握着一个 unlink 掉的日志文件 fd 继续写文件不占目录却占 inode 空间只能等 fd 全关才释放。解决方法是先lsof L1 | grep -E deleted|myservice找到残留 fd 归属的进程再决定是重启进程还是让它 reopen 日志。dup 出来的副本也照样占空间必须每个副本都关闭才能释放这也是第三坑里「全关」比「关主 fd」重要的原因。第五坑从 PDF 使用说明里复制命令粘贴后 shell 报 command not found且报错片段和原命令对不上。现象是命令被断在连字符处行尾混进了页眉或页码甚至dupfd -s 3 -d 5被拆成两段。原因是 PDF 的排版层和文本层分离视觉上是一行复制出来可能是多行pdftotext 不带-layout时还会把表格单元按列拆开。解决方法是提取阶段就用pdftotext -layout粘贴前用cat -A检查换行和隐藏字符必要时用 3.2 节那条 sed 把断行合并回来。凡是 PDF 里抄出来的命令第一遍跑失败不要急着改代码先怀疑是不是命令本身被复制坏了。6. 进阶把 dup 工具做成 5 行巡检脚本用 ltrace 验证每一次复制到这里把 dup 工具接进日常巡检就顺理成章了。我自己的做法是在每台服务器上放一个 5 行的脚本配合 crontab 每天检查关键进程的 fd 数量超过阈值就告警#!/bin/bash # fd 巡检用法 fd_leak_check.sh pid [阈值] PID$1; LIMIT${2:-200} COUNT$(ls /proc/$PID/fd | wc -l) echo $(date %F_%T) fd_count$COUNT limit$LIMIT [ $COUNT -gt $LIMIT ] echo fd leak suspect, check strace output exit 1阈值 200 只是默认值上线前先用第 4 章的方法测一周基线把正常峰值再乘 1.5 作为告警线。巡检脚本只负责发现问题定位还是靠strace -f -e tracedup,dup2,dup3。如果想验证「到底是谁在库函数层调用了 dup」可以加一层 ltrace。它观察的是动态库函数调用和 strace 的系统调用层互为补充ltrace -e dup* -p $PID -o /tmp/dup_ltrace.log需要说明的是ltrace 对 glibc 内部调用的跟踪效果依赖发行版和 libc 版本有些环境输出非常稀疏看不到东西不代表没有调用最终以 strace 为准。我的习惯是两层都留一个现场日志事后对照代码逐帧看。最后说一个我自己的教训早年代码里为了实现「日志既上终端又进文件」把 stdout 的 fd 来回 dup2 了几次结果写日志的模块和重定向模块争同一个 fd顺序稍微变运行时就丢日志。后来我在每一处 dup 调用前都会先ls -l /proc/PID/fd/1确认现状再动手而不是凭对签名的记忆写代码。dup 系列调用的坑十有八九不在调用本身而在你动手之前对当前 fd 表的判断。记住这一点再看任何一份使用说明 PDF你都会先找它的 fd 示例而不是先背参数表。希望帮到你。本文还有配套的精品资源点击获取
返回列表