21.5 源代码

本章的源代码有5个文件,不包括在前面章节中所用的一些公共库例程。

ipp.h 包含IPP定义的头文件。

print.h 包含公用的常数、数据结构定义以及实用工具例程的声明的头文件。

util.c 用于两个程序的实用工具例程。

用于打印文件的命令行程序C代码。

printd.c 用于打印假脱机守护进程的C代码。我们按照所列次序依次分析每个文件。

首先从ipp.h头文件开始。

print.c

1 #ifndef _IPP_H

2 #define _IPP_H 3 /*

4 * Defines parts of the IPP protocol between the scheduler

5 * and the printer. Based on RFC2911 and RFC2910.

6 */

7 /*

8 * Status code classes.

9 */

10  #define STATCLASS_OK(x)    ((x) >= 0x0000 && (x) <= 0x00ff)

11  #define STATCLASS_INFO(x)   ((x) >= 0x0100 && (x) <= 0x01ff)

12 #define STATCLASS_REDIR(x) ((x) >= 0x0300 && (x) <= 0x03ff)

13 #define STATCLASS_CLIERR(x) ((x) >= 0x0400 && (x) <= 0x04ff)

14 #define STATCLASS_SRVERR(x) ((x) >= 0x0500 && (x) <= 0x05ff)

15 /*

16 * Status codes.

17 */

18  #define STAT_OK      0x0000 /* success */

19 #define STAT_OK_ATTRIGN 0x0001 /* OK; some attrs ignored */

20 #define STAT_OK_ATTRCON 0x0002 /* OK; some attrs conflicted */

21 #define STAT_CLI_BADREQ 0x0400 /* invalid client request */

22 #define STAT_CLI_FORBID 0x0401 /* request is forbidden */

23 #define STAT_CLI_NOAUTH 0x0402 /* authentication required */

24 #define STAT_CLI_NOPERM 0x0403 /* client not authorized */

25 #define STAT_CLI_NOTPOS 0x0404 /* request not possible */

26 #define STAT_CLI_TIMOUT 0x0405 /* client too slow */

27 #define STAT_CLI_NOTFND 0x0406 /* no object found for URI */

28 #define STAT_CLI_OBJGONE 0x0407 /* object no longer available */

29 #define STAT_CLI_TOOBIG 0x0408 /* requested entity too big */

30 #define STAT_CLI_TOOLNG 0x0409 /* attribute value too large */

31 #define STAT_CLI_BADFMT 0x040a /* unsupported doc format */

32 #define STAT_CLI_NOTSUP 0x040b /* attributes not supported */

33 #define STAT_CLI_NOSCHM 0x040c /* URI scheme not supported */

34 #define STAT_CLI_NOCHAR 0x040d /* charset not supported */

35 #define STAT_CLI_ATTRCON 0x040e /* attributes conflicted */

36 #define STAT_CLI_NOCOMP 0x040f /* compression not supported */

37 #define STAT_CLI_COMPERR 0x0410 /* data can't be decompressed */

38 #define STAT_CLI_FMTERR 0x0411 /* document format error */

39 #define STAT_CLI_ACCERR 0x0412 /* error accessing data */

[1~14] ipp.h从标准的#ifdef开始,用于防止同一文件被包含两次的错误。然后定义IPP状态码的类(参见RFC 2911的第13节)。

[15~39] 定义基于RFC 2911的状态码,但是本程序不使用,这些状态码的使用留给读者作为练习(参见习题21.1)。

40 #define STAT_SRV_INTERN  0x0500 /* unexpected internal error */

41 #define STAT_SRV_NOTSUP  0x0501 /* operation not supported */42 #define STAT_SRV_UNAVAIL 0x0502 /* service unavailable */

43 #define STAT_SRV_BADVER  0x0503 /* version not supported */

44 #define STAT_SRV_DEVERR  0x0504 /* device error */

45 #define STAT_SRV_TMPERR  0x0505 /* temporary error */

46 #define STAT_SRV_REJECT  0x0506 /* server not accepting jobs */47 #define STAT_SRV_TOOBUSY 0x0507 /* server too busy */

48 #define STAT_SRV_CANCEL  0x0508 /* job has been canceled */

49 #define STAT_SRV_NOMULTI 0x0509 /* multi-doc jobs unsupported */

50 /*

51 * Operation IDs

52 */

53 #define OP_PRINT_JOB      0x02

54 #define OP_PRINT_URI      0x03

55 #define OP_VALIDATE_JOB    0x04

56 #define OP_CREATE_JOB     0x05

57 #define OP_SEND_DOC      0x06

58 #define OP_SEND_URI      0x07

59 #define OP_CANCEL_JOB     0x08

60 #define OP_GET_JOB_ATTR    0x09

61 #define OP_GET_JOBS      0x0a

62 #define OP_GET_PRINTER_ATTR  0x0b

63 #define OP_HOLD_JOB      0x0c

64 #define OP_RELEASE_JOB     0x0d

65 #define OP_RESTART_JOB     0x0e

66 #define OP_PAUSE_PRINTER    0x10

67 #define OP_RESUME_PRINTER   0x11

68 #define OP_PURGE_JOBS     0x12

69 /*

70 * Attribute Tags.

71 */

72 #define TAG_OPERATION_ATTR   0x01 /* operation attributes tag */

73 #define TAG_JOB_ATTR      0x02 /* job attributes tag */

74 #define TAG_END_OF_ATTR    0x03 /* end of attributes tag */

75 #define TAG_PRINTER_ATTR    0x04 /* printer attributes tag */

76 #define TAG_UNSUPP_ATTR    0x05 /* unsupported attributes tag */

[40~49] 继续定义状态码。0x500~0x5ff是服务器错误码。RFC 2911中13.1.1节至13.1.5节描述了所有的状态码。

[50~68] 接着定义各种操作ID。IPP中定义的每个操作有一个ID(参见RFC 2911的4.4.15节)。在本例中,仅用到打印作业操作。

[69~76] 属性标志限定了IPP中请求和响应报文的属性组。这些值定义在RFC 2910的3.5.1节。

77 /*

78 * Value Tags.

79 */

80 #define TAG_UNSUPPORTED    0x10 /* unsupported value */

81 #define TAG_UNKNOWN      0x12 /* unknown value */

82 #define TAG_NONE        0x13 /* no value */

83 #define TAG_INTEGER      0x21 /* integer */

84 #define TAG_BOOLEAN      0x22 /* boolean */

85 #define TAG_ENUM        0x23 /* enumeration */

86 #define TAG_OCTSTR       0x30 /* octetString */

87 #define TAG_DATETIME      0x31 /* dateTime */

88 #define TAG_RESOLUTION     0x32 /* resolution */

89 #define TAG_INTRANGE      0x33 /* rangeOfInteger */

90 #define TAG_TEXTWLANG     0x35 /* textWithLanguage */

91 #define TAG_NAMEWLANG     0x36 /* nameWithLanguage */

92 #define TAG_TEXTWOLANG     0x41 /* textWithoutLanguage */

93 #define TAG_NAMEWOLANG     0x42 /* nameWithoutLanguage */

94 #define TAG_KEYWORD      0x44 /* keyword */

95 #define TAG_URI        0x45 /* URI */

96 #define TAG_URISCHEME     0x46 /* uriScheme */

97 #define TAG_CHARSET      0x47 /* charset */

98 #define TAG_NATULANG      0x48 /* naturalLanguage */

99 #define TAG_MIMETYPE      0x49 /* mimeMediaType */

100 struct ipp_hdr {

101  int8_t major_version; /* always 1 */

102  int8_t minor_version; /* always 1 */

103  union {

104     int16_t op; /* operation ID */

105     int16_t st; /* status */

106  } u;

107  int32_t request_id;  /* request ID */

108  char  attr_group[1]; /* start of optional attributes group */

109  /* optional data follows */

110 };

111 #define operation u.op

112 #define status u.st

113 #endif /* _IPP_H */

[77~99] 值标志指示每个属性和参数的格式,由RFC 2910的3.5.2节定义。[100~113] 定义IPP首部的结构。请求报文与响应报文的首部一样,除了请求中的操作ID被响应中的状态码代替。在头文件尾部我们用#endif来匹配文件开始的#ifdef。

下一个文件是print.h头文件。

1 #ifndef _PRINT_H

2 #define _PRINT_H

3 /*

4 * Print server header file.

5 */

6 #include <sys/socket.h>

7 #include <arpa/inet.h>

8 #include <netdb.h>

9 #include <errno.h>

10 #define CONFIG_FILE      "/etc/printer.conf"

11 #define SPOOLDIR        "/var/spool/printer"

12 #define JOBFILE        "jobno"

13 #define DATADIR        "data"

14 #define REQDIR         "reqs"

15 #if defined(BSD)

16 #define LPNAME           "daemon"

18 #define LPNAME           "_lp"

20 #define LPNAME           "lp"

17 #elif defined(MACOS)

19 #else

21 #endif

[1~9] 在这个头文件中包含所需要的所有头文件。应用程序只需简单地包含 print.h,而不需要跟踪所有的头文件依赖关系。

[10~14] 定义实现所需的文件和目录。包含打印守护进程和网络打印机主机名的配置文件在/etc/printer.conf 中。需要打印的文件副本在目录/var/spool/printer/data中;对于每个请求的控制信息在目录/var/spool/printer/reqs中。包含下一个作业编号的文件是/var/spool/printer/jobno。

目录必须由管理员创建并且由运行打印守护进程的账户所有。如果这些目录不存在,守护进程也不会创建这些目录,因为守护进程需要 root 权限来创建/var/spool中的目录。我们的设计初衷是当以root权限运行时,尽量让守护进程少做一些事情,以减少产生安全漏洞的可能。

[15~21] 接着定义运行打印守护进程的账户名。在Linux和Solaris中,这个账户名是lp。在Mac OS X中,账户名是_lp。FreeBSD没有为打印守护进程定义单独的账户,所以我们使用为系统守护进程保留的账户。

22 #define FILENMSZ 64

23 #define FILEPERM (S_IRUSR|S_IWUSR)

24 #define USERNM_MAX  64

25 #define JOBNM_MAX   256

26 #define MSGLEN_MAX  512

27 #ifndef HOST_NAME_MAX

28 #define HOST_NAME_MAX 256

29 #endif

30 #define IPP_PORT    631

31 #define QLEN 10

32 #define IBUFSZ     512  /* IPP header buffer size */

33 #define HBUFSZ     512  /* HTTP header buffer size */

34 #define IOBUFSZ    8192 /* data buffer size */

35 #ifndef ETIME

36 #define ETIME ETIMEDOUT

37 #endif

38 extern int getaddrlist(const char *, const char *,

39  struct addrinfo **);

40 extern char *get_printserver(void);

41 extern struct addrinfo *get_printaddr(void);

42 extern ssize_t tread(int, void *, size_t, unsigned int);

43 extern ssize_t treadn(int, void *, size_t, unsigned int);

44 extern int connect_retry(int, int, int, const struct sockaddr *,

45  socklen_t);

47  int);

46 extern int initserver(int, const struct sockaddr *, socklen_t,

[22~34] 接下来定义限制和常量。FILEPERM是创建要打印的文件副本使用的权限。这个权限是被限制的,因为我们不希望普通用户在等待打印时能够读取他人的文件。我们定义HOST_NAME_MAX作为用sysconf不能够确定系统的限制时能够支持的最大的主机名。

IPP被定义为使用端口631。QLEN是传递给listen的backlog参数(具体细节见16.4节)。[35~37] 一些平台没有定义错误码ETIME,因此另外定义一个错误码,使得在这些系统上有意义。当读超时时,返回这个错误码(我们不希望在从套接字读的时候服务器无限期地阻塞)。

[38~47] 接着,定义所有包含在util.c中的公共例程(稍后将分析这些例程)。注意,图16-11中的connect_retry函数和图16-22中的initserver函数没有包含在util.c中。

48 /*

49 * Structure describing a print request.

50 */

51 struct printreq {

52   uint32_t size;          /* size in bytes */

53   uint32_t flags;         /* see below */

54   char usernm[USERNM_MAX];     /* user’s name */

55   char jobnm[JOBNM_MAX];      /* job’s name */

56 };

57 /*

58 * Request flags.

59 */

60 #define PR_TEXT    0x01     /* treat file as plain text */

61 /*

62 * The response from the spooling daemon to the print command.

63 */

64 struct printresp {

65   uint32_t retcode;        /* 0=success, !0=error code */

66   uint32_t jobid;         /* job ID */

67   char msg[MSGLEN_MAX];      /* error message */

68 };

69 #endif /* _PRINT_H */

[48~69] printreq结构和printresp结构定义了print程序和打印假脱机守护进程之间的协议。print程序发送printreq结构到打印假脱机守护进程,该结构定义了作业大小(以字节为单位)、作业性质、用户名和作业名。打印假脱机守护进程用printresp结构回应,该结构包括返回码、作业ID和错误消息(如果请求失败)。

PR_TEXT作业性质表明要打印的文件只能被视为纯文本(而不是PostScript)。我们为所有的标志定义一个掩码而非对每个标志定义一个独立的字段。尽管目前只定义了一个标志值,将来还可以增加更多性质来扩展这个协议。例如,我们可以在增加一个标志位用来请求双面打印。不需要改变结构的大小就可以有31个额外的标志位的空间。改变结构的大小意味着可能会引入客户端和服务器的兼容性问题,除非对两边同时更新。另一个可选方案就是增加一个报文版本号,以允许不同版本的结构有所改变。

注意,对协议结构中的所有整数显式地定义了一个长度,这可以在客户端与服务器的整数长度不同时避免错位的结构元素。

下一个文件我们考察util.c,该文件包含实用工具例程。

1 #include "apue.h"

2 #include "print.h"

3 #include <ctype.h>

4 #include <sys/select.h>

5 #define MAXCFGLINE 512

6 #define MAXKWLEN 16

7 #define MAXFMTLEN 16

8 /*

9 * Get the address list for the given host and service and

10 * return through ailistpp. Returns 0 on success or an error

11 * code on failure. Note that we do not set errno if we

12 * encounter an error.

13 *

14 * LOCKING: none.

15 */

16 int

17 getaddrlist(const char *host, const char *service,

18 struct addrinfo **ailistpp)

19 {

20  int       err;

21  struct addrinfo hint;

22  hint.ai_flags = AI_CANONNAME;

23  hint.ai_family = AF_INET;

24  hint.ai_socktype = SOCK_STREAM;

25  hint.ai_protocol = 0;

26  hint.ai_addrlen = 0;

27  hint.ai_canonname = NULL;

28  hint.ai_addr = NULL;

29  hint.ai_next = NULL;

30  err = getaddrinfo(host, service, &hint, ailistpp);

31  return(err);

32 }

[1~7] 首先定义了这个文件中函数中的限制。MAXCFGLINE是打印机配置文件的行的最大长度、MAXKWLEN是配置文件中关键字的最大长度、MAXFMTLEN是传给sscanf的格式化字符串的最大长度。

[8~32] 第一个函数是getaddrlist,是getaddrinfo(16.3.3节)的封装,因为我们常常用同样的结构来调用 getaddrinfo。注意,在这个函数中不需要互斥锁。每个函数前面的 LOCKING 注释是用于多线程锁定的文档编写。这一注释列出了可能的关于锁的假设,告知该函数所需要获得或释放的锁,并告知调用这个函数所需要持有的锁。

33 /*

34 * Given a keyword, scan the configuration file for a match

35 * and return the string value corresponding to the keyword.

36 *

37 * LOCKING: none.

38 */

39 static char *

40 scan_configfile(char *keyword)

41 {

42  int      n, match;

43  FILE      *fp;

44  char      keybuf[MAXKWLEN], pattern[MAXFMTLEN];

45  char      line[MAXCFGLINE];

46  static char  valbuf[MAXCFGLINE];

47  if ((fp = fopen(CONFIG_FILE, "r")) == NULL)

48     log_sys("can't open %s", CONFIG_FILE);

49  sprintf(pattern, "%%%ds %%%ds", MAXKWLEN-1, MAXCFGLINE-1);

50  match = 0;

51  while (fgets(line, MAXCFGLINE, fp) != NULL) {

52     n = sscanf(line, pattern, keybuf, valbuf);

53     if (n == 2 && strcmp(keyword, keybuf) == 0) {

54       match = 1;

55       break;

56     }

57  }

58  fclose(fp);

59  if (match != 0)

60     return(valbuf);

61  else

62     return(NULL);

63 }

[33~46] scan_configfile函数搜索打印机配置文件中指定的关键字。

[47~63] 以读方式打开配置文件,根据搜索模式建立格式字符串。符号%%%ds 建立一个格式指示器来限定字符串长度,这样在栈中存放字符串的缓冲区就不会溢出。在文件中一次读取一行,并且扫描被空格符分开的两个字符串;如果找到它们,就用关键字与第一个字符串比较。如果找到一个匹配或者读到文件尾,则循环结束并关闭文件。如果关键字匹配,则返回一个指向包含关键字后面的字符串的缓冲区的指针;否则返回NULL。返回的字符串存放在静态缓冲区(valbuf)中,该缓冲区会被紧接的调用覆盖。因此,scan_configfile 不能用于多线程程序,除非能够小心地避免同时有多个线程调用它。

64 /*

65 * Return the host name running the print server or NULL on error.

66 *

67 * LOCKING: none.

68 */

69 char *

70 get_printserver(void)71 {

72  return(scan_configfile("printserver"));

73 }

74 /*

75 * Return the address of the network printer or NULL on error.

76 *

77 * LOCKING: none.

78 */

79 struct addrinfo *

80 get_printaddr(void)81 {

82   int       err;

83   char       *p;

84   struct addrinfo *ailist;

85   if ((p = scan_configfile("printer")) != NULL) {

86     if ((err = getaddrlist(p, "ipp", &ailist)) != 0) {

87        log_msg("no address information for %s", p);

88        return(NULL);

89     }

90     return(ailist);

91   }

92   log_msg("no printer address specified");

93   return(NULL);

94 }

[64~73] get_printserver 仅仅是一个简单的函数封装函数,它通过调用 scan_configfile找到运行打印假脱机守护进程的计算机系统名。

[74~94] 使用 get_printaddr 函数找到网络打印机的地址。除了通过配置文件中的打印机名找到相应的网络地址之外,该函数与前面的函数类似。

get_printserver和get_printaddr均调用scan_configfile。如果不能打开打印机配置文件,scan_configfile就调用log_sys打印出错消息并退出。尽管get_printserver由客户端命令调用,get_printaddr由守护进程程序调用,但两者均可调用 log_sys,因为通过设置一个全局变量可以安排日志函数将其打印到标准错误,而不是输出到日志文件。

95 /*

96 * "Timed" read - timout specifies the # of seconds to wait before

97 * giving up (5th argument to select controls how long to wait for

98 * data to be readable). Returns # of bytes read or -1 on error.

99 *

100 * LOCKING: none.

101 */

102 ssize_t

103 tread(int fd, void *buf, size_t nbytes, unsigned int timout)

104 {

105  int       nfds;

106  fd_set     readfds;

107  struct timeval tv;

108  tv.tv_sec = timout;

109  tv.tv_usec = 0;

110  FD_ZERO(&readfds);

111  FD_SET(fd, &readfds);

112  nfds = select(fd+1, &readfds, NULL, NULL, &tv);

113  if (nfds <= 0) {

114     if (nfds == 0)

115       errno = ETIME;

116     return(-1);

117 }

118 return(read(fd, buf, nbytes));

119 }

[95~107] tread的函数读取指定的字节数,在放弃以前至多阻塞timout秒。当我们从一个套接字或一个管道读数据时这个函数很有用。如果在指定的时间期限内没有接收数据,返回−1 并将 errno 设为 ETIME。如果在时间期限内有数据可用,返回最多nbytes字节的数据,但是如果数据没有及时到达,我们可以返回比要求的少的数据。我们用tread在打印假脱机守护进程上防止拒绝服务攻击。一个恶意用户可能重复尝试连接到守护进程而不发送数据,只是为了阻止其他用户提交打印作业。通过一个合理时间内放弃的方式,我们防止这种情况发生。其巧妙之处在于选择一个合理的超时值,当系统负载比较低和任务花费更长时间时,该值足够大能够防止过早夭折。如果我们选择的值太大,通过允许守护进程程序消耗太多资源去处理挂起请求,可能导致拒绝服务攻击。

[108~119] 使用 select 等待指定的文件描述符可读。如果在要读取的数据可用之前超时, select返回0,这种情况将errno设为ETIME。如果select失败或超时,返回−1;否则返回任何可用数据。

120 /*

121 * "Timed" read - timout specifies the number of seconds to wait

122 * per read call before giving up, but read exactly nbytes bytes.

123 * Returns number of bytes read or -1 on error.

124 *

125 * LOCKING: none.

126 */

127 ssize_t

128 treadn(int fd, void *buf, size_t nbytes, unsigned int timout)

129 {

130  size_t nleft;

131  ssize_t nread;

132  nleft = nbytes;

133  while (nleft > 0) {

134     if ((nread = tread(fd, buf, nleft, timout)) < 0) {

135       if (nleft == nbytes)

136         return(-1);  /* error, return -1 */

137       else

138         break;     /* error, return amount read so far */

139     } else if (nread == 0) {

140       break;       /* EOF */

141     }

142     nleft -= nread;

143     buf += nread;144 }

145  return(nbytes - nleft);   /* return >= 0 */

146 }

[120~146] 还提供了tread的变体treadn,它仅读取指定的字节数。这和14.7节中描述的readn类似,但是附加了一个超时参数。

为了正好读取nbytes字节,必须进行多次read调用。其困难之处在于尝试将单个超时值应用到多个read调用。这里不想用闹钟,因为在多线程应用中信号会变乱;也不能依赖系统根据select的返回更新timeval结构,以指示剩余的时间,因为许多平台不支持这个(14.5.1节)。因此,这种情况需要折中并定义一个超时值应用到单独的read调用。它限制循环中每次迭代的等待时间,而不是限制总的等待时间。

总等待的最大时间由nbytes×timout秒限定(最坏情况下,一次仅接收一个字节)。

用 nleft 记录要读取的剩余字节数。如果 tread 失败并在上一个迭代中已经接收到数据,则停止while循环并返回读取的字节数;否则返回−1。

接下来是用于提交打印作业的命令程序。C源代码文件是print.c。

1 /*

2 * The client command for printing documents. Opens the file

3 * and sends it to the printer spooling daemon. Usage:

4  *   print [-t] filename

5 */

6 #include "apue.h"

7 #include "print.h"

8 #include <fcntl.h>

9 #include <pwd.h>

10 /*

11 * Needed for logging funtions.

12 */

13 int log_to_stderr = 1;

14 void submit_file(int, int, const char *, size_t, int);

15 int

16 main(int argc, char *argv[])

17 {

18   int        fd, sockfd, err, text, c;

19   struct stat    sbuf;

20   char       *host;

21   struct addrinfo  *ailist, *aip;

22   err = 0;

23   text = 0;

24   while ((c = getopt(argc, argv, "t")) != -1) {

25     switch (c) {

26     case 't':

27        text = 1;

28        break;

29     case '?':

30        err = 1;

31        break;

32    }

33  }

[1~14] 需要定义一个log_to_stderr整数,通过这个整数能够使用库中的日志函数。如果该整数设为非 0 值,错误消息将被送到一个标准错误流而非日志文件中。尽管在print.c中没有使用任何日志函数,但将util.o链接到print.o构建了一个可执行的print命令,并且util.c包含用于用户命令行程序和守护进程的函数。

[15~33] 支持一个选项,即-t,强行使文件按照文本格式打印(而不是其他格式,如PostScript格式)。使用getopt函数来处理命令选项。

34  if (err || (optind != argc - 1))

35     err_quit("usage: print [-t] filename");

36  if ((fd = open(argv[optind], O_RDONLY)) < 0)

37     err_sys("print: can't open %s", argv[optind]);

38  if (fstat(fd, &sbuf) < 0)

39     err_sys("print: can't stat %s", argv[optind]);

40  if (!S_ISREG(sbuf.st_mode))

41     err_quit("print: %s must be a regular file\n", argv[optind]);

42  /*

44   */

43 * Get the hostname of the host acting as the print server.

45 if ((host = get_printserver()) == NULL)

46    err_quit("print: no print server defined");

47 if ((err = getaddrlist(host, "print", &ailist)) != 0)

48     err_quit("print: getaddrinfo error: %s", gai_strerror(err));

49 for (aip = ailist; aip != NULL; aip = aip->ai_next) {

50     if ((sfd = connect_retry(AF_INET, SOCK_STREAM, 0,

51      aip->ai_addr, aip->ai_addrlen)) < 0) {

52         err = errno;

[34~41] 当getopt 处理完命令选项,将变量optind 设为指向第一个非选项参数的下标。

如果这是一个值而非最后一个参数的下标,那么说明它是错误的参数个数(只支持一个非选项参数)。错误处理包括:检查是否能够打开要打印的文件;检查是否是一个常规文件(而不是一个目录或者其他类型的文件)。

[42~48] 通过调用util.c中的get_printserver函数取得打印假脱机守护进程名,并且调用getaddrlist(也在util.c中)将主机名转换成一个网络地址。

注意,指定服务名为“print”。在系统上安装打印假脱机守护进程时,需要确保/etc/services(或等价的数据库)有打印机服务的条目。当为守护进程选择一个端口时,最好选择特权端口,以防止恶意用户程序假装成一个打印假脱机守护进程,而实际上是要偷取打印文件的副本。这意味着端口号应小于1 024(回忆16.3.4节),并且守护进程运行时必须具有超级用户特权以便能够绑定一个保留端口。

[49~52] 使用getaddrinfo返回的地址列表来尝试连接到守护进程,然后使用能够连接的第一个地址发送文件到守护进程。

53    } else {

54      submit_file(fd, sfd, argv[optind], sbuf.st_size, text);

55      exit(0);

56     }

57  }

58  err_exit(err, "print: can’t contact %s", host);

59 }

60 /*

61 * Send a file to the printer daemon.

62 */

63 void

64 submit_file(int fd, int sockfd, const char *fname, size_t nbytes,

65        int text)

66 {

67  int        nr, nw, len;

68  struct passwd   *pwd;

69  struct printreq  req;

70  struct printresp  res;

71  char        buf[IOBUFSZ];

72 /*

73   * First build the header.

74   */

75  if ((pwd = getpwuid(geteuid())) == NULL) {

76     strcpy(req.usernm, "unknown");

77  } else {

78     strncpy(req.usernm, pwd->pw_name, USERNM_MAX-1);

79     req.usernm[USERNM_MAX-1] = ’\0’;

80  }

[53~59] 如果能够连接到打印假脱机守护进程,则调用submit_file将要打印的文件传送到守护进程,然后用返回值0表示成功后退出。如果不能连接到任何地址,那么就调用err_exit来打印错误消息并且返回1表示失败后退出(附录B包含了err_exit的源代码和其他错误例程)。

[60~80] submit_file发送打印机请求到守护进程并读取响应消息。首先,建立printreq请求头。使用geteuid来获得调用者的有效用户ID并将其传给getpwuid以便查找在系统口令文件中的用户。将该用户名复制到请求头。如果不能识别用户,在请求首部中使用字符串"unknown"。从口令文件中复制用户名时,为避免写超出请求首部的用户名缓冲区,可以使用strncpy。如果用户名比缓冲区长,strncpy不会在缓冲区中存储终止null字节,因此我们需要自己来做。

81  req.size = htonl(nbytes);

82  if (text)

83     req.flags = htonl(PR_TEXT);

84  else

85     req.flags = 0;

86  if ((len = strlen(fname)) >= JOBNM_MAX) {

87     /*

88     * Truncate the filename (+-5 accounts for the leading

89     * four characters and the terminating null).

90     */

91     strcpy(req.jobnm, "... ");

92     strncat(req.jobnm, &fname[len-JOBNM_MAX+5], JOBNM_MAX-5);

93  } else {

94     strcpy(req.jobnm, fname);

95  }

96  /*

97   * Send the header to the server.

98   */

99  nw = writen(sockfd, &req, sizeof(struct printreq));

100  if (nw != sizeof(struct printreq)) {

101    if (nw < 0)

102      err_sys("can’t write to print server");

103    else

104      err_quit("short write (%d/%d) to print server",

105       nw, sizeof(struct printreq));

106 }

[81~95] 将要打印的文件转成网络字节序后,将其文件长度保存在请求首部。如果文件按纯文本格式打印,在请求首部保存PR_TEXT标志。通过将这些整数转化成网络字节序,可以在打印假脱机守护进程在其他计算机系统运行的同时在客户端系统上运行print命令。那么,即便这些系统使用不同字节序的处理器,这些命令仍可运行(在16.3.1节讨论过字节序)。

将作业名设为要打印的文件名。如果作业名的长度超出了报文所能容纳的作业名字段长度,那么仅复制可容纳的作业名的最后部分。这样就有效地将作业名的开头部分截去,并代入省略符,以表示该字段还有更多的字符。

[96~106] 然后使用writen将请求头发送到守护进程(回忆一下我们曾在图14-24中介绍过的writen函数)。writen函数使用多个write调用来传输指定数量的数据。如果写入失败或者传输少于期望的数据,将打印错误消息然后退出。

107   /*

108   * Now send the file.

109   */

110  while ((nr = read(fd, buf, IOBUFSZ)) != 0) {

111     nw = writen(sockfd, buf, nr);

112     if (nw != nr) {

113       if (nw < 0)

114         err_sys("can’t write to print server");

115       else

116         err_quit("short write (%d/%d) to print server",

117          nw, nr);

118    }

119  }

120  /*

121   * Read the response.

122   */

123  if ((nr = readn(sockfd, &res, sizeof(struct printresp))) !=

124   sizeof(struct printresp))

125     err_sys("can’t read response from server");

126  if (res.retcode != 0) {

127     printf("rejected: %s\n", res.msg);

128     exit(1);

129  } else {

130     printf("job ID %ld\n", (long)ntohl(res.jobid));

131  }

132 }

[107~119] 将首部发送到守护进程后,发送要打印的文件。同时读取文件的 IOBUFSZ 字节并用writen发送数据到守护进程。如果写失败或者写少了,那么就打印错误信息并退出。

[120~132] 把要打印的文件发送给守护进程后,读取守护进程的响应数据。如果请求失败,返回码(retcode)为非零值,并且将响应中的本文形式的错误信息打印出来。如果请求成功,将打印作业ID,用户此后可以使用此ID 引用该请求。(我们将写一个命令取消一个挂起的打印请求留作练习;作业ID可以用于取消作业请求,其作用是从打印队列中识别要删除的作业,参见习题21.5)。当 submin_file返回到main函数时,退出,表明请求成功。

注意,一个成功的守护进程响应并不意味着打印机可以打印该文件,仅仅意味着守护进程成功地将其加入到打印作业队列。

现在print命令已经完全了解过了。我们要看的最后一个C源代码文件是打印假脱机守护进程。

1 /*

2 * Print server daemon.

3 */

4 #include "apue.h"

5 #include <fcntl.h>

6 #include <dirent.h>

7 #include <ctype.h>

8 #include <pwd.h>

9 #include <pthread.h>

10 #include <strings.h>

11 #include <sys/select.h>

12 #include <sys/uio.h>

13 #include "print.h"

14 #include "ipp.h"

15 /*

16 * These are for the HTTP response from the printer.

17 */

18 #define HTTP_INFO(x) ((x) >= 100 && (x) <= 199)

19 #define HTTP_SUCCESS(x) ((x) >= 200 && (x) <= 299)

20 /*

21 * Describes a print job.

22 */

23 struct job {

24   struct job    *next;   /* next in list */

25   struct job    *prev;   /* previous in list */

26   long       jobid;  /* job ID */

27   struct printreq  req;   /* copy of print request */

28 };

29 /*

30 * Describes a thread processing a client request.

31 */

32 struct worker_thread {

33   struct worker_thread *next;  /* next in list */

34   struct worker_thread *prev;  /* previous in list */

35   pthread_t       tid;   /* thread ID */

36   int          sockfd;  /* socket */

37 };

[1~19] 打印假脱机守护进程包括前面看到的IPP头文件,因为守护进程需要用这个协议与打印机通信。HTTP_INFO和HTTP_SUCCESS宏定义了HTTP请求的状态(IPP建立在HTTP之上)。RFC 2616第10节定义了HTTP状态码。

[20~37] 假脱机守护进程使用job和worker_thread结构来跟踪相应的打印作业和接受打印请求的线程。

38 /*

39 * Needed for logging.

40 */

41 int log_to_stderr = 0;

42 /*

43 * Printer-related stuff.

44 */

45 struct addrinfo    *printer;

46 char         *printer_name;

47 pthread_mutex_t    configlock = PTHREAD_MUTEX_INITIALIZER;

48 int          reread;

49 /*

50 * Thread-related stuff.

51 */

52 struct worker_thread *workers;

53 pthread_mutex_t    workerlock = PTHREAD_MUTEX_INITIALIZER;

54 sigset_t        mask;

55 /*

56 * Job-related stuff.

57 */

58 struct job       *jobhead, *jobtail;

59 int           jobfd;

[38~41] 日志函数需要定义log_to_stderr变量,并且将其设为0,将日志消息发送到系统日志而不是标准错误。在 print.c 中,即使在用户命令中不使用日志,也定义log_to_stderr 并将其设置为 1。如果将实用工具函数拆分为两个独立的文件:一个用于服务器,另一个用于客户端命令,则可以避免这种情况。

[42~48] 使用全局指针变量printer来保存打印机的网络地址。在printer_name中保存打印机的主机名。configlock 用于防止访问 reread 变量,该变量用来表示守护进程需要再次读取配置文件,原因可能是管理员改变了打印机网络地址。

[49~54] 接着,定义与线程相关的变量。使用workers作为双向链表的头部,该表用于接收来自客户端的文件。采用workerlock互斥量来保护该表。变量mask用于线程的信号掩码。[55~59] 对于挂起作业的链表,定义jobhead为表头,jobtail为表尾。该表也是双向链表,但是需要将作业加入到表尾,所以需要一个指针来记住表尾。至于表中工作者线程的顺序是无关紧要的。因此可以将它们加入到表头而不需要记住尾指针。jobfd是作业文件的文件描述符。

60 int32_t        nextjob;

61 pthread_mutex_t   joblock = PTHREAD_MUTEX_INITIALIZER;

62 pthread_cond_t    jobwait = PTHREAD_COND_INITIALIZER;

63 /*

64 * Function prototypes.

65 */

66 void   init_request(void);

67 void   init_printer(void);

68 void   update_jobno(void);

69 int32_t get_newjobno(void);

70 void   add_job(struct printreq *, int32_t);

71 void   replace_job(struct job *);

72 void   remove_job(struct job *);

73 void   build_qonstart(void);

74 void     *client_thread(void *);

75 void     *printer_thread(void *);

76 void     *signal_thread(void *);

77 ssize_t   readmore(int, char **, int, int *);

78 int     printer_status(int, struct job *);

79 void     add_worker(pthread_t, int);

80 void     kill_workers(void);

81 void     client_cleanup(void *);

82 /*

83 * Main print server thread. Accepts connect requests from

84 * clients and spawns additional threads to service requests.

85 *

86 * LOCKING: none.

87 */

88 int

89 main(int argc, char *argv[])

90 {

91  pthread_t      tid;

92  struct addrinfo  *ailist, *aip;

93  int         sockfd, err, i, n, maxfd;

94  char        *host;

95  fd_set       rendezvous, rset;

96  struct sigaction  sa;

97  struct passwd    *pwdp;

[60~62] nextjob是接收的下一个打印作业的ID。互斥量 joblock保护作业表,同时还有jobwait代表的条件变量。

[63~81] 声明此文件中所有余下的函数的原型。提前做好这些工作可以使得在文件中放置函数时不用担心函数调用的顺序。

[82~97] 打印假脱机守护进程的 main 函数执行两个任务:初始化守护进程然后处理来自客户端的连接请求。

98  if (argc != 1)

99     err_quit("usage: printd");

100  daemonize("printd");

101  sigemptyset(&sa.sa_mask);

102  sa.sa_flags = 0;

103  sa.sa_handler = SIG_IGN;

104  if (sigaction(SIGPIPE, &sa, NULL) < 0)

105     log_sys("sigaction failed");

106  sigemptyset(&mask);

107  sigaddset(&mask, SIGHUP);

108  sigaddset(&mask, SIGTERM);

109  if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)

110     log_sys("pthread_sigmask failed");

111  n = sysconf(_SC_HOST_NAME_MAX);

112  if (n < 0) /* best guess */

113     n = HOST_NAME_MAX;

114  if ((host = malloc(n)) == NULL)

115     log_sys("malloc error");

116  if (gethostname(host, n) < 0)

117     log_sys("gethostname error");

118   if ((err = getaddrlist(host, "print", &ailist)) != 0) {

119     log_quit("getaddrinfo error: %s", gai_strerror(err));

120     exit(1);

121   }

[98~100] 守护进程没有任何选项(唯一的参数是命令名自身),所以如果 argc 不为 1,调用err_quit打印错误信息然后退出。调用图13-1所示程序中的daemonize函数成为一个守护进程。在此之后,不能在标准错误上打印错误消息,而是对其记录日志。

[101~110] 忽略 SIGPIPE。接下来将要写套接字文件描述符,并且不想让写错误触发SIGPIPE,因为其默认动作是杀死进程。下一步,设置线程信号掩码,包括SIGHUP和 SIGTERM。创建的所有进程均继承这个信号掩码。使用 SIGHUP 信号告诉守护进程再次读取配置文件,SIGTERM 信号告诉守护进程执行清理工作并优雅地退出。

[111~117] 调用sysconf来获取主机名的最大长度。如果sysconf失败或者没有定义该限制,采用 HOST_NAME_MAX 作为最佳选择。有时,平台已经定义了此常量,但如果没有定义,则在print.h中选择属于自己的值。分配内存来保存主机名并调用gethostname来获取。

[118~121] 接下来,尝试找到用于守护进程提供打印假脱机服务的网络地址。

122   FD_ZERO(&rendezvous);

123   maxfd = -1;

124   for (aip = ailist; aip != NULL; aip = aip->ai_next) {

125     if ((sockfd = initserver(SOCK_STREAM, aip->ai_addr,

126      aip->ai_addrlen, QLEN)) >= 0) {

127       FD_SET(sockfd, &rendezvous);

128       if (sockfd > maxfd)

129         maxfd = sockfd;

130     }

131   }

132   if (maxfd == -1)

133     log_quit("service not enabled");

134   pwdp = getpwnam(LPNAME);

135   if (pwdp == NULL)

136     log_sys("can’t find user %s", LPNAME);

137   if (pwdp->pw_uid == 0)

138     log_quit("user %s is privileged", LPNAME);

139   if (setgid(pwdp->pw_gid) < 0 || setuid(pwdp->pw_uid) < 0)

140     log_sys("can’t change IDs to user %s", LPNAME);

141   init_request();

142   init_printer();

[122~131] 清零rendezvous变量,该变量将与select一起用来等待客户端连接请求。将最大文件描述符初始化为−1,以确保所分配的第一个文件描述符会大于 maxfd。

对于每个需要提供服务的网络地址,调用initserver(见图16-22)来分配和初始化一个套接字。如果initserver成功,将其文件描述符加入fd_set;如果该描述符大于现有最大值maxfd,将maxfd设为该描述符值。

[132~133] 走完整个addrinfo结构列表后,如果maxfd仍为−1,不能启动打印假脱机服务,记录日志然后退出。

[134~140] 守护进程需要超级用户特权来绑定一个套接字到保留端口。完成绑定后,通过将用户ID改变为lp的用户ID(回忆21.4节的安全方面的讨论)降低该程序特权。这里想遵循最小特权原则,以避免在守护进程中将系统暴露给任何可能的攻击。调用getpwnam来找到与用户lp相关的口令条目。如果没有此用户,或者lp具有超级用户特权,记录日志然后退出。否则,调用setuid将实际用户ID和有效用户ID改为lp用户ID。为了避免暴露系统,如果不能减少特权,那么就选择不提供任何服务。

[141~142] 调用init_request来初始化作业请求并确保只有一个守护进程副本正在运行。调用init_printer初始化打印机信息(稍后就可以看到这两个函数)。

143   err = pthread_create(&tid, NULL, printer_thread, NULL);

144   if (err == 0)

145     err = pthread_create(&tid, NULL, signal_thread, NULL);

146   if (err != 0)

147     log_exit(err, "can’t create thread");

148   build_qonstart();

149   log_msg("daemon initialized");

150   for (;;) {

151     rset = rendezvous;

152     if (select(maxfd+1, &rset, NULL, NULL, NULL) < 0)

153       log_sys("select failed");

154     for (i = 0; i <= maxfd; i++) {

155       if (FD_ISSET(i, &rset)) {

156         /*

157          * Accept the connection and handle the request.

158          */

159         if ((sockfd = accept(i, NULL, NULL)) < 0)

160           log_ret("accept failed");

161         pthread_create(&tid, NULL, client_thread,

162          (void *)((long)sockfd));

163       }

164     }

165   }

166   exit(1);

167 }

[143~149] 创建一个处理信号的线程和一个与打印机通信的线程。(通过限制打印机只与一个线程通信,可以简化与打印机相关的数据结构的锁定。)然后调用build_qonstart在/var/spool/printer目录中搜索任何挂起的作业。对于找到的每个作业,将建立一个结构,让打印机线程将该作业的文件送到打印机。至此,完成守护进程的设置,因此记录一条日志消息,表明守护进程初始化成功完成。

[150~167] 将rendezvous fd_set结构复制到rset,然后调用select等待其中的一个文件描述符变为可读。必须复制rendezvous,因为select会修改传入的fd_set结构来包含满足事件的文件描述符。既然服务器已经将套接字初始化完毕,一个可读的文件描述符就意味着一个连接请求需要处理。当select返回时,检查rset来获取一个可读的文件描述符。如果找到一个,调用accept接受该请求。如果失败,记录日志然后继续检查更多的可读文件描述符。否则,创建一个线程来处理客户端请求。主线程main一直循环,将请求发送到其他线程处理,永远不应到达exit语句。

168 /*

169 * Initialize the job ID file. Use a record lock to prevent

170 * more than one printer daemon from running at a time.

171 *

172 * LOCKING: none, except for record-lock on job ID file.

173 */

174 void

175 init_request(void)176 {

177  int     n;

178  char    name[FILENMSZ];

179 sprintf(name, "%s/%s", SPOOLDIR, JOBFILE);

180 jobfd = open(name, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);

181 if (write_lock(jobfd, 0, SEEK_SET, 0) < 0)

182    log_quit("daemon already running");

183 /*

184 * Reuse the name buffer for the job counter.

185 */

186 if ((n = read(jobfd, name, FILENMSZ)) < 0)

187    log_sys("can’t read job file");

188 if (n == 0)

189    nextjob = 1;

190 else

191    nextjob = atol(name);

192 }

[168~182] 函数 init_request做两件事:在作业文件/var/spool/printer/jobno上放一个记录锁,然后读该文件并确定下一个要赋值的作业编号。在整个文件上放置一把写锁,表明守护进程正在运行。如果当前已有一个守护进程正在运行,想启动另外一个打印假脱机守护进程副本,该程序将无法获得写锁,然后就退出。因此,同时只能有一个守护进程在运行。(图13-6中使用过这种技术,在14.3节中讨论过write_lock宏。)

[183~192] 作业文件包含一个ASCII码的整数字符串来表示下一个作业编号。如果文件刚创建并且为空,那么将nextjob设置为1。否则,使用atol将字符串转换为整数并将其作为下一个作业编号。让 jobfd 对于作业文件保持打开状态,因此当作业创建时能够更新作业编号。不能关闭该文件,因为这将释放已经放置在上面的写锁。在一个长整型数长度为64位的系统上,至少需要一个21字节的缓冲区来存放代表最大长整型数的字符串。这里重用文件名缓冲区,因为在print.h 中FILENMSZ定义为64。

193 /*

194 * Initialize printer information from configuration file.

195 *

196 * LOCKING: none.

197 */

198 void

199 init_printer(void)

200 {

201  printer = get_printaddr();

202  if (printer == NULL)

203     exit(1); /* message already logged */

204  printer_name = printer->ai_canonname;

205  if (printer_name == NULL)

206    printer_name = "printer";

207  log_msg("printer is %s", printer_name);

208 }

209 /*

210 * Update the job ID file with the next job number.

211 * Doesn’t handle wrap-around of job number.

212 *

213 * LOCKING: none.

214 */

215 void

216 update_jobno(void)217 {

218  char buf[32];

219 if (lseek(jobfd, 0, SEEK_SET) == -1)

220 log_sys("can’t seek in job file");

221 sprintf(buf, "%d", nextjob);

222 if (write(jobfd, buf, strlen(buf)) < 0)

223    log_sys("can’t update job file");

224 }

[193~208] init_printer用于设置打印机名和地址。调用get_printaddr(来自util.c)获得打印机地址。如果失败,记录日志并退出。当找不到打印机地址时, get_printaddr 会记录自己的错误信息日志。如果打印机地址未找到,将addrinfo 中的ai_canonname 设为打印机名。如果该字段为空,将打印机名设为默认值。注意,将正在使用的打印机名也记录在日志中,以帮助管理员能够诊断假脱机系统的问题。

[209~224] update_jobno函数用于在作业文件/var/spool/printer/jobno中写入下一个作业编号。首先,找到文件开头。然后,将整数作业编号转换为一个字符串并写入文件。如果写入失败,记录日志并退出。作业编号自动递增。如何处理回绕的作业编号留作一个练习(见习题21.9)。

225 /*

226 * Get the next job number.

227 *

228 * LOCKING: acquires and releases joblock.

229 */

230 int32_t

231 get_newjobno(void)

232 {

233  int32_t jobid;

234  pthread_mutex_lock(&joblock);

235  jobid = nextjob++;

236  if (nextjob <= 0)

237     nextjob = 1;

238  pthread_mutex_unlock(&joblock);

239  return(jobid);

240 }

241 /*

242 * Add a new job to the list of pending jobs. Then signal

243 * the printer thread that a job is pending.

244 *

245 * LOCKING: acquires and releases joblock.

246 */

247 void

248 add_job(struct printreq *reqp, int32_t jobid)

249 {

250 struct job *jp;

251  if ((jp = malloc(sizeof(struct job))) == NULL)

252     log_sys("malloc failed");

253  memcpy(&jp->req, reqp, sizeof(struct printreq));

[225~240] get_newjobno函数用于获得下一个作业编号。首先将joblock互斥量锁住。递增nextjob变量,并处理回绕的情况。然后解锁互斥量并返回递增前的nextjob值。多个线程可以同时调用 get_newjobno;需要串行化访问下一个作业编号,因此每个线程得到一个唯一的作业编号。(见图 11-9,考察在这种情况下,如果不串行化线程会发生什么情况。)

[241~253] add_job 函数用于在挂起的打印作业列表中增加一个新的作业请求。首先为 job结构分配空间。如果失败,记录日志并退出。此时,打印请求已经安全地存储在磁盘上;当打印假脱机守护进程重启时,会重新读取这些请求。当为新作业分配完空间,将客户端的请求结构复制到作业结构。在print.h中一个job结构包含一对列表指针,一个作业ID和一个从客户端print命令发送过来的printreq结构副本。

254 jp->jobid = jobid;

255 jp->next = NULL;

256 pthread_mutex_lock(&joblock);

257 jp->prev = jobtail;

258 if (jobtail == NULL)

259    jobhead = jp;

260 else

261    jobtail->next = jp;

262 jobtail = jp;

263 pthread_mutex_unlock(&joblock);

264 pthread_cond_signal(&jobwait);

265 }

266 /*

267 * Replace a job back on the head of the list.

268 *

269 * LOCKING: acquires and releases joblock.

270 */

271 void

272 replace_job(struct job *jp)

273 {

274  pthread_mutex_lock(&joblock);

275  jp->prev = NULL;

276  jp->next = jobhead;

277  if (jobhead == NULL)

278     jobtail = jp;

279  else

280     jobhead->prev = jp;

281  jobhead = jp;

282  pthread_mutex_unlock(&joblock);

283 }

[254~265] 保存作业ID并锁住joblock互斥量以获得对打印作业链表的独占访问。将在该链表尾增加新的作业结构。将新的作业结构的前项指针(previous pointer)指向链表中最后一个作业。如果链表为空,将jobhead指向新的结构。否则,将链表中最后一项的后项指针(next pointer)指向新的结构。然后设置jobtail指向新的结构。对互斥量解锁,然后给打印机线程发信号,告诉该线程另一个作业可用了。

[266~283] 函数replace_job用于将作业插入到挂起作业队列头部。需要获得joblock互斥量,将job结构中的前项指针设为NULL,将后项指针指向表头。如果表为空,将jobtail指向插入的job结构。否则,将表中第一个作业结构的前项指针指向插入的job结构。然后将jobhead指向插入的job结构,成为新的表头。最后,释放joblock互斥量。

284 /*

285 * Remove a job from the list of pending jobs.

286 *

287 * LOCKING: caller must hold joblock.

288 */

289 void

290 remove_job(struct job *target)

291 {

292  if (target->next != NULL)

293     target->next->prev = target->prev;

294  else

295     jobtail = target->prev;

296  if (target->prev != NULL)

297     target->prev->next = target->next;

298  else

299     jobhead = target->next;

300 }

301 /*

302 * Check the spool directory for pending jobs on start-up.

303 *

304 * LOCKING: none.

305 */

306 void

307 build_qonstart(void)

308 {

309  int           fd, err, nr;

310  int32_t         jobid;

311  DIR           *dirp;

312  struct dirent      *entp;

313  struct printreq     req;

314  char          dname[FILENMSZ], fname[FILENMSZ];

315  sprintf(dname, "%s/%s", SPOOLDIR, REQDIR);

316  if ((dirp = opendir(dname)) == NULL)

317     return;

[284~300] remove_job 将给定的作业从挂起的作业列表中删除。调用者必须已经持有joblock 互斥量。如果后项指针不为空,将下一个条目的前项指针指向被删除目标的前项指针所指向的条目。否则,该条目为列表中最后一个,因此将 jobtail指向被删除目标的前项指针所指向的条目。如果被删除目标的前项指针不为空,将前一个条目的后项指针指向被删除目标的后项指针所指向的条目。否则,这个是表中第一个条目,因此将jobhead指向被删除目标后面的那个条目。

[301~317] 当守护进程启动时,调用build_qonstart从存储在/var/spool/printer/reqs中的磁盘文件建立一个内存中的打印作业列表。如果不能打开该目录,表示没有打印作业要处理,因此就返回。

318   while ((entp = readdir(dirp)) != NULL) {

319     /*

320     * Skip "." and ".."

321     */

322     if (strcmp(entp->d_name, ".") == 0 ||

323      strcmp(entp->d_name, "..") == 0)

324       continue;

325     /*

326     * Read the request structure.

327     */

328     sprintf(fname, "%s/%s/%s", SPOOLDIR, REQDIR, entp->d_name);

329     if ((fd = open(fname, O_RDONLY)) < 0)

330       continue;

331     nr = read(fd, &req, sizeof(struct printreq));

332     if (nr != sizeof(struct printreq)) {

333       if (nr < 0)

334         err = errno;

335       else

336         err = EIO;

337       close(fd);

338       log_msg("build_qonstart: can’t read %s: %s",

339        fname, strerror(err));

340       unlink(fname);

341       sprintf(fname, "%s/%s/%s", SPOOLDIR, DATADIR,

342        entp->d_name);

343       unlink(fname);

344       continue;

345     }

346     jobid = atol(entp->d_name);

347     log_msg("adding job %d to queue", jobid);

348     add_job(&req, jobid);

349   }

350   closedir(dirp);

351 }

[318~324] 在目录中一次读取一个条目,忽略.和..。

[325~345] 对于每个条目,创建一个文件完全路径名并只读打开。如果open调用失败,跳过该文件。否则,将读取保存在文件中的printreq结构。如果不能读取整个结构,关闭该文件,记录日志并unlink该文件。然后建立相应数据文件的完全路径名,再unlink该文件。

[346~351] 如果能够读取一个完整的printreq结构,将文件名转换为作业ID(文件名就是其作业ID),记录日志,然后将请求加入到挂起的打印作业列表。当读完整个目录, readdir返回NULL,关闭目录然后返回。

352  /*

353  * Accept a print job from a client.

354  *

355  * LOCKING: none.

356  */

357  void *

358  client_thread(void *arg)

359  {

360   int          n, fd, sockfd, nr, nw, first;

361   int32_t        jobid;

362   pthread_t       tid;

363   struct printreq    req;

364   struct printresp   res;

365   char          name[FILENMSZ];

366   char          buf[IOBUFSZ];

367   tid = pthread_self();

368   pthread_cleanup_push(client_cleanup, (void *)((long)tid));

369   sockfd = (long)arg;

370   add_worker(tid, sockfd);

371   /*

372    * Read the request header.

373    */

374   if ((n = treadn(sockfd, &req, sizeof(struct printreq), 10)) !=

375    sizeof(struct printreq)) {

376     res.jobid = 0;

377     if (n < 0)

378       res.retcode = htonl(errno);

379     else

380       res.retcode = htonl(EIO);

381     strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);

382     writen(sockfd, &res, sizeof(struct printresp));

383     pthread_exit((void *)1);

384   }

[352~370] 当连接请求被接受时,main 中派生出client_thread。其作用是从客户端print命令中接收要打印的文件。为每个客户端打印请求分别创建一个独立的线程。

首先是安装线程清理处理程序(见11.5节中线程清理处理程序的讨论)。清理处理程序是client_cleanup,将在后面用到。它仅带一个参数:线程ID。然后调用add_worker来创建一个worker_thread结构并将其加入到活跃的客户端线程列表中。

[371~384] 此时,完成了线程的初始化任务,因此从客户端读取请求头。如果客户端发送的数据少于期望或遇到错误,则响应一个消息,该消息指出错误的原因,然后调用pthread_exit结束线程。

385  req.size = ntohl(req.size);

386  req.flags = ntohl(req.flags);

387  /*

388   * Create the data file.

389   */

390  jobid = get_newjobno();

391  sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid);

392  fd = creat(name, FILEPERM);

393  if (fd < 0) {

394     res.jobid = 0;

395     res.retcode = htonl(errno);

396     log_msg("client_thread: can't create %s: %s", name,

397      strerror(res.retcode));

398     strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);

399     writen(sockfd, &res, sizeof(struct printresp));

400     pthread_exit((void *)1);

401  }

402  /*

403   * Read the file and store it in the spool directory.

404   * Try to figure out if the file is a PostScript file

405   * or a plain text file.

406   */

407  first = 1;

408  while ((nr = tread(sockfd, buf, IOBUFSZ, 20)) > 0) {

409     if (first) {

410       first = 0;

411       if (strncmp(buf, "%!PS", 4) != 0)

412         req.flags |= PR_TEXT;

413     }

[385~401] 将请求头中的整数字段转换成主机字节序,调用get_newjobno来保存这个打印请求的下一个作业编号。建立作业数据文件,名为/var/spool/printer/data/jobid,jobid是请求的作业ID。采用权限许可来防止其他人读取这些文件(print.h中定义FILEPERM为S_IRUSR|S_IWUSR)。如果不能创建该文件,记录错误日志,发送失败响应给客户端,调用pthread_exit结束线程。

[402~413] 读取来自客户端的文件内容,要将其写入数据文件的私有副本中。但是在写任何东西之前,需要在第一次循环时检查一下是否是PostScript文件。如果该文件不是以%!PS模式开头,可以假定为其为纯文本文件,这种情况下在请求头中设置PR_TEXT标志。(如果在print命令中有-t标志,那么客户端也会设置此标志。)尽管PostScript程序不要求以模式%!PS开始,但文档格式指南(Adobe Systems [1999])强烈推荐这种方式。

414     nw = write(fd, buf, nr);

415     if (nw != nr) {

416       res.jobid = 0;

417       if (nw < 0)

418         res.retcode = htonl(errno);

419       else

420         res.retcode = htonl(EIO);

421       log_msg("client_thread: can’t write %s: %s", name,

422        strerror(res.retcode));

423       close(fd);

424       strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);

425       writen(sockfd, &res, sizeof(struct printresp));

426       unlink(name);

427       pthread_exit((void *)1);

428     }

429  }

430  close(fd);

431  /*

432   * Create the control file. Then write the

433   * print request information to the control

434   * file.

435   */

436  sprintf(name, "%s/%s/%d", SPOOLDIR, REQDIR, jobid);

437  fd = creat(name, FILEPERM);

438  if (fd < 0) {

439     res.jobid = 0;

440     res.retcode = htonl(errno);

441     log_msg("client_thread: can’t create %s: %s", name,

442     strerror(res.retcode));

443     strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);

444     writen(sockfd, &res, sizeof(struct printresp));

445     sprintf(name, "%s/%s/%d", SPOOLDIR, DATADIR, jobid);

446     unlink(name);

447     pthread_exit((void *)1);

448  }

[414~430] 将来自客户端的数据写入到数据文件。如果 write 失败,记录错误日志,关闭数据文件的文件描述符,发送出错消息给客户端,删除数据文件,调用 pthread_exit退出。注意,不需要显式关闭套接字文件描述符。当调用pthread_exit时,线程清理处理程序会处理这些事情。

当接收到所有要打印的数据,关闭数据文件的文件描述符。

[431~448] 接下来,创建文件/var/spool/printer/reqs/jobid以记住打印请求。如果失败,记录错误日志,发送出错响应给客户端,删除数据文件,终止线程。

449  nw = write(fd, &req, sizeof(struct printreq));

450  if (nw != sizeof(struct printreq)) {

451    res.jobid = 0;

452    if (nw < 0)

453       res.retcode = htonl(errno);

454     else

455       res.retcode = htonl(EIO);

456     log_msg("client_thread: can’t write %s: %s", name,

457      strerror(res.retcode));

458     close(fd);

459     strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);

460     writen(sockfd, &res, sizeof(struct printresp));

461     unlink(name);

462     sprintf(name, "%s/%s/%d", SPOOLDIR, DATADIR, jobid);

463     unlink(name);

464     pthread_exit((void *)1);

465  }

466  close(fd);

467  /*

468   * Send response to client.

469   */

470  res.retcode = 0;

471  res.jobid = htonl(jobid);

472  sprintf(res.msg, "request ID %d", jobid);

473  writen(sockfd, &res, sizeof(struct printresp));

474  /*

475   * Notify the printer thread, clean up, and exit.

476   */

477  log_msg("adding job %d to queue", jobid);

478  add_job(&req, jobid);

479  pthread_cleanup_pop(1);

480  return((void *)0);

481 }

[449~465] 将printreq结构写入控制文件。如果出错,则记录日志,关闭控制文件描述符,发送失败响应给客户端,删除数据和控制文件,终止线程。

[466~473] 关闭控制文件的文件描述符,并发送消息给客户端,该消息包括作业ID和成功状态(retcode设为0)。

[474~481] 调用add_job将接收的文件加入到挂起作业列表中,调用pthread_cleanup_pop完成清理过程。当返回时线程终止。注意,线程退出之前,必须关闭不再使用的任何文件描述符。与线程终止不同,当一个线程退出并且进程中仍有其他线程时,文件描述符不会自动关闭。如果不关闭不需要的文件描述符,终将耗尽资源。

482  /*

483  * Add a worker to the list of worker threads.

484  *

485  * LOCKING: acquires and releases workerlock.

486  */

487  void

488  add_worker(pthread_t tid, int sockfd)

489 {

490   struct worker_thread  *wtp;

491   if ((wtp = malloc(sizeof(struct worker_thread))) == NULL) {

492     log_ret("add_worker: can't malloc");

493     pthread_exit((void *)1);

494   }

495   wtp->tid = tid;

496   wtp->sockfd = sockfd;

497   pthread_mutex_lock(&workerlock);

498   wtp->prev = NULL;

599   wtp->next = workers;

500   if (workers != NULL)

501     workers ->prev = wtp;

503   workers = wtp;

504   pthread_mutex_unlock(&workerlock);

502

505 }

506  /*

507  * Cancel (kill) all outstanding workers.

508  *

509  * LOCKING: acquires and releases workerlock.

510  */

511 void

512 kill_workers(void)

513 {

514   struct worker_thread  *wtp;

515   pthread_mutex_lock(&workerlock);

516   for (wtp = workers; wtp != NULL; wtp = wtp->next)

517     pthread_cancel(wtp->tid);

518   pthread_mutex_unlock(&workerlock);

519 }

[482~505] add_worker 将一个 worker_thread 结构加入活动线程列表中。分配该结构需要的内存,初始化它,锁住 workerlock 互斥量,将结构加入到列表的头部,然后解锁互斥量。

[506~519] kill_workers 函数遍历工作者线程列表,然后一一删除。遍历列表时持有workerlock互斥量。注意,pthread_cancel仅仅将线程列入删除计划,实际的删除动作在每个线程到达下一个删除点时发生。

520  /*

521  * Cancellation routine for the worker thread.

522  *

523  * LOCKING: acquires and releases workerlock.

524  */

525  void

526  client_cleanup(void *arg)

527  {

528   struct worker_thread  *wtp;

529   pthread_t        tid;

530   tid = (pthread_t)((long)arg);

531   pthread_mutex_lock(&workerlock);

532   for (wtp = workers; wtp != NULL; wtp = wtp->next) {

533     if (wtp->tid == tid) {

534        if (wtp->next != NULL)

535          wtp->next->prev = wtp->prev;

536        if (wtp->prev != NULL)

537          wtp->prev->next = wtp->next;

538        else

539          workers = wtp->next;

540        break;

541     }

542   }

543   pthread_mutex_unlock(&workerlock);

544   if (wtp != NULL) {

545     close(wtp->sockfd);

546     free(wtp);

547   }

548 }

[520~542] 函数client_cleanup是与客户端命令通信的工作者线程的线程清理程序。当线程调用pthread_exit时,或者用一个非0参数调用pthread_cleanup_pop,或者响应一个删除请求时,client_cleanup 函数会被调用。其参数是终止线程的线程ID。

锁住workerlock互斥量然后搜索工作者线程列表,直到找到一个匹配的线程ID。当找到一个匹配时,从列表中删除工作者线程结构并且停止搜索。

[543~548] 解锁 workerlock 互斥量,关闭线程用于和客户端通信的套接字文件描述符,然后释放worker_thread结构的内存。

既然要获得workerlock互斥量,当kill_workers函数正在遍历列表时,如果一个线程到达一个删除点时,必须等待直到kill_workers释放互斥量时才可以继续处理。

549  /*

550  * Deal with signals.

551  *

552  * LOCKING: acquires and releases configlock.

553  */

554  void *

555  signal_thread(void *arg)

556  {

557    int   err, signo;

558    for (;;) {

559     err = sigwait(&mask, &signo);

560     if (err != 0)

561       log_quit("sigwait failed: %s", strerror(err));

562     switch (signo) {

563     case SIGHUP:

564       /*

565       * Schedule to re-read the configuration file.

566       */

567       pthread_mutex_lock(&configlock);

568       reread = 1;

569       pthread_mutex_unlock(&configlock);

570       break;

571     case SIGTERM:

572       kill_workers();

573       log_msg("terminate with signal %s", strsignal(signo));

574       exit(0);

575     default:

576       kill_workers();

577       log_quit("unexpected signal %d", signo);

578     }

579   }

580 }

[549~562] 函数signal_thread由负责处理信号的线程运行。在main函数中初始化信号掩码,该掩码包括SIGHUP 和SIGTERM。这里,调用 sigwait来等待这些信号中的一个出现。如果sigwait失败,记录出错日志并退出。

[563~570] 如果接收到SIGHUP,然后获得configlock互斥量,将reread变量设为1,释放互斥量。这就告诉打印机守护进程在其处理循环的下一次迭代时再次读取配置文件。[571~574] 如果接收到SIGTERM,调用 kill_workers来杀死所有的工作者线程,记录日志,然后调用exit终止进程。

[575~580] 如果接收到非期望的信号,则杀死工作者线程并调用log_quit来记录日志然后退出。

581  /*

582  * Add an option to the IPP header.

583  *

584  * LOCKING: none.

585  */

586  char *

587  add_option(char *cp, int tag, char *optname, char *optval)

588  {

589   int  n;

590   union {

591     int16_t s;

592     char c[2];

593  }     u;

594  *cp++ = tag;

595  n = strlen(optname);

596  u.s = htons(n);

597  *cp++ = u.c[0];

598  *cp++ = u.c[1];

699  strcpy(cp, optname);

600  cp += n;

601  n = strlen(optval);

602  u.s = htons(n);

603  *cp++ = u.c[0];

604  *cp++ = u.c[1];

605  strcpy(cp, optval);

606  return(cp + n);

607 }

[581~593] 函数add_option用于在送到打印机的IPP首部中添加一个选项,回忆图21-4,属性的格式是1字节的描述属性类型的标志,然后是以2字节的二进制整数形式存储的属性名字的长度,接着是名字,属性值的长度,最后是属性值本身。

IPP没有打算去控制嵌入在首部的二进制整数的对齐方式。一些处理器架构,例如SPARC,并不能从任意地址装入一个整数。这意味着不能通过如下方式在 IPP 首部存放一个整数:该方式将一个指针转换成 int16_t 指向在首部存放整数的地址。相反,需要一次复制1字节整数。这就是为什么我们定义一个包含16位整数和2字节数组的union。

[594~607] 在首部存储标志并将属性名字的长度转换为网络字节序。一次复制 1 个字节到首部。接着复制属性名字。重复这个过程,继续复制属性值,并返回首部中下一个应该开始的部分的地址。

608 /*

609 * Single thread to communicate with the printer.

610 *

611 * LOCKING: acquires and releases joblock and configlock.

612 */

613 void *

614 printer_thread(void *arg)

615 {

616   struct job   *jp;

617   int        hlen, ilen, sockfd, fd, nr, nw, extra;

618   char        *icp, *hcp, *p;

619   struct ipp_hdr   *hp;

620   struct stat    sbuf;

621   struct iovec    iov[2];

622   char        name[FILENMSZ];

623   char        hbuf[HBUFSZ];

624   char        ibuf[IBUFSZ];

625   char        buf[IOBUFSZ];

626   char        str[64];

627   struct timespec ts = { 60, 0 };  /* 1 minute */

628   for (;;) {

629     /*

630     * Get a job to print.

631     */

632   pthread_mutex_lock(&joblock);

633   while (jobhead == NULL) {

634     log_msg("printer_thread: waiting...");

635     pthread_cond_wait(&jobwait, &joblock);

636   }

637   remove_job(jp = jobhead);

638   log_msg("printer_thread: picked up job %d", jp->jobid);

639   pthread_mutex_unlock(&joblock);

640   update_jobno();

[608~627] 函数printer_thread由与网络打印机通信的线程运行。使用icp和ibuf来建立IPP首部。使用hcp和hbuf建立HTTP首部。需要在独立的缓冲区中建立首部。HTTP首部包括ASCII表示的长度字段,而且在拼装出IPP首部之前,并不知道应该预留多大的空间。在一次调用中使用writev来写这两个头。

[628~640] 打印机线程在一个等待将作业传送到打印机的无限循环中运行。使用 joblock互斥量来保护作业列表。如果作业没有挂起,使用 pthread_cond_wait 来等待到来的作业。当一个作业准备好时,调用 remove_job 将其从列表中删除。

此时仍持有互斥量,因此释放互斥量并调用 update_jobno 将下一个作业号编写入到/var/spool/printer/jobno。

641    /*

642     * Check for a change in the config file.

643     */

644    pthread_mutex_lock(&configlock);

645    if (reread) {

646      freeaddrinfo(printer);

647      printer = NULL;

648      printer_name = NULL;

649      reread = 0;

650      pthread_mutex_unlock(&configlock);

651      init_printer();

652    } else {

653      pthread_mutex_unlock(&configlock);

654    }

655    /*

656     * Send job to printer.

657     */

658    sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jp->jobid);

659    if ((fd = open(name, O_RDONLY)) < 0) {

660      log_msg("job %ld canceled - can't open %s: %s",

661       jp->jobid, name, strerror(errno));

662      free(jp);

663      continue;

664    }

665    if (fstat(fd, &sbuf) < 0) {

666      log_msg("job %ld canceled - can't fstat %s: %s",

667       jp->jobid, name, strerror(errno));

668      free(jp);

669      close(fd);

670      continue;

671    }

[641~654] 现在有了要打印的作业,检查一下配置文件有无改变。锁住configlock互斥量并检查reread变量。如果该值非0,那么释放旧的addrinfo列表,清空指针,解锁互斥量,然后调用init_printer来重新初始化指针信息。既然从main线程初始化后只有这个上下文可以查看并可能更改打印机信息,因此除了使用configlock互斥量来保护reread标志的状态外,不需要任何其他的同步手段。

注意,尽管在此函数中获得和释放两个不同互斥量,但是并没有同时持有两个互斥量,因此不需要建立一个锁层次(见11.6.2节)。

[655~671] 如果不能打开数据文件,则记录出错日志,释放job结构,然后继续。打开文件之后,调用fstat来找到文件的大小。如果失败,记录出错日志并清理,然后继续。

672    if ((sockfd = connect_retry(AF_INET, SOCK_STREAM, 0,

673     printer->ai_addr, printer->ai_addrlen)) < 0) {

674       log_msg("job %d deferred - can’t contact printer: %s",

675        jp->jobid, strerror(errno));

676       goto defer;

677    }

678    /*

679     * Set up the IPP header.

680     */

681    icp = ibuf;

682    hp = (struct ipp_hdr *)icp;

683    hp->major_version = 1;

684    hp->minor_version = 1;

685    hp->operation = htons(OP_PRINT_JOB);

686    hp->request_id = htonl(jp->jobid);

687    icp += offsetof(struct ipp_hdr, attr_group);

688    *icp++ = TAG_OPERATION_ATTR;

689    icp = add_option(icp, TAG_CHARSET, "attributes-charset",

690     "utf-8");

691    icp = add_option(icp, TAG_NATULANG,

692     "attributes-natural-language", "en-us");

693    sprintf(str, "http://%s/ipp", printer_name);

694    icp = add_option(icp, TAG_URI, "printer-uri", str);

695    icp = add_option(icp, TAG_NAMEWOLANG,

696     "requesting-user-name", jp->req.usernm);

697    icp = add_option(icp, TAG_NAMEWOLANG, "job-name",

698     jp->req.jobnm);

[672~677] 打开一个连接到打印机的流套接字。如果connect_retry调用失败,跳到defer处,在这里清理、延迟一段时间,然后再尝试。

[678~698] 接下来,建立IPP首部。其操作是打印作业(print-job)请求。使用htons将2字节的操作ID从主机转换为网络字节序,使用htonl将4字节的作业ID从主机转换为网络字节序。完成首部的初始化之后,设置标志值来指示其后跟随操作属性。

调用add_option将属性添加到报文中。图12-5列出了打印作业请求所需的操作属性,前3个是必需的。将字符集设为UTF-8,该字符集是打印机必须支持的;指定语言为 en-us,即代表美国英语(U.S. English);另外一个必需的属性是 URI (Uniform Resource Identifier),将其设为http://printer_name/ipp。

推荐使用requesting-user-name属性,但不是必需的。job-name属性也是可选的。print命令将要打印的文件名作为作业名发送,该名字能够帮助用户区别多个要处理的作业。

699    if (jp->req.flags & PR_TEXT) {

700      p = "text/plain";

701      extra = 1;

702    } else {

703      p = "application/postscript";

704      extra = 0;

705    }

706    icp = add_option(icp, TAG_MIMETYPE, "document-format", p);

707    *icp++ = TAG_END_OF_ATTR;

708    ilen = icp - ibuf;

709    /*

710     * Set up the HTTP header.

711     */

712    hcp = hbuf;

713    sprintf(hcp, "POST /ipp HTTP/1.1\r\n");

714    hcp += strlen(hcp);

715    sprintf(hcp, "Content-Length: %ld\r\n",

716     (long)sbuf.st_size + ilen + extra);

717    hcp += strlen(hcp);

718    strcpy(hcp, "Content-Type: application/ipp\r\n");

719    hcp += strlen(hcp);

720    sprintf(hcp, "Host: %s:%d\r\n", printer_name, IPP_PORT);

721    hcp += strlen(hcp);

722    *hcp++ = ’\r’;

723    *hcp++ = ’\n’;

724    hlen = hcp - hbuf;

[699~708] 提供的最后一个属性是 document-format。如果省略该属性,则假定文件格式是打印机默认格式。对于PostScript打印机,格式可能是PostScript,但是一些打印机可以自动检测格式并在PostScript与纯文本或 PCL(HP 的打印机命令语言)格式间做选择。如果PR_TEXT标志被设置,则将文档格式设置为text/plain。否则,设置为 application/postscript。然后在属性结束处用结束属性标志定界并计算IPP首部的大小。

整数extra用来记录任何可能需要传输到打印机的附加字符。稍后会看到,需要发送一个附加字符以能够可靠地打印纯文本。当要计算内容长度时,需要考虑这个附加字符。

[709~724] 现在知道了IPP首部的大小,可以建立HTTP首部。将Context-Length设为IPP首部的字节长度加上要打印文件的大小再加上需要发送的附加字符的长度。

Content-Type为application/ipp。用回车换行符结束HTTP首部。最后,计算HTTP首部的大小。

725    /*

726     * Write the headers first. Then send the file.

727     */

728    iov[0].iov_base = hbuf;

729    iov[0].iov_len = hlen;

730    iov[1].iov_base = ibuf;

731    iov[1].iov_len = ilen;

732    if (writev(sockfd, iov, 2) != hlen + ilen) {

733      log_ret("can’t write to printer");

734      goto defer;

735    }

736    if (jp->req.flags & PR_TEXT) {

737      /*

738       * Hack: allow PostScript to be printed as plain text.

739       */

740      if (write(sockfd, "\b", 1) != 1) {

741        log_ret("can’t write to printer");

742        goto defer;

743      }

744    }

745    while ((nr = read(fd, buf, IOBUFSZ)) > 0) {

746      if ((nw = writen(sockfd, buf, nr)) != nr) {

747        if (nw < 0)

748          log_ret("can’t write to printer");

749        else

750          log_msg("short write (%d/%d) to printer", nw, nr);

751        goto defer;

752      }

753    }

[725~735] 将iovec数组的第一个元素指向HTTP首部,第二个元素指向IPP首部。然后采用writev将两个首部送往打印机。如果写失败或者写入少于请求的字节数,则记录日志并跳转到defer,在这里清理并延迟一段时间,然后再次尝试。

[736~744] 即使指明了纯文本,Phaser 8560还是会自动检测文档格式。为了防止它识别出要以纯文本格式打印的文件的开头,将退格作为第一个发送字符,这个字符不会被打印出来,并且能够使自动识别文件格式功能失效。这就可以打印PostScript源文件而不用打印PostScript文件的镜像。

[745~753] 通过IOBUFSZ块将数据文件发往打印机。当套接字缓冲区满的时候,write的发送少于请求,因此可以用write处理这种情况。当写首部时,不必担心这种情况,因为它们都很小,但要打印的文件却是很大的。

754    if (nr < 0) {

755      log_ret("can’t read %s", name);

756      goto defer;

757    }

758    /*

759     * Read the response from the printer.

760     */

761    if (printer_status(sockfd, jp)) {

762      unlink(name);

763      sprintf(name, "%s/%s/%d", SPOOLDIR, REQDIR, jp->jobid);

764      unlink(name);

765      free(jp);

766      jp = NULL;

767    }

768  defer:

769    close(fd);

770    if (sockfd >= 0)

771      close(sockfd);

772    if (jp != NULL) {

773      replace_job(jp);

774      nanosleep(&ts, NULL);

775    }

776  }

777 }

778 /*

779 * Read data from the printer, possibly increasing the buffer.

780 * Returns offset of end of data in buffer or -1 on failure.

781 *

782 * LOCKING: none.

783 */

784 ssize_t

785 readmore(int sockfd, char **bpp, int off, int *bszp)

[754~757] 读到文件末尾时,read返回0。如果读失败,记录错误信息日志并跳至defer。[758~767] 将文件发送给打印机后,调用printer_status来读取打印机对于请求的响应。

如果成功,printer_status返回一个非0值,就可以删除数据文件和控制文件。然后释放job结构,将其指针设为NULL,然后到达defer标签。

[768~777] 在defer标签处,关闭打开的数据文件描述符。如果套接字描述符是有效的,也将其关闭。如出错,jp 指向要打印作业的作业结构,这样就可以将作业放在挂起作业列表的头部然后延迟1分钟。如果成功,jp为NULL,此时只需回到循环开始处,获得下一个要打印的作业。

[778~785] readmore函数用于读取来自打印机的部分响应消息。

786 {

787  ssize_t nr;

788  char  *bp = *bpp;

789  int   bsz =*bszp;

790  if (off >= bsz) {

791     bsz += IOBUFSZ;

792     if ((bp = realloc(*bpp, bsz)) == NULL)

793       log_sys("readmore: can’t allocate bigger read buffer");

794     *bszp =bsz;

795     *bpp =bp;

796  }

797  if ((nr = tread(sockfd, &bp[off], bsz-off, 1)) > 0)

798    return(off+nr);

799  else

800     return(-1);

801 }

802 /*

803 * Read and parse the response from the printer. Return 1

804 * if the request was successful, and 0 otherwise.

805 *

806 * LOCKING: none.

807 */

808 int

809 printer_status(int sfd, struct job *jp)810 {

811  int      i, success, code, len, found, bufsz, datsz;

812  int32_t    jobid;

813  ssize_t    nr;

814  char      *bp, *cp, *statcode, *reason, *contentlen;

815  struct ipp_hdr h;

816 /*

817  * Read the HTTP header followed by the IPP response header.

818  * They can be returned in multiple read attempts. Use the

819  * Content-Length specifier to determine how much to read.

820  */

[786~801] 如果到达缓冲区尾部,通过相应的参数bpp和bszp重新分配一个大一点的缓冲区并返回该新的缓冲区的起始地址以及缓冲区大小。上述任何一种情况下,从缓冲区已读数据的末尾开始读取缓冲区所能容纳的尽可能多的数据。返回相应的已读数据的新偏移量。如果read失败,或者超时,返回−1。

[802~820] printer_status函数读取打印机对一个打印作业请求的响应消息。不知道打印机会如何响应:也许会在多个报文中回送一个响应,也许在一个报文中回送完整的响应,或者包括一个中间确认,诸如HTTP 100 Continue报文。需要处理所有的可能性。

821  success =0 ;

822  bufsz =IOBUFSZ;

823  if ((bp = malloc(IOBUFSZ)) == NULL)

824     log_sys("printer_status: can’t allocate read buffer");

825  while ((nr = tread(sfd, bp, bufsz, 5)) > 0) {

826     /*

827     * Find the status. Response starts with "HTTP/x.y"

828     * so we can skip the first 8 characters.

829     */

830     cp = bp + 8;

831     datsz =nr;

832     while (isspace((int)*cp))

833       cp++;

834     statcode =cp;

835     while (isdigit((int)*cp))

836       cp++;

837     if (cp == statcode) { /* Bad format; log it and move on */

838       log_msg(bp);

839     } else {

840      *cp++ =’\0’;

841      reason =cp;

842      while (*cp != ’\r’ && *cp != ’\n’)

843        cp++;

844      *cp =’\0’;

845      code =atoi(statcode);

846      if (HTTP_INFO(code))

847        continue;

848      if (!HTTP_SUCCESS(code)) { /* probable error: log it */

849        bp[datsz] =’\0’;

850        log_msg("error: %s", reason);

851        break;

852      }

[821~838] 分配一个缓冲区并读取来自打印机的数据,期望5秒之内有可用的响应。跳过HTTP/1.1和报文开始的所有空格,然后是数字状态码。如果不是,在日志中记录报文的内容。

[839~844] 如果在响应中找到一个数字状态码,将其开始的非数字字符转换成null字节(这一字符是某种形式的空白)。接下来是一个表明原因的字符串(文本消息)。搜索回车或换行符,并采用null字节结束文本字符串。

[845~852] 调用atoi函数将状态码字符串转化成一个整数。如果仅是提供信息的报文,将其忽略并继续循环。我们期望看到的要么是成功消息要么是出错消息。如果得到出错消息,记录出错日志并退出循环。

853      /*

854       * HTTP request was okay, but still need to check

855       * IPP status. Search for the Content-Length.

856       */

857      i = cp - bp;

858      for (;;) {

859        while (*cp != ’C’ && *cp != ’c’ && i < datsz) {

860           cp++;

861           i++;

862        }

863        if (i >= datsz) { /* get more header */

864           if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {

865             goto out;

866           } else {

867             cp =&bp[i];

868             datsz += nr;

869           }

870        }

871        if (strncasecmp(cp, "Content-Length:", 15) == 0) {

872           cp += 15;

873           while (isspace((int)*cp))

874             cp++;

875           contentlen =cp;

876           while (isdigit((int)*cp))

877             cp++;

878           *cp++ =’\0’;

879           i = cp - bp;

880           len =atoi(contentlen);

881           break;

882        } else {

883           cp++;

884           i++;

885        }

886      }

[853~870] 如果HTTP请求成功,需要检查IPP状态。搜索整个报文直到找到Content-Length属性。HTTP 首部的关键字是大小写敏感的,因此需要同时检查小写和大写字符。如果缓冲区空间耗尽,需要调用readmore,通过它再调用realloc增加缓冲区大小。

因为缓冲区地址可能改变,需要调整cp指向正确的缓冲区位置。

[871~886] 使用strncasecmp函数进行大小写敏感比较。如果找到Content-Length属性字符串,就搜索它的值。将数字字符串转换为整数并退出这个for循环。如果比较失败,继续逐个字节搜索缓冲区。如果直到缓冲区末尾仍未找到Content-Length属性,就从打印机读取更多数据并继续搜索。

887      if (i >= datsz) { /* get more header */

888        if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {

889           goto out;

890        } else {

891           cp =&bp[i];

892           datsz += nr;

893        }

894      }

895      found =0 ;

896      while (!found) { /* look for end of HTTP header */

897        while (i < datsz - 2) {

898           if (*cp == ’\n’ && *(cp + 1) == ’\r’ &&

899           *(cp + 2) == ’\n’) {

900            found =1 ;

901            cp += 3;

902            i += 3;

903            break;

904           }

905           cp++;

906           i++;

907        }

908        if (i >= datsz) { /* get more header */

909           if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {

910             goto out;

911           } else {

912             cp =&bp[i];

913             datsz += nr;

914           }

915        }

916      }

917      if (datsz - i < len) { /* get more header */

918        if ((nr = readmore(sfd, &bp, i, &bufsz)) < 0) {

919           goto out;

920        } else {

921           cp =&bp[i];

922           datsz += nr;

[887~916] 现在知道报文的长度了(通过 Content-Length 属性)。如果耗尽缓冲区,那么从打印机再次读取。接下来搜索 HTTP 首部的末尾(空白行)。如果找到了,就设置found标志并跳过空白行。无论何时调用readmore,都要将cp设置为与之前指向的缓冲区偏移量相同,以防止重分配时缓冲区地址改变。

[917~922] 如果找到HTTP首部的末尾,计算HTTP首部所用的字节数。如果读取的值减去HTTP首部的大小后不等于IPP报文的数据长度(该值从内容长度Content-Length中计算),需要读取更多的数据。

923 }

924      }

925      memcpy(&h, cp, sizeof) (struct ipp_hdr);

926      i = ntohs(h.status);

927      jobid = ntohl(h.request_id);

928      if (jobid != jp->jobid) {

929        /*

930         * Different jobs. Ignore it.

931         */

932        log_msg("jobid %d status code %d", jobid, i);

933        break;

934      }

935      if (STATCLASS_OK(i))

936        success = 1;

937      break;

938     }

939   }

940  out:

941    free(bp);

942    if (nr < 0) {

943      log_msg("jobid %d: error reading printer response: %s",

944       jobid, strerror(errno));

945    }

946    return(success);

947 }

[923~927] 从IPP首部中获取状态和作业ID。两者均以网络字节序的整数形式存储,因此需要调用ntohs和ntohl将其转换为主机字节序。

[928~939] 如果作业 ID 不匹配,表明并非是对我们请求的响应,那么记录日志并退出外层while循环。如果IPP状态指示为成功,保存返回值并退出循环。

[940~947] 在退出之前,要释放用来存放响应报文的缓冲区。如果打印请求成功则返回 1,否则失败,返回0。

这里总结本章中这个扩展的例子。本章中的程序在Xerox Phaser 8560网络PostScript打印机上测试。遗憾的是,当文档格式设置为 text/plain 时,这个打印机并没有禁止它的自动识别格式功能。我们使用了一个小技巧,使得在想要以纯文本格式对待一个文档时,打印机不自动识别文档格式。一种替代的方法是使用诸如 a2ps(1)这样的实用工具将源打印成一个 PostScript 程序。a2ps(1)可以在打印前封装PostScript程序。

21.6 小结

本章仔细考查了两个完整的程序:一个打印假脱机守护进程将作业发送到网络打印机和一个命令行程序将打印作业提交到假脱机守护进程。这给我们一个机会,考查在一个实际程序中使用前面章节所讲述的许多特性,如线程、I/O多路技术、文件I/O、套接字I/O以及信号。

习题

21.1 将ipp.h中所列的IPP错误码转换成错误消息。然后修改打印假脱机守护进程,当IPP首部指示有打印机错误时,在printer_status函数结尾处记录日志。

21.2 增强print命令和printd守护进程,使得用户可以请求双面打印,并支持横向打印和纵向打印。

21.3 修改打印假脱机守护进程,当其开始时,能够联系打印机并找出所支持的特性,这样守护进程就不会请求打印机不支持的选项。

21.4 写一个命令行程序来报告挂起的打印作业状态。

21.5 写一个命令行程序来取消一个挂起的打印作业。使用作业ID作为命令参数来指明取消哪个作业。如果防止一个用户取消另一个用户的打印作业?

21.6 在打印假脱机守护进程中支持多个打印机,并包括将一个打印作业从本打印机移到另一个打印机的方式。

21.7 解释为什么在打印机守护进程中,当信号处理线程捕捉到 SIGHUP 并将reread 设置为1时,不需要唤醒打印机线程?

21.8 在printer_status函数中,通过查找HTTP的Content-Length属性搜索IPP报文的长度。这一技术在使用块传输编码的打印机上不起作用。在RFC 2616中查找块消息是如何格式化的,然后修改printer_status,使其也能够支持这种形式的响应。

21.9 在update_jobno函数中,当下一个作业编号从最大正值回绕到1时(参见get_newjobno),可能会将一个较大的编号改写为一个较小的编号。这可能导致守护进程重启时读到一个错误的编号。对于这一问题是否有简单的解决方法?

附录A 函数原型

本附录包含了正文中说明过的标准ISO C、POSIX和UNIX系统的函数原型。通常我们想了解的是函数的参数(fgets 的哪一个参数是文件指针?)或者返回值(sprintf 返回的是指针还是计数值?)。这些函数原型还说明了要包含哪些头文件,以获得特定常量的定义,或获得ISO C函数原型,以帮助在编译时进行错误检测。

每个函数原型的引用页号出现在为该函数列出的第一个头文件的右边。引用页号提供的是包含该函数原型的页。为获得该函数原型的附加信息可参阅该页。

某些函数原型仅受本书说明的4种平台中某几种的支持。另外,某些平台支持的函数标志在另一些平台上并不提供支持。对于这些情况,我们通常列出提供支持的平台。但是对于有些情况,我们列出了不提供支持的平台。

本附录中标注的页码为英文版原书的页码,与书中页边标注的页码对应。

附录B 其他源代码

B.1 本书使用的头文件

本书中的大多数程序都包含头文件apue.h,如图 B-1所示。其中定义了常量(如MAXLINE)和我们自编函数的原型。

大多数程序都需要包含下列头文件:<stdio.h>、<stdlib.h>(其中有exit函数原型)和<unistd.h>(其中包含所有标准UNIX函数的原型),因此头文件apue.h 自动包含了这些系统头文件,同时还包含了<string.h>。这样就减少了本书中所有程序的长度。

图B-1 头文件apue.h

程序中先包括 apue.h,然后再包括一般系统头文件,这样就使我们易于做到下列各点:可以先定义一些在此后包括的头文件可能要求的部分;能够控制头文件被包括的顺序;能够重定义某些部分,而这正是为隐藏两个系统之间的差别而需要解决的。

B.2 标准出错例程

我们提供了两套出错函数,用于本书中大多数实例以处理各种出错情况。一套以err_开头,并向标准错误输出一条出错消息。另一套以 log_开头,用于守护进程(见第 13 章),它们多半没有控制终端。

之所以提供我们自己的出错函数,是为了能够编写只有一行C代码的出错处理程序,例如:

if (出错条件)

err_dump(带任意参数的printf格式);

这样就不再需要使用下列代码:

if (出错条件) {

char buf[200];
sprintf(buf, 带任意参数的printf格式);
perror(buf);
abort();

}

我们的出错处理函数使用了 ISO C 的变长参数表功能。其详细说明见 Kernighan 和Ritchie[1988]的7.3节。应当注意的是,这个ISO C功能与早期系统(如SVR3和4.3BSD)提供的varargs功能不同。宏的名字相同,但更改了某些宏的参数。

图B-2列出了各个出错函数之间的区别。

图B-2 标准出错函数

图B-3包括了输出至标准错误的各个出错函数。

图B-3 输出至标准错误的出错函数

图B-4包括了各log_XXX 出错函数。若进程不以守护进程方式运行,那么调用者应当定义变量log_to_stderr,并将其设置为非0值。在这种情况下,出错消息被发送至标准错误。若log_to_stderr标志为0,则使用syslog设施(见13.4节)。

图B-4 用于守护进程的出错函数