Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

051
637
415809


[리눅스커널] 프로세스: ps 명령어로 프로세스 목록 확인 4. 프로세스(Process) 관리

리눅스 커널은 그 내용이 방대하고 여러워서 단기간에 익히기 어렵습니다. 그런데 대부분 “프로세스”란 주제로 리눅스 커널 공부를 시작합니다. 안타깝게도 많은 분들이 프로세스에 대한 내용을 조금 읽다가 포기하는 경우가 많습니다. 그 이유는 프로세스를 설명하는 책이나 블로그에서 프로세스를 이론적으로만 접근하기 때문입니다. 그럼 어떻게 해야 프로세스를 빨리 익힐 수 있을까요?

프로세스에 익숙해지려면 먼저 리눅스 시스템에서 프로세스를 출력하는 명령어를 자주 입력하고 ftrace 로그에서 프로세스 관련 정보를 자주 봐야 합니다. 그래서 이번 장에서는 라즈베리 파이에서 터미널을 열어서 명령어를 입력하고 ftrace 로그로 프로세스 동작을 확인하겠습니다.

ps 명령어로 프로세스 목록 확인

먼저 라즈베리 파이에서 터미널을 실행한 후 다음과 같이 “ps –ely” 명령어를 입력합니다. 

root@raspberrypi:/home/pi# ps -ely
S   UID   PID  PPID  C PRI  NI   RSS    SZ WCHAN  TTY          TIME CMD
S     0     1     0  0  80   0  5956  6991 SyS_ep ?        00:00:02 systemd
S     0     2     0  0  80   0     0     0 kthrea ?        00:00:00 kthreadd
...
S  1000   867   517  0  80   0  7720 12887 poll_s ?        00:00:00 gvfsd-trash
S  1000   876   730  0  80   0 20084 12108 poll_s ?        00:00:07 lxterminal
S  1000   877   876  0  80   0  1324   590 unix_s ?        00:00:00 gnome-pty-helpe
S  1000   878   876  0  80   0  4028  1628 wait   pts/0    00:00:00 bash
S     0   886   878  0  80   0  3380  1908 poll_s pts/0    00:00:00 sudo
S     0   890   886  0  80   0  3076  1818 wait   pts/0    00:00:00 su

리눅스 시스템에서 프로세스 목록을 보려면 “ps” 명령어를 입력하면 됩니다.

---
터미널을 열고 “info ps”를 입력하면 ps 명령어의 의미를 알 수 있습니다.
-------
PS(1)                                   User Commands                                   PS(1)

NAME
       ps - report a snapshot of the current processes.

SYNOPSIS
       ps [options]

보다시피 ps는 리눅스 시스템에서 실행 중인 프로세스를 출력하는 명령어입니다. 리눅스 시스템에서 디버깅할 때 많이 쓰는 명령어이니 자주 활용합시다. 

그런데 ps 명령어를 쓰다 보니 다음과 같은 의문이 생깁니다. “ps 명령어를 입력하면 리눅스 커널 내부의 어떤 자료구조에 접근해서 전체 프로세스 정보를 출력할까?” 

리눅스 커널을 공부할 때 이처럼 사소한 내용에도 의문을 품는 것은 좋은 습관입니다. 질문에 대답하자면 init_task 전역변수를 통해 전체 프로세스 목록을 출력합니다.

리눅스 시스템에서 생성된 모든 프로세스(유저 레벨, 커널 스레드)는 init 프로세스를 표현하는 자료구조인 init_task의 전역변수 필드에 연결 리스트로 등록돼 있습니다. 이 연결 리스트를 순회하면서 프로세스 정보인 task_struct 구조체의 주소를 계산해 프로세스 정보를 출력합니다. 
---

이번에는 다른 방식으로 프로세스 목록을 확인해 봅시다. ps 명령어에 “-ejH”이란 옵션을 지정해서 다음과 같이 입력해봅시다. 

1 root@raspberrypi:/home/pi # ps -ejH
2   PID  PGID   SID TTY          TIME CMD
3    2     0     0 ?        00:00:00 kthreadd
4    4     0     0 ?        00:00:00   kworker/0:0H
5    6     0     0 ?        00:00:00   mm_percpu_wq
6    7     0     0 ?        00:00:00   ksoftirqd/0
...
7  17103     0     0 ?     00:00:00   kworker/1:1
8  17108     0     0 ?     00:00:00   kworker/u8:0
9     1     1     1 ?        00:00:02 systemd
10   94    94    94 ?        00:00:00   systemd-journal
11  127   127   127 ?        00:00:00   systemd-udevd
12  274   274   274 ?        00:00:00   systemd-timesyn

출력 결과를 보니 앞에서 본 프로세스 목록과는 다릅니다. 이 프로세스 목록은 부모 자식 프로세스 관계를 토대로 프로세스를 출력한 것입니다. 

즉, 4~6번째 줄에 보이는 “kworker/0:0H”, “mm_percpu_wq”, “ksoftirqd/0” 프로세스의 부모 프로세스는 3번째 줄에 있는 “kthreadd”입니다. 

pid가 2인 “kthreadd” 프로세스는 커널 공간에서만 실행하는 커널 프로세스를 생성하는 임무를 수행합니다. 위 출력 결과에서 4~8번째 줄에 있는 프로세스들은 같은 열로 정렬돼 있습니다. 이 목록에서 보이는 프로세스를 커널 스레드, 커널 프로세스라고 하며, 커널 공간에서만 실행됩니다. 

커널이 프로세스를 생성할 때는 프로세스에 고유의 정수형 ID 값을 부여합니다. 이를 PID(Process IDentifier)라고 합니다. PID는 리눅스 커널에서 pid_t라는 타입으로 <sys.types.h> 헤더 파일에 저장돼 있습니다.

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/types.h] 
typedef __kernel_pid_t pid_t;

[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/uapi/asm-generic/posix_types.h]
typedef int __kernel_pid_t;

각 선언부를 보면 pid_t는 __kernel_pid_t 형식으로 정의돼 있는데 __kernel_pid_t는 int 타입으로 지정돼 있습니다. pid_t는 int 형 타입입니다. 리눅스 커널에서는 프로세스가 생성될 때 int 형 ID인 PID를 프로세스에게 알려줍니다.


그렇다면 커널은 어떤 기준으로 PID를 프로세스에게 부여할까요? 기준은 간단합니다. PID를 증가시키면서 프로세스에게 부여합니다. 여러분이 은행에 가면 대기 번호를 받을 텐데, 대기 번호는 시간이 흐르면서 증가합니다. 마찬가지로 새로운 프로세스를 생성할 때 커널이 부여하는 PID 정숫값은 계속 증가합니다. 따라서 PID를 보면 어느 프로세스가 먼저 언제 생성됐는지 추정할 수 있습니다.

리눅스 시스템마다 생성하는 프로세스는 대부분 다릅니다. 하지만 리눅스에서 공통으로 커널이 생성하는 프로세스가 있는데 각각 다음과 같은 PID를 부여합니다.

swapper 프로세스: 0
init 프로세스: 1
kthreadd 프로세스: 2

앞에서 언급한 프로세스 외에 다른 일반 프로세스의 PID는 리눅스 시스템이 부팅한 후 바뀔 수 있습니다. 그런데 여기서 한 가지 의문이 생깁니다. PID는 유저 공간에서 어떻게 확인할 수 있을까요?

리눅스 시스템 프로그래밍을 할 때 getpid() 함수를 호출하면 프로세스의 PID를 읽어올 수 있습니다. 그럼 유저 공간에서 getpid() 함수를 호출하면 커널에서는 어떤 함수가 호출될까요? 유저 공간에서 getpid() 함수를 호출하면 이에 대응하는 시스템 콜 핸들러인 sys_getpid() 함수가 호출됩니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/softirq.c/kernel/sys.c
01 SYSCALL_DEFINE0(getpid)
02 {
03 return task_tgid_vnr(current);
04 }

이 함수의 03번째 줄을 보면 task_tpid_vnr() 함수에 현재 실행 중인 프로세스의 태스크 디스크립터 주소가 담긴 current를 인자로 삼아 호출해 PID를 읽습니다. 

이번에는 9번째 줄 로그를 봅시다. pid가 1인 systemd 프로세스가 보입니다.

9     1     1     1 ?        00:00:02 systemd

pid가 1인 프로세스를 임베디드 리눅스에서는 init 프로세스라고 하며 모든 유저 공간에서 생성된 프로세스의 부모 프로세스 역할을 수행합니다.

이해를 위해 'ps -ejH' 명령어의 출력 결과를 다시 보겠습니다. 

1 root@raspberrypi:/home/pi # ps -ejH
2   PID  PGID   SID TTY          TIME CMD
...
9     1     1     1 ?        00:00:02 systemd
10   94    94    94 ?        00:00:00   systemd-journal
11  127   127   127 ?        00:00:00   systemd-udevd
12  274   274   274 ?        00:00:00   systemd-timesyn

9번 째 줄에 PID가 1인 systemd 프로세스가 보입니다. 다음 10~12번째 줄에 있는 systemd-journal부터 systemd-timesyn 프로세스까지는 systemd 프로세스의 자식 프로세스입니다.


systemd는 라즈비안에서 모든 프로세스들을 관리하는 init 시스템 및 프로세스입니다. 그런데 대부분의 리눅스 배포판에서는 PID가 1인 프로세를 init이라고 부릅니다.


각 프로세스는 저마다 부모 자식 프로세스들이 있습니다. 자식 프로세스가 종료될 때 부모 프로세스에게 신호를 알립니다. 조부모, 부모, 자식 프로세스가 있다고 가정했을 때 예외 상황으로 부모 프로세스가 종료되면 자식 프로세스 입장에서는 부모 프로세스가 사라집니다. 이 때 조부모가 부모 프로세스가 되며, 대부분 init 프로세스가 조부모 역할(새로운 부모 프로세스)을 수행합니다.  

이번 절에서는 ps 명령어로 프로세스 목록을 확인했습니다. 이 명령어는 임베디드 리눅스를 개발할 때 가장 자주 쓰는 명령어이므로 사용법과 출력 결과를 눈에 익혀 둡시다.

* 유튜브 강의 동영상도 있으니 같이 들으시면 좋습니다. 




#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  

"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2




핑백

덧글

  • 따뜻한 순록 2021/06/10 16:01 # 답글

    Thanks for thee nice explanation.
    Referred to your comment to print the entire process using int_task global variable.
    -----------------------------------------------------------------------------------------------------------------------------------------
    struct task_struct *task= &init_task;

    do {
    printk(KERN_ALERT "[ Process = %s] [PID = %d] [Parent = %s] n",task->comm, task->pid, task->parent->comm);
    } while ( (task = next_task(task)) != &init_task );
    --------------------------------------------------------------------------------------------------------------------------------------------
  • AustinKim 2021/06/12 10:41 #

    Great debugging code!
    Have a nice day.
댓글 입력 영역