Linuxカーネルのソースコードを読む方法(その4: list.h 後編)

Linux カーネルソースコードを読んだのでメモを公開します.Linuxカーネルを読む際の参考になれば幸いです.

お題: list.h

前回の続き,リスト構造の実装です.

前回のエントリはこちら http://pyopyopyo.hatenablog.com/entry/2019/06/30/150000

リストの操作: ノードの初期化

ノードの初期化は INIT_LIST_HEAD()を使います.

    struct list_head head;
    INIT_LIST_HEAD(&head);

INIT_LIST_HEAD() で初期化されると,ノードのポインタ,head.prev と head.next の値は次のようになります.

   +-----------------------+
   | prev             prev |
   +---  +---------+ <-----+
         |  head   |
   +---> +---------+  ---> +
   |                  next |
   +-----------------------+

INIT_LIST_HEAD()は include/linux/list.h の26行目で次のように実装されています.

26:
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	WRITE_ONCE(list->next, list);
	list->prev = list;
}

WRITE_ONCEはマクロで,WRITE_ONCE(A,B) で変数Aに,値Bをアトミックに代入するインラインアセンブラに展開されます.

アトミック処理を無視すると,INIT_LIST_HEADは下のコードと等価になります.

static inline void INIT_LIST_HEAD(struct list_head *list)
{
        list->next = list; // 実際には,この代入はアトミック処理
        list->prev = list;
}

ここで少し話が脱線しますが,重要な点があります.それは,

  • なぜアトミック処理が必要になるのか?
  • なぜWRITE_ONCE()はnext ポインタのみで,prevポインタは使用されていないのか?

の2点です.

まず1つ目,アトミック処理が必要な理由は「struct list_head が複数のCPUから同時にアクセスされる場合があるから」です.
たとえば list のアドレスが 0x11223344 だったとします.

この場合

list->next = list 

というCのコードは,次のようにコンパイルされる場合があります(アセンブラC言語風に記述します.)

レジスタ0 = list->nextのアドレス
レジスタ1 = 0x1122;
レジスタ2 = 0x3344;
*(レジスタ0) = レジスタ1;
*(レジスタ0+1) = レジスタ2;

このようにC言語では一行の処理でも,実際にはアドレス上位とアドレス下位の2つの転送命令にコンパイルされる場合があります.
上記の例だと,list->next のポインタ変数は,4行目と5行目の2命令で更新されます.

このように2命令で nextのポインタ変数を書き換えてしまうと,その書き換え中,つまり4行目が実行されて,5行目が実行される前の期間は,nextポインタのアドレスが無効な値になります.無効な値になるのは一瞬ですが,その瞬間にハードウェア割り込みやプリエンプション(ソフトウェア割り込み)で別の処理が割り込んだ場合,その割り込み処理は 無効なnextアドレスを参照してしまいます.結果カーネルがクラッシュします.

そこでLinuxカーネルは, struct list_head の操作に対して以下のルールを儲けてアトミック性を保証するようになっています.

  • nextのアクセスはアトミック処理に実装.具体的には WRITE_ONCE(),READ_ONCE()を使う
  • アトミック処理が必要な場面では next のみを使用し, prev は用いない

前述の2つ目の疑問,WRITE_ONCE()がnextのみで prevには使用されていない理由もこれで説明できます.つまり,このルールに従って INIT_LIST_HEAD()が実装されているから,ということになります.

リストの操作: list_empty()

list_empty()は,リストが空である場合 true を返す関数です.

実装は list.hの235行目です

static inline int list_empty(const struct list_head *head)
{
	return READ_ONCE(head->next) == head;
}

前述のようにnextポインタへのアクセスはアトミックであるべきなので,この関数はREAD_ONCEマクロを使用して実装されています.意味的には以下の処理をアトミックに行います.

static inline int list_empty(const struct list_head *head)
{
	return head->next == head;
}

リストの操作: list_add()

ノードをリストに追加する関数です list.h の69行目で実装されています.

69:
/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

実体は __list_addです

50:
/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	if (!__list_add_valid(new, prev, next))
		return;

	next->prev = new;
	new->next = next;
	new->prev = prev;
	WRITE_ONCE(prev->next, new);
}

ここで WRITE_ONCE()の使い方に注意すると

  • 新しく追加するノード new は,まだリストに登録されていないので他のCPUはアクセスできない WRITE_ONCE()は不要
  • 既存のノード prev->next は,他のCPUがアクセスする可能性があるので WRITE_ONCE()を使用

となっています.つまりこの list_add()は排他制御なしで安全に利用できます.

まとめ

  • list.h を読みました
  • リストは Doubly circular linked list を実装していました.
    • ノードは struct list_head; ポインタとして prev と next を持っています
    • リストは出来るだけロックフリーで使えるように工夫されています.
    • リストへのアクセスはアトミックになるように気をつけましょう.
  • API
    • データ構造 struct list_head;
      • 構造体へのアクセスはマクロ container_of() を使う
    • 初期化
      • INIT_LIST_HEAD()
    • 追加
      • list_add()
    • 走査
      • list_for_each()
      • list_for_each_entry()
    • 他にもたくさんマクロや関数が定義されています.詳しいことは include/linux/list.h を直接読みましょう.