进程模型7-进程创建
内核版本 | 架构 | 作者 | GitHub | CSDN |
---|---|---|---|---|
Linux-4.1.5 | armv7-A | Lux1206 |
进程管理是个复杂的过程,包括进程PID管理、进程优先级、创建、启动、销毁、进程调度/抢占、进程切换、调度策略、负载均衡等等。本篇主要介绍进程的进程的创建,梳理内核中创建一个进程涉及很多的重要步骤。
Linux 系统中所有进程的祖先都是 0号进程(init_task ),同时也仅有 0号进程是通过静态方式创建产生,在0号进程产生后,会作为后续所有进/线程的最初模板,然后以动态创建的方式产生 1号进程和 2号进程,1号进程和 2号进程动态创建是以 0号进程作为模版,并加以修改之后分别作为用户空间的根进程和内核空间的根进程。
进程和线程
Linux 系统中并没有对线程和进程做严格的区别,不论是数据结构还是创建流程,内核都是将两者视为相同,线程就是轻量级的进程,两者合称为 task。进程作为内核资源分配的最小单位,每个进程拥有独立的虚拟内存空间和上下文环境,可以独立的执行,与其他进程在内存物理地址上隔离。而线程是内核调度的最小单位,他也有自己的内核栈,用户栈(用户线程)以及寄存器,但是他们共享线程组中主进程的内存空间,也共享同一组文件描述符、信号处理器和进程调度器等,在一定程度上一个进程可以访问到本线程组中其他线程的内存资源。
进程创建
线程是轻量级的进程,在数据结构和创建流程上是一致的。对用用户空间来说进/线程创建是通过 fork / vfork / clone 三个系统调用创建完成,而内核空间的内核线程则是通过 kernel_thread 函数创建。
进程创建的四种方式
进程的克隆flag
内核中线程、进程共用创建流程,他们的需要体现出各自的差异性,内核为了满足这种需求,通过参数实现面向对象的多态,在创建时使用 clone_flags 以 mask 掩码的形式指明行为,调用 do_fork 函数统一创建,在创建过程中根据标志位产生差异,常用标志如下表所示。
内核版本 | 架构 |
---|---|
CLONE_VM | 共享内存描述符和所有页表 |
CLONE_FS | 共享文件根目录和当前所在目录的表 |
CLONE_FILES | 共享所有打开的文件 |
CLONE_SIGHAND | 共享父进程信号处理表、阻塞表和挂起表,需要与CLONE_VM同时设置 |
CLONE_PTRACE | 标识父进程被跟踪时,子进程也会被跟踪 |
CLONE_VFORK | vfrok专用,表示父进程会被阻塞,直到子进程退出后父进程才能运行 |
CLONE_PARENT | 表示子进程与创建者拥有相同的父进程 |
CLONE_THREAD | 加入父进程所在的线程组,需要与CLONE_SIGHAND同时设置 |
CLONE_NEWNS | 表示未子进程创建单独命名空间 |
SIGCHLD | 表示子进程结束后需要向父进程发送信号通知 |
用户进程系统调用
fork
fork 系统调用,最终在内核中调用 do_fork 函数,flags 传入 SIGCHLD 标志,表明在子进程终止后将发送 SIGCHLD 信号通知父进程。
- fork 创建子进程后无法保证子进程优先运行;
- fork 创建子进程后使用 copy-on-write 技术延迟物理内存分配;
在创建子进程时只为子进程创建虚拟内存结构,用来复制于父进程的虚拟内存结构,但是不会立即为其分配物理页内存,即:父子进程虚拟地址空间独立,但是虚拟地址空间映射到同一片物理内存上。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
vfork
fork 系统调用,最终在内核中调用 do_fork 函数,flags 传入 CLONE_VFORK | CLONE_VM | SIGCHLD 标志,CLONE_VFORK 会在创建进程过程中将父进程加入 waitqueue,使子进程优先运行,CLONE_VM 则是知识子进程共有父进程虚拟内存空间。
- 保证子进程优先级运行;
- 直接使用父进程虚拟空间;
内核通过浅拷贝的方式,直接共享了父进程的虚拟空间,也共享了父进程的物理内存空间。即:父子进程既共享虚拟地址空间,又共享物理内存空间。
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
clone
clone 系统调用是一种更加灵活的系统调用,直接调用 do_fork 函数,用户可以根据需要设置对应的参数,应用层中最常用的 pthread 库中创建线程就是使用此函数调用实现,pthread_create() 中创建型的线程时,传递的 clone flags 标志为 CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,线程间共享主进程的虚拟内存空间,共享文件描述符,共享所有打开的文件,共享信号处理。
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);
}
内核线程创建
而在内核中创建内核线程使用的函数为 kernel_thread(),也是对 do_fork() 的封装,同时将内核线程的执行函数和函数参数通过 do_fork() 的 stack_start 和 stack_size 参数传递到内核,然后在线程的入口函数 ret_from_fork 中判断是为线程时则进入内核线程函数中开始 loop 执行。
kernel_thread
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL);
}
写时拷贝
linux 内核中子进程的用户栈、堆、数据段、代码段都是共用父进程的,如果在 fork 中直接为子进程分配物理内存页,会占用大量物理内存同时带来较大性能开销,如果fork后子进程立马调用 exec 函数来加载新的程序,就会导致之前 fork 中进行的物理内存页分配变得毫无意义。所以内核引入Copy-On_Write(写时复制)技术,让内核在 fork 过程中只建立虚拟内存描述符、复制父进程物理页表,而不会为子进程分配独立的物理内存页,让子进程的地址空间指向父进程物理内存页。当父进程或子进程试图写入共享区域的某个页面,那么就会为产生缺页中断,为这个进程创建该页面的新副本。
初始状态
当父进程调用 fork() 时,子进程在 copy_mm 中将父进程物理内存页表拷贝到子进程中,让子进程共享父进程的物理内存页,将他们的内存页标记为只读。
触发写保护
当父进程或子进程尝试写入某个物理内存页时,由于该页是只读的,会触发页面保护异常(page fault)。
处理写保护异常
操作系统捕获这个异常,并执行以下步骤:
ⅰ. 分配一个新的物理内存页。
ⅱ. 将原来只读内存页的内容复制到新的物理页中。
ⅲ. 更新当前进程的页表,使该虚拟地址指向新的物理页。
ⅳ. 将新的物理页设置为可写。
这样,只有试图写入的内存页会被复制,其他未被修改的内存页依然是共享的和只读的。
代码分析
数据结构
进程描述符
不论是线程还是进程都是使用 task_struct 结构进行描述,task_struct 是一个庞大的结构,将所有与进程相关的信息放在一起统一管理,如下是经过删减后常用的成员。
/* 进程描述符:存放在内核栈栈底 */
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped. 进程状态, 调度器只会选择 TASK_RUNNING 状态下的任务进行调度 */
void *stack; /* 内核栈指针指向栈底位置,进程从用户空间进入内核空间时使用,指向union thread_union(struct thread_info)结构体 */
atomic_t usage; /* 进程描述符使用计数,被置为2时,表示进程描述符正在被使用而且其相应的进程处于活动状态 */
unsigned int flags; /* per process flags, defined below,进程标记,不同于进程状态,标记了进程的一些属性如:PF_SUPERPRIV */
unsigned int ptrace; /* 调试跟踪状态,0表示不需要被跟踪,例如PT_PTRACED */
#ifdef CONFIG_SMP
struct llist_node wake_entry;
int on_cpu; /* 标识进程占用cpu */
struct task_struct *last_wakee;
unsigned long wakee_flips;
unsigned long wakee_flip_decay_ts;
int wake_cpu;
#endif
int on_rq; /* 标识进程在运行队列中 */
/* 进程优先级越小优先级越高
* prio: 动态优先级,是调度器使用的优先级,在特殊情况下内核会提高进程优先级,正常情况下prio=normal_prio
* static_prio: 静态优先级,static_prio = 100 + nice + 20 (nice值为-20~19,所以static_prio值为100~139)
* normal_prio: 没有受优先级继承影响的常规优先级,具体见normal_prio函数,普通进程时为static_prio,实时进程时为MAX_RT_PRIO - 1-rt_priority
* 影响调度器优先级的参数作用情况:
* nice --> static_prio --> normal_prio = prio(调度器使用但是可能会被调整)
* rt_priority --> normal_prio = prio(调度器使用但是可能会被调整)
*/
int prio, static_prio, normal_prio;
unsigned int rt_priority; /* 实时进程优先级计算数,值越大优先级越高,真正的实施优先级=MAX_RT_PRIO - 1 - rt_priority */
const struct sched_class *sched_class; /* 调度类,具有调度行为所需要的函数指针,stop_sched_class(优先级最高,可中断其他进程)>rt_sched_class(实时进程)>fair_sched_class(普通进程)>idle_sched_class(空闲进程) */
struct sched_entity se; /* 普通进程调用实体 */
struct sched_rt_entity rt; /* 实时进程调用实体 */
#ifdef CONFIG_CGROUP_SCHED
struct task_group *sched_task_group;
#endif
struct sched_dl_entity dl; /* dl进程调用实体 */
...
unsigned int policy; /* 调度策略 */
int nr_cpus_allowed;
cpumask_t cpus_allowed; /* 系统中CPU掩码,控制进行可以在哪些处理器上运行 */
...
struct list_head tasks; /* 进程链表(所有非线程的进程链表) */
#ifdef CONFIG_SMP
struct plist_node pushable_tasks;
struct rb_node pushable_dl_tasks;
#endif
/* 如果当前内核进程被调度之前运行的也是另外一个内核进程时,mm和avtive_mm都是NULL */
struct mm_struct *mm, *active_mm; /* mm:进程的用户内存空间描述符(包括了用户栈),每个mm都有单独的页表,内核进程mm=NULL;active_mm:进程运行时所使用的内存描述符,普通进程mm=active_mm,内核进程的active_mm等于上一个进程的active_mm */
...
/* task state */
int exit_state; /* 进程退出状态,EXIT_ZOMBIE,EXIT_DEAD,state 也可以持有退出状态 */
int exit_code, exit_signal; /* exit_code: 进程终止码,正常是_exit(),异常时为错误码;exit_signal:线程组退出信号 */
int pdeath_signal; /* The signal sent when the parent dies,保存父进程终止时需要向子进程发送的信号,再父进程终止后可以通过这个信号通知子进程 */
...
pid_t pid; /* 0层命名空间进程或线程的id */
pid_t tgid; /* 0层命名空间线程组id,线程组的所有线程使用和该线程组的领头线程相同的PID */
...
struct task_struct __rcu *real_parent; /* real parent process. 进程的父进程,父进程终止时为1号init进程 */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports. 通常与real_parent相同 */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children,子进程链表头 */
struct list_head sibling; /* linkage in my parent's children list,兄弟进程链表 */
struct task_struct *group_leader; /* threadgroup leader,进程组leader进程 */
struct list_head ptraced; /* 用于断点调试 */
struct list_head ptrace_entry;
/* PID/PID hash table linkage. 三种类型的PID 链表 */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group; /* 线程组链表 */
struct list_head thread_node;
struct completion *vfork_done; /* for vfork() vfork指针 */
int __user *set_child_tid; /* CLONE_CHILD_SETTID 子进程的线程ID将存储在子进程中child_tid所指向的位置 */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
cputime_t utime, stime, utimescaled, stimescaled; /* utime/stime:记录进程在用户态/内核态下所经过的节拍数,utimescaled/stimescaled是缩放后的计数 */
cputime_t gtime; /* 以节拍计数的运行时间 */
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
struct cputime prev_cputime; /* 记录进程之前在用户态/内核态下运行的节拍数 */
#endif
...
unsigned long nvcsw, nivcsw; /* context switch counts, 自愿(voluntary)/非自愿(involuntary)上下文切换计数 */
u64 start_time; /* monotonic time in nsec.进程启动时间,不包含睡眠时间 */
u64 real_start_time; /* boot based time in nsec.进程启动时间,包含睡眠时间 */
...
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs; /* 文件系统的信息的指针 */
/* open file information */
struct files_struct *files; /* 打开的文件描述指针 */
/* namespaces */
struct nsproxy *nsproxy; /* 进程命名空间 */
/* signal handlers */
struct signal_struct *signal; /* 指向进程的信号描述符 */
struct sighand_struct *sighand; /* 指向进程的信号处理程序描述符 */
...
struct rcu_head rcu;
...
};
进程控制块
进程控制块结构是和 cpu 的架构相关的,包括了当前cpu 的特异性成员。可以理解为进程描述符展现了进程在所有cpu下的共性,而一个进程想要在 cpu上运行起来必然要对 cpu 进行一定的操作,例如硬件上下文的入栈和出栈等,很好的体现了“和而不同”的思想,arm32 中的进程控制块如下所示。
struct thread_info {
unsigned long flags; /* low level flags */
/* 抢占标记, 为0可抢占, 大于0不能抢占, 最低字节为抢占计数, 第二字节为软中断计数, 16-25位(10位)为硬中断计数26位为不可屏蔽中断(NMI)标记, 27位为不可抢占标记 */
int preempt_count; /* 0 => preemptable, <0 => bug*/
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure:进程描述符指针 */
__u32 cpu; /* cpu. 进程所在cpu号, 通过raw_smp_processor_id()获取 */
__u32 cpu_domain; /* cpu domain. 保存协处理器状态, __switch_to()中修改 */
struct cpu_context_save cpu_context;/* cpu context. 保存寄存器状态, __switch_to()假定cpu_context紧跟在cpu_domain之后 */
__u32 syscall; /* syscall number. 系统调用号 */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
进程内核栈
进程的内核栈使用一个联合体将进程控制块信息从内核栈空间的低地址开始存放。
/* 内核栈联合体 */
union thread_union {
struct thread_info thread_info; /* 进程控制块 */
unsigned long stack[THREAD_SIZE/sizeof(long)]; /* 8K内核栈空间 */
};
函数分析
do_fork
do_fork
└── copy_process
├── (clone_flags & CLONE_VFORK)>0 ? init_completion : skip
├── wake_up_new_task
└── (clone_flags & CLONE_VFORK)>0 ? wait_for_vfork_done : skip
long do_fork(unsigned long clone_flags, /* 克隆标志控制进程从父进程拷贝哪些资源 */
unsigned long stack_start, /* 子进程用户态堆栈地址 */
unsigned long stack_size, /* 用户态下栈大小 */
int __user *parent_tidptr, /* 父进程在用户态下pid的地址,CLONE_PARENT_SETTID标志被设定时有意义 */
int __user *child_tidptr) /* 子进程在用户态下pid的地址,CLONE_PARENT_SETTID标志被设定时有意义 */
{
struct task_struct *p;
int trace = 0;
long nr;
...
/* 创建子进程,申请内核栈,复制父进程描述符并进行修改,设置进程优先级和调度类,初始化内核栈设置用户栈和进程入口 */
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) /* 设置标志位的情况下将子进程的线程ID将存储在parent_tidptr位置 */
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) { /* vfork() 创建时需要设置vfork结构体,主要是创建一个等待队列用于子进程运行时阻塞父进程 */
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p); /* 选择一个合适 CPU,然后将进程添加到所选的 CPU 的 rq 队列中,检查是否需要进行进程抢占 */
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) { /* 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;
}
do_fork 函数比较简单,主要功能代码被封装在 copy_process 函数中,具体功能如下:
- 调用 copy_process 函数完成子进程资源分配和创建;
- 检查 CLONE_VFORK 标志,如果置位则创建 waitqueue,用于阻塞父进程;
- 为新进程选择 cpu,并加入 cpu所在的 rq 队列,检查是否满足调度条件进行一次内核抢占;
- 检查 CLONE_VFORK 标志,如果置位则阻塞父进程,直到子进程运行结束;
copy_process
copy_process
└── dup_task_struct
├── sched_fork
├── copy_semundo
├── copy_files
├── copy_fs
├── copy_sighand
├── copy_signal
├── copy_mm
├── copy_namespaces
├── copy_io
├── copy_thread
├── alloc_pid
└── attach_pid
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;
...
retval = -ENOMEM;
/* 创建子进程:申请新进程描述符,申请新进程内核栈,并从父节点进程描述符和进程控制块复制(浅拷贝)内容到新进程 */
p = dup_task_struct(current);
if (!p)
goto fork_out;
...
retval = -EAGAIN;
/* 判断创建的进程数量是否超出系统允许范围,max_threads在for_init 中初始化 */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
/* 下面的代码主要是对子进程的描述符修改,继承于父进程的属性需要修改,但大多数保留*/
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
p->flags |= PF_FORKNOEXEC;
INIT_LIST_HEAD(&p->children); /* 初始化孩子链表 */
INIT_LIST_HEAD(&p->sibling); /* 初始化兄弟链表 */
rcu_copy_process(p); /* 初始化task_struct中rcu相关数据和链表 */
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock); /* 初始化自旋锁 */
init_sigpending(&p->pending); /* 初始化信号 */
...
/* Perform scheduler related setup. Assign this task to a CPU. */
retval = sched_fork(clone_flags, p); /* 设置子任务优先级,设定调度类,并进行调度fork,完成进程绑定CPU */
if (retval)
goto bad_fork_cleanup_policy;
...
/*下面的操作中,根据flag中是否设置了相关标志进行重新分配或者共享父进程的内容*/
/* 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);/* 根据flag确定是否为新进程创建新的命名空间,新的命名空间中的pid_ns、uts_ns、ipc_ns、net_ns、mnt_ns可能用新的也可能用旧的 */
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
/* 将通用寄存器压栈到新进程内核堆栈,设置子进程返回值和用户栈位置,最后设置进程控制块下的sp指向内核栈顶,pc指向进程入口ret_from_fork */
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;
if (pid != &init_struct_pid) { /* 创建的不是0号进程时,需要为新进程申请pid */
pid = alloc_pid(p->nsproxy->pid_ns_for_children);/* 为进程分配PID */
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_io;
}
}
...
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);/* 获取顶层pid命名空间的pid的值,复制到进程表述符下,而不需要每次去pid实体下的number[]中找 */
if (clone_flags & CLONE_THREAD) {/* 创建的是线程 */
p->exit_signal = -1;
p->group_leader = current->group_leader; /* 进程组leader等于主进程group_leader */
p->tgid = current->tgid; /* 则进程组id等于主进程组id */
} else { /* 创建的是进程 */
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p; /* 进程组leader默认指向自身 */
p->tgid = p->pid; /* 则进程组id等于自己的pid */
}
...
INIT_LIST_HEAD(&p->thread_group);/* 初始化线程组链表 */
...
/* CLONE_PARENT re-uses the old parent */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {/* 创建的是线程或者指定了进程共享parent时,可以与创建者拥有于相同的父进程 */
p->real_parent = current->real_parent;
p->parent_exec_id = current->parent_exec_id;
} else {/* 新进程的父进程就是当前创建者 */
p->real_parent = current;
p->parent_exec_id = current->self_exec_id;
}
spin_lock(¤t->sighand->siglock);
...
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
init_task_pid(p, PIDTYPE_PID, pid);/* 新进程的 p->pids[PIDTYPE_PID].pid 指向pid描述符*/
/* 如果本进程是线程组的group leader,则本进程的PGID和SID的pid实体共享父进程的leader的PGID和SID的pid实体,并把进程pid_link加入pgid和sid的链表中
* 如果本进程是线程组中一个普通线程,它的PGID和SID从group leader复制继承,并且不会加入到pgid和sid的链表当中 */
if (thread_group_leader(p)) { /* 进程(线程组leader) */
/* 共享父进程的group_leader的pid实体 */
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); /* 新进程共享父进程的group_leader的pids[PIDTYPE_PGID].pid实体 */
init_task_pid(p, PIDTYPE_SID, task_session(current));/* 新进程共享父进程的group_leader的pids[PIDTYPE_SID].pid实体 */
if (is_child_reaper(pid)) { /* 判断进程是不是当前pid命名空间中的1号进程, 1号进程用于收尸 */
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
p->signal->leader_pid = pid;
p->signal->tty = tty_kref_get(current->signal->tty);
list_add_tail(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks); /* 本进程加init进程的进程链表,普通线程不用加入 */
attach_pid(p, PIDTYPE_PGID); /* 将本进程pid节点加入到父进程的组leader 所在的pid->tasks[PIDTYPE_PGID]链表中 */
attach_pid(p, PIDTYPE_SID); /* 将本进程pid节点加入到父进程的组leader 所在的pid->tasks[PIDTYPE_SID]链表中 */
__this_cpu_inc(process_counts); /* 当前cpu上的进程数量 */
} else { /* 普通线程 */
current->signal->nr_threads++;
atomic_inc(¤t->signal->live);
atomic_inc(¤t->signal->sigcnt);
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); /* 将新进程和自己的pid实体关联起来,并加入pid->tasks[PIDTYPE_PID] */
nr_threads++;/* 当前进程/线程数增加 */
}
total_forks++;
spin_unlock(¤t->sighand->siglock);
...
return p;
...
return ERR_PTR(retval);
}
copy_process 中具体完成子进程的创建,主要工作如下:
- 使用 dup_task_struct 申请进程描述符和内核栈,完成从父节点到子进程的浅拷贝
- 检查当前系统上总的进/线程数不能超出最大限制
- 使用 sched_fork 设置子任务优先级,根据进程优先级设定调度类,绑定cpu
- 根据 clone_flags 拷 files / fs / sighand / namespaces / mm 等资源
- 使用 copy_thread 拷贝父节点控制块上的通用寄存器区域并修改,设置子进程的内核栈指针位置和入口地址
- 非0号进程时使用 alloc_pid 为进程分配 PID 描述符,并为每个层级的pid分配具体的值
- 根据创建的是进程还是线程设置 tgid ,group_leader 和 real_parent
- 区分进程还是线程设置 pid,pgid 和sid
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); /* 申请进程描述符空间task_struct */
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; /* 将新的内核栈挂载到新进程描述符上 */
...
setup_thread_stack(tsk, orig); /* 把父进程内核栈下的进程控制块信息复制(浅拷贝)给新进程,并修改新进程控制块下的进程指针执行新进程 */
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk); /* 清除进程控制块flag的需要调度标志位 */
set_task_stack_end_magic(tsk); /* 在内核栈底跳过 struct thread_info 位置后设置为STACK_END_MAGIC */
#ifdef CONFIG_CC_STACKPROTECTOR
tsk->stack_canary = get_random_int();
#endif
atomic_set(&tsk->usage,2); /* 表示进程描述符正在被使用 */
...
return tsk;
...
return NULL;
}
dup_task_struct 主要完成子进程的进程描述符和子内核栈的申请和浅拷贝,主要流程如下:
- 使用 alloc_task_struct_node 申请进程描述符空间
- 使用 alloc_thread_info_node 申请新进程的内核栈,虽然 ti 是 thread_info 类型,但是由于 thread_info 从内核栈底低地址位置开始存放,所以 thread_info 的首地址就是内核栈底的开始地址。
- 使用 arch_dup_task_struct 完成从父进程到子进程描述符的浅拷贝,其中指针成员所指向的位置还是和父进程共用
- 使用 setup_thread_stack 完成子进程控制信息的浅拷贝,并将进程描述符挂载到子进程的控制块上
- 使用 clear_tsk_need_resched 清除可能从父进程得到的需要调度标志位
- 使用 set_task_stack_end_magic 从内核栈底开始,跳过 thread_info 存放位置,设置0x57AC6E9D 作为内存异常检测标志。
sched_fork
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
int cpu = get_cpu();
__sched_fork(clone_flags, p); /* 完成一些进程参数初始化 */
/*
* We mark the process as running here. This guarantees that
* nobody will actually run it, and a signal or other external
* event cannot wake it up and insert it on the runqueue either.
* 设置进程状态为RUNNING表示进程准备好了
*/
p->state = TASK_RUNNING;
/*
* 子进程的动态优先级被设置为父进程普通优先级,确保实时互斥量引起的优先级提高不会传递到子进程.
* 其他类型的优先级通过浅拷贝父进程的描述符得到
*/
p->prio = current->normal_prio;
/* 设置了在fork时调度恢复, 可能是实时父进程中设置了SCHED_RESET_ON_FORK,也可能是普通进程修改进程优先级设置了SCHED_RESET_ON_FORK */
if (unlikely(p->sched_reset_on_fork)) {
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {/* rt/dl父进程指定子进程创建时需要恢复(此时不能使用 rt_prio()来判断因为 prio可能被临时调整过了) */
p->policy = SCHED_NORMAL; /* 恢复到普通调度策略 */
p->static_prio = NICE_TO_PRIO(0); /* 静态优先级恢复到120 */
p->rt_priority = 0; /* 不需要实时优先级所以设置为0 */
} else if (PRIO_TO_NICE(p->static_prio) < 0) /* 普通进程在 nice 的值小于0的情况(100~119)恢复 */
p->static_prio = NICE_TO_PRIO(0); /* 静态优先级恢复到120,p->rt_priority来自子进程继承父进程的普通优先级 */
p->prio = p->normal_prio = __normal_prio(p);/* CFS调度策略下动态优先级和常规优先级等于static_prio */
set_load_weight(p); /* 重新设置普通进程的cpu调度权重 */
/*
* We don't need the reset flag anymore after the fork. It has
* fulfilled its duty:恢复已经完成此时子进程设置为无需调度恢复
*/
p->sched_reset_on_fork = 0;
}
if (dl_prio(p->prio)) { /* SCHED_DEADLINE 类型的父进程不能直接创建子进程 */
put_cpu();
return -EAGAIN;
} else if (rt_prio(p->prio)) {/* 通过父进程常规优先级来决定当前进程应该使用的调度类 */
p->sched_class = &rt_sched_class;
} else {
p->sched_class = &fair_sched_class;
}
if (p->sched_class->task_fork)/* 调度类将进程转为调度实体,更新相关参数,用于后续调度 */
p->sched_class->task_fork(p);
raw_spin_lock_irqsave(&p->pi_lock, flags);
set_task_cpu(p, cpu); /* 进程绑定cpu */
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
...
return 0;
}
sched_fork 中主要是对子进程优先级和调度类的设置,主要如下:
- 修改子进程的动态优先级为父进程的普通优先级,因为父进程在运行的过程中可能会临时提高父进程的动态优先级,而普通优先级时不变的,所以需要重新修改回来,而其他类型的优先级则通过浅拷贝得到
- 检查 sched_reset_on_fork 是否设置,如果设置了,需要将调度策略和优先级恢复到SCHED_NORMAL
- 根据父进程的普通优先级设置子进程的调度策略
- 为新创建的子进程绑定 cpu
copy_mm
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
int retval;
/* 初始化进程的缺页情况 */
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
tsk->mm = NULL;/* 新进程用户内存信息设置为NULL */
tsk->active_mm = NULL;
oldmm = current->mm;/* 如果父进程是内核进程则创建的子进程也是内核进程,此时不需要mm,直接返回 */
if (!oldmm)
return 0;
/* initialize the new vmacache entries */
vmacache_flush(tsk);
if (clone_flags & CLONE_VM) { /* 是一个线程是则使用父节点的内存描述符,实现共享内存 */
atomic_inc(&oldmm->mm_users);/* 父进程内存用户数增加 */
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = dup_mm(tsk);/* 申请独立内存,并复制父进程内存描述符 */
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm; /* 设置新进程的内存描述符和运行时内存描述 */
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
copy_mm 主要完成子进程虚拟内存空间 mm 结构提的申请和浅拷贝,此处不会真正为子进程分配物理空间,主要步骤如下:
- 判断当前父进程是否 mm 为NULL,即为内核线程,如果是则返回,因为内核线程不需要虚拟内存空间,他们共享内核空间。、
- 如果创建的是一个用户线程,共享主进程的 mm 内存空间,并将父进程的 mm 引用计数++
- 如果创建的是一个用户进程,则使用 dup_mm 函数为进程申请 mm 描述符,并复制父进程的进程地址空间页表到子进程,此时是共享父进程的物理地址,并没有真正为子进程分配独立的物理空间,直到发生数据修改时触发写时拷贝才会分配真正的物理页给子进程
copy_thread
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
unsigned long stk_sz, struct task_struct *p)
{
/* 获取进程控制块信息 */
struct thread_info *thread = task_thread_info(p);
/* 获取子进程通用寄存器放置的位置(从内核栈顶-8-sizeof(struct pt_regs)开始),陷入内核时用户态的上下文信息保存在pt_regs数据结构中 */
struct pt_regs *childregs = task_pt_regs(p);
memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
if (likely(!(p->flags & PF_KTHREAD))) { /* 创建的是进程 */
*childregs = *current_pt_regs(); /* 获取父进程的用户上下文寄存器信息复制到当前进程,包括ARM_lr也继承于父节点,所以在应用层调用fork后依然返回父进程的fork下一条语句 */
childregs->ARM_r0 = 0; /* 后面用于子进程返回,也就是为什么fork后子进程返回0的原因 */
if (stack_start) /* stack_start不为0时设置用户态sp位置,否则是从父进程继承而来 */
childregs->ARM_sp = stack_start;
} else { /* 创建的是内核线程 */
memset(childregs, 0, sizeof(struct pt_regs)); /* 内核线程不需要用户上下文,所以此处清0 */
thread->cpu_context.r4 = stk_sz; /* cpu 上下文 r4中保存线程函数参数,先在__switch_to中被出栈,后再ret_from_fork中被使用 */
thread->cpu_context.r5 = stack_start; /* cpu 上下午 r5中存放线程函数,先在__switch_to中被出栈,后再ret_from_fork中被使用 */
childregs->ARM_cpsr = SVC_MODE; /* 设置模式为SVC模式 */
}
thread->cpu_context.pc = (unsigned long)ret_from_fork;/* do_fork完成后的进程首次调度到子进程被调度时从ret_from_fork中进入用户态 */
thread->cpu_context.sp = (unsigned long)childregs; /* 内核栈指针指向用户空间上线文开始的位置 */
clear_ptrace_hw_breakpoint(p);
if (clone_flags & CLONE_SETTLS)
thread->tp_value[0] = childregs->ARM_r3;
thread->tp_value[1] = get_tpuser();
thread_notify(THREAD_NOTIFY_COPY, thread); /* 通知其他进程copy完成 */
return 0;
}
copy_thread 主要完成从内核栈中用户空间上下文设置和,cpu 硬件上下文的 pc 和 sp 设置,主要流程如下:
- 通过 **task_pt_regs() ** 获取子进程内核栈上用户上下文存储位置
- 如果创建的是用户进/线程:则复制父进程的用户空间上下文内容到子进程,并重新设置
ARM_r0 = 0
,这也是为什么我们在用户空间调用一次 fork() 确返回两次的原因(返回时需要用到 lr 的值,lr是从父进程拷贝而来,所以用户空间的返回点也和父进程一样),并且子进程返回时的值是 0,就是因为此处重新设置了 ARM_r0 并将其作为返回值给到用户空间 - 如果创建的是内核线程:内核线程永远不会进入用户空间,所以把用户空间上下文位置清0,然后设置cpu 硬件上下文的 r4 和 r5,内核线程和用户线程是不同的,内核线程传入的 stk_sz 为内核线程执行函数的参数,stack_start 为内核线程执行函数
- 然后设置子进/线程的首次调度入口地址为 ret_from_fork() 和 内核栈的 sp 地址,当进程fork后首次被调度时会进入 ret_from_fork() 执行代码
ret_from_fork
ENTRY(ret_from_fork)
bl schedule_tail @ 执行进程收尾工作
cmp r5, #0 @ 检查进程栈上 cpu_context.r5 存储是是否为0,非0则是内核线程执行函数,copy_thread 中设置
movne r0, r4 @ 获取内核线程函数参数到r0, copy_thread 中设置
adrne lr, BSYM(1f) @ 线程函数如果退出则从 1: 开始
retne r5 @ 执行内核线程函数,否则从ret_slow_syscall进入用户进程
1: get_thread_info tsk
b ret_slow_syscall
ENDPROC(ret_from_fork)
fork 后的子进/线程首次被调度时在进程切换时,在 __switch_to
中会将之前在 copy_thread() 中设置的 cpu硬件上下文 cpu_context 从内核栈中出栈到响应的寄存器中,设置的 cpu_context.sp = childregs
被出栈到 sp_svc 寄存器中,之后子进程在内核态运行程序时就能正确的使用自己的内核栈;设置的 cpu_context.pc = ret_from_fork
,被出栈到 pc 寄存器中,然后程序运行便会跳转到 ret_from_fork中执行函数,主要流程如下:
- 调用 schedule_tail 对上一进程进行收尾工作(例如释放mm描述符和进程描述符等)
- 判断 r5 寄存器为0,即设置了内核线程的执行函数,如果是则进入内核新车执行函数开始执行
- 如果 r5 寄存器非0,即是用户空间的进/线程,则进入 ret_slow_syscall 中返回用户空间。
总结
不论是用户空间的 fork、vfork、clone 系统调用,还是内核空间创建内核线程的 kernel_thread 函数,最终调用的都是 do_fork() 函数完成任务的创建,他们的主要区别就是创建任务时传递的 clone_flags 标志位,通过标志位上的掩码决定所创建任务的具体行为。通过创建过程也可以看到任务的创建是基于父进程 copy 的方式而来,然后在进行必要修改,同时引入 COW 技术避免在fork 过程中为子进程分配物理内存页。
任务创建完毕后内核栈空间的布局如下,总大小一般为 8K 大小,在栈顶高地址位置先预留了 8Bytes 的保留空间,然后是 18*4 = 72Bytes
大小用于存放用户空间上下文用于进出内核空间时保存现场,在栈低地址位置上存放进程的控制块信息,其中包括了cpu 硬件上下文用于进行任务切换时保存现场。
🌀路西法 的CSDN博客拥有更多美文等你来读。