1. 程式人生 > >作業系統中程序的實現

作業系統中程序的實現

// 程序控制結構體(PCB) --> 用來管理程序
struct tack_struct {
    struct List list; // 雙向連結串列, 用於連線各個程序控制結構體, 在Linux中這樣的連結串列建立方式比較常見
    volatile long state; // 表示程序的狀態: 執行態, 停止態, 可中斷態等
    unsigned long flags; // 程序標誌, 是程序還是執行緒, 也許這就是Linux中的執行緒被稱為輕量級的程序的原因

    struct mm_struct *mm; // 記錄記憶體頁表和程式段資訊, 說白了就是管理記憶體中的程式(data, code, rodata, bss), 應用程式的棧頂地址
    struct thread_struct *thread; // 用於儲存程序切換時的資料

    unsigned long addr_limit; // 程序地址空間範圍

    long pid; 
    long counter; // 程序佔用的時間片
    long signal; // 程序的訊號
    long priority; // 程序的優先順序
 
};


struct mm_struct {
    // pgd的值是從cr3暫存器中獲取的, 就是頁表的地址
    pml4t_5 *pgd; // 頁表指標

    unsigned long start_code, end_code;
    unsigned long start_data, end_data;
    unsigned long start_rodata, end_rodata;
    unsigned long start_brk, end_brk;
    // 應用程式的棧頂地址
    unsigned long start_stack;
};


// 用於保留現場
struct thread_struct {
    unsigned long rsp0; // 核心層棧基地址

    unsigned long rip; // 核心層程式碼指標
    unsigned long rsp; // 核心層當前棧指標

    unsigned long fs; // 儲存fs段暫存器的值
    unsigned long gs; // gs段暫存器的值

    unsigned long cr2; // cr2控制暫存器的值
    unsigned long trap_nr; // 產成異常的異常號
    unsigned long error_code; // 異常的錯誤碼
};

/*
 * 在Linux核心中, 將task_truct結構體和程序的核心層棧空間融為一體, 低地址存放task_struct結構體, 餘下的存放程序的核心層棧空間使用
 *
 */

// 通過該聯合體創建出來的是Linux下第一個程序, 注意: 這個程序不是我們提到的init程序, init程序是Linux第二個程序
union task_union {
    struct task_struct task;
    unsigned long stack[STACK_SIZE / sizeof(unsigned long)];
};


#define INIT_TASK(tsk) \
{\
    .state = TASK_UNINTERRUPTIBLE, \
    .flags = PF_KTHREAD,\
    .mm = &init_mm,\
    .thread = &init_thread, \
    .addr_limit = 0xffff800000000000, \
    .pid = 0, \
    .counter = 1, \
    .signal = 0, \
    .priority = 0\
}

// 1, 2, 3都是初始化第一個程序
// 1
union task_union init_task_union __attribute__((__section__(".data.init_task"))) = {INIT_TASK(init_task_union.task)};

// 2
struct task_struct *init_task[NP_CPUS] = {&init_task_union.task, 0};
struct mm_struct init_mm = {0};

// 3
struct thread_struct init_thread = {
    .rsp0 = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),
    .rsp = (unsigned long)(init_task_union.stack + STACK_SIZE / sizeof(unsigned long)),
    .fs = KERNEL_DS,
    .gs = KERNEL_DS,
    .cr2 = 0,
    .trap_nr = 0,
    .error_code = 0
};


struct tss_struct {
    unsigned int reserved0;
    unsigned long rsp0;
    unsigned long rsp1;
    unsigned long rsp2;
    unsigned long reserved1;
    unsigned long ist1;
    unsigned long ist2;
    unsigned long ist3;
    unsigned long ist4;
    unsigned long ist5;
    unsigned long ist6;
    unsigned long ist7;
    unsigned long reserved2;
    unsigned short reserved3;
    unsigned short iomapbaseaddr;
}__attirbute__((packed));

#define INIT_TSS \
{\
    .reserved = 0,
    ... \
    .ist1 = 0xffff800000007c00,\
    ...\
}

struct tss_struct init_tss[NR_CPUS] = { [0 ... NR_CPUS - 1] = INIT_TSS };
/*
 * 在之前我們恢復異常和中斷的現場的時候就發現要對大量的暫存器中的資料壓棧儲存, 有在task_struct的thread在程序切換的時候儲存資料的方式的啟發, 我們可以定義一個結構體來儲存保留
 * 現場的暫存器的資料, 然後直接將這個結構體中的資料直接拷貝到核心棧中, 而不是一個一個地壓入到核心棧中, 這樣效率高
 */

/*
 * 這個程序的現場保留的資料就交給了這個pt_regs結構體了, 他和異常以及中斷時將大部分暫存器中的資料壓棧是一樣的
 */

/*
 * 我們知道一個作業系統會管理很多個程序, 我們需要有一個函式來獲取當前的task_struct
 */

inline struct task_struct *get_current() {
    struct task_struct *current = NULL;
    __asm__ __volatile__("andq %%rsp, %0":"=r"(current):"0"(~3276UL));
    return current;
}


#define current get_current()

#define GET_CURRENT \
    "movq %rsp, %rbx \n\t" \
    "andq $ - 32768, %rbx \n\t"
  • 程序內的切換是在核心空間中的, 如果將這個機制搬運到應用程式中則實現了執行緒間的切換工作
  • 程序間的切換主要涉及到頁目錄的切換和各個暫存器值的儲存和恢復
  • 程序間切換需要在一塊公共區域內進行, 這個區域就是核心空間(注意: 作為的在核心空間執行就是指我們當前的堆疊指標指向的是核心的堆疊)
  • 對於作業系統的第一個PCB我們進行特殊的建立, 就是直接手動的建立, 目前來看這個就是我們的current的程序控制結構體了, 接著我們要生成一個新的程序, 這個程序就是我們熟悉的init程序, 這個程序和剩餘的其他程序都是通過一個kernel_thread函式建立的, 而在kernel_thread函式中核心的建立子函式就是do_fork函式, 這個函式會拷貝當前的current的記憶體資料到一個新的區域, 很熟悉吧, 這個就是所謂的子程序從父程序中的一個複製, 下面就是來介紹這個kernel_thread函式
  • kernel_thread:
    • 先宣告一點, 所謂建立一個新的程序和一個老的程序恢復現場都是一個樣的, 所以我們在建立一個新的程序的時候採用偽造恢復現場的方式來建立
    • 因此在kernel_thread函式中, 我們先建立一個用來儲存恢復現場的資料結構體, 也就是pt_regs struct結構體, 這個結構體的屬性幾乎就是所有的暫存器(因為保留現場我們需要將幾乎所有的暫存器的值都儲存起來), 這個pt_regs結構體在前面有提到過, 這裡還有一點需要注意: 我們在這裡還要提到一個kernel_thread_func函式指標(在這個函式中先進行現場恢復, 再呼叫傳入的程序的入口函式地址使用call指令執行該程序程式碼, 接著返回, 呼叫call do_exit退出程序程式, 如果不在核心層的話, 則呼叫的是另外一個ret_from_intr), 這個函式就是用來在執行我們的程序的function的使用先執行的一段恢復現場的程式碼, 這裡我們又提到了一個function, 其實這個就是一個程式的入口地址而已, 我們所以的程序不就是一個動態的程式嗎, 就是讓cpu去執行, 那麼就需要知道他的入口地址
    • 初始化完畢pt_regs結構體之後, 使用的do_fork函式去fork父程序創建出子程序
      • 在該函式中我們需要呼叫分頁的allo_pages函式去一個物理頁存放task truct(也就是程序控制結構體)
      • 接著講task所在的物理頁的資料清0
      • 這個語句 task = current就是核心了, 就是所謂的子程序從父程序複製資料出來(這裡沒有使用先進的CoW技術)
      • 將這個task結構體新增到程序控制列表中
      • 遞增pid, 原來pid只這樣出來的 :)
      • thread結構體就建立在task之後, thread結構體是用來儲存一些程序的狀態的, 用於現場保留什麼的
      • 判斷task是在核心態還是在應用層
      • 設定task的state為TASK_RUNNING