리눅스 커널 책을 보면 가장 먼저 프로세스에 대한 내용을 읽을 수 있습니다. 그 중에 프로세스 상태가 바뀌는 동작에 대해 혹시 잘 알고 있나요? 예를 들면 프로세스는 Ready 상태에 있다가 CPU를 점유하면 Running 상태로 바뀌고 Running 상태에서 CPU을 양보하면 Ready로 바뀌었다가 나중에 Sleep으로 바뀐다. 이런 방식입니다. 이렇게 프로세스 변화를 State Machine으로 표현한 다이어그램은 눈에 익숙하지 않나요?
이 개념에 대해 잘 알고 있다고 가정하겠습니다. 그럼 실무 프로젝트에서 이 내용을 활용해서 뭘 할 수 있죠? 커널 패닉이 나서 이 문제 해결 못하면 프로젝트 일정을 못 맞추는 상황에 뒷짐지고 “프로세스가 커널 패닉 전에 래디 상태였어. 그 전에는 슬립일꺼야. 왜냐고? 리눅스 커널 이론책에 그렇게 나와 있거던.” 이라고 어떤 동료가 뇌 깔리면 여러분들은 “감사합니다, 리눅스 커널에 대해서 아주 잘 아시는군요” 라고 대답하실 건가요? 이런 대답을 뒷짐 진 동료가 들으면 뿌듯해 하며 “문제 해결에 도움이 된 것 같으니, 전 이만 집에 가볼께요”라고 말할지도 모릅니다. 여러분들, 이런 헛똑똑이를 조심하세요. 개인적으로 전 이런 동료 만나면 정말로 머리통을 깨버리고 싶어요.
커널 관점으로 프로세스 상태를 여러 각도에서 볼 수 있습니다. 코끼리 다리 만지듯이 말이죠. 프로세스를 메모리 관점 그리고 현재 실행 중인 프로그램 입장에서 프로세스를 볼 수 있죠. 그래도 코끼리를 정면에서 본 각도로 프로세스 상태를 실무 관점으로 설명하려고 합니다.
프로세스 실행 상태를 정확히 알려면 런큐에 대해서 살펴봐야 합니다. 그럼, 런큐란 용어 들어보신 적 있나요? 런큐란 런+큐의 합성어로 “실행+큐”라고 말할 수 있어요. 실행하기 위한 큐죠. 잘 이해가 가시나요? 그럼 뭘 실행하기 위한 런큐인가요? 정답은 프로세스입니다.
커널에서 돌고 있는 모든 프로세스는 런큐에 등록해야만 실행할 수 있습니다. 그래서 커널에서 실행 중인 프로세스의 Running 즉 실행 상태를 설명할 때 런큐에 대해 잘 알아야 합니다.
런큐의 자료 구조에 대해서 잠깐 알아볼까요? 요즘에는 CPU를 4개 이상 탑재한 디바이스가 많습니다. 그럼 CPU가 4개면 런큐의 개수는 몇 개일까요? 1개인가요? 정답은 4개입니다. 그래서 런큐는 per-cpu 타입으로 구성돼 있습니다. 정확한 자료구조는 struct rq이니 시간 있는 분은 꼭 소스를 열어서 확인하시길 바래요. 이 멤버 변수들 아주 중요하거든요.
struct rq 구조체는 다음과 같이 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/sched/sched.h#L679]
1 struct rq {
2 /* runqueue lock: */
3 raw_spinlock_t lock;
4
5 unsigned int nr_running;
...
6 unsigned long nr_uninterruptible;
7
8 struct task_struct *curr, *idle, *stop;
9 unsigned long next_balance;
위 구조체 중 8번 째 줄 코드를 보면 curr와 idle 그리고 stop 이란 멤버를 볼 수 있습니다.
각각 멤버 타입은 struct task_struct 으로 프로세스 정보를 표현하는 태스크 디스크립터입니다.
이 중에 현재 런큐에서 구동 중인 프로세스는 curr 멤버로 확인할 수 있습니다.
소스 코드로 struct rq 멤버 변수를 확인했으니 이번에 실제 구동 중인 리눅스 시스템에서 추출한 vmcore에서 struct rq 값을 확인합시다.
다음은 런큐를 Trace32 프로그램으로 확인한 값입니다. 런큐는 runqueues 전역 변수로 관리하고 per-cpu 타입이므로 __per_cpu_offset[cpu] 만큼 주소 오프셋을 줘야합니다. 아래 경우 per-cpu2 런큐에 접근했습니다.
Trace32 프로그램으로 &runqueues + __per_cpu_offset[2] 방식으로 주소 계산을 해야 per-cpu2 변수에 접근할 수 있습니다.
1 (struct rq *) (struct rq*)(((void*)&runqueues)+__per_cpu_offset[2]) = 0xFFFFFFC63A339800 -> (
2 (raw_spinlock_t) lock = ((arch_spinlock_t) raw_lock = ((u16) owner = 39522 = 0x9A62, (u16) next
3 (unsigned int) nr_running = 1 = 0x1,
4 (long unsigned int [5]) cpu_load = (780 = 0x030C, 772 = 0x0304, 621 = 0x026D, 506 = 0x01FA, 435
...
5 (long unsigned int) nr_uninterruptible = 18446744073707675149 = 0xFFFFFFFFFFE35E0D,
6 (struct task_struct *) curr = 0xFFFFFFC5E9DB5580 -> (
7 (struct thread_info) thread_info = ((long unsigned int) flags = 2 = 0x2, (mm_segment_t) addr_l
8 (long int) state = 0 = 0x0,
9 (void *) stack = 0xFFFFFFC5E9D60000,
...
10 (struct cred *) real_cred = 0xFFFFFFC614D24B80,
11 (struct cred *) cred = 0xFFFFFFC614D24B80,
12 (char [16]) comm = "lowi-server",
13 (struct nameidata *) nameidata = 0x0,
6번 째 줄 디버깅 정보를 보면 curr이란 멤버 변수를 볼 수 있습니다. 이 변수 타입은 struct task_struct입니다.
struct task_struct 멤버 변수 중 comm에 접근하면 프로세스 이름을 알 수 있습니다.
12번 째 줄 정보를 보니 프로세스 이름이 "lowi-server"입니다.
이제 프로세스가 런큐에서 실행하기 시작한다고 가정 할께요. 실행 직전 struct rq->curr 멤버 변수로 프로세스를 등록합니다. 대부분 프로세스들이 실행할 때는 태스크 디스크립터에서 스택 Top 주소를 읽어와 스택 Top 주소에서 struct thread_info.cpu_context 멤버 변수에 저장된 레지스터를 로딩해서 동작을 시작합니다. 아 좀 내용이 어렵나요? 프로세스가 런큐에서 잘 실행하다고 있다가 스케줄될 때 struct thread_info.cpu_context 멤버 변수에 레지스터를 저장하고 다시 실행되기를 기다립니다.
다음 코드를 좀 살펴볼까요?
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
struct thread_info 구조체 선언부인데 천천히 코드를 보면 아래 struct cpu_context_save 타입의 cpu_context이란 멤버가 보일 겁니다.
이 cpu_context 멤버 선언부를 보면 역시나 레지스터 값들입니다. 이 멤버 변수에 현재 동작 중인 레지스터를 저장합니다. 그런데 이 값들은 스택 Top 주소 공간에 저장합니다. 계속 낯선 용어만 쓰는 것 같은데요. 나중에 스택 메모리 공간에서 프로세스가 어떻게 도는 지 상세히 짚어보겠습니다.
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};
1> Running State Process
프로세스가 실행되기 위해서는 뭘 해야 할까요. 런큐에 프로세스를 큐잉해야 해요. Runqueue에서 돌고 있는 프로세스를 Running 상태 프로세스라고 볼 수 있어요. 그럼 Crash Tool로 이 디버깅 정보를 확인할 수 있습니다.
아래 로그를 같이 살펴볼까요? 크래시 유틸리티에서는 runq란 명령어를 제공합니다. 런큐에서 실행 중인 프로세스와 런큐에서 대기 중인 프로세스 목록을 확인할 수 있습니다.
crash> runq
CPU 0 RUNQUEUE: c4b4cb80
CURRENT: PID: 163 TASK: df545e80 COMMAND: "irq/328-touch"
RT PRIO_ARRAY: c4b4cc60
[ 48] PID: 163 TASK: df545e80 COMMAND: "irq/328-lge_tou"
[ 49] PID: 4345 TASK: c6215e80 COMMAND: "irq/215-408000."
[ 49] PID: 5630 TASK: de70ee40 COMMAND: "irq/215-410000."
CFS RB_ROOT: c4b4cbf0
[120] PID: 28896 TASK: de7ec440 COMMAND: "kworker/0:2"
[120] PID: 7 TASK: e0051f80 COMMAND: "rcu_preempt"
[120] PID: 3 TASK: e0050a80 COMMAND: "ksoftirqd/0"
[100] PID: 30 TASK: df912a00 COMMAND: "kworker/0:1H"
CPU 1 RUNQUEUE: c4b5ab80
CURRENT: PID: 0 TASK: e0053480 COMMAND: "swapper/1"
RT PRIO_ARRAY: c4b5ac60
[no tasks queued]
CFS RB_ROOT: c4b5abf0
[120] PID: 5608 TASK: de7eaa00 COMMAND: "event_dispatche"
CPU 2 RUNQUEUE: c4b68b80
CURRENT: PID: 5625 TASK: c62639c0 COMMAND: "python"
RT PRIO_ARRAY: c4b68c60
[ 97] PID: 608 TASK: c2f2aa00 COMMAND: "DispSync"
CFS RB_ROOT: c4b68bf0
[120] PID: 4323 TASK: de7eee40 COMMAND: "kworker/2:0"
[120] PID: 16 TASK: e0055e80 COMMAND: "ksoftirqd/2"
CPU 3 RUNQUEUE: c4b76b80
CURRENT: PID: 0 TASK: e0053f00 COMMAND: "swapper/3"
RT PRIO_ARRAY: c4b76c60
[no tasks queued]
CFS RB_ROOT: c4b76bf0
[no tasks queued]
아래 프로세스들이 running 상태 프로세스인거죠. 각 CPU별로 돌고 있는 프로세스 목록을 볼 수 있죠.
CURRENT: PID: 163 TASK: df545e80 COMMAND: "irq/328-touch"
CURRENT: PID: 0 TASK: e0053480 COMMAND: "swapper/1"
CURRENT: PID: 5625 TASK: c62639c0 COMMAND: "python"
CURRENT: PID: 0 TASK: e0053f00 COMMAND: "swapper/3"
"irq/328-touch" 프로세스의 task descriptor를 볼 까요? struct task_struct.state란 멤버를 보면 프로세스의 상태를 볼 수 있는데요.
crash> task df545e80 | grep state
state = 0, //<<-- TASK_RUNNING
exit_state = 0,
아래 코드를 보면 TASK_RUNNING 메크로가 0이란 걸 알 수 있거든요.
[kernel/include/linux/sched.h]
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_DEAD 16
#define EXIT_ZOMBIE 32
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* in tsk->state again */
#define TASK_DEAD 64
스케줄러는 CFS 클래스와 RT(RunTime) 클래스 스케쥴러로 구분할 수 있는데요. 이 내용은 다른 세션에서 다루도록 하죠.
2> Ready State
크게 두 가지로 구분할 수 있어요. Waitqueue에 등록된 프로세스인데요.
프로세스가 잠시 동작을 멈추고 어떤 조건이 만족되기를 기다릴 때 웨이크 큐(Wait queue)에 등록합니다. 이후 특정 조건을 만족하여 Waitqueue에 등록된 프로세스를 누군가가 깨우게 되거든요. 이렇게 Waitqueue에 큐잉된 프로세스들을 Ready 상태라고 볼 수 있어요. 언제든 실행될 준비가 되어 있다는 거죠.
//CrashTool에서는 Waitqueue 명령어를 지원하거든요. 아래 명령어를 입력하면 WaitQueue List와 콜백함수를 볼 수 있어요.
WaitQueue에서 실행되기를 기다리는 다른 예로 mutex lock을 들 수 있어요. Mutex lock을 획득하려고 하는데 다른 프로세스가 이리 해당 Mutex Lock을 잡고 있을 때 비슷한 동작을 합니다.
우선 struct mutex.wait_list 해당 mutex lock 리스트에 등록을 하고 Waitqueue에 등록을 해요.
조금 후 Mutex Lock을 잡았던 프로세스가 Mutex Lock을 해제하면 Waitqueue에 등록된 프로세스는 깨어나서 다시 Mutex lock을 획득하고 다시 실행하죠.
아까 프로세를 Runqueue에 큐잉만 하면 바로 Runqueue을 선점하고 Running 상태로 실행할 수 있을까요? 사실 바로 그렇게 Ruqqueue을 선점한 후 동작하지는 않아요.
우선 아래 리스트로 등록을 하고 Runqueue에서 등록 중인 프로세스가 종료될 때 까지 기다리죠.
아래 예시를 볼까요?
crash> runq
CPU 0 RUNQUEUE: c4b4cb80
CURRENT: PID: 163 TASK: df545e80 COMMAND: "irq/328-touch"
RT PRIO_ARRAY: c4b4cc60
[ 48] PID: 163 TASK: df545e80 COMMAND: "irq/328-touch"
[ 49] PID: 4345 TASK: c6215e80 COMMAND: "irq/215-408000."
[ 49] PID: 5630 TASK: de70ee40 COMMAND: "irq/215-410000."
CFS RB_ROOT: c4b4cbf0
[120] PID: 28896 TASK: de7ec440 COMMAND: "kworker/0:2"
[120] PID: 7 TASK: e0051f80 COMMAND: "rcu_preempt"
[120] PID: 3 TASK: e0050a80 COMMAND: "ksoftirqd/0"
[100] PID: 30 TASK: df912a00 COMMAND: "kworker/0:1H"
여러 가지 프로세스들이 줄서서 기다리는 정보를 볼 수 있습니다.
CFS RB_ROOT에서 대기하고 있는 "kworker/0:2", "rcu_preempt", "ksoftirqd/0", "kworker/0:1H" 프로세스에 대한 정보를 어떻게 확인할 수 있는 지 한번 살펴볼까요?
c4b4cb80 CPU0의 런큐 주소를 통해 struct rq.cfs_tasks 멤버 주소를 확인해요.
crash> struct rq.cfs_tasks c4b4cb80
cfs_tasks = {
next = 0xe0051fcc,
prev = 0xe0050acc
}
오라, 5개의 링크드 리스트가 연결되어 있네요.
crash> list 0xe0051fcc
e0051fcc
de7ec48c
df912a4c
e0050acc
c4b4d060
struct task_struct.se.group_node의 오프셋은 0x4입니다.
crash> eval 0x38+0x14
hexadecimal: 4c
crash> struct -o task_struct.se
struct task_struct {
[0x38] struct sched_entity se;
}
crash> struct -o sched_entity.group_node
struct sched_entity {
[0x14] struct list_head group_node;
}
오라, 5개의 링크드 리스트에서 0x4c만큼 오프셋을 빼주면 아래와 같습니다.
e0051f80 = e0051fcc - 4c
de7ec440 = de7ec48c - 4c
df912a00 = df912a4c - 4c
e0050a80 = e0050acc - 4c
c4b4d014 = c4b4d060 - 4c
0xe0051fcc 주소에 링크트 리스트가 있으니 다음 명령어로 링크드 리스트를 파싱할 수 있습니다.
이 링크드 리스트에 연결된 리스트를 확인하는 겁니다.
(where)
crash> list 0xe0051fcc
e0051fcc
de7ec48c
df912a4c
e0050acc
c4b4d060
crash> task e0051f80
PID: 7 TASK: e0051f80 CPU: 0 COMMAND: "rcu_preempt"
struct task_struct {
state = 0x0,
stack = 0xe0074000,
usage = {
counter = 0x2
},
flags = 0x208040,
crash> task de7ec440
PID: 28896 TASK: de7ec440 CPU: 0 COMMAND: "kworker/0:2"
struct task_struct {
state = 0x0,
stack = 0xdd530000,
usage = {
counter = 0x2
},
crash> task df912a00
PID: 30 TASK: df912a00 CPU: 0 COMMAND: "kworker/0:1H"
struct task_struct {
state = 0x0,
stack = 0xdfb48000,
usage = {
counter = 0x2
},
flags = 0x4208060,
crash> task e0050a80
PID: 3 TASK: e0050a80 CPU: 0 COMMAND: "ksoftirqd/0"
struct task_struct {
state = 0x0,
stack = 0xe006c000,
usage = {
counter = 0x3
},
flags = 0x4208040,
위와 같이 계산 한 원리는,
런큐의 struct rq.cfs_tasks 멤버들은 CFS schedule에서 실행되기를 기다리는 프로세스들의 리스트인데, struct task_struct.se.group_node가 위치한 주소를 가르키고 있어요.
(상세 코드 리뷰는 다음 섹션에서 하려고 해요.)
crash> struct rq.cfs_tasks c4b4cb80
cfs_tasks = {
next = 0xe0051fcc,
prev = 0xe0050acc
}
그런데, 이 프로세스들의 상태를 살펴보면, struct task_struct.state값이 0이라 모두 RUNNING state입니다. 하지만 사실 런큐에서 실행 중인 프로세스는 아닙니다. 개념적으로는 Ready State라고 할 수 있습니다.
crash> ps e0051f80 de7ec440 df912a00 e0050a80
PID PPID CPU TASK ST %MEM VSZ RSS COMM
3 2 0 e0050a80 RU 0.0 0 0 [ksoftirqd/0]
7 2 0 e0051f80 RU 0.0 0 0 [rcu_preempt]
30 2 0 df912a00 RU 0.0 0 0 [kworker/0:1H]
28896 2 0 de7ec440 RU 0.0 0 0 [kworker/0:2]
I/O 디바이스에도 WaitQueue을 많이 쓰는데요. 정리하면 WaitQueue에 등록된 프로세스들은 Ready State 프로세스라고 보면 되요.
3> Sleep 상태
schedule() API를 호출한 다음 Interrupt을 받을 수 있는 상태의 프로세스를 Sleep 상태의 프로세스라고 볼 수 있어요.
Crash tool로는 아래 명령어로 확인 가능합니다.
crash> bt 286
PID: 286 TASK: df435940 CPU: 0 COMMAND: "jbd2/mmcblk0p29"
#0 [<c0bde87c>] (__schedule) from [<c02a05c8>]
#1 [<c02a05c8>] (kjournald2) from [<c014007c>]
#2 [<c014007c>] (kthread) from [<c0105f78>]
crash> ps 286
PID PPID CPU TASK ST %MEM VSZ RSS COMM
286 2 0 df435940 IN 0.0 0 0 [jbd2/mmcblk0p29]
이 정보들은 커널 코드를 수정해서 커널 로그로 확인할 수 있습니다. 다음 시간에는 이 내용을 다루도록 하겠습니다.
Uninterruptible 상태 프로세스에 대해서는 다음에 업데이트하려고 해요.
"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"
# Reference: For more information on 'Linux Kernel';
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1
디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2

최근 덧글