9. 子プロセスの起動

子プロセスを起動する方法を調べてみます.DOSでは実行中のプロセス と子プロセスを同時に実行できませんが,Linux では同時に複数の プログラムを実行することができます.シェルがコマンドを実行したり, バックグラウンドでコマンドを実行したり,ネットワークサーバが複数の 接続を待ちうけたりする場合などにに利用されます.

子プロセスの作成には fork,子プロセスとして任意のプログラムを起動 するには子プロセスから execve システムコールを使用します.

Linux では (他のUnix も同じですが) 子プロセスを生成するために fork システムコールを使って,自分のプロセスのコピーを生成する方法を とります.別のプログラムを子プロセスとして実行する場合でも自分の コピーを生成して実行しなければなりません.非常に無駄なことを しているようですが,実はカーネル内部でうまいことをして無駄にならない ようになっているので安心してください.

自分のコピーを生成して,それを実行してしまっては,またもう一度自分の コピーを生成して... と無限ループとなるような気がします.しかし, コピーされたプロセスは元のプロセスと同じ位置から実行が継続することを 利用して,また fork の返す値は親プロセスと子プロセスでは区別できる 異なった値を返すようになっていることを利用することによって,実行中の プログラムが親か子かを判断することができるようになっています.子ならば 別のプログラムを実行するためにexecve システムコールを実行すれば 子プロセスとして別のコマンドを実行できます.

実際のシステムコールのカーネル中での定義は以下の形式になっています.

int sys_fork(struct pt_regs regs)
int sys_execve(struct pt_regs regs)

sys_fork も sys_execve も struct pt_regs を引数に渡すようになって います.struct pt_regs は次のように宣言されています.

    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;
    };

どちらのシステムコールも引数は (struct pt_regs regs) ですからC 言語では 値渡しとなります.したがって実際の引数は以下の形式で渡されるものと考える ことができます.

sys_fork( 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)

これは int 0x80 を呼び出して,usr/src/linux/arch/i386/kernel/entry.S の ENTRY(system_call) から実際のシステムコールが呼び出された時のスタックの 状態を反映しています.実際にはシステムコール側の関数の引数宣言がどのような 形になっていてもスタックに積まれている値は struct pt_regs の形式になっており, 通常は最初の 5 個のうちのいくつかがシステムコール側で使用されるようになって います.すべてのシステムコールは int 0x80 で entry.S の ENTRY(system_call) を 経由してカーネルの関数が呼び出されるため,実はどのシステムコールでも同じ 情報を受け取っていることになります. したがって sys_fork では引数として何も 設定しなくても自動的にカーネルに必要な情報は渡されています.

現在のプロセスのコピーを生成して実行するシステムコール fork の返す値が 0 なら 新規に生成された子プロセスを示し,0 でなければ親プロセスであることを示して 生成された子プロセスの pid を返します.これでプロセスが親か子かを同じコード でも判断できます.

fork で生成された子プロセスが別のプログラムを実行するには execve システムコールを使います.execve は実行中のプログラムを指定された ファイル名のコマンドに置き換えて最初から実行をはじめます.

execve システムコールではレジスタに以下のような情報を渡す必要があります.

    ebx : (char *)filename,
    ecx : char ** argv,
    edx : char ** envp

なぜなら,sys_execve 中では do_execve に次のように情報を渡しています

do_execve(filename, (char **) regs.ecx, (char **) regs.edx, &regs);

したがって sys_fork と sys_execve では同じように struct pt_regs を引数 として渡すにもかかわらず,呼び出し側で設定する必要のある引数が異なること になります.

別のプログラムに置換して実行する execve システムコールでプログラムのパス名と コマンドライン引数が必要であることは当然として,環境変数へのポインタを execve に渡す理由を調べてみましょう.環境変数へのポインタを正しく渡した場合 とそうでない場合の環境変数を表示するプログラムを execve すれば分かります.

以前に作成した stackdump.asm による stackdump を起動すれば環境変数の内容を 表示することができました.char ** envp に渡す値を変えて execve で stackdump を起動するプログラムを作成すれば疑問は解決できます.

今回の例では親プロセスが子プロセスの終了を待って次の処理に進むため, 子プロセスの終了を wait4 システムコールを使って待ちます. wait4 システムコールの引数は次のように設定します.

  ebx : pid_t pid
  ecx : unsigned int* stat_addr
  edx : int options
  esi : struct rusage * ru

終了を待つプロセス ID を ebx に,プロセスの終了の状態を設定するための領域 のアドレスを ecx, edx に渡す option は WUNTRACED か WNOHANG を設定しますが, 普通はプロセスの終了を素直に待つ WUNTRACED を使います.プロセスのリソースの 使用状況を返すための rusage 構造体へのポインタを渡す必要があります. rusage 構造体と option に指定する定数の定義のために,カーネルのヘッダファイル <linux/resource.h> と <linux/wait.h> からresource.inc を作成しておきます.

;Copyright (C) 2000 Jun Mizutani <mizutani.jun@nifty.ne.jp>
;
; file          : resource.inc
; created       : 2000/07/18
; derived from  : linux-2.2.14/include/linux/resource.h
;                 linux-2.2.14/include/linux/wait.h

%ifndef __RESOURCE_INC %define __RESOURCE_INC %include "vartype.inc" ; struct timeval ru_utime; user time used ; struct timeval ru_stime; system time used struc rusage .ru_utime_tv_sec LONG 1; user time used .ru_utime_tv_usec LONG 1; .ru_stime_tv_sec LONG 1; system time used .ru_stime_tv_usec LONG 1; .ru_maxrss LONG 1; maximum resident set size .ru_ixrss LONG 1; integral shared memory size .ru_idrss LONG 1; integral unshared data size .ru_isrss LONG 1; integral unshared stack size .ru_minflt LONG 1; page reclaims .ru_majflt LONG 1; page faults .ru_nswap LONG 1; swaps .ru_inblock LONG 1; block input operations .ru_oublock LONG 1; block output operations .ru_msgsnd LONG 1; messages sent .ru_msgrcv LONG 1; messages received .ru_nsignals LONG 1; signals received .ru_nvcsw LONG 1; voluntary context switches .ru_nivcsw LONG 1; involuntary endstruc ; from include/linux/wait.h %assign WNOHANG 0x00000001 %assign WUNTRACED 0x00000002 %endif

次のサンプルは execve で環境変数へのポインタを正しく渡した場合と そうでない場合の環境変数を表示するプログラムです.同じディレクトリ に stackdump を用意してから実行してください.

  1. fork で子プロセスを生成
  2. 子プロセスは execve で stackdump を実行して環境変数を表示
  3. wait4 で終了を待つ
  4. fork で子プロセスを生成
  5. 子プロセスは偽の環境変数を渡した stackdump で環境変数を表示
  6. wait4 で終了を待つ
;---------------------------------------------------------------------
; 2000/07/17  forkexec.asm
; nasm -f elf forkexec.asm
; ld -s -o forkexec forkexec.o
; ndisasm -b 32 forkexec
;---------------------------------------------------------------------

%include "resource.inc"
%include "vartype.inc"
%include "syscall.inc"
%include "stdio.inc"

section .bss
        argc    resd    1
        argvp   resd    1
        envp    resd    1

section .data

ru:  istruc  rusage
        .ru_utime_tv_sec   _LONG  0 ;  user time used
        .ru_utime_tv_usec  _LONG  0 ;
        .ru_stime_tv_sec   _LONG  0 ;  system time used
        .ru_stime_tv_usec  _LONG  0 ;
        .ru_maxrss         _LONG  0 ;  maximum resident set size
        .ru_ixrss          _LONG  0 ;  integral shared memory size
        .ru_idrss          _LONG  0 ;  integral unshared data size
        .ru_isrss          _LONG  0 ;  integral unshared stack size
        .ru_minflt         _LONG  0 ;  page reclaims
        .ru_majflt         _LONG  0 ;  page faults
        .ru_nswap          _LONG  0 ;  swaps
        .ru_inblock        _LONG  0 ;  block input operations
        .ru_oublock        _LONG  0 ;  block output operations
        .ru_msgsnd         _LONG  0 ;  messages sent
        .ru_msgrcv         _LONG  0 ;  messages received
        .ru_nsignals       _LONG  0 ;  signals received
        .ru_nvcsw          _LONG  0 ;  voluntary context switches
        .ru_nivcsw         _LONG  0 ;  involuntary
    iend

title     db    "fork & execve.", 0
title2    db    "fork & execve with wrong envp.", 0
parent    db    "Back to Parent Process. Child pid was:", 0
child     db    "Child Process execve ./stackdump", 0
timemsg   dd    " System time in usec :", 0
stat_addr dd    0
command   db    "./stackdump", 0
arg1      dd    0
argv      dd    arg1, 0
fakeenv1  db    "WRONG=1",0
fakeenvp  dd    fakeenv1,0

section .text
global _start

_start:
                pop     dword[argc]     ; argc
                mov     dword[argvp], esp
                mov     ebx, [argc]
                shl     ebx, 2
                lea     eax, [esp+ebx*4+4]  ; envp
                mov     [envp], eax

                mov     eax, title
                call    OutAsciiZ
                call    NewLine
                mov     eax, SYS_fork
                int     0x80
                cmp     eax, 0
                jne     .parent
                ; child
                mov     eax, child
                call    OutAsciiZ
                call    NewLine
                mov     eax, SYS_execve
                mov     ebx, command    ; (char *)filename
                mov     ecx, argv       ; char ** argv
                mov     edx, [envp]     ; char ** envp
                int     0x80
                call    Exit

    .parent
                push    eax
                mov     ebx, eax        ; pid
                mov     eax, SYS_wait4
                mov     ecx, stat_addr
                mov     edx, WUNTRACED  ; WNOHANG
                mov     esi, ru         ; rusage
                int     0x80

                mov     eax, parent
                call    OutAsciiZ
                pop     eax             ; pid
                call    PrintLeft
                call    NewLine
                mov     eax, timemsg
                call    OutAsciiZ
                mov     eax, [ru + rusage.ru_stime_tv_usec]
                call    PrintLeft
                call    NewLine

                mov     eax, title2
                call    OutAsciiZ
                call    NewLine
                mov     eax, SYS_fork
                int     0x80
                cmp     eax, 0
                jne     .parent2
                ; child2
                mov     eax, child
                call    OutAsciiZ
                call    NewLine
                mov     eax, SYS_execve
                mov     ebx, command    ; (char *)filename
                mov     ecx, argv       ; char ** argv
                mov     edx, fakeenvp   ; char ** envp
                int     0x80
                call    Exit

    .parent2
                push    eax
                mov     ebx, eax        ; pid
                mov     eax, SYS_wait4
                mov     ecx, stat_addr
                mov     edx, WUNTRACED  ; WNOHANG
                mov     esi, ru         ; rusage
                int     0x80

                mov     eax, parent
                call    OutAsciiZ
                pop     eax             ; pid
                call    PrintLeft
                call    NewLine
                mov     eax, timemsg
                call    OutAsciiZ
                mov     eax, [ru + rusage.ru_stime_tv_usec]
                call    PrintLeft
                call    NewLine

                call    Exit
;------------------------------------

ほとんど同じことを二度実行していますが,条件を変えて実行してカーネルを 突っついてみることが,カーネルの動作を理解する上で有用であると思います.

以上のコードを発展させると,臨時の環境変数ファイルを使って現在の環境変数を 一時的に変更して任意の環境変数の「環境」のもとでコマンドを実行できるコマンド が作成できます. 実用性は不明ですが :-)