Upgrade to Pro — share decks privately, control downloads, hide ads and more …

パイプラインによるプロセス間通信を理解する

 パイプラインによるプロセス間通信を理解する

社内向け勉強会に用意した資料「パイプラインによるプロセス間通信を理解する」です。

Kotaro Kamashima

October 30, 2022
Tweet

More Decks by Kotaro Kamashima

Other Decks in Programming

Transcript

  1. 本日のアジェンダ
 1. ゴール
 2. 前提知識
 a. ファイルディスクリプタとは?
 b. 標準入出力と標準エラー出力とは? 


    c. パイプライン実行時のデータの流れを概略 
 d. forkを使って子プロセスを作る
 3. パイプラインを使ったプロセス間通信
 a. 子プロセスの処理
 b. 親プロセスの処理

  2. ファイルディスクリプタ 
 • プログラムがファイル等の外部のリソースにアクセスし たり、アクセスされたりする際に使われる抽象的な概 念
 • UNIX/Linux系では、このファイルディスクリプタがファイル だけでなく、ソケットやパイプラインにも使われている 


    • この抽象的なファイルディスクリプタを使って、ファイルの 中身を見たり、ファイルに書き込んだりする 
 • ファイルディスクリプタは小さい整数値から割り当てられる 
 • OS標準規格では、0~2がすでに使用されているため、通 常は3から順番に割り当てられる 
 • プログラム内で使用できるファイルディスクリプタの上限が 決まっている
 #define FILE_NAME "infile.txt" int main(void) { int fd; fd = open(FILE_NAME, O_RDONLY); // 読み込み専用でファイルを開く if (fd == -1) // ファイルを開くのに失敗した場合 { // エラー対応 } printf("File Descriptor = %d\n", fd); return (0); } 
 bash-3.2$ ls Makefile ex00 infile.txt main.c bash-3.2$ ./ex00 File Descriptor = 3 
 サンプルプログラムはこちら 

  3. ファイルディスクリプタ 
 • ulimit -n を使って、使用できるファイルディスクリプタ の上限を確認できる
 • 第2引数に整数値を渡すことで、現在のプロセス内の ファイルディスクリプタ上限値を変更できる


    • 左の例は、ファイルディスクリプタの上限値を3に変更 したため、プログラム内でファイルを開けないエラーが 発生している
 bash-3.2$ ulimit -n 256 bash-3.2$ ulimit -n 3 bash-3.2$ ./ex00 open: Too many open files 

  4. 標準入出力と標準エラー出力 
 #define MAX_BYTES 100 // 最大読み込みバイト数の定義 int main(void) //

    ① プログラムが起動する { ssize_t bytes; // 読み込み成功した分のバイト数を格納する変数 char buffer[MAX_BYTES + 1]; // 読み込んだデータを格納する静的配列 while (1) // ② 無限ループ状態にする { // ③,④ 標準入力からデータを読み取り、バッファに格納する。 bytes = read(STDIN_FILENO, &buffer, MAX_BYTES); if (bytes == -1) // 読み込み失敗時の対応 { // エラーメッセージとともに標準エラー出力する } buffer[bytes] = '\0'; // 終端文字列としてヌル文字を代入する write(STDOUT_FILENO, buffer, bytes); // ⑤ バッファに格納されたデータを標準出力する bzero(buffer, bytes); // バッファの中を初期化する } return (0); }
 cat を使った標準入出力と標準エラー出力例
 1. プログラムが起動する 2. 無限ループ状態にする 3. 標準入力を待つ 4. 標準入力データを読み込む 5. 読み込んだデータを標準出力する 6. 2 に戻る サンプルプログラムはこちら 

  5. パイプライン実行時のデータの流れを概略 
 $ cat access.log | grep “TARGET” ①
 ②


    ③
 ④
 ⑤
 1. ファイルを開いて、中身を読み込む
 2. 読み込んだ内容をパイプラインを通じて、次のプログラム(プロセス)に渡す
 3. パイプラインを通じて、流れてきたデータを読み込む
 4. 渡された引数を検索文字列として、読み込んだデータを抽出する
 5. 抽出したデータを標準出力する

  6. forkを使ったプロセス生成 
 プロセスとは?
 
 • OS上で動いているプログラムまたはタスクのこと 
 ◦ 皆さんが日常で使用しているChromeやGoogleChatアプリ、Gatheアプリも独立したプロセスとして動いている 


    ◦ Linuxコマンドも一つひとつが独立したプロセスで実行されている 
 ◦ 現在実行されているプロセスは、psコマンドやtopコマンドで確認できる 
 • 各プロセスは独立して動いている 
 ◦ 各プロセスは独立したメモリの中で動いている 
 ◦ 独立して動いているので、一つのプロセスに致命的なバグが発生して緊急停止しても、他のプロセスには害を与えない 
 • 実行されるプログラムを親として、forkを実行すると子プロセスが作られる 
 ◦ 子プロセスは親プロセスの複製で作られる(fork) 
 ◦ 親プロセスは子プロセスの状態を監視することができる 
 • プロセス間でのデータのやり取りは大変だが、やる方法は何個かある 
 ◦ シグナル通信
 ◦ ソケット通信
 ◦ セマフォ(プロセス間の排他制御) 
 ◦ パイプライン(親子プロセス間で行う通信) → 今回のテーマ 

  7. forkを使ったプロセス生成 
 int main(void) { pid_t pid; int status; printf("1.

    Execute fork.\n"); pid = fork(); if (pid == -1) { // エラー処理 } else if (pid == 0) // 子プロセスの処理 { printf("2. Executing child process....\n"); sleep(2); printf("3. Finishing child process!\n"); exit(0); // 子プロセスを終了し、親に SIGCHILDシグナルが送信される } else // 親プロセスの処理 { wait(&status); // 子プロセスが終了するまで呼び出し側をブロックする printf("4. Executing parent process....\n"); } printf("5. Finishing program.\n"); return (0); } 
 サンプルプログラムはこちら 

  8. パイプラインを使ったプロセス間通信 
 int main(void) { int pipe_fd[2]; // 生成されたパイプラインの読み込み口と書き込み口のファイルディスクリプタが 格納される静的配列

    pid_t pid; int status; pipe(pipe_fd); // パイプラインを作る return (0); } プロセス 0 1 2 pipe_fd[0] 3 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し
  9. パイプラインを使ったプロセス間通信 
 # define PIPE_READ_FD 0 # define PIPE_WRITE_FD 1

    # define MAX_BYTES 100 int main(void) { int pipe_fd[2]; char buffer[MAX_BYTES + 1]; ssize_t bytes; pipe(pipe_fd); // パイプラインを生成する write(pipe_fd[PIPE_WRITE_FD], "This is test message.", 21); // パイプライン書き込み口に出力する bytes = read(pipe_fd[PIPE_READ_FD], &buffer, MAX_BYTES); // パイプライン書き込み口から読み込む if (bytes == -1) // 読み込み失敗時 { // エラー対応 } buffer[bytes] = '\0'; write(STDOUT_FILENO, buffer, bytes); // 読み込んだデータを標準出力する return (0); } 
 プロセス 0 1 2 pipe_fd[0] 3 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し サンプルプログラムはこちら 

  10. パイプラインを使ったプロセス間通信 
 子プロセス 0 1 2 3 4 標準入力 標準出力

    標準エラー pipe_fd[0] pipe_fd[1] 親プロセス 0 1 2 3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し 書き出し 読み出し パイプライン pipeしてから、fork実行後の様子。この状態では、ちゃんとデータが期待通りに流れない。 

  11. パイプラインを使ったプロセス間通信 
 子プロセス 0 1 2 3 4 標準入力 標準出力

    標準エラー pipe_fd[0] pipe_fd[1] 親プロセス 0 1 2 3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し 書き出し 読み出し パイプライン パイプライン内で出入り口を操作することで、データの通り道を決めてあげる 

  12. パイプラインを使ったプロセス間通信 
 int main(void) { int pipe_fd[2]; pid_t pid; int

    status; pipe(pipe_fd); pid = fork(); if (pid == -1) // エラー対応 { // エラー対応 } else if (pid == 0) // 子プロセス { start_child_process(pipe_fd); } else // 親プロセス { wait(&status); // 子プロセスを待つ start_parent_process(pipe_fd); } return (0); }
 • main 関数の大まかな実装 
 • forkして子プロセスを作る 
 • 子プロセス内で前半の処理 
 (cat access.log)を行う 
 • 親プロセス内で後半の処理 
 (grep “TARGET”)を行う 
 サンプルプログラムはこちら 

  13. パイプラインを使ったプロセス間通信 
 int main(void) { int pipe_fd[2]; pid_t pid; int

    status; pipe(pipe_fd); pid = fork(); if (pid == -1) // エラー対応 { // エラー対応 } else if (pid == 0) // 子プロセス { start_child_process(pipe_fd); } else // 親プロセス { wait(&status); // 子プロセスを待つ start_parent_process(pipe_fd); } return (0); }
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { perror("open"); exit(1); } dup2(file_fd, STDIN_FILENO) // 標準入力を access.logファイルFDのコピーとして作 成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き 込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親に SIGCHILDが送信される }
  14. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 
 子プロセス 0 1 2 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し 3 pipe_fd[0]
  15. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 子プロセス 0 1 2 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し file_fd 5 pipe_fd[0] 3
  16. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 子プロセス 0 → access.log 1 2 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し file_fd 5 3 pipe_fd[0]
  17. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 子プロセス 0 → access.log 1 2 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し pipe_fd[0] 3 file_fd 5
  18. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 子プロセス 0 → access.log 1 → pipe_fd[1] 2 4 pipe_fd[1] 標準エラー 標準出力 標準入力 書き込み 読み出し 3 pipe_fd[0] file_fd 5
  19. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 子プロセス 0 → access.log 1 → pipe_fd[1] 2 標準エラー 標準出力 標準入力 書き込み 読み出し pipe_fd[0] 3 pipe_fd[1] 4 5 file_fd
  20. パイプラインを使ったプロセス間通信 
 static void start_child_process(int *pipe_fd) { int file_fd; close(pipe_fd[PIPE_READ_FD]);

    // パイプライン読み込み口を閉じる file_fd = open("access.log", O_RDONLY); // access.logファイルを開く if (file_fd == -1) { // エラー処理 } dup2(file_fd, STDIN_FILENO) // 標準入力をaccess.logファイルFDのコピーとして作成する close(file_fd); // access.logファイルFDを閉じる dup2(pipe_fd[PIPE_WRITE_FD], STDOUT_FILENO) // 標準出力をパイプライン書き込み口のコピーとして作成する close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親にSIGCHILDが送信される } 
 extern char **environ; // 設定されている環境変数 static void execute_child_command() { char *argv[2]; // char型2次元配列 argv[0] = "/bin/cat"; // 実行するコマンドの絶対パス argv[1] = NULL; // 2次元配列の一番後ろに NULLを代入する if (execve(argv[0], argv, environ) == -1) // プログラムを実行する { // エラー処理 } } #include <unistd.h> int execve(const char * filename , char *const argv [], char *const envp []); filename: プログラムファイルの絶対パス argv: プログラムに渡す引数の 2次元配列(NULLポインタで終端) envp: プログラムに渡す環境変数の 2次元配列(NULLポインタで終端) 

  21. パイプラインを使ったプロセス間通信 
 子プロセス 0 → access.log 1 → pipe_fd[1] 2

    3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 親プロセス 0 1 2 3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し 書き出し 読み出し パイプライン 子プロセスの処理実行時 
 file_fd 5
  22. パイプラインを使ったプロセス間通信 
 int main(void) { int pipe_fd[2]; pid_t pid; int

    status; pipe(pipe_fd); pid = fork(); if (pid == -1) // エラー対応 { // エラー対応 } else if (pid == 0) // 子プロセス { start_child_process(pipe_fd); } else // 親プロセス { wait(&status); // 子プロセスを待つ start_parent_process(pipe_fd); } return (0); }
 static void start_parent_process(int *pipe_fd) { close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる dup2(pipe_fd[PIPE_READ_FD], STDIN_FILENO) // 標準入力をパイプライン読み込み口のコピーとして作成する close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる execute_parent_command(); // 親プロセスの処理を実行する }
  23. パイプラインを使ったプロセス間通信 
 int main(void) { int pipe_fd[2]; pid_t pid; int

    status; pipe(pipe_fd); pid = fork(); if (pid == -1) // エラー対応 { // エラー対応 } else if (pid == 0) // 子プロセス { start_child_process(pipe_fd); } else // 親プロセス { wait(&status); // 子プロセスを待つ start_parent_process(pipe_fd); } return (0); }
 static void start_child_process(int *pipe_fd) { …省略 execute_child_command(); // 子プロセスの処理を実行する exit(0); // 子プロセスを終了し、親に SIGCHILDが送信される } 子プロセス 親プロセス 終了
 SIGCHILD
 waitの
 ブロッキング解除 

  24. テンプレート
 親プロセス 0 1 2 3 4 標準入力 標準出力 標準エラー

    pipe_fd[0] pipe_fd[1] 書き出し 読み出し static void start_parent_process(int *pipe_fd) { close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる dup2(pipe_fd[PIPE_READ_FD], STDIN_FILENO) // 標準入力をパイプライン読み込み口のコピーとして作成する close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる execute_parent_command(); // 親プロセスの処理を実行する } 

  25. テンプレート
 親プロセス 0 → pipe_fd[0] 1 2 3 4 標準入力

    標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し static void start_parent_process(int *pipe_fd) { close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる dup2(pipe_fd[PIPE_READ_FD], STDIN_FILENO) // 標準入力をパイプライン読み込み口のコピーとして作成する close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる execute_parent_command(); // 親プロセスの処理を実行する }
  26. テンプレート
 親プロセス 0 → pipe_fd[0] 1 2 3 4 標準入力

    標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し static void start_parent_process(int *pipe_fd) { close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる dup2(pipe_fd[PIPE_READ_FD], STDIN_FILENO) // 標準入力をパイプライン読み込み口のコピーとして作成する close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる execute_parent_command(); // 親プロセスの処理を実行する }
  27. パイプラインを使ったプロセス間通信 
 static void start_parent_process(int *pipe_fd) { close(pipe_fd[PIPE_WRITE_FD]); // パイプライン書き込み口を閉じる

    dup2(pipe_fd[PIPE_READ_FD], STDIN_FILENO) // 標準入力をパイプライン読み込み口のコピーとして作成する close(pipe_fd[PIPE_READ_FD]); // パイプライン読み込み口を閉じる execute_parent_command(); // 親プロセスの処理を実行する }
 static void execute_parent_command() { char *argv[3]; // char型2次元配列 argv[0] = "/usr/bin/grep"; // 実行するコマンドの絶対パス argv[1] = "POST"; // grep で使用する引数 argv[2] = NULL; // 2次元配列の一番後ろに NULLを代入する if (execve(argv[0], argv, NULL) == -1) // プログラムを実行する { // エラー処理 } }
 #include <unistd.h> int execve(const char * filename , char *const argv [], char *const envp []); filename: プログラムファイルの絶対パス argv: プログラムに渡す引数の 2次元配列(NULLポインタで終端) envp: プログラムに渡す環境変数の 2次元配列(NULLポインタで終端) 

  28. パイプラインを使ったプロセス間通信 
 子プロセス 0 → access.log 1 → pipe_fd[1] 2

    3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 親プロセス 0 → pipe_fd[0] 1 2 3 4 標準入力 標準出力 標準エラー pipe_fd[0] pipe_fd[1] 書き出し 読み出し 書き出し 読み出し パイプライン 親プロセスの処理実行時 
 file_fd 5
  29. パイプラインを使ったプロセス間通信 
 bash-3.2$ make gcc main.c -o ex04 bash-3.2$ ls

    Makefile README.md access.log ex04 main.c bash-3.2$ ./ex04 5.78.198.52 - - [22/Jan/2019:03:56:26 +0330] "POST /m/updateVariation?__amp_source_origin=https%3A%2F%2Fwww.zanbil.ir HTTP/1.1" 200 171 "https://www.zanbil.ir/m/product/33978/64784/%DA%AF%D9%88%D8%B4%DB%8C-%D9%85%D9%88%D8%A8%D8%A7%DB%8C%D9%84-% D8%B3%D8%A7%D9%85%D8%B3%D9%88%D9%86%DA%AF-%D9%85%D8%AF%D9%84-Galaxy-A9-%282018%29-Dual-128GB-%28SM-A9 20%29" "Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G950F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/8.2 Chrome/63.0.3239.111 Mobile Safari/537.36" "-" 5.78.198.52 - - [22/Jan/2019:03:56:28 +0330] "POST /m/updateVariation?__amp_source_origin=https%3A%2F%2Fwww.zanbil.ir HTTP/1.1" 200 171 "https://www.zanbil.ir/m/product/33978/64784/%DA%AF%D9%88%D8%B4%DB%8C-%D9%85%D9%88%D8%A8%D8%A7%DB%8C%D9%84-% D8%B3%D8%A7%D9%85%D8%B3%D9%88%D9%86%DA%AF-%D9%85%D8%AF%D9%84-Galaxy-A9-%282018%29-Dual-128GB-%28SM-A9 20%29" "Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G950F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/8.2 Chrome/63.0.3239.111 Mobile Safari/537.36" "-" 
 プログラム実行時の様子