linux-debug/process
created : 2020-11-10T14:51:56+00:00
modified : 2020-12-09T04:31:33+00:00
프로세스
- x64 용으로 컴파일 해서 작업하고 있어서, 다른부분이 있다.
- 책 151쪽에서 여러 함수들에 filter를 거는데, 당연히도 안걸린다. 그런데 function 을 걸면 당연히 문제가 된다. 스크립트채로 따라쳐서 실행하지 말고, 한줄한줄 실행하고 동작하는지 확인하면서 작업했다.
유저 레벨 프로세스 실행 실습
cat /sys/kernel/debug/tracing/available_filter_functions | grep sys_clone
- 이렇게 하면
__x64_sys_clone
이 나오는데 이걸sys_clone
대신 넣어주자. - 그렇게 해서 나온 스크립트는 아래와 같다.
#!/bin/bash
echo 0 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo "tracing_off"
echo 0 > /sys/kernel/debug/tracing/events/enable
sleep 1
echo "events disabled"
echo __x64_sys_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter init"
echo function > /sys/kernel/debug/tracing/current_tracer
sleep 1
echo "function tracer enabled"
echo __x64_sys_clone do_exit > /sys/kernel/debug/tracing/set_ftrace_filter
echo _do_fork copy_process* >> /sys/kernel/debug/tracing/set_ftrace_filter
sleep 1
echo "set_ftrace_filter enabled"
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_exit/enable
echo 1 > /sys/kernel/debug/tracing/events/signal/enable
sleep 1
echo "event enabled"
echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
echo 1 > /sys/kernel/debug/tracing/options/sym-offset
echo "function stack trace enabled"
echo 1 > /sys/kernel/debug/tracing/tracing_on
echo "tracing_on"
- 프로세스 생성 단계의 함수 흐름 : 책이랑 살짝 다른데 이건 cpu마다 다를듯?
copy_process.part.53+0x5/0x1d40 _do_fork+0xcf/0x3a0 __x64_sys_clone+0x27/0x30 do_syscall_64+0x55/0x110
- 프로세스 종료 단계의 함수 흐름
do_exit+0x5/0xbd0 do_group_exit+0x47/0xb0 get_signal+0xfe/0x7e0 do_signal+0x37/0x650 exit_to_usermode_loop+0x9b/0xb0 do_syscall_64+0x101/0x110
프로세스 실행 흐름
- 프로세스 생성
- raspbian_proc 프로세스 실행
- 프로세스 종료
- 부모 프로세스에게 시그널 전달
배운 내용
- 프로세스가 스스로 exit POSIX 시스템 콜을 호출하면 스스로 소멸할 수 있다.
- exit POSIX 시스템 콜에 대한 시스템 콜 핸들러는 sys_exit_group() 함수이다.
- 프로세스는 소멸되는 과정에서 부모 프로세스에게 SIGCHLD 시그널을 전달해 자신이 종료될 것이라고 통지한다.
커널 스레드
- 커널 스레드는 커널 공간에서만 실행되며, 유저 공간과 상호작용하지 않습니다.
- 커널 스레드는 실행, 휴면 등 모든 동작을 커널에서 직접 제어 관리합니다.
- 대부분의 커널 스레드는 시스템이 부팅할 때 생성되고 시스템이 종료할 때까지 백그라운드로 실행됩니다.
커널 스레드 생성 과정
- kthreadd 프로세스에서 커널 스레드 생성을 요청
- kthread_create()
- kthread_create_on_node()
- kthreadd 프로세스가 커널 스레드를 생성
- kthreadd()
- create_kthread()
커널 내부 프로세스의 생성 과정 (_do_fork() 함수)
- 위에서 말한 대로 실제로 생성하는 곳은 kthreadd가 호출하는 create_kthread 인데, 이건 결국 _do_fork 를 호출한다.
- _do_fork 의 호출 과정
- 프로세스 생성 : copy_process() 함수를 호출해서 프로세스를 생성
- 생성한 프로세스의 실행 요청 : copy_process 함수를 호출해 프로세스를 만든 후, wake_up_new_task 함수를 호출
- copy_process() 함수를 호출해 프로세스를 생성
- wake_up_new_task() 함수를 호출해 생성한 프로세스를 깨움
- 생성한 프로세스 PID를 반환
copy_process 함수 분석
- dup_task_struct : task_struct 구조체와 프로세스가 실행될 스택 공간을 할당, 이후 새로운 구조체 주소를 반환
- 책에서는 여기만 나와 있는데, memory 동적할당이 어떻게 되는지 궁금해서 확인해봤다.
- 쭉쭉 따라가보면, alloc_task_struct_node -> kmem_cache_alloc_node -> kmem_cache_alloc ->slab_alloc 가 호출되는데, slab이 뭔지 몰라서 찾아봤다.
- 참고 : https://lascrea.tistory.com/66
- slab allocator라고 하며, 일종의 자원 할당자 중 하나로 4KB의 크기를 가진 Page로 데이터를 저장하고 관리할 경우 발생하는 단편화를 최소화 하기 위해 만들어졌다.
- 리눅스 커널은 slab을 사용하고 있으며 /proc/meminfo에서 리눅스 커널이 사용하는 cache 크기를 의미한다.
- 리눅스 커널에서 커널과 디바이스 드라이버, 파일시스템 등은 영구적이지 않은 데이터(inode, task 구조체, 장치 구조체 등)들을 저장하기 위한 공간이 필요한데 이것이 slab에 관리된다.
- 그리고 기본적인 자원들(메모리, 파일 등)을 복사한다.
/* copy all the process information */ shm_init_task(p); retval = security_task_alloc(p, clone_flags); if (retval) goto bad_fork_cleanup_audit; retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_security; retval = copy_files(clone_flags, p); if (retval) goto bad_fork_cleanup_semundo; retval = copy_fs(clone_flags, p); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); if (retval) goto bad_fork_cleanup_fs; retval = copy_signal(clone_flags, p); if (retval) goto bad_fork_cleanup_sighand; retval = copy_mm(clone_flags, p); if (retval) goto bad_fork_cleanup_signal; retval = copy_namespaces(clone_flags, p); if (retval) goto bad_fork_cleanup_mm; retval = copy_io(clone_flags, p); if (retval) goto bad_fork_cleanup_namespaces; retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
wake_up_new_task()
- 프로세스 상태를 TASK_RUNNING으로 변경
- 현재 실행 중인 CPU 번호를 thread_info 구조체의 cpu 필드에 저장 (CONFIG_SMP 값이 켜져 있을때)
- 런큐에 프로세스를 큐잉
프로세스의 종료 과정 분석
- 프로세스가 죽는 두가지 흐름
- 유저 애플리케이션에서 exit() 함수를 호출할 때
- 종료 시그널을 전달받을 때
- 이번에는 책 보기 전에 ftrace 결과에 나오는 함수들 다 찾아보자.
kernel/exit.c
SYSCALL_DEFINE1(exit_group, int, error_code) { do_group_exit((error_code & 0xff) << 8); /* NOTREACHED */ return 0; }
kernel/signal.c
bool get_signal(struct ksignal *ksig) { /* skip */ /* * Death signals, no core dump. */ do_group_exit(ksig->info.si_signo); /* NOTREACHED */ } spin_unlock_irq(&sighand->siglock); ksig->sig = signr; return ksig->sig > 0; }
- 이거 이외에도
fpu.c
,seccomp.c
에 있는데, 이건 찾아보니까 fpu 에서 에러가 나서 죽일때랑, [[seccomp]]에서 강제로 죽일때 호출. 일반적으로 죽는 경우는 아니니, 위에 2가지만이 do_group_exit를 호출한다라고 알수 있다.
- do_exit() 함수의 동작 방식 확인
- init 프로세스가 종료하면 강제 커널 패닉 유발 : 보통 부팅 과정에서 발생함
- 이미 프로세스가 do_exit() 함수의 실행으로 프로세스가 종료되는 도중 다시 do_exit() 함수가 호출됐는지 점검
- 프로세스 리소스(파일 디스크립터, 가상 메모리, 시그널) 등을 해제
- 부모 프로세스에게 자신이 종료되고 있다고 알림
- 프로세스의 실행 상태를 task_struct 구조체의 state 필드에 TASK_DEAD로 설정
- do_task_dead() 함수에 호출해 스케줄링을 실행, do_task_dead() 함수에서 __schedule() 함수가 호출되어 프로세스 자료구조인 태스크 디스크립터와 스택 메모리를 해제
- do_task_daed() 함수를 호출하고 난 후의 동작
- __schedule() 함수
- context_switch() 함수
- finish_task_switch() 함수
태스크 디스크립터(task_struct 구조체)
- 프로세스를 식별하는 필드
- comm : 프로세스 이름
- pid : 프로세스 id
- tgid : task_group id, 만약 thread가 leader 인 경우, tgid == pid
- 프로세스 상태 저장
- state: 프로세스 실행 상태
- TASK_RUNNING : CPU에서 실행 중이거나 런큐에서 대기 상태에 있음
- TASK_INTERRUPTIBLE : 휴면 상태
- TASK_UNINTERRUPTIBLE : 특정 조건에서 깨어나기 위해 휴면 상태로 진입한 상태
- flags: 프로세스 세부 동작 상태와 속성 정보
- PF_IDLE : 자신이 IDLE thread 임을 나타내는 flag
- PF_EXITING : 종료되는 중
- PF_EXITPIDONE : 종료 됨
- PF_WQ_WORKER : 워커 쓰레드
- PF_KTHREAD : 커널 스레드
- state: 프로세스 실행 상태
- 프로세스 간의 관계
- real_parent : 자신을 생성한 부모 프로세스의 태스크 디스크립터 주소를 저장
- parent : 부모 프로세스의 태스크 디스크립터 주소를 담고 있음.
- children : 자식 프로세스 리스트
- sibiling : 형제 프로세스 리스트
- 프로세스 실행 시각 정보
- utime : 유저 모드에서 프로세스가 실행한 시각 (account_user_time)
- stime : 커널 모드에서 프로세스가 실행한 시각 (account_system_index_time)
- sched_info.last_arrival : 프로세스가 CPU에서 실행된 시각 (context_switch -> prepare_task_switch -> sched_info_switch -> __sched_info_switch -> sched_info_arrive)
스레드 정보 : thread_info 구조체
- 선점 스케줄링 실행 여부
- 시그널 전달 여부
- 인터럽트 컨텍스트와 Soft IRQ 컨텍스트 상태
- 휴면 상태로 진입하기 직전 레지스터 세트를 로딩 및 백업
struct thread_info {
struct task_struct *task; /* main task structure */
unsigned long flags; /* low level flags */
__u32 cpu; /* current CPU */
__s32 preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
0-0x7FFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread
*/
__u8 supervisor_stack[0];
/* saved context data */
unsigned long ksp;
};
#endif
- preempt_count 가 바뀌는 조건
- 인터럽트 컨텍스트 실행 시작 및 종료 설정
- Soft IRQ 컨텍스트 실행 시작 및 종료 설정
- 프로세스 선점 스케줄링 가능 여부
cpu 필드에 대한 상세 분석
- sm_processor_id()
- set_task_cpu()
thread_info 구조체 초기화 코드 분석
- dup_task_struct()
- setup_thread_stack(tsk, orig);
- alloc_task_struct_node()
- alloc_thread_stack_node()
프로세스의 태스크 디스크립터에 접근하는 매크로 함수
- current : 현재 구동 중인 프로세스의 태스크 디스크립터 주소
- arch/x86/include/asm/current.h
DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
return this_cpu_read_stable(current_task);
}
#define current get_current()
- 보면은, cpu 마다 task_struct를 선언하는데, 이 변수 명은 current_task 인데, arch/x86/kernel/process_64.c 에 있는
__switch_to()
에서
this_cpu_write(current_task, next_p);
this_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p));
/* Reload sp0. */
update_task_stack(next_p);
switch_to_extra(prev_p, next_p);
- 이렇게 매번 task가 전환 될때 cpu 별로 따로 넣어준다. 이때마다, update_task_stack를 호출해주면서 stack을 설정한다.
프로세스 디버깅
- 사용하는 명령어 : layout asm
- 내용 정리
- 리눅스 유틸리티 프로그램을 실행할 때 프로세스는 fork()와 execve() 시스템 콜 함수를 호출한다.
- ftrce의 sched_process_exec 이벤트로 리눅스 유틸리티 프로그램의 파일 위치를 알 수 있다.
- 리눅스 유틸리티 프로그램을 종료할 때의 프로세스는 exit() 시스템 콜 함수를 호출한다.