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

  1. 賀邦
    原創做品轉載請註明出處 
    《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000node

  2. 準備工做 
    task_struct的應該會存在哪些結構:
一、進程狀態、將紀錄進程在等待、運行、或死鎖
二、調度信息、由哪一個調度函數調度、怎樣調度等
三、進程的通信情況
四、有插入進程鏈表的相關操做,所以必須有鏈表鏈接指針、固然是task_struct型
五、時間信息,好比計算好執行的時間、以便CPU資源的分配
六、標號,決定改進程歸屬
七、能夠讀寫打開的一些文件信息
八、進程上下文和內核上下文
九、處理器上下文
十、內存信息等等

課堂提供在/linux-3.18.6/include/linux/sched.h中找到tast_struct的定義: 
如今的Linux系統基本上是按照操做系統理論來進行設計的,可是在實現的過程當中,理論每每是不夠的,爲了實現不少實際的需求,tast_struct還定義了不少額外的結構,來方便系統的相關管理,好比後面沒有列出來的一些文件操做相關的結構,這些結構通常用於當一個進程沒有按照規範來操做文件時,當進程被殺掉後,系統任然能夠對這些不規範的操做進行管理。固然,後面還有不少內容也是如此,咱們就不一一敘說了,咱們只看建立一個進程的相關重點。linux

struct task_struct {
    volatile long state;        //說明了該進程是否能夠執行,仍是可中斷等信息
    unsigned long flags;        //進程號,在調用fork()時給出
    int sigpending;             //進程上是否有待處理的信號
    mm_segment_t addr_limit;    //進程地址空間,區份內核進程與普通進程在內存存放的位置不一樣
                                //0-0xBFFFFFFF for user-thead
                                //0-0xFFFFFFFF for kernel-thread
    //調度標誌,表示該進程是否須要從新調度,若非0,則當從內核態返回到用戶態,會發生調度
    volatile long need_resched;
    int lock_depth;             //鎖深度
    long nice;                  //進程的基本時間片
    //進程的調度策略,有三種,實時進程:SCHED_FIFO,SCHED_RR, 分時進程:SCHED_OTHER
    unsigned long policy;
    struct mm_struct *mm;       //進程內存管理信息
    int processor;
    //若進程不在任何CPU上運行, cpus_runnable 的值是0,不然是1 這個值在運行隊列被鎖時更新
    unsigned long cpus_runnable, cpus_allowed;
    struct list_head run_list;  //指向運行隊列的指針
    unsigned long sleep_time;   //進程的睡眠時間
    //用於將系統中全部的進程連成一個雙向循環鏈表, 其根是init_task
    struct task_struct *next_task, *prev_task;
    struct mm_struct *active_mm;
    struct list_head local_pages;       //指向本地頁面 
    unsigned int allocation_order, nr_local_pages;
    struct linux_binfmt *binfmt;        //進程所運行的可執行文件的格式
    int exit_code, exit_signal;
    int pdeath_signal;                  //父進程終止是向子進程發送的信號
    unsigned long personality;

    int did_exec:1; 
    pid_t pid;                          //進程標識符,用來表明一個進程
    pid_t pgrp;                         //進程組標識,表示進程所屬的進程組
    pid_t tty_old_pgrp;                 //進程控制終端所在的組標識
    pid_t session;                      //進程的會話標識
    pid_t tgid;
    int leader;                         //表示進程是否爲會話主管
    struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
    struct list_head thread_group;      //線程鏈表
    struct task_struct *pidhash_next;   //用於將進程鏈入HASH表
    struct task_struct **pidhash_pprev;
    wait_queue_head_t wait_chldexit;    //供wait4()使用
    struct completion *vfork_done;      //供vfork() 使用
    unsigned long rt_priority;          //實時優先級,用它計算實時進程調度時的weight值
    …… 
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

3.進程建立分析 
fork函數到底如何進行對應的內核處理過程sys_clone。session

#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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
檢測題目的最後一個是什麼鬼
Linux中,fork()系統調用產生的子進程在系統調用處理過程當中從(:ret_from_fork)處開始,我要留着之後看一下,莫名其妙

Linux中,PCB task_struct中不包含哪一個信息()? 
進程狀態 
進程打開的文件 
進程優先級信息 
進程包含的線程列表信息 
建立一個新進程在內核中的執行過程 
fork、vfork和clone三個系統調用均可以建立一個新進程,並且都是經過調用do_fork來實現進程的建立; 
Linux經過複製父進程來建立一個新進程,那麼這就給咱們理解這一個過程提供一個想象的框架: 
複製一個PCB——task_struct數據結構

err = arch_dup_task_struct(tsk, orig);框架

要給新進程分配一個新的內核堆棧dom

ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //這裏只是複製thread_info,而非複製內核堆棧
  • 1
  • 2
  • 3

要修改複製過來的進程數據,好比pid、進程鏈表等等都要改改吧,見copy_process內部。 
從用戶態的代碼看fork();函數返回了兩次,即在父子進程中各返回一次,父進程從系統調用中返回比較容易理解,子進程從系統調用中返回,那它在系統調用處理過程當中的哪裏開始執行的呢?這就涉及子進程的內核堆棧數據狀態和task_struct中thread記錄的sp和ip的一致性問題,這是在哪裏設定的?copy_thread in copy_process函數

*childregs = *current_pt_regs(); //複製內核堆棧
childregs->ax = 0; //爲何子進程的fork返回0,這裏就是緣由!

p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂
p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址
  • 1
  • 2
  • 3
  • 4
  • 5

按流程來講首先會進入do_fork,在do_fork裏,對一些狀況進行判斷。若是沒有什麼危險的狀況,則開始進入copy_process。 
copy_process函數在進程建立的do_fork函數中調用,主要完成進程數據結構,各類資源的初始化。初始化方式能夠從新分配,也能夠共享父進程資源,主要根據傳入CLONE參數來肯定。ui

  1. dup_task_struct()
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    int node = tsk_fork_get_node(orig);
    int err;

    tsk = alloc_task_struct_node(node);
    if (!tsk)
        return NULL;

    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
        goto free_tsk;

    err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto free_ti;

    tsk->stack = ti;
# ifdef CONFIG_SECCOMP

    tsk->seccomp.filter = NULL;
# endif

    setup_thread_stack(tsk, orig);
    clear_user_return_notifier(tsk);
    clear_tsk_need_resched(tsk);
    set_task_stack_end_magic(tsk);

# ifdef CONFIG_CC_STACKPROTECTOR
    tsk->stack_canary = get_random_int();
# endif

    atomic_set(&tsk->usage, 2);
# ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
# endif
    tsk->splice_pipe = NULL;
    tsk->task_frag.page = NULL;

    account_kernel_stack(ti, 1);

    return tsk;

free_ti:
    free_thread_info(ti);
free_tsk:
    free_task_struct(tsk);
    return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

tsk = alloc_task_struct_node(node);爲task_struct開闢內存 
ti = alloc_thread_info_node(tsk, node);ti指向thread_info的首地址,同時也是系統爲新進程分配的兩個連續頁面的首地址。 
err = arch_dup_task_struct(tsk, orig);複製父進程的task_struct信息到新的task_struct裏, (dst = src;) 
tsk->stack = ti;task的對應棧 
setup_thread_stack(tsk, orig);初始化thread info結構 
set_task_stack_end_magic(tsk);棧結束的地址設置數據爲棧結束標示(for overflow detection)atom

  1. gdb跟蹤sys_clone 
    用gdb來跟蹤sys_clone,設置如下斷點 
    這裏寫圖片描述
    運行後首先停在sys_clone處: 
    這裏寫圖片描述
    而後是do_fork,以後是copy_process: 
    接着進入copy_thread: 
    ret_from_fork按照以前的分析被調用,跟蹤到syscall_exit後沒法繼續.若是想在本機調試system call,那麼當你進入system call時,系統已經在掛起狀態了。
  2. 總結 
    新進程是從哪裏開始執行的——————–? 
    在以前的分析中,談到copy_process中的copy_thread()函數,正是這個函數決定了子進程從系統調用中返回後的執行.
int copy_thread(unsigned long clone_flags, unsigned long sp,
    unsigned long arg, struct task_struct *p)
{
    ...

    *childregs = *current_pt_regs();
    childregs->ax = 0;
    if (sp)
        childregs->sp = sp;

    p->thread.ip = (unsigned long) ret_from_fork;

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

子進程執行ret_from_forkspa

ENTRY(ret_from_fork)
    CFI_STARTPROC
    pushl_cfi %eax
    call schedule_tail
    GET_THREAD_INFO(%ebp)
    popl_cfi %eax
    pushl_cfi $0x0202       # Reset kernel eflags
    popfl_cfi
    jmp syscall_exit
    CFI_ENDPROC
END(ret_from_fork)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

===================== 在ret_from_fork以前,也就是在copy_thread()函數中*childregs = *current_pt_regs();該句將父進程的regs參數賦值到子進程的內核堆棧, *childregs的類型爲pt_regs,裏面存放了SAVE ALL中壓入棧的參數 故在以後的RESTORE ALL中能順利執行下去. 7. 未完待續。。 8. 望手下留情

相關文章
相關標籤/搜索