分析Linux內核創建一個新進程的過程


 一、原理分析

1.進程的描述

  進程控制塊PCB——task_struct,為了管理進程,內核必須對每個進程進行清晰的描述,進程描述符提供了內核所需了解的進程信息。

struct task_struct{
    volatile long state; //進程狀態,-1表示不可執行,0表示可執行,大於1表示停止
    void *stack; //內核堆棧
    atomic_t usage;
    unsigned int flags; //進程標識符
    unsigned int ptrace;
    ……
}

2.進程的創建
  道生一(start_ kernel...cpu_ idle),一生二(kernel_ init和kthreadd),二生三(即前面的0、1、2三個進程),三生萬物(1號進程是所有用戶態進程的祖先,2號進程是所有內核線程的祖先)
start_ kernel創建了cpu_ idle,也就是0號進程。而0號進程又創建了兩個線程,一個是kernel_ init,也就是1號進程,這個進程最終啟動了用戶態;另一個是kthreadd。0號進程是固定的代碼,1號進程是通過復制0號進程PCB之后在此基礎上做修改得到的。
  iret與int 0x80指令對應,一個是彈出寄存器值,一個是壓入寄存器的值。
  如果將系統調用類比於fork();那么就相當於系統調用創建了一個子進程,然后子進程返回之后將在內核態運行,而返回到父進程后仍然在用戶態運行。

  Linux中創建進程一共有三個函數:fork,創建子進程 vfork,與fork類似,但是父子進程共享地址空間,而且子進程先於父進程運行。 clone,主要用於創建線程。Linux中所有的進程創建都是基於復制的方式,Linux通過復制父進程來創建一個新進程,通過調用do_ fork來實現。然后對子進程做一些特殊的處理。而Linux中的線程,又是一種特殊的進程。根據代碼的分析,do_ fork中,copy_ process管子進程運行的准備,wake_ up_ new_ task作為子進程forking的完成。fork()函數最大的特點就是被調用一次,返回兩次。

  追蹤do_fork的代碼

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    // ...
    
    // 復制進程描述符,返回創建的task_struct的指針
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);

    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        // 取出task結構體內的pid
        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);

        // 如果使用的是vfork,那么必須采用某種完成機制,確保父進程后運行
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        // 將子進程添加到調度器的隊列,使得子進程有機會獲得CPU
        wake_up_new_task(p);

        // ...

        // 如果設置了 CLONE_VFORK 則將父進程插入等待隊列,並掛起父進程直到子進程釋放自己的內存空間
        // 保證子進程優先於父進程運行
        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

  通過上面的代碼,可以看出,do_fork大概做了這么幾件事情:
(1)調用copy_process,將當期進程復制一份出來為子進程,並且為子進程設置相應地上下文信息。
(2)初始化vfork的完成處理信息(如果是vfork調用)
(3)調用wake_up_new_task,將子進程放入調度器的隊列中,此時的子進程就可以被調度進程選中,得以運行。
(4)如果是vfork調用,需要阻塞父進程,知道子進程執行exec。

3.創建的新進程從哪里開始執行?

  kernel中是可以指定新進程開始的位置(也就是通過eip寄存器指定代碼行)。fork中也有相似的機制 這涉及子進程的內核堆棧數據狀態和task_ struct中thread記錄的sp和ip的一致性問題,這是在copy_ thread in copy_ process設定的。

  copy_thread的流程如下:

(1) 獲取子進程寄存器信息的存放位置

(2) 對子進程的thread.sp賦值,將來子進程運行,這就是子進程的esp寄存器的值。

(3)如果是創建內核線程,那么它的運行位置是ret_from_kernel_thread,將這段代碼的地址賦給thread.ip,之后准備其他寄存器信息,退出 。

(4)將父進程的寄存器信息復制給子進程。

(5)將子進程的eax寄存器值設置為0,所以fork調用在子進程中的返回值為0。

(6)子進程從ret_from_fork開始執行,所以它的地址賦給thread.ip,也就是將來的eip寄存器。 從上面的流程中,我們看出,子進程復制了父進程的上下文信息,僅僅對某些地方做了改動,運行邏輯和父進程完全一致。

  另外,子進程從ret_from_fork處開始執行,子進程的運行是由這幾處保證的:

(1)dup_task_struct中為其分配了新的堆棧 。

(2)copy_process中調用了sched_fork,將其置為TASK_RUNNING 。

(3)copy_thread中將父進程的寄存器上下文復制給子進程,這是非常關鍵的一步,這里保證了父子進程的堆棧信息是一致的。

(4)將ret_from_fork的地址設置為eip寄存器的值,這是子進程的第一條指令。

二、實驗內容

      更新menu,刪除test_fork.c和test.c文件,重新執行make rootfs。首先還是將QEMU啟動,然后在sys_clone處設置一個斷點。執行以下代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0) 
    { 
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid == 0) 
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else 
    {  
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

  執行后,來到了斷點處 

  sys_clone實際上調用的還是do_fork,也就是說真正的進程創建是都是最終調用do_fork,然后進入該函數,里面有一個函數copy_process,這里面就是開始了詳細的創建。首先是異常判斷,然后開始了dup_task_struct,也就是復制父進程,產生子進程。 具體函數為

int __weak arch_dup_task_struct(struct task_struct *dst,
                           struct task_struct *src)
{
    *dst = *src;
    return 0;
}

  復制結束后,子進程和父進程是一樣的,復制過程中內核堆棧也被復制了,接下來是對子進程進行賦值

  復制文件,復制文件系統,復制信號等。 然后單步進入函數copy_thread。

p->thread.sp = (unsigned long) childregs;     //子進程棧地址
*childregs = *current_pt_regs();          //子進程的寄存器
childregs->ax = 0;                  //ax保存返回值,子進程執行fork的返回值是0 
p->thread.ip = (unsigned long) ret_from_fork; //子進程的開始執行地址

   再往下繼續單步 

  編譯內核查看fork命令,得到子進程與父進程結果。

 

啟動gdb調試,並對主要的函數設置斷點。

三、總結

  對於“特別關注新進程是從哪里開始執行的?為什么從哪里能順利執行下去?即執行起點與內核堆棧如何保證一致”,由於ret_ from_ fork決定新進程的第一條指令地址。子進程從ret_ from_ fork處開始執行。因為在ret_ from_ fork之前,也就是在copy_ thread()函數中* childregs = * current_ pt_ regs();該句將父進程的regs參數賦值到子進程的內核堆棧。* childregs的類型為pt_ regs,里面存放了SAVE_ ALL中壓入棧的參數,因此在之后的RESTORE ALL中能順利執行下去。

  新的進程通過克隆舊的程序(當前進程)而建立。fork() 和 clone()(對於線程)系統調用可用來建立新的進程。這兩個系統調用結束時,內核在系統的物理內存中為新的進程分配新的 task_struct 結構,同時為新進程要使用的堆棧分配物理頁。Linux 還會為新的進程分配新的進程標識符。然后,新 task_struct 結構的地址保存在鏈表中,而舊進程的 task_struct 結構內容被復制到新進程的 task_struct 結構中。 在克隆進程時,Linux 允許兩個進程共享相同的資源。可共享的資源包括文件、信號處理程序和虛擬內存等。

 劉帥

原創作品轉載請注明出處

《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM