【问题标题】:function calls from fork() to do_fork()从 fork() 到 do_fork() 的函数调用
【发布时间】:2012-07-18 19:21:24
【问题描述】:

浏览了一些文本和源代码后,我意识到forkvforkclone 这三个都是通过fork.c 中的do_fork 以不同的参数执行的。

但是fork() 究竟是如何调用do_fork()..

当调用fork()时调用了哪些函数?

fork()do_fork()的分步类是什么?

【问题讨论】:

  • fork()-> 系统调用 -> 转换到内核模式 -> 系统调用表查找 -> sys_fork() -> do_fork()
  • 我有点知道.. fork() 究竟是如何调用 NR_fork 的? n 那么 NR_fork 怎么去 sys_fork() ???
  • fork() -> 程序使用依赖于架构的方式进行系统调用 -> 处理器转换到内核模式,转换到内核的架构特定初始化中指定的地址 -> 架构 -特定的系统调用处理程序查询特定于架构的系统调用表 -> sys_fork()
  • 系统如何理解遇到 fork() 时将其视为系统调用。您提到的步骤,即进程转换、转换到内核模式等发生在 ENTRY.S.

标签: linux-kernel


【解决方案1】:

libcfork() 和其他系统调用的实现包含调用系统调用的特殊处理器指令。系统调用调用是特定于架构的,并且可能是一个相当复杂的主题。

让我们从一个“简单”的例子开始,MIPS:

在 MIPS 上,系统调用是通过 SYSCALL 指令调用的。所以,libc 对fork() 的实现最终将一些参数放在一些寄存器上,将系统调用号放在寄存器v0 中,并发出syscall 指令。

在 MIPS 上,这会导致 SYSCALL_EXCEPTION(异常编号 8)。启动时,内核将异常 8 关联到 arch/mips/kernel/traps.c:trap_init() 中的处理例程:

set_except_vector(8, handle_sys);

因此,当 CPU 收到异常 8 时,因为程序发出了 syscall 指令,CPU 转换到内核模式,并开始执行 handle_sys /usr/src/linux/arch/mips/kernel/scall*.S 处的处理程序(有几个文件用于不同的32/64 位内核空间/用户空间组合)。该例程在系统调用表中查找系统调用号并跳转到相应的sys_...() 函数,在本例中为sys_fork()

现在,x86 更加复杂。传统上,Linux 使用中断 0x80 来调用系统调用。这与 arch/x86/kernel/traps_*.c:trap_init() 中的 x86 门相关联:

set_system_gate(SYSCALL_VECTOR,&system_call);

x86 处理器具有多个权限级别(环)(从 80286 开始)。只能通过预定义的门访问(跳转到)较低的环(= 更多特权),这是内核设置的特殊类型的段描述符。所以,当调用int 0x80 时,会产生一个中断,CPU 会查找一个名为 IDT(Interrupt Descriptor Table)的特殊表,发现它有一个门(x86 中的陷阱门,x86 中的中断门- 64),并转换到环 0,开始执行 ia32_syscall 处理程序 arch/x86/kernel/entry_32.S/arch/x86/ia32/ia32entry.S(分别针对 x86/x86_64)。

但是,从 Pentium Pro 开始,有另一种调用系统调用的方法:使用 SYSENTER 指令(AMD 也有自己的 SYSCALL 指令)。这是调用系统调用的更有效方式。这种“较新”机制的处理程序设置为arch/x86/vdso/vdso32-setup.c:syscall32_cpu_init()

#ifdef CONFIG_X86_64
[...]
void syscall32_cpu_init(void)
{
    if (use_sysenter < 0)
            use_sysenter = (boot_cpu_data.x86_vendor == X86_VENDOR_INTEL);

    /* Load these always in case some future AMD CPU supports
       SYSENTER from compat mode too. */
    checking_wrmsrl(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
    checking_wrmsrl(MSR_IA32_SYSENTER_ESP, 0ULL);
    checking_wrmsrl(MSR_IA32_SYSENTER_EIP, (u64)ia32_sysenter_target);

    wrmsrl(MSR_CSTAR, ia32_cstar_target);
}
[...]
#else
[...]
void enable_sep_cpu(void)
{
    int cpu = get_cpu();
    struct tss_struct *tss = &per_cpu(init_tss, cpu);

    if (!boot_cpu_has(X86_FEATURE_SEP)) {
            put_cpu();
            return;
    }

    tss->x86_tss.ss1 = __KERNEL_CS;
    tss->x86_tss.sp1 = sizeof(struct tss_struct) + (unsigned long) tss;
    wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
    wrmsr(MSR_IA32_SYSENTER_ESP, tss->x86_tss.sp1, 0);
    wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) ia32_sysenter_target, 0);
    put_cpu();
}
[...]
#endif  /* CONFIG_X86_64 */

以上使用机器特定寄存器 (MSR) 进行设置。处理程序例程是ia32_sysenter_targetia32_cstar_target(这最后一个仅适用于x86_64)(在arch/x86/kernel/entry_32.Sarch/x86/ia32/ia32entry.S 中)。

选择使用哪种系统调用机制

linux 内核和 glibc 有一种机制可以在调用系统调用的不同方式之间进行选择。

内核为每个进程建立一个虚拟共享库,称为VDSO(virtual dynamic shared object),可以在cat /proc/&lt;pid&gt;/maps的输出中看到:

$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 03:04 1553592    /bin/cat
0804c000-0804d000 rw-p 00003000 03:04 1553592    /bin/cat
[...]
b7ee8000-b7ee9000 r-xp b7ee8000 00:00 0          [vdso]
[...]

这个 vdso 包含一个适当的系统调用调用序列,用于正在使用的 CPU,例如:

ffffe414 <__kernel_vsyscall>:
ffffe414:       51                      push   %ecx        ; \
ffffe415:       52                      push   %edx        ; > save registers
ffffe416:       55                      push   %ebp        ; /
ffffe417:       89 e5                   mov    %esp,%ebp   ; save stack pointer
ffffe419:       0f 34                   sysenter           ; invoke system call
ffffe41b:       90                      nop
ffffe41c:       90                      nop                ; the kernel will usually
ffffe41d:       90                      nop                ; return to the insn just
ffffe41e:       90                      nop                ; past the jmp, but if the
ffffe41f:       90                      nop                ; system call was interrupted
ffffe420:       90                      nop                ; and needs to be restarted
ffffe421:       90                      nop                ; it will return to this jmp
ffffe422:       eb f3                   jmp    ffffe417 <__kernel_vsyscall+0x3>
ffffe424:       5d                      pop    %ebp        ; \
ffffe425:       5a                      pop    %edx        ; > restore registers
ffffe426:       59                      pop    %ecx        ; /
ffffe427:       c3                      ret                ; return to caller

arch/x86/vdso/vdso32/中有使用int 0x80sysentersyscall的实现,内核会选择合适的。

为了让用户空间知道有一个 vdso 及其所在的位置,内核在辅助向量中设置了 AT_SYSINFOAT_SYSINFO_EHDR 条目(auxvmain() 的第四个参数,在 @987654359 之后@,用于将一些信息从内核传递给新启动的进程)。 AT_SYSINFO_EHDR 指向 vdso 的 ELF 头,AT_SYSINFO 指向 vsyscall 实现:

$ LD_SHOW_AUXV=1 id    # tell the dynamic linker ld.so to output auxv values
AT_SYSINFO:      0xb7fd4414
AT_SYSINFO_EHDR: 0xb7fd4000
[...]

glibc 使用此信息来定位vsyscall。它将它存储到动态加载器全局_dl_sysinfo中,例如:

glibc-2.16.0/elf/dl-support.c:_dl_aux_init():
ifdef NEED_DL_SYSINFO
  case AT_SYSINFO:
    GL(dl_sysinfo) = av->a_un.a_val;
    break;
#endif
#if defined NEED_DL_SYSINFO || defined NEED_DL_SYSINFO_DSO
  case AT_SYSINFO_EHDR:
    GL(dl_sysinfo_dso) = (void *) av->a_un.a_val;
    break;
#endif

glibc-2.16.0/elf/dl-sysdep.c:_dl_sysdep_start()

glibc-2.16.0/elf/rtld.c:dl_main:
GLRO(dl_sysinfo) = GLRO(dl_sysinfo_dso)->e_entry + l->l_addr;

并且在TCB(线程控制块)头部的一个字段中:

glibc-2.16.0/nptl/sysdeps/i386/tls.h

_head->sysinfo = GLRO(dl_sysinfo)

如果内核较旧且不提供 vdso,则 glibc 为 _dl_sysinfo 提供默认实现:

.hidden _dl_sysinfo_int80:
int $0x80
ret

当针对 glibc 编译程序时,根据情况,会在调用系统调用的不同方式之间做出选择:

glibc-2.16.0/sysdeps/unix/sysv/linux/i386/sysdep.h:
/* The original calling convention for system calls on Linux/i386 is
   to use int $0x80.  */
#ifdef I386_USE_SYSENTER
# ifdef SHARED
#  define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
#  define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int $0x80
#endif
  • int 0x80←传统方式
  • call *%gs:offsetof(tcb_head_t, sysinfo)%gs 指向 TCB,因此它通过指向存储在 TCB 中的 vsyscall 的指针间接跳转。这对于编译为 PIC 的对象是首选。这需要 TLS 初始化。对于动态可执行文件,TLS 由 ld.so 初始化。对于静态 PIE 可执行文件,TLS 由 __libc_setup_tls() 初始化。
  • call *_dl_sysinfo ← 这通过全局变量间接跳转。这需要重新定位 _dl_sysinfo,因此可以避免编译为 PIC 的对象。

所以,在 x86 中:

                       fork()
                         ↓
int 0x80 / call *%gs:0x10 / call *_dl_sysinfo 
  |                ↓              ↓
  |       (in vdso) int 0x80 / sysenter / syscall
  ↓                ↓              ↓            ↓
      system_call     | ia32_sysenter_target | ia32_cstar_target
                          ↓
                       sys_fork()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-08-13
    • 2013-10-09
    • 1970-01-01
    • 2019-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-09
    相关资源
    最近更新 更多