- 趣味
- その他
なぜ print(‘hello’) は表示されるのか ── 1行のPythonが動くまで

はじめに
#!/usr/bin/env python3
print('hello, world')
このPythonスクリプトを hello.py
として保存し,ターミナルで実行すると,たいていのLinux環境では何事もなく hello, world
の文字列が表示されます。
一見当たり前に思えるこの挙動の裏では,シェル・カーネル・インタプリタが連携して,多くのシステムコールが呼び出されています。
本記事では,その舞台裏をレイヤごとに覗き込みます。
取り上げるレイヤとスコープ
本稿に登場するレイヤは次のような図をイメージしています。
それぞれの説明は本文中に記載します。

どのていど説明するの?
本稿では「実際にカーネルが呼ばれる地点」にフォーカスをあてて構成します。
システムコールやlibc層の関数については名称は出しますが詳細なAPIの説明は割愛します。
ゴール
hello, world
がどの層でどう処理されるかの理解を深める- シェルが悪いのか,Pythonが悪いのか,それともカーネルか。を切り分けるヒントを学ぶ
1. シェルがやっていること
1-1. コマンド解析と実行可能判定
- 構文解釈
bash
は入力された文字列./hello.py
をトークンに分割し,最初の部分が相対パスであることを判断します
ここではカレントディレクトリに存在するhello.py
を指しています - ファイル探索と権限チェック
- ファイルが存在しているか,実行ビット
x
が付与されているかを確認 - 実行ビットが無い場合 → "Permission denied" を即時出力して実行を中断
- ファイルが存在しているか,実行ビット
- 実行準備
bash
は自プロセスを残したまま子プロセスを生成
hint
実行に失敗する場合は権限不足が原因となるケースがあります。トラブル時はまずls -l
で実行ビットx
が付与されているかを確認してみてください。

1-2. プロセス生成
- 子プロセスを生成
bash
はまず自プロセスでaccess()
などを使って実行権限を確認したあと,fork()
(内部ではclone3
またはclone
が呼ばれる) で子プロセスを作ります - 子プロセス側の処理
- 子プロセスで
execve("./hello.py", …)
を発行し,自身を hello.py で置き換えます。 - 親プロセスである
bash
本体はwaitpid()
で,子プロセスの終了を待機
- 子プロセスで
2. shebangとインタプリタの解決
2-1. shebangは誰が読む?
execve()
直後のカーネル処理
カーネルはファイルの先頭を検査し,#!
から始まっていれば,スクリプト扱いと判断されます- shebang文字列を解釈
以降の文字列を実行すべきプログラムと引数としてパースします - メモリ再ロード
元のhello.py
プロセスは破棄され,shebangで指定されたプログラムに置き換わります

2-2. /usr/bin/env python3
の意味とPATH解決
- shebangのコマンド部分は
/usr/bin/env
env
がPATHを走査しpython3
を探索- 見つかった
python3
にhello.py
を渡してexecve()
を実行strace
を実行すると複数回にわたってexecve()
が呼び出されていることが確認できます
hint
複数バージョンのPythonが混在している環境では,どのpython3
が呼ばれたかをwhich -a python3
で確認すると依存関係トラブル解決の助けになります。
3. Pythonがスクリプトをどう処理するか
3-1. ソース読み込み → 抽象構文木(AST)
hello.py
を読み取り字句解析でトークン化- パーサがASTを生成
$ python3 -m ast hello.py
Module(
body=[
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='hello, world')]))])
このように実行すると,スクリプトのASTを確認することができます。
3-2. AST → バイトコード
コンパイラが各ノードをPythonバイトコードに変換します。
バイトコードは,後述する「Python仮想マシン」がスクリプトを実行するための中間表現です。
実際にどんな命令が生成されているかは,標準ライブラリの dis
モジュールで確認できます。
import dis
dis.dis("print('hello, world')")
0 RESUME 0
1 LOAD_NAME 0 (print)
PUSH_NULL
LOAD_CONST 0 ('hello, world')
CALL 1
RETURN_VALUE
3-3. バイトコード評価
生成されたバイトコードは,Pythonインタプリタの評価ループによって命令単位で実行されます。
このループの実装は,次のような処理をしています:
- スタックに値を積む/取り出す
- 関数を呼び出す
- 例外処理を挟む
ここで先程の命令が処理され,最終的に print()
関数が呼ばれます。
note
Python v3.11 以降はSpecializing Adaptive Interpreter (適応的特殊化インタプリタ) が導入され,評価プロセスの最適化が進みました。
4. I/O ルート
4-1. print()
の内部
print()
はPythonの組み込み関数で,内部的には sys.stdout.write()
を呼ぶように実装されています。sys.stdout
オブジェクトは,次のような多層にラップされた出力ストリームオブジェクトです。
# デフォルト設定の例
TextIOWrapper
|
+-- BufferedWriter
|
+-- FileIO (C拡張,最下層)
各層の役割:
TextIOWrapper
: 文字列をバイト列にエンコードBufferedWriter
: バッファリング処理FileIO
: 最終的にOSのファイル記述子に対して書き込み
4‑2. write()
によるファイルディスクリプタへの書き出し
- 内部 C 実装で直接 write(fd=1, buf, len) を呼び出す
fd
= ファイルディスクリプタ。すぐ後ろのセクションに補足を記載しています - カーネルは
fd=1
の対象TTYへバッファをコピー - 仮想端末ドライバが画面バッファに反映し,文字が出力されます
4-3. (補足) ファイルディスクリプタと標準出力
UNIX系のOSでは,すべての入出力はファイルディスクリプタ(fd)を介して行われます。
通常のプロセスは起動時に以下の3つfdを持っています
ファイルディスクリプタ | 意味 |
---|---|
0 | 標準入力 (stdin) |
1 | 標準出力 (stdout) |
2 | 標準エラー出力 (stderr) |
つまり,Pythonから最終的に fd = 1
に "hello, world\n"
が送られることで,ターミナルに文字列が表示されるわけです。
5. カーネルI/Oでの write()
処理(標準出力の場合)
5-1. Virtual File System (VFS)
sys_write()
システムコールは,最初にVFSという抽象レイヤに入ります。
このVFSは,異なる種類のファイルやデバイスに対して統一的なインターフェースを提供する役割を持っています。
呼び出し手順は次のようになります:
- ファイルディスクリプタ(fd) に基づいて,
struct file
を取得します。 - その
struct file
が指すファイル操作の関数ポインタを介して,適切なデバイスドライバ(この場合はTTYドライバ)を呼び出します。
TTY: UNIX系のOSが"端末"として扱う入出力デバイスの総称 - TTYドライバの
tty_write()
を実行し,データを画面に書き込みます。
5‑2. 端末(TTY) へのルート
write()
が呼ばれてから画面に hello, world
が出力されるまで,データは次のように流れていきます。
ユーザ空間
│ write(fd=1, ...)
v
カーネル空間
sys_write() → VFS → tty_write() → バッファ → 画面
実際に画面出力されるまでの過程では,割り込みやスケジューラを介して非同期に処理されます。
これらの処理はスタックに積まれ,すべて解決するとユーザプロンプトが返ってきます。
6. 全レイヤの俯瞰と要点まとめ
6-1. print('hello, world')
という1行の裏起こること
これまで説明したものを短く求めると次のようになります:
処理の主体 | 処理の流れ |
---|---|
ユーザ入力 | ./hello.py |
シェル | access() → clone() → 子プロセスに分離 → execve(hello.py) |
カーネル | shebang解析 → execve(python3) |
Python VM | ソース → AST → バイトコード → print() |
I/Oスタック | TextIO → Buffered → FileIO → write() |
カーネルI/O | VFS → tty_write() → (割り込み処理(IRQ)) |
端末 | 画面に "hello, world" が表示される |
6‑2. レイヤ別 "ここを見る" チェックリスト
今回ご紹介した視点で処理や障害を追跡するツールをまとめました:
レイヤ | ツール | 障害切り分けのポイント |
---|---|---|
シェル | ls -l , which | 実行ビットとPATH |
カーネル | strace , dmesg | execveの呼び出し, I/O ブロック |
Python | python3 -m dis , python3 -u | バッファ遅延・バイトコード確認 |
I/O | stty -a , /dev/pts | tty ごとの設定違い |
ご自身でも確認したい。デバッガで解決しない問題に取り組みたい。といったときの参考にしてみてください。
7. おわりに
print('hello, world')
と書いて実行する。
それだけのことに,これほど多くの仕組みや技術が関わっていたということに,少し驚きを感じた方もいるかもしれません。
こうした仕組みは,普段「意識しなくても動く」ものです。しかし,少しだけ手を伸ばして中を覗いてみると,意外な面白さや発見,そして「わかるって楽しい」という感覚が待っています。
本稿では省略している箇所もたくさんあります。
是非,これを読んで下さってるあなた自身も print('hello, world')
の裏側を覗いてみてください。