TECHNOLOGY LINUX KERNEL

【第3回】元東大教員から学ぶLinuxカーネル「アイソレーションとシステムコール」

2022年8月21日

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOS(Linuxカーネル)の授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
  • プログラミング歴15年以上,習得している言語: C/C++PythonSolidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開
  • 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
  • 最近は自然言語処理AIイーサリアムに関する有益な情報発信に従事.
    • (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
    • (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.

こういった私から学べます.

前回を読んでいない方はこちらからどうぞ.

Linuxカーネルの記事一覧はこちらからどうぞ.

LinuxカーネルはC言語で書かれています.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

今回のテーマは,「アイソレーションとシステムコール」です.

アイソレーションとシステムコールの仕組みを理解するために,まずカーネルコードの読み方を紹介します.

カーネルコードの読み方

Linuxカーネルのソースコード(カーネルコード)は合計2,800万行以上と膨大で,全てを読み解くことは困難です.

そこで,自分が求める部分のカーネルコードを発見して読み解くためのトップダウン法とボトムアップ法,カーネルコードをナビゲートするための方法を解説します.

トップダウン法

トップダウン法で,カーネルコードを読み解く方法を紹介します.

例えば,ext4ファイルシステムの場合は以下になります.

  1. OSのファイルシステムに関する一般的な理解
  2. Linuxカーネルのファイルシステムの理解(後の回で紹介)
  3. ext4のドキュメントディスクレイアウトのチェック
  4. ext4カーネルコードの読解
    • モジュール毎(例:ディレクトリ,ファイル,ブロック管理)
    • システムコールから開始(例:どのようにwriteシステムコールが実装されているのか?)
  5. LWNを検索して最新の変更点を確認(例:ext4の暗号化サポート

ボトムアップ法

ボトムアップ法では,関数トレーサーを利用します.

  • ftrace:関数トレーサーのフレームワーク
  • perf(perf tools):ftraceのフロントエンドのLinuxカーネルの性能解析ツール
  • trace-cmd:ftraceを操作するツール

以下に,trace-cmdでlsコマンドを実行した場合のvfs_read関数を追跡結果を示します.

trace-cmdは,ftraceによるLinuxカーネルの関数呼び出しのグラフを追跡し,呼び出された関数とタイムスタンプを表示していることがわかります.

trace-cmdのGUIツール「KernelShark」を知りたいあなたはこちらからどうぞ.

カーネルコードをナビゲートする方法

カーネルコードをナビゲートするためには,cscopeを使います.

以下にcscopeを利用してvi(vimはviの高機能版)でカーネルコードをナビゲートする例を示します.

※vimのナビゲート方法の詳細は,「Browsing programs with tags」を参照して下さい.

アイソレーションとシステムコール

カーネルコードの読み方がわかったところで,今回のテーマ「アイソレーションとシステムコール」を解説していきます.

第1回でOSデザインは以下の4つの役割があることを述べました.

・利便性や移植性の向上するためにハードウェアの抽象化(ユーザがハードウェアの機能を関数呼び出しで(暗黙的に)利用できること)

・複数のアプリケーション間のハードウェアの多重化(どのアプリケーションがどのハードウェアを利用しているかを関数呼び出しで(暗黙的に)利用できること)

・バグを含むアプリケーションのアイソレーション(分離して独立で実行すること)

・アプリケーション間でのデータ等の共有の許可

https://hiroyukichishiro.com/linux-kernel-vol1/#Linux

今回のテーマは,以下の2つです.

  • ユーザアプリケーションをカーネルから分離するには?
  • ユーザアプリケーションからカーネルに安全にアクセスするには?

それでは,アイソレーションとシステムコールを説明します.

アイソレーションとは

OSにおけるアイソレーション(プロテクション)とは,特定の処理(プロセス等)を他の処理から保護する仕組みです.

アイソレーションによる保護する状況は主に以下の3つになります.

  • プロセスXがプロセスYを破壊したり,(CPU,メモリ,FD,資源の枯渇等)をスパイしたりすることから保護
  • (カーネルによる分離を防止するために)プロセスがOS自体を破壊することから保護
  • (例:悪いプロセスがハードウェアやカーネルを騙そうとする可能性がある)バグや悪意に直面した場合に保護

また,OSにおける分離機構は以下の4つになります.

  • ユーザ・カーネルモードのフラグ(x86-64はリングプロテクション,ARM64(AARCH64)は例外レベル)
  • アドレス空間(後の回で紹介)
  • タイムスライス(後の回で紹介)
  • システムコールのインターフェース

x86-64のアイソレーション

x86-64のアイソレーションを紹介します.

x86-64のリングプロテクションはring 0~ring 3まで,以下のように設定されています.

  • ring 0:OS
  • ring 1,ring 2:デバイスドライバ等(あまり利用されない)
  • ring 3:アプリ

つまり,ring 0は最高の特権レベル,ring 3は最低の特権レベルとなります.

しかし,x86-64のリングプロテクションの登場後に,ring 0のOSよりアイソレーションレベルが高いハイパーバイザーシステムマネジメントモードが登場しました.

そこで,ring 0よりアイソレーションレベルが高いring -1をハイパーバイザー,ring -2をシステムマネジメントモードに利用します.

※x86-64のリングプロテクションにはring -1,ring -2は存在せず,説明上の概念になります.

ARM64のアイソレーション

ARM64のアイソレーションを紹介します.

ARM64は例外レベル(EL:Exception Level)で特権レベルを設定し,x86-64のリングプロテクションと同様にEL0~EL3です.

しかし,x86-64のリングプロテクションとは異なり,EL0が最低の特権レベル,EL3が最高の特権レベルになります.

また,EL0がアプリ,EL1がOSを動作する実装になっているため,EL2をハイパーバイザー,EL3をTrustZoneのセキュアモニターに利用します.

※TrustZoneのセキュアモニターはx86-64のシステムマネジメントモードのようなものです.

ARM64は実際の例外レベルを利用できるので,x86-64のリングプロテクションより概念や実装が理解しやすいです.

x86-64とARM64のハードウェアアイソレーションは下表になります.

x86-64のハードウェア
アイソレーション
ARM64のハードウェア
アイソレーション
用途
ring 3EL0アプリ
ring 0EL1OS
ring -1EL2ハイパーバイザー
ring -2EL3システムマネジメントモード(x86-64)
TrustZoneのセキュアモニター(ARM64)

システムコールとは

システムコールは,ユーザ空間のアプリケーションがカーネル空間に入り,OSのサービスやハードウェアへのアクセスなどの特権的な操作を要求するための唯一無二の方法です.

システムコールにより,x86-64だとring 3からring 0,ARM64だとEL0からEL1に特権レベルを切り替えることができます.

これにより,特権レベルでしかアクセスできないディスプレイのようなI/Oポートへアクセスすることができます.

システムコールは以下の機能を提供します.

  • ハードウェアとユーザ空間プロセスの間のレイヤ
  • ユーザ空間のための抽象的なハードウェアインタフェース
  • システムのセキュリティと安定性を確保

システムコール

上図にシステムコールの呼び出しと復帰の流れを示します.

プログラムはユーザモードで実行し,システムコールの呼び出しでカーネルモードに切り替わります.

そして,システムコールから復帰した時にユーザモードに戻ります.

システムコールは,以下のアーキテクチャ固有のシステムコール専用命令のラッパー関数として実装されます.

また,システムコールから戻る命令は以下になります.

システムコールの例

システムコールの例を以下に紹介します.

  • プロセス管理やスケジューリング
    • fork:子プロセスを生成します.
    • exit:呼び出し元のプロセスを終了させます.
    • execve:プログラムを実行します.
    • nice:プロセスの優先度を変更します.
    • getpriority:プログラムのスケジューリングの優先度を取得します.
    • setpriority:プログラムのスケジューリングの優先度を設定します.
  • メモリ管理
    • brk:データセグメントのサイズを変更します.
    • mmap:ファイルやデバイスをメモリにマップします.
    • munmap:ファイルやデバイスをメモリにアンマップします.
  • ファイルシステム
    • open:ファイルのオープン,作成を行います.
    • read:ファイルディスクリプタから読み込みます.
    • write:ファイルディスクリプタに書き込みます.
    • lseek:ファイルの読み書きオフセットの位置を変えます.
    • stat:ファイルの状態を取得します.
  • プロセス間通信
    • pipe:パイプを生成します.
    • shmget:System V共有メモリセグメントを割り当てられます.
  • 時間管理
  • その他
    • getuid:ユーザIDを取得します.
    • setuid:ユーザIDを設定します.
    • connect:ソケットの接続を行います.

システムコールの実装

システムコールは,システムコールの関数ポインタの配列(syscall table)で管理されています.

x86-64の場合はlinux/arch/x86/entry/syscalls/syscall_64.tbl,ARM64(AARCH64)の場合はlinux/include/uapi/asm-generic/unistd.hにあります.

x86-64はアーキテクチャ固有のシステムコールID,ARM64は他のアーキテクチャと共通のシステムコールIDになります.

システムコール一覧はこちらがわかりやすいです.

例えば,システムコールIDは以下のような違いがあります.

  • readシステムコールのID:x86-64の場合は0,ARM64の場合は3
  • writeシステムコールのID:x86-64の場合は1,ARM64の場合は64

また,システムコールの配列はsys_call_tableという変数で定義され,名前はx86-64やARM64を含む全アーキテクチャ共有です.

sys_call_tableの実態は以下にあります.

  • x86-64:linux/arch/x86/entry/syscall_64.c
  • ARM64:linux/arch/arm/kernel/sys.c

linux/fs/read_write.cにあるreadシステムコールの実装は以下になります.

23行目のSYSCALL_DEFINE3マクロ(3つのパラメータを持つシステムコールを定義するためのマクロ)でreadシステムコールの呼び出しをカーネル側で受け取り,4行目のksys_read関数で実際の読み込み処理をします.

invoke system call from user space

上図にユーザ空間からのシステムコールの呼び出しを示します.

システムコールが直接呼び出されることはほとんどありません.

それらのほとんどはC言語のライブラリ(glibc等)によりラッパー関数として呼び出されます.

syscall関数を利用してアプリケーションからのシステムコールの呼び出し

syscall関数を利用してアプリケーションからのシステムコールを呼び出します.

syscall関数は,システムコールを起動する小さなライブラリ関数です.

numberで指定されたアセンブリ言語インターフェースのシステムコールを,指定された引数を設定して実行します.

syscall関数が役に立つのは,C言語のライブラリにラッパー関数が存在しないシステムコールを呼び出したい場合です.

例えば,sched_setattr/sched_getattrシステムコールが挙げられます.

sched_setattr/sched_getattrシステムコールを利用するリアルタイムスケジューリングRMとEDFの実装を知りたいあなたはこちらからどうぞ.

x86-64のアプリケーションからのwriteシステムコールの呼び出し

x86-64のアプリケーションからのwriteシステムコールの呼び出しは以下のコードになります.

12行目で呼び出しているsyscall関数の引数は以下の意味になります.

  • 第1引数の1:x86-64のシステムコールの1番(writeシステムコール
  • 第2引数の1:writeシステムコールの第1引数fd(1は標準出力)
  • 第3引数のstr:writeシステムコールの第2引数buf(出力する文字列)
  • 第4引数の14:writeシステムコールの第3引数count(出力する文字列"Hello World!\n"のバイト数)

syscall.cのアセンブリ言語(x86-64)は以下の手順で作成して,catコマンドでstack.sの中身を表示します.

30行目でsyscall関数を呼び出していることがわかります.

実行結果は以下になります.

syscall関数でwriteシステムコールを直接呼び出すことで「Hello World!」を表示していることがわかります.

※stdio.hをインクルードしてprintf関数のような標準ライブラリ関数を呼び出さずに文字列を表示していることに着目して下さい.

ARM64のアプリケーションからのwriteシステムコールの呼び出し

ARM64のアプリケーションからのwriteシステムコールの呼び出しは以下になります.

writeシステムコールのIDはx86-64では1ですが,ARM64では64であることに注意して下さい.

つまり,syscall関数の第1引数が1から64に変更します.

実行結果は以下になります.同様です.

gettimeofdayシステムコールの実装と利用例

gettimeofdayシステムコールは,時刻を取得します.

gettimeofdayシステムコールの使い方は以下になります.

実行結果は以下になります.

Linuxのカーネルのgettimeofdayシステムコールの実装はlinux/kernel/time/time.cにあります.

ここで,SYSCALL_DEFINE2は,2つのパラメータを持つシステムコールを定義するためのマクロです.

ユーザ空間とカーネル空間のメモリ転送

ユーザ空間とカーネル空間のメモリ転送を解説します.

アイソレーションにより,ユーザ空間のアプリケーションはカーネル空間のメモリにアクセスできません.

また,カーネル空間ではユーザ空間へのポインタに盲目的に従わないことが大事です.

なぜかというと,間違ったユーザアドレスにアクセスすると,カーネルがクラッシュすることがあります.

そこで,上記のcopy_from_user/copy_to_user関数を利用してカーネル空間からユーザ空間のメモリにアクセスします.

提供されたユーザ空間のメモリが正当でない場合,不正アクセスエラーを発生します.

ユーザ空間のメモリが存在しない場合(スワップアウトされた場合),カーネルはスワップインするまでスリープし,スワップイン後にユーザ空間のメモリにアクセスします.

新しいシステムコールの実装方法

新しいシステムコールの実装方法を知りたいあなたはこちらの記事を読みましょう!

システムコールの性能向上

システムコールの性能は多くのアプリケーションで重要です.

それでは,システムコールの性能向上のためのハードウェアとソフトウェアの方法を紹介していきます.

ハードウェア:Intelのsysenter/sysexit命令,AMDのsyscall/sysret命令

x86-64では,「int 0x80」(ソフトウェア割り込みを発生させるint命令の引数0x80)をシステムコールに利用していました.(ソフトウェア割り込みから戻る時はiret命令を利用します.)

1998年にIntelのPentium IIがsysenter/sysexit命令を実装したことで,Linuxカーネルはsysenter/sysexit命令に置き換えられました.(AMDはsyscall/sysret命令です.)

Intelのsysenter/sysexit命令は,int 0x80と比較してソフトウェア割り込みによるオーバヘッドを削減します.

ソフトウェア:vDSO(virtual dynamically linked shared object)

vDSO(virtual dynamically linked shared object)は,カーネル空間の関数をユーザ空間のアプリケーションにエクスポートすることで性能向上するカーネル機構です.

vDSOは,システムコールインターフェースを使って同じカーネル空間ルーチンを呼び出す場合に利用できます.

ユーザモードからカーネルモードへのモード切り替えによる性能低下を招くことなく,アプリケーションからプロセス内のカーネル空間ルーチンを呼び出せるようになります.

つまり,コンテストスイッチが不要になり,そのオーバヘッドが0になります.

vDSOの主な利用例は,gettimeofdayシステムコールです.

ソフトウェア:例外を少なくする(Exception-Lessな)システムコール

例外を少なくする(Exception-Lessな)システムコールを紹介します.

FlexSCは,FlexSCは2010年に国際会議OSDIで発表された例外を少なくするシステムコールの柔軟なシステムコールのスケジューリングです.

FlexSCの論文とGitHub(論文の著者ではない実装)は以下になります.

vDSOとは異なり,FlexSCは現在Linuxカーネルのメインラインにマージされていないので注意して下さい.

FlexSCによる性能向上は以下になります(アプリケーションを変更せずに実現).

  • Apacheの性能を最大116%向上
  • MySQLの性能を最大40%向上
  • BINDの性能を最大105%向上

他には,nginxのようなWebサーバ(イベントドリブンサーバ)で利用される「Exception-Less System Calls for Event-Driven Servers」の論文もあります.

ソフトウェア:終了を少なくする(Exit-Lessな)システムコール

終了を少なくする(Exit-Lessな)システムコールに関する論文は以下になります.

これらの論文を読んで,終了を少なくするシステムコールと例外を少なくするシステムコールの違いを学びましょう!

まとめ

今回はアイソレーションとシステムコールを紹介しました.

アイソレーションによりユーザアプリケーションをカーネルから分離し,システムコールによりユーザアプリケーションからカーネルに安全にアクセスすることがわかりました.

システムコールを深く理解したいあなたは,以下の記事を読みましょう!

LinuxカーネルはC言語で書かれています.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

次回はこちらからどうぞ.

-TECHNOLOGY, LINUX KERNEL
-, , , , , , ,