LINUXプログラミングインタフェース2章 基礎概念

LINUXプログラミングインタフェースを読みはじめました。

例の巨大な本です。奥行きがあるため自立できます。

f:id:Kenta-s:20200227223556j:plain

https://www.amazon.co.jp/Linux%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9-Michael-Kerrisk/dp/487311585X/

すでに知っていることも含めて、この本で学んだことをまとめておこうと思います。 ただし、あまりにもボリュームがすごいので、直感的に重要だと感じたところだけをピックアップしてまとめていきます。

1章(UNIXの歴史)については書きたいことが特になかったので2章から始めます。

2章は「基礎概念」です

ユーザとグループ

ユーザ

  • システム上のすべてのユーザには一意なログイン名と、対応する数値のユーザID(user ID, UID)が与えられる
  • ログイン名とユーザIDはシステムパスワードファイル /etc/passwd 内で確認できる
  • /etc/passwd は、ユーザのグループID、ホームディレクトリ、ログインシェル といった情報も含まれる
  • すべてのユーザはひとつ以上のグループに属する

いま使っているマシンで # cat /etc/passwd をしてみました

root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin
ftp:x:14:11::/srv/ftp:/usr/bin/nologin
http:x:33:33::/srv/http:/usr/bin/nologin
nobody:x:65534:65534:Nobody:/:/usr/bin/nologin
dbus:x:81:81:System Message Bus:/:/usr/bin/nologin
systemd-journal-remote:x:982:982:systemd Journal Remote:/:/usr/bin/nologin
systemd-network:x:981:981:systemd Network Management:/:/usr/bin/nologin
systemd-resolve:x:980:980:systemd Resolver:/:/usr/bin/nologin
systemd-timesync:x:979:979:systemd Time Synchronization:/:/usr/bin/nologin
systemd-coredump:x:978:978:systemd Core Dumper:/:/usr/bin/nologin
uuidd:x:68:68::/:/usr/bin/nologin
dhcp:x:977:977:DHCP daemon:/:/usr/bin/nologin
kentas:x:1000:1000::/home/kentas:/bin/bash
avahi:x:976:976:Avahi mDNS/DNS-SD daemon:/:/usr/bin/nologin
polkitd:x:102:102:PolicyKit daemon:/:/usr/bin/nologin
rtkit:x:133:133:RealtimeKit:/proc:/usr/bin/nologin
sddm:x:975:975:Simple Desktop Display Manager:/var/lib/sddm:/usr/bin/nologin
usbmux:x:140:140:usbmux user:/:/usr/bin/nologin
colord:x:974:974:Color management daemon:/var/lib/colord:/usr/bin/nologin
git:x:973:973:git daemon user:/:/usr/bin/git-shell

ログイン名:パスワード:user ID:UID::ホームディレクトリ:ログインシェル

のようにコロンで区切られています。

それぞれの項目の意味まではこの章には書いてないのでググりました。

kentas:x:1000:1000::/home/kentas:/bin/bash

この行が僕が普段使っているユーザの情報です。 左から二番目はパスワードの項目なんですが、「x」の場合はシャドウパスワードを使っていることを意味しているそうです。

シャドウパスワードについても記載見当たりませんでしたのでググってみました。

シャドウパスワードとはパスワードを別ファイルで管理する概念で、 /etc/shadow に暗号化されたパスワードが含まれています。

この記事が詳しく調べてくれています

www.server-memo.net

/etc/shadow の中を見てみると、SHA512で暗号化された自分のパスワードが保存されていることが確認できました。

ちなみに、SHAxxxなどのハッシュ関数は一方向性を持っているため暗号化された文字列からもとの文字列を得ることはできません。

グループ

グループもユーザのように、一意なグループ名、グループIDを持っていますが、それに加えてユーザリストも持っています。

/etc/group 内の1行が1グループを表しています。以下は僕が使っているマシンで # cat /etc/group した結果です。

root:x:0:root
sys:x:3:bin
mem:x:8:
ftp:x:11:
mail:x:12:
log:x:19:
smmsp:x:25:
proc:x:26:polkitd
games:x:50:
lock:x:54:
network:x:90:
floppy:x:94:
scanner:x:96:
power:x:98:
adm:x:999:daemon
wheel:x:998:kentas
kmem:x:997:
tty:x:5:
utmp:x:996:
audio:x:995:
disk:x:994:
input:x:993:
kvm:x:992:
lp:x:991:
optical:x:990:
render:x:989:
storage:x:988:
uucp:x:987:
video:x:986:sddm
users:x:985:
systemd-journal:x:984:
rfkill:x:983:
bin:x:1:daemon
daemon:x:2:bin
http:x:33:
nobody:x:65534:
dbus:x:81:
systemd-journal-remote:x:982:
systemd-network:x:981:
systemd-resolve:x:980:
systemd-timesync:x:979:
systemd-coredump:x:978:
uuidd:x:68:
dhcp:x:977:
kentas:x:1000:
avahi:x:976:
polkitd:x:102:
rtkit:x:133:
sddm:x:975:
usbmux:x:140:
colord:x:974:
git:x:973:
docker:x:972:kentas

こちらも/etc/passwdと同じくコロン区切りになっています。

グループ名:パスワード:グループID:ユーザリスト(カンマ区切り)

グループにパスワードって何?と思って調べてみましたが、グループに普通にログインできるんですね。知りませんでした(恥)

$ newgrp groupname

でパスワードを入力すると、そのグループに属していないユーザでもグループにログインすることができるようです。

パスワードが「x」の場合はユーザの場合と同じくシャドウパスワードを使っていることを表しています。

スーパユーザ

  • スーパユーザのIDは0
  • スーパユーザのログイン名は通常root

どのファイルへもアクセスでき、ファイルに設定されているパーミッションも無視、どのユーザプロセスへのシグナルを送信できます。

単一ディレクトリ階層、ディレクトリ、リンク、ファイル

カーネルは単一のディレクトリ階層構造を用い、システム内の全ファイルを管理します。 単一でないディレクトリ階層構造とは何なのかというと、例えばウィンドウズの場合はディスクドライブごとにディレクトリ階層を構築するので単一のディレクトリ階層構造ではありません。

ファイル種類

ディレクトリとリンク

  • ディレクトリは、ファイル名とファイル本体への参照のペアを並べたファイル
  • ファイル名とファイル本体への参照のペアのことをリンクという
  • ファイルは複数のリンクを持つことができる
  • ディレクトリ内には、ファイル、ディレクトリのいずれに対してもリンクを作成できる
  • ディレクトリ内には、少なくとも2つのエントリがある(.と..です。説明は省きます)

シンボリックリンク

  • 他のファイル名を参照する形式を持つ(通常のリンクは、ファイル名とファイル本体への参照を持つが、シンボリックリンクはファイル名と、参照先のファイル名を持つ)
  • シンボリックリンクの参照が循環してしまう可能性があるため、カーネル内では参照先を辿る回数に上限を設けている
  • 通常のリンクとシンボリックリンクを区別した言い方として「ハードリンク」「ソフトリンク」という用語も使われる

ファイル名

  • ほとんどのLinuxファイルシステムでは最長255文字(バイト単位)までのファイル名を使用できる
  • SUSv3で定義されるファイル名ポータブル文字セット(- . _ a-zA-Z0-9)以外の使用は避けるべき

カレントワーキングディレクト

  • プロセスは親プロセスからカレントワーキングディレクトリを受け継ぐ
  • ログインシェルのカレントワーキングディレクトリは、/etc/passwdに記述されたユーザのホームディレクトリに初期設定される

ファイルのオーナとパーミッション

  • ファイルは、オーナを表すユーザIDと、属するグループを表すグループIDという属性を持つ(そのファイルへのアクセス可能なユーザを検査する際に使用される)
  • ファイルへのアクセス時、システムはユーザを3種類(オーナか否か、ユーザが属するグループがファイルのグループIDに 一致するか、その他ユーザか)に分類し、アクセス可否を検査する
  • さらに、ユーザ種類ごとにアクセス種類を3種類(読み取り許可、書き込み許可、実行許可)に分類する

ファイルI/Oモデル

  • UNIXシステムのI/Oモデルの最大の特徴はI/Oの統一性(open()やread()などの一つのシステムコールがデバイスを含めた全種類のファイルに対するI/Oを処理する)
  • カーネルがアプリケーションからのI/O要求を実際にI/Oを実行するファイルシステムデバイスドライバ操作に変換する
  • UNIXシステムにはファイル終端(end-of-file)という文字は存在せず、ファイルの終端はデータを読み取れなくなったことによって判断する

ファイルディスクリプタ

ちなみに普段よく使っている

$ echo "you are awesome" > foo.txt

の > は、実は 1> を省略したもので、

$ echo "you are awesome" 1> foo.txt

のように書いても同様に動作します。

この「1」がまさに標準出力のファイルディスクリプタを指していて、標準入力についても同じく以下のように書くことができ、

$ cat < foo.txt
$ cat 0< foo.txt

これらの2つのコマンドは同様に動作します。

2> はよく使うので知っている方も多いと思いますが、これは標準エラー出力ですね。この「2」を省略することはできません。

stdioライブラリ

  • C言語の)fopen(), fclose(), scanf(), printf(), fgets(), fputs()などのI/O関数を総称し、stdioと呼ぶ
  • stdioライブラリは内部でI/Oシステムコール(open(), close(), read(), write()など)を実行する

プログラム

  • プログラムには2種類あり、一つは人間が読み書きできるテキスト形式のソースコード、もう一つは、コンピュータが入力できるバイナリな機械語命令(しかしソースコードからコンパイルなどでバイナリを生成できるため、等価とみなせる。つまり通常同義と考えて問題ない)

プロセス

  • プロセスは、単純にいうとプログラムを実行するインスタンスであり、プログラムを起動すると、カーネルはプログラムの実行コードを仮想メモリにロードし、プログラム内の変数用の領域を割り当て、カーネル内のプロセスに関するデータを更新する(プロセスID、終了ステータス、ユーザID、グループIDなど)
  • カーネルはプロセスにある程度のリソースを与えて初期化するが、プロセスが要求した場合などに割当量を調整する
  • プロセスが終了すると、使用していたリソースはすべて開放され他のプロセスに割り当て可能になる

プロセスメモリレイアウト

プロセスは論理的に以下の4つの部分に分割され、それぞれをセグメントと言う。

テキストセグメント 実行コード

データセグメント スタティック変数

ヒープセグメント ダイナミックに割り当てるメモリ

スタックセグメント 関数のコール、リターン時に拡張/縮小し、関数のローカル変数や関数コールリンケージ情報を置く

プロセスの作成と終了

  • fork()システムコールによって新規プロセスを作成できる
  • fork()を実行したプロセスを親プロセス、その結果作成されたプロセスを子プロセスと言う(厳密に言うとfork()を実行するのはプロセスではなくカーネルで、親プロセスはカーネルにプロセスの作成を依頼します)
  • カーネルは、親プロセスの複製として子プロセスを作成する。その際、子プロセスのデータ、スタック、ヒープセグメントは親プロセスのものを複製するため、それぞれ独立して変更可能となる。テキストセグメントに関してはメモリ上で読み取り専用としてマークされ、両プロセスから共有される。
  • 子プロセスは実行コード内の親プロセスとは異なる部分を実行する場合もあれば、execve()システムコールを実行して、外部プログラムを新規にロード/実行することもある(こちらの方が一般的、とのことです)
  • execve()は既存のテキスト、データ、スタック、ヒープセグメントをすべて破棄し、新規プログラムの内容で置き換える
  • execve()システムコールやexecve()を実行する関数を実行することを「起動する」「実行する」「exec()する」と表現する(この本の中での話かもしれません)

プロセスIDと親プロセスID

  • プロセスには一意な整数の識別子が割り振られ、これをPID(プロセスID)という
  • プロセス属性として、そのプロセスの作成をカーネルに依頼したプロセス(親プロセス)のID、PPID(親プロセスID)を持つ

こういうRubyプログラムを用意して試してみました。

# process.rb

# 30秒スリープするだけの子プロセスを作成
child_pid = fork do
  sleep(30)
end

# 子プロセスの終了を待って終了
Process.waitpid(child_pid)
$ ruby process.rb & // Rubyプログラムをバックグラウンドで実行
[1] 31709

$ ps -l // psはデフォルトでカレントユーザのプロセスのみ表示します。-l はlong format
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000   31008   30978  0  80   0 -  2322 -      pts/3    00:00:00 bash
0 S  1000   31709   31008  1  80   0 - 19665 -      pts/3    00:00:00 ruby
1 S  1000   31710   31709  0  80   0 - 19665 -      pts/3    00:00:00 ruby
4 R  1000   31711   31008  0  80   0 -  2429 -      pts/3    00:00:00 ps

psコマンドを実行すると4つのプロセスが表示されていることが確認できました。

上から3番目のPID 31710 がこのRubyプログラムが作成した子プロセスです。
PPIDには、親プロセスである31709が表示されています。

この親プロセスのRubyプログラムの親プロセスIDは31008で、これはbashですね。

(ちなみにこのbashの親プロセスID30978はtmuxです)

プロセスの終了とステータス

  • プロセスの終了には、_exit()システムコールを実行してカーネルに自ら終了を要求する場合と、シグナルによって強制的に終了させられる場合の2通りある
  • いずれの場合もプロセスの終了時には値の小さな非負の整数、終了ステータスを生成する。親プロセスはwait()システムコールを使うことでこの値を取得できる。
  • _exit()によって終了した場合は、プロセスが自身の終了ステータスを明示的に指定できる
  • シグナルによって強制終了させられた場合は、原因となったシグナル種類に応じて終了ステータスが設定される。
  • 値が0の終了ステータスは正常終了、0以外は異常終了を表す規約がある
  • 多くのシェルでは、最後に実行したプログラムの終了ステータスをシェル変数 $? に保持する

今度は実験で以下のようなRubyファイル process.rb を用意しました。5秒スリープするだけのクールなプログラムです。

sleep(5)
$ ruby process.rb
$ echo $?
0

rubyプロセスの終了を待って $? を確認すると 0 が出力されました。

次はプロセスの終了を待たずにctrl + cで強制終了します

$ ruby process.rb
^CTraceback (most recent call last):
        1: from process.rb:1:in `<main>'
process.rb:1:in `sleep': Interrupt

$ echo $?
130

今回はシグナルによって強制終了させられたため $? には130が格納されています。

特権プロセス

  • 実行ユーザIDが0(スーパユーザ)のプロセスを、特権プロセスと呼ぶ
  • 特権プロセスは、カーネルが通常実行するパーミッション検査を素通りする
  • スーパユーザ以外のユーザが実行するプロセスはすべて非特権プロセス
  • 非特権プロセスの実行ユーザIDは0以外であり、カーネルによってパーミッションが検査される
  • 特権プロセスが作成したプロセスも特権プロセスになる
  • set-user-IDという機能を使って、実行時にプロセス実行ユーザIDをプログラムファイルのオーナに一致させることで特権を得ることもできる

「スーパユーザ以外のユーザが実行するプロセスはすべて非特権プロセス」
「set-user-IDを使って特権を得ることもできる」

の2つが矛盾しているように感じていて少し混乱しています。
「スーパユーザ以外のユーザが実行するプロセスは基本的に非特権プロセス」ということでしょうか。

もう少しプロセスの理解が進めば理解できるのかもしれません。とりあえず進みます。

ケーパビリティ

  • ケーパビリティはカーネルバージョン2.2から
  • スーパユーザが持つ従来の特権を細分化したものをケーパビリティという
  • 特権を必要とするすべての処理は特定のケーパビリティに対応付けられ、 対応するケーパビリティを持ったプロセスならばその処理を実行できる
  • 従来のスーパユーザプロセス(実行ユーザIDの値が0のプロセス)は、全ケーパ ビリティが 有効の状態に相当する
  • ケーパビリティを有効にしたプロセスは、通常であればスーパユーザのみが実行可能な処理でも実行できるようになるが、有効にしたケーパビリティに対応する処理のみが可能になり、他の処理はやはり禁止される
  • ケーパビリティ名はCAP_KILLのようにCAP_から始まる

ケーパビリティという概念は初めて目にしました。

正直まだピンと来ていませんが、「39章 ケーパビリティ」で詳しく説明があるらしいです

initプロセス

  • システム起動時に、カーネルは /sbin/init というプログラムから、initプロセスという特殊なプロセスを作成する
  • initプロセスはすべてのプロセスの親プロセスとなる(親というより、祖先?)
  • システム上のプロセスはすべて、initプロセスか、またはその子孫プロセスから(fork()によって)作成される
  • initプロセスのIDは常に1で、スーパユーザ権限で動作する
  • initプロセスは終了させることができない(スーパユーザでも)
  • initプロセスが終了するのはシステムシャットダウン時のみ
  • initの主な処理内容は、システムに必要ないくつかのプロセスを作成して、その状態を監視すること

手元のマシンで確認してみたら /sbin/init はバイナリファイルでした。

# ps aux の結果は以下の通りです

# ps aux | head -n 5
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.1 172868 10264 ?        Ss   20:10   0:01 /sbin/init
root           2  0.0  0.0      0     0 ?        S    20:10   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   20:10   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   20:10   0:00 [rcu_par_gp]

PID 1のプロセスは確かに /sbin/init になっていますね

initプロセスはスーパユーザでも殺せないそうなので試しに # kill -9 1 してみました

# kill -9 1
# ps aux | head -n 5
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.1  18320  9904 ?        Ss   20:10   0:01 /usr/lib/systemd/systemd --system --deserialize 13
root           2  0.0  0.0      0     0 ?        S    20:10   0:00 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<   20:10   0:00 [rcu_gp]
root           4  0.0  0.0      0     0 ?        I<   20:10   0:00 [rcu_par_gp]

エラーが出るわけでもなくうんともすんとも言いません。
よく見るとPID 1のコマンドが /usr/lib/systemd/systemd --system --deserialize 13 になっています(大丈夫かな...やっちまったわけではないよな...?)

環境リスト

  • プロセスは環境リストという属性を持つ
  • 環境リストとはユーザメモリ内に設定する環境変数のリストで、各要素が環境変数名とその値を表す
  • fork()によってプロセスを作成すると、子プロセスは親プロセスの環境を受け継ぐ(すなわち、親プロセスから子プロセスへの通信手段にも使える)
  • 実行中のプロセスがexec()によってプログラムを置換すると、新プログラムは旧プログラムの環境を受け継ぐか、exec()によって指定された新環境を得る
  • ほとんどのシェルではexportコマンドで環境変数を設定する

exportコマンドはおなじみですね。

$ export FOO="this is a pen"

子プロセスが親プロセスの環境を受け継ぐというところは「えっそうなの?」と思いましたが、考えてみるとそれはそうかも。

リソース消費制限

  • プロセスはオープンファイル、メモリ、CPU時間などのさまざまなリソースを消費する
  • setrlimit()システムコールを使うと、リソースそれぞれの消費に上限を設けることができる
  • リソース消費制限にはソフトリミット、ハードリミットという2種類の値を設定する
  • ソフトリミットはプロセスが消費していいリソースの上限
  • ハードリミットはソフトリミットの変更可能上限
  • 非特権プロセスは0からハードリミットまでの範囲でソフトリミットを変更できるが、ハードリミットは上限を下げる方向にしか変更できない
  • fork()によって作成したプロセスは親プロセスのリソース消費制限設定を受け継ぐ
  • ulimitコマンドを使用するとシェルのリソース消費制限を変更できる
  • シェルのリソース消費制限設定はそのシェルから実行するすべてのプロセスに受け継がれる

メモリマッピング

  • mmap()システムコールは自プロセスの仮想アドレス空間内に新規メモリマッピングを作成する
  • マッピングは、ファイルマッピングと無名マッピングの2種類ある
  • ファイルマッピングは、ファイル内の指定した範囲を自プロセスの仮想メモリマッピングし、マッピング後はメモリ上でのバイト操作がファイル内容へのアクセスを意味する。マッピングしたメモリページは必要に応じて自動的にファイルからロードされる。
  • 無名マッピングは、対応するファイルを持たず、マッピングしたページ内容は0で初期化される。
  • プロセスがマッピングしたメモリは他のプロセスと共有可能
  • 2つのプロセスがファイルの同じ部分をマッピングすると共有状態になる
  • fork()によって作成した子プロセスが親プロセスからマッピングを受け継いだ場合も共有状態になる
  • 非共有マッピング(プライベートマッピング)の場合、メモリ上の変更は自プロセスに閉じ、他のプロセスからは見えず対応ファイルには反映されない
  • 共有マッピング(シェアードマッピング)の場合、メモリ上の変更はそのメモリページを共有する他のプロセスから参照可能で、対応ファイルにも反映される
  • 用途の例は、プロセスのテキストセグメントを実行ファイル内の実行コードで初期化する、内容を0で初期化済みのメモリを新規に割り当てる、ファイルI/O(メモリマッピングI/O)、共有マッピング経由のプロセス間通信など

スタティックライブラリと共有ライブラリ

  • アプリケーションプログラムからコール可能な関数のコンパイル済みオブジェクトコードを持つファイルをオブジェクトライブラリという
  • 現代のUNIXシステムには、スタティックライブラリ、共有ライブラリの2種類のオブジェクトライブラリがある

スタティックライブラリ

  • スタティックライブラリ内の関数を使用するには、プログラムリンク時にそのライブラリを指定する
  • スタティックリンクされたプログラムではライブラリ内のモジュールのコピーを内部に持つ(そのため、多数の実行ファイルが重複するオブジェクトコードを包含することになり、ファイルサイズが大きくなり、メモリの無駄使いにもつながる)

共有ライブラリ

  • プログラムを共有ライブラリとリンクすると、リンカはライブラリ内のオブジェクトモジュールを実行ファイルへコピーせず、実行時に共有ライブラリが必要であるということだけを実行ファイルに記録する
  • プログラムを実行し、実行ファイルがメモリへロードされると、ダイナミックリンカというプログラムが必要な共有ライブラリを特定し、メモリへロード、共有ライブラリ内の必要な関数を解決しリンクする
  • 実行時にメモリ上に共有ライブラリは一つしか存在せず、すべてのプログラムが同じコードを使用する
  • コンパイルした関数は共有ライブラリ内にしか存在しないため、ディスク領域を節約できる

プロセス間通信と同期

プロセス間通信を実現する方法の一つは、ディスク上のファイルを経由し情報をやり取りするものだが、多くのアプリケーションにとってこの方法は動作が遅く柔軟性も乏しいため、Linuxをはじめとする現代のUNIXシステムでは、多種多様なIPC(interprocess communication、プロセス間通信)手段を実装している。

シグナル

なんらかのイベントが発生したことを通知する

パイプ( | )およびFIFO

プロセス間でデータを送受信する

ソケット

プロセス間でデータを送受信する。同一ホスト上のプロセス間でも、ネットワークを介した他ホスト上のプロセスとも送受信可能

ファイルロック

指定した範囲のファイル内容の他プロセスによる読み取り、または書き込みを禁止する

メッセージキュー

プロセス間でメッセージ(データパケット)を交換する

セマフォ

複数プロセス間で実行を同期させる

共有メモリ

複数のプロセスで特定のメモリ領域を共有する

シグナル

  • シグナルは「ソフトウェア割り込み」ともいう
  • シグナルを受信することで、プロセスはなんらかのイベントや外部条件が発生したことを知ることができる
  • シグナルには様々な種類があり、それぞれに一意な整数が割り振られており、この整数にはSIGxxxxの形式の名前がつけられている
  • プロセスにシグナルを送信するのは、カーネルや他プロセス、自プロセスなど
  • カーネルがシグナルを送信するのは例えば、ユーザがキーボードから割り込みキー(ctrl-c)を入力した、そのプロセスの子プロセスが終了した、プロセスが設定したタイマ(アラームクロック)がタイムアウトした、プロセスが不正なメモリアドレスにアクセスしようとした、などのタイミング
  • シェルからはkillコマンドでプロセスにシグナルを送信できる
  • プロセスからはkill()システムコールでシグナルを送信できる
  • プロセスは、シグナルを受信すると、シグナルを無視する、強制終了させられる、特定のシグナルを受信するまで、実行を一時停止する、などの対応動作を取る。どの動作になるかはシグナル種類に依存するが
  • ほとんどの種類のシグナルは、プログラムが対応動作を選択できる。例えば、シグナルを無視する、シグナルハンドラを設定し自身でシグナルを処理するなど
  • シグナルハンドラとはプログラマが定義する関数であり、プロセスがシグナルを受信すると自動的に実行されるもの
  • シグナルが生成されてプロセスが受信するまでの間、シグナルは保留状態に置かれる
  • プロセスはシグナルマスクを設定してシグナルをブロックすることも可能
  • ブロック中にシグナルが生成されると、アンブロック(シグナルマスクが削除)されるまで保留される

スレッド

  • プロセスは内部に複数のスレッドを持てる
  • スレッドは、属性や仮想メモリを共有する一軍のプロセスと捉えることもできる
  • スレッドは同じプログラム内のコードを実行しデータやヒープを共有するが、ローカル変数や関数コールリンケージ情報を置くスタックは個別に持つ
  • スレッドは共有するグローバル変数を用いて相互に通信可能
  • スレッドAPIでは、スレッド間通信、実行、特に共有変数へのアクセスの同期を実現するプリミティブな条件変数、mutexを実装している
  • IPCや プロセス間通信と同期 に書いた同期を用いても通信可能
  • スレッドの大きな利点は、スレッド間でデータを容易に共有できること(グローバル変数
  • アルゴリズムによってはマルチプロセスよりマルチスレッドのほうが向いているものもある
  • マルチスレッドアプリケーションでは、マルチプロセッサ上で並行処理を透過的に実現できる利点もある

マルチプロセッサ上で並行処理を透過的に実現できる」のところよくわかりませんでした。透過的に実現する、とはどういうことなんだろう...

プロセスグループとジョブコントロール

  • シェルが起動するプログラムはすべて新規プロセス
  • Bourneシェルを除き、現代の主要なシェルはすべてジョブコントロールという昨日を備えていて、同時に複数のコマンドやパイプラインを実行できる
  • ジョブコントロール機能を実装したシェルでは、パイプライン内のすべてのプロセスは同じプロセスグループ(ジョブ)に属する(単一のコマンドを実行する場合も1プロセスだけが存在するプロセスグループが作成される)
  • プロセスグループ内のプロセスのプロセスグループIDはすべて同じ値
  • プロセスグループIDはプロセスグループ内の1プロセスのIDとなり、このプロセスをプロセスグループリーダという
  • カーネルはプロセスグループ内の全メンバに対してさまざまな操作を行うことができ、パイプライン内の全プロセスの実行の一時停止、再開などができる

「シェルが起動するプログラムはすべて新規プロセス」の例ですが、
例えば以下のようにパイプラインを実行すると3つのプロセスが作成されます

$ ls -l | sort -k5n | less

psコマンドに-oオプションと表示したい項目を渡すとプロセスグループIDがわかるらしいので実験してみます。

sleepコマンドを3つパイプでつなげて実行します

$ sleep 3 | sleep 3 | sleep 3 &
[1] 26715

プロセスグループは ps -o pgid で確認できます

$ ps -o pid,ppid,pgid,comm
  PID  PPID  PGID COMMAND
26713 31635 26713 sleep
26714 31635 26713 sleep
26715 31635 26713 sleep
26734 31635 26734 ps
31635 31625 31635 bash

確かに3つのsleepコマンドのPGIDはすべて26713で、これは一番上のsleepコマンドのPIDと一致しています。

セッション、制御端末、制御プロセス

  • プロセスグループ(ジョブ)の集合をセッションという
  • セッションを作成したプロセスをセッションリーダといい、そのプロセスIDがセッションIDとなる
  • セッションを主に使用するのはシェルのジョブコントロール機能
  • ジョブコントロールが作成したプロセスグループはすべて同じセッションに属し、セッションリーダはシェルとなる
  • セッションには通常、対応する制御端末が存在する
  • セッションリーダが最初に端末デバイスをオープンした時に制御端末との接続が確立される
  • インタラクティブシェルが作成したセッションの場合はユーザがログインした端末が制御端末となる
  • 端末は同時に複数のセッションの制御端末になることはできない
  • 制御端末をオープンすると、セッションリーダはその端末の制御プロセスになる
  • 端末が切断されると制御プロセスにはSIGHUP(端末ウィンドウがクローズされたことを通知するシグナル)が送信される
  • セッション内の1プロセスグループはフォアグラウンドプロセスグループ(フォアグラウンドジョブ)になる
  • 同時に複数のプロセスグループがフォアグラウンドプロセスグループになることはない
  • 通常、端末へ入出力するのはフォアグラウンドプロセスグループのみ
  • ユーザが制御端末へ割り込みキー(ctrl-c)や一時停止キー(ctrl-z)を入力すると、端末ドライバはフォアグラウンドプロセスグループへ強制終了または一時停止させるシグナルを送信する
  • コマンド末尾にアンパサンド(&)を付けると、セッションにバックグラウンドプロセスグループ(バックグラウンドジョブ)を作成できる
  • バックグラウンドプロセスグループは複数作成可能
  • ジョブコントロール機能を備えたシェルでは、全ジョブの一覧表示、ジョブへのシグナル送信、ジョブのフォアグラウンド化/バックグラウンド化を実行できる

疑似端末

  • 疑似端末とは仮想的な2つのデバイス、マスタとスレーブを接続したもの
  • 2つの疑似デバイスはIPC通信チャネルとなり、双方向にデータを交換できる
  • 疑似端末ではスレーブデバイスが端末として動作する
  • 端末を操作するプログラムをスレーブデバイスへ接続し、マスタデバイスへ接続したプログラム(ドライバプログラム)からスレーブ上で動作する端末指向プログラムを操作できる
  • ドライバプログラムからの出力は端末ドライバへの通常の入力として処理され、スレーブデバイス上の端末指向プログラムへの入力となる
  • 端末指向プログラムからスレーブデバイスへの出力はドライバプログラムへの入力となる
  • 疑似端末を利用するアプリケーションの代表例は、X Window Systemでログイン処理を実行する端末ウィンドウや、telnet, sshなどネットワークログインサービスを実装するものなど

日付と時刻

  • プロセスが使用する時間には、実時間、プロセス時間の2種類がある
  • 実時間とは、ある既定の時点(カレンダ時間)からの経過時間、またはプロセス開始時などある時点からの経過時間(elapsed time, wall clock time)を表す
  • UNIXシステムのカレンダ時間はUTCの1970年1月1日(協定世界時)を起点とする
  • プロセス時間は、CPU時間とも言い、実行開始以降にそのプロセスが消費したCPUの時間を表す
  • CPU時間は、カーネルモードでコードを実行(システムコールの実行など)した時間を表すシステムCPU時間と、ユーザモードでコードを実行(通常のプログラムコードの実行)した時間を表すユーザCPU時間に細分化される

1970/1/1はおおよそUNIXの誕生日でもあるとのことです

timeコマンドを使うと、プロセス実行に要した実時間、システムCPU時間、ユーザCPU時間を表示できます。

hoge.rbを用意してtimeコマンドを試してみます。3秒スリープしたのちに絵文字を標準出力するプログラムです。

sleep 3
puts "💉"

結果はこうなりました。

$ time ruby hoge.rb 
💉

real    0m3.075s
user    0m0.062s
sys 0m0.012s

実時間は予想通り3秒ちょっとですが、ユーザCPU時間はわずか0.062sで、システムCPU時間は0.012sでした。

sleepを実行している間はCPUを使っていないように見えます。そういえば、sleepがどうやって動いているのかこれまで考えたことがありませんでした。

わかったら追記します

クライアント - サーバアーキテクチャ

  • クライアント - サーバアーキテクチャとは、アプリケーション処理を2つに分割した形態を言う
  • クライアントはサーバへリクエストメッセージを送信し、なんらかのサービスを要求する
  • サーバはクライアントからのリクエストに応じ処理を実行し、クライアントにレスポンスを返す

この概念はおなじみですね。クライアント - サーバを分割することで効率性やセキュリティの向上が期待できます。

リアルタイム性

  • リアルタイムアプリケーションとは入力に対し応答を返す時間を定めたものをいう
  • リアルタイム性を求められるアプリケーションには工場の自動組み立てライン、銀行ATM、航空機誘導システムなどがある

/procファイルシステム

以下は手元のマシンで ls /procした結果です

$ ls /proc
1       105562  106108  106506  106517  106528  106539  106550  106561  106584  106808  1243  173  189  251  273  306  356  404  465  53   6      69490  69657  758  798  851    988        cpuinfo      iomem        latency_stats  pressure       sysvipc
10      105823  106120  106507  106518  106529  106540  106551  106562  106587  106809  13    174  19   252  274  31   361  405  466  539  60     69491  69699  76   8    858    98827      crypto       ioports      loadavg        sched_debug    thread-self
101169  105879  106138  106508  106519  106530  106541  106552  106563  106608  106818  1325  177  2    253  275  311  362  406  47   54   61     69494  69766  763  814  860    99236      devices      irq          locks          schedstat      timer_list
101802  1059    106161  106509  106520  106531  106542  106553  106564  106630  1072    14    178  20   254  276  317  37   408  475  549  62     69495  69779  764  817  864    acpi       diskstats    kallsyms     meminfo        scsi           tty
103328  105905  106270  106510  106521  106532  106543  106554  106565  106678  1077    16    179  200  258  277  319  38   41   477  55   63     69498  69781  77   823  868    asound     dma          kcore        misc           self           uptime
103450  105968  106444  106511  106522  106533  106544  106555  106566  106692  1081    166   18   21   26   278  32   386  42   48   551  65     69516  70156  770  826  873    buddyinfo  driver       keys         modules        slabinfo       version
1036    105995  106486  106512  106523  106534  106545  106556  106567  1067    1085    167   180  217  260  279  33   39   44   49   553  66     69519  70460  771  830  879    bus        execdomains  key-users    mounts         softirqs       vmallocinfo
103685  106020  106502  106513  106524  106535  106546  106557  106568  106716  11      168   183  23   261  28   34   391  45   51   56   67     69583  72     776  838  883    cgroups    fb           kmsg         mtrr           stat           vmstat
104450  106061  106503  106514  106525  106536  106547  106558  106569  106722  1144    17    184  24   262  280  345  394  46   515  570  68     69622  73     786  844  9      cmdline    filesystems  kpagecgroup  net            swaps          zoneinfo
104899  106062  106504  106515  106526  106537  106548  106559  106578  106770  1152    171   185  249  27   3    346  4    463  518  58   69     69638  74     787  848  970    config.gz  fs           kpagecount   pagetypeinfo   sys
105381  1061    106505  106516  106527  106538  106549  106560  106579  106789  12      172   186  25   272  30   35   40   464  52   59   69485  69644  75     789  849  97735  consoles   interrupts   kpageflags   partitions     sysrq-trigger

まとめ

基礎概念というだけあって、ユーザ、グループ、プロセスやファイルといった、Linuxに触れる上で不可欠な概念が多かった一方、
いままで名前さえ聞いたことのなかった概念もいくつかありました(ケーパビリティなど)。

目次を見るかぎり、この章で触れられた概念を別の章で詳細に見ていくような構成になっているようで、
この本を終えることができればいままでにできなかったこと(これまでより低いレイヤーでのプログラミング)ができるようになるのではないかという期待を持ちました。