スレッドを理解したい
README
- https://github.com/mohira/what-is-thread
-
Linuxによる並行プログラミング入門 | Amazon
- 短いコードがたくさん載っているので嬉しい
- 問題がたくさんあるので嬉しい(考えるきっかけになる)
-
オペレーティングシステム: 講義案内
- よさそう
単元
- forkの修行 / プロセスの生成 / プログラムの実行 https://zenn.dev/mohira/scraps/f99095d2fd74da#comment-262f1760eefd4a
- プロセスの変身 / execファミリー / fork()とexecを組み合わせたときの動き https://zenn.dev/mohira/scraps/f99095d2fd74da#comment-e4ee8fb9f42e4f
memo
- プロセス
- 『Linuxによる並行プログラミング入門』1章
- 『Linuxによる並行プログラミング入門』2章
- スレッド
- 『Linuxによる並行プログラミング入門』7章
forkの修行 / プロセスの生成 / プログラムの実行
- 『Linuxによる並行プログラミング入門』1章
5714
だった)
「カーネル」が「このユーザープロセス」に「プロセスID」を与えた(それが- プロセスIDをみるだけ
#include "unistd.h"
#include "stdio.h"
int main() {
printf("プロセスid=%d\n", getpid());
}
$ gcc 01_getpid.c;./a.out
プロセスid=5714
fork()
プロセスを生成するシステムコールa.out
(プロセスID=5714)が fork()
を呼び出す
↓
a.out
(プロセスID=5714)が「カーネル」に対して、「新しいプロセス」の生成を依頼する
↓
その結果、「新しいプロセス」(プロセスID=5715)が生まれた。
#include "stdio.h"
#include "unistd.h"
int main() {
fork();
printf("プロセスid=%d\n", getpid());
}
$ gcc 02_fork.c; ./a.out
プロセスid=5746
プロセスid=5747
printf
は1回しか呼んでないのに)
Q. なんで2行分出力されるの? (そういう仕様だから?
子プロセス は 親プロセス の コピー
で、親子関係がある。
"親子"という表現を使っているけど、人間や動物とは違う。アメーバとかゾウリムシとかの増え方に近いかも。
-
a.out
は、親プロセス - 「新しいプロセス」は、子プロセス
フォークしたときの流れ
Q. ダブルフォークした場合タイムチャートどうなるの?
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
fork();
fork();
printf("プロセスid=%d\n", getpid());
}
$ gcc 03_double_fork.c; ./a.out
プロセスid=10044
プロセスid=10046
プロセスid=10045
プロセスid=10047
Q. 親子孫がどうなっているんだ?
親1、子1、孫2?
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
pid_t val; // pid_t型
val = fork();
printf("forkの戻り値=%d\n", val);
printf("プロセスid=%d\n", getpid());
}
$ gcc 04_process_relationship.c; ./a.out
forkの戻り値=10484 # ← 親プロセスのID == 子プロセスでprintf("forkの戻り値=%d\n", val);した結果
プロセスid=10483 # ← 子プロセスのプロセスID == 子プロセスでprintf("forkの戻り値=%d\n", val);した結果
forkの戻り値=0 # ← 親プロセスでの実行結果
プロセスid=10484 # ← 親プロセスでの実行結果。だから、親プロセスのID
printf
が2回動くのはなんでだぜ問題の回答と、fork()の戻り値の意義
1行のfork()
の戻り値で、プロセスが自分自身を親プロセスか子プロセスかを判断できるのには、理由がある!
「親プロセスとして実行させるプログラム」と「子プロセスとして実行させるプログラム」を、1つのプログラム(ソースコード)に記述できるから!
っていうか、2回動いちゃうのは、「親プロセス」も「子プロセス」も、動いちゃっているから!
なんとなくの先入観として、子プロセスは、生成されても何もしない(生まれるだけ)だと思いこんでいた!
(あるいは、子プロセスに対して明示的な命令をしない限りは、子プロセスに何かをやらせるのは無理だと思っていた)
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
pid_t val; // pid_t型
val = fork();
printf("子プロセス\n");
printf("親プロセス\n");
}
# 親プロセスも子プロセスもそれぞれが、printしちゃうわけ
$ gcc 05_hoge.c; ./a.out
子プロセス
親プロセス
子プロセス
親プロセス
fork()
の返り値を見ると世界観が見えてくる
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
pid_t val;
val = fork();
if (val == 0) {
printf("子プロセス(PID=%d)からみたforkの返り値=%d\n", getpid(), val);
} else {
printf("親プロセス(PID=%d)からみたforkの返り値=%d\n", getpid(), val);
}
}
Q1-3. forkしたあと子プロセスだけ動かす
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
pid_t forked_pid;
forked_pid = fork();
if (forked_pid != 0) {
printf("私はPID=%d。こんにちは。私の親プロセスのIDは%d\n", getpid(), getppid());
}
}
Q1-4. forkしたあとで、親プロセスだけ動かす
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
int main() {
pid_t forked_pid;
forked_pid = fork();
if (forked_pid == 0) {
printf("私はPID=%d。ご機嫌いかがですか。私の親プロセスのIDは%d\n", getpid(), getppid());
}
}
wait()
して、子プロセスの終了を待つ Goのチャネルっぽい?(本当か?)
親プロセスが#include "sys/types.h"
#include "sys/wait.h"
#include "stdio.h"
#include "unistd.h"
int main() {
int status;
if (fork() == 0) {
printf("私は子プロセス\n");
sleep(5);
} else {
wait(&status);
printf("親プロセスですけん\n");
}
}
$ gcc 09_wait_親プロセスが子プロセスの終了を待つ.c;./a.out
私は子プロセス
# ここで5秒間待つ
親プロセスですけん
exit(2)
を使うと、親プロセスに整数値の情報をお知らせできる
子プロセスは、#include "sys/types.h"
#include "sys/wait.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main() {
int status;
if (fork() == 0) {
printf("私は子プロセス\n");
sleep(5);
exit(3);
} else {
wait(&status);
printf("親プロセスですけん\n");
printf("子プロセス終了時の値=%04x\n", status);
}
}
$ gcc 10_.c;./a.out
私は子プロセス
親プロセスですけん
子プロセス終了時の値=0300
親プロセスが終了すると、子プロセスも終了する
#include "sys/types.h"
#include "sys/wait.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main() {
int status;
if (fork() == 0) {
printf("私は子プロセス\n");
sleep(5);
printf("sleepおわたよ");
exit(3);
} else {
exit(1); // 親プロセスが終了すると、子プロセスも終了する
wait(&status);
printf("親プロセスですけん\n");
printf("子プロセス終了時の値=%04x\n", status);
}
}
waitしたあとで子プロセスのpidを知る
#include "sys/types.h"
#include "sys/wait.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main() {
int status;
pid_t pid;
if (fork() == 0) {
printf("私は子プロセス\n");
sleep(5);
printf("sleepおわたよ\n\n");
exit(3);
} else {
pid = wait(&status);
printf("親プロセスですけん\n");
printf("子プロセスのPID=%d\n", pid);
printf("子プロセス終了時の値=%04x\n", status);
}
}
$ gcc 12_waitしたあとで子プロセスのpidを知る.c ;./a.out
私は子プロセス
sleepおわたよ
親プロセスですけん
子プロセスのPID=11769
子プロセス終了時の値=0300
status
の値はどうなる?
Q1-5. 子プロセスがexitしないと、親プロセスで#include "sys/types.h"
#include "sys/wait.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main() {
int status;
if (fork() == 0) {
printf("私は子プロセス\n");
sleep(5);
printf("sleepおわたよ\n\n");
} else {
wait(&status);
printf("親プロセスですけん\n");
printf("子プロセス終了時の値=%04x\n", status);
}
}
$ gcc 13_Q1-5.c; ./a.out
私は子プロセス
sleepおわたよ
親プロセスですけん
子プロセス終了時の値=0000
0
、つまり正常終了ってことね。
プロセスの変身 / execファミリー / fork()とexecを組み合わせたときの動き
範囲
- 『Linuxによる並行プログラミング入門』2.1 2.2
exec
- 現プロセスにおいて、指定したプログラムを実行する
-
exec
を呼び出した続きは実行されない - 呼び出したプロセスが、今持っている状態をすべてを忘れて、指定したプログラムだけを実行するマンに変身する
-
exec
が子プロセスを作るわけではない - 参考: プロセス 田浦健次朗
execファミリーの分類
-
exec
に付け足す構造になっている -
v
: Vector -
l
: List -
e
: Environment -
p
: PATH
引数の型 | 環境変数(Environment)を渡せる | PATHを検索する | |
---|---|---|---|
execl | リスト(List) | No | No |
execv | 配列(Vector) | No | No |
execle | リスト(List) | Yes | No |
execve | 配列(Vector) | Yes | No |
execlp | リスト(List) | No | Yes |
execvp | 配列(Vector) | No | Yes |
execl
#include "stdio.h"
#include "unistd.h"
int main() {
printf("cal 1 2020 に変身\n");
// execl の最後の引数はヌルポインタを渡す
execl("/usr/bin/cal", "cal", "1", "2020", (char *) 0);
}
$ gcc 01_execl.c; ./a.out
cal 1 2020 に変身
January 2020
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
execv
#include "stdio.h"
#include "unistd.h"
int main() {
char *argv[4];
argv[0] = "cal";
argv[1] = "1";
argv[2] = "2020";
argv[3] = (char *) 0;
printf("cal 1 2020 に変身\n");
execv("/usr/bin/cal", argv);
}
$ gcc 02_execv.c; ./a.out
cal 1 2020 に変身
January 2020
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
execvp
と execlp
は、コマンドのパスが使える!
#include "stdio.h"
#include "unistd.h"
int main() {
char *argv[4];
argv[0] = "cal";
argv[1] = "1";
argv[2] = "2020";
argv[3] = (char *) 0;
printf("cal 1 2020 に変身\n");
// ユーザーごとに設定されているパスをそのまま使える
// "/usr/bin/cal" じゃなくて "cal" でおk
execvp("cal", argv);
}
exec
を呼び出した続きは、実行されない!!! 変身すると前世の記憶を失う
#include "stdio.h"
#include "unistd.h"
int main() {
printf("変身前: PID=%d\n", getpid());
// lsプロセスに変身
execlp("ls", "");
// lsプロセスに変身しているので、ここからは実行されない!
printf("lsプロセスに変身しているので、ここからは実行されない!\n");
}
$ gcc 04_.c; ./a.out
変身前: PID=6501
01_execl.c 02_execv.c 03_execvp.c 04_.c a.out
exec
ユーザーからのコマンド入力を受け取って- 1回実行しておしまい
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
#include "string.h"
int main() {
int argc; // コマンドのトークン数をカウントする
char cmd[80]; // 80文字のコマンド入力バッファ
char *cmdp; // 作業用ポインタ
char *argv[10]; // トークンを入れる配列
printf("コマンドを入力してください > ");
if (fgets(cmd, 80, stdin) == NULL) {
exit(0);
}
cmd[strlen(cmd) - 1] = '\0'; // 最後の改行文字をNULL文字に変更
cmdp = cmd;
for (argc = 0;; argc++) {
if ((argv[argc] = strtok(cmdp, " ")) == NULL) {
break;
}
cmdp = NULL;
}
printf("ex1 は %sに変身します\n", cmd);
execvp(argv[0], argv);
}
簡単なシェルを再現する
- 親プロセスはコマンドを受け取って処理を続ける
- 無限ループなので、シェルを再現しているってわけ
- コマンドを使うには、その都度、子プロセスをつくる(で消滅する)
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
#include "string.h"
int main() {
int argc; // コマンドのトークン数をカウントする
char cmd[80]; // 80文字のコマンド入力バッファ
char *cmdp; // 作業用ポインタ
char *argv[10]; // トークンを入れる配列
int status;
for (;;) {
printf("コマンドを入力してください > ");
if (fgets(cmd, 80, stdin) == NULL) {
exit(0);
}
cmd[strlen(cmd) - 1] = '\0';
cmdp = cmd;
// コマンドをトークンに分解
for (argc = 0;; argc++) {
printf("%d\n", argc);
if ((argv[argc] = strtok(cmdp, " ")) == NULL) {
break;
}
cmdp = NULL;
}
// 子プロセスの生成をして、子プロセスをコマンドに変身させる
if (fork() == 0) {
execvp(argv[0], argv);
// ちゃんと exec できない場合だけここから下が実行される
// execに失敗したら、exit(1)で子プロセスを消滅させる && 親プロセスにお知らせする
printf("コマンドが見つかりませんでした\n");
exit(1);
} else {
wait(&status);
}
}
}
このシェル風プログラムにおけるのプロセス生成、返信、エラー処理の流れ
- 親プロセスと子プロセスが平行に実行されている
- 親プロセスは、
wait
にしているので待機している - 子プロセスは、
execvp
したり、変身して何かしらコマンドのプロセスになったりしている
- 親プロセスは、
プロセスの変身による動きがわかりやすい例
#include "stdio.h"
#include "unistd.h"
// http://networkprogramming.blog18.fc2.com/blog-entry-44.html
int main() {
printf("Running ps with execlp\n");
execlp("ps", "ps", NULL);
printf("Done.\n");
}
1つは新しいシェルが起動せずにpsが実行されていることです。これはsystem()関数と比較したときの利点であります。(*1)
2つ目はpsで出力される現在稼働中のプロセスの中に自分(exec_ps)自身がいないことです。これは、execlp()関数でpsに変身してしまったためです。
そして、最後の1つは、"Done."が出力されずにプログラムが終了していることです。これは、プログラムの7行目で"Done.\n"をprintf()する前の行でexeclp()して、別のプログラム(プロセス)psに変身してしまっているためです。変身後はプロセスが完全に置き換わってしまい、もとに戻ることがないため、execlp()より下の行は実行されません。
# Ubuntuだとこう
root@73962b235538:/src# gcc chap02/07_exec_ps.c; ./a.out
Running ps whit execlp
PID TTY TIME CMD
1 pts/0 00:00:00 bash
33 pts/0 00:00:00 ps
# iTerm(macOS)でzshだとこう
$ gcc 07_exec_ps.c;./a.out
Running ps whit execlp
PID TTY TIME CMD
5363 ttys000 0:00.59 /bin/zsh --login -i
7124 ttys001 0:00.03 /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp mohira
7126 ttys001 0:00.12 -zsh
7414 ttys002 0:00.19 /bin/zsh --login -i
ちなみにDockerの準備はコレ
FROM ubuntu:20.04
WORKDIR /src
RUN apt update && \
apt install -y build-essential && \
apt install -y man && \
yes | unminimize
RUN apt install -y less vim strace
version: '3'
services:
ubuntu:
build:
context: .
dockerfile: Dockerfile
volumes:
- type: bind
source: .
target: /src
$ docker-compose up --build
$ docker-compose run ubuntu
ファイル入出力
- 3章 3.3まで
- 4章
memo
- ファイルディスクリプタ
- プロセスレベル / カーネルレベル / IO装置レベル
- forkしたときのfd表の動き
dup
- リダイレクト
- パイプ
p.26 プロセスが生成されると、カーネル内には、そのプロセスに対応して、1つのfd表が生成される
- どの「レベル」の話か? を意識するといい感じっぽい
- C言語での実装は、どのレベルかを区別しやすい
- 高級言語だとファイルディスクリプタを意識しにくいとおもう(そして、それがいい)
p.27 プロセスが終了すると自動的にfd表も消滅する!
- だから、明示的な
close()
をしないってさ。 - したほうがいいと思うけども!
p.29 親プロセスでforkした場合の動き、ややこしいけど、意外と分かる
-
fork()
すると、fd表もコピーされるらしい - 親プロセスと子プロセスは交互に実行される
-
oldfile
を参照するファイル表のエントリーに、ファイルの読み出し位置を記憶するポインタが入っていることが推察される(p.29)- もし、独立しているなら、
newfile1
とnewfile2
の結果がおなじになる図
- もし、独立しているなら、
code
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "unistd.h"
#include "stdlib.h"
#include "stdio.h"
// p.27
// 親プロセスが fork() で子プロセスを生成する。
// この2つの子プロセスが同じファイル(oldfile)から文字を読み、
// それぞれが別のファイル(newfile1, newfile2)に出力する
int main() {
int fd1, fd2;
int status;
int count;
char buf[20]; // 20文字ずつ読んでいくための文字配列
if ((fd1 = open("oldfile", O_RDONLY)) == -1) {
perror("can't open oldfile");
exit(1);
}
if (fork() == 0) {
// 子プロセスの処理
if ((fd2 = creat("newfile1", 0664)) == -1) {
perror("can't create newfile1");
exit(1);
}
while ((count = read(fd1, buf, 20)) != 0) {
write(fd2, buf, count);
write(fd2, "\n", 1); // 改行文字
write(1, "child\n", 6);
sleep(1);
}
exit(0); // 子プロセスは勝手に終了するけど、わかりやすくするために明示的にexitしてみた
} else {
// ここからは親プロセスの処理だよ
if ((fd2 = creat("newfile2", 0664)) == -1) {
perror("can't create newfile2");
exit(1);
}
while ((count = read(fd1, buf, 20)) != 0) {
write(fd2, buf, count);
write(fd2, "\n", 1);
write(1, "parent\n", 7);
sleep(1);
}
wait(&status); // 子プロセスの終了を待つ
}
}
実行結果
$ gcc 04_copy4.c;./a.out
parent
child
child
parent
child
parent
child
parent
cachild
parent
# newfile1 と newfile2 の結果が違う!
$ cat newfile1
operating system th
at was designed to p
uter users a free or
ting system
comparab
d usually more expen
$ cat newfile2
Linux is a Unix-like
rovide
personal comp
very low-cost opera
le to traditional an
sive unix Systems.
図解
dup
システムコールで、ファイルディスクリプタをコピーする
p.37 #include "stdio.h"
#include "unistd.h"
// dup()システムコールで、ファイルディスクリプタをコピーする
int main() {
int fd;
// このfd自体の値は 3 であって 1(標準出力) ではない
// でも、1を複製したので write(fd,...)は標準出力に書き込まれる
fd = dup(1);
printf("複製したfd = %d\n", fd);
write(fd, "test message\n", 13);
}