Skip to content

M1

Introduction

解决问题的一般方法

因为人的脑容量有限,通常解决问题的办法就是把比较复杂的问题分解成小问题,再把小问题继续分解下去。而在学校里所做的训练就是建立问题分解的思路和培养解决问题的能力。如果想自己尝试,我们也鼓励大家忽略下面的教程,自己动手搞定,遇到不明白的地方可以求助 Google (Bing, Stackoverflow, ...);但对于这样的简单任务,大语言模型的表现就有些太好了 (请不要用它们!)。完成之后可以看一下实验指南,看自己的理解是否有可以改进的空间。

Basic implementation

  1. 先不考虑参数,仅实现功能

    1. 获取进程号:/proc/* 的内容

      1. 入门了解 /proc 文件系统的相关内容

        简要看看 man 5 proc,然后即可开始:观测进程的变化,只需解析文本文件。

      2. 如何用 C 语言遍历目录?

        询问 AI 互联网。

        opendir, readdir...

      3. 遇到问题

        1. Q1

          一次偶然随便进了一个目录,发现 /proc 下会有两种表示 进程 的方式,?

          • /proc/[pid]/stat
          • /proc/[pid]/task/[pid].../stat

          其实是自己不仔细看讲义以及自己疏忽:多进程系统,父子进程。

          例如,每个进程的父进程也隐藏在 /proc/[pid]/ 中的某个文件里

        2. Q2

          为什么刚开始没有发现多进程的时候,显示出来的所有的进程买到知道后并没有打印出来呢?kthreadd?

    2. 构建由进程结点组成的树

      1. 学习数据结构 树的相关内容。
    3. 遍历树

      1. 前序遍历
  2. 添加参数

    1. 先添加简单的:-Vshow-pids

    2. 再添加 numeric-sort,但是怎么按照 pid 顺序输出?

      突然有两种想法:

      • 在构建 node 的时候就按照顺序构建(通过标志控制)---> 代码复杂?好像也没那么复杂?
      • 在打印的时候重新按照 pid 号排布节点? ---> 计算量大?
  3. 改进

    输出到 stderr:

    内存一直泄露,使用内存池

reference:

Valgrind errors: "invalid read of size 8" and "...uninitialised value" (pset5, speller) - CS50 Stack Exchange

learn

Linux 中的多进程和多线程

/proc 文件系统中,每个进程都有一个以其进程 ID 命名的目录,例如 /proc/1234。如果进程有线程(在 Linux 中称为任务),则这些线程的信息会位于该进程目录下的 task 子目录中。每个线程在这个 task 目录下都有自己的以其线程 ID 命名的子目录,这些子目录包含了线程特有的信息,如 stat 文件,它包含了线程的状态信息。

我的 read_proc 函数首先被调用来读取进程的信息(不是线程),然后 read_proc_dir 函数遍历每个进程的 task 目录,调用 read_proc 函数来读取每个线程的信息。这样,程序可以构建一个包含所有进程和线程的进程树。

例如, /proc/1898/task 目录下有如下内容:

1898  1899  1900  1902  1903  1905

这表示进程 ID 为 1898 的进程有多个线程,每个线程的 ID 分别是 1898、1899、1900、1902、1903 和 1905。这些线程 ID 并不一定是连续的,它们只是用来标识进程的不同线程。

在 Linux 中,线程被视为轻量级的进程,它们被包含在它们所属的进程下。每个线程都是进程的一部分,它们共享进程的大部分资源,如内存空间、文件描述符等,但每个线程都有自己的执行栈和程序计数器,这样设计是得他们能够并发执行,又使得线程之间的上下文切换比进程之间的切换要快,因为它们不需要切换内存空间。

相同的名字?

在 Linux 中,每个进程都有一个唯一的进程 ID(PID),而且每个线程都属于某个进程。对于单线程进程,进程 ID 和线程 ID 是相同的,因为这个进程只有一个执行流。对于多线程进程,除了主线程(与进程同时创建的线程)的线程 ID 与进程 ID 相同之外,其他线程会有自己的线程 ID,这些线程 ID 通常是唯一的,并且与进程 ID 不同。

/proc/1898/task 目录下,每个线程 ID 对应的子目录(例如 /proc/1898/task/1899)包含了该线程特定的信息,如状态、统计信息等。这些信息可以通过读取子目录下的文件(如 stat 文件)来获取。

/proc/1898/task 目录下的每个数字目录都代表了一个线程,这些线程共同属于进程 ID 为 1898 的进程。这种组织方式使得系统能够以进程为单位管理和调度线程,同时也允许对线程进行单独的监控和调试。

proc(5): process info pseudo-file system - Linux man page (die.net)

The /proc Filesystem — The Linux Kernel documentation

内存池的管理

Memory pool - Wikipedia

目前的简单实现版本:

#define POOL_BLOCK_SIZE 1024
#define POOL_MAX_BLOCKS 1024

typedef struct {
    char *block;
    char *free_ptr;
    size_t remaining;
    char *next_block; // 用于存储下一个块的指针
} memory_pool_t;
memory_pool_t proc_pool;

void memory_pool_init(memory_pool_t *pool) {
    pool->block = malloc(POOL_BLOCK_SIZE + sizeof(char *));
    if (!pool->block) {
        fprintf(stderr, "Failed to allocate memory pool block\n");
        exit(EXIT_FAILURE);
    }
    pool->free_ptr = pool->block + sizeof(char *);
    pool->remaining = POOL_BLOCK_SIZE - sizeof(char *);
    pool->next_block = NULL;
    *(char **)(pool->block + POOL_BLOCK_SIZE) = NULL; // 初始化 next_block 为 NULL
}

void memory_pool_destroy(memory_pool_t *pool) {
    char *block = pool->block;
    while (block) {
        char *next_block = *(char **)(block + POOL_BLOCK_SIZE);
        free(block);
        block = next_block;
    }
    pool->block = NULL;
    pool->free_ptr = NULL;
    pool->remaining = 0;
    pool->next_block = NULL;
}

void *memory_pool_alloc(memory_pool_t *pool, size_t size) {
    if (pool->remaining < size) {
        char *new_block = malloc(POOL_BLOCK_SIZE + sizeof(char *));
        if (!new_block) {
            fprintf(stderr, "Failed to allocate new memory pool block\n");
            exit(EXIT_FAILURE);
        }
        *(char **)(new_block + POOL_BLOCK_SIZE) = pool->block;
        pool->block = new_block;
        pool->free_ptr = new_block + sizeof(char *);
        pool->remaining = POOL_BLOCK_SIZE - sizeof(char *);
    }

    void *ptr = pool->free_ptr;
    pool->free_ptr += size;
    pool->remaining -= size;
    return ptr;
}

*(char **)(pool->block + POOL_BLOCK_SIZE) = NULL; // 初始化 next_block 为 NULL

不知道为什么去掉这一行,就会有内存泄漏。

再更新:

再回来看,就不需要上面这行代码了,有点迷糊。

再看 kimi 的解释。

Own test

qemu-system-arch & qemu-arch

由于无法使用到南大校内oj来测试自己写的代码的移植性,所以考虑使用 qemu 来测试。

有两个qemu,不知道使用哪个以及二者区别,直接找找:

kvm virtualization - Difference between qemu-kvm, qemu-system-x86_64, qemu-x86_64

[Qemu-discuss] Difference between qemu-kvm, qemu-system-x86_64, qemu (gnu.org)

  • qemu-arch like /usr/local/bin/qemu-x86_64 is for running a program of that arch on the host machine of what ever arch, but not a virtual machine
  • qemu-system-arch like /usr/local/bin/qemu-system-x86_64 is for running a system of that arch on the host machine
  • to enable kvm support, qemu parameter -enable-kvm is needed, libvirt should have taken care of this if right xml is configured
  1. qemu-arch (/usr/local/bin/qemu-x86_64):

    • 这个命令用于在当前主机架构上直接运行目标架构的程序,而不是运行一个完整的虚拟机系统。
    • 它允许你在当前操作系统上模拟不同架构的程序执行,但不涉及虚拟化硬件或完整的操作系统。
    • 例如,你可以使用它来测试你的程序在x86_64架构上的行为,而不需要启动一个完整的x86_64系统。
  2. qemu-system-arch (/usr/local/bin/qemu-system-x86_64):

    • 这个命令用于在当前主机上模拟一个完整的目标架构系统。
    • 它提供了虚拟化硬件支持,允许你运行一个完整的操作系统,就像在物理机器上一样。
    • 你可以使用它来测试你的程序在一个完整的操作系统环境中的行为,包括启动过程、系统调用等。

    这就是jyy在课上使用的这个,用于学习整个操作系统的行为,从boot道kernel init

综上:

对于测试代码的移植性,直接用 qemu-x86_64

测试在完整的操作系统环境中的行为,用 qemu-system-x86_64

另外,还要确保程序已经交叉编译为目标架构。

具体代码