分析Linux内核fork子进程的过程

来源:互联网 发布:安卓版数据恢复软件 编辑:程序博客网 时间:2024/06/05 02:17

我们编写程序的时候会用到fork 、vfork 、clone等函数来创建一个子进程,但是,这些函数是怎么做到创建一个子进程的呢,底层是怎样实现的呢?我们通过分析内核代码来了解这些。

进程创建

查看 相关的内核代码 ,可以看到如下程序段

SYSCALL_DEFINE0(fork) {   return do_fork(SIGCHLD, 0, 0, NULL, NULL);} ....SYSCALL_DEFINE0(vfork){   return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);} ...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);} ...

这里只提取出关键信息,可以看到,三个函数都调用了do_fork()来完成。那么既然实际上都是调用一个过程,为什么要有fork,vfork,clone这么多不同的函数呢?实际上这是为了不同的场景需要而设计的,文章结尾部分是查阅资料总结这三个函数的区别,现在的重点是看内核是怎么创建新进程的,所以我们先来看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){  struct task_struct *p;  int trace = 0;  long nr;  //复制进程环境,返回创建的 task_struct 的指针  p = copy_process(clone_flags, stack_start, stack_size,       child_tidptr, NULL, trace);  if (!IS_ERR(p)) {    struct completion vfork;    struct pid *pid;    trace_sched_process_fork(current, p);    //获取 task 结构 pid    pid = get_task_pid(p, PIDTYPE_PID);    nr = pid_vnr(pid);    if (clone_flags & CLONE_PARENT_SETTID)      put_user(nr, parent_tidptr);    if (clone_flags & CLONE_VFORK) {      p->vfork_done = &vfork;      init_completion(&vfork);      get_task_struct(p);    }    //将子进程添加到调度器的队列,等待调度运行    wake_up_new_task(p);    //如果调用的是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()

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;  //分配一个新的 task_struct,内容从调用进程复制过来,仅仅是 stack 地址不同  p = dup_task_struct(current);  if (atomic_read(&p->real_cred->user->processes) >=      task_rlimit(p, RLIMIT_NPROC)) {    //检查该用户是否具有相关权限    if (p->real_cred->user != INIT_USER &&        !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))      goto bad_fork_free;  }  retval = -EAGAIN;  //检查进程数量是否超过 max_threads  if (nr_threads >= max_threads)    goto bad_fork_cleanup_count;  //把新进程的状态设置为TASK_RUNNING  retval = sched_fork(clone_flags, p);  //初始化子进程的内核栈  retval = copy_thread(clone_flags, stack_start, stack_size, p);  if (retval)    goto bad_fork_cleanup_io;  if (pid != &init_struct_pid) {    retval = -ENOMEM;    //为子进程分配新的 pid 号    pid = alloc_pid(p->nsproxy->pid_ns_for_children);    if (!pid)      goto bad_fork_cleanup_io;  }  //设置子进程的 pid  p->pid = pid_nr(pid);  //如果是创建线程  if (clone_flags & CLONE_THREAD) {    p->exit_signal = -1;    p->group_leader = current->group_leader;    //tgid 是当前线程组的 id    p->tgid = current->tgid;  } 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;    p->tgid = p->pid;  }  if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {    //如果是创建线程,那么同一线程组内的所有线程共享进程空间    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;  }  attach_pid(p, PIDTYPE_PID);  nr_threads++;  //返回被创建的 task 结构体指针  return p;}

copy_process函数所做的工作比较复杂,这里只选取了部分做了简单的注释。总结一下,copy_process 函数为了创建子进程,主要做了:

  • 调用 dup_task_struct() 复制当前进程的 task_struct;
  • 调用 sched_fork 初始化进程数据结构,设置新进程状态为 TASK_RUNNING;
  • 复制父进程的进程环境;
  • 调用 copy_thread 初始化子进程内核栈;
  • 为子进程设置新的 pid;

我们知道,fork()函数在子进程返回0,是怎么做的呢。答案在copy_thread 里,可以看到有 childregs->ax = 0 这句,把子进程的 eax 赋值为 0。还有,创建的子进程从何处执行呢?
代码里有p->thread.ip = (unsigned long) ret_from_fork, 将子进程的 ip 设置为 ret_form_fork 的首地址,因此子进程是从 ret_from_fork 开始执行的。

fork,vfork,clone的区别

Linux的用户进程不能直接被创建出来,因为不存在这样的API。它只能调用fork、vfork、clone这样的API从某个进程中复制出来,再通过exec这样的API来切换到实际想要运行的程序。

进程的构成:

  1. 执行代码,该代码是可以被多个进程共享的。

  2. 进程专用的系统堆栈空间。

  3. 进程控制块,在linux中具体实现是task_struct

  4. 有独立的存储空间。

fork

  • fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是复制。

  • 这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个可执行程序,那么在fork过程中对于虚存空间的复制将是一个多余的过程。

  • 现在Linux中采取了 copy-on-write(COW写时复制) 技术,为了降低开销,fork最初并不会真的产生两个不同的拷贝,子进程先共享父进程的空间,若后来进程要修改数据,那意味着parent和child的数据不一致了,于是产生复制动作,每个进程拿到属于自己的那一份,这样就可以降低系统调用的开销。所以有了写时复制后呢,vfork其实现意义就不大了。

vfork

  • vfork系统调用不同于fork,它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit。
  • 用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

  • 子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。

  • 但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。

  • 用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。

vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

clone

  • 系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。

fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。

0 0