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 を直接読みましょう.

Linuxカーネルのソースコードを読む(その3: list.h 前編)

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

今回のお題: include/linux/list.h

今回はカーネル内部で多用されているデータ構造の一つとして linked list の実装を読みました.主なファイルは linux/list.h になります.ソースコードのバージョンは linux-5.1.8です.

カーネル内部でよく利用されているデータ構造

よく利用されているデータ構造としては以下のものがあります.

  • List
  • Queue
  • Map
  • Binary tree

C++であればSTLの std::list とか std::queue を利用するところです.しかしカーネルの実装しかもC言語です. linux kernel は自前の実装を提供しています.

Singly linked list と Doubly circular linked list

今回読むソースコード include/linux/list.h はいわゆる doubly circular linked list を実装しています.

念の為 Singly linked list と Doubly circular linked list の違いを説明しておきます.

singly linked list

リスト構造には様々な種類があります.例えば singly linked list であれば,ノードを

struct Node {
    struct Node *next;
};

のような形で定義します.そしてこのNode構造体を使ってリスト構造を構築します.アスキーアートで図示すると,例えばノード数3のリスト構造は次のようになります.リストの終端はNULLポインタで表現しています

         +---------+       +---------+       +---------+ 
         |  Node   |       |  Node   |       |  Node   |       
         +---------+  ---> +---------+  ---> +---------+  ---> NULL
                     next             next            
doubly circular linked list

一方,linux/list.hが実装している doubly circular linked list では,ノードを次のように定義します.

struct list_head {
	struct list_head *next;
        struct list_head *prev;
};

実際の定義は include/linux/types.h の186行目にあります.

そしてリスト構造を次のように構築します.こちらもノード数3のリスト構造の例です.

   +--------------------------------------------------------+
   | prev             prev             prev                 |  
   +---  +---------+ <--   +---------+ <--   +---------+ <--+  
         |list_head|       |list_head|       |list_head|       
   +---> +---------+  ---> +---------+  ---> +---------+  --+
   |                  next             next            next |
   +--------------------------------------------------------+

singly linked list と異なり doubly linked list には nextのポインタに加えて prevのポインタがあります.この2つのポインタを使って,各ノードは,次ノード,前ノード,双方にリンクを貼っています.

また circular linked list とあるように,リストの終端がありません.リストの末尾はリストの先頭に接続します.

struct list_head の利用例

// ノードの定義
struct Data {
    struct list_head list;
    int value;
};

// ノードを新しく生成する関数
struct Data * new_data()
{
    Data *ptr = kalloc( sizeof (struct Data), GFP_KERNEL );
    INIT_LIST_HEAD(&ptr->list);
    return ptr;
}

// リストを作成する例
void myfunc {
    struct list_head head;
    INIT_LIST_HEAD(&head);
    
    struct Data* A = new_data();
    struct Data* B = new_data();
    list_add(&A->list,  &head);
    list_add(&B->list,  &head);
}

myfunc() の中で生成したリスト構造をアスキーアートで表現すると次のようになります.

   +--------------------------------------------------------+
   | prev             prev             prev                 |  
   +---  +---------+ <--   +---------+ <--   +---------+ <--+  
         |   head  |       | A->list |       | B->list |       
   +---> +---------+  ---> +---------+  ---> +---------+  --+
   |                  next             next            next |
   +--------------------------------------------------------+

リストの操作: list_for_each()

circular linked list にはリストの末尾がありません.foreach()のような,リストの要素を順番に見る処理は次のように実装します.

struct list_head *pos;
for (pos = &head->next; ptr != &head; ptr = ptr->next) {
    // 処理
}

linux カーネルは list_for_each() というマクロを定義しています.(list.hの489行目)

#define list_for_each(pos, head) \
	for (pos = (head)->next; pos != (head); pos = pos->next)

このマクロを使うと上記のコードは

  struct list_head *pos;
  list_for_each(pos, &head) {
      // 処理
  }

という風にコーディングできます.

リストの操作: container_of()

list_for_each は list_head を順に走査するマクロです. list_head から strcut Data のポインタを得るには container_ofマクロを使います

  struct list_head *pos;
  list_for_each(pos, &head) {
       struct Data * p = container_of(pos, struct Data, list);
  }

container_of マクロは linux/kernel.h の970行目で定義されているマクロです.

970:
/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&	\
			 !__same_type(*(ptr), void),			\
			 "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

エラーチェックなどを削除すると,container_of の実装は次のように簡略化できます

#define container_of(ptr, type, member) ((type *)( (char *)(ptr) - offsetof(type,member) ))

要は

  • offsetof()を使って構造体 type のメンバ変数 member のオフセットアドレスを計算
  • ptr からオフセットを引いて,構造体typeの先頭アドレスを計算
  • アドレスを type* 型にキャスト

しているだけです.

リストの操作:list_for_each_entry()

意味的には for_each_list と container_of を組み合わせたマクロです

#define list_for_each_entry(pos, head, member)				\
	for (pos = list_first_entry(head, typeof(*pos), member);	\
	     &pos->member != (head);					\
	     pos = list_next_entry(pos, member))

これをつかうと list_head 構造体を介さず,直接 struct Data のポインタでループを回せます.

  struct Data *p;
  list_for_each_entry(p, &head, list) {
      // 処理
  }