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

罗冲 + 原创作品转载请注明出处 + 《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而言,最终的处理都是调用jmp syscall_exit, 而子进程相对于主进程少了int 80与SAVE_ALL的压栈。但是子进程在 copy_thread()中的

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

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

4. 总结

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值