自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux內(nèi)核代碼追蹤:如何“分裂”出一個新進程的?

系統(tǒng) Linux
隨著 Linux 操作系統(tǒng)的不斷發(fā)展和演進,fork在不同版本中也經(jīng)歷了一系列的優(yōu)化改進 ,以提升性能和資源管理效率 。

在生活中,我們經(jīng)常會進行文件復制操作,比如將一份重要的文檔復制到多個文件夾,以方便在不同場景下使用,每個復制后的文件都擁有獨立的存儲空間,但內(nèi)容最初與原文件一致。在生物學領域,克隆技術也是一種復制,克隆羊多莉就是通過復制母體的遺傳物質(zhì)誕生,擁有和母體幾乎相同的基因。而在 Linux 操作系統(tǒng)中,也存在類似的 “復制” 概念,那就是進程復制,其中fork函數(shù)便是實現(xiàn)進程復制的核心,它如同一個神奇的 “分身術”,讓一個進程能夠創(chuàng)建出與自身幾乎一模一樣的子進程 ,它們是如何實現(xiàn)的。

我們主要聊聊從glibc庫進入內(nèi)核,再從內(nèi)核出來的情景。為了方便期間,我們的硬件平臺為arm,linux內(nèi)核為3.18.3,glibc庫版本為2.20,可從http://ftp.gnu.org/gnu/glibc/下載源碼。接下來,讓我們一起深入探索。

一、Glibc到kernel

我們設定硬件平臺為arm,glibc庫版本為2.20,因為不同的CPU體系結(jié)構中,glibc庫通過系統(tǒng)調(diào)用進入kernel庫的方法是不一樣的。當glibc準備進入kernel時,流程如下:

/* glibc最后會調(diào)用到一個INLINE_SYSCALL宏,參數(shù)如下 */
INLINE_SYSCALL (clone, 5, CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid);

 /* INLINE_SYSCALL的宏定義如下,可以看出在INLINE_SYSCALL宏中又使用到了INTERNAL_SYSCALL宏,而INTERNAL_SYSCALL宏最終會調(diào)用INTERNAL_SYSCALL_RAW */
#define INLINE_SYSCALL(name, nr, args...) \
  ({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
     if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \
       { \
     __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
     _sys_result = (unsigned int) -1; \
       } \
     (int) _sys_result; })

 /* 為了方便大家理解,將此宏寫為偽代碼形式 */
 int INLINE_SYSCALL (name, nr, args...)
 {
    unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args);

    if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) {
        __set_error (INTERNAL_SYSCALL_ERRNO (_sys_result, ));
        _sys_result = (unsigned int) -1;
    }
    return (int)_sys_result;
 }

/* 這里我們不需要看INTERNAL_SYSCALL宏,只需要看其最終調(diào)用的INTERNAL_SYSCALL_RAW宏,需要注意的是,INTERNAL_SYSCALL調(diào)用INTERNAL_SYSCALL_RAW時,通過SYS_ify(name)宏將name轉(zhuǎn)為了系統(tǒng)調(diào)用號
 * name: 120(通過SYS_ify(name)宏已經(jīng)將clone轉(zhuǎn)為了系統(tǒng)調(diào)用號120)
 * err: NULL
 * nr: 5
 * args[0]: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
 * args[1]: NULL
 * args[2]: NULL
 * args[3]: NULL
 * args[4]: &THREAD_SELF->tid
  */
 # define INTERNAL_SYSCALL_RAW(name, err, nr, args...)     \
  ({     \
       register int _a1 asm ("r0"), _nr asm ("r7");     \
       LOAD_ARGS_##nr (args)     \
       _nr = name;     \
       asm volatile ("swi    0x0    @ syscall " #name    \
    : "=r" (_a1)     \
    : "r" (_nr) ASM_ARGS_##nr     \
    : "memory");     \
       _a1; })
 #endif

INTERNAL_SYSCALL_RAW實現(xiàn)的結(jié)果就是將args[0]存到了r0...args[4]存到了r4中,并將name(120)綁定到r7寄存器。然后通過swi 0x0指令進行了軟中斷。0x0是一個24位的立即數(shù),用于軟中斷執(zhí)行程序判斷執(zhí)行什么操作。當執(zhí)行這條指令時,CPU會跳轉(zhuǎn)至中斷向量表的軟中斷指令處,執(zhí)行該處保存的調(diào)用函數(shù),而在函數(shù)中會根據(jù)swi后面的24位立即數(shù)(在我們的例子中是0x0)執(zhí)行不同操作。在這時候CPU已經(jīng)處于保護模式,陷入內(nèi)核中?,F(xiàn)在進入到linux內(nèi)核中后,具體看此時內(nèi)核是怎么操作的吧。

/* 源文件地址: 內(nèi)核目錄/arch/arm/kernel/entry-common.S */

ENTRY(vector_swi)
    /*
     * 保存現(xiàn)場
     */
#ifdef CONFIG_CPU_V7M
    v7m_exception_entry
#else
    sub    sp, sp, #S_FRAME_SIZE
    stmia    sp, {r0 - r12}            @ 將r0~r12保存到棧中
 ARM(    add    r8, sp, #S_PC        )
 ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr
 THUMB(    mov    r8, sp            )
 THUMB(    store_user_sp_lr r8, r10, S_SP    )    @ calling sp, lr
    mrs    r8, spsr            @ called from non-FIQ mode, so ok.
    str    lr, [sp, #S_PC]            @ Save calling PC
    str    r8, [sp, #S_PSR]        @ Save CPSR
    str    r0, [sp, #S_OLD_R0]        @ Save OLD_R0
#endif
    zero_fp
    alignment_trap r10, ip, __cr_alignment
    enable_irq
    ct_user_exit
    get_thread_info tsk

    /*
     * 以下代碼根據(jù)不同arm體系結(jié)構獲取系統(tǒng)調(diào)用號
     */

#if defined(CONFIG_OABI_COMPAT)

    /*
     * 如果內(nèi)核配置了OABI兼容選項,會先判斷是否為THUMB,以下為THUMB情況(我們分析的時候可以忽略這段,一般情況是不走這一段的)
     */
#ifdef CONFIG_ARM_THUMB
    tst    r8, #PSR_T_BIT
    movne    r10, #0                @ no thumb OABI emulation
 USER(    ldreq    r10, [lr, #-4]        )    @ get SWI instruction
#else
 USER(    ldr    r10, [lr, #-4]        )    @ get SWI instruction
#endif
 ARM_BE8(rev    r10, r10)            @ little endian instruction

#elif defined(CONFIG_AEABI)

    /*
     * 我們主要看這里,EABI將系統(tǒng)調(diào)用號保存在r7中
     */
#elif defined(CONFIG_ARM_THUMB)
    /* 先判斷是否為THUMB模式 */
    tst    r8, #PSR_T_BIT
    addne    scno, r7, #__NR_SYSCALL_BASE
 USER(    ldreq    scno, [lr, #-4]        )

#else
    /* EABI模式 */
 USER(    ldr    scno, [lr, #-4]        )    @ 獲取系統(tǒng)調(diào)用號
#endif

    adr    tbl, sys_call_table        @ tbl為r8,這里是將sys_call_table的地址(相對于此指令的偏移量)存入r8

#if defined(CONFIG_OABI_COMPAT)
    /*
     * 在EABI體系中,如果swi跟著的立即數(shù)為0,這段代碼不做處理,而如果是old abi體系,則根據(jù)系統(tǒng)調(diào)用號調(diào)用old abi體系的系統(tǒng)調(diào)用表(sys_oabi_call_table)
     * 其實說白了,在EABI體系中,系統(tǒng)調(diào)用時使用swi 0x0進行軟中斷,r7寄存器保存系統(tǒng)調(diào)用號
     * 而old abi體系中,是通過swi (系統(tǒng)調(diào)用號|magic)進行調(diào)用的
     */
    bics    r10, r10, #0xff000000
    eorne    scno, r10, #__NR_OABI_SYSCALL_BASE
    ldrne    tbl, =sys_oabi_call_table
#elif !defined(CONFIG_AEABI)
    bic    scno, scno, #0xff000000
    eor    scno, scno, #__NR_SYSCALL_BASE
#endif

local_restart:
    ldr    r10, [tsk, #TI_FLAGS]        @ 檢查系統(tǒng)調(diào)用跟蹤
    stmdb     {r4, r5}            @ 將第5和第6個參數(shù)壓入棧

    tst    r10, #_TIF_SYSCALL_WORK        @ 判斷是否在跟蹤系統(tǒng)調(diào)用
    bne    __sys_trace

    cmp    scno, #NR_syscalls        @ 檢測系統(tǒng)調(diào)用號是否在范圍內(nèi),NR_syscalls保存系統(tǒng)調(diào)用總數(shù)
    adr    lr, BSYM(ret_fast_syscall)    @ 將返回地址保存到lr寄存器中,lr寄存器是用于函數(shù)返回的。
    ldrcc    pc, [tbl, scno, lsl #2]        @ 調(diào)用相應系統(tǒng)調(diào)用例程,tbl(r8)保存著系統(tǒng)調(diào)用表(sys_call_table)地址,scno(r7)保存著系統(tǒng)調(diào)用號120,這里就轉(zhuǎn)到相應的處理例程上了。

    add    r1, sp, #S_OFF
2:    cmp    scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
    eor    r0, scno, #__NR_SYSCALL_BASE    @ put OS number back
    bcs    arm_syscall
    mov    why, #0                @ no longer a real syscall
    b    sys_ni_syscall            @ not private func

#if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
    /*
     * We failed to handle a fault trying to access the page
     * containing the swi instruction, but we're not really in a
     * position to return -EFAULT. Instead, return back to the
     * instruction and re-enter the user fault handling path trying
     * to page it in. This will likely result in sending SEGV to the
     * current task.
     */
9001:
    sub    lr, lr, #4
    str    lr, [sp, #S_PC]
    b    ret_fast_syscall
#endif
ENDPROC(vector_swi)            @ 返回

好的,終于跳轉(zhuǎn)到了系統(tǒng)調(diào)用表,現(xiàn)在我們看看系統(tǒng)調(diào)用表是怎么樣的一個形式

/* 文件地址: linux內(nèi)核目錄/arch/arm/kernel/calls.S */

/* 0 */        CALL(sys_restart_syscall)
        CALL(sys_exit)
        CALL(sys_fork)
        CALL(sys_read)
        CALL(sys_write)
/* 5 */        CALL(sys_open)
        CALL(sys_close)
        CALL(sys_ni_syscall)        /* was sys_waitpid */
        CALL(sys_creat)
        CALL(sys_link)
/* 10 */    CALL(sys_unlink)
        CALL(sys_execve)
        CALL(sys_chdir)
        CALL(OBSOLETE(sys_time))    /* used by libc4 */
        CALL(sys_mknod)
/* 15 */    CALL(sys_chmod)
        CALL(sys_lchown16)
        CALL(sys_ni_syscall)        /* was sys_break */
        CALL(sys_ni_syscall)        /* was sys_stat */
        CALL(sys_lseek)
/* 20 */    CALL(sys_getpid)
        CALL(sys_mount)
        CALL(OBSOLETE(sys_oldumount))    /* used by libc4 */
        CALL(sys_setuid16)
        CALL(sys_getuid16)
/* 25 */    CALL(OBSOLETE(sys_stime))
        CALL(sys_ptrace)
        CALL(OBSOLETE(sys_alarm))    /* used by libc4 */
        CALL(sys_ni_syscall)        /* was sys_fstat */
        CALL(sys_pause)

        ......................
        ......................

/* 120 */    CALL(sys_clone)        /* 120在此,之前傳進來的系統(tǒng)調(diào)用號120進入內(nèi)核后會到這 */
        CALL(sys_setdomainname)
        CALL(sys_newuname)
        CALL(sys_ni_syscall)        /* modify_ldt */
        CALL(sys_adjtimex)
/* 125 */    CALL(sys_mprotect)
        CALL(sys_sigprocmask)
        CALL(sys_ni_syscall)        /* was sys_create_module */
        CALL(sys_init_module)
        CALL(sys_delete_module)

        ......................
        ......................

/* 375 */    CALL(sys_setns)
        CALL(sys_process_vm_readv)
        CALL(sys_process_vm_writev)
        CALL(sys_kcmp)
        CALL(sys_finit_module)
/* 380 */    CALL(sys_sched_setattr)
        CALL(sys_sched_getattr)
        CALL(sys_renameat2)
        CALL(sys_seccomp)
        CALL(sys_getrandom)
/* 385 */    CALL(sys_memfd_create)
        CALL(sys_bpf)
#ifndef syscalls_counted
.equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
#define syscalls_counted
#endif
.rept syscalls_padding
        CALL(sys_ni_syscall)
.endr

CALL為一個宏,而我們使用的那一行CALL(sys_clone)配合ldrcc pc,[tbl,scno,lsl #2]使用的結(jié)果就是把sys_clone的地址放入pc寄存器。具體我們仔細分析一下,首先先看看CALL宏展開,然后把CALL代入ldrcc,結(jié)果就很清晰了

/* CALL(x)宏展開 */
#define CALL(x) .equ NR_syscalls,NR_syscalls+1
#include "calls.S"

.ifne NR_syscalls - __NR_syscalls
.error "__NR_syscalls is not equal to the size of the syscall table"
.endif

/* 主要是后面這一段,
 * 上面一段主要用于統(tǒng)計系統(tǒng)調(diào)用數(shù)量,并將數(shù)量保存到NR_syscalls中,具體實現(xiàn)說明可以參考http://www.tuicool.com/articles/QFj6zq
 */

#undef CALL
/* 其實就是生成一個數(shù)為x,相當于.long sys_clone,因為sys_clone是函數(shù)名,所以.long生成的是sys_clone函數(shù)名對應的地址 */
#define CALL(x) .long x

#ifdef CONFIG_FUNCTION_TRACER


/* 配合ldrcc一起看,原來ldrcc是這樣 */
ldrcc    pc, [tbl, scno, lsl #2]

/* 把CALL(x)代入ldrcc,最后是這樣 */
ldrcc    pc, sys_clone(函數(shù)地址)

清楚的看出來,ldrcc最后是將sys_clone的函數(shù)地址存入了pc寄存器,而sys_clone函數(shù)內(nèi)核是怎么定義的呢,如下:

/* 文件地址: linux內(nèi)核目錄/kernel/Fork.c */

/* 以下代碼根據(jù)不同的內(nèi)核配置定義了不同的clone函數(shù)
 * 其最終都調(diào)用的do_fork函數(shù),我們先看看SYSCALL_DEFINE是怎么實現(xiàn)的吧,實現(xiàn)在此代碼片段后面
 */
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int, tls_val,
         int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
        int, stack_size,
        int __user *, parent_tidptr,
        int __user *, child_tidptr,
        int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#endif
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}


 /************************************************
 * 我是代碼分界線
 ************************************************/

/* 文件地址: linux內(nèi)核目錄/include/linux.h */

#define SYSCALL_DEFINE0(sname) \
    SYSCALL_METADATA(_##sname, 0); \
    asmlinkage long sys_##sname(void)

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...) \
    SYSCALL_METADATA(sname, x, __VA_ARGS__) \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx

可以看出系統(tǒng)調(diào)用是使用SYSCALL_DEFINEx進行定義的,以我們的例子,實際上最后clone函數(shù)被定義為:

/* 展開前 */

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#endif
{
    /* 應用層默認fork參數(shù)(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid) */
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

 /* 展開后 */

asmlinkage long sys_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);
}

終于看到最后系統(tǒng)會調(diào)用do_fork函數(shù)進行操作,接下來我們看看do_fork函數(shù)

/* 應用層的fork最后會通過sys_clone系統(tǒng)調(diào)用調(diào)用到此函數(shù) */
/* 應用層默認fork參數(shù)(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid)
 * clone_flags: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
 * stack_start: NULL
 * stack_size: NULL
 * parent_tidptr: NULL
 * child_tidptr: &THREAD_SELF->tid
 * pid: NULL
 */
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;

    /* 判斷是否進行跟蹤 */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    /* 調(diào)用copy_process進行初始化,返回初始化好的struct task_struct結(jié)構體,當我們調(diào)用fork時返回兩次的原因也是在這個函數(shù)當中,下回分析 */
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);


    if (!IS_ERR(p)) {
        /* 創(chuàng)建成功 */
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        /* 獲取子進程PID */
        pid = get_task_pid(p, PIDTYPE_PID);
        /* 返回子進程pid所屬的命名空間所看到的局部PID */
        nr = pid_vnr(pid);

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

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        /* 將新進程加入到CPU的運行隊列中 */
        wake_up_new_task(p);

        /* 跟蹤才會用到 */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        /* 如果是vfork調(diào)用,則在此等待vfork的進程結(jié)束 */
        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        /* 創(chuàng)建失敗 */
        nr = PTR_ERR(p);
    }
        /* 返回新進程PID(新進程在這會返回0) */
    return nr;
}

在do_fork函數(shù)中,首先會根據(jù)clone_flags判斷是否對父進程進行了跟蹤(調(diào)試使用),如果進行了函數(shù)跟蹤(還需要判斷是否對子進程進行跟蹤),之后調(diào)用copy_process(do_fork的核心函數(shù),之后的文章會對它進行分析),在copy_process中會對子進程的許多結(jié)構體和參數(shù)進行初始化(同時在fork正常情況中為什么會返回兩次也是在此函數(shù)中實現(xiàn)的),do_fork最后就判斷是否是通過vfork創(chuàng)建,如果是vfork創(chuàng)建,則會使父進程阻塞直到子進程結(jié)束釋放所占內(nèi)存空間后才繼續(xù)執(zhí)行,最后do_fork子進程pid。

到這里,整個系統(tǒng)調(diào)用的入口就分析完了,其實整個流程也不算很復雜:應用層通過swi軟中斷進入內(nèi)核---->通過系統(tǒng)調(diào)用表選定目標系統(tǒng)調(diào)用--->執(zhí)行系統(tǒng)調(diào)用--->返回。

二、fork的基礎概念

2.1 fork 是什么

在 Linux 系統(tǒng)中,fork是一個系統(tǒng)調(diào)用,用于創(chuàng)建一個新的進程,這個新進程被稱為子進程,而調(diào)用fork的進程則是父進程 。fork函數(shù)就像是一把神奇的 “叉子”,將一個進程 “分叉” 成兩個,這兩個進程(父進程和子進程)從fork調(diào)用之后的代碼開始,各自獨立執(zhí)行,就像兩條從同一節(jié)點出發(fā)的不同路徑,后續(xù)的走向可能截然不同 。例如,一個負責數(shù)據(jù)處理的父進程,調(diào)用fork后,子進程可以繼承父進程的數(shù)據(jù)讀取部分,然后去執(zhí)行數(shù)據(jù)分析,而父進程繼續(xù)進行數(shù)據(jù)的收集工作 ,兩者相互協(xié)作又互不干擾。

2.2 fork 函數(shù)返回值的奧秘

fork函數(shù)的一個獨特之處在于它 “一次調(diào)用,兩次返回” 。當fork被調(diào)用后,操作系統(tǒng)會創(chuàng)建出子進程,然后在父進程和子進程中分別返回不同的值 。在父進程中,fork返回子進程的進程 ID(PID,是一個大于 0 的整數(shù)),這個 ID 就像是子進程的 “身份證號”,父進程可以通過它來識別和管理子進程 ;而在子進程中,fork返回 0,就好像在告訴子進程:“你是新創(chuàng)建的子進程” 。

我們通過一段簡單的 C 語言代碼來直觀感受一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;

    // 調(diào)用fork函數(shù)
    pid = fork();

    if (pid < 0) {
        // fork失敗
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子進程
        printf("I am the child process, my pid is %d, and my parent's pid is %d\n", getpid(), getppid());
    } else {
        // 父進程
        printf("I am the parent process, my pid is %d, and my child's pid is %d\n", getpid(), pid);
    }

    return 0;
}

在上述代碼中,fork函數(shù)執(zhí)行后,父進程會打印出自己的 PID 以及子進程的 PID,而子進程會打印出自己的 PID 和父進程的 PID 。通過返回值的不同,父進程和子進程能夠清晰地知道自己的 “身份”,進而執(zhí)行不同的代碼邏輯 。

三、fork工作原理

3.1進程的關鍵數(shù)據(jù)結(jié)構

在深入探討fork原理之前,我們需要先了解一些與進程密切相關的數(shù)據(jù)結(jié)構,它們是理解fork實現(xiàn)的關鍵 。

task_struct:這是進程描述符,就像是進程的 “身份證”,每個進程在內(nèi)核中都有一個對應的task_struct結(jié)構體 。它記錄了進程的所有相關信息,包括進程 ID(PID)、進程狀態(tài)(是運行態(tài)、就緒態(tài)還是阻塞態(tài)等)、內(nèi)存映射區(qū)域、文件描述符表、信號處理函數(shù)表等 。例如,通過task_struct中的 PID,系統(tǒng)可以唯一標識和區(qū)分不同的進程,就像每個人的身份證號是獨一無二的 ;而進程狀態(tài)信息則幫助內(nèi)核決定該進程何時可以獲得 CPU 資源,是馬上運行,還是需要等待某些條件滿足 。

mm_struct:進程內(nèi)存管理描述符,主要管理每個進程的虛擬內(nèi)存和物理內(nèi)存 。它包含了虛擬內(nèi)存區(qū)域的信息,以及內(nèi)存映射的相關設置等 。比如,當進程申請內(nèi)存時,mm_struct會參與管理內(nèi)存的分配,決定從哪里分配虛擬內(nèi)存,以及如何與物理內(nèi)存進行映射 。它就像是一個內(nèi)存管家,統(tǒng)籌著進程內(nèi)存的使用 。

vm_area_struct:虛擬內(nèi)存描述符,用于描述一個進程的虛擬內(nèi)存區(qū)域,包括起始和結(jié)束地址、訪問權限(是可讀、可寫還是可執(zhí)行)、映射的物理頁框號等信息 。每個vm_area_struct對應著進程虛擬內(nèi)存中的一個連續(xù)區(qū)域 。例如,進程的代碼段、數(shù)據(jù)段、堆、棧等在虛擬內(nèi)存中都有各自對應的vm_area_struct,通過它可以清晰地了解每個內(nèi)存區(qū)域的屬性和范圍 。

這些數(shù)據(jù)結(jié)構相互關聯(lián),共同構成了進程在系統(tǒng)中的完整描述 。task_struct包含了指向mm_struct的指針,通過它可以訪問到進程的內(nèi)存管理信息 ;而mm_struct中又包含了指向vm_area_struct鏈表的指針,用于管理進程的各個虛擬內(nèi)存區(qū)域 。它們之間的關系緊密,就像一個復雜的機器,各個零件協(xié)同工作,保證進程的正常運行 。

3.2fork 的詳細執(zhí)行步驟

當進程調(diào)用fork函數(shù)時,背后會發(fā)生一系列復雜而有序的操作 ,下面我們來詳細剖析:

①進入內(nèi)核態(tài)

進程在用戶態(tài)調(diào)用fork時,會通過軟件中斷(在 x86 架構中,通常是int 0x80或sysenter指令)進入內(nèi)核態(tài) 。這就像是從普通的街道進入了 “核心區(qū)域”,擁有了更高的權限 。

進入內(nèi)核態(tài)后,系統(tǒng)會找到sys_fork()系統(tǒng)調(diào)用處理函數(shù),開始處理fork請求 。這個過程就好比是一個市民向政府部門提交申請,政府部門收到申請后,安排專門的工作人員(sys_fork()函數(shù))來處理 。

②獲取 PID 與創(chuàng)建描述符

內(nèi)核首先會獲取一個可用的 PID,這個 PID 將作為新創(chuàng)建子進程的身份標識 。PID 的分配就像是給新出生的寶寶辦理身份證,每個 PID 在系統(tǒng)中都是唯一的 。接著,內(nèi)核調(diào)用copy_process()函數(shù),為子進程分配和初始化一個全新的task_struct 。在這個過程中,copy_process()會從父進程的task_struct復制大部分內(nèi)容,包括文件系統(tǒng)相關數(shù)據(jù)(如打開的文件描述符表,這樣子進程就可以繼承父進程打開的文件)、信號處理函數(shù)表(使得子進程能像父進程一樣響應各種信號)、命名空間、進程狀態(tài)等 。

同時,它會為新進程設置一些初始狀態(tài),比如將新進程的狀態(tài)設置為TASK_UNINTERRUPTIBLE(不可中斷睡眠狀態(tài)),這就像是新員工入職后,先被安排在一個 “待命” 的狀態(tài) ;還會為新進程分配一個獨立的內(nèi)核棧,用于內(nèi)核態(tài)下的函數(shù)調(diào)用和數(shù)據(jù)存儲 ,并初始化計時器、信號等數(shù)據(jù)結(jié)構 。

③復制內(nèi)存映射區(qū)域

在copy_process()中,會調(diào)用dup_mmap()函數(shù)來復制父進程的內(nèi)存映射區(qū)域 。dup_mmap()會仔細遍歷父進程的所有vm_area_struct,并為子進程創(chuàng)建相應的內(nèi)存映射區(qū)域 。但此時,父子進程只是簡單地共享同一組頁表項,實際的物理內(nèi)存頁還未復制 。

這就好比兩個房間(父子進程)共享了同一份房間布局圖(頁表項),但房間里的實際物品(物理內(nèi)存頁)還沒有復制 。這樣做的好處是可以快速創(chuàng)建子進程,避免了大量物理內(nèi)存的復制開銷 。

④寫時復制(COW)設置

復制完vm_area_struct后,dup_mmap()會調(diào)用pud_mkwrite等函數(shù),將父子進程共享的所有頁表項都標記為只讀(設置頁表項的權限位為非可寫) 。這是寫時復制機制的關鍵一步,當父子進程中有一方試圖寫入共享的內(nèi)存頁時,CPU 會觸發(fā)頁保護異常 。

例如,父進程和子進程一開始共享某一內(nèi)存頁,當子進程想要修改這個內(nèi)存頁時,由于頁表項是只讀的,就會引發(fā)異常,從而觸發(fā)內(nèi)核的異常處理程序執(zhí)行寫時復制操作 ,就像是原本共同使用一份文件的兩人,當其中一人想要修改文件時,系統(tǒng)會為他復制一份獨立的文件副本 。

⑤其他設置

在copy_process()中,還會進行一些其他重要的設置 。比如復制父進程的信號處理程序表,確保子進程也能正確響應不同的信號,就像孩子繼承了父母應對各種情況的能力 ;為子進程設置SIGCHLD信號的默認處理程序,以便父進程能夠捕獲子進程的結(jié)束信號,這就像是給子進程和父進程之間建立了一個特殊的 “通訊渠道”,用于傳遞子進程結(jié)束的消息 。如果新創(chuàng)建的進程是一個內(nèi)核線程,copy_process()會進行一些額外的設置,如禁止內(nèi)核線程加載執(zhí)行用戶空間代碼、禁止訪問用戶態(tài)內(nèi)存等,這是為了保證內(nèi)核線程的安全性和穩(wěn)定性 。

此外,copy_process()會復制父進程的調(diào)度策略、優(yōu)先級等相關信息,并為子進程分配新的運行時統(tǒng)計數(shù)據(jù)結(jié)構,用于 CPU 調(diào)度 ,就像為子進程制定了一份專屬的 “工作安排表” 。最后,新創(chuàng)建的子進程會被加入相應的進程鏈表中,如任務隊列、反饋優(yōu)先級鏈表等,以便內(nèi)核進行進程調(diào)度和管理,這就像是將新員工加入到公司的組織架構中,方便進行工作安排和管理 。

⑥寫時復制異常處理

當子進程對共享內(nèi)存區(qū)域進行寫操作而發(fā)生頁保護異常時,寫時復制異常處理程序do_cow_fault()就會發(fā)揮作用 。它的主要工作包括為發(fā)生寫操作的內(nèi)存頁分配新的內(nèi)核頁框(物理內(nèi)存頁),就像是為需要修改文件的人分配一個新的文件存放空間 ;將原有的內(nèi)存頁內(nèi)容復制到新的頁框中,保證數(shù)據(jù)的一致性 ;修改相應的頁表項,使其指向新分配的物理內(nèi)存頁框,并設置為可寫,這樣子進程就可以在自己獨立的內(nèi)存頁上進行寫操作了 ;

同時,在原有的物理內(nèi)存頁上設置寫保護,避免不必要的復制 。通過這一系列操作,父子進程最終會擁有各自獨立的物理內(nèi)存副本,從而可以進行自身的數(shù)據(jù)寫入而不會相互影響 。

⑦執(zhí)行切換和系統(tǒng)調(diào)用返回

最后,內(nèi)核會決定父進程和子進程的執(zhí)行順序 。一般情況下,內(nèi)核會先讓子進程執(zhí)行,因為子進程的執(zhí)行狀態(tài)被設置為TASK_UNINTERRUPTIBLE 。在子進程執(zhí)行時,會執(zhí)行一些額外的初始化工作,如清理上下文、設置執(zhí)行計數(shù)器等 。

fork系統(tǒng)調(diào)用在父子進程中的返回值不同,在子進程中,fork返回0,就像是在告訴子進程 “你是新創(chuàng)建的,現(xiàn)在可以開始你的獨立旅程了” ;在父進程中,fork返回新創(chuàng)建子進程的PID,父進程可以通過這個PID來識別和管理子進程 。通過這種不同的返回值,父子進程可以區(qū)分不同的執(zhí)行路徑,各自執(zhí)行自己的代碼邏輯 。

3.3copy_process源碼分析

/* 代碼目錄:linux源碼/kernel/Fork.c */

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;

    /* CLONE_FS 不能與 CLONE_NEWNS 或 CLONE_NEWUSER 同時設置 */
    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);

    if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
        return ERR_PTR(-EINVAL);

    /* 創(chuàng)建線程時線程之間要共享信號處理函數(shù) */
    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);

    /*
     * 父子進程共享信號處理函數(shù)時必須共享內(nèi)存地址空間
     * 這就是為什么書上寫的fork出來的父子進程有其獨立的信號處理函數(shù),因為他們的內(nèi)存地址空間不同
     */
    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);

    /*
     * 防止參數(shù)init進程的兄弟進程
     * 只有init進程的 signal->flags & SIGNAL_UNKILLABLE 為真
     * 因為當進程退出時實際上是成為了僵尸進程(zombie),而要通過init進程將它回收,而如果此進程為init的兄弟進程,則沒辦法將其回收
     */
    if ((clone_flags & CLONE_PARENT) &&
                current->signal->flags & SIGNAL_UNKILLABLE)
        return ERR_PTR(-EINVAL);

    /* 如果新的進程將會有新的用戶空間或者pid,則不能讓它共享父進程的線程組或者信號處理或者父進程 */
    if (clone_flags & CLONE_SIGHAND) {
        if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
            (task_active_pid_ns(current) !=
                current->nsproxy->pid_ns_for_children))
            return ERR_PTR(-EINVAL);
    }

    /* 附加安全檢查 */
    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    /* 為新進程分配struct task_struct內(nèi)存和內(nèi)核棧內(nèi)存 */
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    /* ftrace是用于內(nèi)核性能分析和跟蹤的 */
    ftrace_graph_init_task(p);

    /* futex初始化,其用于SYSTEM V IPC,具體可見 http://blog.chinaunix.net/uid-7295895-id-3011238.html */
    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;
    /* 檢查 tsk->signal->rlim[RLIMIT_NPROC].rlim_cur是否小于等于用戶所擁有的進程數(shù),rlim結(jié)構體表示相關資源的最大值 */
     if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) {
        /* INIT_USER是root權限。檢查父進程是否有root權限 */
        if (p->real_cred->user != INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;

    /* 將父進程的cred復制到子進程的real_cred和cred。struct cred用于安全操作的結(jié)構 */
    retval = copy_creds(p, clone_flags);
    if (retval < 0)
        goto bad_fork_free;

    retval = -EAGAIN;
    /* 進程數(shù)量是否超出系統(tǒng)允許最大進程數(shù)量,最大進程數(shù)量跟內(nèi)存有關,一般原則是所有的進程內(nèi)核棧(默認8K)加起來不超過總內(nèi)存的1/8,可通過/proc/sys/kernel/threads-max改寫此值 */
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    /* 如果實現(xiàn)新進程的執(zhí)行域和可執(zhí)行格式的內(nèi)核函數(shù)都包含在內(nèi)核模塊中,則遞增其使用計數(shù) */
    if (!try_module_get(task_thread_info(p)->exec_domain->module))
        goto bad_fork_cleanup_count;

    delayacct_tsk_init(p);    /* Must remain after dup_task_struct() */

    /* 清除 PF_SUPERPRIV(表示進程使用了超級用戶權限) 和 PF_WQ_WORKER(使用了工作隊列) */
    p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
    /* 設置 PF_FORKNOEXEC 表明此子進程還沒有進行 execve() 系統(tǒng)調(diào)用 */
    p->flags |= PF_FORKNOEXEC;

    /* 初始化子進程的子進程鏈表和兄弟進程鏈表為空 */
    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    /* 見 http://www.ibm.com/developerworks/cn/linux/l-rcu/ */
    rcu_copy_process(p);
    p->vfork_done = NULL;
    /* 初始化分配鎖,此鎖用于保護分配內(nèi)存,文件,文件系統(tǒng)等操作 */
    spin_lock_init(&p->alloc_lock);

    /* 信號列表初始化,此列表保存被掛起的信號 */
    init_sigpending(&p->pending);

    /* 代碼執(zhí)行時間變量都置為0 */
    p->utime = p->stime = p->gtime = 0;
    p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
    p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
    seqlock_init(&p->vtime_seqlock);
    p->vtime_snap = 0;
    p->vtime_snap_whence = VTIME_SLEEPING;
#endif

#if defined(SPLIT_RSS_COUNTING)
    memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endif
    /* 此變量一般用于epoll和select,從父進程復制過來 */
    p->default_timer_slack_ns = current->timer_slack_ns;

    /* 初始化進程IO計數(shù)結(jié)構 */
    task_io_accounting_init(&p->ioac);
    acct_clear_integrals(p);

    /* 初始化cputime_expires結(jié)構 */
    posix_cpu_timers_init(p);

    /* 設置進程創(chuàng)建時間 */
    p->start_time = ktime_get_ns();
    p->real_start_time = ktime_get_boot_ns();

    /* io_context 和 audit_context 置空 */
    p->io_context = NULL;
    p->audit_context = NULL;
    /* 如果創(chuàng)建的是線程,因為需要修改到當前進程的描述符,會先上鎖 */
    if (clone_flags & CLONE_THREAD)
        threadgroup_change_begin(current);
    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_threadgroup_lock;
    }
#endif
#ifdef CONFIG_CPUSETS
    p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
    p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
    seqcount_init(&p->mems_allowed_seq);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
    p->irq_events = 0;
    p->hardirqs_enabled = 0;
    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
#ifdef CONFIG_BCACHE
    p->sequential_io    = 0;
    p->sequential_io_avg    = 0;
#endif


    /* 初始化子進程的調(diào)度優(yōu)先級和策略,在此并沒有將此進程加入到運行隊列,在copy_process返回之后加入 */
    retval = sched_fork(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_policy;

    /* perf event是一個性能調(diào)優(yōu)工具,具體見 http://blog.sina.com.cn/s/blog_98822316010122ex.html */
    retval = perf_event_init_task(p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* 初始化 p->sysvshm.shm_clist 鏈表頭 */
    shm_init_task(p);

    /* copy_semundo, copy_files, copy_fs, copy_sighand, copy_signal, copy_mm, copy_namespaces, copy_io都是根據(jù)clone_flags從父進程做相應的復制 */
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;
    retval = copy_fs(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_files;
    /* 判斷是否設置 CLONE_SIGHAND ,如果是(線程必須為是),增加父進行的sighand引用計數(shù),如果否(創(chuàng)建的必定是子進程),將父線程的sighand_struct復制到子進程中 */
    retval = copy_sighand(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_fs;
    /* 如果創(chuàng)建的是線程,直接返回0,如果創(chuàng)建的是進程,則會將父進程的信號屏蔽和安排復制到子進程中 */
    retval = copy_signal(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_sighand;
    /*
     * 如果是進程,則將父進程的mm_struct結(jié)構復制到子進程中,然后修改當中屬于子進程有別于父進程的信息(如頁目錄)
     * 如果是線程,則將子線程的mm指針和active_mm指針都指向父進程的mm指針所指結(jié)構。
     */
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
    retval = copy_namespaces(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_mm;
    retval = copy_io(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_namespaces;

    /*
     * 初始化子進程內(nèi)核棧和thread_struct結(jié)構體
     * 當進程切換時,進程的硬件上下文一般保存于三個地方: tss_struct(保存進程內(nèi)核棧地址,I/O許可權限位),thread_struct(大部分非通用寄存器),進程內(nèi)核棧(通用寄存器)
     * copy_thread函數(shù)會將父進程的thread_struct和內(nèi)核棧數(shù)據(jù)復制到子進程中,并將子進程的返回值置為0(x86返回值保存在eax中,arm保存在r0中,即把eax或者r0所在的內(nèi)核棧數(shù)據(jù)置為0)
     * copy_thread函數(shù)還會將子進程的eip寄存器值設置為ret_from_fork()的地址,即當子進程首次被調(diào)用就立即執(zhí)行系統(tǒng)調(diào)用clone返回。
     * 所以應用層調(diào)用fork()函數(shù)后,子進程返回0,父進程返回子進程ID(返回子進程ID在之后代碼中會實現(xiàn))
     */
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
    if (retval)
        goto bad_fork_cleanup_io;

    /* 判斷是不是init進程 */
    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        /* 分配pid */
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    /* 如果設置了CLONE_CHILD_SETTID則將task_struct中的set_child_tid指向用戶空間的child_tidptr,否則置空 */
    p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
    /* 如果設置了CLONE_CHILD_CLEARTID則將task_struct中的clear_child_tid指向用戶空間的child_tidptr,否則置空 */
    p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;

#ifdef CONFIG_BLOCK
    p->plug = NULL;
#endif
#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
    /*
     * 如果共享VM或者vfork創(chuàng)建,信號棧清空
     */
    if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
        p->sas_ss_sp = p->sas_ss_size = 0;

    /*
     * 系統(tǒng)調(diào)用跟蹤時應該禁止單步執(zhí)行
     */
    user_disable_single_step(p);
    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);


    /* 將子進程的PID設置為分配的PID在全局namespace中分配的值,在不同namespace中進程的PID不同,而p->pid保存的是全局的namespace中所分配的PID */
    p->pid = pid_nr(pid);
    if (clone_flags & CLONE_THREAD) {
        /* 創(chuàng)建的是線程 */
        p->exit_signal = -1;
        /* 線程組的所有線程的group_leader都一致 */
        p->group_leader = current->group_leader;
        /* 線程組的所有線程的tgid都一致,使用getpid返回的就是tgid */
        p->tgid = current->tgid;
    } else {
        /* 創(chuàng)建的是子進程 */
        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一致,所以當創(chuàng)建子線程時,tgid與主線程的一致 */
        p->tgid = p->pid;
    }

    /* 初始化頁框中臟頁數(shù)量為0 */
    p->nr_dirtied = 0;
    /* 初始化臟頁數(shù)量臨界值,當臟頁數(shù)量到達臨界值時,會調(diào)用balance_dirty_pages()將臟頁寫入磁盤 */
    p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
    /* 將臟頁寫入磁盤的開始時間 */
    p->dirty_paused_when = 0;

    p->pdeath_signal = 0;
    /* 初始化線程組鏈表為空 */
    INIT_LIST_HEAD(&p->thread_group);
    p->task_works = NULL;


    /* 到此系統(tǒng)中已經(jīng)存在此進程(線程),但是它還不能夠執(zhí)行,需要等待父進程對其處理,這里會上鎖 */
    write_lock_irq(&tasklist_lock);

    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
        /* 創(chuàng)建的是兄弟進程或者相同線程組線程 */
        /* 其父進程為父進程的父進程 */
        p->real_parent = current->real_parent;
        /* 其父進程執(zhí)行域為父進程的父進程執(zhí)行域 */
        p->parent_exec_id = current->parent_exec_id;
    } else {
        /* 創(chuàng)建的是子進程 */
        /* 父進程為父進程 */
        p->real_parent = current;
        /* 父進程的執(zhí)行域為父進程的執(zhí)行域 */
        p->parent_exec_id = current->self_exec_id;
    }

    /* 當前進程信號處理上鎖,這里應該是禁止了信號處理 */
    spin_lock(¤t->sighand->siglock);

    /*
     * seccomp與系統(tǒng)安全有關,具體見 http://note.sdo.com/u/634687868481358385/NoteContent/M5cEN~kkf9BFnM4og00239
     */
    copy_seccomp(p);

    /*
     * 在fork之前,進程組和會話信號都需要送到父親結(jié)點,而在fork之后,這些信號需要送到父親和孩子結(jié)點。
     * 如果我們在將新進程添加到進程組的過程中出現(xiàn)一個信號,而這個掛起信號會導致當前進程退出(current),我們的子進程就不能夠被kill或者退出了
     * 所以這里要檢測父進程有沒有信號被掛起。
     */
    recalc_sigpending();
    if (signal_pending(current)) {
        /* 包含有掛起進程,錯誤 */
        spin_unlock(¤t->sighand->siglock);
        write_unlock_irq(&tasklist_lock);
        retval = -ERESTARTNOINTR;
        goto bad_fork_free_pid;
    }

    if (likely(p->pid)) {
        /* 如果子進程需要跟蹤,就將 current->parent 賦值給 tsk->parent ,并將子進程插入調(diào)試程序的跟蹤鏈表中 */
        ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

        /* p->pids[PIDTYPE_PID].pid = pid; */
        init_task_pid(p, PIDTYPE_PID, pid);

        /* 如果是子進程(其實就是判斷 p->exit_signal 是否大于等于0,創(chuàng)建的是線程的話,exit_signal的值為-1) */
        if (thread_group_leader(p)) {
            /* p->pids[PIDTYPE_PGID].pid = current->group_leader->pids[PIDTYPE_PGID].pid; PGID為進程組ID,所以直接復制父進程的pgid */
            init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
            /* p->pids[PIDTYPE_SID].pid = current->group_leader->pids[PIDTYPE_SID].pid; SID為會話組ID,當沒有使用setsid()時,子進程的sid與父進程一致 */
            init_task_pid(p, PIDTYPE_SID, task_session(current));

            /* return pid->numbers[pid->level].nr == 1; 判斷新進程是否處于一個新創(chuàng)建的namespace中(新進程所在的新namespace中的pid會為1,以此判斷) */
            if (is_child_reaper(pid)) {
                /* 將當前namespace的init進程設置為此新進程 */
                ns_of_pid(pid)->child_reaper = p;
                p->signal->flags |= SIGNAL_UNKILLABLE;
            }

            p->signal->leader_pid = pid;
            p->signal->tty = tty_kref_get(current->signal->tty);

            /* 將此進程添加到父進程的子進程鏈表 */
            list_add_tail(&p->sibling, &p->real_parent->children);
            /* 將此進程task_struct加入到task鏈表中 */
            list_add_tail_rcu(&p->tasks, &init_task.tasks);
            /* 將新進程描述符的pgid結(jié)構插入pgid_hash */
            attach_pid(p, PIDTYPE_PGID);
            /* 將新進程描述符的sid結(jié)構插入sid_hash */
            attach_pid(p, PIDTYPE_SID);
            /* 當前cpu進程數(shù)量加1 */
            __this_cpu_inc(process_counts);
        } else {
            /* 創(chuàng)建的是線程,這里的處理導致了線程會共享信號 */
            current->signal->nr_threads++;
            atomic_inc(¤t->signal->live);
            atomic_inc(¤t->signal->sigcnt);
            /* 將新線程的thread_group結(jié)點加入到線程組的領頭線程的thread_group鏈表中 */
            list_add_tail_rcu(&p->thread_group,
                      &p->group_leader->thread_group);
            /* 將新線程的thread_node結(jié)點加入的新線程的signal->thread_head中 */
            list_add_tail_rcu(&p->thread_node,
                      &p->signal->thread_head);
        }
        /* 將新進程描述符的pid結(jié)構插入pid_hash */
        attach_pid(p, PIDTYPE_PID);
        /* 當前系統(tǒng)進程數(shù)加1 */
        nr_threads++;
    }

    /* 已創(chuàng)建的進程數(shù)量加1 */
    total_forks++;
    /* 釋放當前進程信號處理鎖 */
    spin_unlock(¤t->sighand->siglock);
    syscall_tracepoint_update(p);
    /* 釋放tasklist_lock鎖 */
    write_unlock_irq(&tasklist_lock);

    /* 將新進程與proc文件系統(tǒng)進行關聯(lián) */
    proc_fork_connector(p);
    cgroup_post_fork(p);
    /* 如果創(chuàng)建的是線程,釋放此鎖 */
    if (clone_flags & CLONE_THREAD)
        threadgroup_change_end(current);
    perf_event_fork(p);

    trace_task_newtask(p, clone_flags);
    uprobe_copy_process(p, clone_flags);

    /* 返回新進程的task_struct結(jié)構 */
    return p;

    /* 以下為執(zhí)行期間的錯誤處理 */
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))
        free_signal_struct(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_perf:
    perf_event_free_task(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
    mpol_put(p->mempolicy);
bad_fork_cleanup_threadgroup_lock:
#endif
    if (clone_flags & CLONE_THREAD)
        threadgroup_change_end(current);
    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);
}

四、fork應用實例與技巧

4.1簡單示例代碼分析

下面我們通過一個簡單的 C 語言示例代碼,來更直觀地了解fork在實際編程中的運用 。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid;
    int num = 10;

    // 調(diào)用fork函數(shù)
    pid = fork();

    if (pid < 0) {
        // fork失敗
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子進程
        num = num * 2;
        printf("I am the child process, num is %d, my pid is %d\n", num, getpid());
    } else {
        // 父進程
        num = num + 5;
        printf("I am the parent process, num is %d, my child's pid is %d\n", num, pid);
    }

    return 0;
}

在這段代碼中,我們首先定義了一個變量num并初始化為 10 。然后調(diào)用fork函數(shù)創(chuàng)建子進程 。如果fork返回值小于 0,表示創(chuàng)建子進程失敗,通過perror函數(shù)輸出錯誤信息并返回 1 。如果返回值為 0,說明當前是子進程,子進程將num乘以 2,然后打印出自己的進程 ID 和修改后的num值 。

如果返回值大于 0,說明當前是父進程,父進程將num加上 5,并打印出自己的信息以及子進程的 PID 。通過這個簡單的例子,我們可以看到父子進程雖然最初共享相同的變量值,但在后續(xù)的執(zhí)行過程中,它們可以獨立地對變量進行修改,互不影響 ,這充分展示了fork創(chuàng)建獨立執(zhí)行路徑的特性 。

4.2解決實際問題場景

網(wǎng)絡服務器場景:在網(wǎng)絡服務器中,fork發(fā)揮著至關重要的作用 。當一個服務器接收到客戶端的連接請求時,它可以調(diào)用fork創(chuàng)建一個子進程來專門處理這個客戶端的請求 。這樣,父進程就可以繼續(xù)監(jiān)聽其他客戶端的連接,從而實現(xiàn)并發(fā)處理多個客戶端請求的功能 。

例如,一個 Web 服務器,當有用戶訪問網(wǎng)頁時,服務器通過fork創(chuàng)建子進程,子進程負責處理用戶的頁面請求,如解析 HTTP 請求、讀取網(wǎng)頁文件、生成響應內(nèi)容等,而父進程則繼續(xù)等待新的用戶連接,大大提高了服務器的處理效率和響應速度 ,能夠同時為多個用戶提供服務 。

數(shù)據(jù)分析場景:在處理大規(guī)模數(shù)據(jù)分析任務時,fork也能派上用場 。假設我們有一個龐大的數(shù)據(jù)集需要進行復雜的統(tǒng)計分析,如計算平均值、方差等 。我們可以利用fork創(chuàng)建多個子進程,每個子進程負責處理數(shù)據(jù)集的一部分 。

比如,將一個包含 100 萬條數(shù)據(jù)的文件分成 10 個子部分,每個子進程處理 10 萬條數(shù)據(jù),最后父進程收集各個子進程的計算結(jié)果并進行匯總,從而加快整個數(shù)據(jù)分析的速度,充分利用多核 CPU 的計算資源,提高數(shù)據(jù)分析的效率 。

4.3注意事項與常見問題

資源競爭問題:在使用fork時,由于父子進程共享部分資源(如打開的文件描述符),可能會出現(xiàn)資源競爭的情況 。例如,父子進程同時對同一個文件進行寫操作,可能會導致文件內(nèi)容混亂 。為了避免這種情況,可以使用文件鎖機制,如flock函數(shù),在進行文件操作前先獲取文件鎖,確保同一時間只有一個進程能夠?qū)ξ募M行寫操作 。

另外,在多線程程序中調(diào)用fork要格外小心,因為多線程程序中每個線程都有自己的棧和寄存器狀態(tài),調(diào)用fork時,子進程會繼承父進程的所有線程,這可能會導致復雜的狀態(tài)不一致性和資源競爭問題,所以通常建議避免在多線程程序中調(diào)用fork,如果確實需要創(chuàng)建新進程,可以考慮使用exec函數(shù)族 。

子進程退出處理:子進程退出時,如果父進程沒有及時處理,子進程就會變成僵尸進程,占用系統(tǒng)資源 。為了避免產(chǎn)生僵尸進程,父進程可以調(diào)用wait或waitpid函數(shù)來等待子進程結(jié)束,并獲取子進程的退出狀態(tài) 。wait函數(shù)會阻塞父進程,直到有子進程結(jié)束;而waitpid函數(shù)則更加靈活,可以指定等待特定的子進程,并且可以設置非阻塞模式 。例如:

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid, wpid;
    int status;

    pid = fork();
    if (pid == -1) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子進程
        sleep(2);
        printf("Child process is exiting\n");
        return 3;
    } else {
        // 父進程
        wpid = waitpid(pid, &status, 0);
        if (wpid == -1) {
            perror("waitpid error");
            return 1;
        }
        if (WIFEXITED(status)) {
            printf("Child exited with status %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child was terminated by signal %d\n", WTERMSIG(status));
        }
    }

    return 0;
}

在上述代碼中,父進程通過waitpid等待子進程結(jié)束,并通過WIFEXITED和WEXITSTATUS宏來判斷子進程是否正常退出以及獲取退出狀態(tài) ,這樣就可以有效地避免僵尸進程的產(chǎn)生 。

五、對比與拓展

5.1 fork與vfork的對比

在 Linux 進程創(chuàng)建中,除了fork,還有一個與之類似的系統(tǒng)調(diào)用vfork ,它們在功能上有相似之處,但也存在著諸多重要的區(qū)別 。

創(chuàng)建進程與地址空間共享:fork創(chuàng)建子進程時,會為子進程復制父進程的地址空間,包括代碼段、數(shù)據(jù)段、堆和棧等 。雖然在復制時采用了寫時復制(COW)技術,即最初父子進程共享物理內(nèi)存頁,只有當有寫操作發(fā)生時才為子進程分配獨立的物理內(nèi)存頁,但從本質(zhì)上來說,子進程擁有自己獨立的虛擬地址空間 ,后續(xù)的寫操作會使父子進程的數(shù)據(jù)相互獨立 。而vfork創(chuàng)建的子進程則直接與父進程共享地址空間 ,子進程對數(shù)據(jù)的修改會直接反映在父進程中 ,它們就像在同一間屋子里活動,所有的物品(數(shù)據(jù))都是共享的 。

執(zhí)行順序:fork創(chuàng)建的父子進程執(zhí)行順序是不確定的 ,這取決于內(nèi)核的調(diào)度算法 。有可能父進程先執(zhí)行,也有可能子進程先執(zhí)行 。而vfork則保證子進程先運行 ,在子進程調(diào)用exec函數(shù)族(用于執(zhí)行另一個程序,替換當前進程的內(nèi)存映像)或exit(用于終止進程)之前,父進程會被阻塞,處于等待狀態(tài) ,只有當子進程執(zhí)行了這兩個操作之一后,父進程才有可能被調(diào)度運行 。

適用場景:由于fork創(chuàng)建的子進程擁有獨立的地址空間,適合用于需要父子進程并發(fā)執(zhí)行且相互獨立工作的場景 ,比如前面提到的網(wǎng)絡服務器中處理多個客戶端請求,每個子進程獨立處理自己的任務,互不干擾 。而vfork由于共享地址空間且保證子進程先運行的特性,適用于子進程創(chuàng)建后立即要執(zhí)行exec函數(shù)族去執(zhí)行另一個程序的場景 ,這樣可以避免不必要的地址空間復制開銷 ,提高效率 。例如,當一個程序需要啟動另一個程序時,可以使用vfork創(chuàng)建子進程,然后子進程調(diào)用exec函數(shù)族來加載并運行新程序 。

5.2 fork在不同Linux版本中的優(yōu)化

隨著 Linux 操作系統(tǒng)的不斷發(fā)展和演進,fork在不同版本中也經(jīng)歷了一系列的優(yōu)化改進 ,以提升性能和資源管理效率 。

早期版本:在早期的Linux版本中,fork采用的是相對簡單直接的復制方式 。當調(diào)用fork時,會將父進程的整個地址空間完整地復制給子進程 ,包括所有的內(nèi)存頁面 。這種方式雖然實現(xiàn)簡單,但效率較低,因為大量的內(nèi)存復制操作會消耗較多的時間和系統(tǒng)資源 ,尤其是在父進程內(nèi)存占用較大時,fork的開銷會非常明顯 。

寫時復制(COW)技術引入:為了提高fork的效率,Linux 內(nèi)核引入了寫時復制(COW)技術 。從2.0版本開始,fork創(chuàng)建子進程時不再立即復制物理內(nèi)存頁,而是讓父子進程共享同一組頁表項,指向相同的物理內(nèi)存頁 。只有當父子進程中有一方試圖對共享內(nèi)存頁進行寫操作時,才會觸發(fā)寫時復制機制 ,為執(zhí)行寫操作的進程分配新的物理內(nèi)存頁,并將原內(nèi)存頁內(nèi)容復制到新頁中 。這種優(yōu)化大大減少了fork時的內(nèi)存復制開銷,加快了子進程的創(chuàng)建速度 ,同時也節(jié)省了內(nèi)存資源 。

后續(xù)版本優(yōu)化:在后續(xù)的 Linux 版本中,對fork的優(yōu)化還在繼續(xù) 。例如,在進程調(diào)度方面,內(nèi)核不斷改進調(diào)度算法,使得fork創(chuàng)建的父子進程能夠更合理地分配 CPU資源 ,提高整體的并發(fā)執(zhí)行效率 。在內(nèi)存管理方面,進一步優(yōu)化了頁表的管理和更新機制 ,減少了寫時復制過程中的開銷 。此外,還針對多處理器系統(tǒng)進行了優(yōu)化,提高了fork在多核環(huán)境下的性能 ,使得父子進程能夠更好地利用多核 CPU 的計算資源 。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-03-07 08:30:00

pwruLinux網(wǎng)絡包追蹤

2018-10-15 10:10:41

Linux內(nèi)核補丁

2021-11-14 07:29:55

Linux 內(nèi)核靜態(tài)追蹤Linux 系統(tǒng)

2021-11-15 04:00:07

Linux 內(nèi)核動態(tài)

2021-07-06 14:36:05

RustLinux內(nèi)核模塊

2009-09-11 08:44:36

2021-11-01 12:13:53

Linux僵尸進程

2021-03-11 12:19:39

Linux運維Linux系統(tǒng)

2015-09-17 13:26:56

線程數(shù)進程Linux

2020-11-10 07:11:23

Linux內(nèi)核補丁

2022-10-10 17:00:19

地址內(nèi)核函數(shù)

2021-05-26 07:53:58

Linux運維Linux系統(tǒng)

2021-09-14 10:03:35

RustLinux開發(fā)工作

2020-09-09 16:00:22

Linux進程

2023-07-25 15:17:38

Linux操作系統(tǒng)開發(fā)

2016-08-23 10:17:42

2009-08-18 11:01:51

2021-02-07 08:02:33

Linux內(nèi)核開源

2021-09-16 10:15:56

Linux內(nèi)核Rust

2022-12-12 11:14:06

LinuxID
點贊
收藏

51CTO技術棧公眾號