linuxを制すものはITを制す

とはよく言ったものだ。 将来の夢はlinuxカーネルを理解することである自分ですが、まあ、そこへのかけ橋が必要ということで、「linuxの仕組み」を読むわけですね。裏表紙を見るだけで良本感が出ているこの本、さっそく読んでいきましょう

ちなみに、筑波大学のこの人の授業もめっちゃためになります。 ref

1章:Linuxの概要

プログラムとは何か、プロセスとは何かを簡単に説明した後、カーネルの必要性について話しています。 これはかなり重要だけど、カーネルモードとユーザモードはcpuによって提供されている機能なんですね。カーネルモードで動いているプログラムは何の制限もないのに対して、ユーザモードで動いているプログラムには特定の命令を実行できなくする等、様々な制限がかかるんですね。 で、linuxの場合は、カーネルだけがカーネルモードで動作しています。つまり、プロセスはユーザモードで動いている、ということですね。だから、プロセスはカーネルを通じてデバイスにアクセスするのですが、この時にカーネルに操作をお願いする命令のことを「システムコール」というんですね。そうなんです、システムコールです。 カーネルの助けがないと実行できない処理をカーネルにお願いするんです。具体的には、nicを介した通信とかはそうですね。あとは、デバイスへのアクセス。プロセスの生成、削除。ファイルシステム操作。メモリ確保、解法。など。 システムコールが発行されると、cpuにおいて例外というイベントが発生するんですね。で、これをきっかけにcpuのモードがユーザモードからカーネルモードに遷移します。で、システムコールが終了すると再びユーザモードに戻ってプロセスが動くって感じですね。 で、straceコマンドってのがプログラムの中で発行されるシステムコール一覧を出してくれるんだよね。

strace python

だけでもシステムコールがたくさん呼び出されることからも、pythonがインタープリタ言語であることがわかると思う。 前に研究室のサーバでwコマンドがハングしてしまう問題が起こったよね。そういう時は

strace w

とかってやると、wコマンド実行中のシステムコールがみられて、どこでハングしているのかとかがわかるわけですね。

こないだ調べた時は、特定のプロセスのプロセス情報 (/proc/<pid>/cmdlineとか) を閲覧しようとするとハングしてしまっていました。strace wとかするとreadシステムコールがハングするのがわかるはず。

ただ、なぜそうなってしまうのかはわかっていません…調べるとcgroupsによるメモリ制限と関係がありそうな情報が出てきましたが、まだ調査していません。
sudo yum install -y sysstat

つぎのコマンドでsysstatを起動しましょう

sudo systemctl start sysstat.service 

sarコマンドで遊びましょう。

sar -P 0 1 1

ちなみに、このコマンドの意味は、論理CPU0の(-P 0)のデータを1秒ごとに1回だけ収集しましょう。てこと。

sar -P 0 1 4

をやると次のような結果が出てくるよ。

Linux 5.15.32-v8+ (zeta)        09/02/23        _aarch64_       (4 CPU)

16:08:49        CPU     %user     %nice   %system   %iowait    %steal     %idle
16:08:50          0      0.00      0.00      0.00      0.00      0.00    100.00
16:08:51          0      0.00      0.00      0.00      0.00      0.00    100.00
16:08:52          0      0.00      0.00      0.00      0.00      0.00    100.00
16:08:53          0      0.00      0.00      0.00      0.00      0.00    100.00
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00

この表の見方は、CPU 0がユーザモードで動作した時間の割合(%userと%nicdの和)、カーネルモードで動作した時間の割合(%system)、何もしていない時間の割合(%idle)って感じだね。

なるほど、でtasksetっていうコマンドを使うと、指定した論理cpu上で任意のコマンドを実行できるらしい。やってみようか。

taskset -c 0 ping google.com &

最後の&を付けることでバックグラウンド実行できる。これは大事だよ!(バックグラウンドで実行されていても所詮はターミナルに紐づいていることを忘れないで。ログアウトしたら終わってしまうよ。ターミナルから完全に分離する為にはdisownってのを使わないといけない。まあ調べてみてくれ。) この状態で先ほどのsarをすると。いや、バックグラウンドで実行される結果が全部前に出てきてよくわからん!!。てのを防ぐためにリダイレクトをするんですね=!!!

taskset -c 0 ping google.com > /dev/null &

こんな感じです。ちなみに、/dev/nullってのは

/dev/nullとは、LinuxなどのUNIX系OSの特殊なデバイスファイルの一種で、投入された入力を単に捨て去る機能を提供するもの。英語圏では俗に “black hole” (ブラックホール)、“bit bucket” (ビットバケツ)などと呼ばれることもある。

です。

あとはそうだな、strace のTオプションでプログラム中の各システムコールにどれくらい時間がかかったかがわかるようになる。

strace -T -o nmap.txt nmap 172.20.2.0/24 > kami.txt &

まあ見てみてください。

ビルド = コードをコンパイルしてオブジェクトファイルの生成 + ライブラリのリンクを実行し、実行ファイルを生成すること。

これだけは忘れないように。で、lddコマンドでその実行binaryが内部でどんなライブラリを使っているかがわかるってやつね。例えば、python3

ldd python3

では動かないので、

which python3

で絶対パスを取得し、

ldd /usr/bin/python3
linux-vdso.so.1 (0x00007ffefa282000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff641939000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff641916000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff641910000)
        libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007ff64190b000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007ff6417bc000)
        libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007ff64178e000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007ff641770000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff641b46000)

こんな感じで、python3も内部的にはめちゃめちゃcのライブラリを使っているということがわかるね。というか、ライブラリって全部cで書かれているものなのか?ってのが気になるところ。コンパイル言語であればライブラリって作れるの?どうなんだろ。

こんな感じで、

linux-vdso.so.1 (0x00007ffefa282000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff641939000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ff641916000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff641910000)
        libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007ff64190b000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007ff6417bc000)
        libexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007ff64178e000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007ff641770000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff641b46000)
ray@evn:~$ ldd /usr/local/bin/node
        linux-vdso.so.1 (0x00007ffc417be000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2dae373000)
        libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f2dae159000)        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2dae00a000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f2dadfef000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2dadfcc000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2daddda000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2dae394000)

内部ではめちゃめちゃcのライブラリを使っていることがわかるかと思います。

で、静的リンクと動的リンクの話が出てくる。 静的ライブラリ(lib.a)と動的(共有)ライブラリ(lib.so)がリンクされる時の話。 静的リンクはビルドするときにライブラリのbinaryを実行ファイルに組み込んでしまうリンク方法。動的(共有)リンクはビルド時にはライブラリの場所だけを記憶しておいて、実行時に実際にメモリに読み込まれるときになって初めてリンクする方法。 (さらに、共有ライブラリはメモリ上に一つしか配置されないっていう話もある)

2章:プロセス管理(基礎編)

まずプロセスの親子関係について説明しています。 プロセスが生成されるタイミングというか、目的には

  1. 同じプログラムの処理を複数のプログラムにわけて処理する
  2. 別のプログラムを生成する の二つがある。

1は普段意外と意識している奴よ。プログラムの中からfork()を使って複製するやつね。ただ、2っていうのが意外と意識しないところで。どういうことかっていうと、プロセスを生成するのも言ってしまえばシステムコールだから、例えば、bashとかのshellからプログラムを起動するときも実は内部ではシステムコールが使われているよ?って話。 それが、execve()システムコールだね。 実際にプロセスから別のプロセスを起動する時はそうやって起動する見たい。

新しいプロセスを作成するとき、Linuxではfork()を使用します。
fork()で親プロセスは子プロセスを生成し、子プロセスで実際に実行しようとするプログラムをexec()を通じて実行します。

これは使えそうだよね。メインのプログラムに関連するプログラムを生成したいときはこうやってfork()からのexec()をやるんだね。 子プロセスを生成してから実行したいプロセスを子供に実行させるというね。

で、プロセスってのは自分のプロセスidと自分を生成した親プロセスのidをコンテクストに持っているんだね。で、bashもプログラムだし、sshで入るときに起動されるbashも実はsshdの子プロセスなんだよね。これは確かにそうだわって感じ。すべての親子関係を表示するのが、

pstree -p 

で、結果が

           ├─blkmapd(170)
           ├─bluetoothd(842)
           ├─containerd(556)─┬─{containerd}(609)
           │                 ├─{containerd}(610)
           │                 ├─{containerd}(611)
           │                 ├─{containerd}(612)
           │                 ├─{containerd}(616)
           │                 ├─{containerd}(781)
           │                 ├─{containerd}(782)
           │                 ├─{containerd}(784)
           │                 ├─{containerd}(788)
           │                 ├─{containerd}(2431)
           │                 ├─{containerd}(251737)
           │                 └─{containerd}(722757)
           ├─cron(758)
           ├─dbus-daemon(462)
           ├─dhcpcd(911)
           ├─dhcpd(938)─┬─{dhcpd}(939)
           │            ├─{dhcpd}(940)
           │            └─{dhcpd}(941)
           ├─dnsmasq(661)
           ├─hciattach(830)
           ├─login(931)───bash(977)───keychain(990)───ssh-add(1106)
           ├─mariadbd(696)─┬─{mariadbd}(783)
           │               ├─{mariadbd}(787)
           │               ├─{mariadbd}(789)
           │               ├─{mariadbd}(792)
           │               ├─{mariadbd}(801)
           │               ├─{mariadbd}(802)
           │               └─{mariadbd}(880362)
           ├─munged(32323)─┬─{munged}(32324)
           │               ├─{munged}(32325)
           │               └─{munged}(32326)
           ├─packagekitd(116547)─┬─{packagekitd}(116548)
           │                     └─{packagekitd}(116549)
           ├─polkitd(116551)─┬─{polkitd}(116552)
           │                 └─{polkitd}(116554)
           ├─rngd(777)─┬─{rngd}(778)
           │           ├─{rngd}(779)
           │           └─{rngd}(780)
           ├─rpc.idmapd(419)
           ├─rpc.mountd(566)
           ├─rpcbind(420)
           ├─rsyslogd(474)─┬─{rsyslogd}(501)
           │               ├─{rsyslogd}(502)
           │               └─{rsyslogd}(503)
           ├─slurmctld(28096)─┬─{slurmctld}(28097)
           │                  ├─{slurmctld}(28098)
           │                  ├─{slurmctld}(28099)
           │                  ├─{slurmctld}(28100)
           │                  ├─{slurmctld}(28101)
           │                  └─{slurmctld}(28103)
           ├─slurmd(32689)
           ├─ssh-agent(1072)
           ├─sshd(707004)───sshd(879832)───sshd(879839)───bash(879840)─┬─pstree(88+           │                                                           └─strace(88+           ├─systemd(962)───(sd-pam)(963)
           ├─systemd-journal(143)
           ├─systemd-logind(482)
           ├─systemd-timesyn(880206)───{systemd-timesyn}(880209)
           ├─systemd-udevd(235)
           ├─thd(483)
           ├─unattended-upgr(569)───{unattended-upgr}(785)
           ├─wpa_supplicant(490)
           └─wpa_supplicant(585)

これを見ると、sshdがsshdを生成していてさらにbashを生成し、その先にpstreeがいることがわかるよね。面白い。 だから、sshdも内部的にはexecve()を発行しているし、bashも内部的にはexecve()を発行しているということになる。素晴らしい。 で?一番上流にはsystemdがいるとね。見ればわかるが、こいつのプロセスidは1になっているというね。素晴らしい!!コンピュータの電源を入れてからシステムが起動するまでの流れはこんな感じになる。

  1. コンピュータの電源を入れる
  2. BIOSやUEFIなどのファームウェアが起動してハードウェアを初期化する
  3. ファームウェアがGRUB等のブートローダを起動する
  4. ブートローダがOSカーネルを起動する
  5. initプロセス(systemdと同義)が子プロセスを起動して、さらにその子プロセスを。。。と続く。
  1. 実行可能状態(idle)
  2. 実行状態
  3. スリープ状態
  4. ゾンビ状態
  5. プロセス状態

htopのsカラム

S - スリーピング
 R - 実行中
 D - ディスクスリープ
 Z - ゾンビ(親の終了ステータスの読み取り待ち)
 T - トレースまたは一時停止
 W - ページング

スリープと実行可能状態はおなじなのかな? わからん。 で、プロセスの終了にはexit_group()システムコールを呼び出す必要があるんだね。これが呼び出されると、カーネルはメモリなどのプロセスのリソースを回収するわけだ。c++とかでexit()関数を呼び出すと内部ではexit_group()が呼び出されているわけだね。 で、親プロセスは子プロセスの戻り値、シグナル、終了までに使ったcpu時間をwait()やwaitpid()システムコールを使って得ることができる。

ゾンビプロセスと孤児プロセス。 ゾンビプロセスについて

新しいプロセスを作成するとき、Linuxではfork()を使用します。
fork()から親プロセスは子プロセスを生成し、子プロセスで実際に実行しようとするプログラムをexec()を通じて実行します。

子プロセスがexit()を呼び出して終了すると、親プロセスはwaitpid()を明示的に呼び出して、この子プロセスのexit値を取得する必要があります。

子プロセスがexit()を呼び出しても、親プロセスがwaitpid()で子プロセスのPIDを回収(reaping)しなかった場合、この子プロセスはゾンビ(zombie)状態になります。

孤児プロセスは親が先に死んでしまったプロセスのこと。こうなると孤児の親はinit(systemd)になる。で、システム上にたくさんゾンビがいて、その親が回収せずに終わると、ゾンビはinitに襲い掛かるわけだね。しかし、initは賢いから、定期的にwait()を発行してゾンビを殺してあげているんだね。やさしい。

シグナルを受け取ると、シグナルハンドっていうコールバック関数が呼び出されるわけだね。これを変えることができるんだよね。 その他、よく使われれるシグナルには例えば、SIGSTOPがある。これはbashでctrl + zを押すとbashからプログラムに送信されるシグナル。あとは、ctrl + zで停止されたジョブを復帰されるのに使うfgコマンドが送るSIGCONTとか。kill < pid >で送られるSIGKILLtか。

man 7 signal

コマンドでシグナルの一覧は見られるようになっているので見てみてくださいね。

sshとかでシステムにログインするとそこにはセッションという概念が生じる。そして、ログインシェルがセッションリーダとなり、セッションリーダのpidがセッションのidとなる。で、例えばセッションに紐づいている端末がハングアップ(sshのターミナルを閉じるとか)すると、セッションリーダにSIGHUPが送られる。で、この時bashは自分が管理するジョブを終了してから自分も終了するんだよね。だからセッションが閉じるとバックグラウンドジョブも全部死ぬわけだね。これを防ぐための仕組みがnohupコマンドとdisownコマンド。 使いわけの仕方だけど、disownは実行が開始してしまったプロセスをbashの管理下から外す時に使う。nohupは実行開始するときにSIGHUPが送られてもプロセスが終了しないようにする。

./a.out 
ctrl + z
bg 
disown 
nohup hoge 

って感じかな。

/usr/lib/systemd/systemdはsystemd本体です。

時間がないので端折っていきますが、 3章から9章まではしっかりと読み込むのがいいと思います。 ここを完璧に理解してはじめて「linuxちょっとわかる」といってもいいレベルになれるとおもいます。いや実際にこれを言っていいのはlinusさんくらいだが。

3章:プロセススケジューラ

予想だけど、おそらくタイムスライスとか、ラウンドロビン(スケジューリング)とか、その辺の話が展開されると思う。 うん、やはりそうか。システム上では複数のプロセスが動いているけど、cpuの数は限られている。一つのcpuはある特定の時間を見ると一つのプロセスしか実行していない。つまり、どのプロセスをどれだけの時間cpuに割当てるか?というのを考えないといけない。どれだけの時間割当てるか?それがタイムスライスだ。時間という無限に長い棒をある一定の時間スライスしたものを割当てるからタイムスライスだ。 で、プロセスの実行に際して、経過時間と使用時間っていうのを理解しないといけない。 経過時間っていうのはプロセスが開始してからの実際の時間。使用時間はプロセスが実際に論理CPUを使用した時間。timeコマンドを使うとわかるよ。 realが経過時間。userとsysは使用時間。それぞれユーザモードとカーネルモードで動作していた時の時間だね。 余談だけど、シェルスクリプトを使った既存のプログラムの組み合わせで大体どんなプログラムでもかけてしまう説を提唱したい。うん。すごい人ほど、「うーん、それはあれとあれを組み合わせてシェルスクリプトでパイプラインかすれば行けるんじゃね?」とか言いそう。

その後いろいろと実験をしたわけですわ。

で、この後、cpu上でタイムスライスを使い切ったプロセスが切り替わる際に、「コンテキストスイッチ」が発生する、っていうことを説明しているね。ただ、実はコンテキストスイッチが発生するパターンっていうのは3つあるって話ですね。1つ目がマルチタスキングで、今説明したやつですね。複数のプロセスを1つのcpuで実行するために各プロセスのタイムスライスを決めて実行するって話ですね。2つ目が、割り込み処理。うん、割り込みです。3つ目が、ユーザモードとカーネルモードの切り替え。これはシステムコールの呼び出しですね。そうなんですよ、これはleoの開発の時も行っていたね。Mutexにロックをかけるためにシステムコールを呼び出すんですが、これがめちゃめちゃコストが高いんですよね。だから、なるべくシステムコールは使いたくないって話です。そうなると、ロックフリープログラムにしないといけなくて、その辺が難しくなるって話でしたね。そう、ロックフリーです。

4章:メモリ管理システム

まずはfreeコマンドの見方を学びましたね。

               total        used        free      shared  buff/cache   available
Mem:         7999972      417812     4080952        7296     3501208     7450204
Swap:         102396         768      101628

こんな感じ。それぞれの項目を説明していきますか。 totalは、システムに搭載されている総合のメモリ容量。usedがシステムが利用中のメモリから、buff/cacheを差し引いたもの。 buff/cach。これは、二次記憶にあるデータへのアクセスを早くするために一次的にメインメモリへ持ってきているデータのこと。 sar -rも使えるよって話。

  1. メモリの断片化
  2. マルチプロセスの実行が困難
  3. 不正な領域へのアクセス

1に関しては、プロセスのテキストとデータが連番で、それが物理アドレスでも連番だとする。プロセスが終了すると、メモリの空きが虫食い状態になる。

2に関しては、コンパイル時点でプログラムってのはどのアドレスにアクセスするかが決まるよね。どの変数がどのアドレスに割当てられているとかそういう話。で、もし物理アドレス上でまったく同じプログラムを走らせるとすると、コンパイルされたプログラムのデータ部分も2つのプログラムはまったく同じのを共有することになるんですね。そうするとうまく動かなくなるよね。はい、そういう話。

3に関しては、プログラムが別のプログラムのアドレスを参照できてしまうかもしれないよね、という話。

以上の3つを解決するためにOSは仮想アドレスというものを提供するわけですね。 筑波大の先生の記述を見た方が早いですね。

・1台のコンピュータで複数のプロセスを走らせる(multiprogramming)。
プロセスは、それぞれ独立したアドレス空間を持つ。多重(仮想)アドレス空間(multiple (virtual) address spaces)
・仮想記憶(virtual memory)を実現する。物理メモリよりも大きなメモリを利用可能にする。 ディスクを二次記憶として使って。
・物理メモリを固定長のページ・フレーム(page frame)に分割する。
・プロセスの仮想アドレス空間は、ページ(page)の並びに分割する。
ページの大きさとページ・フレームの大きさは、同じ。 ページ・サイズと呼ぶ。ページ・サイズは、2のべき乗の大きさ。4KB-64KB が多い。
・任意のページは、任意のページ・フレームにマップできる。
・CPU は、プログラムを実行中に仮想アドレス(virtual address)を 出力して、機械語命令やデータをアクセスする。
・MMU (Memory Management Unit) は、仮想アドレスを物理アドレス(physical address) に変換する。
・MMU は、メモリ中に作られたページテーブル(page table)を見ながら 変換する。ページテーブルは、仮想アドレスと物理アドレスの対応表。
・しばらく使わないページの内容を、ディスクに退避する(page out)。
・ディスクから要求されているページの内容をメモリにコピーする(page in)
・ページアウトされているメモリをアクセスすると、ページフォールト(page fault) が発生する。 OSは、ページインの処理を行い、再開する。
・仮想アドレスの大きさは、普通、32ビットのシステムなら、32ビット。 64ビットのシステムなら、64ビット。
・物理アドレスの大きさは、搭載しているメモリのサイズによる。 32ビットのシステムでも、4GB 以上のメモリを搭載している時には、 64ビットの物理アドレスが使われることがある。 (物理的な配線は、64本より少ないことがある。)

ページテーブルに関してはこちらもためになるかと思います。 ref こちらはもっと詳しく書かれています。 ref2

ちなみに、デフラグメンテーションは2次記憶での話。メモリではMMUによってメモリがいい感じに配置されるようになっているので気にしなくていいです。

ちなみに、プログラムから見えるアドレスは仮想アドレスです。(あたりまえか) 例えば、次のようなプログラムで変数のアドレスを見ることを考えてみましょう。

#include<iostream>

const int test = 1;

int main(void){

    int a = 0;
    int* pa = &a;
    const int* ptest = &test;
    std::cout << pa << std::endl;
    std::cout << ptest << std::endl;

    return 0;
}

グローバル領域に確保されているtestは何回実行してもアドレスは変わらないです。一方、testの方は変わります。というのも、main関数の中に確保される変数って実はヒープ (スタック) に確保されるからなんだよね。これ大事。main関数も関数だからね。ということで、ヒープは高アドレスからとられていくじゃん?で、おそらく、ヒープの末端はカーネルが決めるんだけど、その時のシステムの状況によって末端は変わるから、毎回変わるんだと思う。 ちなみに、スタックが逆からとられるのを確認するために変数bを作ってみましょうか。

出力はこんな感じです。

0xbefd1210
0xbefd120c
0x1084c

うん、c,d,e,f,10だから、ちょうど32bitぶん?そうだよね、1アドレスで8bit=1byteだもんね。 c,d,e,fでちょうど4バイト分。若い方にとられているのがわかるよねー。

unsigned long int physical_address( unsigned long int virtual v ) {
    unsigned long int p, page, offset;
    p = v >> 12;         // 32中、上位20ビット(32-12==20)の取り出し
    offset = v & 0xfff;  // 下位 12 ビットの取り出し
    page = page_table[p]
    return( page + offset );
}

なるほどね、page_tableの配列に4kbiずつアドレスが入っている感じかな。

で、だ。mmap()システムコールがめっちゃ大事ですね。これは結構大事だな。

mmap()は動作中のプロセスに新規メモリ領域を割り当てる時の発行されるシステムコールなんだけど、これが発行されたときのアドレス空間の挙動がまったくわからないんですわ。 プロセスが開始した時にアドレス空間はできるよね?で、mmap()の呼び出しによってアドレス空間はどうやって伸びるのか?って話なんだよね。いや、どうやらmmap()を呼び出すと、仮想アドレス的にはヒープの下に新しい仮想アドレスを追加するみたいだね。で、新しく追加した仮想アドレスと物理アドレスの対応はページテーブルに任せると。はいはい。ページテーブルはカーネル空間にあるよって話だよね、はいはい。

あー、はいはいはい。あのね、おそらく俺は今までヒープとスタックが同じものだと勘違いしていた。しかし、実際は別のものだね。確かに、これで全部の謎が解けた気がする。あのね、ヘネパタは間違っている。stackしか説明していない。ヒープも説明したほうがいい。プロセスアドレス空間 (仮想アドレス空間) には全部で4つの領域があるんです。俺は全部で3つだと思っていた。テキスト領域、静的領域 (グローバル変数、static変数)、ヒープ領域、スタック領域。ヒープはstackのさらに下に確保されるんですね。そうなんです。いや、違うっぽいな。まて、俺はmmapで確保した領域にどうやってアクセスできるようになるかだけ知れればいいんだよ、まじで!!

ファイルマッピング

ファイルを仮想アドレス空間へマッピングした場合、OSはファイル上の対象となる領域のデータを、ビューとして、mmap()を呼び出したプロセスがアクセスできる仮想アドレス空間に割り当てる。そして、プロセスがマッピングされた領域へ書き込みを行った場合、MAP_SHARED を指定した場合は、OSはその変更を同期的あるいは非同期的にファイルへと反映する。MAP_SHARED と排他的な MAP_PRIVATE を指定した場合は、変更はファイルには反映されない。

話は変わるが、dbのページを二次記憶からとってくるときは絶対にmmap()を使ってはいけないとandyが言ってた気がする。

無名マッピング

ファイルの裏付けがないものを、無名 (Anonymous) マッピングと呼ぶ。無名マッピングを使用するには引数flags に MAP_ANONYMOUS を指定し、ファイルディスクリプタに -1 を指定する。無名マッピングを使うと利用可能なメモリ領域を仮想アドレス空間から確保できる。この機能は、アプリケーションの実行中にOSから追加のメモリリソースを獲得する方法として利用される。多くのUNIX系のCライブラリのmalloc()の実装は、小さなメモリ領域の確保はデータセグメントを拡張してそこから小分けに切り分け、大きなメモリ領域の確保のケースには mmap() を内部的に使っている。例えば、Doug Lea の実装した dlmalloc の場合、デフォルト値は256KB以上のメモリ確保に mmap() を使用する[4]。

ですね。そうです、mallocとかはmmap()を使って、OSから新しくメモリ領域をもらっているって話なんですね。これは面白いです。層なんです、ずっと不思議に思ってたんですよね。プロセスが使えるメモリって最初から決まってるのになんでめちゃめちゃメモリ使ってるプロセスが出現することがあるんだろうってね。結論は、mmap()でOSから新しいメモリ領域を割り当てられているからなんです。で、割当てられたメモリ領域は仮想アドレスとページテーブルにマッピングされるんですね。ですね。仮想アドレスでは一応連番になっているはずです。 はい、vectorとかも内部でmmap()を使っているのではないでしょうか?

2018年にmeltdown脆弱性ってのが見つかってからカーネルを仮想アドレスで実行するのをやめたらしい。 ref

ちなみに、ページアウトはmmapでやると思っているんだけど、どうなんだろう。わからないね。

第五章:プロセス管理上級

fork()関数の動作について説明しています。まあ、プロセスの複製がなされるわけだけど、なんと最初はページテーブルエントリーが複製されるだけという。で、コピーおんライトで、プロセスが複製される。だからfork()はすぐに返ってくるって話らしい。

余談ですが、concurrentとpararellは別物なんですよね。並行処理?と並列処理。1つのcpuコアは様々なプロセスをconcurrentに実行しているが、parerellには実行していない。マルチスレッドはpararellにプロセス内の処理を実行している。

第6章:デバイスアクセス

Linuxのデバイスに関してはそれだけで一冊本ができるので非常に簡単にだけまとめておきたいと思います。 ただ、将来的には、自作した通信モジュールを使って地上と気球の通信を実現したいと思っている。 はい、デバイスへのアクセスはプログラムから直接するのは不可能ですね。だから、デバイスファイルってのを使ってどのデバイスにアクセスしたいかを指定して、write()やread(),open()などのシステムコールを使ってOSを介してアクセスするって話ですね。

デバイスファイルは

/dev/

にありますよね。で、デバイスには種類が二つある。キャラクタデバイスとブロックデバイス。 /dev/でls -laをすると、cとbが出てくるので見てみてください。 キャラクタデバイスは読み出しと書き込みの二つだけの操作ができる。ブロックデバイスは読み書き以外に、シークができる。つまり、どの場所に読み書きをするかを決められるってことですね。 キャラクタデバイスには端末、キーボード、マウス等がある。ブロックデバイスは、HDDとかSSDとかのストレージデバイス。 で、プロセスはデバイスファイルを通してデバイスドライバとやり取りをする。デバイスドライバはカーネルの機能ですね。で、デバイスドライバがデバイスのレジスタとやり取りをするんですね。 具体的にどうやってやり取りをするかだけど、デバイスには仕様があって、どのレジスタにどういう書き込みをするとどういう挙動をするかっていうのが全部規定されているんですね。で、だ、メモリマップとi/Oという仕組みを使って、デバイスのレジスタをカーネル空間にマッピングしているんですね。つまり、カーネル空間にマッピングされたレジスタに書き込んだりすることで、デバイスを操作できるって話なんですよ。そう。で、生のCUDAを使ったプログラミングがまさにそんな感じになっていてる。面白い。で、処理の完了を検出するには2種類の方法があって、ポーリングと割り込みだよね。これはもう知っているよね。割り込みは、割り込みが発生した時にどんな処理をするかを規定する、割り込みハンドラっていうのを使うよね。

第7章:ファイルシステム

ここ、めっちゃ大事です。個人的にはかなり大事で。なぜかというと、これからファイルアクセスをたくさんするプログラムを書いていくことになるから、ファイルの基礎を知っていないと少し難しいと思うからです。 まず、ファイルシステムとは何か?って話なんだけど、「実際にデータが保存してあるストレージ上の領域」と「そのストレージ領域を扱う処理」のどちらもファイルシステムなわけですね。 6章でやった通り、ストレージもデバイスだからストレージにアクセスするためにはデバイスドライバを使わないとだめなわけですね。しかしですよ、デバイスドライバを使ってできることって、「ここからここの領域にこういうデータを書いて下さい」ってことだけなんだよね。つまり、データがどこにあるのかの管理、とかね、そういうのは全部自分でやる?? いやいやいやいや、って話なんですよ。そういうのをまとめてやってくれるのが「ファイルシステム」なわけです。すごいねー。すごい。当たり前のように思えてめっちゃすごいんですよね、これって。そう、結局ファイルシステムがやってくれていることも抽象化なわけです。デバイスドライバを使って、「ストレージのここからここの領域でメタデータを管理して、ここからここに実体をおく、」とか、っていうのを全部管理してくれるのが、「ファイルシステム」っていうソフトウェアなんです。そうなんです、ファイルシステムもソフトだよ?今更だけど。そう!!fat32も、xfsも、ext4も全部ソフトなんですね。 chatGPT先生にext4のソースコードがどこで見られるのか聞いてみました。

The source code for the ext4 filesystem can be obtained from the official Linux kernel repository. Here's how you can access it:

    Open a web browser and go to the Linux kernel website: https://www.kernel.org/.

    On the homepage, you will find a section titled "Latest Stable Kernel" which lists the latest stable version of the kernel. Identify the version of the kernel you are interested in obtaining the ext4 source code for.

    Once you have identified the kernel version, click on the "Browse" link next to it. This will take you to the kernel source code repository.

    In the repository, navigate to the fs directory, which contains the source code for various file systems including ext4. You can find it at: fs/ext4/.

    Within the fs/ext4 directory, you will find the source code files for ext4.

はい、そういうことです。有名すぎてgithubにソースコードはありませんでしたね。まあすごいっす。 で、だ、これが、touchコマンドを読んだ時のsystem call のトレースだ。

execve("/usr/bin/touch", ["touch", "test.txt"], 0x7ffd79be17b8 /* 45 vars */) = 0
brk(NULL)                               = 0x560a0cd9e000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffda8699d10) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=53731, ...}) = 0
mmap(NULL, 53731, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9dc8518000
close(3)                                = 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`\256\3\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2093744, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9dc8516000
lseek(3, 808, SEEK_SET)                 = 808
read(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32) = 32
mmap(NULL, 3954880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9dc7f33000mprotect(0x7f9dc80ef000, 2097152, PROT_NONE) = 0
mmap(0x7f9dc82ef000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3,
0x1bc000) = 0x7f9dc82ef000
mmap(0x7f9dc82f5000, 14528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9dc82f5000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f9dc8517580) = 0
mprotect(0x7f9dc82ef000, 16384, PROT_READ) = 0
mprotect(0x560a0c1c6000, 4096, PROT_READ) = 0
mprotect(0x7f9dc8526000, 4096, PROT_READ) = 0
munmap(0x7f9dc8518000, 53731)           = 0
brk(NULL)                               = 0x560a0cd9e000
brk(0x560a0cdbf000)                     = 0x560a0cdbf000
brk(NULL)                               = 0x560a0cdbf000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=217800224, ...}) = 0
mmap(NULL, 217800224, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9dbaf7d000
close(3)                                = 0
openat(AT_FDCWD, "test.txt", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = 3
dup2(3, 0)                              = 0
close(3)                                = 0
utimensat(0, NULL, NULL, 0)             = 0
close(0)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?

execveでtouchを呼び出しているのがわかるね。その次にtouchが走り始めるわけだ。うん。面白い。

なるほど、ext4,XFS,Btrfsなどのふぃあるシステムがあるが、これらのファイルシステムごとにストレージデバイス上に作るデータ構造、およびそれを扱うための処理が異なる。それはそうだ。データ構造が異なるんだったら、扱うための処理は異なるよね。 よく、HDDを捨てるときに初期化だったり、穴をあけたりするよね?あれは、まあ、そこに入っているデータを消去するためだっていうことはわかるんだけど、じゃあ、逆にそのへんに落ちているHDDの中身を見られるか?って言われたら、どうだろう。今だったらみられるっていうかもしれないな。 まずはsataにつなぐ。 いや、sataにつないだ時点で、osはhddのあるパーティションがどのファイルシステムでフォーマットされているかわかるのか。すごいな。 あとおもったのが、HDDでもssdでもNVMEでもどのデバイスでもext4のファイルシステムを作れるよね?で、ファイルシステムが内部でデバイスドライバを動かすわけだけど、そのデバイスドライバはたぶん規格化されているんでしょうね。そんな気がします。統一されているのかな?いや、といういつされていなくてもいいのか、デバイスごとにどういう処理をするかっていうのを書いておけば問題ない。

RAID6:

RAID6では、データからパリティ(誤り訂正符号)を生成し、4台以上のハードディスクに分散して書き込み
 します。RAID5と異なる点は、ハードディスク台数が4台以上である点、許容されるHDDの障害台数が2台と
 なる点、RAID5よりもパリティが増える分はハードディスクの利用効率が下がる点です。RAID6は、その他の
 RAID0、RAID1、RAID5 に比べて耐障害性の高いRAID構成です。ただし、二重にパリティを生成するため、
 RAID5よりもさらに書き込みが速度が低下する点はデメリット

なんか、SSDを2つ使ってBtrfsでRAID0組み立ててみたいですね!!おもしろそう!!まじで!!

最後に、ファイルシステム、ストレージデバイス系のコマンドを何個か書きます!!

  1. lsblk -f This command lists all block devices along with their filesystem information

  2. df -Th This command displays filesystem usage information for mounted filesystems

ってかんじでーす。

第8章:記憶階層

はーい、「out-of-core computingを勉強するじかんだよー!!」 コンピュータはね、計算速度と容量のトレードオフを解消するために、メモリーを階層構造にしているんだよね。で、レジスターの下にはキャッシュが置かれるんだけど、キャッシュも容量が決まっていてねー。で、キャッシュの内容はメインメモリにも反映させないといけないんだけど、その時の方法が、ライトスルーとライトバック。これはヘネパタでもやったよね。 ライトスルーはキャッシュの内容とメモリの内容を常に一致させる方法。で、ライトバックは、キャッシュラインにダーティービットっていう、変更が加わったかどうかを判定するビットをおいておいて、ダーティービットが経っているキャッシュラインが破棄されるときだけメインメモリに書き戻す、っていう方法だ。すごくわかりやすいね。 どのCPUがどっちのポリシーを使っているかをGPT先生に聞いてみたんだよ。

Both Intel and AMD processors support both write-through and write-back caching, but the default caching policy can vary depending on the specific processor model and architecture.

In general, modern Intel processors, particularly those in the Core and Xeon families, typically use a write-back caching policy as the default. Write-back caching improves performance by allowing writes to be initially stored in the cache, reducing the number of write operations to the main memory. Intel processors often employ a combination of write-back and write-through caching in different levels of the cache hierarchy.

AMD processors, including those in the Ryzen and EPYC families, also default to a write-back caching policy. Similar to Intel, AMD processors employ a combination of write-back and write-through caching strategies across different cache levels.

It's important to note that the caching policy can be influenced and modified by the operating system, applications, and BIOS settings. Some server architectures and specific applications might require explicit write-through caching to ensure data integrity.

For precise and up-to-date information on the caching policy of a particular Intel or AMD processor model, it is recommended to consult the official documentation, including the processor's datasheet, technical reference manual, or architecture manual provided by the respective manufacturer.

こんな感じです。

で、なるべくキャッシュのヒット率を上げたいんだよねー。

そのた、キャッシュの速度を買実験しているんで、是非ここはプログラムを書いて実験してみてほしい。

第9章:ブロック層

ストレージデバイスのことを、ブロックデバイスっていうんですね。だから、 lsblkっていうコマンドはブロックデバイス (ストレージデバイス) をlistするんですね。これすごく大事です。 で、本章ではブロックデバイスの性能を引き出すためのブロック層というカーネル機能について述べているわけですね。

第10章:仮想化機能

仮想化機能とは、PCやサーバなどの物理的なマシン上で、仮想マシンを動かすためのソフトウェア機能、および、それを助けるためのハードウェア機能の組み合わせ。仮想マシンの用途は、次のようなものがある。

  1. ハードウェアの有効活用: 一台のマシン上で複数の仮想マシンを作ったうえで、顧客に個々の仮想マシンを貸し出すIaas (Infrastructure as a service)

  2. サーバーの統合: 複数の物理マシンで構成されているシステムの物理マシンを仮想マシンに置き換えて、より少ない数の物理マシンに集約する。

  3. レガシーシステムの延命: ハードウェアサポートが終わった古いシステムを仮想マシン上で動かす。

  4. OS上で別のOSを動かす: windows上でLinuxを動かすとか

  5. 開発、テスト環境: 業務システム環境と同じ、または似た環境を物理マシンなしに構築する

  6. 趣味でLinuxカーネルを開発している時に、変更を加えたカーネルが正しく動作するかどうかの自動テスト。

第11章:コンテナ

コンテナはホストOSどカーネルを共有している。だから実行が早くて、立ち上げも速い。つまり軽量。素晴らしい! で、コンテナには「システムコンテナ」と「アプリケーションコンテナ」の二つがあるんだよね。で、dockerが出てくる前はシステムコンテナっていうのはもう会ったんだよね。で、dockerによって1コンテナ1プロセスの「アプリケーションコンテナ」が一気に普及して、現在はコンテナといえばアプリケーションコンテナを指すようになった。

第12章:cgroup

ついにきたねーー!!cgroup!! まず、cgroupとは「システムのメモリやCPU等のリソースをどのプロセスにどれだけ与えるか、という細かい制御をするための機能」プロセスをグループに分けて各種リソースを制御する」からCgroupという名前がついているらしい。 で、cgrooupにはv1とv2があるけど、この本ではv1を説明している。ただ、k8sのところで出てくるけど、確かk8sはcgroupのv2をつかう?のかな?その辺大事です。 で、話を戻すが、cgroupはどんなところで需要があるか?だけど、例えば、レンタルサーバだよね。VPSとかはまさにcgroupの機能を使って実現されていると思うよ。つまり、VPSは共有サーバ上で仮想マシンを使って動いているわけだけど、この仮想マシンにはこれだけのCPUを割当ててくださいーいとか、これだけのRAMを割当ててくださーいとかね。

  1. CPU
  2. memory
  3. ブロックi/o
  4. ネットワークi/o

ですね。まあ、応用範囲はかなりあるよね。CPUコントローラを使えば、特定のプロセスに資源が全部使われてしまうなんてこともなくなる。 で、それぞれのコントローラはcgoupfsという、特別なファイルシステムを介して使うんですね。このファイルシステムは、/sys/fs/cgroupに存在するわけです。

あとは、k8sとかのマニフェストにリソースの情報を書いたり、dockerコマンドの引数に、コンテナに与えるリソース量を書いたりして、コンテナを起動させるわけだが、この時、カーネルのcgroupが使われているわけなんですよ。

ちなみに、raspberrypiのcgroupのバージョンの確認の仕方は

grep cgroup /proc/filesystems

で、これの結果は

nodev   cgroup
nodev   cgroup2

だったので、おそらく1と2どっちも入っているんだろうな。という感じです。いや、どのシステムでやっても1と2がでる。

全部読み終えた後に次にどのステップに進めばいいか

  1. SREのひと もう手元にあるやつなんだけど、紹介システムパフォーマンスだね。これは修士終わる前に何回か読んでおきたい内容です。

  2. もっとシステムプログラミングを極めたい人 ふつうのLINUXプログラミング オライリーの鈍器本 だね。この二冊。がんばってー。 あと、カーネルの開発に進みたい人向けのページが これ

  3. これを期にハードウェアを極めたい人 ヘネパタぼんよめ!!だってさ。 ヘネパタ本ももう何回か読んだ方がいいよね!!そう思います!!