Open27

スレッドを理解したい

mohiramohira

README

単元

memo

  • プロセス
    • 『Linuxによる並行プログラミング入門』1章
    • 『Linuxによる並行プログラミング入門』2章
  • スレッド
    • 『Linuxによる並行プログラミング入門』7章
mohiramohira

forkの修行 / プロセスの生成 / プログラムの実行

  • 『Linuxによる並行プログラミング入門』1章
mohiramohira

「カーネル」が「このユーザープロセス」に「プロセスID」を与えた(それが5714だった)

  • プロセスIDをみるだけ
#include "unistd.h"
#include "stdio.h"

int main() {
    printf("プロセスid=%d\n", getpid());
}
$ gcc 01_getpid.c;./a.out
プロセスid=5714
mohiramohira

プロセスを生成するシステムコール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

Q. なんで2行分出力されるの? (printfは1回しか呼んでないのに)

そういう仕様だから?

子プロセス は 親プロセス の コピー

で、親子関係がある。

"親子"という表現を使っているけど、人間や動物とは違う。アメーバとかゾウリムシとかの増え方に近いかも。

  • a.outは、親プロセス
  • 「新しいプロセス」は、子プロセス
mohiramohira

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?

mohiramohira
#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
mohiramohira

1行のprintfが2回動くのはなんでだぜ問題の回答と、fork()の戻り値の意義

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);
    }
}
mohiramohira

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());
    }
}
mohiramohira

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());
    }
}
mohiramohira

親プロセスが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秒間待つ
親プロセスですけん

mohiramohira

子プロセスは、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
mohiramohira

親プロセスが終了すると、子プロセスも終了する

#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);
    }
}
mohiramohira

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
mohiramohira

Q1-5. 子プロセスがexitしないと、親プロセスでstatusの値はどうなる?

#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、つまり正常終了ってことね。

mohiramohira

プロセスの変身 / 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
mohiramohira

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     
mohiramohira

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    
mohiramohira

execvpexeclp は、コマンドのパスが使える!

#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);
}
mohiramohira

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
mohiramohira

ユーザーからのコマンド入力を受け取って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);

}
mohiramohira

簡単なシェルを再現する

  • 親プロセスはコマンドを受け取って処理を続ける
    • 無限ループなので、シェルを再現しているってわけ
  • コマンドを使うには、その都度、子プロセスをつくる(で消滅する)
#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したり、変身して何かしらコマンドのプロセスになったりしている

mohiramohira

プロセスの変身による動きがわかりやすい例

#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                           
mohiramohira

ファイル入出力

  • 3章 3.3まで
  • 4章

memo

  • ファイルディスクリプタ
  • プロセスレベル / カーネルレベル / IO装置レベル
  • forkしたときのfd表の動き
  • dup
  • リダイレクト
  • パイプ
mohiramohira

p.26 プロセスが生成されると、カーネル内には、そのプロセスに対応して、1つのfd表が生成される

  • どの「レベル」の話か? を意識するといい感じっぽい
  • C言語での実装は、どのレベルかを区別しやすい
    • 高級言語だとファイルディスクリプタを意識しにくいとおもう(そして、それがいい)

p.27 プロセスが終了すると自動的にfd表も消滅する!

  • だから、明示的なclose()をしないってさ。
  • したほうがいいと思うけども!
mohiramohira

p.29 親プロセスでforkした場合の動き、ややこしいけど、意外と分かる

  • fork()すると、fd表もコピーされるらしい
  • 親プロセスと子プロセスは交互に実行される
  • oldfileを参照するファイル表のエントリーに、ファイルの読み出し位置を記憶するポインタが入っていることが推察される(p.29)
    • もし、独立しているなら、newfile1newfile2の結果がおなじになる図

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.

図解

mohiramohira

p.37 dupシステムコールで、ファイルディスクリプタをコピーする

#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);
}

図解