1. 程式人生 > >Linux進程創建、執行和切換過程理解

Linux進程創建、執行和切換過程理解

關系 copy 實驗 經歷 調度 char earch 取值 static

Linux進程創建、執行和切換過程理解

學號:282

原創作品轉載請註明出處 + https://github.com/mengning/linuxkernel/

實驗內容

  • 進程的創建
    1. 閱讀理解task_struct數據結構
    2. 分析fork函數對應的內核處理過程do_fork,理解創建一個新進程如何創建和修改task_struct數據結構
    3. 使用gdb跟蹤分析一個fork系統調用內核處理函數do_fork
  • 可執行文件的加載
    1. 理解編譯鏈接的過程和ELF可執行文件格式
    2. 編程使用exec*庫函數加載一個可執行文件,動態鏈接分為可執行程序裝載時動態鏈接和運行時動態鏈接
    3. 使用gdb跟蹤分析一個execve系統調用內核處理函數do_execve
  • 進程切換
    1. 分析一個schedule()函數
    2. 仔細分析switch_to中的匯編代碼

實驗過程

一、進程的創建

task_struct分析

為了描述進程的信息,我們引入了進程控制塊這個數據結構。進程控制塊至少應該包含進程標識(是進程的唯一標識,PID),還有進程的優先級,記錄進程的上下文信息,記錄進程下一次下一條指令的地址,進程中的程序的地址,等等。下面就task_struct結構體的數據成員進行簡單分類:

  1. volatile long state. 進程的狀態,可能取值如下

    #define TASK_RUNNING        0        //進程要麽正在執行,要麽準備執行
    #define TASK_INTERRUPTIBLE  1        //可中斷的睡眠,可以通過一個信號喚醒
    #define TASK_UNINTERRUPTIBLE    2    //不可中斷睡眠,不可以通過信號進行喚醒
    #define __TASK_STOPPED      4        //進程停止執行
    #define __TASK_TRACED       8        //進程被追蹤
    /* in tsk->exit_state */ 
    #define EXIT_ZOMBIE     16           //僵屍狀態的進程,表示進程被終止,但是父進程還沒有獲取它的終止信息,比如進程有沒有執行完等信息。                     
    #define EXIT_DEAD       32           //進程的最終狀態,進程死亡。
    /* in tsk->state again */ 
    #define TASK_DEAD       64           //死亡
    #define TASK_WAKEKILL       128  //喚醒並殺死的進程
    #define TASK_WAKING     256      //喚醒進程 
  2. 進程的唯一標識

     pid_t pid;      //進程的唯一標識
     pid_t tgid; // 線程組的領頭線程的pid成員的值
  3. unsigned int flags.進程的標記

    #define PF_ALIGNWARN    0x00000001    /* Print alignment warning msgs */
                        /* Not implemented yet, only for 486*/
    #define PF_STARTING    0x00000002    /* being created */
    #define PF_EXITING    0x00000004    /* getting shut down */
    #define PF_EXITPIDONE    0x00000008    /* pi exit done on shut down */
    #define PF_VCPU        0x00000010    /* I'm a virtual CPU */
    #define PF_FORKNOEXEC    0x00000040    /* forked but didn't exec */
    #define PF_MCE_PROCESS  0x00000080      /* process policy on mce errors */
    #define PF_SUPERPRIV    0x00000100    /* used super-user privileges */
    #define PF_DUMPCORE    0x00000200    /* dumped core */
    #define PF_SIGNALED    0x00000400    /* killed by a signal */
    #define PF_MEMALLOC    0x00000800    /* Allocating memory */
    #define PF_FLUSHER    0x00001000    /* responsible for disk writeback */
    #define PF_USED_MATH    0x00002000    /* if unset the fpu must be initialized before use */
    #define PF_FREEZING    0x00004000    /* freeze in progress. do not account to load */
    #define PF_NOFREEZE    0x00008000    /* this thread should not be frozen */
    #define PF_FROZEN    0x00010000    /* frozen for system suspend */
    #define PF_FSTRANS    0x00020000    /* inside a filesystem transaction */
    #define PF_KSWAPD    0x00040000    /* I am kswapd */
    #define PF_OOM_ORIGIN    0x00080000    /* Allocating much memory to others */
    #define PF_LESS_THROTTLE 0x00100000    /* Throttle me less: I clean memory */
    #define PF_KTHREAD    0x00200000    /* I am a kernel thread */
    #define PF_RANDOMIZE    0x00400000    /* randomize virtual address space */
    #define PF_SWAPWRITE    0x00800000    /* Allowed to write to swap */
    #define PF_SPREAD_PAGE    0x01000000    /* Spread page cache over cpuset */
    #define PF_SPREAD_SLAB    0x02000000    /* Spread some slab caches over cpuset */
    #define PF_THREAD_BOUND    0x04000000    /* Thread bound to specific cpu */
    #define PF_MCE_EARLY    0x08000000      /* Early kill for mce process policy */
    #define PF_MEMPOLICY    0x10000000    /* Non-default NUMA mempolicy */
    #define PF_MUTEX_TESTER    0x20000000    /* Thread belongs to the rt mutex tester */
    #define PF_FREEZER_SKIP    0x40000000    /* Freezer should not count it as freezeable */
    #define PF_FREEZER_NOSIG 0x80000000    /* Freezer won't send signals to it */
  4. 進程之間的親屬關系。

    struct task_struct *real_parent; /* real parent process 父進程*/
    struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports 終止時,向父進程發送信號*/
    struct list_head children;    /* list of my children 子進程鏈表*/
    struct list_head sibling;    /* linkage in my parent's children list */
    struct task_struct *group_leader;    /* threadgroup leader */
  5. 進程調度信息

    int prio, static_prio, normal_prio;      // 優先級
    unsigned int rt_priority;                // 保存實時優先級
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
    unsigned int policy;                 // 進程調度策略
  6. 時間數據成員

    cputime_t utime, stime, utimescaled, stimescaled;
    cputime_t gtime;
    cputime_t prev_utime, prev_stime;    // 記錄當前的運行時間(用戶態和內核態)
    unsigned long nvcsw, nivcsw;         // 自願/非自願上下文切換計數
    struct timespec start_time;          // 進程的開始執行時間    
    struct timespec real_start_time;     // 進程真正的開始執行時間
    unsigned long min_flt, maj_flt;
    struct task_cputime cputime_expires;// cpu執行的有效時間
    struct list_head cpu_timers[3];      // 用來統計進程或進程組被處理器追蹤的時間
    struct list_head run_list;
    unsigned long timeout;               // 當前已使用的時間(與開始時間的差值)
    unsigned int time_slice;         // 進程的時間片的大小
    int nr_cpus_allowed;

gdb跟蹤分析do_fork

  1. 修改MenuOS中的test.c,加入系統調用的代碼

    #include <stdio.h>
    #include <unistd.h>
    int Fork(int argc, char **argv){
        pid_t pid;
    
        pid=fork();
        if(pid==-1)
            printf("fork error\n");
        else if(pid==0){
            printf("the returned value is %d\n",pid);   
            printf("The pid is %d,now in child process\n",getpid());
        }
        else{
            printf("the returned value is %d\n",pid);   
            printf("The pid is %d,now in farther process\n",getpid());  
        }
        return 0;
    }
  2. 啟動MenuOS,使用gdb追蹤

    技術分享圖片

    打斷點
    技術分享圖片

    追蹤斷點
    技術分享圖片

  3. do_fork分析

    Linux系統中,除第一個進程是被捏造出來的,其他進程都是通過do_fork()復制出來的,方法聲明如下。

    int do_fork(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs, unsigned long stack_size)

    為了關註do_fork主體,省略了部分細節

    第一步:然是創建新的進程,首先需要申請進程最基本的單位task_struct結構

    p = alloc_task_struct();
    if (!p)
    goto fork_out;
    
    *p = *current;

    第二步:獲取一個空閑的pid

    static int get_pid(unsigned long flags)

    第三步:復制各種資源

    /* copy all the process information */
    if (copy_files(clone_flags, p))             //復制文件描述符
    goto bad_fork_cleanup;
    if (copy_fs(clone_flags, p))
    goto bad_fork_cleanup_files;
    if (copy_sighand(clone_flags, p))            //復制信號量
    goto bad_fork_cleanup_fs;                    
    if (copy_mm(clone_flags, p))                 //復制虛存空間
    goto bad_fork_cleanup_sighand;
    retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);  //復制系統堆棧

    第四步:copy_thread

    #define savesegment(seg,value) asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value)))
    
    int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
    unsigned long unused,
    struct task_struct * p, struct pt_regs * regs)
    {
    struct pt_regs * childregs;
       //獲取子進程系統堆棧頂部指針
    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;    
    struct_cpy(childregs, regs);    //從父進程拷貝系統堆棧狀態
    childregs->eax = 0;             //子進程返回值設0
    childregs->esp = esp;           //子進程用戶堆棧
    
    p->thread.esp = (unsigned long) childregs;        //初次運行時,子進程系統堆棧位置
    p->thread.esp0 = (unsigned long) (childregs+1);    //子進程系統空間堆棧頂部
    
    p->thread.eip = (unsigned long) ret_from_fork;     //下次運行時的切入點
    
    savesegment(fs,p->thread.fs);
    savesegment(gs,p->thread.gs);
    
    unlazy_fpu(current);
    struct_cpy(&p->thread.i387, ¤t->thread.i387);
    
    return 0;
    }

    一個完整的子進程已經誕生了,這時子進程還不在進程可執行隊列中,不能接受調度,但是隨後就會通過wake_up_process(p)將其加入可執行隊列接受調度。

二、理解編譯鏈接的過程和ELF可執行文件格式

gdb跟蹤do_execve

  1. 修改MenuOS中test.c源文件,加入execlp("/bin/ls","ls",NULL);重新編譯後使用gdb追蹤。

    技術分享圖片

  2. do_execve斷點

    技術分享圖片

小結:

可執行程序的elf格式

我們編譯獲得一個可執行程序,會被加載到內存進行執行。這個可執行程序是有一定的格式的,其中現在用的比較多的就是elf格式。對於一個可執行文件,必須保護程序運行必要的相關信息,比如說這個程序依賴那些動態鏈接庫,程序的入口地址等,我們通過這樣一種格式化的方式,保存了這些信息共系統進行解析,從而可以加載我們的代碼段和數據段進入內存。

程序的加載以及執行過程的關鍵行為分析

execve函數會獲得一些shell通過函數調用的機制傳遞的參數進行執行,經歷上面調試過程講解的一系列步驟:**execve=>do_execve=>do_execve_common=>(do_open_exec/exec_binprm)=>**

search_binary_handler=>list_for_each_entry=>load_elf_binary=>start_thread

總體來講,就是通過構造一些結構體,首先對文件進行打開操作,並且在結構體中保存一些必要的信息,然後根據文件的類型,使用不同的模塊對文件進行解析,解析的過程中知道了這是一個動態鏈接的程序還是一個靜態鏈接的程序,根據這個設置內核棧中的ip,這樣在進程調度的時候,就有了合適的入口。

三、進程切換

? fork會創建一個新的進程,加載文件並進行執行。在這個過程中,涉及到了兩個進程之間的切換。我們依然使用上一步的環境,對fork系統調用進行調試,來完成這個分析。

? 技術分享圖片

? 技術分享圖片

context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next){
  struct mm_struct *mm, *oldmm;
  prepare_task_switch(rq, prev, next);
  mm = next->mm;<br>   switch_to(prev,next,prev);//切換寄存器的狀態和堆棧的切換

我們主要來看switch_to函數

#define switch_to(prev, next, last)                 do {                                        /*                                   * Context-switching clobbers all registers, so we clobber       * them explicitly, via unused output variables.             * (EAX and EBP is not listed because EBP is saved/restored      * explicitly for wchan access and EAX is the return value of        * __switch_to())                            */                                 unsigned long ebx, ecx, edx, esi, edi;                                                      asm volatile("pushfl\n\t"       /* save  flags */                "pushl %%ebp\n\t"      /* save    EBP   */              "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \//保存到了output裏面,棧頂裏面.
             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \//完成了內核堆棧的切換,接下來就是在另外一個進程的棧空間了
             "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */ \//當前進程的eip保持
             "pushl %[next_ip]\n\t" /* restore EIP   */
             __switch_canary                                 "jmp __switch_to\n"    /* regparm call  */ \//jmp的函數完成以後,需要iret,把ip彈出來了,這樣就到了下一行代碼執行.
 
             "1:\t"                     \//新設置的IP是從這裏開始的,也就是movl $1f,從這裏開始就說明是另外一個進程了。所以內核堆棧先切換好,執行了兩句,用的是新的進程的內核堆棧,但是確是在原來的進程的ip繼續執行。
             "popl %%ebp\n\t"       /* restore EBP   */              "popfl\n"          /* restore flags */ \ //原來的進程切換的時候,曾經設置過save ebp和save flags,所以這裏就需要pop來恢復
                                                 /* output parameters */                             : [prev_sp] "=m" (prev->thread.sp),     \ //分別表示內核堆棧以及當前進程的eip
               [prev_ip] "=m" (prev->thread.ip),                    "=a" (last),                                                                    /* clobbered output registers: */                       "=b" (ebx), "=c" (ecx), "=d" (edx),                     "=S" (esi), "=D" (edi)                                                                      __switch_canary_oparam                                                                  /* input parameters: */                           : [next_sp]  "m" (next->thread.sp),                    [next_ip]  "m" (next->thread.ip),     \ //下一個進程的執行起點以及內核堆棧
                                                       /* regparm parameters for __switch_to(): */                 [prev]     "a" (prev),                              [next]     "d" (next)                \  //用a和d來傳遞參數...
                                                   __switch_canary_iparam                                                                : /* reloaded segment registers */                     "memory");                  } while (0)

小結:

先是pushfl \n\t等語句,用來在當前的棧中保持flags,以及當前的ebp,準備進行進程的切換。然後是當前的esp,會保存在當前進程的thread結構體中。其中的movl $1f,%[prev_ip]則是保存當前進程的ip為代碼中標號1的位置。然後是resotre ebp和flags的語句,用於恢復ebp和flags到寄存器中,這些值是保持在內核棧中的。這樣,對於新的進程,我們使用c繼續執行,就可以走到ret_from_fork中了。

Linux進程創建、執行和切換過程理解