はじめに
Linuxでは、アプリケーションとLinuxカーネルでCPUの実行モードが異なります。通常、アプリケーションはユーザーモード、Linuxカーネルはカーネルモードで動作します。
アプリケーションがファイルI/Oやネットワーク通信など、カーネルが管理する機能を利用する際に使われるのがシステムコールです。システムコールを実行すると、CPUはユーザーモードからカーネルモードへ切り替わり、カーネルが必要な処理を実行します。
本記事では、ユーザーモードとカーネルモードの違い、システムコールの仕組みについて、read()やstraceの実例を交えながら分かりやすく解説します。
CPUモードとは
CPUには、実行するプログラムに応じて利用できる機能や権限を制限する仕組みがあります。このCPUの実行権限の違いを「CPUモード」と呼びます。
Linuxでは、CPUモードを大きく次の2つに分けて考えることができます。
・ユーザーモード(User Mode)
・カーネルモード(Kernel Mode)
通常のアプリケーションはユーザーモードで実行され、Linuxカーネルはカーネルモードで実行されます。
ユーザーモードとは
ユーザーモードは、一般的なアプリケーションを実行するためのCPUモードです。例えば、Apache、PostgreSQL、Javaアプリケーション、lsコマンドなども、通常のプログラム部分はユーザーモードで実行されます。
ユーザーモードでは、システム全体に影響するような操作が制限されています。例えば、CPUの特権命令を自由に実行したり、カーネルのメモリ領域へ直接アクセスしたりすることはできません。もしアプリケーションがこのような操作を自由に実行できてしまうと、1つのアプリケーションの不具合によってLinuxカーネルやほかのプロセスまで影響を受ける可能性があります。
そこで、通常のアプリケーションは制限されたユーザーモードで動作するようになっています。
カーネルモードとは
カーネルモードは、Linuxカーネルが動作するためのCPUモードです。カーネルモードでは、ユーザーモードより高い権限でCPUの機能を利用できます。
Linuxカーネルは、この権限を利用して、以下のようなシステム全体に関わる処理を行います。
・プロセスやスレッドの管理
・メモリ管理
・ファイルシステムの処理
・デバイスの制御
・ネットワーク処理
例えば、アプリケーションがファイルからデータを読み込みたい場合、ユーザーモードのアプリケーションがストレージを直接操作するのではありません。Linuxカーネルへ処理を依頼し、カーネルモードで必要な処理が実行されます。
なぜCPUモードを分けるのか
CPUモードを分ける大きな理由は、システムを保護するためです。アプリケーションをユーザーモードで実行することで、実行できる処理を制限できます。例えば、あるアプリケーションに不具合が発生したとしても、そのアプリケーションが自由にカーネルのメモリを書き換えたり、ハードウェアを直接操作したりすることを防げます。
つまり、以下のような役割分担によって、Linuxシステムの安全性を保っています。
ユーザーモード: アプリケーションを制限された権限で実行
カーネルモード:Linuxカーネルを高い権限で実行
ただし、アプリケーションもファイルアクセスやネットワーク通信など、Linuxカーネルの機能を利用する必要があります。そこで、ユーザーモードからカーネルへ安全に処理を依頼するために利用されるのが「システムコール」です。
ユーザーモードとカーネルモードの違い
ユーザーモードとカーネルモードの大きな違いは、CPU上で実行できる処理の権限です。通常のアプリケーションはユーザーモードで動作し、システム全体に影響を与えるような操作は制限されています。一方、Linuxカーネルはカーネルモードで動作し、CPUやメモリ、デバイスなどを管理するために必要な高い権限を持っています。
主な違いを整理すると、次のようになります。
| 項目 | ユーザーモード | カーネルモード |
|---|---|---|
| 主に動作するもの | 一般的なアプリケーション | Linuxカーネル |
| CPUの権限 | 制限あり | 高い権限 |
| 特権命令 | 実行できない | 実行できる |
| カーネル空間へのアクセス | 直接アクセスできない | アクセスできる |
| ハードウェア操作 | 原則としてカーネルへ依頼 | デバイスドライバなどを通じて制御 |
実行できる命令が異なる
CPUには、どのプログラムからでも実行できる命令だけでなく、高い権限を持つ場合にのみ実行できる「特権命令」があります。
ユーザーモードで動作するアプリケーションは、このような特権命令を自由に実行することはできません。
一方、カーネルモードでは、OSがシステムを管理するために必要な特権命令を実行できます。
この制限によって、一般的なアプリケーションがCPUやシステム全体の動作を勝手に変更することを防いでいます。
アクセスできるメモリ領域が異なる
Linuxでは、プロセスから見える仮想アドレス空間において、ユーザー空間とカーネル空間が区別されています。
ユーザーモードで動作するアプリケーションは、自分に許可されたユーザー空間のメモリへアクセスできますが、カーネル空間へ自由にアクセスすることはできません。
一方、Linuxカーネルはカーネルモードで動作し、カーネルの処理に必要なメモリへアクセスできます。
この仕組みによって、アプリケーションのバグなどによってカーネルの重要なデータが直接書き換えられることを防いでいます。
ハードウェアを直接操作できない
ユーザーモードのアプリケーションは、ストレージやNICなどのハードウェアを原則として直接操作しません。
例えば、アプリケーションがファイルを読み込む場合、アプリケーション自身がストレージデバイスを直接制御するのではなく、Linuxカーネルへ読み込み処理を依頼します。
Linuxカーネルは、ファイルシステムやデバイスドライバなどを通じて必要な処理を行い、その結果をアプリケーションへ返します。
システムコールとは
システムコール(System Call)とは、ユーザーモードで動作するアプリケーションが、Linuxカーネルの機能を利用するための仕組みです。
上述の通り、通常のアプリケーションはユーザーモードで動作しているため、ファイルシステムやネットワーク、プロセス管理など、カーネルが管理している機能を自由に操作することはできません。
そこで、アプリケーションはシステムコールを利用してLinuxカーネルへ処理を依頼します。
例えば、ファイルからデータを読み込む場合、アプリケーションがストレージを直接操作するのではなく、システムコールを通じてLinuxカーネルへ読み込みを依頼します。
代表的なシステムコール
Linuxにはさまざまなシステムコールが用意されています。代表的なものには、次のようなものがあります。
| システムコール | 主な役割 |
|---|---|
| read | ファイルなどからデータを読み込む |
| write | ファイルなどへデータを書き込む |
| openat | ファイルを開く |
| close | ファイルを閉じる |
| mmap | メモリ領域をマッピングする |
| clone | プロセスやスレッドの作成に利用される |
| execve | プログラムを実行する |
例えば、アプリケーションがreadシステムコールを実行すると、Linuxカーネルがファイルなどからデータを読み込み、その結果をアプリケーションへ返します。アプリケーションから見ると単純なファイル読み込みでも、その裏ではLinuxカーネルの機能が利用されています。
ライブラリ関数とシステムコールの違い
アプリケーションからシステムコールを利用する場合、プログラムから直接システムコールを呼び出すとは限りません。一般的には、glibcなどが提供するライブラリ関数を利用し、その内部で必要に応じてシステムコールが呼び出されます。
例えば、プログラムからファイルを開く処理を実行した場合、その裏側でopenatなどのシステムコールが利用されることがあります。
ただし、ライブラリ関数を呼び出すたびにシステムコールが発生するわけではありません。ライブラリ内部だけで処理が完了する関数もあります。
重要なのは、システムコールが「ユーザープログラムからLinuxカーネルの機能を利用するためのインターフェース」であるという点です。
システムコール実行時に何が起きるのか
システムコールを実行すると、CPUはユーザーモードからカーネルモードへ切り替わり、Linuxカーネルがアプリケーションから依頼された処理を実行します。
ここでは、アプリケーションがreadシステムコールを利用してファイルからデータを読み込む場合を例に、処理の流れを見ていきます。

read()システムコールの処理の流れ
Linuxでは、アプリケーションがファイルからデータを読み込む場合、read()システムコールを利用します。
アプリケーションから見ると単純にread()を実行しているだけですが、Linux内部ではユーザーモードからカーネルモードへの切り替え、Page Cacheの確認、必要に応じたストレージI/Oなど、複数の処理が行われています。
ここでは、通常のBuffered I/Oでファイルを読み込む場合を例に、read()の大まかな処理の流れを見ていきます。
① アプリケーション
まず、アプリケーションがファイルからデータを読み込もうとします。通常は事前にopen()などでファイルを開き、その際に取得したファイルディスクリプタをread()へ渡します。
read()には主に、ファイルディスクリプタ、読み込んだデータを格納するユーザー空間のバッファ、読み込みたいバイト数を指定します。
② read()を呼び出す
アプリケーションはユーザーモードで動作しており、ファイルを読み込むためにread()を呼び出します。ただし、ユーザーモードで動作するアプリケーションが、ストレージなどのハードウェアを直接操作することはできません。
そのため、read()というシステムコールを通してLinuxカーネルへファイルの読み込みを依頼します。
③ カーネルモードへ切り替わる
read()システムコールが実行されると、CPUはユーザーモードからカーネルモードへ切り替わります。これによって、Linuxカーネルがread()の要求を処理できるようになります。
ここで重要なのは、アプリケーション自体がカーネル空間へ移動するわけではないという点です。同じタスクの実行中にCPUの実行モードがユーザーモードからカーネルモードへ切り替わり、カーネルコードが実行されます。
④ ファイルディスクリプタを確認
カーネルは、read()に指定されたファイルディスクリプタから、対象となるファイルの情報を参照します。ファイルディスクリプタは単なる整数値ですが、カーネル内部ではその値から対象ファイルに対応するstruct file(※)などの情報へたどることができます。
これらの情報を利用して、対象となるファイルや現在のファイル位置などを確認し、VFSや対象ファイルシステムの処理へ進みます。
※Linuxカーネルが「プロセスによって開かれているファイル」を管理するためのカーネル内部のデータ構造
⑤ Page Cacheを確認
通常のBuffered I/Oでは、ファイルデータの読み込みにPage Cacheが利用されます。Page Cacheは、ストレージ上のファイルデータをメモリ上にキャッシュする仕組みです。
カーネルは、アプリケーションが要求しているファイルデータがPage Cache上に存在するか確認します。
ここで必要なデータがPage Cacheに存在するかどうかによって、その後の処理が大きく2つに分かれます。
⑥ キャッシュヒット
必要なデータがすでにPage Cacheに存在する場合は「キャッシュヒット」です。この場合、ディスクやSSDへ読み込みI/Oを発行する必要はありません。
メモリ上に存在するPage Cacheのデータを利用できます。一般的にメモリアクセスはストレージアクセスより高速であるため、Page CacheはLinuxのファイルI/O性能を向上させるうえで重要な役割を持っています。
⑦ キャッシュミス
一方、必要なデータがPage Cacheに存在しない場合は「キャッシュミス」です。この場合、カーネルはストレージに対して読み込みI/Oを発行し、ディスクやSSDから必要なデータを取得します。
ストレージI/Oの完了を待つ必要がある場合、read()を実行したタスクは待機状態となり、CPUを手放すことがあります。その間、CPUでは別の実行可能なタスクを処理できます。
ストレージから読み込まれたファイルデータはPage Cacheに格納されます。そのため、同じデータが再び必要になった場合には、Page Cacheから読み出せる可能性があります。
⑧ Page Cacheからユーザー空間へコピー
必要なデータがPage Cache上に用意されると、カーネルはそのデータをread()で指定されたユーザー空間のバッファへコピーします。
キャッシュヒットの場合は、すでに存在するPage Cacheからコピーします。
キャッシュミスの場合は、ストレージからPage Cacheへデータを読み込んだ後、そのデータをユーザー空間へコピーします。
したがって、キャッシュヒットとキャッシュミスのどちらの場合でも、最終的にはPage Cache上のデータがユーザー空間へ渡されます。
⑨ read()の戻り値を返す
ユーザー空間のバッファへのコピーが完了すると、カーネルはread()の結果をアプリケーションへ返します。
正常にデータを読み込めた場合、read()の戻り値は実際に読み込んだバイト数になります。
また、ファイルの終端に到達した場合は0、エラーが発生した場合は-1が返されるなど、read()の戻り値から処理結果を判断できます。
⑩ ユーザーモードへ戻る
カーネル側でread()の処理が完了すると、システムコールから復帰します。CPUの実行モードはカーネルモードからユーザーモードへ戻り、アプリケーション側の処理へ制御が戻ります。
⑪ アプリケーションが処理を継続
ユーザーモードへ戻ると、アプリケーションはread()の次の処理から実行を継続します。この時点では、読み込んだファイルデータがアプリケーションのユーザー空間のバッファに格納されています。アプリケーションはそのデータを解析したり、画面へ表示したり、別の処理へ利用したりできます。
read()システムコールの処理の流れまとめ
このように、アプリケーションから見ると単純なread()ですが、Linux内部では「ユーザーモードからカーネルモードへの切り替え → ファイル情報の確認 → Page Cacheの確認 → 必要に応じてストレージI/O → ユーザー空間へのコピー → ユーザーモードへ復帰」という処理が行われています。
特に重要なのは、read()を実行するたびにストレージI/Oが発生するわけではない点です。必要なデータがPage Cacheに存在すれば、ストレージへアクセスすることなく、メモリ上のデータを利用できます。
Linuxでシステムコールを確認する
Linuxでは、straceコマンドを利用することで、プログラムが実行しているシステムコールを確認できます。普段何気なく利用しているLinuxコマンドも、内部では多くのシステムコールを利用してLinuxカーネルの機能を呼び出しています。
straceコマンドにて確認できる情報
一例として、straceコマンドを実行することによって、lsコマンドにてどのようなシステムコールが実行されているか確認してみましょう。以下にstraceコマンド実行結果の抜粋を記載します。
[root@almalinux strace]# ls
test1.txt test2.txt
[root@almalinux strace]#
[root@almalinux ~]# strace ls
以下、コマンド抜粋
------------------------------------------------------------------------
①execve("/usr/bin/ls", ["ls"], 0x7ffe02b7bcc0 /* 24 vars */) = 0
②openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\227\2\0\0\0\0\0"..., 832) = 832
mmap(NULL, 2129872, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa90e600000
③openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 5
④getdents64(5, 0x5556ed323970 /* 4 entries */, 32768) = 112
⑤write(1, "test1.txt test2.txt\n", 21test1.txt test2.txt
) = 21
⑥exit_group(0) = ?
+++ exited with 0 +++① lsプログラムを起動
最初にexecve()で/usr/bin/lsを実行しています。
execve(“/usr/bin/ls”, [“ls”], …) = 0
ここからlsプロセスの実行が始まります。
② 必要な共有ライブラリを読み込む
lsを実行するために、libcなどの共有ライブラリをopenat()で開き、mmap()でプロセスの仮想アドレス空間へマッピングしています。
例えばlibc.so.6について、openat() → read() → mmap()という処理が確認できます。
この部分はプログラム起動時の準備処理です。
③ カレントディレクトリを開く
ls本来の処理として重要なのがここです。
openat(AT_FDCWD, “.”, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 5
「.」はカレントディレクトリなので、lsはまず対象ディレクトリを開いています。
④ ディレクトリ内のファイル一覧を取得
続いてgetdents64()を実行しています。
getdents64(5, 0x5556ed323970 /* 4 entries */, 32768) = 112
getdents64()は、ディレクトリ内のエントリを取得するシステムコールです。
⑤ 結果を画面へ出力
取得したファイル名をwrite()で標準出力へ書き出しています。
write(1, “ddagent-install.log jmxterm-1.0″…, 44) = 44
ここで1は標準出力(stdout)のファイルディスクリプタです。この例では標準出力がターミナルに接続されているため、lsの結果が画面に表示されます。
⑥ プロセス終了
最後にexit_group()でlsが終了します。
straceにて特定のシステムコールだけ確認する
straceでは、特定のシステムコールだけに絞って確認することもできます。例えば、writeシステムコールだけを確認する場合は、次のように実行します。
[root@almalinux strace]# strace -e trace=write ls
write(3, "", 0) = 0
write(4, "{\n \"runtime_id\": \"dc8caa21-df"..., 114) = 114
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1144668, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
write(1, "test1.txt test2.txt\n", 21test1.txt test2.txt
) = 21
+++ exited with 0 +++
[root@almalinux strace]#すべてのシステムコールを表示すると出力が非常に多くなるため、調査したい処理が決まっている場合は対象を絞ると確認しやすくなります。
実行中のプロセスを確認する
すでに動作しているプロセスにstraceを接続することもできます。例えばPIDが357466の場合は、strace -p 357466 というコマンドで確認できます。
[root@almalinux strace]# strace -p 357466
strace: Process 357466 attached
pselect6(9, [6 7 8], NULL, NULL, {tv_sec=56, tv_nsec=945681111}, NULL) = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=357472, si_uid=26} ---
newfstatat(AT_FDCWD, "logrotate", 0x7fffc9e87000, 0) = -1 ENOENT (No such file or directory)
getpid() = 357466
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff37540ac10) = 1790873
rt_sigreturn({mask=[]}) = -1 EINTR (Interrupted system call)
rt_sigprocmask(SIG_SETMASK, ~[ILL TRAP ABRT BUS FPE SEGV CONT SYS RTMIN RT_1], NULL, 8) = 0
openat(AT_FDCWD, "postmaster.pid", O_RDWR) = 9
read(9, "357466\n/var/lib/pgsql/data\n17669"..., 8191) = 102
close(9) = 0
getpid() = 357466これによって、そのプロセスが実行しているシステムコールをリアルタイムに確認できます。しかし、straceコマンド実行によりオーバヘッドが増えて性能影響が出る可能性がありますので、本番環境でコマンドを実行する際には注意してください。特にシステムコールを高頻度で実行するプロセスでは影響が大きくなる可能性があります。
CPU使用率のuserとsystemの違い
LinuxでCPU使用率を確認すると、userやsystemといった項目が表示されます。これらは、CPUが「どのモードで処理を実行していたのか」を表しています。
CPUモードの仕組みを理解しておくと、単に「CPU使用率が高い」だけではなく、アプリケーションとLinuxカーネルのどちらでCPU時間を多く消費しているのかを判断しやすくなります。
user CPUとは
user CPUは、CPUがユーザーモードで処理を実行していた時間です。つまり、主にアプリケーション自身の処理にCPUを使用していた時間を表します。
例えば、以下の処理などでユーザー空間でCPUを多く使用するとuser CPUが上昇します。
・Javaアプリケーションによる計算処理
・データの圧縮や暗号化
・データベース内部の計算処理
topでは、主にusとして表示されます。
system CPUとは
system CPUは、CPUがカーネルモードで処理を実行していた時間です。アプリケーションがシステムコールを実行すると、CPUはカーネルモードへ切り替わり、Linuxカーネルが処理を行います。
例えば以下の処理のようなLinuxカーネルが行う処理が発生するとsystem CPUが上昇します。
・ファイルI/O
・ネットワーク処理
・メモリ管理
topでは、主にsyとして表示されます。
userとsystemを比較する
userとsystemの違いを簡単に整理すると、次のようになります。
| 項目 | CPUモード | 主な処理 |
|---|---|---|
| user(us) | ユーザーモード | アプリケーションの処理 |
| system(sy) | カーネルモード | Linuxカーネルの処理 |
例えばCPU使用率が80%であっても、us = 70%、sy = 10%の場合と、us = 20%、sy = 60%の場合では、CPU内部で起きていることが大きく異なります。
前者では、アプリケーション自身の処理によってCPUを多く使用している可能性があります。
一方、後者では、システムコールやネットワーク処理など、Linuxカーネル側の処理にCPU時間を多く使用している可能性があります。
topやvmstatで確認できる
CPUモードごとの使用率は、topやvmstatなどで確認できます。
topコマンドの実行例
AlmaLinux9にてtopコマンドを実行した結果は以下となります。

上記の結果から、以下のことが分かります。
33.9 us:CPU時間の33.9%程度がユーザーモードでの処理に使われている
65.8 sy:CPU時間の65.8%程度がカーネルモードでの処理に使われている
vmstatコマンドの実行例
AlmaLinux9にてvmstatコマンドを実行した結果は以下となります。

us: ユーザーモードで使用したCPU時間。30%前後がユーザモードの処理に使われています。
sy: カーネルモードで使用したCPU時間。70%前後がカーネルモードの処理に使われています。
CPUモードとシステムコールの仕組みを理解しておくことで、topやvmstatのCPU使用率を単なる数値ではなく、「CPUがどのような処理に時間を使っているのか」という観点で確認できるようになります。
まとめ
本記事では、LinuxにおけるCPUモードとシステムコールの仕組みについて解説しました。
Linuxでは、システムを安全に動作させるため、CPUの実行モードを大きくユーザーモードとカーネルモードに分けています。通常のアプリケーションはユーザーモードで動作し、Linuxカーネルはカーネルモードでシステム全体に関わる処理を実行します。
アプリケーションがファイルI/Oやネットワーク通信など、Linuxカーネルの機能を必要とする場合に利用するのがシステムコールです。システムコールを実行するとCPUはカーネルモードへ切り替わり、カーネルが必要な処理を実行した後、再びユーザーモードへ戻ります。
例えばread()によるファイル読み込みでは、カーネルがファイル情報やPage Cacheを確認し、必要に応じてストレージI/Oを行います。その後、データをユーザー空間へコピーしてアプリケーションへ処理を戻します。
また、straceを利用すると、アプリケーションが実際にどのようなシステムコールを実行しているのか確認できます。Linuxの内部処理を理解したり、アプリケーションの動作を調査したりする際に役立ちます。
CPUモードの仕組みを理解すると、topやvmstatに表示されるusとsyについても、
us:ユーザーモードでCPUを使用した時間
sy:カーネルモードでCPUを使用した時間
というように、CPUがどの処理に時間を使っているのかという観点で確認できるようになります。
システムコールとCPUモードは、Linux内部でアプリケーションとカーネルがどのように連携しているのかを理解するための基本的な仕組みです。これらを理解しておくことで、Linuxの動作だけでなく、CPU負荷やパフォーマンスを分析する際にも役立ちます。
LinuxにおけるCPUの仕組みについては、以下の書籍にまとめています。興味がありましたら、ぜひ読んでみてください。
LinuxのCPUの仕組みを
図解で理解したい方へ
命令実行・プロセス・スケジューラ・割り込み・ キャッシュ・マルチコアなど、 LinuxにおけるCPUの仕組みを図解を中心にわかりやすく解説しています。
Kindleで書籍を見る →


コメント