SlideShare une entreprise Scribd logo
1  sur  9
Télécharger pour lire hors ligne
Linux Kernel 3.x 과 2.6.11의
PID Hash Table Data Structures 비교
2014.12.29
양 희철
heecheol.yang@outlook.com
1. Introduction
마지막 3판이 나온 지 10년이 다 되어가는데도 Danidl P.Bovet & Marco Cesati 의
Understanding the Linux Kernel (이하 ULK)은 여전히 많은 사람들이 리눅스 커널을 정복하기 위해
읽는 책이다. 하지만 아무리 책 내용이 방대하고 자세하다고 하더라도 현재의 리눅스 커널 코드
내용과 10년 전의 코드 내용이 완전히 같은 수는 없다. 예를 들어, 리눅스의 프로세스 스케줄러는
이 책이 나온 이후에도 두 번이나 바뀌었다.
이 글에서는 달라진 점 중 하나로 PID Hash Table과 관련된 내용을 소개하고자 한다. 먼저, ULK
에서 말하는 리눅스 커널 버전 2.6.11의 PID Hash Table의 구현을 간략하게 소개 한 후, 현재의 리
눅스 커널은 이것들이 어떻게 바뀌었는가를 설명할 것이다.
2. PID Hash Table Description in ULK
ULK의 Chapter 3: Process 의 ‘Relationship Among Processes’ 절에는 부모-자식 관계 등의 프로
세스 관계를 리눅스 커널이 설계 및 구현하고 있는지 기술되어 있다. 커널은 이런 여러 관계를
표현하기 위하여 Linked List 등의 다양한 자료구조를 활용하고 있는데, 특히 커널은 Process
ID(PID)를 통해 Process descriptor를 빠른 속도로 검색해내기 위하여 PID를 키로 하는 Hash Table
을 유지하고 있고 이 Hash table을 PID Hash Table (pidhash)라고 불린다. ULK에서는 ‘The pidhash
table and chained lists’ 절을 통해서 pidhash 와 관련된 Data structure들의 구조와 그 관계를 설
명하고, 그림을 통해 커널이 특정 PID를 기반으로 Process descriptor을 어떻게 검색하는지 예시를
보여주고 있다. 이 장에서는 ULK의 내용을 인용하여 ULK 가 설명하고 있는 리눅스 커널 2.6.11
에서는 pidhash가 어떻게 구현되어 있는지 간략하게 요약하고자 한다.
앞서 PID Hash Table은 PID를 키로 하여 PID에 대한 Processor Descriptor를 검색하기 위한
Hash Tabled라고 밝혔다. 또한, 2.6.11버전의 리눅스 커널은 4 가지 종류의 PID1
를 유지하고 있기
때문에, 각 종류별로 PID Hash Table이 존재하여야 한다 (그림 1). 따라서, 2.6.11 커널은 pid_hash
1 PID는 Processor ID를 의미하는 것이 맞지만, 여기서는 리눅스 커널에서 Process들의 관계를 나타내기
위해 사용하는 모든 ID, 즉 Process ID, Thread Group ID, Process Group ID, Session Group ID를 모두 PID로
표현하겠다. ULK 역시 같은 맥락으로 표현하고 있다.
는 전역 변수를 유지하며 Hash Table을 관리한다 (그림 2).
그림 1 Linux Kernel 2.6.11 에서의 PID 종류 (출처: ULK Chapter 3)
kernel/pid.c
#define pid_hashfn(nr) hash_long((unsigned long)nr, pidhash_shift)
static struct hlist_head *pid_hash[PIDTYPE_MAX]; //PIDTYPE_MAX : 4
static int pidhash_shift;
그림 2 PID Hash Table의 구현
pid_hash 전역 변수는 struct hlist_head 포인터에 대한 배열로 선언되어 있다. 각 배열 원소는
2.6.11 커널에서 유지하는 4 가지 종류의 PID Hash Table을 가리키는 포인터이며, 부팅 시점에 커
널 초기화 과정에서 Hash Table 들의 크기가 결정되고, 각각 동적 배열로 할당된다(그림 3).
kernel/pid.c
void __init pidhash_init(void)
{
…
for (i = 0; i < PIDTYPE_MAX; i++) {
pid_hash[i] = alloc_bootmem(pidhash_size*sizeof(*(pid_hash[i])));
for (j = 0; j < pidhash_size; j++)INIT_HLIST_HEAD(&pid_hash[i][j]);
}
}
그림 3 PID Hash Table 초기화 과정
결국 각 PID Hash Table 은 struct hlist_head 의 배열이 되는데, 이 배열 원소 하나는 Hash Table
Entry로서 pid_hashfn() 함수에 의해 Hashing 된 PID에 해당하는 Process Descriptor 리스트의
Head를 가리키게 된다2
.
한편, 실제로 2.6.11 커널에서 Hash Table들이 가리키는 리스트는 Process Descriptor의 리스트가
아닌 struct pid 의 리스트이다. 이 리스트는 Hash Collision의 처리를 위한 리스트와, 같은 PID로
이루어진 리스트를 함께 유지하기 위하여 존재한다 (그림 4, 그림 5).
2
여기서 PID Hash Table이 Processor Descriptor 하나가 아닌 List를 가리키는 이유는 PID 종류에 따라서
하나의 PID 에 대해 여러 Process가 검색될 수 있기 때문이다. 예를 들어, 단순히 Process ID Hash Table을
검색하면 그 결과물은 Process Descriptor 하나가 나오지만, Thread Group ID(TGID) Hash Table을 통한 검색의
결과는 해당 Thread Group에 속하는 Process 목록이 된다.
include/linux/pid.h
struct pid
{
int nr;
struct hlist_node pid_chain;
struct list_head pid_list;
};
그림 4 struct pid 구조체 구현
그림 5 struct pid 구조체 내 필드들의 역할 (출처: ULK Chapter 3)
struct pid 를 노드로 하는 리스트는 nr 필드, 즉 같은 PID를 이루는 리스트 나타낸다. 그리고
PID는 4 가지 종류가 존재하므로 모든 Process Descriptor는 4 가지 PID에 struct pid 리스트를 유
지하여야 한다. 이를 위해 Process Descriptor 에는 struct pid 에 대한 배열인 pids 필드가 존재한
다 (그림 6). 이 배열의 각 원소는 각 종류의 PID 리스트를 가리키는 struct pid 에 해당한다.
include/linux/sched.h
struct task_struct {
…
struct pid pids[PIDTYPE_MAX];
…
};
그림 6 Process Descriptor의 pids 필드
ULK에서는 앞서 언급된 자료구조의 전체적인 관계를 보여주기 위하여 같은 TGID를 가지고 있
는 모든 프로세스를 찾는 예제를 그림으로 나타내었다 (그림 7). 만약 TGID가 4351번인 모든
Process들을 검색하고자 한다면, 우선 pid_hashfn() 함수를 통해 4351번에 대한 Hash 값인 70을
얻어낸다. 이 Hash 값은 TGID Hash Table의 배열 Index로 사용되는데, 이 배열은 전역 변수
pid_hash[PIDTYPE_TGID] 가 가리키고 있다.
한편, 예제에서는 Hash 70번이 4351번과 256번에 의하여 Collision이 발생하는데, 이 문제를 해
결하기 위하여 커널은 PID Hash Table이 가리키고 있는 struct pid의 pid_chain 필드와 nr 필드를
이용한다. 즉, 커널은 find__pid() 함수를 통해 PID Hash Table이 가리키고 있는 pid_chain 을 탐색
하면서 nr 값과 4351값을 비교하여 실제로 찾고자 하는 TGID 4351에 대한 리스트를 찾아낸다 (그
림 8).
find_pid() 함수를 통해 원하는 struct pid를 찾았다면, 해당 구조체를 필드로 가지고 있는
Process Descriptor, 즉 4351 TGID를 가지는 Process 리스트의 첫 번째 Process Descriptor를 찾을
수 있다. 따라서 이후에는 struct pid 의 pid_list 필드를 통해 리스트를 탐색 할 수 있게 된다.
그림 7 예제: TGID Hash Table 검색 (출처: ULK Chapter 3)
kernel/pid.c
struct pid * fastcall find_pid(enum pid_type type, int nr)
{
struct hlist_node *elem;
struct pid *pid;
hlist_for_each_entry(pid, elem,
&pid_hash[type][pid_hashfn(nr)], pid_chain) {
if (pid->nr == nr)
return pid;
}
return NULL;
}
그림 8 find_pid() 함수의 동작
3. Linux Kernel 3.x 에서의 PID Hash Table 동작 구조
리눅스 커널이 발전해 나가면서 커널 소스 코드 내 많은 부분이 변경되어 왔고, PID Hash Table
역시 구조적으로 많은 변화가 발생하였다. 이 장에서는 구체적으로 어떠한 변화가 발생하였고, 이
로 인해 리눅스 커널 3.x 버전에서의 PID Hash Table 방식은 어떻게 바뀌었는지 구체적으로 기술
한다. 이 문서에서는 리눅스 커널 3.17.4 버전을 기준으로 한다.
가장 중요한 변화로는 pid_hash 전역 변수가 Process ID 의 Hash Table만 관리하도록 변경되었
다는 점이다 (그림 9)3
. 즉, 2.6.11 버전의 pid_hash는 PID 종류의 개수만큼 배열로 존재하던 것과
달리, 현재는 배열이 아닌 동적으로 할당된 PID Hash Table 하나만을 가리키는 포인터로 변경되었
다.
kernel/pid.c
#define pid_hashfn(nr, ns) 
hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)
static struct hlist_head *pid_hash;
static unsigned int pidhash_shift = 4;
그림 9 pid_hash 전역변수의 변화
또한 pid_hash 전역변수가 단순히 Process ID에 대한 Hash Table만을 유지하게 되었기 때문에
PID 와 PID Type을 매개변수로 받아 PID Hash Table을 탐색하던 find_pid() 함수도 더 이상 PID
Type은 받지 않고, PID 만 받아 검색하도록 변경되었다(그림 10).
kernel/pid.c
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
struct upid *pnr;
hlist_for_each_entry_rcu(pnr,
&pid_hash[pid_hashfn(nr, ns)], pid_chain)
if (pnr->nr == nr && pnr->ns == ns)
return container_of(pnr, struct pid,
numbers[ns->level]);
return NULL;
}
그림 10 find_pid() 함수의 변경
find_pid() 함수의 변경내용 중 중요한 점은 pid_hash 가 가리키고 있는 자료구조가 struct pid
에서 새로 도입된 구조체인 struct upid 로 변경되었다는 점이다. 리눅스 커널 2.6.11 버전에서
find_pid() 함수가 Hash chain 을 탐색하기 위해 사용하던 pid_chain 필드는 struct pid 안에 있는
3 pid_hashfn()함수를 보면 nr 매개변수 외에 Namespace를 의미하는 ns 매개변수가 추가된 것을 볼 수
있다. Namespace는 Hash Table 개념과는 다른 주제이기 때문에, 이 문서에서는 추가적으로 언급하지 않겠
다.
필드였다. 하지만 3.x 버전의 리눅스 커널에서는 struct pid 구조체 내에 numbers 라는 필드명으
로 struct upid 구조체를 포함하도록 하였으며, 이로 인해 struct pid 의 중요한 필드들이 이 구조
체로 이동하였다 (그림 11).
변경된 내용으로 첫째, pid_chain 필드가 struct upid 내로 이동하였다. 때문에 find_pid() 함수는
struct upid 로 이루어져 있는 리스트를 탐색한다. 또한, struct pid 의 nr 필드 역시 struct upid 로
이동하였기 때문에, find_pid() 함수는 nr 값과 검색하고자 하는 PID 를 비교 시 struct upid 를 참
조한다. 하지만 find_pid() 함수는 여전히 struct pid 의 포인터를 반환하고 있기 때문에, 이 함수는
nr 값이 검색하고자 하는 값과 일치하는 struct upid 를 찾았으면 해당 구조체를 감싸고 있는
struct pid 의 주소를 반환하게 된다.
include/linux/pid.h
struct upid {
int nr;
struct pid_namespace *ns;
struct hlist_node pid_chain;
};
struct pid
{
atomic_t count;
unsigned int level;
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
struct upid numbers[1];
};
그림 11 struct upid 의 구조와, struct pid 의 변화
한편, 리눅스 커널 2.6.11 버전에서는 Process Descriptor 의 pids 필드가 struct pid 의 배열이었
지만, 3.x 버전의 리눅스 커널에서는 pids 필드가 새로 도입된 구조체인 struct pid_link 로 변경되
었다 (그림 12, 그림 13).
include/linux/sched.h
struct task_struct {
…
struct pid_link pids[PIDTYPE_MAX];
…
};
그림 12 Process Descriptor의 pids 필드 변경 내용
include/linux/pid.h
struct pid_link
{
struct hlist_node node;
struct pid *pid;
};
그림 13 struct pid_link 의 구조
따라서 리눅스 커널 2.6.11 버전과 다르게 커널 3.x 버전에서는 Process descriptor가 struct pid
를 가지고 있는 것이 아니기 때문에 struct pid 가 주어졌을 때 이를 포함하는 Process Descriptor
를 찾아내는 방법 역시 달라졌고, 이는 pid_task() 함수에 구현되어 있다 (그림 14).
kernel/pid.c
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = hlist_first_rcu(&pid->tasks[type]), lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pids[(type)].node);
}
return result;
}
그림 14 pid_task() 함수의 구현4
pid_task() 함수의 동작을 파악하기 위해서는 struct pid 에 새로 추가 된 필드인 struct
hlist_head tasks[] 배열을 확인 할 필요가 있다. 이 배열은 PID 종류 개수만큼의 원소를 가지고 있
으며, 각각의 원소는 hlist_head 타입으로서 각각의 PID 종류에 대해 같은 PID를 가진 Process
Descriptor의 리스트를 가리키고 있다. 예를 들어, 특정 Session ID를 가지고 있는 모든 Process 의
리스트를 확인하려면 tasks[PIDTYPE_SID] 를 참조하여야 한다.
pid_tasks() 함수는 가장 먼저 주어진 struct pid 가 가리키고 있는 리스트, 즉 pid->tasks[type]
가 가리키는 리스트의 첫 번째 노드를 찾아 first 변수가 가리키도록 한다. 이 노드는 검색하고자
하는 Process Descriptor의 pid_link 가 포함하고 있는 노드이기 때문에 pid_hash() 함수는 이 노드
를 통해 찾고자 하는 Process Descriptor를 찾을 수 있다.
리눅스 3.x 버전에서 변경된 내용들을 정리하여 ULK에서 소개된 예제를 적용해 보면, 다소 구
조가 복잡해짐을 알 수 있다 (그림 15). 예를 들어 PGID가 246번인 프로세스의 목록을 검색하고
자 한다면, Hashing 된 값인 70번을 Index로 pid_hash Hash Table을 검색한다. 예제에서는
Collision이 발생하였으므로 nr 값을 비교하며 pid_chain 을 따라 Hash Chaining 리스트를 탐색하
면 찾고자 하는 struct upid 구조체를 찾을 수 있다.
원하는 struct upid 를 찾았으면, 이를 감싸고 있는 struct pid 구조체를 찾아내어야 한다. 이후
struct pid 내 tasks[PIDTYPE_PGID] 의 hlist_head 가 가리키고 있는 노드를 따라가면 그 노드를 포
함하고 있는 struct pid_link 및 Process Descriptor를 찾을 수 있게 된다. 한편, 해당 PID에 해당하
는 프로세스가 여러 개라면, pid 링크를 통해 모두 탐색 할 수 있을 것이다.
4 가독성을 위하여 함수 동작 파악에 관계가 없는 Locking 관련 내용은 코드에서 삭제하여 나타내었다.
그림 15 예제: PGID Hash Table 검색5
5 리눅스 커널 3.x 버전에서는 PIDTYPE_TGID가 사라진 것을 확인 할 수 있었다. 따라서 이 예제에서는
TGID 대신 PGID를 이용하였다.
struct task_struct
{
…
struct pid_link pids[PIDTYPE_MAX];
…
}
struct pid_link
{
struct hlist_node node;
struct pid *pid;
}
struct pid
{
struct hlist_head tasks[PIDTYPE_MAX];
struct upid numbers[1];
}
struct upid
{
int nr;
struct hlist_node pid_chain;
}
struct hlist_head
{
struct hlist_node *first;
}
700 2047
pid_hashfn(246,ns)
struct hlist_head *pid_hash
struct pid_link pids[PIDTYPE_PGID]
node
pid
struct task_struct
nr=4351
pid_chain
struct upid numbers
struct pid
hlist_head tasks
struct pid_link pids[PIDTYPE_PGID]
node
pid
struct task_struct
nr=4351
pid_chain
struct upid numbers
struct pid
hlist_head tasks
struct pid_link pids[PIDTYPE_PGID]
node
pid
struct task_struct
nr=4351
pid_chain
struct upid numbers
struct pid
hlist_head tasks
struct pid_link pids[PIDTYPE_PGID]
node
pid
struct task_struct
nr=246
pid_chain
struct upid numbers
struct pid
hlist_head tasks
4. 결론
본 문서에서는 리눅스 커널 3.x 버전에서 PID Hash Table을 유지하는 방법이 어떻게 바뀌었는가
를 기술하였고, ULK 에서 기술한 2.6.11 버전 커널의 구현과 비교해 보았다. 10여년이 지난 코드인
만큼, 많은 내용들이 바뀌었으며, 다소 복잡해졌다. 특히, 실제 코드 구현적인 부분뿐만 아니라 설
계와 관련된 부분들도 변경 점이 있었기 때문에 더욱 복잡해진 것으로 보인다.
하지만 PID Hash Table 관리 방식의 설계가 왜 바뀌었는지는 확인하지 못하였다. 특히 이 문서
에서 기술한 내용은 이미 작성 된 소스 코드를 기준으로 그림을 그려나간 것이기 때문에 실제로
는 어떠한 의도로 설계를 하였는지 유추해 내기 힘들다. 이를 확인하기 위해서는 소스 코드 변경
이 언제, 어느 버전에서 이루어 졌는지 확인해 볼 필요가 있다. 버전 업데이트 시점을 확인한 후
해당 시점에서의 문서 업데이트나 메일링 리스트를 확인하면 좀 더 구체적인 내용을 확인 할 수
있을 것이다.
사족을 붙여 보자면, 커널 내 존재하는 PID 관련된 자료구조의 수를 줄여 메모리를 절약하기
위하여 설계가 변경된 것이 아닐까 추측된다. 2.6.11 버전에서는 각 PID 종류 별로 PID Hash Table
을 비롯한 여러 자료구조들을 각각 유지하고 있어야 했다. 하지만 3.x 버전에서의 struct pid 를
살펴보면, 하나의 nr 번호를 이용하여 tasks 배열을 통해 모든 종류의 PID를 관리할 수 있게 해
놓았다. 따라서 예를 들어, 2.6.11 버전에서는 같은 nr 번호 100 번이더라도 PID 종류에 따라
struct pid를 4 개 유지하여야 했으나 3.x 버전과 같은 방식에서는 nr=100 에 대한 struct upid 및
struct pid 는 한 개만 유지하고 tasks 배열을 통해 각 PID 종류 별 리스트만 관리를 하면 된다.
이러한 방식을 통해 리눅스 커널은 struct pid에 소모되는 메모리를 절약할 수 있을 것이라고 추
측 할 수 있을 것이다.

Contenu connexe

Tendances

Processor organization &amp; register organization
Processor organization &amp; register organizationProcessor organization &amp; register organization
Processor organization &amp; register organizationGhanshyam Patel
 
Register organization, stack
Register organization, stackRegister organization, stack
Register organization, stackAsif Iqbal
 
Inter process communication using Linux System Calls
Inter process communication using Linux System CallsInter process communication using Linux System Calls
Inter process communication using Linux System Callsjyoti9vssut
 
Operating system 24 mutex locks and semaphores
Operating system 24 mutex locks and semaphoresOperating system 24 mutex locks and semaphores
Operating system 24 mutex locks and semaphoresVaibhav Khanna
 
Interrupt of 8085
Interrupt of 8085Interrupt of 8085
Interrupt of 8085Nitin Ahire
 
IO Techniques in Computer Organization
IO Techniques in Computer OrganizationIO Techniques in Computer Organization
IO Techniques in Computer OrganizationOm Prakash
 
Intel x86 Architecture
Intel x86 ArchitectureIntel x86 Architecture
Intel x86 ArchitectureChangWoo Min
 
computer fundamentals unit 3 notes
computer fundamentals unit 3 notes  computer fundamentals unit 3 notes
computer fundamentals unit 3 notes Vikram Nandini
 
Computer registers
Computer registersComputer registers
Computer registersJatin Grover
 
Micro Programmed Control Unit
Micro Programmed Control UnitMicro Programmed Control Unit
Micro Programmed Control UnitKamal Acharya
 
Addressing mode Computer Architecture
Addressing mode  Computer ArchitectureAddressing mode  Computer Architecture
Addressing mode Computer ArchitectureHaris456
 

Tendances (20)

VIRTUAL MEMORY
VIRTUAL MEMORYVIRTUAL MEMORY
VIRTUAL MEMORY
 
Processor organization &amp; register organization
Processor organization &amp; register organizationProcessor organization &amp; register organization
Processor organization &amp; register organization
 
Register organization, stack
Register organization, stackRegister organization, stack
Register organization, stack
 
Process scheduling linux
Process scheduling linuxProcess scheduling linux
Process scheduling linux
 
Inter process communication using Linux System Calls
Inter process communication using Linux System CallsInter process communication using Linux System Calls
Inter process communication using Linux System Calls
 
Semaphore
SemaphoreSemaphore
Semaphore
 
Operating system 24 mutex locks and semaphores
Operating system 24 mutex locks and semaphoresOperating system 24 mutex locks and semaphores
Operating system 24 mutex locks and semaphores
 
Interrupt of 8085
Interrupt of 8085Interrupt of 8085
Interrupt of 8085
 
IO Techniques in Computer Organization
IO Techniques in Computer OrganizationIO Techniques in Computer Organization
IO Techniques in Computer Organization
 
Multiprocessor
MultiprocessorMultiprocessor
Multiprocessor
 
Process scheduling
Process schedulingProcess scheduling
Process scheduling
 
Intel x86 Architecture
Intel x86 ArchitectureIntel x86 Architecture
Intel x86 Architecture
 
Process Scheduling
Process SchedulingProcess Scheduling
Process Scheduling
 
computer fundamentals unit 3 notes
computer fundamentals unit 3 notes  computer fundamentals unit 3 notes
computer fundamentals unit 3 notes
 
cache memory
cache memorycache memory
cache memory
 
Computer registers
Computer registersComputer registers
Computer registers
 
Micro Programmed Control Unit
Micro Programmed Control UnitMicro Programmed Control Unit
Micro Programmed Control Unit
 
Addressing mode Computer Architecture
Addressing mode  Computer ArchitectureAddressing mode  Computer Architecture
Addressing mode Computer Architecture
 
Instruction format
Instruction formatInstruction format
Instruction format
 
Swapping | Computer Science
Swapping | Computer ScienceSwapping | Computer Science
Swapping | Computer Science
 

Similaire à Linux kernel 3.x와 2.6.11의 PID Hash Table 자료구조 비교

리눅스 커널 기초 태스크관리
리눅스 커널 기초 태스크관리리눅스 커널 기초 태스크관리
리눅스 커널 기초 태스크관리Seungyong Lee
 
C#을 사용한 빠른 툴 개발
C#을 사용한 빠른 툴 개발C#을 사용한 빠른 툴 개발
C#을 사용한 빠른 툴 개발흥배 최
 
Linux programming study
Linux programming studyLinux programming study
Linux programming studyYunseok Lee
 
Windows via c++ chapter6
Windows via c++   chapter6Windows via c++   chapter6
Windows via c++ chapter6Shin heemin
 

Similaire à Linux kernel 3.x와 2.6.11의 PID Hash Table 자료구조 비교 (6)

ice_grad
ice_gradice_grad
ice_grad
 
리눅스 커널 기초 태스크관리
리눅스 커널 기초 태스크관리리눅스 커널 기초 태스크관리
리눅스 커널 기초 태스크관리
 
C#을 사용한 빠른 툴 개발
C#을 사용한 빠른 툴 개발C#을 사용한 빠른 툴 개발
C#을 사용한 빠른 툴 개발
 
Linux programming study
Linux programming studyLinux programming study
Linux programming study
 
PostGIS 시작하기
PostGIS 시작하기PostGIS 시작하기
PostGIS 시작하기
 
Windows via c++ chapter6
Windows via c++   chapter6Windows via c++   chapter6
Windows via c++ chapter6
 

Linux kernel 3.x와 2.6.11의 PID Hash Table 자료구조 비교

  • 1. Linux Kernel 3.x 과 2.6.11의 PID Hash Table Data Structures 비교 2014.12.29 양 희철 heecheol.yang@outlook.com 1. Introduction 마지막 3판이 나온 지 10년이 다 되어가는데도 Danidl P.Bovet & Marco Cesati 의 Understanding the Linux Kernel (이하 ULK)은 여전히 많은 사람들이 리눅스 커널을 정복하기 위해 읽는 책이다. 하지만 아무리 책 내용이 방대하고 자세하다고 하더라도 현재의 리눅스 커널 코드 내용과 10년 전의 코드 내용이 완전히 같은 수는 없다. 예를 들어, 리눅스의 프로세스 스케줄러는 이 책이 나온 이후에도 두 번이나 바뀌었다. 이 글에서는 달라진 점 중 하나로 PID Hash Table과 관련된 내용을 소개하고자 한다. 먼저, ULK 에서 말하는 리눅스 커널 버전 2.6.11의 PID Hash Table의 구현을 간략하게 소개 한 후, 현재의 리 눅스 커널은 이것들이 어떻게 바뀌었는가를 설명할 것이다. 2. PID Hash Table Description in ULK ULK의 Chapter 3: Process 의 ‘Relationship Among Processes’ 절에는 부모-자식 관계 등의 프로 세스 관계를 리눅스 커널이 설계 및 구현하고 있는지 기술되어 있다. 커널은 이런 여러 관계를 표현하기 위하여 Linked List 등의 다양한 자료구조를 활용하고 있는데, 특히 커널은 Process ID(PID)를 통해 Process descriptor를 빠른 속도로 검색해내기 위하여 PID를 키로 하는 Hash Table 을 유지하고 있고 이 Hash table을 PID Hash Table (pidhash)라고 불린다. ULK에서는 ‘The pidhash table and chained lists’ 절을 통해서 pidhash 와 관련된 Data structure들의 구조와 그 관계를 설 명하고, 그림을 통해 커널이 특정 PID를 기반으로 Process descriptor을 어떻게 검색하는지 예시를 보여주고 있다. 이 장에서는 ULK의 내용을 인용하여 ULK 가 설명하고 있는 리눅스 커널 2.6.11 에서는 pidhash가 어떻게 구현되어 있는지 간략하게 요약하고자 한다. 앞서 PID Hash Table은 PID를 키로 하여 PID에 대한 Processor Descriptor를 검색하기 위한 Hash Tabled라고 밝혔다. 또한, 2.6.11버전의 리눅스 커널은 4 가지 종류의 PID1 를 유지하고 있기 때문에, 각 종류별로 PID Hash Table이 존재하여야 한다 (그림 1). 따라서, 2.6.11 커널은 pid_hash 1 PID는 Processor ID를 의미하는 것이 맞지만, 여기서는 리눅스 커널에서 Process들의 관계를 나타내기 위해 사용하는 모든 ID, 즉 Process ID, Thread Group ID, Process Group ID, Session Group ID를 모두 PID로 표현하겠다. ULK 역시 같은 맥락으로 표현하고 있다.
  • 2. 는 전역 변수를 유지하며 Hash Table을 관리한다 (그림 2). 그림 1 Linux Kernel 2.6.11 에서의 PID 종류 (출처: ULK Chapter 3) kernel/pid.c #define pid_hashfn(nr) hash_long((unsigned long)nr, pidhash_shift) static struct hlist_head *pid_hash[PIDTYPE_MAX]; //PIDTYPE_MAX : 4 static int pidhash_shift; 그림 2 PID Hash Table의 구현 pid_hash 전역 변수는 struct hlist_head 포인터에 대한 배열로 선언되어 있다. 각 배열 원소는 2.6.11 커널에서 유지하는 4 가지 종류의 PID Hash Table을 가리키는 포인터이며, 부팅 시점에 커 널 초기화 과정에서 Hash Table 들의 크기가 결정되고, 각각 동적 배열로 할당된다(그림 3). kernel/pid.c void __init pidhash_init(void) { … for (i = 0; i < PIDTYPE_MAX; i++) { pid_hash[i] = alloc_bootmem(pidhash_size*sizeof(*(pid_hash[i]))); for (j = 0; j < pidhash_size; j++)INIT_HLIST_HEAD(&pid_hash[i][j]); } } 그림 3 PID Hash Table 초기화 과정 결국 각 PID Hash Table 은 struct hlist_head 의 배열이 되는데, 이 배열 원소 하나는 Hash Table Entry로서 pid_hashfn() 함수에 의해 Hashing 된 PID에 해당하는 Process Descriptor 리스트의 Head를 가리키게 된다2 . 한편, 실제로 2.6.11 커널에서 Hash Table들이 가리키는 리스트는 Process Descriptor의 리스트가 아닌 struct pid 의 리스트이다. 이 리스트는 Hash Collision의 처리를 위한 리스트와, 같은 PID로 이루어진 리스트를 함께 유지하기 위하여 존재한다 (그림 4, 그림 5). 2 여기서 PID Hash Table이 Processor Descriptor 하나가 아닌 List를 가리키는 이유는 PID 종류에 따라서 하나의 PID 에 대해 여러 Process가 검색될 수 있기 때문이다. 예를 들어, 단순히 Process ID Hash Table을 검색하면 그 결과물은 Process Descriptor 하나가 나오지만, Thread Group ID(TGID) Hash Table을 통한 검색의 결과는 해당 Thread Group에 속하는 Process 목록이 된다.
  • 3. include/linux/pid.h struct pid { int nr; struct hlist_node pid_chain; struct list_head pid_list; }; 그림 4 struct pid 구조체 구현 그림 5 struct pid 구조체 내 필드들의 역할 (출처: ULK Chapter 3) struct pid 를 노드로 하는 리스트는 nr 필드, 즉 같은 PID를 이루는 리스트 나타낸다. 그리고 PID는 4 가지 종류가 존재하므로 모든 Process Descriptor는 4 가지 PID에 struct pid 리스트를 유 지하여야 한다. 이를 위해 Process Descriptor 에는 struct pid 에 대한 배열인 pids 필드가 존재한 다 (그림 6). 이 배열의 각 원소는 각 종류의 PID 리스트를 가리키는 struct pid 에 해당한다. include/linux/sched.h struct task_struct { … struct pid pids[PIDTYPE_MAX]; … }; 그림 6 Process Descriptor의 pids 필드 ULK에서는 앞서 언급된 자료구조의 전체적인 관계를 보여주기 위하여 같은 TGID를 가지고 있 는 모든 프로세스를 찾는 예제를 그림으로 나타내었다 (그림 7). 만약 TGID가 4351번인 모든 Process들을 검색하고자 한다면, 우선 pid_hashfn() 함수를 통해 4351번에 대한 Hash 값인 70을 얻어낸다. 이 Hash 값은 TGID Hash Table의 배열 Index로 사용되는데, 이 배열은 전역 변수 pid_hash[PIDTYPE_TGID] 가 가리키고 있다. 한편, 예제에서는 Hash 70번이 4351번과 256번에 의하여 Collision이 발생하는데, 이 문제를 해 결하기 위하여 커널은 PID Hash Table이 가리키고 있는 struct pid의 pid_chain 필드와 nr 필드를 이용한다. 즉, 커널은 find__pid() 함수를 통해 PID Hash Table이 가리키고 있는 pid_chain 을 탐색 하면서 nr 값과 4351값을 비교하여 실제로 찾고자 하는 TGID 4351에 대한 리스트를 찾아낸다 (그 림 8). find_pid() 함수를 통해 원하는 struct pid를 찾았다면, 해당 구조체를 필드로 가지고 있는 Process Descriptor, 즉 4351 TGID를 가지는 Process 리스트의 첫 번째 Process Descriptor를 찾을 수 있다. 따라서 이후에는 struct pid 의 pid_list 필드를 통해 리스트를 탐색 할 수 있게 된다.
  • 4. 그림 7 예제: TGID Hash Table 검색 (출처: ULK Chapter 3) kernel/pid.c struct pid * fastcall find_pid(enum pid_type type, int nr) { struct hlist_node *elem; struct pid *pid; hlist_for_each_entry(pid, elem, &pid_hash[type][pid_hashfn(nr)], pid_chain) { if (pid->nr == nr) return pid; } return NULL; } 그림 8 find_pid() 함수의 동작
  • 5. 3. Linux Kernel 3.x 에서의 PID Hash Table 동작 구조 리눅스 커널이 발전해 나가면서 커널 소스 코드 내 많은 부분이 변경되어 왔고, PID Hash Table 역시 구조적으로 많은 변화가 발생하였다. 이 장에서는 구체적으로 어떠한 변화가 발생하였고, 이 로 인해 리눅스 커널 3.x 버전에서의 PID Hash Table 방식은 어떻게 바뀌었는지 구체적으로 기술 한다. 이 문서에서는 리눅스 커널 3.17.4 버전을 기준으로 한다. 가장 중요한 변화로는 pid_hash 전역 변수가 Process ID 의 Hash Table만 관리하도록 변경되었 다는 점이다 (그림 9)3 . 즉, 2.6.11 버전의 pid_hash는 PID 종류의 개수만큼 배열로 존재하던 것과 달리, 현재는 배열이 아닌 동적으로 할당된 PID Hash Table 하나만을 가리키는 포인터로 변경되었 다. kernel/pid.c #define pid_hashfn(nr, ns) hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift) static struct hlist_head *pid_hash; static unsigned int pidhash_shift = 4; 그림 9 pid_hash 전역변수의 변화 또한 pid_hash 전역변수가 단순히 Process ID에 대한 Hash Table만을 유지하게 되었기 때문에 PID 와 PID Type을 매개변수로 받아 PID Hash Table을 탐색하던 find_pid() 함수도 더 이상 PID Type은 받지 않고, PID 만 받아 검색하도록 변경되었다(그림 10). kernel/pid.c struct pid *find_pid_ns(int nr, struct pid_namespace *ns) { struct upid *pnr; hlist_for_each_entry_rcu(pnr, &pid_hash[pid_hashfn(nr, ns)], pid_chain) if (pnr->nr == nr && pnr->ns == ns) return container_of(pnr, struct pid, numbers[ns->level]); return NULL; } 그림 10 find_pid() 함수의 변경 find_pid() 함수의 변경내용 중 중요한 점은 pid_hash 가 가리키고 있는 자료구조가 struct pid 에서 새로 도입된 구조체인 struct upid 로 변경되었다는 점이다. 리눅스 커널 2.6.11 버전에서 find_pid() 함수가 Hash chain 을 탐색하기 위해 사용하던 pid_chain 필드는 struct pid 안에 있는 3 pid_hashfn()함수를 보면 nr 매개변수 외에 Namespace를 의미하는 ns 매개변수가 추가된 것을 볼 수 있다. Namespace는 Hash Table 개념과는 다른 주제이기 때문에, 이 문서에서는 추가적으로 언급하지 않겠 다.
  • 6. 필드였다. 하지만 3.x 버전의 리눅스 커널에서는 struct pid 구조체 내에 numbers 라는 필드명으 로 struct upid 구조체를 포함하도록 하였으며, 이로 인해 struct pid 의 중요한 필드들이 이 구조 체로 이동하였다 (그림 11). 변경된 내용으로 첫째, pid_chain 필드가 struct upid 내로 이동하였다. 때문에 find_pid() 함수는 struct upid 로 이루어져 있는 리스트를 탐색한다. 또한, struct pid 의 nr 필드 역시 struct upid 로 이동하였기 때문에, find_pid() 함수는 nr 값과 검색하고자 하는 PID 를 비교 시 struct upid 를 참 조한다. 하지만 find_pid() 함수는 여전히 struct pid 의 포인터를 반환하고 있기 때문에, 이 함수는 nr 값이 검색하고자 하는 값과 일치하는 struct upid 를 찾았으면 해당 구조체를 감싸고 있는 struct pid 의 주소를 반환하게 된다. include/linux/pid.h struct upid { int nr; struct pid_namespace *ns; struct hlist_node pid_chain; }; struct pid { atomic_t count; unsigned int level; struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; }; 그림 11 struct upid 의 구조와, struct pid 의 변화 한편, 리눅스 커널 2.6.11 버전에서는 Process Descriptor 의 pids 필드가 struct pid 의 배열이었 지만, 3.x 버전의 리눅스 커널에서는 pids 필드가 새로 도입된 구조체인 struct pid_link 로 변경되 었다 (그림 12, 그림 13). include/linux/sched.h struct task_struct { … struct pid_link pids[PIDTYPE_MAX]; … }; 그림 12 Process Descriptor의 pids 필드 변경 내용 include/linux/pid.h struct pid_link { struct hlist_node node; struct pid *pid; }; 그림 13 struct pid_link 의 구조 따라서 리눅스 커널 2.6.11 버전과 다르게 커널 3.x 버전에서는 Process descriptor가 struct pid
  • 7. 를 가지고 있는 것이 아니기 때문에 struct pid 가 주어졌을 때 이를 포함하는 Process Descriptor 를 찾아내는 방법 역시 달라졌고, 이는 pid_task() 함수에 구현되어 있다 (그림 14). kernel/pid.c struct task_struct *pid_task(struct pid *pid, enum pid_type type) { struct task_struct *result = NULL; if (pid) { struct hlist_node *first; first = hlist_first_rcu(&pid->tasks[type]), lockdep_tasklist_lock_is_held()); if (first) result = hlist_entry(first, struct task_struct, pids[(type)].node); } return result; } 그림 14 pid_task() 함수의 구현4 pid_task() 함수의 동작을 파악하기 위해서는 struct pid 에 새로 추가 된 필드인 struct hlist_head tasks[] 배열을 확인 할 필요가 있다. 이 배열은 PID 종류 개수만큼의 원소를 가지고 있 으며, 각각의 원소는 hlist_head 타입으로서 각각의 PID 종류에 대해 같은 PID를 가진 Process Descriptor의 리스트를 가리키고 있다. 예를 들어, 특정 Session ID를 가지고 있는 모든 Process 의 리스트를 확인하려면 tasks[PIDTYPE_SID] 를 참조하여야 한다. pid_tasks() 함수는 가장 먼저 주어진 struct pid 가 가리키고 있는 리스트, 즉 pid->tasks[type] 가 가리키는 리스트의 첫 번째 노드를 찾아 first 변수가 가리키도록 한다. 이 노드는 검색하고자 하는 Process Descriptor의 pid_link 가 포함하고 있는 노드이기 때문에 pid_hash() 함수는 이 노드 를 통해 찾고자 하는 Process Descriptor를 찾을 수 있다. 리눅스 3.x 버전에서 변경된 내용들을 정리하여 ULK에서 소개된 예제를 적용해 보면, 다소 구 조가 복잡해짐을 알 수 있다 (그림 15). 예를 들어 PGID가 246번인 프로세스의 목록을 검색하고 자 한다면, Hashing 된 값인 70번을 Index로 pid_hash Hash Table을 검색한다. 예제에서는 Collision이 발생하였으므로 nr 값을 비교하며 pid_chain 을 따라 Hash Chaining 리스트를 탐색하 면 찾고자 하는 struct upid 구조체를 찾을 수 있다. 원하는 struct upid 를 찾았으면, 이를 감싸고 있는 struct pid 구조체를 찾아내어야 한다. 이후 struct pid 내 tasks[PIDTYPE_PGID] 의 hlist_head 가 가리키고 있는 노드를 따라가면 그 노드를 포 함하고 있는 struct pid_link 및 Process Descriptor를 찾을 수 있게 된다. 한편, 해당 PID에 해당하 는 프로세스가 여러 개라면, pid 링크를 통해 모두 탐색 할 수 있을 것이다. 4 가독성을 위하여 함수 동작 파악에 관계가 없는 Locking 관련 내용은 코드에서 삭제하여 나타내었다.
  • 8. 그림 15 예제: PGID Hash Table 검색5 5 리눅스 커널 3.x 버전에서는 PIDTYPE_TGID가 사라진 것을 확인 할 수 있었다. 따라서 이 예제에서는 TGID 대신 PGID를 이용하였다. struct task_struct { … struct pid_link pids[PIDTYPE_MAX]; … } struct pid_link { struct hlist_node node; struct pid *pid; } struct pid { struct hlist_head tasks[PIDTYPE_MAX]; struct upid numbers[1]; } struct upid { int nr; struct hlist_node pid_chain; } struct hlist_head { struct hlist_node *first; } 700 2047 pid_hashfn(246,ns) struct hlist_head *pid_hash struct pid_link pids[PIDTYPE_PGID] node pid struct task_struct nr=4351 pid_chain struct upid numbers struct pid hlist_head tasks struct pid_link pids[PIDTYPE_PGID] node pid struct task_struct nr=4351 pid_chain struct upid numbers struct pid hlist_head tasks struct pid_link pids[PIDTYPE_PGID] node pid struct task_struct nr=4351 pid_chain struct upid numbers struct pid hlist_head tasks struct pid_link pids[PIDTYPE_PGID] node pid struct task_struct nr=246 pid_chain struct upid numbers struct pid hlist_head tasks
  • 9. 4. 결론 본 문서에서는 리눅스 커널 3.x 버전에서 PID Hash Table을 유지하는 방법이 어떻게 바뀌었는가 를 기술하였고, ULK 에서 기술한 2.6.11 버전 커널의 구현과 비교해 보았다. 10여년이 지난 코드인 만큼, 많은 내용들이 바뀌었으며, 다소 복잡해졌다. 특히, 실제 코드 구현적인 부분뿐만 아니라 설 계와 관련된 부분들도 변경 점이 있었기 때문에 더욱 복잡해진 것으로 보인다. 하지만 PID Hash Table 관리 방식의 설계가 왜 바뀌었는지는 확인하지 못하였다. 특히 이 문서 에서 기술한 내용은 이미 작성 된 소스 코드를 기준으로 그림을 그려나간 것이기 때문에 실제로 는 어떠한 의도로 설계를 하였는지 유추해 내기 힘들다. 이를 확인하기 위해서는 소스 코드 변경 이 언제, 어느 버전에서 이루어 졌는지 확인해 볼 필요가 있다. 버전 업데이트 시점을 확인한 후 해당 시점에서의 문서 업데이트나 메일링 리스트를 확인하면 좀 더 구체적인 내용을 확인 할 수 있을 것이다. 사족을 붙여 보자면, 커널 내 존재하는 PID 관련된 자료구조의 수를 줄여 메모리를 절약하기 위하여 설계가 변경된 것이 아닐까 추측된다. 2.6.11 버전에서는 각 PID 종류 별로 PID Hash Table 을 비롯한 여러 자료구조들을 각각 유지하고 있어야 했다. 하지만 3.x 버전에서의 struct pid 를 살펴보면, 하나의 nr 번호를 이용하여 tasks 배열을 통해 모든 종류의 PID를 관리할 수 있게 해 놓았다. 따라서 예를 들어, 2.6.11 버전에서는 같은 nr 번호 100 번이더라도 PID 종류에 따라 struct pid를 4 개 유지하여야 했으나 3.x 버전과 같은 방식에서는 nr=100 에 대한 struct upid 및 struct pid 는 한 개만 유지하고 tasks 배열을 통해 각 PID 종류 별 리스트만 관리를 하면 된다. 이러한 방식을 통해 리눅스 커널은 struct pid에 소모되는 메모리를 절약할 수 있을 것이라고 추 측 할 수 있을 것이다.