Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

110187
803
94439


4.7.3 프로세스 간 관계/4.7.4 프로세스 연결 리스트 4장. 프로세스 관리

4.7.3 프로세스 간 관계
이전 시간에 유저 공간에서 생성한 모든 프로세스의 부모 프로세스는 init 이고 커널 공간에서 생성한 커널 스레드(프로세스)의 부모 프로세스는 kthreadd라고 했습니다. 태스크 디스크립터에서는 프로세스의 부모와 자식 관계를 상세히 알 수 있습니다.

struct task_struct  *real_parent;

프로세스를 생성한 부모 프로세스의 태스크 디스크립터 주소를 저장합니다.

struct task_struct *parent;

부모 프로세스를 의미합니다. real_parent 란 멤버는 해당 프로세스를 생성해준 프로세스를 의미합니다. 그런데 자식 프로세스 입장에서 부모 프로세스가 소멸된 경우 부모 프로세스를 다른 프로세스로 지정합니다. 프로세스 계층 구조에서 지정한 부모 프로세스가 없을 경우 init 프로세스를 부모 프로세스로 변경합니다.

이 동작은 다음 그림과 같은 코드 흐름에서 수행합니다.
 

예외 상황으로 부모 프로세스가 종료되면 do_exit() 함수에서 화살표 방향으로 함수를 호출합니다. 함수 이름과 같이 forget_original_parent() 함수와 find_new_reaper() 에서 새로운 부모 프로세스를 지정합니다.

커널 스레드의 경우 자신을 생성한 프로세스가 종료하지 않고 계속 실행 중이면 real_parent와 parent가 같습니다.

struct list_head children;

부모 프로세스가 자식 프로세스를 생성할 때 children이란 연결 리스트에 자식 프로세스를 등록합니다.

struct list_head sibling;

같은 부모 프로세스로 생성된 프로세스 연결 리스트입니다. 단어가 의미하는 바와 같이 형제 관계 프로세스입니다.

위 children와 sibling 멤버가 어떤 방식으로 연결됐는지 조금 더 구체적으로 알아봅시다. 이를 위해 라즈베리파이에서 다음 명령어로 부모와 자식 프로세스 관계를 확인해 봅시다.
root@raspberrypi:/home/pi/dev_raspberri# ps axjf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
    2     4     0     0 ?           -1 I<       0   0:00  \_ [kworker/0:0H]
    2     6     0     0 ?           -1 I<       0   0:00  \_ [mm_percpu_wq]
    2     7     0     0 ?           -1 S        0   0:00  \_ [ksoftirqd/0]

출력 결과 kworker/0:0H, mm_percpu_wq 그리고 ksoftirqd/0 프로세스들의 부모 프로세스는 kthreadd 임을 알 수 있습니다. 각 프로세스들 PPID(Parent Process PID) 항목을 보면 모두 2입니다. 커널 스레드를 생성하는 kthreadd 프로세스의 pid는 2입니다.

리눅스를 탑재한 대부분 시스템(안드로이드, 라즈베리파이)에서 kthreadd 프로세스의 PID는 2입니다.
부모 프로세스인 kthreadd 입장에서 태스크 디스크립터는 다음 관계로 구성돼 있습니다.
 

태스크 디스크립터 관점으로 이 구조를 살펴봅시다.

“kthreadd” 프로세스 태스크 디스크립터 children 멤버는 연결 리스트입니다. 연결 리스트 해드에 등록된 자식 프로세스의 struct task_struct.sibling 멤버 주소를 저장합니다.

 “kthreadd” 프로세스의 자식 프로세스인 “kworker/0:0H” 입장에서 “mm_percpu_wq”와 “ksoftirqd/0” 프로세스는 sibling이란 연결 리스트로 연결돼 있습니다. 같은 부모 프로세스에서 생성된 프로세스이기 때문입니다.

조금 더 이해를 돕기 위해 Trace32로 위 자료구조를 분석해 봅시다.
다음 디버깅 정보는 kthreadadd 프로세스의 태스크 디스크립터입니다.
1  (struct task_struct *) (struct task_struct*)0xF1618740 = 0xF1618740 =  -> (
2   (long int) state = 1 = 0x1,
3    (void *) stack = 0xF1606000,
4    (atomic_t) usage = ((int) counter = 2 = 0x2),
...  
5    (struct task_struct *) parent = 0xC1A171B8
6    (struct list_head) children = (
7      (struct list_head *) next = 0xF161926C  
8        (struct list_head *) next = 0xF161A0EC  
9          (struct list_head *) next = 0xF161A82C 

7번째 줄 정보와 같이 children이란 연결 리스트 next 멤버는 0xF161926C 주소를 저장하고 있습니다.

0xF161926C 주소는 자식 프로세스의 struct task_struct 구조체에서 sibling 멤버 오프셋 만큼 떨어진 주소를 가르킵니다. struct task_struct 구조체에서 sibling 멤버 오프셋은 0x3EC 이므로 다음 계산으로 태스크 디스크립터 주소를 얻을 수 있습니다.
0xF1618E80 = 0xF161926C - 0x3EC

다음 명령어를 입력해서 태스크 디스크립터를 확인합시다.
v.v %l %t %h %s (struct task_struct*)(0xF161926C-0x3EC)
1  (struct task_struct *) [-] (struct task_struct*)(0xF161926C-0x3EC) = 0xF1618E80
2    (long int) [D:0xF1618E80] state = 0x1,
3    (void *) [D:0xF1618E84] stack = 0xB1630000,
4    (atomic_t) [D:0xF1618E88] usage = ((int) [D:0xB1618E88] counter = 0x3),
5    (unsigned int) [D:0xF1618E8C] flags = 0x04208040,
... 
6    (struct list_head) [D:0xF161926C] sibling = (
7      (struct list_head *) [D:0xF161926C] next = 0xF161A0EC,
8      (struct list_head *) [D:0xF1619270] prev = 0xF1618B24),
...
9    (char [16]) [D:0xF1619340] comm = "kworker/0:0H",

먼저 9번째 줄 디버깅 정보를 보면 프로세스 이름이 "kworker/0:0H"임을 알 수 있습니다.

2번째 줄 디버깅 정보를 보면 [D:0xF1618E80] 이란 주소가 보입니다.
이는 해당 멤버가 위치한 주소입니다.

struct task_struct 구조체 시작주소는 0xF1618E80이며, state 멤버는 0xF1618E80, stack은 0xF1618E84 주소에 위치합니다.

6번째 줄 디버깅 정보를 보면 sibling 이란 멤버가 위치한 주소인 0xF161926C 가 보입니다.
kthreadadd 프로세스의 태스크 디스크립터 children.next 멤버가 저장한 주소입니다.
6    (struct list_head) children = (
7      (struct list_head *) next = 0xF161926C =  -> (
8        (struct list_head *) next = 0xF161A0EC =  -> (

이번에는 "kworker/0:0H" 프로세스의 태스크 디스크립터 sibling 멤버가 가르키는 주소를 점검합시다.
1  (struct task_struct *)[-](struct task_struct*)(0xF161926C-0x3EC)= 0xF1618E80 -> (
2    (long int) [D:0xF1618E80] state = 0x1,
3    (void *) [D:0xF1618E84] stack = 0xB1630000,
4    (atomic_t) [D:0xF1618E88] usage = ((int) [D:0xB1618E88] counter = 0x3),
5    (unsigned int) [D:0xF1618E8C] flags = 0x04208040,
... 
6    (struct list_head) [D:0xF161926C] sibling = (
7      (struct list_head *) [D:0xF161926C] next = 0xF161A0EC,
8      (struct list_head *) [D:0xF1619270] prev = 0xF1618B24),

7번째 줄 디버깅 정보를 보면 연결 리스트인 sibling.next는 0xF161A0EC를 저장합니다.
이 주소는 형제 프로세스의 struct task_struct 구조체 sibling 멤버가 위치한 주소입니다.

0xF161A0EC 주소에서 struct task_struct 구조체 sibling 멤버 오프셋을 빼서 태스크 디스크립터를 봅시다.
v.v %l %t %h %s (struct task_struct*)(0xF161A0EC-0x3EC)
  (struct task_struct *)[-](struct task_struct*)(0xF161A0EC-0x3EC) = 0xF1619D00 -> (
    (long int) [D:0xF1619D00] state = 0x1,
    (void *) [D:0xF1619D04] stack = 0xF1634000,
    (atomic_t) [D:0xF1619D08] usage = ((int) [D:0xF1619D08] counter = 0x2),
    (unsigned int) [D:0xF1619D0C] flags = 0x04208060,
...
    (pid_t) [D:0xF161A0D0] pid = 0x5,
    (struct list_head) [D:0xF161A0EC] sibling = (
      (struct list_head *) [D:0xF161A0EC] next = 0xF161A82C,
      (struct list_head *) [D:0xF161A0F0] prev = 0xF161926C),
...
    (char [16]) [D:0xF161A1C0] comm = "kworker/0:0H",
...

태스크 디스크립터 정보를 볼 수 있습니다.
4.7.4 프로세스 연결 리스트
struct task_struct 멤버 중 tasks는 struct list_head 구조체로 연결 리스트입니다.

리눅스 커널에서 실행 중인 프로세스는 init_task.tasks 이란 연결 리스트로 등록되어 있습니다. 프로세스는 처음 생성될 때 init_task.tasks 이란 연결 리스트에 등록합니다.

이 동작은 copy_process() 함수에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L1539]
1 static __latent_entropy struct task_struct *copy_process(
2 unsigned long clone_flags,
3 unsigned long stack_start,
4 unsigned long stack_size,
5 int __user *child_tidptr,
6 struct pid *pid,
7 int trace,
8 unsigned long tls,
9 int node)
10 {
11 struct task_struct *p;
12 p = dup_task_struct(current, node);
...
13 list_add_tail_rcu(&p->tasks, &init_task.tasks);

12번째 줄 코드와 같이 태스크 디스크립터를 할당한 다음, 13번째 줄 코드에서 init_task.tasks 연결 리스트 마지막 노드에 현재 프로세스의 struct task_struct.tasks를 등록합니다.

임베디드 디버거의 전설인 Trace32 프로그램으로 태스크 디스크립터에서 프로세스 연결 리스트를 어떻게 관리하는지 점검합시다. 소스 코드 보다 코어 덤프를 통해 리눅스 커널 전역 변수를 직접 분석할 때 더 많은 것을 얻을 때가 있습니다.

다음은 init_task이란 전역 변수를 Trace32로 확인한 결과입니다.
v.v %l %t init_task
1  (static struct task_struct) [D:0xA1A171B8] init_task = (
2    (long int) [D:0xA1A171B8] state = 0,
3    (void *) [D:0xA1A171BC] stack = 0xA1A00000,
...
4    (struct sched_info) [D:0xA1A174A8] sched_info = ((long unsigned int) pcount = 0,
5    (struct list_head) [D: 0xA1A174C8] tasks = (
6      (struct list_head *) [D:0xA1A174E8] next = 0xA1618310 -> (
7        (struct list_head *) [D:0xA1618330] next = 0xA1618A70,
8        (struct list_head *) [D:0xA1618334] prev = 0xA1A174E8),
9      (struct list_head *) [D: 0xA1A174CC] prev = 0xA7778330),

init_task.tasks 멤버 구조체는 struct list_head와 연결 리스트이며 6~7번째 줄 디버깅 정보와 같이 next 멤버를 가르키고 있습니다. init_task 이란 전역 변수는 struct task_struct 구조체와 같이 태스크 디스크립터란 점을 기억합시다.

6번째 줄 로그에서 init_task.tasks.next는 0xA1618330 주소를 가르키고 있습니다. 이 주소는 어떤 의미일까요? init_task.tasks 이란 연결 리스트에 추가된 다음 프로세스의 struct task_struct.tasks 주소를 가르킵니다.

그림으로 표현하면 다음과 같습니다.
 
"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려 드리겠습니다!"


#Reference(프로세스 관리)
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


#Reference 시스템 콜

Reference(워크큐)
워크큐(Workqueue) Overview


핑백

덧글

댓글 입력 영역