简介:这份 cups-2.3.3-source.tar.gz 是 CUPS(通用 Unix 打印系统)2.3.3 的完整源码包,面向需要在 Linux、BSD、macOS 等类 Unix 环境中编译、定制打印服务的开发者与运维人员,可用于搭建网络打印、研究 IPP 协议及裁剪驱动与后端。压缩包共 1294 个文件,约 7.76MB,以 512 个 tmpl 模板、201 个 c 源文件、81 个 html 页面、73 个 h 头文件为主,并含 makefile、m4、po、test、drv、ppd 等构建、国际化、测试与驱动相关文件。源码涵盖 configure 构建脚本、src 服务器与过滤器实现、docs 文档手册、include 头文件、policies 默认打印策略、etc 配置示例以及 tests 测试用例,可据此梳理打印作业从提交、格式转换到设备后端的处理链路,理解 IPP、LPD 与 AirPrint 等协议支持方式。已有 928 人学习,适合用来掌握 CUPS 的过滤器驱动模型、Web 管理界面和本地编译安装方法,并作为定制打印方案或二次开发的参考底包;目录组织完整,便于按模块检索与二次开发,对熟悉类 Unix 打印链路和运维排错较有帮助。
1. 三台机器抢一台打印机,问题往往不在打印机上
工位上三台机器抢一台打印机,是那种最难查的故障:A 机器能打,B 机器点了没反应,C 机器打出来是乱码。驱动版本各不相同,队列名也各不相同,出事的时候没人说得清任务停在哪一环。cups-2.3.3-source.tar.gz 这类 CUPS 源码包就是为这种场面准备的:它给的不是一个装完就能用的二进制,而是一整套打印服务器的源码,编译进什么、监听哪个端口、队列怎么起、日志开到多细,全都由你决定。
包管理器里的 cups 往往跟着发行版走,版本、补丁、默认配置都是别人替你选的。当你要在老旧系统上补一个新特性,要把打印能力塞进一个裁剪过的镜像,或者要读 filter 源码搞清楚某个 PPD 为什么把 A4 打成了 Letter,源码包就是唯一的路。这篇按我实际拆包的顺序写:先编译、再配置、再拆任务链路、最后把踩过的坑一条条摆出来。适合手上正有一台不听话的打印服务器、又不想再靠重启碰运气的人。
2. 编译前的环境盘点:configure 参数与第一次 make 怎么不翻车
CUPS 的构建系统是 autoconf 那一套,configure 会把所有依赖探一遍,缺什么当场报出来。麻烦的地方在于它探测的依赖很多是「有就用、没有就降级」,不会报错,等到运行期你才发现某个 filter 根本不存在。所以编译前先把依赖分成刚需和可选两类看清楚。
2.1 依赖清单:哪些缺了直接编不过,哪些缺了只是少功能
下表是我在一台干净的 Debian 系机器上装 CUPS 2.3.3 时的最小心集,包名按 Debian/Ubuntu 写,RHEL 系把-dev换成-devel即可。
| 组件 | 级别 | 缺失后的表现 | 常见包名 |
|---|---|---|---|
| zlib | 刚需 | configure 直接失败,压缩打印数据无处解压 | zlib1g-dev |
| libjpeg | 刚需 | JPEG 图像转换链路断裂,图片任务报 filter failed | libjpeg-dev |
| libpng | 刚需 | PNG 相关 filter 编不出来 | libpng-dev |
| libtiff | 可选 | TIFF 类任务无法处理 | libtiff-dev |
| libusb-1.0 | 可选 | usb://后端不可用,本地 USB 打印机扫不到 | libusb-1.0-0-dev |
| gnutls 或 openssl | 强烈建议 | 无法启用 TLS,加密队列和远程管理都受限 | libgnutls28-dev |
| avahi-client | 可选 | 没有 DNS-SD 广播,客户端自动发现失效 | libavahi-client-dev |
| dbus | 可选 | 与桌面环境的会话集成能力下降 | libdbus-1-dev |
| pam | 可选 | 无法用系统账户做 Web 管理认证 | libpam0g-dev |
装依赖这一步最容易犯的错,是在一台只跑打印服务的机器上把整个桌面环境的开发包全装上,结果 configure 探到一堆用不上的东西,编出来的 cupsd 拖着一串运行时依赖。我的习惯是先只装 zlib、libjpeg、libpng、gnutls 四个,编一遍,看 configure 末尾的 summary 缺什么再补什么,这样产出的二进制最干净。
2.2 configure 参数逐项拆解:每个开关都在改什么
解包之后先别急着./configure,把--help输出重定向到文件里过一遍,不同分支的选项名会有出入。
tar -xzf cups-2.3.3-source.tar.gz cd cups-2.3.3 ./configure \ --prefix=/usr \ --sysconfdir=/etc \ --localstatedir=/var \ --with-docdir=/usr/share/doc/cups \ --with-tls=gnutls \ --with-dnssd=avahi \ --enable-debug \ 2>&1 | tee /tmp/cups-configure.log几个参数值得单独说明。--prefix=/usr决定二进制和库的落点,CUPS 会把cupsd、lpadmin、lpstat放进$prefix/sbin和$prefix/bin,把 backend 和 filter 放进$prefix/lib/cups/。如果你后面要跟发行版自带的 CUPS 共存,这里就必须换一个 prefix,否则两套程序会互相覆盖。
--sysconfdir=/etc让配置文件落在/etc/cups。这个值不要随便改,很多第三方脚本硬编码了这个路径,改了之后cupsctl和部分 PPD 工具会找不到配置。--localstatedir=/var决定状态目录和 spool 目录的根,任务队列文件最终落在/var/spool/cups,日志落在/var/log/cups。
--with-tls=gnutls指定 TLS 后端。CUPS 2.3 这条线支持 gnutls 和 openssl 两种,选哪个看你系统里哪个库更顺手,功能上没有明显差别。如果 configure 报「unrecognized option」,说明这个分支没集成该项,直接删掉这行重跑即可,不要硬凑。
--with-dnssd=avahi打开 DNS-SD 广播,局域网里的客户端才能自动扫到这台服务器。只在纯静态 IP、客户端全部手工填 URI 的环境里,这一项可以关掉,省一个运行时依赖。
--enable-debug会保留调试符号并打开额外的断言,排障阶段很有用,但二进制会大不少。生产环境编第二遍时我会把它去掉。
2.3 首次构建与安装:make 之后先别急着启动
make -j"$(nproc)" 2>&1 | tee /tmp/cups-make.log make install # 校验配置文件语法,这一步能在启动前拦掉大部分低级错误 /usr/sbin/cupsd -t # 确认版本与安装路径 /usr/bin/cups-config --version /usr/bin/cups-config --serverbinmake -j的并行数不要无脑拉到核数上限,CUPS 里有几个生成 PPD 的大文件,内存吃紧的机器上并行过高会把 OOM killer 招来,编译中途被 kill 却只报一行莫名其妙的错误。4 到 8 之间比较稳。
cupsd -t是启动前必跑的一步,它只做配置解析,不监听端口。配置写错时它会把行号和错误原因打出来,比启动后去 error_log 里捞要快得多。cups-config则用来确认这套源码装出来的路径,后面写启动脚本、配 systemd 单元时都要拿它输出的--serverbin和--serverroot对齐。
安装完成后不要立刻systemctl start cups。发行版自带的 cups 服务很可能已经在跑,源码版再起一个会撞端口。先ss -lntp | grep 631看一眼,有监听就先停掉旧服务,或者干脆给新实例换一个端口做并行验证。
3. 配置落地:cupsd.conf 的访问控制与队列建立的两条路径
CUPS 的配置被拆成了几个文件:cupsd.conf管服务行为和访问控制,cups-files.conf管路径、用户、组和日志位置,printers.conf是队列的运行时状态,不要手改。很多人改完配置重启没反应,八成是改错了文件。
3.1 cupsd.conf:Listen、Location 与访问控制的三层结构
# /etc/cups/cupsd.conf LogLevel warn MaxLogSize 2m Listen 0.0.0.0:631 Listen /run/cups/cups.sock WebInterface Yes DefaultEncryption IfRequested DefaultAuthType Basic MaxJobs 500 PreserveJobHistory Yes PreserveJobFiles No <Location /> Order allow,deny Allow @LOCAL </Location> <Location /admin> AuthType Default Require user @SYSTEM Order allow,deny Allow @LOCAL </Location> <Location /printers> Order allow,deny Allow @LOCAL </Location>Listen 0.0.0.0:631让服务监听所有网卡。只给内网用时,直接写内网地址更安全。Listen /run/cups/cups.sock是本地域套接字,本机客户端走它比走 TCP 快,也不受防火墙影响。
<Location />里的Allow @LOCAL表示只允许同网段访问。这个宏在 2.3 这条线上是按网卡地址和掩码自动算的,多网卡机器上偶尔会出现「明明同网段却 403」的情况,这时把它换成具体的Allow 192.0.2.0/24更可靠。<Location /admin>里的Require user @SYSTEM依赖cups-files.conf中的SystemGroup,两处必须对得上,否则 Web 管理页会一直弹密码框。
PreserveJobFiles No是省磁盘的关键。它关掉之后,任务打完就把 spool 里的数据文件删掉,只留历史记录。开着的话,一个卡住的大任务能把/var/spool/cups撑满,然后整个服务对所有任务返回失败,这个现象特别容易误判成打印机坏了。
路径和权限相关的部分在另一个文件里:
# /etc/cups/cups-files.conf User lp Group lp SystemGroup lpadmin ServerRoot /etc/cups ServerBin /usr/lib/cups StateDir /var/run/cups RequestRoot /var/spool/cups TempDir /var/spool/cups/tmp CacheDir /var/cache/cups AccessLog /var/log/cups/access_log ErrorLog /var/log/cups/error_log PageLog /var/log/cups/page_logSystemGroup lpadmin这一行决定了谁能进管理页、谁能执行lpadmin。把运维账号加进这个组之后必须重新登录才生效,newgrp也行,但在脚本里跑的时候经常忘,于是出现「手动能建队列、定时脚本建不了」的玄学现象。
3.2 队列怎么建:PPD 模式和 IPP Everywhere 模式
CUPS 2.3 建队列有两条路。第一条是传统的 PPD 驱动模式,队列绑定一个描述文件,所有页面设置、纸张来源、双面能力都写在 PPD 里。第二条是 IPP Everywhere,不装驱动,让 CUPS 直接向打印机要能力集,自动生成临时 PPD。
先看设备发现,这一步决定你用哪种 URI:
# 列出当前能发现的所有设备 URI lpinfo -v # 列出可用的驱动/PPD lpinfo -m | grep -i genericlpinfo -v会输出network ipp、network socket、direct usb这几类。如果打印机支持 IPP,优先选ipp://开头的 URI,能力协商和状态回传都走标准协议,比裸 socket 好排错。
IPP Everywhere 建队列:
lpadmin -p Floor3-Mono -E \ -v ipp://printer-floor3.example.internal/ipp/print \ -m everywhere \ -D "3F 黑白激光" \ -L "3F-East" \ -o printer-is-shared=true-p是队列名,建议只用字母数字和连字符,带空格或中文的名字在某些老客户端上会被截断。-E表示启用队列并接受任务,少了它会建出一个暂停状态的队列,任务全堆在那儿不动。-m everywhere触发能力协商,CUPS 会向打印机发Get-Printer-Attributes,拿回来的属性生成临时 PPD 放在/etc/cups/ppd/下。-D是描述,-L是位置,这两个字段在几十个队列的时候是救命的,客户端下拉框里能直接看出是哪台。
PPD 模式建队列:
lpadmin -p Meeting-Room -E \ -v socket://192.0.2.10:9100 \ -m generic.ppd \ -o PageSize=A4 \ -o Duplex=DuplexNoTumblesocket://是最原始的方式,把打印数据裸推到 9100 端口。它没有任何状态回传,打印机缺纸、卡纸、脱机,CUPS 一概不知道,任务照样显示完成。只有在打印机不支持 IPP、又不想配 LPD 的时候才用它。-o后面的选项名必须和 PPD 里定义的一致,拼错不会报错,只会静默失效,所以建完队列一定要lpstat -l回看实际生效的选项。
队列多的时候手工敲不现实,用脚本生成:
#!/usr/bin/env python3 # 从 CSV 批量建队列,格式:name,uri,driver,location import csv, subprocess, sys def build(row): name, uri, driver, loc = row["name"], row["uri"], row["driver"], row["location"] cmd = ["lpadmin", "-p", name, "-E", "-v", uri, "-m", driver, "-L", loc] # -m everywhere 时不传 PPD 文件名,其余情况按驱动名走 print(" ".join(cmd)) subprocess.run(cmd, check=True) if __name__ == "__main__": with open(sys.argv[1], newline="", encoding="utf-8") as f: for row in csv.DictReader(f): build(row)这个脚本只做一件事:把建队列的动作变成幂等的可重放操作。注意lpadmin -p对已存在的队列是修改而不是新建,所以同一份 CSV 重跑不会报错,这个特性在批量修 URI 的时候很好用。subprocess.run(check=True)让任意一条失败就中止,避免批量建到一半留下一堆半成品队列。
3.3 验证队列:把「建完了」和「真能用」分开
lpstat -t lpstat -l -p Floor3-Mono lpstat -W not-completed -o # 打一张测试页 lp -d Floor3-Mono /etc/hostnamelpstat -t一次打出调度器状态、所有队列、所有未完成任务,是最快的体检命令。lpstat -l -p会额外显示队列绑定的 PPD 路径和生效选项,用来确认-m everywhere是不是真的生成了临时 PPD。第三行只列未完成任务,卡住的任务在这里一眼能看到。
用/etc/hostname当测试文件是个小技巧:纯文本、几百字节、任何驱动都能处理。如果它打得出来而 PDF 打不出来,问题一定在 filter 链而不是网络。
4. 打印链路拆解:任务从 lp 到出纸究竟过了哪些进程
排障卡壳的根本原因,是大多数人不知道 CUPS 内部有几个环节。把它拆成六段之后,每一段都有对应的日志和验证命令,就不需要靠重启来碰运气了。
4.1 一次打印任务的六个环节
第一段是应用侧,程序把文档交给 libcups 或直接发 IPP 请求。第二段是 cupsd 的调度器,它收到请求后分配一个任务号,把控制文件写成c000xx、数据文件写成d000xx,落在/var/spool/cups。第三段是 filter 链,按 MIME 类型转换,比如 PDF 转成打印机认识的栅格格式。第四段是 backend,负责把数据推到设备,ipp、socket、usb、lpd都是这一层的实现。第五段是设备回传状态。第六段是任务收尾,写 page_log、清理 spool。
对应到目录:
| 环节 | 位置 | 出问题时的典型现象 |
|---|---|---|
| 调度器 | cupsd 进程,日志在 error_log | 任务停在「已提交」不动 |
| 任务 spool | /var/spool/cups | 磁盘满,所有任务失败 |
| filter | /usr/lib/cups/filter | 状态显示 Filter failed |
| backend | /usr/lib/cups/backend | 状态显示 Unable to write print data |
| PPD | /etc/cups/ppd | 页面尺寸、双面设置不对 |
| 设备状态 | ipp 回传或没有回传 | 显示完成但没出纸 |
filter 和 backend 的报错是最容易被误读的。Filter failed不代表打印机有问题,它只说明某个转换程序退出码非零,原因可能是缺库、可能是权限、也可能是输入文件本身损坏。判断方法是在 error_log 里找到具体的命令行,手动以 lp 用户重跑一遍。
4.2 日志分级与定位命令
CUPS 的日志有三个:access_log记 HTTP 请求,error_log记服务与任务,page_log记每次实际打印的页数。排任务问题只看 error_log 就够了。
# 临时把日志级别压到 debug cupsctl --debug-logging # 盯住某个任务的全部日志 tail -F /var/log/cups/error_log | grep -E '\[Job [0-9]+\]' # 看某个时间段的 backend 调用 grep -E 'backend|filter' /var/log/cups/error_log | tail -50cupsctl --debug-logging等价于把LogLevel改成 debug,重启后生效,排完记得改回 warn,不然日志一天能涨几百兆。[Job N]这个前缀是 CUPS 给任务日志打的标记,用它 grep 能把一个任务从接收到结束的所有行串起来,比按时间翻日志高效得多。
事情出在打印机侧、CUPS 自己又说不清的时候,直接对打印机发 IPP 请求:
ipptool -tv ipp://printer-floor3.example.internal/ipp/print \ /usr/share/cups/ipptool/get-printer-attributes.testipptool是 CUPS 自带的 IPP 客户端,源码包的examples/目录里有几份.test模板,安装后一般在/usr/share/cups/ipptool/下。-t打印传输细节,-v打印属性值。这条命令能拿到打印机的真实状态、支持的操作、耗材余量,绕过 CUPS 直接判断是设备问题还是服务问题。
4.3 客户端对接选型:IPP、LPD、Socket 该怎么选
| 协议 | URI 形式 | 状态回传 | 适用场景 |
|---|---|---|---|
| IPP | ipp://host/ipp/print | 完整 | 新设备首选,能力协商、耗材状态都有 |
| IPPS | ipps://host:631/ipp/print | 完整 | 跨网段、需要加密 |
| Socket | socket://host:9100 | 无 | 老设备兜底,只保证数据能推过去 |
| LPD | lpd://host/queue | 极弱 | 只有 LPD 的老打印机 |
| USB | usb://Vendor/Model | 有限 | 本机直连 |
Windows 客户端通过共享连接时走的是 SMB 或 IPP,这时候要在cupsd.conf里确认WebInterface和共享开关都开着,并且SystemGroup里那个组对队列有访问权限。跨网段共享最常见的翻车是只改了Listen没改<Location />的 Allow 规则,服务在监听但所有远程请求被 403 挡掉。
5. 避坑与排查:五条踩坑记录和对应的定位动作
这一节是按「现象 → 原因 → 解决」整理的。每一条都是我或者同事实际撞过的,写下来是因为它们都不报明显错误,只表现为「打不出来」。
5.1 五条踩坑记录
第一条:源码版启动失败,日志里只有一行 Address already in use。
现象是cupsd前台启动立刻退出,error_log 里翻来覆去就那一行。原因是发行版自带的 cups 服务在 631 端口上跑着,两套程序抢同一个端口。解决方法是先ss -lntp | grep 631找到占用进程,systemctl stop cups停掉旧服务,再决定是让 systemd 指向新二进制还是保留两套并行。并行的话记得给源码版换一个Port,并且客户端 URI 里的端口也要跟着改。
第二条:Web 管理页输对密码也进不去,一直弹框。
原因是SystemGroup指定的组里没有当前用户,或者改了组之后没有重新登录,进程还带着旧的组列表。另一种情况是Require user @SYSTEM写在了<Location /admin>里,但cups-files.conf中SystemGroup被注释掉了,这时只有 root 能进。解决方法是id确认当前用户的组,getent group lpadmin确认组成员,改完之后重新登录再试。用 curl 验证更干脆:curl -s -u user:pass http://127.0.0.1:631/admin/,返回 200 就说明认证链路通了,问题在浏览器缓存。
第三条:任务状态停在「Processing」,最后变成 Filter failed。
原因是 filter 链里某个转换程序退出码非零。常见触发点是缺 libjpeg 或 libpng 的运行时库、PPD 与驱动不匹配、或者输入 PDF 本身损坏。解决方法是去 error_log 里搜这个任务号,找到完整的 filter 命令行,然后以 lp 用户手动跑一遍:
sudo -u lp /usr/lib/cups/filter/pdftoraster 1 user job 1 "PageSize=A4" /tmp/in.pdf > /tmp/out.raster echo $?退出码能直接告诉你卡在哪。如果手动跑能过、走 CUPS 就失败,那大概率是 AppArmor 或 SELinux 拦了 filter 对临时目录的访问,看dmesg里有没有 denied 记录。
第四条:小文件能打,大文件打到一半停住,打印机面板没有任何报错。
这种是典型的传输层半开。socket://后端没有超时协商,防火墙或交换机在中途把空闲连接掐了,CUPS 那边还以为在正常写。表现就是打了十几页然后永远卡住,取消也取消不掉。解决方法是换ipp://,它有请求响应机制,断了能感知到;实在只能用 socket 的,把防火墙的连接跟踪超时调长,或者在cupsd.conf里加Timeout控制单次读写等待。这个坑我用血泪经验换来的结论是:能不选 socket 就别选。
第五条:改完 cupsd.conf 重启,行为一点变化都没有。
原因是 CUPS 的配置分在两个文件里,改了cupsd.conf却把日志路径写在了cups-files.conf,或者反过来。还有一种更隐蔽的情况:systemd 单元的ExecStart带了-c指向另一个配置文件,你怎么改/etc/cups/cupsd.conf都不生效。解决方法是ps -ef | grep cupsd看进程实际带的参数,cupsd -t -c /path/to/conf单独校验你正在改的那个文件,两边对上了再重启。
5.2 日志、备份与回滚三件套
排障阶段最值钱的是「能回到上一个已知可用状态」。CUPS 的配置改动很容易连锁,改一个Allow规则可能让整个网段的客户端全部掉线。我的做法是每次动配置之前先打包:
TS=$(date +%Y%m%d-%H%M%S) tar -czf /root/cups-backup-$TS.tar.gz \ /etc/cups/cupsd.conf \ /etc/cups/cups-files.conf \ /etc/cups/ppd/ 2>/dev/null # 回滚 tar -xzf /root/cups-backup-$TS.tar.gz -C / cupsd -t && systemctl restart cups注意备份里不要包含printers.conf和/var/spool/cups,前者是运行时状态,直接还原容易和当前进程状态打架;后者可能是几个 G 的残留任务。真正需要版本化的只有两个 conf 和ppd/目录。
日志侧的习惯是给MaxLogSize设一个上限,并且把error_log单独挂一个小分区或者用 logrotate 管起来。CUPS 在 debug 级别下的日志增长速度非常夸张,一个卡住的任务循环重试几小时就能把根分区写满,然后整个系统一起遭殃。
6. 进阶:把一整套 CUPS 配置变成可复制模板
配好一台服务器只是开始,真正麻烦的是第二台、第三台。我的做法是把配置拆成「要版本化的」和「运行时生成的」两类,前者进仓库,后者永远由脚本重建。
需要进仓库的只有四样:cupsd.conf、cups-files.conf、一个描述队列的 CSV、以及自定义的 PPD 文件。printers.conf不进仓库,因为它记录的是内部状态号,还原到另一台机器上毫无意义;队列永远用第 3 章那个脚本从 CSV 重建。
模板化之后,部署流程收敛成三步:
# 1. 铺配置 install -m 644 repo/cups/cupsd.conf /etc/cups/cupsd.conf install -m 644 repo/cups/cups-files.conf /etc/cups/cups-files.conf # 2. 校验 + 重建队列 cupsd -t && systemctl restart cups python3 repo/cups/build_queues.py repo/cups/queues.csv # 3. 与基线比对,确认没有漂移 lpstat -l -p > /tmp/queues-now.txt diff /tmp/queues-now.txt repo/cups/queues-baseline.txt && echo "无漂移"第三步是我最看重的一步。lpstat -l -p的输出里包含队列名、URI、PPD 路径、生效选项和状态,把它当基线存下来之后,任何一次手工改动都会在 diff 里暴露。团队里有人临时给某个队列加了个PageSize=Letter又忘了改回去,第二天 diff 就会指出来。这个习惯比任何文档都管用。
还有一个容易被忽略的细节:-m everywhere生成的临时 PPD 存在/etc/cups/ppd/下,文件名是队列名加.ppd。它会在打印机能力变化或队列重建时被覆盖,所以不要把自定义选项直接改在这个文件里,改了下一次重建就没了。要固化选项,用lpadmin -p name -o 选项=值写进队列配置,再让它进基线。
一个具体技巧:把队列的 CSV 和基线文件放在同一个提交里,每个 commit 都能回答「这次动了哪些队列」。新机器上克隆仓库、跑三个步骤、diff 通过,就算部署完成,全程不用打开浏览器。升级 CUPS 版本时也一样,新二进制起来之后先跑 diff,确认队列能力和选项没被新版改掉,再切流量。
从那以后我每次改打印服务器,都强制走一遍cupsd -t、备份、重建、diff 这四步,哪怕只是改一行 Allow 规则。看起来啰嗦,但它把「改完不知道哪里又坏了」这种最耗时间的排查,变成了一个几秒钟就能给出答案的 diff。希望帮到你。
本文还有配套的精品资源,点击获取