探究Linux》-fork()之后子进程到底继承了什么?

当你把标题中的问题放在百度上搜素,你会得到一堆的答案,但是大部分都是告诉你,“子进程继承了父进程大部分的资源,并拥有一小部分自己的私有资源”。

那这个大部分和小部分都有什么呢?

先写一段示例:


```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(&current->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(&current->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、等

在这里插入图片描述欢迎关注我的公众号,定期会分享一些自己在工作和学习中的内容和心得。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值