$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Working with Unix ProcessesをPerlで

 以前 Working with Unix Processesという本を読んだのですが、この本がUnixにおけるプロセスについて非常にわかりやすく解説されていました。それで自分で内容をメモしてみたり、さらにわからないところを調べたり、参考のプログラムをPerlで書いたり(この本ではRubyで書かれています)してみたのですが、ブログにまとめてなかったので、ちょっと書いてみます。

 (注意)書いていたらすごく長くなりました。興味のある方は、適当に時間のあるときにでもどうぞ。


Chapter 2 : Introduction

  • プロセスのことを知るとコードを読むだけでは分からないややこしい問題が分かるようになるよ

Chapter 3 : Primer

  • Unixはユーザ空間とカーネル空間がある
  • kernelの機能はsystem call経由で利用する
  • ユーザ空間ではプログラムが動く

Chapter 4 : Processes Have IDs

  • pid : unique process id
  • psを使えばprocess情報が見れる
    • macでpsとかするとこんな感じに表示される
PID TTY           TIME CMD
391 ttys000    0:00.81 -zsh

perlだと以下のようにすればpidが取れる

use Perl6::Say;

say $$;

# or
use POSIX;
say POSIX::getpid;

Chapter 5 : Processes Have Parents

  • 全てのprocessは親をもつ
    • lsコマンドを実行したら terminal -> bash -> lsみたいな関係ができる
    • ppid = parent id

perlでppidを取得するには以下のようにする

use POSIX;
say POSIX::getppid;

Chapter 6 : Processes Have File Descriptors

  • Unixではすべてはfileであるという思想を持っている
    • files, sockets, pipesがfileっぽく扱える
  • processがresourseを開いたらfile descriptorがつけられる
    • perlだとIO::Fileとか使ったらできる。STDIN, STDOUTとかも同じ。
  • processが閉じたらfile descriptorは全部なくなる
  • forkしたらfile descriptorは共有される
  • 上限はデフォルトで1024まで

Perlでfile descriptorの値を取ってくるには以下のようにする。

warn fileno(STDIN);
warn fileno(STDOUT);
warn fileno(STDERR);

use IO::File;

my $fh = IO::File->new;
if ($fh->open("hoge.pl")) {
    warn $fh->fileno; # filenoでfile descriptorの値がわかる
    $fh->close;
}

Chapter 7 : Processes Have an Environment

  • プロセスは環境変数を持つ
    • export HOGE=FUGAみたいなやつ
    • perlだと$ENV{HOGE}とか
  • 子プロセスにも引き継がれる

Chapter 8 : Processes Have Names

  • processは自身の状態を伝える手段をほとんど持っていない
  • なので二つの仕組みを使う
    • process name
    • exit code
  • process name
    • $0で見れる
    • $0に値を入れると名称が変わる
    • psで見れる

perlでprocess nameを変える方法は以下のとおり。これをうまく利用すると、プロセスの状態をプログラム側から知らせることができる。

for (1..20) {
    $0 = "perl process : $_";
    sleep 1;
}
  PID TTY           TIME CMD
36720 ttys003    0:00.01 perl process : 5 

Chapter 9 : Processes Have Exit Codes

  • exit codeはプロセスが状態を伝える最後のチャンス
  • 0-255
  • 0は成功

perlでexit codeを取得するには以下のようにする。256で割るのは、返ってきた値の下位8bitに受け取ったシグナルの情報と、coredumpを吐いたかどうかの情報が入っているから。

my $code = system("perl exit-with-code.pl") / 256;
warn $code;

waitpidとかしている場合は$?とかでexit codeが取れる。

Chapter 10 : Processes Can Fork

  • forkすることでprocessはcopyを作る
  • child processはparentのメモリ内容のコピーを継承
  • file descriptorsとかも継承される
    • これによりfileとかsocketとかは共有できる
  • childは自身の持っているメモリに対して、parentに影響なく書き込みを行える
  • メモリ内容をコピーするので、forkしまくるとメモリ消費が多くなる
    • Copy on Write的なものも関係するので一概には言えない

perlだとforkという関数でfork可能。forkコマンドは

  • 親だと子プロセスのpidを返す
  • 子供だと0を返す
  • errorが起こったらundefを返す
my $pid = fork;
die "Cannot fork: $!" unless defined $pid;

if ($pid) {
    warn "pid:$$ is a parent process(child pid is $pid)";
    wait;
}
else {
    warn "pid:$$ is a child process(parent pid is $0)";
}

Chapter 11 : Orphaned Processes

  • ちゃんとハンドリングしなかった場合、parentが死んでもchildは生き続ける
  • 管理方法は2つ
    • daemon process
    • processとsignal communicate

以下を実行すると親は死んでいるがI'm an orphan!と5回表示される

my $pid = fork;
die "Cannot fork: $!" unless defined $pid;

if ($pid) {
    warn "pid:$$ is a parent process(child pid is $pid)";
    die;
}
else {
    for (1..5) {
        sleep 1;
        warn "I'm an orphan!"
    }
}

Chapter 12 : Processes Are Friendly

  • Copy on Write
    • processをforkで作った後、書き込みが起こった時にcopyを行うこと

Chapter 13 : Processes Can Wait

  • waitで子プロセスを待てる
  • waitpidとかを使えばあるプロセスを待つことができる
  • $? / 256で子プロセスの終了ステータスが取れる
  • 子プロセスが終わった時に親プロセスが寝てても、その結果はキューイングされている

子プロセスが終わった時に親プロセスはsleepしているがちゃんと$?とかが取れる。

my $pid = fork;

if ($pid) {
    warn 'parent';
    sleep 5;
    my $pid2 = waitpid($pid, 0);
    warn $pid2;
    warn $? / 256;
}
else {
    warn 'child';
    sleep 3;
    warn 'finish child';
    exit 10;
}

Chapter 14 : Zombie Processes

  • 終了状態をqueueingしているので、回収しないとkernelのresourceを使い続ける
    • この状態をzombie状態という
    • なのでwaitを使わず終了したいならdetachしないといけない
  • ps uとかするとzとかZ+とか出てる。こういうのがzombie化したプロセス。
  • perlrubyのProcess.detachをやる方法はわからなかった

Chapter 15 : Processes Can Get Signals

  • processはsignalを受け取れる
  • SIGCHLDとかは子プロセスが終了した時に通知される
  • waitをnonblockingにするにはwaitpidを使う

すべての子供が死ぬまでずっと何かし続けている例。perlだとuse POSIX ":sys_wait_h";をして、WNOHANGをwaitpidの第二引数に渡してあげると非同期にプロセスを待てる。

use POSIX ":sys_wait_h";

# 5回fork
for (0..4) {
    my $pid = fork;
    if (!defined $pid) {
        die "fork failed"; # 生成に失敗した
    }
    elsif (!$pid) {
        sleep int rand(10);
        print "$$ exit";
        exit;
    }
}

my $count = 0;
while (1) {
    my $pid = waitpid(-1, WNOHANG);

    warn '================';

    if ($pid) {
        warn "exit $pid";
        $count++;
        exit if $count == 5;
    }

    warn 'heavy task : ', rand();

    sleep 1;
}
  • signalを受け取った時kernelはいずれかを実行
    • 無視
    • 指定したaction
    • default action
  • signalはkernelを仲介人として、プロセスからプロセスに配信される

perlだとkillを使ってsignal送信できる。

my $pid = ...;
kill(2, $pid);
  • signal handlerの設定も出来る

perlだと%SIGに設定をしてあげる事でsignal handlerの設定を行える。

use strict;
use warnings;

warn $$;

local $SIG{INT} = 'IGNORE';

sleep 100;

Chapter 16 : Daemon Processes

  • daemon processは基本的にbackgroundで動くようなprocess
  • 全ての親はinit process
    • pidは1、ppidは0、つまり親がない
  • forkした親が死んだら子はinit processの子になる

以下を実行するとppidは1を返す。つまり、init processの子になった。

use POSIX qw(setsid);

my $pid = fork;

if ($pid) {
    exit;
}
else {
    sleep 1;
    warn POSIX::getppid;
}

getpgrpを使うとprocess group idがわかる。forkすると親のprocess idを元にしてprocess groupが作られる事がわかる。

use POSIX qw();

warn POSIX::getpid;
warn getpgrp;

if (fork) {
    wait;
}
else {
    warn POSIX::getpid;
    warn getpgrp;
}

pipeをするとそのコマンド群がプロセスグループとなる。

git log | grep shipped | less

session idを見るにはpsにオプションを付けるといい

ps ax -o pid,state,sess,command

以下のようにすることでperldaemon processを作ることができる。

use POSIX qw(setsid);

my $pid = fork();
if ($pid != 0) {
    print "create daemon process...\n";
    exit;
}
else {
    umask 0;
    chdir '/';
    open STDIN,  '<', '/dev/null';
    open STDOUT, '>>', '/dev/null';
    open STDERR, '>>', '/dev/null';
    setsid;

    while(1){
        warn "hello\n";
        sleep 2;
    }
}

またデーモン化するなら、孫プロセスを作ったほうが良いらしい。

Chapter 17 : Spawning Terminal Processes

  • execによってprocessを違うprocessに変換できる
  • execを実行すると元のprocessには返ってこない
  • fork + execの場合も、waitで待てる

途中から子プロセスがsleep 10というprocessに変換される。

my $pid = fork;

if ($pid) {
    my $res = wait;
    warn $res / 256;
}
else {
    sleep 5;
    exec "sleep 10";
}
  • perlではprocessを作成すると...
    • systemはexit codeが返ってくる
    • ``はSTDOUTが返ってくる
    • openとかもいろいろコマンド実行できる
  • 全てのコマンド実行はforkに依存している
    • forkはmemory内容をcopyしてしまうのでボトルネックになる
    • それまで開いた全てのfile descriptorをcopyする

その他

並列処理パターン

Perlにおける並列処理パターン。適当に書くとこんな感じ。

use Proc::Wait3;

my %worker_pids;
my $num_workers = 4; # ワーカープロセスの数
my $exit_loop;

# サーバの初期化処理

# SIGTERM ハンドラをセット
$SIG{TERM} = sub { $exit_loop = 1 };

# メインループ ($num_workers 個のワーカープロセスを駆動)
while (! $exit_loop) {
    my $pid;
    if (keys %worker_pids < $num_workers) {
        $pid = fork;
        die 'fork error' unless defined $pid;
        if ($pid) {
            $worker_pids{$pid} = 1;
        } else {
            exit(child());
        }
        sleep 1;
    }
    if (my ($exit_pid) = wait3(! $pid)) {
        delete $worker_pids{$exit_pid};
    }
}
# ワーカーを停止して終了
foreach my $c (keys %worker_pids) {
    kill 'TERM', $c;
}
while (%worker_pids) {
    if (my $exit_pid = wait) {
        delete $worker_pids{$exit_pid};
    }
}
exit 0;

# ワーカープロセス
sub child {
    # SIGTERM ハンドラをセット
    $SIG{TERM} = sub { exit 0 };
    # 実際の処理
    warn '======= start ===========';
    sleep 10;
    # 終了コードを返す
    0;
}

実際作るなら、Parallel::Preforkとか、Parallel::ForkManagerとか使うといい。

Server::Starter

hot deployを可能にしてくれる。
以前まとめたので、Server::Starterから学ぶhot deployの仕組み - $shibayu36->blog;を参照してください。

最後に

 こんなかんじでWorking with Unix Processesの内容を少しだけまとめ、さらに自分で調べたことを追加し、Perlでやってみました。英語版ですが非常に読みやすく、値段も高くないので一度読んでみると良いと思います。

 更にLinuxについて知りたいという方は、Linuxカーネル2.6解読室がお勧めです。

Linuxカーネル2.6解読室