clone - みる会図書館


検索対象: UNIX MAGAZINE 2003年3月号
17件見つかりました。

1. UNIX MAGAZINE 2003年3月号

連載 / Linux のプートプロセスをみる一 0 図 8 do-fork() を呼び出す直前のスタック だけで、プロセスを生成する処理の実体は do-fork() で実 装されています。 0744 : int sys—clone(struct pt_regs regs) 0745 : { 00010F00 C0105030 clone—flags stack start regs stack Si ze sys—clon e ( ) system—call() 0749 : 0750 : 0751 : 0752 : 0753 : clone—flags = regs. ebx ; newsp = regs . ecx; if ( ! newsp) newsp = regs . esp; return do—fork(clone—flags , newsp , &regs, 0 ) ; 00000000 EBX 戻りアドレス EBX ECX EDX 0754 : } こで注目すべきポイントは構造体 pt-regs です。 の十冓造イ本は、ファイル include/asm-i386/ptrace. h で 以下のように定義されています。 struct pt—regs { 10 Ⅱ g ebx ; 1 ong ecx; 10 Ⅱ g edx ; 1 ong esl; 10 Ⅱ g edi ; 10 Ⅱ g ebp ; 1 ong eax; i nt xds ; int xes; 1 ong orlg—eax ; 10 Ⅱ g elp; int XCS; 10 Ⅱ g eflags; long esp; int XSS,• ています。 #define CLONE_SIGNAL (CLONE_SIGHAND ー CLONE THREAD) したがって、正確には、 CLONE_FS ー CLONE_FILES ー CLONE_SIGHAND ー CLONE_THREAD ー CLONE_VM を未することになります。 舌はそれますが、 do-fork() は sys-clone() だけでな この構造は、 system-call() がレジスタの内容をスタ く、 fork と vfork システムコールを実装する関数からも ックに格納した順番と同しです ( 図 7 も参照してくださ 呼び出されます。その違いは、以下のように引数のフラグ い ) 。そして、 202 行目の CALL 命令により戻りアドレ だけです。 ス ( 203 行目のアドレス ) がスタックに積まれます。この int sys-fork(struct pt-regs regs) ため、 sys-clone() はスタック上のレジスタの内容を、関 0739 : 0740 : { 数に渡された引数のように参照することができます。 return d0—fork(SIGCHLD, regs ・ esp, 0741 : kernel-thread() では、フラグの値を EBX に、 init() &regs , 0 ) ; のアドレスを ECX にオ褓内して INT 命令を実行しました ( 図 4 ) 。よって、 749 ~ 750 行目の regs. ebx と regs. ecx により、それぞれの値を取り出すことができます。 do-fork() を呼び出す直前のスタックの内容を図 8 に 示します。 do%rk() への引数であるフラグ ( 00010F00 ) は、 rest-init() の 532 行目と kernel-thread() の 508 行 目を合わせたものです。しかし、 CLONE-SIGNAL につ いては include/linux/sched. h で以下のように定義され 1 三ロ 0742. int sys—vfork (struct pt—regs regs) 0766 : 0767 : return do—fork ( 0768 : CLONE_VFORK ー CLONE_VM ー SIGCHLD , regs ・ esp, &regs, 0 ) ; 0769. つまり、 do-fork() はプロセス生成の全般を受け持つル ーチンなのです。 154 UNIX MAGAZINE 2003.3

2. UNIX MAGAZINE 2003年3月号

連載 /Linux のプートプロセスをみる一 0 do-fork() kernel/fork. c で定義されています。 り、実行可能プロセスのリストに登録します。コードは do%rk() は、それを呼び出したプロセスのコピーを作 unsigned 10 Ⅱ g stack—size) struct pt—regs *regs , unsigned 10 Ⅱ g stack—start , int do—fork(unsigned long clone-flags , 0562 : 0563 : 0564 : { 0565 : 0566 : 0567 : int retval ; struct completion vfork; struct task—struct *p; CLONE-THREAD スレッドグルーフ ・ stack-start 子プロセスの ESP レジスタに褓内するイつまり、子 プロセスが使用するスタック領域のアドレス。 ・ regs クにオ褓内した pt-regs 構造体へのポインタ。 親フ。ロセスのレジスタの内容。 system-call() がスタッ do-fork() の内容はちょっと複雑なので、すこしすっ説 IA-32 用 Linux では使用されません。 ・ stack_size 明します。 引数の意味は以下のとおりです。 ・ clone-flags この 4 バイトのフラグのなかには、意味の異なる 2 種 類の値が入っています。 上位 3 バイトは、親プロセスと子プロセスのあいだで 共有する資源の不噬頁を指定します。そして、下位 1 バ イトは、子プロセスか終了したときに親プロセスに送る シグナル番号を指定します。 共有する資源の不頁を指定するフラグには以下のものが CLONE-PTRACE プロセス ID CLONE-PID シグナノレ、ンドラ CLONE-SIGHAND オープンしているファイル CLONE-FILES ルート・ディレクトリとカレント・ディレクトリ CLONE-FS メモリ・デスクリプタとページテープル CLONE-VM あります。 UNIX MAGAZINE 2003.3 : 親フ。ロセス CLONE-PARENT vfork システムコーノレ用 CLONE-VFORK ptrace する 親フ。ロセスが ptrace されている場合、子プロセスも 0569 : 0575 : 0576 : 0577 : 0578 : retval -EPERM; if (clone—flags & CLONE—PID) { if (current—>pid) goto fork—out ; フラグ CLONE-PID を指定できるのはプロセス ID が 0 である swapper だけで、その他のプロセスがこのフ ラグを指定した場合はエラーになります。 CLONENID は、マルチプロセッサを匆期化するときに使用します。 0580 : 0581 : 0582 : 0583 : 0584 : 0585 : —ENOMEM ; retval p = alloc—task—struct() ; *p = *current ; got 0 f ork— out ; if (!p) alloc-task-struct() は、メモリアロケータから 8KB のメモリプロックを確保します。このメモリ領域は、子フ ロセス用のデスクリプタとスタック ( 図 2 ) に使用します。 メモリの石呆に成功すると、親フ。ロセスのデスクリプタの 内容を子プロセスのデスクリプタへコピーします ( 図 9 ) 。 0587 : 0588 : 0589 : 0590 : 0591 : 0592 : -EAGAIN ; retval = if (atomic—read(&p—>user—>processes) > = p->rlim [RLIMIT—NPROC] . rlim—cur) goto bad-fork—free ; atomic—inc(&p—>user—>——count) ; atomic—inc(&p—>user—>processes) ; 155

3. UNIX MAGAZINE 2003年3月号

連載 -0 get-pid() Linux のプートプロセスをみる を呼び出して新しいプロセス ID を割り当て ます。 0614 : 0615 : 0616 : 0617 : 0618 : 0619 : 0620 : 0621 : 0622 : 0623 : 0624 : 0625 : 0626 : 0627 : 0628 : 0629 : 614 0630 : 0631 : 0632 : 0633 : 0634 : 0635 : 0636 : 0637 : 0649 : 0650 : 0651 : 0652 : p—>run_list . next = NULL ; p—>run—list . prev = NULL; init—sigpending(&p->pending) ; p->sigpending = 0 ; spin—lock—init(&p—>alloc—lock) ; init—completion(&vfork) ; p—>vfork—done = &vfork ; if (clone—flags & CLONE—VFORK) p—>vfork—done = NULL ; &p->wait—chldexit) ; init—waitqueue—head ( p—>p—cptr = NULL ; init—timer (&p—>real—timer) ; = p—>it—prof—incr = 0 ; p—>it—real—incr = p—>it—virt—incr p—>it—prof—value p—>it—real—value = p—>it—virt—value p->real—timer. data = (unsigned 10 Ⅱ g ) p ; p—>times . tms_utime p—>tty—old—pgrp = 0 ; p—>leader = 0 ; ー 1 ; / * ー 1 = Ⅱ 0 lock * / p—>lock—depth p—>times . tms—cstime p—>times. tms_cutime = p—>times . tms—stlme = jiffies ; p—>start—time 図 9 のようにプロセス・デスクリプタとは別のデータ構造 により管理さプロセス・デスクリプタにはそのポイン タかオ褓内されています。 585 行目でプロセス・デスクリプタの内容をコピーする ことにより、この時点では、子プロセスはすべての資源を 親プロセスと共有する状態になっています。 656 ~ 663 行 目は、引数 cIoneÆags に以下のフラグカ甘旨定されなかっ た場合、新たにメモリ領域を砠呆して、それぞれのデータ 構造をコピーする処理をおこないます。 CLONE-FILES CLONE-FS CLONE-SIGHAND CLONE-VM 0664 : 0665 : 0666 : retval = copy—thread((), clone—flags , stack_start , stack_size , p, regs) ; if (retval) goto bad—fork—cleanup-mm; INIT_LIST_HEAD (&p—>local_pages) ; ~ 652 行目では、親プロセスから受け継ぐことがで きない情報 ( プロセスの親子関係や実行時間など ) を初期 化します。 0654 : 0655 : 0656 : 0657 : 0658 : 0659 : 0660 : 0661 : 0662 : 0663 : retval -ENOMEM ; if (copy—files(clone—flags, p)) goto bad—fork—cleanup ; if (copy—fs(clone-flags, p) ) goto bad—fork—cleanup—files ; if (copy—sighand(clone—flags , p)) goto bad—fork—cleanup-fs ; if (copy—mm(clone-flags, p) ) goto bad-fork—cleanup-sighand ; 親プロセスと子プロセスのあいだで共有する資源は、 158 copy-thread() の処理の概要を図 10 に示します。 copy-thread() は、親プロセスが clone を呼び出した ときのレジスタの内容を子プロセスのカーネルスタック領 域にコピーして初期化します。このコピー処理は、スタッ クに待避させたデータ ( 図 7 ) を構造体 pt-regs にあては めてコピーします。そのため、実際にはスタックに待避さ せていない ESP と SS の領域 ( 図 6 ) まで ( よけいに ) コ ピーしてしまいます。 コピー処理を終えたら EAX の領域に 0 を上書きし、 ESP の領域に stack-start の値を上書きします。 clone システムコールを発行したとき、子プロセス側に返ってく る 0 はここで上書きしている値なのです。 次に、スタック上にコピーした構造体 pt-regs へのポ インタを thread. esp に、 ret-from-fork() のアドレスを thread. eip に設疋します。この 2 つの値は、 CPU カワ。 ロセスを切り替えるときにレジスタ ESP と EIP にロー ドされます。つまり、新たに生まれた子プロセスの第 1 歩 を示す値であり、子プロセスは ret-from-fork() から実行 を開始します。 そして、 ret-from-fork() は、その処理の途中で sys- tem-call() の 205 行目にジャンプし、 211 行目で各レジ スタの内容をスタックからロードします。そしてさらに kernel-thread() の 494 行目に戻ります。 UNIX MAGAZINE 2003.3

4. UNIX MAGAZINE 2003年3月号

連載 / 凵 nux のプートプロセスをみる一 0 図 11 生まれたての子プロセスのスタックの好 P-OPPtr : 自分を生成したプロセス p-pptr : 現在の親プロセス 通常は p-opptr と同し値を設定しますが、 ptrace など でトレースしているときは異なることがあります。 EBX ECX EDX ESI EDI EBP EAX DS ES EAX EIP CS EFLAGS ESP SS retval = p—>pid; p-cptr 0717 : : もっとも山も丘に生成した子プロセス SET_LINKS (p) ; レジスタに ロードされる スタックの底 プロセスは、カーネルのなかで生成と消滅を繰り返しま す。そして、 1 つのプロセスカ吽まれてから消えるまでの あいだに、、状態 " か変化します。 Linux カーネルは、プロセスを効率的に管理できるよ うに、それぞれの目的別にリンクトリスト構造を使用しま す。その 1 つが、すべての生存中のプロセスをつなぐタス クリスト (task list) です。そのほかに、実行可能な状態 のプロセスをつなぐランキュー ( 彳おがあります。 717 行目のマクロ SET-LINKS は、引数に指定したプ ロセスをタスクリストに挿入し、プロセスの兄弟関係を表 すリストを設定します。 マクロ SET-LINKS は、以下のように展開されます。 p—>next—task = &init—task ; p¯>prev—task = init—task. prev—task ; init—task. prev—task¯>next—task = p ; init—task. prev—task = p ; p—>p—ysptr = NULL ; if ( (p->p-osptr = p->p-PPtr¯>P—CPtr) ! = NULL) 関係を表すポインタを図 13 に示します。 タスクリストへの挿入処理を図 12 に、プロセスの親子 p->p-pptr->p-cptr = P ; p->p-osptr->p—ysptr = p; 0701 : 0696 : 0697 : 0698 : 0704 : 0705 : 0706 : 0707 : 0708 : 0709 : 0710 : 0711 : 0712 : 0713 : 0714 : 0715 : 696 160 p—>tgid = retval; 工 NIT_LIST_HEAD (&p->thread-group) ; write—lock—irq(&tasklist—lock) ; P->P—0PPtr = current¯>p—opptr; p—>p—pptr = current—>p-pptr ; if ( ! (clone—flags & (CLONE—PARENT ー CL ONE-THREAD ) ) ) { p->p—opptr = current ; if ( ! (p->ptrace & PT—PTRACED) ) p¯>p—pptr = current ; if (clone-flags & CLONE—THREAD) { p—>tgid = current->tgid ; list—add(&p—>thread—group , &current->thread-group) ; ~ 715 行目では、親プロセスを指すポインタを設定 します。それぞれのポインタの意未は以下のとおりです。 0718 : 0719 : 0720 : 0721 : 0722 : 0723 : 0724 : 0725 : 0726 : 0727 : 0728 : 0729 : 0730 : 0731 : hash-pid(p) ; nr_threads + 十 ; UNIX MAGAZINE 2003.3 return retval ; f ork_out : wait—for—completion(&vfork) ; if (clone-flags & CLONE—VFORK) 十十 total_forks ; wake-up—process (p) ; send—sig(SIGSTOP, p, 1 ) ; if (p->ptrace & PT—PTRACED) write—unlock—irq(&tasklist—lock) ;

5. UNIX MAGAZINE 2003年3月号

連載 /Linux のフートプロセスをみる ー 0 int kernel—thread(int (*fn) (void * ) , void *arg, unsigned 10 Ⅱ g flags) pid—t pid = c10ne(NULL, flags ー CLONE-VM) ; exit ( ) ; fn(arg) ; } else { return pid; if (pid) { UNIX MAGAZINE 2003.3 の内容を自酌にスタックに待避させます。さらに、 INT せると、 CPU は EIP や EFLAGS といったレジスタ 以前にも説明しましたが、ソフトウェア割込みを発生さ の内容もコピーしなけれはなりません。 コピーを作成するため、システムコール発行・時のレジスタ clone システムコールは、それを呼び出したプロセスの てまわったことせなあかんの ? 」 ルーチン・コールしたらええやん。なんで、わざわざもっ 「カーネルがカーネルを呼び出すんやったら、直接サプ のサービスを呼び出す場合もあります。 イメージがありますが、このように、カーネルがカーネル ムコールはユーサー・プログラムが乎び出すものといった システムコールを呼び出すことになります。通常、システ り、 493 行目は d011e システムコールを、 503 行目は exit の割込みをシステムコールの呼出しに利用します。つま 2 月号で説明したように、 Linux ではべクタ番号 80H 三させるところです。 503 行目の INT 命令によりべクタ番号 80H の割込みを kernel-thread() のコードで注目する点は、 493 行目と ような感じ」と曖床な書き方をしたのは、そのためです。 と、リンク時にエラーになってしまいます。「だいたい次の て、 kernel-thread() を叫屯に一 E 記の C 言語で置き換える exit() という名前のサプルーチンはありません。したがっ と思うかもしれません。しかし、カーネルには clone() や えのに」 「これやったら分かるわ。最初から素直にそう書いたらえ えられた関数を実行します。 したカーネルスレッドのプロセス ID を返し、、、子 " が学 して新しいプロセスを作成します。そして、、、親 " は生成 kernel-thread() は、システムコール clone を呼び出 80H のハンドラ system-call() は、その他のレジスタの 内容をスタックに待避させます。つまり、レジスタの内 容をスタックに待避させるためのルーチンをわざわざ別に 用意するより、ユーサー・プログラムと同じようにソフト ウェア割込みを利用するほうかプログラムカ吶単になるた め、サプルーチン・コールではなく INT 命令を実行して いるのだと思われます。 clone システムコールを発行すると、 (fork と同じよう に ) 2 つのプロセスがシステムコールから戻ってきます。 一方は親プロセス、もう一方は子プロセスです。つまり、 493 行目の INT 命令を実行するプロセスは 1 つですが、 次の 494 行目の CPML 命令は親と子の 2 つのプロセ スが実行します。それぞれのプロセスが、自分が、、親 " な のか、、子 " なのかを識別するには、 INT 命令を実行した直 後の EAX レジスタの内容 ( システムコールの戻りイ間を 使います。親プロセスの EAX には子プロセスのプロセ ス ID かオ翻勺されており、子プロセスの EAX の内容は 0 です。 一方、親プロセスと子プロセスは、それぞれ異なるプロ セス・デスクリプタをもつはずであり、 2 つのプロセスの カーネルスタック領域のアドレスも異なるはすです。これ は、 ESP レジスタの変化で判断することができます。っ まり、親プロセスについては INT 命令の前後で ESP レ ジスタは変化しませんが、子プロセスにはまったく異なる 値かオ褓内されています。 492 行目で ESP の内容を ESI に保存し、 494 行目で比 較しているのは、このカーネルスタック領域の違いによっ て親と子を識別しているのです。 システムコール呼出し これまでに説明したとおり、 Linux では INT 80H を 実行してシステムコールを呼び出します。しかし、これだ けでは Linux に用意されている 200 以上ものシステム コールのどれを実行したいのかが分かりません。また、な かには引数を渡さなけれは・ならないシステムコールもある でしよう。 Linux は、レジスタを用いてシステムコール窈旨定と引 2 C 言言韶 ) 関数呼・出しにはスタックを使います。 数の受渡しをおこないます 2 。 149

6. UNIX MAGAZINE 2003年3月号

連載 Linux のフートプロセスをみる ー 0 {RLIM_INFINITY, RLIM_INFINITY}, CPU * / {RL 工 M_INFINITY, RLIM_INFINITY} , / * FSIZE * / {RLIM_INFINITY, RLIM_INFINITY}, DATA * / RLIM_INFINITY} , STACK * / _STK_LIM , RLIM_INFINITY} , CORE * / 0 , {RLIM_INFINITY, RLIM_INFINITY} , 0 } , / * NPROC * / INR_OPEN} , / * NOFILE * / INR_OPEN , {RLIM_INFINITY, / * MEMLOCK * / RLIM_INFINITY} , {RLIM_INFINITY , RLIM_INFINITY}, {RLIM_INFINITY , / * LOCKS * / RLIM_INFINITY} , get—exec—domain(p—>exec—domain) ; if (p->binfmt & & p—>binfmt—>module) _MOD_INC_USE_COUNT ( p—>binfmt—>module) ; Linux では、 BSD や Solaris など、ほかの IA-32 用 OS のバイナリファイルを実行することができます。とこ ろが、同じ POSIX ま材処を謳っていても、システムコー ノレの呼出し方やシグナル番号の意味は OS ごとに異なるた め、これを吸収する仕組みが必要になります。 Linux で は、これらの OS による違いを実行ドメイン (execution コンノヾイル時に、、 6 " に書き換 RLIIA ・ IIT-NPROC は、 domain) とパーソナリティ (personality) と呼はれる機 えられます。よって、 rlim[RLIMIT-NPROC]. rlim-cur 構で実装し、カーネル・モジュールとして重加勺にロードで の初期値は 0 です。しかし、この値は、 start-kernel() か きるようにしています。 ら呼び出される fork-init() ( 図 1 ) により、計算機の物理 メモリのサイズから算出した値に更新されます。 602 ~ 605 行目では、実行ドメインに対するカーネル・ モジュールの参照カウンタを増やし、誤ってモジュールが 0068 : void fork—init (unsigned long mempages) 切り離されないようにしています。 0069 カーネルがサポートしている実行ドメインのリストは、 ファイル /proc/execdomains を見れば分かります。 0607 : p—>did—exec 0608 : p->swappable = 0609 : p->state = TASK—UNINTERRUPTIBLE; プロセスの状態を以下のように初期化します。 ・システムコール execve を夫行したことがない ・スワッフ。アウト不可 ・シグナルを受けても無視するスリーフ状態 copy—flags(clone—flags , p) ; 子プロセスのフラグを以下のように修正します。 ・ PF-SUPERPRIV をクリア ・ PF-USEDFPU をクリア ・ PF-FORKNOEXEC をセット ( 子プロセスがシステ ムコール execve を夫行したことがない ) ・ clone-flags に CLONE-PTRACE が設定されていな い場合は、 p->ptrace もクリア 0602 : 0603 : 0604 : 0605 : max-threads = mempages / 2 / 8 ; init—task. rlim CRLIMIT—NPROC] . rlim max—threads/2 ; init-task. rlim CRLIMIT-NPROC] . rlim_ max = max—threads/2 ; fork-init() の引数 mempages には、 max 」 ow-pfn ( 2003 年 1 月号参照 ) か渡されます。 たとえは、カーネ ルのプート時に On node 0 totalpages : 32768 zone ( 0 ) : 4096 pages ・ zone ( 1 ) : 28672 pages ・ zone(2) : 0 pages ・ のようなメッセージか表示された場合、それぞれのリミッ ト値には 1 , 024 が設定されます。つまり、生成可能なプ ロセスの数は物理メモリの量によって決定されます。 if (nr—threads > = max—threads) 0599 : got0 bad-fork-cleanup—count ; 0600 : システム全体の総プロセス数 (nr-threads) が、リミ ット (max-threads) を超えていれば工ラーになります。 max-threads の値は、 fork-init() の 75 行目で匆祺月化さ れています。 0075 : 0076 : 0077 : 0078 : 0079. 0611 : p¯>pid = get—pid(clone—flags) ; 0612 : 157 UNIX MAGAZINE 2003.3

7. UNIX MAGAZINE 2003年3月号

連載 / 凵 nux のフートプロセスをみる一 0 ジャンプ先で EAX の内容か書き換えられてしまうため 206 行目では、プロセス・デスクリプタの need-re- sched を調べ、その値がセットされていれは reschedule へジャンプします。 reschedule はフロセス・スケジュー ラ schedule() を呼び出し、プロセスの再スケジュールと 実行フ。ロセスの切替えをおこないます。そして、ふたたび 制彳卸カ唳ってくると 205 行目にジャンプして同し処理を 繰り返します。 さらに、 208 行目はデスクリプタの sigpending をチェ ックします。もし、この値がセットされてい川よ、プロセ スに保留中のシグナルがあることを未するため、それを 処理する signal-return へジャンプし、処理カ冬ると 211 popl %ebx popl %edx popl %edi popl %ebp 行目に戻ってきます。 0211 : popl popl popl popl popl addl iret %ecx 0/oesi %eax %d s %es $4,%esp # システムコール番号を捨てる ールの戻り値 システムコ 最後に、スタックに待避させたレジスタの内容をロード UNIX MAGAZINE 2003.3 プロセスの生成 がもつアドレス空間 ( データやスタック領域 ) とファイル・ 不勺な部分といえます。伝糸勺な UNIX は、親プロセス プロセスの生成は、 UNIX の重川のなかでもっとも神 生時の実行位置に復帰する処理をおこないます。 ら EIP と CS 、 EFLAGS の内容をロードし、割込み発 らす IRET 命令を実行します。この命令は、スタックか 、、決まりごと " として、ハンドラルーチンの最後にはかな に戻ります。 ADDL 命令を実行した直後のスタックは、図 6 の状態 ステムコールの戻り値 " かオ褓内されます。 ため、もとの値 ( システムコール番号 ) には戻らす、、、シ ロードする位置の内容は 203 行目で書き換えられている して、割込み発生時の状態に戻します。ただし、 EAX に デスクリプタなどの資源をコピーすることで、子プロセス を生成します。しかし、このガ去は、コピー先ページフレ ームの取得やコピー処理のためにけして短くはない時間が かかります。さらに、多くの場合、子プロセスは execve システムコールを発行して、複製したはかりのアドレス空 間を捨ててしまいます。すぐに不要になって捨ててしまう ものをせっせと作るのは、たいへん無駄な作業てす。この ように 昔々のプロセス生成は、ひどく遅くて非効率的な 処理をおこなっていました。 これに対し、 Linux は次の 3 つのガ去によって上記の 問題に対処しています。 ・ vfork システムコーノレ ・ Copy-On-Write ・ライトウェイト・プロセス vfork システムコールは、親フ。ロセスとアドレス空間を 共有する子プロセスを生成します。アドレス空間を共有す るため、 vfork は上記のコピー処理をおこないません。た だし、親プロセスの処理は、子プロセスか終了するか、 ex- ecve システムコールを実行するまでプロックされます。 Copy-On-Write は、親フロセスと物理ページフレーム を共有する子プロセスを生成します。そして、どちらカー 方がページフレームに書込みをおこなうと、新たに複製し たページフレームを割り当てます。つまり、古 ! 勺なプロ セス生成のコピー処理のうち、、必要なところだけ " をおこ ない、しかも、、遅延処理 " することによってプロセスを早 く生成できるのです。 最後のライトウェイト・プロセス ( L ⅲ ux ではスレッ ドと同しと考えてよいようです ) では、親プロセスと子プ ロセスはアドレス空間全体 ( つまりページング・テープル ) と資源本を共有します。 vfork と異なり、親プロセスが プロックされることはありません。 以降では、ライトウェイト・プロセスを作成するシステ ムコール clone について詳しく説明します。 sys—clone sys-clone() は、システムコール clone を実装するルー チンです。 そのコードは arch/i386/kernel/process. c に定義さ れていますが、以下のように do-fork() を呼び出している 153

8. UNIX MAGAZINE 2003年3月号

連載 / Linux のプートプロセスをみる一 0 図 3 kernel-thread() を呼び出した直後のスタック 図 4 INT 80H を実行する直前のスタック C0105011 C0105030 00000000 00010E00 戻りアドレス スタックの成長方向 ・← ESP EBX ESI 戻りアドレス init NULL フラグ init NULL フラグ kernel_thread() rest 」 nit() ECX EDX EBX ます、すべてのシステムコールにはシステムコール番号 という一意の識別子が与えられています。この識別番号 は、 include/asm-i386/unistd. h で定義されています。 そして、 EAX レジスタを使ってシステムコール番号を指 定します。 次に、 kernel-thread() のコードを見てみましよう。た ただし、レジスタを用いて引数を渡す場合は、 だし、インライン・アセンプリ命令の解説を始めると話が 大きくそれてしまうので、 gdb で逆アセンプルしたコード ・値は 32 ビットで表せる範囲まで ( ポインタも可 ) を読むことにします。以下は、 kernel-thread() の最初か ・引数の数は 5 つまで ら 493 行目の INT 命令までの部分です。 という制限があります ( 6 つ以 E の引数をもつ mmap() 0/oesi 0XC01054f0 push $ 0X78 , %eax 0XC01054f1 mov などのシステムコールの場合は、ちょっと違うガ去を使い %ebx 0XC01054f6 push ますが、カーネルのプート時にそのようなシステムコール %ebx Ox14(%esp, 1 ) , 0XC01054f7 mov Oxc(%esp,1) , %ecx 0XC01054fb mov を呼び出すことはないので、説明は省略します ) 。 Ox10(%esp,1), %edx 0XC01054ff mov システムコールの引数を渡すために用いるレジスタは、 $ 0X100 , %ebx 0XC0105503 %esp, %esi 0XC0105509 mov 順に EBX 、 ECX 、 EDX 、 ESI 、 EDI です。たとえは、 $ 0X80 0XC010550b int 1 つの引数をとるシステムコールはその値を EBX にオ褓内 EAX にオ褓内している値 ( 78H ) は、 cl 。 ne のシステムコ し、引数が 2 つの場合は EBX と ECX にオ褓内します。 ール番号を意味します。この番号は、 incIude/asm-i386/ 以降では、 493 行目でシステムコール clone を呼び出 unistd. h のなかで以下のように定義されています。 す前後の処理を詳しく説明します。 #define __NR_c10ne 120 ます、 rest-init() の 532 行目から kernel-thread() が C01054F7 から続く MOV 命令は、システムコール 呼び出された直後のスタックの様子を図 3 に示します。図 clone への引数をレジスタにオ褓内しています。図 4 にアド に記したアドレスは私がコンパイルしたカーネルでの値で レス C010550B で INT 命令を実行する直前のスタック あり、コンパイル・オプションの選択によって異なりま の内容を示すので、スタックポインタとのオフセットをこ す。本文中では、より具ー純勺な値を示したはうか理解しや の図を見ながら確認してください。 すいと考えて紹介しています。 C0105503 の OR 命令の引数である 100H は、以下の スタックには kernel-thread() べ度す引数が、フラグ ように include/linux/sched. h で定義されているフラグ の値、 NULL 値、 init() のアドレスの順番て坏責まれてい CLONE-VM の値です (kerneLthread() の 508 行目も ます ( それぞれの引数の未は彳します ) 。そして、最後 参照してください ) 。フラグの意味は彳あします。 にサプルーチン・コールの戻りアドレスがスタックに積ま れます。 #define CLONE_VM 0X00000100 150 UNIX MAGAZINE 2003.3

9. UNIX MAGAZINE 2003年3月号

Linux の プートプロセスをみる 0 白崎博生 rest ー init ( ) からカーネルスレッドの生成まで 1 ~ 2 月号では、カーネルの初期イしレーチンである start -kernel() について説明しました。 今月は、 rest-init() とシステムコールの呼出し処理、そ して、カーネルスレッドの生成について説明します。 今回とりあげるサプルーチン rest-init() は、カーネル の起重丿ワ。ロセス全体の流れのなかでは図 1 の濃い網をかけ た部分にあたります。 プロセスとスレッド コードの説明に入る前に、プロセス (process) とスレッ ド (thread) について話しておきましよう。 スレッドとは、 1 つのプロセスを実行するための資源空 間のなかで、論理的な並列処理をおこなっている実行コン テキストのことです。計算機に CPU が 1 つしかない場合 は、ある瞬間に実行できるプログラムは 1 つだけです。し たがって、、、並列処理 " といっても、同時に複数のプログ ラムが CPU で実行されていなくてもかまいません。 1 つ の CPU でも、実行コンテキストを素早く切り替えれは論 理的に並列処理をおこなうことが可能です。これがスレッ ドです。 現在の OS にとって、スレッドのサポートは不可欠な 機能の 1 っといってもよいでしよう。スレッドの実現方 法を大きく分けると、 ・ユーザー空間スレッド ・カーネル空間スレッド の 2 不鶤頁になります。 ューサー空間スレッドは、すべての処理がユーザー空間 でおこなわカーネルのサポートはありません。このた UNIX MAGAZINE 2003.3 め、カーネルからは 1 つのプロセスとしかみえないので、 たとえ計算機がマルチプロセッサ・システムであっても、 プロセス内のスレッドか 1 司時に複数の CPU で実行される ことはありません。しかし、スレッド間のコンテキスト・ スイッチをおこなうとき、ユーサー空間とカーネル空間と の切替えかイ畯であり、オーバーヘッドが少ないという利 点があります。 一方、カーネル空間スレッドは、カーネルがスレッドを 1 つの実行単位として認識し、プロセス・スケジューリン グをおこないます。よって、引・・算機がマルチプロセッサ・ システムであれば、プロセス内のスレッドか 1 司時に複数の CPU て並列に実行できます。ただし、コンテキスト・ス イッチをおこなうときに、ユーサー空間とカーネル空間を 切り替えるためのコストがかかります。 このように、それぞれの実現方法には長所と短戸励ゞあり ますが、この 2 つは互いに排他的なものではないので、 OS によっては両方を組み合わせて実装しているものもありま す。 Linux は、、カーネル空間スレッド " を実装し、スレッド を生成するためのシステムコール clone を提供していま す。 通常の (fork システムコールで生成する ) 子プロセス は、親と資源を共有しません。一方、 (clone システム コールで生成する ) スレッドは、親プロセスとのあいだで 資源を共有する子プロセスとして生成されます。つまり、 Linux ではプロセスもスレッドも 1 つのプロセスとして 管理されるわけです。したがって、スレッドにも独自のプ ロセス ID か割り当てられます。ただし、スレッドは親プ ロセスとのあいだでアドレス空間 ( テキストやデータセグ メント ) 、オープン中のファイルなどの資源を共有すると 145

10. UNIX MAGAZINE 2003年3月号

連載 / 凵 nux のプートプロセスをみる一 0 図 7 system-call() はレジスタの内容をスタックに保存する system-call() system-call() は、すべてのシステムコールのエントリ ポイントとなるサプルーチンです。そのコードは、以下の ように arch/i386/kernel/entry. S で定義されています。 pushl %ebx EBX ECX EDX ESI EDI EBP EAX DS ES system—call() INT 80H EAX EIP CS EFLAGS ます。 0197 : 0198 : 0199 : 0200 : 0201 : movl $ ー 8192 , %ebx andl %esp , %ebx testb $ 0X02 , tsk-ptrace (%ebx) Jne tracesys cmpl $(NR—sysca11s) , %eax jae badsys 197 行目は、カレントプロセスのプロセス・デスクリプ タへのポインタ ( 図 2 ) を EBX に設定します。この処理 は、前述の、、 current" と同しです。 そして、 198 行目でデスクリプタの内容を調べ、シス テムコールのトレースカ甘されているかを判断します。 200 ~ 201 行目では、システムコール番号の有効性をチェ ックしています。 0202 : 0203 : 0204 : 0205 : 0206 : 0207 : 0208 : 0209 : call *sys—call—table(, %eax, 4 ) movl %eax, 24(%esp) # 戻り値 cli # 割込み禁止 cmpl $ 0 , need—resched(%ebx) jne reschedule cmpl $ 0 , sigpending (%ebx) Jne signal—return 0194 : 0195 : 0196 : ENTRY(system-ca11) pushl %eax # システムコール番号を保存する pushl pushl %edx pushl pushl %edi pushl %ebp pushl pushl pushl %es cld %ecx %esi %ds movl $ ( ー KERNEL—DS) , movl %edx , %ds movl %edx , %es %edx system-call() は、ますレジスタの内容をスタックに待 避させます。この段階でのスタックの様子を図 7 に示し 152 202 行目の sys-call-table は、各システムコーノレのサー ビスルーチンのアドレスかオ巒内されているテープルです。 このテープルは、同じく entry. S で定義されています。 IA-32 の仮想アドレスの大きさは 4 バイトなので、シ ステムコール番号 (EAX レジスタの内容 ) を 4 倍した値 をインデックスとして、 sys-call-table から該当するアド レスを取り出して CALL します。 システムコールを実装するサービスルーチンは、それぞ れ EAX レジスタに戻り値をオ内して戻るように決まって います。 kernel-thread() が発行したシステムコールは、 彳する sys-clone() を呼び出し、 sys-clone() は子プ ロセスのプロセス ID を EAX レジスタに↑褓内して ker- nel-thread() へと戻ります。 203 行目は、サービスルーチンの戻り値をスタック上 に保存します。 ESP の内容に 24 バイト加えたところは、 196 行目で EAX の内容を待避させた位置を指していま す。そして、この値は 211 行目の POPL 命令によって EAX にロードされます。ここで戻り値をスタックに保存 しているのは、 206 ~ 209 行目の条件カヾ岡たされたときに UNIX MAGAZINE 2003.3