Closed21
インプットログ: ふつうのLinuxプログラミング 第2版
低レイヤ強くなりたい。
準備
リポジトリ作った。Rustでやる。
第1章 Linuxプログラミングを始めよう
- 本書ではLinux世界が何でできているのか教えるよ
- Linux世界はこれらで成り立つ。本書を呼んだ後これらを理解しているはず。
- ファイルシステム
- プロセス
- ストリーム
第2章 Linuxカーネルの世界
- システムコールはLinuxカーネルを操作するためのIF。なのでそれを理解すればLinuxカーネルが理解できる。
- リーナスが作っているのはカーネルだけで、GNU libcとかは別の人が作ってる。
第3章 Linuxを描き出す3つの概念
ファイルシステム
- 広義のファイル
- シンボリック、ディレクトリ、バイナリ、テキスト、その他lsで表示される全て。
- 何らかのデータを保持する
- メタデータがついている
- 名前で指定できる
- 狭義のファイル
- 内容がそのまま記録されているファイル。テキスト、画像、動画。
- ストリーム
- ファイルシステムはストレージデバイス上にのみある
- これほんとかな?どのように保存するか、とかはカーネルが面倒見ているはず。なのでそういう意味ではカーネル上にも存在するのでは。ファイルシステムを何と定義するかって話になるのかな。
- ストレージデバイスが存在しないファイルシステムもある。それらは疑似ファイルシステムと呼ばれる。それは後に説明する。
プロセス
- 動作中のプログラム
- プロセスにはユニークなIDであるプロセスIDが振られているよ。
ストリーム
- バイトストリームを指す
- ここで説明するストリームはオフィシャルな用語ではない
- プロセスがファイルにアクセスする時は、カーネルにバイトストリームを作ってもらっている。これを単にreadという。逆にバイトストリームをファイルに流し込むことをwriteという。
- キーボードもバイトを送り付けてくるストリームと見なせる。もちろんストレージデバイスも。デバイスファイルはこのストリームを抽象化する。
- プロセスからプロセスにデータを送るパイプもまたストリーム。
- ネットワークもストリーム
まとめ
プロセスとプロセスはストリームを通じてデータをやり取りする。またプロセスはファイルシステムからストリームを通じてデータを取得する。ファイルシステムは過去のプロセスが作成したデータ。
第4章 Linuxとユーザ
- そもそもマルチユーザーが必要な理由
- システムを安全に使うため。権限があれば重要なファイルを消せないようにする制御ができる。
- rootユーザはなんでもできる
- ファイルのパーミッション
- 権限はこれらに対して別々に設定できる
- ファイルを所有するユーザ
- ファイルを所有するグループに所属するユーザ
- それ以外のユーザ
- 権限の種類
- read, r
- write, w
- execute, x
- ディレクトリのexecute権限は直感に反する。それはその中のファイルを見れるかどうかを制御する。
- カーネルはどうやってユーザーを認証するか?
- ユーザーAがアクセスするとは、ユーザーAの属性をもったプロセスがアクセスするということ。
- ログイン時に証明書を持つプロセスが作られる。それが別コマンドを実行する際にもコピーされる。それを使って認証している。
- ユーザーIDとユーザー名の対応は/etc/passwdで見れるよ。passwdの名前の由来は、元々パスワードをそこに書いていたから。いまは/etc/shadowに書かれている。
- グループは/etc/group
- 権限はこれらに対して別々に設定できる
- コマンドラインはシェルと端末に分解できる
- 端末は人間が触れる部分。もともとは一つのコンピュータに複数人が接続していたんです。
- 端末の歴史
- ttyはTeleTYpeから来ているよ
- iterm2とかは端末エミュレータ。端末をソフトウェアだけで動かしている。
- ASCIIの07はBEL。端末のベルを鳴らす。ビルドが終わったときに鳴らしてたのかな。Rustだとこう書く
print!("\x07")
- Linuxではあらゆるものがファイル。端末もファイル。devファイル。
- このおかげで端末からストリームを得ることができる。こうすればプロセスからは端末は見えず、ファイルシステム・プロセス・ストリームしか無い。すごい抽象化。
- シェルはユーザーの命令を解釈して実行するプログラム。ログイン時に起動されるのが特別なだけで、ただのプロセス。ストリームからコマンドを読み込んで実行しているだけ。
第5章 ストリームにかかわるシステムコール
- ストリームに関わるシステムコールはこれら
- ストリームからバイト列を読み込むread
- ストリームにバイト列を書き込むwrite
- ストリームを作るopen
- 用済みのストリームを始末するclose
- プログラムからストリームを扱うにはファイルディスクリプタを使う。整数値。カーネルが割り振るストリームのID。
- 標準入力・標準出力・標準エラーは常に存在するストリーム。
- 多くのLinuxコマンドは、標準入力から読み込んで標準出力に書き込んでいる。また、パイプは前のコマンドの標準出力を次のコマンドの標準入力に渡す。それにより複雑な操作をすることができる。
- 標準エラー出力の存在理由は、パイプがあるため。単にエラーを標準出力に出してしまうとユーザーに読まれない。なので専用のストリームを用意してユーザーに読ませている。
- 本書のストリームの定義は、read()で読めてwrite()で書き込めるもの。
- close()しなくても勝手にカーネルがプロセス終了時点でcloseしてくれる。とはいえ、使えるストリームの数には制限があったり、ストリームの先に相手がいる場合があったりするので、close()するほうが良い。
- Windowsの「開く」は、実行も含んでいる。
- シンプルなcatを実装した
- 同じファイルディスクリプタに対して何度もreadを読むと、前回の途中が返ってくる。つまりストリームはファイルのどの位置を読んでいるのかという情報を保持している。これがファイルオフセット
- lseek(2)でファイルオフセットを操作できる
- 対象がネットワークとかなら使えない。エラーになる。
- lseek(2)でファイルオフセットを操作できる
- dup(2), ファイルディスクリプタを複製する。オフセットはどうなるんだろ。
- ioctl(2): デバイスに特化した操作ができる。DVDドライブの再生・停止とか。
- 抽象化しきれなかったものがここに詰め込まれている
- fcntl(2): ioctl()の中から、ファイルディスクリプタ関連のものを抽出したもの。
- wc書いた
第6章 ストリームにかかわるライブラリ関数
- システムコールは関数呼び出しと比べるととても遅い。なのでライブラリ関数がバッファリングなどをして最適化している。
- writeもバッファしている。ある程度溜まったら書き込んでいる。ただしストリームの先が端末なら\nが書き込まれた時点でwrite()を実行する。これがラインバッファリング。
- 一切バッファしないunbuffered modeもある。これはsetvbuf()でセットできる。
- stderrは最初からunbuffered mode。エラー・デバッグ情報なのですぐに書き出した方が体験がいい。
- gets()は危険。バッファサイズを示す引数が無いのですぐバッファオーバーフローする。その結果別メモリの情報を返してしまうこともある。世界最初のインターネットワームもこれを悪用したらしい。
- なんかCのライブラリの関数名、syscallとライブラリ関数の名前が重複するのを避けるためかおかしな名前多いな
第7章 headコマンドを作る
- とてもシンプルなheadを実装した
- 愚直なtail実装した
- tailをseek使って再実装。ちょっと早くなった。
第8章 grepコマンドを作る
- シンプルなgrep実装した
- 文字コードは大変。UTF-8, Unicodeに対応すればいいんじゃないかな。
第9章 Linuxのディレクトリ構造
- ディレクトリの使われ方を説明する
- FHS (The Filesystem Hierarchy Standard)はディレクトリツリーの標準規格。
- /bin, /usr/binはディストリビューションが管理するディレクトリ。自分でインストールするコマンドは/usr/local/binに置くべき。
- sbinには管理者用コマンドを置く。
- lib, lib64にはライブラリを置く
- /usr
- 複数マシンで共有可能なファイルを置く。例えば学校などで複数マシンをセットアップするとき、強力なマシンにだけ/usrを配置し、それをnetwork filesystemで共有する。
- User Services and Routinesの略
- /usr/srcにはシステムのソースコードを置く。自作プログラムは置かない。
- /usr/includeにはヘッダファイルを置く
- /usr/shareにはman, info等のアーキテクチャに依存しないものを置く。
- /usr/localのサブディレクトリの配置は/usrと同じ。管理者が異なる。そのマシンの管理者が管理する。
- /varには頻繁に書き換えられるファイルを置く。例えばlogや、メールやプリンタの入力。/var/runには起動中のサーバープロセスのプロセスIDを置く。終了時にそれを削除するのがマナー。
- /etcには設定ファイルを置く。前はエトセトラという意味だった。
- /devにはデバイスファイルを置く。前は全てのデバイスファイルを作成していたが、今はdevfsやudevという仕組みで必要になったタイミングで作成している。このようにしているのはハードウェアがとても増えたため。
- /procにはプロセスファイルシステムがマウントされる
- /sys。procfsがあまりにプロセスに関係ない情報で埋まったので作られた。sysfs。システムに関するデバイスやデバイスドライバーの情報がある。
- /boot。
- /root。スーパユーザのホームディレクトリ。
- /tmp, /var/tmp。一時的なやつ。例えばdiffで使われる。/var/tmpは再起動しても残っているのでvimのリカバリ用ファイルとかに使われてる。
- ホームディレクトリ。$HOME。/homeとは違う。多くの場合/homeだが。
第10章 ファイルシステムにかかわるAPI
- 扱う要素はこれら
- ディレクトリ
- ファイル名(パス)
- メタデータ
- ls実装した
- ディレクトリを再帰的に処理するトラバースを実装する際は以下に気をつける
- ., ..の扱いに気をつける。無視すれば良い。
- シンボリックリンクも無視する。
- mkdir(2)の引数のパーミッションはそのまま使われない。umaskのビットを落として適用するパーミッションを計算する。
- rm -rはディレクトリをトラバースしてエントリを一つずつ地道に消している
- mkdir/rmdir実装した
- mkdir -p実装した
- ハードリンク
- ファイルには複数の名前をつけることができる。
ln a b
のようにして。 - rmは実はファイルの名前を消している。全てのハードリンクが消されたら実体となるファイルが消される。
- ファイルには複数の名前をつけることができる。
- ハードリンクとシンボリックリンクの違い
- シンボリックリンクは名前に対してリンクする。なので実態が存在しなくても良い。
- ハードリンクは実態に対してリンクする。
- シンボリックリンクはファイルシステムをまたいで良い。またディレクトリにも別名をつけることができる。
- 今は基本的にシンボリックリンクを使う。
- symlinkでシンボリックリンクを作ることができる。
- Linuxにおける「ファイルを消す」とは、実態につけた名前を減らすこと。なのでunlink(2)という名前のシステムコールがある。
- mvするにはrename(2)を使う
- ファイルのメタデータを見るにはstat(2)を使う
- ファイルのメタデータを変更するには
- パーミッション: chmod(2)
- オーナー・グループ: chown(2)
- lchownを使うと、対象がシンボリックリンクの場合そのリンクのオーナーを変える。
- 最終アクセス時刻と最終更新時刻: utime(2)
第11章 プロセスとハードウェア
- バスを通じて全てのハードウェアが繋がっている
- CPUは機械語しか理解しない
- CPUができる命令は簡単なものだけ。メモリからバイトを読む、書く、四則演算、ビットシフト、原始的な条件分岐(if + goto)
- Linuxは複数のタスクを動かせる。それはハードウェアとカーネルの協力によって成り立つ。
- ダイナミックリンクとスタティックリンクなら常にダイナミックリンクを使おうと書いてある。本当にそうだろうか?トレードオフがあるので場面によって使い分けるべきでは。
- ダイナミックロード: 実行時にリンク作業をする。RubyやPythonがこれ。仕組みとしては、実行中にリンクローダーを走らせる。
第12章 プロセスにかかわるAPI
- 他のプロセスの情報を取得したいならファイルシステム/procを使う
- 別のプロセスにデータを送る仕組みがパイプ。シェルだけの機能ではない。
- 大体Linuxのしくみで見た内容だ
第13章 シグナルにかかわるAPI
- SIGKILLはプロセスを強制終了する。この挙動は上書きできない。
- sigaction(3)を使えばプロセスのシグナルハンドリングを上書きできる。
- シグナルはシグナルキューに一旦保存される。そしてプロセスの次のスケジュール時に実行される。
第14章 プロセスの環境
- この章ではプロセスが保持する属性について説明する
- プロセスはカレントディレクトリを保持している。これはgetcwd(2)で取得できる。なお、getcwdは引数としてパスを格納するバッファを受け取るが、このサイズを決めるには成功するまで何度もトライするのが正攻法。Rustなら多分こんな感じ。
cwd by Rust
use std::vec;
fn main() {
# リトライしている様を確認するためにあえて1にしている。
let mut buf = vec![0u8; 1];
loop {
println!("buf.len() = {}", buf.len());
let n = unsafe { libc::getcwd(buf.as_mut_ptr() as _, buf.len()) };
if n.is_null() {
# メモリが指数的に増えるが問題ないのかな。固定値を足す方がいいだろうか。
buf.resize(buf.len() * 2, 0);
}
if n == buf.as_mut_ptr() as *mut i8 {
break;
}
}
println!("{}", std::str::from_utf8(&buf).unwrap());
}
- 実行ファイルに対して、set-user-ID-bitをセットできる。これをセットすると、実行時の権限を返ることができる。例えばファイルの所有者のbitを立てたなら、ファイルの所有者として実行される。
- このときプログラムを起動したユーザーをreal user, 権限が適応されるユーザーをeffective userという。
- なおこの仕組みはグループにもある
- Linuxカーネルは時刻を1970-01-01からの経過秒数で保持している。UNIXができたのがその頃だから。
- time(2)で現在の経過時間が取れる。オーダーは秒。
- gettimeofday(2)。マイクロ秒単位。なんか名前が他のシステムコールと比べて長いな。timensとかじゃだめか?
- UTCは昔GMTと呼ばれていた
- ログインのときに起こること
- systemd or initが端末の数だけgettyコマンドを実行
- gettyコマンドは端末からユーザー名が入力されるのを待ちloginコマンドを実行
- loginコマンドがユーザーを認証
- シェルを起動
第15章 ネットワークプログラミングの基礎
- ネットワークだろうとなんだろうと我々が扱うのはストリーム。read(), write()で操作する。なので問題はストリームをどう手に入れるか。つまりopen()はどうやるか。
- open()する際は、通信を待ち受けているプロセスを使う。これがファイルに相当する実態。
- インターネットの場合ファイル名に相当するのがIPアドレス + ポート
- IPではパケットでデータのやり取りをする。TCPがパケットをまるでストリームのように変換する。
- あるホストからリモートホストにデータを送るにあたって、TCPがやっていること
- ストリームをブツ切りにする。その一つ一つがパケット。またそれらに通し番号を付けて送信する。
- 受信側がパケットを受け取り通し番号順に並べ替える。届かないデータが有れば再送を求める。
- 今も/etc/hostsは使われている。狭いネットワークでホストに名前をつけるために。
- クライアント側が使うシステムコール
- socket(2)
- ソケットを作成し、対応するfdを返す。引数として接続する対象を表すオプションを渡す。宛先はここでは渡さない。
- connect(2)
- ソケットとサーバーをつなぐ。これによりソケットをストリームとして扱えるようになる。
- socket(2)
- サーバー側が使うシステムコール
- socket(2)
- bind(2)
- アドレスをソケットに割り当てる。このportでlistenすることを宣言するのかな。
- listen(2)
- ソケットがサーバー用のソケットであることをカーネルに伝える。引数backlogで同時に接続可能なコネクション数を指定できる
- accept(2)
- ソケットにクライアントが接続するのを待つ。接続したら接続済みストリームのfdを返す。
- getaddrinfo(3)で、ホスト名からIPアドレスに変換される
第16章 HTTPサーバを作る
- HTTPリクエストの構造
- リクエストライン
- HTTPヘッダ
- \r\n
- エンティティボディ
- HTTPレスポンスの構造
- ステータスライン
- HTTPヘッダ
- \r\n
- エンティティボディ
- HTTPの仕組みとファイルシステムはほぼ同じ。HTTPサーバの仕事はドキュメントルート以下のファイルにマップしてレスポンスとして返すこと。サーバーの全ファイルを公開しないように注意。
- HTTPリクエストパーサー実装した
- HTTPリクエストを受けられるようにした
- HTTPレスポンスを返すようにした
第17章 HTTPサーバを本格化する
- デーモン化だったり並行処理だったりを扱う。前やったときあるので実装はスキップ
第18章 本書を読み終えたあとは
- 詳解UNIXプログラミング
- UNIXプログラミングに関して、一番詳細で一番正確らしい。1,000ページ近くあるしKindle版でも8,000円近くする。。。高い。。。
- Linuxプログラミングインターフェース
- Linux固有の事象も扱っている
感想
とても良かった。いくつかの曖昧な点が明らかになった。今後困ったときに検索できる語彙を手に入れた。
また、実際にコードを書いたおかげで、よく言われている「Linux/Unixでは全てがファイルである」の意味と嬉しさがわかった。つまるところファイルという言葉が指しているのはインターフェース(プログラミング的な使い方。Rustだとtrait。)だ。それはreadとwriteやその他の操作ができることが保証されている。なのでファイルに対してプログラムを書けば、それはあらゆるものに再利用できる。とても優れた抽象化だと思う。
とはいえ割と古い時期に書かれた本だと思うので、新しい情報にも触れたい。
このスクラップは2024/01/06にクローズされました