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

  • 74
    Like
  • 0
    Comment

概要

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普及協議会をご覧ください。協議会の会員募集中です :-)

リンク