meta-fedoraなんてものを作り始めた(*ノω・*)テヘ

meta-fedora

はじめに

もし、meta-fedoraと聞いてピンときた場合はその感は当たりですw

主に組み込み向けのLinuxディストリビューションを作るためのものとしてyocto projectがありますね。yoctoプロジェクトのリファレンスディストリビューションとしてpokyがあります。そして、pokyのカスタムレイヤーでDebianのソースパッケージを利用するmeta-debianがあります。

ここで、例えばfedoraとかでもできるんじゃないだろうか?ということでyoctoの勉強も兼ねて作り出したのがmeta-fedoraです。今日の時点ではbusyboxをpokyからmeta-fedoraに置き換えが出来た程度です。busyboxfedoraのsrc.rpmにはパッチが無かったのでfedora用のパッチを当てるdo_fedora_patchタスクは作ったけど特に処理がない状態です。

イデアはもちろんmeta-debianから来てます。なので、基本的な動作もmeta-debianと同じ感じになるようにしました。ソースパッケージに関する情報はrecipes-fedora/sources/*.incファイルに書きます。

fedoraもaarch64版はあるし行けるんじゃね?みたいなところですね。

.incファイル

src.rpmファイルに関する情報を書きます。このファイルは後述するfedora-source.bbclassが作成します。パッケージを追加する場合は空のファイルを作成します。touch recipes-fedora/sources/<source package name>.inc という感じです。

内容はこんな感じです。

# This file is generated by fedora-source.bbclass

LICENSE = "GPLv2"
FPV = "1.28.33.fc30"
FPV_EPOCH = "1"
REPACK_PV = "3.fc30"
PV = "1.28.3"

FEDORA_SRC_URI = " \
    ${FEDORA_MIRROR}/pub/linux/Fedora/fedora/linux/releases/${DISTRO_VERSION}/Everything/source/tree/Packages/b/busybox-1.28.3-3.fc30.src.rpm;name=busybox-1.28.3-3.fc30.src.rpm \
"

SRC_URI[busybox-1.28.3-3.fc30.src.rpm.sha256sum] = "cb03d63ee3867d406786a0e118540900f8bd9d047637501a271e82755291d23f"

ソースパッケージに関する情報はsha256sum-primary.xmlというファイルに書かれているんですが、パッケージのライセンスもこのファイルから取得できるのでライセンス情報も一緒に書いてます。

classes

fedora-source.bbclass

ソースパッケージの情報をrecipes-fedora/sources/*.incに書くためのクラスです。

まずFedoraのミラーサーバからrepomd.xmlを取得します。この時に読み込むファイルは2つ有って、release時のパッケージ情報があるreleasesディレクトリ以下のファイルと、パッケージの更新版が置かれるupdatesディレクトリ以下のファイルの2つがあります。

これらを読んでprimary.xmlファイルのパスを取得します。

次にprimary.xmlを読みます。このときはreleases -> updatesの順番で読んでsrc.rpmファイルのデータを組み立てて行きます。データはパッケージ名をキーとした連想配列で作っていて、同名のパッケージは後から出てきたほうで上書きしてます。なので最初にリリース時点でのファイルを読んで、次に更新版パッケージのファイルを読んでます。2つのファイルのデータをマージするなら、こうしたほうが単純で楽というのがでかいですね。

その次はrecipes-fedora/sourcesディレクトリ以下に有る.incファイルを調べます。ここで見つかったincファイルに対してprimary.xmlで読んだデータを書き込みます。

このクラスは次のように設定していて、レシピのパース時に実行されます。

fedora_source_eventhandler[eventmask] = "bb.event.ParseStarted"

inheritはディストリビューションの設定ファイルのほうで次のようにしています。

INHERIT += "fedora-source"

最低限の機能はできてますが、meta-debianのようにDEBIAN_SOURCE_ENABLEDなどの環境変数を使ったダウンロードの制御はまだありません。

fedora-package.bbclass

こちらはレシピの中でinheritして使うものです。このファイルではunpackの処理を行ったり、fedoraのパッチを当てるなどの処理を行います。 1つハマってとりあえずのハックでしのいだのがあって、bitbakeのunpackタスクはrpmファイルにも対応しています。rpmファイルの展開はrpm2cpioコマンドではなくてrpm2.cpio.shが使われます。ただ、このファイルだとfedora30のsrc.rpmを展開しようとするとエラーになってしまったので(CentOS7のsrc.rpmは大丈夫でした)、同名のファイルを作って、PATHを通してpokyのrpm2cpio.shではなくて自前のrpm2cpio.shが呼ばれるようにしました。自前のrpm2cpio.shはrpmファイルの展開にホストのrpm2cpioコマンドを利用します。そのため、ディストリビューションの設定ファイルに次の設定を追加しました。

HOSTTOOLS += "rpm2cpio"

動作環境

x86-64とaarch64で動作します。

x86-64

[    5.603340] Run /sbin/init as init process
[    5.610576] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
INIT: version 2.88 booting
Starting udev
[    6.165200] udevd[116]: starting version 3.2.7
[    6.226804] udevd[116]: specified group 'kvm' unknown
[    6.290799] udevd[117]: starting eudev-3.2.7
[    6.484025] udevd[117]: specified group 'kvm' unknown
[    7.985039] uvesafb: SeaBIOS Developers, SeaBIOS VBE Adapter, Rev. 1, OEM: SeaBIOS VBE(C) 2011, VBE v3.0
[    8.079341] uvesafb: no monitor limits have been set, default refresh rate will be used
[    8.082405] uvesafb: scrolling: redraw
[    8.111438] Console: switching to colour frame buffer device 80x30
[    8.115704] uvesafb: framebuffer at 0xfd000000, mapped to 0x00000000076c66c4, using 16384k, total 16384k
[    8.116032] uvesafb: fb0: VESA VGA frame buffer device
[    8.200808] EXT4-fs (vda): re-mounted. Opts: (null)
INIT: Entering runlevel: 5
Configuring network interfaces... ip: RTNETLINK answers: File exists
Starting syslogd/klogd: done

Fedy 30 qemux86-64 /dev/ttyS0

qemux86-64 login: root
root@qemux86-64:~# strings /bin/busybox | grep "1.28"
syslogd started: BusyBox v1.28.3
BusyBox v1.28.3 (2019-06-02 13:34:16 UTC)
udhcp 1.28.3
started, v1.28.3
fsck (busybox 1.28.3)
root@qemux86-64:~#

aarch64

[    2.756128] Freeing unused kernel memory: 1024K
[    2.764907] Run /sbin/init as init process
INIT: version 2.88 booting
[    2.883961] usb 1-2: new high-speed USB device number 3 using xhci_hcd
[    3.041754] input: QEMU QEMU USB Keyboard as /devices/platform/4010000000.pcie/pci0000:00/0000:00:02.0/usb1/1-2/1-2:1.0/0003:0627:0001.0002/input/input1
[    3.100694] hid-generic 0003:0627:0001.0002: input: USB HID v1.11 Keyboard [QEMU QEMU USB Keyboard] on usb-0000:00:02.0-2/input0
Starting udev
[    3.240365] IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
[    3.406764] udevd[106]: starting version 3.2.7
[    3.436698] udevd[106]: specified group 'kvm' unknown
[    3.467389] udevd[107]: starting eudev-3.2.7
[    3.621474] udevd[107]: specified group 'kvm' unknown
[    4.426004] EXT4-fs (vda): re-mounted. Opts: (null)
INIT: Entering runlevel: 5
Configuring network interfaces... ip: RTNETLINK answers: File exists
Starting syslogd/klogd: done

Fedy 30 qemuarm64 /dev/ttyAMA0

qemuarm64 login: root
INIT: Id "hvc0" respawning too fast: disabled for 5 minutes
root@qemuarm64:~# uname -a
Linux qemuarm64 5.0.7-yocto-standard #1 SMP PREEMPT Sun Jun 2 14:47:16 UTC 2019 aarch64 GNU/Linux
root@qemuarm64:~# strings /bin/busybox | grep "1.28"
udhcp 1.28.3
started, v1.28.3
syslogd started: BusyBox v1.28.3
fsck (busybox 1.28.3)
1.28.3
BusyBox v1.28.3 (2019-06-02 14:54:32 UTC)
root@qemuarm64:~#

開発環境

pokyのブランチはwarriorを使ってます。ホストのディストリビューションfedora 30ですが、ビルドにはfedora 28のdockerイメージを使ってます。マニュアルをよくよく見たらyocto 2.7だとfedora 29もサポートしていた。

meta-fedoraで使用するfedoraのソースはfedora 30のsrc.rpmを使っています。

todo

まだfedora用パッチを当てるロジックがないので追加しないとダメっす。あとはcore-image-minimalに入るパッケージをfedora化していったり、initシステムはsystemdに切り替えようとか。。。 fedoraはsysvinitのパッケージはもう無いのでcore-image-minimalを完全にfedoraのソースに置き換えるにはinitシステムをsystemdに変えるしかないんですよね。

Linuxカーネル4.1の名前空間(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

名前空間

名前空間とは、Linuxカーネル内のグローバルなリソースを管理する機構です。名前空間によって管理されるリソースはメモリやCPUなどの物理的なリソースではなく、プロセスID、ユーザID、ファイルシステムのマウントポイントなどのデータです。CPUやメモリなど、ハードウェアよりの制限は「XXX章、cgroups」にて解説します。

Linuxにおけるコンテナ型仮想化技術では名前空間を利用し、ホスト・ゲスト間やゲスト間での環境の分離を行っています。これによりコンテナの独立性を確保しています。名前空間の機能はコンテナ型仮想化で使われることが主ですので、以下の説明でもコンテナ型仮想化を行う前提として、親プロセスから名前空間を分離した子プロセスをコンテナと呼びます。

名前空間の利点

名前空間を使用して環境を分離することで、ある名前空間に所属するプロセスの処理内容が名前空間が違う別のプロセスに対して影響を及ぼさないようにすることができます。これによりプロセス間の独立性を高めたり、 セキュリティの向上に役立てることができます。

例えば、「図_PID名前空間の分離例」のようにPID名前空間AとBがあるときに間違ってPID名前空間Bにて kill -kill 4 を実行したとしても、PID名前空間Bには該当するプロセスは存在しないのでコマンドからエラーが変えるだけでPID名前空間A、Bともにプロセスに対してなんの影響も発生しませせん。また、PID名前空間Bにて kill -kill 2 を実行した場合は、PID名前空間BのPID2のプロセスが終了するだけで、PID名前空間AのPID2には影響はありません。名前空間を分けることでこのようにプロセス間の独立性を高めることができます。

f:id:masami256:20190510232224p:plain

図_PID名前空間の分離例

また、User名前空間の機能を利用して、User名前空間Aでのuid 0をホスト上のuid 1000とマッピングすることで、User名前空間Aの中ではrootとして処理を行いつつも、ホスト上では一般ユーザとして処理を行うため、システムに対してクリティカルな変更を名前空間内では行えないようにすることができます。この機能はプロセスを実行する上でroot権限が必要な場合で、ホストの環境には影響を及ぼさないような処理を実行する場合、例えば、rpmなどのパッケージ作成、ホストから分離したMount名前空間内でのパッケージインストールなどに有用です。その他に、万が一、名前空間内のソフトウェアの脆弱性により任意のシェルが実行されたとしても、ホストからは一般ユーザの権限でしかコマンドを実行できないので、システム全体の制御は奪われにくくなります。

名前空間の種類

Linuxでの名前空間は執筆時点で6つのリソースを管理しています(表_名前空間一覧)。名前空間の実装は、Mount名前空間が最初に実装され、その後も継続的に開発が進み、Linux 3.8でLinux 4.0でも使われている機能が揃いました。

名前空間 使用可能になったバージョン
Mount名前空間 Linux 2.4.19
IPC名前空間 Linux 2.6.19
UTS名前空間 Linux 2.6.19
Net名前空間 Linux 2.6.29
PID名前空間 Linux 2.6.24
User名前空間 Linux 3.8

表_名前空間一覧

Mount名前空間

Mount名前空間ファイルシステムのマウントポイントを管理します。Mount名前空間を分離することで同じストレージ上のファイルシステムであってもプロセス間で別のファイルシステム階層として扱うことができます。これにより、プロセスAがファイルシステムに対して行った変更がプロセスBには影響しないという使用方法が可能になります。chroot(2)ではあるディレクトリをルートファイルシステムとして設定しますが、Mount名前空間によるマウントポイントの管理はchroot(2)とは違い、システムのルートファイルシステムそのものが管理対象となります。

IPC名前空間

IPC名前空間はSystem V IPC オブジェクト、POSIX メッセージキューを管理します。これらのIPCリソースは同一の名前空間にあるリソースに対して通信を行うことができますが、別の名前空間にあるリソースとは通信できません。

UTS名前空間

UTS名前空間はホスト名とNIS ドメイン名を管理します。カーネルバージョンやOS名などは変更できません。

Net名前空間

Net名前空間はネットワークに関するリソースを管理します。この名前空間で管理されるリソースはネットワークデバイスIPv4IPv6プロトコルスタックなどです。NICは1つの名前空間にのみ所属させることができます。そのため、一つのNICを複数の名前空間から使用する場合は仮想ネットワークデバイス(veth)にて別の名前空間へのネットワークブリッジを作成し、このブリッジを経由する必要があります。

PID名前空間

PID名前空間はPIDの管理を行います。この機能を使うことでコンテナ内のプロセス番号はホスト側のプロセス番号と独立することができます。PID名前空間を分離してプロセスを作成した場合でも、分離元となったPID名前空間からはそのPID名前空間の番号体系でプロセスを識別できます。「図_PID名前空間例」はPID名前空間Aを大元として、PID名前空間BとPID名前空間Cが存在する状態です。 ここでは、PID名前空間Bのpid 10はPID名前空間Aではpid 1000となっています。さらにPID名前空間Bのpid 10はPID名前空間Dのpid 1でもあります。PID名前空間Cのpid 10はPID名前空間Aではpid 2010です。例のようにPID名前空間が違えば同じpid番号が存在します。ただし、分離元のPID名前空間上ではユニークなPIDが割り当てられるため、同一名前空間内でのPID番号の重複は発生しません。

f:id:masami256:20190510232310p:plain

図_PID名前空間

カーネル起動時に設定され最初のPID名前空間以外でreboot(2)が実行された場合、カーネルの再起動は行わず、PID名前空間内でのinitプロセスに対してシグナルを送信します。送信するシグナルはreboot(2)のcmd引数の値によって「表_rebootのcmdとシグナル」のように変化します。

cmd 送信するシグナル
LINUX_REBOOT_CMD_RESTART SIGHUP
LINUX_REBOOT_CMD_RESTART2 SIGHUP
LINUX_REBOOT_CMD_POWER_OFF SIGINT
LINUX_REBOOT_CMD_HALT SIGINT

表_rebootのcmdとシグナル

User名前空間

User名前空間はセキュリティに関するリソースを管理します。ここで管理するものとしてはUID、GIDなどがあります。User名前空間の機能でホスト側のuid・gidとコンテナゲスト内で使用するuid・gidマッピングを行うことがきます。コンテナ内のuid 0をホストのuid 1000とマッピングすることで、コンテナ内での操作がホスト上ではuid 1000の権限で行うようにすることができます。これにより、コンテナゲスト内ではroot権限で操作を行うことができますが、ホスト側から見た場合にはroot権限ではないためホストの設定を変更するような操作が行えず、セキュリティの向上につなげることができます。

名前空間の管理

Linuxでは、名前空間は個別に存在していますが、User名前空間以外の名前空間はNSProxyによって管理されています。これらの名前空間を操作する場合はNSProxyを経由して処理を行います。User名前空間に関してはセキュリティ機構の一つであるcredentials機能が管理します。User名前空間は初期の実装ではNSProxyによって管理されていましたが、Linux 2.6.39よりstruct cred構造体にて管理するようになりました。NSProxyとCredentials機能はプロセスのtask_struct構造体より参照できます。図「図_task_struct構造体と名前空間の関連」にtask_struct構造体と各名前空間の関連を示します。User名前空間以外はNSProxyの管理下に置かれます。

f:id:masami256:20190510232513p:plain

図_task_struct構造体と名前空間の関連

図「図_task_struct構造体と名前空間の関連」に示したように、User名前空間以外の名前空間はUser名前空間を参照します。PID、IPC、MNT、PID、UTS名前空間はそれぞれ独立して存在できますが、User名前空間はセキュリティに関する名前空間のため、他の名前空間からも参照する必要があるためです。各名前空間がUser名前空間を参照するのは、主にsetnsシステムコールによりプロセスの名前空間を別の名前空間に所属させるときです。この場合、プロセスに設定されているケーパビリティと所属対象の名前空間に設定されているケーパビリティにCAP_SYS_ADMINが設定されているかをチェックします。

プロセスと名前空間

図_プロセスと名前空間はUTS名前空間を例にプロセスと名前空間がどのように関連しているかを示しています。例ではホスト名がfooで設定されているpid1001とpid1002、ホスト名がbarで設定されているpid1003があります。 これらはpid1000を親プロセスとしています。この図ではpid1001とpid1001が同じ名前空間に所属し、pid1003が別の名前空間に所属しています。簡単にするためにここではpid1000の名前空間に関しては省略しています。

f:id:masami256:20190510232337p:plain

図_プロセスと名前空間

名前空間のエクスポート

名前空間は擬似ファイル表現され、procファイルシステムにエクスポートされます。これらのファイルは/proc//nsに存在します。ファイルはシンボリックリンクとなっていて、リンク先のinode番号によってどの名前空間に所属しているかが確認できます。図_procファイルシステムの場合、pid 1とpid 2は同一の名前空間に所属しています。そのため、リンク先のinode番号は同一の番号を指しています。

# ls -la /proc/1/ns
total 0
dr-x--x--x 2 root root 0 May 27 22:16 ./
dr-xr-xr-x 8 root root 0 May 27 22:12 ../
lrwxrwxrwx 1 root root 0 May 27 22:17 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 May 27 22:17 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 May 27 22:17 net -> net:[4026531969]
lrwxrwxrwx 1 root root 0 May 27 22:17 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 May 27 22:17 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 May 27 22:17 uts -> uts:[4026531838]

# ls -la /proc/2/ns
total 0
dr-x--x--x 2 root root 0 May 27 22:17 ./
dr-xr-xr-x 8 root root 0 May 27 22:12 ../
lrwxrwxrwx 1 root root 0 May 27 22:17 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 May 27 22:17 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 May 27 22:17 net -> net:[4026531969]
lrwxrwxrwx 1 root root 0 May 27 22:17 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 May 27 22:17 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 May 27 22:17 uts -> uts:[4026531838]

図_procファイルシステム

ユーザ空間にエクスポートされた名前空間のファイルを操作するにはsetns(2)を使用します。これらのファイルに対して読み書きを行うことはありません。カーネル側ではこれらのファイルに対する操作の種類はproc_ns_operations構造体にて定義します(表_proc_ns_operations構造体)。

変数名 内容
name ファイル名
type 名前空間のCLONEフラグ
get 名前空間の参照カウントを増やす関数へのポインタ
put 名前空間の参照カウントを減らす関数へのポインタ
install 新しい名前空間を設定する関数へのポインタ

表_proc_ns_operations構造体

「表_proc_ns_operations構造体」で示したように、定義されている操作(関数)は3個ありますが、このうち、putとinstallが名前空間の操作に関する関数です。put関数はプロセスの終了などにより、その名前空間の参照カウンタを減らす際に使用します。この処理は名前空間毎に行うため、参照数を減らしたあとの処理、例えば、リソースの解放を行う、参照が0になった時点でリソースの解放を行うなどは名前空間によって異なります。get関数は名前空間への参照が増える場合に実行されます。get関数はput関数と違い、どの名前空間でも同じように参照カウンタを増やすだけで名前空間毎固有の処理はありません。

nsfs

nsfsは名前空間のファイルをprocファイルシステムにエクポートするための擬似ファイルシステムです。元来、名前空間のエクスポートは単純にprocファイルシステムを利用してエクポートしていましたが、カーネル3.19よりnsfsというファイルシステムとしてエクスポートするようになりました。この変更はカーネル内部での変更なのでABIに変化はありません。ファイルは3.19以前と同様に/proc以下にエクポートします。 nsfsはファイルシステムですがext4やbtrfsなどの通常のファイルシステムと違い、ユーザ空間からのマウント処理は行われません。ファイルシステムのマウントはカーネルの起動時に初期化のタイミングでstart_kernel関数よりnsfsのマウント処理を実施します。また、カーネルに対してファイルシステムの登録を行わないため、/proc/filesystemsファイルにもnsfsは現れません。 nsfsの主な機能は/proc//ns/uts等の名前空間のファイルにアクセス時に適切なdentryを返すことと、ファイルディスクリプタからfile構造体に取得することの2機能です。 dentryの取得はns_get_path関数が行います。対象のファイルに対してns_get_path関数が始めて呼ばれた時点では、ファイルに対してinodeは割当されていません。このため、初回アクセス時に限りinodeの割当を行い、その後にdentryを返します。ファイルディスクリプタからfile構造体への取得はproc_ns_get_path関数が行います。

名前空間共通データ

すべての名前空間で持つデータ構造にns_common構造体があります。この構造体は表「表_ns_common構造体」に示した変数を持ちます。stashedメンバ変数とinumメンバ変数はprocファイルシステムにエクスポートしたファイルのデータです。

変数名 内容
stashed dentry構造体のアドレス
ops /procにエクスポートしたファイルの操作する関数群
inum inode番号

表_ns_common構造体

「図_uts_namespaceからproc_ns_operationsの関連」にUTS名前空間を例に、ns_common構造体、proc_ns_operations構造体の関連を示します。

f:id:masami256:20190510232551p:plain

図_uts_namespaceからproc_ns_operationsの関連

ns_common構造体はinode構造体のi_private変数に設定されています。setns(2)による名前空間の移動時はsetns(2)の引数で渡されたファイルディスクリプタからinode構造体を取得し、その構造体からi_private変数にアクセスします。

NSProxy構造体

NSproxyは、User名前空間以外の名前空間を管理する機能です。User名前空間はcred構造体が管理しています。PID名前空間などの名前空間の分離を行わない場合は、task_struct構造体のnsproxy変数を親プロセスと共有し、分離を行う場合に新規作成します。個々の名前空間はNSProxy構造体のメンバ変数として表されます。

変数名 内容
count このnsproxyの参照カウンタ
uts_ns UTS名前空間の構造体
ipc_ns IPC名前空間の構造体
mnt_ns Mount名前空間の構造体
pid_ns_for_children PID名前空間の構造体
net_ns Net名前空間の構造体

表_nsproxy構造体のメンバ

NSProxyと名前空間

参照カウンタ

NSProxyと各名前空間はデータ構造として参照カウンタを持っており、この参照カウンタを使用し、参照数が0になったらリソースの解放を行うなどの処理を行います。NSProxyの参照カウンタはプロセスの生成時に名前空間の分離を行わなかった場合に増やされます。各名前空間の参照数を増やすパターンは2つあり、1つはプロセス生成時に名前空間を分離する場合に、分離しない名前空間に関して参照数を1増やします。2つめのパターンはsetnsシステムコールを実行した時で、所属する名前空間の参照数を増やします。

UTS、IPC、MNT、NET、PID名前空間は自身が所属するUser名前空間を参照します。これらの名前空間は、新規に名前空間を作成するときに参照しているUser名前空間の参照カウンタを1つ増やします。名前空間は参照カウンタにより名前空間が使用されているかチェックしているため、参照数が0になったら使用していたリソースを解放します。

NSProxyと名前空間の関連

プロセス生成を行う関数(fork、vfork、clone)を実行した場合、copy_process関数よりcopy_namespaces関数が呼ばれます。copy_namespaces関数はflasgを確認し、User名前空間を除く名前空間の分離が指定されていない場合(CLONE_NEWUTSなど、名前空間のフラグが設定されていない)、子プロセスは親プロセスのnsproxy構造体を参照します。この場合、count変数をインクリメントし参照数を増やします。 名前空間を分離する場合、まずNSProxy構造体の新規作成を行いcount変数を1に設定します。その後、各名前空間の分離、もしくは名前空間の参照数を増やします。名前空間とNSProxyの関係は、ある名前空間から見た場合、1:nの関係になります。NSProxyと各名前空間の関連の例を「図_NSProxyと名前空間」に示します。

f:id:masami256:20190510232831p:plain

図_NSProxyと名前空間

「図_NSProxyと名前空間」ではPID 1000とPID 2000はともにPID 1から派生したが、UTS名前空間の分離を行った状態を表しています。PID 1とPID 1000はNSProxyを共有しているため、参照カウントは2となります。このNSproxyが管理しているUTS名前空間はPID 1とPID 1000が参照しているため、countは2になります。PID名前空間はPID 1とPID 1000の他、PID 2000からも参照されているためcountは3となります。この図のように、プロセスがある名前空間を分離した時、分離を行わなかった名前空間に関しては既存の名前空間を参照し続けます。

名前空間へのアクセス

名前空間へのアクセスするには2種類のパターンがあります(図_名前空間へのアクセス)。1つはカーネル空間からアクセスする場合で、この場合はtask_struct構造体からアクセスできます。もう1つはユーザ空間からアクセスする場合で、これはsetns(2)を使用した場合になります。

f:id:masami256:20190510232848p:plain

図_名前空間へのアクセス

task_structから参照する場合、User名前空間へはcred構造体のreal_credメンバ変数からアクセスが可能です。その他の名前空間へはnsproxyメンバ変数よりアクセスが行えます。

    struct task_struct *p = current;
    p->real_cred-_>user_ns = new_user_ns;

図_cred構造体からアクセス

    struct task_struct *p = current;
    p->nsproxy->uts_ns = new_uts_ns;

図_nsproxyからの名前空間へアクセス

/procにエクスポートされたファイルからアクセスする場合、task_struct構造体から参照が行えません。このため、操作対象名前空間のファイルのinodeを取得し、ここからproc_ns_operations構造体を経由して対象の名前空間にアクセスを行います。UTS名前空間へアクセスする場合のフローを「図_UTS名前空間へのアクセス」に示します。

sys_open() ------------------------------------------+
  -> do_sys_open()                                   |
    -> do_filp_open()                                |- vfs層
      -> path_openat()                               |
        -> follow_link() ----------------------------+
          -> proc_ns_follow_link() ------------------- proc filesystem層
            -> ns_get_path() ------------------------- nsfs層
        -> utsns_get() ------------------------- uts namespace層

図_UTS名前空間へのアクセス

/procにエクスポートされたファイルから名前空間へアクセスする場合、「図_UTS名前空間へのアクセス」のようにvfs層からprocfs層、nsfs層を経由し、対象の名前空間へとアクセスします。proc_ns_follow_link関数までの処理で、ファイルのdentry構造体を取得し、proc_ns_follow_link関数でdentry構造体からinode構造体を取得。そして、inode構造体からPROC_I関数を使用してproc_ns_operations構造体を取得します。

デフォルトの名前空間

カーネルが作成するプロセス「init_task」が使用する名前空間がデフォルトの名前空間です。nsproxy構造体はinit_nsproxyという名前の変数で、カーネルコンパイル時に「表_init_nsproxy」に示した内容が設定されます。

変数名 内容
count 1
uts_ns init_uts_ns
ipc_ns init_ipc_ns
mnt_ns NULL
pid_ns_for_children init_pid_ns
net_ns init_net

表_init_nsproxy

Mount名前空間は実際にカーネルが起動するまではデータの設定を行えないのでNULLが設定されますが、その他の名前空間はデフォルトの設定が使用され、名前空間の分離をするまでは子プロセスへと引き継がれていきます。 init_nsproxyはデフォルトの名前空間となっています。特にPID名前空間の場合、全てのPID名前空間のルートがこのinit_nsproxyのPID名前空間となります。 User名前空間はinit_user_nsという名前の変数で定義されています。この変数はデフォルトのcred構造体のuser_ns変数に設定されます。このcred構造体もinit_taskで使用します。 Mount名前空間以外の各名前空間のデフォルト値はcファイルにて定義されています。

名前空間 ファイル
UTS init/version.c
IPC ipc/msgutil.c
PID kernel/pid.c
Net net/core/net_namespace.c
User kernel/user.c

名前空間の共有・分離・移動

通常プロセスは、作成時に親プロセスの名前空間を引き継ぎます。プロセスが親プロセスから名前空間を分離させるには、「表_名前空間を操作するシステムコール」に示したシステムコールを使用します。clone関数システムコール発行時にCLONE_NEWPIDなどの名前空間用フラグを用いて、プロセス生成時に分離させる、もしくはプロセス作成完了後にunshare関数システムコールを用いて親プロセスの名前空間から分離させる、またはsetns関数システムコールにより既存の名前空間に所属させることができます。cloneシステムコールとunshareシステムコールでは「表_名前空間のCLONEフラグ」に示したフラグを用いて分離させたい名前空間を指定します。setnsシステムコールでは、CLONEフラグの他、所属させる名前空間のファイルディスクリプタを引数として受け取ります。ここで渡すファイルディスクリプタは/proc/ns配下に存在する名前空間のファイルのファイルディスクリプタです。setnsシステムコール名前空間を 変更した場合、その効果はPID名前空間を除き、すぐにプロセスに反映されます。PID名前空間変更した場合は、名前空間変更後、そのプロセスがforkやcloneなどのシステムコールで作成した子プロセスから反映されます。

システムコール 機能
clone プロセスの生成時に名前空間を分離する
unshare プロセスの名前空間を分離する
setns プロセスの名前空間を他のプロセスの名前空間に所属させる

表_名前空間を操作するシステムコール

フラグ名 内容
CLONE_NEWIPC IPC名前空間の分離
CLONE_NEWNET NET名前空間の分離
CLONE_NEWNS MOUNT名前空間の分離
CLONE_NEWPID PID名前空間の分離
CLONE_NEWUSER USER名前空間の分離
CLONE_NEWUTS UTS名前空間の分離

表_名前空間のCLONEフラグ

名前空間の共有と複製
fork(2)

fork(2)による名前空間の共有の例を説明します。ここでは2つのプロセスPID1234とPID 1192があり、これらの親プロセスはPID784と同じですが、名前空間は別となっています。「図_fork後の名前空間」では、pid1326は親プロセスと名前空間を共有しているため、同じ名前空間に対して線が繋がっています。

f:id:masami256:20190510233013p:plain

図_fork後の名前空間

clone(2)

clone(2)システムコールはプロセスを作成するための機能です。プロセスの生成はfork(2)と同様の方法で行いますが、より細かい制御が行えます。プロセス生成時にflags引数に 表_名前空間のCLONEフラグ で示した CLONE で始まるフラグを設定することで子プロセスの設定が可能です。CLONEフラグのうち、名前空間に関するうフラグは表CLONEフラグで示すものがあり、フラグで指定された名前空間を親プロセスから分離します。

clone(2)ではflagsで指定した名前空間のみ新規に作成し、それ以外の名前空間は親プロセスと共有します。「図_clone後の名前空間」ではcone(2)にてNet名前空間のみを分離した時の状態です。Net名前空間は親プロセスと別の名前空間を使用しますが、それ以外は共有します。

f:id:masami256:20190510233037p:plain

図_clone後の名前空間

fork(2)・clone(2)時の処理

fork(2)やclone(2)はカーネル内部ではdo_fork関数にて共通化されています。そのため、fork(2)・clone(2)における名前空間 の処理は同じパスを通ります。プロセス生成時のコールフローを図_プロセス生成時のコールフローに示します。 プロセス生成時の名前空間に関する処理はdo_fork関数から呼ばれるcopy_process関数にてユーザ名前空間に対するの処理と、NSProxyに対する処理を行います。

do_fork()
  -> copy_process()                     (1)
    -> copy_creds()                     (2)
      -> prepare_creds()                (3)
      -> create_user_ns()               (4)
        -> set_cred_user_ns()           (5)
    -> copy_namespaces()                (6)
      -> create_new_namespaces()        (7)
        -> create_nsproxy()             (8)
        -> copy_mnt_ns()                (9)
        -> copy_uts_ns()                (10)
        -> copy_ipcs()                  (11)
        -> copy_pid_ns()                (12)
        -> copy_net_ns()                (13)

図_プロセス生成時のコールフロー

(1)のcopy_process関数の最初の処理で親プロセスのtask_struct構造体がこれから作成するプロセスのtask_struct構造体にコピーします。よって、この時点では親プロセス・子プロセスで名前空間を共有した状態になっています。(2)から(5)でUser名前空間に対する処理を行います。CLONE_NEWUSERフラグが設定されていた場合はUser名前空間を新規に作成します。(6)以降はNSProxyとその管理下にある名前空間の参照数の増加または新規作成処理となります。まず(6)のcopy_namespaces関数でフラグを確認します。、名前空間に関するCLONEフラグが設定されていいない場合はNSProxyの参照カウントを増加して関数を終了します。fork(2)の場合は名前空間は親プロセスと共有するためここで名前空間に関する処理は終了となります。clone(2)で名前空間に関するフラグが1つでも設定されていた場合は(7)以降の処理に進みます。まず、名前空間(NSProxy)を親プロセスと共有しないため、(8)のcreate_nsproxy関数でNSProxy構造体のインスタンスを初期化します。 その後、各名前空間のコピーを行う関数*1を順次呼び出していきます。これらの関数の詳細説明は本説では行いません。各名前空間で共通するのはCLONEフラグが設定されていなければ、自身の参照カウントをインクリメントし、CLONEフラグが設定されている場合は新規に名前空間を設定するという処理となります。名前空間の最後の処理は作成中のプロセスのnsproxy構造体の変数を新たに作成したnsproxyに置き換えを行います。これにて作成しているプロセス/スレッドの名前空間が新しい名前空間に切り替わります。

unshare(2)

unshare(2)は名前空間を現在の名前空間から分離し、新規に名前空間を作成します。分離対象の名前空間の指定はclone(2)と同じくCLONEフラグを使用します。「図unshare後の名前空間」ではunshare(2)により、PID1326のNet名前空間を親プロセスから分離して、新規に名前空間を作成した状態です。図_unshare後の名前空間

名前空間の分離処理

名前空間の分離はunshare(2)で行いますが、このシステムコール名前空間以外も操作します。本説では名前空間に関する部分のみ説明します。名前空間分離時のコールフローを 図_名前空間分離のコルーフローに示します。名前空間を分離する場合の主な処理はclone(2)と同じです。大きく違うのは呼び出し元と、名前空間(task_struct構造体のnsproxy変数)を切り替える処理が必要になる点です。

sys_unshare()                            (1)
  -> check_unshare_flags()               (2)
  -> unshare_userns()                    (3)
    -> prepare_creds()                   (4)
    -> create_user_ns()                  (5)
  -> unshare_nsproxy_namespaces()        (6)
    -> create_new_namespaces()           (7)
  -> exit_shm()                          (8)
  -> shm_init_task()                     (9)
  -> switch_task_namespaces()            (10)
    -> free_nsproxy()                    (11)

図_名前空間分離のコルーフロー

(1)のsys_unshare関数は関数を準備呼び出していきます。(2)のcheck_unshare_flags関数はflags引数が妥当かをチェックします。(3)のunshare_userns関数ではCLONE_NEWUSERフラグが設定されているかを確認した後は、prepare_creds関数とcreate_user_ns関数の呼び出しを行います。この処理の流れはfork(2)/clone(2)時のcopy_creds関数と同様です。create_user_ns関数が成功した場合は、関数の引数で渡されたnew_cred変数に作成したcred構造体を設定します。 (6)のunshare_nsproxy_namespaces関数からNSProxy管理下の名前空間の処理になります。最初にflags引数のチェックで分離が必要か確認します。分離を行う場合は分離操作を行う権限があるかを確認し、権限がない場合はエラーを返します。権限に問題がなければ(7)のcreate_new_namespaces関数を呼び出し、作成したNSProxy構造体の変数を引数で渡されたnew_nsp変数に設定します。この時点ではNSProxyの切り替えは行われません。 unshare_nsproxy_namespaces関数が終了したらsys_unshare関数に戻ります。IPC名前空間を分離していた場合、既存のセマフォオブジェクトを(8)のexit_shm関数で解放し、(9)のshm_init_task関数にて再度初期化します。 NSProxy管理下の名前空間を分離した場合は(10)のswitch_task_namespaces関数でプロセスに設定されているNSProxyを作成したNSProxyに差し替えます。 switch_task_namespaces関数はtask_structのロックを取得し、nsproxyを差し替えます。次に元のnsproxyの参照数を減らし、もし他に使用者がいなければnsproyとそれに紐づく名前空間のリソースを(11)のfree_nsproy関数にて解放します。

名前空間の移動
setns(2)

setns(2)は名前空間を既存の名前空間から他の名前空間に移動します。「図_setns後の名前空間」ではsetns(2)により、PID1326のNet名前空間をPID1234の名前空間に変更した状態です。図_setns後の名前空間

名前空間の移動処理
sys_setns()                            (1)
  -> proc_fs_get()                     (2)
  -> get_proc_fs()                     (3)
  -> create_new_namespaces()           (4)
  -> install()                         (5)
  -> switch_task_namespaces()            (6)

図_名前空間移動のコールフロー

名前空間の移動は(1)のsetns(2)により移動先の名前空間のファイルディスクリプタ、移動する名前空間の種類を指定して実行します。まず(2)〜(3)にてsetns(2)の引数で渡されたファイルディスクリプタより、移動先名前空間のns_common構造体を取得します。(4)のcreate_new_namespaces関数で新しくnsproxy構造体を設定します。この時に、setns関数ではcreate_new_namespace関数のflag引数に0を渡して呼び出します。これにより、nsproxy構造体を作成し、ser名前空間を除く全ての名前空間の参照数をインクリメントします。User名前空間はnsproxy構造体の管理対象では無いため、移動する名前空間がUser名前空間の場合はまだ何も設定されていない状態です。次の(5)のns_common構造体のops変数に設定されたinstall関数を実行し、名前空間の移動処理を行います。名前空間移動時に行う処理は各名前空間で違うため、ここでは共通して行われる部分のみ説明します。名前空間の移動では、移動元・移動先の名前空間において、名前空間の移動を行うケーパビリティ(CAP_SYS_ADMIN)があるかのチェックを行い、権限がなければエラーを返します。名前空間を移動するときは、現在の名前空間から抜けるため、移動元の名前空間の参照数を減らす必要があります。これとは逆に、移動先名前空間の参照数を増やす必要があります。この権限チェックと参照カウンタの設定は全ての名前空間において、名前空間の移動時に行われる処理となります。install関数により名前空間の移動処理が完了したら、(6)のswitch_task_namespaces関数でプロセスのnsproxy構造体を入れ替え、名前空間の移動処理を完了します。

Mount名前空間

Mount名前空間ではシステムにマウントするファイルシステムをコンテナ間で分離することができます。コンテナの作成時にMount名前空間を分離した場合、ホスト側でUSBメモリスティックを/mnt配下にマウントしてもコンテナ側の/mntには影響はありません。

Mount名前空間の実装

Mount名前空間はmnt_namespace構造体にて管理されています。

変数名 説明
count この名前空間の参照カウンタ
ns ns_common構造体
root この名前空間におけるルートファイルシステム
list mount構造体のmnt_list変数に繋がるMount名前空間のリスト
user_ns この名前空間が所属するユーザ名前空間
seq マウントがループしないようにするためのシーケンス番号
event マウント/アンマウントのイベント発生回数を記録

表_mnt_namespace構造体

Mount名前空間の初期化

Mount名前空間コンパイル時点ではマウントするファイルシステムの情報が無いため、カーネルのブート時に初期化を行います。初期化の流れは図_Mount名前空間の初期化フローの流れで実行します。名前空間の初期化に関係するのはinit_mount_tree関数とcreate_mnt_ns関数の2関数です。

-> start_kernel()               (1)
  -> vfs_caches_init()          (2)
    -> mnt_init()               (3)
      -> init_mount_tree()      (4)
        -> create_mnt_ns()      (5)

図_Mount名前空間の初期化フロー

create_mnt_ns関数はmnt_namespace構造体のメモリ確保、Mount名前空間のルートファイルシステムを設定を行います。 init_mount_tree関数はcreate_mnt_ns関数を呼び、Mount名前空間インスタンスを生成を行い、init_taskのMount名前空間に作成したmnt_namespace構造体を設定します。そして、参照数を1つ増やしてMount名前空間の初期化処理が完了します。

Mount名前空間の分離

Mount名前空間の分離はcopy_mnt_ns関数により行われます。

create_new_namespaces()        (1)
  -> copy_mnt_ns()             (2)
    -> alloc_mnt_ns()          (3)
    -> copy_tree()             (4
      -> clone_mnt()           (5)

図_Mount名前空間の分離フロー

copy_mnt_ns関数はまずalloc_mnt_ns関数にて新しいmnt_namespace 構造体のインスタンスを生成します。alloc_mnt_ns関数は/proc//ns/mntファイルに使用するinodeの取得、ファイル操作時のオペレーション構造体の設定、参照数の設定などを行います。インスタンスの初期化が終わったらファイルシステムをコピーするためのフラグを設定します。この時、プロセスに設定されているUser名前空間とプロセスが使用しているMount名前空間のUser名前空間が違う場合はCL_SHARED_TO_SLAVEとCL_UNPRIVILEGEDが追加されます。copy_flagsに設定しているフラグの内容を「表_CLフラグ」にて説明します。

フラグ 説明
CL_COPY_UNBINDABLE コピーするマウントポイントにMNT_UNBINDLEが設定されていた場合に、コピーを行わない
CL_EXPIRE マウントポイントの期限切れを管理するリストに登録する
CL_SHARED_TO_SLAVE マウントポイントを複製する時に制限をかける(共有サブツリー機能)
CL_UNPRIVILEGED MS_NOSUIDやMS_RDONLYなどのフラグの設定変更を許可しない

表_CLフラグ

マウントしポイントのコピーは2段階で行われます。最初のステップではclone_mnt関数とcopy_tree関数にて行います。copy_tree関数のインターフェースを表_copy_treeの引数に示します。

引数名 説明
old 現在のMount名前空間に設定されているルートファイルシステム
root oldのディレクトリエントリ
flags 先の手順で設定したcopy_flags

表_copy_treeの引数

copy_tree関数は最初にclone_mnt関数を呼び、mount構造体のインスタンスを作成と引数のflagsの値に応じてmount構造体を設定します。ここではルートファイルシステムを設定します。その後、マウントポイントを走査して、コピーすべきマウントポイントがあればclone_mnt関数を呼びmount構造体を作成し、最初に作成したmount構造体のインスタンスのリスト(mnt_list変数)につなぎます。これを全てのマウントポイントに対して行っていきます。マウントポイントをコピーしない条件はcooy_mnt_ns関数で設定したflagsにCL_COPY_UNBINDABLEが設定されいている場合と、マウントポイントにMNT_UNBINDABLEが設定されている場合です。copy_tree関数にてファイルシステムツリーのコピーが完了したらcopy_mnt_ns関数に戻り、2段階目の処理に入ります。この処理ではcopy_tree関数で作成したmount構造体に対して所属する名前空間の設定を行います。また、マウントポイントの参照数のインクリメントも行います。

IPC名前空間

IPC名前空間はInter Process Communication(プロセス間通信)のうち、System V IPC オブジェクトと、POSIX メッセージキューを分離します。これらの仕組みを使ってプロセス間通信を行う場合、プロセスは同一のIPC名前空間に所属している必要があります。IPC名前区間が管理する機能はセマフォ、System V メッセージキュー、共有メモリ、POSIXメッセージキューです。

IPC名前空間の実装

IPC名前空間はipc_namespace構造体にて管理されています。

変数名 説明
count この名前空間の参照カウンタ
ids セマフォを管理する配列
sem_ctls SEMMSL,SEMMNS,EMOPM,SEMMNIを表す配列
used_sems 作成済みセマフォ
msg_ctlmax SystemVメッセージの最大サイズ
msg_ctlmnb SystemVメッセージキューが保持できるメッセージの最大値
msg_bytes SystemVメッセージキューのサイズ
msg_hdrs SystemVメッセージキュー数
shm_tot 確保された共有メモリ数
shm_ctlmni 共有メモリセグメントの最小サイズ
shm_rmid_forced 1を設定した場合、使用者がなくなれば全てのSystemV共有メモリセグメントに破棄済みマークを設定する
ipcns_nb notifier chain
mq_mnt mqueuefsのマウントデータ
mq_queues_count POSIXメッセージキュー数
mq_queues_max POSIXメッセージキューの最大値
mq_msg_max 1つのキューに入れることができるメッセージの最大数
mq_msgsize_max メッセージの最大サイズ
mq_msg_default mq_open関数呼び出し時にattrをNULLに指定した場合のmq_maxmsgのデフォルト値
mq_msgsize_default mq_open関数呼び出し時にattrをNULLに指定した場合のmq_msgsizeのデフォルト値
user_ns IPC名前空間が所属するユーザ名前空間
ns ns_common構造体

表_ipc_namespace構造体

ipc_ids構造体(表_ipc_ids構造体)はセマフォを管理するに使用します。

変数名 説明
in_use 割り当てたIPC識別子数
seq IPCオブジェクト生成のシーケンス番号
rwsem IPC名前空間を操作するときに使用するセマフォ
ipcs_idr IPCオブジェクトのIDを管理
next_id IPDオブジェクト作成時に設定するID

表_ipc_ids構造体

IPC名前空間の初期化

PID1に設定されるIPC名前空間はinit_ipc_nsで、コンパイル時に図_init_ipc_nsのように設定が行われます。

struct ipc_namespace init_ipc_ns = {
        .count          = ATOMIC_INIT(1),
        .user_ns = &init_user_ns,
         .ns.inum = PROC_IPC_INIT_INO,
#ifdef CONFIG_IPC_NS
        .ns.ops = &ipcns_operations,
#endif
};

図_init_ipc_ns

ただし、この段階ではIPC名前空間に関する部分の初期化だけで、メッセージキューなどの初期化は行われません。これらの初期化はLinuxカーネルの起動時により詳細な設定が行われます。IPCのリソースの初期化はipc_init関数にて行います。

ipc_init()              (1)
 -> sem_init()          (2)
 -> msg_init()          (3)
 -> shm_init()          (4)

セマフォsem_init関数にて初期化を行います。まずipc_namespace構造体のsc_semctls変数、セマフォの使用数、セマフォが使用するipc_ids構造体の初期化を行います。その次に/proc/sysvipc/semファイルを作成します。メッセージキューはmsg_init関数にて初期化します。この関数もセマフォと同様に変数の初期化と/procファイルシステムにファイル(/proc/sysvipc/msg)を作成を行います。共有メモリの初期化はshm_init関数にて行いますが、メッセージキューやセマフォと違い、shm_init関数では/proc/sysvipc/shmファイルの作成のみを行います。変数の初期化は別途ipc_ns_init関数にて行います。

UTS名前空間

UTS名前空間ではunameシステムコールが返すデータのうち、nodenameを名前空間毎に設定することができます。カーネルのバージョンやCPUアーキテクチャなど、カーネルが必要とする項目は扱えません。名前空間の分離を行うときは既存の名前空間のデータを引き継ぎます。

UTS名前空間の実装

UTS名前空間のデータ構造は比較的シンプルで名前空間に必要な最低限の構造となっています。

変数名 説明
kref 参照カウンタ
name ホスト名、バージョンなどを管理
user_ns 所属するユーザ名前空間
ns ns_common構造体

表_uts_namespace構造体

UTS名前空間の作成

clone(2)やunshre(2)などではUTS名前空間を新規作成しますが、UTS名前空間名前空間固有の処理はありません。そのため、copy_utsname関数では/proc//ns/utsファイルのinode取得や参照カウンタの初期化といった全ての名前空間で共通な処理を行い、最後にカレントスレッドに設定されているuts_namespace構造体の参照数を1つ減らします。カレントスレッドのuts_namespace構造体と新しいuts_namespace構造体の入れ替えはcreate_new_namespaces関数が行います。

UTS名前空間の移動

UTS名前空間のインストール関数は現在の名前空間と移動先の名前空間でCAP_SYS_ADMIN権限を持っているかチェックします。次に移動先の名前空間の参照カウンタをインクリメントし、移動前の名前空間の参照カウンタをデクリメントします。そして、nsproxyのUTS名前空間を差し替えて終了します。NSProxyが管理する名前空間は概ね同様の処理を行います。

Net名前空間

Net名前空間ではネットワーク関する設定、例えば、IPv4/IPv6プロトコルスタック、ルーティングの他にsocketが使用するポート番号、仮想ネットワークデバイス機能(veth)などがあります。名前空間を分離するときは既存の設定を引き継がず、新規にデータが作成されます。そのため、名前空間の分離が完了した時点ではloを含めてネットワークデバイスは一切存在しませんので、必要なデバイスをipコマンドなどで設定する必要があります。

Net名前空間の実装

net構造体がNet名前空間を表現します。この構造体に名前空間を管理するためのデータやプロトコルなどのネットワーク機能のデータが含まれます。

変数名 説明
count このNet名前空間の参照数
list 作成したNet名前空間を保持するリスト
user_ns Net名前空間に設定するユーザ名前空間
netns_ids Net名前空間のID
ns ns_common構造体
proc_net /proc/netディレクトリのデータ
proc_net_stat /proc/net/statディレクトリのデータ
gen 汎用のデータを保存するための構造体

表_net構造体

また、機能ごとに任意のデータを設定するための汎用データ構造体としてnet_generic構造体があります。net_genric構造体の内容を表_net_generic構造体に示します。この構造体を配列として扱いますが、実際に配列となるのは変数ptrです。ptrはサイズが1の配列として宣言されていますが、メモリを確保するときにnet_generic構造体のサイズ+(sizeof(void *) * データ数)のようにメモリを確保し、6番目のデータにアクセスする際はptr[5]のようにアクセスします。net_generic構造体はその名前が示す通り汎用の構造体です。net構造体を使用するモジュールがプライベートなデータを設定して使用することができます。名前空間の作成時に要素13の配列としてメモリ確保しますが、足りなくなった場合は新たにメモリを確保し、既存のデータをコピー。そしてnet->genのポインタを新規にメモリ確保した変数に置き換えます。

変数名 説明
len 配列の長さ
rcu rcuによるロックの取得に使用
ptr 実際のデータを格納する配列

net_generic構造体

Net名前空間の管理

Net名前空間を作成した場合、作成した名前空間のデータはnet_namespace_listというリストに登録します。このリストは次節で説明するコンストラクタやデストラクタを実行する際など、全てのNet名前空間に対して処理を行う場合に使用します。net_namespace_listへの登録にはnet構造体のメンバ変数listを使用します。

ネットワーク機能のコンストラクタ・デストラク

ネットワークデバイスプロトコル、ネットワーク機能のモジュールはNet名前空間の初期化・終了時に呼ばれるコンストラクタ・デストラクタを設定することができます。設定にはpernet_operations構造体を使用します。pernet_operations構造体の内容を表_pernet_operationsに示します。

変数名 説明
list linked list
init コンストラク
exit デストラク
net_exit_list デストラク
id id
size size

表_pernet_operations

この構造体うち、idやsizeなどは設定しなくても問題ありません。例えば、IPv4TCPではinitとexitのみが使用されます。この構造体は各モジュールよりregister_pernet_device関数もしくはregister_pernet_subsys関数により登録を行います。いずれの場合もpernet_listというリンクリストに登録します。登録時にはリンクリストへの登録と、init関数が設定されている場合は、作成されている全てのNet名前空間に対してinit関数を実行します。

グローバルなNet名前空間の設定

グローバルな名前空間(init_nsproxy)に設定するNet名前空間のデータ(init_net)は、コンパイル時には双方向リンクリストの初期化しか行われません。そのため、各種の初期化はカーネルの起動時にnet_init_init関数にて行われます。この初期化処理ではNet名前空間の終了時に使用するデータクリーンアップ用のリストの初期化、net構造体のメモリ確保時に使用するSLABキャッシュの作成、net_genric構造体の初期化などを行います。また、Net名前空間の初期化と終了時に呼ばれるコンストラクタ・デストラクタを登録します。コンストラクタではNet名前空間を操作するためのproc_ns_operations構造体の設定と、/proc/<pid>/ns/netに割り当てるinodeの設定を行います。デストラクタではコンストラクタで設定したinodeを解放します。

Net名前空間の作成と分離

Net名前空間の作成はcopy_net_ns関数にて行います。作成処理では最初にnet構造体とnet->gen変数のメモリ確保を行います。net->genはnet_genric構造体です。参照カウンタの設定やユーザ名前空間の設定を行ったら、現在作成中のNet名前空間に対してpernet_listに設定されているモジュールのinit関数を実行します。最後に作成したNet名前空間をnet_namespace_listに登録します。 Net名前空間を分離する場合は、新たにNet名前空間を作成するため、処理としては作成時と同様になります。

Net名前空間の移動

Net名前空間の移動ではNet名前空間独特の処理はありません。現在のスレッドのnsproxyに設定されているNet名前空間の参照を減らし、移動先のNet名前空間のnet構造体をnsproxy構造体のnet_nsに設定します。

PID名前空間

PIDは名前空間はPIDを管理します。プロセスIDの管理を分離し、独立させることでコンテナを別のサーバに移行しても、コンテナ内で動作していたプロセスのPIDには影響が置きません。もし、PID名前空間を使用しない場合、コンテナ内のPID 23414が別のサーバに移行した時に同じPIDが使用されていたら別のPIDを振り直す必要があります。しかし、LinuxにはPIDを変更する機能はありませんので、プロセスを終了し、再度起動させなくてはなりません。PID名前空間を分離させることでこのような問題を解決することができます。しかし、プロセスの情報は/procにエクスポートされるため、PID名前空間を分離する場合、/procも適切に分離を行わないと正しく動作できません。PID名前空間の分離は、分離前のPID名前空間を親として階層構造を作成します。この階層構造はinit_taskのPID名前空間を起点として32段階までと制限されています。

PID名前空間の実装

PID名前空間はpid_namespace構造体にて管理されています。

変数名 説明
kref リファレンスカウンタ
pidmap PIDを管理するビットマップ
rcu pid構造体のロック
last_pid 最後に使用したPID
nr_hashed PID構造体を管理するハッシュテーブルのデータ数
child_reaper ゾンビプロセスを回収するためのプロセス。init_taskを設定する
pid_cachep pid構造体のスラブキャッシュ
level PID名前空間の階層
parent 親のPID名前空間
proc_mnt procファイルシステム
proc_self procファイルシステムのselfファイル
bacct BSDプロセスアカウンティング情報
user_ns この名前空間が所属するUser名前空間
pid_gid procファイルシステムにアクセスする際に利用されるgid
hide_pid procファイルシステムのマウントオプションのhidepid
reboot PID名前空間をリブートするときのexitコード
ns ns_common構造体

表_pid_namespace構造体

PID1に設定されるPID名前空間は図_init_pid_nsのようになります。init_pid_nsはPID1に設定されるので、以降のプロセスはこのPID名前空間を共有、もしくはこのPID名前空間を親とした階層構造上にあるPID名前空間に所属します。

struct pid_namespace init_pid_ns = {
        .kref = {
                .refcount       = ATOMIC_INIT(2),
        },
        .pidmap = {
                [ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
        },
        .last_pid = 0,
        .nr_hashed = PIDNS_HASH_ADDING,
        .level = 0,
        .child_reaper = &init_task,
        .user_ns = &init_user_ns,
        .ns.inum = PROC_PID_INIT_INO,
#ifdef CONFIG_PID_NS
        .ns.ops = &pidns_operations,
#endif
};

図_init_pid_ns

PID名前空間の作成

cloneシステムコールやunshareシステムコールでCLONE_NEWPIDが指定された場合は新たにPID名前空間を作成します。PID名前空間はcreate_pid_namespace関数にて作成します。 PID名前空間の作成では最初に階層数のチェックを行います。階層数がMAX_PID_NS_LEVELを超える場合はエラーとなります。Linuxカーネルv4.1ではMAX_PID_NS_LEVELは32となります。名前空間の初期化処理ではPIDビットマップ領域のメモリ確保、PID構造体のメモリを確保するためのSLABキャッシュ作成などメモリ管理に関する初期化や、/proc//ns/pidファイルに使用するinodeの確保とファイル操作のオペレーション設定などファイルに関する初期化があります。その他の設定として、作成するPID名前空間に対する親となるPID名前空間の設定を行います。処理の最後に作成中のPID名前空間でのPID 1に対する設定を行います。設定はpidmap構造体配列の0番目の要素に対してPID1に該当するビットのセット、空きPID数のデクリメントを行います。そして、pidmap構造体の1番目以降の各pidmap構造体に対して空きビットマップ数を設定します。PID名前空間は分離元の名前空間の下の階層に置かれ、元の名前空間においてもPIDが発行されるので分離元の名前空間の参照数のインクリメントも行います。

PID名前空間の移動

setnsシステムコール実行時には既存のPID名前空間から別の名前空間に所属を変更することができます。ただし、制限として移動可能なPID名前空間は現在と同じ名前空間、もしくは現在のPID名前空間の配下にある名前空間になります。自PID名前空間の上位にある名前空間には移動できません。これにより、名前空間の移動により現在所属している名前空間から抜けてしまうことを防いでいます。このチェック方法としては、pid_namespace構造体のlevel変数を見て、自身の名前空間よりも上位に移動でできないこと、図PID名前空間の移動において、Namespace Dに所属するプロセスはNamespace Bに移動できません。また、移動先の名前空間のparent変数を辿って、現在の名前空間にたどり着けることと確認します。図PID名前空間の移動ではNamespace Dに所属するプロセスはNamespace Cへの移動はできません。後者のチェックは自身の名前空間と無関係な名前空間に移動できないようにするために行います。自名前空間配下にある名前空間は、現在の名前空間の管理下にあるため移動が可能となっています。現在所属している名前空間の参照数、移動先名前空間の参照数の更新は他の名前空間と同様に行います。

                  Namespace A
      |-----------|-----------|
          |                       |
      Namespace B             Namespace C
          |
      Namespace D

図_PID名前空間の移動

PID名前空間の削除とプロセスの終了

あるPID名前空間にてPID 1のプロセスが終了する場合は、プロセスの終了処理の中で対象の名前空間に所属するプロセスを全て終了させます。この終了処理ではまず、これ以上のプロセスを生成させないようにPIDの新規発行をストップします。そして、SIGCHALDによるシグナルを無視するように設定し、PID名前空間が管理しているPIDのビットマップを順に調べながら有効なPIDに対してSIGKILLを送信していきます。このようにして全てのプロセスを終了させますが、SIGCHLDを無視している間にゾンビ状態(EXIT_ZOMBIE)になったプロセスがいるかもしれません。そのため、これらのプロセスを回収するためにwait4(2)をカーネルから呼び出し、EXIT_ZOMBIE状態のプロセスを回収します。ただし、EXIT_DEAD状態のプロセスについてはwait4(2)にて回収できません。しかし、グローバルなPID名前空間(init_pid_nsの名前空間)のinitプロセスで回収可能なため特別な処理は行いません。次にカレントプロセスの状態をシグナル割り込み禁止(TASK_UNINTERRUPTIBLE)にします。そしてnr_hashedの数が1もしくは2になるまでschedule関数を呼び出します。これにより、PID名前空間内のプロセスに対する親プロセスの変更を行います。nr_hashedの値として1または2どちらを使うかはカレントプロセスがスレッドグループリーダーかどうかによります。プロセスがスレッドグループリーダーの場合は1、違う場合は2となります。PIDの解放処理(free_pid関数)によりnr_hashedの数が終了条件に達したらカレントプロセスの状態をTASK_RUNNINGに変更します。最後にBSDプロセスアカウンティング情報を消去します。

fork関数/clone関数実行時の各PID名前空間へのpid番号発行

プロセスの生成時にはpidを発行しますが、fork関数を実行したプロセスが所属するPID名前空間全てでpid番号を発行する必要があります。そのため、pid番号の発行を行うalloc_pid関数では、現在のPID名前空間から階層を上がって行き、各PID名前空間にてpid番号の発行を行います。また、task_struct構造体にはpid変数があり、プロセスに割り当てられたpid番号を設定しますが、ここで設定するpid番号はプロセスが所属するPID名前空間のpid番号となります。例として、デフォルトのPID名前空間から分離したプロセス内でfork関数を行い、pid番号に144が振られた場合、このプロセスのtask_struct構造体のpid変数に設定されるのは144となります。

User名前空間

User名前空間はUID、GIDをホストとコンテナで分離します。より正確にはホストのUIDとコンテナのUIDをマッピングさせます。このマッピングにより、例えば、コンテナでのUID 0をホストのUID 1000にマッピングすることで、コンテナ内ではroot権限を使用することができますが、ホストから見るとコンテナ内のrootユーザはUID 1000ですので、ホストに対して大きな影響を及ぼす操作を制限することができます。この例の場合、コンテナ内のrootユーザが作成したファイルはホストではUID 1000番のユーザが作成したと認識されます。 User名前空間の分離時に明示的にUIDとGIDマッピングを行わなかった場合はUID、GID共に65534番にマッピングされます。IDマッピングは分離元のUID・GIDと分離後のUID・GIDマッピングを行います。よって、User名前空間もPID名前空間と同様に分離元と分離後の名前空間で親子関係になります。

User名前空間の実装
変数名 説明
uid_map uidのマッピング
uid_map gidマッピング
projid_map プロジェクト識別子のマッピング
count 名前空間の参照数
parent 親の名前空間
level ユーザー名前空間の階層数
owner プロセスのeuid
group プロセスのegid
ns ns_common構造体
flags setgroups(2)の実行可否を設定

表_user_namespace

User名前空間の作成

User名前空間の作成時は最初にcred構造体を初期化します。新規にuser_namespace構造体を作成します。構造体の初期化ではprocfsにエクスポートするファイルのinodeなどの設定など、新しい名前空間の設定を行います。User名前空間は最大で32段階の階層を作ることができるため、名前空間の作成により32段階以上ある場合はエラーとします。また、chroot環境で実行された場合もエラーとなります。次に、現在のUser名前空間と作成中の名前空間において、uidとgidマッピングが行われているかチェックを行います。ユーザ名前空間のメモリを確保し、/proc//ns/userファイルに設定するinodeを確保します。inodeが確保できたらユーザ名前空間を操作する構造体を設定します。作成した名前空間の初期設定を行います。他のプロセスと名前空間を共有していないので参照数は1になります。分離元(parent)の設定、uid・gidマッピングなどを設定します。cred構造体に作成したユーザ名前空間を設定します。User名前空間の初期化が完了したら、set_cred_user_ns関数でcred構造体と名前空間を関連付けします。

User名前空間の移動

User名前空間はNSProxyが管理していないため処理が多少変わります。移動先と現在の名前空間が同じな場合はエラーとなります。マルチスレッドのプログラムにおいて、あるスレッドが名前空間の移動を行おうとした場合もエラーとなります。これによってあるプロセスがスレッドに毎に違う名前空間に所属するのを防いでいます。プロセスがファイルシステム情報を子プロセスと共有している場合もエラーとなります。移動先の名前空間でCAP_SYS_ADMINケーパビリティを持っている必要がります。これらのチェックで問題がなければ名前空間の移動処理を行うことができます。prepare_creds関数にて新しいcred構造体を作成、その後、現在の名前空間の参照数を減らします。次に移動先の名前空間の参照数を増やします。そして、set_cred_user_ns関数でcred構造体のuser_ns変数を移動先の名前空間に設定します。最後に現在のtask_structに設定されているcred構造体を新しく作成したcred構造体に変更し、処理が完了します。

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

*1:9)から(13

Linuxカーネル4.1のvmalloc()(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。こちらはLinuxカーネル4.1のメモリレイアウト(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモで説明しなかったvmalloc()の説明です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

スラブアロケータ以外の動的メモリ確保

vmalloc関数

Linuxカーネルの動的メモリ確保手段にはページ単位で確保するバディシステムや、任意のサイズでメモリを確保するスラブアローケータがありますが、これらの他にもvmalloc関数があります。 vmalloc関数のインターフェースはmalloc(3)と同様で確保するサイズを指定します。vmalloc関数で確保したメモリ領域の解放にはvfree関数を使用します。

void *vmalloc(unsigned long size);
void vfree(const void *addr);

非連続領域で使用する仮想アドレスの範囲はアーキテクチャによって違い、x86_64ではarch/x86/include/asm/pgtable_64_types.hにて以下のように32TiBの範囲が割当られています。

#define VMALLOC_START    _AC(0xffffc90000000000, UL)
#define VMALLOC_END      _AC(0xffffe8ffffffffff, UL)

vmalloc関数はkmalloc関数との違い、物理的に連続していないページフレームよりメモリを確保します。kmalloc関数で2ページ分のメモリを確保する場合、2つのページフレームは物理的に連続しますが、vmalloc関数の場合は物理的に連続していません。どちらの場合もリニアアドレスとしては連続しています。このようにvmalloc関数でメモリを確保する場合、ページフレームは物理的に連続していないため、DMAなどで物理的に連続したアドレスを要求するような場合には使用できません。 また、kmalloc関数は要求したサイズに合うサイズのスラブキャッシュからメモリを確保していましたが、vmalloc関数は要求されたサイズを確保できる分のページフレームを確保します。そのため、16バイトのメモリを要求した場合でも1ページをメモリ確保用に使用します。

vmalloc関数で使用するデータ構造

vmalloc関数やioremap関数など、非連続メモリ領域からメモリを確保する場合に使用するデータ構造は主に2つあります。 vm_struct構造体はinclude/linux/vmalloc.hにて定義されており、割当を行ったページフレームや確保したメモリのサイズなど非連続メモリのデータにおける、実際のメモリ管理に関連するデータを扱います。ioremap関数の場合はphys_addrに物理アドレスを設定しますが、それ以外はリニアアドレスを使用します。

変数名 内容
addr 確保した領域の先頭のリニアアドレス
size 確保したメモリのサイズ
flags メモリを確保する時のフラグ。表_非連続メモリ領域の確保時に使用するフラグ参照
pages リニアアドレスに割り当てたページフレームの配列
nr_pages pagesで使用しているpage構造体数
phys_addr 物理メモリのアドレス(ioremap関数の場合に使用)
caller 非連続メモリ領域を確保した関数のアドレス

表_vm_struct構造体

非連続メモリ領域の確保時に使用するフラグは表_非連続メモリ領域の確保時に使用するフラグに示したフラグがあります。これらのフラグの定義はinclude/linux/vmalloc.hにあります。

変数名 内容
VM_IOREMAP ioremap関数でメモリを確保する場合に設定
VM_ALLOC vmalloc関数でメモリを確保する場合に設定
VM_MAP vmap関数でメモリを確保する場合に設定
VM_VPAGES page構造体の配列作成時にvmalloc関数でメモリを確保した場合に設定
VM_UNINITIALIZED メモリの確保途中を表すために設定
VM_NO_GUARD ガード領域を使用しない場合に設定

表_非連続メモリ領域の確保時に使用するフラグ

vmap_area構造体は非連続メモリ領域に関するデータを扱います。この構造体のvm変数から、ページフレームなどを管理しているvm_struct構造体にアクセスできます。vmap_area構造体もvm_struct構造体と同じくinclude/linux/vmalloc.hにあります。

変数名 内容
va_start 開始アドレス
va_and 終了アドレス
flags この構造体の状態を表すフラグ。表_vmap_areaの状態フラグ参照
rb_node vmap_area構造体をアドレスでソートした赤黒木
list vmap_area構造体を管理する連結リスト
purge_list vmap_area構造体のリスト。vmap areaを解放する時に使用する
vm このvmap areaと関連しているvm_strcuct構造体
rcu_head vmap_area構造体のメモリを解放するときに使用するRCU構造体

表_vmap_area構造体

非連続メモリ領域の確保や解放などではvmap_area構造体のflags変数に現在の処理状況を設定します。この内容は mm/vmalloc.cにて定義されています。

変数名 内容
VM_LAZY_FREE 使用していたvmap_area構造体を解放する時に設定
VM_LAZY_FREEING vmap_areaa構造体解放処理が進んだ時に、VM_LAZY_FREEからこの状態に変更
VM_VM_AREA vmalloc用にvmap_areaを設定した場合に設定

表_vmap_areaの状態フラグ

vmap_area構造体とvm_struct構造体は、vmap_area構造体のrb_node、list変数にて赤黒木と連結リストで結びついています。vmap_area構造体とvm_struct構造体は図_vmap_area構造体とvmstruct構造体の関連のようになります。

f:id:masami256:20190510205850p:plain

図_vmap_area構造体とvm_struct構造体の関連

vmallocの初期化

vmalloc関数を使用する前に、カーネルのブート処理の一環としてvmalloc_init関数で初期化を行います。vmalloc_init関数での初期化の前にvm_area_register_early関数で非連続領域の初期設定が終わっている必要があります。vm_area_register_early関数はブート中に非連続メモリ領域をリストに登録しておき、vmalloc_init関数でvmalloc関数で使用できるようにします。 vmalloc_init関数は最初に全cpuを対象にデータを初期化します。各cpuは連結リストのvmap_block_queueと、vfree関数実行時に使用するワークキューのvfree_deferredを持ちます。vmap_block_queueはリストの初期化、vfree_deferredはリストの初期化と、遅延実行時に呼ばれる関数(free_work関数)の登録を行います。

次に、vm_area_register_early関数で非連続メモリ領域がリストに登録されていた場合は、これらの領域用にvmap_area構造体を作成し、__insert_vmap_area関数で赤黒木のvmap_area_rootと、連結リストのvmap_area_listに登録します。 vmap_area_pcpu_holeにはvmallocが使用する領域の最後のアドレスを設定します。最後にvmap_initializedをtrueに設定して、vmap領域の初期化とマークします。

vmallocでのメモリ確保

vmalloc関数がメモリ確保のインターフェースですが、実際に処理を制御するのは__vmalloc_node_range関数です。vmalloc関数の処理の流れを図_vmallocのコールフローに示します。

vmalloc()                                  (1)
  -> __vmalloc_node_flags()                (2)
    -> __vmalloc_node()                    (3)
      -> __vmalloc_node_range()            (4)
        -> __get_vm_area_node()            (5)
      -> alloc_vmap_area()             (6)
          -> setup_vmalloc_vm()            (7)
    -> vmalloc_area_node()             (8)       
      -> remove_vm_area()              (9)
      -> map_vm_area()                 (10)
    -> clear_vm_uninitialized_flag()   (11)

図_vmallocのコールフロー

vmalloc関数によるメモリの確保では、最初に要求されたsizeをPAGE_SIZEの倍数に切り上げます。そして、確保するページ数を割り出します。そして、vmalloc関数で確保した領域を管理するためのデータとしてvm_struct構造体を使用するため、この構造体用にメモリを確保します。ここではスラブアロケータよりメモリを確保します。 次に、vmap_area構造体の設定を行います。この処理はalloc_vmap_area関数にて行います。まず、vmap_area構造体用にスラブアロケータよりメモリを確保します。 vmalloc関数によるメモリ確保ではガード領域として1ページ余分にページを確保します。この領域はプログラムのミスにより、本来のサイズを超えた位置への書き込みが行われた場合への対処です。この結果、PAGE_SIZE以下のメモリを確保する場合、「図_vmallocで確保するメモリ」のように2ページ分のページフレームを確保することになります。

f:id:masami256:20190510205654p:plain

図_vmallocで確保するメモリ

そして、vmalloc関数で確保するアドレス範囲を決定する処理を行います。この時、vmapキャッシュを使用の有無を選択します。 vmapキャッシュを使用するのは以下の条件の場合です。

  • 以前にalloc_vmap_area関数が実行され、vmap探索用のキャッシュ変数のfree_vmap_cacheにデータが設定されている

以下のいずれかの条件を満たす場合にvmapキャッシュを使用しません。

  • vmapキャッシュが設定されていない
  • sizeが前回設定したcached_hole_sizeより小さい
  • vstartが前回設定したcached_vstartより小さい
  • alignが前回設定したalignより小さい

vmapキャッシュを設定しない場合は、cached_hole_sizeを0に、vmapキャッシュをNULLにします。 cached_vstartとcached_alignには、引数で受け取ったvstartとalignをそれぞれ設定します。この設定はvmapキャッシュの使用有無に関わらず行います。

そして、探索の第一弾目の処理に入ります。まず、vmapキャッシュを使用する場合、vmapキャッシュからvmap_area構造体を取得します。そして、このvmap_area構造体に設定されている終了アドレスを、引数のalignの境界に整列させます。 この計算結果のアドレスがvstartより小さい場合は、vmapキャッシュ使用なしとしてアドレスの探索をやり直します。アドレス+sizeの結果がオーバーフローする場合はエラーになります。取得した要素はfirst変数に代入します。

vmapキャッシュを使用しない場合は、vstartをalignの境界に整列させてアドレスを計算します。この場合も、アドレス+sizeの結果がオーバーフローする場合はエラーになります。次に、vmap_area構造体を管理している赤黒木のvmap_area_rootより、全vmpa_area構造体を調べていきます。ツリー構造の左右どちらに進むかは、取得したvmap_area構造体の終了アドレスにより変わります。終了アドレスがアドレスよりも小さい場合は右の要素を取得します。もし、終了アドレスがアドレスよりも大きい場合、まず、この要素をfirst変数に代入しておきます。そして、このvmap_area構造体の開始アドレスよりも大きければ探索はここで終了します。違った場合は左の要素を取得し、再度チェックを行います。これで、要素を全て探索するか、途中で探索を終了できるまで行います。ここでの赤黒木の探索は、startで指定されたアドレスよりも大きなアドレスがva_end変数に設定されているvmap_area構造体を探すことです。 この処理で、first変数が設定された場合は以下に続く処理を行います。見つからなければ、先のfoundラベルからの処理を行います。

キャッシュ未使用時に赤黒木の探索後にfirst変数がNULLで無い場合、もしくはキャッシュから探索した場合は以下の処理でアドレスの再設定を行います。

  1. アドレス+sizeがfirstの開始アドレスより大きく、引数で渡されたvend未満なら2へ
  2. アドレス+cached_hole_sizeがfirstの開始アドレスよりも小さい場合は、cached_hole_sizeを「firstの開始アドレス - アドレス」に設定
  3. firstの終了アドレスをalignの境界に合わせた結果をアドレスに代入
  4. アドレス+sizeがオーバーフローする場合はエラーにする
  5. firstがvmap_area構造体を管理するリスト(vmap_area_list)の最後の要素なら探索終了してループを抜ける
  6. firstのlist変数より、firstとリストでつながっている次の要素を取得し、1に戻る

上記のループを抜けた段階で"アドレス+size"がvendを超えている場合はエラーにします。 ここにたどり着くのは、上記のループを行った場合と、キャッシュ未使用時に、赤黒木の探索結果でfirst変数がNULLだった場合です。この場所にはfoundというgoto文のラベルが設定されていいます。 ここまで着た場合、アドレスを利用することができるので、呼び出し元に返却するvmap_area構造体のデータを設定します。開始アドレスはここまでで計算したアドレスを設定します。終了アドレスはアドレス+sizeになります。そして、このvmap_area構造体を__insert_vmap_area関数を使用して赤黒木のvmap_area_rootと、通常のリストのvmap_area_listへ登録します。最後に、設定を行ったvmap_area構造体のrb_nodeをvmapキャッシュのfree_vmap_cacheに設定します。ここまででvmap_area構造体の設定が完了になります。

リニアアドレスの確保はvmapキャッシュの有無などでコードは複雑になっていますが、シンプルなケースではファーストヒットにより割り当てるリニアアドレスを決定しています。

ここで、リニアアドレスを初めて割り当てるのにvmalloc関数を使用する場合を例に見てみます。

最初のリニアアドレス割当時にはvmapキャッシュは存在しませんので、赤黒木のノードを調べることになりますが、初回の場合は赤黒木にデータはないのでfoundラベルに移動することになります。 この時はaddrはリニアアドレス探索開始アドレスのVMALLOC_STARTが設定されています。結果として、vmap_area構造体のva_startとva_endは以下のようになります。

va->va_start = VMALLOC_START
va->va_end = va->va_start + size

そして、vmapキャッシュにはこのvmap_area構造体が設定されます。

2回目のvmalloc関数呼び出し時にはvmapキャッシュが設定されています。よって、addrは前回割り当てたリニアアドレスの末端(va->va_end)が設定されます。そして、先に説明した、キャッシュから探索した場合の6ステップの処理を実行します。この場合、5ステップ目のfirst変数がリストの最後の要素(実際に1つしか要素が登録されていませんので、最初の要素でもあり最後の要素でもあります)となってループを抜けます。 この場合もfoundラベルの処理になりますので、以下のようにvmap_area構造体の要素を設定します。addrは前回割り当てたリニアアドレスの終端のアドレスです。

va->va_start = addr
va->va_end = addr + size

この時のリニアアドレスの割当状態が図_リニアアドレスの割当です。

f:id:masami256:20190510205719p:plain

図_リニアアドレスの割当

alloc_vmap_area関数の処理が終わり、__get_vm_area_node関数に戻ったら最後にsetup_vmalloc_vm関数を実行します。 ここでは、vmap_area構造体とvm_struct構造体の設定を行います。vm_struct構造体にはアドレス、サイズなどを設定し、vmap_area構造体のvmメンバ変数にvm_struct構造体を設定します。この時にvm_struct構造体のflags変数にVM_UNINITIALIZEDを設定し、この領域はまだ初期化中とマークします。また、この時点でflags変数に対してVM_VM_AREAフラグも設定します。

次にページの割当と設定を行っていきます。まず、メモリを確保するsizeから必要なページフレーム数を計算します。この時点ではsizeは、呼び出し元が設定したサイズではなく、PAGE_SIZEの倍数になっています。 vmalloc関数ではページフレームは物理的に連続していないため、page構造体の配列にて管理する必要があります。 そして、page構造体を管理するためのメモリを確保しますが、この時に、確保するpage構造体の配列のサイズが1ページに収まらない場合、 __vmalloc_node関数を用いてメモリを確保します。1ページに収まる場合はスラブアローケータを利用します。__vmalloc_node関数は__vmalloc_node_range関数を呼び出します。 page構造体管理用の配列のメモリを確保できたら、バディシステムを利用してページフレームを1ページフレームずつ確保していきます。 ページフレームの確保を行ったら、map_vm_area関数にて、ページフレームをページテーブルに設定します。最初にアドレスからページグローバルディレクトリ(PGD)のインデックスを取得し、そこからページアッパーディレクトリ(PUD)、ページミドルディレクトリ(PMD)、ページテーブルエントリ(PTE)と設定していきます。 最後にvm_struct構造体のflagsに設定したVM_UNINITIALIZEDフラグを落としてvmalloc関数の処理は完了です。

vfree関数でのメモリ解放

vfree関数でメモリの解放を行う場合、割り込みコンテキスト中に呼ばれたかを確認します。もし、割り込みコンテキストだった場合は、解放処理を遅延させます。この場合、cpuに設定されているvfree_deferred構造体を取得し、この構造体に解放対象のアドレスを設定し、schedule_work関数でキューに登録して終了します。その後free_work()が呼ばれた時に__vunmap()を呼び出します。 割り込みコンテキストでない場合は__vunmap関数を使用してメモリの解放を行います。メモリの解放は__vunmapがメインの処理です。vfree関数のコールフローを図_vfreeのコールフローに示します。

vfree()                           (1)
  -> free_work()                  (2)
    -> __vunmap()                 (3)
  -> __vunmap()                   (4)
    -> remove_vm_area()           (5)
      -> find_vmap_area()         (6)
      -> free_unmap_vmap_area()   (7)

図_vfreeのコールフロー

メモリを解放する場合、まずは解放するアドレスで使用しているvmap_area構造体を取得します。そして、vmap_area構造体からvm_struct構造体を取得し、vmap_area構造体からvm_struct構造体への参照を外します。また、vmap_area構造体のflagsからVM_AREAフラグを外して、未使用の状態として設定します。 そして、解放対象のアドレスで使用しているページテーブルを全てクリアします。クリアする順番はPGD、PUD、PMD、PTEの順番です。 次に、使用していた非連続メモリ領域の解放処理を行います。この処理は__purge_vmap_area_lazy関数にて行います。解放処理に入る前にvma_area構造体のflags変数にVM_LAZY_FREEフラグをセットして、これから解放するとマークします。また、vmap_area構造体に設定されているva_endからva_startの範囲から、ページ数を割り出し、vmap_lazy_nr変数に設定します。この変数は後ほど使用します。 __purge_vmap_area_lazy関数ではページを割り当てたvmap_area構造体を管理しているvmap_area_listを1要素ずつ辿りながら解放対象のvmap_area構造体を設定していきます。解放するのはvmap_area構造体のflags変数にVM_LAZY_FREEが設定されているものです。 vmap_area構造体に対する操作は以下の3処理です。

  1. 解放対象のvmap_area構造体を登録するリストへ構造体の登録
  2. flags変数にVM_LAZY_FREEINGフラグを設定
  3. flags変数からVM_LAZY_FREEフラグを外す

この時に解放するアドレスの範囲から解放するページフレーム数の計算と、解放するアドレスの範囲の再設定を行います。__purge_vmap_area_lazy関数はtry_purge_vmap_area_lazy関数から呼ばれますが、この時、解放するアドレスの範囲として、開始アドレスをULONG_MAX、終了アドレスを0で設定して関数を呼び出します。そして、ループ内で実際の開始・終了アドレスの設定を行います。 vmap_area構造体をvaとして、以下のように設定します。

  • va->va_start < startなら開始アドレスをva->startのアドレスに変更
  • va->va_end > endなら終了アドレスをva->endのアドレスに変更

ページ数はva->va_end - va->va_startの結果から計算します。

vmap_area_list内の要素に対して設定変更完了後にページ数が設定されている場合、以下の処理を行います。

  • vmap_lazy_nrからループ内で数えたページ数を減らす
  • start - endの範囲のアドレスを無効にするため、TLBをフラッシュ
  • vmap_area構造体の解放

vmap_area構造体の解放処理は__free_vmap_area関数にて行います。この処理ではメモリの確保でvmap_area構造体を設定する時にvmapキャッシュを設定していた場合に、そのキャッシュを無効にします。キャッシュの無効化は2パータンあり、解放するvmap_area構造体の終了アドレスが、cached_vstartに設定されているアドレスより小さい場合はキャッシュをNULLに設定します。 そうでない場合、vmapキャッシュの赤黒木よりvmap_area構造体を取得します。そして、解放対象のvmap_area構造体とキャッシュのvmap_area構造体の開始アドレスを比較し、解放対象の開始アドレスがキャッシュの開始アドレスより小さければ、vmapキャッシュにはキャッシュが登録されている赤黒木のprev要素をvmapキャッシュに設定します。そうでない場合は最初に赤黒木より取得したvmap_area構造体をvmapキャッシュに設定します。 そして、解放対象のvmap_area構造体を赤黒木のvmap_area_rootや、RCUのリストから外します。 解放対象のvmap_area構造体の開始・終了アドレスがvmalloc関数が使用するアドレス範囲(VMALLOC_START - VMALLOC_END)にある時に、既存のvmap_area_per_cpu_holeに設定されているアドレスが、解放対象のvmap_area構造体の終了アドレスより小さい場合はvmap_arep_per_cpu_holeの値を、このvmap_area構造体の終了アドレスに更新します。 最後にvmap_area構造体をメモリを解放します。

その他の非連続メモリ領域からメモリを確保する関数

非連続領域からメモリを確保する関数は、vmalloc関数の他にvmap関数とioremap関数があります。

vmap関数

vmap関数はvmallocと同じくinclude/linux/vmalloc.hにて宣言されています。

extern void *vmap(struct page **pages, unsigned int count,
                        unsigned long flags, pgprot_t prot);
            

vmap関数は引数で渡されたpage構造体の配列pagesをリニアアドレスにマップします。この関数に渡すpageは非連続メモリ領域からアローケートしたページです。よって、リニアアドレスが連続していない複数個のpage構造体を連続したリニアアドレスにマップすることになります。 vmap関数の場合、vmalloc関数と同様に__get_vm_area_node関数でvmap_areaの設定を行います。この処理はvmalloc関数の場合と変わりません。この後、vmap関数の場合はページフレームはすでに確保されているので、ページフレームの確保は行わず、map_vm_area関数を使用してリニアアドレスとページフレームのマッピングを行います。

ioremap関数

ioremap関数は主にデバイスドライバが使用するための関数です。x86x86_64環境では物理アドレス0xa000-0xffff(640KiB~1MiB)の範囲はISAデバイスが使用するために予約されていますが、それ以外のアドレス範囲については規定されていません。このような場合にデバイスの物理メモリ領域をカーネルのリニアアドレスにマッピングするためにioremap関数を使用します。

ioremap関数はアーキテクチャ毎に実装が違います。x86_64ではarch/x86/include/asm/io.hにて宣言されています。

static inline void __iomem *ioremap(resource_size_t offset, unsigned long size);

ioremap関数の処理のうち、メモリのマッピングを行う主要な関数は__ioremap_caller()です。この関数よりget_vma_area_caller関数が呼ばれ、さらに__get_vm_area_node関数を実行することで要求したサイズのリニアアドレス範囲を確保することができます。

/proc/vmallocinfoによるメモリ確保状況の確認

vmalloc領域の使用状況は/proc/vmallocinfoファイルで確認することができます。vmalloc関数やvmap関数などではメモリの確保時に__vmap_area関数にて連結リストのvmap_area_listにvmap_area構造体を登録していますので、cat時に表示する内容はこのリストを辿って、vmap_area構造体のデータを整形して表示します。このファイルをcatすると図_vmallocinfoのように出力されます。

表示内容は以下の通りです。

  1. 使用しているアドレスの範囲
  2. サイズ
  3. 呼び出し元の関数(機械語での呼び出し位置/機械語での関数のサイズ)
  4. 使用しているページフレーム数
  5. 物理アドレス
  6. メモリを確保した関数(vmalloc/vmap/ioremap/user(vmalloc_user関数)/vpages(__vmalloc_area_node関数)
  7. 使用しているNUMAノード(Nの後ろの数値がノードの番号、=の後ろの数字が使用しているページ数)
# cat /proc/vmallocinfo
0xffffc90000000000-0xffffc90000004000   16384 acpi_os_map_iomem+0xef/0x14e phys=bffdf000 ioremap
0xffffc90000004000-0xffffc90000805000 8392704 alloc_large_system_hash+0x160/0x236 pages=2048 vmalloc vpages N0=2048
0xffffc90000805000-0xffffc9000080a000   20480 alloc_large_system_hash+0x160/0x236 pages=4 vmalloc N0=4
0xffffc9000080a000-0xffffc90000c0b000 4198400 alloc_large_system_hash+0x160/0x236 pages=1024 vmalloc vpages N0=1024
0xffffc90000c0b000-0xffffc90000c0e000   12288 alloc_large_system_hash+0x160/0x236 pages=2 vmalloc N0=2
0xffffc90000c0e000-0xffffc90000c2f000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000c2f000-0xffffc90000c50000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000c50000-0xffffc90000c52000    8192 bpf_prog_alloc+0x39/0xb0 pages=1 vmalloc N0=1
0xffffc90000c52000-0xffffc90000c54000    8192 acpi_os_map_iomem+0xef/0x14e phys=fed00000 ioremap
0xffffc90000c54000-0xffffc90000cd5000  528384 alloc_large_system_hash+0x160/0x236 pages=128 vmalloc N0=128
0xffffc90000cd5000-0xffffc90000dd6000 1052672 alloc_large_system_hash+0x160/0x236 pages=256 vmalloc N0=256
0xffffc90000dd6000-0xffffc90000df7000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000df7000-0xffffc90000e18000  135168 alloc_large_system_hash+0x160/0x236 pages=32 vmalloc N0=32
0xffffc90000e18000-0xffffc90000e29000   69632 alloc_large_system_hash+0x160/0x236 pages=16 vmalloc N0=16

図_vmalloinfo

ゼロからよくわかる!  ラズベリー・パイで電子工作入門ガイド

ゼロからよくわかる! ラズベリー・パイで電子工作入門ガイド

Linuxカーネル4.1のメモリレイアウト(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

ユーザプロセス空間とカーネル空間

Linux x86_64では48bit(256TiB)のアドレス空間を使用できます。この256TiBの範囲のうち、128TiBをユーザープロセスが使用し、残りの128TiBをカーネルが使用します。32bitのカーネルではユーザプロセスに3GiB、カーネルに1GiBの割当でしたので、64bit環境では使用可能なメモリを使用量が大幅に増えました。 Linuxではこの256TiBのアドレス空間をプロセスとカーネルが使用しますが、0〜0xffff800000000000までのユーザプロセス空間はプロセスごとに割り当てられ、後半のカーネル空間に関しては常に1つとなり、「図_ユーザプロセス空間とカーネル空間」のようになります。

f:id:masami256:20190510203636p:plain

図_ユーザプロセス空間とカーネル空間

この256TiBのメモリはアドレスの範囲によって使用目的が決まっています。その内訳を「表_メモリマップ」に示します。

x86_64のメモリレイアウト
開始アドレス 終了アドレス サイズ 内容
0x0000000000000000 0x00007fffffffffff 128TiB ユーザプロセス空間
0xffff800000000000 0xffff87ffffffffff 8TiB ハイパーバイザー向けに予約
0xffff880000000000 0xffffc7ffffffffff 64TiB ダイレクトマップ領域
0xffffc80000000000 0xffffc8ffffffffff 1TiB 未使用
0xffffc90000000000 0xffffe8ffffffffff 32TiB vmalloc/ioremapが使用
0xffffe90000000000 0xffffe9ffffffffff 1TiB 未使用
0xffffea0000000000 0xffffeaffffffffff 1TiB 仮想メモリマップ
0xffffeb0000000000 0xffffebffffffffff 1023GiB 未使用
0xffffec0000000000 0xfffffc0000000000 16TiB kasan shadow memory
0xfffffc0000000000 0xfffffeffffffffff 3072GiB 未使用
0xffffff0000000000 0xffffff7fffffffff 512GiB スタック領域
0xffffff8000000000 0xffffffff7fffffff 509GiB 未使用
0xffffffff80000000 0xffffffffa0000000 512MiB カーネルテキストマッピング領域。物理メモリ0~512MiB
0xffffffffa0000000 0xffffffffff5fffff 1525MiB カーネルモジュールマッピング領域
0xffffffffff600000 0xffffffffffdfffff 8MiB vsyscallsで使用
0xffffffffffe00000 0xffffffffffffffff 2MiB 未使用

表_メモリマップ

この内訳はDocumentation/x86/x86_64/mm.txtにて確認することができます。 ユーザープロセス空間は128TiBをフルに使用しますが、カーネル空間の場合、ある特定の範囲を使用目的毎に分けています。128TiB全てが使用されているわけではなく、現在は未使用の領域として空いているところもあります。

また、EFIを使用するシステムでは図_メモリマップのように0xffffffef00000000から0xffffffff00000000の範囲をEFIのランタイムサービスにマッピングします。

f:id:masami256:20190510203723p:plain

図_メモリマップ

ダイレクトマップ(ストレートマップ)領域

カーネルは全ての物理メモリをこの領域にマッピングします。x86_32アーキテクチャではアーキテクチャの制限として、物理アドレスを直接マッピングできるのは896MiBまでで、それ以上のアドレスを使用するためにHIGHMEM領域を使用していましたが、x86_64アーキテクチャではHIGHMEM領域が不要になり、ダイレクトマップ領域だけを使用します。

ダイレクトマップ領域の開始アドレスはPAGE_OFFSETとして定義されていています。

arch/x86/include/asm/page_types.h
#define PAGE_OFFSET             ((unsigned long)__PAGE_OFFSET)

arch/x86/include/asm/page_64_types.h
#define __PAGE_OFFSET           _AC(0xffff880000000000, UL)

図_PAGE_OFFSETの定義

物理アドレスからリニアアドレスへの変換は__vaマクロを使用します。この場合は物理アドレスに対してPAGE_OFFSETを足すことでリニアアドレスを得ることができます。

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

リニアアドレスから物理アドレスへの変換は__pa関数マクロです。このマクロの実体は__phys_addr_nodebug関数です。x86_32ではアドレスからPAGE_OFFSETを引くだけでアドレスを計算できたのですが、x86_64ではカーネルテキストマッピング領域の開始アドレス 0xffffffff80000000(__START_KERNEL_map)を基準としてアドレスの計算を行います。この関数で使用するphys_baseは通常0となっています。アドレスの変換は、「図_リニアアドレスから物理アドレスへの変換」のように行います

f:id:masami256:20190510203830p:plain

図_リニアアドレスから物理アドレスへの変換

vmalloc/ioremap領域

vmalloc関数やioremap関数などでメモリを確保する場合、アドレスをこの領域から割り当てます。vmalloc関数については別記事

Linuxカーネル4.1のvmalloc()(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ

にて説明します。この領域で使用するページフレームは物理的に連続していませんが、論理的に連続します。

仮想メモリマップ

この領域は主にページフレーム番号とpage構造体の相互変換に使用します。ページフレーム番号からページ構造体を取得するには__pfn_to_pageマクロ、page構造体からページフレーム番号の取得には__page_to_pfnマクロを使用します。

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)      (vmemmap + (pfn))
#define __page_to_pfn(page)     (unsigned long)((page) - vmemmap)

図_ページフレーム番号とpage構造体の変換インターフェース

これらの変換操作にはグローバル変数のvmemmapに設定されている仮想メモリマップ領域の開始アドレスを基準として、このアドレスに対してページフレーム番号を足すことで、該当のpage構造体を取得、また、page構造体のアドレスからvmemmapを減算することで、ページフレーム番号の取得を行います。

%esp fixup stack

このスタックはPAGE_SIZEが4096の場合、各CPUに対して64個のスタックを読み込み専用として作成します。このスタックはministacksと呼ばれます。 ministacksはStack-Segment Fault(SS)からiret命令により、カーネルからユーザランドへ制御が戻る時に、iretで使用していたスタックフレームをLDTにコピーしてからユーザランドへ戻ります。 この後、General Faultなどが発生した場合はInterrupt Stack Tableから専用のスタックを使用します。 このような読み込み用のスタックを使用するのはセキュリティ上の理由です(注1)。iret命令は16bitのセグメントを元に戻しますが、%espの16〜31bitはカーネルのスタックを指しているため、カーネルの情報が漏れてしまいます。この状況に対応するため、専用のスタックを用意してiret命令から戻るようになっています。

カーネルテキスト領域

物理メモリのアドレス0〜512MiBをこの範囲にマッピングします。この領域の開始アドレスは__START_KERNEL_mapとして定義されています。この領域にはカーネルマッピングするため、このアドレス範囲に対して操作を行うことはありませんが、先に見たように__paマクロによる物理アドレスの取得など、アドレスを扱う場合に__START_KERNEL_mapを利用することがあります。

モジュールマッピング領域

カーネルモジュールをロードする領域です。この領域はカーネルテキストマッピング領域の直後に位置します。

#define MODULES_VADDR    (__START_KERNEL_map + KERNEL_IMAGE_SIZE)
#define MODULES_END      _AC(0xffffffffff000000, UL)
#define MODULES_LEN   (MODULES_END - MODULES_VADDR)

図_モジュールマッピング領域の定義

カーネルモジュールをロードする時にmodule_alloc関数が__vmalloc_node_range関数を用いて、モジュールのために確保するメモリ領域をMODULES_VADDRからのMODULES_ENDまでの範囲を指定します。

vsyscall

vsyscallのために割り当てられた領域で、開始アドレスはVSYSCALL_ADDRとして以下のように定義されています。値は0xffffffffff600000となります。

10 #define VSYSCALL_ADDR (-10UL << 20)

vsyscall領域は/proc/pid/mapsファイルにて、1ページ分がマッピングされていることを確認できます。

$ cat /proc/self/maps
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
固定マップ領域

機能によって、特定のアドレスにメモリを割り当てて使用したいという要求があり、これを実現するのが固定マップ領域です。固定マップ領域は0xffffffffff7ff000からの領域でアドレスの低位に向かって使用します。 固定マップ領域で使用する機能目的ごとのアドレスの定義はarch/x86/include/asm/fixmap.hで、enumのfixed_addressesで各領域ごとにインデックスが決められています。

固定マップ領域のインデックス(fixed_addresses)からリニアアドレスへの変換は__fix_to_virtマクロ、リニアアドレスからインデックス番号への変換は__virt_to_fixマクロで行います。

20 #define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))
21 #define __virt_to_fix(x)        ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

FIXDADDR_TOPマクロは以下のように定義されています。VSYSCALL_ADDRはvsyscall領域の開始アドレスで、x86_64では0xffffffffff600000となります。

#define FIXADDR_TOP     (round_up(VSYSCALL_ADDR + PAGE_SIZE, 1<<PMD_SHIFT) - \
                         PAGE_SIZE)

固定マップ領域の確保はset_fixmap関数にて行います。これはマクロで__set_fixmap関数を呼び出します。この関数の実装はCPUアーキテクチャに依存します。x86_64アーキテクチャでは__native_set_fixmap関数が使われます。固定マップ領域の設定は先に説明した__fix_to_virtマクロによるリニアアドレスへの変換を行い、変換結果のアドレスをPTEテーブルへの登録を行います。

例えば、固定マップ領域のインデックス「VSYSCALL_PAGE」は0x1ffで、このインデックスに対するアドレスは0xffffffffff600000となります。この値は先に見たようにvsyscall領域のアドレスです。よって、vsyscall領域は固定マップ領域の仕組みを利用して、アドレスを固定して確保していることがわかります。 固定マップ領域のインデックス範囲は0x1ff-0x600です。アドレスに変換すると0xffffffffff600000〜0xffffffffff1ff000となります。

デバッグの理論と実践 ―なぜプログラムはうまく動かないのか

デバッグの理論と実践 ―なぜプログラムはうまく動かないのか

Linuxカーネル4.1のSLUBアローケータ(ドラフト)

はじめに

本記事は過去に書いたけど諸事情により陽の目に出ていなかった文書です。完全版ではありません。satさんがスケジューラの記事を公開したのに影響され、自分も書いたものを死蔵させておくのはもったいないので公開することにしました。

qiita.com

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

スラブアローケータ

Linuxにおけるメモリ確保の方法としてページ単位でメモリ確保を行うバディシステムがあります。バディアロケータはページ単位に管理を行うため、構造体など小規模なメモリを確保するために使用すると無駄が多くなってしまいます。そこで、スラブアローケータというメモリをより小さな単位で管理する仕組みを使用します。

スラブアローケータはアローケータの実装方式の一つで、Jeff Bonwick氏によりSolaris 2.4で最初に実装されました。今日ではスラブアローケータはSolarisだけでなく、LinuxFreeBSDなどのオペレーティングシステムや、memcachedのようなメモリ管理を効率的に行う必要があるミドルウェアでも採用されています。

スラブアローケータの説明に入る前に、基本的な用語を説明します。

  • スラブキャッシュ

スラブオブジェクトを管理します。スラブキャッシュはスラブオブジェクトの種類毎に作成します。メモリの確保や解放処理の要求の受付、スラブアローケータ内でのメモリ管理など、スラブアローケータの機能を提供します。スラブアローケータではメモリの確保時に実行するコンストラクタ関数を設定することもできます。Linuxではkmem_cache構造体がスラブキャッシュです。

  • スラブオブジェクト

スラブキャッシュが管理するオブジェクトです。スラブアローケータはこのスラブオブジェクトをメモリ確保要求時に返却します。

  • スラブ

スラブオブジェクトの集合です。スラブオブジェクトはバディアロケータよりページを確保し、そこにスラブオブジェクトを作成します。

  • freelist

slubアローケータにおいて、次のメモリ確保要求時に返却するするスラブオブジェクトを指すポインタです。

  • 使用中リスト

slubアローケータでは、スラブの管理には使用中リストを使用します。

スラブアローケータは管理するオブジェクトの単位でスラブキャッシュを作成します。たとえば、inode構造体用のスラブキャッシュ、task_struct構造体用のスラブキャッシュなど、通常は構造体毎にスラブキャッシュを作成します。こうして、特定の構造体用にメモリを確保する場合、その構造体に特化したアローケータよりメモリを確保を行います。このように特定のサイズに応じたアローケータを使用することで、メモリのフラグメンテーションを防ぎやすくなります。 任意のサイズのメモリを確保できるアローケータの場合、オブジェクトの確保と解放を繰り返していくうちに、メモリのフラグメンテーションが進み、空き領域の合計は十分にあるのに、メモリが断片化しているためまとまった量のメモリを確保できずに失敗する状況が発生します。

f:id:masami256:20190509234229p:plain

図_メモリのフラグメンテーション

「図_メモリのフラグメンテーション」では、最初のうちはメモリの確保のみが行われていたのでフラグメンテーションは発生していませんが、時刻が進みオブジェクトの解放が行われたため、メモリのフラグメンテーションが発生している状態です。未使用領域が2つあり、メモリ確保の要求がどちらかの未使用領域で満たせる場合は大丈夫ですが、要求サイズが各未使用領域のサイズよりも多いきい場合、仮に2つの未使用領域の合計サイズが、要求サイズよりも大きくてもメモリ確保に失敗します。 スラブアローケータは特定のサイズに特化しているため、このようなメモリのフラグメンテーションを防いでいます。

Linuxのスラブアローケータ

Linuxでは3種類のスラブアローケータの実装があり、カーネルコンフィギュレーションで使用するアロケータを変更できます。Linux 2.6.23よりslubがデフォルトのスラブアローケータとなっています。ディストリビューションによってはslabを選択しているものもあります。

slabアローケータ

slub登場以前はデフォルトのスラブアローケータでした。Solarisのスラブアローケータと同様の設計方針で実装されています。 スラブの管理にはスラブの状態に応じて複数のリストを使用します。使用するリストは、使用中オブジェクトが存在しないempty list、使用中オブジェクトと空きオブジェクトが存在するpartial list、全てオブジェクトが使用中のfull listの3つのリストがあります。 また、cache coloringと呼ぶ領域があり、ページフレーム内で空きオブジェクトの開始位置を他のスラブキャッシュとずらすために使用します。この機能は、スラブが複数存在する場合に、キャッシュラインの競合が起きないようにするための仕組みです。

slobアローケータ

シンプルな実装で、K&Rアローケータとも呼ばれています(注1)。slobアローケータはLinuxで使用可能なスラブアローケータの一つですが、slabやslubアローケータと違い、スラブをオブジェクトのサイズによって分けていません。一つのスラブで様々なサイスを取り扱います。これはプログラミング言語Cにおけるmalloc関数の実装と同様で、これがK&Rアローケータとも言われる所以です。

注1 プログラミング言語C で紹介されているUnixでのmalloc関数の実装に由来します。

slubアローケータ

slubアローケータはLinux固有のアローケータの実装です。本書ではslubアローケータについて解説します。

スラブオブジェクトの管理

slubアローケータでは空きスラブオブジェクトの管理はメタデータを別途持たせずに、スラブオブジェクトそのものに次の空きオブジェクトのアドレスを書き込みます。これにより、空きオブジェクトを管理するために別途メタデータを使用しなくてすむように設計されています。アドレスはスラブオブジェクトの先頭に書き込みます。最後の空きスラブオブジェクトには次のオブジェクトが無いため、NULLを設定します。

f:id:masami256:20190509234329p:plain

図_空きスラブオブジェクトの管理

使用中のスラブオブジェクトは特に管理を行いません。これはオブジェクトが不要になってオブジェクトを解放するときは、そのスラブオブジェクトのアドレスは自明なため、解放対象のオブジェクトが一意に決まるためです。

slubアロケータのスラブキャッシュはfreelist変数により、次回のスラブオブジェクト要求時に返却する空きスラブオブジェクトを参照しています。これにより、スラブ内に空きスラブオブジェクトが存在する間は高速にスラブオブジェクトを返却することができます。slubアローケータはスラブオブジェクトを返却する前に、次回のスラブオブジェクトのalloc時に返却するオブジェクトを参照するようにします。もし、最後の空きオブジェクトを返却した場合は、NULLを参照するようになります。この場合、次回の空きオブジェクト確保要求時に空きスラブオブジェクトの探索、もしくはスラブの作成を行うことになります。

f:id:masami256:20190509234421p:plain

図_空きスラブオブジェクトの確保

スラブオブジェクトを解放する場合、解放するスラブオブジェクトを次回のalloc要求時に返却するようにします。この時に次回のalloc要求時に返却予定だったスラブオブジェクトのアドレスを解放するスラブオブジェクトの先頭に書き込みます。

f:id:masami256:20190509234516p:plain

図_スラブオブジェクトの解放

slubアローケータではオブジェクトの確保に使用するスラブを常にCPUごとのメモリ域に置き、kmalloc関数などでスラブオブジェクトを要求された場合、このスラブから返却を行います。

f:id:masami256:20190509234657p:plain

図_percpuエリアにある構造体からスラブキャッシュの参照

使用中リスト

slubアローケータではスラブを管理する2つのリストがあります。一つはCPUと紐付いているスラブを管理するリストで、もう一つは、CPUとは紐付いておらず、スラブが所属するNUMAノード毎に管理するリストです。 CPUと紐付いている使用中リストに登録するスラブは1つだけ空きオブジェクトが存在するスラブです。また、このリストに登録できるスラブの数はcpu_partial変数にて設定されています。この値はスラブキャッシュ作成時に決定します。このリストはpage構造体のnext変数にてつながっています。この使用中リストは図_CPUに紐づく使用中リストのようになります。

f:id:masami256:20190509235150p:plain

図_CPUに紐づく使用中リスト

NUMAノードごとのは使用中リストは図_NUMAノードごとの使用中リストのように連結リストにて管理します。このリストには登録するスラブの上限はありません。

f:id:masami256:20190509235318p:plain

図_NUMAノードごとの使用中リスト

CPUと紐付いているスラブも、使用中リストにあるスラブもいずれも使用中ですが、CPUと紐付いているスラブは使用中リストでは管理しません。何かしらの理由でCPUとの紐付けを外す時に、CPUごとに保持しているスラブを使用中リストへ移動します。例えば、スラブ中に空きオブジェクトがなくなり、使用中リストから空きオブジェクトのあるスラブを探し、見つかったスラブとCPUごとのメモリ域ににあるスラブと交換したり、または、新規にスラブを作成した時に、作成したスラブをCPUごとのメモリ域に置き、CPUから参照されていたスラブを使用中リストに繋げるなどです。

CPUがスラブオブジェクトを取得するのに使用しているスラブはいずれの使用中リストの管理下にありません。スラブから空きオブジェクトが無くなった等の理由により、オブジェクトの取得対象のスラブで無くなった時に使用中リストに移動します。

スラブのマージ機能

slubとslabアロケータは同サイズのスラブキャッシュを一つのスラブキャッシュで管理する機能があり、デフォルトでこの機能は有効になっています。この機能を使用しない場合、128バイトのスラブオブジェクトを使用する機能が2つ存在した場合、スラブアローケータは2つの128バイトのスラブキャッシュを管理する必要があります。これは管理コストが増えたり、使用するページが違うため、CPUキャッシュヒット率が悪くなります。このような問題を解決するため、slab・slubアローケータでは同サイズのスラブキャッシュは一箇所で管理する仕様となっています。

マージの機能はslabとslubアローケータで詳細は異なっていて、slubアローケータでは、新規作成するスラブオブジェクトが以下の条件に当てはまる場合、新規にスラブキャッシュを作成せずに、既存のスラブキャッシュへのエイリアスを設定します。

  1. スラブオブジェクトのサイズが同一
  2. コンストラクタを使用しない
  3. slubのデバッグ機能を使用しない
  4. スラブ作成時のフラグにマージ機能を使用しないフラグが設定されていない

4のフラグについては「スラブキャッシュ生成時に設定するフラグ」にて説明します。

Chache Coloring

Cacje Coloringはスラブオブジェクトの開始アドレスをスラブ毎にずらすことによって、CPUのキャッシュライン競合を軽減させる仕組みです。slabアローケータはこの機能を採用していますが、 slubアローケータでは採用していません。CPUのキャッシュのライン調整に関してはCPUに任せる方針となっています(注1)。

注1: http://lkml.iu.edu/hypermail/linux/kernel/0808.0/1632.html

スラブの不活性化

slubアローケータはスラブオブジェクトを現在のCPUから参照されているスラブから返却します。参照されていないスラブはNUMAごとの使用中リストに繋がれますが、CPUごとのメモリ域にあるスラブをこの領域から外し、NUMAノードごとの使用中リストに移動することをスラブの不活性化と呼びます。

frozen状態

slubアローケータではスラブに対してfrozenという状態を設定します。fronzen状態にあるスラブはページの属性にPG_activeが設定されます。kmem_cache_alloc関数などによるオブジェクトの取得はこのスラブから行います。他のCPUはこのスラブからオブジェクトを取得することは出来ず、空きオブジェクトを追加する操作のみ行なえます。 CPUと紐付いているスラブはfrozen状態となり、スラブの不活性化によりCPUとのひも付けがなくなり、使用中リストにつなぐ時にスラブはfrozen状態が解除されます。

スラブキャッシュの情報

カーネルが管理しているスラブオブジェクトは/sys/kernel/debugディレクトリにて確認できます。Acpi-Namespaceなどシンボリックリンクとなっているものはslubアローケータのスラブマージ機能により、同サイズのオブジェクトを管理するスラブキャッシュとマージを行ったためです。

$ ls -l /sys/kernel/slab/  
total 0  
drwxr-xr-x 94 root root 0 May 20 20:19 ./  
drwxr-xr-x 11 root root 0 May 20 20:14 ../  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Namespace -> :t-0000040/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Operand -> :t-0000072/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-Parse -> :t-0000048/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-ParseExt -> :t-0000072/  
lrwxrwxrwx  1 root root 0 May 20 20:19 Acpi-State -> :t-0000080/  
lrwxrwxrwx  1 root root 0 May 20 20:19 aio_kiocb -> :t-0000128/  
drwxr-xr-x  2 root root 0 May 20 20:19 anon_vma/  
lrwxrwxrwx  1 root root 0 May 20 20:19 anon_vma_chain -> :t-0000064/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000016/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000032/  
drwxr-xr-x  2 root root 0 May 20 20:14 :at-0000040/  

図_/sys/kernel/slabディレクト

ディレクトリ名には:tから始まるものや:atから始まるものがあります。これらのprefixは、スラブキャッシュ作成時に指定するflags引数に渡した(kmem_cache構造体のflags変数に設定されます)フラグを表します。表_slab_のprefixにフラグとprefixの対応を示します。:atや:dtなどはSLAB_CACHE_DMAとSLAB_NOTRACKの複合となります。これらのprefixはフラグが設定されていれば、そのprefixが使われるのですが、:tに関してはkmem_cache構造体のflagsとSLAB_NOTRACKのAND演算の結果が0の場合に設定されます。これはSLAB_NOTRACKがデバッグ機能の設定のため、値が0に設定されているためです。各フラグの内容については「スラブキャッシュ生成時に設定するフラグ」にて説明します。

prefix フラグ
:d SLAB_CACHE_DMA
:a SLAB_RECLAIM_ACCOUNT
:F SLAB_DEBUG_FREE
:t SLAB_NOTRACK

表_slabのprefix

個々のスラブキャッシュのディレクトリには以下のようなファイルが存在します。これらがスラブキャッシュの詳細になります。これらのファイルでスラブキャッシュが有効にしている機能などを確認できます。図_at-0000032ディレクトリは:at-0000032ディレクトリの様子です。

$ ls
./       align        cpu_partial  destroy_by_rcu  min_partial  objects_partial  partial          red_zone                  sanity_checks  slabs_cpu_partial  total_objects
../      alloc_calls  cpu_slabs    free_calls      objects      objs_per_slab    poison           remote_node_defrag_ratio  shrink         slab_size          trace
aliases  cache_dma    ctor         hwcache_align   object_size  order            reclaim_account  reserved                  slabs          store_user         validate

図_at-0000032ディレクト

slubアローケータのデータ構造

slubアローケータの主要な構造体には以下の4つの構造体です。

  • kmem_cache構造体
  • kmem_cache_cpu構造体
  • kmem_cache_node構造体
  • page構造体

これら4つの構造体のうち、page構造体以外はスラブアローケータのための構造体です。 スラブアローケータの核となる構造体はkmem_cache構造体です。kmem_cache構造体はslabやslubなど、スラブアローケータの実装によってデータ構造は違います。

名前 内容
cpu_slab cpuに紐付いているスラブキャッシュ
flags スラブキャッシュ生成時に指定したフラグ
min_partial 使用中のスラブキャッシュを保持できる最小の数。デフォルトは5、最大で10
size object_sizeをalignの境界に整列したサイズ
object_size メタデータを含まないスラブオブジェクトのサイズ
offset オブジェクトのサイズ+offsetで次の空きスラブオブジェクトを指す
cpu_partial cpu毎に保持できる使用中スラブキャッシュ数
oo スラブキャッシュ作成時に確保するページ数のオーダー
max スラブキャッシュ作成時に確保するページ数のオーダー数最大値
min スラブキャッシュ作成時に確保するページ数のオーダー数最小値
allocflags slabオブジェクトを確保する際に使用するgfpフラグ
refcount スラブマージ機能によってマージされたスラブキャッシュの数
ctor スラブキャッシュ作成時に呼び出すコンストラクタ関数
inuse スラブオブジェクトの使用数
align オブジェクトをsizeof(void *)の境界に整列した値。CPUのキャッシュラインに整列する場合は、先にCPUのキャッシュラインに合わせる
name スラブキャッシュの名前
list 作成したスラブキャッシュのリスト
memcg_params memory cgroup用の設定
remote_node_defrag_ratio 他のNUMAノードからスラブオブジェクトを取得する割合
node NUMAノード毎のスラブキャッシュ情報

表_kmem_cache構造体

slubオブジェクトのsize、object_sizeは図_スラブオブジェクトのようになります。

f:id:masami256:20190509235434p:plain

図_スラブオブジェクト

kmem_cache_cpu構造体はcpuコア毎にデータを保持します。page変数はpage構造体で、この変数はオブジェクトの返却を行うスラブのpage構造体を指します。 partial変数は使用中のスラブをつなぐリストとして使用しますが、list_head構造体は使用せず、page構造体のnext変数を利用してリスト管理を行います。このリストサイズはkmem_cache構造体のcpu_partial変数で設定します。

名前 内容
freelist 次の空きスラブオブジェクトへのポインタ
tid システムでユニークなトランザクションID
page スラブオブジェクトを管理しているページ
partial 使用中スラブの小規模なリスト

表_kmem_cache_cpu構造体

kmem_cache_node構造体はNUMAノード毎のデータを保持する構造体です。この構造体のデータ構造はスラブアローケータにより違い、ifdefによって、スラブアローケータ固有のメンバ変数が定義されます。slobアローケータでは使用しません。

名前 内容
list_lock sl[au]bアローケータ共通
nr_partial 使用中リストで管理しているデータ数
partial 使用中リストにあるスラブが使用しているpage構造体を管理するリスト

表_kmem_cache_node構造体

page構造体はページフレームを管理する構造体ですが、一部のフィールドはスラブアローケータが使用します。

名前 内容
freelist 空きオブジェクトを参照
counters オブジェクト数などを管理するカウンター
inuse 使用中のスラブオブジェクト数
object スラブオブジェクト数
frozen ページのfronzen状態
next 次の使用中リストへのポインタ
pages スラブキャッシュに使用しているページフレーム数
slab_page kmem_cache_cpu構造体が指すpageを指す
slab_cache ページが管理しているスラブへのポインタ

表_page構造体

page構造体はスラブが使用するページフレームに該当します。page構造体はメンバ変数に共用体を多用しているため、データ構造が複雑になっています。そのため、ソースコードを読み進める際には注意が必要です。 特によく使用するfrozen状態を設定するfrozen変数は構造体と共用体が入り組んだデータ構造のメンバ変数となっています。counters変数により1回の処理でfrozen変数を含めてデータを一括で設定した後に、frozenだけ変更する場合もあります。

データの関連

slubアローケータで使用するデータの関連

cpu、kmem_cache_cpu構造体、kmem_cache構造体、kmem_cache_node構造体、page構造体の関連は図_スラブで使用する構造体の関連のようになります。

f:id:masami256:20190509235714p:plain

図_スラブで使用する構造体の関連

スラブのライフサイクル

スラブがkmem_cache_cpu構造体から参照されているかか、kmem_cache_node構造体から参照されているかでfrozen状態が変わります。この時のfrozen状態を図_スラブの状態に示します。

f:id:masami256:20190509235746p:plain

図_スラブの状態

(1)はスラブを作成した時の状態です。スラブを作成した場合は必ずkmem_cache_cpu構造体のpage変数からスラブを参照して、このスラブからスラブオブジェクトを返却します。この状態はkmem_cache_cpu構造体からスラブが参照されているのでfrozenは1となります。

(2)はCPUごとのメモリ域からスラブが外れた状態です。この状態はスラブ内のスラブオブジェクトが全て使用中になる、または現在のCPUが使用するNUMAノードがスラブのNUMAノードと違う場合に、kmem_cache構造体のpage変数が参照するスラブの差し替えた場合です。この状態はスラブはkmem_cache_cpu構造体から参照されていませんのでfrozenは0となります。この状態に変更するために後述するdeactivate_slab関数によるスラブの不活性化処理を行います。

(3)の状態はスラブから空きスラブオブジェクトがなくなり、不活性化されたスラブがスラブオブジェクトの解放によりスラブ内に1つだけ空きオブジェクトができた場合、もしくはkmem_cache_cpu構造体が参照しているスラブのNUMAノードがCPUのNUMAノードと違った場合に、kmem_cache_node構造体の使用中リストからkmem_cache_cpu構造体の使用中リストで参照するように変更する時に起こります。この場合は、kmem_cache_cpu構造体からスラブが参照されるようになるので、frozenは1となります。

スラブアローケータの機能

スラブキャッシュ、スラブ、スラブオブジェクトに対する主な絹おは表_スラブに対する主な操作に示したものがあります。これらのうち、kmem_cache_で始まっている関数はシンボルが公開されていて、他のサブシステムから使用することができます。スラブの作成とスラブの不活性化はslub内部の処理です。

内容 対応する関数
スラブキャッシュの作成 kmem_cache_create
スラブキャッシュの破棄 kmem_cache_destroy
スラブオブエジェクトの確保 kmem_cache_alloc
スラブオブジェクトの解放 kmem_cache_free
スラブの作成 new_slab_objects
スラブの不活性化 deactivate_slab

表_スラブに対する主な機能

スラブキャッシュの作成

スラブキャッシュの作成はkmem_cache_create関数を使用します。スラブキャッシュの作成は各スラブアローケータ共通の処理と、各スラブアローケータ固有部分に分かれています。 スラブキャッシュの作成では最初にスラブがマージ可能かチェックします。作成しようとしたスラブキャッシュがマージ可能な場合は、マージ先のスラブキャッシュの参照数を増やします。既存のobject_size変数と作成しようとしていたスラブキャッシュのオブジェクトの大きさを比較し、大きいほうをobject_sizeに再設定や、オブジェクトの使用数などの設定も行います。最後に/sys/kernel/slabにシンボリックリンクファイルを作成します。シンボリックリンクの指す先はマージ先のスラブキャッシュです。マージに成功した場合はkmem_cache_create関数の処理はこれで完了となります。 スラブキャッシュのマージを行わなかった場合は、スラブキャッシュの新規作成を行います。 スラブキャッシュを新規作成する場合はdo_kmem_cache_create関数にて行います。まず、作成するスラブキャッシュのためにkmem_cache構造体のメモリを確保します。この時もスラブアローケータによりメモリを確保します。ここで、スラブキャッシュの名前、オブジェクトサイズなどのデータを設定します。また、memcgサブシステムで使用するmemcg_params変数の設定も行います。この時にmemcg_chache_params構造体のis_root_cache変数をtrueに設定しています。is_root_chache変数はスラブキャッシュの削除などで使用します。次に、各スラブアローケータ固有の処理を_kmem_cache_create関数にて行います。ここでの処理は主に、スラブキャッシュのオブジェクトサイズ、ページフレームを確保する時のオーダー、kmem_cache_alloc関数時に使用するGFPフラグなどの設定を行います。kmem_cache構造体のcpu_partial変数は各cpu毎にいくつの使用中リストを持てるかの設定です。この設定は表cpu_partial変数の設定にある条件できまります。条件は上から順にチェックします。

条件
kmem_cache_has_cpu_pertial関数がfalseを返す 0
kmem_cache構造体のsize変数がPAGE_SIZEより大きい 2
kmem_cache構造体のsize変数が1024より大きい 6
kmem_cache構造体のsize変数が256より大きい 13
全ての条件に当てはまらない場合 30

表_cpu_partial変数の設定

kmem_cache_has_cpu_partial関数はカーネルの設定でスラブアローケータの設定を有効にしない限り、常にtrueを返すのでcpu_partialが0に設定されることはありません。

次に、各NUMAノードで使用するkmem_cache_node構造体の初期化を行います。この構造体の初期化はリスト構造の初期化やスラブ数、スラブオブジェクト数を0にするなど目立った処理は特にありません。続いて、kmem_cache_cpu構造体の初期化を行いますが、こちらも特筆すべき内容はありません。__kmem_cache_create関数では最後に、作成したスラブキャッシュをsysfsより見えるように、sysfsのエントリーを作成して終了します。 __kmem_cache_create関数に成功し、do_kmem_cache_create関数に戻ったら作成したスラブキャッシュをスラブキャッシュを管理するリストに登録します。これでスラブキャッシュの作成が完了し、スラブオブジェクトの確保ができるようになります。ただし、この時点ではスラブオブジェクトはありませんので、初回のkmem_cache_alloc関数使用時にスラブを作成します。

スラブキャッシュ生成時に設定するフラグ

スラブキャッシュ生成時に設定するフラグのうちデバッグ向けのフラグを表_スラブキャッシュ生成時のフラグに示します。これらフラグの中で、SLAB_DESTROY_BY_RCU、SLAB_DESTROY_BY_RCU、SLAB_NOLEAKTRACEが設定されている場合、マージ機能は使用しません。

フラグ 内容
SLAB_HWCACHE_ALIGN スラブオブジェクトをCPUのキャッシュの境界に合わせる
SLAB_CACHE_DMA GFPフラグにGFP_DMAを設定し、DMA向けのスラブキャッシュを作成する
SLAB_PANIC kmem_cache_crate関数が失敗した場合にカーネルパニックする
SLAB_DESTROY_BY_RCU スラブキャッシュの解放時にRCUを使用して、非同期に行う
SLAB_MEM_SPREAD cpuset機能を使用しているプロセスの場合、cpusetで設定されたcpuのノードよりメモリを確保する(slabアローケータ専用)
SLAB_NOLEAKTRACE kmemleakによるトレースを行わない
SLAB_NOTRACK kmemcheckによるチェックを行わない
SLAB_RECLAIM_ACCOUNT スラブオブジェクトの回収を許する
SLAB_TEMPORARY スラブオブジェクトが短命と設定する。SLAB_RECLAIM_ACCOUNTと同様に回収可能となる

表_スラブキャッシュ作成時のフラグ

空きオブジェクトの管理

スラブ内の空きオブジェクトは、オブジェクト内のポインタによって次の空きオブジェクトを参照します。オブジェクトは使用中はデータが書き込まれていますが、オブジェクトが未使用の場合は重要なデータは存在せず、アクセスもされないため、オブジェクトの先頭に次の空きオブジェクトのアドレスを書き込んでいます。オブジェクトが不要になって解放された場合も、解放するオブジェクトの先頭に次の空きオブジェクトのアドレスを書き込みます。最後の空きオブジェクトは次の空きが無いため、NULLを設定します。ソースコード上は空きオブジェクトをfreelistと呼んでいますが、一般的な単方向連結リストなどと違い、次のオブジェクトを指す専用のポインタを持っておらず、未使用領域を次のオブジェクトを指すポインタとして使用していまます。

空きスラブオブジェクトの設定

スラブオブジェクトを作成するときは、ループしながらスラブ内の作成できるオブジェクトの数だけを設定していきます。 kmem_cache構造体の変数をs、s->offsetを0、s->sizeを16、オブジェクトのポインタをpとした時に、以下の手順で空きオブジェクトのリストを設定していきます。

  1. pの初期値をpageの先頭アドレスにする
  2. 次の空きオブジェクトのアドレスをpのアドレス + s->sizeとする
  3. pのアドレス + s->offsetの位置に、次のオブジェクトのアドレスを書き込む
  4. pの指すアドレスを現在のアドレス + s->sizeの位置に変更する
  5. 2〜4を繰り返す
  6. 作成できるオブジェクト数分、設定を行ったら、次の空きオブジェクトのアドレスをNULLに設定する。

上記の手順を図にすると「図_スラブオブジェクトのセットアップ」のようになります。

f:id:masami256:20190509235826p:plain

図_スラブオブジェクトのセットアップ

スラブオブジェクトを解放する場合は、以下の手順によって空きオブジェクトのリストを更新し、次のkmem_cache_alloc関数時には解放対象のスラブオブジェクトを返却するようにします。

  1. 解放対象のスラブオブジェクトが次の空きオブジェクトとして、現在設定されている次の空きオブジェクトを指すようにする
  2. 現在のCPUに設定されているkmem_cache_cpu構造体のfreelist変数を解放対象のスラブオブジェクトに変更

スラブオブジェクトの確保

スラブオブジェクトの確保はkmem_cache_alloc関数を使用します。kmem_cache_alloc関数はslab、slub、slobの各スラブアローケータ共通のインターフェースですが、実装はアローケータによって異なります。

スラブオブジェクトの確保におけるフローとして、下記の4パターンがあります。1はfastpathと呼ばれるパターンで、現在のCPUで使われているスラブからオブジェクトを確保できる場合で、最も高速に処理が完了します。2〜4のパターンはslowpathとなります。2はAとBのパターンがありますが、これはオブジェクトの確保時にノード番号を指定していない場合(A)・した場合(B)です。ここでオブジェクトが確保できなければ、全NUMAノードより使用中のスラブを探し、そこからオブジェクトの確保を試みます。全ノードを探索したが空きオブジェクトが見つからなかった場合は新規にスラブを作成します。

  1. 現在のCPUにあるスラブからオブジェクトを確保
  2. (A)ローカルメモリにある使用中のスラブからオブジェクトを確保。または、 (B)指定されたNUMAノードにある使用中のスラブからオブジェクトを確保
  3. 全NUMAノードから使用中のスラブを探し、そこからオブジェクトを確保
  4. スラブを新規に作成し、そこからオブジェクトを確保

スラブオブジェクトを確保する流れは図_スラブオブジェクトの確保フローのようになります。

kmem_cache_alloc()                      (1)
  -> slab_alloc()                       (2)
    -> slab_alloc_node()                (3)
      -> __slab_alloc()                 (4)
        -> deactivate_slab()            (5)
        -> get_freelist()               (6)
        -> new_slab_objects()           (7)
          -> get_partial()              (8)
            -> get_partial_node()       (9)
            -> get_any_partial()        (10)
          -> new_slab()                 (11)
            -> allocate_slab()          (12)
        -> set_freepointer()        (13)

図_スラブオブジェクトの確保フロー

スラブオブジェクトの取得はkmem_cache_alloc関数を使用しますが、実際の処理はslab_alloc_node関数から始まります。slab_alloc_node関数は、最初に現在のCPUに設定されているkmem_cache_cpu構造体を取得します。次にこのkmem_cache_cpu構造体より次の空きオブジェクトへのポインタ(freelist変数)を取得します。また、page構造体も取得します。ここで、node_match関数を呼び出し、次の2条件のいずれかを満たす場合はfastpathとなり、freelist変数をスラブオブジェクトとして呼び出し元に返却します。

  1. スラブが作成済み(page構造体がNULLでない)
  2. スラブオブジェクトを確保するNUMAノードが指定されていて、pageが所属するノードが指定されたノードと同じ

fastpathの場合は、次のkmem_cache_alloc関数の呼び出しに備え、freelistを次の空きオブジェクトを指すようにします。この時にCPU固有の先読み命令(*注1)にてfreelistをCPUのキャッシュに読み込みます。この手法により、次回のkmem_cache_alloc関数時にデータをメモリからでなくCPUのキャッシュから読み出せるようにすることで、高速化を図っています。

slowpathの場合は、__slab_alloc関数を実行します。ここでは、ローカルメモリにあるスラブより空きオブジェクトを探索します。kmem_cache_alloc関数によるスラブオブジェクトの確保では、NUMAノードの指定は行わないため、必ずローカルメモリを優先して探索します。ローカルメモリからの探索では、再度、現在のCPUに設定されているスラブキャッシュを取得し、このスラブキャッシュより参照されているスラブを取得します。 もし、スラブが存在しない場合、新たなスラブの作成に進みます。スラブの新規作成処理は後述します。スラブが存在した場合はスラブが所属するNUMAノードのチェックをnode_match関数を用いて行います。(この処理は、後述するkmem_cache_cpu構造体の更新後にジャンプして来る場合があります。)この場合もNUMAノードの指定は行っていません。よって、page構造体がNULLでなければ(スラブ用にページフレームが確保されている)、このスラブが利用可能なスラブオブジェクトの候補となります。 スラブに対する次のチェックとして、PageActiveマクロを利用し、スラブとしてい使用しているページがアクティブか調べます。もし、アクティブで無い場合、gfpフラグにALLOC_NO_WATERMARKSが設定されているか確認します。ALLOC_NO_WATERMARKSはウォーターマークのチェックをしないという設定です。このフラグがセットされていない場合は、deactivate_slab関数を使用して、スラブを不活性化します。この処理は「スラブの不活性化」で説明します。

次にpageのfreelist変数を対象にチェックを行っていきます。まず、kmem_cache_cpu構造体のfreelistが空きオブジェクトを指しているか確認し、参照先がスラブオブジェクトで無い場合は、page構造体のfreelist変数が空きオブジェクトを指しているかget_freelist関数を実行して調べます。

kmem_cache_cpu構造体のfreelist、もしくはpage構造体のfreelist変数が空きオブジェクトを指している場合、kmem_cache_cpu構造体のfreelist変数に空きオブジェクトを指しているfreelist変数を設定します。これでCPUが参照するスラブオブジェクトがfreelistになります。この処理にはload_freelistラベルが設定されて、スラブの新規作成が成功した場合にも実行します。スラブオブジェクトの設定を行ったら、呼び出し元の関数にこのスラブオブジェクトを返して、__slab_alloc関数を終了します。

スラブを新規に作成する前に、kmeme_cache_cpu構造体のpartial変数がNULLでないかチェックします。これまでの処理ではkmem_cache_cpu構造体のpage変数に設定されているスラブを対象に空きオブジェクトを探索しましたが、partial変数のチェックで、kmem_cache_cpu構造体の使用中リストに空きオブジェクトが存在するかチェックします。使用中のスラブオブジェクトが存在している場合に、kmem_cache_cpu構造体を更新し、スラブオブジェクトを取得する対象のスラブをこのスラブにします。この処理でkmem_cache_cpu構造体のpage変数とpartital変数が同じスラブを指すようになります。そして、partial変数はスラブが指す次のpage構造体に設定します。

そして、kmem_cache_cpu構造体のfreelist変数はNULLにします。これで、kmem_cache_cpu構造体のpageとpartialの設定を更新しfreelistがNULLを指すことで、、空きスラブオブジェクトは存在してないという状態になります。ここまで行ったら、slowpathの最初の処理にジャンプします。この時、ページフレームはすでに存在しているので、node_match関数によるノードのチェックから開始します。こまでの処理でローカルメモリからはスラブオブジェクトが見つからなかったため、new_slab_objects関数を実行します。この関数は大きく2つの処理があり、1つ目の処理はリモートメモリより空きオブジェクトを検索します。この処理で空きオブジェクトが見つかれば、このオブジェクトを返却できます。リモートメモリより空きオブジェクトを探すのはget_partial関数にて行います。

get_partial関数で空きスラブオブジェクトを探索する場合は、まずスラブオブジェクトを確保するNUMAノードが指定されているか確認し、ノードが指定されていなければローカルメモリ、指定があればそのノードよりスラブオブジェクトの取得を試みます。指定したノードよりスラブオブジェクトを確保する処理はget_pertial_node関数にて行います。この関数は指定されたNUMAノードにあるスラブのリスト(kmem_cache_node構造体のpartial変数)を辿り、空きスラブオブジェクトが存在するスラブを探します。スラブ中の空きスラブオブジェクトの探索はaqcuire_slab関数にて行います。スラブ内に空きスラブオブジェクトが見つかった場合は、そのスラブをfrozen状態に変更します。そして、使用中リストより、スラブのページフレームを外します。 aquire_slab関数にてり空きスラブオブジェクトが見つかった場合、スラブは以下のいずれかの状態になります。

  1. スラブオブジェクト確保したことで空きスラブオブジェクトなくなる
  2. スラブオブジェクトを確保したが、まだ空きスラブオブジェクトが存在する

1の場合は、kmem_cache_cpu構造体のpage変数にスラブが使用してるpageを設定します。 2の場合は、kmem_cache_cpu構造体のpartial変数にスラブが設定されているか確認し、スラブが設定されていた場合は、そのスラブのfrozen状態を解除し、そして、そのスラブに使用中のスラブオブジェクトがなく、使用中スラブの数が下限以上あればそのスラブはslab_discard関数にてスラブの不活性化処理を行います。使用中のスラブオブジェクトがあれば使用中リストにそのスラブを追加します。これで元々partial変数に設定されていたスラブの処理が完了したので、新しいスラブのための設定を行います。設定するのはpage構造体のデータで、オブジェクト数の設定などを行います。 スラブ中の空きスラブオブジェクト数が十分にあれば、この見つかったスラブオブジェクトを返却します。もし、空きスラブオブジェクト数が十分になければ、別のスラブを探索します。

ローカルメモリもしくは指定したノードよりスラブオブジェクトを確保できなかった場合は、全スラブを対象に空きスラブオブジェクトを探索します。これはget_any_pertial関数にて行いますが、空きスラブオブジェクト探索するか否かは、kmem_cache構造体のremote_node_defrag_ratioの設定値により変わります。remote_node_defrag_ratioが0の場合は他のノードの探索は行いません。次にCPUのタイムスタンプカウンタ値を取得し、この値を1024で割った余りがremote_node_defrag_ratioより小さかった場合も他ノードの探索は行いません。他のノードを探索しなかった場合は、new_slab_objects関数に戻り、新規にスラブオブジェクトを作成します。remote_node_defrag_ratioの値はカーネル内部とsysfsでエクスポートするときで桁数が違います。sysfs経由でこの値を設定/確認する場合は0~100までの数値を使用しますが、カーネル内部ではその値を10倍して使用します。デフォルト値は1000となっています。 スラブオブジェクトを探索する場合は、スラブが使用するZONEより、そのNUMAノードに存在するkmem_cache_node構造体を取得します。そして、そのノードからの空きスラブオブジェクトの確保が許可されている場合は、get_partial_node関数を使用して、空きスラブオブジェクトを探します。これでオブジェクトが見つかれば、そのオブジェクトを返却します。

リモートメモリを検索しても利用可能なかった場合は、new_slab_objects関数の2段階目の処理としてスラブの新規作成処理を行います。スラブの作成処理はnew_slab関数にて行います。スラブの新規作成では、まずページフレームを確保行います。ページフレームの確保とpage構造体の基本的な設定はallocate_slab関数で行います。

最初にpageの確保処理をalloc_slab_page関数にて行います。pageの確保するNUMAオードが指定されていないければ、ローカルメモリから、指定されている場合は対象のNUMAノードよりpageの確保を試みます。pageを確保する時のオーダーはkmem_cache構造体のoo変数に設定されている値を使用します。もしページフレームの確保に失敗した場合、オーダーをkmem_cache構造体のmin変数に設定されている最小のオーダー数に変更して再度alloc_slab_page関数を実行します。これでもページフレームを確保できなければNULLを返却します。

allocate_slab関数にてpageが確保できたらpage構造体のobjects変数にオブジェクト数の設定を行います。また、ページフレームを確保したZONEの状態を変更します。kmem_cache構造体のflags変数にNR_SLAB_RECLAIMABLEが設定されている場合はページ回収可能なページ数を、設定されていない場合はページ回収不可能なページ数を既存の値に追加します。ここまでの処理でスラブのためのpageの確保処理が完了となります。ページフレームの確保ができたら、次にpage構造体への設定として、page構造体のslab_cache変数に現在のkmem_cache構造体を設定し、page構造体とスラブを関連付けます。ページフレームをスラブとしてに使用していることを示すために、pageにPG_slabビットを設定します。page構造体のpfmemallocが設定されている場合は、pageにPG_activeビットを設定します。

次に、pageに対してスラブオブジェクトを1つずつ設定していきます。この処理については「スラブオブジェクトの作成」を参照してください。 最後に、page構造体の設定で、freelist変数をpageの開始アドレスに設定します。pageの開始アドレスは1つ目のスラブオブジェクトのアドレスですので、page構造体のfreelistは最初の空きスラブオブジェクトを指すことになります。inuse変数にはpage構造体のobjects変数を設定します。frozen変数には1を設定し、pageの管理をslubアローケータが行っているとマークします。以上でスラブオブジェクトの作成処理が完了となり、呼び出し元にpage構造体を返却します。

new_slab関数の実行によりスラブの作成が成功した場合は、kmem_cache_cpu構造体の設定を行います。まず、現在のCPUに設定されているkmem_cache構造体よりkmem_cache_cpu構造体のcpu_slab変数を取得します。kmem_cache_cpu構造体のpage変数がスラブのページフレームを指している場合は、現在設定されているkmem_cache_cpu構造体を一旦無効化します。この処理は「kmem_cache_cpu構造体の初期化」にて説明します。そして、page構造体のfreelistを別の変数にコピーし、page構造体のfreelist変数はNULLにします。そして、kmem_cache_cpu構造体のpage変数にnew_slab関数で作成したスラブのページフレームを設定します。kmem_cache_cpu構造体の設定が完了したら、new_slab_object関数の引数で渡されたkmem_cache_cpu構造体の変数に、設定したkmem_cache_cpu構造体を設定します。よって、new_slab_object関数の呼び出しにより、呼び出し元の__slab_alloc関数で使用していたkmem_cache_cpu構造体の内容が変わることになります。new_slab_objects関数では返り値として、コピーしておいたpage構造体のfreelist変数を返却します。

new_slab_objects関数を呼び出した__slab_alloc関数では、スラブの作成に失敗した場合は、これ以上の処理を行えないため、NULLを返します。スラブの作成に成功した場合はload_freelistラベルにジャンプします。

slab_alloc_node関数では、fastpath、slowpathいずれの場合も、スラブオブジェクトを返す前にGFPフラグをチェックし、__GFP_ZEROフラグが設定されている場合は、スラブオブジェクトに0x0を書き込みます。また、スラブオブジェクト確保時にフック関数を登録することができ、これらが設定されている場合は、スラブオブジェクトの確保前、確保後に登録されているフック関数を実行します。これらのフック関数は通常デバッグ機能向けに使われていて、スラブを使用する側からは設定できません。

注1:x86x86_64の場合はprefetcht0命令。

スラブオブジェクトの解放

kmem_cache_alloc関数で確保したスラブオブジェクトはkmem_cache_free関数で解放します。スラブオブジェクトの解放処理は図_スラブオブジェクト解放のようになります。

kmem_cache_free()
  -> cache_from_obj()
  -> slab_free()
    -> set_freepointer()    // fastpath
    -> __slab_free()        // slowpath
      -> put_cpu_partial()
        -> unfreeze_partials()
      -> discard_slab()
      -> remove_partial()

図_スラブオブジェクトの解放

kmem_cache_free関数では実質的な処理はせず、slab_free関数にて解放の処理を行います。スラブオブジェクトの解放は、確保の場合と同じくfastpathとslowpathの2パターンがあります。解放対象のオブジェクトがCPUから参照されている場合がfastpathとなり、そうでない場合はslowpathとなります。fastpathの場合は、解放するオブジェクトを次の空きオブジェクトに設定します。この時の様子を図_スラブオブジェクト解放前と図_スラブオブジェクト解放後に示します。解放前はkmem_cache_cpu構造体のfreelistは解放するスラブオブジェクトの隣にあるスラブオブジェクトを指していますが、スラブの解放後は解放したスラブオブジェクトを指すようになります。fastpathの場合は、現在使用中のスラブに対してオブジェクトの解放処理を行うため、freelistの変更だけで済んでいます。

f:id:masami256:20190509235911p:plain

図_スラブオブジェクト解放前

f:id:masami256:20190509235927p:plain

図_スラブオブジェクト解放後

slowpathの場合は__slab_\free関数にて処理を行います。

まず、以下のループでスラブが使用するpage構造体のfreelistを設定します。この時にpage構造体のデータを一部保存する一時変数として、newを使用します。

  1. kmem_cache_node構造体がNULL出ない場合は、この構造体のロックを取得(ループ1回目はkmem_cache_node構造体はNULL)
  2. page構造体のfreelist変数をprior変数にコピーし、set_freepointer関数で、priorを次に返すオブジェクトに設定
  3. newにスラブが使用しているpage構造体のcountersをコピー
  4. newのスラブオブジェクト使用数をデクリメント
  5. スラブのfrozen状態をwas_frozen変数にコピー
  6. newのオブジェクト使用数が0またはpriorがNULLかつ、スラブがfrozen状態で無かった場合7の処理を行います
  7. page構造体のfreelist変数がNULLの場合、8の処理を行い、NULLでなかった場合は9の処理を行います。いずれの処理も終了したら10に移動します
  8. スラブのfrozen状態を1にセット
  9. スラブが所属するNUMAノードのkmem_cache_node構造体を取得
  10. cmpxchg_double_slab関数でスラブのpage構造体のfreelistとcounters変数をnewに設定したデータに更新
  11. cmpxchg_double_slab関数が変更を行わなかった場合はループを終了

7の処理はソースコード上ではfreelistのチェックだけでなく、kmem_cache_has_cpu_partial関数がtrueを返すことも条件の一つなのですが、スラブのデバック機能を使用しない場合はtrueが返ります。 8の処理でfrozen状態を1に設定していますが、これはスラブが使用中リストで管理されていなかった場合になります。

上述のループを終了した段階で、kmem_cache_node構造体が取得されていない場合で、スラブをfrozen状態に変更した場合は、スラブをkmem_cache_cpu構造体のpartial変数のリストにつなぎます。 スラブをkmem_cache_cpu構造体から外すにはput_cpu_partial関数を使用します。この処理中は割り込みを禁止します。

次に、以下の処理をループで行います。

  1. 現在のCPUが参照しているkmem_cache構造体よりkmem_cache_cpu構造体のpartial変数を取得してoldpage変数に代入
  2. oldpageがNULLで無い場合は3の処理を行い、NULLだった場合はXに移動します
  3. 使用中オブジェクト数(ループ1回目は0、4手順目で更新する)がkmem_cache構造体のcpu_partialより大きい場合、unfreeze_partials関数を実行してkmem_cache_cpu構造体のpartial変数に紐づく全てのスラブのforzne状態を解除
  4. 引数で渡されたpae構造体のpage変数のpage数、使用中オブジェクト数を更新命令
  5. page変数next変数に取得したoldpageを設定
  6. this_cpu_cmpxchg関数でkmem_cache構造体のcpu_slab変数が参照しているpartialを更新したpage変数に変更
  7. this_cpu_cmpxchg関数が更新を行わなかった場合はループを終了

unfreeze_partials関数はkmem_cache_cpuのpartialリストにあるスラブのfrozen状態に変更し、kmem_cache_nodeの使用中リストに登録します。この処理では2段階のループがあります。 page構造体のデータを更新するための作業用変数としてpage、oldとnewを使用します。

  1. kmem_cache_cpu構造体のpartialに設定されているpage構造体をpageに代入
  2. スラブが使用しているpage構造体が所属するNUMAノードのkmem_cache_node構造体を取得
  3. 取得したkmem_cache_node構造体が前回取得したノード違った場合、前回のkmem_cache_nodeがNULLでなければ、ロックを解除
  4. 取得したkmem_cache_node構造体のロックを取得
  5. pageに設定されているfreelistとcountersをoldにコピー
  6. oldに設定したcountersとfreelistをnewにコピー
  7. newのfrozen変数を0に設定
  8. __cmpxchg_double_slab関数でpage変数のfreelistとcountersをnewに設定した値に更新
  9. __cmpxchg_double_slab関数が値の更新を行わなかったら11に進み、値の更新を行った場合は5に戻り、freelistとcouters変数の更新を続けます
  10. newに設定されているスラブオブジェクトの使用数が0かつ、kmem_cache_nodeにある使用中スラブ数が十分にある場合、そのスラブは不活性化の対象とします。そうでない場合はスラブをkmem_cache_node構造体の使用中リストに登録します。

上記手順の5と6は同じデータを設定していますが、7手順目でfrozen状態を解除するため、8手順目でpage変数にはfrozen状態が解除されたデータとして登録されます。

ループを抜けた時にkmem_cache_node構造体で取得したロックを解除します。 そして、ループの10手順目で削除対象のスラブが設定した場合は、discard_slab関数を実行してスラブを不活性化します。この処理は「スラブの不活性化」にて説明します。

kmem_cache_cpu構造体のデータクリア

スラブを無効化する時にkmem_cache_cpu構造体に設定されているデータのクリアを行います。この処理はflush_slab関数で行います。この関数は最初にdeactivate_slab関数を実行し、スラブを無効化します。次に、以下の3変数の値を変更します。

スラブの不活性化

スラブの不活性化処理は、下記3処理を行います。

  1. frozen状態スラブのfreelistで管理されている未使用のスラブオブジェクトを、同じスラブのpage構造体のfreelistへ移動
  2. スラブのfrozen状態解除
  3. 移動後のスラブの状態により、使用中リストへ移動、もしくは使用中リストから削除

これらの処理により、kmem_cache_cpu構造体が使用しているスラブを差し替えることができるようになります。この処理が必要になるのは2パターンあります。

  1. スラブを新規作成し、既存のスラブと置き換える
  2. スラブが不要になった場合

1のパターンは、スラブオブジェクトの確保時に既存のスラブからはスラブオブジェクトが確保できなかったため、新規にスラブを作成する場合です。2のパターンはkmem_cache_free関数によるスラブオブジェクトの解放時に、スラブ内のオブジェクトが全て空きオブジェクトになり、かつ使用中リストの数が下限以上ある場合、もしくはkmem_cache_destroy関数にてスラブキャッシュ自体を削除する場合です。

スラブの不活性化処理を行うのはdeactivate_slab関数です。処理は大きく2段階に別れていて、1段階目は空きオブジェクトの移動。2段階目はスラブのfrozen解除と使用中リストへの追加・削除です。 空きオブジェクトの移動では、frozen状態のスラブにあるfreelist(kmem_cache_cpu構造体のfreelist変数)より、空きオブジェクトを全て、同スラブのpage構造体のfreelistへ移動します。

第2段階では、最初に1段階目の処理の後に、freelistがまだ空きオブジェクトを指しているか確認し、空きオブジェクトを指している場合は、freelistがpage構造体のfreelistに設定した空きオブジェクトを指すように変更します。 この時にテンポラリの変数として、old、newと呼ぶpage構造体を使います。空きオブジェクトを指していた場合は、newの使用中スラブ数を減らし、newのfreelistがfreelistを指すようにします。空きオブジェクトを指していない場合は、newのfreelistとoldのfreelistを同じに設定します。以降の処理で、page構造体の変数を参照する際はnewを使用します。

次に、スラブが使用しているページに対する操作を決定します。どのような操作を行うかはスラブの状態に応じて決定します。スラブの状態を表_スラブの状態に示します。

状態 内容
M_NONE 何もしない(初期値)
M_PARTIAL スラブには使用中のオブジェクトがある
M_FULL スラブに空きオブジェクトが無い
M_FREE スラブに使用中オブジェクトが無い

表_スラブの状態

スラブの状態は2つの変数(l、m)により管理します。両者とも初期値はM_NONEです。まずはmについて、表_スラブの状態設定のようにmの状態を設定します。これら状態のうちM_FULLはデバッグ用途のフラグですので、通常はM_FULL状態の場合の処理はありません。

条件 設定する状態
スラブに使用中のオブジェクトがなく、NUMAノード内の使用中リスト数が下限以上に存在する M_FREE
page構造体のfreelistが空きオブジェクトを指している M_PARTIAL
その他 M_FULL

表_スラブの状態設定

次にlとmが同値でなければ、スラブが使用しているページに対する操作を行います。最初にこのコードパスを通る場合、lにはM_NONEが設定されているため、この条件は必ず真になります。そして、以下の条件に合致する処理を行います。

条件 処理内容
l == M_PARTIAL 使用中リストからスラブを削除
l == M_FULL 空きオブジェクト無しリストからスラブを削除(このリストはデバッグ用のため、通常は存在しません)
m == M_PARTIAL スラブを使用中リストに追加
m == M_FULL スラブを空きオブジェクト無しリストに追加(このリストはデバッグ用のため、通常は存在しません)

この処理が終わった後にlの値をmの値に設定します。次に、引数で渡されたpageのfreelistとcountersがoldのfreelistとcountersが同じなら、これらをnewのfreelist、countersに変更します。変更は__cmpxchg_double_slab関数で行います。もし__cmpxchg_double_slab関数がfalseを返した場合は変更を行わず、2段階目の最初の処理にジャンプし、2段階目の処理を再度実行します。変更を行った場合は最後の処理として、mがM_FREEに設定されているか確認し、M_FREEが設定されている場合はスラブは不要と判断し、discard_slab関数を実行してスラブの削除を行います。

スラブの削除

kmem_cache_free関数や、スラブの不活性化などにより、スラブ内に使用中のオブジェクトがなくなった場合にスラブの削除処理を行います。スラブの削除を行うインターフェースとなる関数はdiscard_slab関数です。スラブの削除はkmem_cache構造体のflags変数を見て、SLAB_DESTROY_BY_RCUが設定されている場合は、RCUを使用して遅延実行し、設定されていない場合はそのまま削除処理を行います。どちらの場合も削除処理として__free_slab関数を使用します。

discard_slab関数は削除対象のスラブが所属するNUMAノードのkmem_cache_node構造体を取得し、nr_slabs変数からスラブ数の減算と、解放することになるスラブオブジェクト数をobjects変数から減算します。次に、free_slab関数にて、kmem_cache構造体のflags変数にSLAB_DESTROY_BY_RCUが設定されているか確認し、設定されている場合はスラブをスラブ削除用のRCUリストにつなぎ、処理を終了します。 SLAB_DESTROY_BY_RCUが設定されていない場合は__free_salb関数にてスラブの削除を行います。RCUを使用する場合もスラブの削除実行には__free_slab関数を使用します。 __free_slab関数が呼ばれる時点で、スラブオブジェクトに対する解放処理は行われているため、この関数ではスラブが使用しているpageの解放処理が主となります。ここでの処理は、ページが所属しているZONEより、スラブが使用していたページ数の減算。ページの属性としてPG_activeとPG_slabビットを下ろします。次にpage構造体の_mapcount変数を-1に設定します。_mapcountは共用体のメンバ変数で、他にinuse、objects、frozenの3変数を持つ構造体も共用体のメンバ変数で、_mapcountに-1を設定することで、他のinuseなどの変数も無効な値に上書きされます。task_struct構造体のreclaim_state変数がNULLでなければ、回収済みのページ数に今回解放するページ数を足します。そして、__free_pages関数を使用し、ページの解放を行います。削除したスラブがkmem_cache_create関数で作成したスラブの場合は、memcgサブシステムのアカウンティング情報よりより使用していたページ数を減らします。

スラブキャッシュの削除

スラブキャッシュが不要になったらkmem_cache_destroy関数でスラブキャッシュを削除します。スラブキャッシュの削除処理はslabアローケータと共通処理とslubアローケータ固有の処理があります。スラブキャッシュの削除はまず、各アローケータ共通であるkmem_cache_destroy関数から処理が始まります。削除処理中はロックを確保し、他のスレッドから操作できないようにします。そして、スラブキャッシュの削除を行います。これは各アローケータ固有の処理で、インターフェースは__kmem_cache_shutdown関数です。__kmem_cache_shutdown関数自体は特に処理は行わず、処理はkmem_cache_close関数にて行います。 slubアローケータでは、最初に全CPUに対して以下の処理を行います。

  1. CPUから参照されているkmem_cache_cpu構造体のpageもしくはpartial変数がNULLの場合(スラブが存在しない)、何もしない
  2. page変数にスラブが設定されている場合、flush_cpu_slab関数が__flush_cpu_slab関数を実行し、3〜5の処理を行う
  3. deactivate_slab関数を実行し、スラブを不活性化。kmem_cache_cpu構造体の変数を初期化
  4. unfreeze_partials関数にてkmem_cache_cpu構造体のpartial変数に設定されてるスラブのfrozen状態を解除
  5. スラブが使用していた不要になったページをdiscard_slab関数にて解放

上記1〜5の処理で、各CPUから参照されていたkmem_cache_cpu構造体のデータを処理したので、次にNUMAノードに設定されているスラブを片付けます。この処理はkmem_cache構造体のnode変数に設定されている、kmem_cache_node構造体のpartialリストよりノードに存在するスラブに対してdiscard_slab関数を実行し、スラブを削除します。

次にCPUごとのメモリ域にあるkmem_cache構造体のcpu_slab変数のメモリを解放します。最後に、kmem_cache構造体のnodeメンバ変数で使用していたkmem_cache_node構造体のメモリを解放します。

ここまででslubアローケータ固有の処理を終了し、スラブアローケータ共通処理に戻ります。共通処理に戻ったら確保していたロックを解除し、do_kmem_cache_release関数を実行し、sysfsでエクスポートしているスラブキャッシュのエントリを削除します。これでスラブキャッシュの削除が完了となります。

スラブキャッシュのセットアップ

スラブアロケータを使用できるようにするために、カーネルの起動時にセットアップを行います。ここでセットアップするのは、kmem_cache構造体とkmem_cache_node構造体のメモリを確保するためのスラブキャッシュと、kmalloc関数で使用する様々なサイズのスラブキャッシュです。slubアローケータのスラブキャッシュを管理するためのメモリ管理機構に、slubスラブアロケータを使用します。

スラブキャッシュのセットアップ状況はいくつかの段階があります。この定義を表_スラブキャッシュのセットアップ状況に示します。セットアップの状態は表のDOWNからFULLに向かって進みます。最初はDOWNから始まります。

状態 内容
DOWN スラブキャッシュをまだ使用できない
PARTIAL slubアローケータのkmem_cache_node構造体が使用可能になった(kmem_cache_init関数内で設定)
PARTIAL_NODE slabアローケータで使用
UP スラブキャッシュを使えるようになったが、まだスラブの全機能は使用可能にはなっていない(create_kmalloc_caches関数で設定)
FULL 全て使用可能(slab_sysfs_init関数で設定)

表_スラブキャッシュのセットアップ状況

スラブキャッシュの初期化はkmem_cache_init関数が行います。この関数では最初に__initdata領域に存在する初期化時用のkmem_cache構造体とkmem_cache_node構造体のデータを使用し、スラブキャッシュを作成します。このスラブキャッシュの作成では、kmem_cache構造体のname、size、alignなどの変数を設定し、__kmem_cache_create関数を呼び出します。__kmem_cache_create関数は通常のスラブキャッシュ作成時にも使用しますが、通常時との違いとして、スラブキャッシュの作成状況による処理の変更が2箇所あります。 1つ目の変更箇所はkmem_cache_open関数から呼ばれるinit_kmem_cache_nodes関数の処理です。init_kmem_cache_nodes関数ではスラブキャッシュの作成状態がDOWNの場合、early_kmem_cache_node_alloc関数を使用してスラブキャッシュを作成します。これは、状態がDOWNの場合、まだスラブキャッシュを使用することが出来ないため、スラブを仮に作成し、そこからメモリを確保してスラブキャッシュを作成します。この場合でもスラブやページフレーム、スラブの使用中リストは設定します。

もう一箇所は__kmem_cache_create関数で、kmem_cache_open関数を実行し、スラブキャッシュが作成できたあとに状態をチェックし、状態がUP以下の場合は、sysfsへのスラブキャッシュ登録処理を行いません。よって、最初期のセットアップではスラブキャッシュを作成しただけで処理を終了します。__kmem_cache_create関数でスラブキャッシュを設定した後は、kmem_cache構造体のrefcountを-1に設定し、スラブのマージ機能対象外になるようにします。 この手順をkmem_cache構造体、kmem_cache_node構造体のスラブキャッシュのために行います。ここまで完了したらスラブキャッシュのセットアップ状況をPARTIALに設定します。

ここまでで仮のセットアップを行ったので、次に本格的なセットアップに入ります。ここからのセットアップもまずは、kmem_cache構造体用のスラブキャッシュと、kmem_cache_node構造体用のスラブキャッシュのセットアップです。これらのセットアップはbootstrap関数にて行います。まず、仮にセットアップしたスラブキャッシュを使用し、kmem_cache構造体用のメモリを確保します。そして、新しい構造体に既存のデータをコピーします。これで__flush_cpu_slab関数を使用し、CPUが参照しているスラブを無効にします。 つぎに、全NUMAノードにあるkmem_cache_node構造体のpartialリストにつながっているページフレームが参照しているslab_cache変数に、先ほど新規作成したkmem_cache構造体を設定します。 そして、スラブキャッシュを管理するslab_cachesリストに作成したkmem_cache構造体を設定します。

kmem_cache構造体とkmem_cache_node構造体のスラブキャッシュを作成したら、次にkmalloc関数用のスラブキャッシュを作成します。この処理はcreate_kmalloc_caches関数にて行います。create_kmalloc_caches関数では作成するサイズの設定を行い、create_kmalloc_cache関数にてスラブキャッシュを作成します。スラブキャッシュの作成はcreate_boot_cache関数を使用します。create_boot_cache関数は__kmem_cache_create関数を使用して、スラブキャッシュを作成しますが、この時点ではスラブキャッシュの作成状況はPARTIALになっているため、最初にkmem_cache構造体・kmem_cache_node構造体を作成した時の用にinit_kmem_cache_nodes関数ではearly_kmem_cache_node_alloc関数ではなく、通常のkmem_cache_alloc_node関数を使用します。kmalloc関数用のスラブキャッシュを作成したらスラブキャッシュの作成状態をUPに変更します。

スラブキャッシュの作成が完了したら、slab_sysfs_init関数にてスラブキャッシュの情報をsysfs経由でエクスポートします。この時にスラブキャッシュの作成状態をFULLに変更し、スラブキャッシュの機能の設定が完了します。

slubアローケータで使用するヘルパー関数

slubアローケータ内でよく使われる関数を紹介します。

__cmpxchg_double_slab関数

__cmpxchg_double_slab関数は引数を7つ受け取りますが、そのうち重要なものを表___cmpxchg_double_slabの引数に示します。

名前
struct kmem_cache * s
struct page * page
void * freelist_old
unsigned long counters_old
void * freelist_new
unsigned long counters_new

表___cmpxchg_double_slabの引数

この関数はpage->freelistとpage->countersの値がfreelist_old、counters_oldと同一の場合は、page->freelistをfreelist_new、page->countersをcounters_newの値に設定します。CPUがアトミックな更新命令を持っている場合はCPUの命令を使用し、アトミックな更新命令を持っていない場合はロックを取得した上で値の更新を行います。

slubアローケータでは他にもthis_cpu_cmpxchg_double関数やcmpxchg_double_slab関数を値の交換のために使用していますが、処理の基本的な考え方は__cmpxchg_double_slab関数と同じです。

kmalloc関数とkfree関数による動的なメモリ確保と解放

kmalloc関数

任意のサイズのメモリを動的に確保したい場合、ユーザランドのプログラムではmalloc(3)を使用しますが、Linuxカーネル内ではkmalloc関数を使用します。 kmalloc関数のインターフェースは「図_kmallocのインターフェース」のようになっています。sizeは確保するメモリの量、flagsは__get_free_pages関数でも使用している、メモリ確保時の制御フラグです。また、メモリを0x0で埋めて返すkzalloc関数などの補助的な関数もあります。

static __always_inline void *kmalloc(size_t size, gfp_t flags);
static inline void *kzalloc(size_t size, gfp_t flags);

図_kmallocのインターフェース

kmalloc関数は確保するサイズに応じて、スラブアローケータよりメモリを確保します。kmalloc関数はカーネルコンパイル時にサイズが確定していて、サイズがkmalloc用のスラブキャッシュの最大サイズ(KMALLOC_MAX_SIZE)を超える場合はページアローケータを使用してメモリを確保します。それ以外の場合はサイズに応じたkmalloc用のスラブキャッシュよりスラブオブジェクトを取得して返します。 kmalloc用のスラブキャッシュはcreate_kmalloc_caches関数にてカーネルの起動時に作成します。

ZONE_NORMALに所属するページフレームからなるkmalloc用のスラブは「図_kmallocのスラブキャッシュ」のようになっています。

lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-1024 -> :t-0001024
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-128 -> :t-0000128
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-16 -> :t-0000016
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-192 -> :t-0000192
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-2048 -> :t-0002048
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-256 -> :t-0000256
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-32 -> :t-0000032
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-4096 -> :t-0004096
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-512 -> :t-0000512
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-64 -> :t-0000064
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-8 -> :t-0000008
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-8192 -> :t-0008192
lrwxrwxrwx  1 root root 0 Jun  9 00:27 kmalloc-96 -> :t-0000096

図_kmallocのスラブキャッシュ

例えば、48バイトのsizeを指定された場合、次に大きいkmalloc-64のスラブキャッシュよりオブジェクトを確保することになります。このようにスラブアロケータを使用することで、要求するsizeによっては無駄が出てしまいますが、メモリを効率的に速く確保できるという利点もあります。

kfree関数

kmalloc関数で確保したメモリを解放するときはkfree関数を使用します。kfree関数のインターフェースは図_kfreeのインターフェースの通りです。引数に渡すのはkmalloc関数の戻り値です。

void kfree(const void *);

図_kfreeのインターフェース

kfree関数でのメモリ解放では、実質的な処理はslab_free関数にて行います。kfree関数固有な処理は特にありません。

fedora 30からgrubの仕様が変わったのでφ(..)メモメモ

Fedora 30でChanges/BootLoaderSpecByDefaultというプロポーザルがあって、Fedora 30βでは実際にこの仕様になってます。カーネルを弄る人にはちょっと影響があるかなって感じです。

仕様

The Boot Loader Specificationに書かれています

どんな風になったか

boot時のgrubの画面は変化はありません。

f:id:masami256:20190409201409p:plain
grub

変わったのはエントリーの書き方です。今まではgrub.cfgにエントリーを追加していたわけですが、カーネルのエントリー部分が独立した感じです。

root@kerntest:/home/masami# ls /boot/loader/entries/
9575947ef933448ea2005318c989c813-0-rescue.conf                     9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64+debug.conf  9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf

たとえば、9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.confはこんなファイルです。

title Fedora (5.1.0-rc4-test+) 30 (Thirty)
version 5.1.0-rc4-test+
linux /boot/vmlinuz-5.1.0-rc4-test+
initrd /boot/initramfs-5.1.0-rc4-test+.img
options $kernelopts
id fedora-20190408151310-5.1.0-rc4-test+
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel-

uefiとnon uefi

従来はuefi環境なら/boot/efi/EFI/fedora/grub.cfg、biosなら/boot/grub2/grub.cfgが使われていましたが、BLSでは/boot/loader/entries/にあるファイルを読むことになります。

uefiな環境でも/boot/loader/entriesにファイルが置かれ、

masami@saga:~$ file /sys/firmware/efi/
/sys/firmware/efi/: directory
masami@saga:~$ sudo ls /boot/loader/entries
6ae2390b6ece4d5c86324fadb81d220d-0-rescue.conf                     6ae2390b6ece4d5c86324fadb81d220d-5.0.5-200.fc29.x86_64+debug.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.4-200.fc29.x86_64.conf        6ae2390b6ece4d5c86324fadb81d220d-5.0.6-300.fc30.x86_64.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.4-200.fc29.x86_64+debug.conf  6ae2390b6ece4d5c86324fadb81d220d-5.0.6-300.fc30.x86_64+debug.conf
6ae2390b6ece4d5c86324fadb81d220d-5.0.5-200.fc29.x86_64.conf        6ae2390b6ece4d5c86324fadb81d220d-5.1.0-rc4-test+.conf

non uefiな環境でも/boot/loader/entriesにファイルが置かれます。

root@kerntest:/boot/loader/entries# file /sys/firmware/efi                                                                                                                                   
/sys/firmware/efi: cannot open `/sys/firmware/efi' (No such file or directory)
root@kerntest:/boot/loader/entries# ls /boot/loader/entries/
9575947ef933448ea2005318c989c813-0-rescue.conf                     9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64.conf
9575947ef933448ea2005318c989c813-5.0.5-200.fc29.x86_64+debug.conf  9575947ef933448ea2005318c989c813-5.0.6-300.fc30.x86_64+debug.conf
9575947ef933448ea2005318c989c813-5.0.6-200.fc29.x86_64.conf        9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf

カーネルのmake install

make modules_install -> make installしますよね。この場合、以下のような流れで処理が走ります。

arch/x86/boot/install.sh 
     -> /sbin/installkernel 
         -> /usr/libexec/installkernel/installkernel-bls 
             -> /bin/kernel-install

make_installの挙動

/sbin/installkernelやusr/libexec/installkernel/installkernel-bls 、それに/bin/kernelinstallはfedora 29でもありますが、f29ではBLSには対応していないのでgrub.cfgにエントリを書く方式が使われます。ちなみに、f29には/usr/libexec/installkernel/installkernel-blsと/usr/libexec/installkernel/installkernelの2つがあります(if文の判定的に-blsのほうは使われない)が、f30では-blsの方しかありません。

これらのコマンドが含まれているパッケージは以下の表のようになります。

コマンド コマンドを提供しているパッケージ
/sbin/installkernel grubby
/usr/libexec/installkernel/installkernel-bls grubby
/bin/kernel-install systemd-udev

パッケージ

カーネルの登録

/bin/kernel-installコマンドでできますが、これは/boot/loader/entriesにファイルを作るだけです。make installするのではなくて、コマンド打っていく場合はこんな感じになります。make moduels_installは済んでいるとして、まずはdracutでinitramfsをつくり、

root@kerntest:/home/masami/linux-kernel (ktest)# dracut /boot/initramfs-5.1.0-rc4-test+.img 5.1.0-rc4-test+                                                                                  

/sbin/installkernelでカーネルを/bootにインストールします。

root@kerntest:/home/masami/linux-kernel (ktest)# /sbin/installkernel 5.1.0-rc4-test+ ./arch/x86/boot/bzImage ./System.map 

そうすると、/bin/kernel-installが内部で呼ばれるのでファイルができてます。

root@kerntest:/home/masami/linux-kernel (ktest)# cat /boot/loader/entries/9575947ef933448ea2005318c989c813-5.1.0-rc4-test+.conf
title Fedora (5.1.0-rc4-test+) 30 (Thirty)
version 5.1.0-rc4-test+
linux /boot/vmlinuz-5.1.0-rc4-test+
initrd /boot/initramfs-5.1.0-rc4-test+.img
options $kernelopts
id fedora-20190409141515-5.1.0-rc4-test+
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel-

もしkernel-installを自分で実行するならこうなります。

root@kerntest:/home/masami/linux-kernel (ktest)# kernel-install add 5.1.0-rc4-test+ /boot/vmlinuz-5.1.0-rc4-test+ 

カーネルの削除

/bootにインストールしたカーネルとinitramfs、/boot/loader/entiresのファイルを消したい場合もkernel-installコマンドで出来ます。removeオプションにカーネルの話バージョンを渡せばokです

root@kerntest:/home/masami/linux-kernel (ktest)# kernel-install remove 5.1.0-rc4-test+

カーネルコマンドラインオプション

特定のエントリーに対してオプションを足したり、全部に足したりしたいですよね。Setting kernel command line arguments with Fedora 30 - Fedora Magazineが参考になります。

grubのメニューへの登録・削除

ブート時に/boot/loader/entriesにあるファイルを見てくれるので、ファイルを置けばメニューに追加され、消せばメニューから消えるという楽な仕様です。

grubの設定を確認したい

grubbyの出番です。

全部見たい場合はgrubby --info=ALLでみれます。エントリのindex番号もこれでわかります。

root@kerntest:/home/masami/linux-kernel (ktest)# grubby --info=ALL                                                                                                                            
index=0                                                                                                                                                                                       
kernel="/boot/vmlinuz-5.1.0-rc4-test+"                                                                                                                                                        
args="ro rhgb quiet nokaslr console=ttyS0,115200"                                                                                                                                             
root="UUID=620d1324-eba7-4beb-aab5-dd9975f25690"                                                                                                                                              
initrd="/boot/initramfs-5.1.0-rc4-test+.img"                                                                                                                                                  
title="Fedora (5.1.0-rc4-test+) 30 (Thirty)"                                                                                                                                                  
id="9575947ef933448ea2005318c989c813-5.1.0-rc4-test+"                                                                                                                                         
index=1                                                                                                                                                                                       
kernel="/boot/vmlinuz-5.0.6-300.fc30.x86_64+debug"

その他はgrubby --helpすれば良いと思います。

dynamic_debugはどのようにソースコードの行数、関数名などを読み取っているのか

dynamic_debugのcontrolファイルを読むとこんな感じでファイル名、関数名、そしてpr_debugに渡している文字列などが見れます。これってどうやってんだろ?というのが今回調べたところです。

masami@kerntest:~/pr_debug_test$ sudo cat /sys/kernel/debug/dynamic_debug/control | head -n 10
# filename:lineno [module]function flags format
init/main.c:804 [main]initcall_blacklisted =p "initcall %s blacklisted\012"
init/main.c:771 [main]initcall_blacklist =p "blacklisting initcall %s\012"
init/initramfs.c:477 [initramfs]unpack_to_rootfs =_ "Detected %s compressed data\012"
arch/x86/events/amd/ibs.c:885 [ibs]force_ibs_eilvt_setup =_ "No EILVT entry available\012"
arch/x86/events/amd/ibs.c:856 [ibs]setup_ibs_ctl =_ "No CPU node configured for IBS\012"
arch/x86/events/amd/ibs.c:850 [ibs]setup_ibs_ctl =_ "Failed to setup IBS LVT offset, IBSCTL = 0x%08x\012"
arch/x86/events/intel/pt.c:736 [pt]pt_topa_dump =_ "# entry @%p (%lx sz %u %c%c%c) raw=%16llx\012"
arch/x86/events/intel/pt.c:727 [pt]pt_topa_dump =_ "# table @%p (%016Lx), off %llx size %zx\012"
arch/x86/kernel/tboot.c:98 [tboot]tboot_probe =_ "tboot_size: 0x%x\012"

自前のカーネルモジュールを作ってロードした後にはcontorolファイルに値が追加されます。

/home/masami/pr_debug_test/pr_debug_test.c:20 [pr_debug_test]pr_debug_test_read =_ "debug pr_debug test\012"                                                                                                      

pr_debugの仕組み

さて、include/linux/printk.hでpr_debugがどのように定義されているか見てみます。dynamic_debugが有効な場合はCONFIG_DYNAMIC_DEBUGが定義されているのでdynamic_pr_debugが使われます。

/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG)
#include <linux/dynamic_debug.h>

/* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */
#define pr_debug(fmt, ...) \
   dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
   printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
   no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

dynamic_pr_debug()はこのようなマクロです。

#define dynamic_pr_debug(fmt, ...)              \
do {                             \
   DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt);     \
   if (DYNAMIC_DEBUG_BRANCH(descriptor))         \
       __dynamic_pr_debug(&descriptor, pr_fmt(fmt),    \
                  ##__VA_ARGS__);      \
} while (0)

ここの最初の処理がDEFINE_DYNAMIC_DEBUG_METADATAですのでこれを見てみるとこうなっています

#define DEFINE_DYNAMIC_DEBUG_METADATA(name, fmt) \
   DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, .key.dd_key_true, \
                     (STATIC_KEY_TRUE_INIT))

DEFINE_DYNAMIC_DEBUG_METADATA_KEYはこのように展開されます。

#define DEFINE_DYNAMIC_DEBUG_METADATA_KEY(name, fmt, key, init) \
   static struct _ddebug  __aligned(8)          \
   __attribute__((section("__verbose"))) name = {       \
       .modname = KBUILD_MODNAME,          \
       .function = __func__,              \
       .filename = __FILE__,              \
       .format = (fmt),                \
       .lineno = __LINE__,                \
       .flags = _DPRINTK_FLAGS_DEFAULT,        \
       dd_key_init(key, init)              \
   }

関数名、ファイル名、行数などがここで出てきてますね。この処理がdynamic_debugの肝っぽいです。nameという変数を定義して、その変数はverboseというセクションに置くようにしています(このマクロはdo {} while(0)の中で展開されるので変数はこのブロック中にあります)。この変数に値を設定してverboseセクションに変数を置くというのがdynamic_debugのポイントですね。ちなみにdd_key_initはjump_labelを使う場合の初期化用マクロです。

というわけで、dynamic_debugが有効なカーネルではpr_debugを使っているところはそのデータが__verboseセクションに置かれるということが分かりました。

__verboseセクションはinclude/asm-generic/vmlinux.lds.hで確認できます。カーネルのビルド後ならarch/x86/kernel/vmlinux.ldsがリンカのファイルです。

    /* implement dynamic printk debug */                \
    . = ALIGN(8);                          \
    __start___verbose = .;                      \
    KEEP(*(__verbose))                                              \
    __stop___verbose = .;                       \

__verboseセクションからデータを読んでいるところ

dynamic_debugの初期化時は以下のようにstartverboseからstopverboseまでのメモリ領域を読んでいってデータをddebug_add_module()でリストに登録しています。

 if (__start___verbose == __stop___verbose) {
        pr_warn("_ddebug table is empty in a CONFIG_DYNAMIC_DEBUG build\n");
        return 1;
    }
    iter = __start___verbose;
    modname = iter->modname;
    iter_start = iter;
    for (; iter < __stop___verbose; iter++) {
        entries++;
        verbose_bytes += strlen(iter->modname) + strlen(iter->function)
            + strlen(iter->filename) + strlen(iter->format);

        if (strcmp(modname, iter->modname)) {
            modct++;
            ret = ddebug_add_module(iter_start, n, modname);
            if (ret)
                goto out_err;
            n = 0;
            modname = iter->modname;
            iter_start = iter;
        }
        n++;
    }
    ret = ddebug_add_module(iter_start, n, modname);
    if (ret)

モジュールのロード時はload_module()からfind_module_sections()が呼ばれ、ここで__verboseセクションのデータを読み取っています。

 info->debug = section_objs(info, "__verbose",
                   sizeof(*info->debug), &info->num_debug);

そして、load_module()からdynamic_debug_setup()を呼び、ddebug_add_module()を読んでデータを登録する形となっています。

static void dynamic_debug_setup(struct module *mod, struct _ddebug *debug, unsigned int num)
{
    if (!debug)
        return;
#ifdef CONFIG_DYNAMIC_DEBUG
    if (ddebug_add_module(debug, num, mod->name))
        pr_err("dynamic debug error adding module: %s\n",
            debug->modname);
#endif
}

まとめ

dynamic_debugが有効なカーネルではソースコード中のpr_debug記述箇所でファイル行数、ファイル名などの情報を変数として登録し、その変数はverboseセクションに置かれる。dynamic_debugはverboseセクションからデータを読んでpr_debugの利用箇所などの情報を取得することができるという感じでした。

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習

ハッキング・ラボのつくりかた 仮想環境におけるハッカー体験学習