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

来源:互联网 发布:淘宝一直等待揽件陷阱 编辑:程序博客网 时间:2024/06/05 04:02

罗冲 + 原创作品转载请注明出处 + 《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_ENDPROCEND(ret_from_fork)

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

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

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

4. 总结

1) 进程的创建也是一种系统调用。 主进程的返回与普通的系统调用的逻辑相同,但是子进程通过ret_from_fork与pt_reg的配合,巧妙的实现了子进程的返回
2)子进程的创建在内核空间,首次调用也是在内核空间,再从内核空间调度到用户空间

0 0