綜合能力訓練:在樹莓派上動手寫一個小OS(6):實驗16-5:程式建立實驗

rlk8888發表於2022-03-18

本文節選自《實驗指導手冊》第二版第16.7章
實驗指導手冊是奔跑吧Linux核心入門篇第二版配套實驗書,pdf版本已經release,可以免費下載和自由列印!
下載方法:
登陸“奔跑吧linux社群”微信公眾號,輸入“奔跑吧2”獲取下載地址。

本文是《奔跑吧Linux核心 入門篇》第二版中第16章的實驗16-5:程式實驗。我們在前面的實驗中已經完成了printk列印函式以及時鐘中斷的實驗了,接下來我們就可以來完成程式建立的實驗了。在本實驗裡,我們要研究程式是如何建立的,瞭解新建立的程式是如何執行的。在這個實驗裡,我們要完成fork()函式,看看傳說中的fork函式究竟是如何實現的。

實驗指導手冊 列印小Tips:

大家可以在某寶上隨便找一個便宜的列印店,列印B5+黑白即可!很便宜,20~30元,還包郵!


1.實驗目的

(1)瞭解程式控制塊的設計與實現。
(2)瞭解程式的建立/執行過程。

2.實驗要求

實現fork函式以建立一個程式,該程式一直輸出數字“12345”。

3.實驗提示

(1)設計程式控制塊。
(2)為程式控制塊分配資源。
(3)設計和實現fork函式。
(4)為新程式分配棧空間。
(5)看看新建立的程式是如何執行的。

4.實驗詳解

4.1 程式控制塊PCB

我們使用struct task_struct資料結構來描述一個程式控制塊。

struct task_struct {

    enum task_state state;
    enum task_flags flags;
    long count;
    int priority;
    int pid;
    struct cpu_context cpu_context;
};

State:表示程式的狀態。使用enum task_state列舉型別來列舉出程式的狀態,有執行狀態TASK_RUNNING、可中斷睡眠狀態TASK_INTERRUPTIBLE、不可中斷的睡眠狀態TASK_UNINTERRUPTIBLE、殭屍態TASK_ZOMBIE以及終止態TASK_STOPPED。

enum task_state {

    TASK_RUNNING =  0,
    TASK_INTERRUPTIBLE =  1,
    TASK_UNINTERRUPTIBLE =  2,
    TASK_ZOMBIE =  3,
    TASK_STOPPED =  4,
};

Flags用來表示程式的某些標誌位。目前只用來表示程式是否為核心執行緒。

enum task_flags {

    PF_KTHREAD =  1 <<  0,
};
  • Count用來表示程式排程用的時間片。

  • Priority用來表示程式的優先順序。

  • Pid用來表示程式的PID。

  • cpu_context用來表示程式切換時候的硬體上下文。

4.2 0號程式

BenOS的啟動流程是:上電->樹莓派韌體->BenOS彙編入口->BenOS kernel_main函式。這個過程,從程式的角度來看,可以看出是系統的“0號程式”。
我們需要對這個0號程式進行管理。0號程式也是需要有一個程式控制塊,以方便管理。下面使用INIT_TASK巨集來靜態初始化0號程式的程式控制塊。


/* 0號程式即init程式 */

#define INIT_TASK(task) \
{                      \
    .state =  0,     \
    .priority =  1,   \
    .flags = PF_KTHREAD,   \
    .pid =  0,     \
}

另外,我們還需要為0號程式分配棧空間。通常的做法是把0號程式的核心棧空間連結到資料段裡。注意,這裡僅僅是0號程式是這麼做的,其他程式的核心棧是動態分配的。
我們首先使用task_union的方式來定義一個核心棧。


/*

 * task_struct資料結構儲存在核心棧的底部
 */

union task_union {
    struct task_struct task;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

這樣,定義了一個核心棧的框架,在核心棧的底部,用來儲存程式控制塊。

目前我們的BenOS還比較簡單,所以核心棧的大小定義為一個頁面大小,即4KB。


/* 暫時使用1個4KB頁面來當作核心棧*/

#define THREAD_SIZE  ( 1 * PAGE_SIZE)
#define THREAD_START_SP  (THREAD_SIZE -  8)

對於0號程式,我們把核心棧放到.data.init_task段裡。下面通過GCC的 attribute屬性來完成編連結,把task_union編譯連結到.data.init_task段中。


/* 把0號程式的核心棧 編譯連結到.data.init_task段中 */

#define __init_task_data __attribute__((__section__( ".data.init_task")))

/* 0號程式為init程式 */
union task_union init_task_union __init_task_data = {INIT_TASK(task)};

另外,我們還需要在BenOS的連結檔案benos.lds.S中新增一個名為.data.init_task段。修改arch/arm64/kernel/benos.lds.S檔案,在資料段中新增.data.init_task段。

4.3 do_fork函式的實現

本實驗需要實現do_fork函式,該函式的功能是為了fork一個新程式。

  • 新建一個task_strut,為期分配4KB頁面用來儲存核心棧, task_struct存在棧底。

  • 為新程式分配PID。

  • 設定程式的上下文。

int do_fork(unsigned long clone_flags, unsigned long fn, unsigned long arg)

{
    struct task_struct *p;
    int pid;

    p = (struct task_struct *)get_free_page();
     if (!p)
        goto error;

    pid = find_empty_task();
     if (pid <  0)
        goto error;

     if (copy_thread(clone_flags, p, fn, arg))
        goto error;

    p->state = TASK_RUNNING;
    p->pid = pid;
    g_task[pid] = p;

     return pid;

error:
     return  -1;
}

其中:

  • get_free_page()分配一個物理頁面,用於程式的核心棧。

  • find_empty_task()查詢一個空閒的PID。

  • copy_thread()用於設定新程式的上下文。

copy_thread()函式也是實現在kernel/fork.c檔案裡。


/*

 * 設定子程式的上下文資訊
 */

static int copy_thread(unsigned long clone_flags, struct task_struct *p,
        unsigned long fn, unsigned long arg)
{
    struct pt_regs *childregs;

    childregs = task_pt_regs(p);
    memset(childregs,  0, sizeof(struct pt_regs));
    memset(&p->cpu_context,  0, sizeof(struct cpu_context));

     if (clone_flags & PF_KTHREAD) {
        childregs->pstate = PSR_MODE_EL1h;
        p->cpu_context.x19 = fn;
        p->cpu_context.x20 = arg;
    }

    p->cpu_context.pc = (unsigned long)ret_from_fork;
    p->cpu_context.sp = (unsigned long)childregs;

     return  0;
}

PF_KTHREAD標誌位表示新建立的程式為核心執行緒,這時候pstate儲存了將要執行的模式為PSR_MODE_EL1h,x19儲存了核心執行緒的回撥函式,x20儲存了回撥函式的引數。
設定pc暫存器為ret_from_fork,即指向ret_from_fork彙編函式。設定sp暫存器指向棧的pt_regs棧框。

4.4 程式上下文切換

BenOS裡的程式上下文切換函式為switch_to(),用來切換到next程式來執行。


void switch_to(struct task_struct *next)

{
    struct task_struct *prev = current;

     if (current == next)
         return;

    current = next;
    cpu_switch_to(prev, next);
}

其中核心的函式為cpu_switch_to()函式,其目的為儲存prev程式的上下文,並且恢復next程式的上下文,函式原型為:

  cpu_switch_to(struct task_struct *prev, struct task_struct *next);

cpu_switch_to()函式實現是在arch/arm64/kernel/entry.S檔案裡。需要儲存的上下文,包括:x19 ~ x29暫存器,sp暫存器以及 lr暫存器,並且儲存到程式的task_struct->cpu_context。

.align

.global cpu_switch_to
cpu_switch_to:
    add     x8, x0, #THREAD_CPU_CONTEXT
    mov     x9, sp
    stp     x19, x20, [x8], # 16
    stp     x21, x22, [x8], # 16
    stp     x23, x24, [x8], # 16
    stp     x25, x26, [x8], # 16
    stp     x27, x28, [x8], # 16
    stp     x29, x9, [x8], # 16
    str     lr, [x8]

    add     x8, x1, #THREAD_CPU_CONTEXT
    ldp     x19, x20, [x8], # 16
    ldp     x21, x22, [x8], # 16
    ldp     x23, x24, [x8], # 16
    ldp     x25, x26, [x8], # 16
    ldp     x27, x28, [x8], # 16
    ldp     x29, x9, [x8], # 16
    ldr     lr, [x8]
    mov     sp, x9
    ret

4.5 新程式的第一次執行

在程式切換時,switch_to()函式會完成程式硬體上下文的切換,即把下一個程式(next程式)的cpu_context資料結構儲存的內容恢復到處理器的暫存器中,從而完成程式的切換。此時,處理器開始執行next程式了。根據PC暫存器的值,處理器會從ret_from_fork彙編函式裡開始執行,新程式的執行過程如圖所示。

圖 新程式的執行過程
ret_from_fork彙編函式實現在arch/arm64/kernel/entry.S檔案中。


1     .align 
2

2     .global ret_from_fork
3     ret_from_fork:
4         cbz x19,  1f
5         mov x0, x20
6         blr x19
7      1:
8         b ret_to_user
9         
10    .global ret_to_user
11    ret_to_user:
12         inv_entry  0, BAD_ERROR

在第4行中,判斷next執行緒是否為核心執行緒。如果next程式是核心執行緒,在建立時會設定X19暫存器指向stack_start。如果X19的值暫存器為0,說明這個next程式是使用者程式,直接跳轉到第6行,呼叫ret_to_user彙編函式,返回使用者空間。不過我們這裡ret_to_user函式並沒有實現。
在第4~6行中,如果next程式是核心執行緒,那麼直接跳轉到核心執行緒的回撥函式裡。
綜上所述,當處理器切換到核心執行緒時,它從ret_from_fork彙編函式開始執行。

5.實驗步驟

我們先在QEMU上模擬。由於本實驗的參考程式碼還沒有實現對GIC-400中斷控制器的支援,因此,在QEMU裡我們只能模擬樹莓派3b了。
在Ubuntu Linux主機中,進入參考實驗程式碼目錄。

rlk@rlk :$ cd /home/rlk/rlk/runninglinuxkernel_5
.0/kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork/

進入make menuconfig選單。

rlk@master:lab05_add_fork $ make menuconfig

其中:

Board selection (Raspberry 
3B) -> 選樹莓派
3b

Uart  for Pi (pl_uart) -> 選PL串列埠

編譯並執行。

rlk@master:lab05_add_fork $ make

rlk@master: lab05_add_fork $ make run


6. 實驗參考程式碼

實驗參考程式碼runninglinuxkenrel_5.0的git repo。

國內訪問:

https: //benshushu.coding.net/public/runninglinuxkernel_5.0/runninglinuxkernel_5.0/git/files

github(國外訪問):
https: //github.com/figozhang/runninglinuxkernel_5.0

本文對應的參考程式碼在如下目錄:kmodules/rlk_lab/rlk_basic/chapter_16_benos/lab05_add_fork

我們提供配置好的實驗環境,基於ubuntu 20.04的VMware/Vbox映象,可以通過如下方式獲取下載地址:
登陸“奔跑吧linux社群”微信公眾號,輸入“奔跑吧2”獲取下載地址。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70005277/viewspace-2872537/,如需轉載,請註明出處,否則將追究法律責任。

相關文章