今天从这篇博客开始记录学习CS之路,本次学习的是南京大学蒋炎岩老师的操作系统:设计与实现 (2023 春季学期)。
蒋老师说,仅完成实验即可完成操作系统的学习,实际完成实验时确实发现了自己非常多有待欠缺的地方,第一个实验完成一个pstree做了数天也只完成了建树部分,打印进程树部分参考了SiyuanYue/NJUOSLab-M1-pstree这个实验才得以完成(我真是太菜了)
0x00阅读实验说明
实验描述/描述
实现 pstree 打印进程之间的树状的父子关系;Linux 的 psmisc 中 pstree 的实现大约有 1,300 行,支持多种命令行参数。这个实验里实现最简单的就行。大家可以先玩一下 Linux 的 pstree,使用 man 命令查看 pstree 支持的功能,并试一试。在这个实验中,我们需要实现它的简化版:
pstree [OPTION]…
把系统中的进程按照父亲-孩子的树状结构打印到终端。
-p 或 –show-pids: 打印每个进程的进程号。
-n 或 –numeric-sort: 按照pid的数值从小到大顺序输出一个进程的直接孩子。
-V 或 –version: 打印版本信息。
你可以在命令行中观察系统的 pstree 的执行行为 (如执行 pstree -V、pstree –show-pids 等)。这些参数可能任意组合,但你不需要处理单字母参数合并的情况,例如 -np。
学习POSIX标准
上述要求是按照man page的格式写出的,POSIX对命令行工具参数也有格式要求,见:Utility Argument Syntax
同时,POSIX标准也制定了程序返回值的含义,返回非0为失败,如diff 返回 1 表示比较的文件不同,返回 2 表示读取文件失败
procfs
在UNIX哲学中,Everything is a file. 因此,读取当前进程状态也使用读取文件的方式。在C语言中使用pid_t类型表示pid,在glibc中是typedef int pid_t,这一点在glibc文档中有说明。
man 5 proc
通过上述命令阅读文档,我们得到关键信息
- /proc/[pid]目录表示PID对应进程的信息
- /proc/[pid]/stat文件包含了进程名,ppid即父进程pid
0x01实验代码
由于对树的打印不够熟悉,参考了他人代码后才勉强写出来,目前这份代码有以下几个问题
- 进程名中出现空格会出现错位问题,这个问题源自读取/proc/[pid]/stat文件时使用空格分隔
- 输出的进程树上下没有连接起来,-+-分支部分没有和GNU pstree完全一致
- 在实验说明中描述到的可能导致UB的问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <dirent.h>
typedef struct pTree *node;
struct pTree
{
int pid;
char* p_name;
node children[100];
};
node node_num[65537];
int pid_num[65537];
char path[100] = {0};
int pid_count = 0;
void init_num()
{
for(int i=0; i<65537; i++)
{
node_num[i] = NULL;
pid_num[i] = 0;
}
}
// 获取stat文件的第[part]部分
char* get_stat(int pid, int part)
{
char* result;
const char space[2] = " ";
char stat_path[128] = {0};
char stat_buffer[128] = {0};
char* proc_result = (char*)malloc(256 *sizeof(char));
sprintf(stat_path ,"/proc/%d/stat", pid);
FILE *stat_fp = fopen(stat_path, "r");
if (stat_fp == NULL)
{
perror("fopen");
return NULL;
}
fread(stat_buffer, 1, sizeof(stat_buffer), stat_fp);
fclose(stat_fp);
/*
* 文件内容为
* 1 (systemd) S 0 1 1 0 -1 4194560...
* 需要获取到进程名system,故使用空格分隔
* 而后去除左右括号
*/
result = strtok(stat_buffer, space);
for(int i=0; i<part; i++)
{
result = strtok(NULL, space);
}
strcpy(proc_result, result);
return proc_result;
}
// 根据stat文件获取ppid
int get_proc_ppid(int pid)
{
int ppid = 0;
char* ppid_s = get_stat(pid, 3);
ppid = atoi(ppid_s);
return ppid;
}
// 根据stat文件获取进程名
char* get_proc_name(int pid)
{
char* name = get_stat(pid, 1);
// 去除左右括号
size_t len = strlen(name);
name[len - 1] = '\0';
for(int j=0; j<len; j++)
{
name[j] = name[j+1];
}
return name;
}
int create_tree()
{
DIR *dir;
struct dirent *entry;
// 获取pid列表
dir = opendir("/proc/");
if (dir == NULL)
{
perror("opendir");
return 1;
}
int i = 0;
while ((entry = readdir(dir)) != NULL)
{
// 建立树节点,并将进程名以及pid放入节点中
char *name = entry->d_name;
if(name[0] >= 48 && name[0] <= 57)
{
// printf("%s\n", name);
int pid = atoi(name);
pid_num[i] = atoi(name);
node pNode = (node)malloc(sizeof(struct pTree));
pNode->pid = pid;
pNode->p_name = get_proc_name(pid);
if(pNode->p_name == NULL)
{
perror("get_proc_name");
return 1;
}
// 全部节点指针存在一个全局指针数组中
node_num[i] = pNode;
i++;
}
pid_count = i;
}
closedir(dir);
// 遍历获取父子关系
for(int i=0; i<pid_count; i++)
{
node curr_node = node_num[i];
int pid = curr_node->pid;
int ppid = get_proc_ppid(pid);
if(ppid == 0)
{
continue;
}
else
{
for(int j=0; j<pid_count; j++)
{
node parent_node = node_num[j];
int parent_pid = parent_node->pid;
if(parent_pid == ppid)
{
int x = 0;
while((parent_node->children)[x] != NULL)
{
x++;
}
(parent_node->children)[x] = curr_node;
// 太菜了,写出这种五层嵌套的屎山代码
}
}
}
}
return 0;
}
void print_tree(node root, int tab_length, int pid_out_flag)
{
int child_flag = 2;
char str[100]={0};
// 叶子节点直接打印后返回
if((root->children)[0] == NULL)
{
if(pid_out_flag)
{
printf("%s(%d)", root->p_name, root->pid);
return;
}
else
{
printf("%s", root->p_name);
return;
}
}
if(pid_out_flag)
{
// 通过将待打印内容放在字符串中便于计算长度
sprintf(str,"%s(%d)--", root->p_name, root->pid);
}
else
{
sprintf(str,"%s--", root->p_name);
}
printf("%s", str);
// 判断该节点是否只有一个子进程
if((root->children)[1] != NULL)
{
printf("+-");
}
else
{
child_flag = 0;
}
// 这个变量相当于i
int while_count = 0;
while((root->children)[while_count] != NULL && while_count < 100)
{
node child_node = (root->children)[while_count];
int next_tab_len = strlen(str) + tab_length + child_flag;
print_tree(child_node, next_tab_len, pid_out_flag);
if((root->children)[while_count+1] != NULL && while_count+1 < 100)
{
printf("\n");
for (int i = 0; i <strlen(str)+tab_length; i++)
{
printf(" ");
}
printf("|-");
}
while_count++;
}
}
int main(int argc, char *argv[])
{
init_num();
int flag = 0;
for (int i = 0; i < argc; i++)
{
assert(argv[i]);
if(!strcmp(argv[i], "-V") || !strcmp(argv[i], "--version"))
{
flag = 1;
break;
}
else if(!strcmp(argv[i], "-p") || !strcmp(argv[i], "--show-pids"))
{
flag = 2;
break;
}
else if(!strcmp(argv[i], "-n") || !strcmp(argv[i], "--numeric-sort"))
{
flag = 3;
break;
}
}
assert(!argv[argc]);
if(create_tree())
{
// 建树期间出现异常,退出
return 1;
}
if(flag == 1)
{
// 将版本信息打印到stderr
fprintf(stderr,"pstree for jyy's os\n");
return 0;
}
// -p参数显示PID
if(flag == 2)
{
node root_node = node_num[0];
print_tree(root_node, 0, 1);
}
// -n参数顺序显示PID
else if(flag == 3)
{
// 在建树时获取到的PID已经是有序的
node root_node = node_num[0];
print_tree(root_node, 0, 1);
}
// 默认情况
else
{
node root_node = node_num[0];
print_tree(root_node, 0, 0);
}
return 0;
}
0x02实验结果
实验环境:Ubuntu 20.04
-p参数正确输出进程名以及顺序输出PID
-V参数正确输出到stderr