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, ®s);
したがって 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 <[email protected]> ; ; 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 を用意してから実行してください.
- fork で子プロセスを生成
- 子プロセスは execve で stackdump を実行して環境変数を表示
- wait4 で終了を待つ
- fork で子プロセスを生成
- 子プロセスは偽の環境変数を渡した stackdump で環境変数を表示
- 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 ;------------------------------------
ほとんど同じことを二度実行していますが,条件を変えて実行してカーネルを 突っついてみることが,カーネルの動作を理解する上で有用であると思います.
以上のコードを発展させると,臨時の環境変数ファイルを使って現在の環境変数を 一時的に変更して任意の環境変数の「環境」のもとでコマンドを実行できるコマンド が作成できます. 実用性は不明ですが :-)