はじめに
Unixシステムについて、Ubuntu環境でプログラムを書きながらゆっくりと学んでいきたいと思います。
言語はC言語を使用します。
UnixとLinuxは異なりますが、
LinuxカーネルはUnixとほぼ同機能を兼ね備えたソフトウェアなので、
Linuxカーネルを使ってもUnixを学ぶ事が可能です。
動機
現在、私はWebアプリケーション開発を行っていますが、
ほとんどのアプリケーションのバックエンドで使用されているLinuxカーネルについてほぼ何も知らないからです。
何故ほぼ何も知らずにアプリケーションができるかというと、バックエンドで使用される言語であるJavaやGo等の仮想環境がLinuxカーネルとのやり取りのほぼ全てを実施してくれるからです。
(WebサーバアプリケーションであるApacheやNginx等もそうです。)
そのおかげで、私たちアプリケーションエンジニアはビジネスロジックに集中する事ができ、低レベルレイヤーについてはほぼ知らなくてもアプリケーションを作成する事ができてしまいます。
しかし、私は上記のような仮想環境やサードパーティーのアプリケーションを使ってみて、どういう仕組みで動作しているのかを使用者は知っておくべきだと思っています。
低レイヤーを知っておけば、仮想環境やサードパーティー製のアプリケーションのコードが読めるようになります。
(現在はほぼオープンなのでコードを見る事が多いと思います。)
コードを読んで理解する事ができれば、アプリケーションのコードを最適化できる可能性があります。
(無駄があるコードを削除する事や、効率のいいIFで置き換える等)
その結果として、性能面やバグの少ない、品質の高いアプリケーションを作成する事ができると思っています。
そういった理由からUnixについて知ろうと思ったのが動機です。
環境
OS:Ubuntu-22.04.3 LTS
コンパイラ:gcc 11.4.0
CPU:Intel Corei7
項目
1.Unixアーキテクチャについて
2.ログインについて
3.ファイルシステムについて
4.入出力について
5.プロセスについて
6.ユーザ、ユーザグループについて
7.シグナルについて
8.時間について
Unixアーキテクチャについて
OSの定義はいくつか存在していますが、ここでの定義はハードウェアの資源を管理し、プログラムが動作する為の環境を提供するソフトウェアと定義します。
カーネルのIFはシステムコールと呼ばれます。
ライブラリはシステムコールを使用して構築されます。
アプリケーションはどちらをコールする事も自由です。
シェルは他のアプリケーションを実行する為のIFを提供するアプリケーションです。
因みに、Linuxとはカーネルの事を指しています。
カーネルだけだと使い勝手が悪いので、ライブラリやシェル、ウインドウシステム等、ユーザに使い勝手がいい物をディストリビューション化したのがUbuntuやCentOS等のディストリビューションになります。
ログインについて
端末からUnixシステムにログインする時は、内部でinitプロセスをforkしてgettyをexecし、
ユーザに対してログイン名の問い合せを行います。ログイン名を入力すると、gettyからloginをexecし、
/etc/passwd内から入力されたログイン名のレコード情報を取得します。
このレコード情報にはログイン名、パスワード(※)、ユーザID、グループID、ホームディレクトリパス、使用するシェルが記載されています。
パスワードを入力した後、暗号化を行い、システム上に記録されている暗号化されたパスワードと比較を行い合致すればログインできます。(passwdは誰でもアクセスできるようになっており、暗号化されたパスワードを本ファイルに記載するのはセキュリティホールにつながる為、xとしか記載されていません。暗号化されたパスワードはシャードファイルとして管理されているのが普通です。)
その時に/etc/passwdのレコードに記載されている使用するシェルをexecします。
恐らく、デフォルトで使用するシェルはBourne-againシェルかと思います。
ファイルシステムについて
Unixのファイルシステムはディレクトリとファイルを階層的に配置しています。
全てはルートディレクトリ「/」から始まり、ツリー上に配置されています。
ディレクトリとはディレクトリエントリ群を納めたファイルです。
各ディレクトリエントリは、ファイル名とファイル属性が納められています。
ファイル属性としては、ファイルの種類、ファイルのサイズ、所有者、アクセス許可、最終修正日等が格納されています。
ファイル名は/とnull以外の文字で構成された名前です。
パス名は1つ以上のファイル名を/で区切って並べたものです。相対パスと絶対パスが存在していて、
相対パスはカレントディレクトリからの相対的なパスで、絶対パスはルートディレクトリからのパスです。
入出力について
Unixシステムでは、入出力は全てファイル記述子を使って行います。
ファイル記述子とは、ファイルを開いた際にカーネルが識別する為の非負の整数値です。
loginを実行する前のgettyにて、標準入力、標準出力、標準エラーを開きます。
これらのファイル記述子は順に0,1,2です。
端末からUnixシステムにログイン後に、シェルからアプリケーションをコールできたり、その結果を確認できたりするのは、上記のファイル標準入出力がオープンしているからです。
プロセス
プロセスはプログラムが動作している実体を指します。
プロセスは一意のIDを割り当てられます。
現在のプロセスのプロセスIDはgetpidをコールする事で確認する事ができます。
プロセスはfork, exec, wait(waitpid,waitid)を使って制御します。
forkを使う事で、プロセスを生成する事ができ(生成元と同じプログラムを実行します。)、
execを使う事で、他のプログラムをコールする事ができ、
waitを使う事で、任意のプロセスの処理を待って、その結果を知る事ができます。
forkで生成したプロセスを子プロセスと言い、生成元を親プロセスと言います。
同一のプログラムではテキストセグメントを共有します。(他のセグメントは共有しません。)
(上記の画像はメモリ実装の一例です。メモリ管理が上記のように実装されているとは限りません。)
スレッドは、スタック領域を別で持ちますが、他のセグメントは全て共有します。
但し、スタック領域は別で持ってはいるものの、同一プロセスの他のスレッドのスタックは参照可能です。
プロセスとスレッドはメモリ空間を共有するかどうかが異なります。
ユーザとユーザグループについて
/etc/passwdのエントリにあるユーザIDをシステムでは使用します。
各ユーザは必ず1つ以上のユーザグループに属しています。ユーザグループにもIDが存在しています。
通常グループはチームやプロジェクト等、複数のユーザをまとめる際に使用します。
(2つ目以降のグループを補助グループと呼ぶ)
ユーザIDとグループIDを使って、ファイルの読み取り、書き込み、実行のパーミッション制御を行います。
スーパーユーザはどちらのIDも0となっており、Unixシステムでは0の場合パーミッションの制御を行わないようになっています。
シグナル
ある条件が生起した事を通知する為の技法をシグナルと呼びます。
プロセスでシグナルを受け取った際に、取れる対応は3つあります。
1.シグナルを無視する
2.デフォルトの動作を行う
3.指定の動作を行う
シグナルを生起させるにはkillをコールします。(killという名前は勘違いする名前ですが。。。)
時間
Unixシステムでは2つの時間を扱います。
1.カレンダ時間
1970年1月1日0:00:00からの経過秒数。
この時間はtime_t型で値を保持します。
(32bitアーキテクチャの場合は、time_t型は32bitの為、2038年で終焉を迎える。)
2.プロセス時間
プロセスが使用したプロセッサ資源を測る。クロック数で時間を測る。
この時間はclock_t型で値を保持します。
このプロセッサ時間は2つに分ける事ができ、それぞれをシステムCPU時間、ユーザCPU時間と呼ぶ。
システムコールとライブラリ
最初のUnixアーキテクチャについての項目でシステムコールはカーネルのIFと説明しました。
ライブラリはシステムコールをよりユーザに対して使いやすくしたIFを提供しています。
また、システムコールをコールする際は割り込みを発生させてカーネルに制御を渡す必要がある為、
コールする際にボトルネックが生じる点、ライブラリが吸収してくれるものもあります。
例えば、C言語の標準入出力ライブラリのprintfでは、標準入出力ライブラリ用にバッファを設けており、
改行文字が現れるまでは、ライブラリ内(ユーザ空間上で動作する)でバッファリングを行い、
改行文字が現れた際に、ライブラリのバッファの内容をカーネル空間のバッファに移してシステムコールを呼ぶようになっています。(出力先がファイルの場合は、ファイルを閉じるまでバッファリングを行う。完全バッファリングと呼ぶ)
このように内部でバッファリングを使ってシステムコールのコールする数を少なくするように設計されていたりするので、性能面で有利に働きます。
今回は概要だけの紹介となりましが、
次回はコードを使って1つ1つの項目をもう少し深堀りしていければと思います。