附录C 习题答案
时间:2005-08-31 来源:mantou
第一章
1.1 利用ls命令中的下面两个选项:-i--显示文件或目录的i节点数目(关于i节点
在4.14节中会详细讨论);-d--如果参数是一目录,只列出其名字,而不是目录中
的所有文件。
执行下面命令的结果为:
$ ls -ldi /etc/. /etc/..
3077 drwxr-sr-x 7 bin 2048 Aug 5 20:12 /etc/./
2 drwxr-xr-x 13 root 512 Aug 5 20:11 /etc/../
$ls -ldi /. /..
2 drwxr-xr-x 13 root 512 Aug 5 20:11 /./
2 srwxr-xr-x 13 root 512 Aug 5 20:11 /../ {.和..的的i节点数均为2
的i节点数均为2
1.3 假如perror的ptr参数是一个指针,则perror就可以改变ptr所指串的内容。所
以利用限定词const使得perror不能修改ptr所指的串。而strerror的参数是错误号
,由于其是整数类型并且C传递的是参数值,因此strerror不能修改参数的值,也
就没有必要使用const属性。
1.4 调用fflush,fprintf和vprintf函数会改变errno的值。如果它的值变了但没
有保存,则最终显示的错误信息是不正确的。
在过去开发许多程序中都可以发现不保存errno的情况,典型的打印出的错误信息
是"Not a typewriter."。在5.4节中标准I/O库根据标准I/O流是否指向终端设备而
改变流的缓存器。istty(11.9节)通常用来判断流是否指向终端设备,如果流不指
向终端设备,errno可能置为ENOTTY,从而引起该错误。程序C.1表现了这个属性。
#include <stdio.h>
/*
* The following prints errno=25 (ENOTTY) under 4.3BSD and SVR2,
* when stdout is redirected to a file.
* Under SVR4 and 44BSD it works OK.
*/
int
main()
{
int fd;
extern int errno;
if ( (fd = open("/no/such/file", 0)) < 0) {
printf("open error: ");
printf("errno = %d ", errno);
}
exit(0);
}
程序 C.1 errno和printf的交互作用
执行上面的程序,结果为:
$ grep BSD /etc/motd
4.3 BSD UNIX #29: Thu Mar 29 11:14:13 MST 1990
$ a.out
open error: error = 2 {工作正常,stdout是一个终端}
$ a.out > temp.foo
$ cat temp.foo
open error: error = 25 {错误}
1.5 2038年。
1.6 大约248天。
第二章
2.1 下面是4.3+BSD中使用的技术。在<machine/ansi.h> 中,用大写字母定义可以
在多个头文件中出现的基本数据类型。例如:
#ifndef _ANSI_--H_
#define _ANSI_H_
#define _CLOCK_T_ unsigned long
#define _SIZE_T_ unsigned int
…
#endif /*_ANSI_H_*/
以下面的顺序可以在这6个头文件中分别定义size_t。
#ifdef _SIZE_T_
typedef _SIZE_T_ size_t;
#undef _SIZE_T_
#endif
这样,实际上只执行一次typedef。
第三章
3.1 所有的磁盘I/O都要经过内核的块缓存器,唯一例外的是对原始磁盘设备的I/
O,但是我们不考虑这种情况(Bach [1986] 中的第三章讲述了这种缓存器的操作)
。既然read或write的数据都要被内核缓存,那么术语"无缓存装置的I/O"指的是在
用户的进程中对这两个函数不会自动缓存,每次read或write就要进行一次系统调
用。
3.3 每次调用open函数就分配一个文件表项,如果两次打开的是相同的文件,则两
个文件表项指向相同的v节点。调用dup引用已存在的文件表项(此处指fd1的文件表
项),见图C.1。当F_SETFD作用于fd1时,只影响fd1的文件描述符标志;F_SETFL作
用于fd1时,则影响fd1及fd2的文件描述符标志。
图C.1 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,然后执行dups将标准输出复制到描述符2(标准错误
)上,其结果是将标准输出和标准错误设置为相同的文件,即描述符1和2指向相同
的文件表项。
而对于命令行
a.out 2 > &1 >outfile
由于首先执行dups,所以描述符2成为终端(假设命令是交互执行的),标准输出重
定向到outfile。结果是描述符1指向outfile的文件表项,描述符2指向终端的文件
表项。
3.6 这种情况之下,仍然可以用lseek和read函数读文件中任何一处的内容。但是
write函数在写数据之前会自动将文件偏移量设置为文件尾,所以写文件时只能从
文件尾开始,不能在任意位置。
第四章
4.1 stat函数总是顺一个符号连接向前,所以修改后的程序不会显示文件类型是"
符号连接"。例如:/bin是/usr/bin的一个符号连接,但是stat函数的结果只显示
/bin是一个目录,而不说明它是一个符号连接。若一个符号连接指向一不存在的文
件,则stat出错返回。
4.2 将下面的几行语句加入<ourhdr.h>
#if defined ( S_IFLNK ) && !defined ( S_ISLNK )
#define S_ISLNK ( mode ) ((( mode ) & S_IFMT ) == S_IFLNK )
#endif
这是一个我们编写的头文件如何屏蔽某些系统差别的实例。
4.3 关闭了该文件的所有存取权限。
$ umask 777
$ data > temp.foo
$ ls -l temp.foo
---------- l stevens 29 Jan 14
6:39 temp.foo
4.4 下面的命令表示关闭用户读权限的情况。
$ data > foo
$ chmod u-r foo {关闭用?nbsp;
读权限}
$ ls -l foo
验证文件的权限}
--w-rw-r-- l stevens 29 Jul 31 09:00
oo
$ cat foo
{读文件}
cat: foo: Permission denied
4.5 如果用open或creat创建已经存在的文件,则该文件的存取权限不变。通过程
序4.3可以验证这点。
$ rm foo bar
删除文件}
$ data > foo
创建文件}
$ data > bar
$ chmod a-r foo bar {关闭所?nbsp;
的读权限}
$ ls -l foo bar {验证其?nbsp;
限}
--w--w---- l stevens 29 Jul 31 10:47
ar
--w--w---- l stevens 29 Jul 31 10:47
oo
$ a.out
运行程序4.3}
$ ls -l foo bar {检查文件的权限?nbsp;
大小}
--w--w---- l stevens 0 Jul 31 10:47 bar
--w--w---- l stevens 0 Jul 31 10:47
oo
可以看出存取权限没有改变,但是文件长度缩短了。
4.6 目录的长度从来不会是0,因为它总是包含 .和 ..两项。符号连接的长度指其
路径名的包含的字符数,由于路径名中至少有一个字符,所以长度也不为0。
4.8 当创建新的core文件时,内核对其存取权限有一个默认设置,在本例中是:r
w-r--r--。这一缺省值可能会也许不会被umask的值修改。Shell对创建的重定向的
新文件也有一个默认的访问权限,本例中为rw-rw-rw-。这个值总是被当前的umas
k修改,在本例中umask为02。
4.9 不能使用du的原因是它需要文件名或目录名作为参数,例如:
du tempfile
du .
只有当unlink函数返回时才释放tempfile的目录项,du命令没有计算仍然被temp
file占用的空间。在本例中只能使用df命令察看文件系统中实际可用的自由空间。
4.10 如果被unlink的链接不是该文件的最后一个链接,该文件不会删除。此时,
unlink函数更新文件的状态改变时间。如果是最后一个链接,该文件将被物理删除
。这时再去更新文件的状态改变时间就没有意义,因为包含文件所有信息的i节点
将会随着文件的删除而被释放。
4.11 用opendir打开一个目录后,循环调用函数dopath。假设opendir使用一个文
件描述符,并且只有处理完目录后调用closedir才释放描述符,这就意味着每次打
开目录就要降一级使用另外一个描述符。所以系统可打开文件数就限制了文件系统
中树的深度。SVR4中的ftw允许调用者指定使用的描述符数,这隐含着该实现可以
关闭描述符并且重用它们。
4.13 chroot函数用于辅助Internet文件传输程序(FTP)中的安全性。系统中没有帐
号的用户(也称为匿名FTP)放在一个单独的目录下,利用chroot将此目录当作新的
根目录就可以阻止用户访问此目录以外的文件。
chroot也用于在另一台机器上构造一文件系统层次结构的一个副本,然后修改此
副本,但不更改原来的文件系统。这可用预测是新软件包装等场合。Chroot只能有
超级用户使用,一旦更改了一个进程的root,该进程及其后代进程就再也不能恢复
至原先的root。
4.14 首先调用stat函数取得文件的三个时间值,然后调用utime设置期望的值。我
们不希望在调用utime改变的值就是stat中相应的值。
4.15 finger(1)对邮箱调用stat函数,最近一次的修改时间是上一次接收邮件的时
间,最近访问时间是上一次读邮件的时间。
4.16 对cpio来说,既可以改变文件的访问时间(st_atime)和修改时间(st_mtime)
也可以都不改变。cpio的-a选项可以在读文件后重新设置文件的访问时间为读文件
前的时间,所以cpio可以不改变文件的访问时间。另一方面,-m选项将文件的修改
时间保存为原来的修改时间。
对tar来说,在抽取文件时,其缺省方式是复原归档时的修改时间,但是-m选项则
将修改时间设置为抽取文件时的时间。无论tar在何种情况,文件的访问时间均是
抽取文件时的时间。
由于不能修改状态改变时间(utime也只能改变访问时间和修改时间),所以没有将
其保存在文档上。
4.17 read改变了文件访问时间,为了消除这一影响,有些版本的file(1)调用uti
me恢复文件的访问时间,但是这样做会修改文件的状态改变时间。
4.18 内核对目录的深度没有内在的限制,但是如果路径名的长度超出了PATH_MAX
有许多命令会失败。程序C.2创建了一个深度为100的目录树,每一级目录名有45个
字符。利用getcwd可以得到第100级目录的绝对路径名(需要多次调用realloc申请
一个足够大的缓存)。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "ourhdr.h"
#define DEPTH 100 /* directory depth */
#define MYHOME "/home/stevens"
#define NAME "alonglonglonglonglonglonglonglonglonglongname"
int
main(void)
{
int i, size;
char *path;
if (chdir(MYHOME) < 0)
err_sys("chdir error");
for (i = 0; i < DEPTH; i++) {
if (mkdir(NAME, DIR_MODE) < 0)
err_sys("mkdir failed, i = %d", i);
if (chdir(NAME) < 0)
err_sys("chdir failed, i = %d", i);
}
if (creat("afile", FILE_MODE) < 0)
err_sys("creat error");
/*
* The deep directory is created, with a file at the leaf.
* Now let's try and obtain its pathname.
*/
path = path_alloc(&size);
for ( ; ; ) {
if (getcwd(path, size) != NULL)
break;
else {
err_ret("getcwd failed, size = %d", size);
size += 100;
if ( (path = realloc(path, size)) == NULL)
err_sys("realloc error");
}
}
printf("length = %d %s ", strlen(path), path);
exit(0);
}
程序C.2 创建深度目录
运行后得到:
$ a.out
getcwd failed, size = 1025: Result too large
getcwd failed, size = 1125: Result too large
… { 33行}
getcwd failed, size = 4525: Result too large
length = 4613
{显示4613字节的路径名}
但是由于文件名太长了,不能用tar或cpio对该目录建立档案文件,而且也不能用
rm -r命令删除该目录。(我们怎样才能删除该目录树?)
4.19 /dev目录关闭了一般用户的写权限,所以用户不能删除目录中的文件,即un
link失败。
第五章
5.2 fgets函数读入数据,直到行结束或缓冲区满(当然会留出一个字节存放'')
。同样,fputs只负责将缓冲区的内容输出,而并不考虑缓冲区中是否包含换行符
。所以,如果将MAXLINE设得很小,这两个函数仍然会正常工作,只不过被执行的
次数要比MAXLINE值较大的时候多。
5.3 当printf没有输出任何字符时,如:printf("") ,返回0。
5.4 这是一个比较常见的错误。getc以及getchar的返回值是整型,而不是字符型
。由于EOF经常定义为-1,那么如果系统使用的是有符号的字符类型,程序还可以
正常工作。但如果使用的是无符号字符类型,那么返回的EOF被保存到字符c后将不
再是-1,所以,程序会进入死循环。
5.5 5个字符长的前缀、4个字符长的进程内唯一标识再加5个字符长的系统内唯一
标识(进程ID)刚好组成14位的UNIX传统文件长度限制。
5.6 使用方法为:先调用fflush后调用fsync,fsync所使用的参数由fileno函数获
得。如果不调用fflush,所有的数据仍然在内存缓冲区中,此时调用fsync将没有
任何效果。
5.7 当程序交互运行时,标准输入输出设备均为行缓冲方式。每次调用fgets时标
准输出设备将自动刷清。
第六章
6.1 在SVR4系统中,用户手册中讲述了获取阴影密码文件的函数。我们不能使用6
.2节所述函数返回的pw_passwd变量来比较加密的口令。正确的方法是使用阴影密
码文件中对应用户的加密口令来进行比较。
在4.3+ BSD系统中,密码文件的阴影是自动建立的。仅当调用者的用户ID为0时,
getpwnam或getpwuid函数返回的passed结构中的pw_passwd字段才包含有加密的口
令。
6.2 在SVR4系统中,程序C.3将输出加密的口令。当然,除非有超级用户权限,否
则调用getspnam将返回EACCESS错误。
#include <sys/types.h>
#include <shadow.h>
#include "ourhdr.h"
int
main(void) /* SVR4 version */
{
struct spwd *ptr;
if ( (ptr = getspnam("stevens")) == NULL)
err_sys("getspnam error");
printf("sp_pwdp = %s ",
ptr->sp_pwdp == NULL || ptr->sp_pwdp[0] == 0 ?
"(null)" : ptr->sp_pwdp);
exit(0);
}
程序C.3 在SVR4系统中输出加密的口令
在4.3+ BSD系统中,具有超级用户权限时,程序C.4将输出加密的口令。否则pw_
passed的返回值为星号(*)。
#include <sys/types.h>
#include <pwd.h>
#include "ourhdr.h"
int
main(void) /* 44BSD version */
{
struct passwd *ptr;
if ( (ptr = getpwnam("stevens")) == NULL)
err_sys("getpwnam error");
printf("pw_passwd = %s ",
ptr->pw_passwd == NULL || ptr->pw_passwd[0] == 0
?
"(null)" : ptr->pw_passwd);
exit(0);
}
程序C.4 在4.3+ BSD系统中输出加密的口令
6.4
#include <time.h>
#include "ourhdr.h"
int
main(void)
{
time_t caltime;
struct tm *tm;
char line[MAXLINE];
if ( (caltime = time(NULL)) == -1)
err_sys("time error");
if ( (tm = localtime(&caltime)) == NULL)
err_sys("localtime error");
if (strftime(line, MAXLINE, "%a %b %d %X %Z %Y ", tm) == 0)
err_sys("strftime error");
fputs(line, stdout);
exit(0);
}
程序C.5 以date(1)的格式输出日期和时间
程序C.5的运行结果如下:
$ echo $TZ
MST7
$ a.out
Wed Jan 15 06:48:57 MST 1992
$ TZ=EST5EDT a.out U.S.East Coast
Wed Jan 15 08:49:06 EST 1992
$ TZ=JST-9 a.out Japan
Wed Jan 15 22:49:12 JST 1992
第七章
7.1 原因在于printf的返回值(输出的字符数)变成了main函数的返回码。当然,并
不是所有的系统都会出现该情况。
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。但是ANSI C并不保证0值与浮点0或空指
针的值相同。
7.7 只有通过exec函数执行一个程序时,才会分配堆和堆栈。
7.8 可执行文件包含了用于调试core文件的符号表信息,用strip(1)可以删除这些
信息,对两个a.out文件执行这条命令,它们的大小减为98304和16384。
7.9 没有使用共享库时,可执行文件的大部分都被标准I/O库所占用。
7.10 这段代码不正确。因为在if语句中定义了自动变量val,所以当if中的复合语
句结束时,该变量就不存在了,但是在if语句之外又用指针引用已经不存在的自动
变量val。
第七章
7.1 原因在于printf的返回值(输出的字符数)变成了main函数的返回码。当然,并
不是所有的系统都会出现该情况。
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。但是ANSI C并不保证0值与浮点0或空指
针的值相同。
7.7 只有通过exec函数执行一个程序时,才会分配堆和堆栈。
7.8 可执行文件包含了用于调试core文件的符号表信息,用strip(1)可以删除这些
信息,对两个a.out文件执行这条命令,它们的大小减为98304和16384。
7.9 没有使用共享库时,可执行文件的大部分都被标准I/O库所占用。
7.10 这段代码不正确。因为在if语句中定义了自动变量val,所以当if中的复合语
句结束时,该变量就不存在了,但是在if语句之外又用指针引用已经不存在的自动
变量val。
第八章
8.1 用下面几行代替程序8.2中调用printf的语句。
i = printf ("pid = %d, glob = %d, var = %d ", getpid( ), glob, var);
sprintf (buf, "%d ", i);
write (STDOUT_FILENO, buf, strlen(buf));
注意要定义变量i和buf。
这里假设子进程调用exit时只关闭标准I/O流,并不关闭与标准输出相关的文件描
述符STDOUT_FILENO。有些版本的标准I/O库会关闭与标准输出相关的文件描述符从
而引起写失败,这种情况就调用dup将标准输出复制到另一个描述符,write则使用
新复制的文件描述符。
8.2 我们可以通过程序C.6来说明这个问题。
#include <sys/types.h>
#include "ourhdr.h"
static void f1(void), f2(void);
int
main(void)
{
f1();
f2();
_exit(0);
}
static void
f1(void)
{
pid_t pid;
if ( (pid = vfork()) < 0)
err_sys("vfork error");
/* child and parent both return */
}
static void
f2(void)
{
char buf[1000]; /* automatic variables */
int i;
for (i = 0; i < sizeof(buf); i++)
buf[i] = 0;
}
程序C.6 错误使用vfork的例子
图C.2 调用vfork时的栈帧
当函数f1调用vfork时,父进程的堆栈指针就指向f1的栈帧,见图C.2。vfork使得
子进程先执行然后从f1返回,接着子进程调用f2并且覆盖了f1的堆栈区间,在f2中
子进程将自动变量buf的值置为0,即将堆栈中的1000个字节的值都置为0。从f2返
回后父进程继续执行调用_exit,这时堆栈中main以下的内容已经被f2修改了,但
是父进程仍然以为调用了vfork后从f1返回。返回信息虽然保存在堆栈中,但是可
能已经被子进程修改了。对这个例子,父进程继续执行的结果要依赖于实际的Uni
x系统。(如:返回信息保存在堆栈的具体位置,修改动态变量时覆盖了哪些信息等
等。)通常的结果是一个core文件。
8.3 程序8.7中我们先让父进程输出,但是当父进程输出完毕子进程要输出时,我
们要让父进程终止。是否父进程先终止或是子进程先执行输出要依赖于内核对两个
进程的调度。shell在父进程终止后会开始执行其它程序,这样也许仍会影响子进
程。要避免这种情况就是在子进程完成输出后才终止父进程。用下面的语句替换程
序中fork后面的代码。由于只有终止父进程才能开始下一个程序,所以不会出现上
面的情况。
else if ( pid == 0 ) {
WAIT_PARENT( ) ;
/*parent goes first */
charatatime ( "output form child " ) ;
TELL_PARENT ( getppid( )) ;
* tell parent we're done */
} else {
charatatime ( "output from parent " ) ;
TELL_CHILD ( pid ) ;
* tell child we're done */
WAIT_CHILD ( ) ;
/* wait for child to finish */
8.4 对argv[2]打印的是相同的值(/home/stevens/bin/testinterp)。原因是ex
eclp在结束时调用了execve,并且与直接调用execl的路径名相同。
8.5不提供返回保存的用户ID的函数,我们必须在进程开始时保存有效的用户ID。
8.6 程序C.7创建了一个zombie。
#include "ourhdr.h"
int
main(void)
{
pid_t pid;
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) /* child */
exit(0);
/* parent */
sleep(4);
system("ps");
exit(0);
}
程序C.7 创建一个zombie并用ps查看其状态
执行程序结果如下(ps(1)用"Z"表示Zombie):
$ a.out
PID TT STAT TIME COMMAND
5940 p3 S 0:00 a.out
5941 p3 Z 0:00 <defunct> the zombie
5942 p3 S 0:00 sh -c ps
5943 p3 R 0:00 ps
第九章
9.1 因为init是login shell的父进程,当login shell终止时它收到SIGCHLD信号
量,所以init进程知道什么时候终端用户注销。
网络login没有包含init,相应的logout项是由一个处理login并监测logout的进
程写的。(本例中为telnetd)
第十章
10.1 当程序第一次接到我们发送给它的信号量就终止了。因为一捕捉到信号量pa
use函数就返回。
10.2 程序C.8实现了raise函数。
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int
raise(int signo)
{
return( kill(getpid(), signo) );
}
程序C.8 raise函数的实现
10.3 见图C.3。
图C.3 longjump前后的堆栈状态
从sig_alrm通过longjmp返回main,有效地避免了继续执行sig_int。
10.4 如果进程在调用alarm和setjmp之间被内核阻塞了,alarm时间走完之后就调
用信号量处理程序,然后调用longjmp。但是由于没有调用setjmp,所以没有设置
env_alrm缓存区。如果longjmp的跳转缓存区没有被setjmp初始化,则说明没有定
义longjmp的操作。
10.5 参见" Implementing Software Timers " by Don Libers ( C user Journa
l, Vol. 8, no. 11, Nov. 1990 )中的例子。
10.7 如果仅仅调用_exit,则进程终止状态就不能表示该进程是由于SIGABRT信号
量而终止的。
10.8 如果信号量是由其它用户的进程发出的,进程必须设置用户的ID为根或者是
接收进程的拥有者,否则kill不能执行。所以实际的用户ID为信号量的接收者提供
了更多的信息。
10.10 对于本书中所用的系统,大约每60-90分钟增加一秒,这个误差是因为每次
调用sleep都要调度一将来的时间事件,但是由于CPU调度,有时我们并没有在事件
发生时被叫醒。另外一个原因是开始运行进程和调用sleep都需要一定的时间。
BSD中的cron每分钟都要取当前时间,它首先设置一个休眠周期,然后在下一分钟
开始时唤醒。大多数调用是sleep(60),偶尔有一个sleep(59)用于在下一分钟同步
。但是若在进程中花费了许多时间执行命令或者系统的负载重调度慢,这时休眠值
可能远小于60。
10.11 在SVR4中,从来没有调用过SIGXFSZ的信号量处理程序,一旦文件的大小达
到1024时,write就返回24。
在4.3+BSD中,文件大小达到1500字节时调用该信号处理程序,write返回-1并且
errno设置为EFBIG。
SunOS4.1.2的情况与SVR4类似,但是调用了该信号量处理程序。
System V在文件大小达到软资源限制时无错返回一个较小的数,而BSD判断文件超
出限制时错误返回,没有写任何数据。
10.12 结果依赖于标准I/O库的实现--fwrite如何处理一个被中断的写。
第十一章
11.1 注意由于终端是非正规模式,所以要用换行符而不是回车终止reset命令。
11.2 它为128个字符建了一张表,根据用户的要求设置奇偶校验位。然后使用8位
I/O处理奇偶位的产生。
11.3 在SVR4中运行stty -a,并且将标准输入重定向到运行vi的终端,结果显示v
i设置MIN为1、TIME为1。reads等待至少敲入一个字符,但是该字符输入后,只对
后继的字符等待十分之一秒即返回。
11.4 在SVR4中使用扩展的通用终端接口。参见AT&T[1991]手册中的temiox(7)。在
4.3+BSD中使用c_cflag字段的CCTS_OFLOW和CRTS_IFLOW标志,参见图11.3。
第十二章
12.1 程序运行正常,不会发生ENOLCK的错误。第一次循环调用writew_lock、wri
te和un_lock。调用un_lock后只保留了第一个字节的锁,第二次循环时,调用wri
tew_lock使得新锁与第一个字节的锁合并,图C.4是第二次循环的结果。
图C.4 第二次循环后锁的状态
每循环一次就扩展一个字节的锁,内核将这些锁合并后就只保持了一个锁,因此
符合锁结构的定义。
12.2 在SVR4、4.3+BSD中,fd_set是只包含一个成员的结构,该成员为一个长整型
数组。数组中每一位对应于一个描述符。四个FD_宏通过开关或测试指定的位来操
纵这个数组。将之定义为一个包含数组的结构而不仅仅是一个数组的原因是:通过
C语言的赋值语句,可以使fd_set类型变量相互赋值。
12.3 在SVR4、4.3+BSD中允许用户包含头文件<sys/types.h> 前定义常数FD_SETS
IZE。例如下面的代码可以使fd_set数据类型包含2048个描述符。
#define FD_SETSIZE 2048
#include <sys/types.h>
12.4 下面的表列出了功能类似的函数。
FD_ZERO sigemptyset
FD_SET sigaddset
FD_CLR sigdelset
FD_ISSET sigismember
没有与sigfillset对应的FD_XXX函数。对信号量来说,指向信号量集合的指针是
第一个参数,信号量数是第二个参数;对于描述符来说,描述符数是第一个参数,
指向描述符集合的指针是第二个参数。
12.5 最多五种信息:数据,数据长度,控制信息,控制信息的长度和标志。
12.6 利用select实现的程序见C.9,利用poll实现的程序见C.10。
#include <sys/types.h>
#include <sys/time.h>
#include <stddef.h>
#include "ourhdr.h"
void
sleep_us(unsigned int nusecs)
{
struct timeval tval;
tval.tv_sec = nusecs / 1000000;
tval.tv_usec = nusecs % 1000000;
select(0, NULL, NULL, NULL, &tval);
}
程序C.9 用select实现sleep_us函数
#include <sys/types.h>
#include <poll.h>
#include <stropts.h>
#include "ourhdr.h"
void
sleep_us(unsigned int nusecs)
{
struct pollfd dummy;
int timeout;
if ( (timeout = nusecs / 1000) <= 0)
timeout = 1;
poll(&dummy, 0, timeout);
}
程序C.10 用poll实现sleep_us函数
BSD中的usleep(3)使用setitimer设置间隔计时器,并且执行8个系统调用。它可
以正确的和调用进程设置的其它计时器交互作用,而且即使捕捉到信号量也不会被
中断。
12.7 不行。我们可以使TELL_WAIT创建一个临时文件,其中一个字节用作为父进程
的锁,另一个字节用作为子进程的锁。WAIT-CHILD使得父进程等待子进程的锁,T
ELL_PARENT使得子进程释放子进程的锁。但是问题在于调用fork后,子进程释放了
所有的锁导致子进程不能具有任何它自己的锁而开始执行。
12.8 用select的方法见程序C.11,使用poll的情况类似。
#include <sys/types.h>
#include <sys/time.h>
#include "ourhdr.h"
int
main(void)
{
int i, n, fd[2];
fd_set writeset;
struct timeval tv;
if (pipe(fd) < 0)
err_sys("pipe error");
FD_ZERO(&writeset);
for (n = 0; ; n++) { /* write 1 byte at a time until pipe is full */
FD_SET(fd[1], &writeset);
tv.tv_sec = tv.tv_usec = 0; /* don't wait at all */
if ( (i = select(fd[1]+1, NULL, &writeset, NULL, &tv)) < 0)
err_sys("select error");
else if (i == 0)
break;
if (write(fd[1], "a", 1) != 1)
err_sys("write error");
}
printf("pipe capacity = %d ", n);
exit(0);
}
程序C.11 用select计算管道的性能
在SVR4和SunOS 4.1.1中使用select和poll计算出的结果等于图2.6的值。在4.3B
SD中使用select计算的结果为3073。
12.9 在SVR4、4.3+BSD和SunOS 4.1.2中,程序12.14确实修改了输入文件的最近一
次访问时间。
第十三章
13.1 如果进程调用chroot就不能打开/dev/log,解决的办法是在chroot之前调用
选项为LOG_NDELAY的openlog。这样即使调用了chroot之后,仍然可以打开特定的
设备文件(UNIX与数据包套接口)并生成一个有效的描述符。
13.3 程序C.12是一种解决方案。
#include "ourhdr.h"
int
main(void)
{
char *ptr, buff[MAXLINE];
daemon_init();
close(0);
close(1);
close(2);
ptr = getlogin();
sprintf(buff, "login name: %s ",
(ptr == NULL) ? "(empty)" : ptr);
write(3, buff, strlen(buff));
exit(0);
}
程序C.12 调用daemon_init获得注册名
结果依赖于不同的系统实现和是否关闭文件描述符1、2和3。关闭描述符影响结果
的原因是:当程序开始执行时与控制终端连接,调用deamon_init后关闭3个描述符
就意味着getlogin没有控制终端,所以不能在utmp文件中看到登录项。
但是在4.3+BSD中,登录名是由进程表维护的,并且可以通过fork复制。也就是说
除非其父进程没有登录名(如系统自引导时调用init),否则进程总能获得其登录名
。
第十四章
14.1 如果写管道端总是不关闭,则读者就决不会看到文件的结束符。页面调度程
序就会一直阻塞在读标准输入。
14.2 父进程向管道写完最后一行以后就终止了,然后读者读到管道的结尾时自动
关闭管道。但是由于子进程(页面调度程序)要等待输出的页,所以父进程可能比子
err_sys ( "setvbuf error" ) ;
while中的read和write用下面的语句代替。
if (fputs ( line, fpout ) == EOF )
err_sys ( "fputs error to pipe" ) ;
if ( fgets ( line, MAXLINE, FPIN ) == NULL ) {
err_msg ( "child close pipe" ) ;
break ;
}
14.6 虽然system函数调用了wait,但是终止的第一个子进程是由popen产生的,所
以它将再次调用wait并一直阻塞到sleep完成,然后system返回。当pclose调用wa
it时,由于没有子进程可等待所以返回出错,导致pclose也返回出错。
14.7 select表明描述符是可读的。调用read读完所有的数据后返回0就表明到了文
件尾。对于poll(假设管道是一个流设备)来说,若返回POLLHUP也许仍有数据可以
读。但是一旦读完了所有的数据read就返回0,即表明到了文件尾。poll读完了所
有的数据后并不返回POLLIN。
对