리눅스커널

[Linux Kernel] Linux Linked List 분석하기

Min00 2024. 7. 2. 23:16
struct klist_node {
	void			*n_klist;	/* never access directly */
	struct list_head	n_node;
	struct kref		n_ref;
}​
/**
 * list_replace - replace old entry by new one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace(struct list_head *old,
				struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}

/**
 * list_replace_init - replace old entry by new one and initialize the old one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace_init(struct list_head *old,
				     struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}​
/**
 * list_del - deletes entry from list.
 * @entry: the element to delete from the list.
 * Note: list_empty() on entry does not return true after this, the entry is
 * in an undefined state.
 */
static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}​
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);
}

/**
 * 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);
}

< include/linux/klist.h>, < include/linux/list.h>에 있는 커널 분석을 진행하며 리눅스 링크드 리스트 구조에 대한 이해를 해보자!

 

아마도 doubly linked list 방식을 사용하고 있는 것 같다. 

struct klist {
	spinlock_t		k_lock;
	struct list_head	k_list;
	void			(*get)(struct klist_node *);
	void			(*put)(struct klist_node *);
} __attribute__ ((aligned (sizeof(void *))));

 

k_lock : mutex를 보장하기 위한 lock 

list_head : linked list의 head node

*get, *put : 노드를 넣고, 빼는 함수 포인터

 

extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
		       void (*put)(struct klist_node *));

 

위처럼 init 함수를 통해서 linked list structure를 선언할 수 있다

따로 alloc 하지 않아도 되고, klist 포인터를 전달해주고 해당 리스트의 get, put의 함수 포인터를 전달해주면 됨

함수 내에서 LIST_HEAD_INIT을 통해서 initialize를 수행한다. 

 

// <linux/include/list.h> 에서 LIST_HEAD_INIT을 확인할 수 있다.

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

// tools/include/compiler.h에서 WRITE_ONCE 함수를 확인할 수 있다.

#define WRITE_ONCE(x, val) \
    ({                          \
     union { typeof(x) __val; char __c[1]; } __u =   \
     { .__val = (__force typeof(x)) (val) }; \
     __write_once_size(&(x), __u.__c, sizeof(x));    \
     __u.__val;                  \
     })
     
static __always_inline void __write_once_size(volatile void *p, void *res, int size)
{
	switch (size) {
	case 1: *(volatile  __u8_alias_t *) p = *(__u8_alias_t  *) res; break;
	case 2: *(volatile __u16_alias_t *) p = *(__u16_alias_t *) res; break;
	case 4: *(volatile __u32_alias_t *) p = *(__u32_alias_t *) res; break;
	case 8: *(volatile __u64_alias_t *) p = *(__u64_alias_t *) res; break;
	default:
		barrier();
		__builtin_memcpy((void *)p, (const void *)res, size);
		barrier();
	}
}

WRITE_ONCE 함수를 살펴보면 2가지 변수, x와 val을 받는다.

typeof(x) __val, char __c[1]을 변수로 가지는 union 공용체 __u를 선언한다.

__val은 x와 같은 타입의 변수를 갖는데 해당 변수에 입력받은 val 값을 강제로 x의 typeof로 형변환을 한 값을 저장한다.

그리고 __write_once_size로 에 값을 써주는데 해당 operation을 확인하면 x의 주소에 __u.__c의 값을 넣어준다. 이때 __u는 union이기 때문에 결국 __u.__c는 실질적으로 x와 같은 값을 가지고 있다. 

 

그리고 write_once_size를 보면 1,2,4,8에 해당하는 size는 volatile로 최적화 과정에서 형변환이 일어나지 않도록 캐스팅을 해주고, 만약 해당 사이즈가 아니라면 barrier를 통해서 연산 순서를 강제하고 있다. 즉 builtin_memcpy가 일어나기 전에 있는 모든 메모리 접근 함수가 완료되었음을 보장하고 그 두번째 barrier는 bullitin_memcpy가 끝난 이후에 메모리 접근이 순서대로 일어나도록 보장한다.

 

해당 함수를 통해 WRITE를 atomically하게 수행할 수 있다

(이것도 아마 멀티 쓰레딩에서 atomic operation을 보장하기 위한 하나의 테크닉으로 볼 수 있을 듯 하다 )

 

2. list_add

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);
}

 

linked list의 꼬리에 node를 새로 추가하는 것을 볼 수 있다.

WRITE_ONCE(prev->next, new); 를 통해서 해당 함수 add하는 동작이 atomic하게 연결되는 것을 확인할 수 있다.

new->next, new->prev를 연결하는 것은 atomic하게 되지 않아도 된다는걸 안다 ( 어차피 새로운 데이터니까 )

근데 next->prev를 연결할 땐 왜 atomic하게 하지 않을까??

 

이유는 __list__add__valid에 있었다!!

 

/*
 * Performs list corruption checks before __list_add(). Returns false if a
 * corruption is detected, true otherwise.
 *
 * With CONFIG_LIST_HARDENED only, performs minimal list integrity checking
 * inline to catch non-faulting corruptions, and only if a corruption is
 * detected calls the reporting function __list_add_valid_or_report().
 */
static __always_inline bool __list_add_valid(struct list_head *new,
					     struct list_head *prev,
					     struct list_head *next)
{
	bool ret = true;

	if (!IS_ENABLED(CONFIG_DEBUG_LIST)) {
		/*
		 * With the hardening version, elide checking if next and prev
		 * are NULL, since the immediate dereference of them below would
		 * result in a fault if NULL.
		 *
		 * With the reduced set of checks, we can afford to inline the
		 * checks, which also gives the compiler a chance to elide some
		 * of them completely if they can be proven at compile-time. If
		 * one of the pre-conditions does not hold, the slow-path will
		 * show a report which pre-condition failed.
		 */
		if (likely(next->prev == prev && prev->next == next && new != prev && new != next))
			return true;
		ret = false;
	}

	ret &= __list_add_valid_or_report(new, prev, next);
	return ret;
}

 

 

if문을 확인해보면 next->prev, prev, prev->next = next로 잘 연결이 되어있는지, 그리고 새로운 node에 연결이 안 되어 있는게 맞는지 체크를 해준다!

그니까 위에서도 마지막 prev->next = new 만 atomic하게 실행을 해주면 그 위에 operation들에서는 atomic하게 실행되지 않아도 다른 스레드에서 저 __list__add__valid에서 false 값이 반환되니까 그 operation은 이전 operation이 끝날때까지 실행되지 못한다!!

 

 

 

 

 

해당 __list_add 함수를 이용해 

// list의 head에 데이터를 추가한다 > stack
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

// list의 tail에 데이터를 추가한다. > queue
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

이렇게 두 가지 메소드를 사용할 수 있다

 

3. list_del

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	WRITE_ONCE(prev->next, next);
}

static inline void __list_del_entry(struct list_head *entry)
{
	if (!__list_del_entry_valid(entry))
		return;

	__list_del(entry->prev, entry->next);
}

 

 실제 다양한 list_del operation은 __list_del_entry로 수행이 되고 add와 같이 valid한지 수행하고 실제 delete를 수행하므로 prev->next = next만 atomic하게 실행이 되면 된다. 

 

/**
 * list_del - deletes entry from list.
 * @entry: the element to delete from the list.
 * Note: list_empty() on entry does not return true after this, the entry is
 * in an undefined state.
 */
static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

/*
 * These are non-NULL pointers that will result in page faults
 * under normal circumstances, used to verify that nobody uses
 * non-initialized list entries.
 */
#define LIST_POISON1  ((void *) 0x100 + POISON_POINTER_DELTA)
#define LIST_POISON2  ((void *) 0x122 + POISON_POINTER_DELTA)

list_del을 호출해서 해당 entry를 삭제할 수 있다

그럼 여기서 entry의 next, prev에 LIST_POISON을 왜 연결하는 걸까??

list del entry에서 지울 entry를 가리키는 포인터들은 없애줬지만, 해당 entry가 next, prev는 그대로 남아있다

이걸 LIST_POISON으로 초기화를 해서 해당 값을 접근을 하면 정상적인 상황임에도 PAGE_FAULT가 일어나도록 한다.

그렇게 하면 저런 초기화되지 않은 값에 접근하려는 사용자를 잡아낼 수 있다!

 

4. list_replace

/**
 * list_replace - replace old entry by new one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace(struct list_head *old,
				struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}

/**
 * list_replace_init - replace old entry by new one and initialize the old one
 * @old : the element to be replaced
 * @new : the new element to insert
 *
 * If @old was empty, it will be overwritten.
 */
static inline void list_replace_init(struct list_head *old,
				     struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}

 

list_replace로 old 값을 new로 바꿔준다. 

list_replace_init은 해당 list의 old>new로 바꿔주고 old의 prev, next가 모두 자기 자신을 가리키도록 해서 초기화를 해준다. 

 

5. list_swap

 

 

/**
 * list_swap - replace entry1 with entry2 and re-add entry1 at entry2's position
 * @entry1: the location to place entry2
 * @entry2: the location to place entry1
 */
static inline void list_swap(struct list_head *entry1,
			     struct list_head *entry2)
{
	struct list_head *pos = entry2->prev;

	list_del(entry2);
	list_replace(entry1, entry2);
	if (pos == entry1)
		pos = entry2;
	list_add(entry1, pos);
}

list swap은 두 entry의 위치를 바꿔준다!

 

이런 로직으로 작동되는 듯 하다.

 

6. list_cut

 

/**
 * list_cut_before - cut a list into two, before given entry
 * @list: a new list to add all removed entries
 * @head: a list with entries
 * @entry: an entry within head, could be the head itself
 *
 * This helper moves the initial part of @head, up to but
 * excluding @entry, from @head to @list.  You should pass
 * in @entry an element you know is on @head.  @list should
 * be an empty list or a list you do not care about losing
 * its data.
 * If @entry == @head, all entries on @head are moved to
 * @list.
 */
static inline void list_cut_before(struct list_head *list,
				   struct list_head *head,
				   struct list_head *entry)
{
	if (head->next == entry) {
		INIT_LIST_HEAD(list);
		return;
	}
	list->next = head->next;
	list->next->prev = list;
	list->prev = entry->prev;
	list->prev->next = list;
	head->next = entry;
	entry->prev = head;
}

 

주어진 entry 이전까지 list를 반 잘라준다.

이런 식으로!!

 

7. list_spslice

 

static inline void __list_splice(const struct list_head *list,
				 struct list_head *prev,
				 struct list_head *next)
{
	struct list_head *first = list->next;
	struct list_head *last = list->prev;

	first->prev = prev;
	prev->next = first;

	last->next = next;
	next->prev = last;
}

/**
 * list_splice - join two lists, this is designed for stacks
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 */
static inline void list_splice(const struct list_head *list,
				struct list_head *head)
{
	if (!list_empty(list))
		__list_splice(list, head, head->next);
}

 

두 list를 합쳐준다.

list가 새로운 list가 되고 

그 좌측, 우측에 각각의 list를 연결시켜서 합쳐준다

 

이 밖에도 list의 node의 갯수를 새는 함수 

해당 entry의 struct를 가져오는 함수 

등등 다양한 함수가 있다! 하지만 위의 함수를 기반으로 하는게 많고, 그렇게 어렵지 않은 내용이라 아마 사용하며 익힐 수 있을 듯 하다!

 

+) 실제 kernel code를 작성할 땐 <list.h>보단 <klist.h>를 많이 사용할 듯 하다

/* SPDX-License-Identifier: GPL-2.0-only */
/*
 *	klist.h - Some generic list helpers, extending struct list_head a bit.
 *
 *	Implementations are found in lib/klist.c
 *
 *	Copyright (C) 2005 Patrick Mochel
 */

#ifndef _LINUX_KLIST_H
#define _LINUX_KLIST_H

#include <linux/spinlock.h>
#include <linux/kref.h>
#include <linux/list.h>

struct klist_node;
struct klist {
	spinlock_t		k_lock;
	struct list_head	k_list;
	void			(*get)(struct klist_node *);
	void			(*put)(struct klist_node *);
} __attribute__ ((aligned (sizeof(void *))));

#define KLIST_INIT(_name, _get, _put)					\
	{ .k_lock	= __SPIN_LOCK_UNLOCKED(_name.k_lock),		\
	  .k_list	= LIST_HEAD_INIT(_name.k_list),			\
	  .get		= _get,						\
	  .put		= _put, }

#define DEFINE_KLIST(_name, _get, _put)					\
	struct klist _name = KLIST_INIT(_name, _get, _put)

extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
		       void (*put)(struct klist_node *));

struct klist_node {
	void			*n_klist;	/* never access directly */
	struct list_head	n_node;
	struct kref		n_ref;
};

extern void klist_add_tail(struct klist_node *n, struct klist *k);
extern void klist_add_head(struct klist_node *n, struct klist *k);
extern void klist_add_behind(struct klist_node *n, struct klist_node *pos);
extern void klist_add_before(struct klist_node *n, struct klist_node *pos);

extern void klist_del(struct klist_node *n);
extern void klist_remove(struct klist_node *n);

extern int klist_node_attached(struct klist_node *n);


struct klist_iter {
	struct klist		*i_klist;
	struct klist_node	*i_cur;
};


extern void klist_iter_init(struct klist *k, struct klist_iter *i);
extern void klist_iter_init_node(struct klist *k, struct klist_iter *i,
				 struct klist_node *n);
extern void klist_iter_exit(struct klist_iter *i);
extern struct klist_node *klist_prev(struct klist_iter *i);
extern struct klist_node *klist_next(struct klist_iter *i);

#endif

klist는 list structure와 klist_node structure가 분류되어 있다

그리고 list_head가 list의 head를 가리키고 여기에 실제 데이터는 저장되지 않는 것 같다.

 

struct klist_node {
	void			*n_klist;	/* never access directly */
	struct list_head	n_node;
	struct kref		n_ref;
}

k_list node는 이런식으로 생겼다

list_head : list.h에서 본 대로 next, prev를 가리키는 포인터가 저장되어있는 구조체

n_ref : reference count 

n_klist : 해당 영역의 bit로 다양한 상태를 체크해서 operation을 수행

 

list와 klist의 차이가 있는 듯 한데, klist를 쓰는게 좀 더 낫지 않을까..

lock이나 이런게 구현이 되어있으서 thread-safe할 것 같다.

 

이런 list 구조체, 데이터를 같이 담은 struct를 선언해서 사용하는 것 같다 보통..

 

ref

 

https://wariua.github.io/facility/long-list-of-linked-lists.html

 

연결 리스트들의 긴 목록

리눅스 커널에는 연결 리스트 구현이 있다. 그것도 다양하게. 몇 가지를 살펴보자. 기본 첫 번째가 뭔지는 정해져 있다. include/linux/types.h: struct list_head { struct list_head *next, *prev; }; include/linux/list.h:

wariua.github.io