进程管理基础学习笔记 - 2. kernel_clone

1. 前言

本专题我们开始学习进程管理部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
进程或线程是通过fork vfork或clone等系统调用来建立,底层都对应kernel_clone函数,本文就主要记录kernel_clone的过程。

kernel版本:5.10
平台:arm64

2. fork/vfork/clone/kernel_thread

commit cad6967ac10843a70842cd39c7b53412901dd21f
Author: Christian Brauner <christian.brauner@ubuntu.com>
Date:   Wed Aug 19 12:46:45 2020 +0200

    fork: introduce kernel_clone()
    
    The old _do_fork() helper doesn't follow naming conventions of in-kernel
    helpers for syscalls. The process creation cleanup in [1] didn't change the
    name to something more reasonable mainly because _do_fork() was used in quite a
    few places. So sending this as a separate series seemed the better strategy.
    
    This commit does two things:
    1. renames _do_fork() to kernel_clone() but keeps _do_fork() as a simple static
       inline wrapper around kernel_clone().
    2. Changes the return type from long to pid_t. This aligns kernel_thread() and
       kernel_clone(). Also, the return value from kernel_clone that is surfaced in
       fork(), vfork(), clone(), and clone3() is taken from pid_vrn() which returns
       a pid_t too.
    
    Follow-up patches will switch each caller of _do_fork() and each place where it
    is referenced over to kernel_clone(). After all these changes are done, we can
    remove _do_fork() completely and will only be left with kernel_clone().

从内核的提交记录来看,在v5.10-rc1将_do_fork替换成了kernel_clone,因此fork/vfork/clone系统调用底层都将调用kernel_clone,下一节将主要介绍kernel_clone的流程,kernel_clone的唯一参数为kernel_clone_args,对于fork/vfork/clone有所不同:

  • fork()是将父进程的全部资源拷贝给子进程;
  • vfork()是将父进程除mm_struct的所有资源拷贝给子进程;
  • clone()是通过CLONE_XXX指定将哪些资源从父进程拷贝到子进程

fork

SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
        struct kernel_clone_args args = {
                .exit_signal = SIGCHLD,
        };

        return kernel_clone(&args);
#else
        /* can not support in nommu mode */
        return -EINVAL;
#endif
}
  1. 使用fork()函数创建子进程时,子进程和父进程有各自独立的进程地址空间,fork后会重新申请一份资源,包括进程描述符、进程上下文、进程堆栈、内存信息、打开的文件描述符、进程优先级、根目录、资源限制、控制终端等,拷贝给子进程。
  2. fork函数会返回两次,一次在父进程,另一次在子进程,如果返回值为0,说明是子进程;如果返回值为正数,说明是父进程
  3. fork系统调用只使用SIGCHLD标志位,子进程终止后发送SIGCHLD信号通知父进程;
  4. fork是重量级调用,为子进程创建了一个基于父进程的完整副本,然后子进程基于此运行,为了减少工作量采用写时拷贝技术。子进程只复制父进程的页表,不会复制页面内容,页表的权限为RD-ONLY。当子进程需要写入新内容时会触发写时复制机制,为子进程创建一个副本,并将页表权限修改为RW。
  5. 由于需要修改页表,触发page fault等,因此fork需要mmu的支持

vfork

SYSCALL_DEFINE0(vfork)
{
        struct kernel_clone_args args = {
                .flags          = CLONE_VFORK | CLONE_VM,
                .exit_signal    = SIGCHLD,
        };

        return kernel_clone(&args);
}
  1. 使用vfork()函数创建子进程时, 子进程和父进程有相同的进程地址空间,vfork会将父进程除mm_struct的资源拷贝给子进程,也就是创建子进程时,它的task_struct->mm指向父进程的,父子进程共享一份同样的mm_struct;
  2. vfork会阻塞父进程,直到子进程退出或调用exec释放虚拟内存资源,父进程才会继续执行;
  3. vfork的实现比fork多了两个标志位,分别是CLONE_VFORK和CLONE_VM。CLONE_VFORK表示父进程会被挂起,直至子进程释放虚拟内存资源。CLONE_VM表示父子进程运行在相同的内存空间中;
  4. 由于没有写时拷贝,不需要页表管理,因此vfork不需要MMU

clone

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 unsigned long, tls, 
                 int __user *, child_tidptr)
{
        struct kernel_clone_args args = {
                .flags          = (lower_32_bits(clone_flags) & ~CSIGNAL),
                .pidfd          = parent_tidptr,
                .child_tid      = child_tidptr,
                .parent_tid     = parent_tidptr,
                .exit_signal    = (lower_32_bits(clone_flags) & CSIGNAL),
                .stack          = newsp,
                .tls            = tls, 
        };   

        return kernel_clone(&args);
}                 
  1. 使用clone()创建用户线程时, clone不会申请新的资源,所有线程指向相同的资源,举例:P1创建P2,P2的全部资源指针指向P1,P1和P2指向同样的资源,那么P1和P2就是线程;
  2. 当调用pthread_create时,linux就会执行clone,并通过不同的clone_flags标记,保证p2指向p1相同的资源。
  3. 创建进程和创建线程采用同样的api即kernel_clone,带有标记clone_filag可以指明哪些是要克隆的,哪些不需要克隆的;
  4. 进程是完全不共享父进程资源,线程是完全共享父进程的资源,通过clone_flags标志克隆父进程一部分资源,部分资源与父进程共享,部分资源与父进程不共享,是位于进程和线程间的临界态

1.Linux将进程和线程都采用task_struct进行管理;
2.理解线程要从调度的角度,理解进程要从资源的角度,而相同资源可调度就是线程,线程也称为轻量级进程 lwp

kernel_thread

/*
 * Create a kernel thread.
 */
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
        struct kernel_clone_args args = {
                .flags          = ((lower_32_bits(flags) | CLONE_VM |
                                    CLONE_UNTRACED) & ~CSIGNAL),
                .exit_signal    = (lower_32_bits(flags) & CSIGNAL),
                .stack          = (unsigned long)fn,
                .stack_size     = (unsigned long)arg,
        };

        return kernel_clone(&args);
}
  1. kernel_thread用于创建一个内核线程,它只运行在内核地址空间,且所有内核线程共享相同的内核地址空间,没有独立的进程地址空间,即task_struct->mm为NULL;
  2. 通过kernel_thread创建的内核线程处于不可运行态,需要wake_up_process()来唤醒并调加到就绪队列;kthread_run()是kthread_create和wake_up_process的封装,可创建并唤醒进程;

注:mm和active_mm区别参考https://01.org/linuxgraphics/gfx-docs/drm/vm/active_mm.html

3. 主要数据结构

在这里插入图片描述

user_pt_regs

/*
* User structures for general purpose, floating point and debug registers.
*/
struct user_pt_regs {
__u64           regs[31];
__u64           sp;
__u64           pc;
__u64           pstate;
};

user_pt_regs结构体保存了用户态的cpu寄存器

pt_regs

/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs {
        union {
                struct user_pt_regs user_regs;
                struct {
                        u64 regs[31];
                        u64 sp;
                        u64 pc;
                        u64 pstate;
                };
        };
        u64 orig_x0;
#ifdef __AARCH64EB__
        u32 unused2;
        s32 syscallno;
#else
        s32 syscallno;
        u32 unused2;
#endif

        u64 orig_addr_limit;
        /* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
        u64 pmr_save;
        u64 stackframe[2];

        /* Only valid for some EL1 exceptions. */
        u64 lockdep_hardirqs;
        u64 exit_rcu;
};

pt_regs 约定了栈中寄存器的排列方式,其既可以表示用户态,也可以表示内核态,如果是用户态,从如上pt_regs 注释中可以看出user_regs必须是pt_regs的第一个变量

thread_struct

struct cpu_context {
        unsigned long x19;
        unsigned long x20;
        unsigned long x21;
        unsigned long x22;
        unsigned long x23;
        unsigned long x24;
        unsigned long x25;
        unsigned long x26;
        unsigned long x27;
        unsigned long x28;
        unsigned long fp;
        unsigned long sp;
        unsigned long pc;
};
struct thread_struct {
        struct cpu_context      cpu_context;    /* cpu context */

        /*
         * Whitelisted fields for hardened usercopy:
         * Maintainers must ensure manually that this contains no
         * implicit padding.
         */
        struct {
                unsigned long   tp_value;       /* TLS register */
                unsigned long   tp2_value;
                struct user_fpsimd_state fpsimd_state;
        } uw;

        unsigned int            fpsimd_cpu;
        void                    *sve_state;     /* SVE registers, if any */
        unsigned int            sve_vl;         /* SVE vector length */
        unsigned int            sve_vl_onexec;  /* SVE vl after next exec */
        unsigned long           fault_address;  /* fault info */
        unsigned long           fault_code;     /* ESR_EL1 value */
        struct debug_info       debug;          /* debugging */
#ifdef CONFIG_ARM64_PTR_AUTH
        struct ptrauth_keys_user        keys_user;
        struct ptrauth_keys_kernel      keys_kernel;
#endif
#ifdef CONFIG_ARM64_MTE
        u64                     sctlr_tcf0;
        u64                     gcr_user_incl;
#endif
};

thread_struct 是当前进程的CPU的特定状态,前面的pt_regs最终要转换保存到这里
TLS是Thread Local Storage的缩写,TLS包含了每个线程的局部变量,在运行时,TLS变量是以模块为单位进行分配的,模块是一个共享对象或可执行文件,如下图,TLS段紧挨TP,位于其后
在这里插入图片描述

关于tls可参考:
https://akkadia.org/drepper/tls.pdf
https://android.googlesource.com/platform/bionic/+/master/docs/elf-tls.md

task_stack_page

#define task_stack_page(task)   ((void *)(task)->stack)
#define task_pt_regs(p) \
			((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)

THREAD_SIZE为进程栈大小,因此task_stack_page返回进程栈起始地址的位置

4. kernel_clone

kernel_clone(struct kernel_clone_args *args)
    |--u64 clone_flags = args->flags;
    |  struct completion vfork;
    |  struct task_struct *p;
    |  int trace = 0;
    |--检查子进程是否允许被跟踪
    |  //创建一个进程并返回task_struct指针
    |--p = copy_process(NULL, trace, NUMA_NO_NODE, args);
    |  //获取pid
    |--pid = get_task_pid(p, PIDTYPE_PID);
    |  //获取虚拟的pid
    |  nr = pid_vnr(pid);
    |  if (clone_flags & CLONE_PARENT_SETTID)
    |      put_user(nr, args->parent_tid);
    |--if (clone_flags & CLONE_VFORK)
    |      p->vfork_done = &vfork;
    |      init_completion(&vfork);
    |      get_task_struct(p);
    |  //将进程加入到就绪队列
    |--wake_up_new_task(p);
    |--if (clone_flags & CLONE_VFORK)
    |      //等待子进程调用exec()或exit()
    |      if (!wait_for_vfork_done(p, &vfork))
    |           ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
    |--put_pid(pid);
    \--return nr;

如下为参数kernel_clone_args 的定义:

struct kernel_clone_args {
        /*创建进程的标志位集合*/
        u64 flags;
        
        int __user *pidfd;
        /*指向用户空间子进程ID*/
        int __user *child_tid;
        /*指向用户空间父进程ID*/
        int __user *parent_tid;
        int exit_signal;
        /*用户态栈起始地址*/
        unsigned long stack;
        /*用户态栈大小,通常设置为0*/
        unsigned long stack_size;
        /*线程本地存储(Thread Local Storage)*/
        unsigned long tls;
        pid_t *set_tid;
        /* Number of elements in *set_tid */
        size_t set_tid_size;
        int cgroup;
        struct cgroup *cgrp;
        struct css_set *cset;
};

kernel_clone的唯一的参数为kernel_clone_args类型
首先看下copy_thread中用到的结构体变量

  1. copy_process:创建一个进程并返回task_struct指针

  2. wake_up_new_task:将新创建的进程加入就绪队列,等待调度器调度

  3. wait_for_vfork_done:如果是vfork创建的子进程,保证子进程先运行,调用exec或exit之前,父子进程共享数据,调用exec或exit之后父进程才可以运行,这主要通过wait_for_vfork_done来保证,它会等待子进程调用exec或exit

  4. 返回到用户空间,父进程返回pid,子进程返回0

|- -copy_process

copy_process(struct pid *pid, int trace,int node,struct kernel_clone_args *args)
    |--u64 clone_flags = args->flags;
    |--根据clone_flags进行检查
    |--对相关信号进行发送或延迟发送处理
    |  //分配一个task_struct实例
    |--p = dup_task_struct(current, node);
    |  //复制父进程的证书
    |--retval = copy_creds(p, clone_flags);
    |  //task_struct中的delays成员记录等待的统计数据供用户空间使用
    |--delayacct_tsk_init(p);
    |  //设置进程为非超级用户,非work线程,不能运行标志,相关标识含义见下
    |--p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER | PF_IDLE);
    |  p->flags |= PF_FORKNOEXEC;
    |  //初始化新进程的子进程链表
    |--INIT_LIST_HEAD(&p->children); 
    |  //初始化新进程的兄弟进程链表
    |  INIT_LIST_HEAD(&p->sibling);
    |  //对PREMPT_RCU和TASKS_RCU进程初始化
    |--rcu_copy_process(p);
    |--对从父进程拷贝来的task_struct进行一些重新初始化
    |  //进程调度相关的初始化,并将进程指派到某一个cpu
    |--retval = sched_fork(clone_flags, p);
    |--copy_semundo(clone_flags, p);
    |  //复制父进程打开的文件等信息
    |--copy_files(clone_flags, p);
    |  //复制父进程的fs_struct信息
    |--copy_fs(clone_flags, p);
    |  //复制父进程的信号系统
    |--copy_sighand(clone_flags, p);
    |--copy_signal(clone_flags, p);
    |  //复制父进程的进程地址空间页表信息
    |--copy_mm(clone_flags, p);
    |  //复制父进程命名空间
    |--copy_namespaces(clone_flags, p);
    |  //复制父进程的io相关内容
    |--copy_io(clone_flags, p);
    |--copy_thread(clone_flags, args->stack, args->stack_size, p, args->tls);
    |  //为新进程分配pid数据结构和PID
    |--if (pid != &init_struct_pid)
    |      pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,args->set_tid_size);
    |--p->pid = pid_nr(pid);
    |  //子进程是归属于父进程线程组
    |--if (clone_flags & CLONE_THREAD)
    |      p->group_leader = current->group_leader;
    |      p->tgid = current->tgid;
    |  ////子进程线程组领头进程
    |  else
    |      p->group_leader = p;
    |       p->tgid = p->pid;
    |--将新进程p加入进程管理管理流程

copy_process主要为新进程创建task_struct,复制父进程资源,并分配pid

  1. 根据clone_flags进行检查:对于互斥的clone_flags,报错退出

  2. 对相关信号进行发送或延迟发送处理:强制在此点之前接收到的任何信号在fork发生之前被传送。 收集在fork过程中发生的发送到多个进程的信号,并将它们延迟,使它们看起来在fork后发生

  3. dup_task_struct:分配一个task_struct实例

  4. copy_creds:复制父进程的证书

  5. sched_fork:进程调度相关的初始化,并将进程指派到某一个cpu

  6. copy_files:复制父进程打开的文件等信息

  7. copy_fs:复制父进程的fs_struct信息

  8. copy_sighand:复制父进程的信号系统

  9. copy_mm: 复制父进程的内存空间,如果父进程为线程无单独的内存空间;如果要与父进程共享内存空间则将进程的mm指针指向父进程的即可

  10. copy_io:复制父进程的io相关内容

  11. copy_thread:如果不是内核线程,将父进程的寄存器复制到子进程中,保存在thread_info->cpu_contex
    设置thread_info->cpu_contex的PC为ret_from_fork(),sp为新进程的内核栈

/*
 * Per process flags
 */
#define PF_VCPU                 0x00000001      /* I'm a virtual CPU */
/*Linux4.10引入,引入后空闲进程不仅包含idle进程还包含设置此标识的进程*/
#define PF_IDLE                 0x00000002      /* I am an IDLE thread */
#define PF_EXITING              0x00000004      /* Getting shut down */
#define PF_IO_WORKER            0x00000010      /* Task is an IO worker */
#define PF_WQ_WORKER            0x00000020      /* I'm a workqueue worker */
#define PF_FORKNOEXEC           0x00000040      /* Forked but didn't exec */
#define PF_MCE_PROCESS          0x00000080      /* Process policy on mce errors */
#define PF_SUPERPRIV            0x00000100      /* Used super-user privileges */
#define PF_DUMPCORE             0x00000200      /* Dumped core */
#define PF_SIGNALED             0x00000400      /* Killed by a signal */
#define PF_MEMALLOC             0x00000800      /* Allocating memory */
#define PF_NPROC_EXCEEDED       0x00001000      /* set_user() noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH            0x00002000      /* If unset the fpu must be initialized before use */
#define PF_USED_ASYNC           0x00004000      /* Used async_schedule*(), used by module init */
#define PF_NOFREEZE             0x00008000      /* This thread should not be frozen */
#define PF_FROZEN               0x00010000      /* Frozen for system suspend */
#define PF_KSWAPD               0x00020000      /* I am kswapd */
#define PF_MEMALLOC_NOFS        0x00040000      /* All allocation requests will inherit GFP_NOFS */
#define PF_MEMALLOC_NOIO        0x00080000      /* All allocation requests will inherit GFP_NOIO */
#define PF_LOCAL_THROTTLE       0x00100000      /* Throttle writes only against the bdi I write to,
                                                 * I am cleaning dirty pages from some other bdi. */
#define PF_KTHREAD              0x00200000      /* I am a kernel thread */
#define PF_RANDOMIZE            0x00400000      /* Randomize virtual address space */
#define PF_SWAPWRITE            0x00800000      /* Allowed to write to swap */
#define PF_NO_SETAFFINITY       0x04000000      /* Userland is not allowed to meddle with cpus_mask */
#define PF_MCE_EARLY            0x08000000      /* Early kill for mce process policy */
#define PF_MEMALLOC_NOCMA       0x10000000      /* All allocation request will have _GFP_MOVABLE cleared */
#define PF_FREEZER_SKIP         0x40000000      /* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK         0x80000000      /* This thread called freeze_processes() and should not be frozen */

如上为进程的重要标识位

|- - -根据clone_flags进行检查

根据clone_flags进行检查
    |  //Don't allow sharing the root directory with processes in a different namespace
    |--if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
    |      return ERR_PTR(-EINVAL);
    |--if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
    |      return ERR_PTR(-EINVAL);
    |  //和父进程同一线程组,必须共享父进程的信号
    |--if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
    |      return ERR_PTR(-EINVAL);
    |  //Shared signal handlers imply shared VM
    |--if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
    |      return ERR_PTR(-EINVAL);
    |--if ((clone_flags & CLONE_PARENT) && 
    |                     current->signal->flags & SIGNAL_UNKILLABLE)
    |      return ERR_PTR(-EINVAL);
    |  //If the new process will be in a different pid or user namespace
    |  //do not allow it to share a thread group with the forking task.
    |--if (clone_flags & CLONE_THREAD)
    |      if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
    |           (task_active_pid_ns(current) != nsp->pid_ns_for_children))
    |      return ERR_PTR(-EINVAL);
    |--if (clone_flags & (CLONE_THREAD | CLONE_VM))
    |      if (nsp->time_ns != nsp->time_ns_for_children)
    |      return ERR_PTR(-EINVAL);
    \--if (clone_flags & CLONE_PIDFD)
           if (clone_flags & (CLONE_DETACHED | CLONE_THREAD))
               return ERR_PTR(-EINVAL);

CLONE_NEWNS:表示父子进程不共享mount命名空间,不同命名空间与共享文件系统的CLONE_FS冲突

CLONE_NEWUSER:表示子进程创建新的user命名空间,不同user命名空间与共享文件系统的CLONE_FS冲突?

CLONE_THREAD: 父子进程在同一个线程组,与父子进程相同信号处理表CLONE_SIGHAND,父子进程相同内存描述符CLONE_VM匹配;

CLONE_PARENT:表示创建的进程为兄弟进程,在用户空间祖先进程为init进程,内核空间为idle进程,用户空间只有init进程才会设置SIGNAL_UNKILLABLE(以此推断current为init进程?)而init创建兄弟进程导致无法被init回收

|- - -dup_task_struct

dup_task_struct(struct task_struct *orig, int node)
    |--struct vm_struct *stack_vm_area __maybe_unused;
    |  //分配一个进程描述符
    |--tsk = alloc_task_struct_node(node);
    |  //为新进程分配内核栈空间,对ARM64分配16KB的页面
    |--stack = alloc_thread_stack_node(tsk, node);
    |--memcg_charge_kernel_stack(tsk)
    |--stack_vm_area = task_stack_vm_area(tsk);
    |  //父进程的进程描述符内容直接复制给子进程
    |--arch_dup_task_struct(tsk, orig)
    |  //新进程的stack指向新分配的内核栈
    |--tsk->stack = stack;
    |  tsk->stack_vm_area = stack_vm_area;
    |  //拷贝thread_info
    |--setup_thread_stack(tsk, orig);
    |  clear_user_return_notifier(tsk);
    |  clear_tsk_need_resched(tsk);
    |  //在内核栈最高地址处设置幻数,用于溢出检测
    |  set_task_stack_end_magic(tsk);
    |  refcount_set(&tsk->rcu_users, 2);
    |  refcount_set(&tsk->usage, 1);
    |  account_kernel_stack(tsk, 1);
    |  kcov_task_init(tsk);
    |--return tsk;
    

dup_task_struct分配一个task_struct实例,并将父进程的实例拷贝给子进程

|- - -sched_fork

sched_fork(unsigned long clone_flags, struct task_struct *p)
    |  // 初始化进程调度相关的数据结构
    |--__sched_fork(clone_flags, p);
    |--p->state = TASK_NEW;
    |  //初始化动态优先级
    |--p->prio = current->normal_prio;
    |  //Revert to default priority/policy on fork if requested
    |--if (unlikely(p->sched_reset_on_fork))
    |      if (task_has_dl_policy(p) || task_has_rt_policy(p))
    |          p->policy = SCHED_NORMAL;
    |          p->static_prio = NICE_TO_PRIO(0);
    |          p->rt_priority = 0;
    |      else if (PRIO_TO_NICE(p->static_prio) < 0)
    |          p->static_prio = NICE_TO_PRIO(0);
    |      p->prio = p->normal_prio = __normal_prio(p);
    |      set_load_weight(p, false);
    |      p->sched_reset_on_fork = 0;
    |--if (dl_prio(p->prio))
    |      return -EAGAIN;
    |  else if (rt_prio(p->prio))
    |      p->sched_class = &rt_sched_class;
    |  else
    |      p->sched_class = &fair_sched_class;
    |--init_entity_runnable_average(&p->se);
    |--rseq_migrate(p);
    |--__set_task_cpu(p, smp_processor_id());
    |  //调用调度类中的task_fork方法,如果调度类为CFS,则调用CFS调度器的task_fork_fair,p为新创建的task_struct
    |--if (p->sched_class->task_fork)
    |      p->sched_class->task_fork(p);
    \--init_task_preempt_count(p);

sched_fork初始化与进程调度相关的数据结构,调度实体用sched_entity数据结构来抽象,每个进程或线程都是一个调度实体

  1. __sched_fork: 初始化进程调度相关的数据结构。把新创建的调度实体se相关成员初始化为0,因为这些值不能复用父进程,子进程将来要加入调度器参与调度,与父进程分道扬镳

  2. p->state = TASK_NEW:Linux4.8新增进程状态,状态设置为TASK_NEW,可以保证调度器不会运行此进程,也不会被信号或外部时间唤醒,但是可以插入就绪队列

  3. p->prio = current->normal_prio:在概述部分讨论过进程优先级,此处可以看到进程(无论实时还是非实时)的动态优先级初始化为父进程的普通优先级,动态优先级将参与进程调度,决定调度的优先顺序

  4. __set_task_cpu:为子进程设置运行的CPU,也就是子进程将加入此cpu的运行队列
    关于组调度可参考http://www.wowotech.net/process_management/449.html

  5. p->sched_class->task_fork§:调用调度类中的task_fork方法,完成与调度器相关的初始化,如果调度类为CFS,则调用CFS调度器的task_fork_fair,p为新创建的task_struct

  6. init_task_preempt_count: 初始化preempt _count计数,为0可以抢占当前进程,大于0禁止抢占当前进程,此处初始化为1禁止抢占当前进程

task_fork_fair(struct task_struct *p)
    |--struct cfs_rq *cfs_rq;
    |  //获取新进程task的调度实体
    |  struct sched_entity *se = &p->se, *curr;
    |  //取出当前进程CFS就绪队列
    |  cfs_rq = task_cfs_rq(current);
    |  //获取CFS就绪队列的当前调度实体
    |  curr = cfs_rq->curr;
    |  //更新当前正在运行的调度实体(即父进程)的虚拟运行时间
    |--update_curr(cfs_rq);
    |     |  //delta_exec为当前调度实体从上次调用update_curr到本次调用update_curr的时间差
    |     |--delta_exec = now - curr->exec_start;
    |     |  //计算delta_exec时间内的父进程虚拟运行时间
    |     |--curr->vruntime +=calc_delta_fair(delta_exec, curr) 
    |     |  //更新当前CFS就绪队列的最小虚拟时间min_vruntime
    |     \--update_min_vruntime(cfs_rq)
    |  // 将当前进程(父进程)的虚拟运行时间复制给子进程  
    |--se->vruntime = curr->vruntime;
    |  //基于上步计算的虚拟时间,进一步考虑对新进程增加惩罚的虚拟时间,得出新进程的虚拟时间
    |--place_entity(cfs_rq, se, 1);
    |      |--u64 vruntime = cfs_rq->min_vruntime
    |      |  //新创建进程
    |      |--if (initial && sched_feat(START_DEBIT))
    |      |      //sched_vslice为对新进程计算的惩罚时间
    |      |      vruntime += sched_vslice(cfs_rq, se);
    |      |           |  //计算调度实体应该得到的调度时间作为惩罚时间(delta为sched_slice(cfs_rq, se))
    |      |           \--calc_delta_fair(sched_slice(cfs_rq, se), se) 
    |      |                  |--if (unlikely(se->load.weight != NICE_0_LOAD))
    |      |                  |      //计算调度实体应该得到的调度时间=slice * se->load.weight/se->load
    |      |                  |      delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
    |      |                  \--return delta;
    |      |  //非新建进程
    |      |--if (!initial)
    |      |      unsigned long thresh = sysctl_sched_latency;
    |      |      vruntime -= thresh;
    |      |  //取(最小虚拟时间+惩罚时间)与se->vruntime的最大值作为进程虚拟时间
    |      \--se->vruntime = max_vruntime(se->vruntime, vruntime);
    |--if (sysctl_sched_child_runs_first && curr && entity_before(curr, se))
    |       swap(curr->vruntime, se->vruntime);
    |       //触发抢占场景之一
    |       resched_curr(rq);
    \--se->vruntime -= cfs_rq->min_vruntime

如果调度类为CFS,则调用CFS调度器的task_fork_fair,p为新创建的task_struct

  1. update_curr: 更新当前正在运行的调度实体(即父进程)的虚拟运行时间。通过获取当前就绪队列的clock_task值,并计算与上次调用update_curr的时间差。
    (1)calc_delta_fair:计算delta_exec时间差内的父进程虚拟运行时间
    (2)update_min_vruntime:更新当前CFS就绪队列的最小虚拟时间min_vruntime,min_vruntime也是不断更新的,主要就是跟踪就绪队列中所有调度实体的最小虚拟时间。如果min_vruntime一直不更新的话,由于min_vruntime太小,导致后面创建的新进程根据这个值来初始化新进程的虚拟时间,新创建的进程有可能疯狂占据CPU。

  2. se->vruntime = curr->vruntime: 初始化新进程的虚拟运行时间为父进程的虚拟运行时间

  3. place_entity:基于上步计算的虚拟时间,进一步考虑对新进程增加惩罚的虚拟时间,得出新进程的虚拟运行时间。
    根据sched_slice得到的调度时间计算虚拟时间,此虚拟时间作为对新进程的惩罚,也是为了防止新进程恶意占用CPU。如果是新创建进程调用该函数的话,参数initial参数是1。因此这里是处理创建的进程,针对刚创建的进程会进行一定的惩罚,将虚拟时间加上一个值就是惩罚,毕竟虚拟时间越小越容易被调度执行。惩罚的时间由sched_vslice()计算

  4. se->vruntime -= cfs_rq->min_vruntime:对于新创建的进程将vruntime减去min_vruntime,后面在入队就绪队列时会重新加回

sched_slice(cfs_rq, se)
    |  //根据就绪队列调度实体个数计算调度周期(时间片)
    |--slice=__sched_period(cfs_rq->nr_running + !se->on_rq)
    |  //针对没有使能组调度的情况下,for_each_sched_entity(se)就是for (; se; se = NULL),循环一次
    |--for_each_sched_entity(se)
    |      cfs_rq = cfs_rq_of(se);
    |      //得到就绪队列的权重,也就是就绪队列上所有调度实体权重之和
    |      load = &cfs_rq->load;
    |      //计算调度实体应该得到的调度时间=slice * se->load.weight/load
    |      slice = __calc_delta(slice, se->load.weight, load);
    \--return slice;

sched_slice根据计算的就绪队列调度周期,计算出调度实体se的调度时间片

  1. __sched_period
    计算CFS就绪队列一个调度周期的长度,可以理解为调度周期的时间片,会根据就绪进程的数目来计算
    (1).默认调度时间片sysctl_sched_latency为6ms
    (2).当进程数>8,时间片为最小调度延时乘以进程数,否则用系统默认时间片

  2. __calc_delta()
    计算调度实体se的权重占整个就绪队列权重的比例,然后乘以调度周期时间即可得到当前调度实体应该运行的时间(参数weight传递调度实体se权重,参数lw传递就绪队列权重cfs_rq->load)。例如,就绪队列权重是3072,当前调度实体se权重是1024,调度周期是6ms,那么调度实体应该得到的时间是6*1024/3072=2ms

|- - -copy_files

copy_files(unsigned long clone_flags, struct task_struct *tsk)
     |  //获取父进程打开文件描述符表
     |--oldf = current->files;
     |  //与父进程共享打开文件描述符表,增加引用计数后退出
     |--if (clone_flags & CLONE_FILES)
     |      atomic_inc(&oldf->count);
     |      goto out;
     |  //将当前进程的打开文件描述符表拷贝给子进程
     \--newf = dup_fd(oldf, NR_OPEN_MAX, &error);
        tsk->files = newf;

如果父子进程共享files_struct则只是增加引用计数,否则将父进程的files_struct拷贝给子进程

|- - -copy_fs

copy_fs(unsigned long clone_flags, struct task_struct *tsk)
    |--struct fs_struct *fs = current->fs;
    |  //与父进程共享根目录和当前目录,增加引用计数后退出
    |--if (clone_flags & CLONE_FS)
    |      fs->users++;
    |      return 0;
    |  //拷贝父进程的fs_struct到子进程
    \--tsk->fs = copy_fs_struct(fs);

如果父子进程共享fs_struct则只是增加引用计数,否则将父进程的fs_struct拷贝给子进程

|- - -copy_mm

copy_mm(unsigned long clone_flags, struct task_struct *tsk)
    |--struct mm_struct *mm, *oldmm;
    |--tsk->min_flt = tsk->maj_flt = 0;
    |  tsk->nvcsw = tsk->nivcsw = 0;
    |  tsk->mm = NULL;
    |  tsk->active_mm = NULL;
    |  //mm为空则为内核线程,无用户地址空间
    |--oldmm = current->mm;
    |  if (!oldmm)
    |      return 0;
    |  //initialize the new vmacache entries
    |--vmacache_flush(tsk);
    |  //父子进程共享内存描述符和所有的页表,是vfork创建子进程
    |--if (clone_flags & CLONE_VM)
    |      mmget(oldmm);
    |      mm = oldmm;
    |      goto good_mm;
    |--mm = dup_mm(tsk, current->mm);
good_mm:
    \--tsk->mm = mm;
       tsk->active_mm = mm;

如果父子进程共享mm,如vfork创建子进程,则只是增加引用计数,否则将父进程的mm_struct通过dup_mm拷贝给子进程。dup_mm会通过allocate_mm为子进程创建mm内存描述符,并将父进程的mm拷贝给子进程的mm,mm_init会为子进程创建pgd, dup_mm->dup_mmap会遍历父进程所有的VMAs,并为子进程创建对应VMA,将所有父进程VMA对应的pte页表项复制到新进程的VMA对应的pte页表项
注:只复制pte页表项,并未复制vma对应页面内容

dup_mm(tsk, current->mm);
    |--mm = allocate_mm()
    |--memcpy(mm, oldmm, sizeof(*mm))
    |--mm_init(mm, tsk, mm->user_ns)
    |      |--mm_alloc_pgd(mm)
    |      |--init_new_context(p, mm)
    |      |--mm->user_ns = get_user_ns(user_ns)
    \--dup_mmap(mm, oldmm)
           |--struct vm_area_struct *mpnt, *tmp;
           |--for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next)
                  //分配新的vma
                  tmp = vm_area_dup(mpnt);
                  tmp->vm_mm = mm;
                  vma_dup_policy(mpnt, tmp)
                  //创建属于子进程的anon_vma数据结构
                  anon_vma_fork(tmp, mpnt)
                  //把新创建的vma插入到子进程的mm中
                  __vma_link_rb(mm, tmp, rb_link, rb_parent)
                  //拷贝页表项到子进程
                  copy_page_range(tmp, mpnt);
                  rb_parent = &tmp->vm_rb;

dup_mm会通过allocate_mm为子进程创建mm内存描述符,并将父进程的mm拷贝给子进程的mm,mm_init会为子进程创建pgd, dup_mm->dup_mmap会遍历父进程所有的VMAs,并为子进程创建对应VMA,将所有父进程VMA对应的pte页表项复制到新进程的VMA对应的pte页表项。其中copy_page_range->copy_pte_range->copy_one_pte最能体现写时复制的精华。

copy_one_pte中会判断父进程vma属性,如果是一个写时复制映射,即没有设置VM_SHARED,那么父进程和子进程对应的PTE都要设置为写保护,通过pte_wrprotect函数设置为PTE属性为只读

|- - -copy_thread

copy_thread(clone_flags, stack_start, stk_sz, struct task_struct *p, unsigned long tls)
    |--struct pt_regs *childregs = task_pt_regs(p);
    |  //对用户进程的处理
    |--if (likely(!(p->flags & PF_KTHREAD)))
    |      //获取父进程的pt regs复制给子进程
    |      *childregs = *current_pt_regs();
    |      //此处将x0置为0,就保证了子进程返回用户空间时的返回值为0,通过x0来传递返回值
    |      childregs->regs[0] = 0;
    |      //Read the current TLS pointer from tpidr_el0
    |      *task_user_tls(p) = read_sysreg(tpidr_el0);
    |      childregs->sp = stack_start;
    |      if (clone_flags & CLONE_SETTLS)
    |           p->thread.uw.tp_value = tls;
    |  //对内核线程的处理
    |  else
    |      memset(childregs, 0, sizeof(struct pt_regs));
    |      childregs->pstate = PSR_MODE_EL1h;
    |      p->thread.cpu_context.x19 = stack_start;
    |      p->thread.cpu_context.x20 = stk_sz;
    \--p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
       p->thread.cpu_context.sp = (unsigned long)childregs;
    

copy_thread:如果是用户线程,将父进程的pt regs复制给子进程,设置栈指针

  1. task_pt_regs:task_pt_regs为进程栈顶的位置,它保存了cpu的寄存器上下文

  2. 对于用户进程:read_sysreg(tpidr_el0)通过tpidr_el0寄存器可以查到当前进程的TLS 指针,存放到thread.uw.tp_value中

  3. 对于内核线程:x19保存了栈起始地址,x20保存了栈大小

  4. 更新子进程的PC为ret_from_fork,更新子进程的SP为childregs,这样子进程在加入就绪队列后就会从PC指向的地址即ret_from_fork开始执行

/*      
 * This is how we return from a fork.
 */     
SYM_CODE_START(ret_from_fork)
        bl      schedule_tail
        cbz     x19, 1f                         // not a kernel thread
        mov     x0, x20
        blr     x19
1:      get_current_task tsk
        b       ret_to_user
SYM_CODE_END(ret_from_fork)
NOKPROBE(ret_from_fork)

子进程在经过kernel_clone后会加入到就绪队列,当得到调度执行时会执行PC指向的函数指针,也就是ret_from_fork,ret_from_fork中通过检查x19来区分用户进程还是内核线程,如果是内核线程将跳转到x19,即内核线程处理函数;如果是用户进程,则会跳转到ret_to_user来返回到用户空间,对于新创建的用户进程,ret_to_user会返回到用户空间中调用fork或clone系统调用的的下一条指令,这里可参考glibc中clone或fork函数的反汇编实现。

/*
 * "slow" syscall return path.
 */
SYM_CODE_START_LOCAL(ret_to_user)
        disable_daif
        gic_prio_kentry_setup tmp=x3
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        ldr     x19, [tsk, #TSK_TI_FLAGS]
        and     x2, x19, #_TIF_WORK_MASK
        cbnz    x2, work_pending
finish_ret_to_user:
        user_enter_irqoff
        /* Ignore asynchronous tag check faults in the uaccess routines */
        clear_mte_async_tcf
        enable_step_tsk x19, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
        bl      stackleak_erase
#endif
        kernel_exit 0

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
        mov     x0, sp                          // 'regs'
        mov     x1, x19
        bl      do_notify_resume
        ldr     x19, [tsk, #TSK_TI_FLAGS]       // re-check for single-step
        b       finish_ret_to_user
SYM_CODE_END(ret_to_user)

关于ret_to_user的实现可进一步追踪(todo)

|- - -将新进程p添加到进程管理流程(todo)

将新进程p添加到进程管理流程
    |--if (likely(p->pid)) {
                ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

                init_task_pid(p, PIDTYPE_PID, pid);
                //线程组领头进程
                if (thread_group_leader(p)) {
                        init_task_pid(p, PIDTYPE_TGID, pid);
                        init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
                        init_task_pid(p, PIDTYPE_SID, task_session(current));

                        if (is_child_reaper(pid)) {
                                ns_of_pid(pid)->child_reaper = p;
                                p->signal->flags |= SIGNAL_UNKILLABLE;
                        }
                        p->signal->shared_pending.signal = delayed.signal;
                        p->signal->tty = tty_kref_get(current->signal->tty);
                        /*
                         * Inherit has_child_subreaper flag under the same
                         * tasklist_lock with adding child to the process tree
                         * for propagate_has_child_subreaper optimization.
                         */
                        p->signal->has_child_subreaper = p->real_parent->signal->has_child_subreaper ||
                                                         p->real_parent->signal->is_child_subreaper;
                        list_add_tail(&p->sibling, &p->real_parent->children);
                        list_add_tail_rcu(&p->tasks, &init_task.tasks);
                        attach_pid(p, PIDTYPE_TGID);
                        attach_pid(p, PIDTYPE_PGID);
                        attach_pid(p, PIDTYPE_SID);
                        __this_cpu_inc(process_counts);
                //非线程组领头进程
                } else {
                        current->signal->nr_threads++;
                        atomic_inc(&current->signal->live);
                        refcount_inc(&current->signal->sigcnt);
                        task_join_group_stop(p);
                        list_add_tail_rcu(&p->thread_group,
                                          &p->group_leader->thread_group);
                        list_add_tail_rcu(&p->thread_node,
                                          &p->signal->thread_head);
                }
                attach_pid(p, PIDTYPE_PID);
                nr_threads++;
        }

|- -wake_up_new_task

wake_up_new_task(struct task_struct *p)
    |--p->state = TASK_RUNNING;
    |--p->recent_used_cpu = task_cpu(p)
    |  //select_task_rq会调用CFS的select_task_rq来选择一个调度域最闲的CPU
    |--__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
    |--activate_task(rq, p, ENQUEUE_NOCLOCK);
    |--check_preempt_curr(rq, p, WF_FORK);
    |      \--check_preempt_wakeup() 假设为CFS调度器
    |             |  //检查唤醒的进程是否满足抢占当前进程的条件
    |             |--if (wakeup_preempt_entity(se, pse) == 1)
    |             |       if (!next_buddy_marked)
    |             |           set_next_buddy(pse);
    |             |           goto preempt;
preempt:         |  //如果可以抢占当前进程,设置TIF_NEED_RESCHED flag
                  \--resched_curr(rq)
    

wake_up_new_task 将新创建的进程加入就绪队列,等待调度器调度

  1. set_task_cpu: 将select_task_rq获取的CPU绑定到新的进程, 此处重新设置CPU的原因是在fork新进程的过程中,cpus_allowed有可能发生变化,另外一个原因是之前选择的CPU有可能已经关闭了,因此重新选择CPU

  2. activate_task:将进程加入就绪队列,通过调用调度类中enqueue_task方法

  3. check_preempt_curr:既然新进程已经准备就绪,那么此时需要检查新进程是否满足抢占当前正在运行进程的条件,如果满足抢占条件需要设置当前进程TIF_NEED_RESCHED标志位。
    当唤醒一个新进程的时候,此时也是一个检测抢占的机会。因为唤醒的进程有可能具有更高的优先级或者更小的虚拟时间,分两种情况:
    (1).唤醒的进程和当前的进程同属于一个调度类,直接调用调度类的check_preempt_curr方法检查抢占条件。毕竟调度器自己管理的进程,自己最清楚是否适合抢占当前进程。
    (2).如果唤醒的进程和当前进程不属于一个调度类,就需要比较调度类的优先级。例如,当期进程是CFS调度类,唤醒的进程是RT调度类,自然实时进程是需要抢占当前进程的,因为优先级更高。

wakeup_preempt_entity()函数检查唤醒的进程是否满足抢占当前进程的条件,可以返回3种结果:
(1)如果curr虚拟时间比se小,返回-1;
(2)如果curr虚拟时间比se大,并且两者差值小于gran,返回0;
(3)否则返回1
默认情况下,wakeup_gran()函数返回的值是1ms根据调度实体se的权重计算的虚拟时间。因此,满足抢占的条件就是,唤醒的进程的虚拟时间首先要比正在运行进程的虚拟时间小,并且差值还要小于一定的值才行(这个值是sysctl_sched_wakeup_granularity,称作唤醒抢占粒度)。这样做的目的是避免抢占过于频繁,导致大量上下文切换影响系统性能。

activate_task(struct rq *rq, struct task_struct *p, int flags)
    |--enqueue_task(rq, p, flags);
    |      |--if (!(flags & ENQUEUE_NOCLOCK))
    |      |      //更新cpu运行队列的clock
    |      |      update_rq_clock(rq);
    |      |--if (!(flags & ENQUEUE_RESTORE))
    |      |      sched_info_queued(rq, p);
    |      |      psi_enqueue(p, flags & ENQUEUE_WAKEUP);
    |      |  //把新进程加入到CFS就绪队列中
    |      |--enqueue_task_fair(rq, p,flags)
    |             |--struct sched_entity *se = &p->se;
    |             |  //对于没有定义FAIR_SCHED_GROUP的指的是调度实体,只循环一次
    |             |--for_each_sched_entity(se)
    |             |       |  //把调度实体加入到CFS就绪队列中
    |             |       \--enqueue_entity(cfs_rq, se, flags) 
    |             |              |  //新创建的进程需要加上min_vruntim
    |             |              |--se->vruntime += cfs_rq->min_vruntime
    |             |              |  //更新当前进程(父进程)的vruntime和CFS就绪队列的min_vruntime
    |             |              |--update_curr(cfs_rq)
    |             |              |  //When enqueuing a sched_entity, we must:
    |             |              |  //Update task and its cfs_rq load average
    |             |              |--update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
    |             |              |  //Add its load to cfs_rq->runnable_avg
    |             |              |  se_update_runnable(se);
    |             |              |  //For group_entity, update its weight to reflect the new share of its group cfs_rq
    |             |              |  update_cfs_group(se);
    |             |              |  //Add its new weight to cfs_rq->load.weight
    |             |              |--account_entity_enqueue(cfs_rq, se) 
    |             |              |--if (flags & ENQUEUE_WAKEUP)
    |             |              |      place_entity(cfs_rq, se, 0) 
    |             |              |  //将se加入就绪队列维护的红黑树中,所有的se以vruntime为key
    |             |              |--__enqueue_entity(cfs_rq, se) 
    |             |              \--se->on_rq = 1//调度实体已经加入到CFS就绪队列中,置位
    |             \--for_each_sched_entity(se)
    |                    cfs_rq = cfs_rq_of(se)
    |                    //更新该调度实体的负载load_avg_contrib和就绪队列的负载runnable_load_avg
    |                    update_load_avg(cfs_rq, se, UPDATE_TG);
    |                    se_update_runnable(se);
    |                    update_cfs_group(se);
    \--p->on_rq = TASK_ON_RQ_QUEUED

以CFS为例,来说明activate_task的执行过程,activate_task将进程加入就绪队列,通过调用调度类中enqueue_task方法
,此处对于CFS来说就是enqueue_task_fair

  1. se->vruntime += cfs_rq->min_vruntime:新创建的进程需要加上min_vruntim,因为之前task_fork_fair在计算新进程虚拟时间时会减去min_vruntime。每个就绪队列的min_vruntime可能发生变化,因此此时加较准确

  2. When enqueuing a sched_entity, we must:(todo)
    -Update loads to have both entity and cfs_rq synced with now.
    -Add its load to cfs_rq->runnable_avg
    -For group_entity, update its weight to reflect the new share of its group cfs_rq
    -Add its new weight to cfs_rq->load.weight

  3. place_entity(cfs_rq, se, 0) :针对唤醒的进程(flag有ENQUEUE_WAKEUP标识),我们是需要根据情况给予一定的补偿。
    当然这里针对新进程第一次加入就绪队列是不需要调用的

5. 关于fork/vfork/clone的返回

fork/vfork/clone系统调用一共会返回两次,以fork为例,一次是从父进程的fork中返回,返回值为子进程的pid;
由于子进程加入到就绪队列后也会得到调度执行,copy_thread设置的子进程pc值ret_from_fork,因此第二次返回就是从子进程返回。根据copy_thread一节中的介绍,copy_thread会将子进程的cpu上下文x0清0,因此返回值就是0.

参考文献

奔跑吧,Linux内核
ULK
ULA

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值