レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ プロセス生成 x86編 ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回はプロセスの生成について見ていく。

プロセス生成

現在のUNIXカーネルでは以下の方法で効率よくプロセス生成を行なっている。

  • コピーオンライトを用いることで親子両方のプロセスが同一の物理ページを読む。資源のトラバースや更新を行う際にカーネルは新たにページフレームを複製する。
  • 軽量プロセスを用いることで親子両方のプロセスでページテーブルやファイルテーブル、シグナルのハンドル処理などのカーネルデータ構造を共有できる。

do_fork()

プロセスを新たに生成するためのシステムコールであるclone(), fork(), vfork()が内部でコールしているdo_fork()という関数を見ていく。

その前に上記のシステムコール3つの定義を以下に示す。

// arch/i386/kernel/process.c
asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;
    int __user *parent_tidptr, *child_tidptr;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    parent_tidptr = (int __user *)regs.edx;
    child_tidptr = (int __user *)regs.edi;
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);
}

/*
 * This is trivial, and on the face of it looks like it
 * could equally well be done in user mode.
 *
 * Not so, for quite unobvious reasons - register pressure.
 * In user mode vfork() cannot have a stack frame, and if
 * done by calling the "clone()" system call directly, you
 * do not have enough call-clobbered registers to hold all
 * the information you need.
 */
asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

上記見るといずれの関数でも内部でdo_fork()が呼ばれていることがわかる。それぞれの違いは後ほど説明するCLONE_の接頭語から始めるフラグを見るとわかってくると思う。

余談だがasmlinkageとは引数をスタックから必ずもらうという意味で、以下のように定義されている。

// include/asm-i386/linkage.h
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))

これはなぜかというとシステムコールを使用する段階でCPUのレジスタに渡された引数をスタックに積み直す処理があるためのようだ。システムコールの詳細についてはまた後日追っていく。

話がそれたが本題のdo_fork()関数の定義を以下に示す。

// kernel/fork.c
/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
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 pid = alloc_pidmap();

    if (pid < 0)
        return -EAGAIN;
    if (unlikely(current->ptrace)) {
        trace = fork_traceflag (clone_flags);
        if (trace)
            clone_flags |= CLONE_PTRACE;
    }

    p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
    /*
    * Do this prior waking up the new thread - the thread pointer
    * might get invalid after that point, if the thread exits quickly.
    */
    if (!IS_ERR(p)) {
        struct completion vfork;

        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
        }

        if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
            /*
            * We'll start up with an immediate SIGSTOP.
            */
            sigaddset(&p->pending.signal, SIGSTOP);
            set_tsk_thread_flag(p, TIF_SIGPENDING);
        }

        if (!(clone_flags & CLONE_STOPPED))
            wake_up_new_task(p, clone_flags);
        else
            p->state = TASK_STOPPED;

        if (unlikely (trace)) {
            current->ptrace_message = pid;
            ptrace_notify ((trace << 8) | SIGTRAP);
        }

        if (clone_flags & CLONE_VFORK) {
            wait_for_completion(&vfork);
            if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
                ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
        }
    } else {
        free_pidmap(pid);
        pid = PTR_ERR(p);
    }
    return pid;
}

最初に引数を見ておこうと思う。

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)
{
:

引数は以下のようになっている。

引数名 用途
clone_flags 様々な意味を持ち、下位1byteはプロセスの子プロセス終了時に親プロセスに送られるシグナル番号で一般的にはSIGCHLDを指定する。上位3byteは親子で共有する資源を示すフラグ。
stack_start 親プロセスが子プロセスに割り当てるユーザ空間でのスタックポインタ。
regs ユーザモードからカーネルモードへの切り替え時にカーネルスタックに保存される汎用レジスタの値へのポインタ
stack_size 使用されていない
parent_tidptr 新しい軽量プロセスのPIDを保持するための親プロセスのユーザ空間内の変数アドレス。 CLONE_PARENT_SETTIDが設置されている時のみ有効。
child_tidptr 新しい軽量プロセスのPIDを保持するための子プロセスのユーザ空間内の変数アドレス。CLONE_CHILD_SETTIDが設置されている時のみ有効。

clone_flagsに指定されるフラグは以下のように定義されている。

// include/linux/sched.h

/*
 * cloning flags:
 */
#define CSIGNAL        0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM   0x00000100 /* set if VM shared between processes */
#define CLONE_FS   0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES    0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND  0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE   0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK    0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT   0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD   0x00010000 /* Same thread group? */
#define CLONE_NEWNS    0x00020000 /* New namespace group? */
#define CLONE_SYSVSEM  0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS   0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID    0x00100000 /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID   0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED     0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED     0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
#define CLONE_STOPPED      0x02000000 /* Start in stopped state */
フラグ名 意味
CLONE_VM メモリディスクリプタ及び全ページテーブルを共有
CLONE_FS ルートディレクトリ及びワーキングディレクトリのテーブルを共有。ファイルパーミッションの初期値であるビットマスクも含まれる
CLONE_FILES オープンされているファイルテーブルを共有
CLONE_SIGHAND シグナルハンドラ、ブロックするシグナル及び保留中のシグナルのテーブルを共有。CLONE_SIGHANDフラグを指定する時にはCLONE_VMも指定する必要がある
CLONE_PTRACE 親プロセスが監視対象である時、子プロセスも同様に監視対象とする。カーネルは当該フラグを強制的に立てる
CLONE_VFORK vfork()から呼ばれていることを示すフラグ
CLONE_PARENT 子プロセスの親として呼び出しプロセスの親を設定する(parent, real_parentと共に)
CLONE_THREAD 子プロセスを親プロセスと同じスレッドグループに入れ、シグナルディスクリプタを共有する。(CLONE_SIGHANDと一緒に指定する必要がある)
CLONE_NEWNS cloneが独自の名前空間(ファイルシステム)を必要とする場合に指定。CLONE_FSと同時には指定不可
CLONE_SYSVSEM System VのIPCのundo可能なセマフォ処理を親子で共有
CLONE_SETTLS 子プロセス用のTLSを生成
CLONE_PARENT_SETTID 親プロセスのユーザモード空間のptid引数が示す変数に子プロセスのPIDを書く
CLONE_CHILD_CLEARTID 子プロセスの終了や別プログラム実行をカーネルが通知する。これらの事象が発生するとctidが指す変数に0を書き込み当該事象を待っていたプロセスを起こす
CLONE_DETACHED 現在は使用されていない
CLONE_UNTRACED CLONE_PTRACEフラグを無効にするためにカーネルが設定するフラグ
CLONE_CHILD_SETTID 子プロセスのユーザモード空間のctidが指す変数に子プロセスのPIDを書き込む
CLONE_STOPPED 子プロセスの実行をTASK_STOPPED状態で開始させる

次に生成するプロセスのディスクリプタ作成及び当該プロセスPIDの生成を行なっている以下の箇所。traceはトレースを確認する際に用いられる。

struct task_struct *p;
int trace = 0;
long pid = alloc_pidmap();

次に以下ではPIDが生成できたかを確認し失敗した場合はエラーを返している。次にプロセスディスクリプタptraceを確認し親別のプロセスから監視されているかをチェックする(1:監視されている, 0:監視されていない)。次にfork_traceflag()で実行プロセスがカーネルスレッドではないことを確かめ、そうでなければ子プロセスを監視対象とする旨を伝えるためclone_flagsCLONE_PTRACEをセットする。

if (pid < 0)
    return -EAGAIN;
if (unlikely(current->ptrace)) {
    trace = fork_traceflag (clone_flags);
    if (trace)
        clone_flags |= CLONE_PTRACE;
}

ちなみに上記で用いられているCLONE_の接頭語から始まるフラグは先ほどの

次に実際にプロセスを複数性する処理。

p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);

上記がfork()の主要な処理となる。これは後で詳細を追う。

実際にcopy_process()が成功した際の処理を見ていく。

if (!IS_ERR(p)) {
    :

以下はvfork()でコールされた場合の処理。子プロセスの終了状態を取得するための構造体を初期化している。

struct completion vfork;

if (clone_flags & CLONE_VFORK) {
    p->vfork_done = &vfork;
    init_completion(&vfork);
}

以下では親プロセスが監視されている場合つまり子プロセスも監視対象となっている場合、若しくはCLONE_STOPPEDがセットされていた場合、子プロセスにシグナルであるSIGSTOPをセットする。そしてset_tsk_thread_flagでプロセスのthread_infoのメンバであるflagsにシグナルが待機していることを示すフラグをセットする。

if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
    /*
    * We'll start up with an immediate SIGSTOP.
    */
    sigaddset(&p->pending.signal, SIGSTOP);
    set_tsk_thread_flag(p, TIF_SIGPENDING);
}

// include/asm-i386/thread_info.h
#define TIF_SIGPENDING     2  /* signal pending */

次にCLONE_STOPPEDがセットされていない場合の処理。当該フラグがセットされていない場合にはすぐに子プロセスを起こし、セットされている場合には単純にstateTASK_STOPPEDをセットする。

if (!(clone_flags & CLONE_STOPPED))
    wake_up_new_task(p, clone_flags);
else
    p->state = TASK_STOPPED;

以下は親プロセスが監視されている場合の処理で、監視しているプロセス(parent)にカレントプロセス(current)が子プロセスをfork()したことをptrace_notify()で通知する。その際にptrace_messageから生成されたプロセスのPIDを得る。

if (unlikely (trace)) {
    current->ptrace_message = pid;
    ptrace_notify ((trace << 8) | SIGTRAP);
}

CLONE_VFORKフラグがセットされていた場合は生成した子プロセスの終了を待つ(wait_for_completion()で親プロセスを待ちキューに入れる)。その後先ほど使用したptrace_notify()で終了したことを通知する。

if (clone_flags & CLONE_VFORK) {
    wait_for_completion(&vfork);
    if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
        ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}

最後に子プロセスのPIDを返して処理は以上となる。

return pid;

copy_process()

次に先ほどdo_fork()関数の中でコールされていたprocess()関数を見ていく。

// kernel/fork.c
static task_t *copy_process(unsigned long clone_flags,
                 unsigned long stack_start,
                 struct pt_regs *regs,
                 unsigned long stack_size,
                 int __user *parent_tidptr,
                 int __user *child_tidptr,
                 int pid)
{
    int retval;
    struct task_struct *p = NULL;

    if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
        return ERR_PTR(-EINVAL);

    if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
        return ERR_PTR(-EINVAL);

    if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
        return ERR_PTR(-EINVAL);

    retval = security_task_create(clone_flags);
    if (retval)
        goto fork_out;

    retval = -ENOMEM;
    p = dup_task_struct(current);
    if (!p)
        goto fork_out;

    retval = -EAGAIN;
    if (atomic_read(&p->user->processes) >=
            p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
        if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
                p->user != &root_user)
            goto bad_fork_free;
    }

    atomic_inc(&p->user->__count);
    atomic_inc(&p->user->processes);
    get_group_info(p->group_info);

    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    if (!try_module_get(p->thread_info->exec_domain->module))
        goto bad_fork_cleanup_count;

    if (p->binfmt && !try_module_get(p->binfmt->module))
        goto bad_fork_cleanup_put_domain;

    p->did_exec = 0;
    copy_flags(clone_flags, p);
    p->pid = pid;
    retval = -EFAULT;
    if (clone_flags & CLONE_PARENT_SETTID)
        if (put_user(p->pid, parent_tidptr))
            goto bad_fork_cleanup;

    p->proc_dentry = NULL;

    INIT_LIST_HEAD(&p->children);
    INIT_LIST_HEAD(&p->sibling);
    p->vfork_done = NULL;
    spin_lock_init(&p->alloc_lock);
    spin_lock_init(&p->proc_lock);

    clear_tsk_thread_flag(p, TIF_SIGPENDING);
    init_sigpending(&p->pending);

    p->it_real_value = 0;
    p->it_real_incr = 0;
    p->it_virt_value = cputime_zero;
    p->it_virt_incr = cputime_zero;
    p->it_prof_value = cputime_zero;
    p->it_prof_incr = cputime_zero;
    init_timer(&p->real_timer);
    p->real_timer.data = (unsigned long) p;

    p->utime = cputime_zero;
    p->stime = cputime_zero;
    p->rchar = 0;       /* I/O counter: bytes read */
    p->wchar = 0;       /* I/O counter: bytes written */
    p->syscr = 0;       /* I/O counter: read syscalls */
    p->syscw = 0;       /* I/O counter: write syscalls */
    acct_clear_integrals(p);

    p->lock_depth = -1;     /* -1 = no lock */
    do_posix_clock_monotonic_gettime(&p->start_time);
    p->security = NULL;
    p->io_context = NULL;
    p->io_wait = NULL;
    p->audit_context = NULL;
#ifdef CONFIG_NUMA
    p->mempolicy = mpol_copy(p->mempolicy);
    if (IS_ERR(p->mempolicy)) {
        retval = PTR_ERR(p->mempolicy);
        p->mempolicy = NULL;
        goto bad_fork_cleanup;
    }
#endif

    p->tgid = p->pid;
    if (clone_flags & CLONE_THREAD)
        p->tgid = current->tgid;

    if ((retval = security_task_alloc(p)))
        goto bad_fork_cleanup_policy;
    if ((retval = audit_alloc(p)))
        goto bad_fork_cleanup_security;
    /* copy all the process information */
    if ((retval = copy_semundo(clone_flags, p)))
        goto bad_fork_cleanup_audit;
    if ((retval = copy_files(clone_flags, p)))
        goto bad_fork_cleanup_semundo;
    if ((retval = copy_fs(clone_flags, p)))
        goto bad_fork_cleanup_files;
    if ((retval = copy_sighand(clone_flags, p)))
        goto bad_fork_cleanup_fs;
    if ((retval = copy_signal(clone_flags, p)))
        goto bad_fork_cleanup_sighand;
    if ((retval = copy_mm(clone_flags, p)))
        goto bad_fork_cleanup_signal;
    if ((retval = copy_keys(clone_flags, p)))
        goto bad_fork_cleanup_mm;
    if ((retval = copy_namespace(clone_flags, p)))
        goto bad_fork_cleanup_keys;
    retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
    if (retval)
        goto bad_fork_cleanup_namespace;

    p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
    p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;

    clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);

    p->parent_exec_id = p->self_exec_id;

    p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
    p->pdeath_signal = 0;
    p->exit_state = 0;

    sched_fork(p);

    p->group_leader = p;
    INIT_LIST_HEAD(&p->ptrace_children);
    INIT_LIST_HEAD(&p->ptrace_list);

    write_lock_irq(&tasklist_lock);

    p->cpus_allowed = current->cpus_allowed;
    set_task_cpu(p, smp_processor_id());

    if (sigismember(&current->pending.signal, SIGKILL)) {
        write_unlock_irq(&tasklist_lock);
        retval = -EINTR;
        goto bad_fork_cleanup_namespace;
    }

    if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
        p->real_parent = current->real_parent;
    else
        p->real_parent = current;
    p->parent = p->real_parent;

    if (clone_flags & CLONE_THREAD) {
        spin_lock(&current->sighand->siglock);
        if (current->signal->flags & SIGNAL_GROUP_EXIT) {
            spin_unlock(&current->sighand->siglock);
            write_unlock_irq(&tasklist_lock);
            retval = -EAGAIN;
            goto bad_fork_cleanup_namespace;
        }
        p->group_leader = current->group_leader;

        if (current->signal->group_stop_count > 0) {
            current->signal->group_stop_count++;
            set_tsk_thread_flag(p, TIF_SIGPENDING);
        }

        spin_unlock(&current->sighand->siglock);
    }

    SET_LINKS(p);
    if (unlikely(p->ptrace & PT_PTRACED))
        __ptrace_link(p, current->parent);

    attach_pid(p, PIDTYPE_PID, p->pid);
    attach_pid(p, PIDTYPE_TGID, p->tgid);
    if (thread_group_leader(p)) {
        attach_pid(p, PIDTYPE_PGID, process_group(p));
        attach_pid(p, PIDTYPE_SID, p->signal->session);
        if (p->pid)
            __get_cpu_var(process_counts)++;
    }

    nr_threads++;
    total_forks++;
    write_unlock_irq(&tasklist_lock);
    retval = 0;

fork_out:
    if (retval)
        return ERR_PTR(retval);
    return p;

bad_fork_cleanup_namespace:
    exit_namespace(p);
bad_fork_cleanup_keys:
    exit_keys(p);
bad_fork_cleanup_mm:
    if (p->mm)
        mmput(p->mm);
bad_fork_cleanup_signal:
    exit_signal(p);
bad_fork_cleanup_sighand:
    exit_sighand(p);
bad_fork_cleanup_fs:
    exit_fs(p); /* blocking */
bad_fork_cleanup_files:
    exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
    exit_sem(p);
bad_fork_cleanup_audit:
    audit_free(p);
bad_fork_cleanup_security:
    security_task_free(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
    mpol_free(p->mempolicy);
#endif
bad_fork_cleanup:
    if (p->binfmt)
        module_put(p->binfmt->module);
bad_fork_cleanup_put_domain:
    module_put(p->thread_info->exec_domain->module);
bad_fork_cleanup_count:
    put_group_info(p->group_info);
    atomic_dec(&p->user->processes);
    free_uid(p->user);
bad_fork_free:
    free_task(p);
    goto fork_out;
}

まず引数だが以下のように定義されており、do_fork()の引数とdo_dork()内で生成した子プロセスのPIDとなる。

static task_t *copy_process(unsigned long clone_flags,
    unsigned long stack_start,
    struct pt_regs *regs,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr,
    int pid)
{
    int retval;
    struct task_struct *p = NULL;
:

次に以下の箇所では指定したフラグに矛盾がないかをチェックしている。

if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
    return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
    return ERR_PTR(-EINVAL);

例えばCLONE_THREADを指定した場合には同じスレッドグループとなり、シグナルと共有するためCLONE_SIGHANDを一緒に指定する必要がある。またシグナルハンドラを共有する場合(CLONE_SIGHAND)はメモリディスクリプタも共有する必要がある。

次に以下の箇所では追加のセキュリティチェックを行う。これはカーネルコンフィギュレーションで指定があった場合のみとなる。

retval = security_task_create(clone_flags);
if (retval)
    goto fork_out;

次に以下の箇所ではdup_task_struct()で子プロセスのプロセスディスクリプタを生成する。

retval = -ENOMEM;
p = dup_task_struct(current);
if (!p)
    goto fork_out;

dup_task_struct()は以下のように定義されており、task_structとthread_info構造体を確保してから、子プロセスに親の両構造体をセットしている。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;

    prepare_to_copy(orig);

    tsk = alloc_task_struct();
    if (!tsk)
        return NULL;

    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    }

    *ti = *orig->thread_info;
    *tsk = *orig;
    tsk->thread_info = ti;
    ti->task = tsk;

    /* One for us, one for whoever does the "release_task()" (usually parent) */
    atomic_set(&tsk->usage,2);
    return tsk;
}

最後にメンバ変数であるusageに2をセットしているのは当該プロセスが使用中であり生きている(EXIT_ZOMBIEEXIT_DEADである)ことを示す。

次に以下の箇所では現在当該プロセスを実行しているユーザが実行しているプロセスの総数を取得し、資源制限値以上の実行プロセス数でないかを確認している。もし超えていた際には権限を見て特権を持っていない場合にはエラーを返す。

retval = -EAGAIN;
if (atomic_read(&p->user->processes) >=
        p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
    if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
            p->user != &root_user)
        goto bad_fork_free;
}

ちなみに上記で参照されているp->useruser_struct構造体で以下のように定義されている。

// include/linux/sched.h
struct user_struct {
    atomic_t __count;   /* reference count */
    atomic_t processes; /* How many processes does this user have? */
    atomic_t files;     /* How many open files does this user have? */
    atomic_t sigpending;    /* How many pending signals does this user have? */
    /* protected by mq_lock    */
    unsigned long mq_bytes;   /* How many bytes can be allocated to mqueue? */
    unsigned long locked_shm; /* How many pages of mlocked shm ? */

#ifdef CONFIG_KEYS
    struct key *uid_keyring;   /* UID specific keyring */
    struct key *session_keyring;   /* UID's default session keyring */
#endif

    /* Hash table maintenance information */
    struct list_head uidhash_list;
    uid_t uid;
};

次に以下の箇所。上記に示したuser_structのカウンタメンバと当該ユーザの総プロセス数ををインクリメントする。get_group_info()ではプロセスグループであるgroup_info構造体のメンバであるusageをインクリメントしている。

atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
get_group_info(p->group_info);

以下ではシステムの総プロセス数(nr_threads)がmax_threadsの値を超えていないことの確認及び、新しいプロセスの実行ドメインと実行形式用のカーネル関数がカーネルモジュールとして実装されている場合に利用度数のカウンタをインクリメントする処理が行われている。

if (nr_threads >= max_threads)
    goto bad_fork_cleanup_count;

if (!try_module_get(p->thread_info->exec_domain->module))
    goto bad_fork_cleanup_count;

if (p->binfmt && !try_module_get(p->binfmt->module))
    goto bad_fork_cleanup_put_domain;

max_threadsの値はシステムに搭載されているRAMの容量から求められ、thread_infoカーネルスレッドのサイズが物理メモリの1/8を超過しないように設定される。システム管理者であれば/proc/sys/kernel/threads-maxへの書き込みで当該値を設定できる。

ちなみに自身のクラウドに構築した環境では以下のようになっている。

$ sudo cat /proc/meminfo | head
MemTotal:        8168004 kB
MemFree:         6951192 kB
MemAvailable:    7793540 kB
Buffers:          150652 kB
Cached:           870852 kB
SwapCached:            0 kB
Active:           691532 kB
Inactive:         384532 kB
Active(anon):      55012 kB
Inactive(anon):     2564 kB
ubuntu@linux-server:~$ sudo cat /proc/sys/kernel/threads-max
63382

次に以下の箇所。

p->did_exec = 0;
copy_flags(clone_flags, p);
p->pid = pid;
retval = -EFAULT;
if (clone_flags & CLONE_PARENT_SETTID)
    if (put_user(p->pid, parent_tidptr))
        goto bad_fork_cleanup;

まずプロセスがexecve()をコールした回数を記録するp->did_execを0クリアする。

そして次にcopy_flag()でプロセスに引数で受け取ったフラグをセットするのだが、ここではスーパーユーザ権限を使用したかどうかを記録しているPF_SUPERPRIVを落とし、まだexecve()を使用していないことを示すフラグであるPF_FORKNOEXECをセットする。

p->pid = pidは単純にdo_fork()で生成したPIDをセットしているだけである。

最後のネストした条件分岐ではCLONE_PARENT_SETTIDフラグがセットされていた場合に、引数であるparent_tidptrにPIDをセットする。

次に以下の箇所。ここでは統計情報に関するメンバを初期化している。

p->proc_dentry = NULL;

INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
p->vfork_done = NULL;
spin_lock_init(&p->alloc_lock);
spin_lock_init(&p->proc_lock);

clear_tsk_thread_flag(p, TIF_SIGPENDING);
init_sigpending(&p->pending);

p->it_real_value = 0;
p->it_real_incr = 0;
p->it_virt_value = cputime_zero;
p->it_virt_incr = cputime_zero;
p->it_prof_value = cputime_zero;
p->it_prof_incr = cputime_zero;
init_timer(&p->real_timer);
p->real_timer.data = (unsigned long) p;

p->utime = cputime_zero;
p->stime = cputime_zero;
p->rchar = 0;       /* I/O counter: bytes read */
p->wchar = 0;       /* I/O counter: bytes written */
p->syscr = 0;       /* I/O counter: read syscalls */
p->syscw = 0;       /* I/O counter: write syscalls */
acct_clear_integrals(p);

p->lock_depth = -1;     /* -1 = no lock */
do_posix_clock_monotonic_gettime(&p->start_time);
p->security = NULL;
p->io_context = NULL;
p->io_wait = NULL;
p->audit_context = NULL;

次の箇所もメンバの初期化だがcloneに関するフラグをチェックして行なっている。

p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
    p->tgid = current->tgid;

if ((retval = security_task_alloc(p)))
    goto bad_fork_cleanup_policy;
if ((retval = audit_alloc(p)))
    goto bad_fork_cleanup_security;
/* copy all the process information */
if ((retval = copy_semundo(clone_flags, p)))
    goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
    goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
    goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
    goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
    goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p)))
    goto bad_fork_cleanup_signal;
if ((retval = copy_keys(clone_flags, p)))
    goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(clone_flags, p)))
    goto bad_fork_cleanup_keys;

以下の箇所では子プロセスのthreadメンバを初期化している。

retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
    goto bad_fork_cleanup_namespace;

copy_threadは以下のように定義されており、ESP(スタックポインタ)やIP(インストラクションポインタ)などをコピーしているのがわかる。親プロセスつまり現行プロセスがI/Oパーミッションビットマップを保持している場合はそれもコピーする。関数の最後の部分でCLONE_SETTLSフラグがセットされているかを確認しセットされていれば子プロセスにTLSを用意する。

// arch/i386/kernel/process.c
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
    unsigned long unused,
    struct task_struct * p, struct pt_regs * regs)
{
    struct pt_regs * childregs;
    struct task_struct *tsk;
    int err;

    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1;
    *childregs = *regs;
    childregs->eax = 0;
    childregs->esp = esp;

    p->thread.esp = (unsigned long) childregs;
    p->thread.esp0 = (unsigned long) (childregs+1);

    p->thread.eip = (unsigned long) ret_from_fork;

    savesegment(fs,p->thread.fs);
    savesegment(gs,p->thread.gs);

    tsk = current;
    if (unlikely(NULL != tsk->thread.io_bitmap_ptr)) {
        p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
        if (!p->thread.io_bitmap_ptr) {
            p->thread.io_bitmap_max = 0;
            return -ENOMEM;
        }
        memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
            IO_BITMAP_BYTES);
    }

    /*
    * Set a new TLS for the child thread?
    */
    if (clone_flags & CLONE_SETTLS) {
        struct desc_struct *desc;
        struct user_desc info;
        int idx;

        err = -EFAULT;
        if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
            goto out;
        err = -EINVAL;
        if (LDT_empty(&info))
            goto out;

        idx = info.entry_number;
        if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
            goto out;

        desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
        desc->a = LDT_entry_a(&info);
        desc->b = LDT_entry_b(&info);
    }

    err = 0;
 out:
    if (err && p->thread.io_bitmap_ptr) {
        kfree(p->thread.io_bitmap_ptr);
        p->thread.io_bitmap_max = 0;
    }
    return err;
}

上記の処理でchildregs->eax = 0;という記述があるが、これはfork()した際の子プロセスの戻り値で、これに0がセットされているために子プロセスに対するfork()の戻り値が常に0となり条件分岐で親と子の処理を分けることができる。ちなみに先ほど見たdo_fork()の戻り値は子プロセスのPIDとなっており、戻り値が親と子で違うことがわかる。

copy_process関数に戻るが、以下の箇所では対応するフラグが存在すれば当該フラグに対応する変数に値をセットする。これは後に当該メンバを変更する必要があるということを示している。

p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;

次に以下の箇所だが、当該部分ではTIF_SYSCALL_TRACEフラグを落とす。これによりret_from_fork()関数はデバッガプロセスにシステムコールの終了を通知しない。子プロセスのシステムコールへの監視はtsk->ptracePTRACE_SYSCALLフラグで制御するため禁止されない。

clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);

以下の部分では終了のステータスやシグナルを初期化している。

p->parent_exec_id = p->self_exec_id;

p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
p->pdeath_signal = 0;
p->exit_state = 0;

次に以下の箇所では新しいプロセスのスケジューラ用のデータ構造を初期化している。

sched_fork(p);

以下にsched_fork()の定義を示す。

void fastcall sched_fork(task_t *p)
{
    p->state = TASK_RUNNING;
    INIT_LIST_HEAD(&p->run_list);
    p->array = NULL;
    spin_lock_init(&p->switch_lock);
#ifdef CONFIG_SCHEDSTATS
    memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#ifdef CONFIG_PREEMPT
    p->thread_info->preempt_count = 1;
#endif
    local_irq_disable();
    p->time_slice = (current->time_slice + 1) >> 1;
    p->first_time_slice = 1;
    current->time_slice >>= 1;
    p->timestamp = sched_clock();
    if (unlikely(!current->time_slice)) {
        current->time_slice = 1;
        preempt_disable();
        scheduler_tick();
        local_irq_enable();
        preempt_enable();
    } else
        local_irq_enable();
}

上記ではまず新しく生成したプロセスのステータスをTASK_RUNNIGにしている。そしてlocal_irq_disable()でローカルCPUからの割り込みを禁止した後、現行のプロセス(親)のタイムスライスを新たに生成したプロセス(子)と分け合っている。(以下は当該処理の抜粋)

p->time_slice = (current->time_slice + 1) >> 1;
p->first_time_slice = 1;
current->time_slice >>= 1;

copy_process()に戻り、次は以下の箇所。

p->group_leader = p;
INIT_LIST_HEAD(&p->ptrace_children);
INIT_LIST_HEAD(&p->ptrace_list);

上記ではgroup_leaderに新たに生成したプロセス自身を設定しているがこれはまだシステムから当該プロセスを隠すための働きをしている。

次に以下の箇所。これは簡単でこれからtasklistをに対してデータの変更がかからないようにロックするための処理である。

write_lock_irq(&tasklist_lock);

次に以下ではCPUの周りの処理を行なっている。現行プロセスのCPUパーミッションビットマップを新たに生成したプロセスにコピーし、現行のプロセスが使用しているCPUも同じくコピーする。

p->cpus_allowed = current->cpus_allowed;
set_task_cpu(p, smp_processor_id());

ちなみにsmp_processor_id()は以下のように定義されている。

// include/linux/smp.h
#  define smp_processor_id() __smp_processor_id()

// include/asm-i386/smp.h
#define __smp_processor_id() (current_thread_info()->cpu)

次に以下の箇所。以下ではSIGKILLシグナルが送られてきていた場合の処理である。仮に当該シグナルを受診していた場合は先ほどとったロックを解除しエラーを返す。

/*
 * Check for pending SIGKILL! The new thread should not be allowed
 * to slip out of an OOM kill. (or normal SIGKILL.)
 */
if (sigismember(&current->pending.signal, SIGKILL)) {
    write_unlock_irq(&tasklist_lock);
    retval = -EINTR;
    goto bad_fork_cleanup_namespace;
}

以下の処理では親子関係の初期化を行う。CLONE_PARENT又はCLONE_THREADフラグがセットされている場合はcurrentの実の親プロセスであるreal_parentをセットし、当該フラグがセットされていない場合には現行プロセスであるcurrentをセットする。その後セットされたreal_parentparentをセットする。

if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
    p->real_parent = current->real_parent;
else
    p->real_parent = current;
p->parent = p->real_parent;

以下ではCLONE_THREADフラグがセットされていた場合の処理になっている。

if (clone_flags & CLONE_THREAD) {
    spin_lock(&current->sighand->siglock);
    if (current->signal->flags & SIGNAL_GROUP_EXIT) {
        spin_unlock(&current->sighand->siglock);
        write_unlock_irq(&tasklist_lock);
        retval = -EAGAIN;
        goto bad_fork_cleanup_namespace;
    }
    p->group_leader = current->group_leader;

    if (current->signal->group_stop_count > 0) {
        current->signal->group_stop_count++;
        set_tsk_thread_flag(p, TIF_SIGPENDING);
    }
    spin_unlock(&current->sighand->siglock);
}

まずシグナルのフラグにSIGNAL_GROUP_EXITがある場合には結局現行プロセスのスレッドグループ内に存在する全てのスレッドはexitしてしまうため、このプロセスのコピー処理においてもそれを見越してエラーを返す。

次にグループリーダをセットする。

そして2つ目の条件分岐では現行プロセスのグループ内で停止しているプロセスの数が0以上の場合、すなわち停止状態のプロセスが存在する場合には当該グループ内のプロセスは全て止まるため新たに生成したプロセスにもその旨のフラグをセットする。

次には以下の箇所。

SET_LINKS(p);
if (unlikely(p->ptrace & PT_PTRACED))
    __ptrace_link(p, current->parent);

上記の処理では新たに生成したプロセスディスクリプタをプロセスリストに追加し、次にPT_PTRACEDフラグが設定されている場合には__ptrace_link()を呼び出す。当該関数は以下のように定義されている。

// kernel/ptrace.c
void __ptrace_link(task_t *child, task_t *new_parent)
{
    if (!list_empty(&child->ptrace_list))
        BUG();
    if (child->parent == new_parent)
        return;
    list_add(&child->ptrace_list, &child->parent->ptrace_children);
    REMOVE_LINKS(child);
    child->parent = new_parent;
    SET_LINKS(child);
}

上記ではまずptraceリスト(監視対象リスト)に新たに生成したプロセスを追加し、当該プロセスの親に現行プロセスの親すなわちデバッガプロセスを設定する。

ちなみに先ほどのPT_PTRACEDフラグは以下のように定義されている。

// include/linux/ptrace.h
#define PT_PTRACED 0x00000001
#define PT_DTRACE  0x00000002 /* delayed trace (used on m68k, i386) */
#define PT_TRACESYSGOOD    0x00000004
#define PT_PTRACE_CAP  0x00000008 /* ptracer can follow suid-exec */
#define PT_TRACE_FORK  0x00000010
#define PT_TRACE_VFORK 0x00000020
#define PT_TRACE_CLONE 0x00000040
#define PT_TRACE_EXEC  0x00000080
#define PT_TRACE_VFORK_DONE    0x00000100
#define PT_TRACE_EXIT  0x00000200
#define PT_ATTACHED    0x00000400 /* parent != real_parent */

次に以下の処理。PIDハッシュテーブルを初期化している。そして自身がスレッドグループのリーダであった場合にはセッションID(SID)はプロセスグループID(PGID)を初期化する。プロセスのPIDが0以外だった場合は現行のCPUの総プロセス数のインクリメントする。

attach_pid(p, PIDTYPE_PID, p->pid);
attach_pid(p, PIDTYPE_TGID, p->tgid);
if (thread_group_leader(p)) {
    attach_pid(p, PIDTYPE_PGID, process_group(p));
    attach_pid(p, PIDTYPE_SID, p->signal->session);
    if (p->pid)
        __get_cpu_var(process_counts)++;
}

以下の処理でcopy_processの処理は最後となる。

 nr_threads++;
    total_forks++;
    write_unlock_irq(&tasklist_lock);
    retval = 0;

fork_out:
    if (retval)
        return ERR_PTR(retval);
    return p;

まずnr_threadsはシステム全体のプロセスの総数でこれは新たにプロセスが生成されたのでインクリメントする。そしてtotal_forksはforkしたプロセス数を記録しておく変数なのでこれもインクリメント。

先ほどとったロックを解除に最後に戻り値となるretvalに0をセットする。そして最後にreturnで生成したプロセスのディスクリプタを返して処理は全てとなる。

まだreturn文の後にも以下のような処理の記述があるがこれはエラー処理となる。

bad_fork_cleanup_namespace:
    exit_namespace(p);
bad_fork_cleanup_keys:
    exit_keys(p);
bad_fork_cleanup_mm:
    if (p->mm)
        mmput(p->mm);
bad_fork_cleanup_signal:
    exit_signal(p);
bad_fork_cleanup_sighand:
    exit_sighand(p);
bad_fork_cleanup_fs:
    exit_fs(p); /* blocking */
bad_fork_cleanup_files:
    exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
    exit_sem(p);
bad_fork_cleanup_audit:
    audit_free(p);
bad_fork_cleanup_security:
    security_task_free(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
    mpol_free(p->mempolicy);
#endif
bad_fork_cleanup:
    if (p->binfmt)
        module_put(p->binfmt->module);
bad_fork_cleanup_put_domain:
    module_put(p->thread_info->exec_domain->module);
bad_fork_cleanup_count:
    put_group_info(p->group_info);
    atomic_dec(&p->user->processes);
    free_uid(p->user);
bad_fork_free:
    free_task(p);
    goto fork_out;

参照