Linuxカーネルについて

勉強用めも 自分用 写経 書いたりしないと頭にはいらないので

カーネルがあってデバイスドライバがある。
それはとっても素敵なことだなって。
ファック。
それらの上でアプリケーションやそのライブラリが動く。
カーネルとデバイスドライバが動くのはカーネル空間。
アプリケーション、そのライブラリが動くのはユーザ空間。

プロプライエタリはオープンソースの反対で、ソースを公開しない分、ドキュメントが充実させないといけない。
オープンソースは自分でコードを読める人にとっては使いやすいかもしれないが、スキルがなければ使いこなすのはそれだけむつかしくなるかもしれない。

C言語、アセンブリ言語、スクリプト言語がC言語で使用されている。
コードが読めるだけじゃだめ、前提の知識を知らなければ。


段階を踏んで基礎を抑える必要あり。
ハードウェアの理解も必要。デバイスドライバはI/Oデバイスというハードウェアを制御する。カーネルも同じものを制御するが、デバイスドライバとは趣旨が異なる。
PCIカードやキーボード、マウスなどの周辺デバイスがI/Oデバイス。
Peripheral Component Interconnnect

カーネルはハードウェアと協同して、アプリケーション(ユーザプロセス)を健全に動作させることができる基盤を構築することが第一の目的

コンピュータアーキテクチャの理解が必要

ユーザプロセスとカーネルの動作。ユーザプロセスとカーネルは違う。カーネルは基盤。
ユーザプロセスはアプリケーションとも言い、カーネルの上で動かす、ユーザーの目的。

ユーザプロセスがカーネルの機能を利用することをシステムプログラミングという。
システムプログラミングという概念が重要。

これは、単にC言語を知っているだけでは理解できない部分。
ソースコードを読むためにはCを知ってるだけじゃダメ。

技術を完全に理解するのは不可能。
理解できると思い込むのは技術者のおごり。

ユーザ空間とカーネル空間の違いを理解する。
ユーザプロセスとカーネルとでプログラムの設計がまったく変わってくる。
たとえばユーザプロセスにはmain関数が存在するが、カーネルやデバイスドライバにはない。

Linuxカーネルの学び方についてのてっとり早いやり方は、こう。

まずはコンピュータアーキテクチャを知ること。
つまりはハードの動き、ハードの仕様を知っておく必要がある。
しかしハードウェアの中身もソフトウェアであるため、ハードとソフトの境界を決めるのはむつかしい。
一般的に、目に見える機器をハードという括りにしておくとよい。

ハードといっても無数にある。
さまざまなプラットフォーム(アーキテクチャ)がある。
LINUXはどれでも動くようになっている。対応の幅広さは業界一。オープンソースのすごさ。
プラットフォームごとにハードウェアが全くことなるため、プラットフォーム依存のソースもまったく変わってくる。

なので、どのプラットフォームに対応するLINUXカーネルを学びたいかを明確にしておく必要あり。
使用しているLinuxがどのプラットフォームに対応してるかはunameコマンドでわかる。
uname -a すると、情報が出てくる。普通だとx86_64。これがアーキテクチャで、x86_64はIntelおよび互換CPUの64bitのこと。
32bitだとi386とかi686とか。

Linuxカーネルのビルド時に、Linuxカーネルが内部にアーキテクチャ文字列を保持していることになり、unameコマンド実行時に、Linuxカーネルから文字列を渡してもらってそれを表示している。
ということらしい。
ビルドというのはソースコードから機械語に変換すること。

ちなみにi386やi686の前は8086や80286、80486だった。それがi386やi686になり、総称して80x86やx86になった。そののち、PentiumやCoreとなり、それが今ではx86_64。

Linuxは組み込み機器にも使用されてる。Androidスマホなど。端末情報を開くとカーネルバージョンを見れるが、これはスマホで使用されてるLinuxバージョンのこと。

組み込み系のCPUはARMが一般的。36bitのarmと64bitのarm64がある。

ハードウェアの仕様書すべての無料入手はむつかしい。機密の塊。ベンダーとNDAを結んで情報ゲット。Non-Disclosure Agreement。
ハードウェア情報が公開されてない製品はオープンソースでハードを制御するソフトウェアは作れない。Windowsのデバイスドライバは提供されているけれど、Linuxのデバイスドライバは探しても見つからない、というような事例。

企業自体がLinuxに関するパッチやデバイスドライバを開発して一般公開してることもある。たとえばラズパイとか。ラズパイはOSが組み込みLinuxでソースコードは一般公開。
ただしSocつまりはSystem-On-a-Chip、つまりは一つのチップ上にCPU、インターフェイスなどの機能を搭載した集積回路、つまりはマザーボードの縮小版そのものの仕様書は未公開。
broadcomのBCM28xxシリーズが採用されているらしい。

Intelアーキテクチャには、32bitのIA-32、64bitのIA-64、32&64bitのIntel 64がある。
AI32は8086からPentiumまでのもの。IntelArchtectureの略。Linuxではx_64。
IA64はItanimuという64bitプロセッサ。IA-32との互換性なし。OSとしてはHP-UXのみが対応している。オワコン。
Intel 64はIA-32の64bit拡張。32bitと互換性あり。Linuxではx86_64。
32bit時代は長かったため、IA32の資料の入手はしやすい。

intelの一般公開情報としては、IA-32インテル アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアルがある。
https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol3_i.pdf

ただしこれらは合計2500ページもある邪悪な書物なので心をむしばんでくることはめいはくであるといえよう

https://gist.github.com/tenpoku1000/2250ec65264ff2d639ddeeffd305fe68
https://intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol1_Online_i.pdf
http://uno036.starfree.jp/PRGmanual/masm_pdf/IA32_Arh_Dev_Man_Vol2A_i.pdf
https://intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol2B_i.pdf
https://intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol3_i.pdf


システムプログラミングの定義は様々だが、基本的には「ユーザシステムがシステムコールを使ってカーネルの機能を呼び出すこと」とおぼえる。システムコールというのはユーザプロセスとカーネルをつなげるインターフェイス。

カーネルには、カーネル内部を保護するために、ユーザプロセスからカーネル内部に自由にアクセスできないようになっている。これは仮想メモリという仕組みによって実現されている。しかし高度な処理にはカーネル機能呼び出しが必要。そこで使われるのがシステムコールという仕組み。すごいね!

たとえばhello worldコードを作成したあとにccコマンド(gcc GNU Compiler Collectionパッケージに含まれるコマンド。ソースコードから機械語への変換が可能。もともとC言語にしか対応してなかった名残でコマンド名がC Compilerとなっている)でコンパイルすると実行プログラムがa.out(Assenbler Output)というデフォルト名でアウトプットされる(これはa.outというファイルフォーマットが由来だが、file a.outすると、実際のファイルフォーマットはELFであることがわかる)

hello worldプログラムではprintf()で文字出力する。これはカーネルの「端末へ出力する」機能を呼び出しているが、この関数はシステムコールではない。C言語におけるシステムコールは関数に相当するが、使用してる関数がシステムコールかどうかはman -a printfでmanページを見るとわかる。
PRINTF(3)と書いてある、かっこの中の数字をみる(ただしwslでは1)。これはセクション番号。セクション番号が2のものがシステムコール。
実際の関数の処理の流れとしては、printf関数自体が内部的にwrite()という関数を呼び出している。これがシステムコールになる。man -a writeで調べると、セクション番号は2番になってる。ただしwslでは1。

なおプログラムが発行するシステムコールの確認にはstrace コマンドを使い strace ./a.out とするとよい。./a.outの部分は、適宜自分のつくった実行ファイル名に変える。pythonファイルの場合はstrace python3 test.py とするとよい。

c言語の入門書にはc言語の基本文法が書かれているが、manページのセクション番号3の関数の説明がほとんどで、システムコールの開設はたいてい乗ってない。
セクション番号2を読み、システムコールとして使われる関数の意味を知ることが重要。

ユーザ空間は制限空間。カーネル空間とユーザ空間ではプログラムの動作が変わる。ユーザ空間はユーザプロセスの自分自身以外のメモリ領域にアクセス不能。システム全体に悪影響を及ぼさないため。自分以外のメモリ領域にアクセスするには特殊な手段が必要。ユーザプロセスにroot権限を持たせると間接的にアクセスがかのう。
カーネル空間では制限なし。なんでもできる。カーネル空間で動くLinuxカーネルやデバイスドライバには高品質が求められる。ちょっとしたバグでシステム全体がクラッシュする可能性あり。セキュリティホールになる可能性も。

コードを読み解くのは大変。自分のコードでもわからなくなるのに、他人のコードはさらに難しい。理解のためにはドキュメントだが、Linuxカーネルはソースコードだけしかない。
なのでコードだけ読んでもわからない。なので実際に動かすのが大事。
プログラム動作時、変数の内容を知りたいときは、printk()関数が常套手段。俗にいうprintfデバッグ。ただし、printk()埋め込みのために再ビルドが必要なのがめんどくさい。再ビルドしたくないときはSystemTapというツールもおすすめ。カーネル動作中にprintfデバッグが使える。
※なお、printkはprintfとは違う。printfはユーザプロセスで使用するのが前提だが、これが使えるのはユーザプロセスが端末を持つから。端末を持たないカーネルプロセスはprintfを使えない。だから端末を持たないカーネル用にprintkが用意された。
※SystemTapでは、専用の言語を使ってスクリプトを記述し、カーネルモジュールを自動生成し、カーネル内にブレークポイントを埋め込むことで、オンラインデバッグを可能にしている。


Linuxカーネル理解のためのC言語の学習法について。
LinuxカーネルはC言語でできてる。
Linux Kernel Archivesで中身が見れる。
https://www.kernel.org/

なお、Linuxディストリビューションのひとつubuntuの19.04はlinuxのカーネル5.0がベースだが、独自にパッチを適用してある。

LinuxカーネルはほとんどがC言語で、数パーセントがアセンブラ。なのでCを知らないと読めない。コード自体は1200万行くらいある。
C言語の学習だが、C言語自体はほとんど進化してない言語なので昔の本で学ぶのでOK。ただしポインタの概念がむつかしい。
なお言語の確認には仕様書をみないとダメだが、C言語の日本語版の仕様書は有料なんだとくそったれー。日本規格協会グループのサイトから13000円くらいで買える。要するにJISである。JIS X3010。
https://webdesk.jsa.or.jp/books/W11M0090/index/?bunsyo_id=JIS%20X%203010:2003
まあ実際はこのへんで読める。
https://kikakurui.com/x3/X3010-2003-01.html

規格になる直前の仕様書としてworking paperが以下で読める。
http://www.open-std.org/jtc1/sc22/wg14/www/standards.html

C言語はアセンブリに比べれば高級だがハードウェアを制御可能。C++でも制御はできるが、C#とかJAVAだとむつかしいらしい。なので組み込み系は全部CとかC++。ゲームプログラミング系もそう。組み込みのハード制御はC系統だが、Web機能を触る場合はHTMLやJavaScriptの知識も要求され、上から下までのレイヤの幅が広い。

なおLinuxカーネルで使われるC言語はC89。標準にはないgcc拡張を使用。なのでLinuxカーネルの理解にはCの最新の参考書を買いそろえる必要はない。
Linuxカーネルのビルド時はgccを使用するが、どの規格に従って動作させるかは-stdオプションで指定できる(gcc自身のデフォルトはgnu11。C11とgcc拡張の両方が使用可能という意味)。Linuxの場合は-std=gnu89。これはカーネルソースコードのMakefileを見ればわかる。

Makefileは、makeコマンド一発でソースをビルドするためのしくみのこと。ソースファイルの数が多い場合に威力を発揮。通常のビルドでは、gccコマンドを使用するが、ソースファイルの数が増えるとファイルを指定しての実行が手間。なのでMakefileに設定をまとめると、内容に従いgccコマンドを自動で起動してくれる。Makefile自体はUNIXから存在する。

Linuxには、long long型という、C89の仕様書にない型が使用されている。
それは、C89の仕様制定が32bitのCPUの時代だったから。ソフトウェアが2の32乗である32bitまでしか使えなかったため、longも32bitが最大だった。そのため、gccの拡張としてlong long型が作られた。なおlong longはC99で仕様として取り込まれた。

なおgcの拡張機能の一部は以下がある。
・max(x, y)という、大きいほうの数字を返すマクロがある。
#define __cmp_once(x, y, unique_x, unique_y, op) ({ \
typeof(x) unique_x = (x); \
typeof(y) unique_y = (y); \
__cmp(unique_x, unique_y, op); })

そのマクロの一部分が上なんだけど、中に{}で囲われた部分がある。これはC89にはない機能で、C89では{}だと評価結果を受け取れない。しかし、拡張機能でさらに()で囲むことで評価結果を受け取るkとができるようになる。


三項演算子はC86だとz = x ? x : y が正確。ただ、Linuxの拡張機能だとz = x ? : yという書き方で書ける。

c言語で使われる整数データ型は大小関係だけが仕様で規定されておりデータ型のサイズはコンパイラ任せとなっている。charは1バイトと仕様で決められている。大小関係は「char < short < long < long long」の順番。Linuxだと以下。

32bit: char 1bite short 2bite int 4bite long 4bite long long 8bite
64bit: char 1bite short 2bite int 4bite long 8bite long long 8bite

longのサイズが可変なのは、longがポインタのサイズに対応しているから。32と64の両方でmaxは8バイトまで。
8バイトを超える整数を扱うときは128bit整数型(__int128)がサポートされている。64bit型と32bit型の掛け算をすると64bitに収まらないため128bit型にキャストされる。

c89の構造体は空だとダメ。Linuxだと拡張機能でOK。空の構造体は意味がないが、コンパイルエラーを回避する目的で使われる。

struct empty {
};

Linuxカーネルのソースコードはプリプロセス機能のマクロが多様されている。理由は、移植(ポーティング)を容易にして互換性(コンパチビリティ)を保つため。
 プリプロセス機能とは、コンパイラーがコンパイルを行う前に、プリプロセッサがプリプロセッサ命令(#で始まるコマンドなど)を処理するもの。
便利で強力だが、ソースがどう展開されるかさっぱりわからないことになるので要注意。
#include <stdio.h>
とか
#line 1 "dir/stdio.h"
とか(↑はプログラマーが使うことはたぶんない。プリプロセッサが#includeを展開したとき、元となった位置(ファイル名と行)が記述される)
#define INCLUDE_HOGE_H
#define ARRAY_MAX 100
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define STR(no) #no
とか(マクロを定義する)

#if , #ifdef , #ifndef , #elif , #endif のことを条件コンパイル文という。この処理はコードの実行時ではなく、ソースのコンパイル時に実行される。
機械語変換時に不要なコードを除去して実行プログラムのサイズ増加を抑止できる。
ただし多用するとコードが読みにくくなるので条件コンパイルを嫌う人もたくさんいる。CとC++には条件コンパイル文がサポートされているが、JAVAではサポートされていない。C#ではサポートされている。

Linuxカーネルでは条件コンパイルを活用して、カーネルコンフィグレーションというしくみが実現している。
#define FUNC1_ENABLED
#undef FUNC1_ENABLED
#define FUNC1_ENABLED

みたいにしたあと、
#ifdef FUNC1_ENABLED
void func1(void) { printf("%s\n", __func); }

とやって、

int main(void)
{
#ifdef FUNC1_ENABLED
func1();
}

などとする。
#define , #undif などで機能を切り替える。

これは、複数の場所に#define ABABAがあったとき、#ifdef ABABAがどのABABAを参照してるのかわからなくなるのがつらい。
(実際には、プラットフォームの違いを吸収する目的でカーネルコンフィグレーションが使われる)
実際にどのような定義になっているかは、実際にコンパイルすることで確認できる。そのための方法がある。
まずMakefile中の一行目にEXTRA_CFLAGS += -save-temps=objを追加する。
この状態で、特定のソースのみをコンパイルする。全部コンパイルすると死ぬほど時間がかかるため。
make kernel/printk/printk_safe.o
終了後、cd kernel/printk
ls -l printk_safe.* で生成ファイルを確認


Linuxの動作フローについて。
Linuxカーネルはソフトウェアだが、単なるアプリと違うところは、ハードウェアと協同しながら動作するという点。
ところでLinuxという単語だが、これはLinuxディストリビューションのことなのか、Lixunカーネルのことなのか、二通りの意味がある。
LinuxディストリビューションとOSは同じ意味。
Linuxカーネルはリーナスが開発してるOSの中核部分。

カーネルだけではOSとして成立しないので、パソコンに導入できない。
カーネルに必要なソフトウェアを集めてきて、一つのOSとして構築したものがディストリビューション。
Linuxディストリビューションの構成は、
ブートローダー(GRUB2)、インストーラー
アプリケーション、ライブラリ、カーネル、カーネル中デバイスドライバ、デバイスドライバ
このうち、カーネルとカーネル中デバイスドライバをリーナス氏が開発している。
カーネルの実態は、/boot/vmlinuz-*.*.*-**-genericというファイル。*には数字が入る。
cd /boot して lsすれば見える。

カーネルにはデバイスドライバが含まれる。
vmlinazに直接取り込むタイプを静的型static型、vmlinuzとは別のファイルのタイプを動的型dynamic型という。動的型デバイスドライバのことをカーネルモジュールといい、/lib/modules配下に格納されている。

電源が切れたときに電源が投入されることをパワーオンリセットと呼ぶ。POR。雷で電源が一瞬電源が切れ再度電源がつくことを瞬停とか瞬断というが、これもPOR。電源ケーブルが差し込まれた状態のPCには電気が流れるがこれもPOWでスタンバイ電源が入ってるという。電源さしてるだけで電気代がかかる現象。組み込み系で動作するLINUXはこのスタンバイ電源で動作するよう設計されている。
効果なPCサーバにはBMC、baseboard Management Controllerというチップが組み込まれている。このチップにはチップを制御するファームウェアという種類のソフトウェアが組み込まれており、スタンバイ電源の電力で動作する。ファームウェアは別名組み込みソフトウェア、制御ソフトウェア。

PCの電源ボタンを押すとCPUに電源が入り、CPUが動き出す。CPUはメモリのリセットベクタ、つまり物理アドレスFFFFFFFOHにある命令命令コードを読みだして実行を開始。リセットベクタにはBIOSのブートストラップコードが格納。なのでリブート。

Basic Input Output System, BIOSはハードウェア情報を抽象化してくれる。マザボと周辺機器の仕様が無数にあるため、すべての組み合わせにどうやって対応するかという問題が生じる。これを解決するために、BIOSがハードウェアを抽象化してOSに渡す。この仕組みをAdvanced Configuration and Power Interface, ACPIという。ACPIの規格はLinuxカーネルもサポートしている。マザボとOSのメーカーはWINDOWSのサポートがメインなので、LinuxカーネルはどのバージョンのWindows向けの情報かを読み取る。

なおBIOSは昔の名称で、メーカーごとに無数のBIOSがレガシーBIOSとして乱立していた。アセンブラで書かれていた。それに対し共通仕様を策定してC言語にしたExtensive Firmware Interface、EFIが生まれた。2000年ごろ。そのあと、Unified EFI、UEFIに進化した。2005年。

組み込みの場合はBIOSを使わず、パワーオンリセットPOR後に直接ブートローダが起動してOSが起動する。しかしハードウェアが異なると対応法も異なるという問題は共通のため、Device Treeという仕組みが導入されている。

BIOSし、BIOSがディスクからブートローダを読み込み、ブートローダを起動する。ここでBIOSの役目は終了。

ここからはブートローダ様のお出まし
ei, yo, 俺らの動きよくチェックしとけ

Linuxのカーネル起動には/boot/vmlinuz-*.*.*-*-genericを実行するだけでよいが、カーネルに不備があった場合どうする?ってなったとき、起動は失敗することになり、OSにログインもできなくなっちまう。Oh, Shit
 このまま何もできないで終わるのか? YES。あとはOSをクリアインストールするしかなくなる。
 自前で構築したカーネルのテスト時、/boot配下に配置したカーネルで起動するわけだが、起動に失敗した場合、以前のカーネルで起動できるようにすれば、いちおうOSは使える。この用途のためにブートローダが役に立つ。ブートローダも壊れた時は、作成した外部メディアからUSBやDVDのようなレスキューディスクを作成し、ディスクをマウントすることでデータ復旧が可能。

Linuxでは今おもにGRUB、Grand Unified Bootloaderのバージョン2が使用されている。それ以前はLILO、Linux Bootloaderが使用されていた。
なお今はデフォルトではブートローダは表示されない。開発者しか意味わかめだし、ユーザーにとってはどうでもいいことだからだ。くそだせえってこと。

ブートローダから/boot/vmlinuzが起動されて、Linuxカーネルの起動が完了すると、最後にinitデーモンを呼び出す。kernel_init()関数に記載された4つのパスからinitデーモンを起動できなければ、カーネルパニックを起こす。カーネルパニックはpanic()関数が呼び出されて起こる動作であって、カーネルが完全に壊れたわけではない。完全に壊れたら、panic()も呼べずにハング(ストール)した状態になる。
 なおLinuxが起動するのはinitデーモンだが、Ubuntuではsystemdというデーモンが起動している。これは/sbin/initがsystemdへのシンボリックリンクになっているから。

Linuxカーネルの存在意義はユーザプロセスを健全に動作させるたえ。ユーザプロセスはアプリケーションつまりはアプリのこと。Linuxでは一つのプログラムをプロセスと呼ぶ。ユーザー空間で動作するプロセスなのでユーザプロセスと呼ぶ。psコマンドで動作中プロセスを見れる。

あるユーザプロセスが誤作動しても他プロセスは影響を受けない。カーネルが落ちることもない。高負荷を食らってほかのプロセスが巻き添えになることはあるが、プロセスのメモリ空間が独立してるので干渉しあうことはない。
メモリが大量に喰われたら、OOM Killer、Out of Memory Killerという機能を発動させる。Linuxをハングさせる怪しいプロセス絶対に殺すマンがOOM Killerの正体さ。

ユーザプロセスの利点は、ハードウェア制御を考えなくてもいいこと。MS-DOSなどではアプリケーション作成時にハードウェア制御も必要だったがその必要がなくなった。ほかにも、複雑な機能を遮蔽化して純粋にアプリケーション層の開発に専念できるというメリットもある。たとえばネットワーク通信を行うユーザプロセスを開発しようとした場合、TCP/IPプロトコルの詳細を知らなくても、ソケットというライブラリを使用することで、TCP/IPプロトコルについての知識がないまま開発できる。

なおLinuxカーネルは、起動完了後、自分から自発的に動くことはない。なおここでいうカーネルにはデバイスドライバも含まれている。

ユーザプロセスごとにメモリ領域が確保されている。Linuxはマルチタスクだが、それは、ユーザプロセスを瞬間的に切り替えることで達成される。プロセスの切り替えはハードウェア自体に実装されているが、Linuxではソフトウェア的に行う。プロセス切り替えはcontext_switchからはじまる。これで、CPUが管理する仮想メモリ空間をもう一方のユーザプロセスのものに変更する。
 次にswitch_to()を呼び出す。この実態はマクロであり、__swicth_to_asm()というアセンブラコードを呼び出す。CPUのレジスタを直接操作するためにアセンブラが必要なのだ。
 __switch_to_asm()ではスタック領域の切り替えを行う。ユーザプロセスAのスタック領域(%rsp)をtask_struct構造体prevのthread.spに保存後、%rspにユーザプロセスBのスタック領域を設定する。この瞬間からユーザプロセスBのスタック領域に切り替わることは確実。
 次に__switch_to()関数へジャンプ。ここではjmp命令が使われる。普通、関数呼び出しにはcallを使用するが、jmpでは命令をジャンプさせて指定先アドレスに命令を飛ばすことができる。C言語のgoto文って感じ。リターンアドレスをスタック領域に積んだうえで命令のジャンプを行うので、後で処理が戻ってくることを期待している。ということらしい。
 __switch_to()関数は、プロセス切り替えに必要な設定を行い、最後の関数をreturn文で返す。これをアセンブラに変換するとret。ret命令呼び出し後、スタック領域先頭から命令ポインタを取り出し、そこから実行を開始。
 スタック領域の先頭にユーザプロセスBの命令コードをセットすることで、__switch_to()関数のreturn実行により、ユーザプロセスBが実行される。

仮想メモリの話。Linuxの仮想メモリはハードウェアが持つMMU, Memory Management Unitという機構で実現される。PCやPCサーバにはMMUが標準搭載されているが、組み込みでは使えない。だから仮想メモリが使えない。仮想メモリの利点は二つあり、それはメモリ空間の保護と、物理メモリの不足をハードディスクで補えること。
メモリ空間の保護は別名ページング。これにより、一プロセスがアクセス可能なメモリ領域を制限可能。ユーザが不正なメモリアクセス違反をしても、他プログラムには影響を与えない。なお、カーネル内部で不正なメモリアクセスが起きたらシステム再起動が必要。仮想メモリのターゲットはあくまでユーザプロセスのみなので。
物理メモリの不足をハードディスクを補う仕組みはスワップと呼ばれる。使ってなさそうなメモリをハードディスクに退避して物理メモリの空きを作る。freeコマンドで、メモリのスワップ状況を確認可能。ただしスワップ発生で動作が遅くなる。スワップはメモリが8MBだったWincdows95のときの機能で、当時はスワップがなければOSは動かなかった。

ユーザプロセスは仮想アドレスにアクセスし、仮想アドレスは物理アドレスに変換される。そののちに、バス上にトランザクションを発行する。一連の流れはすべてハードウェアが実行する。
メモリはページサイズ単位(通常4KB。getconf PAGE_SIZEで見れる。wslの場合は4096つまりは4KBだった)で分割管理される。
アドレスの変換はページテーブルという変換表に登録されるが、この準備はLinuxカーネルが行う。
変換先が不正アドレスだった場合はカーネルがユーザプロセスにシグナルSIGSEGVを送り、プロセスは強制終了される。これがセグメンテーションフォルト。セグフォった状態。
正しい場合、必要に応じて物理メモリが割り当てられる。デマンドページング。
正しいけどスワップされている場合、物理メモリの空きを作ってハードディスクからメモリデータ読み出し(スワップイン、ページイン)、物理メモリに読み込み。これが激しくなると、システム全体が遅くなるスラッシングとなる。

ユーザプロセスはfork()で新規作成できる。このときプロセス間には親子関係が生じる。プロセスAがプロセスBを作成後、プロセスAとBは互いに独立なので、Aが死んでもBは死なないし、Bが死んでもAは死なない。
ユーザプロセスはcloneでスレッド化される。スレッドはユーザプロセスの中に生じるから、スレッドを作成したユーザプロセスCが死ぬと、スレッドも死ぬ。

ソースコードを読むにはvimかemacsが必須。なぜならタグジャンプでソースコードを効率的に追えるから。
vim用のソースのタグを生成するにはctagsが必要。
sudo apt install exuberant-ctags

sudo apt install universal-ctags
で入る。前者は開発停止のため後者がよい。
入ったら、ソースコードが置いてあるディレクトリにcdして、
ctags -R .
とする。
あとはいつでもタグジャンプ可能。
vi -t panic
でタグジャンプ。
:tsで候補選択画面。番号入力してエンターでジャンプ。
vi中は、ctrl + ] でタグジャンプ、ctrl + tで復帰。

タグジャンプできるようになったら、キーワード検索しよう。
タグジャンプは関数定義を検索する。
関数呼び出し場所を調べるにはfind とgrepの組み合わせが高速らしい。

# cat ~/g
find . -name "*.[chS]" -print -type f | xargs grep -n "$1" -dskip /dev/null | more

というようにスクリプト化しておけば、~/g keyword で簡単に検索できるらしい。
もしくは、sudo apt install global でGLOBALを導入すると弁rにらしい。
ソース子0度があるディレクトリでmake gtagsしてタグファイルを作成。
そのあと、 grobal -rx keyword で関数呼び出し箇所を調査可能。

この記事が気に入ったらサポートをしてみませんか?