- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. なぜOS知識が重要なのか
- 2. プロセス管理
- 3. スレッド(Thread)
- 4. CPUスケジューリング
- 5. 同期化(Synchronization)
- 6. デッドロック(Deadlock)
- 7. メモリ管理
- 8. 仮想メモリ
- 9. ファイルシステム
- 10. I/O管理
- 11. Linuxカーネル基礎
- 12. コンテナのOS観点
- 13. 面接質問25選
- 14. クイズ
- 参考資料
はじめに
オペレーティングシステム(OS)の知識は、開発者面接で最も頻繁に出題されるCS基礎分野の一つです。単なる面接対策を超えて、OS概念を深く理解すれば、パフォーマンス最適化、同時実行バグの解決、システム設計で確実な差を生み出すことができます。
この記事では、プロセス管理、スレッド、CPUスケジューリング、同期化、デッドロック、メモリ管理、仮想メモリ、ファイルシステム、I/O管理、Linuxカーネル基礎、そしてコンテナのOS観点まで — 開発者が知るべきOSの全てを実践コードと共に体系的に整理します。
1. なぜOS知識が重要なのか
面接で頻出する理由
ほぼ全ての技術面接でOS質問が登場します。特に以下の質問が頻出です。
- プロセスとスレッドの違いを説明してください
- デッドロックの4つの条件と解決方法は?
- 仮想メモリとは何で、なぜ必要ですか?
- コンテキストスイッチのコストを減らす方法は?
- ミューテックスとセマフォの違いは?
実務での重要性
- パフォーマンス最適化: CPUキャッシュ、メモリ階層、I/Oパターンの理解が必須
- 並行プログラミング: レースコンディション、デッドロック防止のための同期理解
- システム設計: プロセス間通信、分散システムの基礎
- トラブルシューティング: strace、perf、eBPF等のシステムツール活用
- コンテナ/クラウド: namespace、cgroupsの理解がDocker/K8s活用の核心
2. プロセス管理
プロセスとは?
プロセスは実行中のプログラムのインスタンスです。各プロセスは独立したメモリ空間を持ちます。
PCB(Process Control Block)
┌─────────────────────────────────┐
│ PCB (Process │
│ Control Block) │
├─────────────────────────────────┤
│ Process ID (PID) │
│ Process State │
│ Program Counter (PC) │
│ CPU Registers │
│ CPU Scheduling Info │
│ Memory Management Info │
│ I/O Status Info │
│ Accounting Info │
└─────────────────────────────────┘
fork/exec — プロセス生成
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // プロセス複製
if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子プロセス
printf("Child PID: %d, Parent PID: %d\n", getpid(), getppid());
execlp("ls", "ls", "-la", NULL);
perror("exec failed");
} else {
// 親プロセス
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid);
int status;
waitpid(pid, &status, 0);
printf("Child exited with status: %d\n", WEXITSTATUS(status));
}
return 0;
}
Copy-on-Write(COW)
fork()時に親と子は最初同じ物理ページを共有します。一方が書き込みを試みた時にページをコピーします。これにより不要なメモリコピーを防止します。
IPC(プロセス間通信)
| IPC方式 | 特徴 | 使用例 |
|---|---|---|
| Pipe | 単方向、親子間 | シェルパイプライン |
| Named Pipe (FIFO) | 双方向、非関連プロセス間 | 簡単なデータ転送 |
| Socket | 双方向、ネットワーク対応 | クライアント-サーバー通信 |
| Shared Memory | 最速、同期化必要 | 高性能データ交換 |
| Message Queue | 非同期、バッファ | タスクキュー、イベントシステム |
| Signal | 非同期通知 | プロセス制御(SIGTERM, SIGKILL) |
3. スレッド(Thread)
プロセス vs スレッド
プロセス A プロセス B
┌───────────────┐ ┌───────────────┐
│ Code │ Data │ │ Code │ Data │
│───────│───────│ │───────│───────│
│ Heap │ Stack │ │ Heap │ Stack │
└───────────────┘ └───────────────┘
独立メモリ空間 独立メモリ空間
プロセス C(マルチスレッド)
┌───────────────────────────────┐
│ Code(共有) │ Data(共有) │
│────────────│──────────────────│
│ Heap(共有) │ Stack1 │ Stack2 │
│ │ (T1) │ (T2) │
└───────────────────────────────┘
| 項目 | プロセス | スレッド |
|---|---|---|
| メモリ空間 | 独立 | 共有(Code, Data, Heap) |
| 生成コスト | 高い | 低い |
| コンテキストスイッチ | 高コスト(TLBフラッシュ) | 低コスト |
| 通信 | IPC必要 | 共有メモリ直接アクセス |
| 安定性 | クラッシュが他に影響なし | 1スレッドのクラッシュが全体に影響 |
POSIX pthread(C)
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 4
typedef struct {
int thread_id;
int start;
int end;
long result;
} ThreadArg;
void* sum_range(void* arg) {
ThreadArg* targ = (ThreadArg*)arg;
targ->result = 0;
for (int i = targ->start; i <= targ->end; i++) {
targ->result += i;
}
printf("Thread %d: sum(%d..%d) = %ld\n",
targ->thread_id, targ->start, targ->end, targ->result);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
ThreadArg args[NUM_THREADS];
int range_per_thread = 250;
for (int i = 0; i < NUM_THREADS; i++) {
args[i].thread_id = i;
args[i].start = i * range_per_thread + 1;
args[i].end = (i + 1) * range_per_thread;
pthread_create(&threads[i], NULL, sum_range, &args[i]);
}
long total = 0;
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
total += args[i].result;
}
printf("Total sum: %ld\n", total); // 500500
return 0;
}
Go goroutine
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
results := make(chan int, 4)
for i := 0; i < 4; i++ {
wg.Add(1)
go func(id, start, end int) {
defer wg.Done()
sum := 0
for j := start; j <= end; j++ {
sum += j
}
results <- sum
fmt.Printf("Goroutine %d: sum(%d..%d) = %d\n", id, start, end, sum)
}(i, i*250+1, (i+1)*250)
}
go func() {
wg.Wait()
close(results)
}()
total := 0
for r := range results {
total += r
}
fmt.Printf("Total: %d\n", total)
}
Goのgoroutineは数KBのスタックで開始し、GoランタイムスケジューラがM:Nスレッディングを管理します。
Python GIL(Global Interpreter Lock)
import threading
import multiprocessing
import time
# CPU-bound作業:GILによりスレッドは利点なし
def cpu_bound(n):
total = 0
for i in range(n):
total += i * i
return total
# スレッド方式(GIL制限)
def thread_test():
threads = [threading.Thread(target=cpu_bound, args=(10_000_000,))
for _ in range(4)]
start = time.time()
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Threads: {time.time() - start:.2f}s")
# プロセス方式(GIL回避)
def process_test():
processes = [multiprocessing.Process(target=cpu_bound, args=(10_000_000,))
for _ in range(4)]
start = time.time()
for p in processes:
p.start()
for p in processes:
p.join()
print(f"Processes: {time.time() - start:.2f}s")
Python 3.13+では実験的にGIL-freeビルドがサポートされ始めました(PEP 703)。
4. CPUスケジューリング
スケジューリングアルゴリズム比較
| アルゴリズム | 特徴 | メリット | デメリット |
|---|---|---|---|
| FCFS | 先着順 | 実装が簡単 | コンボイ効果 |
| SJF | 最短ジョブ優先 | 最小平均待ち時間 | 飢餓問題 |
| Round Robin | タイムクォンタム循環 | 公平、応答時間良い | クォンタム設定が重要 |
| Priority | 優先度ベース | 重要タスクの高速処理 | 飢餓問題(エイジングで解決) |
| CFS | Linux標準 | 公平なCPU時間配分 | レイテンシ保証困難 |
CFS(Completely Fair Scheduler)
CFSは全プロセスに公平なCPU時間を割り当てることを目標とします。赤黒木を使用し、vruntime(仮想実行時間)が最小のプロセスを次に実行します。
# nice値でプロセス優先度調整(-20 ~ +19)
nice -n 10 ./my_program # 低い優先度で実行
renice -n -5 -p 1234 # PID 1234の優先度を上げる
# cgroupsでCPUリソース制限
mkdir /sys/fs/cgroup/my_group
echo "25000 50000" > /sys/fs/cgroup/my_group/cpu.max # 50%制限
echo 1234 > /sys/fs/cgroup/my_group/cgroup.procs
5. 同期化(Synchronization)
レースコンディション
// レースコンディション例 — 同期化なしのカウンター
#include <pthread.h>
#include <stdio.h>
int counter = 0; // 共有変数
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
counter++; // アトミックでない!(read -> modify -> write)
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter: %d (expected 2000000)\n", counter);
// 実際には2000000より小さい値を出力
return 0;
}
ミューテックス(Mutex)
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void* safe_increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock); // ロック取得
counter++; // クリティカルセクション
pthread_mutex_unlock(&lock); // ロック解放
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, safe_increment, NULL);
pthread_create(&t2, NULL, safe_increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter: %d\n", counter); // 正確に2000000
return 0;
}
ミューテックス vs セマフォ vs スピンロック
| 特性 | ミューテックス | セマフォ | スピンロック |
|---|---|---|---|
| 同時アクセス数 | 1 | N | 1 |
| 待機方式 | Sleep(ブロッキング) | Sleep(ブロッキング) | ビジーウェイト |
| 所有権 | あり | なし | あり |
| 適合する場合 | 一般的相互排除 | リソースプール管理 | 短いクリティカルセクション |
6. デッドロック(Deadlock)
デッドロックの4つの必要条件
- 相互排除(Mutual Exclusion): リソースを一度に1つのプロセスだけ使用
- 保持と待機(Hold and Wait): リソースを保持したまま他のリソースを待機
- 非奪取(No Preemption): 他プロセスのリソースを強制的に奪えない
- 循環待機(Circular Wait): プロセスが循環的にリソースを待機
デッドロック例(Python)
import threading
import time
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread_1():
lock_a.acquire()
time.sleep(0.1)
lock_b.acquire() # デッドロック!Thread 2がlock_bを保持中
lock_b.release()
lock_a.release()
def thread_2():
lock_b.acquire()
time.sleep(0.1)
lock_a.acquire() # デッドロック!Thread 1がlock_aを保持中
lock_a.release()
lock_b.release()
デッドロック解決戦略
1. 予防 — 循環待機を排除(ロック順序を強制)
def safe_thread_1():
lock_a.acquire() # 常にlock_aを先に
lock_b.acquire()
lock_b.release()
lock_a.release()
def safe_thread_2():
lock_a.acquire() # 常にlock_aを先に(同じ順序)
lock_b.acquire()
lock_b.release()
lock_a.release()
2. 回避 — Banker's Algorithm
3. 検出と回復 — リソース割当グラフでサイクルを検出
4. 無視(Ostrich Algorithm) — デッドロックが稀な場合、発生時にシステムを再起動
7. メモリ管理
メモリ階層構造
┌────────────────────┐ 速度:非常に速い
│ CPU Registers │ 容量:数KB
├────────────────────┤
│ L1 Cache │ ~1ns, 64KB
├────────────────────┤
│ L2 Cache │ ~4ns, 256KB
├────────────────────┤
│ L3 Cache │ ~12ns, 数MB
├────────────────────┤
│ Main Memory │ ~100ns, 数GB
├────────────────────┤
│ SSD │ ~100us, 数TB
├────────────────────┤ 速度:非常に遅い
│ HDD │ ~10ms, 数TB
└────────────────────┘
ページング
論理アドレス空間 物理メモリ
┌───────────┐ ┌───────────┐
│ Page 0 │──────────▶ │ Frame 3 │
├───────────┤ ├───────────┤
│ Page 1 │──────────▶ │ Frame 7 │
├───────────┤ ├───────────┤
│ Page 2 │──────────▶ │ Frame 1 │
└───────────┘ └───────────┘
TLB(Translation Lookaside Buffer)
TLBはページテーブルのキャッシュです。コンテキストスイッチ時にTLBがフラッシュ(無効化)されるため、プロセス切り替えコストが高いです。スレッド切り替えはTLBフラッシュ不要です。
8. 仮想メモリ
デマンドページング
全ページを最初からメモリにロードせず、実際にアクセスした時のみロードします。
ページフォルト処理
1. CPUが論理アドレスにアクセス
2. ページテーブルで有効ビットを確認
3. 無効 → ページフォルト割込み発生
4. OSがディスクから該当ページを探す
5. 空きフレーム割当(なければページ置換)
6. ディスクからフレームにロード
7. ページテーブル更新(有効ビット = 1)
8. 中断された命令を再実行
ページ置換アルゴリズム
| アルゴリズム | 説明 | 性能 |
|---|---|---|
| FIFO | 最も先に入ったページを置換 | Beladyの異常あり |
| LRU | 最も長く使われてないページ | 良好、最適に近似 |
| Clock | LRU近似、参照ビット使用 | 良好、効率的 |
| Optimal | 最も遅く使われるページ | 理論的最適、実装不可 |
スラッシング(Thrashing)
プロセスがワーキングセットより少ないフレームを割り当てられると、ページフォルトが極度に頻発してCPU使用率が急激に低下する現象です。
解決策:ワーキングセットモデル、PFF調整、マルチプログラミング度の調整。
9. ファイルシステム
ext4 vs XFS
| 特性 | ext4 | XFS |
|---|---|---|
| 最大ファイルサイズ | 16TB | 8EB |
| 最大ボリュームサイズ | 1EB | 8EB |
| ジャーナリング | メタデータ + データ | メタデータのみ |
| 並列I/O | 普通 | 優秀 |
| 適合用途 | 汎用、小規模ファイル | 大容量ファイル、高性能 |
VFS(Virtual File System)レイヤー
VFSは様々なファイルシステムに対する統一されたインターフェースを提供します。
ユーザープログラム
|
v
VFS (Virtual File System)
|
+--> ext4
+--> XFS
+--> NFS
+--> procfs (/proc)
+--> sysfs (/sys)
10. I/O管理
epoll(Linux)
#include <sys/epoll.h>
int epoll_fd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // Edge-Triggered
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
int conn_fd = accept(listen_fd, ...);
} else {
handle_client(events[i].data.fd);
}
}
}
io_uring(Linux 5.1+)
io_uringはシステムコールオーバーヘッドなしで非同期I/Oを実行する最新Linuxインターフェースです。Submission Queue(SQ)とCompletion Queue(CQ)はカーネルとユーザースペースが共有するリングバッファです。
Zero-Copy
#include <sys/sendfile.h>
// ファイルをソケットに直接送信(ユーザー空間コピーなし)
sendfile(socket_fd, file_fd, NULL, file_size);
11. Linuxカーネル基礎
strace — システムコール追跡
# プロセスのシステムコール追跡
strace -p 1234
# 特定のシステムコールのみフィルタリング
strace -e trace=open,read,write ./my_program
# 統計要約
strace -c ./my_program
perf — パフォーマンス分析
# CPUプロファイリング
perf record -g ./my_program
perf report
# キャッシュミス確認
perf stat -e cache-misses,cache-references ./my_program
eBPF
eBPFはカーネルを修正せずにカーネル内部でプログラムを実行できる技術です。
# 全システムコールカウント
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# プロセス別ファイルオープン追跡
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%s opened %s\n", comm, str(args->filename));
}'
12. コンテナのOS観点
Dockerの実体 — Namespace + cgroups + Union FS
DockerコンテナはVMではありません。Linuxカーネルの機能を組み合わせてプロセスを隔離します。
Namespace — プロセス隔離
| Namespace | 隔離対象 | 説明 |
|---|---|---|
| PID | プロセスID | コンテナ内部でPID 1から開始 |
| NET | ネットワークスタック | 独立ネットワークインターフェース |
| MNT | ファイルシステムマウント | 独立マウントポイント |
| UTS | ホスト名 | 独立hostname |
| IPC | IPCリソース | 独立メッセージキュー、セマフォ |
| USER | ユーザー/グループID | コンテナ内rootを非特権ユーザーにマッピング |
cgroups — リソース制限
# Dockerのcgroupsリソース制限
docker run --cpus="0.5" # CPU 50%制限
docker run --memory="512m" # メモリ512MB制限
docker run --pids-limit=100 # 最大100プロセス
Union File System(OverlayFS)
OverlayFSは複数のファイルシステムレイヤーを一つにマージして表示します。書き込み時はCopy-on-Writeで上位レイヤーにのみ変更を記録します。
13. 面接質問25選
プロセス/スレッド(1-5)
Q1: プロセスとスレッドの違いを説明してください。
プロセスは独立したメモリ空間(Code, Data, Heap, Stack)を持つ実行単位です。スレッドは同じプロセス内でCode, Data, Heapを共有し、独立したStackのみ持ちます。
核心的な違い:
- メモリ:プロセスは独立、スレッドは共有
- 生成コスト:プロセスがはるかに高い
- 通信:プロセスはIPC必要、スレッドは共有メモリ直接アクセス
- 安定性:プロセス隔離がより安全
Q2: コンテキストスイッチとは何で、なぜコストが発生しますか?
コストの理由:
- PCB保存/復元: レジスタ、プログラムカウンター等の状態保存
- TLBフラッシュ: プロセス切り替え時TLB無効化
- キャッシュ無効化: L1/L2キャッシュデータが新プロセスと無関係
- パイプラインフラッシュ: CPUパイプラインの命令を破棄
Q3: fork()とexec()の違いを説明してください。
fork(): 現在のプロセスを複製して子プロセスを生成。COWで効率的。exec(): 現在のプロセスのメモリを新プログラムに置換。PIDは維持。
一般的パターン:fork()後に子プロセスでexec()を呼び出し。
Q4: ゾンビプロセスと孤児プロセスの違いは?
- ゾンビプロセス: 子が終了したが親がwait()を呼んでない状態。PCBのみ残存。
- 孤児プロセス: 親が先に終了した子プロセス。init(PID 1)が養親となりwait()を呼ぶため大きな問題なし。
Q5: IPC方法を比較してください。
- Pipe: 単方向、親子間。シェルパイプライン。
- Socket: 双方向、ネットワーク対応。クライアント-サーバー通信。
- Shared Memory: 最速、同期化必要。高性能データ交換。
- Message Queue: 非同期、バッファ。タスクキューシステム。
- Signal: 非同期通知。プロセス制御。
メモリ(6-10)
Q6: 仮想メモリとは何で、なぜ必要ですか?
仮想メモリは各プロセスに独立した連続的なアドレス空間を提供する抽象化レイヤーです。
必要な理由:
- メモリ保護:プロセス間のメモリアクセスを遮断
- メモリ拡張:物理メモリより大きなプログラム実行可能
- メモリ効率:実際使用するページのみ物理メモリにロード
- プログラミング簡素化:0番地から始まる連続アドレス使用
Q7: ページフォルト処理過程を説明してください。
- CPUが仮想アドレスにアクセス
- MMUがページテーブルで有効ビットを確認
- 無効 → ページフォルトトラップ発生
- OSがディスクからページ位置を探す
- 空き物理フレームがなければページ置換アルゴリズム実行
- ディスクから物理フレームにロード
- ページテーブル更新
- 中断された命令を再実行
Q8: LRUページ置換の実装方法は?
- カウンター方式: 各ページに最終アクセス時間を記録
- スタック方式: アクセス時にスタック最上部に移動
- 近似LRU(Clockアルゴリズム): 参照ビットを使用した循環リスト。実際のOSで最も多く使用。
Q9: 内部断片化と外部断片化の違いは?
- 内部断片化: 割り当てられたメモリブロック内部の未使用空間
- 外部断片化: 空きメモリの合計は十分だが連続的でなく割当不可能
ページングは外部断片化を排除しますが内部断片化が発生します。
Q10: スラッシングとは何で、どう防止しますか?
ワーキングセットより少ないフレームが割り当てられ、ページフォルトが極度に頻発する現象です。
防止:ワーキングセットモデル、PFF調整、マルチプログラミング度の調整。
同期化/デッドロック(11-15)
Q11: ミューテックスとセマフォの違いは?
ミューテックス:バイナリロック、所有権あり、1スレッドのみ。 セマフォ:カウンティング可能、所有権なし、N個のスレッド同時アクセス。
使用例:ミューテックスは相互排除、セマフォはリソースプール管理。
Q12: デッドロックの4条件とそれぞれを破る方法は?
- 相互排除: リソースを共有可能に変更
- 保持と待機: 全リソースを一度に要求、または保持リソースを解放してから要求
- 非奪取: リソースを強制的に奪うメカニズム導入
- 循環待機: リソースに番号を付けて常に昇順でのみ要求
実務で最も効果的:循環待機防止(ロック順序強制)。
Q13: スピンロックを使うべき時は?
適合条件:
- クリティカルセクションが非常に短い場合
- マルチコア環境
- コンテキストスイッチコスト > スピン待機コスト
不適合:長いクリティカルセクション、シングルコア環境。
Q14: プロデューサー-コンシューマー問題を説明してください。
import threading
import queue
buffer = queue.Queue(maxsize=10)
def producer():
for i in range(20):
buffer.put(i)
def consumer():
while True:
item = buffer.get()
if item is None:
break
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
buffer.put(None)
t2.join()
Q15: Priority Inversionとは何で、どう解決しますか?
高優先度タスクが低優先度タスクが保持するリソースを待機する状況。
解決:
- Priority Inheritance: 低優先度タスクが高優先度を一時的に継承
- Priority Ceiling: リソースの優先度上限を事前に設定
実例:Mars PathfinderのリセットバグがPriority Inversionが原因でした。
Linux/実践(16-25)
Q16: Linuxでファイルを削除してもディスク空間が回収されない場合は?
プロセスがファイルを開いたままの場合、ファイル名は削除されますがinodeは維持されます。
確認:lsof +L1
解決:該当プロセスを再起動。
Q17: LinuxのOOM Killerとは?
メモリが枯渇した時にカーネルがプロセスを選択して強制終了するメカニズムです。
保護:oom_score_adjを-1000に設定するとOOM Kill対象から除外。
Q18: epollのLevel TriggeredとEdge Triggeredの違いは?
- LT: 条件が維持される間継続的に通知。安全だが不要な呼び出し可能性。
- ET: 状態変更時のみ通知。一度に全データを読む必要あり。高性能だがプログラミング注意必要。
Q19: straceでどんな問題を診断できますか?
- ファイルアクセス問題
- ネットワーク問題
- パフォーマンス問題
- シグナル処理
- リソース不足
Q20: DockerコンテナがVMと異なる点をOS観点で説明してください。
- VM: ハイパーバイザー上に完全なゲストOSを実行。各VMは独自カーネルを持つ。
- コンテナ: ホストOSのカーネルを共有。Namespaceで隔離、cgroupsでリソース制限。
核心的な違い:コンテナはカーネルを共有するため起動が速くオーバーヘッドが低いが、カーネル脆弱性が全コンテナに影響。
Q21: CFSスケジューラの動作原理を説明してください。
- 各プロセスにvruntime(仮想実行時間)を追跡
- 赤黒木にvruntime順でソート
- 常にvruntimeが最小のプロセスを次に実行
- nice値が低いほどvruntime増加速度が遅い(より多くのCPU時間)
Q22: Copy-on-Writeの動作原理と活用事例は?
COWはリソースコピーを実際の修正が発生するまで遅延します。
活用事例:
- fork():子プロセス生成時のメモリコピー遅延
- mmap():ファイルマッピングの共有
- Redis RDB保存:fork()でスナップショット生成
Q23: カーネルモードとユーザーモードの違いは?
- ユーザーモード: 制限された命令のみ実行可能。ハードウェア直接アクセス不可。
- カーネルモード: 全命令とハードウェアにアクセス可能。特権命令実行可能。
モード切替コスト:数百ナノ秒のオーバーヘッド。
Q24: 割込み(Interrupt)とトラップ(Trap)の違いは?
- 割込み: 外部イベントによる非同期的信号。ハードウェアデバイスがCPUに通知。
- トラップ: ソフトウェアによる同期的信号。システムコール、ゼロ除算、ページフォルト等。
Q25: Linuxプロセスのメモリレイアウトを説明してください。
高いアドレス
┌─────────────────┐
│ Kernel Space │
├─────────────────┤
│ Stack │ ↓ 成長
├─────────────────┤
│ Shared Libs │
├─────────────────┤
│ Heap │ ↑ 成長
├─────────────────┤
│ BSS │ (未初期化グローバル/静的変数)
├─────────────────┤
│ Data │ (初期化済みグローバル/静的変数)
├─────────────────┤
│ Text(Code) │ (実行コード、読み取り専用)
└─────────────────┘
低いアドレス
/proc/PID/mapsで実際のメモリレイアウトを確認できます。
14. クイズ
クイズ1: プロセスがfork()を3回呼ぶと、合計何個のプロセスになりますか?
8個(2の3乗)
各fork()は現在存在する全プロセスを複製します。
- 1回目:1 → 2
- 2回目:2 → 4
- 3回目:4 → 8
クイズ2: リソースA, Bがあり、スレッド1はA→B順にロック、スレッド2もA→B順にロックする場合、デッドロックは発生しますか?
いいえ、デッドロックは発生しません。
両スレッドが同じ順序でロックするため、循環待機条件が満たされません。デッドロックはスレッド1がA->B、スレッド2がB->Aの順でロックする時に発生します。
クイズ3: TLBミスがページフォルトよりコストが低い理由は?
- TLBミス: メモリにあるページテーブルを参照。数百ナノ秒。
- ページフォルト: ディスクからページをロード。ミリ秒単位。TLBミスの数千~数万倍。
クイズ4: DockerでPID 1のプロセスが重要な理由は?
- シグナル処理:デフォルトハンドラが適用されずSIGTERMを無視する可能性
- ゾンビプロセス回収:PID 1が孤児プロセスの親となりwait()を呼ぶ必要
- コンテナ終了:PID 1が終了すると全コンテナが終了
解決:tini等の軽量initをPID 1として使用、またはDockerの--initオプション。
クイズ5: 物理メモリ4GBのシステムで各プロセスに4GB仮想アドレス空間を提供できる理由は?
仮想メモリの核心原理によるものです。
- デマンドページング:実際使用するページのみ物理メモリにロード
- スワップ空間:未使用ページをディスクに交換
- ページ共有:同じライブラリを使用するプロセスは1つの物理コピーを共有
全プロセスが同時に4GB全体を使用しないため、物理メモリより大きな仮想空間を提供できます。
参考資料
- "Operating System Concepts"(第10版) — Silberschatz, Galvin, Gagne
- "Modern Operating Systems"(第4版) — Andrew S. Tanenbaum
- "Linux Kernel Development"(第3版) — Robert Love
- Linux man pages: https://man7.org/linux/man-pages/
- eBPF公式ドキュメント: https://ebpf.io/
- Linuxカーネルソース: https://github.com/torvalds/linux
- OSDev Wiki: https://wiki.osdev.org/
- Julia Evansのシステムプログラミングブログ: https://jvns.ca/
オペレーティングシステムは全てのソフトウェアの基盤です。この記事で扱った概念は、単なる面接対策を超えて、パフォーマンス問題の診断、並行性バグの予防、そしてシステム設計での正しい意思決定に役立ちます。特にLinuxカーネルの動作原理を理解すれば、コンテナ、クラウド、分散システムで発生する問題を根本的に解決できる能力を身につけることができます。