리눅스 커널은 리스트를 연결하는 포인터와 노드에 저장되는 데이터를 분리하는 독특한 구현 방식을 적용하여 Circular Linked List
를 제공한다. 링크드 리스트의 공식 구현은 v2.1 커널 개발에서 도입되었다.
링크드 리스트의 각 노드는 v6.4.6 kernel 기준 /include/kernel/types.h
에 정의된 list_head
를 이용하여 다음 혹은 이전 노드를 가리킨다. 따라서 Doubly Circular Linked List
로 이해해도 좋다.
따라서 fox
라는 노드를 연결하는 링크드 리스트를 생성하기 위해서, fox
에 list_head
필드를 추가하여 노드들이 서로 연결되도록 구현한다.
위 코드는 kmalloc()
을 이용해 노드를 동적 생성할 경우를 보인다. 이때 list_head
필드는 INIT_LIST_HEAD()
함수를 사용해 초기화한다.
위 코드는 노드를 정적 생성하는 코드이다. 이때 LIST_HEAD_INIT
매크로가 사용되는데, 역할은 INIT_LIST_HEAD()
함수와 동일하다.
리스트 헤드는 링크드 리스트의 시작 노드를 가리키는 포인터로 사용된다. 이 포인터의 타입 또한 struct list_head
이고, LIST_HEAD
매크로를 이용하여 생성한다.
리스트를 구성하는 노드 구조체 내에는 struct list_head
field가 존재하여 이것의 내부 포인터 prev
, next
가 주변 노드들의 내부에 있는 list_head
를 가리킨다. 따라서 노드의 데이터에 접근하기 전에 먼저 그 노드의 list_head
에 접근하게 되므로, 접근한 list_head
를 field로 갖고 있는 노드에 접근해야만 노드의 데이터에 접근할 수 있다.
container_of
매크로는 member
가 이름인 field를 갖고, 그 field가 ptr을 가리키는 type
자료형의 구조체를 찾아 그 구조체의 포인터를 반환한다. 이 구현은 구조체 내 특정 field의 offset이 ABI 수준에서 고정됨을 이용한 것이다. 이 매크로를 호출하는 list_entry
매크로를 이용하여 특정 list_head
구조체를 field로 갖는 리스트 노드를 찾을 수 있다.
링크드 리스트를 조작하는 함수는 모두 /include/linux/list.h
에 인라인 함수로 정의되어 있으며, 인자로 struct list_head
를 받는다. 시간복잡도는 O(1)
이다.
/include/linux/list.h
에 구현되어 있는 list_add()
함수는 head
노드 다음에 new
노드를 추가한다.
반대로 list_add_tail()
함수는 head
노드 이전에 new
노드를 추가한다.
list_add()
, list_add_tail()
모두 내부적으로 __list_add
를 호출한다. 위 함수는 prev
와 next
노드 사이에 new
노드를 추가한다.
이전에 호출되는 __list_add_valid()
함수는 디버깅 용도로 사용되는데, build config에서 CONFIG_DEBUG_LIST
가 비활성화되어 있다면 항상 true를 반환한다.
리스트에서 노드를 제거할 때는 list_del()
함수를 사용한다. 이 함수는 entry
노드를 포함된 리스트에서 제거한다. 그러나 해당 함수는 노드를 리스트에서 제거하는 동작만 수행할 뿐, 메모리를 해제하지 않는다.
해당 함수는 내부적으로 __list_del_entry()
->__list_del()
을 호출하는데, 이때 인자로 entry
노드의 prev
와 next
를 넘긴다. 이 두 노드를 받은 __list_del()
함수는 prev
와 next
를 연결하는 동작을 수행한다.
만약 리스트에서 entry
노드를 제거하고 재사용이 필요한 경우, list_del_init()
함수를 사용한다. 이 함수는 entry
노드를 리스트에서 제거할 뿐만 아니라 노드의 초기화 작업도 진행한다.
list_move()
함수는 list
노드를 리스트에서 제거한 후, head
노드 다음에 list
노드를 추가한다.
list_move_tail()
함수는 마찬가지로 특정 리스트에서 list
노드를 제거한 후, head
노드의 앞에 list
노드를 추가한다.
list_empty()
함수는 head
가 연결된 리스트가 비어 있는지 확인한 후 참이면 true를 반환한다. 확인 방식은 해당 리스트에서 head
, 즉 리스트 헤드만 존재하는지를 확인한다.
list_splice()
함수는 list
노드가 연결된 리스트를 head
와 연결된 리스트와 연결하는 함수이다.
list_splice()
는 list
노드가 연결된 링크드 리스트가 비어있지 않다면 __list_splice()
를 호출한다. 이 함수는 list
의 prev
, next
노드를 head
, head->next
노드에 연결한다.
list_splice_init()
함수는 두 링크드 리스트를 병합할 뿐만 아니라, 병합 대상인 list
노드를 초기화한다.
이러한 함수들을 호출할 때,
list
의prev
,next
노드를 알고 있다면__
으로 시작하는 내부 함수를 직접 호출하는 것이 CPU cycle을 절약할 수 있다.
리스트를 조작하는 함수의 시간 복잡도는 O(1)
이지만, n개의 노드가 있는 리스트를 탐색하는 함수의 시간 복잡도는 O(n)
이다.
list_for_each
매크로는 첫 번째 인자로 해당 매크로 함수의 호출자가 사용하는 임시 변수를, 두 번째 인자는 리스트 헤드를 전달받는다. 그리고 임시 변수가 circular list를 한 바퀴 돌아 리스트 헤드에 도달할 때까지 리스트의 각 노드들을 순회한다.
반복의 조건에서 사용되는 list_is_head()
함수는 단순히 list
노드가 head
노드와 동일한지 확인한다.
위 코드에서 p
는 struct list_head
이므로, 데이터가 담긴 노드에 접근하기 위해서는 list_entry
매크로를 사용한다.
커널에서는 깔끔한 코드를 지향하므로 list_for_each_entry
매크로를 주로 사용한다. 이 매크로는 내부적으로 list_entry
매크로와 동일한 역할을 하는 list_first_entry
, list_next_entry
매크로를 사용하여, 바로 list_head
를 포함하는 노드 포인터를 얻을 수 있다.
인자로 전달받는 pos
는 list_head
를 포함하는 노드 포인터, head
는 리스트 헤드, member
는 노드 내에 정의된 list_head
field의 이름이다.
list_for_each_entry
를 이용하여 위와 같이 코드를 재작성할 수 있다.
list_for_each_entry_reverse
매크로는 역방향으로 순회한다는 점만 제외하고 list_for_each_entry
와 동일하다.
list_for_each_entry_safe
매크로는 리스트를 탐색할 때 노드를 리스트에서 제거하는 동작이 가능하도록 한다. 새롭게 추가된 인자는 n
으로, pos
의 다음 노드를 저장한다. 기존의 list_for_each_entry
에서 pos
를 제거하면, 다음 노드를 탐색하기 위해 제거된 pos
에 다시 접근하므로 버그가 발생한다. 그러나 다음 노드를 저장하는 n
변수가 존재하여 이 문제를 해결할 수 있다.
list_for_each_entry_safe_reverse
매크로는 역방향으로 순회한다는 점만 제외하고 list_for_each_entry_safe
와 동일하다.
커널은 kfifo
라는 이름으로 queue 자료구조를 구현하였고, enqueue와 dequeue 두 가지 기능을 제공한다. /include/linux/kfifo.h
에 정의되어 있다.
kfifo
객체는 두 가지 offset 정보를 갖고 있는데, 다음에 enqueue될 위치를 저장하는 in offset
과, 다음에 dequeue될 위치를 저장하는 out offset
이 있다. 두 offset은 각각 enqueue하거나 dequeue한 만큼의 값이 더해진다.
kfifo
를 사용하기 전에 먼저 동적 또는 정적으로 kfifo
를 생성하고 초기화해야 한다. 주로 동적 할당하여 사용하며, 이 작업을 진행하는 매크로는 kfifo_alloc
이다.
이 매크로는 내부적으로 __kfifo_alloc()
함수를 호출하는데, 해당 함수 내에서 동적 할당을 진행한다.
만약 직접 버퍼를 할당하여 사용하고 싶다면 kfifo_init
매크로를 사용하도록 한다.
이 매크로도 마찬가지로 내부 함수 __kfifo_init()
을 호출하는데, kfifo
의 data field를 buffer로 설정한다.
kfifo
에 데이터를 넣으려면 kfifo_in
매크로 함수를 호출하면 된다. 이 함수는 buf가 가리키는 위치에서 n만큼의 데이터를 enqueue하고, enqueue한 바이트 수를 반환한다. 이때 kfifo
의 빈 공간이 n보다 작으면 반환 값은 n보다 작거나 0이 될 수도 있다.
kfifo
에 데이터를 빼려면 kfifo_out
매크로 함수를 호출한다. 이 함수는 buf가 가리키는 위치에서 n의 데이터를 dequeue하고, dequeue한 바이트 수를 반환한다. 이때 kfifo
에 있는 데이터 크기가 n보다 작으면 반환 값 또한 n보다 작거나 0이 된다.
kfifo_size
매크로는 kfifo
에 데이터를 저장하기 위해 사용되는 버퍼의 크기를 반환한다.
kfifo_len
매크로는 kfifo
에 넣은 데이터의 바이트 수를 계산한다.
kfifo_avail
은 kfifo
의 여분 공간의 크기를 반환한다.
kfifo_is_empty
, kfifo_is_full
은 kfifo
가 비어있는지, 또는 차 있는지 확인하는 매크로이다.
kfifo
내에 저장된 데이터를 지우고 초기화하기 위해서는 kfifo_reset
매크로 함수를 사용한다.
kfifo_alloc
으로 할당된 kfifo
를 삭제하기 위해서는 kfifo_free
매크로 함수를 사용한다. 다만 kfifo_init
으로 버퍼를 직접 할당했을 경우, 할당된 버퍼는 따로 할당 해제해야 한다.
rbtree
는 리눅스에서 사용하는 자료구조로, 최적화까지 구현한 red-black tree이다. 루트 노드는 struct rb_root
를, 나머지 노드들은 struct rb_node
로 정의된다.
한편 트리에서 가장 작은 값의 노드를 찾기 위해, 캐시를 사용하는 트리의 루트 노드인 rb_root_cache
도 정의한다.
Red-Black Tree: https://code-lab1.tistory.com/62
트리를 새로 생성하기 위해, 먼저 루트 노드인 rb_root
를 새로 할당한 후 RB_ROOT
매크로로 초기화한다.
rb_root_cached
의 초기화를 위한 매크로도 존재한다.
rbtree
에서는 탐색, 삽입 기능을 제공하지 않고, 이 자료구조를 활용하는 개발자가 직접 구현해야 한다. 먼저 탐색의 구현은 이진 탐색 트리와 동일하다.
삽입도 탐색과 마찬가지로 개발자가 직접 구현해야 한다.
먼저 추가하고자 하는 데이터의 키 값을 루트 노드부터 비교하여 새로 추가될 노드의 자리를 찾는다. 그리고 필요한 경우 recoloring
을 한다. 위 코드에서 rb_link_node()
함수는 인자로 넘겨진 위치에 노드를 추가하고, rb_insert_color()
는 트리 균형을 맞추는 함수이다.