第6章:オペレーティングシステムの内部構造
オペレーティングシステムは、ハードウェアとアプリケーションの間に位置し、リソース管理と抽象化を提供する基盤ソフトウェアである。その内部動作を理解することは、システムのボトルネックを特定し、適切なチューニングを行う上で不可欠である。
なぜカーネル空間とユーザー空間は分離されているのか。プロセススケジューリングはどのような原理で動作し、どこに限界があるのか。メモリ管理の仕組みが、アプリケーションの性能にどのような影響を与えるのか。これらの疑問に答えることで、表面的な設定変更ではない、本質的なシステム最適化が可能となる。
6.1 カーネル空間とユーザー空間の分離原理
保護リングの実装とオーバーヘッド
現代のプロセッサは、異なる特権レベルを提供することで、システムの安定性とセキュリティを確保している。この仕組みを理解することが、オペレーティングシステムの動作原理を把握する出発点となる。
x86アーキテクチャの保護リング
x86プロセッサは、4つの特権レベル(Ring 0-3)を提供するが、実際のオペレーティングシステムでは主に2つのレベルが使用される:
Ring 0(カーネルモード):
- 最高特権レベル
- 全ハードウェアリソースへのアクセス可能
- メモリ管理、I/O操作、割り込み処理
- カーネルコード、デバイスドライバが実行
Ring 3(ユーザーモード):
- 制限された特権レベル
- ハードウェアへの直接アクセス禁止
- システムコール経由でのカーネル機能利用
- アプリケーションプログラムが実行
この分離により、以下の保護機能が実現される:
- メモリ保護:ユーザープロセスは他のプロセスやカーネルのメモリにアクセス不可
- ハードウェア保護:不正なI/O操作やハードウェア設定変更を防止
- システム保護:悪意のあるプログラムによるシステム破壊を防止
特権レベル遷移のメカニズム
ユーザーモード → カーネルモード(例:システムコール)
1. アプリケーションがシステムコール命令実行
x86_64: syscall命令
引数設定: rax=システムコール番号, rdi,rsi,rdx,r10,r8,r9=引数
2. プロセッサの自動処理:
- 現在のレジスタ状態を保存
- 特権レベルをRing 0に変更
- カーネル専用スタックに切り替え
- システムコールハンドラへジャンプ
3. カーネル処理:
- 引数の妥当性確認
- 要求された機能の実行
- 結果をレジスタに設定
4. ユーザーモードへの復帰:
- 保存されたレジスタ状態を復元
- 特権レベルをRing 3に変更
- ユーザー空間のスタックに復帰
特権レベル遷移のオーバーヘッド
オーバーヘッドの内訳(x86_64環境):
1. モード切り替え:50-100 CPU cycles
- レジスタ保存・復元
- スタックポインタ切り替え
- 特権レベル変更
2. TLBフラッシュ:100-500 cycles
- アドレス変換キャッシュの部分的無効化
- PCID(Process Context ID)が使用可能な場合は軽減
3. キャッシュ汚染:可変(ワークロード依存)
- カーネルコードによるCPUキャッシュの使用
- ユーザーコードのキャッシュエントリが追い出される
総オーバーヘッド:200-1000 cycles(ワークロード依存)
実時間:現代のCPU(3GHz)で約0.1-0.3マイクロ秒
システムコールの実装メカニズム
システムコールは、ユーザー空間からカーネル機能を利用するための標準化されたインターフェースである。その実装を理解することで、アプリケーションの性能特性を予測できるようになる。
システムコールテーブル
Linuxカーネルでは、システムコール番号と実装関数の対応表が管理されている:
// arch/x86/entry/syscalls/syscall_64.tbl の抜粋
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
4 common stat sys_newstat
5 common fstat sys_newfstat
システムコール実行の詳細フロー
// ユーザー空間での write() システムコール例
ssize_t result = write(fd, buffer, count);
// 上記は以下のアセンブリに変換される:
mov rax, 1 // write のシステムコール番号
mov rdi, [fd] // 第1引数:ファイルディスクリプタ
mov rsi, [buffer] // 第2引数:バッファのアドレス
mov rdx, [count] // 第3引数:書き込みバイト数
syscall // カーネルモードへ遷移
// カーネル側での処理(簡略化)
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
if (f.file) {
ret = vfs_write(f.file, buf, count, &pos);
fdput_pos(f);
}
return ret;
}
パフォーマンス最適化技術
- vDSO(Virtual Dynamic Shared Object)
```
高速システムコールの実現:
- カーネル提供の共有ライブラリ
- ユーザー空間で実行されるシステムコール
- 特権レベル遷移が不要
vDSO対応システムコール例:
- gettimeofday():時刻取得
- clock_gettime():高精度時刻
- getcpu():現在のCPU番号取得
性能向上:10-100倍高速化 ```
- システムコールバッチング
// 効率の悪い例:個別のシステムコール for (int i = 0; i < 1000; i++) { write(fd, &data[i], sizeof(data[i])); // 1000回のシステムコール } // 効率の良い例:バッチ処理 write(fd, data, sizeof(data)); // 1回のシステムコール
- io_uring による高度な最適化
// 従来の非同期I/O for (int i = 0; i < num_operations; i++) { aio_read(&requests[i]); // 各操作でシステムコール } // io_uring による効率化 io_uring_prep_readv(sqe, fd, iovec, 1, offset); io_uring_submit(&ring); // 複数操作をまとめて投入
セキュアブートとカーネル完全性の検証
現代のシステムでは、起動時からカーネルの完全性を保証することが重要である。
# Secure Bootの設定確認
mokutil --sb-state
# カーネルモジュールの署名確認
modinfo -F signer <module_name>
# IMA(Integrity Measurement Architecture)ポリシーの設定
cat > /etc/ima/ima-policy << EOF
# 実行ファイルの測定
measure func=BPRM_CHECK
measure func=FILE_MMAP mask=MAY_EXEC
# モジュールの測定
measure func=MODULE_CHECK
# ファイルの検証
appraise func=BPRM_CHECK appraise_type=imasig
appraise func=MODULE_CHECK appraise_type=imasig
EOF
6.2 プロセススケジューリングの実装
CFSの設計思想と限界
Completely Fair Scheduler(CFS)は、Linux 2.6.23から導入されたプロセススケジューラーである。「完全に公平」という名前の通り、理想的なマルチタスキングプロセッサの動作を模倣することを目標としている。
仮想ランタイムの概念
CFSの核心概念は「仮想ランタイム(vruntime)」である:
// 仮想ランタイムの計算式
vruntime = 実際の実行時間 × (NICE_0_LOAD / プロセスの重み)
// NICE値による重み(抜粋)
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
// 例:nice値-10のプロセス
// 重み = 9548
// vruntime = 実行時間 × (1024 / 9548) ≈ 実行時間 × 0.107
// → 同じ実行時間でも、vruntime の増加が遅い(高優先度)
赤黒木による効率的な管理
CFSは赤黒木(Red-Black Tree)データ構造を使用してプロセスを管理する:
struct sched_entity {
struct rb_node run_node; // 赤黒木のノード
u64 vruntime; // 仮想ランタイム
u64 exec_start; // 実行開始時刻
u64 sum_exec_runtime; // 累積実行時間
// その他のスケジューリング情報
};
// スケジューリング決定
// 1. 赤黒木の最左ノード(最小vruntime)を取得:O(log n)
// 2. そのプロセスを実行
// 3. 実行後、vruntimeを更新して適切な位置に再挿入:O(log n)
CFSの利点と限界
利点:
- O(log n)の計算量:多数のプロセスでも効率的
- 完全な公平性:長期的には全プロセスが等しい時間を得る
- 応答性:新しいプロセスの迅速な実行開始
限界:
1. レイテンシ敏感なワークロード:
問題:タイムスライス(通常6-24ms)がリアルタイム要求を満たさない
例:音声処理(1-5ms要求)、高頻度取引(マイクロ秒レベル)
2. キャッシュ効率:
問題:頻繁なコンテキストスイッチによるキャッシュ汚染
例:科学計算(大きなデータセット)、データベース(バッファプール)
3. NUMA環境での課題:
問題:プロセスの異なるNUMAノード間での移動
メモリアクセス性能の劣化
リアルタイムスケジューラーの適用判断
リアルタイムスケジューラーは、決定論的な応答時間を要求するアプリケーション向けに設計されている。ただし、その使用には慎重な判断が必要である。
リアルタイムスケジューリング方式
// SCHED_FIFO:先入先出し
// 最高優先度のプロセスが実行継続(yield、block、高優先度プロセス登場まで)
struct sched_param param;
param.sched_priority = 80; // 1-99(99が最高優先度)
sched_setscheduler(getpid(), SCHED_FIFO, ¶m);
// SCHED_RR:ラウンドロビン
// 同優先度内でタイムスライスによる切り替え
param.sched_priority = 50;
sched_setscheduler(getpid(), SCHED_RR, ¶m);
// SCHED_DEADLINE:デッドライン指向
// 各タスクにデッドライン、実行時間、周期を設定
struct sched_attr attr;
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 10 * 1000 * 1000; // 10ms
attr.sched_deadline = 30 * 1000 * 1000; // 30ms
attr.sched_period = 100 * 1000 * 1000; // 100ms
sched_setattr(getpid(), &attr, 0);
適用が適切な場面
- 音声・映像処理
```
要求:一定間隔での処理(44.1kHz音声 = 22.7μs間隔)
設定例:
- SCHED_FIFO、優先度90
- CPUアフィニティ設定(専用コア)
- メモリロック(swapの防止) ```
- 制御システム
```
要求:センサー読み取り→計算→アクチュエータ制御の決定論的実行
設定例:
- SCHED_DEADLINE
- 周期:制御ループ周期(例:1ms)
- 実行時間:最悪実行時間の見積もり ```
- 高頻度取引
```
要求:市場データの受信→判断→注文送信の最小遅延
設定例:
- SCHED_FIFO、最高優先度
- 割り込み無効化(局所的)
- 専用CPU隔離(isolcpus) ```
使用時の注意点
# システム保護設定の確認
cat /proc/sys/kernel/sched_rt_runtime_us # RT実行時間制限
cat /proc/sys/kernel/sched_rt_period_us # RT周期
# デフォルト:950ms/1000ms(95%)がRT実行時間の上限
# 残り5%は通常プロセス用に予約(システム保護)
# 制限の調整(注意深く)
echo 980000 > /proc/sys/kernel/sched_rt_runtime_us # 98%に拡大
# CPU隔離設定(ブート時)
# isolcpus=2,3 rcu_nocbs=2,3 nohz_full=2,3
# CPU 2,3を通常スケジューリングから隔離
監視とトラブルシューティング
# リアルタイムプロセスの確認
ps -eo pid,comm,class,priority,rtprio,ni | grep -E "(FF|RR|DLN)"
# スケジューリング統計
cat /proc/schedstat
# プロセス別詳細情報
cat /proc/[PID]/sched
# レイテンシ測定ツール
cyclictest -p 90 -m -n -a3 -t1 -d0
# 結果例:最大レイテンシが10μs以下なら良好
6.3 メモリ管理の詳細実装
仮想メモリとページング戦略
仮想メモリは、プロセスごとに独立したアドレス空間を提供し、物理メモリの効率的な利用を実現する。この仕組みを理解することで、メモリ関連の性能問題を的確に分析できる。
ページテーブルの階層構造
x86_64アーキテクチャでは、4段階のページテーブルを使用する:
仮想アドレス(48ビット)の分割:
[47:39] PML4 Index (Page Map Level 4)
[38:30] PDP Index (Page Directory Pointer)
[29:21] PD Index (Page Directory)
[20:12] PT Index (Page Table)
[11:0] Offset (ページ内オフセット)
アドレス変換の流れ:
1. CR3レジスタからPML4テーブルのベースアドレス取得
2. PML4[PML4_index] → PDPテーブルアドレス
3. PDP[PDP_index] → PDテーブルアドレス
4. PD[PD_index] → PTテーブルアドレス
5. PT[PT_index] → 物理ページアドレス
6. 物理ページアドレス + オフセット → 最終物理アドレス
TLB(Translation Lookaside Buffer)の重要性
アドレス変換の性能:
TLBヒット:1-2 CPU cycles
TLBミス:100-1000 CPU cycles(メモリアクセス4回)
TLBの階層構造(Intel Skylake例):
L1 DTLB:64エントリ(4KBページ)+ 32エントリ(2MB/4MBページ)
L1 ITLB:128エントリ(4KBページ)+ 8エントリ(2MB/4MBページ)
L2 STLB:1536エントリ(4KBページ + ラージページ)
TLBミス率の改善策:
1. ラージページ(Huge Pages)の使用
2. メモリアクセスパターンの最適化
3. プロセスのメモリフットプリント削減
ページフォルト処理
// ページフォルトの種類と処理時間
1. マイナーフォルト(Soft Page Fault)
原因:ページは物理メモリ内にあるが、ページテーブルエントリが未設定
例:fork()後のCopy-on-Write、mmap()の遅延割り当て
処理時間:1,000-5,000 cycles(1-2μs)
2. メジャーフォルト(Hard Page Fault)
原因:ページがディスク(スワップ)に退避されている
処理時間:10,000,000-100,000,000 cycles(3-30ms)
3. セグメンテーションフォルト
原因:不正なメモリアクセス(未割り当て領域、権限違反)
結果:プロセス終了(SIGSEGV)
Huge Pages による最適化
# Huge Pages の設定と効果
echo 1024 > /proc/sys/vm/nr_hugepages # 2GB分のHuge Pages確保
# アプリケーションでの利用例
void* ptr = mmap(NULL, 1024*1024*1024, // 1GB
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
# 効果:
# 通常ページ(4KB):1GBで256,000エントリ必要
# Huge Pages(2MB):1GBで512エントリのみ
# TLBミス率が大幅に削減(特に大量メモリ使用アプリ)
# 監視
cat /proc/meminfo | grep -i huge
AnonHugePages: 102400 kB
HugePages_Total: 1024
HugePages_Free: 512
HugePages_Rsvd: 0
Hugepagesize: 2048 kB
OOM Killerの動作と回避策
OOM(Out of Memory)Killerは、システムのメモリ不足時に動作する緊急メカニズムである。その動作を理解し、適切な対策を講じることで、重要なプロセスの予期しない終了を防げる。
OOM Killerのスコア計算
// OOMスコアの基本計算式
oom_score = (プロセスのメモリ使用量 / 総物理メモリ) × 1000 + oom_score_adj
// メモリ使用量の詳細
使用メモリ = RSS + SWAP使用量 + 子プロセスのメモリ使用量(一部)
// oom_score_adj による調整
範囲:-1000 ~ +1000
-1000:OOM Killerから完全に保護(カーネルプロセス用)
0:調整なし(デフォルト)
+1000:優先的に終了対象
プロセス選択のアルゴリズム
# 現在のOOMスコア確認
for pid in $(ps -eo pid --no-headers); do
if [[ -r /proc/$pid/oom_score ]]; then
score=$(cat /proc/$pid/oom_score 2>/dev/null)
adj=$(cat /proc/$pid/oom_score_adj 2>/dev/null)
comm=$(cat /proc/$pid/comm 2>/dev/null)
echo "PID:$pid Score:$score Adj:$adj Command:$comm"
fi
done | sort -k2 -nr | head -10
# 出力例:
# PID:1234 Score:512 Adj:0 Command:java
# PID:5678 Score:256 Adj:0 Command:python
# PID:9012 Score:128 Adj:0 Command:mysqld
OOM発生時の詳細ログ
# /var/log/messages または dmesg での確認例
Out of memory: Kill process 1234 (java) score 512 or sacrifice child
Killed process 1234 (java) total-vm:4194304kB, anon-rss:2097152kB, file-rss:0kB, shmem-rss:0kB
# ログ情報の解読:
# total-vm:仮想メモリ総使用量
# anon-rss:匿名ページ(ヒープ、スタック)の物理メモリ使用量
# file-rss:ファイルマップページの物理メモリ使用量
# shmem-rss:共有メモリの物理メモリ使用量
OOM回避策の実装
- 重要プロセスの保護
# データベースサーバーの保護 echo -500 > /proc/$(pidof mysqld)/oom_score_adj # systemd サービスでの設定 [Service] OOMScoreAdjust=-500
- cgroupによるメモリ制限
# cgroup v2での設定 mkdir /sys/fs/cgroup/myapp echo "2G" > /sys/fs/cgroup/myapp/memory.max echo $$ > /sys/fs/cgroup/myapp/cgroup.procs # アプリケーション起動 ./myapp # メモリ制限に達した場合、OOMではなくcgroup内でkill
- swap設定による調整
# swappiness の調整 echo 10 > /proc/sys/vm/swappiness # デフォルト60から削減 # 効果: # 高値(60-100):積極的にswap使用、OOM遅延 # 低値(0-10):swap使用を控える、応答性重視 # 0:swap無効(非推奨、OOM発生率増加)
予防的監視
#!/bin/bash
# メモリ監視スクリプト例
THRESHOLD=90 # 90%でアラート
while true; do
MEMORY_USAGE=$(free | awk 'NR==2{printf "%.0f", $3*100/$2}')
if [[ $MEMORY_USAGE -gt $THRESHOLD ]]; then
echo "WARNING: Memory usage is ${MEMORY_USAGE}%"
# 上位メモリ消費プロセス
ps -eo pid,ppid,cmd,%mem --sort=-%mem | head -10
# 必要に応じてアラート送信
# send_alert "High memory usage: ${MEMORY_USAGE}%"
fi
sleep 60
done
6.4 I/Oサブシステムの設計
同期I/Oと非同期I/Oの実装差異
I/O操作は、多くのアプリケーションにとってボトルネックとなる。同期I/Oと非同期I/Oの違いを理解し、適切な選択を行うことで、大幅な性能向上が期待できる。
従来の同期I/O
// ブロッキングI/Oの例
ssize_t bytes_read = read(fd, buffer, size);
// この時点でデータの読み込みが完了
// プロセスは read() 完了まで待機(ブロック)
// 内部動作:
// 1. システムコール発行
// 2. カーネルがディスクI/O開始
// 3. プロセスはSLEEP状態に移行
// 4. I/O完了時にプロセスがRUNNABLE状態に復帰
// 5. データをユーザー空間にコピー
// 6. システムコールから復帰
問題点:
- I/O待機中のCPU時間の浪費
- 同時実行可能なI/O数の制限
- スレッド/プロセス数の増大によるオーバーヘッド
POSIX AIO(非同期I/O)
#include <aio.h>
// 非同期読み込み開始
struct aiocb aio_req;
memset(&aio_req, 0, sizeof(aio_req));
aio_req.aio_fildes = fd;
aio_req.aio_buf = buffer;
aio_req.aio_nbytes = size;
aio_req.aio_offset = 0;
int result = aio_read(&aio_req);
// この時点では読み込み開始のみ、完了を待たない
// 他の処理を実行可能
do_other_work();
// 完了確認
while (aio_error(&aio_req) == EINPROGRESS) {
// まだ完了していない
usleep(1000); // 1ms待機
}
// 結果取得
ssize_t bytes_read = aio_return(&aio_req);
課題:
- Linux実装の制約(内部でスレッドプール使用)
- デバッグの困難さ
- エラーハンドリングの複雑化
io_uringによる高速化の原理
io_uringは、Linuxカーネル5.1で導入された新しい非同期I/Oインターフェースである。従来のシステムコールベースI/Oの制約を解決し、劇的な性能向上を実現する。
従来手法の課題
システムコールオーバーヘッド問題:
1. 各I/O操作でカーネル/ユーザー空間の切り替え
2. システムコール実行のたびにコンテキストスイッチ
3. 小さなI/O操作では、オーバーヘッドがデータ処理時間を上回る
例:1MB のファイルを 4KB ずつ読み込む場合
システムコール回数:256回
総オーバーヘッド:256 × 300ns = 76.8μs
実際のI/O時間:~100μs
オーバーヘッド比率:43%
io_uringの革新的アプローチ
#include <liburing.h>
// io_uring初期化
struct io_uring ring;
io_uring_queue_init(256, &ring, 0); // 256エントリのキュー
// バッチでI/O操作を投入
for (int i = 0; i < 100; i++) {
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buffers[i], 4096, i * 4096);
sqe->user_data = i; // 識別用データ
}
// バッチ投入(1回のシステムコール)
io_uring_submit(&ring);
// 他の処理実行
do_other_work();
// 完了確認(ポーリングまたはブロッキング)
struct io_uring_cqe *cqe;
for (int completed = 0; completed < 100; ) {
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret == 0) {
int request_id = cqe->user_data;
ssize_t result = cqe->res;
// 結果処理
io_uring_cqe_seen(&ring, cqe);
completed++;
}
}
性能向上のメカニズム
- 共有メモリリング
Submission Queue (SQ):ユーザー→カーネル(I/O要求) Completion Queue (CQ):カーネル→ユーザー(完了通知) 利点: - システムコール回数の削減 - メモリコピーの削減 - CPU キャッシュ効率の向上
- ポーリングモード
// SQ Polling:カーネルが継続的にSQを監視 io_uring_queue_init(256, &ring, IORING_SETUP_SQPOLL); // 効果:io_uring_submit() システムコールが不要 // カーネルスレッドが自動的にI/O処理
- カーネルバイパス(特定条件下)
```
条件:
- ファイルがページキャッシュに存在
- O_DIRECT使用時の特定パターン
効果:
- I/O完了の即座の検出
- 割り込み処理の削減 ```
実測性能比較
# fio(Flexible I/O Tester)による測定例
# 従来の同期I/O
fio --name=sync --rw=randread --size=1G --bs=4k --ioengine=sync
# POSIX AIO
fio --name=aio --rw=randread --size=1G --bs=4k --ioengine=libaio --iodepth=32
# io_uring
fio --name=uring --rw=randread --size=1G --bs=4k --ioengine=io_uring --iodepth=32
# 典型的な結果(NVMe SSD):
# sync: 100,000 IOPS, CPU使用率 80%
# libaio: 300,000 IOPS, CPU使用率 60%
# io_uring: 500,000 IOPS, CPU使用率 40%
実装時の考慮事項
// エラーハンドリング
if (cqe->res < 0) {
int error = -cqe->res;
switch (error) {
case EAGAIN:
// リソース不足、後で再試行
retry_request(request_id);
break;
case EIO:
// I/Oエラー、アプリケーションレベルで対処
handle_io_error(request_id);
break;
default:
// その他のエラー
log_error("Unexpected error: %d", error);
}
}
// メモリ管理
// io_uringは固定バッファを効率的に処理
struct iovec iovecs[1024];
io_uring_register_buffers(&ring, iovecs, 1024);
// 固定バッファ使用のI/O(コピー削減)
io_uring_prep_read_fixed(sqe, fd, buffer, size, offset, buffer_index);
6.5 カーネルチューニングの実践
sysctlパラメータの影響範囲
カーネルパラメータの調整は、システム全体の性能と安定性に大きな影響を与える。各パラメータの意味と相互作用を理解し、慎重に調整する必要がある。
ネットワーク関連のチューニング
# TCP バッファサイズの最適化
# 現在の設定確認
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem
# 高帯域・高遅延環境での設定例
net.ipv4.tcp_rmem = 16384 131072 134217728 # 16KB, 128KB, 128MB
net.ipv4.tcp_wmem = 16384 131072 134217728 # 送信バッファ
# 帯域遅延積(BDP)計算例:
# 1Gbps × 100ms RTT = 12.5MB
# 設定値(128MB)は十分な余裕を持つ
# TCP輻輳制御アルゴリズム
net.ipv4.tcp_congestion_control = bbr
# TCP Fast Open(再接続高速化)
net.ipv4.tcp_fastopen = 3 # クライアント・サーバー両方で有効
# SYN flood対策
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
メモリ管理のチューニング
# スワップ動作の調整
vm.swappiness = 10 # デフォルト60から削減
# 効果の定量化:
# 60:物理メモリの40%使用時からスワップ開始
# 10:物理メモリの90%使用時までスワップ抑制
# 0:カーネルがOOM状態まで追い込まれる可能性
# ダーティページの書き込み制御
vm.dirty_ratio = 15 # 物理メモリの15%がダーティページ上限
vm.dirty_background_ratio = 5 # 5%でバックグラウンド書き込み開始
# 影響:
# 高い値:メモリ効率向上、書き込みバーストリスク
# 低い値:安定性向上、メモリ効率低下
# プロセス数制限(fork bomb対策)
kernel.pid_max = 32768
kernel.threads-max = 65536
ファイルシステム関連
# ファイルディスクリプタ制限
fs.file-max = 2097152 # システム全体の上限
fs.nr_open = 1048576 # プロセスあたりの上限
# 現在の使用状況確認
cat /proc/sys/fs/file-nr
# 出力例:1024 0 2097152
# 使用中 未使用 最大値
# inode/dentry キャッシュ
vm.vfs_cache_pressure = 50 # デフォルト100から削減
# 効果:
# 100:均等にキャッシュ回収
# 50:inode/dentryキャッシュを優先保持
# 200:inode/dentryキャッシュを積極的に回収
パフォーマンス測定と最適化
システムの性能問題を特定し、適切な対策を講じるためには、体系的な測定と分析が不可欠である。
CPU性能の詳細分析
# perf による包括的分析
perf record -g ./application
perf report
# 出力例:
# 56.78% application [kernel.kallsyms] [k] copy_user_generic_string
# 12.34% application libc-2.31.so [.] __memcpy_avx_unaligned
# 8.90% application application [.] compute_heavy_function
# ホットスポットの特定:
# 1. カーネル関数が上位→システムコール最適化検討
# 2. ライブラリ関数が上位→使用方法の見直し
# 3. アプリケーション関数が上位→アルゴリズム最適化
# CPU使用率の詳細内訳
perf stat -d ./application
# 出力例:
# 1,234,567,890 cycles
# 987,654,321 instructions # 0.80 insn per cycle
# 123,456,789 cache-misses # 10.0% of all cache refs
# 12,345,678 page-faults # 0.01 M/sec
# 解釈:
# insn per cycle < 1.0:メモリ待機またはパイプライン停止
# cache-miss率 > 5%:キャッシュ効率の改善余地
# page-fault数:メモリアクセスパターンの問題
メモリアクセス性能の最適化
# メモリ帯域幅の測定
# STREAM ベンチマーク
./stream
# 結果例:
# Copy: 25000.0 MB/s
# Scale: 24000.0 MB/s
# Add: 23000.0 MB/s
# Triad: 22000.0 MB/s
# 理論値との比較:
# DDR4-3200:理論値 25.6GB/s
# 実測値(Triad):22GB/s
# 効率:86%(良好)
# NUMA topology の確認
numactl --hardware
# 出力例:
# node 0 cpus: 0 1 2 3 4 5 6 7
# node 1 cpus: 8 9 10 11 12 13 14 15
# node distances:
# node 0 1
# 0: 10 21
# 1: 21 10
# NUMAアフィニティの最適化例
numactl --cpunodebind=0 --membind=0 ./application
I/O性能の総合診断
# iostat による I/O 統計
iostat -x 1
# 重要な指標:
# %util:デバイス使用率(100%に近いと飽和)
# await:平均応答時間(msec)
# r/s, w/s:読み書きIOPS
# rkB/s, wkB/s:読み書きスループット
# 出力例解釈:
# Device r/s w/s rkB/s wkB/s await %util
# nvme0n1 1000 50 40000 2000 2.5 85.0
# 分析:
# IOPS:1050(読み書き合計)
# スループット:42MB/s
# 応答時間:2.5ms(SSDとしては標準的)
# 使用率:85%(やや高負荷)
# I/O パターンの詳細分析
iotop -ao # 累積I/O量でソート
# ブロックI/Oトレース
blktrace -d /dev/nvme0n1 -o trace
blkparse trace.nvme0n1.0
# 結果から読み取れる情報:
# - シーケンシャル vs ランダムアクセス比率
# - I/Oサイズ分布
# - キューイング深度
システム統合監視
#!/bin/bash
# 統合監視スクリプト例
# CPU使用率(user, system, wait)
CPU_STATS=$(sar -u 1 1 | tail -1)
CPU_USER=$(echo $CPU_STATS | awk '{print $3}')
CPU_SYS=$(echo $CPU_STATS | awk '{print $5}')
CPU_WAIT=$(echo $CPU_STATS | awk '{print $6}')
# メモリ使用率
MEM_STATS=$(free | awk 'NR==2{printf "%.1f", $3*100/$2}')
# I/O統計
IO_STATS=$(iostat -x 1 1 | grep -E "nvme|sda" | head -1)
IO_UTIL=$(echo $IO_STATS | awk '{print $NF}')
# ネットワーク統計
NET_STATS=$(sar -n DEV 1 1 | grep eth0 | tail -1)
NET_RX=$(echo $NET_STATS | awk '{print $5}')
NET_TX=$(echo $NET_STATS | awk '{print $6}')
# アラート条件
if (( $(echo "$CPU_WAIT > 20" | bc -l) )); then
echo "ALERT: High I/O wait time: ${CPU_WAIT}%"
fi
if (( $(echo "$MEM_STATS > 90" | bc -l) )); then
echo "ALERT: High memory usage: ${MEM_STATS}%"
fi
if (( $(echo "$IO_UTIL > 85" | bc -l) )); then
echo "ALERT: High I/O utilization: ${IO_UTIL}%"
fi
# ログ出力
echo "$(date): CPU(usr:${CPU_USER}% sys:${CPU_SYS}% wait:${CPU_WAIT}%) MEM:${MEM_STATS}% IO:${IO_UTIL}% NET(rx:${NET_RX} tx:${NET_TX})"
まとめ
オペレーティングシステムの内部構造を理解することで、システムの動作を表面的な現象ではなく、根本的な原理から把握できるようになる。カーネル空間とユーザー空間の分離、プロセススケジューリングの仕組み、メモリ管理の複雑さ、I/Oサブシステムの進化。これらの知識は、性能問題の的確な診断と効果的な最適化の基盤となる。
特に重要なのは、各技術の設計思想と限界を理解することである。CFSは公平性を重視するが、リアルタイム性には限界がある。仮想メモリは柔軟性を提供するが、TLBミスのオーバーヘッドが存在する。io_uringは高性能だが、実装の複雑性が増大する。
これらのトレードオフを理解し、アプリケーションの要求に応じた適切な選択を行うことが、効果的なシステム設計の鍵となる。次章では、このOS基盤の上で動作するストレージシステムの設計と実装を探る。データの永続化というより具体的な課題において、OS の知識がどのように活用されるかを見ていく。