Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

0107
469
422726


Process 프로세스 상태 (1) - 런큐(Runqueue) 디버깅 4. 프로세스(Process) 관리

리눅스 커널 책을 보면 가장 먼저 프로세스에 대한 내용을 읽을 수 있습니다. 그 중에 프로세스 상태가 바뀌는 동작에 대해 혹시 잘 알고 있나요? 예를 들면 프로세스는 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




    핑백

    덧글

    • 컴알못컴공 2019/10/03 18:38 # 삭제 답글

      정말 잘 정리 해놓으셨네요 많은 도움 되었습니다.
    • AustinKim 2019/10/03 22:57 #

      도움이 됐다니 기쁘군요. :)
      감사합니다.
    • 딩굴딩굴냠 2020/09/30 08:42 # 삭제 답글

      Austin kim님책을 보면 중간중간 crash를 이용해서 설명을 해주시네요 ..
      위 해설을 보면 crash에 연결하여 runqueue등의 주소를 확인하고 있네요 .. 실제 동작하는 기기를 jtag으로 연결하신건지요?
      아니면 crash를 연결하기 위한 kgdb ? gdb를 쓰신건지요?
      또다른 포스팅을 보면 ramdump를 떠서 분석하는 내용이 있는데 램덤프를 할수 있는 방법이 따로 있는지요?

    • AustinKim 2020/09/30 09:50 #

      주신 질문에 대해, 댓글로 답신드립니다.

      Q: 실제 동작하는 기기를 jtag으로 연결하신건지?
      ❑ 아닙니다. TRACE32 프로그램을 시뮬레이터 모드로 활용해 실행했습니다.

      Q: 램덤프를 할수 있는 방법이 따로 있는지?
      ❑ 특정 SoC에서는 크래시 발생하면 램덤프를 추출할 수 있는 툴을 제공합니다.
      이 램덤프를 TRACE32 시뮬레이터로 로딩해 디버깅을 하는 겁니다.

      ❑ 혹은 리눅스 커널에서 제공하는 기능을 활용해 vmcore(램덤프에 대응)를 추출할 수도 있습니다.
      이 경우 크래시 유틸리티라는 오픈 소스 프로그램을 사용해 디버깅합니다.

      추가로 궁금한 점이 있으면 댓글로 질문 주시면 됩니다.
      추석 잘 보내세요.


    • 2020/09/30 08:53 # 답글 비공개

      비공개 덧글입니다.
    • 딩굴딩굴냠 2020/09/30 09:47 # 삭제 답글

      벌써 추석에 코로나라 집에서 딩굴딩굴대다 설겆이 하고 왔는데 빠른 답변이 달렸네요 감사합니다.
      쓰신 책을 지금 읽고 있는데 trace32와 ftrace를 사용하여 설명하고 계시더군요. 다른글을 보니 여러사람이 읽고 실습을 하기 위한 환경에 대한
      고민을 많이 하신것 같습니다. trace32가 라이센스가 필요하고 최근에는 실제 단말을 연결해야 시뮬레이터를 쓸수 있어서 제약이 많네요
      그렇다고 독자들보고 옛날 버젼을 구해서 보라고 하기도 힘들고 .. 그래서 제가 생각한것이 램덤프와 crash툴인데 이것도 난관이 많네요
      램덤프를 위해 kexec로 2nd kernel을 올려 덤프를 뜨거나 부트로더에서 덤프 기능을 구현해야 할지모르겠네요 정보찾기가 쉽지 않네요
      아무튼 빠른 답변 감사합니다.
    • AustinKim 2020/09/30 09:55 #

      저도 TRACE32의 라이센스 때문에 많은 고민을 했는데요.
      사실 이 책의 대부분 실습은 ftrace를 활용한 부분이 많은데, 리눅스 커널을 이해하는데 TRACE32보다 더 많은 도움을 줄 것이라 생각했습니다.

      그리고 TRACE32의 시뮬레이터를 실행하는 방법은 제가 만든 유튜브 동영상을 한 번 참고하셔도 좋을 것 같습니다.
      https://www.youtube.com/watch?v=WBn4gD86rv8

      그럼, 즐거운 추석 보내세요.
    • 2020/09/30 09:55 # 답글 비공개

      비공개 덧글입니다.
    댓글 입력 영역