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

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

Linux Kernel ~ カーネルスレッドとプロセスの破棄及び削除 x86編 ~

概要

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

今回はカーネルスレッドとプロセスの破棄及び削除について見ていく。

カーネルスレッド

伝統的なUNIXシステムではディスクキャッシュの書き出し、使用されていないページのスワップアウト、ネットワーク接続の管理といった重要な処理をカーネルスレッドに任せている。

カーネルスレッドは以下の点で通常のプロセスとは異なる。

  • カーネルスレッドはカーネルモードでしか実行されないが、ユーザプロセスはユーザモードとカーネルモードを交互に実行する。
  • カーネルスレッドはカーネルモードでしか実行されないためPAGE_OFFSET以降のアドレスしか使用しない。通常のプロセスではリニアアドレスとしてユーザモード及びカーネルモードで4GBを使用する。

カーネルスレッドの生成

カーネルスレッドの生成はkernel_thread()を呼び出して行われる。

// arch/i386/kernel/process.c
/*
 * Create a kernel thread
 */
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
    struct pt_regs regs;

    memset(&regs, 0, sizeof(regs));

    regs.ebx = (unsigned long) fn;
    regs.edx = (unsigned long) arg;

    regs.xds = __USER_DS;
    regs.xes = __USER_DS;
    regs.orig_eax = -1;
    regs.eip = (unsigned long) kernel_thread_helper;
    regs.xcs = __KERNEL_CS;
    regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;

    /* Ok, create the new process.. */
    return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}

上記を見るとdo_fork()が以下のように呼び出されているのがわかる。

do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);

CLONE_VMフラグはページテーブルの複製を回避し、CLONE_UNTRACEDフラグは呼び出したプロセスが監視されている場合に新しいスレッドが監視されないことを保証する。

kernel_thread()内部で生成したpregsはCPUレジスタの値を保存する構造体で以下のように定義されている。

// include/asm-i386/ptrace.h
/* this struct defines the way the registers are stored on the 
   stack during a system call. */

struct pt_regs {
    long ebx;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};

kernel_thread()では以下のようにrepsebxedxに関数ポインタであるfnとその引数argが設定される。そしてipには(unsigned long) kernel_thread_helperが設定される。

// arch/i386/kernel/process.c
regs.ebx = (unsigned long) fn;
regs.edx = (unsigned long) arg;
:
regs.eip = (unsigned long) kernel_thread_helper;

kernel_thread_helperは以下のように定義された関数で、edxをスタックに積み(引数)、ebxに設定された値(fn)をアドレスとして引数に取りcall命令を発行している。関数実行後はcallした関数の戻り値であるeaxをスタックに積み、do_exit()を呼んでいる。`

/*
 * This gets run with %ebx containing the
 * function to call, and %edx containing
 * the "args".
 */
extern void kernel_thread_helper(void);
__asm__(".section .text\n"
    ".align 4\n"
    "kernel_thread_helper:\n\t"
    "movl %edx,%eax\n\t"
    "pushl %edx\n\t"
    "call *%ebx\n\t"
    "pushl %eax\n\t"
    "call do_exit\n"
    ".previous");

上記を見るとカーネルスレッドは指定された関数を実行し、処理が終了するとプロセスを終了することがわかる。

プロセス0

プロセスの先祖はLinuxの初期化時にゼロから生成されるカーネルスレッドのことで、静的に割り当てられたデータ構造を使用する。(他の全てのプロセスは動的にデータが確保される)

プロセス0が生成される際に使用されるデータを以下に示す。

  • init_task変数に設定されたプロセスディスクリプタがプロセス0となる。以下のようにINIT_TASKマクロによって初期化される。
// arch/i386/kernel/init_task.c
/*
 * Initial task structure.
 *
 * All other task structs will be allocated on slabs in fork.c
 */
struct task_struct init_task = INIT_TASK(init_task);
  • init_thread_union変数に格納されたthread_info構造体とカーネルスタック。以下のように定義されており、INIT_THREAD_INFOマクロによって初期化される。
// include/linux/sched.h
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

extern union thread_union init_thread_union;

// include/asm-i386/thread_info.h
#define INIT_THREAD_INFO(tsk)          \
{                      \
   .task       = &tsk,         \
   .exec_domain    = &default_exec_domain, \
   .flags      = 0,           \
   .cpu        = 0,           \
   .preempt_count  = 1,           \
   .addr_limit = KERNEL_DS,        \
   .restart_block = {          \
       .fn = do_no_restart_syscall,    \
   },                  \
}
  • プロセスディスクリプタが指す以下のテーブル(括弧内は初期化用のマクロ関数)
    • init_mm(INIT_MM)
    • init_fs(INIT_FS)
    • init_files(INIT_FILES)
    • init_signals(INIT_SIGNALS)
    • init_sighand(INIT_SIFHAND)
// arch/i386/kernel/init_task.c
static struct fs_struct init_fs = INIT_FS;
static struct files_struct init_files = INIT_FILES;
static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);
struct mm_struct init_mm = INIT_MM(init_mm);

// include/linux/fs_struct.h
#define INIT_FS {              \
   .count      = ATOMIC_INIT(1),  \
   .lock       = RW_LOCK_UNLOCKED, \
   .umask      = 0022, \
}

// include/linux/init_task.h
#define INIT_FILES \
{                          \
   .count      = ATOMIC_INIT(1),      \
   .file_lock  = SPIN_LOCK_UNLOCKED,       \
   .max_fds    = NR_OPEN_DEFAULT,      \
   .max_fdset  = __FD_SETSIZE,         \
   .next_fd    = 0,               \
   .fd     = &init_files.fd_array[0],     \
   .close_on_exec  = &init_files.close_on_exec_init, \
   .open_fds   = &init_files.open_fds_init,    \
   .close_on_exec_init = { { 0, } },      \
   .open_fds_init  = { { 0, } },          \
   .fd_array   = { NULL, }            \
}

#define INIT_MM(name) \
{                              \
   .mm_rb      = RB_ROOT,              \
   .pgd        = swapper_pg_dir,           \
   .mm_users   = ATOMIC_INIT(2),          \
   .mm_count   = ATOMIC_INIT(1),          \
   .mmap_sem   = __RWSEM_INITIALIZER(name.mmap_sem),   \
   .page_table_lock =  SPIN_LOCK_UNLOCKED,         \
   .mmlist     = LIST_HEAD_INIT(name.mmlist),      \
   .cpu_vm_mask    = CPU_MASK_ALL,             \
   .default_kioctx = INIT_KIOCTX(name.default_kioctx, name),   \
}

#define INIT_SIGNALS(sig) {    \
   .count      = ATOMIC_INIT(1),      \
   .wait_chldexit  = __WAIT_QUEUE_HEAD_INITIALIZER(sig.wait_chldexit),\
   .shared_pending = {                 \
       .list = LIST_HEAD_INIT(sig.shared_pending.list),    \
       .signal =  {{0}}}, \
   .posix_timers    = LIST_HEAD_INIT(sig.posix_timers),        \
   .rlim       = INIT_RLIMITS,                 \
}

#define INIT_SIGHAND(sighand) {                        \
   .count      = ATOMIC_INIT(1),              \
   .action     = { { { .sa_handler = NULL, } }, },        \
   .siglock    = SPIN_LOCK_UNLOCKED,               \
}
// include/asm-i386/pgtable.h
extern pgd_t swapper_pg_dir[1024];

カーネルが必要とする全てのデータ構造の初期化、割り込みの許可、プロセス1(initプロセス)と呼ばれるカーネルスレッドの生成を行うのがstart_kernel()である。

// init/main.c
/*
 * Activate the first processor.
 */

asmlinkage void __init start_kernel(void)
{
    char * command_line;
    extern struct kernel_param __start___param[], __stop___param[];
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
    lock_kernel();
    page_address_init();
    printk(linux_banner);
    setup_arch(&command_line);
    setup_per_cpu_areas();

    /*
    * Mark the boot cpu "online" so that it can call console drivers in
    * printk() and can access its per-cpu storage.
    */
    smp_prepare_boot_cpu();

    /*
    * Set up the scheduler prior starting any interrupts (such as the
    * timer interrupt). Full topology setup happens at smp_init()
    * time - but meanwhile we still have a functioning scheduler.
    */
    sched_init();
    /*
    * Disable preemption - early bootup scheduling is extremely
    * fragile until we cpu_idle() for the first time.
    */
    preempt_disable();
    build_all_zonelists();
    page_alloc_init();
    printk("Kernel command line: %s\n", saved_command_line);
    parse_early_param();
    parse_args("Booting kernel", command_line, __start___param,
           __stop___param - __start___param,
           &unknown_bootoption);
    sort_main_extable();
    trap_init();
    rcu_init();
    init_IRQ();
    pidhash_init();
    init_timers();
    softirq_init();
    time_init();

    /*
    * HACK ALERT! This is early. We're enabling the console before
    * we've done PCI setups etc, and console_init() must be aware of
    * this. But we do want output early, in case something goes wrong.
    */
    console_init();
    if (panic_later)
        panic(panic_later, panic_param);
    profile_init();
    local_irq_enable();
    vfs_caches_init_early();
    mem_init();
    kmem_cache_init();
    numa_policy_init();
    if (late_time_init)
        late_time_init();
    calibrate_delay();
    pidmap_init();
    pgtable_cache_init();
    prio_tree_init();
    anon_vma_init();
    fork_init(num_physpages);
    proc_caches_init();
    buffer_init();
    unnamed_dev_init();
    security_init();
    vfs_caches_init(num_physpages);
    radix_tree_init();
    signals_init();
    /* rootfs populating might need page-writeback */
    page_writeback_init();
    check_bugs();

    acpi_early_init(); /* before LAPIC and SMP init */

    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

そしてそのプロセス1、多くの場合はinitプロセスと呼ばれるプロセスを生成しているのがrest_init()となる。

// init/main.c
static void noinline rest_init(void)
    __releases(kernel_lock)
{
    kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
    numa_default_policy();
    unlock_kernel();
    preempt_enable_no_resched();
    cpu_idle();
}

rest_init()内のkernel_thread()でPID1のカーネルスレッドが生成され、第一引数で渡されたinitが実行される。

initプロセスを生成した後、プロセス0は上記にもあるようにcpu_idle()関数を実行する。当該関数は割り込みを許可した状態でアセンブリ命令であるhltを繰り返し実行する。スケジューラは他に実行可能なプロセスが存在しない場合に当該関数を実行する。

// arch/i386/kernel/process.c
/*
 * The idle thread. There's no useful work to be
 * done, so just try to conserve power and have a
 * low exit latency (ie sit in a loop waiting for
 * somebody to say that they'd like to reschedule)
 */
void cpu_idle (void)
{
    int cpu = _smp_processor_id();

    /* endless idle loop with no priority at all */
    while (1) {
        while (!need_resched()) {
            void (*idle)(void);

            if (cpu_isset(cpu, cpu_idle_map))
                cpu_clear(cpu, cpu_idle_map);
            rmb();
            idle = pm_idle;

            if (!idle)
                idle = default_idle;

            irq_stat[cpu].idle_timestamp = jiffies;
            idle();
        }
        schedule();
    }
}

マルチコアシステムではそれぞれのCPUに対してプロセス0が存在する。電源投入後はBIOSが他のCPUの動作を禁止して単一のCPUのみ動作を許可する。

最初に起動するプロセスをスワッパープロセスと呼び、最初に実行を許可されたCPU上で動作するスワッパープロセスがカーネルデータ構造を初期化する。その後他のCPUを実行を許可しcopy_process()によって追加のスワッパープロセスを生成する。このプロセスもPIDは0で生成したそれぞれのプロセスのthread_infocpuメンバに適切なCPU番号を設定する。

プロセス1

先ほど見たプロセス0内でkernel_thread()を用いて生成したカーネルスレッド、プロセス1を見ていく。

まずプロセス1が実行するのはkernel_thread()の第一引数に渡されていたinit()を実行する。

// init/main.c
static int init(void * unused)
{
    lock_kernel();
    /*
    * Tell the world that we're going to be the grim
    * reaper of innocent orphaned children.
    *
    * We don't want people to have to make incorrect
    * assumptions about where in the task array this
    * can be found.
    */
    child_reaper = current;

    /* Sets up cpus_possible() */
    smp_prepare_cpus(max_cpus);

    do_pre_smp_initcalls();

    fixup_cpu_present_map();
    smp_init();
    sched_init_smp();

    /*
    * Do this before initcalls, because some drivers want to access
    * firmware files.
    */
    populate_rootfs();

    do_basic_setup();

    /*
    * check if there is an early userspace init.  If yes, let it do all
    * the work
    */
    if (sys_access((const char __user *) "/init", 0) == 0)
        execute_command = "/init";
    else
        prepare_namespace();

    /*
    * Ok, we have completed the initial bootup, and
    * we're essentially up and running. Get rid of the
    * initmem segments and start the user-mode stuff..
    */
    free_initmem();
    unlock_kernel();
    system_state = SYSTEM_RUNNING;
    numa_default_policy();

    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
        printk("Warning: unable to open an initial console.\n");

    (void) sys_dup(0);
    (void) sys_dup(0);
    
    /*
    * We try each of these until one succeeds.
    *
    * The Bourne shell can be used instead of init if we are 
    * trying to recover a really broken machine.
    */

    if (execute_command)
        run_init_process(execute_command);

    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");

    panic("No init found.  Try passing init= option to kernel.");
}

init()は次々に初期化処理を行い最後にrun_init_process()で実行可能なinitプログラムを実行する。

// init/main.c
static void run_init_process(char *init_filename)
{
    argv_init[0] = init_filename;
    execve(init_filename, argv_init, envp_init);
}

その後initカーネルスレッドは自身のカーネルデータを持った通常プロセスになる。その後も当該プロセスはOSの機能を実装する全プロセス群の生成及び監視を行うためにinitプロセスが終了することはない。

その他のカーネルスレッド

Linuxの多くはカーネルスレッドを利用する。いくつかのカーネルスレッドはシステム停止時まで動作するし続ける。他のカーネルスレッドは自分自身のコンテキスト上で実行するよりうまく実行できる時などに必要に応じて生成する。

プロセス0とプロセス1以外のカーネルスレッドを以下に示す。

  • keventd: eventsとも呼ばれ、keventd_wqワークキューに登録された関数を実行する。
  • kampd: APM関連の事象を処理する。
  • kswapd: メモリの回収を行う。
  • pdflush: メモリにある「汚れた」バッファをディスクへ書き戻す。
  • kblockd: kblockワークキューに登録された関数を実行する。実質的には定期的にブロックデバイスドライバを駆動する。
  • ksostirqd: タスクレットを実行する。システム上のCPUに1つのカーネルスレッドを割り当てる。

プロセスの破棄

ほとんどのプロセスは実行を終了することにより「死ぬ」ことになる。カーネルはプロセスを検知して、プロセスが所有するメモリ、オープンされたファイル、セマフォといった資源を解放する必要がある。

exit() ライブラリ関数を呼び出すことでプロセスを終了し、ライブラリが確保した資源の解放やプログラマが登録した関数の実行、システムから当該プロセスを立ち退かせる処理を行う。Cコンパイラはmain関数実行後にもexit()を呼んでいる。

カーネル自身がプロセスを強制的に終了させることもある。グループが処理できないシグナルや無視できないシグナルを受信した時やプロセスの延長でカーネルが回復不能な例外が発生した時などである。

プロセスの終了

Linux 2.6ではユーザ空間で動作するアプリケーションを終了する2つのシステムコールが用意されている。

  • exit_group()システムコール: スレッドグループを丸ごと、つまりマルチスレッドアプリケーションを丸ごと終了する。
  • _exit()システムコール: 1つのプロセスのみを終了する。同一スレッドグループの他のプロセスに影響を与えない。このシステムコールを実装しているのはdo_exit()である。

do_group_exit()

do_group_exit()currentと同じスレッドグループに属するプロセスを終了する。引数には終了コードを受け取り、exit_group()で指定された値(通常終了)若しくはカーネルによって与えられたエラーコード(異常終了)となる。

// kernel/exit.c
/*
 * Take down every thread in the group.  This is called by fatal signals
 * as well as by sys_exit_group (below).
 */
NORET_TYPE void
do_group_exit(int exit_code)
{
    BUG_ON(exit_code & 0x80); /* core dumps don't get here */

    if (current->signal->flags & SIGNAL_GROUP_EXIT)
        exit_code = current->signal->group_exit_code;
    else if (!thread_group_empty(current)) {
        struct signal_struct *const sig = current->signal;
        struct sighand_struct *const sighand = current->sighand;
        read_lock(&tasklist_lock);
        spin_lock_irq(&sighand->siglock);
        if (sig->flags & SIGNAL_GROUP_EXIT)
            /* Another thread got here before we took the lock.  */
            exit_code = sig->group_exit_code;
        else {
            sig->flags = SIGNAL_GROUP_EXIT;
            sig->group_exit_code = exit_code;
            zap_other_threads(current);
        }
        spin_unlock_irq(&sighand->siglock);
        read_unlock(&tasklist_lock);
    }

    do_exit(exit_code);
    /* NOTREACHED */
}

do_group_exit()では以下の処理を行う。

  1. 最初の条件分岐(if (current->signal->flags & SIGNAL_GROUP_EXIT))が真である場合、すなわちフラグにSIGNAL_GROUP_EXITが設定されている場合は、当該スレッドグループの終了処理が開始しているのでcurrent->signal->group_exit_codeの値を終了コード(exit_code)として設定する。
  2. 次の条件分岐(else if (!thread_group_empty(current)))ではcurrentのスレッドグループに他のプロセスが存在する場合にはzap_other_threads(current);でそれらのプロセスを終了する。zap_other_threads()current->tgidに対応するPIDTYPE_TGIDハッシュテーブルのPIDリストを捜査し、current以外のプロセスにSIGKILLシグナルを送る。結果的にそれらのプロセスはdo_exit()を実行して終了する。
  3. do_exit(exit_code);ではプロセスの終了コードを引数としてプロセスを終了させる。

上記でカレントプロセスが属しているスレッドグループにSKILLSIGを与えるzap_other_threads()を見ていく。

/*
 * Nuke all other threads in the group.
 */
void zap_other_threads(struct task_struct *p)
{
    struct task_struct *t;

    p->signal->flags = SIGNAL_GROUP_EXIT;
    p->signal->group_stop_count = 0;

    if (thread_group_empty(p))
        return;

    for (t = next_thread(p); t != p; t = next_thread(t)) {
        /*
        * Don't bother with already dead threads
        */
        if (t->exit_state)
            continue;

        /*
        * We don't want to notify the parent, since we are
        * killed as part of a thread group due to another
        * thread doing an execve() or similar. So set the
        * exit signal to -1 to allow immediate reaping of
        * the process.  But don't detach the thread group
        * leader.
        */
        if (t != p->group_leader)
            t->exit_signal = -1;

        sigaddset(&t->pending.signal, SIGKILL);
        rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);
        signal_wake_up(t, 1);
    }
}

まず最初の条件分岐if (thread_group_empty(p))ではカレントプロセスが属している。thread_group_empty()を見るとスレッドグループのPIDハッシュテーブルを確認しているのがわかる。

そしてfor文ではカレントプロセスのスレッドグループを走査しスレッドリーダ以外にSIGKILLを送信し、対象のプロセスを起こしているのがわかる。

do_exit()

全てのプロセスの終了はdo_exit()関数が行い、カーネルデータ構造から終了プロセスへのほとんどの参照を削除する。当該関数は以下のように定義されている。

// kernel/exit.c
fastcall NORET_TYPE void do_exit(long code)
{
    struct task_struct *tsk = current;
    int group_dead;

    profile_task_exit(tsk);
    
    :
    (省略)
    :

    tsk->flags |= PF_EXITING;
    del_timer_sync(&tsk->real_timer);

    if (unlikely(in_atomic()))
        printk(KERN_INFO "note: %s[%d] exited with preempt_count %d\n",
                current->comm, current->pid,
                preempt_count());

    acct_update_integrals();
    update_mem_hiwater();
    group_dead = atomic_dec_and_test(&tsk->signal->live);
    if (group_dead)
        acct_process(code);
    exit_mm(tsk);

    exit_sem(tsk);
    __exit_files(tsk);
    __exit_fs(tsk);
    exit_namespace(tsk);
    exit_thread();
    exit_keys(tsk);

    if (group_dead && tsk->signal->leader)
        disassociate_ctty(1);

    module_put(tsk->thread_info->exec_domain->module);
    if (tsk->binfmt)
        module_put(tsk->binfmt->module);

    tsk->exit_code = code;
    exit_notify(tsk);
#ifdef CONFIG_NUMA
    mpol_free(tsk->mempolicy);
    tsk->mempolicy = NULL;
#endif

    BUG_ON(!(current->flags & PF_DEAD));
    schedule();
    BUG();
    /* Avoid "noreturn function does return".  */
    for (;;) ;
}

do_exit()関数はプロセスの終了コードを引数として受け取り次の処理を行う。

  • プロセスディスクリプタflagsメンバにPF_EXITINGをセットする。

    ```c tsk->flags |= PF_EXITING;

    #define PF_EXITING 0x00000004 / getting shut down / ```

  • del_timer_sync()で動的タイマーからプロセスを削除する(タイマーは後日追う)

  • exit_の接頭語で始まる関数で以下の処理を行う。(以下のほとんどがメンバ変数にNULLを代入するか、レジスタの値をクリアするというもの)

    • exit_mm(): ページング関連のデータを削除する。
    • exit_sem(): セマフォ関連のデータを削除する。
    • __exit_files(): オープンしているファイルのディスクリプタ関連のデータを削除する。
    • __exit_fs: ファイルシステム関連のデータを削除する。
    • exit_namespace(): 名前空間関連のデータを削除する。
    • exit_thread(): I/Oパーミッションビットマップ関連のデータを削除する。

      ```c exit_mm(tsk);

      exit_sem(tsk); exit_files(tsk); exit_fs(tsk); exit_namespace(tsk); exit_thread(); ```

  • 実行ドメインと実行形式を処理するカーネル関数がカーネルモジュールとして実装されている場合にはカーネルモジュールの利用度数カウンタをデクリメントする。

    ```c // include/asm-i386/local.h static inline void module_put(struct module module) { if (module) { unsigned int cpu = get_cpu(); local_dec(&module->ref[cpu].count); / Maybe they're waiting for us to drop reference? */ if (unlikely(!module_is_live(module))) wake_up_process(module->waiter); put_cpu(); } }

    // include/asm-i386/local.h static inline void local_dec(local_t *v) { asm volatile( "decl %0" :"=m" (v->counter) :"m" (v->counter)); } ```

  • exit_notify()で以下の処理を行う。

    • exit対象のプロセスの子プロセスをinitに引き継がせ、停止中のプロセスがあればSIGHUPを送った後SIGCONTを送る。

      c INIT_LIST_HEAD(&ptrace_dead); forget_original_parent(tsk, &ptrace_dead);

    • 終了するプロセスのディスクリプタexit_signalメンバが-1でなく、かつプロセスのスレッドグループの最後のメンバであることを確認する。その後終了するプロセスの親に子プロセスの死を通知する。

      c if (tsk->exit_signal != SIGCHLD && tsk->exit_signal != -1 && ( tsk->parent_exec_id != t->self_exec_id || tsk->self_exec_id != tsk->parent_exec_id) && !capable(CAP_KILL)) tsk->exit_signal = SIGCHLD;

    • exit_signalメンバが-1の場合またはスレッドグループに他のプロセスが残っている場合にはプロセスが監視されている時に限り(parent != real_parent)親プロセスにSIGCHLDを送信する。

      c if (tsk->exit_signal != -1 && thread_group_empty(tsk)) { int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD; do_notify_parent(tsk, signal); } else if (tsk->ptrace) { do_notify_parent(tsk, SIGCHLD); }

    • プロセスディスクリプタexit_signalメンバが-1で且つプロセスが監視されていない場合にはプロセスディスクリプタexit_stateメンバをEXIT_DEADを設定する。

    • プロセスディスクリプタexit_signalメンバが-1ではないか、またはプロセスが監視されている場合にはexit_stateメンバをEXIT_ZOMBIEに設定する。

      c state = EXIT_ZOMBIE; if (tsk->exit_signal == -1 && (likely(tsk->ptrace == 0) || unlikely(tsk->parent->signal->flags & SIGNAL_GROUP_EXIT))) state = EXIT_DEAD; tsk->exit_state = state;

    • プロセスディスクリプタexit_stateメンバにEXIT_DEADが設定されているプロセスを対象にrelease_task()を呼び出す。プロセスデータ小周防のメモリを回収し、プロセスディスクリプタの利用数カウンタを減らす。

      c list_for_each_safe(_p, _n, &ptrace_dead) { list_del_init(_p); t = list_entry(_p,struct task_struct,ptrace_list); release_task(t); }

    • プロセスディスクリプタのフラグにPF_DEADを設定する。

      c /* PF_DEAD causes final put_task_struct after we schedule. */ preempt_disable(); tsk->flags |= PF_DEAD;

  • schedule()関数を呼び出す。スケジューラはEXIT_ZOMBIEのプロセスを無視するのでschedule()switch_toマクロが実行された直後にプロセスの実行が終了する。

プロセスの削除

UNIX系のOSではカーネルに親プロセスのPIDや子プロセスの実行結果などを問い合わせることができる。親プロセスが子プロセスを生成しwait()系のライブラリ関数を実行して子プロセスを待つ場合、子プロセスの終了後、親プロセスがその終了コードで処理が正常に完了したことを確認できる。

上記からUNIXカーネルはプロセスディスクリプタをプロセスの終了直後には破棄できない。削除できるのは親プロセスのwait()システムコールが完了した後になる。これがEXIT_ZOMBIE状態が存在する理由となる。理論上では子プロセスは既に死んでいるが親プロセスに通知するまではディスクリプタを保存して置く必要がある。

親プロセスが子プロセスよりも先に終了した場合には、その子プロセスはRAM上には残らずinitプロセスの子プロセスとなる。initプロセスは子プロセスの終了をwait()系のシステムコールで待ち、子プロセスの終了後を確認してゾンビプロセスを削除する。

release_task()はゾンビプロセスのディスクリプタから最後のデータ構造を削除する。当該関数は二種類の方法でゾンビプロセスに適応される。親プロセスが子プロセスからの終了ステータスを受け取らない場合do_exit()からrelease_rask()を呼び出し、親プロセスが子プロセスからの終了ステータスを受け取る場合には、終了ステータスが親プロセスに送信された後のwait()システムコール又はwaitpid()からrelease_task()を呼び出す。プロセスディスクリプタのデータ構造はwait4()の場合はスケジューラで解放し、waitpid()の場合はrelease_task()が解放する。

release_task()の処理

// kernel/exit.c
void release_task(struct task_struct * p)
{
    int zap_leader;
    task_t *leader;
    struct dentry *proc_dentry;
    :
  • 終了対象プロセスの所有者に属するプロセス数をデクリメント。

    c atomic_dec(&p->user->processes);

  • プロセスが監視されている場合にはデバッガの監視対象(ptrace_children)から元の親プロセスに割り当て直す。

    c if (unlikely(p->ptrace)) __ptrace_unlink(p);

  • __exit_signal()を呼び出し保留中のシグナルの取り消しとプロセスのsignal_structディスクリプタの解放を行う。このディスクリプタが他の軽量プロセスで使用されていない場合には当該データ構造を削除する。exit_itimers()を呼び出しPOSIXインターバル大麻を削除する。

    c __exit_signal(p);

  • __exit_sighand()を呼び出し、シグナルハンドラを削除する。

    c __exit_sighand(p);

  • __unhash_process()

    • nr_thread変数の値をデクリメントする。

      nr_threads--;

    • PIDおよびTGIDのハッシュテーブルからプロセスディスクリプタを削除する。

      c detach_pid(p, PIDTYPE_PID); detach_pid(p, PIDTYPE_TGID);

    • プロセスがスレッドグループリーダの場合にはPGID及びSIDのハッシュテーブルからプロセスディスクリプタを削除する。

      c if (thread_group_leader(p)) { detach_pid(p, PIDTYPE_PGID); detach_pid(p, PIDTYPE_SID); if (p->pid) __get_cpu_var(process_counts)--;

    • プロセスリストからディスクリプタを削除する。

      c REMOVE_LINKS(p);

  • プロセスがプロセスリーダでなくかつプロセスの親プロセスがゾンビ状態である場合には親プロセスの親プロセスにプロセスが死んだことを通知する。

    ```c /*

    • If we are the last non-leader member of the thread
    • group, and the leader is zombie, then notify the
    • group leader's parent process. (if it wants notification.) / zap_leader = 0; leader = p->group_leader; if (leader != p && thread_group_empty(leader) && leader->exit_state == EXIT_ZOMBIE) { BUG_ON(leader->exit_signal == -1); do_notify_parent(leader, leader->exit_signal); /
      • If we were the last child thread and the leader has
      • exited already, and the leader's parent ignores SIGCHLD,
      • then we are the one who should release the leader. *
      • do_notify_parent() will have marked it self-reaping in
      • that case. */ zap_leader = (leader->exit_signal == -1); } ```
  • sched_exit()を呼び出しタイムスライスを調整する。

    c sched_exit(p);

  • put_task_struct()でプロセスディスクリプタの利用カウンタをデクリメントする。カウンタが0になった場合にはプロセスに関する残りの参照関係も解放する。

    • プロセスのuser_structの利用数カウンタをデクリメントする。使用数カウンタが0になった場合には当該データ構造を削除する。

      c #define put_task_struct(tsk) \ do { if (atomic_dec_and_test(&(tsk)->usage)) __put_task_struct(tsk); } while(0)

    • プロセスディスクリプタthread_infoディスクリプタカーネルモードスタックを格納していたメモリ領域を解放する。

      ```c void free_task(struct task_struct *tsk) { free_thread_info(tsk->thread_info); free_task_struct(tsk); } EXPORT_SYMBOL(free_task);

      void __put_task_struct(struct task_struct *tsk) { WARN_ON(!(tsk->exit_state & (EXIT_DEAD | EXIT_ZOMBIE))); WARN_ON(atomic_read(&tsk->usage)); WARN_ON(tsk == current);

        if (unlikely(tsk->audit_context))
            audit_free(tsk);
        security_task_free(tsk);
        free_uid(tsk->user);
        put_group_info(tsk->group_info);
      
        if (!profile_handoff_task(tsk))
            free_task(tsk);
      

      } ```