简介:CUPS 2.3.3 源代码压缩包面向 Linux、BSD、macOS 等类 Unix 平台的系统管理员与打印系统开发者,用于在本地编译、定制和二次开发通用 Unix 打印系统,解决跨平台打印作业管理、驱动适配与打印服务部署问题。压缩包共 1294 个文件、约 7.76MB,以 C/C++ 源码与头文件为主体,配合构建脚本、m4 宏、po 多语言本地化资源、PPD/DRV 驱动描述、HTML 文档与 man 手册页,以及测试用例和默认策略、配置示例,基本覆盖服务端、过滤器、后端工具与文档的完整结构。已有 928 人学习。读者可据此走通 configure、make、make install 全流程,研读 IPP 协议处理、过滤器驱动模型、Web 管理接口与 SSL/TLS 安全传输的实现,并借助测试脚本和文档验证配置,适合需要深入理解打印子系统或定制专属打印方案的中高级运维与开发人员。
1. 从一台卡在 Processing 的打印机说起:CUPS 2.3.3 源码包能接手的那段链路
系统升级完的第二天早上,整层楼的网络打印机集体罢工:任务能提交,状态永远停在 Processing,纸一张不出。查了两个小时才发现打印机没坏,是中间那层打印服务起不来了。在类 Unix 系统上,应用提交的每一次打印,最后都要落进 CUPS 这条链路:它把应用程序、格式转换、设备驱动和物理端口串成一根管子,管子中间任何一节断掉,症状都是“没反应”。手里这份 CUPS 2.3.3 源码包就是这条链的完整实现,调度进程 cupsd、客户端库 libcups、十几个 filter 与 backend、外加 lp/lpadmin 这些命令行工具全在里面。它适合两类人:想把打印服务从发行版二进制包换成自己编译版本、好控制编译选项的运维,以及需要在打印链路上插自定义逻辑(计费、水印、转发)的开发者。下面按能跑、能读、能改的顺序往下拆。
2. 架构拆解:cupsd 调度进程、libcups 与 filter/backend 三段式怎么咬合
CUPS 的模块划分不是按“功能”切的,而是按“进程边界”切的。理解这一点,后面读代码和排错都会顺很多:libcups 跑在应用进程里,cupsd 是常驻的调度进程,filter 和 backend 是 cupsd 每次处理任务时 fork/exec 出来的短命进程。三段之间只靠两样东西通信——HTTP/IPP 和管道。
2.1 一次打印任务在进程间怎么走
应用侧调用cupsPrintFile(),libcups 把它翻译成一条 IPP 的 Print-Job 请求,套在 HTTP POST 里发到localhost:631/printers/<队列名>。这条请求可能走 TCP 631,也可能走本地域套接字/run/cups/cups.sock,后者是同一台机器上最快也最不容易被防火墙拦的路子。
cupsd 收到后干的第一件事是“落盘”而不是“发出去”:它在/var/spool/cups/下写两个文件,c00001是控制文件,内容是这条 IPP 请求的二进制副本(作业属性、请求者、份数全在里面),d00001-001是数据文件,也就是原始文档字节。先落盘再处理,是为了断电重启后还能把队列捡回来,这也是cupsdLoadAllJobs在启动时干的事。
落盘之后,调度器按队列绑定的 PPD 和/etc/cups/mime.convs里的转换规则,算出一条 filter 链,比如application/pdf → pdftoraster → rastertodymo,然后 fork 出一串进程,前一个的 stdout 接后一个的 stdin,最后一段接 backend,backend 才真正去开设备节点或者连 9100 端口。整条链跑完,cupsd 从 backend 的 stderr 里读STATE:、PAGE:这类状态行,更新作业状态、写page_log,一个任务才算闭环。
这条链路里最容易出问题的地方往往不是代码,而是“哪一节断了没人吭声”——cupsd 默认日志级别只记错误,filter 转换失败常常只留一行Unable to convert file,不打开调试日志根本看不出断在哪。
2.2 目录结构与模块职责对照
源码包解开之后目录不少,但真正要动的其实集中在五六个。先按职责过一遍,后面读代码时就不会在berkeley/这种历史遗留目录里绕圈。
| 目录 | 职责 | 值得先翻的文件 |
|---|---|---|
cups/ | libcups 客户端库,IPP/HTTP/PPD 的编解码都在这里 | ipp.c、http.c、request.c、dest.c |
scheduler/ | cupsd 主进程,配置解析、请求分发、队列管理 | cupsd.c、ipp.c、job.c、print.c、conf.c |
filter/ | 格式转换器,把文档转成设备能吃的光栅或页面描述 | pdftops.c、imagetoraster.c、texttopdf.c |
backend/ | 真正碰硬件的部分,一个 backend 对应一种连接方式 | socket.c、usb.c、ipp.c、lpd.c、dnssd.c |
tools/ | 调试利器,ipptool 和 ippfind 都从这里编出来 | ipptool.c、ippfind.c |
systemv/、berkeley/ | lp/lpstat/lpadmin 与 lpr/lpq 两套命令行 | lp.c、lpadmin.c、lpstat.c |
ppdc/ | PPD 编译器,把.drv源文件编成 PPD | ppdc.c、ppdhtml.c |
cgi-bin/ | 631 端口那个 Web 管理界面 | admin.c、printers.c |
scheduler/是整个包的重心,ipp.c单独一个文件就上万行,几乎所有“打印机为什么不响应我”的答案都在这。backend/里socket.c最常用,绝大多数网络打印机走的就是 AppSocket(9100 端口)这套最土但最通的协议。
2.3 libcups 的接口层:把 IPP 那套繁琐封装掉
libcups 的价值在于,你不需要自己拼 IPP 二进制。它把“连服务器、建请求、加属性、发出去、解析响应”压成了四五个函数。下面这段是查本机所有队列的完整程序,编译完直接能跑:
/* showdests.c —— 列出本机 cupsd 上的所有打印队列 */ #include <cups/cups.h> #include <stdio.h> int main(void) { cups_dest_t *dests = NULL; /* CUPS_HTTP_DEFAULT 表示复用库内部的默认连接(本地套接字优先) */ int n = cupsGetDests2(CUPS_HTTP_DEFAULT, &dests); if (n <= 0) { fprintf(stderr, "没拿到队列: %s\n", cupsLastErrorString()); return 1; } for (int i = 0; i < n; i++) { /* 每个 dest 上挂着一串 option,printer-info 就是 lpadmin -D 写的描述 */ const char *info = cupsGetOption("printer-info", dests[i].num_options, dests[i].options); printf("%-24s %-6s %s\n", dests[i].name, dests[i].is_default ? "[默认]" : "", info ? info : "-"); } cupsFreeDests(n, dests); /* 必须释放,内部是一整块连续内存 */ return 0; }编译命令直接用包里自带的cups-config,别手写-lcups,否则头文件路径和依赖库容易漏:
gcc -O2 -o showdests showdests.c $(cups-config --cflags --libs) ./showdests几个参数值得说清楚。CUPS_HTTP_DEFAULT是个宏,传它等于告诉库“你自己挑连接方式”,库会优先试本地域套接字,失败再退回localhost:631,所以这个程序在 cupsd 只监听套接字、没开 TCP 的机器上也能跑。cupsGetDests2的第一个参数也可以传自己httpConnect2()建出来的连接,做远程管理时就得这么写。cupsLastErrorString()是排错的入口,它返回的是最近一次失败的文本描述,比看返回值猜要靠谱得多。
如果要做“提交任务”而不是“查队列”,套路一样但换成cupsPrintFile();要自己发冷门 IPP 操作(比如Get-Printer-Attributes里那些 lpstat 没暴露的字段),就得下沉到ippNewRequest()+cupsDoRequest()这一层,自己往请求里ippAddString()塞属性。这层用熟了,再看 cupsd 里ipp.c的分发逻辑会非常顺,因为两边是同一套属性名。
3. 编译落地:从 configure 参数到 cupsd 前台跑起来的最小闭环
源码包能不能跑起来,八成的问题出在 configure 阶段。CUPS 的configure脚本是 autoconf 生成的,探测项很多,缺一个可选库不会立刻报错,而是悄悄关掉某个功能,等你运行到一半才发现 DNS-SD 发现不了打印机、或者 TLS 连不上。
3.1 依赖清单与 configure 参数怎么选
编译期至少要 gcc、make、autoconf 生成的 configure(包里已带,不用自己 autoreconf)。运行期和可选功能相关的库按需装:zlib 基本必装,libusb 给 USB 后端用,GnuTLS 或 OpenSSL 给 TLS 用,Avahi 给 DNS-SD 自动发现用,PAM 给系统账号认证用。头文件包(开发包)和运行库是两个包,只装运行库在 configure 阶段是探测不到的,这是新手最常踩的坑。
我的习惯是显式把所有路径和关键开关都写出来,不用默认值:
# 在源码根目录执行,所有路径显式指定,避免装到 /usr/local 下和系统包打架 ./configure \ --prefix=/usr \ --sysconfdir=/etc \ --localstatedir=/var \ --with-system-groups=lpadmin \ --with-tls=gnutls \ --enable-libusb \ --enable-debug \ --disable-static--prefix=/usr而不是默认的/usr/local,是为了让编出来的cupsd覆盖系统包的位置,lp、lpadmin这些命令才能找到同一套配置;如果装到/usr/local,你会得到两个 cupsd 抢 631 端口,症状极其玄学。--sysconfdir=/etc决定配置文件落在/etc/cups/,--localstatedir=/var决定 spool 和日志落在/var/spool/cups、/var/log/cups。--with-system-groups=lpadmin指定哪个组算“管理组”,后面 Policy 里的Require user @SYSTEM就是拿它做判断,漏了这个参数,非 root 用户调 lpadmin 会一直 403。--enable-debug打开调试符号和cupsctl --debug-logging的完整支持,生产环境可以关,但第一次编译建议留着。
如果 configure 报 TLS 库找不到而你不想折腾,换成--with-tls=openssl是最快的绕法;两个都装不上就--without-tls,代价是 IPPS 用不了,内网纯 IPP 场景够用。
3.2 编译、安装与安装后的目录布局
make这一步没什么花活,多核并行就行,但注意scheduler/ipp.c这种巨无霸文件本身就慢,-j开太大在某些小内存机器上会被 OOM killer 干掉,表现是“编译到一半进程莫名消失”,我第一次遇到时以为是代码问题,查了半天内存。
make -j"$(nproc)" # 并行编译,内存小于 2G 的机器建议 -j2 sudo make install # 安装到 configure 指定的 prefix装完之后,几个位置要记住:
/usr/sbin/cupsd:调度进程本体,前台调试就靠它。/usr/lib/cups/backend/、/usr/lib/cups/filter/:后端和过滤器都作为独立可执行文件放在这,cupsd 运行时按名字去找,新增一个 backend 就是往这个目录扔一个可执行文件,不用改 cupsd 代码。/etc/cups/cupsd.conf、/etc/cups/cups-files.conf:主配置和路径/权限配置,两个文件分开是有意为之,cups-files.conf里的SystemGroup、ServerBin这些涉及安全边界。/etc/cups/ppd/:每个队列一个 PPD,队列名即文件名,删队列要连这里一起清。/var/spool/cups/:控制文件c000NN和数据文件d000NN-NNN,作业卡住时先看这里的文件时间戳。/run/cups/cups.sock:本地域套接字,本机客户端优先走它。
3.3 首次启动与最小可用验证
别急着让它后台常驻,第一次一定要前台跑,日志直接刷屏,出问题一眼能看到。启动前先做配置语法检查,-t只解析不启动,能省不少重启时间:
sudo /usr/sbin/cupsd -t -c /etc/cups/cupsd.conf # 只校验配置语法 sudo /usr/sbin/cupsd -f -c /etc/cups/cupsd.conf # 前台运行,Ctrl+C 退出前台跑着的时候另开一个终端做验证,顺序是从“看得见设备”到“打得出一页纸”:
lpinfo -v # 期望输出形如: # network ipp # network socket # direct usb://Vendor/Model?serial=xxxx # network dnssd://Lab%20Printer._ipp._tcp.local/cups lpadmin -p lab-printer -E \ -v ipp://192.0.2.25/ipp/print \ -m everywhere \ -D "三楼实验室打印机" -L "三楼东侧" lpstat -l -p lab-printer lp -d lab-printer /etc/hostname lpstat -olpinfo -v列出的是当前能用的 backend 和它们发现的设备,如果这一行都出不来,说明ServerBin路径不对或者 backend 没有可执行权限,跟打印机本身没关系。-v后面的 URI 里ipp://走的是 IPP backend,socket://走 9100,usb://走 libusb,选错协议的症状是任务瞬间完成但不出纸。-m everywhere是让 cupsd 自己去问设备要能力集、生成 PPD,前提是设备支持 IPP Everywhere;老设备得换成-m <厂商PPD名>或者-P /path/to/xxx.ppd。-E表示立即启用队列,不加这一项队列是 disabled 状态,任务会一直 pending,这个坑后面还会提。
lp -d lab-printer /etc/hostname是最小验证用例:纯文本、体积极小、几乎不可能被 filter 卡住。如果这一页能出来,链路就是通的,后面再上 PDF、图片才有意义。
4. 核心代码走读:一个 IPP 请求在 scheduler 里的分发、排队与出纸
读懂scheduler/三个文件,CUPS 的行为就基本没有黑匣子了:ipp.c管“请求怎么被理解”,job.c管“作业怎么排队和流转”,print.c管“作业怎么被真的送出去”。
4.1 scheduler/ipp.c 的操作分发
cupsd 是单线程事件循环(scheduler/select.c里那套 select/poll 驱动),每 accept 一个连接就建一个cupsd_client_t,读满一个 HTTP 请求后调cupsdProcessIPPRequest()。这个函数的骨架非常直白:校验版本、定位操作对象、按操作号分发、统一回包。把日志和错误分支剥掉,主干就是这个形状:
/* scheduler/ipp.c —— 主干逻辑,省略了鉴权、日志与错误分支 */ void cupsdProcessIPPRequest(cupsd_client_t *con) { ipp_attribute_t *attr; cupsd_printer_t *printer = NULL; /* 1. IPP 版本低于 1.1 的直接拒绝,老客户端会在这里被挡掉 */ if (ippGetVersion(con->request) < 0x0101) { con->response = ippNew(); ippSetStatusCode(con->response, IPP_STATUS_ERROR_VERSION_NOT_SUPPORTED); goto send; } /* 2. 用 printer-uri 定位队列;找不到就返回 0x0406(NotFound) */ if ((attr = ippFindAttribute(con->request, "printer-uri", IPP_TAG_URI)) != NULL) printer = cupsdFindDest(ippGetString(attr, 0, NULL)); con->response = ippNew(); ippSetVersion(con->response, ippGetVersion(con->request) >> 8, ippGetVersion(con->request) & 255); /* 3. 按操作号分发,2.3 这个 switch 覆盖了几十个 IPP 操作 */ switch (ippGetOperation(con->request)) { case IPP_OP_GET_PRINTER_ATTRIBUTES : get_printer_attrs(con, printer); /* 组装 printer-attributes-tag */ break; case IPP_OP_VALIDATE_JOB : validate_job(con, printer); /* 只校验参数,不入队 */ break; case IPP_OP_PRINT_JOB : print_job(con, printer); /* 收数据、写 spool、建作业 */ break; case IPP_OP_CREATE_JOB : create_job(con, printer); /* 建空作业,等后续 Send-Document */ break; default : ippSetStatusCode(con->response, IPP_STATUS_ERROR_OPERATION_NOT_SUPPORTED); break; } send: cupsdSendIPPHeader(con); /* 回 HTTP 头 + IPP 状态行,最后 flush */ }几个关键点。con->request和con->response都是完整的 IPP 消息对象,不是流式写入,所以 cupsd 可以边处理边往 response 里加属性组,最后一次性发出去。cupsdFindDest()是 2.x 里统一找队列/类的入口,它查的是内存里的队列表,这张表在启动时由conf.c按/etc/cups/printers.conf建好,运行时增删队列只改内存加回写,不用重启。get_printer_attrs()这类函数负责往 response 里按printer-attributes-tag塞几十个属性,从printer-state到marker-levels(墨量)都在里面,lpstat -l显示的就是这一坨。
要加自定义 IPP 操作,改这里是最直接的:在switch里加一个case,写一个处理函数把结果塞进 response。但注意客户端也得用同样的操作号,标准 ipptool 发不了私有操作,得自己用 libcups 拼。
4.2 job.c 的作业状态机与 spool 文件
IPP 的 job-state 是一组固定枚举:3 pending、4 pending-held、5 processing、6 processing-stopped、7 canceled、8 aborted、9 completed。cupsd 里所有作业流转都是在这几个值之间跳,跳错了就是典型症状“任务显示完成但没出纸”或者“一直在处理中”。
job.c里的cupsdProcessJob()是推进状态的主力。它按顺序做几件事:先判断队列是不是可接受(printer-state是 stopped 就直接把作业挂成 pending-held)、再算 filter 链、再调cupsdStartProcess()把进程链拉起来、最后盯子进程退出码决定是 completed 还是 aborted。作业对象本身反序列化自那个c000NN控制文件,所以 cupsd 重启后能原样恢复,这一点比很多“内存里排队”的打印服务稳。
有个细节值得记住:d000NN-NNN数据文件在作业完成后不会立刻删,默认保留一段时间(由PreserveJobHistory、PreserveJobFiles控制)。所以磁盘被 spool 撑爆时,先看这两个指令,而不是急着重启服务。
4.3 filter/backend 的管道与状态回传
filter 和 backend 都是普通可执行文件,cupsd 用 exec 拉起它们,靠环境变量传上下文,靠管道传数据。约定如下:
- 环境变量:
CUPS_SERVER(回连地址)、DEVICE_URI(设备 URI)、PRINTER(队列名)、PPD(PPD 路径)、CONTENT_TYPE(本段输入 MIME)、FINAL_CONTENT_TYPE(最终要产出的 MIME)。 - 输入:前一级 filter 的 stdout,或者 backend 直接读原始文件。
- 输出:filter 写 stdout;backend 不写 stdout 传数据,而是把
STATE: +connecting-to-device、PAGE: 1 1、ATTR: marker-levels=80这类状态行写到 stderr,cupsd 解析后更新作业和打印机属性。
这套约定的好处是“加一个协议支持”= “扔一个可执行文件进backend/目录”,不用动 cupsd 一行代码;代价是链路调试全靠日志,因为进程之间没有共享内存,断了就是断了。cupsctl --debug-logging打开之后,/var/log/cups/error_log会出现每一级 filter 的启动命令和退出码,这是排查白纸问题最有效的一手资料。
5. 排查清单:源码编译到出纸失败最容易翻车的五个位置
下面五条都是我自己或者周围人真实踩过的,按“现象 → 原因 → 解决”写,遇到对应症状可以直接对照。
5.1 现象:configure 过了,make 却挂在某个头文件
现象:./configure跑完没报错,make编译到cups/http.c或scheduler/时报fatal error: gnutls/gnutls.h: No such file or directory。
原因:configure 探测的是链接期能不能找到库,但头文件路径缺失时它有时会退化成“关闭该功能”,而部分源文件又硬引用了对应头文件;另一种情况是之前跑过一次 configure 留下了config.cache,换了参数但缓存没清,探测结果还是旧的。
解决:先看config.log,搜gnutls那一段,里面会明确写“checking for ... no”。装开发包后rm -f config.cache && ./configure ...重新来一遍。实在不想装就把--with-tls改成另一个实现,或者直接--without-tls,别硬扛。
5.2 现象:cupsd 起来了,631 端口连不上
现象:cupsd -f前台跑着没报错,浏览器打不开http://localhost:631,lpstat报连接被拒。
原因:三种可能——cupsd.conf里只有Listen localhost,远程访问当然不通;系统自带的 cups.service 还在后台占着 631,你新编的进程其实没绑上端口;或者cups-files.conf里的ServerBin、StateDir指到了别的路径,进程起来但用的是另一套配置。
解决:ss -lntp | grep 631看是谁占着。如果是旧服务,systemctl stop掉再前台跑自己编的。要远程访问就把Listen改成Listen 0.0.0.0:631或Listen 631,同时确认防火墙放行。改完记得用cupsd -t校验,配置
本文还有配套的精品资源,点击获取