UNIX环境高级编程 学习笔记 第二十一章 与网络打印机通信
现在我们开发一个能与网络打印机通信的程序。这些打印机通过以太网与多个计算机互联,并且通常既支持纯文本文件也支持PostScript(一种页面描述语言,用于描述图形和页面布局,以便在打印机或其他输出设备上进行呈现)文件。应用一般使用网络打印协议(Internet Printing Protocol,IPP)与打印机通信,但一些应用程序也支持其他通信协议。
两个程序:
1.打印假脱机守护进程:将作业发送到打印机。
2.命令行程序:将打印作业提交到假脱机守护进程。
网络打印协议为建立基于网络的打印系统制定了通信规则,通过将一个IPP服务器嵌入到带网卡的打印机中,打印机就能对许多计算机系统的请求加以服务,这些计算机系统实际上并不需要在同一个物理网络中,因为IPP是建立在标准的因特网协议上的,所以任何一台能与打印机建立TCP/IP连接的计算机都能向打印机提交打印作业。
候选标准5100.12-2011明确规定了符合不同版本IPP标准的实现必须支持的所有功能,这些功能被分成不同的符合性级别,每个级别对应一个不同的协议版本。为了兼容性,较高级别的符合性要求实现满足较低版本标准所定义的大部分要求。本章示例中使用的是IPP 1.1版本。
IPP建立在超文本传输协议(Hypertext Transfer Protocol,HTTP)之上,HTTP又建立在TCP/IP之上。IPP报文结构如下:
IPP是请求响应协议,客户端发送请求到服务器,服务器用响应报文回答这个请求。IPP首部包含一个域来标识操作,操作可以是提交打印作业、取消打印作业、获取作业属性、获取打印机属性、暂停和重启打印机、挂起一个作业、释放一个挂起的作业(将之前被暂停的作业重新恢复到活动状态)。
IPP首部结构:
前两个字节表示IPP版本号,对于1.1版本协议,每个字节的值是1。
对于一个请求报文,接下来两个字节包含一个值指示请求操作的类型;对于一个响应报文,这两个字节包含一个状态码。
接下来4字节的请求ID字段包含一个整数以标识请求,使得请求和响应相匹配。
接着是可选的属性字段,用属性结束标志终止。紧接着是与请求相关联的数据。
在首部,整数以有符号二进制补码以及大端字节序(即网络字节序)方式存储。属性字段按组来存储,每个组都以标识该组的一个字节开始。在每个组中,属性通常表示为:1字节的标志,后跟2字节的属性名长度,接着是属性名,然后是2字节的属性值长度,最后是属性值本身。属性值字段可以编码成字符串、二进制整数或更复杂的结构,如日期/时间戳
。
下图显示了attributes-charset属性是如何编码成utf-8类型的值的:
根据所请求的操作,一些属性需要在请求报文中提供,一些是可选的,下图是对于打印作业请求,哪些属性是必需的,哪些属性是可选的:
IPP首部包含了文本和二进制混合数据。属性名存储为文本,数据大小存储为二进制整数,这使得构建和分析首部的过程变得复杂,因为需要考虑诸如网络字节序、主机处理器是否在任意字节边界编址对齐等问题。一个较好的可选方案是将首部设计成仅包含文本,这样以稍微膨胀一些协议报文为代价简化处理过程。
HTTP V1.1由RFC 2616说明。HTTP也是请求响应协议,请求报文包含一个开始行,然后是首部行,接着是空白行,然后是一个可选的实体主体(如IPP首部和数据)。
HTTP首部是ASCII码,每行以回车\r和换行符\n结束。
HTTP开始行包含一个method来告诉客户端要做些什么(如POST、GET等)、一个统一资源定位符(Uniform Resource Locator,URL)来描述服务器和协议、一个字符串来表示HTTP版本。IPP所用的方法仅为POST,用于将数据发送到服务器。
HTTP首部行指定属性,如实体主体的格式和长度。一个首部行包含一个属性名,后紧随一个冒号,接着是可选的空格符,然后是属性值,最后以回车和换行符结束。如为了指定实体主体包含IPP报文,应包含如下首部行:
Content-Type: application/ipp\r\n
下面是Xerox Phaser 8560打印机的打印请求的HTTP首部:
Content-Length行指明了HTTP报文中数据的字节大小,它不包括HTTP首部的大小,但包括IPP首部大小。Host行指明了报文要发送到的服务器主机名和端口号。
每行后面的^M是换行符前面的回车符,而换行符不会以可打印字符的形式出现。首部的最后一行是空的,只有回车和换行符。
HTTP响应报文的起始行包含了版本字符串,紧接着是一个数字状态码和状态信息,最后以一个回车和换行结束。HTTP响应报文的剩余部分和请求报文的格式一样。首部后是一个空白行和可选的实体主体。
打印机发送给我们如下的报文作为打印请求的回应:
对于打印假脱机守护进程,我们只关心HTTP响应报文的第一行,它说明了请求成功或用数字错误码以及一个短字符串表示请求失败。剩下的报文包含一些附加信息,可用来控制在客户端和服务器间的可能得节点上进行缓存以及表明运行在服务器上的软件版本号。
本章要开发的程序是一个基本的打印假脱机守护进程。一个简单的用户命令发送一个文件到打印假脱机守护进程,假脱机守护进程将其保存到磁盘,并将打印请求送入队列,最终将文件发送到打印机。
所有UNIX系统至少提供一个打印假脱机系统。FreeBSD安装的是BSD的打印假脱机系统LPD;Linux和Mac OS X安装CUPS,即Common UNIX Printing System;Solaris提供标准的System V打印假脱机守护程序(lpsched)。我们不关心假脱机系统本身,而关心如何与网络打印机通信,我们需要开发一个假脱机系统,解决多用户访问单一资源(打印机)问题。
我们使用命令行程序print读取一个文件,将其送到打印假脱机守护进程,print命令有一个选项来强制将文件按照文本来处理(默认是PostScript文件)。
在我们的打印假脱机守护进程printd中,使用多线程将任务分解给守护进程来完成:
1.一个线程在套接字上监听从运行print命令的客户端发来的新打印请求。
2.对于每个客户端产生一个独立的线程,将要打印的文件复制到假脱机区域。
3.一个线程与打印机通信,一次发送一个队列中的作业。
4.一个线程处理信号。
上图中的打印配置文件为/etc/printer.conf
,其中标识了运行打印假脱机守护程序的服务器主机名(printserver)和网络打印机的主机名(printer):
我们假设上图中的两个主机名已经在/etc/hosts中列出或者已经通过正在使用的任意名字服务进行了注册,这样我们就可以将这些名字转换成网络地址。
可以在运行打印假脱机守护进程的同一台机器上运行print命令,也可以在同一个网络中的任意机器上运行它。运行print命令的机器上的/etc/printer.conf中只需要有printserver字段,printer字段只有打印假脱机守护进程需要用到。
拥有超级用户特权的程序可能让计算机系统受到攻击。这些程序通常并不比其他程序更脆弱,但被攻破时将导致攻击者能完全访问你的计算机。
本章中的打印假脱机守护进程拥有超级用户特权,因为它需要将一个特权TCP端口号绑定一个套接字,为了使守护进程能更好的地抵御攻击,我们可以:
1.按最少特权原则设计守护进程,我们获得一个绑定到特权端口的套接字后,可以将守护进程的用户ID和组ID改为非root。所有用于存储队列中打印作业的文件和目录的拥有者应该是非特权用户。如果被攻击,这样攻击者只能通过守护进程访问到打印子系统,虽然这仍是个隐患,但比起攻击者可以完全访问系统,其危害性已经大大降低了。
2.审计守护进程源代码中所有已知的潜在脆弱性漏洞,如缓冲区溢出。
3.对不期望或可疑的行为做日志,可引起管理员注意并进一步调查。
本章的打印假脱机守护进程和打印文件的命令行进程的源代码包含5个文件:
1.ipp.h
包含IPP定义的头文件。
2.print.h
包含公用的常数、数据结构定义、实用工具例程的声明的头文件。
3.util.c
用于两个程序的实用工具例程。
4.print.c
用于打印文件的命令行程序C代码。
5.printd.c
用于打印假脱机守护进程的C代码。
ipp.h:
#ifndef _IPP_H // 用于防止同一文件被包含两次
#define _IPP_H
/*
* Defines parts of the IPP protocol between the scheduler
* and the printer. Based on RFC2911 and RFC2910.
*/
/*
* Status code classes.
*/
#define STATCLASS_OK(x) ((x) >= 0x0000 && (x) <= 0x00ff)
#define STATCLASS_INFO(x) ((x) >= 0x0100 && (x) <= 0x01ff)
#define STATCLASS_REDIR(x) ((x) >= 0x0300 && (x) <= 0x03ff)
#define STATCLASS_CLIERR(x) ((x) >= 0x0400 && (x) <= 0x04ff)
#define STATCLASS_SERVER(x) ((x) >= 0x0500 && (x) <= 0x05ff)
/*
* Status codes.
* 定义基于RFC 2911的状态码,但本程序不使用
*/
#define STAT_OK 0x0000 /* success */
#define STAT_OK_ATTRIGN 0x0001 /* OK; some attrs ignored */
#define STAT_OK_ATTRCON 0x0002 /* OK; some attrs conflicted */
#define STAT_CLI_BADREQ 0x0400 /* invalid client request */
#define STAT_CLI_FORBID 0x0401 /* request is forbidden */
#define STAT_CLI_NOAUTH 0x0402 /* authentication required */
#define STAT_CLI_NOPERM 0x0403 /* client not authorized */
#define STAT_CLI_NOTPOS 0x0404 /* request not possible */
#define STAT_CLI_TIMOUT 0x0405 /* client too slow */
#define STAT_CLI_NOTFND 0x0406 /* no object found for URI */
#define STAT_CLI_OBJGONE 0x0407 /* object no longer available */
#define STAT_CLI_TOOBIG 0x0408 /* requested entity too big */
#define STAT_CLI_TOOLNG 0x0409 /* attrbute value too large */
#define STAT_CLI_BADFMT 0x040a /* unsupported doc format */
#define STAT_CLI_NOTSUP 0x040b /* attribute not supported */
#define STAT_CLI_NOSCHM 0x040c /* URI scheme not supported */
#define STAT_CLI_NOCHAR 0x040d /* charset not supported */
#define STAT_CLI_ATTRCON 0x040e /* attributes conflicted */
#define STAT_CLI_NOCOMP 0x040f /* compression not supported */
#define STAT_CLI_COMPERR 0x0410 /* data can't be decompressed */
#define STAT_CLI_FMTERR 0x0411 /* document format error */
#define STAT_CLI_ACCERR 0x0412 /* error accessing data */
#define STAT_SRV_INTERN 0x0500 /* unexpected internal error */
#define STAT_SRV_NOTSUP 0x0501 /* operation not supported */
#define STAT_SRV_UNAVAIL 0x0502 /* service unavailable */
#define STAT_SRV_BADVER 0x0503 /* version not supported */
#define STAT_SRV_DEVERR 0x0504 /* device error */
#define STAT_SRV_TMPERR 0x0505 /* temporaty error */
#define STAT_SRV_REJECT 0x0506 /* server not accepting jobs */
#define STAT_SRV_TOOBUSY 0x0507 /* server too busy */
#define STAT_SRV_CANCEL 0x0508 /* job has been canceled */
#define STAT_SRV_NOMULTI 0x0509 /* multi-doc jobs unsupported */
/*
* Operation IDs.
* IPP中定义的每个操作都有一个ID
*/
#define OP_PRINT_JOB 0x02
#define OP_PRINT_URI 0x03
#define OP_VALIDATE_JOB 0x04
#define OP_CREATE_JOB 0x05
#define OP_SEND_DOC 0x06
#define OP_SEND_URI 0x07
#define OP_CANCEL_JOB 0x08
#define OP_GET_JOB_ATTR 0x09
#define OP_GET_JOBS 0x0a
#define OP_GET_PRINTER_ATTR 0x0b
#define OP_HOLD_JOB 0x0c
#define OP_RELEASE_JOB 0x0d
#define OP_RESTART_JOB 0x0e
#define OP_PAUSE_PRINTER 0x10
#define OP_RESUME_PRINTER 0x11
#define OP_PURGE_JOBS 0x12
/*
* Attribute Tags.
*/
#define TAG_OPERATION_ATTR 0X01 /* operation attributes tag */
#define TAG_JOB_ATTR 0X02 /* job attributes tag */
#define TAG_END_OF_ATTR 0X03 /* end of attributes tag */
#define TAG_PRINTER_ATTR 0X04 /* printer attributes tag */
#define TAG_UNSUPP_ATTR 0X05 /* unsupported attributes tag */
/*
* Value Tags.
*/
#define TAG_UNSUPPORTED 0x10 /* unsupported value */
#define TAG_UNKNOWN 0x12 /* unknown value */
#define TAG_NONE 0x13 /* no value */
#define TAG_INTEGER 0x21 /* integer */
#define TAG_BOOLEAN 0x22 /* boolean */
#define TAG_ENUM 0x23 /* enumeration */
#define TAG_OCTSTR 0x30 /* octetString */
#define TAG_DATETIME 0x31 /* dateTime */
#define TAG_RESOLUTION 0x32 /* resolution */
#define TAG_INTRANGE 0x33 /* rangeOfInteger */
#define TAG_TEXTWLANG 0x35 /* textWithLanguage */
#define TAG_NAMEWLANG 0x36 /* nameWithLanguage */
#define TAG_TEXTWOLANG 0x41 /* textWithoutLanguage */
#define TAG_NAMEWOLANG 0x42 /* nameWithoutLanguage */
#define TAG_KEYWORD 0x44 /* keyword */
#define TAG_URI 0x45 /* URI */
#define TAG_URISCHEME 0x46 /* uriScheme */
#define TAG_CHARSET 0x47 /* charset */
#define TAG_NATULANG 0x48 /* naturalLanguage */
#define TAG_MIMETYPE 0x49 /* mimeMediaType */
// IPP首部结构
struct ipp_hdr {
int8_t major_version; /* always 1 */
int8_t minor_version; /* always 1 */
union {
int16_t op; /* operation ID */
int16_t st; /* status */
} u;
int32_t request_id; /* request ID */
char attr_group[1]; /* start of optional attributes group */
/* optional data follows */
};
#define operation u.op
#define status u.st
#endif /* _IPP_H */
print.h:
#ifndef _PRINT_H
#define _PRINT_H
/*
* Print server header file.
* 包含所有需要的头文件,应用程序只需包含print.h
* 而不需要跟踪所有的头文件依赖关系
*/
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
/*
* 定义实现所需的文件和目录
* 以下目录必须由打印守护进程的用户所有,且必须由管理员创建
* 如果没有这些目录,守护进程不会创建这些目录,因为守护进程需要root权限创建/var/spool中的目录
* 而我们设计的初衷是当以root权限运行是,尽量让守护进程少做一些事情,以减少产生安全漏洞的可能
*/
// 打印守护进程和网络打印机的主机名的配置文件
#define CONFIG_FILE "/etc/printer.conf"
// 打印相关信息所在的根目录
#define SPOOLDIR "/var/spool/printer"
// /var/spoll/printer/jobno目录中包含下一个作业编号
#define JOBFILE "jobno"
// /var/spoll/printer/data目录中包含需要打印的文件的副本
#define DATADIR "data"
// /var/spoll/printer/reqs目录中包含每个请求的控制信息
#define REQDIR "reqs"
// 定义运行打印守护进程的用户名
// Linux和Solaris中,这个用户是lp(line printer,行式打印机的缩写)
// Mac OS X中,这个用户是_lp
// FreeBSD没有为打印守护进程定义单独的用户,所以我们使用为系统守护进程保留的用户
#if defined(BSD)
#define LPNAME "daemon"
#elif defined(MACOS)
#define LPNAME "_lp"
#else
#define LPNAME "lp"
#endif
#define FILENMSZ 64
// 创建要打印的文件副本时使用的权限,我们不希望普通用户能读取他人等待打印的文件
#define FILEPERM (S_IRUSR|S_IWUSR)
#define USERNM_MAX 64
#define JOBNM_MAX 256
#define MSGLEN_MAX 512
// 当sysconf函数不能确定系统对最长主机名的限制时使用
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif
// IPP使用端口631
#define IPP_PORT 631
// 传递给listen函数的第二个参数,是未完成连接的队列和已完成连接的队列大小之和的建议值
#define QLEN 10
#define IBUFSZ 512 /* IPP header buffer size */
#define HBUFSZ 512 /* HTTP header buffer size */
#define IOBUFSZ 8192 /* data buffer size */
// 一些平台没有定义错误码ETIME,当读超时时,返回ETIME错误码
// 我们不希望在从套接字读的时候服务器无限期地阻塞
#ifndef ETIME
#define ETIME ETIMEDOUT
#endif
// 定义公共例程
extern int getaddrlist(const char *, const char *, struct addrinfo **);
extern char *get_printserver(void);
extern struct addrinfo *get_printaddr(void);
extern ssize_t tread(int, void *, size_t, unsigned int);
extern ssize_t treadn(int, void *, size_t, unsigned int);
extern int connect_retry(int, int, int, const struct sockaddr *, socklen_t);
extern int initserver(int, const struct sockaddr *, socklen_t, int);
/*
* Structure describing a print request.
* print程序发送printreq结构到打印假脱机守护进程
*/
struct printreq {
uint32_t size; /* size in bytes */
uint32_t flags; /* see below */
char usernm[USERNM_MAX]; /* user's name */
char jobnm[JOBNM_MAX]; /* job's name */
};
/*
* Request flags.
* 我们为每个标志定义一个掩码,而非对每个标志定义一个独立的字段
* 虽然现在只定义了一个标志,将来还可以增加更多性质来扩展这个协议
* 如我们可以增加一个标志位来请求双面打印,不需要改变结构大小就可以有31个额外的标志位空间
* 改变结构大小意味着可能会引入客户端和服务器的兼容性问题,除非对两边同时更新
* 还有一个解决方法是增加一个报文版本号,以允许不同版本的结构有所改变
*/
#define PR_TEXT 0x01 /* treat file as plain text,而不是PostScript */
/*
* The response from the spooling daemon to the print command.
* 打印假脱机守护进程用printresp结构回应print程序发送的printreq结构
* 这两个结构对整数显式地定义长度,可以在客户端和服务器的整数长度不同时避免结构元素错位
*/
struct printresp {
uint32_t retcode; /* 0=success, !0=error code */
uint32_t jobid; /* job ID */
char msg[MSGLEN_MAX]; /* error message */
};
#endif /* _PRINT_H */
util.c:
#include "print.h"
#include <string.h>
#include <unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/select.h>
// 打印机配置文件的行的最大长度
#define MAXCFGLINE 512
// 配置文件中关键字的最大长度
#define MAXKWLEN 16
// 传给sscanf函数的格式化字符串的最大长度
#define MAXFMTLEN 16
/*
* Get the address list for the given host and service and
* return through ailistpp. Returns 0 on success or an error
* code on failure. Note that we do not set errno if we
* encounter an error.
*
* LOCKING: none.
* 此函数是getaddrinfo函数的封装
*/
int getaddrlist(const char *host, const char *service, struct addrinfo **ailistpp) {
int err;
struct addrinfo hint;
hint.ai_flags = AI_CANONNAME;
hint.ai_family = AF_INET;
hint.ai_socktype = SOCK_STREAM;
hint.ai_protocol = 0;
hint.ai_addrlen = 0;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
// host参数可以是域名,也可以是点分十进制的IP地址
// service参数可以是ftp、ipp等服务名,也可以是端口号,如"8080"
err = getaddrinfo(host, service, &hint, ailistpp);
return err;
}
/*
* Given a keyword, scan the configuration file for a match
* and return the string value corresponding to the keyword.
*
* LOCKINT: none.
*/
static char *scan_configfile(char *keyword) {
int n, match;
FILE *fp;
char keybuf[MAXKWLEN], pattern[MAXFMTLEN];
char line[MAXCFGLINE];
// valbuf是static的,会被后面的调用覆盖
// 因此此函数不能用于多线程程序,除非能避免同时有多个线程调用它
static char valbuf[MAXCFGLINE];
if ((fp = fopen(CONFIG_FILE, "r")) == NULL) {
printf("can't open %s\n", CONFIG_FILE);
exit(1);
}
// %是转义符,因此pattern内容为%16s %16s
sprintf(pattern, "%%%ds %%%ds", MAXKWLEN - 1, MAXCFGLINE - 1);
match = 0;
while (fgets(line, MAXCFGLINE, fp) != NULL) {
// 搜索配置文件中一行有没有符合pattern的字符串(本例中是空格分开的两个字符串)
// 如果有,将空格前的部分存入keybuf,空格后的部分存入valbuf
n = sscanf(line, pattern, keybuf, valbuf);
if (n == 2 && strcmp(keyword, keybuf) == 0) {
match = 1;
break;
}
}
fclose(fp);
if (match != 0) {
return valbuf;
} else {
return NULL;
}
}
/*
* Return the host name running the print server or NULL on error.
* 获取配置文件中运行假脱机守护进程的主机名,由客户端命令调用
*
* LOCKING: none.
*/
char *get_printserver(void) {
return scan_configfile("printserver");
}
/*
* Return the address of the network printer or NULL on error.
* 获取配置中打印机的网络地址,由守护进程调用
*
* LOCKING: none.
*/
struct addrinfo *get_printaddr(void) {
int err;
char *p;
struct addrinfo *ailist;
if ((p = scan_configfile("printer")) != NULL) {
if ((err = getaddrlist(p, "ipp", &ailist)) != 0) {
printf("no address infomation for %s\n", p);
return NULL;
}
return ailist;
}
printf("no printer address specified\n");
return NULL;
}
/*
* "Timed" read - timeout specifies the # of seconds to wait before
* giving up (5th argument to select controls how long to wait for
* data to be readable). Returns # of bytes read or -1 on error.
* tread函数用于打印假脱机守护进程,它可防止拒绝服务攻击
* 一个恶意用户可能重复尝试连接到守护进程而不发送数据,只是为了阻止其他用户提交打印作业
* 通过设置一个合理的读超时时间,可防止这种情况发生
* 选择的超时时间过小可能导致程序过早夭折
* 过大时守护进程会消耗很多资源处理挂起请求,不能防止拒绝服务攻击
*
* LOCKINT: none.
*/
ssize_t tread(int fd, void *buf, size_t nbytes, unsigned int timeout) {
int nfds;
fd_set readfds;
struct timeval tv;
tv.tv_sec = timeout;
tv.tv_usec = 0;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
nfds = select(fd + 1, &readfds, NULL, NULL, &tv);
// 如果在等待时间内没有接收数据,返回-1并将errno设为ETIME
if (nfds <= 0) {
if (nfds == 0) {
errno = ETIME;
}
return -1;
}
// 如果时间期限内有数据可用,则最多返回nbytes字节的数据
return read(fd, buf, nbytes);
}
/*
* "Timed" read - timeout specifies the number of seconds to wait
* per read call before giving up, but read exactly nbytes bytes.
* Returns number of bytes read or -1 on error.
* 为了正好读取nbytes字节,需要多次调用read函数
* 由于将单个超时值应用到多个read调用比较困难,如用闹钟,多线程应用中信号会变乱
* 如用select函数的返回更新timeval结构,许多平台不支持它
* 因此这里折中定义了一个超时值应用到单独的read函数
* 最坏情况下,一次仅接收一个字节,总最大等待时间为nbytes*timeout
*
* LOCKING: none.
*/
ssize_t treadn(int fd, void *buf, size_t nbytes, unsigned int timeout) {
size_t nleft;
ssize_t nread;
nleft = nbytes;
while (nleft > 0) {
if ((nread = tread(fd, buf, nleft, timeout)) < 0) {
// 如果一个字节都还没读到,即第一次读就失败了
if (nleft == nbytes) {
return -1; /* error, return -1 */
} else {
break; /* error, return amount read so far */
}
} else if (nread == 0) {
break; /* EOF */
}
nleft -= nread;
buf += nread;
}
return nbytes - nleft; /* return >= 0 */
}
用于提交打印作业的客户命令程序print.c:
/*
* The client command for printing documents. Opens the file
* and sends it to the printer spooling daemon. Usage:
* print [-t] filename
*/
#include "print.h"
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
/*
* Needed for logging function.
* 我们的日志函数需要定义log_to_stderr变量,0表示将日志消息发送到syslog而非标准错误
* 如果非0,则错误消息被送到标准错误而非日志文件中
*/
int log_to_stderr = -1;
void submit_file(int, int, const char *, size_t, int);
int main(int argc, char *argv[]) {
int fd, sockfd, err, text, c;
struct stat sbuf;
char *host;
struct addrinfo *ailist, *aip;
err = 0;
text = 0;
// -t选项强行使文件按文本格式打印
while ((c = getopt(argc, argv, "t")) != -1) {
switch (c) {
case 't':
text = 1;
break;
case '?':
err = 1;
break;
}
}
// 当getopt函数处理完命令选项时,会将变量optind设为argv中第一个非选项参数的下标
// 即要打印的文件
if (err || (optind != argc - 1)) {
printf("usage: print [-t] filename\n");
exit(1);
}
if ((fd = open(argv[optind], O_RDONLY)) < 0) {
printf("print: can't open %s\n", argv[optind]);
exit(1);
}
if (fstat(fd, &sbuf) < 0) {
printf("print: can't stat %s\n", argv[optind]);
exit(1);
}
if (!S_ISREG(sbuf.st_mode)) {
printf("print: %s must be a regular file\n", argv[optind]);
exit(1);
}
/*
* Get the hostname of the host acting as the print server.
*/
if ((host = get_printserver()) == NULL) {
printf("print: no print server defined\n");
exit(1);
}
// 获取print服务的地址,需要保证/etc/services中有print服务对应的条目
// 为守护进程选择一个端口时,最好选择特权端口
// 以防止恶意用户程序假装成一个打印假脱机守护进程,而实际是要偷取打印文件的副本
// 因此端口号应小于1024,且守护进程运行时需要具有超级用户特权以便能绑定一个保留端口
if ((err = getaddrlist(host, "print", &ailist)) != 0) {
printf("print: get addrinfo error: %s\n", gai_strerror(err));
exit(1);
}
// 根据getaddrinfo函数返回的地址列表来尝试连接到守护进程
// 使用能够连接的第一个地址发送文件到守护进程
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sfd = connect_retry(AF_INET, SOCK_STREAM, 0, aip->ai_addr, aip->ai_addrlen)) < 0) {
err = errno;
} else {
submit_file(fd, sfd, argv[optind], sbuf.st_size, text);
exit(0);
}
}
printf("print: can't contact %s", host);
exit(1);
}
/*
* Send a file to the printer daemon.
*/
void submit_file(int fd, int sockfd, const char *fname, size_t nbytes, int text) {
int nr, nw, len;
struct passwd *pwd;
struct printreq req;
struct printresp res;
char buf[IOBUFSZ];
/*
* First build the header.
*/
// 查找有效用户id在系统口令文件中的用户信息
if ((pwd = getpwuid(geteuid())) == NULL) {
strcpy(req.usernm, "unknown");
} else {
strncpy(req.usernm, pwd->pw_name, USERNM_MAX - 1);
// strncpy函数不会在缓冲区最后存储null字节
req.usernm[USERNM_MAX - 1] = '\0';
}
// 将要打印的文件长度转换为网络字节序,这样不管运行print程序的主机的机器字节序是什么
// 打印假脱机守护进程都能处理
req.size = htonl(nbytes);
if (text) {
req.flags = htonl(PR_TEXT);
} else {
req.flags = 0;
}
if ((len = strlen(fname)) >= JOBNM_MAX) {
/*
* Trancate the filename (+-5 accounts for the leading
* four characters and the terminating null).
*/
strcpy(req.jobnm, "... ");
strncat(req.jobnm, &fname[len - JOBNM_MAX + 5], JOBNM_MAX - 5);
} else {
strcpy(req.jobnm, fname);
}
/*
* Send the header to the server.
*/
nw = writen(sockfd, &req, sizeof(struct printreq));
if (nw != sizeof(struct printreq)) {
if (nw < 0) {
printf("Can't write to print server\n");
exit(1);
} else {
printf("short write (%d/%d) to print server\n", nw, sizeof(struct printreq));
}
}
/*
* Now send the file.
*/
while ((nr = read(fd, buf, IOBUFSZ)) != 0) {
nw = writen(sockfd, buf, nr);
if (nw != nr) {
if (nw < 0) {
printf("can't write to print server\n");
exit(1);
} else {
printf("short write (%d/%d) to print sever\n", nw, nr);
}
}
}
/*
* Read the response.
*/
if ((nr = readn(sockfd, &res, sizeof(struct printresp))) != sizeof(struct printresp)) {
printf("can't read response from server\n");
exit(1);
}
// 返回码非0表示请求失败
if (res.retcode != 0) {
printf("rejected: %s\n", res.msg);
exit(1);
} else {
// 请求成功,打印作业ID,后续可用此作业ID引用该请求
printf("job ID %ld\n", (long)ntohl(res.jobid));
}
// 到此请求成功,但一个成功的守护进程响应仅代表守护进程成功将其加入到打印作业队列
}
打印假脱机守护进程:
/*
* Print server daemon.
*/
#include <fcntl.h>
#include <dirent.h>
#include <stddef.h>
#include <sys/stat.h>
#include <ctype.h>
#include <pwd.h>
#include <pthread.h>
#include <signal.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/uio.h>
#include "print.h"
#include "ipp.h"
/*
* These are for the HTTP response from the printer.
*/
#define HTTP_INFO(x) ((x) >= 100 && (x) <= 199)
#define HTTP_SUCCESS(x) ((x) >= 200 && (x) <= 299)
/*
* Describes a print job.
*/
struct job {
struct job *next; /* next in list */
struct job *prev; /* previous in list */
long jobid; /* job ID */
struct printreq req; /* copy of print request */
};
/*
* Describes a thread processing a client request.
*/
struct worker_thread {
struct worker_thread *next; /* next in list */
struct worker_thread *prev; /* previous in list */
pthread_t tid; /* thread ID */
int sockfd; /* socket */
};
/*
* Needed for logging.
* 我们的日志函数需要定义log_to_stderr变量
* 0表示将日志消息发送到系统日志而不是标准错误
*/
int log_to_stderr = 0;
/*
* Printer-related stuff.
*/
struct addrinfo *printer; // 保存打印机的网络地址
char *printer_name; // 保存打印机的主机名
// 用来互斥访问reread变量,该变量用来表示守护进程需要再次读取配置文件
// 读取配置文件的原因可能是管理员改变了打印机的网络地址
pthread_mutex_t configlock = PTHREAD_MUTEX_INITIALIZER;
int reread;
/*
* Thread-realated stuff.
*/
struct worker_thread *workers; // 双向链表头
// 保护workers双向链表
pthread_mutex_t workerlock = PTHREAD_MUTEX_INITIALIZER;
sigset_t mask; // 线程的信号掩码
/*
* Job-realted stuff.
*/
// 挂起作业的双向链表,由于需要将作业加入表尾,因此需要一个指针来记住表尾
struct job *jobhead, *jobtail;
int jobfd; // 作业文件的文件描述符
int32_t nextjob; // 接收的下一个打印作业的ID
// 保护作业双向链表
pthread_mutex_t joblock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t jobwait = PTHREAD_COND_INITIALIZER;
/*
* Function prototypes.
* 提前声明好所有函数原型可以在文件中放置函数时不用担心函数的顺序
*/
void init_request(void);
void init_printer(void);
void update_jobno(void);
int32_t get_newjobno(void);
void add_job(struct printreq *, int32_t);
void replace_job(struct job *);
void remove_job(struct job *);
void build_qonstart(void);
void *client_thread(void *);
void *printer_thread(void *);
void *signal_thread(void *);
ssize_t readmore(int, char **, int, int *);
int printer_status(int, struct job *);
void add_worker(pthread_t, int);
void kill_workers(void);
void client_cleanup(void *);
/*
* Main print server thread. Accepts connect requests from
* clients and spawns additional threads to service requests.
* 打印假脱机守护进程的main函数执行两个任务:
* 1.初始化守护进程
* 2.处理来自客户端的连接请求
*
* LOCKING: none.
*/
int main(int argc, char *argv[]) {
pthread_t tid;
struct addrinfo *ailist, *aip;
int sockfd, err, i, n, maxfd;
char *host;
fd_set rendezvous, rset;
struct sigaction sa;
struct passwd *pwdp;
if (argc != 1) {
printf("usage: printd\n");
exit(1);
}
// 成为守护进程后,不能再在标准错误上打印消息,而是记入日志
daemonize("printd");
// 忽略SIGPIPE,因为要写套接字文件描述符,不想让写错误触发SIGPIPE
// SIGPIPE的默认动作是杀死进程
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &sa, NULL) < 0) {
printf("sigaction failed\n");
exit(1);
}
// 设置线程信号掩码,创建的所有线程均继承这个信号掩码
sigemptyset(&mask);
// SIGHUP用于通知守护进程再次读取配置文件
sigaddset(&mask, SIGHUP);
// SIGTERM告诉守护进程执行清理工作,并优雅地退出
sigaddset(&mask, SIGTERM);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0) {
printf("pthread_sigmask failed\n");
exit(1);
}
n = sysconf(_SC_HOST_NAME_MAX);
if (n < 0) { /* best guess */
n = HOST_NAME_MAX;
}
if ((host = malloc(n)) == NULL) {
printf("malloc error\n");
exit(1);
}
// 获取当前主机的主机名,存入host变量
if (gethostname(host, n) < 0) {
printf("gethostname error\n");
exit(1);
}
if ((err = getaddrlist(host, "print", &ailist)) != 0) {
printf("getaddrinfo error: %s\n", gai_strerror(err));
exit(1);
}
// 清零文件描述符集,该变量将与select函数一起用来等待客户端连接请求
FD_ZERO(&rendezvous);
maxfd = -1; // 确保分配的第一个文件描述符会大于maxfd
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
// 守护进程需要超级用户特权绑定一个套接字到保留端口
// 对于每一个提供服务的网络地址,调用initserver(在16章中定义)分配和初始化一个监听套接字
if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr, aip->ai_addrlen, QLEN)) > 0) {
FD_SET(sockfd, &rendezvous);
if (sockfd > maxfd) {
maxfd = sockfd;
}
}
}
if (maxfd == -1) {
log_quit("service not enabled");
}
pwdp = getpwnam(LPNAME);
if (pwdp == NULL) {
log_sys("can't find user %s", LPNAME);
}
// uid为0的用户是超级用户,为了避免在守护进程中将系统暴露给任何可能的攻击,选择不提供服务
if (pwdp->pw_uid == 0) {
log_quit("user %s is privileged", LPNAME);
}
// 降低程序特权,最小特权原则
if (setgid(pwdp->pw_gid) < 0 || setuid(pwdp->pw_uid) < 0) {
log_sys("can't change IDs to user %s", LPNAME);
}
init_request(); // 初始化作业请求,确保只有一个守护进程副本正在运行
init_printer(); // 初始化打印机信息
// 创建一个与打印机通信的线程,打印机只与该线程通信
err = pthread_create(&tid, NULL, printer_thread, NULL);
if (err == 0) {
err = pthread_create(&tid, NULL, signal_thread, NULL);
}
if (err != 0) {
log_exit(err, "can't create thread");
}
// 在/var/spool/printer目录中搜索被挂起的作业
// 对于每个找到的作业,将建立一个结构,让打印机线程将该作业的文件送到打印机
build_qonstart();
log_msg("daemon initialized");
for ( ; ;) {
// 此处我们必须复制rendezvous结构,因为事件发生后,select函数会修改传入的fd_set结构
rset = rendezvous;
if (select(maxfd + 1, &rset, NULL, NULL, NULL) < 0) {
log_sys("select failed");
}
// 一个可读的文件描述符意味着一个连接请求需要处理
for (i = 0; i <= maxfd; ++i) {
if (FD_ISSET(i, &rset)) {
/*
* Accept the connection and handle the request.
*/
if ((sockfd = accept(i, NULL, NULL)) < 0) {
// log_ret函数会根据全局变量log_to_stderr的值
// 决定将其记入日志还是打印到标准错误
log_ret("accept failed");
}
// 创建一个线程处理客户端请求
// pthread_create函数的最后一个参数类型是void *,而我们要传的是int
// 此处我们先将int转换为了long,然后再将其转换为void *
// 原因在于在LP 64的C语言数据模型中,int的大小是4字节,指针的大小是8字节
// 因此直接转换会报以下警告:
// warning: cast to pointer from integer of different size \
[-Wint-to-pointer-cast]
// 先将int转成long,long在LP 64的C语言数据模型中也是8字节,长度和指针相等
// 因此就消除了警告
// 我们也可以在编译时加上选项[-Wno-int-to-pointer-cast]来消除此警告
// 如果使用的C语言版本大于等于C99,也可使用(void *)((uintptr_t)sockfd)消除警告
// uintptr_t就是为了跨平台地将int转为指针,不同平台只需修改uintptr_t类型的定义即可
// uintptr_t比intptr_t更安全,intptr_t是有符号数
// 由于指针值通常是无符号的,将其存储在有符号整数类型中可能导致一些问题
// 如符号扩展和数值溢出等
pthread_create(&tid, NULL, client_thread, (void *)((long)sockfd));
}
}
}
// 主线程main一直循环,将请求发送到其他线程处理,永远不应到达此处的exit语句
exit(1);
}
/*
* Initialize the job ID file. use a record lock to prevent
* more than one printer daemon from running at a time.
*
* LOCKING: none, except for record-lock on job ID file.
*/
void init_request(void) {
int n;
char name[FILENMSZ];
sprintf(name, "%s/%s", SPOOLDIR, JOBFILE);
// open函数默认不会截断文件
jobfd = open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
// 在整个文件上放一把写锁,表明守护进程正在运行,只能有一个守护进程在运行
if (write_lock(jobfd, 0, SEEK_SET, 0) < 0) {
log_quit("daemon already running");
}
/*
* Reuse the name buffer for the job counter.
*/
if ((n = read(jobfd, name, FILENMSZ)) < 0) {
log_sys("can't read job file");
}
if (n == 0) {
nextjob = 1;
} else {
// 作业文件包含一个ASCII码的整数字符串来表示下一个作业编号
// 在长整型长度为64位的系统上,至少需要一个21字节的缓冲区来存放代表最大整型数的字符串
// 这里可以重用文件名缓冲区,FILENMSZ在print.h中定义为64
nextjob = atol(name);
}
// 不能关闭jobfd,否则将释放已经放置在上面的写锁
}
/*
* Initialize printer information from configuration file.
* 用于设置打印机名和地址
*
* LOCKING: none.
*/
void init_printer(void) {
printer = get_printaddr();
if (printer == NULL) {
/* message already logged,get_printaddr函数会自己记录错误消息 */
exit(1);
}
printer_name = printer->ai_canonname;
if (printer_name == NULL) {
printer_name = "printer";
}
log_msg("printer is %s", printer_name);
}
/*
* Update the job ID file with the next job number.
* Doesn't handle wrap-around of job number.
*
* LOCKING: none.
*/
void update_jobno(void) {
char buf[32];
if (lseek(jobfd, 0, SEEK_SET) == -1) {
log_sys("can't seek in job file");
}
sprintf(buf, "%d", nextjob);
if (write(jobfd, buf, strlen(buf)) < 0) {
log_sys("can't update job file");
}
}
/*
* Get the next job number.
*
* LOCKING: acquires and releases joblock.
*/
int32_t get_newjobno(void) {
int32_t jobid;
pthread_mutex_lock(&joblock);
jobid = nextjob++;
// 处理回绕
if (nextjob <= 0) {
nextjob = 1;
}
pthread_mutex_unlock(&joblock);
// 返回递增前的nextjob值
return jobid;
}
/*
* Add a new job to the list of pending jobs. Then signal
* the printer thread that a job is pending.
*
* LOCKING: acquires and releases joblock.
*/
void add_job(struct printreq *reqp, int32_t jobid) {
struct job *jp;
// 为job结构分配空间,如果失败,记录日志并退出,此时打印请求已经安全地存储在磁盘上
if ((jp = malloc(sizeof(struct job))) == NULL) {
log_sys("malloc failed");
}
memcpy(&jp->req, reqp, sizeof(struct printreq));
jp->jobid = jobid;
jp->next = NULL;
pthread_mutex_lock(&joblock);
jp->prev = jobtail;
if (jobtail == NULL) {
jobhead = jp;
} else {
jobtail->next = jp;
}
jobtail = jp;
pthread_mutex_unlock(&joblock);
// 向打印机线程发信号,告诉它有作业可用了
pthread_cond_signal(&jobwait);
}
/*
* Replace a job back on the head of the list.
*
* LOCKING: acquires and releases joblock.
*/
void replace_job(struct job *jp) {
pthread_mutex_lock(&joblock);
jp->prev = NULL;
jp->next = jobhead;
if (jobhead == NULL) {
jobtail = jp;
} else {
jobhead->prev = jp;
}
jobhead = jp;
pthread_mutex_unlock(&joblock);
}
/*
* Remove a job from the list of pending jobs.
*
* LOCKING: caller must hold joblock.
*/
void remove_job(struct job *target) {
if (target->next != NULL) {
target->next->prev = target->prev;
} else {
jobtail = target->prev;
}
if (target->prev != NULL) {
target->prev->next = target->next;
} else {
jobhead = target->next;
}
}
/*
* Check the spool directory for pending jobs on start-up.
* 在守护进程启动时,从/var/spool/printer/reqs目录中的磁盘文件建立一个打印作业列表
*
* LOCKING: none.
*/
void build_qonstart(void) {
int fd, err, nr;
int32_t jobid;
DIR *dirp;
struct dirent *entp;
struct printreq req;
char dname[FILENMSZ], fname[FILENMSZ];
sprintf(dname, "%s/%s", SPOOLDIR, REQDIR);
// 无法打开此目录说明没有打印作业要处理
if ((dirp = opendir(dname)) == NULL) {
return;
}
while ((entp = readdir(dirp)) != NULL) {
/*
* Skip "." and ".."
*/
if (strcmp(entp->d_name, ".") == 0
|| strcmp(entp->d_name, "..") == 0) {
continue;
}
/*
* Read the request struture.
*/
sprintf(fname, "%s/%s/%s", SPOOLDIR, REQDIR, entp->d_name);
if ((fd = open(fname, O_RDONLY)) < 0) {
continue;
}
nr = read(fd, &req, sizeof(struct printreq));
if (nr != sizeof(struct printreq)) {
if (nr < 0) {
err = errno;
} else {
err = EIO;
}
close(fd);
log_msg("build_qonstart: can't read %s: %s", fname, strerror(err));
// 如果不能读取整个结构,则删除该文件,并将该文件对应的数据文件也删除
unlink(fname);
sprintf(fname, "%s/%s/%s", SPOOLDIR, DATADIR, entp->d_name);
unlink(fname);
continue;
}
// 如果能读取一个完整的printreq结构,则将文件名转换为作业ID(文件名就是作业ID)
jobid = atol(entp->d_name);
log_msg("adding job %d to queue", jobid);
add_job(&req, jobid);
}
closedir(dirp);
}
/*
* Accept a print job from a client.
* 当客户的连接请求被接受时,main函数会派生出线程client_thread
* 该函数作用是从客户端的print命令中接收要打印的文件
*
* LOCKING: none.
*/
void *client_thread(void *arg) {
int n, fd, sockfd, nr, nw, first;
int32_t jobid;
pthread_t tid;
struct printreq req;
struct printresp res;
char name[FILENMSZ];
char buf[IOBUFSZ];
tid = pthread_self();
// 安装线程清理处理程序
pthread_cleanup_push(client_cleanup, (void *)((long)tid));
sockfd = (long)arg;
// 创建一个worker_thread结构,并将其加入活跃的客户端线程列表中
add_worker(tid, sockfd);
/*
* Read the request header.
*/
if ((n = treadn(sockfd, &req, sizeof(struct printreq), 10)) != sizeof(struct printreq)) {
res.jobid = 0;
if (n < 0) {
// htonl函数可将一个32位的数从主机字节序转换为网络字节序
res.retcode = htonl(errno);
} else {
res.retcode = htonl(EIO);
}
strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
writen(sockfd, &res, sizeof(struct printresp));
pthread_exit((void*)1);
}
// 将请求头中的整数字段转换成主机字节序
req.size = ntohl(req.size);
req.flags = ntohl(req.flags);
/*
* Create the data file.
*/
jobid = get_newjobno();
// 数据文件名为/var/spool/printer/data/jobid,其中jobid是请求的作业ID
// 并且权限FILEPERM为S_IRUSR|S_IWUSR可防止其他人读取这些文件
sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid);
fd = creat(name, FILEPERM);
if (fd < 0) {
res.jobid = 0;
res.retcode = htonl(errno);
log_msg("client_therad: can't create %s: %s", name, strerror(res.retcode));
strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
writen(sockfd, &res, sizeof(struct printresp));
pthread_exit((void *)1);
}
/*
* Read the file and store it in the spool directory.
* Try to figure out if the file is a PostScript file
* or a plain text file.
* 读取来自客户端的文件内容
*/
first = 1;
while ((nr = tread(sockfd, buf, IOBUFSZ, 20)) > 0) {
// 第一次循环读取时,判断该文件是否是PostScript文件
if (first) {
first = 0;
// 如果文件内容不以%!PS开头,则可以假定其为纯文本,此时在请求头中设置PR_TEXT标志
// 如果客户的print命令中有-t标志,那么客户端也会设置此标志
// 尽管PostScript程序(PostScript是一种编程语言,最适用于列印图像和文字)不要求以%!PS开始
// 但文档格式指南(Adobe Systems[1999])强烈推荐这种方式
if (strncmp(buf, "%!PS", 4) != 0) {
req.flags |= PR_TEXT;
}
}
nw = write(fd, buf, nr);
if (nw != nr) {
res.jobid = 0;
if (nw < 0) {
res.retcode = htonl(errno);
} else {
res.retcode = htonl(EIO);
}
log_msg("client_thread: can't write %s: %s", name, strerror(res.retcode));
close(fd);
strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
writen(sockfd, &res, sizeof(struct printresp));
unlink(name);
// 此处不需要关闭套接字文件描述符,因为在调用pthread_exit时,线程清理处理程序会处理它
pthread_exit((void *)1);
}
}
close(fd);
/*
* Create the control file. Then write the
* print request information to the control
* file.
*/
sprintf(name, "%s/%s/%d", SPOOLDIR, REQDIR, jobid);
fd = creat(name, FILEPERM);
if (fd < 0) {
res.jobid = 0;
res.retcode = htonl(errno);
log_msg("client_thread: can't create %s: %s", name, strerror(res.retcode));
strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
writen(sockfd, &res, sizeof(struct printresp));
sprintf(name, "%s/%s/%d", SPOOLDIR, DATADIR, jobid);
unlink(name);
pthread_exit((void *)1);
}
nw = write(fd, &req, sizeof(struct printreq));
if (nw != sizeof(struct printreq)) {
res.jobid = 0;
if (nw < 0) {
res.retcode = htonl(errno);
} else {
res.retcode = htonl(EIO);
}
log_msg("client_thread: can't write %s: %s", name, strerror(res.retcode));
close(fd);
strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
writen(sockfd, &res, sizeof(struct printresp));
unlink(name);
sprintf(name, "%s/%s/%d", SPOOLDIR, DATADIR, jobid);
unlink(name);
pthread_exit((void *)1);
}
close(fd);
/*
* Send response to client.
*/
res.retcode = 0;
res.jobid = htonl(jobid);
// res.msg是给客户的文本消息
sprintf(res.msg, "request ID %d", jobid);
writen(sockfd, &res, sizeof(struct printresp));
/*
* Notify the printer thread, clean up, and exit.
*/
log_msg("adding job %d to queue", jobid);
// add_job函数将接受的文件加入到挂起作业列表中
add_job(&req, jobid);
// 用非0参数调用pthread_cleanup_pop,执行线程清理程序
// pthread_cleanup_pop和pthread_cleanup_push函数必须在同一作用域成对出现
pthread_cleanup_pop(1);
return (void *)0;
// 用return退出线程时,不会执行线程清理程序,因此在
// 线程退出前,必须关闭不再使用的文件描述符,因为线程退出后且进程中仍有其他线程时
// 文件描述符不会自动关闭,如果不关闭不需要的文件描述符,将耗尽资源
}
/*
* Add a worker to the list of worker threads.
*
* LOCKING: acquires and releases workerlock.
*/
void add_worker(pthread_t tid, int sockfd) {
struct worker_thread *wtp;
if ((wtp = malloc(sizeof(struct worker_thread))) == NULL) {
log_ret("add_worker: can't malloc");
pthread_exit((void *)1);
}
wtp->tid = tid;
wtp->sockfd = sockfd;
pthread_mutex_lock(&workerlock);
wtp->prev = NULL;
wtp->next = workers;
if (workers != NULL) {
workers->prev = wtp;
}
workers = wtp;
pthread_mutex_unlock(&workerlock);
}
/*
* Cancel (kill) all outstanding workers.
*
* LOCKING: acquires and releases workerlock.
*/
void kill_workers(void) {
struct worker_thread *wtp;
pthread_mutex_lock(&workerlock);
for (wtp = workers; wtp != NULL; wtp = wtp->next) {
// pthread_cancel函数仅仅将线程列入删除计划
// 实际的删除动作在线程到达下一个删除点时发生
pthread_cancel(wtp->tid);
}
pthread_mutex_unlock(&workerlock);
}
/*
* Cancellation routine for the worker thread.
* 该函数是与客户端通信的工作线程的线程清理程序
* 当线程调用pthread_exit、用非0参数调用pthread_cleanup_pop、
* 响应pthread_cancel函数的线程取消请求时,client_cleanup清理函数会被调用
*
* LOCKING: acquires and releases workerlock.
*/
void client_cleanup(void *arg) {
struct worker_thread *wtp;
pthread_t tid;
tid = (pthread_t)((long)arg);
// 当kill_workers函数正在遍历列表来取消所有线程时
// 如果有一个线程到达了取消点,也需要等待kill_workers函数
// 对所有线程发送完取消请求后,然后释放互斥量后此处才能继续处理
pthread_mutex_lock(&workerlock);
for (wtp = workers; wtp != NULL; wtp = wtp->next) {
if (wtp->tid == tid) {
if (wtp->next != NULL) {
wtp->next->prev = wtp->prev;
}
if (wtp->prev != NULL) {
wtp->prev->next = wtp->next;
} else {
workers = wtp->next;
}
break;
}
}
pthread_mutex_unlock(&workerlock);
if (wtp != NULL) {
close(wtp->sockfd);
free(wtp);
}
}
/*
* Deal with signals.
*
* LOCKING: acquires and releases configlock.
*/
void *signal_thread(void *arg) {
int err, signo;
for (; ; ) {
// 等待信号集mask中的信号发生
err = sigwait(&mask, &signo);
if (err != 0) {
log_quit("sigwait failed: %s", strerror(err));
}
switch (signo) {
case SIGHUP:
/*
* Schedule to re-read the configureation file.
*/
pthread_mutex_lock(&configlock);
reread = 1;
pthread_mutex_unlock(&configlock);
break;
case SIGTERM:
kill_workers();
log_msg("terminate with signal %s", strsignal(signo));
exit(0);
default:
kill_workers();
log_quit("unexpected signal %d", signo);
}
}
}
/*
* Add an option to the IPP header.
* 属性的格式是1字节的描述属性类型的标志,然后是2字节的二进制整数形式存储的属性名长度
* 接着是属性名、属性值长度,最后是属性值本身
*
* LOCKING: none.
*/
char *add_option(char *cp, int tag, char *optname, char *optval) {
int n;
union {
int16_t s;
char c[2];
} u;
*cp++ = tag;
n = strlen(optname);
// 将属性名长度以二进制数字形式嵌入IPP首部,IPP不控制该二进制长度值的对齐方式
// 某些处理器架构中,如SPARC,不能从任意地址读取一个整数
// 因此我们不能通过读取指向int16_t的指针所指的值,然后再存到首部中属性名长度的地址
// 我们只能一次复制1字节,这就是为什么我们定义了一个union来存储一个16位整数和2字节
u.s = htons(n);
*cp++ = u.c[0];
*cp++ = u.c[1];
strcpy(cp, optname);
cp += n;
n = strlen(optval);
u.s = htons(n);
*cp++ = u.c[0];
*cp++ = u.c[1];
strcpy(cp, optval);
return cp + n;
}
/*
* Single thread to communicate with the printer.
*
* LOCKING: acquires and releases joblock and configlock.
*/
void *printer_thread(void *arg) {
struct job *jp;
int hlen, ilen, sockfd, fd, nr, nw, extra;
char *icp, *hcp, *p;
struct ipp_hdr *hp;
struct stat sbuf;
// 用iovec在一次writev调用中来写http和ipp这两个首部
struct iovec iov[2];
char name[FILENMSZ];
char hbuf[HBUFSZ];
char ibuf[IBUFSZ];
char buf[IOBUFSZ];
char str[64];
struct timespec ts = {60, 0}; /* 1 minute */
// 会使用icp和ibuf建立IPP首部,hcp和hbuf建立HTTP首部
for (; ; ) {
/*
* Get a job to print.
*/
pthread_mutex_lock(&joblock);
while (jobhead == NULL) {
log_msg("printer_thread: waiting...");
pthread_cond_wait(&jobwait, &joblock);
}
remove_job(jp = jobhead);
log_msg("printer_thread: picked up job %d", jp->jobid);
pthread_mutex_unlock(&joblock);
update_jobno();
/*
* Check for a change in the config file.
* 由于从main线程初始化后,只有这个上下文可以查看并更改打印机信息
* 因此除需要configlock互斥量来保护reread标志状态外,不需要任何其他同步手段
* 虽然该函数中使用了两个互斥量,但没有同时持有两个互斥量,因此不需要建立一个锁层次
*/
pthread_mutex_lock(&configlock);
if (reread) {
// 释放旧的addrinfo列表,清空指针,解锁互斥量
// 然后调用init_printer重新初始化指针信息
freeaddrinfo(printer);
printer = NULL; // 打印机的网络地址,addrinfo结构
printer_name = NULL; // 打印机名
reread = 0;
pthread_mutex_unlock(&configlock);
init_printer();
} else {
pthread_mutex_unlock(&configlock);
}
/*
* Send job to printer.
*/
sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jp->jobid);
if ((fd = open(name, O_RDONLY)) < 0) {
log_msg("job %ld canceled - can't open %s: %s", jp->jobid, name, strerror(errno));
free(jp);
continue;
}
if (fstat(fd, &sbuf) < 0) {
log_msg("job %ld canceled - can't fstat %s: %s", jp->jobid, name, strerror(errno));
free(jp);
close(fd);
continue;
}
if ((sockfd = connect_retry(AF_INET, SOCK_STREAM, 0, printer->ai_addr,
printer->ai_addrlen)) < 0) {
log_msg("job %d deferred - can't contact printer: %s", jp->jobid, strerror(errno));
goto defer;
}
/*
* Set up the IPP header.
*/
icp = ibuf;
// ipp_hdr是IPP首部结构体
hp = (struct ipp_hdr *)icp;
hp->major_version = 1;
hp->minor_version = 1;
// operation宏定义为了u.op
hp->operation = htons(OP_PRINT_JOB);
hp->request_id = htonl(jp->jobid);
icp += offsetof(struct ipp_hdr, attr_group);
*icp++ = TAG_OPERATION_ATTR;
// 调用add_option将属性添加到IPP首部,前3个属性是必需的
// 此处将字符集设为utf-8,该字符集属性的值必须是打印机支持的
icp = add_option(icp, TAG_CHARSET, "attributes-charset", "utf-8");
// 指定语言为美国英语
icp = add_option(icp, TAG_NATULANG, "attributes-natural-language", "en-us");
sprintf(str, "http://%s/ipp", printer_name);
icp = add_option(icp, TAG_URI, "printer-uri", str);
// 推荐使用requesting-user-name属性,但不是必须
icp = add_option(icp, TAG_NAMEWOLANG, "requesting-user-name", jp->req.usernm);
// job-name属性也是可选的,此处将要打印的文件名作为作业名发送,能帮助用户区别多个要处理的作业
icp = add_option(icp, TAG_NAMEWOLANG, "job-name", jp->req.jobnm);
if (jp->req.flags & PR_TEXT) {
p = "text/plain";
// 如果打印纯文本,下面会看到,需要发送一个附加字符以可靠地打印纯文本
extra = 1;
} else {
p = "application/postscript";
extra = 0;
}
// 如果省略document-format属性,则假定文件格式是打印机默认格式
// 对于PostScript打印机,默认格式可能是PostScript
// 一些打印机可以自动检测格式并在PostScript、纯文本、PCL(HP的打印机命令语言)格式间做选择
icp = add_option(icp, TAG_MIMETYPE, "document-format", p);
// 用结束属性标志定界
*icp++ = TAG_END_OF_ATTR;
ilen = icp - ibuf;
/*
* Set up the HTTP header.
*/
hcp = hbuf;
sprintf(hcp, "POST /ipp HTTP/1.1\r\n");
hcp += strlen(hcp);
// 将Context-Length设为IPP首部的字节长度加上要打印文件的大小再加上需要发送的附加字符的长度
sprintf(hcp, "Content-Length: %ld\r\n", (long)sbuf.st_size + ilen + extra);
hcp += strlen(hcp);
strcpy(hcp, "Content-Type: application/ipp\r\n");
hcp += strlen(hcp);
sprintf(hcp, "Host: %s:%s\r\n", printer_name, IPP_PORT);
hcp += strlen(hcp);
// 回车换行符结束HTTP首部
*hcp++ = '\r';
*hcp++ = '\n';
hlen = hcp - hbuf;
/*
* Write the headers first. Then send the file.
*/
iov[0].iov_base = hbuf;
iov[0].iov_len = hlen;
iov[1].iov_base = ibuf;
iov[1].iov_len = ilen;
if (writev(sockfd, iov, 2) != hlen + ilen) {
log_ret("can't write to printer");
goto defer;
}
if (jp->req.flags & PR_TEXT) {
/*
* Hack: allow PostScript to be printed as plain text.
* 即使指明了纯文本,Phaser 8560还是会自动检测文档格式
* 为防止它忽视纯文本打印要求转而以文档自己的格式打印,将退格作为文件的第一个发送字符
* 这个字符不会被打印出来,并且能使自动识别文件格式的功能失效
* 这样就可以打印PostScript源文件,而不用打印PostScript程序文件生成的结果镜像
* 另一种替代方式是使用a2ps这样的程序将源文件打印成一个PostScript程序
*/
if (write(sockfd, "\b", 1) != 1) {
log_ret("can't write to printer");
goto defer;
}
}
while ((nr = read(fd, buf, IOBUFSZ)) > 0) {
if ((nw = writen(sockfd, buf, nr)) != nr) {
if (nw < 0) {
log_ret("can't write to printer");
} else {
log_msg("short write (%d/%d) to printer", nw, nr);
}
goto defer;
}
}
if (nr < 0) {
log_ret("can't read %s", name);
goto defer;
}
/*
* Read the response from the printer.
* 将文件发送完毕后,读取打印机对于请求的响应
*/
if (printer_status(sockfd, jp)) {
// 如果printer_status函数返回非0值,说明请求成功
// 此时就可以删除数据文件和控制文件、释放job结构jp,然后将其设为NULL
unlink(name);
sprintf(name, "%s/%s/%d", SPOOLDIR, REQDIR, jp->jobid);
unlink(name);
free(jp);
jp = NULL;
}
defer:
close(fd);
if (sockfd >= 0) {
close(sockfd);
}
// 如果jp不为空,说明出错,此时将作业放在挂起作业列表头部,然后延迟1分钟
if (jp != NULL) {
replace_job(jp);
nanosleep(&ts, NULL);
}
// 如果jp为空,只需回到循环开始处,获得下一个要打印的作业
}
}
/*
* Read data from the printer , possibly increasing the buffer.
* Returns offset of end of data in buffer or -1 on failure.
* 用于读取来自打印机的部分响应消息
*
* LOCKING: none.
*/
ssize_t readmore(int sockfd, char **bpp, int off, int *bszp) {
ssize_t nr;
char *bp = *bpp;
int bsz = *bszp;
// 如果到达缓冲区尾部,重新分配一个大点的缓冲区
if (off >= bsz) {
bsz += IOBUFSZ;
if ((bp = realloc(*bpp, bsz)) == NULL) {
log_sys("readmore: can't allocate bigger read buffer");
}
*bszp = bsz;
*bpp = bp;
}
// 如果读到了数据,返回已读数据的偏移量,否则read函数失败或超时返回-1
if ((nr = tread(sockfd, &bp[off], bsz - off, 1)) > 0) {
return off + nr;
} else {
return -1;
}
}
/*
* Read and parse the response from the prnter. Return 1
* if the request was successful, and 0 otherwise.
* 读取打印机对一个打印作业请求的响应消息
* 我们不知道打印机会如何响应,也许会在多个报文中回送一个响应
* 也许在一个报文中回送完整的响应
* 或者收到响应前会收到一个确认,如HTTP100 Continue报文
* HTTP100 Continue报文是对请求头部的响应,用于告知客户端可以继续发送请求主体
*
* LOCKING: none;
*/
int printer_status(int sfd, struct job *jp) {
int i, success, code, len, found, bufsz, datsz;
int32_t jobid;
ssize_t nr;
char *bp, cp, *statcode, *reason, *contentlen;
struct ipp_hdr h;
/*
* Read the HTTP header followed by the IPP response header.
* They can be returned in multiple read attempts. Use the
* Content-Length specifier to determine how much to read.
* HTTP响应的第一行是状态行,格式如下:
* HTTP/x.y 状态码 状态码描述\r\n
*/
success = 0;
bufsz = IOBUFSZ;
if ((bp = malloc(IOBUFSZ)) == NULL) {
log_sys("printer_status: can't allocate read buffer");
}
// 我们期望5秒内有可用的响应
while ((nr = tread(sfd, bp, bufsz, 5)) > 0) {
/*
* Find the status. Response starts with "HTTP/x.y"
* so we can skip the first 8 characters.
* 跳过HTTP/1.1和之后的所有空白字符
*/
cp = bp + 8;
datsz = nr;
while (isspace((int)*cp)) {
cp++;
}
// 然后是数字状态码
statcode = cp;
while (isdigit((int)*cp)) {
cp++;
}
// 如果不是数字状态码,在日志中记录报文内容
if (cp == statcode) { /* Bad format; log it and move on */
log_msg(bp);
} else {
// 把空格改为null字节
*cp++ = '\0';
reason = cp;
// 接下来从reason开始是状态码描述,搜索回车或换行符,并用null字节结束状态码描述字符串
while (*cp != '\r' && *cp != '\n') {
cp++;
}
*cp = '\0';
// 将状态码字符转换为一个整数
code = atoi(statcode);
// 如果只是提供信息的报文,则将其忽略并继续循环读接下来的响应
if (HTTP_INFO(code)) {
continue;
}
// 如果是出错消息,则记入出错日志并退出循环
if (!HTTP_SUCCESS(code)) { /* probable error: log it */
bp[datsz] = '\0';
log_msg("error: %s", reason);
break;
}
/*
* HTTP request was okay. but still need to check
* IPP status. Search for the Content-Length.
*/
i = cp - bp;
for (; ; ) {
// 由于HTTP首部关键字是分大小写的,因此大小写都需要搜索
// 此处有一个bug,如果我们只读到了Content-Length:关键字的一部分
// 我们会跳出该while循环,又由于i此时还小于datsz
// 因此不会读取更多数据,从而导致后面匹配该关键字时失败
// 从而导致cp指向关键字中的o,导致跳过了该关键字
while (*cp != 'C' && *cp != 'c' && i < datsz) {
cp++;
i++;
}
if (i >= datsz) { /* get more header,缓冲区耗尽 */
if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {
goto out;
} else {
// bp缓冲区被realloc,需要调整cp的指向
cp = &bp[i];
datsz += nr;
}
}
// 忽略大小写的比较
if (strncasecmp(cp, "Content-Length:", 15) == 0) {
cp += 15;
while (isspace((int)*cp)) {
cp++;
}
contentlen = cp;
while (isdigit((int)*cp)) {
cp++;
}
*cp++ = '\0';
i = cp - bp;
len = atoi(contentlen);
break;
} else {
cp++;
i++;
}
}
// 现在,我们知道了Content-Lenth属性值,即IPP报文长度
// 如果此时缓冲区耗尽,再次从打印机读取
if (i >= datsz) { /* get more header */
if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {
goto out;
} else {
cp = &bp[i];
datsz += nr;
}
}
// 寻找HTTP首部的末尾(即空白行)
found = 0;
while (!found) { /* look for end of HTTP header */
while (i < datsz - 2) {
if (*cp == '\n' && *(cp + 1) == '\r' && *(cp + 2) == '\n') {
found = 1;
cp += 3;
i += 3;
break;
}
cp++;
i++;
}
if (i >= datsz) { /* get more header */
if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {
goto out;
} else {
cp = &bp[i];
datsz += nr;
}
}
}
// 如果已经读取的值减去HTTP首部大小后不等于IPP报文的长度,则需要读取更多数据
if (datsz - i < len) { /* get more header */
if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {
goto out;
} else {
cp = &bp[i];
datsz += nr;
}
}
memcpy(&h, cp, sizeof(struct ipp_hdr));
i = ntohs(h.status);
jobid = ntohl(h.request_id);
if (jobid != jp->jobid) {
/*
* Different jobs. Ignore it.
*/
log_msg("jobid %d status code %d", jobid, i);
break;
}
if (STATCLASS_OK(i)) {
success = 1;
}
break;
}
}
out:
free(bp);
if (nr < 0) {
log_msg("jobid %d: error reading printer response: %s", jobid, strerror(errno));
}
return success;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2020-09-14 LeetCode 1436. 旅行终点站