Quantcast
Channel: φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ
Viewing all 191 articles
Browse latest View live

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

$
0
0

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) \staticstruct _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");
        return1;
    }
    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()を読んでデータを登録する形となっています。

staticvoid dynamic_debug_setup(struct module *mod, struct _ddebug *debug, unsignedint num)
{
    if (!debug)
        return;
#ifdef CONFIG_DYNAMIC_DEBUGif (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の利用箇所などの情報を取得することができるという感じでした。


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

$
0
0

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/installkernelgrubby
/usr/libexec/installkernel/installkernel-blsgrubby
/bin/kernel-installsystemd-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すれば良いと思います。

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

$
0
0

はじめに

本記事は過去に書いたけど諸事情により陽の目に出ていなかった文書です。完全版ではありません。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フラグ
:dSLAB_CACHE_DMA
:aSLAB_RECLAIM_ACCOUNT
:FSLAB_DEBUG_FREE
:tSLAB_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_slabcpuに紐付いているスラブキャッシュ
flagsスラブキャッシュ生成時に指定したフラグ
min_partial使用中のスラブキャッシュを保持できる最小の数。デフォルトは5、最大で10
sizeobject_sizeをalignの境界に整列したサイズ
object_sizeメタデータを含まないスラブオブジェクトのサイズ
offsetオブジェクトのサイズ+offsetで次の空きスラブオブジェクトを指す
cpu_partialcpu毎に保持できる使用中スラブキャッシュ数
ooスラブキャッシュ作成時に確保するページ数のオーダー
maxスラブキャッシュ作成時に確保するページ数のオーダー数最大値
minスラブキャッシュ作成時に確保するページ数のオーダー数最小値
allocflagsslabオブジェクトを確保する際に使用するgfpフラグ
refcountスラブマージ機能によってマージされたスラブキャッシュの数
ctorスラブキャッシュ作成時に呼び出すコンストラクタ関数
inuseスラブオブジェクトの使用数
alignオブジェクトをsizeof(void *)の境界に整列した値。CPUのキャッシュラインに整列する場合は、先にCPUのキャッシュラインに合わせる
nameスラブキャッシュの名前
list作成したスラブキャッシュのリスト
memcg_paramsmemory cgroup用の設定
remote_node_defrag_ratio他のNUMAノードからスラブオブジェクトを取得する割合
nodeNUMAノード毎のスラブキャッシュ情報

表_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_locksl[au]bアローケータ共通
nr_partial使用中リストで管理しているデータ数
partial使用中リストにあるスラブが使用しているpage構造体を管理するリスト

表_kmem_cache_node構造体

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

名前内容
freelist空きオブジェクトを参照
countersオブジェクト数などを管理するカウンター
inuse使用中のスラブオブジェクト数
objectスラブオブジェクト数
frozenページのfronzen状態
next次の使用中リストへのポインタ
pagesスラブキャッシュに使用しているページフレーム数
slab_pagekmem_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_DMAGFPフラグにGFP_DMAを設定し、DMA向けのスラブキャッシュを作成する
SLAB_PANICkmem_cache_crate関数が失敗した場合にカーネルパニックする
SLAB_DESTROY_BY_RCUスラブキャッシュの解放時にRCUを使用して、非同期に行う
SLAB_MEM_SPREADcpuset機能を使用しているプロセスの場合、cpusetで設定されたcpuのノードよりメモリを確保する(slabアローケータ専用)
SLAB_NOLEAKTRACEkmemleakによるトレースを行わない
SLAB_NOTRACKkmemcheckによるチェックを行わない
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スラブキャッシュをまだ使用できない
PARTIALslubアローケータのkmem_cache_node構造体が使用可能になった(kmem_cache_init関数内で設定)
PARTIAL_NODEslabアローケータで使用
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 longcounters_old
void *freelist_new
unsigned longcounters_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関数固有な処理は特にありません。

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

$
0
0

はじめに

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

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

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

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

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

f:id:masami256:20190510203636p:plain

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

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

x86_64のメモリレイアウト
開始アドレス終了アドレスサイズ内容
0x00000000000000000x00007fffffffffff128TiBユーザプロセス空間
0xffff8000000000000xffff87ffffffffff8TiBハイパーバイザー向けに予約
0xffff8800000000000xffffc7ffffffffff64TiBダイレクトマップ領域
0xffffc800000000000xffffc8ffffffffff1TiB未使用
0xffffc900000000000xffffe8ffffffffff32TiBvmalloc/ioremapが使用
0xffffe900000000000xffffe9ffffffffff1TiB未使用
0xffffea00000000000xffffeaffffffffff1TiB仮想メモリマップ
0xffffeb00000000000xffffebffffffffff1023GiB未使用
0xffffec00000000000xfffffc000000000016TiBkasan shadow memory
0xfffffc00000000000xfffffeffffffffff3072GiB未使用
0xffffff00000000000xffffff7fffffffff512GiBスタック領域
0xffffff80000000000xffffffff7fffffff509GiB未使用
0xffffffff800000000xffffffffa0000000512MiBカーネルテキストマッピング領域。物理メモリ0~512MiB
0xffffffffa00000000xffffffffff5fffff1525MiBカーネルモジュールマッピング領域
0xffffffffff6000000xffffffffffdfffff8MiBvsyscallsで使用
0xffffffffffe000000xffffffffffffffff2MiB未使用

表_メモリマップ

この内訳は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のvmalloc()(ドラフト)

$
0
0

はじめに

前回の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_pagespagesで使用しているpage構造体数
phys_addr物理メモリのアドレス(ioremap関数の場合に使用)
caller非連続メモリ領域を確保した関数のアドレス

表_vm_struct構造体

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

変数名内容
VM_IOREMAPioremap関数でメモリを確保する場合に設定
VM_ALLOCvmalloc関数でメモリを確保する場合に設定
VM_MAPvmap関数でメモリを確保する場合に設定
VM_VPAGESpage構造体の配列作成時に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_nodevmap_area構造体をアドレスでソートした赤黒木
listvmap_area構造体を管理する連結リスト
purge_listvmap_area構造体のリスト。vmap areaを解放する時に使用する
vmこのvmap areaと関連しているvm_strcuct構造体
rcu_headvmap_area構造体のメモリを解放するときに使用するRCU構造体

表_vmap_area構造体

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

変数名内容
VM_LAZY_FREE使用していたvmap_area構造体を解放する時に設定
VM_LAZY_FREEINGvmap_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の名前空間(ドラフト)

$
0
0

はじめに

前回の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_RESTARTSIGHUP
LINUX_REBOOT_CMD_RESTART2SIGHUP
LINUX_REBOOT_CMD_POWER_OFFSIGINT
LINUX_REBOOT_CMD_HALTSIGINT

表_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ファイルシステムにエクスポートしたファイルのデータです。

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

表_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_nsUTS名前空間の構造体
ipc_nsIPC名前空間の構造体
mnt_nsMount名前空間の構造体
pid_ns_for_childrenPID名前空間の構造体
net_nsNet名前空間の構造体

表_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」に示した内容が設定されます。

変数名内容
count1
uts_nsinit_uts_ns
ipc_nsinit_ipc_ns
mnt_nsNULL
pid_ns_for_childreninit_pid_ns
net_nsinit_net

表_init_nsproxy

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

名前空間ファイル
UTSinit/version.c
IPCipc/msgutil.c
PID kernel/pid.c
Netnet/core/net_namespace.c
Userkernel/user.c

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

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

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

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

フラグ名内容
CLONE_NEWIPCIPC名前空間の分離
CLONE_NEWNETNET名前空間の分離
CLONE_NEWNSMOUNT名前空間の分離
CLONE_NEWPIDPID名前空間の分離
CLONE_NEWUSERUSER名前空間の分離
CLONE_NEWUTSUTS名前空間の分離

表_名前空間の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この名前空間の参照カウンタ
nsns_common構造体
rootこの名前空間におけるルートファイルシステム
listmount構造体の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_UNPRIVILEGEDMS_NOSUIDやMS_RDONLYなどのフラグの設定変更を許可しない

表_CLフラグ

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

引数名説明
old現在のMount名前空間に設定されているルートファイルシステム
rootoldのディレクトリエントリ
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_ctlsSEMMSL,SEMMNS,EMOPM,SEMMNIを表す配列
used_sems作成済みセマフォ
msg_ctlmaxSystemVメッセージの最大サイズ
msg_ctlmnbSystemVメッセージキューが保持できるメッセージの最大値
msg_bytesSystemVメッセージキューのサイズ
msg_hdrsSystemVメッセージキュー数
shm_tot確保された共有メモリ数
shm_ctlmni共有メモリセグメントの最小サイズ
shm_rmid_forced1を設定した場合、使用者がなくなれば全てのSystemV共有メモリセグメントに破棄済みマークを設定する
ipcns_nbnotifier chain
mq_mntmqueuefsのマウントデータ
mq_queues_countPOSIXメッセージキュー数
mq_queues_maxPOSIXメッセージキューの最大値
mq_msg_max1つのキューに入れることができるメッセージの最大数
mq_msgsize_maxメッセージの最大サイズ
mq_msg_defaultmq_open関数呼び出し時にattrをNULLに指定した場合のmq_maxmsgのデフォルト値
mq_msgsize_defaultmq_open関数呼び出し時にattrをNULLに指定した場合のmq_msgsizeのデフォルト値
user_nsIPC名前空間が所属するユーザ名前空間
nsns_common構造体

表_ipc_namespace構造体

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

変数名説明
in_use割り当てたIPC識別子数
seqIPCオブジェクト生成のシーケンス番号
rwsemIPC名前空間を操作するときに使用するセマフォ
ipcs_idrIPCオブジェクトのIDを管理
next_idIPDオブジェクト作成時に設定する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所属するユーザ名前空間
nsns_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_nsNet名前空間に設定するユーザ名前空間
netns_idsNet名前空間のID
nsns_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配列の長さ
rcurcuによるロックの取得に使用
ptr実際のデータを格納する配列

net_generic構造体

Net名前空間の管理

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

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

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

変数名説明
listlinked list
initコンストラク
exitデストラク
net_exit_listデストラク
idid
sizesize

表_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リファレンスカウンタ
pidmapPIDを管理するビットマップ
rcupid構造体のロック
last_pid最後に使用したPID
nr_hashedPID構造体を管理するハッシュテーブルのデータ数
child_reaperゾンビプロセスを回収するためのプロセス。init_taskを設定する
pid_cacheppid構造体のスラブキャッシュ
levelPID名前空間の階層
parent親のPID名前空間
proc_mntprocファイルシステム
proc_selfprocファイルシステムのselfファイル
bacctBSDプロセスアカウンティング情報
user_nsこの名前空間が所属するUser名前空間
pid_gidprocファイルシステムにアクセスする際に利用されるgid
hide_pidprocファイルシステムのマウントオプションのhidepid
rebootPID名前空間をリブートするときのexitコード
nsns_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_mapuidのマッピング
uid_mapgidマッピング
projid_mapプロジェクト識別子のマッピング
count名前空間の参照数
parent親の名前空間
levelユーザー名前空間の階層数
ownerプロセスのeuid
groupプロセスのegid
nsns_common構造体
flagssetgroups(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

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

$
0
0

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カーネルのBuildbot + LAVAでCI環境を作るめも

$
0
0

ちょっと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 python3import xmlrpc.client
import sys
import time
import yaml

defcreate_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)

defsubmit_job(server, job):

    return server.scheduler.submit_job(job)
    
defread_job_yaml(filepath):
    withopen(filepath, 'r') as f:
        return f.read()

defwait_until_job_finish(server, job_id):

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

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

defcheck_job_result(server, job_id):
    results_raw = server.results.get_testjob_results_yaml(job_id)
    try:
        from yaml import CLoader as Loader
    exceptImportError:
        from yaml import Loader
        
    results = yaml.load(results_raw, Loader=Loader)
    
    for ret in results:
        r = ret['result']
        if r == 'fail':
            returnFalsereturnTrueif __name__ == '__main__':
    ifnotlen(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 testdevice_type: qemu
job_name: kernel build test x86-64 , boot test

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

context:arch: amd64
  memory:4096extra_options:- --append "root=/dev/sda1 audit=0 console=tty0 console=ttyS0,115200"- -serial stdio
actions:- deploy:timeout:minutes:3to: 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:2method: 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を使い、ここまでの設定でカーネルを弄ってコミットするとビルド・テストが自動で実行できるようになりました🎉


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

$
0
0

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/bashconfigs=(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$configdone

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でのカーネルハックが捗ると思います😃

UEFIのSecure boot + kdump・kexec

$
0
0

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カーネルの修正コードから学ぶソフトウェア品質~

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

$
0
0

このブロクで使ってるカテゴリは↓のようになっていて、こいつ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

Fedora Kernel test week参加記録

$
0
0

FedoraではkernelのテストイベントとかGnome 3.34 Test DayI18N Test Dayなどのテストイベントがちょくちょくあります。 今回はKernel 5.3 Test Weekの備忘録です。ちなみに、この手のイベントはfedora MAGAZINEで紹介されることがあります。

今回の参加結果の最終成果はこちらです。

github.com

参加ε≡≡ヘ( ´Д`)ノ

今回のtest weekは開催時期が9月末から10月の最初の週でちょうどfedora 31のリリースが近かったころです。当時のfedora 29と30はカーネルは5.2系が使われていて、fedora 31が5.3系って感じでした。メインラインは5.4のrc1が出てた辺りです。それでfedora 31もそろそろ出るし、ベータとかrcのテストも参加してなかったし、メインラインのカーネルも最近使ってないなーなんてことでテストに参加しとくかーって感じで参加したわけです。

バグ発見/(^o^)\

テストに使うカーネルrpmをデスクトップ環境のfedora 30マシンにインストールして再起動すると、本来GDMのログイン画面がでるところでスクリーンがブリンクし続ける状態でした。一応メインラインのカーネル(5.4-rc1)も試してみたけど結果は同じでした。ここまでで5.3〜で症状が出るらしいってことは確定。というわけでここまでの内容で一旦bugzillaにバグレポートしました。

1758831 – GDM login screen doesn't show up on kernel 5.3.2

その後ですが、じゃあ、fedora 31ならどうよ?と思ってテスト用にビルドされたライブデスクトップのisoイメージを使ってみたところこちらも同様の現象。ただブートメニューからTroubleshooting" ->"Start kernel in basic graphic mode"を選ぶと問題なし。間違いなくGPU関連のバグですねって感じです。この情報もbugzillaに追記しました。

debug🐝

一応ここまででバグを見つけた人として一応のことはしたのですが、自分のPCで新しいカーネルが使えないのは最高に困るのでここからはbisectして原因を探りました。バグに当たっている人がテスト時点では自分くらいしかいなかったし、他のディストリビューションを見てもArch Linuxで同じような現象の人が1人くらいいたかなーって感じでした。そうするとバグは機種依存っぽいし自分でデバッグしたほうが良さげだったんですね。

ということでbisectを開始したわけですがバグが実機じゃないと再現できないのでダラダラやりつつ土曜日の朝から夕方まで使った気がします。そして、first bad commitは36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099というのがわかりました。ちなみにこのpatchは一連のシリーズの中の一つなのと、このコミット以降でリファクタリングされていてファイルのパスやヘルパー関数が追加されています。さて、差分ですがpatchだけ見るとわかりにくいので自分の環境で動かなくなった原因のsanitize_aux_ch()より主要部分のbefore/afterを示すとこうなります。

before

conststruct ddi_vbt_port_info *info =
        &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        i->supports_dp = false;
        i->alternate_aux_channel = 0;
    }

after

struct ddi_vbt_port_info *info = &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        info->supports_dp = false;
        info->alternate_aux_channel = 0;
    }

大きな違いはfor文中のif文でcontinueしなかった場合に使用している変数です。変更前は配列のp番目の要素をiにセットしてその変数の構造体のデータに値を設定していますが、変更後の方は最初にinfoに設定した値をそのまま使っていてp番目の要素に対する変更は行っていません。そんなわけで、変更前と同様の挙動にしたらどう?と思って作ったのが以下の修正です。

diff --git a/drivers/gpu/drm/i915/display/intel_bios.c b/drivers/gpu/drm/i915/display/intel_bios.c
index efb39f350b19..c886dae82aa7 100644
--- a/drivers/gpu/drm/i915/display/intel_bios.c
+++ b/drivers/gpu/drm/i915/display/intel_bios.c
@@ -1313,6 +1313,8 @@ staticvoid sanitize_aux_ch(struct drm_i915_private *dev_priv,
 
        p = get_port_by_aux_ch(dev_priv, info->alternate_aux_channel);
        if (p != PORT_NONE) {
+               info = &dev_priv->vbt.ddi_port_info[p];
+
                DRM_DEBUG_KMS("port %c trying to use the same AUX CH (0x%x) as port %c, ""disabling port %c DP support\n",
                              port_name(port), info->alternate_aux_channel,
@@ -1330,6 +1332,7 @@ staticvoid sanitize_aux_ch(struct drm_i915_private *dev_priv,
                info->supports_dp = false;
                info->alternate_aux_channel = 0;
        }
+
 }
 
 staticconst u8 cnp_ddc_pin_map[] = {

このpatchをstableの5.3.2に当てたところちゃんと動くようになりましたワーイヽ(゚∀゚)メ(゚∀゚)メ(゚∀゚)ノワーイ これでとりあえず自分の環境では新しいカーネルでもなんとか動くようになったわけですが、ローカルパッチを当て続けるとかあり得ないですね。なので次はupstreamへの報告となります。

36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099を見るとsanitize_ddc_pin()も同じような変更が入ってるのですが、こちらは自分の環境では影響出てなかったです。GPUわからん😨

freedesktop.orgのbugzillaに苦戦😭

upstreamにバグ報告と言っても報告先として考えられるのはlkml、サブシステムのメーリングリストkernelのbugzilla、またはそれ以外とあります。GPU(i915)のバグ報告先をさがしたところ↓が見つかりました。ここにレポートの仕方が書かれていたのでまずはそれに従います。drm-tipツリーをcloneしてきてこのツリーの最新コードを試したり。。

How to report bugs | 01.org

バグのレポートはfreedesktop.orgのbugzillaを使うということだったのでアカウントを作りました。そしていざレポートってところで色々書いてsubmitボタンを押すと空白ページになるだけでバグが登録できません😨 たまたまバグったのかな?と思ってもう一度試してもダメ、じゃあ時間を置いてと思って次の日に試してもダメ。。。バグ報告したいのにバグ報告できないというバグにハマったわけです\(^o^)/オワタ こんなのわからんよと思ってbugzillaについて聞けるところはないかな?と探したところircで聞けるというのがわかったのでircクライアントをインストールし、freenodeにある#freedesktopに入って状況説明したところ中の人がログを確認したりデバッグを手伝ってくれました。このときのバグレポートはfedoraと同じ感じで書いていたのでタイトルにloginという単語が入っていたのですが、これがバグの原因というのがわかりました。だいたい2時間位色々と試したんじゃないかな。。 ちなみにLoginだと大丈夫で小文字のloginという文字列がバグレポートの新規作成時に入っているとダメという状況でした。既存のバグレポートの編集時だと大丈夫という謎。 それはともかく、手伝ってくれた方には感謝しかありません👃

upstreamへの報告

なんだかんだで無事に報告できました。タイトルについてるbisectedというのはbisect結果を貼ったあとに変更されました。ちゃんと見てくれてますね( ̄ー ̄)bグッ!

111966 – [bisected] GDM Login screen doesn't show up on kernel 5.3 or later

まず普通にバグの報告したあとでbisectの結果やquick hackなパッチをレポートしました。その後、同じような現象の人が現れ、この方も自分が作ったパッチでバグが直ったという報告をしてくれました👃 その後のちゃんとしたパッチの作成までは結構速く進み、コミット36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099の設定方法だとダメな機種があるということでコメント等適切に直したパッチが作成されました。自分ともう一人の方はこのパッチをテストして動くのを確認しました。

drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin · torvalds/linux@0336ab5 · GitHub

修正方法は自分がやった方法と同じなので自分のパッチも暫定修正とはいえ正しい修正方法にたどり着けててエライって感じです😃

サブシステムへの取り込み -> mainlineへの取り込み -> stable treeへの取り込み待ち

修正パッチがサブシステムのメーリングリストに投げられたのであとは次の3個の待ちとなります。とくに自分がすることはありません。

  1. サブシステムのtreeへの取り込み
  2. mainlineへの取り込み
  3. stable treeへの取り込み

3のときに Patch "drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin" has been added to the 5.3-stable treeってメールが届いて5.3.8(5.3.7はすでにリリースされてたので次は5.3.8のときだった)にパッチが入るんだなーってのがわかりました。

freedesktop.orgのbugzillaは1か2のタイミングでcloseされたと思います。

fedoraへの取り込み待ち

新しいstable releaseが出れば fedoraカーネルが最新のstable releaseに追従したときにfedoraカーネルでもバグが直ります。なのでこちらも待ちです(-_-)zzz fedoraのパッケージは最初updates-testingというリポジトリにアップロードされます。そこで人間がテストするか一定期間経てばupdatesリポジトリにパッケージが入ります。で、fedoraカーネル5.3.8のパッケージがアップロードされたので早速テストして結果を書きました。

https://bodhi.fedoraproject.org/updates/FEDORA-2019-0e85bbd15b

あと、fedoraのbugzillaのほうにも5.3.8のカーネルパッケージで直ったという報告を書いておきました。で、こちらもクローズとなりました。

終わり

というわけでkernel test weekへの参加とバグ発見、bisectとローカルパッチの作成、upstreamへの報告とupstreamへの協力、fedoraでの修正確認ということを最近しました( ´ー`)フゥー... 今回一番難しかったのはbugzillaでバグ報告できない問題でしたね。自分の環境で動かせられないし/(^o^)\

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

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

unameコマンドから始めるデバッグ&カーネルハック入門

$
0
0

この記事はLinux Advent Calendar 2019の1日目の記事です。

はじめに

本記事ではLinuxサーバのホスト名、Linuxカーネルのバージョン、cpuアーキテクチャなどのシステム情報を表示するuname(1)を利用してLinux環境でのデバッグカーネルハックについて説明していきます。本記事ではコマンドやツールの使い方の説明ではなくて、それらを使ってどのようにデバッグするのかというところを説明します。

環境

ディストリビューションにはFedora 31(x86_64)を利用します。動作環境はQEMUlibvirtOracleVMVirtualBoxなどの仮想環境でも良いですし、物理マシンにインストールしても構いませんが仮想環境を利用したほうが手軽に環境構築できるのでおすすめです。ユーザーはsudoを使えるようにしておいてください。インストール時もしくはインストール後に以下のグループをインストールすると後で楽かもしれません。

  • C Development Tools and Libraries
  • Development Tools
  • RPM Development Tools
  • System tools
  • Guest Agents(仮想環境にインストールする場合)

パッケージグループのインストールは以下のコマンドで行えます。

[masami@unamebook ~]$ sudo dnf group install -y "C Development Tools and Libraries" "Development Tools" "RPM Development Tools" "System tools" "Guest Agents"

ファイルの編集をするのにエディタが必要です。これは好きなエディタを使ってください。

おことわり

本記事で行うツールの利用方法などはディストリビューションに依存しないことが多いと思いますが、コマンドの出力結果やソースコードについてはディストリビューション固有のpatchが当たっている場合もあり、fedora 31以外のディストリビューション(場合によってはfedora 31でもパッケージのバージョン違い)と違うことがあります。

uname(1)

uname(1)*1を実行したことのある方は多いと思いますが、まずは普通にuname(1)を実行しましょう。このコマンドはcoreutilsパッケージに含まれています。

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

unameコマンドに渡した-aオプションはすべての情報を表示させるオプションです。上記の出力結果を分解したものが以下の「表1 unameの出力内容」です。

出力内容該当するunameコマンドのオプション意味
Linux-sカーネル
unamebook-nホスト名
5.3.11-300.fc31.x86_64-rカーネルリリース
#1 SMP Tue Nov 12 19:08:07 UTC 2019-vカーネルバージョン*2
x86_64-mマシンのハードウェア名
x86_64-pプロセッサ名
x86_64-iハードウェアプラットフォーム
GNU/Linux-oオペレーティングシステム

表1 unameの出力内容

uname(1)が出力する内容

uname(1)を実行してシステム情報を見ることができましたがこの情報はどこから取得したのでしょうか?この情報はLinuxカーネルが持っていて、uname(1)はシステムコールを通じてカーネルに問い合わせを行い、カーネルから受け取った結果を表示しています。このときに利用するシステムコールuname(2)*3です。uname(2)のプロトタイプは次のようになっています。カーネルは引数で渡されたbuf変数にシステム情報をセットします。

int uname(struct utsname *buf)

uname(2)のインターフェース

uname(2)の詳細はmanページで確認してください。

[masami@unamebook ~]$ sudo dnf install -y man-pages

strace(1)

strace(1)は、uname(1)の実行時にuname(2)が呼ばれていることを確認しましょう。ここではデバッガは使わずにstrace(1)を利用します。strace(1)は簡単に言うと指定したコマンド・プロセスによるシステムコール呼び出しをトレースするコマンドです。まずはstraceパッケージをインストールします。

[masami@unamebook ~]$ sudo dnf install -y strace

straceパッケージをインストールしたら実行してみましょう。オプションの細かい内容は説明しませんが、ここではuname(2)に絞って表示させるように実行しました。

[masami@unamebook ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000007           2         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000007                     3           total

uname(2)の呼び出しが3回ありますね。なぜ3回uname(2)を実行しているのでしょうか?これはソースコードを確認しないとわかりません。unameコマンドを提供しているのはcoreutilsパッケージなのでcoreutilsパッケージのソースコードをダウンロードしましょう。利用しているのはfedoraにインストールされているcoreutilsパッケージなのでupstreamではなくfedoraソースコードを取得します。

[masami@unamebook ~]$ dnf download --source coreutils

ソースパッケージ(.src.rpm)をダウンロードしたらインストールします。

まず展開先のディレクトリをセットアップします。

[masami@unamebook ~]$ rpmdev-setuptree

そしてrpmコマンドでインストールします。

[masami@unamebook ~]$ rpm -i coreutils-8.31-6.fc31.src.rpm

インストール時に次のようなwarningが出ますが無視して大丈夫です。

warning: user mockbuild does not exist - using root
warning: user mockbuild does not exist - using root

これで~/rpmbuildにソースパッケージがインストールされましたが、ソースコードはまだ展開されていないでソースコードの展開&patchを当てましょう。と、その前にいくつか必要なパッケージをインストールします。

[masami@unamebook ~]$ sudo dnf builddep -y coreutils

これで大丈夫です。ではソースコードの準備をしましょう。

[masami@unamebook ~]$ cd rpmbuild/SPECS/
[masami@unamebook SPECS]$ rpmbuild -bp coreutils.spec

ソースコードを展開しpatchが適用されたソースコードは~/rpmbuild/BUILDにあります。

[masami@unamebook SPECS]$ cd ~/rpmbuild/BUILD/coreutils-8.31/
[masami@unamebook coreutils-8.31]$ ls
ABOUT-NLS   autom4te.cache  build-aux  configure     DIR_COLORS               dist-check.mk  GNUmakefile  lib       Makefile.am  NEWS    src     thanks-gen             THANKStt.in
aclocal.m4  bootstrap       cfg.mk     configure.ac  DIR_COLORS.256color      doc            init.cfg     m4        Makefile.in  po      tests   THANKS.in              TODO
AUTHORS     bootstrap.conf  ChangeLog  COPYING       DIR_COLORS.lightbgcolor  gnulib-tests   INSTALL      maint.mk  man          README  THANKS  THANKS-to-translators

uname(1)のソースコードはsrc/uname.cです。grepuname(2)の呼び出し箇所を調べると3箇所あることがわかります。

[masami@unamebook coreutils-8.31]$ grep -n "uname *(" src/uname.c
286:      if (uname (&name) == -1)
313:    uname(&u);
364:    uname(&u);

286行目付近を見るとこのようになっています。toprint変数はuname(1)の引数に応じてビットが立ちます。オプションに-aを渡した場合はこのif文は真になるので286行目のuname(2)が実行されます。

280if (toprint
    281& (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
    282           | PRINT_KERNEL_VERSION | PRINT_MACHINE))
    283     {
    284struct utsname name;
    285286if (uname (&name) == -1)
    287         die (EXIT_FAILURE, errno, _("cannot get system name"));

313行目はどうかというと、ここは-pオプションの処理部分です。マクロのelseブロックが実行されるようです。

304 #if HAVE_SYSINFO && defined SI_ARCHITECTURE
    305       {
    306staticchar processor[257];
    307if (0<= sysinfo (SI_ARCHITECTURE, processor, sizeof processor))
    308           element = processor;
    309       }
    310 #else311       {
    312staticstruct utsname u;
    313         uname(&u);
    314         element = u.machine;
    315       }

残りの364行目も確認しましょう。こちらは-iオプションのハードウェアプラットフォーム取得部分です。こちらもマクロのelseブロックが呼ばれているようです。

354 #if HAVE_SYSINFO && defined SI_PLATFORM
    355       {
    356staticchar hardware_platform[257];
    357if (0<= sysinfo (SI_PLATFORM,
    358                           hardware_platform, sizeof hardware_platform))
    359           element = hardware_platform;
    360       }
    361 #else362       {
    363staticstruct utsname u;
    364         uname(&u);
    365         element = u.machine;
    366if(strlen(element)==4&& element[0]=='i'&& element[2]=='8'&& element[3]=='6')
    367                 element[1]='3';
    368       }
    369 #endif

この3箇所のuname(2)の呼び出しですが、286行目はupstreamのコードにもあります。しかし、残りの2箇所はupstreamのコードには存在しません。この2箇所はfedoracoreutilsパッケージが独自当てているpatchです。このpatchは~/rpmbuild/SOURCES/coreutils-8.2-uname-processortype.patchです。興味のある方は確認してみてください。

gdb(1)

ソースコードを読んでuname(2)の呼び出し箇所が3箇所あることはわかったのですが、一応実際の動作を見てみましょう。ここではgdbを利用します。gdbを使ってデバッグをする場合はデバッグ情報があると便利です。通常のパッケージに含まれるバイナリファイルはデバッグ情報が存在しないため、デバッグが不便です。fedoraの場合はデバッグ情報はdebuginfoパッケージとして存在しているのでこれをインストールします。glibcのdebuginfoパッケージも合わせてインストールします。

と、その前にdebuginfo-installコマンドが必要なのでパッケージをインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y dnf-utils

dnf-utilsパッケージをインストールしたらdebuginfo-installコマンドが利用できますので、これでdebuginfoのパッケージをインストールします。

[masami@unamebook ~]$ sudo debuginfo-install -y coreutils glibc

debuginfoパッケージをインストールしたらgdbを起動しましょう。gdbは自動的にデバッグ情報を読み込んでくれます。

[masami@unamebook ~]$ gdb -q /usr/bin/uname
Reading symbols from /usr/bin/uname...
Reading symbols from /usr/lib/debug/usr/bin/uname-8.31-6.fc31.x86_64.debug...
(gdb) 

gdbが立ち上がったらとりあえず280行目のif文あたりにブレークポイントを張りましょう。listコマンドで280行目付近を表示します。

(gdb) list 280
275       toprint = decode_switches (argc, argv);
276
277       if (toprint == 0)
278         toprint = PRINT_KERNEL_NAME;
279
280       if (toprint
281            & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
282               | PRINT_KERNEL_VERSION | PRINT_MACHINE))
283         {
284           struct utsname name; 

breakコマンド(略性はb)で280行目にブレークポイントをセットします。

(gdb) b 280
Breakpoint 1 at 0x27b4: /usr/src/debug/coreutils-8.31-6.fc31.x86_64/separate/../src/uname.c:280. (2 locations)

runコマンド(略称はr)でunameコマンドを実行します。uname(1)のオプションには-aを渡しています。unameコマンドを実行すると先程設定したブレークポイントで止まるので、あとはnでステップ実行していけばuname(2)の呼び出しが3回行われるのがわかります。

(gdb) r -a
Starting program: /usr/bin/uname -a

Breakpoint 1, main (argc=2, argv=0x7fffffffe448) at ../src/uname.c:280
280       if (toprint
(gdb) n
286           if (uname (&name) == -1)
(gdb)  n
~略~
301       if (toprint & PRINT_PROCESSOR)
(gdb) 
313             uname(&u);
(gdb) n
~略~
(gdb) n
364             uname(&u);
(gdb) c
Continuing.
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
[Inferior 1 (process 6522) exited normally]

今回はプログラムが単純で短いのでステップ実行で済ませています。 ここまででstrace(1)、ソースコードリーディング、gdb(1)を使いuname -aの実行時にuname(2)が3回呼ばれる謎を調べることに成功しました。

bpftrace

bpftraceはLinuxのeBPFを使用したトレースツールです。eBFPとはextended Berkeley Packet Filterの略で元来はパケットフィルタの機能ですがLinuxではこのパケットフィルタ機能が進化し、パケットフィルタだけでは収まらなくなった機能です。Linuxではbpf(2)があり、c言語でeBPFの機能を使ったプログラムを作成できますが、これは大変なので普通はBCCを使うことが多いのではないでしょうか。BCCはBPF Compiler Collectionの略でPythonc言語を使ってeBFPを利用するプログラムを作ることが出来ます。そしてbpftraceですがBCCをバックエンドとして使った高レベルのトレース用言語と説明されています。bpftraceではカーネルレベルのトレースを取ることが出来ます。Fedoraにはパッケージがあるのでインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y bpftrace

インストールが完了したらuname(2)のトレースを行ってみましょう。まずはシステムコールの呼び出しをトレースします。 この他に、カーネルのソースも読みたいのでこちらもインストールしましょう。今回はdebuginfo-installコマンドを使用してデバッグ情報とソースをインストールします。coreutilsのときは単にソースを取得したいだけだったのでsrpmパッケージをインストールしましたが、debuginfoパッケージの場合はデバッグ情報とソースコードをインストールできます。gdb(1)の章でcoreutilsglibcのdebuginfoパッケージをインストールしましたが、このときにソースコードもインストールされいます。じゃあ、この2つの使い分けは?という疑問があると思いますが、ソースを変更して再ビルドする用途にはsrpmパッケージを、デバッグ情報のみが必要な場合はdebuginfoパッケージを使用すれば良いと思います。では、カーネルのdebuginfoパッケージをインストールします。

[masami@unamebook ~]$ sudo debuginfo-install -y kernel-debuginfo-common-x86_64

debuginfoパッケージに含まれるソースコードは/usr/src/debug以下にインストールされます。

[masami@unamebook ~]$ ls /usr/src/debug/
coreutils-8.31-6.fc31.x86_64  glibc-2.30-13-g919af705ee  kernel-5.3.fc31

インストールできたらカーネル側のuname(2)を処理する関数を探します。システムコールはinclude/linux/syscalls.hで探すことが出来ます。カーネルソースコードは以下の場所にあります。

[masami@unamebook ~]$ cd /usr/src/debug/kernel-5.3.fc31/linux-5.3.11-300.fc31.x86_64/

まずはunamegrepをかけましょう。

[masami@unamebook linux-5.3.11-300.fc31.x86_64]$ grep uname include/linux/syscalls.h
asmlinkage long sys_newuname(struct new_utsname __user *name);
asmlinkage long sys_memfd_create(const char __user *uname_ptr, unsigned int flags);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);

いくつか見つかりますね。sys_のprefixはシステムコールを表しています。名前からsys_newuname、sys_unameのどちらかだろうと想像できますね。ファイルを実際に読んで確認しましょう。まずはそのままな名前のsys_unameを見てみます。

/* obsolete: kernel/sys.c */
asmlinkage long sys_gethostname(char __user *name, int len);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);

obsoleteってコメントがありますね。こちらではなさそうです。ではsys_newunameはどうでしょうか。こちらは特にコメントもありません。uname(2)の実行時はsys_newuname()が実行されるようですね。

asmlinkage long sys_setpriority(int which, int who, int niceval);
~略~
asmlinkage long sys_newuname(struct new_utsname __user *name);

コメントにkernel/sys.cとあるので実装はこのファイルにあるようです。では、このファイルも見てみましょう。SYSCALL_DEFINE1というのはシステムコールの関数に使われるマクロです。SYSCALL_DEFINE1の1は引数を一つ受け取るという意味です。実装は比較的単純ですね。

SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
        struct new_utsname tmp;

        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;

        if (override_release(name->release, sizeof(name->release)))
                return -EFAULT;
        if (override_architecture(name))
                return -EFAULT;
        return0;
}

ここまででユーザー空間でuname(2)を実行するとカーネル空間のsys_newuname()が実行されることがわかりました。前置きが長くなりましたが実際にbfptrace(1)を使って見ましょう。まずは単純にsys_newutsname()の実行をトレースします。この関数が実行されたときに呼び出し元のコマンド名を表示させるのが以下のコマンドラインです。

[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'

コマンドを入力してちょっとすると次のようなメッセージが表示されます。終了したい場合はCtrl-Cで終了できます。

Attaching 1 probe...

メッセージが表示されたら別の端末からuname -aを実行すると以下のようにunameが3回表示されます。uname -aを実行するとuname(2)が3回実行されるというのはstrace(1)の章で調べましたね。

[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'
Attaching 1 probe...
uname
uname
uname
^C

systemtap

systemtapはbpftrace同様にカーネルのトレーシングを行うことができるツールです。systemtapを応用するとトレース以上のこともできます。まずはインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y systemtap

systemtapはstap(1)がコマンドラインのプログラムとなります。systemtapは専用のスクリプト言語があり、c言語のコードを埋め込むこともできます。本章ではsystemtapを使ってsys_newutsname()の挙動を変更してみましょう。uname(1)で表示するノード名などはutsname()から取得しています。utsname()の戻り値をtmp変数に一旦保存し、それをcopy_to_user()でユーザー空間のプログラムから渡されたnameにコピーします。

SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
        struct new_utsname tmp;

        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;

        if (override_release(name->release, sizeof(name->release)))
                return -EFAULT;
        if (override_architecture(name))
                return -EFAULT;
        return0;
}

では、systemtapを使ってプログラムにnodenameを書き換えてみましょう。systemtapを利用したライブパッチ機能の実装という感じです。systemtapのプログラムは次のようになります。

#!/usr/bin/env stap

%{
#include <linux/string.h>
#include <linux/uaccess.h>
#include <uapi/linux/utsname.h>
%}

function set_dummy_uname:long(name:long)
%{
        struct new_utsname u = { 0 };
           
        if (copy_from_user(&u, (struct new_utsname *) STAP_ARG_name, sizeof(u)))
        STAP_RETURN(-EFAULT);
    
        strcpy(u.nodename, "livepatched");

        if (copy_to_user((struct new_utsname *) STAP_ARG_name, &u, sizeof(u)))
                STAP_RETURN(-EFAULT);
    
        STAP_RETURN(0);

%}

probe begin {
        printf("start livepatch code. Ctrl-C to stop\n")
}

probe kernel.function("__do_sys_newuname").return {
    // Only set dummy nodename to test program
    if (execname() == "uname-test") {
        // Use @entry() to get name parameter in return probe.
        set_dummy_uname(@entry($name))
    }
}

probe end {
        printf("Done\n")
}

このプログラムではシステムコールのreturn時に処理を入れています。まずシステムコールを呼び出したプログラム名を調べ、テストプログラムでなければ特に変更は行わないで終了します。nodenameを設定しているのはset_dummy_uname()で呼び出し時にシステムコールのパラメータとして渡されたname変数をset_dummy_uname()の引数として渡しています。システムコールを呼び出したのがテストプログラムならnodenameをlivepatchedに変更します。systamtapスクリプトの特徴として、自分で実装した関数の引数にアクセスする場合はSTAP_ARG_変数名でアクセスします。このスクリプトではnameという変数名で引数を受け取っているためSTAP_ARG_nameと書いています。

ここで使用するテストプログラムですが以下のような簡単なものです。

#include <stdio.h>#include <sys/utsname.h>int main(int argc, char **argv)
{
    struct utsname name = { 0 };
    
    if (uname(&name)) {
        perror("uname");
        return -1;
    }

    printf("nodename: %s\n", name.nodename);
    return0;
}

このcコードをコンパイルしましょう。

[masami@unamebook ~]$ gcc uname-test.c -o uname-test

では、このsystemtapスクリプトを実行しましょう。ここでも端末を2つ使います。1つはuname-testプログラムを実行します。もう1つは次のようにsystemtapスクリプトを実行します。start livepatch ~と出たら準備完了です。

[masami@unamebook ~]$ sudo stap -g uname_livepatch.stp 
start livepatch code. Ctrl-C to stop

別の端末でテストします。

[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$ ./uname-test 
nodename: livepatched
[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$ 

次にsystemtapのプログラムをCtrl-Cで止めてuname-testを実行してみます。systemtapスクリプトを終了したことでsys_newuname()の挙動がもとに戻ったことでuname(2)の戻り値がもとに戻りましたね。

[masami@unamebook ~]$ ./uname-test 
nodename: unamebook

Kernel Hack

前章のsystemtapではsystemtapを利用してカーネルの挙動を変えてみました。本章ではカーネルソースコードを変更し、前章と同様の挙動に変えてみます。カーネルのソースパッケージにパッチを当てるにはgit-am(1)で当てることができるパッチを作る必要があります。パッチの作り方としてメインラインのカーネルを変更してパッチをつくるかFedoraカーネルソースを変更するの2パターンがあります。FedoraカーネルにはFedoraプロジェクトが適用したパッチも含まれているため、メインラインのカーネルを変更した場合、変更内容によってはFedoraカーネルソースとコンフリクトが発生する可能性もあります。そこで本章ではFedoraカーネルソースを変更してパッチを作ります。まずはカーネルのソースパッケージをダウンロードしてインストールします。以前coreutilsのソースパッケージをインストールしているので綺麗なrpmbuildディレクトリを作りたいところです。よって、既存のrpmbuildディレクトリはリネームして置いておき、新たにrpmbuildディレクトリを作成します。そしてソースパッケージをダウンロードしてインストールします。

[masami@unamebook ~]$ dnf download --source kernel
[masami@unamebook ~]$ rpm -i kernel-5.3.11-300.fc31.src.rpm 

カーネルパッケージのビルドに必要な依存パッケージのインストールを行います。

[masami@unamebook ~]$ sudo dnf builddep -y kernel
[masami@unamebook ~]$ sudo dnf install -y pesign

これでカーネルソースパッケージをビルドする準備は整ったのでですが、specファイルにあるbuildidを設定してFedorarpmと区別できるようにします。お好みのエディタでkernel.specを開き# define buildid .localの下に%define buildid .unametestを追加します。

# define buildid .local
%define buildid .unametest

この段階ではまだ~/rpmbuild/SOURCESにカーネルのコードやパッチが置かれているだけなのでソースコードを展開します。

[masami@unamebook SPECS]$ rpmbuild -bp kernel.spec

展開に成功すると~/rpmbuild/BUILD/kernel-5.3.fc31/linux-5.3.11-300.unametest.fc31.x86_64/にパッチが当てられたカーネルのコードが置かれます。また通常のカーネルソースツリーとは違いconfigsディレクトリが存在します。このディレクトリにはfedoraの各アーキテクチャ向けのconfigファイルが存在します。rpmbuildではなく、ローカル環境でビルドを試す際はこのディレクトリにあるconfigを使用してmake oldconfigを行うか、使用しているカーネルのコンフィグファイルが/bootにあるのでそれをコピーして使いましょう。 では、ここからはカーネルソースコードを弄っていきます。まずは~/rpmbuild/BUILD/kernel-5.3.fc31に移動し、 linux-5.3.11-300.unametest.fc31.x86_64をコピーして作業ディレクトリを作ります。コピー先の名前は任意です。ここでは linux-5.3.11-300.unametest.fc31.x86_64.hackとしました。

[masami@unamebook kernel-5.3.fc31]$ cp -a linux-5.3.11-300.unametest.fc31.x86_64 linux-5.3.11-300.unametest.fc31.x86_64.hack

一旦ソースをいじっていない状態でビルドができることを確認しましょう。この段階でビルドに成功することを確認しておけば、ソースの変更後にビルドが通らなくなった場合の問題の切り分けに役立ちます。この段階でgitのブランチも作っておきます。 linux-5.3.11-300.unametest.fc31.x86_64には.gitディレクトリがあり、gitがすでに利用できるようになっています。

[masami@unamebook kernel-5.3.fc31]$ cd linux-5.3.11-300.unametest.fc31.x86_64.hack/
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git checkout -b uname-hack
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp configs/kernel-5.3.11-x86_64.config .config
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make oldconfig
scripts/kconfig/conf  --oldconfig Kconfig
#
# configuration written to .config
#

make oldconfigは特に問題なく完了すると思います。では、カーネルカーネルモジュールをビルドします。bzImageで圧縮されたカーネルのイメージをビルドし、modulesでカーネルモジュールをビルドしています。-j$(nproc)は並列ビルドのオプションです。ログインしている環境で利用可能なcpu数をもとにビルドを並列実行します。カーネルのビルドは最後にarch/x86/boot/bzImage is readyというメッセージが出れば成功です。rpmのソースパッケージを使ってるのでrpmbuildでビルドしても良いのですが、それだと時間がかかるのでまずは手軽にmakeでビルドしています。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
BUILD   arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9085 kB
CRC dde74ef3
Kernel: arch/x86/boot/bzImage is ready  (#1)

モジュールのビルドも実行します。モジュールのビルドは成功時にこれといったログはありません。次のようにしれっとプロンプトが表示されていたら成功しています。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) modules
~略~
  LD [M]  sound/x86/snd-hdmi-lpe-audio.ko
  LD [M]  virt/lib/irqbypass.ko

さて、カーネルとモジュールのビルドに成功したら準備は完了です。実際にカーネルのソースに手を加えましょう。変更するのはkernel/sys.cです。カレントプロセス名を調べてプロセス名がuname-testならnodenameを変更します。やっていることはsystemtapの場合と同様です。変更内容は次のようになります。

diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
+
+       if (!strcmp(current->comm, "uname-test"))
+               strcpy(tmp.nodename, "livepatched");
+
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;
 

systemtapの場合は関数の途中にコードを入れていないので関数から返るところに処理を入れ込みましたが、今回は直接カーネルのコードを弄っているので素直なコードの変更になってますね。変更したらカーネルがビルドできるか確認しましょう。ビルド方法は先ほどと同じです。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
  BUILD   arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9089 kB
CRC 27223b7c
Kernel: arch/x86/boot/bzImage is ready  (#2)

今回も成功したらarch/x86/boot/bzImage is readyというメッセージが出ます。このメッセージの最後にある#2というのはこのカーネルをビルドしたのが2回目という意味です。ビルドに成功したのでパッチを作成しましょう。まず最初にgitのメールアドレスと名前を変更します。これはFedoraカーネルの場合、デフォルトではFedoraカーネルチームのメールアドレスが使われるので上書きして自分のメールアドレスと名前に変更します。そうしたら変更をコミットし、git-format-patch(1)でパッチを作ります。出来たパッチはカレントディレクトリに置かれます。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.email <your email>
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.name "your name"
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git add kernel/sys.c
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git commit -m "uname: add test code"
[uname-hack 9ea2b59] uname: add test code
 1 file changed, 4 insertions(+)
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git format-patch -s master
0001-uname-add-test-code.patch
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$

ここで出来たパッチは次のようになります。コミットメッセージ、変更内容、メール送信用のサブジェクトなどが設定されています。最後の2.23.0というのはgitのバージョンです。

From 9ea2b595c1c246a7ff94e6c4c721896ffcf9f5a6 Mon Sep 17 00:00:00 2001
From: Your Name <your email>
Date: Mon, 25 Nov 2019 23:26:46 +0900
Subject: [PATCH] uname: add test code

Signed-off-by:Your Name <your email>
---
 kernel/sys.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
+
+       if (!strcmp(current->comm, "uname-test"))
+               strcpy(tmp.nodename, "livepatched");
+
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;
 
-- 
2.23.0

では、このパッチをFedoraカーネルソースパッケージに追加してrpmbuild(8)でビルドしましょう。パッチを~/rpmbuild/SOURCESディレクトリにコピーします。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp 0001-uname-add-test-code.patch ../../../SOURCES/.

そして~/rpmbuild/SPECSに移動します。kernel.specファイルをエディタで開き、# END OF PATCH DEFINITIONSの行まで移動します。PatchXXX(Xは数字)の行がありますが、この数字は任意です。ここでは999としました。次のように# END OF PATCH DEFINITIONSの上に2行を追加します。

# uname hack patch
Patch999: 0001-uname-add-test-code.patch

# END OF PATCH DEFINITIONS

そうしたらビルドしましょう。ここでは必要最小限のカーネルパッケージだけ作成します。次のようなコマンドライン引数でビルドを行います。

[masami@unamebook SPECS]$ rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.0BA8HX
+ umask 022
+ cd /home/masami/rpmbuild/BUILD
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.unametest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0

出来上がったrpmファイルは~ rpmbuild/RPMS/x86_64/に置かれます。

[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.unametest.fc31.x86_64.rpm       kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm    kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm  kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm

出来上がったrpmをインストールします。

[masami@unamebook x86_64]$ sudo dnf localinstall -y ./*.rpm

インストールしたらカーネルの優先順を確認しましょう。通常はインストールしたカーネルで起動できると思いますが、出来なかった場合はここで調べたindexを使ってgrub2-rebootコマンドで起動するカーネルを設定してリブートします。

[masami@unamebook ~]$ sudo grubby --info=ALL
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
args="ro resume=UUID=c98315dc-7144-427b-a6a0-2aad53d8ebc5 rhgb quiet"
root="UUID=0b584087-de7b-42f8-8dea-d3b679d22613"
initrd="/boot/initramfs-5.3.11-300.unametest.fc31.x86_64.img"
title="Fedora (5.3.11-300.unametest.fc31.x86_64) 31 (Thirty One)"
id="24b7d4741c7c4982889ac4d4acb27ae1-5.3.11-300.unametest.fc31.x86_64"

再起動したらuname(1)でカーネルを確認しましょう。unametestが入っているのでビルドしたカーネルで正しく起動したかわかりますね。

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.unametest.fc31.x86_64 #1 SMP Mon Nov 25 23:32:51 JST 2019 x86_64 x86_64 x86_64 GNU/Linux

そして、systemtapのときにも使ったuname-testを実行してみましょう。

[masami@unamebook ~]$ ./uname-test 
nodename: livepatched

nodenameが変わりましたね。ここでuname-testをuname-test2としてコピーして実行すると、オリジナルのnodenameがでましたね。ということでカーネルに手を入れてuname(2)の挙動を変更することにも成功しました🎉

[masami@unamebook ~]$ cp uname-test uname-test2
[masami@unamebook ~]$ ./uname-test2
nodename: unamebook

Livepatch

systemtapの章ではsystemtapを利用してカーネルの挙動を変えてみましたが、LinuxカーネルにはLivepatchという機能があり、実行中のカーネルの挙動を再起動なしに変更する(patchを当てる)ことができる機能があります。本章ではこの機能でunameシステムコールの挙動を変更してみます。ただしFedoraカーネルではLivepatch機能は有効になっていないためカーネルのソースパッケージよりLivepatchを有効にしたカーネルを作る必要があります。カーネルのソースは前章でダウンロードしてきてあるのでこれに対してコードを弄っていきましょう。ここで本当はunameの挙動を変更するのがベターなところですが、unameの実装をLivepatchするのはちょっと手間なので単純に弄ることができるgetpid(2)を使います。

まず~/rpmbuild/SOURCESに移動し、Livepatchの機能を有効にする設定を追加します。

[masami@unamebook ~]$ cd rpmbuild/SOURCES/
[masami@unamebook SOURCES]$ echo "CONFIG_LIVEPATCH=y" >> kernel-local
[masami@unamebook SOURCES]$ echo "CONFIG_TEST_LIVEPATCH=m" >> kernel-local

kernel-localファイルはこのようになります。

[masami@unamebook SOURCES]$ cat kernel-local 
# This file is intentionally left empty in the stock kernel. Its a nicety
# added for those wanting to do custom rebuilds with altered config opts.
CONFIG_LIVEPATCH=y
CONFIG_TEST_LIVEPATCH=m

Fedoraではkernel-localファイルにカーネルコンフィギュレーションを設定することでベースとなる設定ファイル(x86_64ならkernel-x86_64.config)に設定を追加できます。 設定を追加したら~/rpmbuild/SPECに移動し依存パッケージのインストールを行います。kernel-localファイルを作ったらあとはビルドするだけです。

[masami@unamebook SOURCES]$ cd ~/rpmbuild/SPECS/

先程はunametestという名称をカーネルパッケージに追加しましたが、今度はgetpidtestという名称を付けましょう。kernel.specを以下のように変更します。

# define buildid .local
%define buildid .getpidtest

specファイルを変更したらカーネルをビルドします。

[masami@unamebook SPECS]$  rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.getpidtest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0

ビルドが終了したら~/rpmbuild/RPMS/x86_64に移動してlsするとgetpidtestと名前の付いたrpmが出来ているのが確認できます。

[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.getpidtest.fc31.x86_64.rpm       kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm    kernel-modules-5.3.11-300.getpidtest.fc31.x86_64.rpm        kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-5.3.11-300.unametest.fc31.x86_64.rpm        kernel-devel-5.3.11-300.getpidtest.fc31.x86_64.rpm  kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.getpidtest.fc31.x86_64.rpm  kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm   kernel-modules-extra-5.3.11-300.getpidtest.fc31.x86_64.rpm

新しくビルドしたrpmをインストールします。

[masami@unamebook x86_64]$ sudo dnf localinstall -y  kernel-*.getpidtest.*

次にgrubのエントリを確認します。

[masami@unamebook x86_64]$ sudo grubby --info=ALL | grep index -A1
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
--
index=1
kernel="/boot/vmlinuz-5.3.11-300.getpidtest.fc31.x86_64"
--
index=2
kernel="/boot/vmlinuz-5.3.11-300.fc31.x86_64"
--
index=3
kernel="/vmlinuz-0-rescue-24b7d4741c7c4982889ac4d4acb27ae1"

getpidtestのカーネルはindexが1となっているのでこのカーネルで起動するようにします。

[masami@unamebook x86_64]$ sudo grub2-reboot 1

これで準備完了なので再起動します。再起動したらもちろんuname(1)で正しいカーネルで起動したか確認します😄

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.getpidtest.fc31.x86_64 #1 SMP Tue Nov 26 23:10:42 JST 2019 x86_64 x86_64 x86_64 GNU/Linux

カーネルビルド時にライブパッチ機能が有効になっていたか確認します。fedoraは/bootにカーネルのコンフィグが置かれるのでこれで確認できます。

[masami@unamebook ~]$ grep CONFIG_LIVEPATCH /boot/config-5.3.11-300.getpidtest.fc31.x86_64 
CONFIG_LIVEPATCH=y

ではライブパッチを作りましょう。カーネルのライブパッチはカーネルモジュールとして作成します。まず作業ディレクトリを作ります。

[masami@unamebook ~]$ mkdir getpid-livepatch-module

そして、getpid-livepatch.cを以下の内容で作成します。your nameのところは適当に変えてください。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt#include <linux/module.h>#include <linux/kernel.h>#include <linux/livepatch.h>#include <linux/string.h>#include <linux/sched.h>

MODULE_DESCRIPTION("getpid livepatch test module");
MODULE_AUTHOR("your name");
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

asmlinkage long livepatch_getpid(void)
{
        pr_info("%s\n", __func__);
        
        if (unlikely(!strcmp(current->comm, "getpid-test")))
                return1;

        return  task_tgid_vnr(current);
}

staticstruct klp_func funcs[] = {
        {
                .old_name = "__x64_sys_getpid",
                .new_func = livepatch_getpid,
        }, {}
};

staticstruct klp_object objs[] = {
        {
                .funcs = funcs,
        }, {}
};

staticstruct klp_patch patch = {
        .mod = THIS_MODULE,
        .objs = objs,
};

staticint getpid_livepatch_init(void)
{
        int ret = 0;

        ret = klp_enable_patch(&patch);
        if (ret) {
                pr_info("failed to enable getpid live patch\n");
                return ret;
        }

        pr_info("getpid livepatch test module is enabled\n");
        return ret;
}

staticvoid getpid_livepatch_cleanup(void)
{
}

module_init(getpid_livepatch_init);
module_exit(getpid_livepatch_cleanup);

このライブパッチではgetpid(2)を実行したプログラム名がgetpid-testの場合に1を返し、それ以外の場合は普通にpidを返しています。このファイルをビルドするのにMakefileが必要ですのでこれも作成します。Makefileのインデントはtabなんで気をつけてください。

KERNDIR := /lib/modules/`uname -r`/build
BUILD_DIR := $(shell pwd)
VERBOSE = 0

obj-m := getpid-livepatch.o
smallmod-objs := getpid-livepatch.o

all:
        make -C $(KERNDIR) M=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules

clean:
        rm -f *.o
        rm -f *.ko
        rm -f *.mod.c
        rm -f *~

ビルドします。ビルドに成功するとgetpid-livepatch.koというファイルができます。

[masami@unamebook getpid-livepatch-module]$ make
make -C /lib/modules/`uname -r`/build M=/home/masami/getpid-livepatch-module KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'
  CC [M]  /home/masami/getpid-livepatch-module/getpid-livepatch.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/masami/getpid-livepatch-module/getpid-livepatch.mod.o
  LD [M]  /home/masami/getpid-livepatch-module/getpid-livepatch.ko
make[1]: Leaving directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'

次にテストプログラムを用意しましょう。getpid-test.cという名前でファイルを作り、以下の内容で保存します。

#include <sys/types.h>#include <unistd.h>#include <stdio.h>int main(int argc, char **argv)
{
        printf("pid is %d\n", getpid());
        return0;
}

ビルドします。

[masami@unamebook getpid-livepatch-module]$ gcc getpid-test.c -o getpid-test

実行すると以下のようにpidが表示されます。

[masami@unamebook getpid-livepatch-module]$ ./getpid-test 
pid is 1435

では、ライブパッチのモジュールをロードします。

[masami@unamebook getpid-livepatch-module]$ sudo insmod ./getpid-livepatch.ko

dmesgコマンドでカーネルのログを見ると以下のような行があるのが確認できます。

[  959.659727] getpid_livepatch: loading out-of-tree module taints kernel.                                                                                                                                                                    
[  959.659729] getpid_livepatch: tainting kernel with TAINT_LIVEPATCH                                                                                                                                                                         
[  959.659787] getpid_livepatch: module verification failed: signature and/or required key missing - tainting kernel                                                                                                                          
[  959.668116] livepatch: enabling patch 'getpid_livepatch'                                                                                                                                                                                   
[  959.670655] livepatch: 'getpid_livepatch': starting patching transition                                                                                                                                                                    
[  959.670802] getpid_livepatch: getpid livepatch test module is enabled      

ただ、livepatch_getpid()の最初にpr_info()で関数名を表示させているので以下のようなログが大量に出てると思いますが。

[ 1042.927756] getpid_livepatch: livepatch_getpid

それはさておきgetpid-testを再度実行するとpidとして1が返ってきます。ということでカーネルのライブパッチ機能を使ったライブパッチも成功です🎉

[masami@unamebook getpid-livepatch-module]$ ./getpid-test 
pid is 1

プログラム名が違えばもちろんpidは普通に返ってきます。

[masami@unamebook getpid-livepatch-module]$ cp getpid-test getpid-test2
[masami@unamebook getpid-livepatch-module]$ ./getpid-test2
pid is 1455

おまけ

FedoraをupstreamとしているRHELCentOSuname(1)の挙動も紹介します。

RHEL 8.1

バージョンは8.1です。これでuname(1)を実行するとuname(2)が3回呼ばれているのがわかりますね。

[masami@rhel8 ~]$ cat /etc/os-release  | grep VERSION_ID
VERSION_ID="8.1"
[masami@rhel8 ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux rhel8 4.18.0-147.0.3.el8_1.x86_64 #1 SMP Mon Nov 11 12:58:36 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000068          22         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000068      

coreutilsのバージョンは8.30です。

[masami@rhel8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version     : 8.30
Release     : 6.el8

CentOS 8

CentOSのバージョンは8です。こちらもuname(2)が3回呼ばれてますね。

masami@centos8 ~]$ cat /etc/os-release  | grep VERSION_ID
VERSION_ID="8"
[masami@centos8 ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux centos8 4.18.0-80.11.2.el8_0.x86_64 #1 SMP Tue Sep 24 11:32:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000013           4         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000013                     3           total

coreutilsのバージョンはこちらも8.30です。

[masami@centos8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version     : 8.30
Release     : 6.el8

まとめ

最後はgetpid(2)を使いましたが、主にuname(1)とuname(2)を使ってユーザーランドでのデバッグからカーネルを弄るところまでを行いました(_´Д`)ノ~~オツカレー

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

*1:1というのはmanセクションの番号です。1は通常のコマンドです。

*2:カーネルバージョンにある#1というのはビルド環境にてビルドした回数です。

*3:システムコールのmanセクションは2です

killコマンドのあれこれ

$
0
0

(´-`).。oO(killコマンドを提供するプロジェクト多いなって

killコマンドを提供するプロジェクト

とりあえず気付いた範囲で4つ。

上のリストはmanページへのリンクになっているのでオプションの違いとかはそちらを参照してください。busyboxのkillは最小構成って感じなのでオプションは1つしかないですね。-sオプションはbusybox以外は実装してますね。で、-lオプションは全てに存在。まあ、これはシグナル名を表示するので無いと困りますね。

ディストリビューションのkillコマンド

じゃあ、ディストリビューションはどのkillコマンドを採用してるのか?ということで手元にあるディストリビューションを確認したところこんなふうになってました。

ディストリビューションkillコマンドを提供するパッケージ
fedora 31util-linux
centos 8util-linux
arch linux(docker image)util-linux
ubuntu 18.04 procps-ng
alpine linux(docker image)busybox

killコマンドの実装

ソースを見てみます。調べるときに実際に動作させてなくてコードを読んだだけなのでもしかしたら間違ってる可能性もありますが。

util-linux

kill_verbose()がkill(2)でsignalをプロセスに投げているところのようです。

staticint kill_verbose(conststruct kill_control *ctl)
{
    int rc = 0;

    if (ctl->verbose)
        printf(_("sending signal %d to pid %d\n"), ctl->numsig, ctl->pid);
    if (ctl->do_pid) {
        printf("%ld\n", (long) ctl->pid);
        return0;
    }
#ifdef HAVE_SIGQUEUEif (ctl->use_sigval)
        rc = sigqueue(ctl->pid, ctl->numsig, ctl->sigdata);
    else#endif
        rc = kill(ctl->pid, ctl->numsig);

    if (rc < 0)
        warn(_("sending signal to %s failed"), ctl->arg);
    return rc;
}

if文でctl->do_pidをチェックしているところは-pオプションの処理です。util-linuxのkillはsigqueue(3)が利用できる場合はkill(2)ではなくてsigqueue(3)を使うんですね。sigqueue(3)の場合はデータを付加できるのでできるならこっちを使おうってことですね。これはmanにも書かれていますが-qオプションを使うとプロセスに送るデータを設定できます。

procps-ng

procps-ngの場合はkill_main()がkillを実現する関数っぽいです。 この関数の主要なところはここです。

for (i = 0; i < argc; i++) {
        pid = strtol_or_err(argv[i], _("failed to parse argument"));
        if (!kill((pid_t) pid, signo))
            continue;
        error(0, errno, "(%ld)", pid);
        exitvalue = EXIT_FAILURE;
        continue;
    }

こちらはこれといって特殊なことはしてないですね。

coreutils

coreutisの場合はsend_signals()という関数がkill(2)を呼んでいます。

staticint
send_signals (int signum, char *const *argv)
{
  int status = EXIT_SUCCESS;
  charconst *arg = *argv;

  do
    {
      char *endp;
      intmax_t n = (errno = 0, strtoimax (arg, &endp, 10));
      pid_t pid = n;

      if (errno == ERANGE || pid != n || arg == endp || *endp)
        {
          error (0, 0, _("%s: invalid process id"), quote (arg));
          status = EXIT_FAILURE;
        }
      elseif (kill (pid, signum) != 0)
        {
          error (0, errno, "%s", quote (arg));
          status = EXIT_FAILURE;
        }
    }
  while ((arg = *++argv));

  return status;
}

これも短い関数ですね。こちらもprocps-ng同様にkill(2)を呼ぶだけです。

busybox

busyboxの場合はkill_main()という関数でkillの処理を行っています。余談ですがbusyboxのkill.cはprocps/にあるし、procps-ngと同じ感じにしてるんですかね?

#if ENABLE_KILL/* Looks like they want to do a kill. Do that */while (arg) {
# if SH_KILL/*        * We need to support shell's "hack formats" of        * " -PRGP_ID" (yes, with a leading space)        * and " PID1 PID2 PID3" (with degenerate case "")        */while (*arg != '\0') {
            char *end;
            if (*arg == '')
                arg++;
            pid = bb_strtoi(arg, &end, 10);
            if (errno && (errno != EINVAL || *end != '')) {
                bb_error_msg("invalid number '%s'", arg);
                errors++;
                break;
            }
            if (kill(pid, signo) != 0) {
                bb_perror_msg("can't kill pid %d", (int)pid);
                errors++;
            }
            arg = end; /* can only point to '' or '\0' now */
        }
# else/* ENABLE_KILL but !SH_KILL */
        pid = bb_strtoi(arg, NULL, 10);
        if (errno) {
            bb_error_msg("invalid number '%s'", arg);
            errors++;
        } elseif (kill(pid, signo) != 0) {
            bb_perror_msg("can't kill pid %d", (int)pid);
            errors++;
        }
# endif
        arg = *++argv;
    }
    return errors;
#endif

コードにはSH_KILLは通常定義されてるのかどうかわかりませんが、定義の有無にかかわらずkill(2)を使うだけですね。

実装の差異

util-linuxはsigqueue(3)が利用可能ならそれを使い、利用できなければkill(2)を呼ぶという実装になっていますが、それ以外はkill(2)を呼ぶ形でした。

まとめ

Linuxでは基本的とも言えるkillコマンドですが、このコマンドを提供するパッケージは複数あり、ディストリビューションによってどのパッケージのkillコマンドを使うか選択していました。 また、実装でも微妙な違いがあるということがわかりました( ´ー`)フゥー...

secure bootが有効な環境で自前ビルドのカーネルに署名する方法(fedora向け)

$
0
0

fedoraなら多分一番手軽だと思う署名方法です。他のディストリビューションはわかりません😭

テスト環境

ディストリビューションは(もちろん) fedoraです。バージョンは31です。実機ではなくてqemuでテストしました。

準備

まずpesignとnss-toolsパッケージをインストールします。pesignパッケージをインストールするとpesignコマンドなどのバイナリの他に/etc/pki/pesign/と/etc/pki/pesign-rh-testにデータベースがインストールされます。今回は/etc/pki/pesign-rh-testにあるものを使います。

署名前の準備

まずcertificateファイルを取り出します。

$ sudo certutil -d /etc/pki/pesign-rh-test -L -n "Red Hat Test CA" -r > rhca.cer

次に取り出したcertファイルを登録します。このときにパスワードを聞かれるのでお好きなパスワードを入れてください。

$ sudo mokutil --import ./rhca.cer
input password: 
input password again:

インポートに成功すると次のように確認できます。

$ sudo mokutil --list-new | head -n 10
[key 1]
SHA1 Fingerprint: 65:37:ee:ed:f1:ea:fe:d0:a4:cd:d8:91:eb:f2:2b:45:45:41:cd:44
Certificate:
    Data:

そうしたらここで一旦rebootします。

enroll

再起動するとgrubなどは起動せずにuefiのアプリが起動します。

f:id:masami256:20200112150853p:plain

なにか適当にキーを押すとメニューが出るのでEnroll MOKを選択してエンターキーを押します。

f:id:masami256:20200112150941p:plain

View Key 0を選択してエンターキーを押すと鍵の内容が確認できます。

f:id:masami256:20200112151052p:plain

Continueを選ぶと鍵を登録するか聞かれるのでYesを選択してエンターキーを押しましょう。

f:id:masami256:20200112151206p:plain

パスワードを聞かれるので先程入力したパスワードを打ち込みます。

f:id:masami256:20200112151303p:plain

パスワードが正しければメニュー画面に戻ります。Rebootを選択すればシステムが再起動して通常のブートフローになります。

f:id:masami256:20200112151353p:plain

カーネルへの署名

参考文献の手順だとpesignのオプションでエラーになったんですが、fedoraカーネルパッケージをビルドしたときのログを見てたらコマンドラインオプションがわかりました。

↓がログです。

+ '[' -f arch/x86_64/boot/zImage.stub ']'
+ _pesign_nssdir=/etc/pki/pesign
+ '[' 'Red Hat Test Certificate' = 'Red Hat Test Certificate' ']'
+ _pesign_nssdir=/etc/pki/pesign-rh-test
+ '[' -x /usr/bin/pesign ']'
+ '[' x86_64 == x86_64 -o x86_64 == aarch64 ']'
+ '[' 0 -ge 7 -a -f /usr/bin/rpm-sign ']'
++ id -un
++ uname -m
+ '[' '%{vendor}' == 'Fedora Project' -a mockbuild == mockbuild -a x86_64 == x86_64 ']'
+ '[' -S /var/run/pesign/socket ']'
+ /usr/bin/pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i arch/x86/boot/bzImage -o vmlinuz.signed -s
+ '[' '!' -s -o vmlinuz.signed ']'
+ '[' '!' -s vmlinuz.signed ']'

コマンドはこんな感じになります。-iオプションにbzImageのパスを渡してください。

$ pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i ./bzImage -o vmlinuz.signed -s

自分の場合はカーネルのmake installまで済ませていて、カーネルはvmlinuz-5.4.8-testとして置いておいたのでvminuz.signedを/boot/vmlinuz-5.4.8-testとしてコピーして完了でした。 あとは再起動してこのカーネルで起動します。

自前のカーネルでブート

起動してカーネルのバージョン、secure bootが有効なことを確認してすべてOKって感じです🎉

$ uname -a
Linux secureboot-test 5.4.8-test #1 SMP Thu Jan 9 00:04:00 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ dmesg | grep Secure
[    0.008666] Secure boot enabled

鍵を消したら

deleteオプションで鍵を消せます。再起動すると登録したときと同じようにShimのアプリが起動するのでそこで鍵を削除できます。

$ sudo mokutil --delete ./rhca.cer 
input password: 
input password again:

鍵を消すとその鍵で署名したカーネルでは起動できなくなります。

f:id:masami256:20200112152306p:plain

カーネルモジュール

自作モジュールもロードできます。

$ lsmod | grep test_mod
test_mod               16384  0

dmesgでも

[  104.658715] test_mod: loading out-of-tree module taints kernel.
[  104.658741] test_mod: module verification failed: signature and/or required key missing - tainting kernel
[  104.658958] test_mod: install test_mod : insmod

参考文献

次のブログを参考にしました。

Booting custom kernels in F18 with Secure Boot - pointless pontifications of a back seat driver — LiveJournal


Linux起動時のカーネルパニック(at ring_buffer_set_clock())修正めも

$
0
0

機種依存ですが、fedoraで5.4系のカーネルで起動時にカーネルパニックするようになり、自分もこのバグに当たったので修正してパッチをlkmlに投稿しました。 このパッチはLinux5.5.に取り込まれました🎉

github.com

ついでなのでどんな感じでデバッグしたかの記録です。10日前くらいの話だし、デバッグ時の思考の時系列は多少違う気もしますがだいたいこんな感じということで。

環境

こんな感じです。

バグの内容

自分がこのバグに当たったときにはfedoraのbugzillaにすでにバグ登録されてました

bugzilla.redhat.com

5.4系のカーネルだと起動時にカーネルパニックする☠という内容です。実際こんな感じでした。

f:id:masami256:20200127191203j:plain
oops

ただ、自分が普段使ってるデスクトップPCでも同じカーネルを使ってますがこちらでは同様の現象は起きてませんでした。ということで機種依存っぽい感じがします。そして、bugzillaを見てると共通点が見えてきました。 それは次の二点です。

  • Ryzenが載ってるThinkPadを使っている
  • Secure Bootが有効

暫定対策

上に貼り付けたoopsはカーネルコマンドラインオプションにquietが付いているので表示されていないですが、quietを外すと↓のprintk()の部分が表示されました。

/* sched_clock_stable() is determined in late_initcall */if (!trace_boot_clock && !sched_clock_stable()) {
        printk(KERN_WARNING
               "Unstable clock detected, switching default tracing clock to \"global\"\n""If you want to keep using the local clock, then add:\n""  \"trace_clock=local\"\n""on the kernel command line\n");
        tracing_set_clock(&global_trace, "global");
    }

ってことで、これを試すと起動成功したのでbugzillaにtrace_clock=locakを付けたら大丈夫だったよとコメントしました。また、他の人はSecure bootを無効にすることで対応したりもしてました。

原因を探す

NULL pointer dereference の原因箇所

ring_buffer_set_clock()はこんな関数です。

void ring_buffer_set_clock(struct ring_buffer *buffer,
               u64 (*clock)(void))
{
    buffer->clock = clock;
}

どこでエラーになったか一目瞭然ですね。

この関数に至る流れはtracing_set_default_clock()から始まり、 tracing_set_clock() ->ring_buffer_set_clock()となります。

初期化されていない状態でring_buffer_set_clock()が呼ばれてる感じですね。

ちなみにここで変更しようとしているclockは/sys/kernel/debug/tracing/trace_clockとして見えます。

masami@moon:~$ sudo cat  /sys/kernel/debug/tracing/trace_clock
[local] global counter uptime perf mono mono_raw boot x86-tsc

Unstable clock detected?

5.3系では問題なかったので5.4系からのバグではあるのですが、まず以下の部分でUnstable clock detectedとなるのは5.3ではどうだったのか?というのを調べます。trace_boot_clockはtrace_clockオプションなので普段は設定してないので気にしません。

if (!trace_boot_clock && !sched_clock_stable()) {

調べるといっても、5.3系カーネルの起動時にquietオプションを外すだけですが。そして結果はどうだったかと言うと5.3系でも5.4.7でも同じパスを通っていました。このPCはUnstable clock detectedと判断されるようですね。デスクトップPC(cpuはi7-9700K)のほうはこのパスは通ってませんでした。

upstraem / 他のディストリビューションの様子

とくに同じようなエラーが出てる感じはありませんでした。

ここまでのまとめ

以下の条件に当てはまる人がバグに当たっている感じです。

回避策はtrace_clock=localを設定するかSecure Bootを無効にする。

原因特定のためのデバッグ

Secure Boot環境で動くカーネルを作る方法を調べる

Secure Bootを無効にしちゃったら再現しないので自前でビルドしたカーネルへの署名方法を調べます。調べた結果は↓にまとめました。

kernhack.hatenablog.com

upstreamカーネルを試す

まずはバージョン同じものを使って、カーネルのコンフィグはfedoraの5.4.7のコンフィグを使います。ビルドしてこのカーネルから起動してみると無事に起動しました。ということで、次の手順に進みます。

fedora固有のパッチを調べる

調べると言うかパッチを一旦外せるだけ外してから足していくとかそんな感じですね。fedoraカーネルパッケージのソース一式はgitリポジトリにあるのでクローンしてきて、f31ブランチからブランチを切手作業していきます。この時点でf31ブランチは5.4.12カーネルになっていました。

src.fedoraproject.org

手順的にはspecファイルでパッチをコメントアウトして、fedpkgでsrpmを作ります。そして、必要最小限のパッケージだけ作りたかったので出来たsrpmをインストールしてrpmbuildでビルドするという方法を取りました。fedpkgで同じようなことができるんでしょうか? それはともかくrpmbuildはこんなオプションでやりました。

$ rpmbuild -bb --with baseonly --without debuginfo --target=$(uname -m) kernel.spec

arm64-Add-option-of-13-for-FORCE_MAX_ZONEORDER.patchのように外すと他に直さなきゃいけないところが出てくるパッチもあったりするのですが、まあこの辺は今回のバグに関係ないだろってことでこういうパッチは外さずに進めました。そして、efi-secureboot.patchを外せば問題ないというところまで判明しました。

efi-secureboot.patchの内容を調べる

efi-secureboot.patchLinux 5.4から入ったlockdownに関係するパッチです。

lockdownについてはこちらを参照してもらうとして。。

kernelnewbies.org

このパッチの内容は大きく分けると

最後のCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTの値に応じて処理してる部分は以下のifdefのところです。

diff --git a/arch/x86/kernel/setup.c b/arch/x86/kernel/setup.c
index 77ea96b794bd..a119e1bc9623 100644
--- a/arch/x86/kernel/setup.c
+++ b/arch/x86/kernel/setup.c
@@ -73,6 +73,7 @@
 #include <linux/jiffies.h>#include <linux/mem_encrypt.h>#include <linux/sizes.h>
+#include <linux/security.h>
 
 #include <linux/usb/xhci-dbgp.h>#include <video/edid.h>
@@ -1027,6 +1028,13 @@ void __init setup_arch(char **cmdline_p)
    if (efi_enabled(EFI_BOOT))
        efi_init();
 
+   efi_set_secure_boot(boot_params.secure_boot);
+
+#ifdef CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT
+   if (efi_enabled(EFI_SECURE_BOOT))
+       security_lock_kernel_down("EFI Secure Boot mode", LOCKDOWN_CONFIDENTIALITY_MAX);
+#endif
+
    dmi_setup();
 
    /*

上記のコードはカーネルuefiのsecure boot環境で起動されてたらlockdownしてます。

ここまでくるとCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが疑わしい感じしてきます。そこでこのオプションは選択せずにカーネルをビルドします。オプションはkernel-x86_64-fedora.configにてこのように行われています。

CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY is not set
# CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY is not set
CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y

オプションの設定はkernel-x86_64-fedora.configでCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTをコメントアウトしてsrpmを作ってからビルドしました。そしたら予想通り起動できたので、さらなるデバッグに入ります。

upstreamのソースを再確認

5.4.12のコードを見てたらこんなコードが入っていたんですね。これはregister_tracer()のコードです。

 if (security_locked_down(LOCKDOWN_TRACEFS)) {
        pr_warning("Can not register tracer %s due to lockdown\n",
               type->name);
        return -EPERM;
    }

他にも同じようなチェックがいくつかありました。それでlockdownされているとtracerの初期化しないんだなということかってわかり、初期化されていないのにtrace clockを変更しようとしたからヌルポになるということなんだなと理解しました。

upsteamで再現できるか確認

このときは5.4.7ではなくてfedoraカーネルパッケージのgitでは5.4.12だったのでstable treeの5.4.12を使いました。原因はほぼ掴めたというところなんですが、upstreamで再現するかどうか再度検証します。

まずlockdown機能に関するコンフィグを調べます。lockdownではCONFIG_SECURITY_LOCKDOWN_LSMが最重要の設定でY/Nの2択です。これでYを選べばlockdownの機能が使えます。Yを選択すると更にどのレベルでlockするかという選択があります。選択肢は3個でデフォルトはNoneです。fedoraカーネルもCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEを選択していました。

  • CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY

fedoraの場合はCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが4個目の選択肢ってところでしょうか。

オプションの内容ですがKconfigによるとCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEは何もしません。CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITYは「Features that allow the kernel to be modified at runtime are disabled.」とあり、CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY「Features that allow the kernel to be modified at runtime or that permit userland code to read confidential material held inside the kernel are disabled」で一番制限が厳しくなっています。

最初にfedoraのコンフィグでmainlineカーネルをビルドしたときはCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEが選択されてたので残りの2パターンを試したところCONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITYを設定したときに再現しましたキタ━━━━(゚∀゚)━━━━!!

修正する

lockdownが有効になっているときはtrace_clockを変更できないようにするのが良いだろうと思い、このようなパッチを書きました。

@@ -9420,6 +9420,11 @@ __init staticint tracing_set_default_clock(void)
 {
        /* sched_clock_stable() is determined in late_initcall */if (!trace_boot_clock && !sched_clock_stable()) {
+               if (security_locked_down(LOCKDOWN_TRACEFS)) {
+                       pr_warn("Can not set tracing clock due to lockdown\n");
+                       return -EPERM;
+               }
+
                printk(KERN_WARNING
                       "Unstable clock detected, switching default tracing clock to \"global\"\n""If you want to keep using the local clock, then add:\n"

patchを送る

upstreamのカーネルでも再現できたのでupstreamにパッチを送ります。lkmlをみてたらtracing系はlkmlにccで良さげだったのでccにはlkml、toはscripts/get_maintainer.plで出てきたメンテナの人にしてgit format-patchでパッチを作りgit send-emailで送りました。

パッチを送ってしばらくしたらメンテナの人からmainlineへのpull rquestに自分のパッチが入っていたのでacceptされということがわかり、良かったって感じでしたね。5.5-rc8が出るのかと思ってたけどrc8は出ずに5.5が2020/01/27にでて、これに修正が含まれました。

lkml.org

mainlineに修正が取り込まれたのでそのうち5.4のstable treeにも修正が取り込まれるんじゃないかと思います。

まとめ

今回のバグは機種依存・カーネルの設定依存な面もありましたが、バグの原因はわかってしまえば簡単だったのと修正も単純な方法で済ますことができて良かったです😊 あとfedora向けの単純なsecure boot用の署名方法がわかったのは良かったかも。

Linux Advent Calendar 2019で書いた「unameコマンドから始めるデバッグ&カーネルハック入門」を電子書籍化しました

$
0
0

Linux Advent Calendar 2019でunameコマンドから始めるデバッグカーネルハック入門という記事を書きました。

kernhack.hatenablog.com

このときはFedora 31を実験環境として選んで書いたんですが、環境をCentOS 8にして内容を加筆修正して電子書籍にしました。

masami256.booth.pm

目次はこんな感じです。

f:id:masami256:20200311224120p:plain

strace、gdbbcc、bpftrace、カーネルハック、Livepatch、systemtapunameコマンドと戯れる内容です。

BOOTH様便利ですね👍

booth.pm

kdump: fedora系カーネルとupstreamカーネルの微妙な差

$
0
0

kdumpで利用するcrashkernelパラメータに渡せる値はfedoraカーネルとupstreamカーネルの微妙な差があるので自分でビルドしたカーネルを使う場合は気をつけろという自分へのメモです。

kdumpをfedorarhelなどでdumpを利用する場合、crashkernelに渡す値にautoが利用できます。rhel8のドキュメント(How should the crashkernel parameter be configured for using kdump on Red Hat Enterprise Linux 8 ? - Red Hat Customer Portal)だと通常はautoを指定するのを勧めてますね。autoを指定するとcrashkernelに指定するメモリ量をよしなに設定しれくれます。それってどこでやってんの?ということですが、fedoraの場合は0001-kdump-add-support-for-crashkernel-auto.patchが処理してます。このパッチの中の↓の部分がautoを指定された場合の処理です。

diff --git a/kernel/crash_core.c b/kernel/crash_core.c
index d631d22089ba..c252221b2f4b 100644
--- a/kernel/crash_core.c
+++ b/kernel/crash_core.c
@@ -258,6 +258,20 @@ staticint __init __parse_crashkernel(char *cmdline,
    if (suffix)
        return parse_crashkernel_suffix(ck_cmdline, crash_size,
                suffix);
+
+   if (strncmp(ck_cmdline, "auto", 4) == 0) {
+#ifdef CONFIG_X86_64
+       ck_cmdline = "1G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_S390)
+       ck_cmdline = "4G-64G:160M,64G-1T:256M,1T-:512M";
+#elif defined(CONFIG_ARM64)
+       ck_cmdline = "2G-:512M";
+#elif defined(CONFIG_PPC64)
+       ck_cmdline = "2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G";
+#endif
+       pr_info("Using crashkernel=auto, the size choosed is a best effort estimation.\n");
+   }
+
    /*    * if the commandline contains a ':', then that's the extended    * syntax -- if not, it must be the classic syntax

見ての通りコマンドラインの内容を書き換えてるだけですね。設定内容はドキュメントにも追加されてます。

diff --git a/Documentation/admin-guide/kdump/kdump.rst b/Documentation/admin-guide/kdump/kdump.rst
index 2da65fef2a1c..d53a524f80f0 100644
--- a/Documentation/admin-guide/kdump/kdump.rst
+++ b/Documentation/admin-guide/kdump/kdump.rst
@@ -285,6 +285,17 @@ This would mean:
     2) if the RAM size is between 512M and 2G (exclusive), then reserve 64M
     3) if the RAM size is larger than 2G, then reserve 128M

+Or you can use crashkernel=auto if you have enough memory.  The threshold
+is 2G on x86_64, arm64, ppc64 and ppc64le. The threshold is 4G for s390x.
+If your system memory is less than the threshold crashkernel=auto will not
+reserve memory.
+
+The automatically reserved memory size varies based on architecture.
+The size changes according to system memory size like below:
+    x86_64: 1G-64G:160M,64G-1T:256M,1T-:512M
+    s390x:  4G-64G:160M,64G-1T:256M,1T-:512M
+    arm64:  2G-:512M
+    ppc64:  2G-4G:384M,4G-16G:512M,16G-64G:1G,64G-128G:2G,128G-:4G


 Boot into System Kernel

upstreamカーネルの場合はautoは当然解釈されないので適切にメモリ量を設定できません😨 ということで、利用したい機能がどこからきているか調べないとハマる場合があるということでした。

Tiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べる

$
0
0

この記事はLinux Advent Calendar 2020 - Qiitaの1日目の記事です。

Tiny Core Linux(以下tcl)を使ってLinuxのブートプロセスを見てましょう。Tiny Core Linuxは軽量ディストリビューションで最小のisoイメージだと11MBほどです😃 ブートプロセスを見ると言っても電源onからの流れではなくてinitプロセスの実行に関する部分です。

この記事ではバージョン11.1のCore-current.iso を利用しています。

www.tinycorelinux.net

isoファイルの構成

まずはtclのisoがどんな感じで構成されていて、Linuxを起動させるのか確認しましょう。 isoファイルの構成はこのようになっています。

$ sudo mount -o loop Core-current.iso ./mnt
$ tree ./mnt/
./mnt/
└── boot
    ├── core.gz
    ├── isolinux
    │   ├── boot.cat
    │   ├── boot.msg
    │   ├── f2
    │   ├── f3
    │   ├── f4
    │   ├── isolinux.bin
    │   └── isolinux.cfg
    └── vmlinuz

2 directories, 9 files

vmlinuzはカーネル、core.gzはinitramfsのファイル、そして起動にはisolinuxを使っているのでそのファイル類が含まれています。

起動の設定はisolinux.cfgに書かれています。

display boot.msg
default microcore
label microcore
        kernel /boot/vmlinuz
        initrd /boot/core.gz
        append loglevel=3

label mc
        kernel /boot/vmlinuz
        append initrd=/boot/core.gz loglevel=3
implicit 0
prompt 1
timeout 300
F1 boot.msg
F2 f2
F3 f3
F4 f4

Tiny Core Linuxnのinitramfsの中身

今回はisoファイルの内容を作業用のディレクトリを作ってそこでファイルを読んだりしていきます。

$ mkdir tcl-work
$ cp -a mnt/boot/ tcl-work/.

core.gzの中も展開します。core.gzはcpioファイルをgzipで圧縮したものなので展開してからcpioコマンドで中身を取り出します。

$ cd tcl-work
$ mkdir core-work
$ cd core-work
$ zcat ../../mnt/boot/core.gz | sudo cpio -i -H newc -dv

展開するとこのようになります。

$ ls -la
total 72
drwxrwxr-x. 17 masami masami 4096 Nov 22 10:30 ./
drwxrwxr-x.  4 masami masami 4096 Nov 22 10:30 ../
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 bin/
drwxrwxr-x.  7 masami masami 4096 Nov 22 10:30 dev/
drwxr-xr-x.  8 masami masami 4096 Nov 22 10:30 etc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 home/
-rwxr-xr-x.  1 masami masami  496 Nov 22 10:30 init*
drwxr-xr-x.  4 masami masami 4096 Nov 22 10:30 lib/
lrwxrwxrwx.  1 masami masami   11 Nov 22 10:30 linuxrc -> bin/busybox*
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 mnt/
drwxrwsr-x.  2 masami masami 4096 Nov 22 10:30 opt/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 proc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 root/
drwxrwxr-x.  3 masami masami 4096 Nov 22 10:30 run/
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 sbin/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 sys/
drwxrwxrwt.  2 masami masami 4096 Nov 22 10:30 tmp/
drwxr-xr-x.  7 masami masami 4096 Nov 22 10:30 usr/
drwxrwxr-x.  8 masami masami 4096 Nov 22 10:30 var/

システムの起動に関連しそうなファイルとして /linuxrc/initがありますね。linuxrcはbusyboxコマンドへのリンクで/initはシェルスクリプトになっています。このファイルの内容は後で見てみます。

ちなみにfedora 33のinitramfsはこのようになっていて/linuxrcはありません。

$ /usr/lib/dracut/skipcpio ./initramfs-5.9.9-200.fc33.x86_64.img | zcat | cpio -idv
$ ls -la
total 32680
drwxrwxr-x. 12 masami masami     4096 Nov 22 02:07 ./
drwxrwxr-x.  5 masami masami     4096 Nov 22 12:45 ../
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 bin -> usr/bin/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 dev/
drwxr-xr-x. 11 masami masami     4096 Nov 22 02:07 etc/
lrwxrwxrwx.  1 masami masami       23 Nov 22 02:07 init -> usr/lib/systemd/systemd*
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 lib -> usr/lib/
lrwxrwxrwx.  1 masami masami        9 Nov 22 02:07 lib64 -> usr/lib64/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 proc/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 root/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 run/
lrwxrwxrwx.  1 masami masami        8 Nov 22 02:07 sbin -> usr/sbin/
-rwxr-xr-x.  1 masami masami     3418 Nov 22 02:07 shutdown*
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sys/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sysroot/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 tmp/
drwxr-xr-x.  8 masami masami     4096 Nov 22 02:07 usr/
drwxr-xr-x.  3 masami masami     4096 Nov 22 02:07 var/

では/linuxrcと/initはどのように使われるのでしょうか。/initや/linuxrcが利用される仕組みはEarly userspace support — The Linux Kernel documentationに説明があります。tclではinitramfsが利用されてます。この場合、以下の部分が該当します。/initが必須ということですね。

using initramfs. The call to prepare_namespace() must be skipped. This means that a binary must do all the work. 
Said binary can be stored into initramfs either via modifying usr/gen_init_cpio.c or via the new initrd format, an cpio archive. It must be called “/init”. 
This binary is responsible to do all the things prepare_namespace() would do.

起動時のログからもinitramfsが利用されていることがわかります。

tc@box:~$ dmesg | grep initramfs
Trying to unpack rootfs image as initramfs...

Tiny Linux Coreで起動シーケンスの確認

ではこれらの動作を確認してみましょう。 と、その前にqemuGUIを立ち上げるとコンソール出力が見にくいのでシリアルコンソールを有効にしてしまいます。 編集するのはisolinux.confです。このファイルにラベルは2つありますがデフォルトはmicrocoreのほうなのでこちらに設定しましょう。console=ttyS0,115200をappendの行に追加します。 ついでなのでログレベルも最大にしてしまいます。そうするとこのような感じになります。

append loglevel=7 console=ttyS0,115200

つぎにinitramfsのファイルを弄りましょう。シリアルコンソールの設定をetc/inittabに追加します。

ttyS0::respawn:/sbin/getty -nl /sbin/autologin 115200 ttyS0 vt102

そうしたら本題の/iniitと/linuxrcをrenameしてみます。

-rwxr-xr-x.  1 root   root    496 Nov 22 12:14 init.bk*
drwxr-xr-x.  4 root   root   4096 Nov 22 12:14 lib/
lrwxrwxrwx.  1 root   root     11 Nov 22 12:14 linuxrc.bk -> bin/busybox*

core.gzを作成しましょう。

$ sudo find | sudo cpio -o -H newc | gzip -2 > ../boot/core.gz

これで新しいintramfsのイメージもできたのでisoイメージを作成します。 実行はtcl-workディレクトリの1つ上の場所で行います。

$ sudo mkisofs -l -J -R -r -V TC-custom -no-emul-boot -boot-load-size 4 \
 -boot-info-table -b boot/isolinux/isolinux.bin \
 -c boot/isolinux/boot.cat -o test.iso tcl-work/

これでisoファイルが出来たのでqemuで起動します。

$ qemu-system-x86_64 -boot d -cdrom test.iso -m 512 -nographic

見事にカーネルパニックします😃

Floppy drive(s): fd0 is 2.88M AMI BIOS
FDC 0 is a S82078B
blk_update_request: I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
floppy: error 10 while reading block 0
VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled
Rebooting in 60 seconds..

まずは/linuxrcの名前だけ戻して起動させます。

VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled

そうするとカーネルパニックとなりました。ではlinuxrcの名前は変更した状態で、/initのほうは名前を戻して起動してみましょう。

   ( '>')
  /) TC (\   Core is distributed with ABSOLUTELY NO WARRANTY.
 (/-_--_-\)           www.tinycorelinux.net

tc@box:~$ e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 52:54:00:12:34:56
e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX

tc@box:~$ ls -la /
total 4
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ./
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ../
drwxr-xr-x    2 root     root          1380 Nov 22 03:14 bin/
drwxrwxr-x   12 root     staff         4260 Nov 22 05:32 dev/
drwxr-xr-x    8 root     root           680 Nov 22 05:32 etc/
drwxrwxr-x    3 root     staff           60 Nov 22 05:32 home/
-rwxr-xr-x    1 root     root           496 Nov 22 03:14 init
drwxr-xr-x    4 root     root           800 Nov 22 03:14 lib/
lrwxrwxrwx    1 root     root            11 Nov 22 03:14 linuxrc.bk -> bin/busybox
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 mnt/
drwxrwsr-x    2 root     staff          160 Nov 22 03:14 opt/
dr-xr-xr-x   64 root     root             0 Nov 22 05:32 proc/
drwxrwxr-x    2 root     staff           80 Nov 22 03:14 root/
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 run/
drwxr-xr-x    2 root     root          1200 Nov 22 03:14 sbin/
dr-xr-xr-x   12 root     root             0 Nov 22 05:32 sys/
drwxrwxrwt    4 root     staff          120 Nov 22 05:32 tmp/
drwxr-xr-x    7 root     root           140 Nov 22 03:14 usr/
drwxrwxr-x    8 root     staff          180 Nov 22 03:14 var/

今度は起動します。ということでinitramfsを利用する環境では/linuxrcなくてもOKというのが実際の動作からわかりました。

/initの処理

/initは次のような短いシェルスクリプトです。

#!/bin/sh
mount proc
grep -qw multivt /proc/cmdline && sed -i s/^#tty/tty/ /etc/inittab
if ! grep -qw noembed /proc/cmdline; then

  inodes=`grep MemFree /proc/meminfo | awk '{print $2/3}' | cut -d. -f1`

  mount / -o remount,size=90%,nr_inodes=$inodes
  umount proc
  exec /sbin/init
fi
umount proc
if mount -t tmpfs -o size=90% tmpfs /mnt; then
  if tar -C / --exclude=mnt -cf - . | tar -C /mnt/ -xf - ; then
    mkdir /mnt/mnt
    exec /sbin/switch_root mnt /sbin/init
  fi
fi
exec /sbin/init

execコマンドの実行箇所が3箇所ありますが、今回の環境では最初のifブロック部分が実行されます。tclの環境でmountコマンドの出力を見るとこんなふうになってます。

tc@box:~$ mount
rootfs on / type rootfs (rw,size=400480k,nr_inodes=141441)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)

この環境では搭載されているメモリサイズに応じたinode数やメモリの何%をファイルシステム容量上限とするかを設定した上でルートファイルシステムを再マウントし直しています。そして/sbin/initを実行します。

先に引用したinitramfsnの説明文で次のような一文がありました。

This binary is responsible to do all the things prepare_namespace() would do.

ここでのnamespaceはコンテナの文脈で使うnamepspaceとは別のものです。prepare_namespace()はルートファイルシステムをマウントしてそこにchrootする処理を行います。initramfsを使う場合はその処理は/initで行ってねということです。

カーネルの動作

起動処理において/initが必要というのはわかりましたが、カーネルからはどう動くのか?というのを見ていきます。tclの11.1ではカーネル5.4.3が使われているのでこのバージョンで確認しましょう。/initを実行するのはinit/main.cのrun_init_process()にて行います。このときの関数の呼ばれ方は次のようになります。

  1. start_kernel()
  2. arch_call_rest_init()
  3. rest_init()
  4. kernel_init()
  5. run_init_process()

start_kernel()の一番最後でarch_call_rest_init()を呼び出して流れていきます。arch_call_rest_init()は名前からしアーキテクチャ毎に実装しする感じですが、s390以外は独自の実装はなくてinit/main.cにあるarch_call_rest_init()を利用しています。

run_init_process()は次のように実行します。

if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

ramdisk_execute_commandはkernel_init_freeable()にて次のように/initを設定します。

if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";

ramdisk_execute_commandのデフォルト値はNULLですが、カーネルコマンドラインパラメータでrdinit=で指定することができます。

staticint __init rdinit_setup(char *str)
{
    unsignedint i;

    ramdisk_execute_command = str;
    /* See "auto" comment in init_setup */for (i = 1; i < MAX_INIT_ARGS; i++)
        argv_init[i] = NULL;
    return1;
}
__setup("rdinit=", rdinit_setup);

特に指定がなければ/initが使われます。ということでinitramfsに/initがないとエラーになるのはこれってことがわかります。/initの実行に成功したら0を返してkernel_init()の処理は終了です。 /initは/sbin/initを実行するので本当のinit処理は/initから実行される感じです。

HiFive1 Rev Bを買ったのでRISC-V実機に入門する

$
0
0

この記事はRISC-V Advent Calendar 2020の1日目の記事です。

RISC-Vの実機としてHiFive1 Rev Bを勢いで買いました😊 

f:id:masami256:20201126200139j:plain
HiFive1 Rev B

64bitのRISC-V64GCならLinuxも動くんですけど、HiFive UnleashedはディスコンだしHiFive Unmatchedはまだ出てないので手軽に遊べそうなHiFive1 Rev Bで良いじゃんって感じです。手頃なサイズ感のRISC-V64GCが乗ったボードがあれば欲しいかも。

開発環境を整える

それはさておき、うちのメイン環境はfedora 33です。そして開発環境を整えようと思ってこれらをダウンロードしてセットアップします。

Linux向けと言ってもCentOS版、Ubuntu版とありましたのでCentOS版のほうを選びました。あとはrpmじゃなくてtar.gz形式のファイルを選んで/optに置く感じにしました。 で、SDKはビルドが必要なんですが、fedora33環境でSDKをビルドしようとしたんだけどFIX: build on cpython master branch by tacaswell · Pull Request #128 · python/typed_ast · GitHubに当たってtyped_astの新しいバージョンが出るのを待つか、vmかコンテナ使うかという感じだったのですが、第3の選択肢としてSDKを使わない方法で遊んでみました。

コードを動かしてみる

SDKでビルドしないとなると他に適当なものが必要なので探してみたところサイズも手頃なRustのコードをriscv-rust-quickstartを見つけました。

github.com

動かし方はREADME.md通りにやれば簡単です。

JLinkGDBServerを起動しておいて、

masami@moon:~/projects/hello$ sudo /opt/risc-v-tools/JLink_Linux_V688a_x86_64/JLinkGDBServer -device FE310 -if JTAG -speed 4000 -port 3333 -nogui                                                                                             

cargoコマンドでビルドして実行

masami@moon:~/projects/hello$ cargo run --example hello_world

そうするとgdb server側ではこんな感じにバイナリがダウンロードされ、

J-Link found 1 JTAG device, Total IRLen = 5
JTAG ID: 0x20000913 (RISC-V)
Connected to target
Waiting for GDB connection...Connected to 127.0.0.1                                   
Reading all registers
Received monitor command: reset halt
Expected an decimal digit (0-9)
Downloading 15074 bytes @ address 0x20010000
Downloading 3204 bytes @ address 0x20013AF0
Comparing flash   [....................] Done.
Writing register (pc = 0x20010000)
Starting target CPU...

cargoを実行した方ではこんな感じの出力になります。

masami@moon:~/projects/hello$ cargo run --example hello_world
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world`
/home/masami/.gdbinit:8: Error in sourced command file:
No symbol table is loaded.  Use the "file" command.
Reading symbols from target/riscv32imac-unknown-none-elf/debug/examples/hello_world...
0x20010974 in main () at examples/hello_world.rs:25
25          loop {}
Expected an decimal digit (0-9)
Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

そして、uartでデバイスに接続しといたほうではこんな感じで文字列がでます。

masami@moon:~$ sudo picocom -b 115200 /dev/ttyACM0 -q
hello world!

簡単😊

実行の仕組みを調べる

cargo runを実行すると次のログが出ているのでgdbを起動してるのがわかります。

riscv64-unknown-elf-gdb -q -x gdb_init target/riscv32imac-unknown-none-elf/debug/examples/hello_world

この時にgdb_initファイルを指定しています。gdb_initは次のような内容でgdb serverに接続してバイナリを送って処理を続けるような流れになっていました。

set history save on
set confirm off
set remotetimeout 240
target extended-remote :3333
set print asm-demangle on
monitor reset halt
load
continue
# quit

cargoでrunを指定した時にどう動くのかよくわかってないので、どこでgdbコマンドライン作ってんだと思って調べて見ると.cargo/にconfigファイルがありそこに書かれてました。リンカーの設定もありますね。

masami@moon:~/projects/hello$ cat .cargo/config 
[target.riscv32imac-unknown-none-elf]
runner = "riscv64-unknown-elf-gdb -q -x gdb_init"
rustflags = [
  "-C", "link-arg=-Thifive1-link.x",
]

[build]
target = "riscv32imac-unknown-none-elf"

サンプルコードを読む

hello_world.rsを見てみます。30行もありません。

#![no_std]#![no_main]externcratepanic_halt;

useriscv_rt::entry;
usehifive1::hal::prelude::*;
usehifive1::hal::DeviceResources;
usehifive1::{sprintln, pin};

#[entry]fnmain() ->! {
    let dr =DeviceResources::take().unwrap();
    let p = dr.peripherals;
    let pins = dr.pins;

    // Configure clockslet clocks =hifive1::clock::configure(p.PRCI, p.AONCLK, 320.mhz().into());

    // Configure UART for stdouthifive1::stdout::configure(p.UART0, pin!(pins, uart0_tx), pin!(pins, uart0_rx), 115_200.bps(), clocks);

    sprintln!("hello world!");

    loop {}
}

hello worldの出力に必要なことはriscvとかhifeve1のcrateでサポートされてるんですね。コードとしてはクロックの設定とUARTで出力するための設定してから文字列表示させてるだけですね。この単純明快さは😃

bootの仕組みを調べる

.cargo/confgにリンカの指定があってhifive1-link.xが指定されているのでこれを見てみます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-link.x
INCLUDE hifive1-memory.x
INCLUDE link.x

2つのファイルがincludeされてます。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/hifive1-d6f67d95492879dd/out/hifive1-memory.x 
INCLUDE memory-fe310.x
MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このファイルも最初にmemory-fe310.xをincludeしてます。これはというと、RAMのアドレスが0x80000000から0x80000000+16Kとなってますね。

masami@moon:~/projects/hello$ cat ./target/riscv32imac-unknown-none-elf/debug/build/e310x-9f201268daf213f7/out/memory-fe310.x
MEMORY
{
    RAM : ORIGIN = 0x80000000, LENGTH = 16K
}

そして、hifive1-memory.x に戻ると最初のほうはこんな感じで、FLASHは0x20000000から始まってサイズは4Mとなっていて、TEXT領域やRODATA領域はFLASHのアドレス範囲、BSSなどはmemory-fe310.xで設定してたRAMのアドレス範囲に置かれるというのがわかりました。

MEMORY
{
    FLASH : ORIGIN = 0x20000000, LENGTH = 4M
}

REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);

最後のところで0x20010000が出てきています。

/* Skip first 64k allocated for bootloader */
_stext = 0x20010000;

このアドレスはcargo runした時に表示されていたstart addressですね。

Loading section .text, size 0x3ae2 lma 0x20010000
Loading section .rodata, size 0xc84 lma 0x20013af0
Start address 0x20010000, load size 18278
Transfer rate: 5949 KB/sec, 9139 bytes/write.

このアドレスはHiFive1 Rev B Getting Started Guideに次のように書かれています。

f:id:masami256:20201203000340p:plain
9.1 Bootloader recovery

bootloarderは0x20000000から始まり、ユーザーのコードは0x20010000から始まると書かれているのでリンカースクリプトのコメントに書いてあった /* Skip first 64k allocated for bootloader */の意味がわかりますね。

まとめ

Rust良い👍

bootの仕組みもなんとなくわかったので今度はなにか作っていこう…

Viewing all 191 articles
Browse latest View live