第1章
1.1 这个习题利用ls(1)命令的下面两个参数:-i打印文件或目录的i节点编号(4.14节详细讨论了i节点);-d仅打印目录信息,而不是打印目录中所有文件的信息。
执行下列命令:
$ ls -ldi /etc/. /etc/.. -i要求打印i节点编号
162561 drwxr-xr-x 66 root 4096 Feb 5 03:59 /etc/./
2 drwxr-xr-x 19 root 4096 Jan 15 07:25 /etc/../
$ ls -ldi /. /.. .和..的i节点编号均为2
2 drwxr-xr-x 19 root 4096 Jan 15 07:25 /./
2 drwxr-xr-x 19 root 4096 Jan 15 07:25 /../
1.2 UNIX系统是多道程序或多任务系统,所以,在图1-6所示程序运行的同时其他两个进程也在运行。
1.3 因为perror的msg参数是一个指针,perror就可以改变msg指向的字符串。然而使用限定符const限制了perror不能修改msg指针指向的字符串。而对于strerror,其错误号参数是整数类型,并且C是按值传递所有参数,因此即使strerror函数想修改参数的值也修改不了,也就没有必要使用const属性。(如果对C中函数参数的处理不是很清楚,可参见Kernighan和Ritchie[1988]的5.2节。)
1.4 在2038年。将time_t数据类型定为64位整型,就可以解决该问题了。如果它现在是32位整型,那么为使应用程序正常工作,应当对其重编译。但是这一问题还有更糟糕之处。
某些文件系统及备份介质以32位整型存放时间。对于这些同样需要加以更新,但又需要能读旧的格式。
1.5 大约248天。
第2章
2.1 下面是FreeBSD中使用的技术。在头文件<machine/_types.h>中定义可在多个头文件中出现的基本数据类型。例如:
typedef int __int32_t;
typedef unsigned int __uint32_t;
#ifndef _MACHINE__TYPES_H_
#define _MACHINE__TYPES_H_
┇
typedef __uint32_t __size_t;
┇
#endif /* _MACHINE__TYPES_H_ */
在每个可以定义基本数据类型size_t的头文件中,包含下面的语句序列。
#ifndef _SIZE_T_DECLARED
typedef __size_t size_t;
#define _SIZE_T_DECLARED
#endif
这样,实际上只执行一次size_t的typedef。
2.3 如果OPEN_MAX是未确定的或大得出奇(即等于LONG_MAX),那么可以使用getrlimit得到每个进程的最大打开文件描述符数。因为可以修攺对每个进程的限制,所以我们不能将前一个调用得到的值高速缓存起来(它可能已被更改),见图C-1。
图C-1 标识最大可能文件描述符的替换方法
第3章
3.1 所有磁盘I/O都要经过内核的块缓存区(也称为内核的缓冲区高速缓存)。唯一例外的是对原始磁盘设备的I/O,但是我们不考虑这种情况(Bach[1986]的第3章讲述了这种缓存区高速缓存的操作)。既然read或write的数据都要被内核缓冲,那么术语“不带缓冲的I/O”指的是在用户的进程中对这两个函数不会自动缓冲,每次read或write就要进行一次系统调用。
3.3 每次调用open函数就分配一个新的文件表项。但是因为两次打开的是同一个文件,则两个文件表项指向相同的v节点。调用dup引用已存在的文件表项(此处指fd1的文件表项),见图C-2。当F_SETFD作用于fd1时,只影响fd1的文件描述符标志;F_SETFL作用于fd1时,则影响fd1及fd2指向的文件表项。
图C-2 open和dup的结果
3.4 如果fd是1,执行dup2(fd, 1)后返回1,但没有关闭文件描述符1(见3.12节)。调用3次dup2后,3个描述符指向相同的文件表项,所以不需要关闭描述符。
如果fd为3,调用3次dup2后,有4个描述符指向相同的文件表项,这种情况下就需要关闭描述符3。
3.5 因为shell从左到右处理命令行,所以
./a.out > outfile 2>&1首先设置标准输出到outfile,然后执行dup将标准输出复制到描述符2(标准错误)上,其结果是将标准输出和标准错误设置为同一个的文件,即描述符 1 和 2 指向同一个文件表项。而对于命令行
./a.out 2>&1 > outfile
由于首先执行dup,所以描述符2成为终端(假设命令是交互执行的),标准输出重定向到outfile。结果是描述符1指向outfile的文件表项,描述符2指向终端的文件表项。
3.6 这种情况下,仍然可以用lseek和read函数读文件中任意一个位置的内容。但是write函数在写数据之前会自动将文件偏移量设置为文件尾,所以写文件时只能从文件尾端开始。
第4章
4.1 stat函数总是跟随符号链接(见图4-17),所以该程序决不会显示文件类型是“符号链接”。
例如,正如本书正文中所示,/dev/cdrom是/dev/sr0的一个符号链接,但是stat函数的结果只显示/dev/cdrom 是一个块特殊文件,而不报告它是一个符号链接。若符号链接指向一个不存在的文件,stat会出错返回。
4.2 将关闭该文件的所有访问权限。
$ umask 777
$ date > temp.foo
$ ls -l temp.foo
---------- 1 sar 29 Feb 5 14:06 temp.foo
4.3 下面的命令显示了关闭用户读权限时所发生的情况。
$ data > foo
$ chmod u-r foo 关闭用户读权限
$ ls -l foo 验证文件的权限
--w-r--r-- 1 sar 29 Feb 5 14:21 foo
$ cat foo 读文件
cat: foo: Permission denied
4.4 如果用open或creat创建已经存在的文件,则该文件的访问权限位不变。运行图4-9中的程序可以验证这点。
$ rm foo bar 删除文件
$ data > foo 创建文件
$ data > bar
$ chmod a-r foo bar 关闭所有的读权限
$ ls -l foo bar 验证其权限
--w------- 1 sar 29 Feb 5 14:25 bar
--w------- 1 sar 29 Feb 5 14:25 foo
$ ./a.out 运行图4-9程序
$ ls -l foo bar 检查文件的权限和大小
--w------- 1 sar 0 Feb 5 14:26 bar
--w------- 1 sar 0 Feb 5 14:26 foo
可以看出访问权限没有改变,但是文件被截断了。
4.5 目录的长度从来不会是0,因为它总是包含.和..两项。符号链接的长度指其路径名包含的字符数,由于路径名中至少有一个字符,所以长度也不为0。
4.7 当创建新的core 文件时,内核对其访问权限有一个默认设置,在本例中是rw-r--r--。这一默认值可能会也可能不会被umask的值修改。shell对创建的重定向的新文件也有一个默认的访问权限,本例中为rw-rw-rw-,这个值总是被当前的umask修改,在本例中umask为02。
4.8 不能使用du的原因是它需要文件名,如
du tempfile
或目录名,如
du .
只有当 unlink 函数返回时才释放 tempfile 的目录项,du .命令没有计算仍然被tempfile占用的空间。本例中只能使用df命令查看文件系统中实际可用的空闲空间。
4.9 如果被删除的链接不是该文件的最后一个链接,则不会删除该文件。此时,文件的状态更改时间被更新。但是,如果被删除的链接是最后一个链接,则该文件将被物理删除。这时再去更新文件的状态更改时间就没有意义,因为包含文件所有信息的i节点将会随着文件的删除而被释放。
4.10 用opendir打开一个目录后,递归调用函数dopath。假设opendir使用一个文件描述符,并且只有在处理完目录后才调用closedir释放描述符,这就意味着每次降一级就要使用另外一个描述符。所以进程可打开的最大描述符数就限制了我们可以遍历的文件系统树的深度。Single UNIX Specification的XSI扩展中说明的ftw允许调用者指定使用的描述符数,这隐含着可以关闭描述符并且重用它们。
4.12 chroot函数被因特网文件传输协议(Internet File Transfer Protocol,FTP)程序用于辅助安全性。系统中没有账户的用户(也称为匿名 FTP)放在一个单独的目录下,利用 chroot将此目录当作新的根目录,就可以阻止用户访问此目录以外的文件。
chroot也用于在另一台机器上构造一个文件系统层次结构的副本,然后修改此副本,不会更改原来的文件系统。这可用于测试新软件包的安装。
chroot只能由超级用户执行,一旦更改了一个进程的根,该进程及其后代进程就再也不能恢复至原先的根。
4.13 首先调用 stat 函数取得文件的 3 个时间值,然后调用 utimes 设置期望的值。在调用utimes时我们不希望改变的值应当是stat中相应的值。
4.14 finger(1)对邮箱调用stat函数,最近一次的修改时间是上一次接收邮件的时间,最近访问时间是上一次读邮件的时间。
4.15 cpio和tar存储的只是归档文件的修改时间(st_mtim)。因为文件归档时一定会读它,所以该文件的访问时间对应于创建归档文件的时间,因此没有存储其访问时间。cpio的-a选项可以在读输入文件后重新设置该文件的访问时间,于是创建归档文件不改变文件的访问时间。(但是,重置文件的访问时间确实改变了状态更改时间。)状态更改时间没有存储在文挡上,因为即使它曾被归档,在抽取时也不能设置其值。(utimes 函数极其相关的futimens和utimensta函数可以更改的仅仅是访问时间和修改时间。)
对tar来说,在抽取文件时,其默认方式是复原归档时的修改时间值,但是tar的-m选项则将修改时间设置为抽取文件时的时间,而不是复原归档时的修改时间值。对于 tar,无论何种情况,在抽取后,文件的访问时间均是抽取文件时的时间。
另一方面,cpio将访问时间和修改时间设置为抽取文件时的时间。默认情况下,它并不试图将修改时间设置为归档时的值。cpio 的-m 选项将文件的修改时间和访问时间设置为归档时的值。
4.16 内核对目录树的深度没有内在的限制,但是如果路径名的长度超出了PATH_MAX,则有许多命令会失败。图C-3程序创建了一个深度为1 000的目录树,每一级目录名有45个字符。
在所有平台上我们都能构建这样的结构,但并不是在所有平台上都能用getcwd得到第1 000级目录的绝对路径名。在Mac OS X 10.6.8中,当到达长路径的目录尾部时,getcwd就不再成功了。在FreeBSD 8.0、Linux 3.2.0和Solaris 10中,getcwd可以获得路径名,但是需要多次调用realloc得到一个足够大的缓冲区。在Linux 3.2.0上运行该程序后得到:
$ ./a.out
getcwd failed, size = 4096: Numerical result out of range
getcwd failed, size = 4196: Numerical result out of range
... 省略了418行
getcwd failed, size = 45896: Numerical result out of range
getcwd failed, size = 45996: Numerical result out of range
length = 46004
显示46004字节的路径名
然而,不能用cpio归档此目录,因为文件名太长了。事实上,cpio在所有4种平台上都不能归档此目录。于此对比的是,在FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8上,可以用tar归档此目录。然而,在Linux 3.2.0上,我们不能从归档文件中抽取出目录的层次结构。
图C-3 创建深目录树
4.17 /dev目录关闭了一般用户的写访问权限,以防止普通用户删除目录中的文件名。这就意味着unlink失败。
第5章
5.2 fgets函数读入数据,直到行结束或缓冲区满(当然会留出一个字节存放终止null字节)。
同样,fputs只负责将缓冲区的内容输出直到遇到一个null字节,而并不考虑缓冲区中是否包含换行符。所以,如果将MAXLINE设得很小,这两个函数仍然会正常工作;只不过在缓冲区较大时,函数被执行的次数要多于MAXLINE值设置得较大的时候。
如果这些函数删除或添加换行符(如gets和puts函数的操作),则必需保证对于最长的行,缓冲区也足够大。
5.3 当printf没有输出任何字符时,如printf("");,函数调用返回0。
5.4 这是一个比较常见的错误。getc以及getchar的返回值是int类型,而不是char类型。
由于EOF经常定义为−1,那么如果系统使用的是有符号的字符类型,程序还可以正常工作。但如果使用的是无符号字符类型,那么返回的EOF被保存到字符c后将不再是−1,所以,程序会进入死循环。本书说明的4种平台都使用带符号字符,所以实例代码都能工作。
5.5 使用方法为:先调用fflush后调用fsync。fsync所使用的参数由fileno函数获得。
如果不调用fflush,所有的数据仍然在内存缓冲区中,此时调用fsync将没有任何效果。
5.6 当程序交互运行时,标准输入和标准输出均为行缓冲方式。每次调用fgets时标准输出设备将自动冲洗。
5.7 基于BSD系统的fmemopen的实现如图C-4所示。
图C-4 BSD系统的fmemopen实现
第6章
6.1 6.3节讲述了在Linux和Solaris系统中访问阴影口令文件的函数。不能使用6.2节所述函数返回的pw_passwd字段值与加密口令相比较,因为此字段不是加密的口令。正确的方法是使用阴影口令文件中对应用户的加密口令字段来进行比较。
在FreeBSD和Mac OS X中,口令文件的阴影是自动建立的。FreeBSD 8.0中,仅当调用者的有效用户ID为0时,getpwnam或getpwuid函数返回的passed结构中的pw_passwd字段包含有加密口令。在Mac OS X 10.6.8上,加密口令不能通过这些接口访问。
6.2 在Linux 3.2.0和Solaris 10中,图C-5程序输出加密口令。当然,除非有超级用户权限,否则调用getspnam将返回EACCES错误。
图C-5 在Linux和Solaris系统中输出加密口令
在FreeBSD 8.0中,具有超级用户权限时,图C-6程序将输出加密口令,否则pw_passed的返回值为星号(*)。在Mac OS X 10.6.8中,不管其运行时的用户权限是什么都输出星号。
图C-6 在FreeBSD和Mac OS X中输出加密口令
6.5 图C-7程序以类似于date命令的格式输出日期。图C-7中程序的运行结果如下:
图C-7 以date(1)的格式输出日期和时间
$ ./a.out 作者的默认格式是美国东部
Wed Jul 25 22:58:32 EDT 2012
$ TZ=US/Mountain ./a.out 美国山地时间
Wed Jul 25 20:58:32 MDT 2012
$ TZ=Japan ./a.out 日本
Thu Jul 26 11:58:32 JST 2012
第7章
7.1 原因在于 printf 的返回值(输出的字符数)变成了 main 函数的返回值。为了验证这一结论,改变打印字符串的长度,然后运行程序,查看返回值是否与新的字符串长度值匹配。
当然,并不是所有的系统都会出现该情况。还要注意的是,如果在gcc中允许ISO C扩展的编译选项,返回值将总是0,这是标准要求的。
7.2 当程序处于交互运行方式时,标准输出通常处于行缓冲方式,所以当输出换行符时,上次的结果才被真正输出。如果标准输出被定向到一个文件,而标准输出处于全缓冲方式,则当标准I/O清理操作执行时,结果才真正被输出。
7.3 由于agrc和argv的副本不像environ一样保存在全局变量中,所以在大多数UNIX系统中没有其他办法。
7.4 当C程序解引用一个空指针出错时,执行该程序的进程将终止。可以利用这种方法终止进程。
7.5 定义如下:
typedef void Exitfunc(void);
int atexit(Exitfunc *func);
7.6 calloc将分配的内存空间初始化为0。但是ISO C并不保证0值与浮点0或空指针的值相同。
7.7 只有通过exec函数执行一个程序时,才会分配堆和栈(见8.10节)。
7.8 可执行文件(a.out)包含了用于调试core文件的符号表信息。用strip(1)命令可以删除这些信息,对两个a.out文件执行这条命令,它们的大小减为798 760和6 200字节。
7.9 没有使用共享库时,可执行文件的大部分都被标准I/O库所占用。
7.10 这段代码不正确。因为在自动变量val已经不存在之后,代码还通过指针引用这个已经不存在的自动变量。自动变量val在复合语句开始的左花括号之后声明了,但当该复合语句结束时,即在匹配的右花括号之后,自动变量就不存在了。
第8章
8.1 为了仿真子进程终止时关闭标准输出的行为,在调用exit之前加下列代码行:
fclose(stdout);
为了观察其效果,用下面几行代替程序中调用printf的语句。
i = printf("pid = %ld, glob = %d, var = %d\n",
(long)getpid(), glob, var);
sprintf(buf, "%d\n", i);
write(STDOUT_FILENO, buf, strlen(buf));
还需要定义变量i和buf。
这里假设子进程调用exit时关闭标准I/O流,但不关闭文件描述符STDOUT_FILENO。有些版本的标准I/O库会关闭与标准输出相关联的文件描述符从而引起write标准输出失败。在这种情况下,调用dup将标准输出复制到另一个描述符,write则使用新复制的文件描述符。
8.2 可以通过图C-8程序来说明这个问题。
图C-8 错误使用vfork的例子
当函数f1调用vfork时,父进程的栈指针指向f1函数的栈帧,见图C-9。vfork使得子进程先执行然后从f1返回,接着子进程调用f2,并且f2的栈帧覆盖了f1的栈帧,在f2中子进程将自动变量buf的值置为0,即将栈中的1 000个字节的值都置为0。从f2返回后子进程调用_exit,这时栈中main栈帧以下的内容已经被f2修改了。然后,父进程从vfork调用后恢复继续,并从f1返回。返回信息虽然常常保存在栈中,但是多半可能已经被子进程修改了。对于这个例子,父进程恢复继续执行的结果要依赖于你所使用的 UNIX系统的实现特征(如返回信息保存在栈帧中的具体位置、修改动态变量时覆盖了哪些信息等)。通常的结果是一个core文件,但在你的系统中,产生的结果可能不同。
8.4 在图8-13中,我们先让父进程输出,但是当父进程输出完毕子进程要输出时,要让父进程终止。
是父进程先终止还是子进程先执行输出,要依赖于内核对两个进程的调度(另一个竞争条件)。在父进程终止后,shell会开始执行下一个程序,它也许会干扰子进程的输出。为了避免这种情况,要在子进程完成输出后才终止父进程。用下面的语句替换程序中fork后面的代码。
else if (pid == 0) {
WAIT_PARENT(); /* parent goes first */
charatatime("output from child\n");
TELL_PARENT(getppid()); /* tell parent we're done */
} else {
charatatime("output from parent\n");
TELL_CHILD(pid); /* tell child we're done */
WAIT_CHILD(); /* wait for child to finish */
}
图C-9 调用vfork时的栈帧
由于只有终止父进程才能开始下一个程序,而该程序让子进程先运行,所以不会出现上面的情况。
8.5 对argv[2]打印的是相同的值(/home/sar/bin/testinterp)。原因是execlp在结束时调用了execve,并且与直接调用execl的路径名相同。回忆图8-15。
8.6 图C-10程序创建了一个僵死进程。
图C-10 创建一个僵死进程并用ps查看其状态
执行程序结果如下(ps(1)用Z表示僵死进程):
$ ./a.out
PID PPID S TT COMMAND
2369 2208 S pts/2 -bash
7230 2369 S pts/2 ./a.out
7231 7230 Z pts/2 [a.out] <defunct>
7232 7230 S pts/2 sh -c ps -o pid,ppid,state,tty,command
7233 7232 R pts/2 ps -o pid,ppid,state,tty,command
第9章
9.1 因为init是登录 shell的父进程,当登录shell终止时它收到SIGCHLD信号量,所以init进程知道什么时候终端用户注销。
网络登录没有包含init,在utmp和wtmp文件中的登录项和相应的注销项是由一个处理登录并检测注销的进程写的(本例中为telnetd)。
第10章
10.1 当程序第一次接收到发送给它的信号时就终止了。因为一捕捉到信号,pause函数就返回。10.2 栈帧见图C-11。
图C-11 longjmp前后的栈帧
在sig_alrm中通过longjmp返回sleep2,有效地避免了继续执行sig_int。从这一点,sleep2返回main(回忆图10-8)。
10.4 在第一次调用 alarm 和 setjmp 之间又有一次竞争条件。如果进程在调用 alarm 和setjmp之间被内核阻塞了,闹钟时间超过后就调用信号处理程序,然后调用longjmp。
但是由于没有调用过setjmp,所以没有设置env_alrm缓冲区。如果longjmp的跳转缓冲区没有被setjmp初始化,则说明longjmp的操作是未定义的。
10.5 参见Don Libes的论文“Implementing Software Timers”(C users Journal,Vol.8,no.11,Nov. 1990)中的例子。可以访问http:// www.kohala.com/start/ libes.timers.txt获得该论文的电子版。
10.7 如果仅仅调用_exit,则进程终止状态不能表示该进程是由于SIGABRT信号而终止的。
10.8 如果信号是由其他用户的进程发出的,进程必须设置用户ID为根或者是接收进程的所有者,否则kill不能执行。所以实际用户ID为信号的接收者提供了更多的信息。
10.10 对于本书作者所用的一个系统,每60~90分钟增加一秒,这个误差是因为每次调用sleep都要调度一次将来的时间事件,但是由于CPU调度,有时并没有在事件发生时立即被唤醒。
另外一个原因是进程开始运行和再次调用sleep都需要一定量的时间。
cron守护进程这样的程序每分钟都要获取当前时间,它首先设置一个休眠周期,然后在下一分钟开始时唤醒。(将当前时间转换成本地时间并查看 tm_sec 值。)每一分钟,设置下一个休眠周期,使得在下一分钟开始时可以唤醒。大多数调用是sleep(60),偶尔有一个sleep(59)用于在下一分钟同步。但是,若在进程中花费了许多时间执行命令或者系统的负载重、调度慢,这时休眠值可能远小于60。
10.11 在Linux 3.2.0、Mac OS X 10.6.8和Solaris 10中,从来没有调用过SIGXFSZ的信号处理程序,一旦文件的大小达到1 024时,write就返回24。
在FreeBSD 8.0中,当文件大小已达到1 000字节,在下一次准备写100字节时调用该信号处理程序,write返回−1,并且将errno设置为EFBIG(文件太大)。
在所有 4 种平台上,如果在当前文件偏移量处(文件尾端)尝试再一次 write,将收到SIGXFSZ信号,write将失败,返回-1,并将errno设置为EFBIG。
10.12 结果依赖于标准I/O库的实现:fwrite函数如何处理一个被中断的write。
例如,在Linux 3.2.0上,当使用fwrite函数写一个大的缓冲区时,fwrite以相同的字节数直接调用write。在write系统调用当中,闹钟时间到,但我们直到写结束才看到信号。看上去就好像在write系统调用进行当中内核阻塞了信号。
第11章
11.1 图C-12给出了一个没有使用自动变量,而采用动态内存分配的程序。
图C-12 线程返回值的正确使用
11.2 要改变挂起作业的线程ID,必须持有写模式下的读写锁,防止ID在改变过程中有其他线程在搜索该列表。目前定义该接口的方式存在的问题在于:调用 job_find 找到该作业以及调用job_remove从列表中删除该作业这两个时间之间作业ID可以改动。这个问题可以通过在job结构中嵌入引用计数和互斥量,然后让job_find增加引用计数的方法来解决。这样修改ID的代码就可以避免对列表中非零引用计数的任何作业进行ID改动的情况。
11.3 首先,列表是由读写锁保护的,但条件变量需要互斥量对条件进行保护。其次,每个线程等待满足的条件应该是有某个作业进行处理时需要的条件,所以需要创建每线程数据结构来表示这个条件。或者,可以把互斥量和条件变量嵌入到queue结构中,但这意味着所有的工作线程将等待相同的条件。如果有很多工作线程存在,当唤醒了许多线程但又没有工作可做时,就可能出现惊群效应问题,最后导致CPU资源的浪费,并且增加了锁的争夺。
11.4 这根据具体情况而定。总的来说,两种情况都可能是正确的,但每一种方法都有不足之处。在第一种情况下,等待线程会被安排在调用pthread_cond_broadcast之后运行。如果程序运行在多处理器上,由于还持有互斥锁(pthread_cond_wait返回持有的互斥锁),一些线程就会运行而且马上阻塞。在第二种情况下,运行线程可以在第 3 步和第 4 步之间获取互斥锁,然后使条件失效,最后释放互斥锁。接着,当调用pthread_cond_broadcast时,条件不再为真,线程无需运行。这就是为什么唤醒线程必须重新检查条件,不能仅仅因为pthread_cond_wait返回就假定条件就为真。
第12章
12.1 就像人们首先会猜到的,这并不是一个多线程问题。这些标准I/O例程事实上是线程安全的。我们调用fork时,每个进程获得了标准I/O数据结构的一份副本。程序运行时把标准输出定向到终端时,输出是行缓冲的,所以每次打印一行时,标准I/O库就把该行写到终端上。但是,如果把标准输出重定向到文件的话,则标准输出就是全缓冲的。当缓冲区满或者进程关闭流时,输出才会写到文件。在这个例子中,执行fork时,缓冲区中包含了还未写的几个打印行,所以当父进程和子进程最终冲洗缓冲区中的副本时,最初的复制内容就会写入文件。
12.3 理论上来讲,如果在信号处理程序运行时阻塞所有的信号,那么就能使函数成为异步信号安全的。问题是我们并不能知道调用的某个函数可能并没有屏蔽已经被阻塞的信号,这样通过另一个信号处理程序可能会使该函数变成可重入的。
12.4 在FreeBSD 8.0上,程序抛出core。用gdb的话,可以看到程序初始化过程将调用线程函数,这些函数调用getenv找到环境变量LIBPTHREAD_SPINLOOPS和LIBPTHREAD_YIELDLOOPS的值。然而,我们的线程安全版本的getenv回调pthread库函数会处于一种中间的不一致状态。另外,线程初始化函数会调用 malloc,并在 malloc 中调用 getenv 来查找环境变量MALLOC_OPTIONS的值。
为了避开这个问题,我们可以合理假定程序启动是单线程的,并使用一个标志来指示线程初始化已经通过我们的getenv来完成了。但这个标志为假时,我们版本的getenv会和不可重入版本一样操作(并且避免调用任何pthread函数和malloc)。然后我们提供一个独立的初始化函数来调用 pthread_once,而非从 getenv 里面来调用它。这就要求在调用getenv 之前程序调用我们的初始化函数。这就解决了我们的问题,因为只有程序启动初始化完成后才能进行。当程序调用了我们的初始化函数后,这个版本的getenv就是线程安全的。
12.5 如果希望在一个程序中运行另一个程序,还需要fork(即在调用exec之前)。
12.6 图C-13给出了使用select实现线程安全的sleep函数,延迟一定数量的时间。它是线程安全的,因为它并不使用任何未经保护的全局或静态数据,并且只调用其他线程安全的函数。
图C-13 sleep的线程安全实现
12.7 很多时候条件变量的实现都使用互斥锁来保护它的内部结构。由于这是实现细节,因而通常是被隐藏起来的,所以在fork处理程序中没有可移植的方法获取或释放锁。既然在调用fork后并不能确定条件变量中的内部锁状态,所以在子进程中使用条件变量是不安全的。
第13章
13.1 如果进程调用chroot,它就不能打开/dev/log。解决的办法是,守护进程在调用chroot之前调用选项为LOG_NDELAY的openlog。它打开特殊设备文件(UNIX域数据报套接字)并生成一个描述符,即使调用了 chroot 之后,该描述符仍然是有效的。这种场景在诸如ftpd(文件传输协议守护进程)这样的守护进程中出现,为了安全起见,专门调用了chroot,但仍需要调用syslog来对出错条件记录日志。
13.4 图C-14展示了一种解决方案。
图C-14 调用daemonize然后获得登录名
其结果依赖于不同的系统实现。daemonize关闭所有打开文件描述符,然后向/dev/null再打开前3个。这意味着进程不再有控制终端,所以getlogin不能在utmp文件中看到进程的登录项。于是在Linux 3.2.0 和Solaris 10中,我们发现守护进程没有登录名。
但是在FreeBSD 8.0和Mac OS X 10.6.8中,登录名是由进程表维护的,并且在执行fork时复制。也就是说,除非其父进程没有登录名(如系统自引导时调用 init),否则进程总能获得其登录名。
第14章
14.1 测试程序如图C-15所示。
图C-15 判断记录锁的行为
在FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8上,记录锁的行为是相同的,后增加的读者可使未决的写者不断等待。运行该程序得到
child 1: obtained read lock on file
child 2: obtained read lock on file
child 3: can't set write lock: Resource temporarily unavailable
child 3 about to block in write-lock...
parent: obtained additional read lock while write lock is pending
killing child 1...
child 1: exit after pause
killing child 2...
child 2: exit after pause
killing child 3...
child 3: can't write-lock file: Interrupted system call
14.2 大多数系统将数据类型fd_set定义为只包含一个成员的结构,该成员为一个长整型数组。
数组中每一位(bit)对应于一个描述符。4个FD_宏通过开、关或测试指定的位对这个数组进行操作。
将之定义为一个包含数组的结构而不仅仅是一个数组的原因是:通过 C 语言的赋值语句,可以使fd_set类型的变量相互赋值。
14.3 大多数系统允许用户在包括头文件<sys/select.h>前定义常量FD_SETSIZE。例如,我们可以写下面这样的代码来定义fd_set数据类型,使其可以包含2 048个描述符:
#define FD_SETSIZE 2048
#include <sys/select.h>
遗憾的是,事情并非如此简单。为了在现代系统使用该技术,我们需要做以下几件事情。
(1)在包含任何头文件之前,我们需要定义哪种符号来防止包含<sys/select.h>。一些系统会使用一个单独的符号来保护fd_set类型的定义,我们也需要如此定义。
例如,在FreeBSD 8.0中,我们需要定义_SYS_SELECT_H_来防止包含<sys/select.h>,定义_FD_SET来防止包含fd_set数据类型的定义。
(2)有时,为了和旧应用程序兼容,<sys/types.h>定义了fd_set 的大小,所以我们必须首先包含它,然后去掉FD_SETSIZE的定义。注意,一些系统用__FD_SETSIZE来代替。
(3)想能够使用 select 时,我们需要重新定义 FD_SETSIZE(或__FD_SETSIZE)来最大化文件描述符的数量。
(4)我们需要取消定义第一步定义的符号。
(5)最终,我们能够包含<sys/select.h>。
在运行程序之前,我们需要配置系统允许我们打开所需的文件描述符数量,这样我们能够实际利用的文件描述符数量达到FD_SETSIZE个。
14.4 下面列出了功能类似的函数。
没有与sigfillset对应的FD_xxx函数。对信号量集来说,指向信号量集的指针总是第一个参数,信号编号是第二个参数。对于描述符来说,描述符编号是第一个参数,指向描述符集的指针是第二个参数。
14.5 利用select实现的程序见图C-16。
图C-16 用select实现sleep_us函数
利用poll实现的程序见图C-17。
图C-17 用poll实现sleep_us函数
如BSD usleep(3)手册页中所说明的,usleep使用nanosleep函数,该函数没有与调用进程设置的定时器交互。
14.6 不行。我们可以使TELL_WAIT创建一个临时文件,其中1个字节用做父进程的锁,另外1个字节用做子进程的锁。WAIT_CHILD 使得父进程等待获取子进程字节上的锁, TELL_PARENT使得子进程释放子进程字节上的锁。但是问题在于,调用fork会释放所有子进程中的锁,使得子进程开始运行时不具有任何它自己的锁。
14.7 图C-18中示出了一种解决方法。
图C-18 用非阻塞写计算管道的容量
下表列出了在本书所述的4种平台上计算出来的值。
这些值可能与对应的PIPE_BUF 值不同,其原因是,PIPE_BUF 被定义为可被自动原子地写至一个管道的最大数据量。这里,我们计算的是一个管道独立于任何原子性限制可保持的数据量。
14.10 图 14-27 中的程序是否更新输入文件的上一次访问时间依赖于操作系统以及文件所属的文件系统的类型。在所有 4 种平台中,当文件具有给定操作系统默认的文件系统类型,上一次访问时间就会更新。
第15章
15.1 如果管道的写端总是不关闭,则读者就决不会看到文件结束符。分页程序就会一直阻塞在读标准输入。
15.2 父进程向管道写完最后一行以后就终止,当父进程终止时管道的读端自动关闭。但是由于子进程(分页程序)要等待输出的页,所以父进程可能比子进程领先一个管道缓冲区。如果正在运行的是一个可对命令行进行编辑的交互式shell,如Korn shell,那么当父进程终止时,shell 多半会改变终端的模式并打印一个提示。这个无疑会影响已经对终端模式进行修改的分页程序(由于大部分分页程序在等待处理下一个页面时将终端置为非正规模式)。
15.3 因为执行了shell,所以popen返回一个文件指针。但是shell不能执行不存在的命令,因此在标准错误上打印下面信息后终止:
sh: line 1: ./a.out: No such file or directory
其退出状态为127(该值取决于shell的类型)。pclose返回该命令的终止状态,这如同从waitpid返回一样。
15.4 当父进程终止时,用shell看它的终止状态。对于Bourne shell、Bourne-again shell和Korn shell,所用的命令是echo $?,打印的结果是128加信号编号。
15.5 首先加入下面的声明:
FILE *fpin, *fpout;
然后用fdopen关联管道描述符和标准I/O流,并将流设置为行缓冲的。在从标准输入读的while循环之前做此工作。
if ((fpin = fdopen(fd2[0], "r")) == NULL)
err_sys("fdopen error");
if ((fpout = fdopen(fd1[1], "w")) == NULL)
err_sys("fdopen error");
if (setvbuf(fpin, NULL, _IOLBF, 0) < 0)
err_sys("setvbuf error");
if (setvbuf(fpout, NULL, _IOLBF, 0) < 0)
err_sys("setvbuf error");
while循环中的write和read用下面的语句代替:
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
if (fgets(line, MAXLINE, fpin) == NULL) {
err_msg("child closed pipe");
break;
}
15.6 system函数调用了wait,终止的第一个子进程是由popen产生的。因为该子进程不是system创建的,所以它将再次调用wait并一直阻塞到sleep完成。然后system返回。当pclose调用wait时,由于没有子进程可等待所以返回出错,导致pclose也返回出错。
15.7 尽管具体细节会随平台不同而不同(见图C-19),但是 select表明描述符是可读的。调用read 读完所有的数据后,返回 0 就表明到达了文件尾端。但是对于 poll 来说,若返回POLLHUP 事件,则表明也许仍有数据可读。但是一旦读完了所有的数据,read 就返回 0表明到达了文件尾端。在读完了所有的数据后,POLLIN事件就不会再返回了,即使需要再调用一次read以接收文件尾端通知(返回值为0)。
图C-19 select和poll的管道行为
图C-19中所示的条件包括R(可读)、W(可写)、E(异常)、HUP(挂断)、ERR(错误)和INV(无效文件描述符)。对于引用已被读者关闭的管道的输出描述符来说,select表明该描述符是可写的。但当我们调用write时,产生SIGPIPE信号。如果忽略该信号或从其信号处理程序中返回,write就会失败,将error设置成EPIPE。而对于poll,具体的行为则会根据平台的不同而不同。
15.8 子进程向标准错误写的内容同样也会在父进程的标准错误中出现。只要在cmdstring中包含shell重定向2>&1,就可以将标准错误发回给父进程。
15.9 popen函数fork一个子进程,子进程执行shell。然后shell再调用fork,最后由shell的子进程执行命令串。当cmdstring终止时,shell恰好在等待该事件。然后shell退出,而这一事件又是pclose中的waitpid所等待的。
15.10 解决的办法是打开(open)FIFO两次:一次读;一次写。我们决不会使用为写而打开的描述符,但是使该描述符打开就可在客户数从1变为0时,阻止产生文件尾端。打开FIFO两次需要注意下列操作方式(如非阻塞open所要求的):第一次以非阻塞、只读方式open;第二次以阻塞、只写方式open。(如果先用非阻塞、只写方式open,将返回错误。)然后关闭读描述符的非阻塞属性。参见图C-20所示的代码。
图C-20 以非阻塞方式打开FIFO进行读、写操作
15.11 随意读取现行队列中的消息会干扰客户进程-服务器进程协议,导致丢失客户进程请求或者服务器进程的响应。只要知道队列的标识符或者该队列允许所有的用户读,进程就可以读队列。
15.13 由于服务器进程和各客户进程可能会将段连接到不同的地址,所以在共享存储段中决不会存储实际物理地址。相反,当在共享存储段中建立链表时,链表指针的值会设置为共享存储段内另一对象的偏移量。偏移量为所指对象的实际地址减去共享存储段的起始地址。
15.14 图C-21显示了相关的事件。
图C-21 图15-33中父进程和子进程之间的交替过程
第16章
16.1 图C-22显示了一个打印系统字节序的程序。
图C-22 判断系统字节序
16.3 对于我们将要监听的每个端点,需要绑定到一个合适的地址,并对应每个描述符在fd_set结构中写一条记录。然后使用select等待从多个端点来的连接请求。回忆16.4节,当一个连接请求达到时,一个被动的端点将会变得可读。当一个连接请求真的到达时,我们接受该请求,并如以前一样处理。
16.5 在main过程中,通过调用我们的signal函数(见图10-18)来捕捉SIGCHLD,该函数将使用sigaction来安装处理程序指定可重启的系统调用选项。下一步,从serve函数中删除waitpid调用。当fork完子进程来处理请求后,父进程关闭新的文件描述符并继续监听新的连接请求。最后,需要一个针对于SIGCHLD的信号处理程序,如下:
void
sigchld(int signo)
{
while (waitpid((pid_t)-1, NULL, WNOHANG) > 0)
;
}
16.6 为了允许异步套接字I/O,需要使用F_SETOWN fcntl命令建立套接字所有权,然后使用FIOASYNC ioctl 命令允许异步信号。为了不允许异步套接字 I/O,只要简单地禁用异步信号即可。我们混合使用 fcntl 和 ioctl 命令的理由是,想找到最可移植的方法。代码如图C-23所示。
第17章
图C-23 允许与不允许异步套接字I/O
17.1 常规管道提供了一个字节流接口。为了确定消息边界,我们必须增加给每个消息增加一个头部来指示长度。但这个仍涉及两个额外的复制操作:一个是写入至管道,另一个是从管道读出。更加有效的方法是仅将管道用于告知主线程有一个新消息可用。我们用单个字节用作通知。采用这种方法,我们需要移动mymesg结构到threadinfo结构,并使用一个互斥量(mutex)和一个条件变量(condition variable)来防止辅助线程在主线程完成之前重新使用mymesg结构。解决方案如图C-24所示。
图C-24 使用管道的XSI消息轮询
17.3 声明指定了标识符集合的属性(如数据类型)。如果声明也导致分配了存储单元,那么这就是定义。
在头文件opend.h中,我们用extern存储类声明了3个全局变量,这时并没有为它们分配存储单元。在文件main.c中,我们定义了3个全局变量。有时,我们也会在定义全局变量时就初始化它,但通常是使用C的默认值。
17.5 select和poll返回就绪的描述符个数作为函数值。当将这些就绪描述符都处理完后,操作client数组的循环就可以终止。
17.6 建议的解决方案存在的第一个问题是,在文件可能发生变化的地方,调用 stat 和调用unlink之间存在竞争。第二个问题是,如果名字是一个指向UNIX域套接字文件的符号链接,那么stat会报告名字是一个套接字(回想一下后面跟一个符号链接的stat函数),但是调用 unlink 时,实际上我们是删除了这个符号链接而不是套接字文件。为了解决第二个问题,应该使用lstat而不是stat,但这解决不了第一个问题。
17.7 第一种选择是将两个文件描述符在一个控制消息中的发送,每一个文件描述符存储在相邻的内存位置中。下面的代码展示了这种方法:
struct msghdr msg;
struct cmsghdr *cmptr;
int *ip;
if ((cmptr = calloc(1, CMSG_LEN(2*sizeof(int)))) == NULL)
err_sys("calloc error");
msg.msg_control = cmptr;
msg.msg_controllen = CMSG_LEN(2*sizeof(int));
/* continue initializing msghdr... */
cmptr->cmsg_len = CMSG_LEN(2*sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
ip = (int *)CMSG_DATA(cmptr);
*ip++ = fd1;
*ip = fd2;
这种方法在本书中涉及的4个平台上全都可以工作。第二种选择是将两个独立的cmsghdr结构打包到一个消息中。
struct msghdr msg;
struct cmsghdr *cmptr;
if ((cmptr = calloc(1, 2*CMSG_LEN(sizeof(int)))) == NULL)
err_sys("calloc error");
msg.msg_control = cmptr;
msg.msg_controllen = 2*CMSG_LEN(sizeof(int));
/* continue initializing msghdr... */
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmptr) = fd1;
cmptr = CMPTR_NXTHDR(&msg, cmptr);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(cmptr) = fd2;
与第一种方法不同,这个方法只在FreeBSD 8.0上能工作。
第18章
18.1 注意,由于终端是非规范模式的,所以必须要用换行符而不是回车符终止reset命令。18.2 它为128个字符建了一张表,根据用户的要求设置最高位(奇偶校验位)。然后使用8位I/O处理奇偶位的产生。
18.3 如果你使用的是窗口终端,那么你无需登录两次。在两个分开的窗口之间,你可以做这样的实验。在Solaris中,运行stty -a,并且将标准输入重定向到运行vi的终端。结果显示vi设置MIN为1、TIME为1。read调用会一直等待,直到至少键入一个字符,但是该字符输入后,只对后继的字符等待十分之一秒即返回。
第19章
19.1 telnetd 和 rlogind 两个服务器均以超级用户权限运行,所以它们都可以成功地调用chown和chmod。
19.2 执行pty -n stty –a以避免伪终端从设备的termios结构和winsize结构初始化。
19.4 很不幸,fcntl的F_SETFL命令不允许改变读写状态。
19.5 有3个进程组:(1)登录shell,(2)pty父进程和子进程,(3)cat进程。前两个进程组组成了一个会话,其中,登录shell为会话首进程。第二个会话仅包含cat进程。第一个进程组(登录shell)是后台进程组,其他两个进程组是前台进程组。
19.6 首先,当cat从其行规程模块接收到文件结束符时会终止。这导致PTY从设备终止,进而导致PTY主设备终止。接着,对于正从PTY主设备读取的pty父进程产生一个文件结束符。该父进程将SIGTERM信号发送给子进程,于是子进程终止。(子进程不捕捉该信号。)
最后,父进程调用main 函数尾端的exit(0)。
图8-29所示程序的相关输出为:
cat e = 270, chars = 274, stat = 0:
pty e = 262, chars = 40, stat = 15: F X
pty e = 288, chars = 188, stat = 0:
19.7 这可通过使用shell的echo命令和date(1)命令实现,它们都在一个子shell中:
#!/bin/sh
(echo "Script started on " `date`;
pty "${SHELL:-/bin/sh}";
echo "Script done on " `date`) | tee typescript
19.8 PTY从设备上的行规程能够回显,所以pty从其标准输入所读取的以及写向PTY主设备的按默认都回显。尽管程序(ttyname)从不读取数据,但是该回显也可通过从设备上的行规程模块实现。
第20章
20.1 _db_dodelete中保守的加锁操作是为了避免和db_nextrec发生竞争条件。如果没有使用写锁保护_db_writedat 调用,则有可能在db_nextrec读某个记录时,该记录已被删除:db_nextrec 首先读入一个索引记录,判定该记录非空,接着读数据记录,但是在它调用_db_readidx和_db_readdat之间,该记录却可能被_db_dodelete删除了。
20.2 假定db_nextrec调用_db_readidx,它将记录的键读入索引缓冲区。然后,该进程被内核调度进程暂停,另一个进程运行,它刚好调用db_delete删除了这一条记录,使得索引文件和数据记录文件中对应部分都被清空。当第一个进程恢复执行并调用_db_readdat(在db_nextrec函数体中)时,返回的是空数据记录。db_nextrec中的读锁使得读入索引记录的过程和读入数据记录的过程是一个原子操作(对于其他操作同一数据库的合作进程而言)。
20.3 强制性锁对其他的读进程和写进程产生了影响。在_db_writeidx和_db_writedat设置的锁被解除之前,其他的读操作和写操作都将被阻塞。
20.5 在写索引记录之前写数据记录,通过这一方法来防止如下情形:若该进程在两次写之间被杀死从而产生不正常的记录。如果进程先写索引记录,而在写数据记录之前被杀死,那么就会得到一个有效的索引记录,但它却指向一个无效的数据记录。
第21章
21.5 这里有一些提示。有两个地方可以检查队列中的作业:打印守护进程的队列和网络打印机的内部队列。注意,不要让一个用户可以取消其他用户的打印作业。当然,超级用户可以取消任何作业。
21.7 不需要唤醒守护进程,因为知道需要打印一个文件时才需要重读配置文件。printer_thread函数在每次向打印机发送作业之前检查是否需要重读配置文件。
21.9 需要使用null字节来终止写到作业文件的字符串(strlen在计算字符串长度时不包含终止null字节)。有两种简单的方法:要么对写入的字节数加1,要么使用dprintf函数而不是调用sprintf和write。