OS・実行環境

ユーザー空間 / カーネル空間

アプリ領域とOS中核領域の分離。

概要

ユーザー空間(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モノリシックカーネルはドライバがカーネル空間にあるため、ドライバのバグがカーネルパニックを起こす可能性があります。

関連用語

参考リンク