1クール続けるブログ

とりあえず1クール続けるエンジニアの備忘録

試して理解 Linuxのしくみ 読みました 1章~3章

gihyo.jp

上記を読んだので学んだことをアウトプットしたいと思います。大学の授業で低レイヤのことを学んできませんでした。そのため、本書でLinuxに関して体系的に学べたの非常に良かったです。
本書の中で手を動かすパートに関しては、C言語Pythonの代わりにGo言語を使って試してみてます。また、PCはMacを使っているのですが、Linuxの環境で試したいという気持ちが有ったので、Dockerで仮想的にDebian系のOS上で動かすようにしています。リポジトリは以下になります。

github.com

第1章概要

ハードウェアの動作の流れ

1.外部デバイスから処理を依頼される

2.メモリに存在する命令を読み出してCPUにおいて実行
3.出力する

  • HDDやSSDに書き込む
  • ネットワークを介して別のコンピュータに転送する
  • 出力デバイスを介して人間に見せる

4.1に戻る

コンピュータの起動

  1. BIOSUEFIと呼ばれるハードウェア組込ソフトウェアによるハードウェア初期化処理
  2. 起動させるOSを選択するブートローダが動作
  3. ストレージデバイスからOSを読み出す コンピュータにとってストレージデバイスは欠かせない存在

2章ユーザモードで実現する機能

ユーザモードとカーネルモード

バイスの操作やプロセス管理、メモリ管理、プロセススケジューラなど、通常のプロセスから実行できると困る、OSの核となる処理をまとめたプログラムをカーネルという。
これらは特権モード、即ちカーネルモードで動作する。
OS=カーネルではなく、カーネル以外にもユーザモードで動作する様々なプログラムから構成される。
ユーザモードのプロセス処理から、システムコールを通じてカーネルの処理を呼び出す。それを呼び出すのは、プロセス固有のコードやそれが使用するライブラリ(OS提供のもの・そうでないもの)

CPUのモード遷移

システムコールを発行すると、CPU割り込みが発生。これによって、CPUでは、ユーザモードからカーネルモードに遷移して、カーネルの処理を行う。終わったらユーザモードに戻る。

システムコールの呼び出し

sh main.sh -d 02-syscall-and-non-kernel-os/hello
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

-------------------------PROGRAM START-------------------------
hello world
--------------------------PROGRAM END--------------------------
-------------------------SYSCALL START-------------------------
execve("/go/bin/app", ["/go/bin/app"], [/* 7 vars */]) = 0 <0.000401>
arch_prctl(ARCH_SET_FS, 0x54c6b0)       = 0 <0.000087>

***(省略)*******************************************************

fcntl(2, F_GETFL)                       = 0x1 (flags O_WRONLY) <0.000139>
write(1, "hello world\n", 12)           = 12 <0.000149>  
exit_group(0)                           = ?
+++ exited with 0 +++
--------------------------SYSCALL END--------------------------

下から3行目のwriteがデータを画面やファイルに出力する「write()」システムコール。省略している部分もシステムコールだが、それはプログラムの開始処理が発行するもの。

ユーザモードとカーネルモードの割合

自前のアプリケーションを走らせない場合

以下のようにUbuntuの環境でsarコマンドを発行してみた

docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install -y sysstat
# sar -P ALL 1 1

結果は以下のように得られた

12:36:45        CPU     %user     %nice   %system   %iowait    %steal     %idle
12:36:46        all      0.00      0.00      0.50      0.00      0.00     99.50
12:36:46          0      0.00      0.00      0.00      0.00      0.00    100.00
12:36:46          1      0.00      0.00      0.99      0.00      0.00     99.01

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.00      0.00      0.50      0.00      0.00     99.50
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00
Average:          1      0.00      0.00      0.99      0.00      0.00     99.01

各行が1つのコアに対応する。今回は殆どがidleモードとなっている。

  • %user -> ユーザモード
  • %nice -> ユーザモード
  • %system -> カーネルモード
  • %idle -> 何もしていない時間

自前の無限ループするだけのアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/loop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (aad086a81679)    01/14/19    _x86_64_    (2 CPU)

13:18:18        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:18:19        all     50.00      0.00      0.50      0.00      0.00     49.50
13:18:19          0      0.00      0.00      1.00      0.00      0.00     99.00
13:18:19          1    100.00      0.00      0.00      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     50.00      0.00      0.50      0.00      0.00     49.50
Average:          0      0.00      0.00      1.00      0.00      0.00     99.00
Average:          1    100.00      0.00      0.00      0.00      0.00      0.00

CPUコア1上でユーザプロセス、すなわちloopプログラムが常に動作していることがわかる。

自前の親プロセス取得を無限ループするアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/ppidloop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (4bbb02f02797)    01/14/19    _x86_64_    (2 CPU)

13:26:00        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:26:01        all     20.71      0.00     30.30      0.00      0.00     48.99
13:26:01          0     41.00      0.00     59.00      0.00      0.00      0.00
13:26:01          1      0.00      0.00      1.02      0.00      0.00     98.98

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     20.71      0.00     30.30      0.00      0.00     48.99
Average:          0     41.00      0.00     59.00      0.00      0.00      0.00
Average:          1      0.00      0.00      1.02      0.00      0.00     98.98

CPUコア0上でppidloopプログラムを41%の割合で実行し、親プロセス取得のシステムコールを59%の割合で実行していた。

システムコールのラッパー関数

システムコールC言語などの高級言語から直接呼び出せない。アーキテクチャ依存(amd64とかarmとか?)のアセンブリコードを使って呼び出す必要がある。もしシステムコールをラップする関数を内包するライブラリ群がなければ、アーキテクチャ依存のアセンブリソースを書かなくてはいけない。

標準Cライブラリ

GNUプロジェクトが提供するglibcLinuxの標準Cライブラリとして使用する。glibcシステムコールをラップする関数を内包している。またPOSIXという規格に定義されている関数も提供。
プログラムがどのようなライブラリをリンクしているかは以下でlddコマンドでわかる。

% docker run --rm -it golang:1.11.4 /bin/bash
# which go
/usr/local/go/bin/go
# ldd /usr/local/go/bin/go
    linux-vdso.so.1 (0x00007ffef2965000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fe2c2ba2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe2c2803000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe2c2dbf000)

第3章プロセス管理

fork関数

同じプロセスの処理を複数のプロセスに分ける際に呼び出される(例:Apacheで子プロセスを生成する)。 子プロセス用メモリ領域を作成し、親プロセスのメモリをコピーする。fork関数はカーネルモードからユーザモードに処理が再び移るときに、親プロセスには子プロセスのプロセスIDを、子プロセスには0を返す。これを利用して親子で処理を分岐させる。

sh main.sh -d 03-process-management/fork
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。 fork()されたときに同じプログラムが複製されて2個同時に動いているような感覚。

I'm parent! my pid is 7 and the pid of my child is 11.
I'm child! my pid is 11.

execve関数

全く別のプログラムを生成するときに呼び出される(例:bashからsleepコマンド実行)。 ます実行ファイルを読み出し、その後現在のプロセスのメモリを上書く。 実行ファイルの中身 * プロセスの実行に必要なコード、データ * コードを含むデータ領域のオフセット(ベースアドレスに加えられるアドレスの数)-> ファイルの情報 * コードを含むデータ領域のサイズ * コードを含むデータ領域のメモリマップ開始アドレス -> コンピュータのメモリのどこにマッピングするか * 最初に実行する命令のメモリアドレス(エントリポイント)

Linuxの実行ファイルはELFというフォーマットを使用する。
エントリポイントのアドレスを得るには「-h」オプション、ファイル内オフセット、サイズ、メモリマップ開始アドレスを得るには「-S」オプション

% docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install binutils
# readelf -h /bin/sleep
ELF Header:
------(省略)---------------------------------------------
  Entry point address:               0x401760
------(省略)---------------------------------------------

# readelf -S /bin/sleep
There are 29 section headers, starting at offset 0x7370:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
------(省略)--------------------------------------------------------
  [14] .text             PROGBITS         00000000004014e0  000014e0
       0000000000003319  0000000000000000  AX       0     0     16
------(省略)--------------------------------------------------------
  [25] .data             PROGBITS         00000000006071c0  000071c0
       0000000000000074  0000000000000000  WA       0     0     32
------(省略)--------------------------------------------------------

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

全く新規のプロセスを新規生成する場合には、親となるプロセスからfork()を発行して、復帰後に子プロセスがexec()を発行する。 sh main.sh -d 03-process-management/execve
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。fork()の直後にexec()が動いています。

sh main.sh -d 03-process-management/execve
I'm parent! my pid is 6 and the pid of my child is 10.
I'm child! my pid is 10.
hello

第4章プロセススケジューラ