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
}
- 使用fork()函数创建子进程时,子进程和父进程有各自独立的进程地址空间,fork后会重新申请一份资源,包括进程描述符、进程上下文、进程堆栈、内存信息、打开的文件描述符、进程优先级、根目录、资源限制、控制终端等,拷贝给子进程。
- fork函数会返回两次,一次在父进程,另一次在子进程,如果返回值为0,说明是子进程;如果返回值为正数,说明是父进程
- fork系统调用只使用SIGCHLD标志位,子进程终止后发送SIGCHLD信号通知父进程;
- fork是重量级调用,为子进程创建了一个基于父进程的完整副本,然后子进程基于此运行,为了减少工作量采用写时拷贝技术。子进程只复制父进程的页表,不会复制页面内容,页表的权限为RD-ONLY。当子进程需要写入新内容时会触发写时复制机制,为子进程创建一个副本,并将页表权限修改为RW。
- 由于需要修改页表,触发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);
}
- 使用vfork()函数创建子进程时, 子进程和父进程有相同的进程地址空间,vfork会将父进程除mm_struct的资源拷贝给子进程,也就是创建子进程时,它的task_struct->mm指向父进程的,父子进程共享一份同样的mm_struct;
- vfork会阻塞父进程,直到子进程退出或调用exec释放虚拟内存资源,父进程才会继续执行;
- vfork的实现比fork多了两个标志位,分别是CLONE_VFORK和CLONE_VM。CLONE_VFORK表示父进程会被挂起,直至子进程释放虚拟内存资源。CLONE_VM表示父子进程运行在相同的内存空间中;
- 由于没有写时拷贝,不需要页表管理,因此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);
}
- 使用clone()创建用户线程时, clone不会申请新的资源,所有线程指向相同的资源,举例:P1创建P2,P2的全部资源指针指向P1,P1和P2指向同样的资源,那么P1和P2就是线程;
- 当调用pthread_create时,linux就会执行clone,并通过不同的clone_flags标记,保证p2指向p1相同的资源。
- 创建进程和创建线程采用同样的api即kernel_clone,带有标记clone_filag可以指明哪些是要克隆的,哪些不需要克隆的;
- 进程是完全不共享父进程资源,线程是完全共享父进程的资源,通过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);
}
- kernel_thread用于创建一个内核线程,它只运行在内核地址空间,且所有内核线程共享相同的内核地址空间,没有独立的进程地址空间,即task_struct->mm为NULL;
- 通过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中用到的结构体变量
-
copy_process:创建一个进程并返回task_struct指针
-
wake_up_new_task:将新创建的进程加入就绪队列,等待调度器调度
-
wait_for_vfork_done:如果是vfork创建的子进程,保证子进程先运行,调用exec或exit之前,父子进程共享数据,调用exec或exit之后父进程才可以运行,这主要通过wait_for_vfork_done来保证,它会等待子进程调用exec或exit
-
返回到用户空间,父进程返回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
-
根据clone_flags进行检查:对于互斥的clone_flags,报错退出
-
对相关信号进行发送或延迟发送处理:强制在此点之前接收到的任何信号在fork发生之前被传送。 收集在fork过程中发生的发送到多个进程的信号,并将它们延迟,使它们看起来在fork后发生
-
dup_task_struct:分配一个task_struct实例
-
copy_creds:复制父进程的证书
-
sched_fork:进程调度相关的初始化,并将进程指派到某一个cpu
-
copy_files:复制父进程打开的文件等信息
-
copy_fs:复制父进程的fs_struct信息
-
copy_sighand:复制父进程的信号系统
-
copy_mm: 复制父进程的内存空间,如果父进程为线程无单独的内存空间;如果要与父进程共享内存空间则将进程的mm指针指向父进程的即可
-
copy_io:复制父进程的io相关内容
-
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数据结构来抽象,每个进程或线程都是一个调度实体
-
__sched_fork: 初始化进程调度相关的数据结构。把新创建的调度实体se相关成员初始化为0,因为这些值不能复用父进程,子进程将来要加入调度器参与调度,与父进程分道扬镳
-
p->state = TASK_NEW:Linux4.8新增进程状态,状态设置为TASK_NEW,可以保证调度器不会运行此进程,也不会被信号或外部时间唤醒,但是可以插入就绪队列
-
p->prio = current->normal_prio:在概述部分讨论过进程优先级,此处可以看到进程(无论实时还是非实时)的动态优先级初始化为父进程的普通优先级,动态优先级将参与进程调度,决定调度的优先顺序
-
__set_task_cpu:为子进程设置运行的CPU,也就是子进程将加入此cpu的运行队列
关于组调度可参考http://www.wowotech.net/process_management/449.html -
p->sched_class->task_fork§:调用调度类中的task_fork方法,完成与调度器相关的初始化,如果调度类为CFS,则调用CFS调度器的task_fork_fair,p为新创建的task_struct
-
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
-
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。 -
se->vruntime = curr->vruntime: 初始化新进程的虚拟运行时间为父进程的虚拟运行时间
-
place_entity:基于上步计算的虚拟时间,进一步考虑对新进程增加惩罚的虚拟时间,得出新进程的虚拟运行时间。
根据sched_slice得到的调度时间计算虚拟时间,此虚拟时间作为对新进程的惩罚,也是为了防止新进程恶意占用CPU。如果是新创建进程调用该函数的话,参数initial参数是1。因此这里是处理创建的进程,针对刚创建的进程会进行一定的惩罚,将虚拟时间加上一个值就是惩罚,毕竟虚拟时间越小越容易被调度执行。惩罚的时间由sched_vslice()计算 -
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的调度时间片
-
__sched_period
计算CFS就绪队列一个调度周期的长度,可以理解为调度周期的时间片,会根据就绪进程的数目来计算
(1).默认调度时间片sysctl_sched_latency为6ms
(2).当进程数>8,时间片为最小调度延时乘以进程数,否则用系统默认时间片 -
__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复制给子进程,设置栈指针
-
task_pt_regs:task_pt_regs为进程栈顶的位置,它保存了cpu的寄存器上下文
-
对于用户进程:read_sysreg(tpidr_el0)通过tpidr_el0寄存器可以查到当前进程的TLS 指针,存放到thread.uw.tp_value中
-
对于内核线程:x19保存了栈起始地址,x20保存了栈大小
-
更新子进程的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(¤t->signal->live);
refcount_inc(¤t->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 将新创建的进程加入就绪队列,等待调度器调度
-
set_task_cpu: 将select_task_rq获取的CPU绑定到新的进程, 此处重新设置CPU的原因是在fork新进程的过程中,cpus_allowed有可能发生变化,另外一个原因是之前选择的CPU有可能已经关闭了,因此重新选择CPU
-
activate_task:将进程加入就绪队列,通过调用调度类中enqueue_task方法
-
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
-
se->vruntime += cfs_rq->min_vruntime:新创建的进程需要加上min_vruntim,因为之前task_fork_fair在计算新进程虚拟时间时会减去min_vruntime。每个就绪队列的min_vruntime可能发生变化,因此此时加较准确
-
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 -
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