Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


4.2 프로세스 확인하기 4장. 프로세스 관리

리눅스 커널은 그 내용이 방대하고 여러워서 단기간에 익히기 어렵습니다. 그런데 대부분 “프로세스”를 시작으로 리눅스 커널을 공부를 시작합니다. 안타깝게도 프로세스에 대한 내용을 조금 읽다가 대부분 포기합니다. 그 이유는 프로세스를 설명하는 도서나 블로그에서 프로세스를 이론으로 설명하기 때문입니다.

   "그러면 어떻게 해야 프로세스를 빨리 익힐 수 있을까?"

프로세스에 익숙해지려면 먼저 리눅스 시스템에서 프로세스를 출력하는 명령어를 자주 입력하고 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 전역 변수 필드에 연결 리스트로 등록돼 있습니다. 이 연결 리스트를 순회하면서 프로세스 정보인 struct 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번째 줄에 있는 프로세스들은 같은 행으로 정렬돼 있습니다. 이 목록에서 보이는 프로세스를 커널 스레드, 커널 프로세스라고 합니다. 커널 공간에서만 실행합니다. 

이번에는 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 프로세스의 자식 프로세스입니다.

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

PID에 대해서
커널이 프로세스를 생성할 때 프로세스에게 고유의 정수형 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://github.com/raspberrypi/linux/blob/rpi-4.19.y/kernel/sys.c]
01 SYSCALL_DEFINE0(getpid)
02 {
03 return task_tgid_vnr(current);
04 }

위 함수 03번째 줄 코드를 보면 task_tpid_vnr() 함수에 현재 실행 중인 프로세스의 태스크 디스크립터 주소를 담고 있는 current 를 인자로 호출해 PID를 읽습니다. 

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


"혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주시면 아는한 성실히 대답해드리겠습니다!"


Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 




핑백

덧글

댓글 입력 영역