Jyy’s OS-[M1]pstree
本文最后更新于 471 天前,其中的信息可能已经有所发展或是发生改变。

今天从这篇博客开始记录学习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

通过上述命令阅读文档,我们得到关键信息

  1. /proc/[pid]目录表示PID对应进程的信息
  2. /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

上一篇
下一篇