vforkシステムコール
Rev.2を表示中。最新版はこちら。
forkで子プロセスを作成すると、この時点では子プロセスは親の実メモリは共有し、親子プロセスに関係なくメモリの更新処理は、新規に実メモリを割り当てる(COW)ことで、親子間のメモリを管理しています。従って参照だけなら親子プロセスはメモリを共有しつずけることになります。なお、子プロセスをexec()で差し替えれば、子プロセスに新規の実メモリを割り当て、execされるオブジェクトの実行イメージをメモリに設定することになります。
fork()で子プロセスを作成後、execでプロセスを差し替えるような実装だと、子プロセスがexecをコールするまでに、親プロセスがCOWで新規にメモリを取得するかもしれません。fork後の実行権は親プロセスのままだからです。このような処理だと親のCOWが冗長的です。で、vforkシステムコールについてです。
vforkシステムコールは、do_fork()をCLONE_VFORKでコールし、作成された子プロセスにp->vfork_done = &vforkと、完了通知(completion)をし、wake_up_new_task()で子プロセスをアクティブリストに登録後、wait_for_vfork_done()でカレントプロセスの親プロセスをウエイトさせ、子プロセスに実行権が移ることにあります。親プロセスは、子プロセスのp->vfork_doneに設定された完了通知によるイベントで動作します。
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; if (clone_flags & CLONE_NEWUSER) { if (clone_flags & CLONE_THREAD) return -EINVAL; if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID)) return -EPERM; } if (likely(user_mode(regs)) && !(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, regs, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; trace_sched_process_fork(current, p); nr = task_pid_vnr(p); 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); /* forking complete and child started to run, tell ptracer */ if (unlikely(trace)) ptrace_event(trace, nr); if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event(PTRACE_EVENT_VFORK_DONE, nr); } } else { nr = PTR_ERR(p); } return nr; }wait_for_completion_killable()でカレントプロセス(親)を、vfork->waitのリストに追加し、実行権を放棄し、アクチィブリストのプロセスを起動します。この時それが子プロセスというわけではありませんが、親プロセスは子プロセスが起動した後でないと、子プロセスからのcompletionの通知がない限り、実効権を取得することはできません。
static int wait_for_vfork_done(struct task_struct *child, struct completion *vfork) { int killed; freezer_do_not_count(); killed = wait_for_completion_killable(vfork); freezer_count(); if (killed) { task_lock(child); child->vfork_done = NULL; task_unlock(child); } put_task_struct(child); return killed; }mm_release()は、exit()/exece()からmm_release()がコールされます。tsk->vfork_done!=NULLなら、呼び出したプロセスは、vforkで作成されたプロセスです。complete_vfork_done()でvfork->waitにリストされているプロセス(親プロセス)を起床させます。
void mm_release(struct task_struct *tsk, struct mm_struct *mm) { #ifdef CONFIG_FUTEX if (unlikely(tsk->robust_list)) { exit_robust_list(tsk); tsk->robust_list = NULL; } #ifdef CONFIG_COMPAT if (unlikely(tsk->compat_robust_list)) { compat_exit_robust_list(tsk); tsk->compat_robust_list = NULL; } #endif if (unlikely(!list_empty(&tsk->pi_state_list))) exit_pi_state_list(tsk); #endif deactivate_mm(tsk, mm); if (tsk->vfork_done) complete_vfork_done(tsk); if (tsk->clear_child_tid) { if (!(tsk->flags & PF_SIGNALED) && atomic_read(&mm->mm_users) > 1) { put_user(0, tsk->clear_child_tid); sys_futex(tsk->clear_child_tid, FUTEX_WAKE, 1, NULL, NULL, 0); } tsk->clear_child_tid = NULL; } } static void complete_vfork_done(struct task_struct *tsk) { struct completion *vfork; task_lock(tsk); vfork = tsk->vfork_done; if (likely(vfork)) { tsk->vfork_done = NULL; complete(vfork); } task_unlock(tsk); }
備考
子プロセスがexecとする場合、親プロセスがCOWしないことが保障される。と言うことで、forkよりベターということですが、たいした効果は期待できそうもありません。またexecしない子プロセスの処理が終わってからでないと、親プロセスを動作させないような処理ということですが、それだと通常の関数コールで言い訳です。外部コマンドを関数のように使うことができるように、execで親プロセスを起動させない、execのオプションNO_VFORKを追加するってのもいいのでは。execしてもプロセス構造体は継承しますから、そのvforkの完了通知も継承してるはずですので、実装は簡単なはずです。