iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 10

Day 10|Process 是什麼?第一次建立兩個 Kernel Process

  • 分享至 

  • xImage
  •  

今天讓核心印出兩筆行程資料。
它們有不同的 PID、不同的堆疊位址,各自保留入口函式與準備好的 CPU context,狀態都標成 RUNNABLE

CPU 此時仍在執行 kernel_main(),尚未切進這兩個入口。
建立行程需要先準備可被排程的資料與資源,Day 11 才會讓 CPU 真正使用它們。

一份程式碼如何變成可管理的執行工作?

《Operating System Concepts》第 10 版第 3.1 節,把程式與行程分開討論。
程式是指令的集合,行程則包含執行進度與所使用的資源,例如程式計數器、暫存器內容和堆疊。
即使兩個行程執行相同程式,也可以有不同的執行狀態。

核心使用行程控制區塊(Process Control Block,PCB)保存這些資訊。
我們今天的 struct process 就是它的最小版本,先放入識別碼、狀態、入口與堆疊,再預留切換所需的 context。

《OS in 1,000 Lines》的〈行程〉把行程資料和切換程式一起建立。
本系列依大綱分成兩天,今天先驗證配置與資料內容,讓下一篇切換出錯時,能先排除「根本沒準備好堆疊」的情況。

目前兩個工作都會在 S-mode 共享核心位址空間,技術上更接近核心執行緒。
沿用教材稱為 Kernel Process,但還不具備 Unix 使用者行程的獨立位址空間或權限隔離。

把狀態與 context 放進結構

以下都在 30-days-os-kernel/tiny-kernel/,接續 Day 09。
新增 process.h

#pragma once
#include "memory.h"

#define MAX_PROCESSES 2u

enum process_state {
    PROC_UNUSED,
    PROC_EMBRYO,
    PROC_RUNNABLE,
    PROC_RUNNING
};

struct context {
    uint32_t ra;
    uint32_t sp;
    uint32_t s[12];
};

struct process {
    unsigned int pid;
    enum process_state state;
    void (*entry)(void);
    void *stack_base;
    struct context context;
    unsigned int cookie;
};

struct process *process_create(void (*entry)(void), unsigned int cookie);
void process_dump(const struct process *process);
void process_test(void);

PROC_EMBRYO 表示正在建立,等必要資源都到位後才改成 PROC_RUNNABLE
PROC_RUNNING 已先定義,但今天不會有人進入這個狀態。
其他等待或結束狀態,等真正需要相應行為時再加入。

context 保存 rasps0s11,這是為 Day 11 的普通函式邊界切換準備的格式。
它和 Day 08 的 Trap Frame 用途不同,不能拿這個較小的結構直接替代完整的 trap 現場。

取得一頁堆疊,最後才把行程設為可執行

新增 process.c

#include "process.h"

static struct process processes[MAX_PROCESSES];
static unsigned int next_pid = 1;

struct process *process_create(void (*entry)(void), unsigned int cookie) {
    if (!entry)
        return NULL;
    for (unsigned int i = 0; i < MAX_PROCESSES; i++) {
        struct process *p = &processes[i];
        if (p->state != PROC_UNUSED)
            continue;
        p->state = PROC_EMBRYO;
        void *stack = alloc_pages(1);
        if (!stack) {
            p->state = PROC_UNUSED;
            return NULL;
        }
        p->pid = next_pid++;
        p->entry = entry;
        p->stack_base = stack;
        p->context.ra = (uint32_t) entry;
        p->context.sp = (uint32_t) stack + PAGE_SIZE;
        memset(p->context.s, 0, sizeof(p->context.s));
        p->cookie = cookie;
        p->state = PROC_RUNNABLE;
        return p;
    }
    return NULL;
}

void process_dump(const struct process *p) {
    const char *state = "OTHER";
    if (p->state == PROC_RUNNABLE)
        state = "RUNNABLE";
    else if (p->state == PROC_RUNNING)
        state = "RUNNING";
    printf("pid=%u state=%s entry=0x%x stack=0x%x top=0x%x cookie=%u\n",
           p->pid, state, (unsigned int) p->entry,
           (unsigned int) p->stack_base, p->context.sp, p->cookie);
}

堆疊朝低位址成長,所以初始 sp 位於配置區塊的尾端。
這是合法的一端界線,第一次保留堆疊空間時才向下移入區塊,不能把頂端直接當成可寫入的一個 word。

只有最後一步才設成 RUNNABLE,可以避免日後排程器選到還沒有堆疊的行程。
配置失敗時則把狀態還原,讓這個槽位仍可重試。
目前沒有結束行程的 API,因此也還不會重用已建立行程的 PID 或槽位。

準備兩個入口,但先不呼叫它們

新增 process_test.c

#include "process.h"

static void task_p1(void) {
    for (;;)
        __asm__ __volatile__("wfi");
}

static void task_p2(void) {
    for (;;)
        __asm__ __volatile__("wfi");
}

void process_test(void) {
    unsigned int before = free_page_count();
    KASSERT(process_create(NULL, 0) == NULL);
    struct process *p1 = process_create(task_p1, 111);
    struct process *p2 = process_create(task_p2, 222);
    KASSERT(p1 != NULL && p2 != NULL);
    KASSERT(p1 != p2 && p1->pid != p2->pid);
    KASSERT(p1->stack_base != p2->stack_base);
    KASSERT(p1->state == PROC_RUNNABLE && p2->state == PROC_RUNNABLE);
    KASSERT(p1->context.ra == (uint32_t) task_p1);
    KASSERT(p2->context.ra == (uint32_t) task_p2);
    KASSERT((p1->context.sp & 15) == 0 && (p2->context.sp & 15) == 0);
    KASSERT(p1->context.sp == (uint32_t) p1->stack_base + PAGE_SIZE);
    KASSERT(p2->context.sp == (uint32_t) p2->stack_base + PAGE_SIZE);
    uint32_t *a = p1->stack_base;
    uint32_t *b = p2->stack_base;
    *a = p1->cookie;
    *b = p2->cookie;
    KASSERT(*a == 111 && *b == 222);
    *a = *b = 0;
    process_dump(p1);
    process_dump(p2);
    KASSERT(free_page_count() == before - 2);
    KASSERT(process_create(task_p1, 333) == NULL);
    KASSERT(free_page_count() == before - 2);
    printf("process setup=ok free pages=%u\n", free_page_count());
}

cookie 是這個測試用來辨認資料的數字,沒有 CPU 或排程上的特殊意義。
我們用不同數字寫入兩塊堆疊底部,驗證兩者不會因重複配置而互相覆蓋,再清回零。
這是空間不重疊的測試,不代表硬體已經阻止 P1 存取 P2 的資料。

兩個入口暫時都是不返回的等待迴圈,將來第一次切換時會用 context 的 ra 抵達入口。
若之後要允許工作自然返回,還需要一層啟動包裝與行程結束處理,不能讓入口隨意落回不存在的呼叫端。

看見兩筆資料,確認兩頁被占用

在 Makefile 的 SOURCES 追加 process.c process_test.c
kernel.c 加入 #include "process.h",並取代 kernel_main()

void kernel_main(void) {
    WRITE_CSR(stvec, (unsigned int) trap_entry);
    memory_init();
    allocator_test();
    process_test();
    for (;;)
        __asm__ __volatile__("wfi");
}

tiny-kernel/ 執行 makemake run
Day 09 的測試先把全部頁面歸還,接著今天的輸出應有以下關係:

pid=1 state=RUNNABLE entry=0x... stack=0x... top=0x... cookie=111
pid=2 state=RUNNABLE entry=0x... stack=0x... top=0x... cookie=222
process setup=ok free pages=14

位址以自己的編譯結果為準,每個 top - stack 都應為 0x1000
建立第三個行程應回傳 NULL,因為表格目前只有兩個槽位,失敗也不應偷偷多占一頁。

若要從 GDB 直接檢查,在 process_dump 設斷點並讀取傳入的 struct process
第一次與第二次呼叫應指向不同結構,並有不同的 stack_base

RUNNABLE 和 RUNNING 之間還缺一次切換

直接在 kernel_main() 依序呼叫 task_p1()task_p2(),不會自動使用它們的新堆疊。
普通 C 呼叫仍沿用目前的 sp,第一個無限迴圈也會讓第二次呼叫永遠等不到執行。

真正的切換必須保存目前的執行現場,再載入下一個行程的 context。
我們今天已準備好切換所需的資料,也以配置數量與獨立空間驗證建立流程,下一篇才讓 CPU 跨過這個界線。

今天新增 process.hprocess.cprocess_test.c,修改 kernel.c 與 Makefile。
建議 commit 訊息為:

day10: introduce process structure

Day 11 將實作 switch_context(),讓兩個入口真正輪流取得執行機會。

參考資料


上一篇
Day 09|Kernel 怎麼管理 RAM?實作自己的 Page Allocator
下一篇
Day 11|Context Switch:讓 CPU 真正從 P1 切換到 P2
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言