Linux Process/Thread Creation、Linux Process Principle、sys_fork、sys_execve、glibc fork/execve api sourcecode

Andrew.Hann發表於2014-07-18

相關學習資料

linux核心設計與實現+原書第3版.pdf(3.3章)
深入linux核心架構(中文版).pdf
深入理解linux核心中文第三版.pdf
《獨闢蹊徑品核心Linux核心原始碼導讀》
http://www.yanyulin.info/pages/2013/11/linux0.html
http://blog.csdn.net/ddna/article/details/4958058
http://www.cnblogs.com/coolgestar02/archive/2010/12/31/1922629.html
http://blog.sina.com.cn/s/blog_4ba5b45e0102e3to.html
http://www.kernel.org/

 

目錄

1. Linux/Unix程式建立相關基本知識
2. Linux程式管理
3. sys_fork() 
4. sys_execve()函式
5. Copy On Write COW(寫時複製)技術
6. Linux Glibc提供的建立程式的7種API方式
7. Glibc execve、fork API原始碼分析
8. 檢視程式的啟動過程工具
9. Linux下執行緒建立
10. Posix執行緒

 

1. Linux/Unix程式建立相關基本知識

0x1: linux和windows在程式建立上的區別

unix/linux的程式建立和Windows有很大不一樣,windows對執行緒和程式的實現非常標準,windows核心有明確的執行緒和程式的概念。在windows API中,可以使用明確的API: CreateProcess和CreateThread來建立程式和執行緒,並且有一系列的API來操縱它們,但對於Linux來說,執行緒並不是一個強制性明確的概念

在Linux核心中並不存在真正意義上的執行緒概念,Linux將所有的執行實體(程式或執行緒)都稱為"任務(task)",每一個任務概念上都類似於一個單執行緒的程式,具有記憶體空間、執行實體、檔案資源等。但是Linux下不同的任務之間可以選擇共享記憶體空間,因此在實際意義上,共享了同一個記憶體空間的多個任務構成了一個程式,這些任務也可以稱之為這個程式中的執行緒

1. windows
windows採用了createProcess()來進行新程式的建立,大致流程如下:
    1) 申請一塊全新的記憶體(包括核心空間和使用者空間)
    2) 開啟新程式對應的磁碟檔案,將檔案內容複製到新申請的記憶體中
    3) 啟動主執行緒從新程式的函式入口點(預設是main)開始順序執行
在windows的哲學中,每一個新程式都是一個新的、獨立的記憶體空間,程式之間彼此相對獨立。
雖然在核心物件中也有父程式和子程式這些欄位,但是這只是一個弱關係,windows中的父子程式並沒有強制性的依賴關係。
關於windows的程式建立過程,請參閱另一篇文章
http://www.cnblogs.com/LittleHann/p/3458736.html

2. linux/unix
對於linu/unix的作業系統來說,它並不像windows那樣採用"產生(spawn)"程式的機制。
而是將建立程式的步驟分解到兩個單獨的函式中去執行:
    1) fork()
    fork()通過"拷貝"當前程式,建立一個子程式。這個時候的子程式和父程式的區別僅僅在於PID(程式號)、PPID(父程式號)、和某些資源和統計量
    2) exec()
    exec()函式則負責讀取可執行檔案並將其載入地址空間開始執行
把這兩個函式(fork、exec)組合起來的最終效果就等同於windows中的createProcess

需要明白的是,fork和exec並不是強制一定要按順序執行的,實際上,可以單獨只執行fork、或者單獨執行exec、或者執行fork+exec。在呼叫fork和exec之間插入額外的程式碼執行也是可行的,fork和exec在原理上兩個獨立的概念

0x2: linux中的0號、1號程式

1. 程式0
Linux引導中建立的第一個程式,完成載入系統後,演變為程式排程、交換及儲存管理程式(也就是說0號程式自從建立完1號程式後就不會再次去建立其他程式了,之後由1號程式負責新子程式的建立)
Linux中1號程式是由0號程式來建立的,由於在建立程式時,程式一直執行在核心態,而程式執行在使用者態,因此建立0號程式涉及到特權級的變化,即從特權級0變到特權級3,Linux是通過模擬中斷返回來實現特權級的變化以及建立0號
程式,通過將0號程式的程式碼段選擇子以及程式計數器EIP直接壓入核心態堆疊,然後利用iret彙編指令中斷返回跳轉到0號程式執行。
2. 程式1 init 程式,由0程式建立,完成系統的初始化。是系統中所有其它使用者程式的祖先程式。

 

2. Linux程式管理
0x1: 程式概念

程式就是處於執行期的程式(目標碼存放在某種儲存介質上),從廣義上講,它包括

1. 一般的可執行程式碼(即程式碼段)
2. 開啟的檔案
3. 掛起的訊號
4. 核心內部資料結構
5. 處理器狀態
6. 一個或多個具有記憶體對映的記憶體地址空間
7. 一個或多個執行執行緒(thread of execution)
8. 存放全域性變數的資料段
//程式就是正在執行的程式程式碼的"實時結果",核心需要有效而又透明地管理所有細節

0x2: 建立程式 && 建立新程式
在學習Linux程式建立相關知識的時候,我們需要對Linux下"程式建立"和"新程式建立"這兩個概念進行區分,完整地說,Linux下程式建立有如下幾個場景

1. 從當前程式複製一份和父程式完全一樣的新程式: 準確地說是複製了一份父程式作為新程式
從系統呼叫的角度來說,和程式建立相關的系統呼叫只有fork(),程式在呼叫fork()建立它的時刻開始存活,fork()通過"複製"(Linux下所有程式都是"複製"出來的)一個現有程式來建立一個新的程式,呼叫fork()的程式稱為父程式,新產生的程式稱為子程式。在該呼叫結束時,在返回到這個相同位置上,父程式恢復執行,子程式開始執行。
fork()系統呼叫從核心返回兩次,一次返回到父程式、另一次返回到新產生的子程式
    1) 呼叫fork()
    or
    2) 呼叫clone()
/*
就像一個細胞複製了一份和自己相同的新細胞,兩個細胞同時執行
*/

2. 執行新程式碼的新程式建立: 在呼叫fork的基礎上,繼續呼叫exec(),讀取並載入新程式程式碼並繼續執行
通常,建立新的程式都是為了立即執行新的、不同的程式碼,而接著呼叫exec這組函式就可以建立新的"地址空間",並把新的程式載入其中。在現代Linux核心中,fork()實際上是由clone()系統呼叫實現的
    1) fork()/clone() + exec()
/*
就像一個細胞複製了一份和自己相同的新細胞,並填充進了新的細胞核,兩個細胞同時執行
*/

3. 執行新程式: 直接將當前程式轉變為一個包含不同程式碼的新程式
    1) exec()
/*
就像一個細胞使用新的蛋白質將自己的細胞核改變了,並繼續執行
*/

0x3: 程式描述符及任務(task)結構

核心把程式的的列表存放在"任務佇列(task list)"(這是一個雙向迴圈連結串列)中,連結串列中的每一項都是型別為task_struct稱為程式描述符(process descriptor)的結構,該結構中包含了具體程式的所有相關資訊,例如

1. 開啟的檔案
2. 程式的地址空間
3. 掛起的訊號
4. 程式的狀態
..

關於task_struct資料結構的相關知識,請參閱另一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜尋:0x1: struct task_struct

0x4: Linux程式建立方法

從程式設計師的角度來說,Linux下實現程式建立可以通過以下方法

1. 通過系統提供的系統呼叫
    1) fork()/clone(): 複製一份新程式
    2) exec(): 執行新程式
    3) fork()/clone() + exec(): 複製並執行一個新程式(父程式和子程式執行不同的程式碼)
2. 通過glibc提供的API函式: exec系列函
    1) exec系列函式: glibc實現對系統呼叫exec()的一層包裝
    2) fork api

我們接下來先了解核心態的fork、execve系統呼叫開始,然後再學習使用者態Glibc提供的程式建立相關API

 

3. sys_fork()

使用fork建立的程式被稱為原父程式(parents process)的子程式(child process)。從使用者的角度來看,子程式是父程式的一個精確副本,兩個程式只是PID不同,fork系統呼叫從核心態返回2次,PID分別為

1. 子程式: PID = 0
2. 父程式: PID = 子程式的PID
//程式可以通過檢測fork的返回值來判斷當前程式是父程式還是子程式

從整體上來看,一次fork呼叫包括了以下幾步

1. 為子程式分配和初始化一個新的task_struct結構
    1) 從父程式中複製: 包括所有從父程式繼承而來的特權和限制
        1.1) 程式組和會話資訊
        1.2) 訊號狀態(忽略、捕獲、阻塞訊號的掩碼)
        1.3) kg_nice排程引數
        1.4) 對父程式憑據的引用
        1.5) 對父程式開啟檔案的引用(即檔案控制程式碼表。以及相關引用資料結構,使用這些資料結構可以操作對應的檔案)
        1.6) 對父程式限制(resources limitation)的引用
    2) 清零
        2.1) 最近CPU利用率
        2.2) 等待通道
        2.3) 交換和睡眠時間
        2.4) 定時器
        2.5) 跟蹤機制
        2.6) 掛起訊號的資訊
    3) 顯式地進行初始化
        3.1) 包括所有程式的連結串列的入口
        3.2) 父程式的子程式連結串列的入口以及指向其父程式的返回指標
        3.3) 父程式的程式組連結串列的入口
        3.4) 雜湊結構的入口,該結構使得程式可以通過其PID進行查詢
        3.5) 指向程式統計結構的指標,該結構位於使用者結構中
        3.6) 指向程式訊號處理結構的指標,該結構位於使用者結構中
        3.7) 該程式的新PID
2. 複製父程式的地址空間
在複製一個程式的映像時,核心通過vm_forkproc()來呼叫記憶體管理機制。vm_forkproc()例程的引數是一個指向一個已經初始化過的子程式的task_struct的指標,它的任務是為該子程式分配其執行所需的全部資源。vm_forkproc()呼叫在子程式中通過另一條直接進入使用者態的執行線路返回,而在父程式中沿著正常的執行線路返回(即一次呼叫、2次返回)
將父程式的上下文複製給子程式,包括
    1) 執行緒結構
    2) 父程式的register暫存器狀態,fork系統呼叫結束後,父程式程式從同一個程式碼位置開始繼續執行
    2) 虛擬記憶體資源,只是複製了一份引用,copy on write機制
3. 排程子程式執行(execve)
子程式最終建立完畢之後,就被放入執行佇列,這樣排程程式就知道這個新程式了

Fork的系統呼叫程式碼在\linux-2.6.32.63\arch\x86\kernel\process.c中

/*
Sys_fork系統呼叫通過 do_fork()函式實現,通過對do_fork()函式傳遞不同的clone_flags來實現:
1. fork
2. clone
3. vfork
*/
int sys_fork(struct pt_regs *regs)
{
    return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}

我們繼續跟蹤do_fork()的程式碼

\linux-2.6.32.63\kernel\fork.c

/*
1. clone_flags: 指定了子程式結束時,需要向父程式傳送的訊號,通常這個訊號是SIGCHLD,同時clone_flags還指定子程式需要共享父程式的哪些資源
2. stack_start: 子程式使用者態堆疊起始地址。通常設定為0,父程式會複製自己的堆疊指標,當子程式對堆疊進行寫入時,缺頁中斷處理程式會設定新的物理頁面(即copy on write 寫時複製)
3. regs: pt_regs結構,儲存了進入核心態時的儲存器的值,父程式會將暫存器狀態完整的複製給子程式
4. stack_size: 預設為0
5. parent_tidptr: 使用者態記憶體指標,當CLONE_PARENT_SETTID被設定時,核心會把新建立的子程式ID通過parent_tidptr返回
6. child_tidptr: 使用者態記憶體指標,當CLONE_CHILD_SETTID被設定時,核心會把新建立的子程式ID通過child_tidptr返回
*/
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
     * Do some preliminary argument and permissions checking before we actually start allocating stuff
    */
    if (clone_flags & CLONE_NEWUSER) 
    {
        if (clone_flags & CLONE_THREAD)
            return -EINVAL;
        /* hopefully this check will go away when userns support is
         * complete
         */
        if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID))
            return -EPERM;
    }

    /*
    We hope to recycle these flags after 2.6.26
    採用向下相容的模式,2.6.26之後,將CLONE_STOPPED廢除
    */
    if (unlikely(clone_flags & CLONE_STOPPED)) 
    {
        static int __read_mostly count = 100;

        if (count > 0 && printk_ratelimit()) 
        {
            char comm[TASK_COMM_LEN];

            count--;
            printk(KERN_INFO "fork(): process `%s' used deprecated clone flags 0x%lx\n", get_task_comm(comm, current), clone_flags & CLONE_STOPPED);
        }
    }

    /*
    When called from kernel_thread, don't do user tracing stuff.
    */
    if (likely(user_mode(regs)))
        trace = tracehook_prepare_clone(clone_flags);
    
    /*
    Do_fork()函式的核心是copy_process()函式,該函式完成了程式建立的絕大部分工作
    分配子程式的task_struct結構,並複製父程式的資源
    */
    p = copy_process(clone_flags, stack_start, regs, stack_size, child_tidptr, NULL, trace);
    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    if (!IS_ERR(p)) 
    {
        struct completion vfork;

        trace_sched_process_fork(current, p);
        /*
        /source/include/linux/sched.h
        /source/kernel/pid.c
        設定pid namespace,不同的namespace中,可以建立相同的pid的程式
        */
        nr = task_pid_vnr(p);

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

        /*
        CLONE_VFORK要求父程式進入子程式,現在初始化一個等待物件
        */
        if (clone_flags & CLONE_VFORK) 
        {
            p->vfork_done = &vfork;
            init_completion(&vfork);
        }

        audit_finish_fork(p);
        tracehook_report_clone(regs, clone_flags, nr, p);

        /*
         We set PF_STARTING at creation in case tracing wants to use this to distinguish a fully live task from one that hasn't gotten to tracehook_report_clone() yet.  
         Now we clear it and set the child going.
         */
        p->flags &= ~PF_STARTING;

        /*
        如果被設定了CLONE_STOPPED標誌,則向程式傳送SIGSTOP訊號
        */
        if (unlikely(clone_flags & CLONE_STOPPED)) 
        {
            /*
            We'll start up with an immediate SIGSTOP.
            */
            sigaddset(&p->pending.signal, SIGSTOP);
            set_tsk_thread_flag(p, TIF_SIGPENDING);
            __set_task_state(p, TASK_STOPPED);
        } 
        else 
        {
            //如果沒有設定CLONE_STOPPED標誌,就把程式加入就緒佇列
            wake_up_new_task(p, clone_flags);
        }

        tracehook_report_clone_complete(trace, regs, clone_flags, nr, p);

        if (clone_flags & CLONE_VFORK) 
        {
            freezer_do_not_count();
            //當前程式進入之前初始化好等待佇列
            wait_for_completion(&vfork);
            freezer_count();
            tracehook_report_vfork_done(p, nr);
        }
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

Do_fork()函式的核心是copy_process()函式,該函式完成了程式建立的絕大部分工作
繼續跟蹤copy_process()
\linux-2.6.32.63\kernel\fork.c

/*
This creates a new process as a copy of the old one, but does not actually start it yet.
It copies the registers, and all the appropriate parts of the process environment (as per the clone * flags). The actual kick-off is left to the caller.
*/
static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    struct pt_regs *regs,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;
    int cgroup_callbacks_done = 0;

    /*
    1. 對傳入的clone_flag進行檢查
    */
    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);
 
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);
 
    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);
 
    if ((clone_flags & CLONE_PARENT) &&
                current->signal->flags & SIGNAL_UNKILLABLE)
        return ERR_PTR(-EINVAL);

    /*
    LSM安全框架檢查,利用它可以在程式建立之前檢查是否允許檢查,利用這個核心框架,可以開發出程式監控功能。預設呼叫dummy_task_create函式,這是一個空函式
    */
    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    /*
    2. 呼叫了dup_task_struct()函式,該函式的主要作用是
        1) 為子程式建立一個新的核心棧
        2) 複製父程式的task_struct結構和thread_info結構,這裡只是對結構完整的複製,所以子程式的程式描述符跟父程式完全一樣
    */
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    ftrace_graph_init_task(p);

    rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
    DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
    DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
    retval = -EAGAIN;
    /*
    檢查程式的資源限制
    */
    if (atomic_read(&p->real_cred->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur)
    {
        if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER)
            goto bad_fork_free;
    }

    /*
    複製父程式的cred訊號,這個結構儲存的是程式的身份許可權資訊(例如UID)
    */
    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;
 
    retval = -EAGAIN;
    /*
    3. 檢查建立的程式是否超過了系統程式總量
    */
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    if (!try_module_get(task_thread_info(p)->exec_domain->module))
        goto bad_fork_cleanup_count;

    p->did_exec = 0;
    delayacct_tsk_init(p);    /* Must remain after dup_task_struct() */
    //複製clone_flags到子程式的task_struct結構中
    copy_flags(clone_flags, p);
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    rcu_copy_process(p);
    p->vfork_done = NULL;
    spin_lock_init(&p->alloc_lock);

    init_sigpending(&p->pending);

    /*
    4. 開始對子程式task_struct結構的初始化過程
    */
    p->utime = cputime_zero;
    p->stime = cputime_zero;
    p->gtime = cputime_zero;
    p->utimescaled = cputime_zero;
    p->stimescaled = cputime_zero;
    p->prev_utime = cputime_zero;
    p->prev_stime = cputime_zero;

    p->default_timer_slack_ns = current->timer_slack_ns;

    task_io_accounting_init(&p->ioac);
    acct_clear_integrals(p);

    posix_cpu_timers_init(p);

    p->lock_depth = -1;        /* -1 = no lock */
    do_posix_clock_monotonic_gettime(&p->start_time);
    p->real_start_time = p->start_time;
    monotonic_to_bootbased(&p->real_start_time);
    p->io_context = NULL;
    p->audit_context = NULL;
    cgroup_fork(p);
#ifdef CONFIG_NUMA
    p->mempolicy = mpol_dup(p->mempolicy);
     if (IS_ERR(p->mempolicy)) {
         retval = PTR_ERR(p->mempolicy);
         p->mempolicy = NULL;
         goto bad_fork_cleanup_cgroup;
     }
    mpol_fix_fork_child_flag(p);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
    p->irq_events = 0;
#ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
    p->hardirqs_enabled = 1;
#else
    p->hardirqs_enabled = 0;
#endif
    p->hardirq_enable_ip = 0;
    p->hardirq_enable_event = 0;
    p->hardirq_disable_ip = _THIS_IP_;
    p->hardirq_disable_event = 0;
    p->softirqs_enabled = 1;
    p->softirq_enable_ip = _THIS_IP_;
    p->softirq_enable_event = 0;
    p->softirq_disable_ip = 0;
    p->softirq_disable_event = 0;
    p->hardirq_context = 0;
    p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
    p->lockdep_depth = 0; /* no locks held yet */
    p->curr_chain_key = 0;
    p->lockdep_recursion = 0;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
    p->blocked_on = NULL; /* not blocked yet */
#endif

    p->bts = NULL;

    /* Perform scheduler related setup. Assign this task to a CPU. */
    sched_fork(p, clone_flags);

    retval = perf_event_init_task(p);
    if (retval)
        goto bad_fork_cleanup_policy;

    if ((retval = audit_alloc(p)))
        goto bad_fork_cleanup_policy;
    /* 
    copy all the process information 
    根據clone_flags複製父程式的資源到子程式,對於clone_flags指定共享的資源,父子程式間共享這些資源,僅僅設定子程式的相關指標,並增加資源資料結構的引用計數
    */
    if ((retval = copy_semundo(clone_flags, p)))
        goto bad_fork_cleanup_audit;
    if ((retval = copy_files(clone_flags, p)))
        goto bad_fork_cleanup_semundo;
    if ((retval = copy_fs(clone_flags, p)))
        goto bad_fork_cleanup_files;
    if ((retval = copy_sighand(clone_flags, p)))
        goto bad_fork_cleanup_fs;
    if ((retval = copy_signal(clone_flags, p)))
        goto bad_fork_cleanup_sighand;
    if ((retval = copy_mm(clone_flags, p)))
        goto bad_fork_cleanup_signal;
    if ((retval = copy_namespaces(clone_flags, p)))
        goto bad_fork_cleanup_mm;
    if ((retval = copy_io(clone_flags, p)))
        goto bad_fork_cleanup_namespaces;
    //複製父程式的核心態堆疊到子程式
    retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);
    if (retval)
        goto bad_fork_cleanup_io;

    if (pid != &init_struct_pid) 
    {
        retval = -ENOMEM;
        pid = alloc_pid(p->nsproxy->pid_ns);
        if (!pid)
            goto bad_fork_cleanup_io;

        if (clone_flags & CLONE_NEWPID) 
        {
            retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
            if (retval < 0)
                goto bad_fork_free_pid;
        }
    }

    p->pid = pid_nr(pid);
    /*
    5. 如果設定了同在一個執行緒組則繼承TGID。對於普通程式來說TGID和PID相等,對於執行緒來說,同一執行緒組內的所有執行緒的TGID都相等,這使得這些多執行緒可以通過呼叫getpid()獲得相同的PID
    如果建立的是輕權程式,那麼父子程式在同一個執行緒組中,就設定子程式的tgid
    */
    p->tgid = p->pid;
    if (clone_flags & CLONE_THREAD)
        p->tgid = current->tgid;

    //建立新的namespace
    if (current->nsproxy != p->nsproxy) 
    {
        retval = ns_cgroup_clone(p, pid);
        if (retval)
            goto bad_fork_free_pid;
    }

    p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
    /*
     * Clear TID on mm_release()?
     */
    p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
#ifdef CONFIG_FUTEX
    p->robust_list = NULL;
#ifdef CONFIG_COMPAT
    p->compat_robust_list = NULL;
#endif
    INIT_LIST_HEAD(&p->pi_state_list);
    p->pi_state_cache = NULL;
#endif 

    if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
        p->sas_ss_sp = p->sas_ss_size = 0;
 
    clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
    clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
    clear_all_latency_tracing(p);

    /*
    父程式是否要求子程式退出時傳送訊號 
    ok, now we should be set up.. 
    */
    p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
    p->pdeath_signal = 0;
    //子程式預設的退出狀態
    p->exit_state = 0;
 
    p->group_leader = p;
    INIT_LIST_HEAD(&p->thread_group);
 
    cgroup_fork_callbacks(p);
    cgroup_callbacks_done = 1;

    /* Need tasklist lock for parent etc handling! */
    write_lock_irq(&tasklist_lock);

    /* 
    CLONE_PARENT re-uses the old parent 
    */
    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) 
    {
        //把子程式的real_parent設定為父程式的real_parent
        p->real_parent = current->real_parent;
        p->parent_exec_id = current->parent_exec_id;
    } 
    else 
    {
        p->real_parent = current;
        p->parent_exec_id = current->self_exec_id;
    }

    spin_lock(&current->sighand->siglock);
 
    recalc_sigpending();
    if (signal_pending(current)) {
        spin_unlock(&current->sighand->siglock);
        write_unlock_irq(&tasklist_lock);
        retval = -ERESTARTNOINTR;
        goto bad_fork_free_pid;
    }

    if (clone_flags & CLONE_THREAD) {
        atomic_inc(&current->signal->count);
        atomic_inc(&current->signal->live);
        p->group_leader = current->group_leader;
        list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
    }

    if (likely(p->pid)) 
    {
        //把子程式新增到父程式的子程式連結串列中,這樣組成了兄弟程式連結串列
        list_add_tail(&p->sibling, &p->real_parent->children);
        tracehook_finish_clone(p, clone_flags, trace);

        if (thread_group_leader(p)) 
        {
            if (clone_flags & CLONE_NEWPID)
                p->nsproxy->pid_ns->child_reaper = p;

            p->signal->leader_pid = pid;
            tty_kref_put(p->signal->tty);
            p->signal->tty = tty_kref_get(current->signal->tty);
            attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
            attach_pid(p, PIDTYPE_SID, task_session(current));
            list_add_tail_rcu(&p->tasks, &init_task.tasks);
            __get_cpu_var(process_counts)++;
        }
        attach_pid(p, PIDTYPE_PID, pid);
        nr_threads++;
    }

    total_forks++;
    spin_unlock(&current->sighand->siglock);
    write_unlock_irq(&tasklist_lock);
    proc_fork_connector(p);
    cgroup_post_fork(p);
    perf_event_fork(p);
    return p;
/*
出錯退出
*/
bad_fork_free_pid:
    if (pid != &init_struct_pid)
        free_pid(pid);
bad_fork_cleanup_io:
    if (p->io_context)
        exit_io_context(p);
bad_fork_cleanup_namespaces:
    exit_task_namespaces(p);
bad_fork_cleanup_mm:
    if (p->mm)
        mmput(p->mm);
bad_fork_cleanup_signal:
    if (!(clone_flags & CLONE_THREAD))
        __cleanup_signal(p->signal);
bad_fork_cleanup_sighand:
    __cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
    exit_fs(p); /* blocking */
bad_fork_cleanup_files:
    exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
    exit_sem(p);
bad_fork_cleanup_audit:
    audit_free(p);
bad_fork_cleanup_policy:
    perf_event_free_task(p);
#ifdef CONFIG_NUMA
    mpol_put(p->mempolicy);
bad_fork_cleanup_cgroup:
#endif
    cgroup_exit(p, cgroup_callbacks_done);
    delayacct_tsk_free(p);
    module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
    atomic_dec(&p->cred->user->processes);
    exit_creds(p);
bad_fork_free:
    free_task(p);
fork_out:
    return ERR_PTR(retval);
}

繼續跟蹤dup_task_struct()
\linux-2.6.32.63\kernel\fork.c

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    unsigned long *stackend;

    int err;

    prepare_to_copy(orig);

    /*
    1. 通過alloc_task_struct()函式建立核心棧和task_struct結構空間
    */
    tsk = alloc_task_struct();
    if (!tsk)
        return NULL;
    /*
    2. 分配thread_info結構空間
    */
    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    }
    /*
    3. 為整個task_struct結構複製
    */
     err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto out;

    tsk->stack = ti;

    err = prop_local_init_single(&tsk->dirties);
    if (err)
        goto out;
    /*
    4. 呼叫setup_thread_stack()函式為thread_info結構複製
    */
    setup_thread_stack(tsk, orig);
    stackend = end_of_stack(tsk);
    *stackend = STACK_END_MAGIC;    /* for overflow detection */

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

    /*
    更新該使用者的user_struct結構,累加相應的計數器,由atomic_inc()函式完成
    */
    atomic_set(&tsk->usage,2);
    atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
#endif
    tsk->splice_pipe = NULL;

    account_kernel_stack(ti, 1);

    return tsk;

out:
    free_thread_info(ti);
    free_task_struct(tsk);
    return NULL;
}

copy_process()完成的工作主要是進行必要的檢查、初始化、複製必要的資料結構。這裡我們重點分析兩個函式

1. copy_mm(): 涉及到父子程式的copy on write,以及共享核心虛擬地址的實現
2. copy_thread(): 涉及到父子程式返回的實現(一次呼叫、2次返回)

//複製父程式的核心態堆疊到子程式
retval = copy_thread(clone_flags, stack_start, stack_size, p, regs);

應用程式通過fork()系統呼叫進入核心空間,其核心態堆疊上儲存著該程式的"程式上下文(暫存器狀態)",通過copy_thread將複製父程式的核心態堆疊上的"程式上下文"到子程式中,同時把子程式堆疊上的EAX設定為0。由於父子程式的程式碼和資料是共享的,所以在返回後將接著執行,所以會發現以下現象

1. 父子程式從同一個程式碼位置開始繼續執行: 因為它們的"程式上下文"相同
2. 父程式呼叫fork()返回子程式的PID: 父程式是正常呼叫
3. 子程式返回0,因為核心態的EAX被設定為了0
4. 父子程式不一定同時開始執行,但會有從核心態返回2次,一次是父程式,一次是子程式

if ((retval = copy_mm(clone_flags, p)))
        goto bad_fork_cleanup_signal;

核心呼叫copy_mm()來建立子程式的記憶體區域

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
    struct mm_struct * mm, *oldmm;
    int retval;

    tsk->min_flt = tsk->maj_flt = 0;
    tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
    tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

    tsk->mm = NULL;
    tsk->active_mm = NULL;

    /*
     * Are we cloning a kernel thread?
     *
     * We need to steal a active VM for that..
     */
    oldmm = current->mm;
    if (!oldmm)
        return 0;

    /*
    如果要共享mm,則增加父程式mm的引用計數,同時把子程式的mm設定為current->mm
    */
    if (clone_flags & CLONE_VM) 
    {
        atomic_inc(&oldmm->mm_users);
        mm = oldmm;
        goto good_mm;
    }

    retval = -ENOMEM;
    /*
    複製mm_struct的工作由dup_mm()來完成,這個函式會複製父程式的頁表到子程式,這樣父子程式就共享同樣的物理頁面,同時也共享了整個核心空間。
    但是對於可寫的使用者空間對應的頁表,dup_mm()會把它們設定為"只讀",這樣當程式(父程式或子程式)對它進行寫入時,do_page_fault()函式將分配新的物理頁面,為程式複製一份私有資料,這就是copy on write的機制
    當父子程式的任何一個返回使用者態首次對堆疊進行寫入操作時,父子程式就會有各自獨立的使用者態堆疊了,但是對於程式碼段,它們卻始終共享同一份物理頁面,除非子程式呼叫exec()系列
    */
    mm = dup_mm(tsk);
    if (!mm)
        goto fail_nomem;

good_mm:
    /* Initializing for Swap token stuff */
    mm->token_priority = 0;
    mm->last_interval = 0;

    tsk->mm = mm;
    tsk->active_mm = mm;
    return 0;

fail_nomem:
    return retval;
}

繼續跟進dup_mm

/*
Allocate a new mm structure and copy contents from the mm structure of the passed in task structure.
*/
struct mm_struct *dup_mm(struct task_struct *tsk)
{
    struct mm_struct *mm, *oldmm = current->mm;
    int err;

    if (!oldmm)
        return NULL;
    //分配mm_struct結構
    mm = allocate_mm();
    if (!mm)
        goto fail_nomem;
    //複製mm_struct結構
    memcpy(mm, oldmm, sizeof(*mm));

    /* Initializing for Swap token stuff */
    mm->token_priority = 0;
    mm->last_interval = 0;

    /*
    初始化,同時分配頁表
    mm_init()初始化mm_struct結構中的自旋鎖、連結串列等資源,然後呼叫mm_alloc_pgd()函式分配頁表,同時把父程式的核心虛擬地址對應的頁表項複製到子程式的頁表,因此父子程式共享了核心態地址空間
    */
    if (!mm_init(mm, tsk))
        goto fail_nomem;

    if (init_new_context(tsk, mm))
        goto fail_nocontext;

    dup_mm_exe_file(oldmm, mm);

    //拷貝vm_area_struct結構
    err = dup_mmap(mm, oldmm);
    if (err)
        goto free_pt;

    mm->hiwater_rss = get_mm_rss(mm);
    mm->hiwater_vm = mm->total_vm;

    if (mm->binfmt && !try_module_get(mm->binfmt->module))
        goto free_pt;

    return mm;

free_pt:
    /* don't put binfmt in mmput, we haven't got module yet */
    mm->binfmt = NULL;
    mmput(mm);

fail_nomem:
    return NULL;

fail_nocontext:
    /*
     * If init_new_context() failed, we cannot use mmput() to free the mm
     * because it calls destroy_context()
     */
    mm_free_pgd(mm);
    free_mm(mm);
    return NULL;
}

繼續跟進mm_alloc_pgd()

\linux-2.6.32.63\arch\x86\mm\pgtable.c

static inline int mm_alloc_pgd(struct mm_struct * mm)
{
    mm->pgd = pgd_alloc(mm);
    if (unlikely(!mm->pgd))
        return -ENOMEM;
    return 0;
}

pgd_t *pgd_alloc(struct mm_struct *mm)
{
    pgd_t *pgd;
    pmd_t *pmds[PREALLOCATED_PMDS];

    pgd = (pgd_t *)__get_free_page(PGALLOC_GFP);

    if (pgd == NULL)
        goto out;

    mm->pgd = pgd;

    if (preallocate_pmds(pmds) != 0)
        goto out_free_pgd;

    if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

    /*
    Make sure that pre-populating the pmds is atomic with respect to anything walking the pgd_list, 
    so that they never see a partially populated pgd.
    */
    spin_lock(&pgd_lock);

    pgd_ctor(pgd);
    pgd_prepopulate_pmd(mm, pgd, pmds);

    spin_unlock(&pgd_lock);

    return pgd;

out_free_pmds:
    free_pmds(pmds);
out_free_pgd:
    free_page((unsigned long)pgd);
out:
    return NULL;
}

至此,頁表分配完畢,同時核心態地址空間的對映關係已經建立。我們繼續學習dup_mmap()是如何處理使用者態地址空間的相關資料結構的,它主要完成以下幾件事

1. 分配並複製vm_area_struct結構
2. 根據vm_area_struct結構的屬性標誌設定頁表項,把可寫入的記憶體片段設定為只讀
3. 當某個程式(父程式、或子程式)對其進行寫入時,do_page_fault()將分配新的物理頁面,為該程式建立私有資料,同時修改頁表,指向新的物理頁面

err = dup_mmap(mm, oldmm);

static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
    struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
    struct rb_node **rb_link, *rb_parent;
    int retval;
    unsigned long charge;
    struct mempolicy *pol;

    down_write(&oldmm->mmap_sem);
    flush_cache_dup_mm(oldmm);
    /*
     * Not linked in yet - no deadlock potential:
     */
    down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);

    mm->locked_vm = 0;
    mm->mmap = NULL;
    mm->mmap_cache = NULL;
    mm->free_area_cache = oldmm->mmap_base;
    mm->cached_hole_size = ~0UL;
    mm->map_count = 0;
    cpumask_clear(mm_cpumask(mm));
    mm->mm_rb = RB_ROOT;
    rb_link = &mm->mm_rb.rb_node;
    rb_parent = NULL;
    pprev = &mm->mmap;
    retval = ksm_fork(mm, oldmm);
    if (retval)
        goto out;

    prev = NULL;
    //處理每一個vm_area_struct結構
    for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) 
    {
        struct file *file;
        //不需要複製
        if (mpnt->vm_flags & VM_DONTCOPY) 
        {
            long pages = vma_pages(mpnt);
            mm->total_vm -= pages;
            vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
                                -pages);
            continue;
        }
        charge = 0;
        //需要安全計數檢查
        if (mpnt->vm_flags & VM_ACCOUNT) 
        {
            unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
            if (security_vm_enough_memory(len))
                goto fail_nomem;
            charge = len;
        }
        //為子程式分配新的vm_area_struct結構
        tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
        if (!tmp)
            goto fail_nomem;
        //複製整個結構
        *tmp = *mpnt;
        pol = mpol_dup(vma_policy(mpnt));
        retval = PTR_ERR(pol);
        if (IS_ERR(pol))
            goto fail_nomem_policy;
        vma_set_policy(tmp, pol);
        tmp->vm_flags &= ~VM_LOCKED;
        tmp->vm_mm = mm;
        tmp->vm_next = tmp->vm_prev = NULL;
        anon_vma_link(tmp);
        file = tmp->vm_file;
        
        //如果這篇記憶體對應的是一個檔案對映,則設定檔案相關資訊,增加檔案的引用計數等
        if (file) 
        {
            struct inode *inode = file->f_path.dentry->d_inode;
            struct address_space *mapping = file->f_mapping;

            get_file(file);
            if (tmp->vm_flags & VM_DENYWRITE)
                atomic_dec(&inode->i_writecount);
            spin_lock(&mapping->i_mmap_lock);
            if (tmp->vm_flags & VM_SHARED)
                mapping->i_mmap_writable++;
            tmp->vm_truncate_count = mpnt->vm_truncate_count;
            flush_dcache_mmap_lock(mapping);
            /* insert tmp into the share list, just after mpnt */
            vma_prio_tree_add(tmp, mpnt);
            flush_dcache_mmap_unlock(mapping);
            spin_unlock(&mapping->i_mmap_lock);
        }

        /*
         * Clear hugetlb-related page reserves for children. This only
         * affects MAP_PRIVATE mappings. Faults generated by the child
         * are not guaranteed to succeed, even if read-only
         */
        if (is_vm_hugetlb_page(tmp))
            reset_vma_resv_huge_pages(tmp);

        /*
        Link in the new vma and copy the page table entries.
        */
        //把新的vm_area_struct結構新增到子程式
        *pprev = tmp;
        pprev = &tmp->vm_next;
        tmp->vm_prev = prev;
        prev = tmp;
        //新增紅黑樹
        __vma_link_rb(mm, tmp, rb_link, rb_parent);
        rb_link = &tmp->vm_rb.rb_right;
        rb_parent = &tmp->vm_rb;

        mm->map_count++;
        /*
        分配設定頁表,並不需要分配物理頁面
        copy_page_range()函式,需要為vm_area_struct結構指定的記憶體區域分配並設定頁表,同時把頁表的實體地址設定到頁目錄(即上級頁表中)
        */
        retval = copy_page_range(mm, oldmm, mpnt);

        if (tmp->vm_ops && tmp->vm_ops->open)
            tmp->vm_ops->open(tmp);

        if (retval)
            goto out;
    }
    /* a new mm has just been created */
    arch_dup_mmap(oldmm, mm);
    retval = 0;
out:
    up_write(&mm->mmap_sem);
    flush_tlb_mm(oldmm);
    up_write(&oldmm->mmap_sem);
    return retval;
fail_nomem_policy:
    kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
    retval = -ENOMEM;
    vm_unacct_memory(charge);
    goto out;
}

繼續跟進copy_page_range()

\linux-2.6.32.63\mm\memory.c

static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm, pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma, unsigned long addr, unsigned long end)
{
    pte_t *orig_src_pte, *orig_dst_pte;
    pte_t *src_pte, *dst_pte;
    spinlock_t *src_ptl, *dst_ptl;
    int progress = 0;
    int rss[2];

again:
    rss[1] = rss[0] = 0;
    dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
    if (!dst_pte)
        return -ENOMEM;
    src_pte = pte_offset_map_nested(src_pmd, addr);
    src_ptl = pte_lockptr(src_mm, src_pmd);
    spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
    orig_src_pte = src_pte;
    orig_dst_pte = dst_pte;
    arch_enter_lazy_mmu_mode();

    do {
        /*
         * We are holding two locks at this point - either of them
         * could generate latencies in another task on another CPU.
         */
        if (progress >= 32) {
            progress = 0;
            if (need_resched() ||
                spin_needbreak(src_ptl) || spin_needbreak(dst_ptl))
                break;
        }
        if (pte_none(*src_pte)) {
            progress++;
            continue;
        }
        /*
        由於為了支援多級分頁,從程式碼上看copy_pte_range比較繁瑣,在多級分頁中,copy_pte_range需要不斷地為vm_start、vm_end指定的虛擬地址設定頁表,最終它呼叫copy_one_pte設定頁表項
        */
        copy_one_pte(dst_mm, src_mm, dst_pte, src_pte, vma, addr, rss);
        progress += 8;
    } while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end);

    arch_leave_lazy_mmu_mode();
    spin_unlock(src_ptl);
    pte_unmap_nested(orig_src_pte);
    add_mm_rss(dst_mm, rss[0], rss[1]);
    pte_unmap_unlock(orig_dst_pte, dst_ptl);
    cond_resched();
    if (addr != end)
        goto again;
    return 0;
}

繼續跟進copy_one_pte()

/*
copy one vm_area from one task to the other. Assumes the page tables already present in the new task to be cleared in the whole range  covered by this vma.
*/
static inline void copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm, pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,    unsigned long addr, int *rss)
{
    unsigned long vm_flags = vma->vm_flags;
    pte_t pte = *src_pte;
    struct page *page;

    //pte contains position in swap or file, so copy. 
    /*
    虛擬地址對應的頁表被交換到磁碟上,需要注意的是,缺頁中斷可以從磁碟交換分割槽調入記憶體,但是缺頁中斷自身所用的記憶體及其頁表是不可交換的,因此核心空間使用的頁表是不可交換的
    */
    if (unlikely(!pte_present(pte))) {
        if (!pte_file(pte)) {
            swp_entry_t entry = pte_to_swp_entry(pte);

            swap_duplicate(entry);
            /* make sure dst_mm is on swapoff's mmlist. */
            if (unlikely(list_empty(&dst_mm->mmlist))) {
                spin_lock(&mmlist_lock);
                if (list_empty(&dst_mm->mmlist))
                    list_add(&dst_mm->mmlist,
                         &src_mm->mmlist);
                spin_unlock(&mmlist_lock);
            }
            if (is_write_migration_entry(entry) && is_cow_mapping(vm_flags)) 
            {
                /*
                 * COW mappings require pages in both parent
                 * and child to be set to read.
                 */
                make_migration_entry_read(&entry);
                pte = swp_entry_to_pte(entry);
                set_pte_at(src_mm, addr, src_pte, pte);
            }
        }
        goto out_set_pte;
    }

    /*
    If it's a COW mapping, write protect it both in the parent and the child
    如果是可寫記憶體區域,則利用頁表把該段記憶體區域設定為只讀,以實現copy on write機制
    */
    if (is_cow_mapping(vm_flags)) 
    {
        ptep_set_wrprotect(src_mm, addr, src_pte);
        pte = pte_wrprotect(pte);
    }

    /*
     * If it's a shared mapping, mark it clean in
     * the child
     */
    if (vm_flags & VM_SHARED)
        pte = pte_mkclean(pte);
    pte = pte_mkold(pte);

    page = vm_normal_page(vma, addr, pte);
    if (page) {
        get_page(page);
        page_dup_rmap(page);
        rss[PageAnon(page)]++;
    }

out_set_pte:
    set_pte_at(dst_mm, addr, dst_pte, pte);
}

繼續回到copy_process中,當父程式進行系統呼叫時,在父程式的核心態儲存了程式的"程式上下文(通用暫存器)",這是一個pt_regs結構,copy_thread()會複製父程式的pt_regs結構到子程式的核心態堆疊

\linux-2.6.32.63\arch\x86\kernel\process_32.c

int copy_thread(unsigned long clone_flags, unsigned long sp, unsigned long unused, struct task_struct *p, struct pt_regs *regs)
{
    struct pt_regs *childregs;
    struct task_struct *tsk;
    int err;

    //核心態堆疊
    childregs = task_pt_regs(p);
    //父程式核心態堆疊中的pt_regs複製到子程式的核心態堆疊
    *childregs = *regs;
    //子程式的pt_regs結構的eax設定為0,所以子程式的fork()"返回"的值為0
    childregs->ax = 0;
    //調整子程式核心態堆疊指標
    childregs->sp = sp;

    p->thread.sp = (unsigned long) childregs;
    p->thread.sp0 = (unsigned long) (childregs+1);

    //設定子程式的thread.eip,這樣當子程式被排程執行時,就直接從ret_from_fork返回,這也就是"一次呼叫、2次返回的原理"
    p->thread.ip = (unsigned long) ret_from_fork;

    task_user_gs(p) = get_user_gs(regs);

    tsk = current;
    // I/O許可權位
    if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) 
    {
        p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
                        IO_BITMAP_BYTES, GFP_KERNEL);
        if (!p->thread.io_bitmap_ptr) {
            p->thread.io_bitmap_max = 0;
            return -ENOMEM;
        }
        set_tsk_thread_flag(p, TIF_IO_BITMAP);
    }

    err = 0;

    /*
    Set a new TLS for the child thread
    執行緒本地儲存機制
    */
    if (clone_flags & CLONE_SETTLS)
        err = do_set_thread_area(p, -1,
            (struct user_desc __user *)childregs->si, 0);

    if (err && p->thread.io_bitmap_ptr) {
        kfree(p->thread.io_bitmap_ptr);
        p->thread.io_bitmap_max = 0;
    }

    clear_tsk_thread_flag(p, TIF_DS_AREA_MSR);
    p->thread.ds_ctx = NULL;

    clear_tsk_thread_flag(p, TIF_DEBUGCTLMSR);
    p->thread.debugctlmsr = 0;

    return err;
}

這樣,子程式建立的工作就結束了,當排程到這個程式時,它將從ret_from_fork開始執行,然後跳轉到syscall_exit,即"1次呼叫fork、兩次從核心態返回到使用者空間",其使用者空間的返回地址儲存在核心態堆疊的pt_regs結構中,這個返回地址和父程式是一致的
最後,再回到do_fork函式,如果copy_process()函式成功返回,新建立子程式被喚醒並讓其投入執行。核心有意選擇子程式首先執行,因為一般子程式都會馬上呼叫exec()函式,這樣可以避免寫時拷貝的額外開銷,如果父程式首先執行的話,有可能會開始向地址空間寫入

0x1: 建立執行緒

首先說明Linux下的程式與執行緒比較相近,很大的原因是它們都需要相同的資料結構來表示,即task_struct。區別在於程式有獨立的使用者空間,而執行緒是共享的使用者空間
對於Linux下執行緒建立的理解,我們需要抓住以下幾個重點

1. Linux系統的執行緒實現很特別,它對執行緒和程式並不特別區分,對Linux而言,執行緒只不過是一種特殊的程式罷了
2. 父程式複製子程式的API包括三種(使用者態)
    1) fork
    2) clone
    3) vfork
//這三個API的內部實際都是呼叫一個核心內部函式do_fork,只是填寫的引數不同而已,通過flags引數來指明父、子程式需要共享的資源
3. 從核心的角度來說,Linux並沒有執行緒這個概念,Linux把所有執行緒都當作任務(task_struct)來實現,核心並沒有準備特殊的排程演算法或是定義特殊的資料結構來表現執行緒,執行緒僅僅被視為一個與其他程式共享某些資源的程式,每個執行緒都有屬於自己獨立的task_strcut,所以在核心中,執行緒就是一個普通的程式,在Linux下,區分執行緒和程式的關係是父子程式資源上的共享程度,從這個意義上來說,執行緒只是一種程式間共享資源的手段

執行緒的建立和普通程式的建立類似(本質上就是程式建立),只不過在呼叫clone()的時候需要傳遞一些引數標誌來指明需要共享的資源(資源的共享是Linux執行緒的核心概念)

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
/*
上面的程式碼和呼叫fork差不多,只是父子程式共享地址空間、檔案系統資源、檔案描述符、訊號處理程式
在這種情況下,新建的程式和它的父程式就是流行的所謂執行緒
*/

傳遞給clone()的引數標誌決定了新建立程式的行為方式和父子程式之間共享的資源種類

#define CSIGNAL            0x000000ff    /* 父子程式共享訊號處理函式及被阻斷的訊號 */
#define CLONE_VM        0x00000100    /* 父子程式共享地址空間 */
#define CLONE_FS        0x00000200    /* 父子程式共享檔案系統資訊 */
#define CLONE_FILES        0x00000400    /* 父子程式共享開啟的檔案 */
#define CLONE_SIGHAND        0x00000800    /* 父子程式共享訊號處理資訊 */
#define CLONE_PTRACE        0x00002000    /* 繼續除錯子程式,即如果父程式正在處於被除錯狀態,要求子程式也處於被除錯狀態 */
#define CLONE_VFORK        0x00004000    /* 建立子程式後,父程式保持阻塞狀態,直到子程式退出或者呼叫execve() */
#define CLONE_PARENT        0x00008000    /* 指定子程式與父程式擁有同一個父程式 */
#define CLONE_THREAD        0x00010000    /* 父子程式放入相同的執行緒組,這樣子程式的tgid、group_leader都會做相應的設定,可以把它理解為同一個程式中的多個執行緒 */
#define CLONE_NEWNS        0x00020000    /* 為子程式建立新的名稱空間 */
#define CLONE_SYSVSEM        0x00040000    /* 父子程式共享SystemV IPC Semaphore語義 */
#define CLONE_SETTLS        0x00080000    /* 為子程式設定獨立的執行緒本地儲存(TLS) */
#define CLONE_PARENT_SETTID    0x00100000    /* 設定父程式的TID */
#define CLONE_CHILD_CLEARTID    0x00200000    /* 清除子程式的TID */
#define CLONE_DETACHED        0x00400000    /* Unused, ignored */
#define CLONE_UNTRACED        0x00800000    /* 防止跟蹤程式在子程式上強制執行CLONE_PTRACE,建立一個不允許被除錯的程式,通常核心態執行緒會設定此標誌 */
#define CLONE_CHILD_SETTID    0x01000000    /* 設定子程式的TID */
#define CLONE_STOPPED        0x02000000    /* 以TASK_STOPPED狀態開始子程式,將來由掐程式來改變這種狀態 */
#define CLONE_NEWUTS        0x04000000    /* New utsname group */
#define CLONE_NEWIPC        0x08000000    /* New ipcs */
#define CLONE_NEWUSER        0x10000000    /* New user namespace */
#define CLONE_NEWPID        0x20000000    /* New pid namespace */
#define CLONE_NEWNET        0x40000000    /* New network namespace */
#define CLONE_IO        0x80000000    /* Clone io context */

 

4. sys_execve()函式

我們知道,所有的exec家族的函式最終都是呼叫了sys_execve()這個系統呼叫來實現的,exce呼叫並不建立新程式,所以前後的程式ID並未改變,exec只是用一個全新的程式替換了當前程式的正文、資料、堆和棧段

\arch\x86\kernel\process_32.c

int sys_execve(struct pt_regs *regs)
{
    int error;
    char *filename;
    /*
    1. 將可執行檔案的名稱裝入到一個新分配的頁面中
    */
    filename = getname((char __user *) regs->bx);
    error = PTR_ERR(filename);
    if (IS_ERR(filename))
        goto out;

    /*
    呼叫do_execve()執行可執行檔案
    */
    error = do_execve(filename,
            (char __user * __user *) regs->cx,
            (char __user * __user *) regs->dx,
            regs);
    if (error == 0) {
        /* Make sure we don't return using sysenter.. */
        set_thread_flag(TIF_IRET);
    }
    putname(filename);
out:
    return error;
}

繼續跟蹤do_execve()

linux-2.6.32.63\fs\exec.c

/*
sys_execve() executes a new program.
1. filename: 可執行檔名稱
2. argv: 指向程式引數的指標陣列
3. envp: 指向程式環境變數的指標陣列
4. regs: 暫存器集合

核心在訪問使用者空間記憶體時需要十分謹慎,而__user註釋允許自動化工具來檢測是否有相關事宜都處理得當
*/
int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs)
{
    //將執行可執行檔案時所需的資訊組織到一起
    struct linux_binprm *bprm;
    struct file *file;
    struct files_struct *displaced;
    bool clear_in_exec;
    int retval;

    retval = unshare_files(&displaced);
    if (retval)
        goto out_ret;

    retval = -ENOMEM;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    if (!bprm)
        goto out_files;

    /*
    1. LSM Hook Point 1: 
    retval = prepare_bprm_creds(bprm);
    prepare_exec_creds->security_prepare_creds
    int security_prepare_creds(struct cred *new, const struct cred *old, gfp_t gfp)
    {
        return security_ops->cred_prepare(new, old, gfp);
    }
    */
    retval = prepare_bprm_creds(bprm);
    if (retval)
        goto out_free;

    retval = check_unsafe_exec(bprm);
    if (retval < 0)
        goto out_free;
    clear_in_exec = retval;
    current->in_execve = 1;
    //找到並開啟給定的可執行程式檔案,open_exec()返回file結構指標,代表著讀入可執行檔案的上下文
    file = open_exec(filename);
    //強制轉換
    retval = PTR_ERR(file);
    //判斷open_exec()返回的是否是無效指標
    if (IS_ERR(file))
        goto out_unmark;
     
    sched_exec();

    //要執行的檔案
    bprm->file = file;
    //要執行的檔案的名字
    bprm->filename = filename;
    bprm->interp = filename;

    /*
    處理若干管理型任務
    1. mm_alloc生成一個新的mm_struct例項來管理程式地址空間
    2. init_new_context是一個特定於體系結構的函式,用於初始化該例項
    3. __bprm_mm_init建立初始的棧
    */
    retval = bprm_mm_init(bprm);
    if (retval)
        goto out_file;

    //統計命令汗引數的個數
    bprm->argc = count(argv, MAX_ARG_STRINGS);
    if ((retval = bprm->argc) < 0)
        goto out;
    //統計環境變數引數的個數
    bprm->envc = count(envp, MAX_ARG_STRINGS);
    if ((retval = bprm->envc) < 0)
        goto out;
    
    //新程式的各個引數(euid、egid、引數列表、環境、檔名..)會被合併成一個型別為linux_biprm的結構,用於之後傳遞給核心函式

    /*
    可執行檔案中讀入開頭的128個位元組到linux_binprm結構brmp中的緩衝區,用於之後核心根據這頭128位元組判斷應該呼叫哪個解析引擎來處理當前檔案
    prepare_binprm用於提供一些父程式相關的值
    */
    retval = prepare_binprm(bprm);
    if (retval < 0)
        goto out;

    //檔名拷貝到新分配的頁面中
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
    if (retval < 0)
        goto out;

    bprm->exec = bprm->p;
    //將環境變數拷貝到新分配的頁面中
    retval = copy_strings(bprm->envc, envp, bprm);
    if (retval < 0)
        goto out;

    //將命令列引數拷貝到新分配的頁面中
    retval = copy_strings(bprm->argc, argv, bprm);
    if (retval < 0)
        goto out;

    //所有準備工作已經完成,所有必要的資訊都已經蒐集到了linux_binprm結構中的bprm中
    current->flags &= ~PF_KTHREAD;
    
    //呼叫search_binary_handler()裝入並執行目標程式,根據讀入資料結構linux_binprm內的二進位制檔案128位元組頭中的關鍵字,決定呼叫哪種載入函式
    /*
    . LSM Hook Point 2: 
    retval = security_bprm_check(bprm); 
    int security_bprm_check(struct linux_binprm *bprm) 
    { 
        return security_ops->bprm_check_security(bprm); 
    } 

    search_binary_handler用於在do_execve結束時查詢一種適當的二進位制格式,用於所要執行的特定檔案(通常根據檔案頭的一個"魔數")
    二進位制格式處理程式負責將新程式的資料載入到舊的地址空間中,通常它們執行以下操作
    1. 釋放原程式使用的"所有"資源
    2. 將應用程式對映到虛擬地址空間中,必須考慮下列段的處理(涉及的變數是task_struct的成員,由二進位制格式處理程式設定為正確的值)
        1) text段包含程式的可執行程式碼,start_code、end_code指定該段在地址空間中駐留的區域
        2) 預先初始化的資料(在編譯時指定了具體值的變數)位於start_data、end_data之間,對映自可執行檔案的對應段
        3) 堆(heap)用於動態記憶體分配,也置於虛擬地址空間中,start_brk、brk指定了其邊界
        4) 棧的位置由start_stack定義,向下增長
        5) 程式的引數和環境也對映到虛擬地址空間中,位於arg_start、arg_end之間,以及env_start、env_end之間
    3. 設定程式的指令指標和其他特定於體系結構的暫存器,以便在排程器選擇該程式時開始執行程式的main函式
    */
    retval = search_binary_handler(bprm, regs);
    if (retval < 0)
        goto out;

    /* execve succeeded */
    current->fs->in_exec = 0;
    current->in_execve = 0;
    acct_update_integrals(current);    

    free_bprm(bprm);
    if (displaced)
        put_files_struct(displaced);
    return retval;

out:
    if (bprm->mm) {
        acct_arg_size(bprm, 0);
        mmput(bprm->mm);
    }

out_file:
    //發生錯誤,返回inode,並釋放資源
    if (bprm->file) 
    {
        //呼叫allow_write_access()防止其他程式在讀入可執行檔案期間通過記憶體對映改變它的內容
        allow_write_access(bprm->file);
        //遞減file檔案中的共享計數
        fput(bprm->file);
    }

out_unmark:
    if (clear_in_exec)
        current->fs->in_exec = 0;
    current->in_execve = 0;

out_free:
    free_bprm(bprm);

out_files:
    if (displaced)
        reset_files_struct(displaced);
out_ret:
    return retval;
}

 

5. Copy On Write COW(寫時複製)技術 

fork產生新任務的速度非常快,因為fork並不複製原任務的記憶體空間,而是和原任務一起共享一個"寫時複製(Copy On Write)的記憶體空間",關於寫時複製,我們需要重點理解它的概念和存在的意義

1. 兩個任務(task)可以同時自由地讀取記憶體,但任意一個任務試圖對記憶體進行修改時,記憶體就會複製一份提供給修改方單獨使用,以免影響到其他的任務使用
2. 從產生的意義的角度來理解,寫時複製和動態連結庫的延遲繫結技術有異曲同工之妙,正常來說,執行了fork之後,作業系統應該將父程式(父任務)的記憶體空間複製一份給子程式,但是這個複製過程可能需要消耗較多的時間,而且在於子程式也並不一定會對這塊記憶體進行"寫操作",所以作業系統採用了一種"延遲複製"的思想,即等到子程式確實需要修改的時候再進行從父程式到子程式記憶體空間的複製
/*
fork()的實際開銷就是複製父程式的頁表以及給子程式建立唯一的程式描述符,在一般情況下,程式建立後都會馬上執行一個可執行的檔案(ELF檔案),這種優化可以避免拷貝大量根本就不會被使用的資料(地址空間裡常常包含數十兆的資料),這個技術大大加快的Linux程式建立的速度
*/

寫 入時複製(Copy-on-write)是一個被使用在程式設計領域的最佳化策略。其基礎的觀念是,如果有多個呼叫者(callers)同時要求相同資 源,他們會共同取得相同的指標指向相同的資源,直到某個呼叫者(caller)嘗試修改資源時,系統才會真正複製一個副本(private copy)給該呼叫者,以避免被修改的資源被直接察覺到,這過程對其他的呼叫者都是透明的(transparently)。此作法主要的優點是如果呼叫者 並沒有修改該資源,就不會有副本(private copy)被建立

寫時複製是“延遲計算(lazy  evaluation)”這一計算技術(evaluation  technique)的一個例子,記憶體管理器廣泛地使用了延遲計算的技術。延遲計算使得只有當絕對需要時才執行一個昂貴的操作--如果該操作從來也不需要 的話,則它不會浪費任何一點時間。

Relevant Link:

http://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD
http://cookies5000.blog.163.com/blog/static/995922052009223112797/
http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
http://www.programlife.net/copy-on-write.html

 

6. Linux下建立程式的7種API方式

#include <unistd.h>
1. int execl(const char *path, const char *arg, ...);
2. int execlp(const char *file, const char *arg, ...);
3. int execle(const char *path, const char *arg, ..., char *const envp[]);
4. int execv(const char *path, char *const argv[]);
5. int execvp(const char *file, char *const argv[]);
6. int execve(const char *path, char *const argv[], char *const envp[]);

這些都是用以執行一個可執行檔案的函式,它們統稱為"exec函式",它們的差異在於對命令列引數和環境變數引數的傳遞方式不同
以上函式的本質都是呼叫\arch\x86\kernel\process_32.c檔案中實現的系統呼叫sys_execve()來執行一個可執行檔案
exec系列函式共有7函式可供使用,這些函式的區別在於

1. 使用"路徑"指示新程式的位置
    1) 如果是使用檔名,則在系統的PATH環境變數所描述的路徑中搜尋該程式
2. 使用"檔名"指示新程式的位置
3. 使用引數列表的方式作為傳入引數
4. 使用argv[]陣列的方式傳入引數

0x1: int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );

execl()函式用來執行引數pathname字串所指向的程式,第二個及以後的引數代表執行檔案時傳遞的引數列表,最後一個引數必須是空指標以標誌引數列表為空.

//File: execl.c 
#include <unistd.h>

main()
{
    // 執行/bin目錄下的ls, 第一引數為程式名ls, 第二個引數為"-al", 第三個引數為"/etc/passwd"
    execl("/bin/ls", "ls", "-al", "/etc/passwd", (char *) 0);
    
    //最後一個引數傳入NULL也是可以的
    execl("/bin/ls", "ls", "-al", "/etc/", NULL);
}

0x2: int execv(const char *pathname, char *const argv[]);

execv()函式函式用來執行引數path字串所指向的程式,第二個為陣列指標維護的程式引數列表,該陣列的最後一個成員必須是空指標.

#include <unistd.h>

int main()
{
        char *argv[] = {"ls", "-l", "/etc", (char *)0};
        execv("/bin/ls", argv);
        return 0;
}

0x3: int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );

execle()函式用來執行引數pathname字串所指向的程式,第二個及以後的引數代表執行檔案時傳遞的引數列表,最後一個引數必須指向一個新的環境變數陣列,即新執行程式的環境變數.

#include <unistd.h>

int main(int argc, char *argv[], char *env[])
{
        execle("/bin/ls", "ls", "-l", "/etc", (char *)0,env);
        return 0;
}

0x4: int execve(const char *pathname, char *const argv[], char *const envp[]);

execve()用來執行引數filename字串所代表的檔案路徑,第二個引數是利用指標陣列來傳遞給執行檔案,並且需要以空指標(NULL)結束,最後一個引數則為傳遞給執行檔案的新環境變數陣列

#include<unistd.h>

main()
{
    char * argv[ ]={"ls", "-al", "/etc/passwd", (char *)0};
    char * envp[ ]={"PATH=/bin", 0};
    execve("/bin/ls", argv, envp);
}

0x5: int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );

execlp()函式會從PATH環境變數所指的目錄中查詢檔名為第一個引數filename指示的字串,找到後執行該檔案,第二個及以後的引數代表執行檔案時傳遞的引數列表,最後一個引數必須是空指標.

#include <unistd.h>

int main()
{
        execlp("ls", "ls", "-l", "/etc", (char *)0);
        return 0;
}

0x6: int execvp(const char *filename, char *const argv[]);

execvp()函式會從PATH環境變數所指的目錄中查詢檔名為第一個引數file指示的字串,找到後執行該檔案,第二個及以後的引數代表執行檔案時傳遞的引數列表,最後一個成員必須是空指標.

#include <unistd.h>

int main()
{
        char *argv[] = {"ls", "-l", "/etc", (char *)0};
        execvp("ls", argv);
        return 0;
}

0x7: int fexecve(int fd, char *const argv[], char *const envp[]);

fexecve()執行的任務與execve()相同,所不同的是執行的檔案通過檔案描述符fd指定,而不是通過路徑。檔案描述符fd必需以只讀方式開啟,並且呼叫者必需有執行相應檔案的許可權,在 Linux系統裡,fexecve()的實現使用了proc()檔案系統,所以 /proc 必需被掛載並在呼叫時可用
值得注意的是fexecve的開啟物件是檔案描述符(file discriptor fd),在Linux下,檔案描述符可以是通過open開啟的可執行檔案、也可以是通過父程式繼承的命名管道(named pipe)

//lgo.c 
#include <stdio.h> 
#include <unistd.h> 

int main(int argc, char **argv) 
{ 
    extern char **environ; 
    (void) argc; 
    fexecve(0, argv, environ); 
    perror("fexecve"); 
    return 1; 
} 

Relevant Link:

http://www.2cto.com/os/201410/342362.html
http://cpp.ezbty.org/import_doc/linux_manpage/fexecve.3.html
http://stackoverflow.com/questions/13690454/how-to-compile-and-execute-from-memory-directly
http://security.stackexchange.com/questions/20974/how-a-malware-executes-remote-payload

值得注意的是,glibc提供的這7種程式執行的API,只是起到一個適配轉接的作用,最終在內部都會呼叫到同一個函式"__execve"

\glibc-2.18\posix\execle.c\

/* Execute PATH with all arguments after PATH until a NULL pointer,
   and the argument after that for environment.  */
int execle (const char *path, const char *arg, ...)
{
#define INITIAL_ARGV_MAX 1024
  size_t argv_max = INITIAL_ARGV_MAX;
  const char *initial_argv[INITIAL_ARGV_MAX];
  const char **argv = initial_argv;
  va_list args;
  argv[0] = arg;

  va_start (args, arg);
  unsigned int i = 0;
  while (argv[i++] != NULL)
    {
      if (i == argv_max)
    {
      argv_max *= 2;
      const char **nptr = realloc (argv == initial_argv ? NULL : argv,
                       argv_max * sizeof (const char *));
      if (nptr == NULL)
        {
          if (argv != initial_argv)
        free (argv);
          return -1;
        }
      if (argv == initial_argv)
        /* We have to copy the already filled-in data ourselves.  */
        memcpy (nptr, argv, i * sizeof (const char *));

      argv = nptr;
    }

      argv[i] = va_arg (args, const char *);
    }

  const char *const *envp = va_arg (args, const char *const *);
  va_end (args);

  int ret = __execve (path, (char *const *) argv, (char *const *) envp);
  if (argv != initial_argv)
    free (argv);

  return ret;
}
libc_hidden_def (execle)

\glibc-2.18\sysdeps\unix\sysv\linux\execve.c

int __execve (file, argv, envp)
     const char *file;
     char *const argv[];
     char *const envp[];
{
  return INLINE_SYSCALL (execve, 3, file, argv, envp);
}
weak_alias (__execve, execve)

 

7. Glibc execve、fork API原始碼分析

0x1: execve

\glibc-2.18\sysdeps\unix\sysv\linux\execve.c

int __execve (file, argv, envp)
     const char *file;
     char *const argv[];
     char *const envp[];
{
  return INLINE_SYSCALL (execve, 3, file, argv, envp);
}
weak_alias (__execve, execve)

0x2: fork

pid_t __libc_fork (void)
{
  pid_t pid;
  struct used_handler
  {
    struct fork_handler *handler;
    struct used_handler *next;
  } *allp = NULL;

  /* Run all the registered preparation handlers.  In reverse order.
     While doing this we build up a list of all the entries.  */
  struct fork_handler *runp;
  while ((runp = __fork_handlers) != NULL)
    {
      /* Make sure we read from the current RUNP pointer.  */
      atomic_full_barrier ();

      unsigned int oldval = runp->refcntr;

      if (oldval == 0)
    /* This means some other thread removed the list just after
       the pointer has been loaded.  Try again.  Either the list
       is empty or we can retry it.  */
    continue;

      /* Bump the reference counter.  */
      if (atomic_compare_and_exchange_bool_acq (&__fork_handlers->refcntr,
                        oldval + 1, oldval))
    /* The value changed, try again.  */
    continue;

      /* We bumped the reference counter for the first entry in the
     list.  That means that none of the following entries will
     just go away.  The unloading code works in the order of the
     list.

     While executing the registered handlers we are building a
     list of all the entries so that we can go backward later on.  */
      while (1)
    {
      /* Execute the handler if there is one.  */
      if (runp->prepare_handler != NULL)
        runp->prepare_handler ();

      /* Create a new element for the list.  */
      struct used_handler *newp
        = (struct used_handler *) alloca (sizeof (*newp));
      newp->handler = runp;
      newp->next = allp;
      allp = newp;

      /* Advance to the next handler.  */
      runp = runp->next;
      if (runp == NULL)
        break;

      /* Bump the reference counter for the next entry.  */
      atomic_increment (&runp->refcntr);
    }

      /* We are done.  */
      break;
    }

  _IO_list_lock ();

#ifndef NDEBUG
  pid_t ppid = THREAD_GETMEM (THREAD_SELF, tid);
#endif

  /* We need to prevent the getpid() code to update the PID field so
     that, if a signal arrives in the child very early and the signal
     handler uses getpid(), the value returned is correct.  */
  pid_t parentpid = THREAD_GETMEM (THREAD_SELF, pid);
  THREAD_SETMEM (THREAD_SELF, pid, -parentpid);

#ifdef ARCH_FORK
  pid = ARCH_FORK ();
#else
# error "ARCH_FORK must be defined so that the CLONE_SETTID flag is used"
  pid = INLINE_SYSCALL (fork, 0);
#endif


  if (pid == 0)
    {
      struct pthread *self = THREAD_SELF;

      assert (THREAD_GETMEM (self, tid) != ppid);

      if (__fork_generation_pointer != NULL)
    *__fork_generation_pointer += 4;

      /* Adjust the PID field for the new process.  */
      THREAD_SETMEM (self, pid, THREAD_GETMEM (self, tid));

#if HP_TIMING_AVAIL
      /* The CPU clock of the thread and process have to be set to zero.  */
      hp_timing_t now;
      HP_TIMING_NOW (now);
      THREAD_SETMEM (self, cpuclock_offset, now);
      GL(dl_cpuclock_offset) = now;
#endif

#ifdef __NR_set_robust_list
      /* Initialize the robust mutex list which has been reset during
     the fork.  We do not check for errors since if it fails here
     it failed at process start as well and noone could have used
     robust mutexes.  We also do not have to set
     self->robust_head.futex_offset since we inherit the correct
     value from the parent.  */
# ifdef SHARED
      if (__builtin_expect (__libc_pthread_functions_init, 0))
    PTHFCT_CALL (ptr_set_robust, (self));
# else
      extern __typeof (__nptl_set_robust) __nptl_set_robust
    __attribute__((weak));
      if (__builtin_expect (__nptl_set_robust != NULL, 0))
    __nptl_set_robust (self);
# endif
#endif

      /* Reset the file list.  These are recursive mutexes.  */
      fresetlockfiles ();

      /* Reset locks in the I/O code.  */
      _IO_list_resetlock ();

      /* Reset the lock the dynamic loader uses to protect its data.  */
      __rtld_lock_initialize (GL(dl_load_lock));

      /* Run the handlers registered for the child.  */
      while (allp != NULL)
    {
      if (allp->handler->child_handler != NULL)
        allp->handler->child_handler ();

      /* Note that we do not have to wake any possible waiter.
         This is the only thread in the new process.  The count
         may have been bumped up by other threads doing a fork.
         We reset it to 1, to avoid waiting for non-existing
         thread(s) to release the count.  */
      allp->handler->refcntr = 1;

      /* XXX We could at this point look through the object pool
         and mark all objects not on the __fork_handlers list as
         unused.  This is necessary in case the fork() happened
         while another thread called dlclose() and that call had
         to create a new list.  */

      allp = allp->next;
    }

      /* Initialize the fork lock.  */
      __fork_lock = LLL_LOCK_INITIALIZER;
    }
  else
    {
      assert (THREAD_GETMEM (THREAD_SELF, tid) == ppid);

      /* Restore the PID value.  */
      THREAD_SETMEM (THREAD_SELF, pid, parentpid);

      /* We execute this even if the 'fork' call failed.  */
      _IO_list_unlock ();

      /* Run the handlers registered for the parent.  */
      while (allp != NULL)
    {
      if (allp->handler->parent_handler != NULL)
        allp->handler->parent_handler ();

      if (atomic_decrement_and_test (&allp->handler->refcntr)
          && allp->handler->need_signal)
        lll_futex_wake (allp->handler->refcntr, 1, LLL_PRIVATE);

      allp = allp->next;
    }
    }

  return pid;
}
weak_alias (__libc_fork, __fork)
libc_hidden_def (__fork)
weak_alias (__libc_fork, fork)

 

8. 檢視程式的啟動過程工具

要想檢視程式的啟動過程,可以使用兩個工具: strace和LD_DEBUG

source:

#include <stdlib.h>  
#include <stdio.h>  
 
int main()
{  
    printf("hello world\n");  
    return 0;  
} 

編譯程式:

gcc -o hello -O2 hello.c 

strace -tt ./hello 

05:47:11.645477 execve("./hello", ["./hello"], [/* 38 vars */]) = 0
05:47:11.646521 brk(0)                  = 0x82f8000
05:47:11.646660 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77fd000
05:47:11.646745 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
05:47:11.646929 open("/etc/ld.so.cache", O_RDONLY) = 3
05:47:11.647012 fstat64(3, {st_mode=S_IFREG|0644, st_size=50450, ...}) = 0
05:47:11.647176 mmap2(NULL, 50450, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77f0000
05:47:11.647223 close(3)                = 0
05:47:11.647348 open("/lib/libc.so.6", O_RDONLY) = 3
05:47:11.647409 read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0@\356X\0004\0\0\0"..., 512) = 512
05:47:11.647496 fstat64(3, {st_mode=S_IFREG|0755, st_size=1906124, ...}) = 0
05:47:11.647605 mmap2(0x578000, 1665416, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x578000
05:47:11.647648 mprotect(0x708000, 4096, PROT_NONE) = 0
05:47:11.647693 mmap2(0x709000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x190) = 0x709000
05:47:11.647761 mmap2(0x70c000, 10632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x70c000
05:47:11.647819 close(3)                = 0
05:47:11.648707 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ef000
05:47:11.648797 set_thread_area({entry_number:-1 -> 6, base_addr:0xb77ef6c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, 
useable:1}) = 0 05:47:11.649201 mprotect(0x709000, 8192, PROT_READ) = 0 05:47:11.649272 mprotect(0x570000, 4096, PROT_READ) = 0 05:47:11.649326 munmap(0xb77f0000, 50450) = 0 05:47:11.649560 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 05:47:11.649678 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77fc000 05:47:11.649754 write(1, "hello world\n", 12hello world ) = 12 05:47:11.649829 exit_group(0) = ?

LD_DEBUG=libs ./hello 

     26605:    find library=libc.so.6 [0]; searching
     26605:     search cache=/etc/ld.so.cache
     26605:      trying file=/lib/libc.so.6
     26605:    
     26605:    
     26605:    calling init: /lib/libc.so.6
     26605:    
     26605:    
     26605:    initialize program: ./hello
     26605:    
     26605:    
     26605:    transferring control: ./hello
     26605:    
hello world
     26605:    
     26605:    calling fini: ./hello [0]
     26605:    
     26605:    
     26605:    calling fini: /lib/libc.so.6 [0]
     26605:    

 

9. Linux下執行緒建立

Linux下沒有像windows那樣明確的執行緒定義,或者從另一個角度來說,Linux下的執行緒更加切進"執行緒"本質的概念,即執行緒和程式的差別本質上是"資源共享程度"問題,程式和執行緒之間的界限從某種程度上來說也不應該是那麼明確,Linux下建立執行緒來如下幾種方法

1. fork()+execve: 需要共享的資源: CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
2. Linux POSIX Thread
3. LinuxThreads:was a partial implementation of POSIX Threads
4. Native POSIX Thread Library(NPTL)

需要明白的是,Linux下執行緒建立的最底層的核心級支援還是fork,fork提供了豐富的flog共享引數,以此提供"父子程式(執行緒)"不同程度的共享,POSIX執行緒以及基於POSIX實現的執行緒庫都是基於fork實現的,但是要注意的,Linux的執行緒不是簡單的包裝使用者空間的fork,而是一種基於核心級支援的執行緒庫實現

0x1: Linux POSIX Thread

POSIX(可移植作業系統介面)執行緒是提高程式碼響應和效能的有力手段

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

 void *thread_function(void *arg) 
 {
    int i;
    for ( i=0; i<20; i++) 
    {
        printf("Thread says hi!\n");
        sleep(1);
    }
    return NULL;
}

int main(void) 
{
    pthread_t mythread;

    if ( pthread_create( &mythread, NULL, thread_function, NULL) ) 
    {
        printf("error creating thread.");
        abort();
    }
    if ( pthread_join ( mythread, NULL ) ) 
    {
        printf("error joining thread.");
        abort();
    }
    exit(0);
}

我們知道,當用 fork() 建立另一個新程式時,新程式是子程式,原始程式是父程式。這建立了可能非常有用的層次關係,尤其是等待子程式終止時。例如,waitpid() 函式讓當前程式等待所有子程式終止。waitpid() 用來在父程式中實現簡單的清理過程
而 POSIX 執行緒中不存在這種層次關係。雖然主執行緒可以建立一個新執行緒,新執行緒可以建立另一個新執行緒,POSIX 執行緒標準將它們視為等同的層次。所以等待子執行緒退出的概念在這裡沒有意義 POSIX 執行緒標準不記錄任何"家族"資訊

缺少家族資訊有一個主要含意:如果要等待一個執行緒終止,就必須將執行緒的 tid 傳遞給 pthread_join()。執行緒庫無法為您斷定 tid 

Relevant Link:

http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/

0x2: LinuxThreads:was a partial implementation of POSIX Threads

In the Linux operating system, LinuxThreads was a partial implementation of POSIX Threads. It has since been superseded by the Native POSIX Thread Library (NPTL)

Relevant Link:

http://www.ibm.com/developerworks/cn/linux/l-threading.html
http://en.wikipedia.org/wiki/LinuxThreads

0x3: Native POSIX Thread Library(NPTL)

Native POSIX Thread Library(NPTL)是一個能夠使使用POSIX Threads編寫的程式在Linux核心上更有效地執行的軟體
NPTL的解決方法與LinuxThreads類似,核心看到的首要抽象依然是一個程式,新執行緒是通過clone()系統呼叫產生的。但是NPTL需要特殊的核心支援來解決同步的原始型別之間互相競爭的狀況。在這種情況下執行緒必須能夠入眠和再復甦。用來完成這個任務的原始型別叫做futex
NPTL是一個所謂的"1 x 1執行緒函式庫"。使用者產生的執行緒與核心能夠分配的物件之間的聯絡是一對一的。這是所有執行緒程式中最簡單的

getconf GNU_LIBPTHREAD_VERSION

Relevant Link:

http://zh.wikipedia.org/wiki/Native_POSIX_Thread_Library

 

10. Posix執行緒

0x1: 執行緒建立

1. 執行緒與程式

相對程式而言,執行緒是一個更加接近於執行體的概念,它可以與同程式中的其他執行緒共享資料,但擁有自己的棧空間,擁有獨立的執行序列。在序列程式基礎上引入執行緒和程式是為了提高程式的併發度,從而提高程式執行效率和響應時間。對於Linux來說,執行緒其實是一個偽概念

1. Linux下的所有新建程式都是通過父程式"複製"出來的,父子程式之間可以實現不同程度的資源共享
2. 父子程式本來就是不同的程式,有自己的棧空間、執行序列是顯然的
3. Linux上區分程式、執行緒只是通過clone_flags,即資源共享程度來定義區分的

執行緒和程式在使用上各有優缺點:執行緒執行開銷小,但不利於資源的管理和保護;而程式正相反。同時,執行緒適合於在SMP機器上執行,而程式則可以跨機器遷移

2. 建立執行緒

POSIX通過pthread_create()函式建立執行緒,API定義如下

int  pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);

與fork()呼叫建立一個程式的方法不同

1. pthread_create()建立的執行緒並不具備與主執行緒(即呼叫pthread_create()的執行緒)同樣的執行序列,而是使其執行start_routine(arg)函式
2. thread引數接收返回建立的執行緒ID
3. attr引數返回的是建立執行緒時設定的執行緒屬性
4. pthread_create()的返回值表示執行緒建立是否成功
5. 儘管arg是void *型別的變數,但它同樣可以作為任意型別的引數傳給start_routine()函式
6. start_routine()可以返回一個void *型別的返回值,而這個返回值也可以是其他型別,並由pthread_join()獲取

我們從更本質的角度來看Linux下POSIX執行緒庫的執行緒建立

1. Linux中呼叫fork、clone系統呼叫都可以產生新程式,而在這兩個系統呼叫內部都是呼叫的do_fork()核心函式實現的,只是所傳的引數不同
2. 而POSIX執行緒庫本質上是對clone系統呼叫的封裝,對pthread_create程式碼進行strace可以看到
/*clone(child_stack=0x7fe4b92daff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fe4b92db9d0, tls=0x7fe4b92db700, child_tidptr=0x7fe4b92db9d0) = 4634
*/
3. fork和clone在這裡的區別在於
    1) fork不能修改複製出來的子程式的棧空間,所以只能複製一份"完整的"父程式
    2) clone允許修改子程式的棧空間,所以允許建立任意入口函式的子程式,即我們可以線上程中部署不同的我們所需要的任務函式

3. 執行緒建立屬性

pthread_create()中的attr引數是一個結構指標,結構中的元素分別對應著新執行緒的執行屬性,主要包括以下幾項

typedef struct
{
    /*
    執行緒的分離狀態
    detachstate表示新執行緒是否與程式中其他執行緒脫離同步
    1. 如果置位則新執行緒不能用pthread_join()來同步,且在退出時自行釋放所佔用的資源。預設為PTHREAD_CREATE_JOINABLE狀態
    2. 這個屬性也可以線上程建立並執行以後用pthread_detach()來設定
    3. 一旦設定為PTHREAD_CREATE_DETACH狀態(不論是建立時設定還是執行時設定)則不能再恢復到PTHREAD_CREATE_JOINABLE狀態 
    */
    int detachstate;  
    
    /*
    執行緒排程策略
    schedpolicy表示新執行緒的排程策略,主要包括
    1. SCHED_OTHER(正常、非實時): 預設為SCHED_OTHER
    2. SCHED_RR(實時、輪轉法): 僅對超級使用者有效
    3. SCHED_FIFO(實時、先入先出): 僅對超級使用者有效
    執行時可以用過pthread_setschedparam()來改變 
    */
    int schedpolicy;   
    
    /*
    執行緒的排程引數
    schedparam是一個struct sched_param結構,目前僅有一個sched_priority整型變數表示執行緒的執行優先順序。這個引數僅當排程策略為實時(即SCHED_RR或SCHED_FIFO)時才有效
    並可以在執行時通過pthread_setschedparam()函式來改變,預設為0
    */
    struct sched_param schedparam;  
    inheritsched有兩種值可供選擇:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新執行緒使用顯式指定排程策略和排程引數(即attr中的值),而後者表示繼承呼叫者執行緒的值。預設為PTHREAD_EXPLICIT_SCHED。

    //執行緒的繼承性
    int inheritsched;   
    
    /*
    執行緒的作用域
    scope表示執行緒間競爭CPU的範圍,也就是說執行緒優先順序的有效範圍。POSIX的標準中定義了兩個值
    1. PTHREAD_SCOPE_SYSTEM: 表示與系統中所有執行緒一起競爭CPU時間,目前LinuxThreads僅實現了PTHREAD_SCOPE_SYSTEM一值
    2. PTHREAD_SCOPE_PROCESS: 表示僅與同程式中的執行緒競爭CPU 
    */
    int scope;            
    size_t    guardsize;        //執行緒棧末尾的警戒緩衝區大小
    int stackaddr_set;
    void * stackaddr;        //執行緒棧的位置
    size_t stacksize;        //執行緒棧的大小
}pthread_attr_t;

pthread_attr_t結構中還有一些值,但不使用pthread_create()來設定,為了設定這些屬性,POSIX定義了一系列屬性設定函式,包括

1. pthread_attr_init()
2. pthread_attr_destroy()
3. 與各個屬性相關的pthread_attr_getxxx / pthread_attr_setxxx函式

4. 執行緒建立的Linux POSIX實現

我們知道,Linux的執行緒實現是核心外進行的,核心內提供的是建立程式的介面do_fork()。核心提供了兩個系統呼叫__clone()和fork(),最終都用不同的引數呼叫do_fork()核內API。當然,要想實現執行緒,沒有核心對多程式(其實是輕量級程式)共享資料段的支援是不行的,因此,do_fork()提供了很多引數,包括

1. CLONE_VM(共享記憶體空間)
2. CLONE_FS(共享檔案系統資訊)
3. CLONE_FILES(共享檔案描述符表)
4. CLONE_SIGHAND(共享訊號控制程式碼表)
5. CLONE_PID(共享程式ID,僅對核內程式,即0號程式有效)

當使用fork系統呼叫時,核心呼叫do_fork()不使用任何共享屬性,程式擁有獨立的執行環境
而使用pthread_create()來建立執行緒時,則最終設定了所有這些屬性來呼叫__clone(),而這些引數又全部傳給核內的do_fork(),從而建立的"程式"擁有共享的執行環境,只有棧是獨立的
Linux執行緒在核內是以輕量級程式的形式存在的,擁有獨立的程式表項,而所有的建立、同步、刪除等操作都在核外pthread庫中進行。pthread庫使用一個管理執行緒(__pthread_manager(),每個程式獨立且唯一)來管理執行緒的建立和終止,為執行緒分配執行緒ID,傳送執行緒相關的訊號(比如Cancel),而主執行緒(pthread_create())的呼叫者則通過管道將請求資訊傳給管理執行緒。這在JVM For Linux上的實現也是類似的原理

0x2: 執行緒取消

1. 執行緒取消的定義

一般情況下,執行緒在其主體函式退出的時候會自動終止,但同時也可以因為接收到另一個執行緒發來的終止(取消)請求而強制終止

2. 執行緒取消的語義

執行緒取消的方法是向目標執行緒發Cancel訊號,但如何處理Cancel訊號則由目標執行緒自己決定

1. 或者忽略
2. 或者立即終止
3. 或者繼續執行至Cancelation-point(取消點),由不同的Cancelation狀態決定 

執行緒接收到CANCEL訊號的預設處理(即pthread_create()建立執行緒的預設狀態)是繼續執行至取消點,也就是說設定一個CANCELED狀態,執行緒繼續執行,只有執行至Cancelation-point的時候才會退出

3. 取消點

根據POSIX標準,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函式以及read()、write()等會引起阻塞的系統呼叫都是Cancelation-point,而其他pthread函式都不會引起Cancelation動作,但是CANCEL訊號會使執行緒從阻塞的系統呼叫中退出,並置EINTR錯誤碼,因此可以在需要作為Cancelation-point的系統呼叫前後呼叫pthread_testcancel(),從而達到POSIX標準所要求的目標,即如下程式碼段

pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();

4. 程式設計方面的考慮

如果執行緒處於無限迴圈中,且迴圈體內沒有執行至取消點的必然路徑,則執行緒無法由外部其他執行緒的取消請求而終止。因此在這樣的迴圈體的必經路徑上應該加入pthread_testcancel()呼叫

5 與執行緒取消相關的pthread函式

1. int pthread_cancel(pthread_t thread);
傳送終止訊號給thread執行緒,如果成功則返回0,否則為非0值。傳送成功並不意味著thread會終止

2. int pthread_setcancelstate(int state, int *oldstate);
設定本執行緒對Cancel訊號的反應,state有兩種值
    1) PTHREAD_CANCEL_ENABLE(預設): 收到訊號後設為CANCLED狀態
    2) PTHREAD_CANCEL_DISABLE: 收到訊號後忽略CANCEL訊號繼續執行
old_state如果不為NULL則存入原來的Cancel狀態以便恢復

3. int pthread_setcanceltype(int type, int *oldtype);
設定本執行緒取消動作的執行時機,type由兩種取值
    1) PTHREAD_CANCEL_DEFFERED: 收到訊號後繼續執行至下一個取消點再退出
    2) PTHREAD_CANCEL_ASYCHRONOUS: 收到訊號後繼續立即執行取消動作(退出),僅當Cancel狀態為Enable時有效
oldtype如果不為NULL則存入運來的取消動作型別值

4. void pthread_testcancel(void);
檢查本執行緒是否處於Canceld狀態,如果是,則進行取消動作,否則直接返回 

0x3: 通過FLAG判斷當前是否為執行緒新建

包括pthread_create建立的執行緒,或者是其他任何POSIX庫、包括原生的建立執行緒的過程。其本質都是都是通過clone系統呼叫建立的

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

flags:
1. CLONE_CHILD_CLEARTID 
2. CLONE_CHILD_SETTID 
3. CLONE_FILES 
4. CLONE_FS 
5. CLONE_IO 
6. CLONE_NEWIPC 
7. CLONE_NEWNET 
8. CLONE_NEWNS 
9. CLONE_NEWPID 
10. CLONE_NEWUTS 
11. CLONE_PARENT 
12. CLONE_PARENT_SETTID 
13. CLONE_PID 
14. CLONE_PTRACE 
15. CLONE_SETTLS 
16. CLONE_SIGHAND: 共享訊號
17. CLONE_STOPPED 
18. CLONE_SYSVSEM 
19. CLONE_THREAD: 宣告所有的同一程式的執行緒要在一個執行緒組裡面了
20. CLONE_UNTRACED
21. CLONE_VFORK
22. CLONE_VM: 共享虛擬記憶體空間

從最基本的角度來看,要構成一個執行緒的要求

1. 一個執行緒和同一程式的別的執行緒必須共享地址空間
2. 按照POSIX的約定,執行緒們必須共享訊號,因此,CLONE_SIGHAND也是必須的
3. 所有的同一程式的執行緒要在一個執行緒組裡面了,因此CLONE_THREAD也是必須的

/*
從man手冊可以看出,CLONE_THREAD的設定要求CLONE_SIGHAND被設定,而CLONE_SIGHAND的設定要求CLONE_VM被設定,在核心的copy_process函式裡面有: 
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) 
                 return ERR_PTR(-EINVAL); 
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) 
                 return ERR_PTR(-EINVAL); 
*/

通過判斷傳入的clone_flags引數,可以判斷出當前是建立程式還是建立執行緒

CLONE_VM | CLONE_THREAD | CLONE_SIGHAND
//只要同時出現這3個FLAG,說明此時正在進行執行緒建立

Relevant Link:

https://www.ibm.com/developerworks/cn/linux/thread/posix_threadapi/part1/ 

 

Copyright (c) 2014 LittleHann All rights reserved

 

相關文章