zoukankan      html  css  js  c++  java
  • 关于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.tuicool.com/articles/QFj6zq
    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内核的入口也是比较合适的。
  • 相关阅读:
    makefile 3
    makefile 3
    wzplayer for delphi demo截图
    makefile 2
    makefile
    wzplayer for delphi demo截图
    clang complete
    makefile
    clang complete
    linux最常用命令集合
  • 原文地址:https://www.cnblogs.com/tolimit/p/4262851.html
Copyright © 2011-2022 走看看