M1任务回顾
- 实现pstree,打印树状的进程结构
- 支持
-p
、-n
、-V
三个基本选项
实现过程
框架代码
当我怀着喜庆的心情迎接OSLabs的框架代码的时候,打开发现pstree.c
其实就是个Hello World,只想说一句nmd wsm。
这个框架代码真是太精致(简陋)了啊
识别选项
这个部分就用到了ICS-PA带来的丰富经验。与其用循环来一个字符一个字符的判断选项,不如直接开一个数组,用一次循环来匹配所有可能的选项字符串,如果成功就把对应的开关打开,如果没有匹配到就报错结束运行。
读取进程文件
读取文件夹需要<dirent.c>
,使用DIR *dir = opendir('path_to_dir');
来打开文件夹,然后用while ((dp = readdir(dir)) != NULL)
来读取所有的内容。此处的dp
需要是struct dirent*
类型。
有一点比较奇幻的是,readdir
的读取顺序是先.
和..
,然后按照字母顺序读,最后按照数字从小到大(非字典序)读取文件夹。这样就省去了手动排序的麻烦。
读取线程文件
/proc/$PID/task
文件夹中存放了线程相关信息,但对应的stat文件中的内容和进程不同。进程的父母进程使用ppid
表示,而线程则是使用tgid
来表示。
但是在pstree里,只需要把父母进程的pid
直接赋值成线程的ppid
,然后把线程的名称更改为{ParentName}
,就可以和进程同等对待了。
构建进程树
进程树的根是systemd(1)
进程。这个节点是一个静态的全局变量。整个树使用指针相互连接,每个节点分别有指向先♂辈、第一个子孙和下一个兄妹的3个指针。
构建进程树我使用了两个函数,分别是
struct process* findProcess(pid_t pid)
,用于在树上遍历找到指定pid的进程节点并返回地址,或者返回NULL
。void addProcess(struct process*)
,用于往树中插入一个节点,首先找到父亲节点,然后根据是否需要排序决定其在兄弟链表上的插入位置。
打印进程树
打印进程树使用了双层递归的想法,整个代码没超过20行。
void printProcess(struct process*)
,打印当前节点并递归打印子孙和兄弟。由于先打印子孙,整个树先在横向上扩展,然后向下扩展打印子孙的兄弟。子孙递归结束后再打印本层的其他兄弟。void printParentProcesses(struct process*)
,打印当前节点的前辈。这个函数是用来打印一个节点前面的空格和竖线的。方法是不断向parent
方向递归,打印和名称一样数量的空格,然后再打印一条竖线。
用到的小技巧
- ICS-PA式的数组和字符串判断,上面已经介绍过了。
- 如果要打印pid,就把pid打印到进程结构体的名称数组里,这样在打印空格的时候就可以直接代码复用。
- 利用函数参数是否为
NULL
来执行不同的功能,实现代码复用。读取进程的时候,如果parent == NULL
,则是读取进程,否则是读取子进程/线程。 - 输出多个空格的方法:
printf("%*s", n, "");
踩到的坑
sizeof
函数对移植并不友好。他的长度在32和64位系统下不一致。同时由于一开始使用了#define
导致无法指定数字的格式,就产生了错误。sprintf
需要考虑数组的长度。如果打印%d
,就可能由于字符串长度不足产生溢出(提示为warning)。对于字符串可以使用%.ns
来限制打印的长度。如果是数字可以先打印成字符串再进行第二次打印。sprintf(tmp, "%d", src); sprintf(dst, "%.ns", tmp);
- 递归是先递归还是先执行函数,这是一个很玄学的问题。如果脑子转不过弯来不如排列组合尝试一下总归有一种是对的。
FILE stream
一定要记得关掉。测试的时候由于只有fopen
而没有fclose
导致后面很多进程文件没能打开,整个进程树少了一大半。
总结与经验
- KISS。美观程度不重要,先把功能做出来了再说。在实现功能的时候我总是想着一步到位,结果打印出来的格式乱七八糟,也找不到问题出在哪。(后来发现是链表插入和递归打印格式同时出错。)
- 写程序前先总体构思,由上至下,有利于提高代码模块化、复用程度并减少bug出现的概率。
Loading Comments By Disqus