附录D 精选习题解答

第1章

1.1 这两个进程只需给open函数指定O_APPEND标志,或者给fopen函数指定添加模式。内核保证每次write都将新的数据添加到该文件的末尾。这是可指定的最容易的文件同步形式(APUE第60~61页 [1] 对此有具体的讨论)。当更新文件中已有的数据时,同步问题会变得复杂起来,数据库系统中的情况就是这样。

1.2 典型的定义类似如下:

#ifdef _REENTRANT

#define errno (*_errno())

#else

extern int errno;

#endif

如果_REENTRANT已定义,引用errno时就调用一个名为_errno的函数,该函数返回调用线程的errno变量的地址。该变量可能是作为线程特定数据(UNPv1的23.5节 [2] )存储的。如果_REENTRANT未定义,errno就是一个全局int变量。

第2章

2.1 这两位能改变待运行程序的有效用户ID和/或有效组ID。2.4节中用到了这两个有效ID。

2.2 首先同时指定O_CREAT和O_EXCL标志,如果成功返回,那么已创建了一个新对象。然而如果调用失败并返回EEXIST错误,那么对象已经存在,程序于是得再次调用打开函数,不过不再同时指定O_CREAT和O_EXCL标志。第二次调用应该成功,但是调用失败并返回ENOENT错误的机会仍然存在(尽管很小),它表明在这两次调用之间,另外某个线程或进程已将该对象删除了。

第3章

3.1 我们的程序如图D-1所示。

3.2 第二个程序运行时,第一次调用msgget使用的是第一个可用的消息队列,其槽位使用序列号在运行图3-7中的程序两次之后变为20,因而返回的标识符值为1000。假设下一个可用的消息队列从未使用过,其槽位使用序列号于是为0,因而返回的标识符值为1。

3.3 我们的简单程序如图D-2所示。

图D-1 输出标识符和槽位使用序列号

图D-2 测试msgget是否使用文件模式创建掩码

从下面的程序运行情况可以看出,文件模式创建掩码是2(关掉其他用户写位),该位在FIFO中确实关掉,在消息队列中却并未关掉。

solaris % umask

02

solaris % testumask

solaris % ls -l /tmp/fifo.1

prw-rw-r--  1 rstevens other1      0 Mar 25 16:05 /tmp/fifo.1

solaris % ipcs -q

IPC status from <running system> as of Wed Mar 25 16:06:03 1998

T      ID    KEY     MODE     OWNER   GROUP

Message Queues:

q     200  00000000 --rw-rw-rw- rstevens   other1

3.4 使用ftok的话,系统中另外某个路径名所形成的键与我们的服务器所用的键相同的可能性总是存在。使用IPC_PRIVATE的话,服务器尽管知道它是在创建新的消息队列,但它必须接着把所创建消息队列的标识符写到某个文件中,供客户读取。

3.5 下面是检测冲突的方法之一:

solaris % find / -links 1 -not -type l -print |

xargs -n1 ftok1 > temp.1

solaris % wc -l temp.1

109351 temp.1

solaris % sort +0 -1 temp.1 |

nawk '{ if (lastkey == $1)

print lastline,$0

lastline = $0

lastkey = $1

}' > temp.2

solaris % wc -l temp.2

82188 temp.2

在find程序中,我们忽略链接数多于一个的文件(因为每个链接都有相同的索引节点),符号链接也忽略(因为stat函数沿循符号链接,也就是说解释并替换符号链接,直到不再有新的符号链接)。很高的冲突比率(75.2%)是由于Solaris 2.x只使用了索引节点号中的12位。这意味着在文件数多于4096的任何文件系统中,有许多冲突会发生。例如索引节点号分别为4096、8192、12288和16384的4个文件都有相同的IPC键(假设它们在同一个文件系统中)。

下一个例子运行在同样的文件系统上,但使用的是来自BSD/OS的ftok函数。由于该函数把整个索引节点号加到键中,因此冲突数只有849(少于1%)。

第4章

4.1 当父进程终止时,如果子进程中fd[1]处于打开状态,那么子进程对fd[0]的read不会返回文件结束符,因为fd[1]在子进程中仍然打开。在子进程中关闭fd[1]保证一旦父进程终止,它的所有描述符即关闭,从而使得子进程对fd[0]的read返回0。

4.2 如果调用关系反转了,另外某个进程就有可能在本进程的open和mkfifo两个调用之间创建本进程想要创建的FIFO,结果导致本进程的mkfifo调用失败。

4.3 如果执行如下命令:

solaris % mainpopen 2>temp.stderr

/etc/ntp.conf > /myfile

solaris % cat temp.stderr

sh: /myfile: cannot create

那么我们看到popen返回成功,但是我们用fgets读到的只是一个文件结束符。该shell出错消息是写到标准错误输出的。

4.5 把第一个open调用改为指定非阻塞标志:

readfifo = Open(SERV_FIFO,O_RDONLY | O_NONBLOCK,0);

该调用将立即返回,接下去的open调用(用于只写)也立即返回,因为它要打开的FIFO已经由第一个open调用打开用于读。但是为了避免从readline返回错误,描述符readfifo的O_NONBLOCK标志必须在调用readline之前关掉。

4.6 如果客户在打开服务器的众所周知FIFO(用于只写)之前先打开它的客户特定FIFO(用于只读),那么会发生死锁。避免这种死锁的唯一办法是如图4-24中所示的顺序open这两个FIFO,也可以使用非阻塞标志。

4.7 写进程关闭管道或FIFO的信息通过文件结束符传递给读进程。

4.8 图D-3给出了我们的程序。

4.9 select返回说该描述符是可写的,但调用write却引发SIGPIPE信号。这个概念在UNPv1第153~155页 [3] 说明过,当发生读(或写)错误时,select返回说相应描述符是可读的(或可写的),真正的错误则由read(或write)返回。图D-4给出了我们的程序。

图D-3 判定fstat是否返回在某个FIFO中的字节数

图D-4 当一个管道的读出端关闭时,判定select为可写性返回的是什么

第5章

5.1 先不指定任何属性创建该队列,紧接着调用mq_getattr取得默认属性。随后删除该队列并重新创建,对未指定的那个属性使用其默认值。

5.2 对应第二个消息的信号没有产生是因为注册在每次通知发生之后即撤销。

5.3 对应第二个消息的信号没有产生是因为接收该消息时队列不空。

5.4 Solaris 2.6把这两个常值定义成调用sysconf,其上的GNU C编译器将产生如下出错消息:

test1.c:13: warning: int format,long int arg (arg 2)

test1.c:13: warning: int format,long int arg (arg 3)

5.5 在Solaris 2.6下,我们指定1 000 000个消息,每个消息10字节。这使文件大小为20 000 536字节,它与我们运行图5-5中程序所得的结果是一致的:每个消息占据10字节数据、8字节开销(也许是为存放指针)以及另外2字节开销(也许因4字节对齐之需),每个文件再占据536字节开销。在调用mq_open之前,由ps所报告的该程序大小为1052KB,该消息队列创建之后,大小变为20MB。这使得我们认为Posix消息队列是使用内存映射文件实现的,mq_open把该文件映射到调用进程的地址空间中。在Digital Unix 4.0B下我们也取得了类似的结果。

5..对于ANS..memXXX函数来说,大小参数为0不成问题。最初的198.ANS.C标准X3.159-1989(也称为ISO/IE.9899:1990)并没有这么说,作者能找到的手册页面也没有一个提及这一点,然而“Technica.Corrigendu.Numbe.1(1号技术勘误)”却明确陈述大小为0可行(不过指针参数仍必须有效)。要参阅有关C语言的信息,http://www.lysator.liu.se/c/是个颇值访问的地方。

5.7 两进程之间的双向通信需2个消息队列(图A-30是这样的一个例子)。事实上,要是我们把图4-14中程序改为使用Posix消息队列而不是管道,就会看到父进程读回它写到队列中的东西。

5.8 互斥锁和条件变量包含在内存映射文件中,而该文件是由打开了相应队列的所有进程共享的。其他进程也许打开着该队列,因此即将关闭该队列本地句柄的一个进程不能摧毁该互斥锁和条件变量。

5.9 C语言中数组不能通过等号赋值,结构却可以。

5.10 main函数几乎把所有时间都花在select调用的阻塞之中,等待管道变为可读。每次提交相应信号时,其信号处理程序的返回会中断这个select调用,使得它返回一个EINTR错误。为处理这种情形,我们的Select包裹函数检查这个错误,并重新调用select,如图D-5所示。

图D-5 处理EINTR的Select包裹函数

图D-5(续)

UNPv1第124页 [4] 有关于被中断系统调用的详细讨论。

第6章

6.1 其余程序必须接受数值形式的消息队列标识符,而不是路径名(回想一下图6-3中程序的输出)。这些程序上的如此变动既可通过增设一个新的命令行选项做到,也可假设完全为数值的路径名参数是标识符而不是真的路径名。既然传递给ftok的多数路径名是绝对路径名而不是相对路径名(也就是说它们至少包含一个斜杠符),这样的假设也许可行。

6.2 类型为0的消息是不被允许的,而客户是决不可能有1这个进程ID的,因为它通常是init进程的进程ID。

6.3 当如图6-14所示只使用一个队列时,这个恶意的客户影响所有其他客户。当给每个客户准备一个返送队列时(图6-19),这个客户只能影响它自己的队列。

第7章

7.2 进程将终止,而且可能是在消费者线程完成之前,因为调用exit将终止任何仍在运行中的线程。

7.3 Solaris 2.6下,省略destroy函数的调用导致内存泄漏,暗示init函数是在执行动态内存分配。在Digital Unix 4.0B下,我们没有看到这种现象,这意味着实现上存在差异。

不过调用匹配的destroy函数仍是需要的。从实现的角度看,Digital Unix像是把attr_t变量用作属性对象本身,Solaris则把该变量用作指向动态分配对象的指针。这两种实现都是可行的。

第9章

9.1 你可能需要把原来为20的循环计数加大才能看到这些错误,这取决于你的系统。

9.2 要使标准I/O流不缓冲,我们在main函数的for循环之前插入如下行:

setvbuf(stdout,NULL,_IONBF,0);

这么修改不应该有任何效果,因为printf调用只有一个,而且所输出字符串是以换行符结尾的。通常情况下,标准输出是行缓冲的,因此不论哪种缓冲方式(行缓冲或不缓冲),这个单独的printf调用最终变为对内核的单个write调用。

9.3 我们把printf调用改为:

snprintf(line,sizeof(line),"%s: pid = %ld,seq# = %d\n",

argv[0],(long)pid,seqno);

for (ptr = line; (c = *ptr++)!= 0; )

putchar(c);

并声明c是一个整数,ptr的类型为char *。保留上一道习题所加的setvbuf调用不变,从而使得标准输出变为不缓冲,于是标准I/O函数库给所输出的每个字符调用一次write,而不是每行调用一次。这么一来需要更多的CPU时间,内核在两个进程之间来回切换的机会也增多。我们应该从这个程序的运行中看到更多的错误。

9.4 既然对于一个文件的同一区段允许多个进程有读出锁,那么就我们的例子而言,这与没有任何锁是一样的。

9.5 没有任何变化,因为一个描述符的非阻塞标志对于fcntl劝告性上锁没有影响。决定fcntl调用是否阻塞的是其命令:F_SETLKW表明总是阻塞,F_SETLK则表明永不阻塞。

9.6 loopfcntlnonb程序运行如常,因为我们已在上一道习题展示,非阻塞标志对于执行fcntl上锁的程序没有影响。然而非阻塞标志确实影响不执行上锁的loopnonenonb程序。我们在9.5节说过,如果对启用了强制性上锁的文件所进行的read或write非阻塞调用与已有的锁发生冲突,那么会返回一个EAGAIN错误。我们看到的这个错误或者是

read error: Resource temporarily unavailable

或者是

write error: Resource temporarily unavailable

通过执行如下命令就能验证这个错误是EAGAIN:

solaris % grep Resource /usr/include/sys/errno.h

#define EAGAIN 11   /* Resource temporarily unavailable */

9.7 Solaris 2.6下,强制性上锁增加了约16%的时钟时间和约20%的系统CPU时间。用户CPU时间保持不变,正如我们所预期的那样,这是因为额外的时间花在了内核对每个read和write调用的检查上,而不是在用户进程上。

9.8 锁是以每个进程为基而不是以每个线程为基授予的。要看到上锁请求的竞争现象,我们必须让不同的进程来尝试获取锁。

9.9 如果本守护进程的另一个副本正在运行,当使用O_TRUNC标志open时,由本守护进程的第一个副本存放的进程ID就会被冲掉。我们只有获悉自己是唯一在运行的副本后,才能截掉文件内容。

9.10 SEEK_SET总是最可取的。SEEK_CUR的问题是它取决于文件中的当前偏移量,而该值是由lseek指定的。但是如果在调用lseek之后调用fcntl,那么我们是在使用两个函数调用完成单个操作的任务,而这两个函数调用之间存在由另外一个线程通过调用lseek修改当前偏移量的机会。(回想一下所有线程共享相同的描述符。另外回想一下fcntl记录锁用于不同进程之间的上锁,而不是单个进程内的不同线程之间的上锁。)同样,如果我们指定SEEK_END,那么在基于所认定的文件尾获得一个锁之前,另外一个线程有可能已往该文件添加数据。

第10章

10.1 以下是在Solaris 2.6下的结果输出:

solaris % deadlock 100

prod: calling sem_wait(nempty)        生产者i=0时的循环

prod: got sem_wait(nempty)

prod: calling sem_wait(mutex)

prod: got sem_wait(mutex),storing 0

prod: calling sem_wait(nempty)        生产者i=1时的循环

prod: got sem_wait(nempty)

prod: calling sem_wait(mutex)

prod: got sem_wait(mutex),storing 1

prod: calling sem_wait(nempty)        开始下一轮循环,但没有空槽位

上下文从生产者切换到消费者

cons: calling sem_wait(mutex)        消费者i=0时的循环

cons: got sem_wait(mutex)

cons: calling sem_wait(nstored)

cons: got sem_wait(nstored)

cons: fetched 0

cons: calling sem_wait(mutex)        消费者i=1时的循环

cons: got sem_wait(mutex)

cons: calling sem_wait(nstored)

cons: got sem_wait(nstored)

cons: fetched 1

cons: calling sem_wait(mutex)

cons: got sem_wait(mutex)

cons: calling sem_wait(nstored)       消费者永远阻塞于此

上下文从消费者切换到生产者

prod: got sem_wait(nempty)

prod: calling sem_wait(mutex)        生产者永远阻塞于此

10.2 在我们描述sem_open时指定了信号量初始化规则的前提下,这是可行的。该规则说,如果信号量已经存在,它就不被初始化。因此这4个进程中,只有第一个调用sem_open的进程才真正把信号量的值初始化为1。当其余3个进程以O_CREAT标志调用sem_open时,所需信号量已经存在,其值于是不再被初始化。

10.3 这确实是个问题。信号量在该进程终止时被自动关闭,但是其值并没有改变。这将妨碍其他3个进程中的任何一个取得所需的锁,从而导致另外一种类型的死锁。

10.4 要是我们没有把这两个描述符初始化为-1,那么它们的初始值是不确定的,因为malloc并不对它分配的内存进行初始化。这么一来,如果有一个open调用失败,error标号处的close调用就可能关闭该进程正在使用的某个描述符。把描述符初始化为-1后,我们可知如果描述符尚未被打开则close调用不会有什么后果(除返回一个我们忽略掉的错误外)。

10.5 尽管很小,却存在这样的机会:close调用虽然针对一个有效的描述符,但仍可能返回某个错误,从而把errno由我们想返回的值改为其他值。既然我们想要保存errno值以返回给调用者,显式地去做总比依赖某些副作用(例如当关闭的是一个有效的描述符时,close调用不会返回错误)来得好。

10.6 这个函数中不存在竞争状态,因为如果所需的FIFO已经存在,mkfifo函数将返回一个错误。如果有两个进程几乎同时调用这个函数,那么相应的FIFO只创建一次。调用mkfifo的第二个进程将收到一个EEXIST错误,导致O_CREAT标志被关掉,从而防止再次初始化同一个FIFO。

10.7 图10-37没有我们随图10-43描述的竞争状态,因为其信号量的初始化是通过写数据到相应的FIFO完成的。如果创建该FIFO的进程在调用mkfifo之后但在向该FIFO write数据字节之前被内核挂起,那么第二个进程只是打开该FIFO,随后在首次调用sem_wait处阻塞,因为这个新创建的FIFO在第一个进程 (即创建该FIFO的进程)往它写数据字节前一直为空。

10.8 图D-6给出了该测试程序。Solaris 2.6和Digital Unix 4.0B上的实现都检测被某个捕获的

信号中断的情况,并返回EINTR错误。

图D-6 测试sem_wait是否返回EINTR

我们使用FIFO的实现会返回EINTR,因为sem_wait阻塞在对于一个FIFO的某个read调用中,而read调用必须返回EINTR错误。我们使用内存映射I/O的实现不返回任何错误,因为sem_wait阻塞在某个pthread_cond_wait调用中,而该函数被一个捕获的信号中断时并不返回EINTR。(我们在图5-29中看到过另外一个例子。)我们使用System V信号量的实现返回EINTR,因为sem_wait阻塞在某个semop调用中,而semop调用返回EINTR错误。

10.9 使用FIFO的实现(图10-40)是异步信号安全的,因为write是异步信号安全的。使用内存映射文件的实现(图10-47)则不是,因为没有一个pthread_XXX函数是异步信号安全的。使用System V信号量的实现(图10-56)也不是,因为Unix 98没有把semop列为异步信号安全函数。

第11章

11.1 只需修改其中一行:

<   semid = Semget(Ftok(argv[optind],0),0,0);

---

>   semid = atol(argv[optind]);

11.2 ftok调用将失败,从而导致我们的Ftok包裹函数的终止。my_lock函数可在调用semget之前调用ftok,检查是否返回ENOENT错误,若LOCK_PATH文件不存在则创建它。

第12章

12.1 文件大小将再增长4 096字节(达到36 864字节),最后一个printf对新文件结束符(ptr字符数组的对应下标为36863)所作的引用可能引发SIGSEGV信号,因为内存映射区的大小为32 768字节。我们说“可能”而不是“将”的原因在于,该信号产生与否取决于页面大小。

12.2 图D-7是使用System V消息队列发送消息的示意图,图D-8是使用通过mmap实现的Posix消息队列发送消息的示意图。图D-8中发送者的memcpy发生在调用mq_send期间(图5-30),接收者的memcpy则发生在调用mq_receive期间 (图5-32)。

图D-7 使用System V消息队列发送消息

图D-8 使用通过mmap实现的Posix消息队列发送消息

12.3 对/dev/zero设备文件的任何read,所返回的是所请求数目的全为0的字节。write到该设备的任何数据被直接丢弃掉,就像write到/dev/null设备一样。

12.4 该文件的最终内容为4字节的0(假设int类型为32位)。

12.5 图D-9给出了我们的程序。

图D-9 父子进程设置成对System V消息使用select的例子

第13章

13.1 图D-10给出了图12-16的修改后版本,图D-11给出了图12-19的修改后版本。注意在第一个程序中,我们必须使用ftruncate设置共享内存对象的大小,而不能使用lseek和write。

图D-10 访问其大小可能不同于共享内存区大小的mmap

图D-11 允许共享内存区大小增长的内存映射例子

图D-11(续)

13.2 *ptr++可能存在的问题之一是由mmap返回的指针被改掉,从而妨碍以后调用munmap。

如果以后要用到该指针,我们就得把它保存起来,或者不作修改。

第14章

14.1 只有一行需要改动:

第15章

13c13

<   id = Shmget(Ftok(argv[1],0),0,SVSHM_MODE);---

>   id = atol(argv[1]);

15.1 这样的参数的字节数为data_size+(desc_num*sizeof(door_desc_t))。

15.2 没有调用fstat的必要。如果所打开的描述符指代的不是一个门,那么door_info将返回一个EBADF错误:

solaris % doorinfo /etc/passwd

door_info error: Bad file number

15.3 该手册页面是错误的。Posix.1对此的正确陈述为“The sleep()function shall cause the current thread to be suspended from execution.”(sleep函数会导致当前线程从执行状态挂起)。

15.4 结果难以预料(尽管核心转储是一个相当安全的猜测),因为与其中的门相关联的服务器过程的地址会导致在新执行的程序中,某段随机的代码会被作为一个函数来调用。

15.5 当客户的door_call被所捕获的信号中断时,服务器进程必须被通知到,因为它需随后向处理该客户的服务器线程(它的ID在我们的输出例子中为4)发送一个取消请求,然而我们随图15-23说过,对于由门函数库自动创建的所有服务器线程来说,取消操作是被禁止的,我们正讨论的线程于是不会因取消而终止。相反,当客户的door_call被终止时,服务器过程阻塞在其上的sleep(6)调用看来在该过程被调用后约2秒钟就过早地返回了。但是执行该过程的服务器线程仍持续运行到完成为止。

15.6 我们看到的错误如下:

solaris % server6 /tmp/door6

my_thread: created server thread 4

door_bind error: Bad file number

当我们连续启动该服务器20次时,该错误出现了5次。该错误是不确定的。

15.7 不需要。我们需做的只是如图15-31中那样,每次服务器过程被调用时就启用取消。

这种技术尽管在服务器过程每次被激活时都要调用pthread_setcancelstate,而不是仅在执行该过程的线程启动时调用一次,但其开销却可能很小。

15.8 为验证这一点,我们将某个服务器程序(譬如说图15-9)改为从服务器过程中调用door_revoke。由于门描述符是door_revoke的参数,因此我们还得把fd改成一个全局变量。我们随后执行相应的客户程序(譬如说图15-2)两次:

solaris % client8 /tmp/door8 88

result: 7744

solaris % client8 /tmp/door8 99

door_call error: Bad file number

第一次激活服务器过程成功返回,从而证实door_revoke不影响已在进行的调用。第二次激活告知从door_call返回的错误是EBADF。

15.9 为避免使fd成为一个全局变量,我们使用传递给door_create的cookie指针,该指针随后在服务器过程每次被调用时传递给它。图D-12给出了服务器程序。

图D-12 使用cookie指针以避免使fd成为一个全局变量

我们可以很容易地对图15-22和图15-23作同样的修改,因为cookie指针对我们的my_create函数而言是可得的(该指针在door_info_t结构中),而该函数又把指向该结构的指针传递给新创建的线程(它需要对应door_bind调用的描述符)。

15.10 本例中线程属性从不改变,因此我们可以只初始化一次线程属性(在main函数中完成)。

第16章

16.1 端口映射器并不监视已向它注册的各个服务器,因而无法检测它们是否崩溃。终止其中某个服务器后,它在端口映射器中注册的映射关系并不注销,这一点可使用rpcinfo程序来验证。这么一来,该服务器终止后,与端口映射器联系以获取该服务器端口号的某个客户将得到肯定的答复,由端口映射器返回该服务器在终止之前使用的端口号。假设该服务器是一个TCP服务器,当该客户试图与它联系时,客户方RPC运行时环境将收到RST(复位)分节作为对SYN分节的响应(前提是自该服务器终止以来,服务器主机上没有其他进程被赋予同样的端口),从而导致从clnt_create返回一个错误。UDP客户调用clnt_create将成功(因为没有连接需要建立)。但是当它向以前的服务器端口发送UDP数据报时,什么应答都不会返回(同样假设自该服务器终止以来,服务器主机上没有其他进程被赋予同样的端口),该客户的远程过程调用最终将超时。

16.2 当收到服务器的第一个应答后,RPC运行时环境把它返回给客户,这发生在客户发出远程过程调用后约20秒。因超时重传导致的服务器的下一个应答将一直存留在客户方端点的网络缓冲区中,直到该端点被关闭,或者直到RPC运行时环境下一次读该缓冲区为止。现在假设客户在收到第一个应答后立即向服务器再次发出调用 [5] ,再假设没有网络分组丢失现象,下一个到达客户方端点的数据报将是服务器对于客户的超时重传的应答。然而RPC运行时环境将忽略这个应答,因为它的XID对应于客户的第一次远程过程调用,它不可能与客户的第二次过程调用所用的XID相同。

16.3 构造一个成员为char c[10]的C结构,不过XDR将把它编码成10个4字节整数。要是你确实需要定长的字符串,那就使用定长的不透明数据类型。

16.4 xdr_data调用返回FALSE,因为它的xdr_string调用返回FALSE(参见data_xdr.c文件)。

当指定一个最大长度时,它将作为xdr_string的最后一个参数编码。当省略这个最大长度时,最后一个参数是0的反码(在32位整数前提下,其值为232 -1)。

16.5 所有的XDR例程都检查缓冲区中是否有足够的空间以存放将编码到其中的数据,当缓冲区满时返回FALSE错误。不幸的是,没有办法区别来自XDR函数的各种不同的可能错误。

16.6 我们可以说TCP使用序列号检测重复数据在效果上等同于重复请求高速缓存,因为对于作为含有TCP已确认过的重复数据而到达的任何旧分节,这些序列号都能将其标识出来。对于一个给定的连接(例如一个给定客户的IP地址和端口),该高速缓存的大小是TCP的32位序列号空间的一半,也就是231 或约2G字节。

16.7 由于对一个给定的请求来说,它的所有5个值必须等于某个高速缓存项中对应的5个值,因此第一个作比较的值应该是最可能不等的那个值,而最后一个作比较的值应该是最可能相等的那个值。在TI-RPC软件包中,真正的比较顺序是:(1) XID,(2)过程号,(3)版本号,(4)程序号,(5)客户地址。在XID随每个请求变化的前提下,首先对它进行比较是明智的。

16.8 在图16-30中,从标志/长度字段开始,包括4字节的长整数过程参数在内,共有12个4字节字段,合计48字节。按照默认的无认证配置,凭证数据和验证器数据均为空。也就是说,凭证和验证器均占用8字节:4字节指定认证方式 (AUTH_NONE),另4字节指定认证长度(其值为0)。

在应答分节中(看图16-32,但要意识到由于在使用TCP,因此在XID之前有一个4字节的标志/长度字段),共有8个4字节字段,从标志/长度字段开始,到4字节的长整数过程结果为止,共计32字节。

当使用UDP时,请求和应答的唯一变动是不存在4字节的标志/长度字段。因此请求的大小为44字节,应答的大小为28字节,这一点可使用tcpdump验证。

16.9 可以。不论在客户端还是在服务器端,参数处理上的差异都局限于主机,而与穿越网络的分组无关。客户的main函数调用客户存根中的某个函数以产生一个网络记录,服务器的main函数则调用服务器存根中的某个函数处理这个网络记录。跨越网络传送的RPC记录是由RPC协议定义的,而RPC协议不论客户端或服务器端是否支持线程都不变。

16.10 XDR运行时环境给这些字符串动态分配字间。给read程序增加下面一行就能验证这个事实:

printf("sbrk()= %p,buff = %p,in.vstring_arg = %p\n",

sbrk(NULL),buff,in.vstring_arg);

其中sbrk函数返回处于程序数据段顶部的当前地址,而在此以下的内存空间通常就是malloc从中分配内存的区段。运行该程序产生如下输出:

sbrk()= 29638,buff = 25e48,in.vstring_arg = 27e58

它表明指针vstring_arg指向malloc使用的内存区段内。8192字节的buff地址为0x25e48~0x27e47,字符串就存放在该缓冲区之后。

16.11 图D-13给出了客户程序。注意clnt_call的最后一个参数是一个真正的timeval结构,而不是指向某个这种结构的指针。还要注意clnt_call的第三个和第五个参数必须是指向XDR例程的非空函数指针,因此我们指定的是不做任何工作的XDR函数xdr_void。(编写一个很小的RPC规范文件,其中定义一个既没有参数也没有返回值的函数,运行rpcgen,然后检查所生成的客户存根,这样你就能验证图D-13中给出的确实是调用一个既没有参数又没有返回值的函数的方法。)

图D-13 调用服务器空过程的客户程序

16.12 所产生的UDP数据报大小(65536+20+RPC开销)超过了IPv4数据报的最大大小65535。图A-4中对于使用UDP的RPC来说,不存在消息大小为16384和32768的项的原因是,这是一个较早的RPCSRC 4.0实现,它把UDP数据报的大小限制在约9000字节左右。


[1]. 此处为APUE第1版英文原版书页码,第2版为第77~78页,中文版为第61~62页。——编者注

[2]. 此处为UNPv1第2版英文原版书节号,第3版为26.5节。——编者注

[3]. 此处为UNPv1第2版英文原版书页码,第3版为第160~163页。——编者注

[4]. 此处为UNPv1第2版英文原版书页码,第3版为第134~135页。——编者注

[5]. 注意区别这个由客户主动发出的再次调用和因超时重传导致的由RPC运行时环境完成的再次调用,后者对客户不可见。——译者注