一、分析 Linux 内核创建一个新进程的过程实验
1、在MenuOS中增加fork命令,使用help查看现有命令,步骤如下:
cd menu
mv test_fork.c test.c//在MenuOS中增加fork命令,并覆盖掉test.c文件
make rootfs
MenuOS>>help
MenuOS>>fork
2、在gdb中调试
//shell1中启动内核
cd ~/LinuxKernel
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
// shell2中使用gdb调试
cd ~/LinuxKernel
gdb
file linux-3.18.6/vmlinux
target remote:1234
然后在sys_clone
、do_fork
、dup_task_struct
、copy_process
、copy_thread
、ret_from_fork
处设置断点:
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_fork
二、分析代码
1、fork()函数
fork()在父、子进程各返回一次。在父进程中返回子进程的 pid,在子进程中返回0。fork一个子进程的代码如下:
int Fork(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
2、进程创建流程:
- fork 通过0x80中断(系统调用)来陷入内核,由系统提供的相应系统调用来完成进程的创建。PCB包含了一个进程的重要运行信息,所以我们将围绕在创建一个新进程时,如何来建立一个新的PCB的这一个过程来进行分析,在Linux系统中,PCB主要是存储在一个叫做task_struct这一个结构体中,创建新进程仅能通过fork,vfork,clone等系统调用的形式来进行。
//fork
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
//vfork
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif
//clone
#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);
}
#endif
通过看上边的代码,我们可以清楚的看到,不论是使用 fork 还是 vfork 来创建进程,最终都是通过 do_ fork() 方法来实现的。
-
do_ fork()的代码中可以看出过程是通过copy_ process来复制进程描述符,返回新创建的子进程的task_ struct的指针(即PCB指针),将新创建的子进程放入调度器的队列中,让其有机会获得CPU,并且要确保子进程要先于父进程运行。
-
copy_process函数主要完成以下工作:用 dup_ task_ struct 复制当前的 task_ struct,并且检查进程数是否超过限制,初始化自旋锁、挂起信号、CPU 定时器等,调用 sched_ fork 初始化进程数据结构,并把进程状态设置为 TASK_ RUNNING,复制所有进程信息:包括文件系统、信号处理函数、信号、内存管理等,最后调用 copy_ thread 初始化子进程内核栈,为新进程分配并设置新的pid。
-
dup_ task_ struct()中可以看出在该函数中,调用了alloc_ task_ struct_ node分配一个 task_ struct 节点,调用alloc_ thread_ info_ node分配一个 thread_ info 节点,其实是分配了一个thread_ union联合体,将栈底返回给 ti。最后将栈底的值 ti 赋值给新节点的栈,最终执行完dup_ task_ struct之后,子进程除了tsk->stack指针不同之外,全部都一样!
三、总结
可以通过fork、vfork和clone来创建一个新进程,而他们又都是通过调用do_ fork方法来实现的。do_ fork函数主要是调用copy_ process函数来为子进程复制父进程信息的。copy_ process函数调用 dup_task_struct为子进程分配新的堆栈;调用sched_ fork 初始化进程数据结构,并把进程状态设置为TASK_ RUNNING。copy_ process函数尤为重要,我们可以看到为什么fork()函数返回值为0,并且fork出的子进程是从哪里开始执行的:将子进程的ip设置为ret_ from_ fork的首地址,子进程从ret_ from_ fork开始执行。