M1: 打印进程树 (pstree)

实验指向:

https://jyywiki.cn/OS/2024/labs/M1.md

感谢jyy老师开源的OS课程,受益匪浅。
同时根据《操作系统》实验须知 与 Academic Integrity ,故不直接提供代码(水平也有限)
介绍:

1
计算机世界有趣的地方在于,你可以动手构建任何你认为应该可以实现的东西。我们的热身实验是一个很简单的代码练习——但我们并不是实现类似于排序、最短路径这样的算法习题,而是一个真正有意义的 “实用工具”:了解操作系统中运行的程序 (进程) 之间的层级关系,并打印进程树。如果你觉得打印进程树这个问题比较困难,我们也把问题分解一下:
  1. 得到命令行的参数,根据要求设置标志变量的数值;
  2. 得到系统中所有进程的编号 (每个进程都会有唯一的编号) 保存到列表里;
  3. 对列表里的每个编号,得到它的的父亲是谁;
  4. 在内存中把树建好,按命令行参数要求排序;
  5. 把树打印到终端上。

实验要求:实现 pstree 打印进程之间的树状的父子关系

Linux 系统中可以同时运行多个程序。运行的程序称为进程。除了所有进程的根之外,每个进程都有它唯一的父进程,你的任务就是把这棵树在命令行中输出。你可以自由选择展示树的方式 (例如使用缩进表示父子关系)。

实验流程 + 所思

☕️解决问题

我们把问题分解一下:

  1. 得到命令行的参数,根据要求设置标志变量的数值;
  2. 得到系统中所有进程的编号 (每个进程都会有唯一的编号) 保存到列表里;
  3. 对列表里的每个编号,得到它的的父亲是谁;
  4. 在内存中把树建好,按命令行参数要求排序;
  5. 把树打印到终端上。

万物之始,开始去做

功能实现:

实现在pstree命令中,-p-n-V是可选参数的作用,具体情况如下:

  1. -p:此选项用于显示进程的PID(进程ID)。当使用-p选项时,pstree会在输出中显示每个进程的PID。
  2. -n:此选项用于以数值排序的方式显示进程。通常情况下,pstree会按照字母顺序对进程进行排序,但使用了-n选项后,它会以数值的方式进行排序。
  3. -V:此选项用于显示pstree命令的版本信息。当使用了-V选项时,pstree会显示版本信息并退出,不会执行其他操作。

这些选项可以根据需要单独使用,也可以组合使用,例如-pn-np等。每个选项都可以根据具体需求来选择是否使用,以满足用户的特定要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc, char *argv[]) {

char *ops = readcmdops(argc, argv);// 获取命令行参数
if (!ops)
{
//default
}
else{
if(!strcmp(ops,"p"))
{
// -p:此选项用于显示进程的PID(进程ID)。
}else if(!strcmp(ops,"n"))
{
// -n:此选项用于以数值排序的方式显示进程。
}else if(!strcmp(ops,"V"))
{
// -V:此选项用于显示pstree命令的版本信息。
printf("stree (PSmisc) Cloudh2o \nCopyright (C) 1993-2019 Werner Almesberger and Craig Small\nPSmisc comes with ABSOLUTELY NO WARRANTY.\nThis is free software, and you are welcome to redistribute it under\nthe terms of the GNU General Public License.\nFor more information about these matters, see the files named COPYING.\n Klone by Cloudh2o for NJUOS M1.");
}else{
printf("This ops is: %s\n ","Wrong ops");
return -1;
}
}
return 0;
}

进程

进程是操作系统中的对象,因此操作系统一定提供了 API 访问它们。已经剧透过,系统里的每个进程都有唯一的编号,它在 C 语言中的类型是 pid_t

有关Linux使用C语言获取进程信息的内容借鉴了

https://blog.csdn.net/Once_day/article/details/136245660

前情

在计算机的世界里,Linux进程是一个非常基础而且关键的概念。它可以被理解为正在执行的一个程序的实例。每个进程都有自己独特的身份,我们称之为进程ID(PID),就像每个人都有自己的身份证号码一样。

进程有不同的状态,包括运行(正在使用CPU)、等待(等待资源可用)、停止(被挂起)和僵尸(已完成执行但等待父进程读取状态的进程)。进程的终止可以通过多种方式,如正常退出、被其他进程杀死(通过发送信号)、或系统强制终止。进程结束后,其资源会被回收,但进程的退出状态(exit status)会被保存,这对于调试和错误处理非常重要。

1
Linux系统还支持进程的层级关系,也就是父子进程关系。当一个进程创建一个新进程时,原来的进程就是父进程,新创建的就是子进程。
介绍/proc目录信息

在Linux操作系统中,/proc目录是一个非常特殊和有趣的存在,它实际上并不是存储在硬盘上的真实文件系统,而是一个虚拟文件系统,通常被称为进程信息伪文件系统(Process Information Pseudo-File System)。/proc提供了一个窗口,通过它可以窥见内核中的世界,包括系统信息及正在运行的进程详情。

/proc目录中的文件和子目录大都以数字命名,这些数字对应着系统中的进程ID(PID)。每一个这样的目录里面,包含了与该PID相关的信息,例如进程的内存映射(/proc/<pid>/maps)、环境变量(/proc/<pid>/environ)、可执行文件的链接(/proc/<pid>/exe)等等。这些文件为系统管理员和程序员提供了一种简便的方式来监控进程和系统的内部状态。

除了进程相关信息,/proc目录还包含了许多全系统范围的信息。例如,/proc/meminfo文件包含了内存使用情况的详细信息,/proc/cpuinfo提供了CPU的相关信息,/proc/net目录包含了网络协议和配置的信息,等等。

值得一提的是,/proc目录下的文件大多是可读的文本文件,可以直接使用常用的文本工具查看,如catless等。但也有一些文件是可写的,通过对这些文件写入特定的值,用户或程序员可以调整或配置内核参数,这是实现内核动态调优的一种方式。

一个常见的应用场景是,当系统管理员想要快速检查系统的运行状态,或者程序员在开发过程中需要获取系统或进程的某些信息时,他们就会查阅/proc目录下的文件。例如,通过读取/proc/uptime文件,可以获取系统已经运行了多长时间;通过查看/proc/loadavg文件,可以了解系统的平均负载情况。

常见的子目录和文件及其功能:

路径 功能
/proc/[pid] 包含每个进程的信息
/proc/cpuinfo 包含有关CPU的信息
/proc/meminfo 包含有关内存的信息
/proc/net 包含有关网络的信息
/proc/filesystems 包含当前系统支持的文件系统类型列表
/proc/loadavg 包含系统平均负载的信息
/proc/sys/kernel 包含内核参数的目录,可用于设置和查询内核参数
/proc/sys/fs 包含文件系统参数的目录,可用于设置和查询文件系统参数
/proc/sys/net 包含网络参数的目录,可用于设置和查询网络参数
/proc/sys/vm 包含虚拟内存参数的目录,可用于设置和查询虚拟内存参数
/proc/version 包含有关内核版本的信息
/proc/cmdline 包含启动时内核的命令行参数
/proc/self 一个指向当前进程自身的符号链接
/proc/sysinfo 包含系统信息的文件,如架构、主机名等
大思路

1.遍历/proc目录下的数字开头的目录(数字即进程pid)

2.建树和打印

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
函数 readcmdops(argc, argv):
如果 argc 等于 1:
返回空指针
否则:
从第二个参数开始的字符串去除 '-' 符号,得到指令参数 ops
返回 ops

函数 readprocessname_ppid(pid, name[]):
从 /proc/pid/stat 文件中读取进程名称和父进程ID
将读取的进程名称和父进程ID存储到 name 数组中
返回父进程ID

函数 setProcessInfo():
打开 /proc 目录
遍历目录下的文件,解析文件名为进程ID
读取每个进程的名称和父进程ID,并存储到数组 pidinfos 中

函数 findallchildrens(pid, index[]):
根据给定的父进程ID pid,在 pidinfos 数组中查找所有子进程的索引,并存储到 index 数组中

函数 creat_tree(root, tab_length):
根据给定的根节点 root 和 tab_length,递归地创建进程树
遍历根节点的子节点,并根据 tab_length 控制缩进
打印每个节点的名称和进程ID
递归地对每个子节点调用 creat_tree

函数 creat_tree_nopid(root, tab_length):
与 creat_tree 类似,但不打印进程ID,只打印名称

主函数 main(argc, argv):
读取命令行参数 ops
如果 ops 为空:
调用 setProcessInfo() 读取进程信息
创建根节点 root
调用 creat_tree_nopid(root, 0) 打印进程树
否则:
根据 ops 的值选择不同的操作:
如果 ops 为 'p':
调用 setProcessInfo() 读取进程信息
创建根节点 root
调用 creat_tree(root, 0) 打印进程树(包含进程ID)
如果 ops 为 'n':
调用 setProcessInfo() 读取进程信息
创建根节点 root
调用 creat_tree_nopid(root, 0) 打印进程树(不包含进程ID)
如果 ops 为 'V':
打印版本信息
否则:
输出错误提示信息