这是对 22-23 年秋季学期南京大学操作系统课程的 Lab 的实验记录,这边为蒋炎岩老师班的课程。

本博客是对 OS Lab M1 的编程实验的个人的思考过程与部分解答。

课程主页: http://jyywiki.cn/OS/2023/

pstree实验主页: https://jyywiki.cn/OS/2023/labs/M1.html

注意:这不是非官方答案,不完全正确,仅适当参考。

注意:请勿直接复制粘贴,否则后果自负。


​ 在这个光怪陆离的人间,没有谁可以将日子过得行云流水。但我始终相信,走过平湖烟雨,岁月山河,那些历尽劫数、尝遍百味的人,会更加生动而干净。 时间永远是旁观者,所有的过程和结果,都需要我们自己承担。

​ ——《因为懂得,所以慈悲——张爱玲的倾城往事》


整体思路

实现的整体思路在实验主页已经较为详细的描述,这边再复述一遍:

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

因此我们需要做的便是逐项完成上面的步骤,便可完成对pstree的编写。

准备工作

由于编写程序的过程中会遇到各种各样千奇百怪的bug,并且面对这些Bug时,我们都无法快速的定位到Bug的位置,从而在Debug的时候花费大量的时间做无用功。

对此,我们并非束手无策,jyy说过,当你遇到一些反复重复的无意义的工作的时候,一定会有某种方法去简化它,因此,在这个问题上,我们可以通过插入大量的assert断言来进行Bug的定位,也就是jyy所说的防御性编程。这些assert代码对你的程序逻辑没有任何的帮助,但他们却能够在你遇到Bug时能够迅速的缩小Bug的范围并去定位它,从而帮助你快速的找到Bug的所在地。

当然,为了方便我们的理解,我们稍微对assert进行了一些包装,能够在定位错误的同时告知我们错误的原因:

1
2
3
4
5
6
7
8
9
10
11
#define RED "\33[1;33m"
#define ORI "\33[0m"
#define Assert(_con, _fmt, ...) \
do \
{ \
if (!(_con)) \
{ \
fprintf(stderr, RED "Assertion failed:\nLine:%d"_fmt, __LINE__, ##__VA_ARGS__); \
assert(0); \
} \
} while (0)

这样做之后,我们便可以在之后的细节的实现的过程中,通过插入大量的 Assert 来定位 bug,从而帮助我们更快的完成任务。

获取命令行参数

接下来便是按照流程来逐步完成pstree的构建,首先便是获取命令行参数。

这一环节较为的基础,拥有一定的c语言基础便可以轻松完成,如果没有尝试过从命令行获取参数,可以参考如下这篇文章: https://zhuanlan.zhihu.com/p/140534864

得到命令行的参数之后,根据要求设置标志变量的数值,这部分的内容较为简单,只需要对 argc 和 argv 参数进行分析即可:

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
50
51
52
53
54
55
56
57
58
59
60
61
int main(int argc, char *argv[])
{
bool isP = false; // Determine whether there is -p or --show-pids in the command line
bool isN = false; // Determine whether there is -n or --numeric-sort in the command line
bool isV = false; // Determine whether there is -V or --version in the command line
for (int i = 1; i < argc; i++)
{
/*
Output command line parameters
assert(argv[i]);
printf("argv[%d] = %s\n", i, argv[i]);
*/

// Judge whether the -p or --show-pids instruction appears
if (!strcmp(argv[i], "-p") || !strcmp(argv[i], "--show-pids"))
{
isP = true;
}
// Judge whether the -n or --numeric-sort instruction appears
else if (!strcmp(argv[i], "-n") || !strcmp(argv[i], "--numeric-sort"))
{
isN = true;
}
// Judge whether the -V or --version instruction appears
else if (!strcmp(argv[i], "-V") || !strcmp(argv[i], "--version"))
{
isV = true;
}
else
{
/*
In case of illegal instructions,
the program is forced to end to increase the robustness of the grogram
*/
printf("Unsupported arg(s):" RED "%s\n" ORI, argv[i]);
exit(0);
}
}

/*
Checkpoint one: the correct of isP, isN, isV
printf("%d %d %d \n", isP, isN, isV);
*/

// assert(!argv[argc]);
if (isV)
{
// Output version number
fprintf(stderr, "pstree (PSmisc) 1.0 CopyRight (C) 2023 Confetti-lxy\n");
fprintf(stderr, "PSmisc 不提供任何保证。\n");
fprintf(stderr, "该程序为自由软件,欢迎你在 GNU 通用公共许可证 (GPL) 下重新发布。\n");
fprintf(stderr, "详情可参阅 COPYING 文件。\n");
}
else
{
getCourseTree();
sortCourseTree(isN);
printCourseTree(isP, root, 0);
}
return 0;
}

注意注意: 强烈不推荐写一个超级长的main函数,一镜到底的coding方案不仅不利于阅读,也不利于你自己的代码编写和逻辑构建。建议一个逻辑块写成一个函数,通过函数调用的方式来进行,这种方案可以帮助你更加清晰的了解自己的代码结构和逻辑。

获取进程号

到了这个环节,一开始的反应可能都是一点头绪都没有,很正常,不然这个Lab也就没有存在的意义了。jyy说过,一切上层的程序都是在调用操作系统的接口,我们所进行的一切操作都是建立在系统调用的基础上。因此,我们需要知道,这一步的完成我们要基于对操作系统的理解。

那么我们该如何去了解操作系统呢,很简单,jyy说过: RTFM and STFW。

对此,大家可以参考如下博客的内容并在此基础上进一步的去延申:

  1. https://blog.csdn.net/chen1415886044/article/details/128071810
  2. https://stackoverflow.com/questions/39066998/what-are-the-meaning-of-values-at-proc-pid-stat

在粗略的了解过后,便大该能够理解如何获取了,但这不够,我们需要自己coding来进一步明确。

由于我们要用一定的结构来承载我们获取到的进程号,因此,我们可以创建如下的结构体:

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
struct subprocessNode
{
pid_t subprocessNodePid;
char fileName[512];
/*
A structure dedicated to sorting,
which can satisfy the sorting of process number and process name
subprocessNodePid: The pid of the child node
fileName: The name of the file
*/
};
struct courseNode
{
char fileName[512];
pid_t pid;
pid_t ppid;
int subprocessCount;
struct subprocessNode subprocessNodes[512];
struct courseNode *next;
/*
Construction of data structure of process node:
fileName: The name of the file
pid: the pid_t of the process
ppid:the father process number of this process
subprocessCount: the number of subprocesses
subprocessNodes: An array that stores pids and names of the child node
next: Linked list structure, pointing to the next independent process
*/
};

然后我们便可以在构造我们的进程号读取函数了:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void getCourseTree()
{
struct dirent *entry;
DIR *dirp;
FILE *fp;
Assert((dirp = opendir("/proc")), "Can not open /proc\n"); // Get file directory
while (true)
{
entry = readdir(dirp);
// If it is empty, the traversal will be ended
if (entry == NULL)
{
break;
}
/*
If it is not empty,
judge whether it is a directory and whether the name is a number
*/
if (entry->d_type == 4 && isAllNumString(entry->d_name))
{
/*
d_type == 4 means this is a directory
isAllNumString(d_name) == true means
the name of this directory consists of numbers
*/
char statPath[512] = "/proc/";
// Complete the acquisition of stat file address
strcat(statPath, entry->d_name), strcat(statPath, "/stat");
Assert((fp = fopen(statPath, "r")), "Can not open %s\n", statPath); // Open the stat file
struct courseNode *cur;
// Conduct classification discussion based on whether the name is 1
if (!strcmp(entry->d_name, "1"))
{
cur = root;
}
else
{
// Avoid stackOverflow
Assert((cur = (struct courseNode *)malloc(sizeof(struct courseNode))),
"malloc size for process failed");
tail->next = cur, tail = tail->next;
}
// Write to content
fscanf(fp, "%d (%s %*c %d", &cur->pid, cur->fileName, &cur->ppid);
cur->fileName[strlen(cur->fileName) - 1] = '\0', fclose(fp);
}
}
// Traverse the linked list and write the pid of the child process
// into the child process array of the parent process
for (struct courseNode *head = root; head != NULL; head = head->next)
{
for (struct courseNode *cur = head; cur != NULL; cur = cur->next)
{
if (cur != head && cur->ppid == head->pid)
{
bool flag = false;
// If it already exists, modify the process name to the same
for (int i = 0; i < head->subprocessCount; i++)
{
if (head->subprocessNodes[i].subprocessNodePid == cur->pid)
{
strcpy(head->subprocessNodes[i].fileName, cur->fileName);
flag = true;
break;
}
}
// Otherwise, write it to the array
if (!flag)
{
head->subprocessNodes[head->subprocessCount].subprocessNodePid = cur->pid;
strcpy(head->subprocessNodes[head->subprocessCount].fileName, cur->fileName);
head->subprocessCount++;
}
// Prevent overflow and increase robustness
Assert((head->subprocessCount <= 512), "Too many processes to fully load\n");
}
}
}
}

这一步完成后,我们也就获取到了现有的所有进程的进程号了,我们的任务也完成了一大半了,接下来的任务就是对获取到的信息进行整合排序即可。

对应父进程

在第二步中其实已经完成了对进程号的获取和其父进程的获取,结构体中的ppid就是这个进程的父进程,因此这一环节可以直接跳过,不再赘述。

建立进程树

建立进程树的环节在我的逻辑中也可以忽略掉,因为这边并未构建进程的树状结构,而是选择在打印的时候直接进行遍历(虽然这样会浪费大量的时间在无意义的查找上,但是由于进程的数量有限且较少,因此对时间的浪费总量还是较少的。虽然相对而言还是较多的,但就绝对用时而言还是可以接受的)

因此需要做的只是在需要按进程号排序的时候进行排序即可,排序这边选择的也是最简单直接的冒泡排序:

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
void sort(struct subprocessNode *nums, int len)
{
/*
Because the amount of data is not large,
the simplest bubble sorting can be used
*/
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1; j++)
{
if (strcmp(nums[j].fileName, nums[j + 1].fileName) > 0)
{
pid_t tmp1 = nums[j].subprocessNodePid;
nums[j].subprocessNodePid = nums[j + 1].subprocessNodePid;
nums[j + 1].subprocessNodePid = tmp1;
char tmp2[512];
strcpy(tmp2, nums[j].fileName);
strcpy(nums[j].fileName, nums[j + 1].fileName);
strcpy(nums[j + 1].fileName, tmp2);
}
}
}
}
void sortCourseTree(bool isNModel)
{
/*
Because it is sorted by process number by default,
it only needs to be processed when sorting by process name
*/
if (isNModel == false)
{
struct courseNode *head = root;
while (head != NULL)
{
sort(head->subprocessNodes, head->subprocessCount), head = head->next;
}
}
}

至此任务基本已经完成,剩下来的就是打印罢了。

打印进程树

打印过程没有什么特殊的地方,只是我由于没有建树,因此在打印的过程需要额外添加搜索的过程,其他就没什么特殊的了。

当然,由于构造完善的树状打印过于耗时,因此这边选择直接用缩紧来区分父子进程:

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
void printCourseTree(bool isPModel, struct courseNode *r, int level)
{
for (int i = 0; i < level; i++)
{
printf("\t"); // Indent the relationship of processes
}
// Select the output mode according to the value of isPModel
if (isPModel)
{
printf("%s(%d)\n", r->fileName, r->pid); // Only output process name
}
else
{
// Output both process name and process numbers
printf("%s\n", r->fileName);
}
// Recursive output subprocess
for (int i = 0; i < r->subprocessCount; i++)
{
struct courseNode *node = find(r->subprocessNodes[i].subprocessNodePid);
if (node != NULL)
{
printCourseTree(isPModel, node, level + 1);
}
}
}

至此,pstree的任务基本上就算是完成了!

总结

将上述内容进行组合,便可以完成对 pstree 的构造。

这样的实现不一样是最优的,我为了可读性和可理解性牺牲了大量的效率,但个人认为这是相对而言可接受的。当然我的实现中还是有很多不足以及偷懒的地方,改进优化就后续再说吧!

此外可以考虑去看一看 busybox 里面对 pstree 的实现(某种意义上算不算作弊(大雾))