Linuxカーネルのコードを読んで勉強になったこと

Linuxカーネルのコードを読んでて、なるほど〜と思うことはよくあるけど、その中でも特に今までの考え方をぶち壊してくれたのはなんだっけと思ったところ、やっぱりリスト構造かなと言うところ。

c言語でリスト構造を作る場合、一般的な教科書方式だと↓のようにデータとnextポインタは密結合になってると思います。これの場合、struct foobarのポインタをnext要素に使っているので、他の構造体(例えば、struct hogehoge)で同じことをしようとすると、その構造体ではstruct hogehoge *nextというメンバ変数を持つ必要があります。 ヘッド要素はstruct foobarです。

struct foobar {
  int n;
  char s[64];
  struct foobar *next;
};

struct foobar head;

Linuxカーネルの場合、データとリスト構造の管理が別になっていて、リストはstruct list_headとして独立しているんですよね。

185 struct list_head {
186         struct list_head *next, *prev;
187 };

これのおかげで、リストの管理をデータに非依存で行えるようになってます。

さきのstruct foobarにstruct list_headを適用するとこのようになって、ヘッド要素はstruct list_headになります。

struct foobar {
 int n;
 char s[64];
 struct list_head *next;
};

struct list_head head;

struct list_headを使うと、データのリンクはstruct list_headを使って行うので、このようになります。

f:id:masami256:20160605225918p:plain

データはstruct list_headのnext変数でつながってる感じです。なので、リストを辿る場合はnext変数(双方向リストならprevも)を辿っていきます。struct list_headはprevとnextを持っていて、list_addマクロで登録するときはprev、nextともに設定されます。 では、データにはどうやってアクセスするのか?なると、container_ofマクロを使用します。

823 #define container_of(ptr, type, member) ({                      \
824         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
825         (type *)( (char *)__mptr - offsetof(type,member) );})

それと、offsetofマクロも必要です。こちらはコンパイラがoffsetofの機能をサポートしていればそれを使うようになってます。機能的には構造体のメンバ変数「MEMBER」が構造体の先頭から何バイト目にあるかを取得するものです。

 14 #undef offsetof
 15 #ifdef __compiler_offsetof
 16 #define offsetof(TYPE, MEMBER)  __compiler_offsetof(TYPE, MEMBER)
 17 #else
 18 #define offsetof(TYPE, MEMBER)  ((size_t)&((TYPE *)0)->MEMBER)
 19 #endif

この辺は、Linux: inodeからtask_struct構造体を取得 - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモで説明したりしてます。ようは、メモリ上にある構造体のメンバ変数のアドレスから、そのメンバ変数の構造体先頭からのオフセットを引けば、その構造体の先頭アドレスが取得できるという感じです。これにより、struct list_head構造体とデータを粗結合にすることができています。

一般的なリスト構造だとデータとリストが密に結合していたのに、Linuxカーネルの場合はそのへんが上手く抽象化されてて、勉強になったなと思ったわけです。 といっても、これはoffsetofとかcontainer_ofといったテクニックがあった上での実装方法なので、初学者向けではないと思いますが。でも、データとリストを別に管理するという方法は良いですよね。

page cacheの検索と作成・登録

詳解Linuxカーネル読書会 - 詳解Linuxカーネル読書会 | Doorkeeperのもくもく結果。今日はページキャッシュの処理を調べました。 参考資料はUnderstanding Linux Kernelですが、長いので赤本と呼びます。

kernelは4.6を参照

buffer_head構造体の初期化。

buffer_init()で実施。

  • buffer_headという名前のスラブキャッシュ作成
  • buffer_head用に確保できる最大のページ数を設定
  • cpu hotplugでcpuが外れた時のbuffer_headの後処理関数を設定

ページキャッシュの検索とキャッシュがなかった時に追加

赤本ではページキャッシュの追加はadd_to_page_cache()って書いているんだけど、検索した限りあまり使われていない感じです。

f:id:masami256:20160528133650p:plain

それよりも、add_to_page_cache_lru()や、find_or_create_page()が使われている。find_or_create_page()はページキャッシュを検索して、見つからなければページを確保してページキャッシュに登録してくれる関数。内部的にはadd_to_page_cache_lru()を使ってます。

赤本で説明されているfind_get_page()は内部の実装としてはpagecache_get_page()を呼びます。find_or_create_page()pagecache_get_page()を呼びます。違いはpagecache_get_page()の引数fgp_flagsにFGP_CREATを設定しているかどうかです。FGP_CREATのビットが立っている場合は、ページキャッシュがなかった時にキャッシュを登録します。ページキャッシュを検索する系の関数は基本的にpagecache_get_page()を使う感じですね。

ページキャッシュの検索は、最初に find_get_entry()でページを検索します。このfind_get_entry()は赤本でradix treeを探索してるって説明のところを行っている関数っぽいです。find_get_page()の場合は、find_get_entry()でページが見つかれば処理完了です。pageが見つかった場合は参照数を増やします。find_lock_page()の場合はpage->mappingが引数で渡されたmappingと同じかをチェックします。この時はpage構造体のロックを取る必要があって、ロックが取れなければキャッシュ見つからずという感じでNULLを返します。page->mapping != mappingの場合は確保したロックの解除と、pageの参照数を減らして、再度ページキャッシュの検索を行います。 つぎにfgp_flagsにFGP_ACCESSEDが設定されていた場合はmark_page_accessed()を呼びます。これはLRUキューの操作をしたりといったところです。

ページキャッシュが見つかった場合の処理はここまでです。次にページキャッシュが見つからなかった場合です。pageがNULLでfgp_flagsにFGP_CREATが設定されている場合にキャッシュの登録があります。最初にpageをallocします(これは通常はalloc_page()で)。pageを確保できなければそこで終了で、NULLを返します。次にfgp_flags関連の処理で、FGP_LOCKが設定されていなければ、FGP_LOCKを追加します。それと、FGP_ACCESSEDが設定されていれば、__SetPageReferenced()を実行します。

最後にadd_to_page_cache_lru()でLRUにページキャッシュを登録します。 LRUに登録するのはpage構造体だけど、まだpageとaddress_space構造体は関連付いていません。なので、その関連付けが最初に行うことになります。この処理を行うのは__add_to_page_cache_locked()です。ここでpage->mappingにpagecache_get_page()に渡されたmappingを設定したり、radix treeへの登録を行ってます。__add_to_page_cache_locked()の処理が終わったら、page構造体をLRUに登録します。

パーフェクトPython

パーフェクトPython

Dockerコンテナにalpine linuxを使って、headlessなXサーバでSeleniumを動かせるようにする

Dockerコンテナ内で使うディストリビューションはAlpine Linuxがマイブームですm( )m

Dockerfileはこんな感じ。

FROM alpine:3.3

RUN apk update && \
apk add xvfb dbus firefox imagemagick ruby libffi && \
apk add --virtual=build-deps gcc make libc-dev ruby-dev libffi-dev && \
gem install --no-ri --no-rdoc selenium-webdriver && \
gem install --no-ri --no-rdoc json && \
dbus-uuidgen && \
mkdir /tests && \
apk del build-deps && \
rm -rf /var/cache/apk/*

ENV DISPLAY :1

CMD Xvfb :1 -screen 0 1024x768x24

とりあえず、xvfbを動かすのに必要なのがこれら。

dbusdbusパッケージにあるdbus-genuuidを実行しておかないと、xvfb実行時にエラーになります。

--virtualを使ってインストールしているところは、gemのインストール時にビルドが必要になるので、そこで使うものをグループ化してインストール。gemのインストールが終わったら消してます。

後はこのようなdocker-compose.ymlを作成。

version: "2"

services:
  selenium:
    hostname: selenium
    build: .
    volumes:
      - ./tests:/tests:rw

Dockerfileがあるディレクトリにtestsを作って、そこに下のファイルを置いて、

#!/usr/bin/env ruby

require 'selenium-webdriver'

driver = Selenium::WebDriver.for :firefox 

driver.navigate.to "http://www.kernel.org"
driver.save_screenshot("./screenshot.png")

driver.quit

docker-compose upでコンテナを起動して、別のターミナルからdocker-compose execで起動中にコンテナに入って、/testsに移動してrubyのコードを動かすと、こんなスクリーンショットが撮れます。

f:id:masami256:20160528011844p:plain

Docker実践ガイド

Docker実践ガイド

systemdでユーザー固有のunitを動かす

systemdは~/.config/systemd/userにserviceファイルを置くことで、そのユーザー用のinit時の処理を動かすことができるんですね。基本的に使い方は通常と同じで、唯一違うのは--userをパラメータとして使用すること。詳細はArch Wikiを見ましょう。

このようにserviceファイルを書いたとして、

masami@kerntest:~$ cat ~/.config/systemd/user/foobar.service 
[Unit]
Description=User's service file

[Service]
Type=oneshot
ExecStart=/usr/bin/touch /tmp/test.txt
RemainAfterExit=yes

[Install]
WantedBy=default.target

このように実行します。

masami@kerntest:~$ systemctl --user enable foobar
Created symlink from /home/masami/.config/systemd/user/default.target.wants/foobar.service to /home/masami/.config/systemd/user/foobar.service.

再起動してstatusを確認するとちゃんと動作したのがわかります。

masami@kerntest:~$ systemctl --user status foobar
● foobar.service - User's service file
   Loaded: loaded (/home/masami/.config/systemd/user/foobar.service; enabled; vendor preset: enabled)
   Active: active (exited) since Thu 2016-05-05 11:32:14 JST; 25s ago
  Process: 342 ExecStart=/usr/bin/touch /tmp/test.txt (code=exited, status=0/SUCCESS)
 Main PID: 342 (code=exited, status=0/SUCCESS)

May 05 11:32:14 kerntest systemd[336]: Starting User's service file...
May 05 11:32:14 kerntest systemd[336]: Started User's service file.

ちなみに--userを付けないと~/config/systemd/userを見ないので、そんなサービスは無いって怒られます。

masami@kerntest:~$ systemctl  status foobar
● foobar.service
   Loaded: not-found (Reason: No such file or directory)
   Active: inactive (dead)

これは良いですね。

Amazon Web Services クラウドネイティブ・アプリケーション開発技法 一番大切な知識と技術が身につく

Amazon Web Services クラウドネイティブ・アプリケーション開発技法 一番大切な知識と技術が身につく

free(1)のtotalとかusedなどの各項目をカーネルの方から見てみる

free(1)は/proc/meminfoを読みに行くので、Linuxカーネルでどのような変数を見せているのかを調べてみます。カーネルのバージョンは4.5です。あ、swapのほうは今回は見ません。

最初に見るのはfs/proc/meminfo.cで、このファイルが/proc/meminfoに対する操作を定義しています。/proc/meminfoをopenする処理はmeminfo_proc_open()で、実際の処理はmeminfo_proc_show()が行います。

211 static int meminfo_proc_open(struct inode *inode, struct file *file)
212 {
213         return single_open(file, meminfo_proc_show, NULL);
214 }
215 
216 static const struct file_operations meminfo_proc_fops = {
217         .open           = meminfo_proc_open,
218         .read           = seq_read,
219         .llseek         = seq_lseek,
220         .release        = single_release,
221 };
222 
223 static int __init proc_meminfo_init(void)
224 {
225         proc_create("meminfo", 0, NULL, &meminfo_proc_fops);
226         return 0;
227 }

この関数の中で、メモリに関する情報はsi_meminfo()とsi_swapinfo()で取得します。

 41 #define K(x) ((x) << (PAGE_SHIFT - 10))
 42         si_meminfo(&i);
 43         si_swapinfo(&i);

si_meminfo()はこのような関数で、引数で渡されたsysinfo構造体にデータを設定します。

3606 void si_meminfo(struct sysinfo *val)
3607 {
3608         val->totalram = totalram_pages;
3609         val->sharedram = global_page_state(NR_SHMEM);
3610         val->freeram = global_page_state(NR_FREE_PAGES);
3611         val->bufferram = nr_blockdev_pages();
3612         val->totalhigh = totalhigh_pages;
3613         val->freehigh = nr_free_highpages();
3614         val->mem_unit = PAGE_SIZE;
3615 }

変数の内容は名前から大体想像つきますね。totalram_pagesがfree(1)実行時のtotalのところに出る値です。ここでの単位はbyteではなくてページ数です。他のデータも単位はページ数です。これをKiBにするのはmeminfo_proc_show()にあるK()マクロです。x86_64環境ならPAGE_SHIFTは12となっています。なので、2bit左シフトしてページ数からKiBに変更しています。

 41 #define K(x) ((x) << (PAGE_SHIFT - 10))

totalhighとfreehighはCONFIG_HIGHMEMが設定されている場合に意味があるんですが、x86_64用のカーネルならCONFIG_HIGHMEMは定義されていないので、値は0です。

で、free(1)のtotalですが、totalram_pagesはman 5 procに書かれているように、搭載しているRAMの総量ではありません。細かくは追ってないですが、このページ数を設定しているのは主にfree_bootmem_late()free_all_bootmem_core()ですね。 この値を表示するときは下記のようにしています。

 85         seq_printf(m,
 86                 "MemTotal:       %8lu kB\n"
〜略〜
143                 K(i.totalram),

物理メモリの搭載量を知りたい場合は↓でできます。

$ sudo dmidecode -t memory | grep "Size:.*MB" | awk '{ m += $2} END {print m}'

次にusedを見てみようと思いますが、man 1 freeUsed memory (calculated as total - free - buffers - cache)と書かれているので、先にfreeを見ましょう。

まず、表示する時にどの値を見ているか確認して、freeramのまま使っているというのがわかります。

 87                 "MemFree:        %8lu kB\n"
〜略〜
144                 K(i.freeram),

freeramはこのように設定されます。

3610         val->freeram = global_page_state(NR_FREE_PAGES);

procのmanではMemFree(LowFree+HighFree)というように書かれていますが、64bit環境にはLowFreeもHighFreeもないので、単なる空きページ数です。空きページ数の取得にはglobal_page_state()を使っているので、これを見てみます。引数のitemはNR_FREE_PAGESですね。値の読み出しはatomic_long_read()を使っていますが、これはlong型のデータをアトミックに読み込む関数です。なので、読み込んでいるのはvm_stat[NR_FREE_PAGES]です。

120 static inline unsigned long global_page_state(enum zone_stat_item item)
121 {
122         long x = atomic_long_read(&vm_stat[item]);
123 #ifdef CONFIG_SMP
124         if (x < 0)
125                 x = 0;
126 #endif
127         return x;
128 }

というわけで、vm_stat[NR_FREE_PAGES]に値を増やすところを見てみます。これを設定するのは__mod_zone_freepage_state()です。

259 static inline void __mod_zone_freepage_state(struct zone *zone, int nr_pages,
260                                              int migratetype)
261 {
262         __mod_zone_page_state(zone, NR_FREE_PAGES, nr_pages);
263         if (is_migrate_cma(migratetype))
264                 __mod_zone_page_state(zone, NR_FREE_CMA_PAGES, nr_pages);
265 }

実際は、__mod_zone_page_state() -> zone_page_state_add()という流れで下記のように値が設定されます。

113 static inline void zone_page_state_add(long x, struct zone *zone,
114                                  enum zone_stat_item item)
115 {
116         atomic_long_add(x, &zone->vm_stat[item]);
117         atomic_long_add(x, &vm_stat[item]);
118 }

それで、__mod_zone_freepage_state()が呼ばれるのはどんな時かというと、ページを取得するときと解放するときですよね。なので、__free_pages()free_pages()が呼ばれた時にということになります。

取得時はpage_alloc.cにあるbuffered_rmqueue()でpageが取得できている場合に、取得したページ数を減らしています。

2265                 if (!page)
2266                         goto failed;
2267                 __mod_zone_freepage_state(zone, -(1 << order),
2268                                           get_pcppage_migratetype(page));

そんなわけで、ここまでがMemFree/freeの値です。

つぎはbuffersを見ましょう。これはmanでMemory used by kernel buffers (Buffers in /proc/meminfo)と書かれています。 procのmanにはこのように書かれています。

Buffers %lu
                     Relatively temporary storage for raw disk blocks that
                     shouldn't get tremendously large (20MB or so).

Cachedはこうです。

              Cached %lu
                     In-memory cache for files read from the disk (the page
                     cache).  Doesn't include SwapCached.

BuffersとCachedは違いますね。 Buffersの表示は下記のようにやっているのでbufferramそのものです。

146                 K(i.bufferram),

Buffersはnr_blockdev_pages()という関数から取得していますので、これを見てましょう。

3611         val->bufferram = nr_blockdev_pages();

nr_blockdev_pages()はこのようにblock_device構造体にあるnrpagesの合計になっています。

677 long nr_blockdev_pages(void)
678 {
679         struct block_device *bdev;
680         long ret = 0;
681         spin_lock(&bdev_lock);
682         list_for_each_entry(bdev, &all_bdevs, bd_list) {
683                 ret += bdev->bd_inode->i_mapping->nrpages;
684         }
685         spin_unlock(&bdev_lock);
686         return ret;
687 }

block_device構造はメンバ変数が結構あるのでbd_inodeのがあるところだけ抜粋です。米野都の意味は全くわかりません/(^o^)\

452 struct block_device {
453         dev_t                   bd_dev;  /* not a kdev_t - it's a search key */
454         int                     bd_openers;
455         struct inode *          bd_inode;       /* will die */

bd_inodeは見ての通りinodeです。

inode構造体のi_mappingはaddress_space構造体です。

600         struct address_space    *i_mapping;

address_space構造体のnrpagesはこれです。

435         unsigned long           nrpages;        /* number of total pages */

というわけで、Buffersに表示される値はブロックデバイスに関連したinodeのaddress_space構造体に設定されているpage数ということがわかります。そして、Cachedですが、これはsi_meminfo()では設定していません。これを設定するのはmeminfo_proc_show()で計算にはbufferramの値が必要になります。

meminfo_proc_show()でのcachedの計算はこのようにやっています。global_page_state()](http://lxr.free-electrons.com/source/include/linux/vmstat.h?v=4.5#L120)はさっき見たやつですね。

 46         cached = global_page_state(NR_FILE_PAGES) -
 47                         total_swapcache_pages() - i.bufferram;

NR_FILE_PAGESはenumなんですが、コメントがないので定義している場所だと何をする変数なのかわかりません。コミットメッセージを見てみると、もともとはnr_pagecacheという変数だったのが2.6.18-rc1から変更されたようです。

Remove the special implementation for nr_pagecache and make it a zoned counter
named NR_FILE_PAGES.

これもわかりにくいので、使っているところを探します。例えば、__delete_from_page_cache()。これはページキャッシュからページを削除する関数です。__dec_zone_page_state()で値を減らす処理をしています。

227         /* hugetlb pages do not participate in page cache accounting. */
228         if (!PageHuge(page))
229                 __dec_zone_page_state(page, NR_FILE_PAGES);

また、replace_page_cache_page()でページキャッシュにあるページを別のpage構造体に置き換える場合、古いほうのページをキャッシュから削除して、新しいページをセットするときに__inc_zone_page_state()を使っています。

566                 __delete_from_page_cache(old, NULL, memcg);
〜略〜
574                 if (!PageHuge(new))
575                         __inc_zone_page_state(new, NR_FILE_PAGES);

他にも__add_to_page_cache_locked()で使ってます。これはpageをページキャッシュに登録する時に使われるものです。

その他、swapキャッシュへの登録時とswapキャッシュからの削除時にも値の更新があります。swapキャッシュ登録時は__add_to_swap_cache()で。

 96         if (likely(!error)) {
 97                 address_space->nrpages++;
 98                 __inc_zone_page_state(page, NR_FILE_PAGES);
 99                 INC_CACHE_INFO(add_total);
100         }

削除は__delete_from_swap_cache()で。

135 void __delete_from_swap_cache(struct page *page)
136 {
〜略〜
149         address_space->nrpages--;
150         __dec_zone_page_state(page, NR_FILE_PAGES);

そんなわけで、NR_FILE_PAGESはpage cacheとswap cacheに登録されたpageの数でしょう。しかし、procのmanではCachedにはswap cacheは含まないと書かれています。

              Cached %lu
                     In-memory cache for files read from the disk (the page
                     cache).  Doesn't include SwapCached.

もう一度計算式を見ると、total_swapcache_pages()の返り値を減算してますね。これは名前からしてswap cacheに登録されているページ数でしょう。

 46         cached = global_page_state(NR_FILE_PAGES) -
 47                         total_swapcache_pages() - i.bufferram;

total_swapcache_pages()はこのような処理で、address_space構造体の配列「swapper_spaces」のnrpagesの総数です。

 35 struct address_space swapper_spaces[MAX_SWAPFILES] = {
 36         [0 ... MAX_SWAPFILES - 1] = {
 37                 .page_tree      = RADIX_TREE_INIT(GFP_ATOMIC|__GFP_NOWARN),
 38                 .i_mmap_writable = ATOMIC_INIT(0),
 39                 .a_ops          = &swap_aops,
 40         }
 41 };
〜略〜
 52 unsigned long total_swapcache_pages(void)
 53 {
 54         int i;
 55         unsigned long ret = 0;
 56 
 57         for (i = 0; i < MAX_SWAPFILES; i++)
 58                 ret += swapper_spaces[i].nrpages;
 59         return ret;
 60 }

nrpagesを更新しているのはNR_FILE_PAGESを更新するタイミングでやってます。

そして、また計算式に戻ります。global_page_state(NR_FILE_PAGES) がpage cache/swap cacheにあるpage数。total_swapcache_pages()はswap cacheに登録されているpage数。bufferramはblock_device構造体に関連したファイルが使用しているpage数で、page cacheのpage数から色々差っ引いてCachedの値が出ます。

 46         cached = global_page_state(NR_FILE_PAGES) -
 47                         total_swapcache_pages() - i.bufferram;

usedはtotal - free - buffers - cacheということなので、これでuserの値に必要なデータは全部揃ったので表示可能ですね。

free(1)の最後の項目はavailableで、これはmanによると/proc/meminfoのMemAvailableということです。

      available
              Estimation of how much memory is available for starting new
              applications, without swapping. Unlike the data provided by
              the cache or free fields, this field takes into account page
              cache and also that not all reclaimable memory slabs will be
              reclaimed due to items being in use (MemAvailable in
              /proc/meminfo, available on kernels 3.14, emulated on kernels
              2.6.27+, otherwise the same as free)

このavailableもmeminfo_proc_show()で計算します。

 57         /*
 58          * Estimate the amount of memory available for userspace allocations,
 59          * without causing swapping.
 60          */
 61         available = i.freeram - totalreserve_pages;
 62 
 63         /*
 64          * Not all the page cache can be freed, otherwise the system will
 65          * start swapping. Assume at least half of the page cache, or the
 66          * low watermark worth of cache, needs to stay.
 67          */
 68         pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];
 69         pagecache -= min(pagecache / 2, wmark_low);
 70         available += pagecache;
 71 
 72         /*
 73          * Part of the reclaimable slab consists of items that are in use,
 74          * and cannot be freed. Cap this estimate at the low watermark.
 75          */
 76         available += global_page_state(NR_SLAB_RECLAIMABLE) -
 77                      min(global_page_state(NR_SLAB_RECLAIMABLE) / 2, wmark_low);
 78 

最初に、空きページ数のMemFree(i.freeram)から予約済みのページ数を引きます。 次に、使用可能な分のpage cacheのページ数を足します。ここはまず、LRU_ACTIVE_FILEとLRU_INACTIVE_FILEを探します。これは本当にindex用途っぽくて、include/linux/mmzone.hでこのように定義されています。

171 #define LRU_BASE 0
172 #define LRU_ACTIVE 1
173 #define LRU_FILE 2
174 
175 enum lru_list {
176         LRU_INACTIVE_ANON = LRU_BASE,
177         LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
178         LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
179         LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
180         LRU_UNEVICTABLE,
181         NR_LRU_LISTS
182 };

変数のpagesはこのように取得しています。global_page_state()はいつものやつです。NR_LRU_LISTSは5なので、0から4までのindexを使いますね。

 34         unsigned long pages[NR_LRU_LISTS];
〜略〜
 51         for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
 52                 pages[lru] = global_page_state(NR_LRU_BASE + lru);

global_page_state()に渡すのは列挙型のzone_stat_itemです。NR_ACTIVE_ANONまでの値を取ってきてますね。

114 enum zone_stat_item {
115         /* First 128 byte cacheline (assuming 64 bit words) */
116         NR_FREE_PAGES,
117         NR_ALLOC_BATCH,
118         NR_LRU_BASE,
119         NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */
120         NR_ACTIVE_ANON,         /*  "     "     "   "       "         */
121         NR_INACTIVE_FILE,       /*  "     "     "   "       "         */

そして、↓なので、LRU_ACTIVE_FILEは3、LRU_INACTIVE_FILEは2です。よって、zone_stat_itemでいうと、3に該当するのはNR_ACTIVE_ANON、2の方はNR_LRU_BASE/NR_INACTIVE_ANONです。なので、Anonymous mappingされたpage数の合計がpagecacheの初期値でしょうね。

 68         pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];

NR_LRU_BASEは使用箇所からして、pageをlruリストに登録・削除した時に設定されます。これの実装はlinux/mm_inline.hにあります。

 25 static __always_inline void add_page_to_lru_list(struct page *page,
 26                                 struct lruvec *lruvec, enum lru_list lru)
 27 {
 28         int nr_pages = hpage_nr_pages(page);
 29         mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
 30         list_add(&page->lru, &lruvec->lists[lru]);
 31         __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
 32 }
 33 
 34 static __always_inline void del_page_from_lru_list(struct page *page,
 35                                 struct lruvec *lruvec, enum lru_list lru)
 36 {
 37         int nr_pages = hpage_nr_pages(page);
 38         mem_cgroup_update_lru_size(lruvec, lru, -nr_pages);
 39         list_del(&page->lru);
 40         __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, -nr_pages);
 41 }

次の行はどちらか小さいほうが選択されて引かれます。

 69         pagecache -= min(pagecache / 2, wmark_low);

wmark_lowはこのように設定されます。実行中のLinuxカーネルが管理している各zoneのwatermarkのindexをWMARK_LOWとしたほうのwatermarkを使います。

 54         for_each_zone(zone)
 55                 wmark_low += zone->watermark[WMARK_LOW];

そして、pagecacheの値をavailableに足します。その次のNR_SLAB_RECLAIMABLEはslabオブジェクトのうち、page回収が可能とセットされているslabのページ数です。

 70         available += pagecache;
 71 
 72         /*
 73          * Part of the reclaimable slab consists of items that are in use,
 74          * and cannot be freed. Cap this estimate at the low watermark.
 75          */
 76         available += global_page_state(NR_SLAB_RECLAIMABLE) -
 77                      min(global_page_state(NR_SLAB_RECLAIMABLE) / 2, wmark_low);

これは、例えば、allocate_slab()でこのように使っています。sはkmem_cache構造体です。この構造体のflags変数にSLAB_RECLAIM_ACCOUNTがセットされていればNR_SLAB_RECLAIMABLEを使う感じです。

1482 
1483         mod_zone_page_state(page_zone(page),
1484                 (s->flags & SLAB_RECLAIM_ACCOUNT) ?
1485                 NR_SLAB_RECLAIMABLE : NR_SLAB_UNRECLAIMABLE,
1486                 1 << oo_order(oo));

で、flagsにSLAB_RECLAIM_ACCOUNTを設定するのはkmem_cache_create()の実行時です。例えば、fs/inode.cにあるinode_init()でinode用のslabを作る時に設定しています。

1887         /* inode slab cache */
1888         inode_cachep = kmem_cache_create("inode_cache",
1889                                          sizeof(struct inode),
1890                                          0,
1891                                          (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
1892                                          SLAB_MEM_SPREAD|SLAB_ACCOUNT),
1893                                          init_once);
1894 

そして、回収可能なslabのページ数から引き算を行うわけですが、これもどちらか小さいほうが引かれます。

 76         available += global_page_state(NR_SLAB_RECLAIMABLE) -
 77                      min(global_page_state(NR_SLAB_RECLAIMABLE) / 2, wmark_low);

これでavailableの値決定ですね。

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

Linuxカーネルのコマンドラインはブートローダーからどう渡されるのか?

先日参加した自作OSもくもく会で「Linuxカーネルコマンドラインブートローダーからどう渡されるのか?」のような話が聞こえたので、調べようと思い調べてみました。確認はLinux kernel v4.5とsystemd-bootの2016/05/02 23:00 JSTのコードです。 uefiじゃない環境も確認しようかなと思ってgrubのコードをgit cloneはしました。が、うちのメイン環境で使っているのはsystemd-bootだしってことで確認してません。

で、カーネルコマンドラインは↓のようなやつですね。

masami@saga:~/codes$ cat /proc/cmdline
initrd=\initramfs-4.6.0-rc5-ktest+.img root=/dev/sda2 rw crashkernel=256M

コマンドラインカーネルに渡すとしたら、どこかしらのアドレスに置くんだろうというのは想像できますが、ブートローダーの好きな場所に置くとも考えにくいので、何かしらのプロトコルは決まっているはずです。というわけで、カーネルのドキュメントを確認します。確認するのはDocumentation/x86/boot.txtです。 これを見ると、カーネルコマンドラインはヒープの終わりから0xA0000までの間の好きなところに置けると書かれています。

532   Set this field to the linear address of the kernel command line.
533   The kernel command line can be located anywhere between the end of
534   the setup heap and 0xA0000; it does not have to be located in the
535   same 64K segment as the real-mode code itself.

そして、systemd-bootのsrc/boot/efi/linux.cのコードを見ると、0xA0000がありますね。systemd-bootはアドレス0xA0000にカーネルコマンドラインを置くようです。

        if (cmdline) {
                addr = 0xA0000;
                err = uefi_call_wrapper(BS->AllocatePages, 4, AllocateMaxAddress, EfiLoaderData,
                                        EFI_SIZE_TO_PAGES(cmdline_len + 1), &addr);
                if (EFI_ERROR(err))
                        return err;
                CopyMem((VOID *)(UINTN)addr, cmdline, cmdline_len);
                ((CHAR8 *)addr)[cmdline_len] = 0;
                boot_setup->cmd_line_ptr = (UINT32)addr;
        }

そして、if文を抜けた後は、linux_efi_handover()を呼び、この関数からLinuxカーネルに制御が移ります。

#ifdef __x86_64__
typedef VOID(*handover_f)(VOID *image, EFI_SYSTEM_TABLE *table, struct SetupHeader *setup);
static inline VOID linux_efi_handover(EFI_HANDLE image, struct SetupHeader *setup) {
        handover_f handover;

        asm volatile ("cli");
        handover = (handover_f)((UINTN)setup->code32_start + 512 + setup->handover_offset);
        handover(image, ST, setup);
}
#else

ここから呼ばれるのはarch/x86/kernel/head_32.Sのstartup_32だと思います。そして、この辺でブートパラメータのコピー処理があります。

127 /*
128  * Copy bootup parameters out of the way.
129  * Note: %esi still has the pointer to the real-mode data.
130  * With the kexec as boot loader, parameter segment might be loaded beyond
131  * kernel image and might not even be addressable by early boot page tables.
132  * (kexec on panic case). Hence copy out the parameters before initializing
133  * page tables.
134  */
135         movl $pa(boot_params),%edi
136         movl $(PARAM_SIZE/4),%ecx
137         cld
138         rep
139         movsl
140         movl pa(boot_params) + NEW_CL_POINTER,%esi
141         andl %esi,%esi
142         jz 1f                   # No command line
143         movl $pa(boot_command_line),%edi
144         movl $(COMMAND_LINE_SIZE/4),%ecx
145         rep
146         movsl

boot_command_lineはinit/main.cにあります。linux/include/init.hでextern宣言しているのでhead_32.Sからもアクセスできます。

122 /* Untouched command line saved by arch-specific code. */
123 char __initdata boot_command_line[COMMAND_LINE_SIZE];

今回はこんなところで( ´ー`)フゥー...

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

systemptapでプロセスが所属する各pid名前空間におけるpid番号を取得

systemptapでプロセスが所属する各pid名前空間におけるpid番号を取得する方法のめも。

プロセスのPIDは普通に見れますが、そこで見えているPIDというのは自身が所属しているPID名前空間においてのものです。なので、あるプロセスがunshareなりでpid名前空間を親プロセスと別の場合に、親プロセスの名前空間から見たPIDを知ることはできません。逆に親プロセスからは自身のPID名前空間での子プロセスのPIDを見ることはできますが、子プロセスのPID名前空間におけるPIDはわかりません。ってことで、あるプロセスが所属するPID名前空間でのPIDを全部見ることができるツールsystemtapで書いてみました。

ソースはこちらです。stapのコマンドライン

gist.github.com

まず、dockerコンテナ内でnginxを動かしてみます。

[root@6d02a9c04d45 /]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:13 ?        00:00:00 /bin/bash
root        51     1  0 15:15 ?        00:00:00 nginx: master process nginx
http        52    51  0 15:15 ?        00:00:00 nginx: worker process
root        59     1  0 15:19 ?        00:00:00 ps -ef

このときのホスト側ではプロセスはこう見えています。docker(2768)の下にnginxのプロセスがいますね。

$ pstree -p
systemd(1)-+-agetty(259)
           |-agetty(260)
           |-dbus-daemon(235)
           |-dhcpcd(328)
           |-docker(2768)-+-bash(12494)---nginx(12572)---nginx(12573)
           |              |-{docker}(2769)
           |              |-{docker}(2770)
           |              |-{docker}(2771)
           |              |-{docker}(2772)
           |              |-{docker}(2773)
           |              |-{docker}(2774)
           |              |-{docker}(2775)
           |              |-{docker}(2776)
           |              |-{docker}(2777)
           |              |-{docker}(3019)
           |              |-{docker}(3023)
           |              |-{docker}(3024)
           |              `-{docker}(3025)
           |-sshd(330)-+-sshd(1587)---sshd(1595)---bash(1596)---docker(12432)-+-{docker}(12433)
           |           |                                                      |-{docker}(12434)
           |           |                                                      |-{docker}(12435)
           |           |                                                      |-{docker}(12436)
           |           |                                                      |-{docker}(12437)
           |           |                                                      |-{docker}(12438)
           |           |                                                      |-{docker}(12439)
           |           |                                                      `-{docker}(12532)
           |           |-sshd(1674)---sshd(1676)---bash(1677)---pstree(12949)
           |           `-sshd(12270)---sshd(12272)---bash(12273)
           |-systemd(1589)---(sd-pam)(1590)
           |-systemd-journal(169)
           |-systemd-logind(244)
           |-systemd-timesyn(192)---{sd-resolve}(195)

そして、ホスト側でスクリプトを動かすとこうなって、コンテナ内 -> ホストという感じでpidが表示できました。

masami@nlkb:~$ sudo stap -v -g ./pid.stp 12573
Pass 1: parsed user script and 113 library scripts using 69844virt/36104res/4924shr/31496data kb, in 90usr/10sys/109real ms.
Pass 2: analyzed script: 2 probes, 2 functions, 1 embed, 0 globals using 70636virt/37136res/5180shr/32288data kb, in 10usr/0sys/3real ms.
Pass 3: translated to C into "/tmp/stapfubRbf/stap_6dd962b4096d287515d5450e69c15b96_2149_src.c" using 70636virt/37136res/5180shr/32288data kb, in 0usr/0sys/0real ms.
Pass 4: compiled C into "stap_6dd962b4096d287515d5450e69c15b96_2149.ko" in 1550usr/510sys/2322real ms.
Pass 5: starting run.
pid 52
  pid 12573
Done
Pass 5: run completed in 10usr/80sys/467real ms.

つぎはnsenterでdockerコンテナのpid名前空間に入り、そこでスクリプトを実行してみます。 まずはnsenter。

$ sudo nsenter -t 12494 -p

systemtapスクリプトを実行。

# ./tools/systemtap/bin/stap -v -g ./pid.stp 52                                                                                                                                                  
Pass 1: parsed user script and 113 library scripts using 69840virt/36220res/5048shr/31492data kb, in 100usr/10sys/105real ms.
Pass 2: analyzed script: 2 probes, 2 functions, 1 embed, 0 globals using 70632virt/37252res/5304shr/32284data kb, in 0usr/0sys/3real ms.
Pass 3: translated to C into "/tmp/stapRubZUb/stap_96165e2d709fb6669fa3a3854fe6f030_2146_src.c" using 70632virt/37252res/5304shr/32284data kb, in 0usr/0sys/0real ms.
Pass 4: compiled C into "stap_96165e2d709fb6669fa3a3854fe6f030_2146.ko" in 1540usr/480sys/2312real ms.
Pass 5: starting run.
pid 52
  pid 12573
Done
Pass 5: run completed in 20usr/80sys/496real ms.

nsenterの場合、ファイルシステムは元の名前空間を使ってるので/procが完全に切り替わってません。なので、/procまで完全に切り離した状態でやってないのですが多分動くはず。。。

以下、systemtapのめもをば。 systemtapは実行時にコマンドライン引数を渡すことができます。今回は./pid.stp 52という感じでpidを渡しています。コマンドライン引数をprobeのところで使用するには$1や@1などで使用できます。$や@は型で、$は整数、@は文字列です。

このようにc言語で書いた関数にパラメータを渡しています。

probe begin {
        /* pass pid that is command line argument */
        show_pidtree($1)
        exit()
}

今回作ったスクリプトc言語の関数を組み込んでいて、これも引数を取ることができます。使えるのはstringかlong型です。

function show_pidtree:long(pn:long) %{
%}

このスクリプトだとlong型のpnという引数を受け取ります。 ただ、引数はpnという名前ではアクセスできなくて(undefinedで怒られる)、STAP_ARGというprefixが必要です。 そのため、スクリプトでは下記のようにSTAP_ARG_pnとして引数のpnにアクセスしています。

        int pid_nr = STAP_ARG_pn;

systemtapスクリプトでは多少癖はありますが、c言語を使えるので便利ですね。

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ