当你把标题中的问题放在百度上搜素,你会得到一堆的答案,但是大部分都是告诉你,“子进程继承了父进程大部分的资源,并拥有一小部分自己的私有资源”。
那这个大部分和小部分都有什么呢?
先写一段示例:
```c
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid;
fpid = fork();
if (fpid < 0)
printf("fork error!");
else if (fpid == 0)
{
printf("这是子进程代码区/n");
}
else
{
printf("这是父进程代码区/n");
}
return 0;
}
## 在应用空间调用fork函数,返回两个pid,根据pid区分父子进程,那么内核如何处理这个接口呢?
读源码。
版本:Linux-4.1.15
路径:kernel\fork.c
```c
//先看这么一个系统调用的定义,这里定义了一个系统调用,没有参数即 fork(void)。
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
//下边详细分析
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
先忽略MMU的配置宏,直接去看 do_fork 的实现;
/* 参数解释
* clone_flags 就是上边 SIGCHLD=20
* stack_start 0 用户态栈起始地址
* stack_size 0 用户态栈大小
* parent_tidptr 父进程PID指针
* child_tidptr 子进程PID指针
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p; //首先定义一个用于描述进程的结构
............
/*这个宏与调试有关,相与之后条件不成立
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;
}
*/
//这个接口完成了主要工作,详细分析看下边
//另外,参数除了 clone_flags,其余都是0
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
//上边结束之后新进程的 task_struct 已经构建完成并已经加入内核了
........
//接口没找到,猜测父进程调度子进程?
trace_sched_process_fork(current, p);
........
//首次唤醒进程,并初始化和加入到运行队列
wake_up_new_task(p);
return nr; //这里也就是把子进程的pid返回出去了
/* 是不是奇怪明明返回了一个PID,为什么应用层会收到两个?
* 父进程调用 fork 返回之后得到子进程pid,子进程继承了父进程的代码段,
* 所以子进程也会执行一次 fork,这时候就会返回0
*/
}
接下来看一下 copy_process 是怎么拷贝进程的。
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
struct task_struct *p; //描述进程的指针
..........
//
retval = security_task_create(clone_flags);
/*函数内部实现如下,其实是对父进程的权限检查,返回0则检查通过
* return security_ops->task_create(clone_flags);
* 而 task_create = selinux_task_create
*/
........
//参数表示当前进程,看后边详细分析,最后返回了tsk
p = dup_task_struct(current);
........
//和trace相关的函数返回栈地址初始化
ftrace_graph_init_task(p);
//带有优先级的锁初始化,和实时性相关
rt_mutex_init_task(p);
........
//在子进程内存中分配一个新的creds结构,并复制父进程creds中的数据,把操作权限交给自己
retval = copy_creds(p, clone_flags);
........
//任务延迟初始化
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
........
//把TASKS_RCU相关的结构全部置0
rcu_copy_process(p);
........
//初始化进程拥有的信号和信号链表
init_sigpending(&p->pending);
........
//进程 I/O 计数初始化
task_io_accounting_init(&p->ioac);
//清除tsk->acct_xxx相关字段,(内存累计相关)
acct_clear_integrals(p);
//初始化POSIX计时器
posix_cpu_timers_init(p);
.......
//初始化task的cgroup字段
cgroup_fork(p);
.......
//调度相关,执行完成之后任务就分配了CPU
retval = sched_fork(clone_flags, p);
//初始化perf_event字段,(和trace配合的,用来跟踪进程分析数据的)
retval = perf_event_init_task(p);
//给audit分配一个上下文块
retval = audit_alloc(p);
/* copy all the process information */
//给子进程设置调度器,设置进程状态为运行态
shm_init_task(p);
//拷贝父进程的SEM_UNDO
retval = copy_semundo(clone_flags, p);
//把父进程的 files_struct 拷贝一份交给子进程,包含文件描述符集fd_array[]
retval = copy_files(clone_flags, p);
//把父进程的 fs_struct 拷贝一份交给子进程,包含文件路径等(文件系统相关)
retval = copy_fs(clone_flags, p);
//拷贝父进程的信号处理函数
retval = copy_sighand(clone_flags, p);
//拷贝父进程的信号集合
retval = copy_signal(clone_flags, p);
//拷贝父进程的 mm_struct,包含虚拟地址空间、代码段数据段堆等,注意这里只拷贝了页目录表,并且页面设置为不可写
retval = copy_mm(clone_flags, p);
//拷贝父进程的namespace,其实是建立自己的并拷贝父进程的数据
retval = copy_namespaces(clone_flags, p);
//拷贝父进程的块设备IO上下文
retval = copy_io(clone_flags, p);
//拷贝父进程进行上下文切换时需要的信息,但是有些内容比如程序指针是指向子进程的
retval = copy_thread(clone_flags, stack_start, stack_size, p);
........
//下边子进程申请自己的pid
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
........
//关闭子进程的跟踪
user_disable_single_step(p);
//下边3个都是在清除标志
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
clear_all_latency_tracing(p);
........
//任务链表锁,需等父进程来调度
write_lock_irq(&tasklist_lock);
........
/* 下边都是和安全相关的检查 */
//当前进程信号处理上锁,这里应该是禁止了信号处理
spin_lock(¤t->sighand->siglock);
//seccomp与系统安全有关
copy_seccomp(p);
//这也和信号处理有关
recalc_sigpending();
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
//初始化PID
init_task_pid(p, PIDTYPE_PID, pid);
........
attach_pid(p, PIDTYPE_PID);
nr_threads++;
}
total_forks++;
spin_unlock(¤t->sighand->siglock);
//设置任务线程标志
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
//将新进程与proc文件系统进行关联
proc_fork_connector(p);
//当进程被加入内核进程链表之后调用,为了防止竞争
cgroup_post_fork(p);
........
//设置perf_task_event和子进程的关联
perf_event_fork(p);
//没找到这个函数的实现
trace_task_newtask(p, clone_flags);
//给子进程设置了需要运行的task,但是没有调度
uprobe_copy_process(p, clone_flags);
//把构建出来的task_struct返回出去
return p;
}
现在来分析一下 dup_task_struct 做了什么?
//注意函数返回值,传进的参数是当前进程
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk; //上来就是两个关键指针
struct thread_info *ti;
//给当前进程分配一个inode,返回节点号
int node = tsk_fork_get_node(orig);
.......
/* 内部调用 kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
* 其结果就是用slab算法在高速缓冲区给node分配内存页,构建了task_struct
*/
tsk = alloc_task_struct_node(node);
........
//把申请的page地址返回出来交给ti
ti = alloc_thread_info_node(tsk, node);
//内部让 tsk = orig,也就是tsk这个结构现在就是复制的父进程orig的
err = arch_dup_task_struct(tsk, orig);
//新进程的栈顶就等于了刚才在高速缓冲区申请的页地址,
//到这里是不是发现栈是新申请的,也就是子进程私有的
tsk->stack = ti;
........
//内部让tsk->stack = orig->stack,但是有个强转(struct thread_info *)
//笔者在这里并不是很明白,猜测是在设置子进程的线程相关的东西
setup_thread_stack(tsk, orig);
//清除了notified相关的bit
clear_user_return_notifier(tsk);
//清除了reschedule相关的bit,表示现在不需要调度
clear_tsk_need_resched(tsk);
//在子进程栈底设置了一个幻数,用于检测栈溢出
set_task_stack_end_magic(tsk);
.........
//给进程的引用设置为2
atomic_set(&tsk->usage, 2);
//这个函数似乎是给中断恢复计数加1??
account_kernel_stack(ti, 1);
return tsk;
}
读到这里应该对进程的创建过程有大致的了解吧,其实这中间有很多的安全检查,且机制很复杂,有空了再继续研究。
(以上内容仅代表个人理解,应该有很多不准确的地方,欢迎指正!)
那么子进程到底继承了父进程的什么呢?
1、父进程的 creds
2、父进程的 SEM_UNDO
3、父进程的 files_struct
4、父进程的 fs_struct
5、父进程的信号处理函数
6、父进程的信号集合
7、进程的 mm_struct
8、父进程的namespace
9、父进程的块设备IO上下文
10、父进程进行上下文切换时需要的信息
11、等
子进程又拥有自己的什么呢?
1、自己的PID
2、自己的栈地址(就是重新申请的page)
3、自己的寄存器组(代码里没有表现出来)
4、等
欢迎关注我的公众号,定期会分享一些自己在工作和学习中的内容和心得。