在阅读了一些文本和源代码后,我意识到fork、vfork和clone都是通过fork.c中的do_fork以不同的参数执行的。
但是fork()到底是如何调用do_fork()的..
当调用fork()时,哪些函数都会被调用?
从fork()到do_fork()的逐步类是什么
发布于 2012-07-20 07:03:37
libc的fork()和其他系统调用的实现包含调用系统调用的特殊处理器指令。系统调用是特定于体系结构的,可能是一个相当复杂的主题。
让我们从一个“简单”的例子开始,MIPS:
在MIPS上,系统调用是通过SYSCALL指令调用的。因此,libc的fork()实现最终将一些参数放在一些寄存器上,将系统调用号放在regiter v0中,并发出一条syscall指令。
在MIPS上,这会导致SYSCALL_EXCEPTION (异常编号8)。引导时,内核将异常8与arch/mips/kernel/traps.c:trap_init()中的处理例程相关联
set_except_vector(8, handle_sys);因此,当CPU因为程序发出了syscall指令而收到异常8时,CPU会转换到内核模式,并开始在/usr/src/linux/arch/mips/kernel/scall*.S的handle_sys上执行处理程序(对于不同的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 (中断描述符表)的特殊表,发现它有一个门(在x86中是一个陷阱门,在x86-64中是一个中断门),然后转换到环0,开始在arch/x86/kernel/entry_32.S/arch/x86/ia32/ia32entry.S (分别针对x86/x86_64 )执行system_call/ia32_syscall处理程序。
但是,由于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_target和ia32_cstar_target (最后一个例程仅用于x86_64) (在arch/x86/kernel/entry_32.S或arch/x86/ia32/ia32entry.S中)。
选择要使用的系统调用机制
linux内核和glibc有一种机制,可以在调用系统调用的不同方式之间进行选择。
内核为每个进程设置一个虚拟共享库,称为VDSO (虚拟动态共享对象),您可以在cat /proc/<pid>/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 0x80、sysenter和syscall的实现,内核选择合适的实现。
为了让用户空间知道有一个vdso,以及它所在的位置,内核在辅助向量中设置了AT_SYSINFO和AT_SYSINFO_EHDR条目(auxv是main()的第四个参数,位于argc, argv, envp之后,用于将一些信息从内核传递给新启动的进程)。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传统的vsyscall %gs指向TCB,因此这会通过指向存储在TCB中的
int 0x80←的指针间接跳转。对于编译为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()https://stackoverflow.com/questions/11548779
复制相似问题