안녕하세요.
유저 프로세스와 커널 프로세스에 전반전 내용은 끝내고 이제 중요한 구조체 2개(태스크 디스크립터(task_struct 구조체), 스레드 정보(thread_info 구조체))를 확인하려고 합니다.
그중에 태스크 디스크립터(task_struct 구조체) 먼저 확인해 보겠습니다.
태스크 디스크립터(task_struct 구조체)에는 많은 필드가 있어서 책에서 소개하는 정도만 따라가 보겠습니다.
struct task_struct 정의한 내용만 봐도 참 따라가기 쉽지 않겠구나 생각이 듭니다.
1. 프로세스 식별 필드
ps를 통해 systemd, kthreadd, rcu_gp 등 프로세스 이름을 확인할 수 있습니다.
이것은 task_struct의 comm 필드에 접근해서 가져옵니다.
linux# ps -ely
현재 실행 중인 프로세스의 태스크 디스크립터 구조체인 task_struct에 접근하는 current 매크로와 프로세스 이름을 저장하는 comm 필드를 조합해서 간단한 디버깅 메시지를 출력해 봅니다.
core.c 파일 ttwu_do_wakeup() 함수에 내용을 추가합니다. 함수의 인자인 task_struct *p가 깨우려는 프로세스의 태스크 디스크립터입니다. 여기서 comm 필드를 통해 프로세스의 이름을 알 수 있습니다.
linux# vim kernel/sched/core.c
linux# ../build_rpi_kernel.sh
linux# ../install_rpi_kernel_img.sh
linux# reboot
linux# dmesg | grep ==== -A 20 | head -n 20
그리고, 프로세스를 식별하는 정수형 값 pid와 스레드의 그룹 아이디가 있습니다. 해당 프로세스가 스레드 리더인 경우는 tgid와 pid가 같고, 자식 스레드인 경우는 tgid와 pid가 다릅니다.
pid_t pid;
pid_t tgid;
2. 프로세스 상태 저장 필드
1) 프로세스 실행 상태
프로세스 실행 상태를 저장하는 필드는 volatile long state; 로 프로세스 상태 정의는 linux# vim include/linux/sched.h 에서 확인할 수 있습니다.
TASK_RUNNING : CPU에서 실행 중이거나 런큐에서 대기 상태에 있음
TASK_INTERRUPTIBLE : 휴먼 상태
TASK_UNINTERRUPTIBLE : 특정 조건에서 깨어나기 위해 휴먼 상태로 진입한 상태
프로세스들은 대부분 TASK_INTERRUPTIABLE 상태이며, 나머지 상태가 비정상적으로 많으면 시스템에 문제가 있을 수 있는 경우가 많습니다.
2) 프로세스 세부 동작 상태
프로세스 세부 동작 상태와 속성 정보를 저장하는 필드는 unsigned int flags;로 PF_로 시작하는 매크로 필드를 OR 연산한 결과를 저장합니다.
실행 상태 정의는 sched.h 에서 확인할 수 있습니다. 더 만은 상태가 있지만 몇 가지만 알아보겠습니다.
linux# vim include/linux/sched.h
프로세스의 flags 필드에 저장된 값으로 프로세스의 세부 실행 상태를 알 수 있습니다.
PF_IDLE : 작업이 없거나 입력 대기 상태
PF_EXITING : 프로세스가 종료 중인 상태
PF_EXITPIDONE : 프로세스가 종료를 마무리한 상태
PF_KTHREAD : 프로세스가 커널 스레드인 경우
3) 프로세스 종료 상태
프로세스 종료 상태를 저장하는 int exit_state; 로
아래 매크로 중 하나의 값을 저장합니다.
linux# vim include/linux/sched.h
4) 프로세스 종료 코드
프로세스의 종료 코드를 저장 필드는 int exit_code; 로 do_exit() 함수에서 저장합니다.
linux# vim include/linux/exit.c
3. 프로세스 간의 관계
커널에서 프로세스는 다양한 방식으로 서로 연결되어 있고, 프로세스 사이의 관계를 나타내는 필드를 확인합니다.
유저 공간에서 생성한 프로세스의 부모는 대부분 init이고 커널 공간에서 생성한 프로세스(스레드)의 부모는 kthreadd 프로세스입니다.
struct task_struct *real_parent; 는 자신을 생성한 부모 프로세스의 태스크 디스크립터 주소를 저장합니다.
struct task_struct *parent; 는 부모 프로세스의 태스크 디스크립터 주소를 담고 있습니다. 일반적으로는 동일합니다. 그러나 부모 프로세스가 소멸 혹은 없을 경우 init 프로세스를 부모로 변경합니다.
프로세스 간의 관계를 저장하는 children과 sibling 필드로 확인합니다. ps명령어로 프로세스의 부모와 자식 관계를 확인할 수 있습니다.
rcu_gp, rcu_par_gp, mm_percpu_wq 등 프로세스들 확인할 수 있으며, PPID(Parent Process ID)가 모두 2인 것으로 볼 때 부모 프로세스는 kthreadd임을 알 수 있습니다.
linux# ps axjf
부모 프로세스의 kthreadd 입장에서 태스크 디스크립터는 아래와 같이 구성됩니다.
kthread프로세스 태스크 디스크립터의 children 필드는 연결 리스트이고 여기에는 등록된 자식 프로세스의 task_struct 구조체의 sibling 필드 주소를 저장합니다.
그러나 kthread 입장에서 rcu_gp는 자식 프로세스이지만, rcu_gp입장에서 rcu_par_gp, mm_parcpu_wq 등은 같은 부모에서 생성된 프로세스 이므로 자신의 sibling 연결 리스트로 이어져 있습니다.
4. 프로세스 연결 리스트
task_struct구조체의 tasks 필드는 list_head 구조체로 연결리스트 타입입니다. 커널에서 구동하는 모든 프로세스는 이 tasks에 등록되어 연결됩니다. 그렇다면 언제 init 프로세스의 태스크 디스크립터 tasks 필드에 저장되는지 확인합니다.
이전 시간에 본 fork() 함수 분석할 때 copy_process() 함수도 보았을 것입니다.
여기에 힌트가 있습니다.
1738줄은 커널로부터 태스크 디스크립트를 할당받습니다. 2055줄에서 init.tasks 연결 리스트의 마지막 노드에 현재 프로세스의 task_struct 구조체의 tasks 주소를 등록합니다.
linux# vim kernel/fork.c
이 부분은 책에서 TRACE32를 사용해서 좀 더 연결되는 것을 보여주고 있는데 여기서는 실제 사용할 수 없기에 책 내용을 참고하시면 됩니다. 참고로 책에 나온 연결 리스트 그림을 첨부합니다.
핵심적으로 알고 있어야 하는 것은 [1], [2]이라고 표시(tasks.next)의 의미는 연결리스트에 등록된 다음 프로세스 태스크 디스크립터의 next 필드 주소를 나타냅니다. 이와 같은 방식으로 모든 프로세스의 태스크 디스크립터 주소를 알 수 있습니다.
5. 프로세스 실행 시각 정보
프로세스의 실행 시각 정보를 알 수 있는 필드들이 있습니다.
u64 utime; 필드는 유저모드에서 프로세스가 실행한 시간을 나타냅니다. 이 필드는 account_user_time() 함수의 121번째에서 값이 저장됩니다.
linux# vim kernel/sched/cputime.c
u64 stime; 필드는 커널 모드에서 프로세스가 실행한 시각을 저장합니다. 이 필드는 account_system_index_time() 함수에서 변경됩니다.
struct sched_info.last_arrival 필드는 프로세스 스케줄링 정보를 저장하며, last_arrival은 프로세스가 마지막에 CPU에서 실행된 시간을 저장합니다.
sched_info_arrive() 함수 96번째 줄에서 저장합니다.
linux# vim kernel/sched/stats.h
sched_info_arrive() 함수는 context_switch() 함수 내에서 컨텍스트 스위칭을 수행하기 직전에 prepare_task_switch() 함수를 호출하고 그다음 아래 순서로 호출합니다.
context_switch() -> prepare_task_switch() -> sched_info_switch() -> __sched_info_switch() -> sched_info_arrive() 순으로 호출됩니다.
참고) crash utility를 이용한 실행시간 확인 하는 방법 (실행이 안됨, 못함(?))
책에 있는 내용을 현재 커널 버전의 라즈베리파이에서도 가능할까 해서 따라 해 보았는데, 몇 가지 문제사항이 있어서 실행할 수 없었습니다.
제가 사용법을 몰라서 못하는 것일 수도 있으므로, 아래 내용은 참고 정도로 생각하고 혹시 해결 방법을 아시는 분은 댓글 부탁드립니다. ~~
crash 설치는 apt-get으로 가능했습니다.
# sudo apt-get install crash
그러나 실행을 하면 아래와 같이 에러가 발생해서 검색해 봤습니다.
# crash
crash 실행하려면 기본 2가지 조건이 있는데, 첫 번째 vmlinux 파일이 있어야 하고,
두 번째 /proc/kcore 파일도 있어야 한다는 것입니다.
첫 번째는 커널 빌드하면서 생성했기 때문에 문제가 없는데, 두 번째 방법은 커널 옵션에서 CONFIG_PROC_KCORE=y 설정을 해주어야 한다고 해서 make menuconfig를 실행했습니다.
"Configure standard kernel features (expert users)" 메뉴에 나와있다고 했지만, 항목이 없었습니다.
menuconfig 버전에 지원하지 않을 수 있다, 수동으로 추가해 보라고 하는데 항목에 없는데 될까 해서 여기까지만 확인했습니다.
우선 결론은 책에서 나온 내용으로는 쉽게 crash 프로그램을 실행할 수 없다.
그래서 crash를 사용하기 위해서는 추가적인 정보가 필요하다입니다.
감사합니다.
<참고 자료>
1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p202~216, wikibook