您的位置:首页 > 运维架构 > Linux

Linux内核创建一个新进程的过程

2016-04-03 01:53 555 查看
罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1. 准备

在menu代码中增加fork函数

int pid;
/* fork another process */
asm volatile(
"mov $0x78, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
:"=m"(pid)
:
);
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");
}
return 0;


按照课程中给定的方法进行编译。

2 实验开始

按照之前课程的gdb的方法,进行启动跟踪



启动完成后,在弹出窗口输入:



程序会在断点处停下:



3. 代码分析

3.1 系统调用分析

在本例中,通过int 0x80来进行系统调用时,其进入内核空间与返回用户空间的堆栈情况



从这里可以看到在中断进入时,系统会保存一系列寄存器的值。

3.2 fork主进程的返回

当执行mfork的时候,会调用中断:

int pid;
/* fork another process */
asm volatile(
"mov $0x78, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
:"=m"(pid)
:
);


对于主进程,即当前正在执行上述代码的进程来说,其过程与普通的系统调用没有差别:



1)用户程序利用int 80,进入到内核态。此时CPU会自动保存用户的SS、ESP、EFLAGS、CS、EIP

2 ) 接着执行entry_32.S,即总控程序(调用entry_32.S中的ENTRY(system_call)),调用SAVE_ALL保存数据

3) entry_32.S保存完数据后,会调用

call *sys_call_table(,%eax,4)


其中sys_call_table为函数入口地址表。

4)经过计算后,找到函数,

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}


其中返回值为do_fork()的返回值,即子进,程的pid的值

5)经过总控程序后,返回用户空间,继续执行后面的代码,即汇编之后的代码。

3.3 fork子进程的创建与返回

相对于主进程来说,子进程是由主进程复制而来。接下来分析子进程的创建过程。当系统进入到sys_clone之后,会调用do_fork函数创建子进程。而do_fork()函数中,有一条语句:

p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);


子进程就是在copy_process中进行创建,并运行的。

3.3.1 copy_process函数的分析

copy_process()整个函数可以分为三个部分:创建、修改、复制。

static struct task_struct *copy_process(...)
{
...
//1.复制。在这个函数中,它创建了两个对象task_struct与thread_info,并且将部分基本信息复制过去了。
p = dup_task_struct(current);
//2. 修改。 当task_struct复制过来之后,修改时间之类的。
//3. 复制。诸如下面的代码
...
retval = copy_thread(clone_flags, stack_start, stack_size, p);
...
}


3.3.2 copy_thread()分析

copy_thread()是子进程创建的一个关键部分,其函数是arch\x86\kernel\process_32.c中。下面结合代码,继续分析

int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{

...
//1. 获取当前主进程的堆栈数据的地址。拷贝当前主进程的已有数据。
//pt_regs的内容就是int 80与SAVE_ALL的值
*childregs = *current_pt_regs();
//设置子进程堆栈的eax的值为0,保证子进程的返回值为0
childregs->ax = 0;
if (sp)
childregs->sp = sp;
//子进程的ip返回地址,即子进程返回时的执行位置
p->thread.ip = (unsigned long) ret_from_fork;
...
}


3.3.3 子进程的返回

当子进程创建成功后,系统会将它加入到运行列表中,等待CPU调度,当CPU执行时,此时子进程的eip指向是entry_32.S中的ret_from_fork,子进程也从此处开始执行

ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi %eax
pushl_cfi $0x0202      # Reset kernel eflags
popfl_cfi
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)


查看ret_from_fork函数,可以看到相对于主进程的system_call而言,最终的处理都是调用
4000
jmp syscall_exit, 而子进程相对于主进程少了int 80与SAVE_ALL的压栈。但是子进程在 copy_thread()中的

*childregs = *current_pt_regs();这里写代码片


已经保存了相应的值,因此,子进程执行到此处时又与主进程相同了,但是其中eax的值已经修改为0, 以保证子进程可以执行它自己的程序。

4. 总结

1) 进程的创建也是一种系统调用。 主进程的返回与普通的系统调用的逻辑相同,但是子进程通过ret_from_fork与pt_reg的配合,巧妙的实现了子进程的返回

2)子进程的创建在内核空间,首次调用也是在内核空间,再从内核空间调度到用户空间
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux kernel