概要
ユーザー空間(User Space)とカーネル空間(Kernel Space)は、Linuxをはじめとする近代的なOSがメモリとCPU権限を2つの領域に分離する仕組みです。カーネル空間はOSの中核コードとデバイスドライバが動作する特権領域、ユーザー空間はアプリケーションが動作する非特権領域です。
この分離によって、アプリケーションのバグがカーネル(ひいてはシステム全体)を破壊しないよう保護されます。ユーザー空間のプロセスはシステムコールを通じてのみカーネルの機能を呼び出せます。組み込みLinuxではこの分離がセキュリティ・安定性の基盤となっており、コンテナ(Docker)や仮想化の基礎にもなっています。
歴史・背景
ユーザー空間とカーネル空間の分離は、1960〜70年代のメインフレームOSから始まった「特権モード」の概念に由来します。Multics・Unix系OSがこの設計を採用し、汎用コンピュータのデファクトスタンダードとなりました。
x86アーキテクチャでは「リングプロテクション」として実装され、Ring 0(カーネル)〜Ring 3(アプリ)の4段階があります(Linuxは実際にはRing 0とRing 3のみ使用)。ARMではPL0(非特権)・PL1(カーネル)・PL2(ハイパーバイザー)、ARMv8以降はEL0〜EL3(Exception Level)として実装されています。
組み込みLinuxにおいても、MMU(メモリ管理ユニット)を持つARMマイコン(Cortex-A系)では同様の分離が機能します。一方、MMUを持たないマイコン(Cortex-M系)やベアメタル・RTOS環境では、この分離は存在しないかオプション機能(Cortex-M33のTrustZoneなど)です。
技術仕様
メモリ空間の分割
Linux(32bitARMの例)のメモリアドレス空間は3GB/1GB分割が一般的です:
仮想アドレス空間(32bit = 4GB):
0x00000000 ┌─────────────────────────┐
│ ユーザー空間(3GB) │
│ プロセスごとに独立した │
│ 仮想アドレス空間 │
│ コード・データ・ヒープ │
│ スタック・mmap領域 │
0xC0000000 ├─────────────────────────┤
│ カーネル空間(1GB) │
│ カーネルコード・データ │
│ ドライバ・カーネルスタック│
│ (全プロセス共通) │
0xFFFFFFFF └─────────────────────────┘
※ ARM64(64bit)ではユーザー空間は128TB、カーネル空間も128TBの広大な空間を持つ
CPUの特権モード
ARM Cortex-Aの例(AArch64):
| Exception Level | 用途 |
|---|---|
| EL0(非特権) | ユーザーアプリケーション |
| EL1(特権) | OSカーネル |
| EL2(ハイパーバイザー) | 仮想化(KVM等) |
| EL3(Secure Monitor) | ARM TrustZone・セキュアファームウェア |
システムコール一覧(主要なもの)
プロセス管理:
fork() プロセス複製
exec() プログラム実行
exit() プロセス終了
waitpid() 子プロセスの終了待ち
ファイルI/O:
open() ファイルオープン
read() データ読み取り
write() データ書き込み
close() ファイルクローズ
ioctl() デバイス制御(デバイス固有コマンド)
mmap() メモリマップドI/O
ネットワーク:
socket() ソケット生成
bind() アドレスバインド
connect() 接続
send()/recv() データ送受信
メモリ:
mmap() メモリマッピング
munmap() マッピング解除
brk() ヒープ領域拡張
IPC(プロセス間通信):
pipe() パイプ生成
shm_open() 共有メモリ
mq_open() POSIXメッセージキュー
sem_open() POSIXセマフォ
動作原理
システムコールの実行フロー
ユーザー空間のアプリがread()を呼んだときの流れ(ARM64の場合):
アプリケーション(EL0)
→ read(fd, buf, len) 呼び出し
→ C標準ライブラリ(glibc)がSVC命令(Supervisor Call)を発行
→ CPU が EL0→EL1 に切り替わる(モードスイッチ)
→ カーネルの syscall エントリポイントへ
→ システムコール番号(NR_read = 63)に対応するハンドラ呼び出し
→ ファイルディスクリプタに対応するドライバの read() を呼び出し
→ データをユーザー空間バッファにコピー(copy_to_user())
→ カーネルから戻り値を返してEL0に復帰
→ アプリケーションの read() が値を返す
コンテキストスイッチ
プロセスを切り替えるコンテキストスイッチでは、現在のプロセスのCPUレジスタ・PCカウンタをカーネルが保存し、次のプロセスの状態を復元します:
/* カーネル内の switch_to()(概念的な擬似コード) */
#define switch_to(prev, next, last) \
do { \
save_context(prev); /* 現プロセスの状態保存 */ \
load_context(next); /* 次プロセスの状態復元 */ \
/* MMUのページテーブルも切り替え(プロセスごとの仮想空間) */ \
switch_mm(prev->mm, next->mm); \
} while (0)
copy_to_user / copy_from_user
カーネルはユーザー空間のポインタを直接デリファレンスせず、必ずcopy_to_user()・copy_from_user()を使います。これはユーザー空間の不正なポインタ(NULLや範囲外)によるカーネルクラッシュを防ぐためです:
/* カーネルドライバの read() 実装例 */
static ssize_t my_driver_read(struct file *f, char __user *buf,
size_t count, loff_t *ppos) {
uint8_t kbuf[64]; /* カーネル空間バッファ */
/* ハードウェアからデータ読み取り */
read_hardware_data(kbuf, min(count, sizeof(kbuf)));
/* カーネル→ユーザー空間へ安全にコピー */
if (copy_to_user(buf, kbuf, count))
return -EFAULT; /* アドレス不正エラー */
return count;
}
用途・ユースケース
アプリケーション開発の安全性
ユーザー空間の分離により、アプリがクラッシュしてもカーネルは生き続け、他のプロセスへの影響を防ぎます。これは組み込みLinuxシステムの信頼性の基盤です。
デバイスドライバ開発
デバイスドライバはカーネル空間で動作し、ioctl・sysfs・chardev等のインターフェースでユーザー空間アプリとやり取りします。
セキュリティの境界
Linux Security Module(LSM)、AppArmor、SELinuxはカーネル空間のフックとして実装され、ユーザー空間のアプリが行えるシステムコールを制限します。
コンテナ技術の基盤
Dockerコンテナはカーネル空間を共有しながら、名前空間(namespace)・cgroupsを使ってユーザー空間のリソースを分離します。
実装・開発のポイント
カーネルモジュールとユーザー空間の通信
デバイスドライバ(カーネル空間)とアプリ(ユーザー空間)の通信方法:
/* 方法1: キャラクタデバイス(/dev/mydev)経由 */
/* ドライバ: open/read/write/ioctl ハンドラを登録 */
/* 方法2: sysfs(/sys/class/mydev/property) */
/* ドライバ: show()/store()コールバックを登録 */
/* 方法3: procfs(/proc/mydev/info) */
/* ドライバ: seq_file インターフェース */
/* 方法4: netlink ソケット */
/* カーネルとユーザー空間の双方向非同期通信 */
/* 方法5: UIO (Userspace I/O) */
/* ドライバ処理の一部をユーザー空間に移す設計 */
mmap によるゼロコピーアクセス
ハードウェアレジスタ・DMAバッファを直接ユーザー空間にマップすることで、コピーなしの高速I/Oが可能です:
/* カーネルドライバの mmap() 実装 */
static int my_driver_mmap(struct file *f, struct vm_area_struct *vma) {
/* DMAバッファの物理アドレスをユーザー空間にマップ */
return remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dma_buf) >> PAGE_SHIFT,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}
/* ユーザー空間アプリからmmap()でDMAバッファに直接アクセス */
int fd = open("/dev/mydev", O_RDWR);
void *dma = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* dmaポインタを通じてDMAバッファを直接読み書き */
strace によるシステムコールトレース
# アプリが呼んでいるシステムコールを全トレース
strace -e trace=all my_app
# ファイル関連のみ
strace -e trace=file my_app
# システムコール統計
strace -c my_app
他技術との比較
ユーザー/カーネル分離 vs ベアメタル
ベアメタルやRTOS(FreeRTOS等)では、MMUがないかMMUを使わない場合がほとんどで、ユーザー空間/カーネル空間の分離がありません。全コードが同じアドレス空間・同じ特権で動作するため、バグが致命的な問題に直結しやすいですが、オーバーヘッドが最小化されます。
ユーザー/カーネル分離 vs マイクロカーネル(QNX等)
QNX等のマイクロカーネルではドライバ・プロトコルスタックもユーザー空間で動作し、カーネルへの影響を最小化します。Linuxモノリシックカーネルはドライバがカーネル空間にあるため、ドライバのバグがカーネルパニックを起こす可能性があります。