Atsushi2022の日記

データエンジニアリングに関連する記事を投稿してます

読書メモ~Linuxのしくみ(更新中)

概要

Linuxのしくみ」を読んで大事そうなところをメモ

コンピューターシステムの階層

階層
ユーザープログラム
OS外ライブラリ
OSライブラリ
カーネル
ハードウェア
  • 実際にはこんなに綺麗に階層化されてない

用語

プログラム

  • Go言語などのコンパイラが多言語だと、ビルドした後の実行ファイルがプログラム
  • Pythonとかのスクリプト言語だと、ソースコードそのものがプログラム
  • カーネルもプログラムの一種。マシンの電源を入れるとカーネルが起動し、それ以外のプログラムはすべてカーネルの後に起動する
  • プログラムとは、コンピュータ上で動作する一連の命令およびデータをひとまとめにしたもの

プロセス

  • 動作中のプログラムのことをプロセスと呼ぶ
  • 動作中のプロセスのことをプログラムと呼ぶこともあるので、プログラムの方がより広義な概念

カーネル

  • プロセスからデバイスに直接アクセスさせたくない
  • ハードウェア(CPU)にはモードという機能がある
  • カーネルモードとユーザモードがある
  • カーネルモードであればCPUは制限をかけない
  • ユーザモードであれば、CPUは特定の命令を実行できないようにする
  • Linuxの場合、カーネルのみがカーネルモードで動作し、デバイスにアクセスできる
  • プロセスはユーザモードで動作するため、デバイスにアクセスできない
  • カーネルは、プロセスが共有するリソースを一元管理して、プロセスに配分している

システムコール

  • プロセスからカーネルへの処理の依頼
  • 新規プロセス生成やハードウェア操作などの処理を依頼する際にシステムコールを発行する
  • プロセスがカーネルに対してシステムコールを発行すると、CPUで「例外」というイベントが発生し、CPUのモードがユーザモードからカーネルモードに遷移する。そして、カーネルが処理を終えたら、再びユーザモードに戻る

標準Cライブラリ

  • Linuxでも標準Cライブラリが提供されている(libc)
  • C言語で書かれたプログラムはほとんどすべてlibcをリンクしている
  • bashやecho、Pythonも標準ライブラリをリンクしている
  • lss libc.so.6が標準Cライブラリを差す
$ ldd /bin/bash
 linux-vdso.so.1 (0x00007ffd3afbb000)
 libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007efda6879000)
 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007efda6873000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efda6681000)
 /lib64/ld-linux-x86-64.so.2 (0x00007efda69e0000)
 $ ldd /bin/echo
 linux-vdso.so.1 (0x00007ffcc7529000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fba60498000)
 /lib64/ld-linux-x86-64.so.2 (0x00007fba606a0000)
$ ldd /usr/bin/python3
 linux-vdso.so.1 (0x00007fff9bcdb000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f99cd902000)
 libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f99cd8df000)
 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f99cd8d9000)
 libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f99cd8d4000)
 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f99cd785000)
 libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f99cd757000)
 libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f99cd739000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f99cdaff000)

システムコールのラッパー

システムコールアセンブリコードを使って呼び出す必要がある

X86_64X86_64アーキテクチャのCPUだと、親のプロセスIDを取得するシステムコールは次のアセンブリプリコードで発行される

mov     $0x6e,%eax
syscall

一方、arm64アーキテクチャでは、次のようになる

mov     x8,  <システムコール番号>
svc     #0

C言語ではラッパー関数 getppid() が用意されており、これを使って親のプロセスIDを取得できる

pause()もラッパー関数である

ラッパー関数により、わざわざアセンブリコードを書く必要がない

CPUアーキテクチャ

X86

https://e-words.jp/w/x86.html

インテルIntel)社がパソコンなど向けに開発・製造しているマイクロプロセッサ(MPU/CPU)製品のシリーズ名。また、同シリーズのプロセッサを動作させるための命令語の体系(命令セットアーキテクチャ)の名称。1978年に開発された16ビットMPU「8086」を始祖とするためこのように呼ばれる。

AMD64 (x86_64)

https://e-words.jp/w/AMD64.html

AMD社が開発したマイクロプロセッサ(CPU/MPU)の64ビット命令セットで、32ビットのx86系命令セットとの互換性を維持したまま64ビットコードの実行を可能にしたもの。

Intel64

https://e-words.jp/w/x86.html

インテルIntel)社がx86系マイクロプロセッサ(CPU/MPU)製品に組み込んだ64ビット命令セットで、32ビットのx86系命令セットとの互換性を維持したまま64ビットコードの実行を可能にしたもの。米AMD社のAMD64x86-64)とほぼ同じもの。

ARM64

https://e-words.jp/w/ARM64.html

英アーム(Arm)社が開発したマイクロプロセッサ(MPU/CPU)の基本設計(アーキテクチャ)の一つで、プログラムやデータを64ビット単位で処理するためのもの。

静的ライブラリと共有ライブラリ

プログラム生成の流れ

  1. ソースコードコンパイルしてオブジェクトファイルを作る
  2. オブジェクトファイルが使うライブラリをリンクし、実行ファイルを作る

  3. 静的ライブラリ

    • リンク時に、ライブラリ内の関数をプログラムに埋め込む
  4. 共有ライブラリ
    • リンク時に、ライブラリの関数の参照先を実行ファイルに埋め込む
    • プログラムの実行中にライブラリをメモリにロードして、ライブラリの関数を利用する

以下のようにpause.cというコードを用意する。

#include <unistd.h>
int main(void) {
        pause(5);
        return 0;
}

静的リンクする場合は次のようにコンパイルする

これでpauseという名前の実行ファイルが作成される

cc -static -o pause pause.c

ls -llddでサイズ・リンク状態を確認する。

一方、共有ライブラリをリンクさせる場合は次のようにコンパイルする

cc -o pause pause.c

プロセスを分裂させるfork()関数

  • fork()関数はプロセスを分裂させるシステムコールのラッパー関数
  • 親プロセスのメモリを子プロセス用にコピーを作成する
  • fork()関数の戻り値は、親プロセスからの戻り値は子プロセスのIDで、子プロセスからの戻り値は0である
  • 戻り値を利用して、処理を分岐させることができる
#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
    print("子プロセス: pid{}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
    exit()
elif ret > 0:
    print("親プロセス: pit={}, 子プロセスのpid={}".format(os.getpid(), ret))
    exit()
sys.exit(1)
  • 実行すると次のように親プロセスと子プロセス両方にPIDが表示される
$ ./fork.py
親プロセス: pit=355, 子プロセスのpid=356
子プロセス: pid356, 親プロセスのpid=355

別のプログラムを起動するexecve()関数

  • execve()関数は現在のプロセスのメモリを、実行ファイルから読みだした内容で書き換える
  • それによって、新プロセス用のプロセスメモリが作成され、新プロセスを開始できる
  • fork()関数と組み合わせることで親プロセスから新しいプロセスを生成できる
#!/usr/bin/python3
import os, sys
ret = os.fork()
if ret == 0:
    print("子プロセス: pid={}, 親プロセスのpid={}".format(os.getpid(), os.getppid()))
    os.execve("/bin/echo", ["echo", "子プロセス pid={} からこんにちは".format(os.getpid())], {})
    exit()
elif ret > 0:
    print("親プロセス: pit={}, 子プロセスのpid={}".format(os.getpid(), ret))
    exit()
sys.exit(1)
$ ./fork-and-exec.py
親プロセス: pit=387, 子プロセスのpid=388
子プロセス: pid=388, 親プロセスのpid=387
子プロセス pid=388 からこんにちは

initプロセス起動までの流れ

  1. コンピュータ電源オン
  2. BIOSUEFIなどのファームウェアが起動してハードウェア初期化
  3. ファームウェアGRUBなどのブートローダを起動
  4. ブートローダがOSカーネルLinuxカーネル)を起動
  5. Linxカーネルがinitプロセスを起動
  6. initプロセスが子プロセスを起動していく

↓ initプロセスが子プロセスを起動した様子

$ pstree -p
init(1)─┬─init(13)─┬─init(14)───bash(15)───sudo(303)───unshare(304)───bash(305)
        │          └─init(84)
        ├─init(320)───init(321)───bash(322)───pstree(390)
        └─{init}(6)

プロセスの状態

  • 実行可能状態:CPU実行権がない状態
  • 実行状態:CPU実行権を得た状態
  • スリープ状態:アイドルプロセス動作している状態。イベントが発生すると、実行可能状態に遷移
  • ゾンビ状態
  • プロセス終了

プロセスの終了

exit_group()というシステムコールでプロセスを終了できる

このシステムコールの中で、カーネルがメモリなどのプロセスのリソースを回収する

プロセス終了後に、wait()waitpid()というシステムコールを呼び出すことで、プロセスの戻り値、シグナルによって終了したか否か、終了までにどれだけのCPU時間を使ったかを確認できる。

wait()システムコールを呼ぶことで上記情報を得られるということは、プロセス終了後もなんらかの形で終了した子プロセスがシステム上に存在していることを意味する

オフセット、サイズ、メモリマップ開始アドレス、エントリポイント

実行ファイルでは、プログラムのコードやデータに加えて、以下を保持している

  • コード領域のファイル上オフセット、サイズ、メモリマップ開始アドレス
  • データ領域のファイル上オフセット、サイズ、メモリマップ開始アドレス
  • エントリポイント(最初に実行する命令のメモリアドレス)

ファイル上オフセット、サイズ、メモリマップ開始アドレスはreadelf -S <実行ファイル名>で確認できる。

エントリポイントはreadelf -h <実行ファイル名>で確認できる。

$ cc -o pause -no-pie pause.c
$ readelf -S pause
There are 31 section headers, starting at offset 0x3938:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.propert NOTE             0000000000400338  00000338
       0000000000000020  0000000000000000   A       0     0     8
  [ 3] .note.gnu.build-i NOTE             0000000000400358  00000358
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000040037c  0000037c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000004003a0  000003a0
       000000000000001c  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000004003c0  000003c0
       0000000000000060  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           0000000000400420  00000420
       000000000000003e  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           000000000040045e  0000045e
       0000000000000008  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000400468  00000468
       0000000000000020  0000000000000000   A       7     1     8
  [10] .rela.dyn         RELA             0000000000400488  00000488
       0000000000000030  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             00000000004004b8  000004b8
       0000000000000018  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000401000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000401020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.sec          PROGBITS         0000000000401040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
  [15] .text             PROGBITS         0000000000401050  00001050
       0000000000000175  0000000000000000  AX       0     0     16
  [16] .fini             PROGBITS         00000000004011c8  000011c8
       000000000000000d  0000000000000000  AX       0     0     4
  [17] .rodata           PROGBITS         0000000000402000  00002000
       0000000000000004  0000000000000004  AM       0     0     4
  [18] .eh_frame_hdr     PROGBITS         0000000000402004  00002004
       0000000000000044  0000000000000000   A       0     0     4
  [19] .eh_frame         PROGBITS         0000000000402048  00002048
       0000000000000100  0000000000000000   A       0     0     8
  [20] .init_array       INIT_ARRAY       0000000000403e10  00002e10
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .fini_array       FINI_ARRAY       0000000000403e18  00002e18
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000403e20  00002e20
       00000000000001d0  0000000000000010  WA       7     0     8
  [23] .got              PROGBITS         0000000000403ff0  00002ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000404000  00003000
       0000000000000020  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000404020  00003020
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000404030  00003030
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00003030
       000000000000002b  0000000000000001  MS       0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00003060
       00000000000005e8  0000000000000018          29    45     8
  [29] .strtab           STRTAB           0000000000000000  00003648
       00000000000001ca  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  00003812
       000000000000011f  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
$ readelf -h pause
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401050
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14648 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 30

エントリポイントの値以下の通り。

Entry point address:               0x401050

namespace

システムに存在する様々な種類のリソースに対してそれぞれnamespaceが存在する

あるnamespaceに所属するプロセスに見かけ上は独立したリソースを見せる機能

例えば以下の種類がある。

pid namespace

以下の例では、pid nsが4026532192である。

$ ls -l /proc/$$/ns/pid
lrwxrwxrwx 1 bluen bluen 0 Mar 16 23:21 /proc/15/ns/pid -> 'pid:[4026532192]'

unshare --pidコマンドで、pid nsを新規作成しbashを新規作成したnamespace上で実行する。

pid nsが4026532211になっており、親pid nsとIDが異なっていることがわかる。

さらに、子pid nsからは、親pid nsのプロセスが見えないことがわかる。

$ sudo unshare --fork --pid --mount-proc bash
$ echo $$
1
$ ls -l /proc/1/ns/pid
lrwxrwxrwx 1 root root 0 Mar 16 23:23 /proc/1/ns/pid -> 'pid:[4026532211]'
$ ps ax
  PID TTY      STAT   TIME COMMAND
    1 pts/0    S      0:00 bash
    9 pts/0    R+     0:00 ps ax

別の端末をひらいて、親pid nsから確認すると、子pid nsに属するbash (PID=305)は確認できる。

つまり、親pid nsからは子pid nsのプロセスが確認できる。

さらに親pid nsで確認できるbashのプロセスID (PID=305)と子pid ns内で確認できるプロセスID (PID=1)が異なる点にも注意。

$ pstree -p
init(1)─┬─init(13)─┬─init(14)───bash(15)───sudo(303)───unshare(304)───bash(305)
        │          └─init(84)
        ├─init(320)───init(321)───bash(322)───pstree(387)
        ├─{init}(6)
        └─{init}(386)

Linuxカーネルで利用できるnamespaceが増えていっている。

コンテナ

コンテナは前述のnamespaceを利用して、他のプロセスとは実行環境がわかれているプロセスのこと。

どのnamespace (pid ns, user ns, mount ns)を分離するかは、コンテナランタイムによって異なる。

他のプロセスと実行環境が分かれているため、ホストOSや他のコンテナに起因する問題があった場合には、コンテナの中からではわからないことに注意が必要。

さまざまなコンテナランタイム

コンテナの場合、ホストとなるシステムとホスト上の全コンテナがカーネルを共有する

カーネル脆弱性がある場合、コンテナのユーザによってホストOSや他のコンテナの情報を盗み見られるリスクがある

Docker以外に、さまざまなコンテナランタイムが生まれている

  • Kata Container
  • gVisor