简介:CUPS 2.3.3 源代码包面向 Linux 与类 Unix 系统管理员、打印机驱动开发者及需要定制打印服务的技术人员,用于在本地编译、裁剪和调试这套通用 Unix 打印系统。其核心围绕 IPP 作业管理展开,兼容 LPD、IPP Everywhere 与 AirPrint,采用过滤器模型完成 PostScript、PDF、PCL 等格式转换,并内置 Web 管理界面与 SSL/TLS 加密,便于排查队列阻塞与权限问题。
压缩包共 1294 个文件、约 7.76MB,以 tmpl 模板、c 源码、html 文档页、h 头文件与 test 测试用例为主,另含 cxx 与 makefile 构建脚本,源码、手册、默认打印策略和测试脚本分目录存放,结构清晰。
已有 928 人学习。读者可据此完成配置、编译与安装全流程,研读作业调度和过滤器实现,编写自定义后端适配新机型,也可为打包分发与安全加固提供参考。
1. 拿到一个 2.3.3 的打印系统源码包,先别急着 configure
现场几种情况会逼你回到 cups-2.3.3-source.tar.gz 这种源码包:系统自带的 CUPS 被安全策略升级到新版,老队列全丢;离线产线设备要一个不依赖外网的最小打印服务;厂商给的过滤插件只认某个 2.3.x 的 ABI,换了版本直接链接失败。这些场景下,发行版打包的二进制都不好使,只能自己从源码编译 CUPS。这套源码包交付的是完整打印服务端:调度进程、IPP 协议栈、过滤器、后端、PPD 编译工具、Web 管理界面,一个不少。适合谁?需要在离线环境自己掌控版本与编译参数的系统集成、驱动适配、嵌入式维护岗,以及想搞清楚“一份 PDF 从 lp 提交到出纸中间过了几道手”的后端开发。
2. 拆开 tarball:哪些目录决定打印链路能不能跑通
解开压缩包之前先有一个整体认识:CUPS 不是一个单体程序,它是由一个常驻调度进程 cupsd 加一堆可执行小工具构成的体系。你后面遇到的“打不出来”,九成不是 cupsd 本身坏了,而是这条链路上某个环节没有接上。
2.1 目录分工:从任务提交到出纸的四段链路
解压后逐个目录看,职责是这样的:
| 目录 | 产物 | 在链路中的位置 |
|---|---|---|
| scheduler/ | /usr/sbin/cupsd | 调度进程:接收 IPP 请求、排队、决定过滤器链、派发后端 |
| cups/ | libcups.so、lp、lpadmin、lpstat | 客户端库与命令行,负责把任务用 IPP over HTTP 送到 cupsd |
| filter/ | /usr/lib/cups/filter/* | 格式转换:把 PDF、PS、文本、光栅转成打印机认识的数据 |
| backend/ | /usr/lib/cups/backend/* | 传输:socket、ipp、lpd、usb、serial,把数据真正写到设备 |
| ppdc/ | ppdc 编译器 | 把 PPD 源文件编译成设备可用的 PPD |
| conf/ | cupsd.conf、cups-files.conf、mime.convs 模板 | 默认配置与 MIME 转换规则 |
| data/ | 初始 mime.types、驱动相关数据 | 安装时拷到 /usr/share/cups |
| systemv/、berkeley/ | lp、lpr、lpq、lprm | 两套命令行风格,指向同一个 cupsd |
| cgi-bin/、templates/ | Web 管理界面 | 631 端口上的那个 HTML 后台 |
| tools/ | ipptool、ippfind、cups-config | 排障与探测,后期最常用的就是它们 |
数据流是这样走的:应用调用 libcups 发起 IPP 请求,cupsd 收到后读 PPD 里的cupsFilter行,拼出一条过滤器命令链,一级一级把文档转换成最终格式,最后交给 backend 的 URI 去连接设备。任何一环的产物缺失,任务就会停在某个中间状态。我一般会先在源码目录里把 filter/ 和 backend/ 两个目录看一遍,确认这个版本自带哪些转换器和传输方式,因为很多“接了打印机没反应”的问题,本质上就是这一版根本没编进对应的 backend。
2.2 先读三个文件,判断这个版本能不能接你的活
不要上来就看 C 代码,先看版本信息和默认路径,这几个文件决定了后面 configure 该给什么参数:
| 文件 | 看什么 | 为什么重要 |
|---|---|---|
| CHANGES.txt | 2.3.3 相对上一版修了哪些 bug | 确认你踩的坑是否已经在 2.3.3 里修掉 |
| config-scripts/cups-defaults.m4 | 默认用户组、默认目录、默认 TLS 实现 | configure 不传参时用的就是这里的值,和你系统的用户组很可能对不上 |
| scheduler/conf.h | 配置文件搜索顺序、状态目录宏 | 排查“改了 cupsd.conf 为什么不生效”的第一手依据 |
| INSTALL.txt | 依赖清单与编译顺序 | 缺依赖时给出的报错名和这里的描述对得上,方便定位 |
cups-defaults.m4这个文件特别值得花两分钟看,它里面定义了CUPS_DEFAULT_SYSTEM_GROUPS之类的东西。很多团队在容器里编译完,Web 管理页怎么都进不去,最后发现是默认的用户组名和自己系统里lpadmin组对不上,改的其实就是这个宏。
2.3 版本边界:2.3.3 上能做什么、不能做什么
干活前需要知道这一版的边界,不然会在错误的方向上耗掉半天。
这一版已经彻底关闭了老的 CUPS 私有浏览协议,网络发现走的是 DNS-SD,配置里再写Browsing On基本上不会有你期望的效果。驱动侧支持lpadmin -m everywhere生成无驱动队列,但完整跑通依赖系统里有一个能提供 driverless 相关工具的套件,纯净源码装上之后这个工具不在里面,需要另外准备。如果你要的是最传统的那条路——一个 PPD 加一个 socket:// 后端,这一版是完全够用的,而且这也是离线设备里最稳的做法。老式的imagetops、imagetoraster从更早的版本就迁出去了,2.3.3 的 filter 目录里没有它们,接老式光栅打印机时要确认 PPD 引用的过滤器在当前系统上是否存在,否则任务会直接以Filter failed收场。
3. 从 configure 到 make install:依赖取舍与构建参数
编译 CUPS 本身不难,难在你得清楚每个依赖被关掉之后会失去什么能力。离线设备上讲的是“最小可用”,联网环境讲的是“功能完整”,两种目标对应的配置参数完全不一样。
3.1 依赖清单与最小化取舍
| 依赖 | 用途 | 关掉的代价 | 能否关 |
|---|---|---|---|
| zlib | gziptoany 过滤器、压缩传输 | 部分压缩文档无法处理 | 不建议关 |
| gnutls 或 openssl | TLS,IPPS 与 631 加密 | ipps:// 队列不可用,Web 后台只能走明文 | 可关,用--with-tls=no |
| avahi-client + dbus | DNS-SD 服务发现 | 不能自动发现打印机,需手工建队列 | 可关,--disable-avahi |
| libusb-1.0 | usb:// 后端 | USB 直连打印机不可用 | 可关,--disable-libusb |
| libpaper | 纸张尺寸数据库 | 只能用手工指定的尺寸 | 可关 |
| libpng / libjpeg / freetype / fontconfig | pdftops、texttopdf 的渲染依赖 | 文本打印和 PS 转换会退化或失败 | 视需求 |
离线产线设备我一般会关掉 avahi 和 libusb,然后把 TLS 保留下来——因为管理后台走明文容易被扫描,而 gnutls 的开发包通常本来就装着了。反过来,如果是办公网里的通用打印服务器,avahi 要留着,不然用户体验会回到“手动填 IP 和队列名”的年代。
3.2 configure 参数怎么配
解压之后先别急着./configure,把前缀和系统组想清楚:
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-cups-user=lp \ --with-cups-group=lp \ --with-system-groups=lpadmin \ --enable-webif \ --disable-avahi \ --disable-libusb \ --with-tls=gnutls \ 2>&1 | tee configure.log逐个说清楚:--prefix=/usr意味着接管系统路径,装完之后/usr/sbin/cupsd就是你的 2.3.3,这条路径最省事,但会覆盖包管理器装的版本,装之前必须备份配置目录。--sysconfdir=/etc让配置文件落在/etc/cups,--localstatedir=/var让假脱机目录落在/var/spool/cups,这两个基本不能改,改了之后大量第三方脚本会找不到配置文件。--with-cups-user和--with-cups-group指定 cupsd 降权后运行的身份,容器镜像里如果没有 lp 用户,要提前建。--with-system-groups=lpadmin是关键参数,只有这个组里的用户才能做管理操作,写错了的表现是后台页面能打开但任何提交都返回 403。--enable-webif打开 631 的 Web 管理界面,排查阶段很值。--with-tls=gnutls指定 TLS 实现,跟系统里已有的库保持一致,避免装上之后 cupsd 在启动时报符号找不到。
如果你不想覆盖系统里的打印服务,把--prefix换成/opt/cups做旁路安装也行,但代价是运行时要显式指定CUPS_SERVERBIN、CUPS_DATADIR、CUPS_SERVER这几个环境变量,任何漏掉一个都会出现“命令行找得到队列、后端却找不到过滤器”的诡异现象。旁路安装适合做验证,正式上线我还是倾向接管系统路径。
3.3 编译与安装:make 报错先看这三个地方
make -j"$(nproc)" 2>&1 | tee build.log # 安装前务必备份原配置,make install 会覆盖 /etc/cups 下的模板文件 cp -a /etc/cups /etc/cups.bak.$(date +%F) 2>/dev/null sudo make installmake -j的并行度按核数给,但内存小于 2G 的机器建议降到-j2,编 scheduler 那几个大文件时容易 OOM。tee build.log是为了失败之后能grep,别指望在滚动的终端里靠眼睛抓错。
编译失败常见就三类:一是符号找不到,报undefined reference to一串,基本都是某个-dev或-devel包没装,按报错里的函数名反查对应的库;二是cannot find -lxxx,库装了但开发包没装,装上带头文件的那个包即可;三是头文件版本不匹配,典型报错是某个宏在头文件里没定义,这通常意味着系统里同时存在两份同名的库,pkg-config --list-all | grep gnutls看一下是不是指向了旧路径。
安装完成后必须验证一遍产物,不然你会在后面花几倍时间去猜:
cups-config --version ldd /usr/sbin/cupsd | grep -E 'cups|gnutls' ls /usr/lib/cups/filter | head ls /usr/lib/cups/backend第一条确认版本号是不是 2.3.3;第二条确认 cupsd 链接的是刚装的那份 libcups,如果指向/usr/lib/x86_64-linux-gnu/libcups.so.2且时间戳很旧,说明系统里还有一份,需要调整ldconfig顺序;后两条确认过滤器和后端确实被编出来了。我见过一次 filter 目录是空的,原因是在 configure 阶段关掉了某个可选依赖,导致整个 filter 目录跳过编译,但 configure 没有报错,只有到打印时才发现没有转换器。
3.4 装完之后的第一件事:确认服务能起来
sudo systemctl stop cups 2>/dev/null || true # 先停掉系统自带的 sudo /usr/sbin/cupsd -t # 只做配置语法检查 sudo /usr/sbin/cupsd -f & # 前台跑,日志直接打到终端cupsd -t只检查配置文件语法,不启动服务,是改完 cupsd.conf 之后的固定动作。前台跑是为了能在终端里直接看到启动日志,包括监听端口、加载的 MIME 规则、以及配置文件的实际路径。等你看到Listening on那几行,再切回后台服务模式。
4. 起一个能打的队列:cupsd.conf、PPD 与 lpadmin 三件套
配置这一章的目标很具体:从零到能用lp打出一页纸。中间要动的就三个东西——cupsd.conf 里的监听与权限、cups-files.conf 里的目录与用户组、以及 lpadmin 建出来的队列。
4.1 cupsd.conf 里真正要动的那几行
配置项一大堆,但日常真正会调的就那么几条。下面这份是排查阶段用的最小集合:
LogLevel debug MaxLogSize 0 Listen localhost:631 Listen /run/cups/cups.sock WebInterface Yes DefaultAuthType Basic <Location /> Order allow,deny Allow @LOCAL </Location> <Location /admin> Order allow,deny Allow localhost AuthType Default Require user @SYSTEM </Location>LogLevel debug会记录每个任务的过滤器命令行,是排查Filter failed的唯一可靠信息源;上线后记得降到warn,否则日志量会很吓人。Listen localhost:631只监听本机,如果要让局域网内其他机器提交任务,改成Listen 631或写具体网卡地址,同时把<Location />里的Allow @LOCAL换成明确的网段。Require user @SYSTEM里的SYSTEM指的就是 cups-files.conf 里SystemGroup定义的那个组,这条写错的表现是后台管理页能打开、提交按钮点了没反应。
配套的/etc/cups/cups-files.conf里需要确认这四行:
SystemGroup lpadmin User lp Group lp RequestRoot /var/spool/cupsSystemGroup里放的是能做管理操作的组名,多个组用空格分隔。RequestRoot是假脱机目录,这个目录的属主必须是 root、属组必须是 lp,权限 710。有一次在容器里这个目录被挂载成了普通目录,属主是 root:root,结果任务提交成功了,队列里也能看到,但永远停在 “Processing” 状态,日志里只有一句轻飘飘的Unable to create request file。
4.2 建队列:三种 URI 方案怎么选
| URI 形式 | 适用设备 | 失败时的典型表现 |
|---|---|---|
| ipp://host/ipp/print | 支持 IPP Everywhere 或 AirPrint 的现代网络打印机 | 建队列成功但状态一直是 “paused”,ipptool 探测返回 404 |
| socket://host:9100 | 只认 9100 raw 端口的老设备,必须配 PPD | 能连上但打出来是乱码或只出半页 |
| lpd://host/queue | 老式 LPD 服务端,队列名要和设备侧一致 | 提交即失败,日志里是 connection refused 或 unknown queue |
| usb://厂商/型号 | USB 直连,依赖 libusb 后端 | 报权限错误或设备忙 |
选型原则很简单:设备支持 IPP 就走 ipp:// 并且优先用-m everywhere,数据不经过额外过滤器,链路最短;设备只开 9100 就必须配 PPD,因为没有 PPD 就生成不出正确的过滤器链;USB 直连在容器里坑多,能走网络就走网络。
# 方案一:IPP Everywhere 设备,无驱动队列 lpadmin -p lab_printer -E \ -v ipp://192.168.10.20/ipp/print \ -m everywhere \ -D "Lab Printer" -L "Room 3" # 方案二:只认 9100 的老设备,指定 PPD lpadmin -p legacy_printer -E \ -v socket://192.168.10.21:9100 \ -P /root/drv/legacy.ppd \ -D "Legacy Printer" -L "Room 5" # 确认队列状态 cupsenable lab_printer cupsaccept lab_printer lpstat -v -p -l-E这一位同时做两件事:启用队列、并设置为接受任务。很多人建完队列忘了它,lpstat -p显示disabled since ...,然后开始怀疑网络。-m everywhere让 cupsd 在提交时去设备侧查询能力并生成临时 PPD,如果设备不支持这个查询会返回错误,此时退回-P指定 PPD 更稳。cupsenable和cupsaccept分别控制“是否派发”和“是否接收”,两者是独立的,排查时分开看能少走弯路。
4.3 用 ipptool 在提交任务前做一次链路体检
桌面软件报的错基本没用,真正有价值的判断来自 ipptool:
# 1. 直接问设备支持什么格式 ipptool -tv ipp://192.168.10.20/ipp/print get-printer-attributes.test # 2. 绕过 cupsd,直接向设备提交一个测试文件 ipptool -tv -d file=/root/test.pdf \ ipp://192.168.10.20/ipp/print print-job.test # 3. 经由本地 cupsd 提交,验证调度器这一侧 ipptool -tv -d file=/root/test.pdf \ ipp://localhost:631/printers/lab_printer print-job.test第一条输出的document-format-supported是选型的关键:如果里面只有application/octet-stream,说明设备只吃原始数据,你必须建 raw 队列或者确保 PPD 里的过滤器链能把它转成设备语言;如果里面有application/pdf,那 PDF 可以直通,链路会短很多。第二条和第三条的差别在于,第二条失败说明是设备或网络问题,第三条失败说明是 cupsd 的配置或过滤器问题,这个二分法在排障时能省掉大量来回猜的时间。
5. 排障实录:编译、权限、filter 链上的五个翻车点
下面五条都是实际撞过的,按“现象 → 原因 → 解决”写,遇到相似的照着对。
5.1 cupsd 起不来,日志里只有一行端口占用
现象:systemctl start cups返回失败,/var/log/cups/error_log里只有Unable to bind socket for address 127.0.0.1:631 - Address already in use。
原因:系统自带的 cupsd 还在跑,只是包管理器把它托管成了另一个服务名,你没注意。两个 cupsd 抢 631,先启动的那个赢。
解决:先ss -lntp | grep 631找到占用的进程,确认是新装的还是旧的,然后停掉旧的并禁用开机自启,再启动手装的这份。容器环境里更常见的是当初为了调试在后台&跑了一个,忘了杀。查完再启,别反复重启服务碰运气。
5.2 Web 后台能打开,但任何管理操作都返回 403
现象:浏览器访问 631 正常,点“添加打印机”转圈之后返回 403 Forbidden,error_log 里是IsAuthorized: username=""。
原因:当前登录用户不在SystemGroup定义的组里,或者DefaultAuthType设成了Negotiate而环境里没有可用的认证后端,导致用户名根本传不上去。
解决:先在 cups-files.conf 确认SystemGroup lpadmin,再id 用户名确认这个用户确实在 lpadmin 组里,最后cupsctl看一眼运行时生效的配置。改完组之后要重新登录会话才生效,只重启 cupsd 是不够的。如果确实只需要本地操作,可以把管理权限限制在 localhost,用Require user @SYSTEM配合本地 root,不走网络认证。
5.3 任务停在 Processing,最后变成 Filter failed
现象:lpstat -o能看到任务,状态从 Processing 直接跳 Aborted,页面显示 “Filter failed”,打印机的面板毫无反应。
原因:PPD 里写的cupsFilter链上某一级过滤器不存在,或者存在但对输入格式判断错了,返回非零退出码。最常见的是 PPD 从别的系统直接拷过来,引用了当前系统里没有的转换器。
解决:把LogLevel调到debug,重打一次,在 error_log 里搜Started filter,能看到 cupsd 拼出的完整命令行;把这条命令手工复制出来跑一遍,看它到底输出什么、返回码是多少。手工跑一般会立刻暴露问题——要么是找不到命令,要么是缺少某个字体文件,要么是输入格式被误判。确认是 PPD 引用错误的话,用ppdc重新编译一份,或者直接改 PPD 里那几行cupsFilter。
5.4 USB 直连打印机报权限错误
现象:队列建好,提交任务后日志是Unable to open device: Permission denied,lsusb能看到设备。
原因:/dev/bus/usb/...节点的属组不是你 cupsd 运行用户的组,或者 udev 规则没有给 lp 组读写权限。
解决:确认ls -l那个设备节点,属组通常是lp,权限 664;如果不是,加一条 udev 规则把该厂商 ID 的设备节点属组设成 lp、权限设成 664,然后重新插拔。这一步做完还是不行的话,看是不是容器里根本没有映射/dev/bus/usb,这种情况换网络方案比折腾设备映射划算得多。
5.5 装完之后整个系统的打印反而全挂了
现象:本来自带驱动好好的机器,装上 2.3.3 之后所有应用都报“找不到打印机”。
原因:make install覆盖了/usr/lib/cups和/etc/cups下的文件,但系统里原来那套 PPD 和队列配置依赖的是另一个版本的过滤器命名或数据结构,两边对不上。
解决:装之前备份整个/etc/cups是唯一可靠的后悔药。已经覆盖了的,从备份里恢复printers.conf和ppd/目录,再逐个用lpadmin -P重新挂一次 PPD,让 cupsd 重新生成内部状态。这件事说明一个原则:接管系统路径的安装必须当成一次“升级”对待,装前备份、装后逐项验证,不能当成一次普通的源码编译。
6. 用一个自定义 backend 把最后一公里握住
当设备协议不标准、厂商又不给驱动时,最后一个能自己掌控的环节就是 backend。它本质上就是一个可执行文件,cupsd 把过滤器产出的数据交给它,它负责写到设备。协议怎么写、走串口还是走私有 TCP、要不要加校验,全在你手里。
部署位置是cups-config --serverbin返回目录下的backend/,文件名就是 URI 的 scheme,权限必须 755 且属主 root——权限不对的话 cupsd 会直接忽略它,lpinfo -v里根本看不到。
#!/bin/bash # 自定义 backend:把打印数据落到本地目录,用于验证前面过滤器链的真实产出 # 部署:cp mypipe $(cups-config --serverbin)/backend/ && chmod 755 $(cups-config --serverbin)/backend/mypipe set -u # 无参数调用 = 设备发现模式,格式:class info make-model serial device-uri if [ $# -eq 0 ]; then echo 'direct mypipe "Virtual Pipe" "Virtual Pipe" "MFG:Virtual;MDL:Pipe;" "mypipe://sink"' exit 0 fi URI="$1"; JOB="$2"; USER="$3"; TITLE="$4"; COPIES="${5:-1}"; OPTS="$6"; FILE="${7:-}" # CUPS 通常直接把数据文件路径放在第 7 个参数,没有的话退回读 stdin if [ -n "$FILE" ] && [ -f "$FILE" ]; then exec 0<"$FILE" fi OUT="/var/spool/mypipe" [ -d "$OUT" ] || { echo "ERROR: $OUT 不存在" >&2; exit 1; } i=1 while [ "$i" -le "$COPIES" ]; do cat > "${OUT}/job-${JOB}-${i}.bin" || { echo "ERROR: 写入失败" >&2; exit 1; } i=$((i + 1)) done echo "INFO: job ${JOB} 已落盘,份数 ${COPIES}" >&2 exit 0参数是按位置传的:$1是设备 URI,$2是任务号,$3是提交用户,$4是任务标题,$5是份数,$6是所有选项拼成的字符串,$7是数据文件路径。退出码有明确语义:0 表示成功,1 表示失败但保留任务,2 表示认证失败——乱返回码会让 cupsd 做出完全错误的后续动作,比如把硬件故障当成认证问题反复重试。所有调试信息写 stderr,不要写 stdout,stdout 在发现模式下是有格式约定的,混进去会让lpinfo -v输出乱掉。
建队列和验证:
lpadmin -p myqueue -E -v mypipe://sink -m raw lp -d myqueue -o raw /etc/hostname ls -l /var/spool/mypipe/-m raw表示不经过任何过滤器,直接把原始数据交给 backend,这样你看到的文件就是应用提交的原始字节,方便判断问题出在转换环节还是传输环节。反过来,把-m raw换成真实 PPD 再看落盘文件,就能对比过滤器到底改了什么。
这套哑后端的价值在于把“设备不听话”这个变量彻底消掉。以前我排查一个偶发丢数据的现场,来回换驱动、换网线、换交换机折腾了两天,最后挂上这个后端才发现过滤器产出的文件本身就是截断的,跟设备一点关系没有。从那以后我每接一个说不清症状的打印现场,都先挂一个落盘后端,确认前面的链路是干净的,再去碰硬件。希望帮到你。
本文还有配套的精品资源,点击获取