Skip to content

Latest commit

 

History

History
 
 

Chapter-15

进程间通信

随书代码

15.2.c

$ gcc 15.2.c -lapue
$ ./a.out
hello world

pipe 创建的管道, 1 为写端, 0 为读端

15.2.2.c && 15.3.c

pass

15.3.1.c && 15.3.2.c

$ gcc 15.3.1.c -lapue -o myuclc
$ gcc 15.3.2.c -lapue
$ ./a.out
prompt> a
a
prompt> A
a
prompt> ABC
abc

15.4.c && 15.4.2.c

$ gcc 15.4.c -lapue -o add2
$ gcc 15.4.2.c -lapue
$ ./add2
1 2
3
^C
$ ./a.out
1 2
3
3 4
7
^C

15.4.3.c

$ gcc 15.4.3.c -lapue -o add2
$ ./a.out
1 2
3 4
(无响应)
^C

因为管道是全缓冲的, 程序运行时管道阻塞

15.4.4.c

$ gcc 15.4.4.c -lapue -o add2
$ ./a.out
1 2
3
3 4
7
^C

修改流缓冲方式为行缓存之后, 程序运行成功

15.9.c

$ gcc 15.9.c -lapue
$ ./a.out
array[] from 0x55ab62824060 to 0x55ab6282dca0
stack around 0x7ffd1bc6ef74
malloced from 0x55ab637b1670 to 0x55ab637c9d10
shared memory attached from 0x7f9327e7d000 to 0x7f9327e956a0

15.9.2.c

$ gcc 15.9.2.c -lapue
$ ./a.out
update 0 from parent
update 1 from child
update 2 from parent
update 3 from child
......
update 998 from parent
update 999 from child

15.9.3.c

$ gcc 15.9.3.c -lapue
$ ./a.out
update 0 from parent
update 1 from child
update 2 from parent
update 3 from child
......
update 998 from parent
update 999 from child

可以很方便的实现在父子进程之间共享内存

课后习题

15.1

在图 15-6 的程序中, 在父进程代码的末尾删除 waitpid 前的 close, 结果将如何?
在文件没有读取结束的时候表现相同, 在读取到文件末尾的时候, 因为管道未关闭, 分页程序会一直阻塞读取标准输入.

15.2

在图 15-6 的程序中, 在父进程代码的末尾删除 waitpid, 结果将如何?
分页程序输出的内容比文件的内容少一行, 因为父进程输出完所有的内容之后终止, 父进程终止时管道的读端自动关闭, 这时子进程正在等待输出, 因此父进程比子进程领先一个缓冲区的内容.

15.3

如果 popen 函数的参数是一个不存在的命令, 会造成什么结果? 编写一段小程序对此进行测试.
因为执行了 shell, 所有 popen 返回一个文件指针. 但是 shell 不能执行不存在的命令, 因此在标准错误上打印下面信息后终止:

sh: line 1: ./a.out: No such file or directory

其退出状态为 127(该值取决于 shell 的类型). pclose 返回该命令的终止状态, 这如同从 waitpid 返回一样.

15.4

在图 15-18 的程序中, 删除信号处理程序, 执行该进程, 然后终止子进程. 输入一行输入后, 怎样才能说明父进程是由 SIGPIPE 终止的?

当父进程终止时, 用 shell 看它的终止状态. 打印的结果是 128 加上信号编号

$ ./a.out
1 2
3
^C
$ echo $?
130

15.5

在图 15-18 的程序中, 用标准 I/O 库代替管道读、写的 read 和 write.
使用 fdopen 关联管道描述符和标准 I/O 流, 然后将流设置为行缓冲的

 gcc 15.4.c -lapue -o add2
$ gcc p5.c -lapue
$ ./a
a.out  add2
$ ./a.out
1 2
3
3 4
7
^C

15.6

POSIX.1 加入 waitpid 函数的理由之一是, POSIX.1 之前的大多数系统不能处理下面的代码.

if ((fd = popen("/bin/true", "r")) == NULL) {
    // ...
}
if ((rc = system("sleep 100")) == -1) {
    // ...
}
if (pclose(fp) == -1) {
    // ...
}

若这段代码中不使用 waitpid 函数会怎样? 用 wait 代替呢?
system 函数调用了 wait , 终止的第一个子进程是由 popen 产生的. 因为该子进程不是 system 创建的, 所以它将再次调用 wait 并一直阻塞到 sleep 完成. 然后 system 返回. 当 pclose 调用 wait 时, 由于没有子进程可等待所以返回出错, 导致 pclose 也返回出错

15.7

当一个管道被写者关闭后, 解释 selectpoll 是如何处理改管道的输入描述符的. 为了确定答案是否正确, 编两个小测试程序, 一个用 select, 另一个用 poll.

15.8

如果 popentype 为 "r" 执行 cmdstring, 并将结果写到标准错误输出, 结果会怎样?
子进程想标准错误写的内容也会在父进程的标准错误中出现, 只要在 cmdstring 中包含 shell 重定向 2>&1 , 就可以将标准错误发回给父进程.

15.9

既然 popen 函数能使 shell 执行它的 cmdstring 参数, 那么 cmdstring 终止时会产生什么结果?(提示: 画出与此相关的所有进程.)
popen 函数 fork 一个子进程, 子进程执行 shell , 然后 shell 再调用 fork , 最后由 shell 的子进程执行命令串. 当 cmdstring 终止时, shell 恰好在等待该事件. 然后 shell 退出, 而这一事件又是 pclose 中的 waitpid 所等待的.

15.10

POSIX.1 特别声明没有定义为读写而打开 FIFO. 虽然大多数 UNIX 系统允许读写 FIFO, 但是请用非阻塞方法实现为读写而打开FIFO.
解决的办法是打开(open) FIFO 两次: 一次读; 一次写.

15.11

除非文件包含敏感数据或机密数据, 否则允许其他用户读文件不会造成损害. 但是, 如果一个恶意进程读取了被一个服务器进程和几个客户进程使用的消息队列中的一条消息后, 会产生什么后果? 恶意进程需要知道哪些信息就可以读消息队列?
随意读取现行队列中的消息会干扰客户进程=服务器进程协议, 导致丢失客户进程请求或者服务器进程的响应. 只要知道队列的标识符或者该队列允许所有的用户读, 进程就可以读队列.

15.12

编写一段程序完成下面的工作. 执行一个循环 5 次, 在每次循环中, 创建一个消息队列, 打印该队列的标识符, 然后删除队列. 接着再循环 5 次, 在每次循环中利用键 IPC_PRIVATE 创建消息队列, 并将一条消息放在队列中. 程序终止后用 ipcs(1) 查看消息队列. 解释队列标识符的变化.
执行 p12.c

$ sudo ./a.out
id = 1310720
id = 1376256
id = 1441792
id = 1507328
id = 1572864


id = 1638400
id = 524289
id = 131074
id = 131075
id = 131076
$ ipcs
IPC status from <running system> as of Tue Jul 17 13:02:04 CST 2018
T     ID     KEY        MODE       OWNER    GROUP
Message Queues:
q 1638400 0x00000000 -----------     root    wheel
q 524289 0x00000000 -----------     root    wheel
q 131074 0x00000000 -----------     root    wheel
q 131075 0x00000000 -----------     root    wheel
q 131076 0x00000000 -----------     root    wheel

T     ID     KEY        MODE       OWNER    GROUP
Shared Memory:

T     ID     KEY        MODE       OWNER    GROUP
Semaphores:

15.13

描述如何在共享存储段中建立一个数据对象的链接列表. 列表指针如何存储?
由于服务器进程和各客户进程可能会将段连接到不同的地址, 所以在共享存储段中绝不会存储实际物理地址. 相反, 当在共享存储段中建立链表时, 链表指针的值会设置为共享存储段内另一对象的偏移量. 偏移量为所指对象的实际地址减去共享存储段的起始地址.

15.14

画出图 15-33 中程序运行时下列值随时间变化的曲线图: 父进程和子进程中的变量 i、共享存储区中的长整型值以及 update 函数的返回值. 假设子进程在 fork 后先执行.

父进程的 i 设置成 子进程的 i 设置成 共享值设置成 update 返回 注释
0 由 mmap 初始化
1 子进程先运行, 然后被阻塞
0 父进程先运行
1
0 然后父进程被阻塞
2 子进程继续
1
3 然后子进程被阻塞
2 父进程继续
3
2 然后父进程被阻塞
4
3
5 然后子进程被阻塞
4 父进程继续

15.15

使用 15.9 节中的 XSI 共享存储函数代替共享存储映射区, 改写图 15-33 中的程序.
p15.c, 将之前的 mmap 一句改为了 shmgetshmat 函数.

15.16

使用 15.8 节中的 XSI 信号量函数改写图 15-33 中的程序, 实现父进程与子进程间的交替.
p16.c, 每次申请 i 个资源, 释放 i + 1 个, 从而让父子进程交替执行.

15.17

使用建议性记录锁改写图 15-33 中的程序, 实现父进程与子进程间的交替.

15.18

使用 15.10 节中的 POSIX 信号量函数改写 15-33 中的程序, 实现父进程与子进程间的交替.
p18.c, 通过两个信号量, 实现父子进程的交替.