Edited at
LinuxDay 17

Linuxプロセスとカーネル読解のとっかかり

More than 1 year has passed since last update.


概要

Linux Kernelを読み解くためのとっかかりとしてLinuxのプロセスの理解(SoftwareDesign2014年8月号の特集参照)に焦点をあててみたいと思います。プロセス、スレッド、CPUについての話の後、最新カーネルのダウンロードからちょっとした中身の確認までやります。サンプルコードもありますが、サービス影響のあるサーバでは実行しないでください。


プロセスとスレッド


  • プロセスとは


    • Linux上で動いているプログラム


      • スレッドが無い時はプロセスが実行単位





  • スレッドとは


    • Linuxプロセスにおけるスレッド


      • 「1つのプロセスの中で複数の実行単位を持てるように機能拡張したもの」(SoftwareDesign2014年8月号)



    • CPUにおけるスレッド


      • 「最小の処理単位」





01.png


  • プロセス2が終わった後に処理できるプロセス3がある例


    • スレッドを利用しない場合(左図)


      • プロセス2の処理が終わるまでのプロセス3の待ち時間が長くなる

      • 複数のCPUがあっても実質一つしか動いてない



    • スレッドを利用する例(右図)


      • プロセス2を複数のCPUに分けて処理することができる





02.png


  • その他プロセス、スレッドの特徴


    • プロセスはメモリ空間やファイルハンドラといったリソースを共有「しない」

    • スレッドはメモリ空間やファイルハンドラといったリソースを共有「する」

    • あるスレッドがグローバル変数に変更を加えると、同じプロセス内の他のスレッドから参照すると変更された値が読み込まれる

    • オープン中のファイルや確保したメモリはプロセス単位なので、一つのスレッドが変更を加えると他のスレッドにも影響する

    • プロセスが終了すると、そこに含まれるすべてのスレッドが終了する




Apacheでの確認例


2つのMPM(マルチプロセッシングモジュール)


  • prefork


    • 先行してプロセスのforkを行なう

    • 1つのプロセスが1つのリクエストに応答する

    • スレッドは使わない


      • スレッドセーフではないサードパーティモジュールでも使える



    • Linux系OSデフォルト値



  • worker


    • マルチプロセス+マルチスレッド

    • preforkよりも少ないメモリで多くのリクエストに応答することができる


      • スレッドでリクエストに応答するのでプロセス数がpreforkよりも少なくて済む



    • 1つのスレッドが停止するか制御不能になると、そのプロセス全体が強制終了される



※ プロセスやスレッドのでき方は設定値によります


prefork

$ httpd -V | grep MPM

================================================
Server MPM: prefork
================================================

$ ps aux -L | head -1; ps aux -L | grep httpd | grep -v grep
================================================
USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
root 6735 6735 0.0 1 0.3 228564 6324 ? Ss 17:31 0:00 /usr/sbin/httpd -DFOREGROUND ※ セッションリーダー
apache 6791 6791 0.0 1 0.1 228564 3188 ? S 17:31 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6793 6793 0.0 1 0.1 228564 3188 ? S 17:31 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6794 6794 0.0 1 0.1 228564 3188 ? S 17:31 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6796 6796 0.0 1 0.1 228564 3188 ? S 17:31 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6797 6797 0.0 1 0.1 228564 3188 ? S 17:31 0:00 /usr/sbin/httpd -DFOREGROUND
================================================


  • 全て PID = LWP


    • スレッド無し



prefork.png


worker

$ httpd -V | grep MPM

================================================
Server MPM: worker
================================================

$ ps aux -L | head -1; ps aux -L | grep httpd | grep -v grep
================================================
USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND
root 6547 6547 0.0 1 0.3 228768 6524 ? Ss 17:26 0:00 /usr/sbin/httpd -DFOREGROUND ※ セッションリーダー
apache 6575 6575 0.0 1 0.1 228516 3188 ? S 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6602 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND ※ プロセス
apache 6602 6648 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND ※ 以下26個のスレッド
apache 6602 6649 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6650 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6651 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6652 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6653 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6654 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6655 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6656 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6657 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6658 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6659 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6661 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6662 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6663 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6664 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6665 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6667 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6668 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6669 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6670 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6671 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6672 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6673 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6674 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
apache 6602 6675 0.0 27 0.1 515596 3660 ? Sl 17:26 0:00 /usr/sbin/httpd -DFOREGROUND
...(PID 6603, 6604も同様に26本のスレッドを作成しているが長くなるので省略)
================================================


  • LWPはスレッドのID

  • PIDとLWPの値が同じものがプロセス。それ以外がスレッド

  • NLWPはスレッド数(大元のプロセスも含めた数)「27」


    • 3つのプロセス(6602,6603,6604)がそれぞれ26本のスレッドを作成

    • 長くなるので6602の数本のスレッドのみ表示している



worker.png


参考


Linuxプログラミングでforkの挙動を見てみる

ここではC言語で簡単なforkを見るプログラムを作って動作を確認します


forkで親プロセス、子プロセスを見てみる


  • 概要


    • システムコールを用いて親プロセスが自分自身を複製した子プロセスを生成するようにカーネルに依頼する

    • 子プロセスは親プロセスと同じプログラムコードを実行する


      • 子プロセスはすべてのプログラムではなくfork()以下を実行する






fork.c

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define P_MAX 3

int main() {
int pid[P_MAX];
int status, i;

for (i=0; i<P_MAX; i++) {
pid[i] = fork();
if (pid[i] == 0) {
printf("子プロセス No.%d: 開始\n", i);
sleep(i+1);
printf("子プロセス No.%d: 終了\n", i);
exit(0);
} else {
printf("親プロセス: 子プロセスNo.%d(pid=%d)を開始\n", i, pid[i]);
}
}
printf("すべての子プロセスの終了を待ちます\n");
for (i=0; i<P_MAX; i++) { /* forループ2 */
wait(&status);
}
return;
}




  • #define P_MAX 3 : 作る子プロセスの数を3つに設定


  • pid[i] = fork(); : 子プロセスを作成する



    • pid[i] の値で親プロセスと子プロセスを判別して処理を分けている



      • pid[i] == 0 : 子プロセスの処理。子プロセスが作成されたときと処理が終わったときにprintfでその旨出力する。sleepで少し動きを止める


      • pid[i] が子プロセスのID : 親プロセス






  • wait(&status);


    • 任意の子プロセスが終了するまで待ち、その子プロセスが終了すると &status に終了した子プロセスの情報が入る


      • 親プロセスに子プロセスの情報が受け渡されて子プロセスは完全に消滅する



    • Linuxカーネルの挙動


      • 子プロセスが終了すると、カーネルは親プロセスにSIGCHLD(CHLD)シグナルを送信する

      • 親プロセスがwait()で子プロセスの終了を待っている場合


        • 関数wait()から抜けて引数の変数に終了した子プロセスの情報が入り、子プロセスは消滅する



      • 子プロセスが終了するより先に親プロセスが終了した場合


        • /sbin/init (PID=1) が代わりの親プロセスとなり、wait()を実行する







実行結果

$ ./a.out

================================================
親プロセス: 子プロセスNo.0(pid=7476)を開始
子プロセス No.0: 開始
親プロセス: 子プロセスNo.1(pid=7477)を開始
子プロセス No.1: 開始
親プロセス: 子プロセスNo.2(pid=7478)を開始
子プロセス No.2: 開始
すべての子プロセスの終了を待ちます
子プロセス No.0: 終了
子プロセス No.1: 終了
子プロセス No.2: 終了
================================================


親プロセスの親プロセスは誰か


  • 概要


    • ヘルパー関数を呼び出さず指定のシステムコールを直接実行 (厳密には syscall() もヘルパー関数の一種)


      • syscall命令を発行するための必要最小限の処理に限っている






clone.c

#include <unistd.h>

#include <stdio.h>
#include <sys/syscall.h>
#include <sys/signal.h>

int main() {
int status;
int pid = syscall(SYS_clone, SIGCHLD, 0, 0, 0, 0);

if (pid == 0) {
int gpid = getpid();
int gppid = getppid();
printf("Child, my pid = %d\n", gpid);
printf("Child, my parent pid = %d\n", gppid);
} else if (pid == -1) {
perror("SYS_clone: ");
} else {
int gpid = getpid();
int gppid = getppid();
printf("Parent, my pid = %d\n", gpid);
printf("Parent, my parent pid = %d\n", gppid);
}
wait(&status);
return;
}




  • int pid = syscall(SYS_clone, SIGCHLD, 0, 0, 0, 0); : ヘルパー関数fork()を使わずにcloneシステムコールを呼び出している


    • 引数1 SYS_clone : システムコールの番号を表すマクロでヘッダファイルの中でCloneシステムコールに対応する「56」が定義されている

    • 引数2 SIGCHLD : 子プロセスが終了した時に親プロセスにSIGCHILDシグナルを送ることを指定


      • ここには他に、メモリ空間を共有したりスレッドを生成するなどのオプションを指定できる



    • 引数3 以降


      • スレッドに関する指定


        • ここではスレッドを使っていない(すべて0)







実行結果

Child, my pid = 7896

Child, my parent pid = 7895
Parent, my pid = 7895
Parent, my parent pid = 7418


  • 子プロセスのPIDは 7896

  • 親プロセスのPIDは 7895

  • 親プロセスの親プロセスのPIDは 7418


    • 親プロセスの親プロセスの正体はbash



$ ps aux | head -1; ps aux | grep 7418 | grep -v grep

================================================
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
vagrant 7418 0.0 0.1 115516 2200 pts/0 Ss 08:35 0:00 -bash
================================================


psコマンドで表示されるプロセス名を変えてみる


  • 概要


    • 別プログラムを実行する


      • execlの「l」は引数が可変長であることの意






fork_execl.c

#include <stdio.h>

#include <unistd.h>
#include <sys/wait.h>

int main() {
int status, pid;

pid = fork();

if (pid == 0) {
printf("子プロセス No.%d: 3秒後にpwdが走ります\n", pid);
sleep(3);
execl("/bin/pwd", "/bin/pwd", NULL);
printf("子プロセスは上で終了するのでこの処理は実行されません");
} else {
printf("親プロセス: 子プロセス pid=%d の終了を待ちます\n", pid);
wait(&status);
printf("親プロセス: 終了\n");
}
return;
}



  • 子プロセスがシステムコールによってプロセスの中身を/bin/pwdに入れ替えている

  • 下のprintfは実行されず、/bin/pwdが終了次第子プロセスは終了する


    • 親プロセスとの親子関係は失われないので子プロセスが完了次第SIGCHLDシグナルは送信される



  • 引数



    • execl("/bin/pwd", "/bin/pwd", NULL);


      • 引数1 "/bin/pwd" : 実行するプログラムのPATH

      • 引数2 "/bin/pwd" : プロセス名の指定 (任意の文字列)





実行結果

親プロセス: 子プロセス pid=8072 の終了を待ちます

子プロセス No.0: 3秒後にpwdが走ります
/home/vagrant/c_samaple_code
親プロセス: 終了

execlの第二引数には任意の文字列が入れられる。半角英数以外も設定可能。


  • サービス影響のあるサーバでは実行しないでください

  • Ctrl+cで抜けてください


fork_execl_pugya.c

#include <stdio.h>

#include <unistd.h>

int main() {
execl("/usr/bin/yes", "プロセスm9(^Д^)プギャー", NULL);
}

実行結果

pugya2.png


カーネルソースのダウンロードと確認


最新のアップストリームKernelダウンロード


  • cloneはかなり時間がかかります

$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git


  • RHELなどは、このアップストリームの特定バージョンのカーネルをベースに独自にカスタマイズしたものを使っている


    • 「独自のカスタマイズ」というのはRHEL固有の追加機能を加えているという事ではなく、最新のアップストリームで追加されたパッチを選択的にあてているという意味




Kernelのfork.cを見てみる

コードの冒頭にLinusさんのコメントがあります。

Forkはシンプルだけどメモリ管理関連は難しいとのことです。

$ cd linux

$ less kernel/fork.c
================================================
/*
* linux/kernel/fork.c
*
* Copyright (C) 1991, 1992 Linus Torvalds
*/

/*
* 'fork.c' contains the help-routines for the 'fork' system call
* (see also entry.S and others).
* Fork is rather simple, once you get the hang of it, but the memory
* management can be a bitch. See 'mm/memory.c': 'copy_page_range()'
*/
...
================================================

以下、コードの内容については言及しません :-P


コミッタとコミット日時の表示


  • 行頭はコミットID

$ git blame kernel/fork.c

================================================
...
ad8d75fff (Steven Rostedt 2009-04-14 19:39:12 -0400 86)
43d2b1132 (KAMEZAWA Hiroyuki 2012-01-10 15:08:09 -0800 87) #define CREATE_TRACE_POINTS
43d2b1132 (KAMEZAWA Hiroyuki 2012-01-10 15:08:09 -0800 88) #include <trace/events/task.h>
43d2b1132 (KAMEZAWA Hiroyuki 2012-01-10 15:08:09 -0800 89)
^1da177e4 (Linus Torvalds 2005-04-16 15:20:36 -0700 90) /*
^1da177e4 (Linus Torvalds 2005-04-16 15:20:36 -0700 91) * Protected counters by write_lock_irq(&tasklist_lock)
^1da177e4 (Linus Torvalds 2005-04-16 15:20:36 -0700 92) */
...
================================================


コミットIDからパッチの内容を確認


  • 「Signed-off-by」はパッチの作成者、およびメンテナがこのパッチを制式採用することに同意したことを示す署名


    • KAMEZAWAさんがパッチを作成

    • 最終的にLinusさんが採用について同意している。



$ git show 43d2b1132

================================================
commit 43d2b113241d6797b890318767e0af78e313414b
Author: KAMEZAWA Hiroyuki <kamezawa.hiroyu@jp.fujitsu.com>
Date: Tue Jan 10 15:08:09 2012 -0800
tracepoint: add tracepoints for debugging oom_score_adj
...
Signed-off-by: KAMEZAWA Hiroyuki <kamezawa.hiroyu@jp.fujitsu.com>
Cc: KOSAKI Motohiro <kosaki.motohiro@jp.fujitsu.com>
Acked-by: David Rientjes <rientjes@google.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
...
================================================


カーネルの読解のとっかかり

一般的に /linux/init ディレクトリから始めるか、気になるトピックから始めると良いみたいです。


終わりに

この記事は関東に居たときに 身内の勉強会 で発表した内容をまとめなおしたものです。現在はUターン就職して徳島に住んでいますが、徳島県は地方でもITに理解のある県で、時々IT勉強会を開催しています。詳細はとくしまOSS普及協議会をご覧ください。協議会の会員募集中です :-)


リンク