M1: 打印进程树 (pstree)
实验目的:使用c语言实现pstree,理解参数解析原理、Linux中的进程架构,了解相应的api调用
实验准备:Linux环境、vscode、gcc编译器
尽量不使用大模型,通过搜索引擎、官方文档和man page主动解决问题,英文的话硬读呗,一读一个不吱声/
首先无关进程和api那些,我首先考虑的是如何编写一个可以合理解析命令行参数的程序
从参数解析开始
如果你把玩一下Linux原生的pstree,你会发现这个样子:

他会给你很多的选项声明,有的选项还包含参数。
比如最简单的gcc
我们可以直接从main函数中的argc、argv获取传入参数数量和具体内容,但POSIX对选项和参数的格式有一定规定,我们需要按照它的要求来,比如-h,–help
Utility Conventions
幸运的是这个早就有人帮我们做好了,从实验手册上我了解到我不必亲自去实现这个解析参数的过程,就是getopt函数
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>
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'} }; 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 '?': putchar('\n'); print_help(); break; default: printf("Something went wrong......\n"); printf("opt: %c\n", opt); } }每次调用getopt它就会自动遍历下一个选项和参数,直到读取完毕返回-1 }
|
除了实验要求的选项之外我还添加了–help与-h,照着原版pstree的报错界面搓了个打印界面

看起来像那么回事。
一点点歪路
关于接下来具体实现pstree的内容,我的第一个想法是先去看一下原生pstree的源码,如何找呢?

用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相关的内容
文档很长,看过了很多无趣又难以理解的文件说明之外一个条目吸引了我:

看来这就是我想要的滑板鞋!”This is used by ps(1)“更是印证了我之前的猜想,有种打破第六面墙的感觉!
在另一个终端上执行一个sleep 5000d的命令,通过ps找到它的pid,我确认了stat文件的内容:

接下来就是将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
|

哇哦,的确如此。

有意思。
##构建进程树
从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]; 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); 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); 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; 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数组
使用了很多的打印查错……
用函数分治的好处除了简化复杂问题并提高可维护性之外,还能方便你gdb调试
如果不想查错的时候对着汇编调,编译的时候-g选项不会刨符号表,gdb里使用list命令可以查看对应的c源码位置
sizeof不是函数……他是一个运算符,在编译时计算,这让我的输出结果摸不着头脑的只打印了少量字符,在例如strncmp的长度参数的情况下需要用strlen()替代之。尽管之前知道这个但还是铸币了
没有使用宏定义数组大小导致第二天自己的代码都找半天……也不利于维护
以上就构建好了一个刚刚能跑的进程树,相当于一个小小的数据结构课后作业……好吧,数据结构课的作业我是一次也没做过(
接下来就是如何打印一个能看的进程树了
打印进程树
先序遍历,并记录递归深度打印层次。没有花里胡哨,属实是丐中之丐(
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); } }
|

然而可能是readdir是固定按照文件名的ascii码来读取进程文件夹,读取进程的顺序一开始就是排好序的,这导致numeric_sort可有可无……
那就这样吧。
32位可移植代码
实验要求编写可移植的代码,要同时测试32位的版本。直接-m32,不出意外的打印过程中segmentation fault了。


原来是忘记把child初始化了,导致跳一大堆野指针(
1 2 3
| for (int i = 0; i < 20; i++){ proc->child[i] = NULL; }
|

测试了-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){ for (int i = 0; name[i]!='\0'; i++){ if(name[i] < '0' || name[i] > '9'){ 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]; 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); 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, " "); } proc->ppid = atoi(token); fclose(stat_file); 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"); } 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; struct proc_info *curr2 = head; while(curr2 != NULL){ if(curr2->pid == pid){ curr2 = curr2->next; continue; } if(curr2->ppid == 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; if(check_proc(name) == 0){ 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){ 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': show_pid = 1; break; case '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; }
|