歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

關於Linux系統如何實現fork的研究

引言

    fork函數是用於在linux系統中創建進程所使用,而最近看了看一個fork()調用是怎麼從應用到glibc,最後到內核中實現的,這片文章就聊聊最近對這方面研究的收獲吧。我們主要聊聊從glibc庫進入內核,再從內核出來的情景,而從應用到glibc這部分本片文章就不詳細說明了。為了方便期間,我們的硬件平台為arm,linux內核為3.18.3,glibc庫版本為2.20,可從http://ftp.gnu.org/gnu/glibc/下載源碼。

Glibc到kernel

    我們設定硬件平台為arm,glibc庫版本為2.20,因為不同的CPU體系結構中,glibc庫通過系統調用進入kernel庫的方法是不一樣的。當glibc准備進入kernel時,流程如下 


 1 /* glibc最後會調用到一個INLINE_SYSCALL宏,參數如下 */
 2 INLINE_SYSCALL (clone, 5, CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid);
 3
 4  /* INLINE_SYSCALL的宏定義如下,可以看出在INLINE_SYSCALL宏中又使用到了INTERNAL_SYSCALL宏,而INTERNAL_SYSCALL宏最終會調用INTERNAL_SYSCALL_RAW */
 5 #define INLINE_SYSCALL(name, nr, args...) \
 6  ({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
 7      if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \
 8        { \
 9      __set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
10      _sys_result = (unsigned int) -1; \
11        } \
12      (int) _sys_result; })
13
14  /* 為了方便大家理解,將此宏寫為偽代碼形式 */
15  int INLINE_SYSCALL (name, nr, args...)
16  {
17    unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args);
18
19    if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) {
20        __set_error (INTERNAL_SYSCALL_ERRNO (_sys_result, ));
21        _sys_result = (unsigned int) -1;
22    }
23    return (int)_sys_result;
24  }
25
26 /* 這裡我們不需要看INTERNAL_SYSCALL宏,只需要看其最終調用的INTERNAL_SYSCALL_RAW宏,需要注意的是,INTERNAL_SYSCALL調用INTERNAL_SYSCALL_RAW時,通過SYS_ify(name)宏將name轉為了系統調用號
27  * name: 120(通過SYS_ify(name)宏已經將clone轉為了系統調用號120)
28  * err: NULL
29  * nr: 5
30  * args[0]: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
31  * args[1]: NULL
32  * args[2]: NULL
33  * args[3]: NULL
34  * args[4]: &THREAD_SELF->tid
35  */
36  # define INTERNAL_SYSCALL_RAW(name, err, nr, args...)    \
37  ({    \
38        register int _a1 asm ("r0"), _nr asm ("r7");    \
39        LOAD_ARGS_##nr (args)    \
40        _nr = name;    \
41        asm volatile ("swi    0x0    @ syscall " #name    \
42    : "=r" (_a1)    \
43    : "r" (_nr) ASM_ARGS_##nr    \
44    : "memory");    \
45        _a1; })
46  #endif

 

 

    INTERNAL_SYSCALL_RAW實現的結果就是將args[0]存到了r0...args[4]存到了r4中,並將name(120)綁定到r7寄存器。然後通過swi  0x0指令進行了軟中斷。0x0是一個24位的立即數,用於軟中斷執行程序判斷執行什麼操作。當執行這條指令時,CPU會跳轉至中斷向量表的軟中斷指令處,執行該處保存的調用函數,而在函數中會根據swi後面的24位立即數(在我們的例子中是0x0)執行不同操作。在這時候CPU已經處於保護模式,陷入內核中。現在進入到linux內核中後,具體看此時內核是怎麼操作的吧。

 

  1 /* 源文件地址: 內核目錄/arch/arm/kernel/entry-common.S */
  2
  3 ENTRY(vector_swi)
  4    /*
  5      * 保存現場
  6      */
  7 #ifdef CONFIG_CPU_V7M
  8    v7m_exception_entry
  9 #else
 10    sub    sp, sp, #S_FRAME_SIZE
 11    stmia    sp, {r0 - r12}            @ 將r0~r12保存到棧中
 12  ARM(    add    r8, sp, #S_PC        )
 13  ARM(    stmdb    r8, {sp, lr}^        )    @ Calling sp, lr
 14  THUMB(    mov    r8, sp            )
 15  THUMB(    store_user_sp_lr r8, r10, S_SP    )    @ calling sp, lr
 16    mrs    r8, spsr            @ called from non-FIQ mode, so ok.
 17    str    lr, [sp, #S_PC]            @ Save calling PC
 18    str    r8, [sp, #S_PSR]        @ Save CPSR
 19    str    r0, [sp, #S_OLD_R0]        @ Save OLD_R0
 20 #endif
 21    zero_fp
 22    alignment_trap r10, ip, __cr_alignment
 23    enable_irq
 24    ct_user_exit
 25    get_thread_info tsk
 26
 27    /*
 28      * 以下代碼根據不同arm體系結構獲取系統調用號
 29      */
 30
 31 #if defined(CONFIG_OABI_COMPAT)
 32
 33    /*
 34      * 如果內核配置了OABI兼容選項,會先判斷是否為THUMB,以下為THUMB情況(我們分析的時候可以忽略這段,一般情況是不走這一段的)
 35      */
 36 #ifdef CONFIG_ARM_THUMB
 37    tst    r8, #PSR_T_BIT
 38    movne    r10, #0                @ no thumb OABI emulation
 39  USER(    ldreq    r10, [lr, #-4]        )    @ get SWI instruction
 40 #else
 41  USER(    ldr    r10, [lr, #-4]        )    @ get SWI instruction
 42 #endif
 43  ARM_BE8(rev    r10, r10)            @ little endian instruction
 44
 45 #elif defined(CONFIG_AEABI)
 46
 47    /*
 48      * 我們主要看這裡,EABI將系統調用號保存在r7中
 49      */
 50 #elif defined(CONFIG_ARM_THUMB)
 51    /* 先判斷是否為THUMB模式 */
 52    tst    r8, #PSR_T_BIT           
 53    addne    scno, r7, #__NR_SYSCALL_BASE   
 54  USER(    ldreq    scno, [lr, #-4]        )
 55
 56 #else
 57    /* EABI模式 */
 58  USER(    ldr    scno, [lr, #-4]        )    @ 獲取系統調用號
 59 #endif
 60
 61    adr    tbl, sys_call_table        @ tbl為r8,這裡是將sys_call_table的地址(相對於此指令的偏移量)存入r8
 62
 63 #if defined(CONFIG_OABI_COMPAT)
 64    /*
 65      * 在EABI體系中,如果swi跟著的立即數為0,這段代碼不做處理,而如果是old abi體系,則根據系統調用號調用old abi體系的系統調用表(sys_oabi_call_table)
 66      * 其實說白了,在EABI體系中,系統調用時使用swi 0x0進行軟中斷,r7寄存器保存系統調用號
 67      * 而old abi體系中,是通過swi (系統調用號|magic)進行調用的
 68      */
 69    bics    r10, r10, #0xff000000
 70    eorne    scno, r10, #__NR_OABI_SYSCALL_BASE
 71    ldrne    tbl, =sys_oabi_call_table
 72 #elif !defined(CONFIG_AEABI)
 73    bic    scno, scno, #0xff000000       
 74    eor    scno, scno, #__NR_SYSCALL_BASE   
 75 #endif
 76
 77 local_restart:
 78    ldr    r10, [tsk, #TI_FLAGS]        @ 檢查系統調用跟蹤
 79    stmdb    {r4, r5}            @ 將第5和第6個參數壓入棧
 80
 81    tst    r10, #_TIF_SYSCALL_WORK        @ 判斷是否在跟蹤系統調用
 82    bne    __sys_trace
 83
 84    cmp    scno, #NR_syscalls        @ 檢測系統調用號是否在范圍內,NR_syscalls保存系統調用總數
 85    adr    lr, BSYM(ret_fast_syscall)    @ 將返回地址保存到lr寄存器中,lr寄存器是用於函數返回的。
 86    ldrcc    pc, [tbl, scno, lsl #2]        @ 調用相應系統調用例程,tbl(r8)保存著系統調用表(sys_call_table)地址,scno(r7)保存著系統調用號120,這裡就轉到相應的處理例程上了。
 87
 88    add    r1, sp, #S_OFF
 89 2:    cmp    scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE)
 90    eor    r0, scno, #__NR_SYSCALL_BASE    @ put OS number back
 91    bcs    arm_syscall
 92    mov    why, #0                @ no longer a real syscall
 93    b    sys_ni_syscall            @ not private func
 94
 95 #if defined(CONFIG_OABI_COMPAT) || !defined(CONFIG_AEABI)
 96    /*
 97      * We failed to handle a fault trying to access the page
 98      * containing the swi instruction, but we're not really in a
 99      * position to return -EFAULT. Instead, return back to the
100      * instruction and re-enter the user fault handling path trying
101      * to page it in. This will likely result in sending SEGV to the
102      * current task.
103      */
104 9001:
105    sub    lr, lr, #4
106    str    lr, [sp, #S_PC]
107    b    ret_fast_syscall
108 #endif
109 ENDPROC(vector_swi)            @ 返回

 

 

    好的,終於跳轉到了系統調用表,現在我們看看系統調用表是怎麼樣的一個形式

 

 1 /* 文件地址: linux內核目錄/arch/arm/kernel/calls.S */
 2
 3 /* 0 */        CALL(sys_restart_syscall)
 4        CALL(sys_exit)
 5        CALL(sys_fork)
 6        CALL(sys_read)
 7        CALL(sys_write)
 8 /* 5 */        CALL(sys_open)
 9        CALL(sys_close)
10        CALL(sys_ni_syscall)        /* was sys_waitpid */
11        CALL(sys_creat)
12        CALL(sys_link)
13 /* 10 */    CALL(sys_unlink)
14        CALL(sys_execve)
15        CALL(sys_chdir)
16        CALL(OBSOLETE(sys_time))    /* used by libc4 */
17        CALL(sys_mknod)
18 /* 15 */    CALL(sys_chmod)
19        CALL(sys_lchown16)
20        CALL(sys_ni_syscall)        /* was sys_break */
21        CALL(sys_ni_syscall)        /* was sys_stat */
22        CALL(sys_lseek)
23 /* 20 */    CALL(sys_getpid)
24        CALL(sys_mount)
25        CALL(OBSOLETE(sys_oldumount))    /* used by libc4 */
26        CALL(sys_setuid16)
27        CALL(sys_getuid16)
28 /* 25 */    CALL(OBSOLETE(sys_stime))
29        CALL(sys_ptrace)
30        CALL(OBSOLETE(sys_alarm))    /* used by libc4 */
31        CALL(sys_ni_syscall)        /* was sys_fstat */
32        CALL(sys_pause)
33
34        ......................
35        ......................
36       
37 /* 120 */    CALL(sys_clone)        /* 120在此,之前傳進來的系統調用號120進入內核後會到這 */
38        CALL(sys_setdomainname)
39        CALL(sys_newuname)
40        CALL(sys_ni_syscall)        /* modify_ldt */
41        CALL(sys_adjtimex)
42 /* 125 */    CALL(sys_mprotect)
43        CALL(sys_sigprocmask)
44        CALL(sys_ni_syscall)        /* was sys_create_module */
45        CALL(sys_init_module)
46        CALL(sys_delete_module)
47 
48        ......................
49        ......................
50       
51 /* 375 */    CALL(sys_setns)
52        CALL(sys_process_vm_readv)
53        CALL(sys_process_vm_writev)
54        CALL(sys_kcmp)
55        CALL(sys_finit_module)
56 /* 380 */    CALL(sys_sched_setattr)
57        CALL(sys_sched_getattr)
58        CALL(sys_renameat2)
59        CALL(sys_seccomp)
60        CALL(sys_getrandom)
61 /* 385 */    CALL(sys_memfd_create)
62        CALL(sys_bpf)
63 #ifndef syscalls_counted
64 .equ syscalls_padding, ((NR_syscalls + 3) & ~3) - NR_syscalls
65 #define syscalls_counted
66 #endif
67 .rept syscalls_padding
68        CALL(sys_ni_syscall)
69 .endr

 

 

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

 

 1 /* CALL(x)宏展開 */
 2 #define CALL(x) .equ NR_syscalls,NR_syscalls+1
 3 #include "calls.S"
 4
 5 .ifne NR_syscalls - __NR_syscalls
 6 .error "__NR_syscalls is not equal to the size of the syscall table"
 7 .endif
 8
 9 /* 主要是後面這一段,
10  * 上面一段主要用於統計系統調用數量,並將數量保存到NR_syscalls中,具體實現說明可以參考 http://www.linuxidc.com/Linux/2015-02/112909.htm
11  */
12
13 #undef CALL
14 /* 其實就是生成一個數為x,相當於.long sys_clone,因為sys_clone是函數名,所以.long生成的是sys_clone函數名對應的地址 */
15 #define CALL(x) .long x
16
17 #ifdef CONFIG_FUNCTION_TRACER
18
19
20 /* 配合ldrcc一起看,原來ldrcc是這樣 */
21 ldrcc    pc, [tbl, scno, lsl #2]   
22
23 /* 把CALL(x)代入ldrcc,最後是這樣 */
24 ldrcc    pc, sys_clone(函數地址) 

清楚的看出來,ldrcc最後是將sys_clone的函數地址存入了pc寄存器,而sys_clone函數內核是怎麼定義的呢,如下

 1 /* 文件地址: linux內核目錄/kernel/Fork.c */
 2
 3 /* 以下代碼根據不同的內核配置定義了不同的clone函數
 4  * 其最終都調用的do_fork函數,我們先看看SYSCALL_DEFINE是怎麼實現的吧,實現在此代碼片段後面
 5  */
 6 #ifdef __ARCH_WANT_SYS_CLONE
 7 #ifdef CONFIG_CLONE_BACKWARDS
 8 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
 9          int __user *, parent_tidptr,
10          int, tls_val,
11          int __user *, child_tidptr)
12 #elif defined(CONFIG_CLONE_BACKWARDS2)
13 SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
14          int __user *, parent_tidptr,
15          int __user *, child_tidptr,
16          int, tls_val)
17 #elif defined(CONFIG_CLONE_BACKWARDS3)
18 SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
19        int, stack_size,
20        int __user *, parent_tidptr,
21        int __user *, child_tidptr,
22        int, tls_val)
23 #else
24 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
25          int __user *, parent_tidptr,
26          int __user *, child_tidptr,
27          int, tls_val)
28 #endif
29 {
30    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
31 }
32
33
34  /************************************************
35  * 我是代碼分界線
36  ************************************************/
37
38 /* 文件地址: linux內核目錄/include/linux.h */
39
40 #define SYSCALL_DEFINE0(sname) \
41    SYSCALL_METADATA(_##sname, 0); \
42    asmlinkage long sys_##sname(void)
43
44 #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
45 #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
46 #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
47 #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
48 #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
49 #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
50
51 #define SYSCALL_DEFINEx(x, sname, ...) \
52    SYSCALL_METADATA(sname, x, __VA_ARGS__) \
53    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
54
55 #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
56 #define __SYSCALL_DEFINEx

可以看出系統調用是使用SYSCALL_DEFINEx進行定義的,以我們的例子,實際上最後clone函數被定義為

 1 /* 展開前 */
 2
 3 SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
 4          int __user *, parent_tidptr,
 5          int __user *, child_tidptr,
 6          int, tls_val)
 7 #endif
 8 {
 9    /* 應用層默認fork參數(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid) */
10    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
11 }
12
13  /* 展開後 */
14
15 asmlinkage long sys_clone (unsigned long clone_flags, unsigned long newsp, int __user * parent_tidptr, int __user * child_tidptr, int tls_val)
16 {
17    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
18 }

 

 

    終於看到最後系統會調用do_fork函數進行操作,接下來我們看看do_fork函數

 

 1 /* 應用層的fork最後會通過sys_clone系統調用調用到此函數 */
 2 /* 應用層默認fork參數(CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, NULL, NULL, NULL, &THREAD_SELF->tid)
 3  * clone_flags: CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
 4  * stack_start: NULL
 5  * stack_size: NULL
 6  * parent_tidptr: NULL
 7  * child_tidptr: &THREAD_SELF->tid
 8  * pid: NULL
 9  */
10 long do_fork(unsigned long clone_flags,
11      unsigned long stack_start,
12      unsigned long stack_size,
13      int __user *parent_tidptr,
14      int __user *child_tidptr)
15 {
16    struct task_struct *p;
17    int trace = 0;
18    long nr;
19
20    /* 判斷是否進行跟蹤 */
21    if (!(clone_flags & CLONE_UNTRACED)) {
22        if (clone_flags & CLONE_VFORK)
23            trace = PTRACE_EVENT_VFORK;
24        else if ((clone_flags & CSIGNAL) != SIGCHLD)
25            trace = PTRACE_EVENT_CLONE;
26        else
27            trace = PTRACE_EVENT_FORK;
28
29        if (likely(!ptrace_event_enabled(current, trace)))
30            trace = 0;
31    }
32
33    /* 調用copy_process進行初始化,返回初始化好的struct task_struct結構體,當我們調用fork時返回兩次的原因也是在這個函數當中,下回分析 */
34    p = copy_process(clone_flags, stack_start, stack_size,
35              child_tidptr, NULL, trace);
36
37
38    if (!IS_ERR(p)) {
39        /* 創建成功 */
40        struct completion vfork;
41        struct pid *pid;
42
43        trace_sched_process_fork(current, p);
44
45        /* 獲取子進程PID */
46        pid = get_task_pid(p, PIDTYPE_PID);
47        /* 返回子進程pid所屬的命名空間所看到的局部PID */
48        nr = pid_vnr(pid);
49
50        if (clone_flags & CLONE_PARENT_SETTID)
51            put_user(nr, parent_tidptr);
52
53        if (clone_flags & CLONE_VFORK) {
54            p->vfork_done = &vfork;
55            init_completion(&vfork);
56            get_task_struct(p);
57        }
58
59        /* 將新進程加入到CPU的運行隊列中 */
60        wake_up_new_task(p);
61
62        /* 跟蹤才會用到 */
63        if (unlikely(trace))
64            ptrace_event_pid(trace, pid);
65
66        /* 如果是vfork調用,則在此等待vfork的進程結束 */
67        if (clone_flags & CLONE_VFORK) {
68            if (!wait_for_vfork_done(p, &vfork))
69                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
70        }
71
72        put_pid(pid);
73    } else {
74        /* 創建失敗 */
75        nr = PTR_ERR(p);
76    }
77        /* 返回新進程PID(新進程在這會返回0) */
78    return nr;
79 }


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

小結

    到這裡,整個系統調用的入口就分析完了,其實整個流程也不算很復雜:應用層通過swi軟中斷進入內核---->通過系統調用表選定目標系統調用--->執行系統調用--->返回。之後的文章我會詳細分析copy_process函數,此函數中涉及相當多的知識,作為學習linux內核的入口也是比較合適的。

更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2015-02/112908p2.htm

Copyright © Linux教程網 All Rights Reserved