某某茶叶有限公司欢迎您!
金沙棋牌在线 > 服务器&运维 > Linux内核创建一个新进程的过程

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

时间:2020-04-06 15:14

进程描述

进程描述符(task_struct)

用来描述进程的数据结构,可以理解为进程的属性。比如进程的状态、进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,该数据结构被定义为task_struct

进程控制块(PCB)

是操作系统核心中一种数据结构,主要表示进程状态。

进程状态

金沙棋牌在线 1

fork()

fork()在父、子进程各返回一次。在父进程中返回子进程的 pid,在子进程中返回0。

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");
  }
}

此文仅用于MOOCLinux内核分析作业

张依依+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

.
ret_from_fork

曹朋辉
原创作品转载请注明出处
《Linux内核分析》MOOC课程

进程创建


分析fork函数对应的系统调用处理过程

启动保护fork命令的menuOS

金沙棋牌在线 2

设置断点进行调试

金沙棋牌在线 3

金沙棋牌在线 4

内核里操作系统的三大功能:
内存管理
进程管理
文件系统
其中最核心的是进程管理

大致流程

fork 通过0×80中断(系统调用)来陷入内核,由系统提供的相应系统调用来完成进程的创建。

fork.c

//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()的代码(部分代码,经过笔者的精简):

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;

        //……

        //复制进程描述符,copy_process()的返回值是一个 task_struct 指针。
        p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);

        if (!IS_ERR(p)) {
            struct completion vfork;
            struct pid *pid;

            trace_sched_process_fork(current, p);

            //得到新创建的进程描述符中的pid
            pid = get_task_pid(p, PIDTYPE_PID);
            nr = pid_vnr(pid);

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

            //如果调用的 vfork()方法,初始化 vfork 完成处理信息。
            if (clone_flags & CLONE_VFORK) {
                p->vfork_done = &vfork;
                init_completion(&vfork);
                get_task_struct(p);
            }

            //将子进程加入到调度器中,为其分配 CPU,准备执行
            wake_up_new_task(p);

            //fork 完成,子进程即将开始运行
            if (unlikely(trace))
                ptrace_event_pid(trace, pid);

            //如果是 vfork,将父进程加入至等待队列,等待子进程完成
            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;
}

task_struct数据结构

根据wiki)的定义,进程是计算机中已运行程序的实体。在面向线程设计的系统(Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

A computer program is a passive collection of instructions; a process is the actual execution of those instructions. Several processes may be associated with the same program; for example, opening up several instances of the same program often means more than one process is being executed.

在Linux中,task_struct其实就是通常所说的PCB。该结构定义位于:

/include/linux/sched.h

task_struct比较庞大,大致可以分为几个部分:

  • 进程状态(State)
  • 进程调度信息(Scheduling Information)
  • 各种标识符(Identifiers)
  • 进程通信有关信息(IPC:Inter_Process Communication)
  • 时间和定时器信息(Times and Timers)
  • 进程链接信息(Links)
  • 文件系统信息(File System)
  • 虚拟内存信息(Virtual Memory)
  • 页面管理信息(page)
  • 对称多处理器(SMP)信息
  • 和处理器相关的环境(上下文)信息(Processor Specific Context)
  • 其它信息

其中比较重要的几个参数:

  • volatile long state;进程状态,可见/include/linux/sched.h文件中的宏,TASK_RUNNING等
  • unsigned int rt_priority;实时优先级
  • unsigned int policy;调度策略
  • pid_t pid;进程标识符
  • struct task_struct __rcu *real_parent;real parent
  • struct list_head children;list of my children
  • struct files_struct *files;系统打开文件

进程的创建

从了解进程的创建,进程间调度切换,来从总体把握进程工作

当前进程复制一个子进程,然后进行修改

fork一个子进程的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc , char * argv[])
{
    int pid;
    pid = fork();
    if(pid<0)
    {
        fprintf(stderr,"Fork Failed !");
        exit(-1);
    }
    else if (pid==0)
    {
        printf("This is Child Process!n");
     }
    else
    {
        printf("This is Parent Process !n");
        wait(NULL);
        printf("Child Complete!n:);
    }

}

-fork()是用户态用于创建子进程的系统调用
-pid<0时打印出错信息
-子进程中fork()返回值为0,父进程中fork()返回值为进程pid值
-所以else if和else都将被执行
理解进程创建过程复杂代码的方法

金沙棋牌在线 5

创建一个新进程在内核中的执行过程
-fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建
-创建新进程是通过复制当前进程来实现
-复制一个PCB task_struct
-给新进程分配一个新内核堆栈
-修改进程数据如pid、进程链表等
-从用户态代码中可以看到fork()函数返回两次,在父进程和子进程中各返回一次,父进程从系统调用中返回,子进程从系统调用中返回涉及了子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,在copy_thread in copy_process里设定

do_fork

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;
}

copy process创建进程内容
-dup_task_struct复制pcb,分配新空间
-然后初始化赋值子进程信息
-copy thread从子进程pid内核堆栈位置栈底,拷贝内核堆栈数据和指定新进程的第一条指令地址

进程描述符task_struct数据结构

do_fork 流程

  • 调用 copy_process 为子进程复制出一份进程信息
  • 如果是 vfork 初始化完成处理信息
  • 调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
  • 如果是 vfork,父进程等待子进程完成 exec 替换自己的地址空间

分析内核处理过程sys_clone

fork、vfork和clone三个系统调用实际上都是通过do_fork来实现进程的创建.
见如下语句:

return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
  • do_fork 函数

而do_fork函数真正实现复制是copy_process

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{

    ...

    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace);
  ...
}
  • copy_process函数

copy_process()主要完成进程数据结构,各种资源的初始化。

p = dup_task_struct(current);
  1. (省略的IF语句)检查clone_flags参数,防止无效的组合进入
  2. p = dup_task_struct(current);调用dup_task_struct()为新进程创建一个内核栈
  3. 判断权限及允许范围的代码
  4. 对子进程的描述符初始化和复制父进程的资源给子进程
- `retval = sched_fork(clone_flags, p);`完成调度相关的设置,将这个task分配给CPU
- `if (retval)`语句群,复制共享进程的的各个部分
- `retval = copy_thread(clone_flags, stack_start, stack_size, p);`复制父进程堆栈的内容到子进程的堆栈中去.这其中,copy_thread()函数中的语句`p->thread.ip = (unsigned long) ret_from_fork;`决定了新进程的**第一条指令地址**.
  • dup_task_struct()
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    int node = tsk_fork_get_node(orig);
    int err;

    tsk = alloc_task_struct_node(node);
    if (!tsk)
        return NULL;

    ti = alloc_thread_info_node(tsk, node);
    if (!ti)
        goto free_tsk;

    err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto free_ti;

    tsk->stack = ti;
# ifdef CONFIG_SECCOMP

    tsk->seccomp.filter = NULL;
# endif

    setup_thread_stack(tsk, orig);
    clear_user_return_notifier(tsk);
    clear_tsk_need_resched(tsk);
    set_task_stack_end_magic(tsk);

# ifdef CONFIG_CC_STACKPROTECTOR
    tsk->stack_canary = get_random_int();
# endif

    atomic_set(&tsk->usage, 2);
# ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
# endif
    tsk->splice_pipe = NULL;
    tsk->task_frag.page = NULL;

    account_kernel_stack(ti, 1);

    return tsk;

free_ti:
    free_thread_info(ti);
free_tsk:
    free_task_struct(tsk);
    return NULL;
}
  1. tsk = alloc_task_struct_node(node);为task_struct开辟内存
  2. ti = alloc_thread_info_node(tsk, node);ti指向thread_info的首地址,同时也是系统为新进程分配的两个连续页面的首地址。
  3. err = arch_dup_task_struct(tsk, orig);复制父进程的task_struct信息到新的task_struct里, (*dst = *src;)
  4. tsk->stack = ti;task的对应栈
  5. setup_thread_stack(tsk, orig);初始化thread info结构
  6. set_task_stack_end_magic(tsk);栈结束的地址设置数据为栈结束标示(for overflow detection)

创建的新进程从哪里开始执行

·p->thread.ip = (unsigned long) ret_from_fork;·
ret_from_fork返回了子进程调度的第一条指令
在复制内核堆栈时只复制了其中一部分SAVE_ALLL相关的部分,int指令和cpu压栈内容,即最栈底的部分
ret_from_fork会跳转syscall exit,最终返回用户态,此时返回到子进程用户空间
所以创建的新进程从ret_from_fork开始执行

王潇洋
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

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

copy_process 流程

追踪copy_process 代码(部分)

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;

    //创建进程描述符指针
    struct task_struct *p;

    //……

    //复制当前的 task_struct
    p = dup_task_struct(current);

    //……

    //初始化互斥变量  
    rt_mutex_init_task(p);

    //检查进程数是否超过限制,由操作系统定义
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }

    //……

    //检查进程数是否超过 max_threads 由内存大小决定
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    //……

    //初始化自旋锁
    spin_lock_init(&p->alloc_lock);
    //初始化挂起信号
    init_sigpending(&p->pending);
    //初始化 CPU 定时器
    posix_cpu_timers_init(p);

    //……

    //初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
    retval = sched_fork(clone_flags, p);

    //复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
    if (retval)
        goto bad_fork_cleanup_policy;

    retval = perf_event_init_task(p);
    if (retval)
        goto bad_fork_cleanup_policy;
    retval = audit_alloc(p);
    if (retval)
        goto bad_fork_cleanup_perf;
    /* copy all the process information */
    shm_init_task(p);
    retval = copy_semundo(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_audit;
    retval = copy_files(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_semundo;
    retval = copy_fs(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_files;
    retval = copy_sighand(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_fs;
    retval = copy_signal(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_sighand;
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
    retval = copy_namespaces(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_mm;
    retval = copy_io(clone_flags, p);

    //初始化子进程内核栈
    retval = copy_thread(clone_flags, stack_start, stack_size, p);

    //为新进程分配新的 pid
    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    //设置子进程 pid  
    p->pid = pid_nr(pid);

    //……

    //返回结构体 p
    return p;
  • 调用 dup_task_struct 复制当前的 task_struct
  • 检查进程数是否超过限制
  • 初始化自旋锁、挂起信号、CPU 定时器等
  • 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
  • 复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
  • 调用 copy_thread 初始化子进程内核栈
  • 为新进程分配并设置新的 pid

gdb跟踪sys_clone

  • 用GDB来跟踪sys_clone,设置以下断点:

金沙棋牌在线 6

fork1.png

  • 运行后首先停在sys_clone处:

金沙棋牌在线 7

  • 然后是do_fork,之后是copy_process:

金沙棋牌在线 8

fork3.png

  • 进入copy_thread:

金沙棋牌在线 9

fork4.png

  • 在copy_thread中,我们可以查看p的值

金沙棋牌在线 10

fork5.png

  • 但是回到copy_process后再查看,将得到一个value optimized out的提示,这是因为Linux内核打开gcc的-O2选项优化导致.如果想要关掉,可以参考:这里

金沙棋牌在线 11

fork6.png

  • ret_from_fork按照之前的分析被调用,跟踪到syscall_exit后无法继续.如果想在本机调试system call,那么当你进入system call时,系统已经在挂起状态了。如果想要跟踪调试system_call,可以使用kgdb等

金沙棋牌在线 12

fork7.png

金沙棋牌在线 13