操作系统基础 | 5.8 进程实验
进程家族树 (Process Family Tree)
实验报告:准备好实验报告
simple_fork.c:编写一个名为
simple_fork.c
的简短程序,使用fork()
函数从父进程生成一个子进程。父进程应在fork
之前打印一条语句,并在fork
之后打印出子进程的 PID。子进程应在被生成后打印一条语句,使用getpid()
和getppid()
函数打印出其自身的 PID 及其父进程的 PID。在您的树莓派上编译并运行您的程序,并将程序的输出作为此问题的答案。 提示:gcc编译gcc [source] -o [destination]
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 个进程)的输出。
修改 kobject 示例模块:现在我们将回到使用这些思想进行内核模块设计。首先,从您的 linux 源码目录中,找到并将文件
samples/kobject/kobject-example.c
复制到您保存内核模块代码的目录中。这是一个使用称为kobjects
的功能的内核模块,它提供了一个在内核和用户空间之间交换数据的接口。每个数据项称为一个属性(attribute),对于每个属性,您需要提供一个show
和store
函数,分别在用户空间读取和写入这些值时被调用。- 该特定模块提供三个属性:
foo
、baz
和bar
。加载后,您可以在 sysfs 文件系统中的/sys/kernel/kobject_example/
目录下找到它们。 - 修改此文件,以便(通过使用
printk
)在更新foo
、baz
或bar
中的任何一个时打印一条系统日志消息,并在消息中显示被更新变量的旧值和新值。 - 现在我们可以像以前一样构建您修改后的模块。首先,更新您的
Makefile
,使其包含新模块的相应.o
文件目标,然后为您的模块生成.ko
文件,如下所示:1
2
3module 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
命令将值写入这些属性,例如:注意:您必须有一个 root 终端(1
2
3
4
5sudo bash
cd /sys/kernel/kobject_example/
cat foo # 显示 foo 属性的当前(原始)值
echo 42 > foo # 将值 42 写入属性 foo
cat foo # 显示属性 foo 的新值sudo bash
可以给您)才能写入这些命令(即sudo echo
不起作用)。 - 作为此练习的答案,请展示使用这些命令成功更改
foo
、bar
和baz
值的输出演示。
- 该特定模块提供三个属性:
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.h
和kernel/pid.c
),它返回一个struct pid *
。此函数可能失败,因此在解引用指针之前务必检查其返回值。 - 接下来,您可以通过将
struct pid *
和标志PIDTYPE_PID
传递给函数get_pid_task()
(请参阅include/linux/pid.h
和kernel/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/
下),使用cat
和echo
读取和写入该模块属性的值,然后使用dmesg
确认系统日志显示您的模块工作正常。 - 使用
ps
命令查找在您当前终端窗口中运行的sudo
进程的 PID,使用echo
将您的模块属性设置为该 PID,然后使用dmesg
查看该进程的祖先谱系。作为此练习的答案,请展示显示sudo
进程祖先谱系的系统日志消息。
- 创建一个基于您修改后的
可选拓展练习 (Optional Enrichment Exercises)
- 探索 task_struct:
task_struct
包含许多有趣的进程数据和进程记账信息。尝试将其他字段打印到系统日志中,并作为此练习的答案,请简要描述您打印了哪些内容,并展示执行此操作后的一些系统日志消息。 - 探索调度程序功能:
kernel/sched
和include/linux/sched
中的文件包含许多用于处理任务的功能,包括修改特定任务或迭代系统中每个任务的能力。例如,include/linux/sched/signal.h
定义了诸如for_each_process()
之类的宏。尝试使用其中一些功能,并作为此练习的答案,请简要描述您做了什么以及观察到了什么。