『ふつうのLinuxプログラミング』読書メモ
Linuxの概念で重要な三つ
ファイル
データに名前をつけて保存する
プロセス
何らかの活動をする主体。動作中のプログラム。
ストリーム
バイトの流れ。プロセスがファイルや他のプロセスとデータをやりとりする手段
ストリーム
システムコール + ライブラリ関数
システムコール
- ストリームからバイト列を読み込むread
- ストリームにバイト列を書き込むwrite
- ストリームを作るopen
- 用済みのストリームを始末するclose
他にもあるけど代表例はこんな感じ
cat < hello.txt
-xxx>をストリームとすると、
hello.txt -標準入力> catプロセス -標準出力> ターミナル(ディスプレイ) という感じ。
逆にもっと簡単にいえば、標準入力っていうのはキーボードもそうなわけで、
キーボード -標準入力> catプロセス -標準出力> ターミナル
もストリーム同士をつないでる。
じゃパイプって何者だ?
という話。この場合を考える。
grep print < hello.txt | head
これは、
hello.txt -標準入力> grepプロセス -標準出力> -標準入力> headプロセス -標準出力> ターミナル(ディスプレイ)
つまりパイプとは、標準出力を標準入力として渡すもの というわけである。
システムコールの問題点
- シンプルに遅い
- 特定のバイト単位でしか入出力できない
stdio
システムコールの問題点を解決してくれるのがコイツ。
では、どのように解決してるのか?という話。
バッファ
システムコールで読み書きするデータを一時的に保存しておく場所のこと。
stdioはバッファに対して読み書きの指示を出すことで、汎用性を上げている。
読み
読みの場合は、ディスクからバッファに塊で書き出したデータを、「1バイトとか10バイトとかの単位でちょーだい」と命令することで読み込める。
システムコールも別に1バイト単位で読めるけど、遅いという問題があるので現実的じゃない。
書き
書き込みの場合、バッファに書き込んでから一定の単位でシステムコールのwrite()を呼ぶ。
一定の単位というのがミソで、これはストリームの向こうにターミナル(端末)がある場合は改行単位になるだろうし、ファイルがある場合は例えば1Mバイトとかの塊で書き出した方が効率がいい。
メモリとCPU
0か1の列(バイト列)を記憶できるデバイス。
バイトを記憶する箱にはバイトひとつ分ごとに番号が振ってあり、その番号(アドレス)を指定すると中身を見ることができる。
- どんなコンピューターにもCPUとメモリがある
- メモリにはビットの列が記憶されている
- CPUはメモリの内容を加工する
- バスを経由して各種デバイスとビット列をやりとりする
CPU
メモリ上にあるプログラムに従ってメモリに記憶されているバイト列を変化させる。
機械語
プロセス = 動作中のプログラム = CPUがメモリ上にあるバイト列を読み込んで演算する行為
ということは、メモリ上にあるバイト列というのはなんでもいいわけではなく、一定の規則体系(0と1の羅列)に基づいて記述される必要がある。
これが機械語。
機械語はどのような言語なのか?
機械語はCPUの種類によってバラバラ。(ARM,x86とか)
だが、概して以下のような命令を持つ。
- メモリからバイトを読む
- メモリにバイトを書く
- 四則演算
- ビットシフト
- 原始的な条件分岐
マルチタスク
CPUとメモリ一つで動かせるプロセスは一つだけ。
けど、psとかでプロセスを確認すると複数のプログラムが同時に動いていることがわかる。
では、どうやって動かしているのか?
仮想CPUと仮想メモリがあるからである。
仮想CPU
「仮想ってなんやねん」という感じですが、要は一つのCPUを非常に短い一定時間ごとに実行プロセスを切り替えていることをそう呼んでいるだけ。
これにより、プロセスからはCPUが自分専用のように見えている。
仮想メモリ
メモリを増やすにはどうするかというと、物理的なメモリ空間を分割してあげるとよい。
メモリは番号で管理されていると言ったので、こういうイメージ。
0123 | 4567 | 891011
けど、そうすると実行するたびに割り当てられるメモリ番号が変わっちゃうことになる。
例えば、ある時は0123
のメモリ空間を使うことになったかと思えば、891011
を使うことになることもある。
これを解消するために、カーネルとCPUによって提供されるのが仮想メモリ。仮想メモリのアドレスを 論理アドレス 、実際のメモリのアドレスを 物理アドレス という。
どういうことかというと、
論理アドレス 0123 | 0123 | 0123
物理アドレス 0123 | 4567 | 891011
のようにすることで、プログラムは常に0123
のような連番を使っていると思えば良い。
実際は論理アドレスの0番は物理アドレスの0 or 4 or 8のどれかになるが、それはカーネルとCPUが管理するのでプログラムは無関心でいられるというわけ。
ネットワークプログラミング
マシン内だろうとネットワーク越しだろうと、相手にするのは結局ストリーム。
どうやってストリームを手に入れて、プロセス間でバイト列をやりとりするかという話。
ではどうやってストリームを手に入れるのか?
ファイルの場合はパスを指定したが、ネットワークの場合はサーバー側で通信を待ち受けているプロセスが存在している。これがファイルに対応する実体。
これを指定するために、IPアドレスとポートを指定する。
ストリームを手に入れる方法はわかったけど、、、
ではストリームはどうつくるのか?それはIPとTCP(UDP)を使う。
IP
IPの世界に存在するのはパケット。
パケットはデータの切れ端で、IPではこれをホスト同士が投げ合うことで通信が成立する。
ホスト同士が投げ合う、というのは1:1ではなく、宛先ホストに到達するまでに幾つかのホストをバケツリレーしているということである。
つまり、パケットは送った順番には届かないし、ちゃんと宛先に届くとも限らない。
TCPとUDP
IPはパケットだけの世界と言ったが、ではストリームはどこから湧いてくるんだという話で。
まず前提として、ストリームとはバイト列であるということ。
このバイト列を先頭から切り分けていき、その切り身に通し番号を振ってパケットにして投げる。
受け取る側ではパケットを開封してデータを通し番号の通り並べ、通し番号が全部揃ったらストリームとしてマシン上に再現することができる。
この手順をTCPと呼んでいる(厳密にはもうすこし手続きがあるが)。
一方でUDPの場合はパケットが届く順番や完全性も保証しないが、処理が速く、シンプルになる。
ソケット
Linuxではネットワーク通信にソケットなるものを使う。
ソケットは電球のソレと同じく、つなぎ口的な意味。
ソケットはクライアントとサーバー側の両方に必要。
TCP/IP,ソケットを使って接続を確立さえできれば、あとはストリームを相手にしているのと同じ。
複数の接続を並列に扱う
1プロセスが担当する接続は1つだけ、という前提がある。
となると、クライアントが複数になってくることを考えると、普通に考えればうまくいかない。
ではどうするか?
それは、接続は親が担うけれど、あとの処理本体とレスポンスに関してはfork()により子プロセスを生み出すなどして、接続の口を空けるというようなことをするのである。
またこのようなサーバーを並行サーバーという。
プリフォークサーバー
上記の例では元締めプロセスが接続を担って子プロセスを生み出したが、逆に最初にいくつか子プロセスを作っておいて、子プロセスそれぞれが接続を担って処理も行うという方法がある。
マルチスレッド
プロセス = 実行したプログラムそれ自体
スレッド = プロセスの中で動くそれぞれの処理
スレッド∈プロセスであるが、大きく違う点が一つ。
それは、スレッドはメモリ空間が分かれていないということ。
つまりstaticに宣言された変数やグローバル変数を共有しているということで、マルチスレッドでのstatic変数やグローバル変数の扱いには注意が必要。