意識低い系会社員

意識低い系会社員の日常

【スポンサーリンク】

筑波大の講義「システムプログラミング」第4回メモ

【スポンサーリンク】

こんにちは。

 

プログラミングの勉強のために筑波大の講義「システムプログラミング」をやってみようと思います。

このエントリは完全な個人のメモです。

 

筑波大の講義ページはこちら

 

お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。

 

正直、講義ページがすでにかなり簡潔にまとまっているのでほぼコピペになってしまう気もしています。

 

第3回の講義内容は以下。

amistad06-a.hatenablog.com

  

プロセスの概念と機能

プログラムとプロセス

プロセスは実行中のプログラムのことを指す。

プログラムの実行ファイルには

・デバッグ情報

・データ領域(初期値あり)

・機械語、テキスト領域

・ヘッダ(プログラム情報)

が含まれる

 

プロセスには上記の内容に加え、

・データ領域(初期値なし)

・スタック領域

が含まれる。

 

実行ファイルに初期値なしのデータ領域が含まれないのは、実行するときにサイズだけわかればいいから。

スタック領域には実行の履歴や状態(関数呼び出しやローカル変数)が記憶される。

これらの領域はプログラムの実行中に必要に応じて書き換えられる。

実行ファイルにしかない領域のものは通常書き換わらない。

 

プロセスの機能

・資源割り当て

プロセッサの時間、メモリ、ファイル、IO機器などのデバイスを資源という。

プログラムが実行されるとOSカーネルにより資源が割り当てられる。

ファイルのアクセス権などはユーザやグループに対して与えられるが、資源はプロセスに与えられる。

基本的にOSカーネルは資源を各プロセスに平等に割り当てる。

※ファイルやデバイスにアクセスするための記述子は先着順

 

・資源の保護

あるプロセスに割り当てられた資源に対して、他のプロセスは許可なくアクセスすることができない。

これにより、1つのプロセスが暴走しても他のプロセスに影響するのを防ぐことができる。

 

プロセスのメモリマップ

プロセスのメモリマップのイメージは以下。

f:id:amistad06-k:20190325202525p:plain

メモリ領域のイメージ

※このイメージ図はここ筑波大講義(システムプログラミング)のページから拝借。

それぞれの領域についての説明は以下。

テキスト領域

→機械語命令。読み出し専用。

データ領域(初期値あり)

→以外の初期値を持つグローバル変数と静的ローカル変数。

データ領域(初期値なし)

→初期値が0のグローバル変数や静的ローカル変数など。プログラム実行時に生成されて0初期化される。通称BSS領域。

ヒープ領域

→プロセス実行中に動的に確保されるデータ領域。

共有ライブラリ

→共有ライブラリ。読み出し専用。

スタック領域

→ローカル変数や引数、関数呼び出し時の戻り番地など。

引数、環境変数

→プログラムに渡される引数と環境変数はスタック領域の最上位部に保持される。

 

以下のプログラムで各領域のアドレスを見ることができる。

 

#include <stdio.h>

// 環境変数が格納されている文字列へのポインタ
extern char **environ;

// 初期値なしのグローバル変数
int data0;
// 初期値ありのグローバル変数
int data1 = 10;

int main(int argc, char *argv[])
{
        // 初期値なしのローカル変数
        char c;

        // それぞれのアドレスを表示
        printf("environ:\t%p\n", environ);
        printf("argv:\t\t%p\n", argv);
        printf("stack:\t\t%p\n", &c);
    
        printf("bss:\t\t%p\n", &data0);
        printf("data:\t\t%p\n", &data1);

        return 0;
}

 

プログラムの説明はプログラム中のコメントを参照。

 

プロセスの属性

PIDやGIDの説明。

知らなかった、または曖昧だったものだけ抜粋。

PPID:そのプロセスを生成したプロセス(親プロセス)のID

PGID:プロセスグループID

制御端末:シグナルを受け取る端末

ルートディレクトリ:ルートディレクトリはプロセスごとに決めることができる。アクセスできるファイルを制限したいときに使用する

優先順位:プロセスの実行優先順位

シグナル制御情報:シグナルに対応してどの処理が行われるかの情報

利用可能資源量:プロセスが使える資源の上限

実行統計情報:リソース使用量などの統計

 

環境変数

環境変数はプロセスに文字列で渡され、その場所は外部変数environで取得できる。

環境変数の構造はargvと同じで、environは配列へのポインタとなっている。

環境変数を扱うには以下のライブラリ関数が便利。

getenv

putenv

setenv

unsetenv

 

プロセスを操作するコマンド

ps, kill, niceなどの説明。

 

プロセスの操作(生成、実行、終了)

プロセスの生成とプログラムの実行

コマンド(プログラム)を実行するときの大まかな流れは以下。

  1. シェルを実行するプロセスがコマンドを実行するプロセスを生成
  2. 生成されたプロセスでコマンドを実行
  3. シェルはコマンドのプロセスが終了するのを待つ
  4. コマンドが終了すると、シェルの実行が再開

このそれぞれのステップに以下のシステムコールが対応している

  1. fork
  2. execve
  3. wait
  4. exit

この一連の流れを図で書くと以下のようになる。

f:id:amistad06-k:20190326205231p:plain

コマンド(プログラム)の実行イメージ図

※このイメージ図はここ筑波大講義(システムプログラミング)のページから拝借。

UNIXではプロセスの生成はfork、プログラムの実行はexecveでしかできない(cloneというシステムコールもあるがこれは邪道)。

 

以下のプログラムではプロセスの生成(fork)と子プロセスの終了待ち(wait)を行っている。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
    
void do_child(void)
{
        printf("This is child(pid=%d).\n", getpid());
        exit(2);
}
    
int main(void)
{
        int child, status;
        
        // プロセスをfork()で複製
        // 子プロセスには0が、
        // 親プロセスには子プロセスのプロセスIDが返る
        // 子プロセスの処理はforkから戻ってきたところから始まる
        if ((child = fork()) < 0) {
                // 複製に失敗したらプログラムを終了する
                perror("fork");
                exit(1);
        }

        // 親プロセスと子プロセスの判定
        // 子プロセスではdo_child()を実行
        // 親プロセスでは子プロセスの完了を待つ
        if (child == 0) {
                do_child();
        } else {
                if (wait(&status) < 0) {
                        perror("wait");
                        exit(1);
                }
                // WEXITSTATUSマクロで子プロセスの終了ステータスが取得できる
                printf("The child (pid=%d) exited with status(%d).\n",
                       child, WEXITSTATUS(status));
        }

        return 0;
}

 

 以下のプログラムはexecveのサンプル。

プログラムの中で別のプログラムをロードし実行する。

 

#include <unistd.h>

extern char **environ;

int main(void)
{
        char *argv[2];

        // execvに渡す引数の準備
        // execvはコマンドをサーチしてくれないので、
        // 絶対パスで実行するプログラムを指定する
        argv[0] = "/bin/ls";
        argv[1] = NULL;

        // ここで実行中のプロセスにlsコマンドのプログラムがロードされ、
        // lsのmain関数から実行される
        // execvにはそのプログラムを実行する際の環境変数も一緒に渡す必要がある
        execve(argv[0], argv, environ);

        return 0;
}

 

以下のように書いても実行結果は同じ。

#include <unistd.h>

extern char **environ;

int main(void)
{
        char *argv[2];

        // execveに渡す引数の準備
        // argv[0]は使用されない
        argv[0] = "ls";
        argv[1] = NULL;

        // 第一引数で直接lsコマンドの絶対パスを渡す
        execve("/bin/ls", argv, environ);

        return 1;
}

 

引数を渡したい場合はargv[1]以降に指定する。

このプログラムはルートディレクトリを表示する。

 

#include <unistd.h>

extern char **environ;

int main(void)
{
        char *argv[3];

        // execveに渡す引数の準備
        // コマンドのフルパス
        argv[0] = "/bin/ls";
        // コマンドに渡す引数
        argv[1] = "/";
        argv[2] = NULL;

        // コマンドの実行
        execve(argv[0], argv, environ);

        return 1;
}

 

以下のプログラムはforkとexecveを組み合わせたもの。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

extern char **environ;

void do_child(void)
{
        char *argv[3];

        printf("This is child (pid=%d).\n", getpid());

        // execveに渡す引数の準備
        argv[0] = "/bin/ls";
        argv[1] = "/";
        argv[2] = NULL;

        execve(argv[0], argv, environ);
}

int main(void)
{
        int child, status;

        // プロセスを複製
        if ((child = fork()) < 0) {
                perror("fork");
                exit(1);
        }

        // 親プロセスと子プロセスの判定
        if (child == 0) {
                // 子プロセス
                do_child();
        } else {
                // 親プロセス
                // 子プロセスの終了を待つ
                if (wait(&status) < 0) {
                        perror("wait");
                        exit(1);
                }
                // 子プロセスの終了ステータスを取得して表示
                printf("The child (pid=%d) exited with status(%d).\n",
                       child, WEXITSTATUS(status));
        }

        return 100;
}

 

execveを使用する際、execveが失敗する可能性も考慮する。

失敗を考慮したプログラムが以下。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

extern char **environ;

void do_child(void)
{
        char *argv[2];

        printf("This is child (pid=%d).\n", getpid());

        // execveを失敗させるために
        // 存在しないコマンドを指定してみる
        argv[0] = "/bin/xxxxx";
        argv[1] = NULL;

        // execveの実行と結果判定
        // execveは失敗すると負の値を返す
        if (execve(argv[0], argv, environ) < 0) {
                perror("execve");
                exit(1);
        }
}

int main(void)
{
        int child, status;

        // プロセスを複製
        if ((child = fork()) < 0) {
                perror("fork");
                exit(1);
        }

        // 子プロセスと親プロセスの判定
        if (child == 0) {
                do_child();
        } else {
                // 子プロセスの終了を待つ
                if (wait(&status) < 0) {
                        perror("wait");
                        exit(1);
                }
                // 子プロセスの終了ステータスを取得して表示
                printf("The child (pid=%d) exited with status(%d).\n",
                       child, WEXITSTATUS(status));
        }
        return 2;
}

 

豆知識①

実はexit()はシステムコールではなくライブラリ関数。

システムコールは_exit()。

exitがライブラリ関数になっている理由はatexitのmanでわかる。

 

豆知識②

特定のプロセスの終了を待ちたいときはwaitpidを使用する。

 

プログラム実行のためのライブラリ関数

execveは引数の設定が面倒なのと、プログラムのサーチをしてくれないので使い勝手が良くない。

以下のライブラリ関数を使う方が楽。

execl

execlp

execle

execv

execvp

それぞれの使い方はman参照。

 

リダイレクション、パイプ

リダイレクション

標準入出力をファイルに置き換えること。

出力先はファイルディスクリプタによって指定される。

ファイルディスクリプタの0, 1, 2はそれぞれ標準入力、標準出力、標準エラー出力となっている。

ファイルディスクリプタによる出力先の変更にはシステムコールdup()またはdup2()を使用する。

これらはファイルディスクリプタを複製するシステムコールで、複製した後は古いファイルディスクリプタを必要に応じてcloseする。

 

以下のプログラムはwcコマンドの出力先を標準出力から変更するプログラム。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
        int file_fd;

        // 引数で出力先のファイル名が指定されていない場合
        // プログラムを終了する
        if (argc != 2) {
                fprintf(stderr, "Usage: %s file_name\n", argv[0]);
                exit(1);
        }

        // 出力先のファイルを開く
        file_fd = open(argv[1], O_RDONLY);
        if (file_fd < 0) {
                perror("open");
                exit(1);
        }

        // 標準出力を閉じる
        close(0);
        // 開いたファイルディスクリプタを複製する
        // 複製するディスクリプタを標準入力として扱う
        if (dup2(file_fd, 0) < 0) {
                perror("dup2");
                close(file_fd);
                exit(1);
        }
        // ファイルディスクリプタを閉じる
        close(file_fd);

        // wcコマンドを実行
        execlp("wc", "wc", NULL);

        return 1;
}

 

パイプ

Linuxではコマンドの結果をそのまま別のコマンドの入力として使える。

この機能のことをパイプという。

パイプを行うためにはシステムコールはpipe()を使用する。

 

パイプの仕組みは以下のような流れで実現される。

 

ãã¤ãã®ä½æ

※画像は筑波大講義のページから拝借

 

以下のプログラムはパイプを用いて子プロセスから親プロセスに文字列を送るもの。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>

int pipe_fd[2];

void do_child(void)
{
        // 親プロセスに送る文字列
        char *p = "Hello, dad!!\n";

        printf("This is child.\n");

        // パイプの出口を閉じる
        // 子プロセスにはパイプの入り口だけが残る
        close(pipe_fd[0]);

        // パイプに文字列を書き込む
        while (*p) {
                if (write(pipe_fd[1], p, 1) < 0) {
                        perror("write");
                        exit(1);
                }
                p++;
        }
}

void do_parent(void)
{
        char c;
        int count, status;

        printf("This is parent.\n");

        // パイプの入り口を閉じる
        // 親プロセスにはパイプの出口だけが残る
        close(pipe_fd[1]);

        // パイプの出口から文字を1バイトずつ読み込んで表示する
        while ((count = read(pipe_fd[0], &c, 1)) > 0) {
                putchar(c);
        }

        // readに失敗したときの処理
        if (count < 0) {
                perror("read");
                exit(1);
        }

        // 子プロセスが異常終了したときの処理
        if (wait(&status) < 0) {
                perror("wait");
                exit(1);
        }
}

int main(void)
{
        int child;

        // パイプを生成
        // この段階ではパイプの入り口も出口も同一プロセスにある
        if (pipe(pipe_fd) < 0) {
                perror("pipe");
                exit(1);
        }

        // プロセスを複製する
        // このときにパイプも一緒に複製される
        if ((child = fork()) < 0) {
                perror("fork");
                exit(1);
        }

        // 親プロセスと子プロセスの判定
        if (child) {
                do_parent();
        } else {
                do_child();
        }

        return 0;
}

 

以下のプログラムはファイルディスクリプタの操作とpipeの操作を組み合わせたプログラム。

動きは先ほどのものと同じ。

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int pipe_fd[2];

void do_child(void)
{
        // 親プロセスに送る文字列
        char *p = "Hello, dad!!\n";

        printf("This is child.\n");

        // パイプの出口を閉じる
        // 子プロセスにはパイプの入り口だけが残る
        close(pipe_fd[0]);

        // 標準出力を閉じる
        close(1);
        // パイプの入り口を標準出力として扱う
        if (dup2(pipe_fd[1], 1) < 0) {
                perror("dup2 (child)");
                exit(1);
        }
        // パイプの入り口を閉じる
        // 子プロセスに繋がるパイプがなくなる
        close(pipe_fd[1]);

       // 標準出力に文字列を書き込む
        while (*p) {
                putchar(*p++);
        }
}

void do_parent(void)
{
        char c;
        int count, status;

        printf("This is parent.\n");

        // パイプの入り口を閉じる
        // 親プロセスにはパイプの出口だけが残る
        close(pipe_fd[1]);

        // 標準入力を閉じる
        close(0);
        // パイプの出口を標準入力として扱う
        if (dup2(pipe_fd[0], 0) < 0) {
                perror("dup2 (parent)");
                exit(1);
        }
        // パイプの出口を閉じる
        // 親プロセスに繋がるパイプがなくなる
        close(pipe_fd[0]);

        // 標準入力から文字を読み込んで表示する
        while ((c = getchar()) != EOF) {
                putchar(c);
        }

        // 子プロセスが異常終了したときの処理
        if (wait(&status) < 0) {
                perror("wait");
                exit(1);
        }
}

int main(void)
{
        int child;

        // パイプを生成
        // この段階ではパイプの入り口も出口も同一プロセスにある
        if (pipe(pipe_fd) < 0) {
                perror("pipe");
                exit(1);
        }

        // プロセスを複製する
        // このときパイプも一緒に複製される
        if ((child = fork()) < 0) {
                perror("fork");
                exit(1);
        }

        // 親プロセスと子プロセスの判定
        if (child) {
                do_parent();
        } else {
                do_child();
        }

        return 0;
}

 

他のプロセス操作のためのシステムコール、ライブラリ関数

プロセスの強制終了

プロセスを終了するときのkillコマンドは、システムコールkill()で実現される。

 

メモリ領域の確保

mallocなどでメモリを使用したいが、メモリが不足している場合システムが暗黙的に以下のシステムコールを呼ぶことがある。

自分で呼ぶ機会は少ない。

brk()

sbrk()

詳細はman参照。

 

その他

nice(), ptrace(), popen(), pclose(), system()などのシステムコールの簡単な紹介。

 

まとめ

第4回の講義内容は以上です。

正直私のメモ見るよりも講義ページを見るのが良いと思います。

非常に簡潔にまとまっていてわかりやすいです。

なお、この記事の中で使用した図はすべて筑波大学の講義ページから引用しています。

 

第5回の講義の内容はこちら。

amistad06-a.hatenablog.com

 

おわり。

 

【スポンサーリンク】