《Linux內核分析》第六週學習筆記

《Linux內核分析》第六週學習筆記 進程的描述和建立

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

【學習視頻時間:1小時 撰寫博客時間:2小時】編程

【學習內容:進程建立的過程、使用gdb跟蹤分析內核處理函數sys_clone】數據結構

1、進程的描述

1.1 進程描述符task_struct數據結構(一)

1. 進程控制塊PCB——task_structdom

爲了管理進程,內核必須對每一個進程進行清晰的描述,進程描述符提供了內核所需瞭解的進程信息。函數

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

struct task_ struct數據結構很龐大學習

  • Linux進程的狀態與操做系統原理中的描述的進程狀態彷佛有所不一樣,好比就緒狀態和運行狀態都是TASK_ RUNNING,爲何呢?
  • 進程的標示pid
  • 全部進程鏈表struct list_ head tasks;
    • 內核的雙向循環鏈表的實現方法
    • 一個更簡略的雙向循環鏈表
  • 程序建立的進程具備父子關係,在編程時每每須要引用這樣的父子關係。進程描述符中有幾個域用來表示這樣的關係
  • Linux爲每一個進程分配一個8KB大小的內存區域,用於存放該進程兩個不一樣的數據結構:Thread_ info和進程的內核堆棧
    • 進程處於內核態時使用,不一樣於用戶態堆棧,即PCB中指定了內核棧,那爲何PCB中沒有用戶態堆棧?用戶態堆棧是怎麼設定的?
    • 內核控制路徑所用的堆棧不多,所以對棧和Thread_ info來講,8KB足夠了
  • struct thread_ struct thread; //CPU-specific state of this task
  • 文件系統和文件描述符
  • 內存管理——進程的地址空間

Linux進程狀態轉換圖this

1.2 進程描述符task_struct數據結構(二)

1242#ifdef CONFIG_SMP //條件編譯,多處理器會用到
1243    struct llist_node wake_entry;
1244    int on_cpu;
1245    struct task_struct *last_wakee;
1246    unsigned long wakee_flips;
1247    unsigned long wakee_flip_decay_ts;
1248
1249    int wake_cpu;
1250#endif
1251    int on_rq;
1252
1253    int prio, static_prio, normal_prio;
1254    unsigned int rt_priority; //與優先級相關
1255    const struct sched_class *sched_class;
1256    struct sched_entity se;
1257    struct sched_rt_entity rt;

……

1295    struct list_head tasks; //進程鏈表
1296#ifdef CONFIG_SMP
1297    struct plist_node pushable_tasks;
1298    struct rb_node pushable_dl_tasks;
1299#endif

 

Linux的狀態圖atom

1301    struct mm_struct *mm, *active_mm; //內存管理進程的地址空間
1302#ifdef CONFIG_COMPAT_BRK
1303    unsigned brk_randomized:1;
1304#endif
1305    /* per-thread vma caching */
1306    u32 vmacache_seqnum;
1307    struct vm_area_struct *vmacache[VMACACHE_SIZE];
1308#if defined(SPLIT_RSS_COUNTING)
1309    struct task_rss_stat    rss_stat;
1310#endif
1311/* task state */
1312    int exit_state;  //任務的狀態
1313    int exit_code, exit_signal;
1314    int pdeath_signal;  /*  The signal sent when the parent dies  */
1315    unsigned int jobctl;    /* JOBCTL_*, siglock protected */
1316
1317    /* Used for emulating ABI behavior of previous Linux versions */
1318    unsigned int personality;
1319
1320    unsigned in_execve:1;   /* Tell the LSMs that the process is doing an
1321                 * execve */
1322    unsigned in_iowait:1;

……

1337    //進程的父子關係
1338     * pointers to (original) parent process, youngest child, younger sibling,
1339     * older sibling, respectively.  (p->father can be replaced with
1340     * p->real_parent->pid)
1341     */
1342    struct task_struct __rcu *real_parent; /* real parent process */
1343    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
1344    /*
1345     * children/sibling forms the list of my natural children
1346     */
1347    struct list_head children;  /* list of my children */
1348    struct list_head sibling;   /* linkage in my parent's children list */
1349    struct task_struct *group_leader;   /* threadgroup leader */

1356    struct list_head ptraced; //調試
1357    struct list_head ptrace_entry;
1358
1359    /* PID/PID hash table linkage. */
1360    struct pid_link pids[PIDTYPE_MAX];
1361    struct list_head thread_group;
1362    struct list_head thread_node; //將進程鏈表鏈接起來

……

1411/* 與當前任務CPU狀態相關,對進程上下文切換有關鍵性做用 */
1412    struct thread_struct thread;
1413/* filesystem information */
1414    struct fs_struct *fs;
1415/* 打開文件描述符列表 */
1416    struct files_struct *files;
1417/* namespaces */
1418    struct nsproxy *nsproxy;
1419/* 信號處理 */
1420    struct signal_struct *signal;
1421    struct sighand_struct *sighand;
1422
1423    sigset_t blocked, real_blocked;
1424    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
1425    struct sigpending pending;
……
1598    struct pipe_inode_info *splice_pipe;
1599    //與管道相關
1600    struct page_frag task_frag;
1601
1602#ifdef  CONFIG_TASK_DELAY_ACCT
1603    struct task_delay_info *delays;
1604#endif
1605#ifdef CONFIG_FAULT_INJECTION
1606    int make_it_fail;
1607#endif

 

2、進程的建立

2.1 進程的建立概覽及fork一個進程的用戶態代碼

回顧:spa

  • 道生一(start_ kernel...cpu_ idle)
  • 一輩子二(kernel_ init和kthreadd)
  • 二生三(即前面的0、一、2三個進程)
  • 三生萬物(1號進程是全部用戶態進程的祖先,2號進程是全部內核線程的祖先)
  • start_ kernel建立了cpu_ idle,也就是0號進程。而0號進程又建立了兩個線程,一個是kernel_ init,也就是1號進程,這個進程最終啓動了用戶態;另外一個是kthreadd。0號進程是固定的代碼,1號進程是經過複製0號進程PCB以後在此基礎上作修改獲得的
  • iret與int 0x80指令對應,一個是彈出寄存器值,一個是壓入寄存器的值
  • 若是將系統調用類比於fork();那麼就至關於系統調用建立了一個子進程,而後子進程返回以後將在內核態運行,而返回到父進程後仍然在用戶態運行

fork一個子進程的代碼操作系統

#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) //pid == 0和下面的else都會被執行到(一個是在父進程中即pid ==0的狀況,一個是在子進程中,即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");
    }
}

 

2.2 理解進程建立過程複雜代碼的方法

Linux中建立進程一共有三個函數:

  • fork,建立子進程
  • vfork,與fork相似,可是父子進程共享地址空間,並且子進程先於父進程運行。
  • clone,主要用於建立線程

  這裏值得注意的是,Linux中得線程是經過模擬進程實現的,較新的內核使用的線程庫通常都是NPTL。

  • 複製一個PCB——task_struct

err = arch_dup_task_struct(tsk, orig);

 

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

ti = alloc_ thread_ info_ node(tsk, node);
tsk->stack = ti;
setup_ thread_ stack(tsk, orig); //這裏只是複製thread_ info,而非複製內核堆棧

 

  • 要修改複製過來的進程數據,好比pid、進程鏈表等。具體見copy _process內部
  • 從用戶態的代碼看fork(),函數返回了兩次,即在父子進程中各返回一次。

2.3 瀏覽進程建立過程相關的關鍵代碼

經過以前的學習,咱們知道fork是經過觸發0x80中斷,陷入內核,來使用內核提供的提供調用:

SYSCALL_DEFINE0(fork)
{
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
#endif

SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

 

經過以上精簡後的代碼,咱們能夠看出,fork、vfork和clone這三個函數最終都是經過do_ 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。上面的過程對vfork稍微作了處理,由於vfork必須保證子進程優先運行,執行exec,替換本身的地址空間。

拋開vfork,進程建立的大部分過程都在copy_ process函數中copy_process的代碼很是複雜,這裏我精簡了大部分,只留下最重要的一些:

/*
    建立進程描述符以及子進程所須要的其餘全部數據結構
    爲子進程準備運行環境
*/
static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;

    // 分配一個新的task_struct,此時的p與當前進程的task,僅僅是stack地址不一樣
    p = dup_task_struct(current);

    // 檢查該用戶的進程數是否超過限制
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        // 檢查該用戶是否具備相關權限,不必定是root
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }

    retval = -EAGAIN;
    // 檢查進程數量是否超過 max_threads,後者取決於內存的大小
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    // 初始化自旋鎖

    // 初始化掛起信號

    // 初始化定時器

    // 完成對新進程調度程序數據結構的初始化,並把新進程的狀態設置爲TASK_RUNNING
    retval = sched_fork(clone_flags, p);
    // .....

    // 複製全部的進程信息
    // copy_xyz

    // 初始化子進程的內核棧
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
    if (retval)
        goto bad_fork_cleanup_io;

    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        // 這裏爲子進程分配了新的pid號
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    /* ok, now we should be set up.. */
    // 設置子進程的pid
    p->pid = pid_nr(pid);
    // 若是是建立線程
    if (clone_flags & CLONE_THREAD) {
        p->exit_signal = -1;
        // 線程組的leader設置爲當前線程的leader
        p->group_leader = current->group_leader;
        // tgid是當前線程組的id,也就是main進程的pid
        p->tgid = current->tgid;
    } else {
        if (clone_flags & CLONE_PARENT)
            p->exit_signal = current->group_leader->exit_signal;
        else
            p->exit_signal = (clone_flags & CSIGNAL);
        // 建立的是進程,本身是一個單獨的線程組
        p->group_leader = p;
        // tgid和pid相同
        p->tgid = p->pid;
    }

    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
        // 若是是建立線程,那麼同一線程組內的全部線程、進程共享parent
        p->real_parent = current->real_parent;
        p->parent_exec_id = current->parent_exec_id;
    } else {
        // 若是是建立進程,當前進程就是子進程的parent
        p->real_parent = current;
        p->parent_exec_id = current->self_exec_id;
    }

    // 將pid加入PIDTYPE_PID這個散列表
    attach_pid(p, PIDTYPE_PID);
    // 遞增 nr_threads的值
    nr_threads++;

    // 返回被建立的task結構體指針
    return p;
}

 

根據這份精簡代碼,我總結出copy_process的大致流程

  1. 檢查各類標誌位(已經省略)
  2. 調用dup_ task_ struct複製一份task_ struct結構體,做爲子進程的進程描述符。
  3. 檢查進程的數量限制。
  4. 初始化定時器、信號和自旋鎖。
  5. 初始化與調度有關的數據結構,調用了sched_ fork,這裏將子進程的state設置爲TASK_ RUNNING。
  6. 複製全部的進程信息,包括fs、信號處理函數、信號、內存空間(包括寫時複製)等。
  7. 調用copy_ thread,這又是關鍵的一步,這裏設置了子進程的堆棧信息。
  8. 爲子進程分配一個pid
  9. 設置子進程與其餘進程的關係,以及pid、tgid等。這裏主要是對線程作一些區分。

2.4 建立的新進程從哪裏開始執行?

一個新建立的子進程,(當它得到CPU以後)是從哪一行代碼進程執行的?

  • 與以前寫過的my_ kernel相比較,kernel中是能夠指定新進程開始的位置(也就是經過eip寄存器指定代碼行)。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; //調度到子進程時的第一條指令地址

 

2.5 使用gdb跟蹤建立新進程的過程(實驗)

1. 更新menu,刪除test_fork.c和test.c文件,從新執行make rootfs

2. 編譯內核查看fork命令

3. 啓動gdb調試,並對主要的函數設置斷點

執行一個fork,會發現只輸出一個fork的命令描述,後面並無執行,由於它停在了sys_ clone這個位置。

4. 特別關注新進程是從哪裏開始執行的?爲何從哪裏能順利執行下去?即執行起點與內核堆棧如何保證一致。

  答:ret_ from_ fork決定了新進程的第一條指令地址。子進程從ret_ from_ fork處開始執行。由於在ret_ from_ fork以前,也就是在copy_ thread()函數中* childregs = * current_ pt_ regs();該句將父進程的regs參數賦值到子進程的內核堆棧。* childregs的類型爲pt_ regs,裏面存放了SAVE_ ALL中壓入棧的參數,所以在以後的RESTORE ALL中能順利執行下去。

總結

  Linux中全部的進程建立都是基於複製的方式,Linux經過複製父進程來建立一個新進程,經過調用do_ fork來實現。而後對子進程作一些特殊的處理。而Linux中的線程,又是一種特殊的進程。根據代碼的分析,do_ fork中,copy_ process管子進程運行的準備,wake_ up_ new_ task做爲子進程forking的完成。fork()函數最大的特色就是被調用一次,返回兩次。

相關文章
相關標籤/搜索