雑文:Linuxとか低レイヤが好きで趣味で色々やってたら趣味が仕事になったのと、知識が足りないとこを勉強しようと思って学生にもなった件

このブロクで使ってるカテゴリは↓のようになっていて、こいつLinuxとかカーネル好きだなって感じなんですが、前まではLinux自体は使うけどそれくらいでガッツリとLinuxに絡んだりとかはしてませんでした。

f:id:masami256:20190905215910p:plain

前職はこんな感じのところでnodejsなんかのコード書いてわけですね。 f:id:masami256:20140629161407j:plain

web系っぽい記事もたまに書いてましたよw

qiita.com

とまあ、前職はweb系だったわけですが今はLinuxがっつりな仕事になってます。転職したのは去年の12月ですけどね。Linuxが好きでいろいろやってきて、そこでやったことをblogに書いたりしたわけですが、それも今の仕事で役立つことがあったりとまあ何が役立つかわからないものですね。メモリ管理に興味を持ったり自作OSに興味を持ったり、CTFだったりと低レイヤ周辺をウロウロしてたけど基本的な部分はブレなかったのが良かった気がします。 継続は力なりなんて格言もあるし、続けるってのは大事っすね。とゆーか、ものすごく運が良かったとは思ってます。すごい人達とも知り合うことができたし。

もともとソフトウェアエンジニアになったのも趣味を仕事にしたら一石二鳥じゃね?ってところから仕事にした感じですね。今流行りの未経験からエンジニアになった系ですw もうかなり前だけどw で、そんなことはどうでもよくて、趣味&独学でやってきたので自分の好きな分野はまあわかるけど情報理論とか理論系よくわからん😭という状況だったりというのもありました。じゃあ、ちゃんと勉強しちゃうか?ってことで2019年4月から放送大学の情報コースに全科履修生で入学したりしました。まあ、この辺の決断はかなり軽くて昼に町に出てどのごはん屋さんに行くかを決めるよりは軽い感じで決めてます。会社員兼学生という感じなので履修ペースは遅いけどそこはしょうがない。。。今のところ1学期に4科目くらいのペースですね。

2019度第1学期にとったこれは自分のわかる分野だったので単位認定試験で一番良い評価とれました😃

f:id:masami256:20190905222227j:plain

だがしかし、こっちはギリギリ単位取れた。。。数学苦手😨

f:id:masami256:20190905222242j:plain

ちなみに、コンピュータの動作と管理を見てたらこんなことが😄

\177ELF on Twitter: "@uchan_nos @d_kami 放送大学の情報コースの教科書に見慣れたお名前を発見😇… "

さて、2019度第2学期も無事に単位を取ることができるのか💀

Joel on Software

Joel on Software

UEFIのSecure boot + kdump・kexec

UEFIのSecure boot + kdump・kexecの動作確認

uefiのSecure bootが有効な環境でkdump・kexecは動作するのか?ってところの確認です。

Secure bootとは?というところはこちらのドキュメントを参照してください。 access.redhat.com

テスト環境

テスト環境にはlibvirtを使います。ホストはFedora 30 x86_64、ゲストもFedora 30 x86_64です。

テスト環境セットアップ

まずはSecure bootを有効にした環境を作ります。まずは普通にlibvirtを使ってOSをインストールします。 このときに、UEFIを使うように設定します。そのため、edk2-ovmfパッケージをインストールしておく必要があります。

f:id:masami256:20190830235150p:plain

インストール段階だとセキュアブート用のファームウェアは指定できないのでとりあえずはセキュアブートなしでインストールします。

kdumpの設定

インストールが終わったら普通に設定しましょう。

access.redhat.com

この時点で動作確認もしてkdumpが正常動作することを確認しておきます。

Secure bootの設定

virsh editでxmlファイルを編集します。VMは一旦shutdownしておきましょう。

まず、OVMF_VARS.secboot.fdをコピーします。コピー先の名前はなんでも良いです。ここではvm名と合わせてます。

$ sudo cp /usr/share/edk2/ovmf/OVMF_VARS.secboot.fd /var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd

そしたらvirsh editで編集します。以下の部分を探します。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS.fd</nvram>
    <boot dev='hd'/>
  </os>

そして、以下のように編集します。変更したのはloaderのところでOVMF_CODE.secboot.fdを指定したのと、nvramには先ほどコピーしたファイルを指定しました。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd</nvram>
    <boot dev='hd'/>
  </os>

セキュアブート有効で起動

普通にvmを起動すればセキュアブートが有効な状態で立ち上がります。 dmesgで確認するとこんなふうになります。

[masami@secureboot-test ~]$ dmesg | grep secureboot
[    0.000000] secureboot: Secure boot enabled
[    0.928493] systemd[1]: Set hostname to <secureboot-test>.
[    3.770079] systemd[1]: Set hostname to <secureboot-test>.
[masami@secureboot-test ~]$ 

これでvmのセキュアブートが有効になりました。

kdumpのテスト

セキュアブートが有効な状態でkdumpをスタートすると失敗します。エラーメッセージはこのようになってます。

[masami@secureboot-test ~]$ sudo systemctl status kdump
● kdump.service - Crash recovery kernel arming
   Loaded: loaded (/usr/lib/systemd/system/kdump.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Fri 2019-08-30 22:29:52 JST; 12s ago
  Process: 1316 ExecStart=/usr/bin/kdumpctl start (code=exited, status=1/FAILURE)
 Main PID: 1316 (code=exited, status=1/FAILURE)

Aug 30 22:29:51 secureboot-test systemd[1]: Starting Crash recovery kernel arming...
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Secure Boot is enabled. Using kexec file based syscall.
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec_file_load failed: Operation not permitted
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec: failed to load kdump kernel
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Starting kdump: [FAILED]
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Main process exited, code=exited, status=1/FAILURE
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Failed with result 'exit-code'.
Aug 30 22:29:52 secureboot-test systemd[1]: Failed to start Crash recovery kernel arming.

dmesgでカーネルのエラーを確認するとこのようなメッセージを発見できます。

[  173.215036] Lockdown: kexec: /proc/kcore is restricted; see man kernel_lockdown.7

カーネルのコンフィグを確認するとLockdown機能が有効になってますね。

[masami@secureboot-test ~]$ grep LOCK_DOWN /boot/config-5.2.9-200.fc30.x86_64 
CONFIG_LOCK_DOWN_KERNEL=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE is not set
CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y

Lockdownの解除

手元にmanがなかったので検索するとこちらが見つかりました。

lwn.net

manによると物理的に接続されたキーボードからのsysrq + xでlockdownが解除できるとあります。というわけで、virsh send-keyでsysrq + xを送ります。

$ sudo virsh send-key secureboot-test KEY_LEFTALT KEY_SYSRQ KEY_X

そうすると以下のようにlockdownを解除したよってメッセージがでます。

[masami@secureboot-test ~]$ dmesg
[ 2236.328389] sysrq: Disabling Secure Boot restrictions
[ 2236.328934] Lifting lockdown

kdumpのテスト

まずセキュアブートが有効か確認します。

[masami@secureboot-test ~]$ sudo mokutil --sb-state
SecureBoot enabled
[masami@secureboot-test ~]$ 

で、kdumpを起動

[masami@secureboot-test ~]$ sudo systemctl start kdump

エラーが出なければOKなので、クラッシュさせてみると /var/crash/に該当時刻のディレクトリが出来ていてコアダンプが取得できてるのが確認できます😃

[masami@secureboot-test ~]$ sudo sh -c "echo c > /proc/sysrq-trigger"
client_loop: send disconnect: Broken pipe
masami@moon:~$ ssh secureboot-fedora 
Web console: https://secureboot-test:9090/ or https://192.168.122.191:9090/

Last login: Fri Aug 30 22:58:25 2019 from 192.168.122.1
[masami@secureboot-test ~]$ ls /var/crash/
127.0.0.1-2019-08-30-01:05:24  127.0.0.1-2019-08-30-23:38:24
[masami@secureboot-test ~]$ date
Fri 30 Aug 2019 11:38:53 PM JST
[masami@secureboot-test ~]$ 

まとめ

セキュアブートが有効な環境でもkdump・kexecは使えますが、カーネルのLockdown機能が有効な場合は使えません。もしkdump・kexecを使用したい場合はLockdownを止める必要があるということでした。

追記1 2019/08/31

kexecとセキュアブート

セキュアブートが有効な場合と無効な場合ではkexec実行時のオプションがちょっと違います。では具体的に何が違うのか?というとセキュアブートが有効な場合は-sオプションを使います。 Fedoraの場合はkdumpctlコマンドがこの辺の処理をしていて、以下のようなコードとなってます。

    
    # For secureboot enabled machines, use new kexec file based syscall.
    # Old syscall will always fail as it does not have capability to
    # to kernel signature verification.
    if is_secure_boot_enforced; then
        echo "Secure Boot is enabled. Using kexec file based syscall."
        KEXEC_ARGS="$KEXEC_ARGS -s"
    fi
 
    $KEXEC $KEXEC_ARGS $standard_kexec_args \
        --command-line="$KDUMP_COMMANDLINE" \
        --initrd=$TARGET_INITRD $kdump_kernel
    if [ $? == 0 ]; then
        echo "kexec: loaded kdump kernel"
        return 0
    else
        echo "kexec: failed to load kdump kernel" >&2
        return 1
    fi

セキュアブートが有効な場合は-sオプション付けて使用するシステムコールを変える必要があります。

f:id:masami256:20190831104347p:plain

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

raspberry pi 3 b+ and u-boot and mainline kernel, and meta-fedora

Raspberry Pi(以下rpi)のカーネルではなくて、mainline等のカーネルを使いたい場合のメモです。

TL;DR

基本的に次のwebサイトを見れば事足ります。

elinux.org

概要

rpi向けのカーネルは以下なのですが、mainlineのカーネルを使いたい等の理由がある場合の環境構築方法のメモです。

github.com

uart、wifihdmiを使えるところまでが対象です。archはaarch64を対象とします。使っているのはRaspberry Pi 3 B+です。

f:id:masami256:20190821212408j:plain

必要なもの

これらが必要です。

最初の2つはそりゃそうだなって感じですね。次のrpiで〜というところはstart.elfやfixup.datといったブートに必要なバイナリファイル群です。最後のfirmwarewifiを使うのに必要になります。

u-boot

RPi U-Boot - eLinux.orgを見てビルドしてください。

Linux Kernel

適当なaarch64環境でコンパイルするか、クロスコンパイルします。

Fedoraならgcc-aarch64-linux-gnuあたりのパッケージをインストールします。クロスコンパイル時のmakeコマンドは基本的に次の通りです。これにoldconfigだったりがオプションで付きます。

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- 

defconfig

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig

configオプションで設定するものがいくつかあります。defconfigでmakeしたあとに以下のスクリプトを実行し、最後にmake oldconfigを実行します。ここで設定しているのは有線LAN、wifiドライバ、HDMI出力、broadcomのドライバ、フレームバッファです。面倒なのでモジュールではなくてカーネルに組み込んでます。

#!/bin/bash

configs=(USB_LAN78XX
RFKILL
BRCMFMAC
RCMFMAC_PROTO_BCDC
BRCMFMAC_PROTO_MSGBUF
BRCMFMAC_SDIO
BCMFMAC_USB
BRCMFMAC_PCIE
CFG80211
CFG80211_REQUIRE_SIGNED_REGDB
CFG80211_USE_KERNEL_REGDB_KEYS
CFG80211_DEFAULT_PS
CFG80211_DEBUGFS
CFG80211_CRDA_SUPPORT
CFG80211_WEXT
ARCH_BCM2835
SERIAL_8250_BCM2835AUX
HW_RANDOM_BCM2835
I2C_BCM2835
SPI_BCM2835
SPI_BCM2835AUX
PINCTRL_BCM2835
BCM2835_THERMAL
BCM2835_WDT
SND_BCM2835_SOC_I2S
MMC_BCM2835
DMA_BCM2835
BCM2835_VCHIQ
SND_BCM2835
VIDEO_BCM2835
BCM2835_MBOX
BCM2835_POWER
PWM_BCM2835
HW_RANDOM_BCM2835
ROCKCHIP_DW_HDMI
ROCKCHIP_INNO_HDMI
ROCKCHIP_RK3066_HDMI
DRM_SUN4I_HDMI_CEC
DRM_MSM_HDMI_HDCP
DRM_VC4_HDMI_CEC
HDMI
FB_SIMPLE
LOGO
LOGO_LINUX_CLUT224
)

for config in ${configs[@]};
do
    ./scripts/config --enable $config
done

HDMIで画面出力する場合はフレームバッファが必要で、mainlineのカーネルだとsimple-framebuffer(CONFIG_FB_SIMPLE)を有効にする必要があります。

.configが作れたらカーネルとdevicetreeをビルドします。

Imageのビルド

make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

DeviceTreeのビルド

make -j$(nproc) ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs

カーネルのビルドはこれにて終了です。

Raspberry Pifirmwareダウンロード

ラズパイ用のfirmwareをgitリポジトリからcloneしましょう。

github.com

Linuxfirmwareダウンロード

wifiを動かすのに必要なfirmwareを一式gitリポジトリからcloneしましょう。

git.kernel.org

sdカードの準備

こんな感じで最初のパーティションfat32で作ります。起動はここからです。もう一つのほうはroot filesystemです。

root@qemuarm64:~# fdisk -l
Disk /dev/mmcblk0: 30 GB, 31914983424 bytes, 62333952 sectors
973968 cylinders, 4 heads, 16 sectors/track
Units: cylinders of 64 * 512 = 32768 bytes

Device       Boot StartCHS    EndCHS        StartLBA     EndLBA    Sectors  Size Id Type
/dev/mmcblk0p1    4,4,1       261,4,2           2048     133119     131072 64.0M  c Win95 FAT32 (LBA)
Partition 1 does not end on cylinder boundary
/dev/mmcblk0p2    261,5,1     1023,254,2      133120    2097151    1964032  959M 83 Linux
Partition 2 does not end on cylinder boundary

bootパーティションの設定

firmwareのコピー

/mnt/rpiにマウントしているとして、cloneしてきたrpiのfirmwareディレクトリに移動しファイルをコピーします。

$ cd firmware
$ cp -a boot/start* /mnt/rpi/.
$ cp -a boot/fixup* /mnt/rpi/.
$ cp -a boot/overlays/ /mnt/rpi/.
$ cp -a boot/bootcode.bin /mnt/rpi/.

カーネルのコピー

arch/arm64/boot/Imageとarch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b-plus.dtbを/mnt/rpiにコピーします。

u-bootのコピー

ビルドしたu-boot.binを/mnt/rpiにコピーします。

config.txtの作成

つぎに/mnt/rpi/config.txtを以下の内容で作ります。

[all]
boot_delay=1
kernel=u-boot.bin
arm_control=0x200
upstream_kernel=1
audio_pwm_mode=0
enable_uart=1
gpu_mem=16
mask_gpu_interrupt1=0x100

u-bootの設定

以下のようなファイルを(boot.cmd)を作ります。作成する場所は任意です。

fatload mmc 0:1 ${kernel_addr_r} Image
fatload mmc 0:1 ${fdt_addr} bcm2837-rpi-3-b-plus.dtb
setenv bootargs dwc_otg.lpm_enable=0 earlyprintk root=/dev/mmcblk0p2 rootfstype=ext4 rootwait
booti ${kernel_addr_r} - ${fdt_addr}

そして以下のコマンドでboot.scrを/mnt/rpiに作成します。

$ mkimage -C none -A arm64 -T script -d ./boot.cmd /mnt/rpi/boot.scr

上記のmkimageコマンドはfedoraならuboot-toolsパッケージにあります。

uartの設定

u-bootの段階からuartでログを表示したい場合は以下のようにします。

$ sed -i -e "s/BOOT_UART=0/BOOT_UART=1/" /mnt/rpi/bootcode.bin 

と、ここまででブートローダカーネルの準備はOKです。が、これだとルートファイルシステムがないので面白くないですよね。なのでルートファイルシステムも用意しましょう。

ルートファイルシステム

ルートファイルシステムのビルド

まあなんでもいいのですが、せっかくなので自作のyoctoのレイヤーを使いましょう(´ω`)

github.com

MACHINEにはqemuarm64を指定してcore-image-minimalでビルドしてください。build/tmp-glibc/deploy/images/qemuarm64/にルートファイルシステムのtarファイルがあるのでそれをルートファイルシステムパーティションに展開します。

/mnt/rootfsがルートファイルシステムパーティションだとしてこうします。

$ sudo tar xvf build/tmp-glibc/deploy/images/qemuarm64/core-image-minimal-qemuarm64.tar.bz2 -C /mnt/rootfs/

firmwareのコピー

broadcomファームウェアをルートファイルシステムにコピーしましょう。こんな感じです。

$ sudo mkdir /mnt/rootfs/lib/firmware
$ sudo cp -a linux-firmware/brcm/ /mnt/rootfs/lib/firmware/.

起動

ここまででホストでの作業は完了したのであとはsdカードから起動させましょう。

TV側

起動時の🐧

f:id:masami256:20190821211838j:plain

ログイン

f:id:masami256:20190821211912j:plain

uartでもログインしてるのでttyS1もありますね。

uart側

f:id:masami256:20190821212710p:plain

有線LAN、wifiが認識されてます。

wlan0

root@qemuarm64:~# ip link show wlan0
2: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000

eth0

root@qemuarm64:~# ip link show wlan0
2: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000

まとめ

rpiのカーネルでなくてもhdmiを使ったり有線・wifi使えるようになりましたΣ(゚∀゚ノ)ノキャー tftpブートすればrpiでのカーネルハックが捗ると思います😃

自分用にLinuxカーネルのBuildbot + LAVAでCI環境を作るめも

ちょっとCI環境でも作るか〜ということで。

CI

だいたいこんな構成です。

f:id:masami256:20190714155852p:plain
構成

いまのところすべてローカル環境で閉じてます。うちはマンションタイプのBフレッツでそんなに速いわけでもないからローカルで閉じてるほうがテスト時間が短くて済むというとこです。AWSとかVPSを使っても良いんですけどね。

とりあえず動くようにはなったので、ブラッシュアップするところはたくさんあります😨

CIのシステムで使用するソフトウェア

次の2つを使います。

Buildbotは有名ですよね。LAVAはkernelci.orgで使われています。実ボードとかqemuなどの環境でLinuxをテストするのに向いているツールです。

インストール

buildbot

pythonのvirtualenvを使って専用のpython環境作ってインストールして使ってます。

LAVA

LAVAは一回スクラッチから設定したことあるんですが面倒なのでdockerを使います。githubkernelciのプロジェクトのgitリポジトリがあるのでcloneして使いました。

Webサーバ

Webサーバも使うんですがこれはLAVAからカーネルなどのファイルをダウンロードするのに使うだけなのでpythonワンライナーで済ませてます。

流れ

ファイルを編集してコミットしたらbuildbotがリポジトリをポーリングしているので、変更を検知したらビルド開始します。make bzImageとmake modules、make modules_installまで成功したらdracutでinitramfsを作り、bzImageとinitramfsをwebサーバのディレクトリに置きます。そしたらbuildbotのジョブからLAVAのAPIを叩いてboot testを実行します。buildbotはLAVAのテストが終わるのを待って、LAVAのテスト結果を最終的なビルドの結果とします。

画面

画面はこんな感じですね。

buildbot

f:id:masami256:20190714154349p:plain
buildbot

lavaテスト実行画面

f:id:masami256:20190714154731p:plain
lava

lavaテスト実行画面の最下部

f:id:masami256:20190714154753p:plain
lava

設定

buildbot

buildbotでのテストステップはmaster.cfgで完結してますが、テストステップの記述はファイルを分けたほうが綺麗でしょうね。

pollingの設定はこんな感じでローカルのディレクトリを直に設定。このディレクトリはgit --bare initしたリモートリポジトリではなく、単純にcloneしてきたディレクトリです。ポーリング間隔はもっと短くて良いかな。

c['change_source'].append(changes.GitPoller(
        '/home/masami/projects/linux-kernel',
        workdir='linux-workdir', branch='ktest',
        pollInterval=300))

スケジューリングの設定はこんな感じ。forceビルドもしますよねということで。

c['schedulers'].append(schedulers.SingleBranchScheduler(
                            name="ktest branch test",
                            change_filter=util.ChangeFilter(branch='ktest'),
                            treeStableTimer=None,
                            builderNames=["linux-master-builder"]))
c['schedulers'].append(schedulers.ForceScheduler(
                            name="force",
                            builderNames=["linux-master-builder"]))

テストステップはこうです。

 check out the source
factory.addStep(steps.Git(repourl='/home/masami/projects/linux-kernel', mode='incremental'))

# start build

factory.addStep(steps.ShellCommand(command=["make", "mrproper"]))
factory.addStep(steps.ShellCommand(command=["cp", "/home/masami/projects/build-test/vm-config", ".config"]))

config_path = "%s/.config" % tmpdir_path
factory.addStep(steps.ShellCommand(command=["./scripts/config", "--set-str", "CONFIG_LOCALVERSION", "-build-test"]))

import multiprocessing
parallel_opt = '-j%s' % multiprocessing.cpu_count()


# build kernel and modules
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "bzImage"]))
factory.addStep(steps.ShellCommand(command=["make", parallel_opt, "modules"]))

# install modules
factory.addStep(steps.ShellCommand(command=["make", "INSTALL_MOD_PATH=/tmp/linux-build-test", "modules_install"]))

factory.addStep(steps.ShellCommand(command=["cp", "-f", "/home/masami/projects/builder/worker/linux-master-builder/build/arch/x86/boot/bzImage", "/home/masami/projects/web/bzImage"]))
                
factory.addStep(steps.ShellCommand(command=["dracut", "--force", "-k", "/tmp/linux-build-test/lib/modules/5.2.0-build-test+", "--kernel-image", "/home/masami/projects/web/bzImage", "--kver", "5.2.0-build-test+", "/home/masami/projects/web/initramfs-build-test.img"]))

factory.addStep(steps.ShellCommand(command=["/home/masami/projects/build-test/boot-test.py", "/home/masami/projects/build-test/boot-test.yaml"]))

factory.addStep(steps.ShellCommand(command=["rm", "-fr", "/tmp/linux-build-test"]))

やってることはこんな感じです。

  1. git cloneする
  2. make mrproper
  3. 別のディレクトリからカーネルのコンフィグをコピーする
  4. CONFIG_LOCALVERSIONをセット
  5. 使えるcpu数を調べる
  6. make bzImage
  7. make modules
  8. make modules_install 9 bzImageをwebサーバのディレクトリにコピー
  9. dracutでinitramfsを作る
  10. 作ったinitramfsをwebサーバのディレクトリにコピー
  11. LAVAのAPIを実行するスクリプトを実行してLAVAでテスト
  12. 後始末

カーネルのバージョンを直書きしてるのは直さないとねとか色々。

LAVA

LAVAはdockerイメージをそのまま使っていて特に設定の変更はしてません。テストはAPIから実行するのでAPI用のトークンを作ったくらいですね。APIを実行するスクリプトは↓です。テストはjobという形で登録します。jobを記述するときのフォーマットはyaml形式で、APIから登録するときはyaml形式の文字列を送ります。

#!/usr/bin/env python3

import xmlrpc.client
import sys
import time
import yaml

def create_server_instance():
    username = 'user name'
    token = 'access token'
    hostname = 'localhost:10080'
    return xmlrpc.client.ServerProxy('http://%s:%s@%s/RPC2' % (username, token, hostname), allow_none=True)

def submit_job(server, job):

    return server.scheduler.submit_job(job)
    
def read_job_yaml(filepath):
    with open(filepath, 'r') as f:
        return f.read()

def wait_until_job_finish(server, job_id):

    while True:
        ret = server.scheduler.job_health(job_id)
        health = ret['job_health']

        if not health == 'Unknown':
            return
        time.sleep(5)

def check_job_result(server, job_id):
    results_raw = server.results.get_testjob_results_yaml(job_id)
    try:
        from yaml import CLoader as Loader
    except ImportError:
        from yaml import Loader
        
    results = yaml.load(results_raw, Loader=Loader)
    
    for ret in results:
        r = ret['result']
        if r == 'fail':
            return False

    return True

if __name__ == '__main__':
    if not len(sys.argv) == 2:
        print('[-]usage: %s <path to job definition yaml file>' % sys.argv[0])
        exit(1)

    server = create_server_instance()        
    job = read_job_yaml(sys.argv[1])
    job_id = submit_job(server, job)

    print('job: %d created' % job_id)

    wait_until_job_finish(server, job_id)

    ret = check_job_result(server, job_id)

    if ret:
        print('Test: success')
        exit(0)

    print('Test: fail')
    exit(1)

上記のスクリプトはscheduler.submit_job()でjobを登録し、scheduler.job_health()で終わるのを待っています。その後、get_testjob_results_yaml()でテスト結果をステップごとに見ていってるんですが、これは実際不要ですね。scheduler.job_health()の結果としては以下の4つで、Unkownはテスト実行中に返ってくるので結果のステータスは3個。なのでComplete以外ならエラーとしても大丈夫だと思います。

  • UNKONW(実行中はこれが返ってくる)
  • Complete(成功時)
  • Incomplete(失敗時)
  • Cancel
LAVA test case

APIでjob登録するとして、どんなyamlを書いてるのかというとこんなのです。

# simple boot test
device_type: qemu
job_name: kernel build test x86-64 , boot test

timeouts:
  job:
    minutes: 15
  action:
    minutes: 10
  connection:
    minutes: 10
priority: medium
visibility: public

context:
  arch: amd64
  memory: 4096
  extra_options:
    - --append "root=/dev/sda1 audit=0 console=tty0 console=ttyS0,115200"
    - -serial stdio
actions:
- deploy:
    timeout:
      minutes: 3
    to: tmpfs

    images:
      kernel:
        image_arg: -kernel {kernel}
        url: http://192.168.11.18:12080/bzImage
      initrd: 
        url: http://192.168.11.18:12080/initramfs-build-test.img
        image_arg: -initrd {initrd}        
      rootfs:
        image_arg: -drive file={rootfs} 
        url: http://192.168.11.18:12080/ktest.qcow2
- boot:
    timeout:
      minutes: 2
    method: qemu
    media: tmpfs
    prompts: 
      - "Last login:"
      - "root@ktest:"
    auto_login:
      login_prompt: "ktest login:"
      username: "root"

contextのところでqemuに渡すパラーメータを設定しています。actionsはテスト対象のカーネル、initrdなどの取得先とqemuに渡すパラメータの設定です。rootfsはその名の通りroot filesystemです。これはfedora30のserver版を使って作りました。ディスクに3GB割り当てたけど1GBでも充分だったな。 bootのところがbootテストの内容です。auto_loginのところで指定したログインプロンプトが表示されたらusernameを送ってます。rootfsのほうでrootのパスワードは無しに設定しているのでパスワード無しでログインしてます。もちろんパスワードプロンプトとパスワードを設定することできます。promptsは期待値になる部分で、ログインに成功したときに表示されるものをリストしてます。

lavaの出力のこの辺です。

f:id:masami256:20190714162213p:plain
lava

まとめ

BuildbotとLAVAを使い、ここまでの設定でカーネルを弄ってコミットするとビルド・テストが自動で実行できるようになりました🎉

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

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

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