操作系统基础 | 5.8 进程实验

进程家族树 (Process Family Tree)

  1. 实验报告:准备好实验报告

  2. simple_fork.c:编写一个名为 simple_fork.c 的简短程序,使用 fork() 函数从父进程生成一个子进程。父进程应在 fork 之前打印一条语句,并在 fork 之后打印出子进程的 PID。子进程应在被生成后打印一条语句,使用 getpid()getppid() 函数打印出其自身的 PID 及其父进程的 PID。在您的树莓派上编译并运行您的程序,并将程序的输出作为此问题的答案。 提示:gcc编译gcc [source] -o [destination]

  3. tree_fork.c:编写第二个名为 tree_fork.c 的程序,该程序将一个整数作为命令行参数,并在最多 10 代的固定限制内,生成一个具有指定代数的“二进制家族树”进程。这将创建 2^n - 1 个进程,其中 n 是代数。对于 n=1,程序将创建 1 个进程;对于 n=5,将创建 31 个进程;对于 n=10,将创建 1023 个进程,依此类推。我们将代数限制为最多 10,因为更大的数字会运行很长时间,甚至可能冻结您的树莓派!

    • 如果未提供命令行参数,程序应输出有用的用法消息并退出。
    • 否则,它应将命令行参数转换为整数并存储在 generations 变量中。
    • 如果 generations 变量小于 1 或大于 10,程序应输出有用的用法消息并退出。
    • 否则,它(以及它生成的任何子进程)应执行以下操作(原始程序为第一代):
      • 打印一行,说明它属于哪一代及其自身的 PID;
      • 增加当前代数计数器;
      • 如果已达到最后一代(根据 generations 变量),则直接返回 0;否则,生成两个子进程;
      • 使用 wait(0) 等待任何成功生成的子进程完成;
      • 如果两个子进程都成功生成,则返回 0;如果任一生成(或两者都)失败,则返回 -1。
    • 提示:仔细检查每次调用 fork()(或您用于生成每个子进程的任何调用)返回的 pid_t 值非常重要,因为 (1) 这些调用可能失败(由负返回值指示),(2) 0 表示子进程正在运行,(3) 正数表示父进程正在运行。
    • 提示:基于此,使用递归,或使用带有几个 pid_t 变量(每个子进程一个)的 while 循环并明智地在子进程中运行的代码分支使用 continue 语句,可以很直接地实现本练习的逻辑。
    • 作为此练习的答案,请展示您的程序在运行 4 代时(如果正确实现,应总共生成 15 个进程)的输出。
  4. 修改 kobject 示例模块:现在我们将回到使用这些思想进行内核模块设计。首先,从您的 linux 源码目录中,找到并将文件 samples/kobject/kobject-example.c 复制到您保存内核模块代码的目录中。这是一个使用称为 kobjects 的功能的内核模块,它提供了一个在内核和用户空间之间交换数据的接口。每个数据项称为一个属性(attribute),对于每个属性,您需要提供一个 showstore 函数,分别在用户空间读取和写入这些值时被调用。

    • 该特定模块提供三个属性:foobazbar。加载后,您可以在 sysfs 文件系统中的 /sys/kernel/kobject_example/ 目录下找到它们。
    • 修改此文件,以便(通过使用 printk)在更新 foobazbar 中的任何一个时打印一条系统日志消息,并在消息中显示被更新变量的旧值和新值。
    • 现在我们可以像以前一样构建您修改后的模块。首先,更新您的 Makefile,使其包含新模块的相应 .o 文件目标,然后为您的模块生成 .ko 文件,如下所示:
      1
      2
      3
      module add arm-rpi
      LINUX_SOURCE=您的Linux内核源代码路径
      make -C $LINUX_SOURCE ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=$PWD modules
    • 然后,使用 sftp 将生成的 .ko 文件复制到您的树莓派上,并使用 sudo insmod 加载该模块。
    • 在具有 root 权限的终端中,您可以使用 cat 命令读取这些属性的值,并使用 echo 命令将值写入这些属性,例如:
      1
      2
      3
      4
      5
      sudo bash
      cd /sys/kernel/kobject_example/
      cat foo # 显示 foo 属性的当前(原始)值
      echo 42 > foo # 将值 42 写入属性 foo
      cat foo # 显示属性 foo 的新值
      注意:您必须有一个 root 终端(sudo bash 可以给您)才能写入这些命令(即 sudo echo 不起作用)。
    • 作为此练习的答案,请展示使用这些命令成功更改 foobarbaz 值的输出演示。
  5. family_reader.c 模块:现在我们将编写一个内核模块,通过 sysfs 接口读取一个 PID,并在系统日志中打印该进程的祖先谱系。

    • 创建一个基于您修改后的 kobject-example.c 文件的新内核模块文件 family_reader.c(即,首先请复制它)。该模块应在 /sys/kernel/fam_reader/ 下创建单个系统属性(像上一个练习一样是整数值)。当您向此属性写入一个整数时,您的模块应尝试打印出该 PID 的祖先谱系(即,它的父进程,然后是父进程的父进程,依此类推,一直追溯到 init 任务)。涉及几个步骤:
      • 旁注:现代 Linux 内核为了便于在不同虚拟主机之间迁移进程,区分了“真实”PID 和“虚拟”PID。虚拟 PID 是进程从用户空间看到的 PID。
      • 您需要将整数输入转换为合适的内核 PID。使用函数 find_vpid()(请参阅 include/linux/pid.hkernel/pid.c),它返回一个 struct pid *。此函数可能失败,因此在解引用指针之前务必检查其返回值。
      • 接下来,您可以通过将 struct pid * 和标志 PIDTYPE_PID 传递给函数 get_pid_task()(请参阅 include/linux/pid.hkernel/pid.c)来将其转换为 struct task_struct *。此函数可能失败,因此在解引用指针之前务必检查其返回值。
      • 一旦您有了 struct task_struct *,就可以访问它存储的任何数据。特别是,real_parent 字段存储了生成它的进程的 struct task_struct * 指针,comm 字段是给出命令名称的字符串。
        • 注意:有一个单独的字段叫做 parent,这不是我们本练习想要的。parent 是共享进程组信号并允许父子进程之间等待的逻辑父进程。
      • 回溯家族树,打印出每个任务的 PID 和命令名称,一直回溯到 PID 为 1 的 init 任务。
    • 像上一个模块一样编译您的新内核模块,然后使用 sftp 将生成的 .ko 文件复制到您的树莓派上。在您的树莓派上,安装该模块,然后使用 sudo bash 为您的终端会话提供 root 访问权限。在该模块的目录下(将在 /sys/kernel/ 下),使用 catecho 读取和写入该模块属性的值,然后使用 dmesg 确认系统日志显示您的模块工作正常。
    • 使用 ps 命令查找在您当前终端窗口中运行的 sudo 进程的 PID,使用 echo 将您的模块属性设置为该 PID,然后使用 dmesg 查看该进程的祖先谱系。作为此练习的答案,请展示显示 sudo 进程祖先谱系的系统日志消息。

可选拓展练习 (Optional Enrichment Exercises)

  1. 探索 task_structtask_struct 包含许多有趣的进程数据和进程记账信息。尝试将其他字段打印到系统日志中,并作为此练习的答案,请简要描述您打印了哪些内容,并展示执行此操作后的一些系统日志消息。
  2. 探索调度程序功能kernel/schedinclude/linux/sched 中的文件包含许多用于处理任务的功能,包括修改特定任务或迭代系统中每个任务的能力。例如,include/linux/sched/signal.h 定义了诸如 for_each_process() 之类的宏。尝试使用其中一些功能,并作为此练习的答案,请简要描述您做了什么以及观察到了什么。