オペレーティングシステム 演習 02¶

プロセス¶

名前と学生証番号を書け. Enter your name and student ID.

  • 名前 Name:
  • 学生証番号 Student ID:

1. プロセス関連コマンド¶

1-1. ps auxww¶

  • ps は現存するプロセスを表示するコマンド
  • 話せば長い(詳細はmanページを参照)が, auxwwですべてのプロセスがコマンドラインとともに表示される
  • 以下によりシステムのすべてのプロセスが表示される
  • 長過ぎる出力がうっとおしければファイルに出力するコマンドに書き換えて実行しなおせば良い
  • 例えば
ps auxww > ps.txt
In [ ]:
ps auxww

2. fork¶

  • forkはUnixでプロセスを生成する手段

  • プロセスを生成 = (例えば実行するファイルを指定して)プログラムを起動するということかと思いきやそうではなく, forkは何の引数もとらず, 呼び出したプロセスのコピーを作るというもの

  • 以下は全く役に立たないが, ともかくforkが何をするシステムコールかを教えてくれるプログラム

  • OSのシステムコールは特定のプログラミング言語には依存していない

  • そこで意味がある場合, なるべく同じことをPythonとCの両方で示すことにする

2-1. C¶

In [ ]:
%%writefile fork.c
#include <stdio.h>
#include <unistd.h>

int main() {
  printf("%d : before fork\n", getpid());
  fflush(stdout);
  fork();           /* 現プロセスをコピー */
  printf("%d : after fork\n", getpid());
  return 0;
}
In [ ]:
gcc -Wall fork.c -o fork
In [ ]:
./fork

2-2. Python¶

In [ ]:
%%writefile fork.py
import os

print(f"{os.getpid()} : before fork", flush=True)
os.fork()
print(f"{os.getpid()} : after fork")
In [ ]:
# 何も表示されない場合, もう一度実行せよ (Jupyter環境の不具合)
python3 fork.py

3. 親と子で違う動作をする例¶

  • forkを使うもう少し完結した例
  • 親と子で違う動作をする

3-1. C¶

In [ ]:
%%writefile fork_pc.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  pid_t pid = fork();           /* 現プロセスをコピー */
  if (pid == -1) {
    err(1, "fork");
  } else if (pid == 0) {        /* 新しいプロセス(子プロセス) */
    for (int i = 0; i < 5; i++) {
      printf("child  %d: %d\n", getpid(), i);
      fflush(stdout);
      usleep(100 * 1000);
    }
  } else {                      /* 元いたプロセス(親プロセス)
                                   forkの返り値は子プロセスのプロセスID */
    for (int i = 0; i < 5; i++) {
      printf("parent %d: %d\n", getpid(), i);
      fflush(stdout);
      usleep(100 * 1000);
    }
  }
  return 0;
}
In [ ]:
gcc -Wall fork_pc.c -o fork_pc
In [ ]:
./fork_pc

3-2. Python¶

In [ ]:
%%writefile fork_pc.py
import os
import time

pid = os.fork()
if pid == 0:
    # 新しいプロセス(子プロセス)
    for i in range(5):
        print(f"child  {os.getpid()}: {i}", flush=True)
        time.sleep(0.1)
else:
    # 元いたプロセス(親プロセス) forkの返り値は子プロセスのプロセスID
    for i in range(5):
        print(f"parent {os.getpid()}: {i}", flush=True)
        time.sleep(0.1)
In [ ]:
python3 fork_pc.py

4. wait¶

  • forkを使うさらにまともな例
  • 親が子のwait処理をする

4-1. C¶

In [ ]:
%%writefile fork_wait.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
  pid_t pid = fork();
  if (pid == -1) {
    err(1, "fork");
  } else if (pid == 0) {          /* child */
    for (int i = 0; i < 5; i++) {
      printf("%d: %d\n", getpid(), i);
      fflush(stdout);
      usleep(100 * 1000);
    }
    return 123;
  } else {
    int ws;
    printf("parent: wait for child (pid = %d) to finish\n", pid);
    fflush(stdout);
    pid_t cid = waitpid(pid, &ws, 0);
    if (cid == -1) err(1, "waitpid");
    if (WIFEXITED(ws)) {
      printf("exited, status=%d\n", WEXITSTATUS(ws));
      fflush(stdout);
    } else if (WIFSIGNALED(ws)) {
      printf("killed by signal %d\n", WTERMSIG(ws));
      fflush(stdout);
    }
  }
  return 0;
}
In [ ]:
gcc -Wall fork_wait.c -o fork_wait
In [ ]:
./fork_wait

4-2. Python¶

In [ ]:
%%writefile fork_wait.py
import os
import time

pid = os.fork()
if pid == 0:
    # 新しいプロセス(子プロセス)
    for i in range(5):
        print(f"child  {os.getpid()}: {i}", flush=True)
        time.sleep(0.1)
else:
    # 元いたプロセス(親プロセス) forkの返り値は子プロセスのプロセスID
    print(f"parent: wait for child (pid = {pid}) to finish", flush=True)
    cid, ws = os.waitpid(pid, 0)
    if os.WIFEXITED(ws):
        print(f"exited, status={os.WEXITSTATUS(ws)}", flush=True)
    elif os.WIFSIGNALED(ws):
        print(f"killed by signal {os.WTERMSIG(ws)}", flush=True)
In [ ]:
python3 fork_wait.py

5. exec¶

  • execは指定したプログラムを実行するシステムコール
  • 呼び出したプロセスをそのまま, 指定したプログラムを先頭から実行するものに「変える」というイメージ
  • execがプロセスを生成するのではないので注意
  • execはあくまで呼び出したプロセスを, これまでのことをすべて忘れて所定のことをするプロセスに「変身させる」
  • なお, execという名のシステムコールが実在するのではなく, execv, execveなど色々な変種の総称
  • 以下は ls -l を実行するプロセスを作るプログラム

5-1. C¶

In [ ]:
%%writefile fork_execv.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

extern char ** environ;

int main() {
  pid_t pid = fork();           /* 現プロセスをコピー */
  if (pid == -1) {
    err(1, "fork");
  } else if (pid == 0) {        /* 新しいプロセス(子プロセス) */
    char * const argv[] = { "/bin/ls", "-l", 0 };
    execv(argv[0], argv);
    /* 成功すればexecveはリターンしない.
       i.e., リターンしたらエラー */
    err(1, "execve");
  } else {
    int ws;
    pid_t cid = waitpid(pid, &ws, 0);
    if (cid == -1) err(1, "waitpid");
    if (WIFEXITED(ws)) {
      printf("exited, status=%d\n", WEXITSTATUS(ws));
      fflush(stdout);
    } else if (WIFSIGNALED(ws)) {
      printf("killed by signal %d\n", WTERMSIG(ws));
      fflush(stdout);
    }
  }
  return 0;
}
In [ ]:
gcc -Wall fork_execv.c -o fork_execv
In [ ]:
./fork_execv

5-2. Python¶

In [ ]:
%%writefile fork_execv.py
import os

pid = os.fork()
if pid == 0:                    # 新しいプロセス(子プロセス)
    argv = [ "/bin/ls", "-l" ]
    os.execv(argv[0], argv)
else:
    cid, ws = os.waitpid(pid, 0)
    if os.WIFEXITED(ws):
        print(f"exited, status={os.WEXITSTATUS(ws)}", flush=True)
    elif os.WIFSIGNALED(ws):
        print(f"killed by signal {os.WTERMSIG(ws)}", flush=True)
In [ ]:
python3 fork_execv.py

6. execvp¶

  • execv関数では実行したいコマンド(ls)のファイル名(/bin/ls)を指定しなくてはならない
  • 普段シェルでコマンドを実行する際は ls と打つだけで実行できているのは, シェルがPATHという環境変数を見てコマンドを探してくれているから
  • 以下でPATHという環境変数の中身が表示できる
In [ ]:
echo $PATH
  • シェルは, PATHに指定されているディレクトリを順に見ていって, lsという名前のファイルが見つかったらそれを実行する
  • 同じことはexecの変種 execvp という関数を呼べばやってくれる

6-1. C¶

In [ ]:
%%writefile fork_execvp.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

extern char ** environ;

int main() {
  pid_t pid = fork();           /* 現プロセスをコピー */
  if (pid == -1) {
    err(1, "fork");
  } else if (pid == 0) {        /* 新しいプロセス(子プロセス) */
    char * const argv[] = { "ls", "-l", 0 };
    execvp(argv[0], argv);
    /* 成功すればexecveはリターンしない.
       i.e., リターンしたらエラー */
    err(1, "execve");
  } else {
    int ws;
    pid_t cid = waitpid(pid, &ws, 0);
    if (cid == -1) err(1, "waitpid");
    if (WIFEXITED(ws)) {
      printf("exited, status=%d\n", WEXITSTATUS(ws));
      fflush(stdout);
    } else if (WIFSIGNALED(ws)) {
      printf("killed by signal %d\n", WTERMSIG(ws));
      fflush(stdout);
    }
  }
  return 0;
}
In [ ]:
gcc -Wall fork_execvp.c -o fork_execvp
In [ ]:
./fork_execvp

6-2. Python¶

In [ ]:
%%writefile fork_execvp.py
import os

pid = os.fork()
if pid == 0:                    # 新しいプロセス(子プロセス)
    argv = [ "ls", "-l" ]
    os.execvp(argv[0], argv)
else:
    cid, ws = os.waitpid(pid, 0)
    if os.WIFEXITED(ws):
        print(f"exited, status={os.WEXITSTATUS(ws)}", flush=True)
    elif os.WIFSIGNALED(ws):
        print(f"killed by signal {os.WTERMSIG(ws)}", flush=True)
In [ ]:
python3 fork_execvp.py
  • なお, PATHの中身を見て, コマンド名に対応するファイル名を表示してくれるのが which というコマンド
  • コマンドを打ち込んで実行されているファイルがどこにあるのかを知りたいときに使う
  • Linux, Macを使っている人は普段使っているプログラム, firefox, zoomなどがどこにあるのかを探ってみよ
In [ ]:
which firefox
which zoom

7. forkにまつわる悲劇¶

  • 以下のようなプログラムを書いたらプロセスはいくつ生成されるのだろう?
  • そして以下を実行するとどんな表示が出てくるか?
  • 頭で予想してから実行してみよ
  • n=100としたら何が起こるか(<- 決してやってはいけない)?

7-1. C¶

In [ ]:
%%writefile fork_n.c
#include <stdio.h>
#include <unistd.h>

int main() {
  int n = 5;
  for (int i = 0; i < n; i++) {
    pid_t cid = fork();
    printf("%d -> %d\n", getpid(), cid);
    fflush(stdout);
  }
  return 0;
}
In [ ]:
gcc -Wall fork_n.c -o fork_n
  • 以下を実行して (左側のファイル一覧から) out.txt を開いて見てみよ
  • プログラムと結果を見て, このプログラムを実行した結果何が起きたのかを考えよ (わかりにくければ n を小さくして)
In [ ]:
./fork_n > out.txt

7-2. Python¶

In [ ]:
%%writefile fork_n.py
import os

n = 5
for i in range(5):
    cid = os.fork()
    print(f"{os.getpid()} -> {cid}", flush=True)
  • 以下を実行して (左側のファイル一覧から) out.txt を開いて見てみよ
  • プログラムと結果を見て, このプログラムを実行した結果何が起きたのかを考えよ (わかりにくければ n を小さくして)
In [ ]:
python3 fork_n.py > out.txt
  • 注: > out.txt なしで直接表示することもできるが, 結果をすべて出力てくれないことがしばしばある. これは多分, Jupyterのbashカーネルのバグ
  • 多分以下のように, このセルの終了まで間を作ると全部出力してくれる(Jupyterのバグを回避しているだけで全く本質的なことではない. 端末で実行すればこんなことをする必要はない)
In [ ]:
./fork_n
sleep 1

Problem 1 : fork, exec, waitの練習¶

以下を行うプログラムを書け

  1. 時刻をナノ秒単位で取得($t_0$とする)
  2. 以下を多数回($n$回)繰り返す
  • 子プロセスをforkする
  • 子プロセスはただちに ./do_nothing という, 何もしないプログラムをexecする
  • 親プロセスはただちに子プロセスの終了をwaitする
  1. 時刻をナノ秒単位で取得($t_1$とする)
  2. 1回あたりの時間($(t_1 - t_0)/n$) をナノ秒単位で出力
  • do_nothingは以下のような, 何もしないプログラム
In [ ]:
%%writefile do_nothing.c
int main() {
  return 0;
}
In [ ]:
gcc -Wall do_nothing.c -o do_nothing
  • $n$はコマンドラインから取得できるようにする
  • 以下のコードを修正して上記を達成せよ(PythonとC両方)

7-3. C¶

In [ ]:
%%writefile time_fork_exec_wait.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();

  
  ここにプログラムを書く

  
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to fork-exec-wait %d processes (%ld nsec/proc)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -Wall time_fork_exec_wait.c -o time_fork_exec_wait
In [ ]:
%%writefile time_fork_exec_wait_ans.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();
  for (int i = 0; i < n; i++) {
    pid_t pid = fork();           /* 現プロセスをコピー */
    if (pid == -1) {
      err(1, "fork");
    } else if (pid == 0) {        /* 新しいプロセス(子プロセス) */
      char * const argv[] = { "./do_nothing", 0 };
      execvp(argv[0], argv);
      err(1, "execve");
    } else {
      int ws;
      pid_t cid = waitpid(pid, &ws, 0);
      if (cid == -1) err(1, "waitpid");
    }
  }
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to fork-exec-wait %d processes (%ld nsec/proc)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -Wall time_fork_exec_wait_ans.c -o time_fork_exec_wait_ans
  • 同様に以下のコマンドラインを色々変更して, 1回あたりの時間を計測せよ
In [ ]:
./time_fork_exec_wait 10
In [ ]:
./time_fork_exec_wait_ans 10

7-4. Python¶

In [ ]:
%%writefile time_fork_exec_wait.py
import os
import sys
import time

def cur_time():
    return int(time.time() * 1.0e9)

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()

  
    ここにプログラムを書く

  
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to fork-exec-wait {n} processes ({dt / n} nsec/proc)")

main()
In [ ]:
%%writefile time_fork_exec_wait_ans.py
import os
import sys
import time

def cur_time():
    return int(time.time() * 1.0e9)

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()
    for i in range(n):
        pid = os.fork()             # 現プロセスをコピー
        if pid == 0:                #  新しいプロセス(子プロセス)
            argv = [ "./do_nothing" ]
            os.execvp(argv[0], argv)
        else:
            cid, ws = os.waitpid(pid, 0)
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to fork-exec-wait {n} processes ({dt / n} nsec/proc)")

main()
  • 以下のコマンドラインを色々変更して, 1回あたりの時間を計測せよ
  • これは概ね, fork + exec + exit + wait の時間 (プログラムを起動して終了するまでにかかる最小の時間)を計測していることに相当する
  • 正しく動いているかを確認するために, do_nothingで何かをprintするとか, time_fork_exec_wait中でwaitpidの結果を表示するようにせよ
  • 時間を計測するときはそれらの表示を消すこと(消さないと, 測っているのは出力時間が大半を占めることになる)
In [ ]:
python3 time_fork_exec_wait.py 10
In [ ]:
python3 time_fork_exec_wait_ans.py 10

Problem 2 : fork + waitの測定¶

  • 子プロセスが do_nothing を exec する代わりに, 直ちにexit した場合の時間(fork + wait の時間)も計測せよ

7-5. C¶

In [ ]:
%%writefile time_fork_exit_wait.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();

  
  ここにプログラムを書く

  
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to fork-wait %d processes (%ld nsec/proc)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -Wall time_fork_exit_wait.c -o time_fork_exit_wait
In [ ]:
./time_fork_exit_wait 10
In [ ]:
%%writefile time_fork_exit_wait_ans.c
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>

long cur_time() {
  struct timespec ts[1];
  clock_gettime(CLOCK_REALTIME, ts);
  return ts->tv_sec * 1000000000L + ts->tv_nsec;
}

int main(int argc, char ** argv) {
  int n = (argc > 1 ? atoi(argv[1]) : 5);
  long t0 = cur_time();
  for (int i = 0; i < n; i++) {
    pid_t pid = fork();           /* 現プロセスをコピー */
    if (pid == -1) {
      err(1, "fork");
    } else if (pid == 0) {        /* 新しいプロセス(子プロセス) */
      exit(0);
    } else {
      int ws;
      pid_t cid = waitpid(pid, &ws, 0);
      if (cid == -1) err(1, "waitpid");
    }
  }
  long t1 = cur_time();
  long dt = t1 - t0;
  printf("%ld nsec to fork-wait %d processes (%ld nsec/proc)\n",
         dt, n, dt / n);
  return 0;
}
In [ ]:
gcc -Wall time_fork_exit_wait_ans.c -o time_fork_exit_wait_ans
In [ ]:
./time_fork_exit_wait_ans 10

7-6. Python¶

In [ ]:
%%writefile time_fork_exit_wait.py
import os
import sys
import time

def cur_time():
    return int(time.time() * 1.0e9)

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()

  
    ここにプログラムを書く

  
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to fork-wait {n} processes ({dt / n} nsec/proc)")

main()
In [ ]:
python3 time_fork_exit_wait.py 10
In [ ]:
%%writefile time_fork_exit_wait_ans.py
import os
import sys
import time

def cur_time():
    return int(time.time() * 1.0e9)

def main():
    n = int(sys.argv[1]) if 1 < len(sys.argv) else 5
    t0 = cur_time()
    for i in range(n):
        pid = os.fork()             # 現プロセスをコピー
        if pid == 0:                #  新しいプロセス(子プロセス)
            os._exit(0)
        else:
            cid, ws = os.waitpid(pid, 0)
    t1 = cur_time()
    dt = t1 - t0
    print(f"{dt} nsec to fork-wait {n} processes ({dt / n} nsec/proc)")

main()
In [ ]:
python3 time_fork_exit_wait_ans.py 10