6. Linux内核创建一个新进程的过程分析

#####################################
作者:张卓
原创作品转载请注明出处:《Linux操作系统分析》MOOC课程 http://www.xuetangx.com/courses/course-v1:ustcX+USTC001+_/about
 #####################################

一 进程的描述

1. 进程控制块PCB——task_struct
为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。task_struct 数据结构很庞大, 比较重要的定义有:
  ● 进程状态
      ○ Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING。
  ● 进程的标示pid
  ● 所有进程链表struct list_head tasks;
      ○ 内核的双向循环链表的实现方法 - 一个更简略的双向循环链表
  ● 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
  ● Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈
      ○ 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈
      ○ 内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够了
  ● struct thread_struct thread; //CPU-specific state of this task
  ● 文件系统和文件描述符
  ● 内存管理——进程的地址空间

2.task_struct 结构体图示:


3. 进程状态
进程根据运行状态可分为: 就绪态、运行态、可中断睡眠态、不可中断睡眠态和僵死态。
每个状态都对应一个宏定义,定义的宏在:
include/linux/sched.h:

203 #define TASK_RUNNING		0
204 #define TASK_INTERRUPTIBLE	1
205 #define TASK_UNINTERRUPTIBLE	2

208 /* in tsk->exit_state */
209 #define EXIT_DEAD		16
210 #define EXIT_ZOMBIE		32


进程状态转化图如下:

 

2. 进程的创建

1. fork一个进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(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");
    }
}

上面一段小程序在用户态创建一个子进程。调用fork()函数之后,会有两次返回:一次在父进程中返回子进程的PID,一次在子进程中返回0。Linux通过复制父进程来创建一个新进程,我们可以简单的猜想一下,调用fork()之后,陷入内核主要做了什么?简单设想一下内核执行过程的框架:
○ 复制一个PCB——task_struct
○ 要给新进程分配一个新的内核堆栈
○ 要修改复制过来的进程数据,比如pid、进程链表等等

2. 创建一个新进程在内核中的执行过程
fork、vfork 和 clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建; 上面程序中调用fork(),调用过程实际是:

fork/vfork --> sys_clone--> do_fork

下面我们直接分析内核创建新进程的核心代码:

kernel/fork.c:
do_fork():
...
copy_process(): /* 在do_fork()调用copy_process()复制老进程的寄存器,所有适当的进程环境 */
	dup_task_struct(): /* 在copy_process()中调用dup_task_struct(),复制task_struct */
		alloc_thread_info_node()
		arch_dup_task_struct()
		setup_thread_stack()
	copy_files()
	copy_fs()
	...
	copy_thread()(arch/x86/kernel/process_32.c ):
		*childregs = *current_pt_regs(); /*复制内核堆栈的一部分(struct pt_rergs) ,int指令和SAVE_ALL压到内核栈的内容*/
		childregs->ax= 0; /* 子进程的返回值为什么是0,这就是答案*/
		p->thread.ip = (unsigned) ret_from_fork;
		/* 拷贝内核堆栈数据和指定新进程的第一条指令地址 */

/*对创建的进程进行一些错误检查, 检查标志位;最后唤醒新进程*/
if (!IS_ERR(p)) {
...
wake_up_new_task(p); /*首次唤醒创建的进程,同时完成一些初始化调度的所需的工作,并将进程放入运行队列中*/
...
} else {
nr = PTR_ERR(p);
}
return nr; /* 这里返回的就是子进程的PID */

在调用copy_thread()的时候,子进程thread.ip 被赋值为ret_from_fork的地址,所以子进程开始执行的第一条指令就是ret_from_fork.
arch/x86/kernel/entry_32.S:
ENTRY(ret_from_fork)

子进程执行完ret_form_fork之后,将又一次返回用户空间,且返回值为0。

下面附上do_fork()函数的整个代码:
linux-3.18.6/kernel/fork.c:

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
     * Determine whether and which event to report to ptracer.  When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if ((clone_flags & CSIGNAL) != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    if (!IS_ERR(p)) {
        struct completion vfork;
        struct pid *pid;

        trace_sched_process_fork(current, p);

        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

        if (clone_flags & CLONE_PARENT_SETTID)
            put_user(nr, parent_tidptr);

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        wake_up_new_task(p);

        /* forking complete and child started to run, tell ptracer */
        if (unlikely(trace))
            ptrace_event_pid(trace, pid);

        if (clone_flags & CLONE_VFORK) {
            if (!wait_for_vfork_done(p, &vfork))
                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
        }

        put_pid(pid);
    } else {
        nr = PTR_ERR(p);
    }
    return nr;
}

3. GDB 跟踪调试

对于上面的代码分析,我们可以用gdb设断点跟踪,加强我们对fork()创建新建的认识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值