从0.1开始实现丐版pstree
Marce1

M1: 打印进程树 (pstree)

实验目的:使用c语言实现pstree,理解参数解析原理、Linux中的进程架构,了解相应的api调用

实验准备:Linux环境、vscode、gcc编译器

尽量不使用大模型,通过搜索引擎、官方文档和man page主动解决问题,英文的话硬读呗,一读一个不吱声/

首先无关进程和api那些,我首先考虑的是如何编写一个可以合理解析命令行参数的程序

从参数解析开始

如果你把玩一下Linux原生的pstree,你会发现这个样子:

image-20241230232550421

他会给你很多的选项声明,有的选项还包含参数。

比如最简单的gcc

1
gcc -o test test.c

我们可以直接从main函数中的argc、argv获取传入参数数量和具体内容,但POSIX对选项和参数的格式有一定规定,我们需要按照它的要求来,比如-h,–help

Utility Conventions

幸运的是这个早就有人帮我们做好了,从实验手册上我了解到我不必亲自去实现这个解析参数的过程,就是getopt函数

1
man 3 getopt #3代表man中的第三章节,库函数部分

getopt只支持解析字符选项,比如 -a -h,getopt_long在此基础上还支持解析字符串选项和对应的参数,比如–help –show-pids

读man page的确是一种煎熬,我磕磕绊绊读完了getopt的部分,大致知道了怎么使用,后面getopt_long看得一头雾水查了搜索引擎,看了篇csdn再回头看了遍man page,感觉的确是那么个意思,如果能坚持阅读英文文档rtfm或许真正意味着“read the friendly manual”?

C语言linux getopt_long()函数(命令行解析)(getopt、getopt_long_only)(短选项 -,长选项 –)(option结构体)(optind、optarg变量)_getopt——long-CSDN博客

然后理所当然是实践部分

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>

//展示版本,对应指令-V、--version
void show_version(){
printf("pstree 0.0.1\n");
}

//方便打印帮助时遍历选项和参数
static struct option_info{
char short_opt;
char long_opt[16];
char description[100];
}option_infos[] = {
{'p', "show-pids", "show PIDs"},
{'n', "numeric-sort", "sort output by PID"},
{'h', "help", "show this page"},
{'V', "version", "show version"}
};

//打印帮助
void print_help(){
printf("Useage: ./pstree [-pnh]\n or: ./pstree -V\n\n");
printf("Display a tree of processes.\n\n");
for (int i = 0; i < sizeof(option_infos)/sizeof(option_infos[0]); i++){
printf(" -%c --%-14s %s\n", option_infos[i].short_opt, option_infos[i].long_opt, option_infos[i].description); //逐个遍历结构体,字符串向左对齐
}
}


int main(int argc, char *argv[])
{
int show_pid = 0;
int numeric_sort = 0;
int opt;
static struct option long_options[] = {
{"show-pids", no_argument, 0, 'p'},
{"numeric-sort", no_argument, 0, 'n'},
{"version", no_argument, 0, 'V'},
{"help", no_argument, 0, 'h'}
}; //getopt_long的第三个参数结构体
while((opt = getopt_long(argc, argv, "pnVh", long_options, 0)) != -1){
switch (opt){
case 'p':
printf("show pids\n");
show_pid = 1;
break;
case 'n':
printf("numeric sort\n");
numeric_sort = 1;
break;
case 'V':
show_version();
break;
case 'h':
print_help();
break;
case '?': //当解析选项失败时getopt返回字符'?',表示输入选项与支持不匹配
putchar('\n');
print_help();
break;
default:
printf("Something went wrong......\n");
printf("opt: %c\n", opt);
}
}每次调用getopt它就会自动遍历下一个选项和参数,直到读取完毕返回-1
}

除了实验要求的选项之外我还添加了–help与-h,照着原版pstree的报错界面搓了个打印界面

image-20241230234115828

看起来像那么回事。

一点点歪路

关于接下来具体实现pstree的内容,我的第一个想法是先去看一下原生pstree的源码,如何找呢?

image-20241230234303486

用dpkg -S命令可以找到包含pstree的软件包,我得知了psmisc内包含pstree

1
sudo apt-get install psmisc

解压后通过find能找到pstree.c

读着读着我开始觉得这不是一个好的切入,我觉得应当理解pstree的程序框架和实现思路再去读源码,直接读源码根本不知道他在干什么,效率低下难以为续。

话说pwn学了堆之后我一直想读读ptmalloc 的源码,我又去找了下载glibc源码

1
2
git clone https://mirrors.tuna.tsinghua.edu.cn/git/glibc.git
find ./ -name malloc.c

malloc.c位于malloc文件夹下,6000多行,他日再叙吧

获取pid

接下来我们需要获取当前会话内的所有进程的pid和他们之间的父子关系,

中我得知有两种可能的方法获取进程相关的信息:操作系统提供了类似于迭代器的api、Linux中有一个文本文件会储存关于进程的信息。

看到后一个方法我立即联想到在做pwn题时有时需要查看libc与程序基址的虚拟地址,一般可以在pwngdb里通过指令vmmap查看,但是同样可以在/proc下找到相应进程文件夹中的maps文件中找到同样的信息,这也对应着Linux中一切皆文件的理念。我确信一定有一个文件能查看到所有进程对象的相应信息与关系,说不定ps、top这类指令的底层实现就是通过这样的方式获取的api?

其实答案就在谜面上,就是proc目录。第一时间我通过man page了解proc相关的内容

1
man 5 proc #5代表系统配置文件章节

文档很长,看过了很多无趣又难以理解的文件说明之外一个条目吸引了我:

image-20241231005038668

看来这就是我想要的滑板鞋!”This is used by ps(1)“更是印证了我之前的猜想,有种打破第六面墙的感觉!

在另一个终端上执行一个sleep 5000d的命令,通过ps找到它的pid,我确认了stat文件的内容:

image-20241231005410392

接下来就是将man page里的内容一一对应,通过遍历proc目录下所有进程的方式获取每个进程的信息与联系,自然就能顺理成章的实现pstree了。

操作系统课上通过strace验证了gcc的系统调用过程,我们也可以通过strace查看ps是否真正打开过各个进程的stat文件

1
2
3
strace -o output.txt ps
grep -v "*-1*" output.txt | grep open > filtered_output.txt
cat filtered_output.txt

image-20241231012220459

哇哦,的确如此。

image-20241231012656024

有意思。

##构建进程树

从man page中我得知stat文本中的第四个占位符ppid表示该进程的父进程,接下来需要考虑的就是如何遍历各个进程文件夹和建立一个树形数据结构了。

Linux C 讲解系统调用readdir, readdir_r 以及如何遍历目录下的所有文件 - 明明1109 - 博客园

由于/proc下的进程文件夹都是数字(pid)为名,所以很容易把他们筛选出来。

man page读一下opendir、snprintf和strtok的用法,在费劲调了不少segmental fault,虽然无比丑陋还是写出来了(

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
static struct proc_info{
char name[100];
int pid;
int ppid;
struct proc_info *next;
struct proc_info *child[20];
}*proc_infos;

int check_proc(char *name){
printf("checking %s\n", name);
for (int i = 0; name[i]!='\0'; i++){
if(name[i] < '0' || name[i] > '9'){
printf("checking %s fail, because %c\n", name, name[i]);
return 1;
}
}
return 0;
}

const char *proc_dir_path = "/proc";
struct proc_info *read_proc_info(char *name){
char file_path[300];
FILE *stat_file;
FILE *status_file;
char stat_content[1024];
char status_content[1024];
//pid
struct proc_info *proc = (struct proc_info *)malloc(sizeof(struct proc_info));
if(proc == NULL){
perror("malloc proc info fail\n");
exit(-1);
}
proc->next = NULL;
proc->pid = atoi(name);
//ppid
snprintf(file_path, sizeof(file_path), "/proc/%s/stat", name);
stat_file = fopen(file_path, "r");
if(stat_file == NULL){
perror("open stat file fail\n");
exit(-1);
}
char *token;
fgets(stat_content, sizeof(stat_content), stat_file);
token = strtok(stat_content, " ");
for (int i = 0; i < 3; i++){
token = strtok(NULL, " ");
printf("token%d: %s\n", i, token);
}
proc->ppid = atoi(token);
fclose(stat_file);
//name
snprintf(file_path, sizeof(file_path), "/proc/%s/status", name);
status_file = fopen(file_path, "r");
fgets(status_content, sizeof(status_content), status_file);
if(strncmp(status_content, "Name:", 5) == 0){
token = strtok(status_content, ": \n\t");
token = strtok(NULL, ": \n\t");
}
printf("name: %s\n", token);
strncpy(proc->name, token, strlen(token)+1);
fclose(status_file);
return proc;
}

void sort_proc_info(){
struct proc_info *head = proc_infos->next;
struct proc_info *curr1 = head;
int pid;
int i;
while(curr1 != NULL){
i = 0;
pid = curr1->pid;
printf("parrent pid: %d\n", pid);
struct proc_info *curr2 = head;
while(curr2 != NULL){
if(curr2->pid == pid){
curr2 = curr2->next;
continue;
}
if(curr2->ppid == pid){
printf("child pid: %d\n", curr2->pid);
curr1->child[i++] = curr2;
}
curr2 = curr2->next;
}
curr1 = curr1->next;
}
}

//构建进程树
void build_tree(int show_pid, int numeric_sort){
DIR *proc_dir = opendir(proc_dir_path);
if (proc_dir == NULL){
perror("open proc dir fail\n");
exit(-1);
}
struct dirent *entry;
proc_infos = (struct proc_info *)malloc(sizeof(struct proc_info));
struct proc_info *front = proc_infos;
//判别文件名是否为数字
while((entry = readdir(proc_dir)) != NULL){
char *name = entry->d_name;
//读取stat文件
if(check_proc(name) == 0){
printf("checking %s success\n", name);
front->next = read_proc_info(name);
front = front->next;
}
}
closedir(proc_dir);
sort_proc_info();
//打印进程树
struct proc_info *curr = proc_infos->next;
struct proc_info *root = NULL;
while(curr != NULL){
printf("pid: %d, ppid: %d, name: %s\n", curr->pid, curr->ppid, curr->name);
if(curr->pid == 1){
root = curr;
}
curr = curr->next;
}
}

首先遍历所有进程文件夹,筛选以数字为名的文件夹,然后读取stat和status,将pid、ppid、proc name写入proc_info结构体,通过链表管理,然后遍历链表,通过确认ppid将子进程放入结构体child数组

  1. 使用了很多的打印查错……

  2. 用函数分治的好处除了简化复杂问题并提高可维护性之外,还能方便你gdb调试

  3. 如果不想查错的时候对着汇编调,编译的时候-g选项不会刨符号表,gdb里使用list命令可以查看对应的c源码位置

  4. sizeof不是函数……他是一个运算符,在编译时计算,这让我的输出结果摸不着头脑的只打印了少量字符,在例如strncmp的长度参数的情况下需要用strlen()替代之。尽管之前知道这个但还是铸币了

  5. 没有使用宏定义数组大小导致第二天自己的代码都找半天……也不利于维护

以上就构建好了一个刚刚能跑的进程树,相当于一个小小的数据结构课后作业……好吧,数据结构课的作业我是一次也没做过(

接下来就是如何打印一个能看的进程树了

打印进程树

先序遍历,并记录递归深度打印层次。没有花里胡哨,属实是丐中之丐(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void print_procs(struct proc_info *proc, int show_pid, int numeric_sort, int deep){
if(proc == NULL){
return;
}
for (int i = 0; i < deep; i++){
printf(" ");
}
if(show_pid == 0){
printf("%s\n", proc->name);
}else{
printf("(%d)%s\n", proc->pid, proc->name);
}
for (int i = 0; i < 20; i++){
if(proc->child[i] == NULL){
break;
}
print_procs(proc->child[i], show_pid, numeric_sort, deep+1);
}
}

image-20250120110632369

然而可能是readdir是固定按照文件名的ascii码来读取进程文件夹,读取进程的顺序一开始就是排好序的,这导致numeric_sort可有可无……

那就这样吧。

32位可移植代码

实验要求编写可移植的代码,要同时测试32位的版本。直接-m32,不出意外的打印过程中segmentation fault了。

image-20250120111624387

image-20250120112414637

原来是忘记把child初始化了,导致跳一大堆野指针(

1
2
3
for (int i = 0; i < 20; i++){
proc->child[i] = NULL;
}

image-20250120112705903

测试了-h -V -p,顺利通过了。

最后附上全部源码:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <fcntl.h>

//打印版本号
void show_version(){
printf("pstree 0.0.1\n");
}

//选项信息,用于打印帮助
static struct option_info{
char short_opt;
char long_opt[16];
char description[100];
}option_infos[] = {
{'p', "show-pids", "show PIDs"},
{'n', "numeric-sort", "sort output by PID"},
{'h', "help", "show this page"},
{'V', "version", "show version"}
};

//打印帮助
void print_help(){
printf("Useage: ./pstree [-pnh]\n or: ./pstree -V\n\n");
printf("Display a tree of processes.\n\n");
for (int i = 0; i < sizeof(option_infos)/sizeof(option_infos[0]); i++){
printf(" -%c --%-14s %s\n", option_infos[i].short_opt, option_infos[i].long_opt, option_infos[i].description);
}
}

static struct proc_info{
char name[100];
int pid;
int ppid;
struct proc_info *next;
struct proc_info *child[20];
}*proc_infos;

int check_proc(char *name){
//printf("checking %s\n", name);
for (int i = 0; name[i]!='\0'; i++){
if(name[i] < '0' || name[i] > '9'){
//printf("checking %s fail, because %c\n", name, name[i]);
return 1;
}
}
return 0;
}

const char *proc_dir_path = "/proc";
struct proc_info *read_proc_info(char *name){
char file_path[300];
FILE *stat_file;
FILE *status_file;
char stat_content[1024];
char status_content[1024];
//pid
struct proc_info *proc = (struct proc_info *)malloc(sizeof(struct proc_info));
if(proc == NULL){
perror("malloc proc info fail\n");
exit(-1);
}
proc->next = NULL;
for (int i = 0; i < 20; i++){
proc->child[i] = NULL;
}
proc->pid = atoi(name);
//ppid
snprintf(file_path, sizeof(file_path), "/proc/%s/stat", name);
stat_file = fopen(file_path, "r");
if(stat_file == NULL){
perror("open stat file fail\n");
exit(-1);
}
char *token;
fgets(stat_content, sizeof(stat_content), stat_file);
token = strtok(stat_content, " ");
for (int i = 0; i < 3; i++){
token = strtok(NULL, " ");
//printf("token%d: %s\n", i, token);
}
proc->ppid = atoi(token);
fclose(stat_file);
//name
snprintf(file_path, sizeof(file_path), "/proc/%s/status", name);
status_file = fopen(file_path, "r");
fgets(status_content, sizeof(status_content), status_file);
if(strncmp(status_content, "Name:", 5) == 0){
token = strtok(status_content, ": \n\t");
token = strtok(NULL, ": \n\t");
}
//printf("name: %s\n", token);
strncpy(proc->name, token, strlen(token)+1);
fclose(status_file);
return proc;
}

void sort_proc_info(){
struct proc_info *head = proc_infos->next;
struct proc_info *curr1 = head;
int pid;
int i;
while(curr1 != NULL){
i = 0;
pid = curr1->pid;
//printf("parrent pid: %d\n", pid);
struct proc_info *curr2 = head;
while(curr2 != NULL){
if(curr2->pid == pid){
curr2 = curr2->next;
continue;
}
if(curr2->ppid == pid){
//printf("child pid: %d\n", curr2->pid);
curr1->child[i++] = curr2;
}
curr2 = curr2->next;
}
curr1 = curr1->next;
}
}

void print_procs(struct proc_info *proc, int show_pid, int numeric_sort, int deep){
if(proc == NULL){
return;
}
for (int i = 0; i < deep; i++){
printf(" ");
}
if(show_pid == 0){
printf("%s\n", proc->name);
}else{
printf("(%d)%s\n", proc->pid, proc->name);
}
for (int i = 0; i < 20; i++){
if(proc->child[i] == NULL){
break;
}
print_procs(proc->child[i], show_pid, numeric_sort, deep+1);
}
}

//构建并打印进程树
void build_tree(int show_pid, int numeric_sort){
DIR *proc_dir = opendir(proc_dir_path);
if (proc_dir == NULL){
perror("open proc dir fail\n");
exit(-1);
}
struct dirent *entry;
proc_infos = (struct proc_info *)malloc(sizeof(struct proc_info));
struct proc_info *front = proc_infos;
//判别文件名是否为数字
while((entry = readdir(proc_dir)) != NULL){
char *name = entry->d_name;
//读取stat文件
if(check_proc(name) == 0){
//printf("checking %s success\n", name);
front->next = read_proc_info(name);
front = front->next;
}
}
closedir(proc_dir);
sort_proc_info();
//打印进程树
struct proc_info *curr = proc_infos->next;
struct proc_info *root = NULL;
while(curr != NULL){
//printf("pid: %d, ppid: %d, name: %s\n", curr->pid, curr->ppid, curr->name);
if(curr->pid == 1){
root = curr;
}
curr = curr->next;
}
print_procs(root, show_pid, numeric_sort, 0);
}




int main(int argc, char *argv[])
{
int show_pid = 0;
int numeric_sort = 0;
int opt;
static struct option long_options[] = {
{"show-pids", no_argument, 0, 'p'},
{"numeric-sort", no_argument, 0, 'n'},
{"version", no_argument, 0, 'V'},
{"help", no_argument, 0, 'h'}
};
while((opt = getopt_long(argc, argv, "pnVh", long_options, 0)) != -1){
switch (opt){
case 'p':
//printf("show pids\n");
show_pid = 1;
break;
case 'n':
//printf("numeric sort\n");
numeric_sort = 1;
break;
case 'V':
show_version();
return 0;
case 'h':
print_help();
return 0;
case '?':
putchar('\n');
print_help();
return 1;
default:
printf("Something went wrong......\n");
printf("opt: %c\n", opt);
return 1;
}
}
build_tree(show_pid, numeric_sort);
return 0;
}
由 Hexo 驱动 & 主题 Keep
总字数 47.9k 访客数 访问量