『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』第4章[前篇]
第4章 プロセススケジューラ[前篇]
はじめに
武内 覚 著『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』をテキストとして学習した記録です。
本書を読んで筆者が解釈した内容について記述しています。
なので本書の内容の間違った解釈やあるいは単純に間違った記述がある可能性があります。
プロセススケジューラとは
一つのCPUで同時に処理できるプロセスは一つだけ。一つのCPUで複数のプロセスを並行して処理する必要がある場合、処理する対象のプログラムを適切な単位(時間,タイムスライスと呼ぶ)に区切ってそれぞれのプロセスを順番に処理していく仕組みがある。この仕組みをプロセススケジューラと呼ぶ。
この章ではプロセススケジューラの働きを確認するためのプログラムを作成し実際の動きを確認する。
一つのCPUが処理するプロセスについて確認するが、現代のPCのCPUはマルチコアが実装されている場合が多い。
マルチコアのCPUの場合、Linuxは各コアをそれぞれ独立したCPUとみなす。これを論理CPUと呼ぶ。
コンテキストスイッチ
CPUがプロセススケジューラによって処理する対象のプロセスを切り替えることをコンテキストスイッチと呼ぶ。
コンテキストスイッチは通常タイムスライスによって行われる。
コンテキストスイッチが発生するとプロセスの途中で処理が中断されるため、CPUがマルチプロセスで処理を行なっている場合は各プロセスは必ずしも時間に比例して進捗する訳ではないことを考慮する必要がある。
テストプログラム
#include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <err.h> #define NLOOP_FOR_ESTIMATION 1000000000UL #define NSECS_PER_MSEC 1000000UL #define NSECS_PER_SEC 1000000000UL static inline long diff_nsec(struct timespec before, struct timespec after) { return ((after.tv_sec * NSECS_PER_SEC + after.tv_nsec) - (before.tv_sec * NSECS_PER_SEC + before.tv_nsec)); } static unsigned long loops_per_msec() { struct timespec before, after; // 処理が開始前の時間をbeforeに代入 clock_gettime(CLOCK_MONOTONIC, &before); // 適当な回数loopするだけのプログラム unsigned long i; for (i = 0; i < NLOOP_FOR_ESTIMATION; i++) ; // 処理が終了後の時間をafterに代入 clock_gettime(CLOCK_MONOTONIC, &after); int ret; // 何回loopを回せば1ミリ秒かかるかを推定 return NLOOP_FOR_ESTIMATION * NSECS_PER_MSEC / diff_nsec(before, after); } static inline void load(unsigned long nloop) { unsigned long i; for(i = 0; i < nloop; i++) ; } static void child_fn(int id, struct timespec *buf, int nrecord, unsigned long nloop_per_resol, struct timespec start) { int i; for(i = 0; i < nrecord; i++){ struct timespec ts; load(nloop_per_resol); clock_gettime(CLOCK_MONOTONIC, &ts); buf[i] = ts; } for(i = 0; i < nrecord; i++){ printf("%d\t%ld\t%d\n", id, diff_nsec(start, buf[i])/NSECS_PER_MSEC, (i + 1) * 100 / nrecord); } exit(EXIT_SUCCESS); } static void parent_fn(int nproc) { int i; for(i = 0; i < nproc; i++) wait(NULL); } static pid_t *pids; int main(int argc, char *argv[]) { // returnを初期化 int ret = EXIT_FAILURE; if(argc < 4){ fprintf(stderr, "usage: %s <nproc> <total[ms]> <resolution[ms]>\n", argv[0]); exit(EXIT_FAILURE); } // ascii to integer int nproc = atoi(argv[1]); int total = atoi(argv[2]); int resol = atoi(argv[3]); if(nproc < 1){ fprintf(stderr, "<nproc>(%d) should be >= 1\n", nproc); exit(EXIT_FAILURE); } if(total < 1){ fprintf(stderr, "<total>(%d) should be >= 1\n", total); exit(EXIT_FAILURE); } if(resol < 1) { fprintf(stderr, "<resol>(%d) should be >= 1\n", resol); exit(EXIT_FAILURE); } if(total % resol) { fprintf(stderr, "<total>(%d) should be multiple of <resolution>(%d)\n", total, resol); exit(EXIT_FAILURE); } // 統計取得の回数 int nrecord = total / resol; // メモリを確保する struct timespec *logbuf = malloc(nrecord * sizeof(struct timespec)); if(!logbuf) err(EXIT_FAILURE, "malloc(logbuf) failed"); // ちょうど1ミリ秒かかる負荷の測定中 puts("estimating workload which takes just one milisecond"); unsigned long nloop_per_resol = loops_per_msec() * resol; // 測定終了 puts("end estimation"); fflush(stdout); pids = malloc(nproc * sizeof(pid_t)); if(pids == NULL){ warn("malloc(pids) failed"); goto free_logbuf; } struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); int i, ncreated; for(i = 0, ncreated = 0; i < nproc; i++, ncreated++){ pids[i] = fork(); if(pids[i] < 0){ goto wait_children; }else if(pids[i] == 0){ // children child_fn(i, logbuf, nrecord, nloop_per_resol, start); /* shouldn't reach here */ } } ret = EXIT_SUCCESS; // parent wait_children: if(ret == EXIT_FAILURE) for(i = 0; i < ncreated; i++) if(kill(pids[i], SIGINT) < 0) warn("wait() faild."); free_pids: free(pids); free_logbuf: free(logbuf); exit(ret); }
『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』第3章を読む
第3章 プロセス管理
はじめに
武内 覚 著『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』をテキストとして学習した記録です。
本書を読んで筆者が解釈した内容について記述しています。
なので本書の内容の間違った解釈やあるいは単純に間違った記述がある可能性があります。
二段階のプロセスの生成
プロセスの生成方法は二つある
- 同じプログラムを別のプロセスとして処理する
- 異なるプログラムを生成する
fork()
前者の同じプログラムを別のプロセスとして処理する場合に用いる。 あるプロセス内でfork()を発行するとその呼び出した元のプロセスが複製される。 fork()関数は下記のような手順でプロセスの複製を行う。
- あるプロセス内でfork()が発行される。
- 複製するプロセスのためのメモリ領域を確保する
- fork()の発行元のプログラムを複製して用意したメモリ領域に書き込む。
- fork()の発行元のプロセス(親)と複製したプロセス(子)それぞれのfork()発行地点に処理を返す。この時、親プロセスにはfork()の返り値として複製したプロセスのpidを返し、子プロセスには返り値として0を返す。
execve()
後者の異なるプロセスを生成するための関数はexecve()である。 execve()は新しいプロセスを一つ増やすのではなく既存のプロセスを新しいプロセスとして書き換えるという働きをする。 その際に引数として書き込むプログラムやその引数を渡す。
execve()の性質上それを呼び出したプロセスはそこで書き換えられてしまう。多くの場合は異なるプロセスを元のプロセスの他に生成したい場合は予めfork()を発行し、その子プロセスでexecve()を発行するというような流れになる。「fork and exec」と呼ばれるフローである。
終了処理
_exit()
_exit()を発行し終了処理を行う。標準Cライブラリのexit()関数から呼び出されることが多い。
_exit()が発行されるとプロセスで処理されていたプログラムに割り当てられていたメモリ領域が解放される。
main()関数から復帰した場合も_exit()の発行が行われる。
『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』第2章を読む
第二章ユーザモードで実現する機能
はじめに
武内 覚 著『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』をテキストとして学習した記録です。
本書を読んで筆者が解釈した内容について記述しています。
なので本書の内容の間違った解釈やあるいは単純に間違った記述がある可能性があります。
システムコール
基本的なプロセスの流れはそれぞれのコードからシステムコールを通してカーネルの処理を呼び出し、プロセスが意図する処理を実現するという流れになっている。
システムコールにはいくつかの種類がある
CPUのモードについて
CPUにはユーザモードとカーネルモードの二つのモードがある。 プロセスは普段ユーザモードで実行されているがプロセスの中でシステムコールが呼ばれるとCPUがカーネルモードへ変化する。そしてシステムコールの命令の内容が実行される。それが終わるとユーザモードの戻る、という流れになっている。
システムコール発行
プロセスがどのようなシステムコールを発行するかはstrace
というコマンドで確認することができる。
カーネルに処理を依頼する場合はどんな言語で書かれたプログラムであっても最後には必ずシステムコールを発行するようになっている。
strace
コマンドに-T
オプションを付与することでシステムコールに要した時間を計測することもできる。
システムコールのラッパー関数
システムコールはアーキテクチャ依存のアセンブリコードからのみ呼び出すことができる。
OSはこのシステムコール発行の際のアーキテクチャの違いも吸収してくれる。 具体的にはシステムコールラッパーと呼ばれるシステムコール呼び出し専用のプログラムを使ってこの問題を解決している。
これにより高級言語で書かれたユーザプログラムであってもアーキテクチャの違いを気にすることなく各種ラッパー関数を呼び出すだけでシステムコールを発行することができる。
標準Cライブラリ
ISOによって定められた標準Cライブラリというものがある。
ライブラリは処理系の一種でそれが提供される環境によって内容が異なる場合がある。 この違いを無くし、プログラムの互換性を高めるためにISOによって定められているレギュレーションがあり、それを満たすものとしてOSが提供するC言語のライブラリのことを標準Cライブラリと呼ぶ。
glibcはGNUプロジェクトが提供するCのライブラリであり、通常のLinuxではこれが標準Cライブラリとして提供されている。 glibcは前述のシステムコールのラッパー関数やPOSIXというUNIXの機能に関する規格に対応する関数を提供している。
その他OSが提供するプログラム
OSはライブラリ以外にも様々なプログラムを提供している。いずれもユーザプログラムやシステム自身を動作させるためなどに不可欠なものである。 例えばbashもその一つである。
『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』第1章を読む
第1章 コンピュータシステムの概要
はじめに
武内 覚 著『Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識』をテキストとして学習した記録です。
本書を読んで筆者が解釈した内容について記述しています。
なので本書の内容の間違った解釈やあるいは単純に間違った記述がある可能性があります。
コンピュータとは
- 狭義のコンピュータとはcpuとメモリのこと
- cpuとメモリをコンピュータと呼ぶことにする
プログラムとは
- 入力デバイスあるいはネットワークアダプタから処理の依頼を受け取る
- メモリ上に定義されている命令を読み込んでaluで演算を実行し、結果をメモリに書き込む
- メモリ上のデータを出力する(デバイスやネットワーク経由で別のコンピュータへ)
これらの手順を定義し、コンピュータが期待通りの動作をするために命令をまとめたドキュメントをプログラムと呼ぶ。 プログラムは大きく三つに分けることができる。
- アプリケーション
- ミドルウェア
- OS
LinuxはOSの一種である。 OSはハードウェアを直接操作する。
OSの役割と意義
アプリケーションやミドルウェアはOSに依頼して(OSを経由して)ハードウェアを操作する。 OSはそのためのプロトコルを用意している。
アプリケーションやミドルウェアのプログラムを実行させるのはOSの役割である。 実行時のメモリにはプログラムがロードされているがそのどの部分を実行するのかを決めてCPUを制御しているのはOSである。
CPUがプログラムを実行する単位をプロセスやスレッドと呼ぶ。
OSはプログラムをプロセスという単位で実行している。
前述のようにOSはハードウェアを操作する。
OSがなくてもハードウェアの操作をすることは可能。 ただしハードウェアの違いによってそれぞれ独自の方法でそれを操作するための実装が必要。
OSがあれば、アプリケーションのプログラムはハードウェアの違いを意識することなく命令を実行することができる。
OSの役割の一つはハードウェアによる違いを吸収することにもある。
またプロセスを管理しているのもOSである。 プロセスは同時に実行され得るため管理するOSがない場合デバイスに矛盾する命令が発行されたりと誤動作の原因になる。
OSがデバイスを操作するための処理はデバイスドライバと呼ばれるプログラムにまとめられている。
カーネル
CPUにはユーザーモードとカーネルモードがあり、カーネルモードでしか許されていない処理とそうでない処理がある。デバイスドライバの処理はカーネルモードでしか許されない。このような制約によってアプリケーションプログラムからデバイスへの問題のある命令を阻止することができる。
このようにカーネルとはシステム全体が安全性を保つための核となるような処理がまとめられたものである。カーネルの持つプログラムを実行したい場合はシステムコールを通してカーネルに処理の依頼を発行する。
カーネルはメモリやCPUなどのリソースの管理も行なっている。
Illuminate\Http\JsonResponseインスタンスからjsonを取り出す
レスポンスをjsonで返すapiを別のアクションで呼んでその結果に対して配列処理をかけたい場合。
参考
そのapiが下記のようにレスポンスを返す場合はIlluminate\Http\JsonResponseインスタンスが結果として返されている。
return response()->json( $result_data );
これはjsonではないのでjson_decodeをかけるとnullが帰る(json_decodeはjsonとして正しい形式でないデータを引数に渡すとnullを返す)。Illuminate\Http\JsonResponseインスタンスからjson形式のデータのみ抽出したい場合はIlluminate\Http\JsonResponseが持つcontent()メソッドを利用する。
$result = $this->getUsers( $request, $some_attr ); $result_json = $result->content() $result_array = json_decode( $result_json, true );
上記のようにすれば結果を配列として処理することができる。
syncで生成されるSQL
参考
概要
Eloquentのメソッドにsyncというのがある。
これはbelongsToManyの定義されている多対多のリレーション下で使うことができる。
これによって要素同士の関連を定義することができる。つまり自動で中間テーブルを作ることができる。
$team->users()->sync( $user_ids, false );
第一引数は配列で渡す。第二引数にfalseを渡さない場合は第一引数で渡した配列以外の中間テーブルのレコードが削除されてしまうので注意。
またこれによってinsertされるレコードにはcreate_atとupdated_atが付与されない。必要な場合はモデルのリレーションを定義しているメソッドの返り値にwithTimestamps()をつける。
public function users() { return $this->belongsToMany(User::class)->withTimestamps(); }
利点
単に指定された要素同士のリレーションのレコードを作るだけではなく、既に中間テーブルに同じ要素同士のレコードがあり、リレーションがある場合には新たなレコードの作成を中止してくれる。
クエリ
生成されるクエリを確認してみる
$user_ids = [868, 911]; DB::enableQueryLog(); $team->users()->sync( $user_ids, false ); dd(DB::getQueryLog());
array:3 [ 0 => array:3 [ "query" => "select `user_id` from `team_user` where `team_id` = ?" "bindings" => array:1 [ 0 => 15 ] "time" => 1.0 ] 1 => array:3 [ "query" => "insert into `team_user` (`created_at`, `team_id`, `updated_at`, `user_id`) values (?, ?, ?, ?)" "bindings" => array:4 [ 0 => Carbon {#898 +"date": "2019-03-24 12:10:02.000000" +"timezone_type": 3 +"timezone": "Asia/Tokyo" } 1 => 15 2 => Carbon {#898} 3 => 868 ] "time" => 1.02 ] 2 => array:3 [ "query" => "insert into `team_user` (`created_at`, `team_id`, `updated_at`, `user_id`) values (?, ?, ?, ?)" "bindings" => array:4 [ 0 => Carbon {#899 +"date": "2019-03-24 12:10:02.000000" +"timezone_type": 3 +"timezone": "Asia/Tokyo" } 1 => 15 2 => Carbon {#899} 3 => 911 ] "time" => 0.9 ] ]
まず、中間テーブルの既存のレコードでダブりのチェックをするためのsqlが走る。
そのあと、配列で渡す要素分の本数のinsertクエリが走ることになる。
注意点
配列で渡す要素の数がわからない場合や、ここで言う$teamが複数あってforeachなどで繰り返し処理を行いたい場合などはクエリが予期しない本数走る場合があるので避けるべき
条件分岐の処理を書くときに気をつけること
例
if文の処理を下のように3ステップで考えていく。
- if文によって処理自体を条件分岐させる。これで意図した動きにはなっている。
$response['name'] = 'default' . $extention; if( !is_null( $request->file_name ) ) { $response['file_name'] = $request->file_name . $extention; }
- if文によって変数の場合分けをする。行数は増えている。
$file_name = 'default'; if( !is_null( $request->file_name ) ) { $file_name = $request->file_name; } $response['file_name'] = $file_name . $extention;
- 三項演算子を使う。
$file_name = is_null( $request->file_name ) ? 'default': $request->file_name; $response['file_name'] = $file_name . $extention;
それぞれの変更理由
- 1から2に修正する理由
- 1だとif文の条件が真の場合に文字列結合の処理が二回実行されることになる。
- 次の処理に渡すための最も重要な変数が$response['file_name']なのでその変数を操作する処理は最後に一回だけ行うようにしてif文によって処理が分散するのを防ぎたい。
- 2から3から修正する理由
特に重要だと思うこと
1から2の修正がポイントだと思っている。
今回は条件分岐も一つだけでかつ変数に値を格納するだけの処理なので大した違いにはならず行数が増えた分返って冗長になっているような気もする。
ただし条件分岐のパターンが多く複雑になる場合にはこのように最後に処理を一度だけ行うようにした方がわかりやすくなる。
またもしsaveの対象を条件分岐したい場合などはこのような考え方がより重要になる。saveは最後に一度だけ走らせるようにするべきで直接条件分岐のなかに入れるべきではない。