vforkシステムコール
Rev.5を表示中。最新版はこちら。
forkで子プロセスを作成すると、子プロセスは親の実メモリを共有し、親子プロセスに関係なくメモリの更新処理(変数への代入等)は、更新したプロセスがその仮想アドレスエリアに割り当てられている実メモリをPAGE単位(通常4K)で新規に割り当てる(COW)ことで、親子間のメモリを管理しています。従って参照だけなら親子プロセスはメモリを共有しつずけることになります。なお、子プロセスをexecv()で差し替えれば、子プロセスは親と同じである仮想メモリ(この空間は、子プロセスが独自に有しています。)を破棄し、新規に割り当て、同時に必要とする実メモリを割り当て、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); }
備考
子プロセスがexecveとする場合、親プロセスがCOWしないことが保障される。と言うことで、forkよりベターということですが、たいした効果は期待できそうもありません。またexecしない子プロセスの処理が終わってからでないと、親プロセスを動作させないような処理ということですが、それだと通常の関数コールで言い訳です。外部コマンドを関数のように使うことができるように、execで親プロセスを起動させない、execのオプションNO_VFORKを追加するってのもいいのではと。execvしてもプロセス構造体は継承(その物を使用)しますから、そのvforkの完了通知も継承していますので、実装はmm_release()でcomplete_vfork_done()をコールしなければいいだけです。
ただし、execveシステムコールはオプションのパラメータは無いので、パラメータのargv内にマジックコードによる設定とする必要がありそうです。