Linux进程创建
linux下创建进程的方式有三种,通过fork vfork clone系统调用实现进程的创建
1. fork
fork函数用于创建一个新的进程,其创建的进程和当前进程为父关系,子进程创建自己的task_struct 之后初始化子进程的互斥变量、cpu定时器、自旋锁、挂起信号、进程数据结构等并且设置进程状态, 然后子进程复制父进程的各项信息(包括文件系统、信号处理函数、信号、内存管理等)但是拷贝父进程的mm_struct时采用写时拷贝(后面详解)。之后会为子进程设置内核栈(thread_info) 和进程的pid 以及与调用进程的关系设置,调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU;
使用fork()创建的子进程和和父进程通过写时拷贝共享同一虚拟地址空间,子进程和父进程各自拥有不同的内核栈,子进程共享父进程的时间片。
写时复制(copy _on_write)
子进程与父进程共享同一页帧,不是复制,并且将子进程和父进程对该页帧的权限均设置成只读(页帧被保护),当有任意一方需要进行写入时会触发出错异常(page_fault int14)中断,此时系统会为其重新分配一个新的页帧并且复制之前页帧内容标记可写, 然后返回继续执行写操作,此时原来的页帧仍然被写保护当,再有进程对其进程写操作时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。
另外Linux下c++标准库中string类也使用了写时拷贝
2. vfork
vfork也是用于创建一个新的进程,创建过程和fork相似,不过在vfork进行拷贝父进程mm_struct时进行的是指针值拷贝也就是说父子进程指向同一进程地址空间,不过vfork建立的子进程同样会为其创建自己的内核栈,另外vfork 需要(设置CLONE_VFORK和ptrace标志)初始化完成处理信息,还有父进程会阻塞等待。vfork()保证子进程先调度运行。因为父进程和子进程指向同一进程地址空间则使用vfork函数后会立即使用exec函数进行替换,当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以前后的进程id 并未改变,exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈段。
如果在进程不去替换子进程,子进程继续运行将可能破坏父进程的数据结构或栈而出错。
3. fork和vfork调用流程
fork, vfork和clone的系统调用的入口地址分别是sys_fork, sys_vfork和sys_clone
以下代码均来自Linux-4.5
推荐一个可以看各版本内核的网站并且支持检索
https://elixir.bootlin.com/linux/v4.5/source/kernel/fork.c
sys_fork()实现
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
sys_vfork()实现
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL, 0);
}
#endif
fork和vfork实现最后都调用了_do_fork函数区别在于参数不同,其中vfork函数clone_flags参数多了两个属性,CLONE_VFORK 和CLONE_VM 接着看_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,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
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, tls);
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);
/* forking complete and child started to run, tell ptracer */
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;
}
其主要完成
- 调用 copy_process 为子进程复制出一份进程信息
- 如果是 vfork(设置了CLONE_VFORK和ptrace标志)初始化完成处理信息
- 调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU
- 如果是 vfork,父进程加入等待队列,待子进程完成 exec 替换自己的地址空间,或者子进程结束
另外查看copy_process函数
https://elixir.bootlin.com/linux/v4.5/source/kernel/fork.c#L1242
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,
unsigned long tls)
{
// 函数实现很长就不具体贴了如果要查看可以在点上面链接
// 这个函数的主要功能
// 调用 dup_task_struct 复制当前的 task_struct
// 检查进程数是否超过限制
// 初始化自旋锁、挂起信号、CPU 定时器等
// 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
// 复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
// 调用 copy_thread_tls 初始化子进程内核栈
// 为新进程分配并设置新的 pid
//...
//在这省略了函数的其他实现主要看进程的信息中内存地址空间的拷贝这块的
//复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
形式类似于copy_xxx的形式 */
//...
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
}
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
//...
//当vfork调用时其clone_flags拥有CLONE_VM属性即其实现值拷贝
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
//fork调用dup_mm->dup_mmap->copy_page_range->copy_pud_range->
//copy_pmd_range->copy_pte_range->copy_one_pte实现写时复制
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
最后fork() vfork() 函数执行完之后在以后的进程切换中,调度程序继续完善子进程:把子进程描述符thread(即内核栈中的值,因为父子进程内核栈不同,故可以保留不同的值以供esp寄存器读取因此父子进程返回不同的值)字段的值(TSS值)装入几个CPU寄存器。特别是把thread.esp装入esp寄存器,把函数ret_from_fork()的地址装入eip寄存器。这个汇编语言函数调用schedule_tail()函数,用存放在栈中的值再装入所有寄存器,并强迫CPU返回到用户态。这样,eax寄存器就装过两个值,一个是子进程的值0,一个是父进程的值——子进程的PID。
1 #include <unistd.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <errno.h>
5 int main()
6 {
7 int val = 100;
8 pid_t pid = fork();
9 if(pid < 0)
10 {
11 perror("creat process error");
12 }
13 else if(pid == 0) //子进程返回0
14 {
15 val = 1;
16 printf("this is child[pid:%d] val:%d\n", getpid(), val);
17 }
18 else if(pid > 0) //父进程返回子进程的pid
19 {
20 printf("this is parent[pid:%d] val:%d\n", getpid(), val);
21 }
22 sleep(1);
23 return 0;
24 }
运行结果
参考:
Linux中fork,vfork和clone详解(区别与联系)Linux下进程的创建过程分析(_do_fork/do_fork详解)–Linux进程的管理与调度(八