深入理解计算机系统(CSAPP)
  • 本电子书信息
  • 出版信息
    • 出版者的话
    • 中文版序一
    • 中文版序二
    • 译者序
    • 前言
    • 关于作者
  • 第 1 章:计算机系统漫游
    • 1.1 信息就是位 + 上下文
    • 1.2 程序被其他程序翻译成不同的格式
    • 1.3 了解编译系统如何工作是大有益处的
    • 1.4 处理器读并解释储存在内存中的指令
    • 1.5 高速缓存至关重要
    • 1.6 存储设备形成层次结构
    • 1.7 操作系统管理硬件
    • 1.8 系统之间利用网络通信
    • 1.9 重要主题
    • 1.10 小结
  • 第一部分:程序结构和执行
    • 第 2 章:信息的表示和处理
      • 2.1 信息存储
      • 2.2 整数表示
      • 2.3 整数运算
      • 2.4 浮点数
      • 2.5 小结
      • 家庭作业
    • 第 3 章:程序的机器级表示
      • 3.1 历史观点
      • 3.2 程序编码
      • 3.3 数据格式
      • 3.4 访问信息
    • 第 4 章:处理器体系结构
    • 第 5 章:优化程序性能
    • 第 6 章:存储器层次结构
  • 第二部分:在系统上运行程序
    • 第 7 章:链接
      • 7.1 编译器驱动程序
      • 7.2 静态链接
      • 7.3 目标文件
      • 7.4 可重定位目标文件
      • 7.5 符号和符号表
      • 7.6 符号解析
      • 7.7 重定位
      • 7.8 可执行目标文件
      • 7.9 加载可执行目标文件
      • 7.10 动态链接共享库
      • 7.11 从应用程序中加载和链接共享库
      • 7.12 位置无关代码
      • 7.13 库打桩机制
      • 7.14 处理目标文件的工具
      • 7.15 小结
      • 家庭作业
    • 第 8 章:异常控制流
      • 8.1 异常
      • 8.2 进程
      • 8.3 系统调用错误处理
      • 8.4 进程控制
      • 8.5 信号
      • 8.6 非本地跳转
      • 8.7 操作进程的工具
      • 8.8 小结
      • 家庭作业
    • 第 9 章:虚拟内存
      • 9.1 物理和虚拟寻址
      • 9.2 地址空间
      • 9.3 虚拟内存作为缓存的工具
      • 9.4 虚拟内存作为内存管理的工具
      • 9.5 虚拟内存作为内存保护的工具
      • 9.6 地址翻译
      • 9.7 案例研究:Intel Core i7 / Linux 内存系统
      • 9.8 内存映射
      • 9.9 动态内存分配
      • 9.10 垃圾收集
      • 9.11 C 程序中常见的与内存有关的错误
      • 9.12 小结
      • 家庭作业
  • 第三部分:程序间的交互和通信
    • 第 10 章:系统级 I/O
      • 10.1 Unix I/O
      • 10.2 文件
      • 10.3 打开和关闭文件
      • 10.4 读和写文件
      • 10.5 用 RIO 包健壮地读写
      • 10.6 读取文件元数据
      • 10.7 读取目录内容
      • 10.8 共享文件
      • 10.9 I/O 重定向
      • 10.10 标准 I/O
      • 10.11 综合:我该使用哪些 I/O 函数?
      • 10.12 小结
      • 家庭作业
    • 第 11 章:网络编程
      • 11.1 客户端—服务器编程模型
      • 11.2 网络
      • 11.3 全球 IP 因特网
      • 11.4 套接字接口
      • 11.5 Web 服务器
      • 11.6 综合:TINY Web 服务器
      • 11.7 小结
      • 家庭作业
    • 第 12 章:并发编程
      • 12.1 基于进程的并发编程
      • 12.2 基于 I/O 多路复用的并发编程
      • 12.3 基于线程的并发编程
      • 12.4 多线程程序中的共享变量
      • 12.5 用信号量同步线程
      • 12.6 使用线程提高并行性
      • 12.7 其他并发问题
      • 12.8 小结
      • 家庭作业
  • 附录 A:错误处理
  • 参考文献
  • 实验
    • 实验总览
      • 常见问题
    • 实验 1:Data Lab
      • README(讲师版)
      • README(学生版)
      • Writeup
    • 实验 2:Bomb Lab
      • README(讲师版)
      • Writeup
    • 实验 3:Attack Lab
    • 实验 4:Architechture Lab
    • 实验 5:Cache Lab
    • 实验 6:Performance Lab
    • 实验 7:Shell Lab
    • 实验 8:Malloc Lab
    • 实验 9:Proxy Lab
由 GitBook 提供支持
在本页
  • 练习题 8.9
  • 练习题 8.10
  • 练习题 8.11
  • 练习题 8.12
  • 练习题 8.13
  • 练习题 8.14
  • 练习题 8.15
  • 练习题 8.16
  • 练习题 8.17
  • 练习题 8.18
  • 练习题 8.19
  1. 第二部分:在系统上运行程序
  2. 第 8 章:异常控制流

家庭作业

练习题 8.9

考虑四个具有如下开始和结束时间的进程:

进程

开始时间

结束时间

A

5

7

B

2

4

C

3

6

D

1

8

对于每对进程,指明它们是否是并发地运行的:

进程对

并发地?

AB

AC

AD

BC

BD

CD

练习题 8.10

在这一章里,我们介绍了一些具有不寻常的调用和返回行为的函数:setjmp、longjmp、execve 和 fork。找到下列行为中和每个函数相匹配的一种:

A. 调用一次,返回两次。

B. 调用一次,从不返回。

C. 调用一次,返回一次或者多次。

练习题 8.11

这个程序会输出多少个 “hello” 输出行?

code/ecf/forkprob1.c
#include "csapp.h"

int main()
{
    int i;

    for (i = 0; i < 2; i++)
        Fork();
    printf("hello");
    exit(0);
}

练习题 8.12

这个程序会输出多少个 “hello” 输出行?

code/ecf/forkprob4.c
#include "csapp.h"

void doit()
{
    Fork();
    Fork();
    printf("hello\n");
    return;
}

int main()
{
    doit ();
    printf("hello\n");
    exit(0);
}

练习题 8.13

下面程序的一种可能的输出是什么?

code/ecf/forkprob3.c
#include "csapp.h"

int main() 
{
    int x = 3;

    if (Fork() != 0)
        print! ("x=%d\n", ++x);

    printf("x=%d\n", --x);
    exit(0);
}

练习题 8.14

这个程序会输出多少个 “hello” 输出行?

code/ecf/forkprob5.c
#include "csapp.h"

void doit()
{
    if (Fork() == 0) {
        Fork();
        printf("hello\n");
        exit(0);
    }
    return;
}

int main()
{
    doit();
    printf("hello\n");
    exit(0);
}

练习题 8.15

这个程序会输出多少个 “hello” 输出行?

code/ecf/forkprob6.c
#include "csapp.h"

void doit()
{
    if (Fork() == 0) {
        Fork();
        printf("hello\n");
        return;
    }
    return;
}

int main()
{
    doit();
    printf("hello\n");
    exit(0);
}

练习题 8.16

下面这个程序的输出是什么?

code/ecf/forkprob7.c
#include "csapp.h"
int counter = 1;

int main()
{
    if (fork() == 0) {
        counter--;
        exit(0);
    }
    else {
        Wait(NULL);
        printf("counter = %d\n", ++counter);
    }
    exit(0);
}

练习题 8.17

列举练习题 8.4 中程序所有可能的输出。

练习题 8.18

考虑下面的程序:

code/ecf/forkprob2.c
#include "csapp.h"

void end(void)
{
    printf("2"); fflush(stdout);
}

int main()
{
    if (Fork() == 0)
        atexit(end);
    if (Fork() == 0) {
        printf("0"); fflush(stdout);
    }
    else {
        printf("1"); fflush(stdout);
    }
    exit(0);
}

判断下面哪个输出是可能的。注意:atexit 函数以一个指向函数的指针为输入,并将它添加到函数列表中(初始为空),当 exit 函数被调用时,会调用该列表中的函数。

A. 112002

B. 211020

C. 102120

D. 122001

E. 100212

练习题 8.19

code/ecf/forkprob8.c
void foo(int n)
{
    int i;

    for (i = 0; i < n; i++)
        Fork();
    printf("hello\n");
    exit(0);
}

练习题 8.20

使用 execve 编写一个叫做 myls 的程序,该程序的行为和 /bin/ls 程序的一样。你的程序应该接受相同的命令行参数,解释同样的环境变量,并产生相同的输岀。

Is 程序从 COLUMNS 环境变量中获得屏幕的宽度。如果没有设置 COLUMNS,那么 ls 会假设屏幕宽 80 列。因此,你可以通过把 COLUMNS 环境设置得小于 80,来检查你对环境变量的处理:

linux> setenv COLUMNS 40
linux> ./myls
.
. // Output is 40 columns wide
.
linux> unsetenv COLUMNS
linux> ./myls
.
. // Output is now 80 columns wide
.

练习题 8.21

下面的程序可能的输岀序列是什么?

code/ecf/waitprob3.c
int main()
{
    if (fork() == 0) {
        printf("a"); fflush(stdout);
        exit(0);
    }
    else {
        printf("b"); fflush(stdout);
        waitpid(-1, NULL, 0);
    }
    printf("c"); fflush(stdout);
    exit(0);
}

练习题 8.22

编写 Unix system 函数的你自己的版本

int mysystem(char* command);

mysystem 函数通过调用 “/bin/sh-ccommand” 来执行 command,然后在 command 完成后返回。如果 command(通过调用 exit 函数或者执行一条 return 语句)正常退出,那么 mysystem 返回 command 退出状态。例如,如果 command 通过调用 exit(8) 终止,那么 mysystem 返回值 8。否则,如果 command 是异常终止的,那么 mysystem 就返回 shell 返回的状态。

练习题 8.23

你的一个同事想要使用信号来让一个父进程对发生在子进程中的事件计数。其想法是每次发生一个事件时,通过向父进程发送一个信号来通知它,并且让父进程的信号处理程序对一个全局变量 counter 加一,在子进程终止之后,父进程就可以检查这个变量。然而,当他在系统上运行图 8-45 中的测试程序时,发现当父进程调用 printf 时,counter 的值总是 2,即使子进程向父进程发送了 5 个信号也是如此。他很困惑,向你寻求帮助。你能解释这个程序有什么错误吗?

code/ecf/counterprob.c
#include "csapp.h"

int counter = 0;

void handler(int sig)
{
    counter++;
    sleep(1); /* Do some work in the handler */
    return;
}

int main()
{
    int i;

    Signal(SIGUSR2, handler);

    if (Fork() == 0) { /* Child */
        for (i = 0; i < 5; i++) {
            Kill(getppid(), SIGUSR2);
            printf("sent SIGUSR2 to parent\n");
        }
        exit(0);
    }
    
    Wait(NULL);
    printf("counter=%d\n", counter);
    exit(0);
}

图 8-45 家庭作业 8.23 中引用的计数器程序

练习题 8.24

修改图 8-18 中的程序,以满足下面两个条件:

1)每个子进程在试图写一个只读文本段中的位置时会异常终止。

2)父进程打印和下面所示相同(除了 PID)的输出:

child 12255 terminated by signal 11: Segmentation fault
child 12254 terminated by signal 11: Segmentation fault

提示:请参考 psignal(3) 的 man 页。

练习题 8.25

编写 fgets 函数的一个版本,叫做 tfgets,它 5 秒钟后会超时。tfgets 函数接收和 fgets 相同的输入。如果用户在 5 秒内不键入一个输入行,tfgets 返回 NULL。否则,它返回一个指向输入行的指针。

练习题 8.26

以图 8-23 中的示例作为开始点,编写一个支持作业控制的 shell 程序。shell 必须具有以下特性:

  • 用户输入的命令行由一个 name、零个或者多个参数组成,它们都由一个或者多个空格分隔开。如果 name 是一个内置命令,那么 shell 就立即处理它,并等待下一个命令行。否则,shell 就假设 name 是一个可执行文件,在一个初始的子进程(作业)的上下文中加载并运行它。作业的进程组 ID 与子进程的 PID 相同。

  • 每个作业是由一个进程 ID(PID)或者一个作业 ID(JID)来标识的,它是由一个 shell 分配的任意的小正整数。JID 在命令行上用前缀 “%” 来表示。比如,“%5” 表示 JID 5,而 “5” 表示 PID 5。

  • 如果命令行以 & 来结束,那么 shell 就在后台运行这个作业。否则,shell 就在前台运行这个作业。

  • 输入 Ctrl+C(Ctrl+Z),使得内核发送一个 SIGINT(SIGTSTP)信号给 shell,shell 再转发给前台进程组中的每个进程。✦

  • 内置命令 jobs 列出所有的后台作业。

  • 内置命令 bg job 通过发送一个 SIGCONT 信号重启 job,然后在后台运行它。job 参数可以是一个 PID,也可以是一个 JID。

  • 内置命令 fg job 通过发送一个 SIGCONT 信号重启 job,然后在前台运行它。

  • shell 回收它所有的僵死子进程。如果任何作业因为收到一个未捕获的信号而终止,那么 shell 就输出一条消息到终端,消息中包含该作业的 PID 和对该信号的描述。

✦:注意这是对真实的 shell 工作方式的简化。真实的 shell 里,内核响应 Ctrl+C(Ctrl+Z),把 SIGINT(SIGTSTP)直接发送给终端前台进程组中的每个进程。shell 用 tcsetpgrp 函数管理这个进程组的成员,用 tcsetattr 函数管理终端的属性,这两个函数都超出了本书讲述的范围。可以参考【62】获得详细信息。

图 8-46 展示了一个 shell 会话示例。

linux> ./shell                              # Run your shell program

>bogus
bogus: Command not found.                   # Execve can’t find executable

>foo 10
Job 5035 terminated by signal: Interrupt    # User types Ctrl+C

>foo 100 &
[1] 5036 foo 100 &

>foo 200 &
[2] 5037 foo 200 &

>jobs
[1] 5036 Running foo 100 &
[2] 5037 Running foo 200 &

>fg %1
Job [1] 5036 stopped by signal: Stopped     # User types Ctrl+Z

>jobs
[1] 5036 Stopped foo 100 &
[2] 5037 Running foo 200 &

>bg 5035
5035: No such process

>bg 5036
[1] 5036 foo 100 &

>/bin/kill 5036
Job 5036 terminated by signal: Terminated

> fg %2                                     # Wait for fg job to finish

>quit

linux>                                      # Back to the Unix shell

图 8-46 家庭作业 8.26 的 shell 会话示例

上一页8.8 小结下一页第 9 章:虚拟内存

最后更新于4年前

下面的函数会打印多少行输出?用一个 n 的函数给出答案。假设n⩾1\small n \geqslant 1n⩾1。