入門 モダンLinux めも

環境: WSL2 Ubuntu-20.04

はじめに
本書の使い方
- 本をただ読むだけでなく、無意味な入力や不正な入力をして”壊そう”としてみる。その際にコマンド実行前に仮説を立てる。
- 常に疑問を持つ。出力の由来となるコンポーネントは何であるか理解に努める。
サンプルコード

1章
Linuxは最も広く利用されているOS。
そもそもOSってなんだ?→この章でお話しします...
1.1 モダンな環境とは?
本書におけるモダンとは、クラウドコンピューティングからRaspberry Piまで色々。
AndroidもLinuxベースだし、IoTの文脈など広い分野でLinuxが動作しているか使われている。
1.2 Linuxの歴史
1990年代
1991-08-25にリーナストーバルズが送ったメールから始まった。
そこから三年足らずでLinux 1.0.0がリリースされた。この時点でほとんどのUnix/GNUソフトウェアを実行可能であり、当初の目標は達成されていた。
1990年代にはRedHatLinuxが初の商用Linuxとして提供された。
200x年代
UNIXを超えて成長。Google, Amazon, IBMなど大手企業が商用利用。
distro warsのピーク。
201x年代
Linuxの地位が確立された。(distro warsの終焉)

1.3 なぜOSなのか?
Q. OSが使えなかったら?
A. メモリ管理、割込み処理、I/Oデバイスとのやり取り、ファイル管理、ネットワークスタックの設定&管理、etc...を自分でやる必要がある
実はOSは必須ではない!Rustならコアライブラリと標準ライブラリのみでbare metal上で行けます。
なので、回答としては「OSは上記の処理をやってくれるので、ロジックに集中できる」となる。ハードウェアを抽象化(デバイスドライバ?)し、API(システムコール)を提供。
例えばgetuid(2) 現在のユーザのIDを表示するAPI.
コマンドラインでは$ id --user
1.4 Linuxディストリビューション
Linuxカーネルorカーネル:システムコールとデバイスドライバの集合
Linuxディストリビューション:カーネルとパッケージ管理、initシステム、shellなどのコンポーネントをまとめたもの。
ディストリビューションを用いなくても、自前で諸々構成はできるが、今ではすでに完成され配布されてるのを用いるのが一般的。
https://distrowatch.com/ で色々みれるようだ。

1.5 リソースの可視性
(可視性の話はコンテナと関わってくる)
Q. リソースとは?
Linuxではデフォルトではすべてのプロセスにシステムの全リソースが見える。
A. ソフトウェアを実行するために使用できる諸々。
例1:CPU情報を確認
❯ bat /proc/cpuinfo| rg "model name"
model name : Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
model name : Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
model name : Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
model name : Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
…(略)
core i7が12個。
例2:プロセス情報を確認
❯ bat /proc/$$/status | head -n6
Name: zsh
Umask: 0022
State: S (sleeping)
Tgid: 2025
Ngid: 0
Pid: 2025
$$
は現在のプロセスを参照する変数
PidはプロセスIDであり、LinuxはこのIDを通してプロセスを識別している。
じゃあ重複はないのかというと、そうでもない。
コンテナ化された環境があればnamespaceは異なるので、同じPIDを持つプロセスが存在しうる。
namespaceを通じてユーザやプロセスに見せるリソースの範囲を制限できる。
また、プロセス間でリソースを分離することで互いに影響を及ぼさないようにすることもできる。(ex: out-of-memory killer)

1.6 Linuxの全体像
Linuxディストリビューションの中心にはカーネル、そしてそれが提供するAPIがある。
POSIX(Portable Operating System Interface)とはUNIX系OSのサービスインターフェースを提供するIEEE規格。
POSIX準拠であれば移植可能ということです。
LinuxはPOSIXおおよそ準拠。

2章
OSはデバイスを抽象化し、プログラムにAPIを提供する。
この章ではLinuxカーネルとは何かについて見ていく。
2.1 Linuxアーキテクチャ
Linuxアーキテクチャは大別して以下の3つの階層に分けられる。
- ハードウェア
CPU、メモリ、ディスクドライブ、ネットワークインターフェース、キーボード、モニタなど - カーネル
コア - ユーザ空間
OSコンポーネント(シェルなど)、ユーティリティ(ps, sshなど)、GUIを含む多くのアプリケーションが動作している。
各層の間のインターフェースは定義されている。カーネルとユーザ空間の間にはシステムコールがある。
ハードウェアとカーネルの間は、各ハードウェアごとにちゃんとある。

ユーザ空間に関して、カーネルモードとユーザモードという概念がある。
カーネルモードではハードウェアに特権的にアクセスできる。一方ユーザモードではデバイスファイルを通じて制限されてアクセスとなる。
当然前者の方が高速だが、後者の方が抽象化を重ねており安全である。
ただ全てのアプリはユーザ空間で実行されるので(?)、カーネルモードは気にしなくてよい。

2.2 CPUアーキテクチャ
コンピュータアーキテクチャ=CPUファミリ。
Linuxカーネルは 汎用的なコードとドライバ と アーキテクチャに特化したコード からなる。
だからLinuxの移植は容易なんですね~

2.2.x
lscpu
コマンドでハードウェアに関する情報を確認できる
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
CPU family: 6
...
x86アーキテクチャはIntelが開発し、AMDにライセンス供与された。x86_64は64bitプロセッサ。
広く利用されているが、エネルギー効率が良くない。
ARMはRISCアーキテクチャの一種。高速で安価、x86より発熱が少ない。
RISC (Reduced Instruction Set Computing)は通常多くの汎用CPUレジスタと小さな命令セットで構成されている。
RISC-Vは比較的新しい新しいRISC規格。多くの実装が存在。

2.3 カーネルコンポーネント
Linux カーネルはモノリシック(各コンポーネントは単一のバイナリに含まれる)。

2.3.1 プロセス管理
セッション
複数のプロセスグループを含む。tty(制御ターミナル)を持つことがあり、セッション内のプロセス全てに共有される。
一つのフォアグラウンドプロセスグループを持つ。
SIDで識別される。
プロセスグループ
複数のプロセスからなる。PGIDで識別される。
プロセス
プログラムの実行に必要なリソース(スレッド、アドレス空間、ソケットなど)をまとめたもの。
/proc/self
にてプロセス情報が提供される。PIDで識別される。
スレッド
実装としてはプロセスと同じ。カーネルはスレッドを他のプロセスとリソースを共有するプロセスとして扱う。
タスク
task_struct
というデータ構造をベースにプロセスやスレッドが実装される。
この構造体にはPIDやシグナルハンドラ、パフォーマンスやセキュリティ関連情報などを保持しているが、一部しかユーザ空間には提供されない。
スケジューリングに関連する情報も含まれている。大別すると待機、実行中、待ち、終了があり、イベントによって状態が遷移する。
図はhttps://www.coins.tsukuba.ac.jp/~yas/classes/os2-2010/2010-12-14/index.html より引用
❯ ps -j
PID PGID SID TTY TIME CMD
11 11 11 pts/0 00:00:01 zsh
548 548 11 pts/0 00:00:00 ps
より詳細な情報はls -al /proc/$$/task/$$
にて確認できる。
❯ ls -al /proc/$$/task/$$
.r--r--r-- 0 yukari 26 Jan 02:17 arch_status
dr-xr-xr-x - yukari 26 Jan 02:17 attr
.r-------- 0 yukari 26 Jan 02:17 auxv
...

2.3.2 メモリ管理
仮想メモリ機能によって実際の物理メモリ以上のメモリを扱える。
物理メモリと仮想メモリはページと呼ばれる固定長(4KB)のチャンクに分割されている。
❯ getconf PAGESIZE
4096
仮想ページはページテーブルによって物理ページによりマッピングされる。
プロセスごとにページテーブルが存在し、同じ物理ページを指すこともあり得る。
プロセスが仮想ページにアクセスするたびに、CPUが物理ページに変換する必要がある。これを補助するために、TLB(Translation Lookaside Buffer)が内蔵されており、キャッシュの役割を果たす。
Linux2.6.3以降ではHugepageがサポートされており、TLBのミスが減るためパフォーマンス向上が期待できる。
❯ grep MemTotal /proc/meminfo # 物理メモリ
MemTotal: 8088756 kB
❯ grep VmallocTotal /proc/meminfo # 仮想メモリ
VmallocTotal: 34359738367 kB
❯ grep page /proc/meminfo
Hugepagesize: 2048 kB

2.3.3 ネットワーク
階層化されたアーキテクチャに従っている。
ソケット
通信を抽象化する
TCP/UDP (Transmission Control Protocol/User Datagram Protocol)
コネクション指向通信/コネクションレス通信によるデータ転送
インターネットプロトコル(IP)
アドレスに基づいたマシン間の通信
カーネルの役割は上記三つの動作のみで、HTTPやSSHなどアプリケーション層のプロトコルはユーザ空間で実装される。

2.3.4 ファイルシステム
HDD,SSD,フラッシュメモリなどの記憶装置上にファイルやディレクトリを構築するシステムのこと。
ext4, btrfs, NTFSなど種類は多岐にわたるが、VFS(Virtual File System)によって異なるファイルシステムの共存が可能になっている。
VFSの上位層(ユーザ空間側)はopen, close, read, writeなどの操作を共通API(ファイルシステムインターフェース)で抽象化されている。下位層(ファイルシステム側)には各ファイルシステムのためのプラグインによって抽象化されている。

2.3.5 デバイスドライバ
キーボード、マウス、HDD、GPUなどのハードウェアや/dev/pts/
いかに存在する仮想デバイスを制御する。
デバイスドライバはカーネルに静的に組み込まれるorカーネルモジュールとして組み込まれ必要に応じて動的にロードされる。
❯ ls -al /sys/devices # デバイスを確認
drwxr-xr-x - root 26 Jan 12:18 breakpoint
drwxr-xr-x - root 26 Jan 12:18 cpu
drwxr-xr-x - root 26 Jan 12:18 kprobe
drwxr-xr-x - root 25 Jan 18:36 LNXSYSTM:00
drwxr-xr-x - root 26 Jan 12:18 msr
drwxr-xr-x - root 26 Jan 12:18 platform
drwxr-xr-x - root 26 Jan 12:18 pnp0
drwxr-xr-x - root 26 Jan 12:18 software
drwxr-xr-x - root 25 Jan 18:36 system
drwxr-xr-x - root 26 Jan 12:18 tracepoint
drwxr-xr-x - root 26 Jan 12:18 uprobe
drwxr-xr-x - root 26 Jan 12:18 virtual
❯ mount # マウントされているデバイス一覧

2.3.6 システムコール
アプリケーションで行われるいかなる操作も、最終的にはシステムコールに変換される。
Linuxには数百ものシスコールが存在し、glibc
やmusl
に代表される標準ライブラリがラッパー関数を提供する。
システムコールはソフトウェア割込みとして実装されており、例外を発生させて例外ハンドラが呼ばれる。
システムコールテーブルと呼ばれるメモリ上の関数ポインタの配列があり、システムコールとその関数(ハンドラ)が登録されている。
システムコールの呼び出しは以下の手順で行われる。
-
system_call()
の呼び出しがあると、ハードウェアコンテキスト(状態?)をスタックに保存し、トレースの有無などをチェックし、sys_call_tableのindexに対応するハンドラへジャンプする。 -
sysexit
によってシステムコールが終了すると、ハードウェアコンテキストを復元し、プログラムの実行がユーザ空間で再開される。
上記過程でユーザ空間モードとカーネルモードの切り替えが行われているが、これは時間のかかる処理であることが知られている。
システムコールのトレースはstrace
コマンドで可能。-c
オプションを付けると実行時間の割合などが確認できる。
malloc関数システムコールではなく、内部でbrk, mmapなどのシステムコールを呼んでいる。

2.4 カーネルの拡張
カーネルの情報を確認(s,r,mオプションはそれぞれカーネルネーム、リリースバージョン、ハードウェアネームに対応)
❯ uname -srm
Linux 5.15.167.4-microsoft-standard-WSL2 x86_64

2.4.1 モジュール
モジュールとは、再コンパイルやマシンの再起動なしにカーネルにロードできるプログラムのこと。
Linuxではハードウェアを自動で検出し、対応するモジュールをロードしてくれる。
利用可能なモジュール一覧
❯ find /lib/modules/$(uname -r) -type f -name '*.ko*'
/lib/modules/5.15.167.4-microsoft-standard-WSL2/kernel/drivers/block/nbd.ko
/lib/modules/5.15.167.4-microsoft-standard-WSL2/kernel/drivers/net/bonding/bonding.ko
/lib/modules/5.15.167.4-microsoft-standard-WSL2/kernel/drivers/net/dummy.ko
...
カーネルがロードしているモジュールはlsmod
にて確認できる。/proc/modules
の情報を整形して表示してくれるらしいが、wslでは空だった。
モジュールの情報取得、操作にはmodprobe
を使用する。
❯ modprobe --show-depends async_memcpy
builtin async_memcpy

2.4.2 モダンなカーネル拡張:eBPF
Berkrley Packet Filterとして知られていた。現在ではextended BPFとしてLinuxカーネルの機能として組み込まれている。カスタム64bitRISC命令セットを用いたカーネル内仮想マシンとして実装されている。

2.5 まとめ
LinuxカーネルはLinuxの中核である(小泉構文みがある)。
カーネルはハードウェアを抽象化して差分を吸収し、高い移植性を実現している。ユーザ空間にはプロセス管理やファイルシステム操作、メモリ管理といった機能をシステムコールを通じてユーザ空間にインターフェースとして提供している。

3章 シェルとスクリプト
CLIの観点からLinuxと対話する方法は以下の二つ。
- 手動でインタラクティブにやりとりする。
- シェルスクリプトで自動実行する
後者は沼りがちなので注意!

3.1 基本用語
3.1.1 ターミナル
テキストベースのUIを提供するプログラム。
基本的な文字の入出力に加えて、エスケープシーケンス(もしくはエスケープコード)をサポートしている。
3.1.2 シェル
ターミナル内で動作し、コマンドインタプリタとして機能するプログラム。POSIXにおいてshとして定義されている。
もともとはStephen Bourneが開発したBourne shellのsh
があったが、現在はbash
(Bourne Again Shell)が広くデフォルトとして使われている。
3.1.2.1 ストリーム
入力ストリームと出力ストリームとがある。これらを合わせてI/Oと言う。
シェルはすべてのプロセスに対してIO用の以下の三つのファイルディスクリプタを用意している。
- FD0: stdin(標準入力)
- FD1: stdout(標準出力)
- FD2: stderr(標準エラー出力)
デフォルトではstdinはキーボードに、stdout, stderrはスクリーンに接続されている。
これを変更したい場合、ストリームをリダイレクトすることでファイルに保存できたりする。
ストリームのリダイレクトには$FD>
,<$FD
を使用する。
例)2>
はstderrをリダイレクト。1>
と>
は(stdoutがデフォルトのFDのため?)同義。
stdout, stderrの両方をリダイレクトしたい場合は&>
を使用し、ストリームの出力が不要ならば/dev/null
を使用する。
❯ curl example.com > /tmp/content.txt 2> /tmp/curl-status
❯ head -3 /tmp/content.txt
<!doctype html>
<html>
<head>
❯ cat /tmp/curl-status
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1256 100 1256 0 0 1690 0 --:--:-- --:--:-- --:--:-- 1692
特殊文字
-
&
コマンドの最後に置くと、コマンドをバックグラウンドで実行する。 -
\(backslash)
長いコマンドを折り返して複数行で記述できる。 -
|
あるプロセスのstdoutと次にプロセスのstdinとを接続し、データを直接受け渡せる。
❯ curl https://example.com 2> /dev/null | wc -l
46

3.1.2.2 変数
- 環境変数: シェル全体の設定。
env
で確認できる。export
で作成できる。 - シェル変数: 現在実行中のシェルでのみ有効。bashでは
set
コマンドで設定できる。子プロセスには継承されない。
「set」はシェルの設定を確認、変更するコマンドです。setはbashのビルトインコマンド(シェルの組み込みコマンド)です(※1)。このためmanコマンドではなく、helpコマンドで詳細を表示できます。(引用元)
❯ set | grep MY_VAR
MY_VAR=42
❯ export MY_GLOBAL_VAR="hoge global var"
❯ set | grep '^MY_*'
MY_GLOBAL_VAR='hoge global var'
MY_VAR=42
❯ env | grep '^MY_*'
MY_GLOBAL_VAR=hoge global var
❯ zsh # 子プロセスを起動
❯ set | grep MY_VAR
❯ env | grep '^MY_*'
MY_GLOBAL_VAR=hoge global var
❯ exit
❯ unset MY_VAR
❯ set | grep '^MY_*'
MY_GLOBAL_VAR='hoge global var'
❯ unset MY_GLOBAL_VAR
❯ set | grep '^MY_*'
主なシェル変数、環境変数
変数名 | 種別 | 意味 | 実行結果 |
---|---|---|---|
EDITOR | env | デフォルトエディタへのpath. | vim |
PS1 | env | プロンプト文字列 | 略 |
PWD | env | working directoryのpath | /home/yukari |
SHELL | env | 現在使用しているshell | /usr/bin/zsh |
TERM | env | 現在使用しているterminal emulator | xterm-256color |
UID | env | ユーザID | 1000 |
USER | env | current user name | yukari |
HOME | POSIX | current userのhome directoryのpath. | /home/yukari |
IFS | POSIX | フィールドを区切る文字のリスト。単語の展開時に用いられる。 | \t\n |
PATH | POSIX | シェルが実行可能なプログラムを探すディレクトリの一覧 | 略 |
HOSTNAME | bash | 現在のホスト名 | empty |
OLDPWD | bash | 最後にcd コマンドを実行したdirectoryのpath |
/home/yukari/atcoder |
RANDOM | bash |
|
15677 |
_ |
bash | foregroundで実行された直前のコマンドの最後の引数 | USER |
? |
bash | 直前に実行したコマンドの終了ステータス | 0 |
$ |
bash | current process PID | 11 |
0 |
bash | current process name | zsh |

3.1.2.3 終了ステータス
シェルはコマンド実行が完了すると、終了ステータスを呼び出し元に返す。0
なら正常終了、1
-255
なら異常終了。これを確認するにはecho $?
を使用。
パイプラインで処理するときは最後に実行されたコマンドの終了ステータスしか利用できないため注意が必要。
3.1.2.4 ビルトインコマンド
シェルに組み込まれているコマンドもある。
そうでないものは外部プログラムであり、/usr/bin
(ユーザコマンド)もしくは/usr/sbin
(管理コマンド)にある。
❯ which cat
/usr/bin/cat
❯ which read
read: shell built-in command
which
はPOSIXで規定されていない外部プログラムであるため、command -v
の使用が提案されている。

3.1.2.5 ジョブ制御
デフォルトではコマンドを入力すると、画面とキーボードの制御を受けるフォアグラウンド実行となる。
一方、対話的に実行しないバックグラウンドジョブが存在する。
バックグラウンドでプロセスを起動するには最後に&
を、フォアグラウンドのプロセスをバックグラウンドに送るにはctrl + z
を押下する。
厳密にはctrl + z
の場合はプロセスがサスペンドされるので注意。
❯ sleep 5 &
[1] 2101
❯
[1] + 2101 done sleep 5 # バックグラウンドで(勝手に)実行され、結果が表示される
❯ sleep 5
^Z
[1] + 2155 suspended sleep 5
❯ jobs
[1] + suspended sleep 5
❯ fg
[1] + 2155 continued sleep 5
ジョブ制御よりもターミナルマルチプレクサを使いなさ~い という旨で〆られている...

3.1.3 モダンなコマンド
紹介されているものを表にする。
コマンド | 元のコマンド | 機能 |
---|---|---|
bat | cat | 表示、ページ、syntax highlight |
envsubst | - | テンプレートから環境変数を展開 |
eza | ls | lsの上位互換(?) |
dog | dig | DNS参照 |
fx | jq | json 処理 |
fzf | ls, find, grep | fuzzy finder |
gping | ping | ping with graph |
httpie | curl | 色を付けてくれる? |
jo | - | json/array生成 |
rg | find, grep | ripgrep is a line-oriented search tool that recursively searches the current directory for a regex pattern. |
tldr | man | コマンドの使用例 |
zoxide | cd | path を記憶しており、雑な入力でも良い |
以下のページによくまとまっている。

brew install hoge
で諸々を入れていたらcleanupが自動で走っていくつかのコマンドが壊れてしまった...
/home/linuxbrew/.linuxbrew/bin/starship: /lib/x86_64-linux-gnu/libc.so.6: version GLIBC_2.33 not found (required by /home/linuxbrew/.linuxbrew/bin/starship)
とあり解決法を調べてみるとwsl のubuntuをversionを更新するのが早そうだったので、そうする。
これは何ら関係なく、brew link gcc
で直った。

3.1.4 一般的なタスク
3.1.4.1 よく使うコマンドを短くする
エイリアスやfishのアブリビエーションがある。
3.1.4.2 操作
コマンド | 作用 |
---|---|
ctrl + a |
カーソルを行頭に移動 |
ctrl + e |
カーソルを行末に移動 |
ctrl + f |
カーソルを1文字 forword 移動 |
ctrl + b |
カーソルを1文字 back 移動 |
alt + f |
カーソルを1単語 forward 移動 |
alt + b |
カーソルを1単語 back 移動 |
ctrl + d |
カーソル上の文字を削除 |
ctrl + h |
カーソルの左の文字を削除 |
ctrl + w |
カーソルの左の単語を削除 |
ctrl + k |
カーソルから行末まで削除 |
ctrl + u |
カーソルから行頭まで削除 |
ctrl + l |
画面クリア |
ctrl + r |
コマンド履歴を検索 |
ctrl + g |
上記の検索をキャンセル |
上記の操作はemacsベースらしい。.bashrcにset -o vi
と記載してsourceするとviベースの操作が使えるようだ。
zsh用のプラグインがあったので導入してみた。