[読書録]Linuxのしくみ
OS
OSのない世界だと、複数のプロセスが独自にデバイスやメモリにアクセスする。
→ 開発コスト高。複数のアプリのプロセス同士が競合するリスクも、、
→ デバイスへのアクセスを引き受けるプログラム(デバイスドライバ)、メモリ管理を引き受けるプログラムがあったら便利。
→ ルールに従わない独自の方法でデバイスやメモリにアクセスする輩がいたら困らない?
→ メモリ/プロセス管理とか、デバイスアクセスとか勝手に触られると困る処理はまとめて提供し、アプリケーションプロセス側から好き放題去れないようにしよう。 = これらの機能群をカーネル(OSの核)という。
どうするの?
CPUにカーネルモードとユーザモードの2つのモードを持たせる。カーネルの機能はカーネルモード、プロセスの処理はユーザモードで稼働させ、カーネルモードで稼働する時にのみデバイスなどのリソースにアクセスできる。
ユーザモードで動くプロセスからカーネルの管理するリソースにアクセスしたいなら、カーネルにシステムコールでお願いする必要がある。
OS = カーネル(中核機能) + その他のプログラム(ユーザモードで動作する。ライブラリとか。)
ユーザモードで稼働するOSライブラリやユーザのプロセスからカーネルの管理する機能にアクセスしたい場合はシステムコールを使う。これはOSライブラリであっても一緒。
システムコール
pythonのprint関数を使って出力する時にどれだけのシステムコールが発行されるかをstraceコマンドで確認できる。
MacOSにはstraceがないのでコンテナを立てた。
環境構築〜実行まで
FROM alpine:3.16
WORKDIR /app
COPY . /app
RUN apk update && apk add --no-cache python3 && python3 --version
RUN apk add strace
RUN apk add sysstat
#!/bin/python3
print("hello")
$ docker build -t [arbitrary_name] .
$ docker run -it [arbitrary_name]
/app # strace -o out.txt python3 test.py
(略)
write(1, "hello\n", 6) = 6
(略)
1行ごとにシステムコールの情報が書かれてる。
writeシステムコールで出力している。
ユーザモードで実行されるたった1行の出力するだけのpythonコードが何回もカーネルを出しているのがわかる。
/app # sar -P ALL 1
Linux 5.10.25-linuxkit (19b3d6b785f9) 01/20/23 _x86_64_ (2 CPU)
17:58:17 CPU %user %nice %system %iowait %steal %idle
17:58:18 all 0.50 0.00 0.50 0.00 0.00 98.99
17:58:18 0 0.00 0.00 0.00 0.00 0.00 100.00
17:58:18 1 1.00 0.00 1.00 0.00 0.00 98.00
(略)
▼ pythonスクリプトを実行したタイミング。systemの割合が増えた。
18:07:23 CPU %user %nice %system %iowait %steal %idle
18:07:24 all 1.08 0.00 3.23 0.00 0.00 95.70
18:07:24 0 1.09 0.00 3.26 0.00 0.00 95.65
18:07:24 1 1.06 0.00 3.19 0.00 0.00 95.74
- 各カラムの意味は以下。https://blog.denet.co.jp/sarcomm01/
-
%systemはカーネルモードで処理している時間の割合。一般にこの値が高い(数十)場合、システムの負荷を見直した方がいい。
標準Cライブラリ
Linuxで提供されている。glibc(←GNUプロジェクト提供のもの)。
lddコマンドで指定したプログラムが共有するダイナミックリンクライブラリ(実行時にリンクするライブラリ)を一覧で出力する。
/app # ldd /bin/echo
/lib/ld-musl-x86_64.so.1 (0x7f2db208c000)
libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f2db208c000)
プロセス
生成
-
fork()で子プロセス用のメモリ領域を作成後、親プロセスのメモリの中身をコピー。 - 親プロセスと子プロセスで
fork()の返り値が異なるので異なるコードを実行させるのは返り値を利用する。
- 子プロセス : 0
- 親プロセス : 子プロセスのID
- 子プロセス側で
execve()を実行し、パスで指定されたプログラム(実行可能なファイル(※))からプログラムやデータ、メモリマップに必要なメタデータを取得し、自身のプロセスのメモリを新たなプログラムを上書き、実行する。
fork、execveのシステムコールの発行をstraceで確認するには監視用に別のターミナルを開いてstrace -pf <PID>を実行する必要がある。
* pオプション: 実行中のプロセスに対してstrace
* fオプション: 子プロセスも追跡
- https://endy-tech.hatenablog.jp/entry/system_call_fork_clone_execve
- https://qiita.com/sxarp/items/d66a13c58ad99dbf43ea
終了
-
_exit()システムコールにより終了する。終了時に開いている全てのファイルディスクリプタは閉じられ、割り当てられていたメモリは回収される。子プロセスを所有している場合はinitプロセス(PID1)によって継承され、親プロセスに対してSIGCHLDシグナルが送出される。明示的に呼び出す機会は少なく標準Cライブラリではexit()関数に内包される。
実行可能なファイル(ELFバイナリ)
プロセスを生成し実行する実行ファイルには、コードや変数などのデータのほかにそれらがファイル上のどの位置(オフセット)に書かれ、サイズがいくつか。メモリに展開する時の開始アドレスや実行時のエントリポイントといったメタデータが含まれる。
実行時にはそれらメタ情報に従って適切にメモリに展開し、エントリポイントから実行が行われる。
Executable and Linkable formatの略。readelfコマンドでメタデータを確認可能。