読書メモ「[試して理解] Linux のしくみ〜実験と図解で学ぶ OS とハードウェアの基礎知識」

読書メモ「[試して理解] Linux のしくみ〜実験と図解で学ぶ OS とハードウェアの基礎知識」

Linux の勉強で図解が多く評判が良い本書を読んだメモ。概念を説明するための図やグラフが多く非常にわかりやすかったです。普通に大学の講義くらい体系的にまとめあげられていて満足度が高いです。実験パートでは実際に動作させるプログラムも掲載されていますが、Kindle 版だと少しコピーがしづらかったのが難点でした。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

武内 覚
3,278円(04/12 06:05時点)
発売日: 2018/02/23
Amazonの情報を掲載しています
目次

OS とは

プログラムの種類

プログラムにはいくつかの種類がある。

  • アプリケーション:ユーザーに直接機能を提供するプログラム
  • ミドルウェア:多くのアプリケーションに共通した処理をまとめて実行を支援するプログラム
  • OS:ハードウェアを直接操作してアプリケーションやミドルウェアの動作に必要な機能を提供するプログラム

プログラムはプロセスという単位で実行される。 ほとんどの OS は複数のプロセスを同時に実行可能である。 ソフトウェアプログラムは1つ以上のプロセスで構成される。

デバイスドライバ

デバイスの操作を標準化し、管理するためにデバイスドライバがある。 バグや悪意のあるプログラムによってハードウェアを不正に操作できないように Linux はプロセスからデバイスに直接アクセスできないようにしている。

ユーザーモードとシステムコール

プロセスは動作させるモードによって権限が異なる。ユーザーが実行するプロセスはユーザーモードで動作し、デバイスドライバなどシステムに関わるプロセスはカーネルモードで動作する。カーネルモードで動作するプロセスには次のようなものがある。

  • プロセス管理システム
  • プロセススケジューラー
  • メモリ管理システム

OS の核となる処理をまとめたものをカーネルと呼ぶ。 システムコールを介してカーネルに操作を依頼する。ハードウェアの操作など、セキュリティ上重要な権限を必要とするものはカーネルモードでしか扱えない。

システムコール

システムコールで扱う操作には次のようなものがある。

  • プロセス生成、削除
  • メモリ確保、解放
  • プロセス間通信
  • ネットワーク
  • ファイルシステム操作
  • ファイル操作(デバイスアクセス)

CPU モード遷移

プロセスはシステムコールでカーネルに処理を依頼する。 その時 CPU では割り込みというイベントが発生する。 これにより CPU ではユーザーモードからカーネルモードに遷移し、処理が実行される。 カーネルは処理の冒頭で要求が正当なものかどうかをチェックする。 不正な要求であればシステムコールを失敗させる。

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

システムコールを呼び出すだけの関数が各言語ごとに提供されている。

OS が提供するプログラム

  • システムの初期化: init
  • OS の挙動を変える: sysctl, nice, sync
  • ファイル操作: touch, mkdir
  • テキストデータの加工: grep, sort, uniq
  • 性能測定: sar, iostat
  • コンパイラ: gcc
  • スクリプト言語実行環境: perl, python, ruby
  • シェル: bash
  • ウィンドウシステム: X

プロセス管理

プロセス生成が生成されるのは次のいずれかの目的である。

  • 同じプログラムの処理内容を複数のプロセスに分けて処理する。
  • 全く別のプログラムを生成する

fork() 関数

同じプログラムの処理を複数のプロセスに分けて処理するために利用する。 発行したプロセスをもとに新たにプロセスを生成する。

  1. 子プロセス用のメモリ領域を作成し、親プロセスのメモリをコピーする。
  2. 親プロセスと子プロセスは違うコードを実行するように分岐する。

execve() 関数

  1. 実行ファイルを読み出して、プロセスのメモリマップに必要な情報を読み出す
  2. 現在のプロセスのメモリを新しいプロセスのデータで上書きする
  3. 新しいプロセスの最初の命令から実行開始する

ELF

Executable and Linkable Format 実行ファイルの情報を定義したフォーマット。ファイルの先頭に付与されている。

終了処理

_exit() 関数でプロセスが利用していたメモリ領域を回収する。 標準 C ライブラリの exit() 関数を呼び出すと内部的には _exit() 関数を呼び出す。

プロセススケジューラ

プロセススケジューラ:複数のプロセスを同時に動作させているように見せかける。原則として 1 つの CPU 上で同時に処理されるプロセスは 1 つだけ。複数のプロセスを適当な長さごとで順番に実行する。

マルチコアでは 1 コアが 1 つの CPU、ハイパースレッド有効であればそれぞれのハイパースレッドが 1 つの CPU として認識される。

同時にプロセスが実行される時、等しい長さのタイムスライスによって順番に実行させる。 そのためプロセス数に比例して実行時間が長くなる。プロセスの切り替わりをコンテキストスイッチという。

プロセスの状態

STAT状態意味
R実行状態現在論理 CPU を使っている
R実行待ち状態CPU 時間が割り当てられるのを待っている
S or Dスリープ状態何らかのイベントが発生するのを待っている
Zゾンビ状態プロセスが終了した後に親プロセスの終了を待ってる

アイドル状態

CPU 上で動作していない状態のプロセス=アイドルプロセス。

アイドル状態では論理 CPU 上でプロセスが実行されていない状態 CPU からの命令で論理 CPU を休止状態にし、1つ以上のプロセスが実行可能状態になるまで待機する。

命令の遷移

  1. ユーザーからの入力を受け付ける
  2. 入力をもとにファイルを読み込む
  3. プロセス生成: 実行状態
  4. ユーザーからの入力待ち: スリープ状態
  5. ユーザー入力: 実行状態
  6. ファイル読み出し: スリープ状態
  7. 読み出し完了: 実行状態

性能

  • スループット
    • 単位時間あたりの層仕事量。高いほどよい。
    • 完了したプロセスの数 / 経過時間
  • レイテンシ
    • それぞれの処置開始から終了までの経過時間
    • 処理終了時刻 – 処理開始時刻

アイドル状態が少ないほどスループットが高くなる。プロセスを増やすほどレイテンシーは高くなる。

アイドル状態←→実行状態←→実行可能状態

マルチコア CPU でのスケジューリング

グローバルスケジューラが複数の論理 CPU を扱うために負荷分散する。 プロセスに割り振られた各論理 CPU 内で、各プロセスに平等に CPU 時間を分配する。

優先度の変更

nice() 関数でプロセスの実行優先度を -19 ~ 20 で設定できる。 数字が低いほど優先度が高い。 優先度を上げられるのは root 権限を持ったユーザーだけ。

メモリ管理

カーネルがシステムに搭載されているメモリを管理する。

free コマンド

システムが搭載するメモリの量と使用中のメモリの量

  • total: システムに搭載されている全メモリの量。
  • free: 見かけ上の空きメモリ量。
  • buff/cache: バッファキャッシュ、ページキャッシュが利用するメモリ。free が減ると解放される。
  • available: 実質的空きメモリ。free + 解放可能なカーネル内のメモリ領域。

Out of Memory

空き領域が減るとシステムがメモリを解放(Available) メモリ使用量が増えて解放領域が取れなくなることを OOM (Out of Memory) と呼ぶ OOM になると敵牢なプロセスを選んで強制終了する OOM killer が働く。

強制終了されては困るのでサーバーでは vm.panic_on_oom をデフォルトの 0 から 1 にかえる。

  • 0: OOM killer 発動
  • 1: OOM 時システムを強制終了

単純なメモリ割り当て

プロセスにメモリが割り当てられるタイミングは次のいずれか。

  • プロセス生成時
  • プロセス生成後、追加で動的にメモリを割り当てる時

メモリの追加割り当てにはいくつかの問題がある。

  • メモリの断片化
  • 別用途のメモリにアクセス
  • マルチプロセス扱いが困難

メモリ断片化

合計領域が多くてもだめ 何この領域にまたがっているかを意識しないといけない。 また、ここの領域以上の配列などを作る用途に使えない。

別用途のメモリにアクセス

別のプロセスが利用しているメモリ領域にアクセスすると、データの破損や漏洩の可能性がある。

マルチプロセス扱いが困難

同じプログラムを実行しようとした時に同じメモリマップを観に行くので問題が生じる。

仮想記憶

システムに搭載されているメモリにプロセスから直接アクセスさせるのではなく、 仮想アドレスで間接的にアクセスさせる。 仮想記憶ではプロセスがみるアドレスを 仮想アドレス、実際のアドレスを 物理アドレス と呼ぶ。 プロセスから実際のメモリに直接アクセスする方法はない。

ページテーブル

仮想アドレスから物理アドレスへの変換はカーネルが使うメモリに保存されている ページテーブル を参照する。 ページテーブル内の 1 つのページに対応するデータを ページテーブルエントリ と呼ぶ。 x86_64 アーキテクチャにおいては 4K バイト。

ページフォールト: 仮想アドレス内で物理アドレスが未割り当ての領域にアクセスした時の割り込み ページフォールトハンドラ: 実行中の命令が中断され、不正なアクセス検出として強制終了する。 →SIGSEGV というシグナル

メモリ割り当て

補助メモリ内のコード領域サイズとデータ領域サイズの分だけプロセス生成時に領域を確保する。 その後仮想アドレスと物理アドレスをページテーブルにマッピングする。 追加割り当て時、メモリの新規割り当て領域分をページテーブルに拡張して追加する。

malloc() 関数

C 言語標準ライブラリ内にあるメモリ獲得のための関数(バイト単位で指定)。 glibc があらかじめ mmap() システムコールによってメモリ領域をプールしておく。 関数発行時に必要な量を切り出す。

問題の解決

メモリ断片化

→仮想アドレス上では連結して見える

別用途メモリへのアクセス

→プロセス単位でページテーブルと物理アドレスが確保されるので被ることがなくなる

マルチプロセスの扱い

→プロセスごとに生成するので同じプログラムであってもマッピングが異なる

ファイルマップ

プロセスがファイルにアクセスするとき(read(), write(), lseek(), など)、 ファイルの領域を仮想アドレス空間上にマップする。 メモリ上に読み出したファイルデータはアクセスが不要になったタイミングでストレージに書き戻される。

write() 関数でなくても memcpy() 関数などでデータをメモリ領域にコピーできる。

デマンドページング

カーネルが事前にメモリ領域を確保すると、使わない領域が発生する。 それを解決するためにデマンドページングという仕組みでメモリをプロセスに割り当てる。

  • プロセス未割り当て
  • プロセス割り当て済みで物理メモリも割り当て済み
  • プロセス割り当て済みだが物理メモリ未割り当て

物理メモリ未割り当ての仮想アドレスにアクセスしたときは、自動的にカーネルがページフォールトで物理領域を確保する。 仮想メモリ上に mmap() で領域を確保しても実際にアクセスするまではページフォールトは発生しない。

x86 アーキテクチャではカーネル空間が 4G しかないのでメモリ枯渇が頻発していた。 x86_64 アーキテクチャでは仮想アドレス空間は 128 TB。

コピーオンライト

fork() システムコール時にはおやプロセスのメモリをページテーブルだけコピーする。 fork() システムコール発行時にはすべてのページテーブルに対する書き込み権限を一時的に無効にする。 →この時親プロセスと子プロセスのページテーブルの内容は同一である。

コピーオンライトはその後でプロセスがメモリにアクセスしようとした時、 アクセスするアドレスのデータを別の場所にコピーして書き込み権限を与える。 これ以降はどちらのプロセスも書き込み可能となる。

共有されているメモリは二重計上される。

スワップ

物理メモリがなくなると OOM ではなくスワップが発生する。 スワップではスワップ領域として確保したストレージデバイスの一部をメモリの代わりに使用する。 メモリ空間をストレージデバイスに退避することで空きメモリを作る。

使ってないメモリ領域をスワップ領域に退避させることをスワップアウトと呼ぶ。 スワップアウトする対象はカーネルが所定のアルゴリズムで決定する。 スワップアウトした領域を読み戻すことをスワップインと呼ぶ。 両方を合わせてスワッピングと呼ぶ。 Linux においてはスワップする単位がページなのでページングとも(ページアウト、ページイン)。

ストレージへのアクセス速度はメモリよりも遅いため、頻繁にスワッピングが繰り返される(スラッシング)とパフォーマンスが低下する。

  • メジャーフォールト: ストレージデバイスへのアクセスが発生するページフォールト
  • マイナーフォールト: ストレージデバイスへのアクセスが発生しないページフォールト

階層型ページテーブル

フラットにすべてのページテーブルの対応表を持つのではなく、ある程度の塊で括っていく。 使っていない領域については対応がないのでテーブルを保持していない。

ヒュージページ

通常より大きなサイズのページ。 プロセスのページテーブルに必要なメモリの量を減らせる。 ページテーブルエントリの数を削減できるのでメモリ使用量の削減と fork() システムコールの高速化につながる。

  • mmap() の flag に MAP_HUGETLB フラグを与える
  • ヒュージページ利用設定を有効化する →DB や VM マネージャーなど

トランスペアレントヒュージページ

仮想アドレス空間内の連続する複数の 4K バイトページを自動的にヒュージページにする。 自動的な再分解などが発生するため局所的に性能が劣化する場合もある。 /sys/kernel/mm/transparent_hugepage/enabled

記憶階層

名称サイズ価格アクセス速度
レジスタ
キャッシュメモリ
メモリ
ストレージデバイス

キャッシュメモリ

  1. 命令をもとにメモリからレジスタにデータを読み出す
  2. レジスタ上で計算する
  3. 結果をメモリに書き戻す

レジスタとメモリの速度差を埋めるためにキャッシュメモリがある。 キャッシュメモリでの読み出しサイズは CPU ごとに定められるキャッシュラインサイズ

レジスタでデータを変更した場合、キャッシュメモリへ書き戻す際にダーティという印をつける。 ダーティマークがついたものはメモリへの書き戻しが行われた時にダーティマークが取れる。

キャッシュメモリがいっぱいの時はどれかのデータを廃棄する。 廃棄するときにダーティなデータの場合はメモリに書き戻してから廃棄する。 ダーティなデータが多くスラッシングが多発する場合は性能が劣化する。

  • type: キャッシュするデータの種類。Data はデータのみ、Code はコードのみ、Unified は両方。
  • shared_cpu_list: キャッシュを共有する論理 CPU のリスト
  • size: サイズ
  • coherency_line_size: キャッシュラインサイズ

プロセスのデータがすべてキャッシュにあるうちはアクセス速度は高速になる。 時間的もしくは空間的に局所性を持たせることで性能が安定する。

Translation Lookaside Buffer

ページテーブルで保持する仮想アドレスと物理アドレスの対応表を高速にアクセスできる TLB。

ページキャッシュ

キャッシュメモリはメモリのデータを一時的に保持する。 ページキャッシュはストレージ上のデータをキャッシュメモリ上に保持する。 メージキャッシュではページ単位でデータを扱う。

カーネル内に保持するページキャッシュにあるデータについてはコピーを渡す。 ページキャッシュ内のデータは共有資源なので、全プロセスから読みだせる。 プロセスからのデータ更新時はページキャッシュにだけデータを書き込む。

更新されたページキャッシュには、データストレージよりも新しいものという印をつける。 =ダーティページ ダーティページはバックグラウンド処理によってストレージに反映される。 メモリ領域の開放時、ダーティページ以外から解放して空き領域を確保する。 書き戻し発生時は速度が低下する。

ダーティページが存在する場合に強制電源断が発生するとデータは消失する。 その防止策として opne() システムコールで O_SYNC フラグを設定すると、 write() システムコールを発行するごとに、メモリとストレージの両方へ書き込みを行う。

バッファキャッシュ

ファイルシステムを使わずにデバイスファイルを用いてストレージデバイスに直接アクセスする領域。 ライトバックは sysctl の vm.dirty_writeback_centisecs で調整可能。デフォルトは5秒で1回。

ハイパースレッド

CPU 時間はデータ転送待ち時間が多い。

ファイルシステム

ストレージデバイスに対してメモリ上のデータを書き込む操作を管理する。 データがどこにあるか、どこが空き領域か。 データの塊をファイルという単位で補助情報を付与した上で管理する。

  • データ: 作成した文書や画像、動画、プログラムなど
  • メタデータ: ファイルの名前やストレージデバイス上の位置、サイズなどの補助的情報
    • 種類
    • 時刻情報
    • 権限情報

ディレクトリ: ファイルを格納するファイル ファイルシステムごとにデータ構造や扱えるファイルサイズが異なる。

  • ファイルの作成、削除: creat(), unlink()
  • ファイルを開く、閉じる: open(), close()
  • 開いたファイルからデータを読み出す: read()
  • 開いたファイルにデータを書き込む: write()
  • 開いたファイルの所定の位置に移動: lseek()
  • 上記以外のファイルシステム依存の特殊な処理: ioctl()

容量制限

ファイルシステム容量が足りなくなるとシステムに異常をきたす。 用途別に領域を確保しておく=クォータ

  • ユーザクォータ: ファイルの所有者となるユーザーごとに容量を制限
  • ディレクトリクォータ: 特定のディレクトリごとに容量を制限
  • サブボリュームクォータ: ファイルシステム内のサブボリューム単位ごとに容量を制限

ファイルシステム不整合

ファイルシステムの処理中にシステムが停止などをすると不整合が発生する。

  • ジャーナリング
  • コピーオンライト

ジャーナリング

ユーザーに認識できないメタデータ領域。 ファイル操作に関する処理(アトミックな処理)の一覧をジャーナル領域に書き出す=ジャーナルログ。 ジャーナルログに従ってファイルシステムの内容を更新する。

ジャーナルログの更新中に異常終了が発生した場合、実データに影響はない。 実データの影響中に異常終了が発生した場合、ジャーナル領域に従ってデータを更新する。

コピーオンライト

ext4 や XFS は更新したデータは同じ場所に書き戻す。 コピーオンライト方式の場合は更新したファイルを別の場所に書き戻す。

対策

ファイルシステム不整合への対策は定期的なバックアップからの復元。 復旧コマンドは用意しておく。

ファイルの種類

デバイスファイルとして、ハードウェアを管理する。(/dev 以下に存在)

  • キャラクタデバイス: 読み出しと書き込みはできるがシークはできない。
    • ターミナル
    • キーボード
    • マウス
  • ブロックデバイス: 読み書き以外にランダムアクセスが可能。
    • HDD
    • SSD

ブロックデバイスに対する直接のデータ操作は不整合を引き起こす可能性があるので危険。

メモリ部0酢ファイルシステム: tmpfs 電源を切るとなくなるが高速にアクセスできる。

ネットワークファイルシステム

ネットワーク越しのホストファイルシステムにアクセスする。

  • CIFS: Windows ホスト上のファイルシステムにアクセス
  • NFS: Linux 系ホスト上のファイルシステムにアクセス

仮想ファイルシステム

procfs

システム上に存在するプロセスの情報を管理する。

  • /proc/pid/maps: プロセスのメモリマップ
  • /proc/pid/cmdline: プロセスのコマンドライン引数
  • /proc/pid/stat: プロセスの状態、CPU 時間、優先度、使用メモリ
  • /proc/cpuinfo: CPU に関する情報
  • /proc/diskstat: ストレージデバイスに関する情報
  • /proc/meminfo: メモリに関する情報
  • /proc/sys: カーネルの各種チューニングパラメータ

sysfs

カーネルのプロセスに関するもの以外の雑多な情報

  • /sys/devices: システムに搭載されているデバイスに関する情報
  • /sys/fs: システムに存在する各種ファイルシステムに関する情報

cgroupfs

プロセスのグループに対して、リソース使用量の制限をかけるグループ cgroup。 CPU やメモリの使用量に制限をかけられる。

Btrfs

最新の豊富な機能を提供するファイルシステム マルチボリュームを構成できる。 ファイルシステム + LVM のようなボリュームマネージャー。

サブボリューム単位でスナップショットを採取できる。 バックアップはノードへのリンクを張るだけなのでデータコピーはしない。

ストレージデバイス

HDD のデータ読み書き

データを磁気情報で記憶し、プラッタというディスクに保持する。 セクタ単位で読み書きし、半径方向及び円周方向に分割される。 プラッタ上のセクタを磁気ヘッドで読み書きする。 読み書き時にはセクタ番号、セクタ数、アクセス種類を渡す。

スイングアームとプラッタ回転の機械的処理に律速される。 連続した領域にデータが配置されていない場合、特に読み書きには時間がかかる。

  • シーケンシャルアクセス: 連続したデータの読み出し
  • ランダムアクセス: ランダムに配置されたデータの読み出し

ブロックデバイス層がストレージデバイスに向けた処理を行う。

I/O スケジューラによって、アクセス要求を一定期間溜めた後に次のような加工をする。

  • マージ: 複数の連続するセクタへの要求を1つにまとめる
  • ソート: 複数の不連続なセクタへの要求をセクタ番号じゅんに並べる

先読み

続けて読み込む可能性の高いセクタを事前に読み終わっておく。 使わなかった場合は読み取ったデータは破棄する。

SSD のデータ読み書き

電気的アクセスなので HDD よりも支援機能による性能差が少ない シーケンシャルサイズがそこまで大きくなくても性能上限が出る。