IT/Linux Kernel

디버깅을 통해 배우는 리눅스 커널의 구조와 원리 1, 도서 공부하기 9 - 프로세서란

변화의 물결1 2025. 3. 12. 14:26

 

 

 

 안녕하세요.

 

 드디어 디버깅의 영역에서 벗어나 프로세스의 영역으로 들어오게 되었습니다. 프로세스 부분도 양이 많아서, 배워야 할 것이 적지 않을 것으로 생각됩니다.

 책 내용을 천천히 정리해 가는 것으로 생각하면 좋을 것 같습니다.

 


 

1. 프로세스란?

 

 리눅스 개발자 입장에서 프로세스는 리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 의미합니다.

 

 프로세스를 관리하는 자료구조이자 객체를 태스크 디스크립터(task descriptor)라고 부르고 task_struct구조체로 표현합니다. 이 구조체에 프로세스가 사용하는 메모리 리소스, 프로세스 이름, 실행 시각, 프로세스 아이디(PID), 프로세스 최상단 주소와 같은 속성정보가 지정되어 있습니다. 그러나 이것만으로 프로세스의 흐름까지 설명할 수 없습니다.

 

 흐름을 표현하는 중요한 것이 프로세스 스택 공간이며 프로세스 스택의 최상단 주소에 thread_info 구조체가 있습니다.

 

 함수흐름을 보면서 프로세스 흐름도 같이 볼 수 있습니다. 아래에서 보는 것은 이전 글에서도 보았듯이 콜 스택의 한 부분이라고 생각하면 됩니다.

 

 즉 함수 호출은 유저공간 프로그램에서 sigtimewait() 함수를 호출하고 이에 대응하는 시스템 콜 핸들러 함수인 sys_rt_sigtimewait 호출하는데 이때 커널 모드로 진입하면서 복귀할 수 있게 CPU가 유저 공간의 RIP(return address)와 레지스터 값을 저장하고 실행하기 위한 어셈블리 코드가 들어가고, sys_rt_sigtimewait() 함수를 실행하고 내부적으로 do_sigtimedwait()가 호출됩니다. 필요한 경우 프로세스를 대기 상태로 변경하고 스케줄러 실행합니다. 여기서는 스케줄러를 실행한 것을 알 수 있습니다.

 

예시)

00 | __schedule()

01 | __schedule_time()

02 | __do_sigtimewait()

03 | __sys_rt_sigtimewait()

04 | ret_fast_syscall(asm)

 

  수행을 완료하고 어떻게 다시 돌아오는 것에 대해 의문이 생길 것입니다. 그것은 프로세스가 마지막에 실행했던 레지스터 세트와 실행 흐름이 프로세스 스택 공간에 저장되어 있기 때문입니다.

 

 리눅스 커널에서 프로세스를 표현할 수 있는 자료구조체는 task_struct, thread_info입니다.

 

 task_struct 구조체는 프로세스의 전체적인 정보(스케줄링, 메모리, 파일, 부모-자식 관계 등 모든 정보)를 저장하는 핵심 구조체 (PCB)입니다.

 

 thread_info 구조체는 보통 커널 스택과 함께 존재하며, 컨텍스트 스위칭 시 핵심 정보(커널 스택 위치, 실행 중인 프로세스 정보, 실행 상태 플래그, 레지스터 정보)를 저장합니다. 즉, 현재 실행 중인 프로세스가 어디까지 실행되었는지 기록하고, 필요할 때 복원하는 역할을 합니다.

 

 

2. 태스크(Task)란

 

 예전에는 특정코드나 프로그램을 실행을 일괄 처리했고, 이런 실행 및 작업 단위를 태스크라고 했습니다.

 임베디드 시스템에서는 태스크 2개가 서로 주고받으며 시스템 전체를 제어했고, 이것을 이용하던 임베디드 시스템 개발자 유입으로 프로그램을 실행하는 단위라고 생각했던 태스크 개념은 프로세스와 겹쳐지는 부분이 많았다고 합니다.

 

 프로세스마다 속성을 표현하는 process_struct 대신 task_struct로 쓰고 있는 것처럼 task란 단어를 process라고 처리하는 것으로 생각해도 무리가 없을 것이라고 합니다.

 

 프로세스를 관리하는 함수를 아래와 같은 개념으로 볼 수도 있다고 예를 들 수 있을 것입니다.

 

dump_task_regs() ===== dump_(process)_regs()

get_task_mm() ==== get_(process)_mm()

 

 

3. 스레드(Thread)란

 

 유저레벨에서 생성된 가벼운 프로세서라고 할 수 있습니다.

 그렇다면 스레드와 프로세서 비교해서 알아볼 수밖에 없을 것입니다.

 

 1) Process 특징

 

  - 독립적인 메모리 공간을 가짐. (코드, 데이터, 힙, 스택 분리됨)

  - CPU 스케줄링의 기본 단위 (운영체제가 프로세스를 스케줄링함)

  - 각 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없음

  - 프로세스 간 데이터를 주고받으려면 IPC (Inter-Process Communication)가 필요함

    예: 파이프(pipe), 메시지 큐(message queue), 공유 메모리(shared memory) 등

 

 

 2) Thread 특징

 

 - 하나의 프로세스 안에서 실행됨.

 -같은 프로세스의 다른 스레드와 메모리를 공유함.

  코드, 데이터, 힙을 공유함 하지만 각 스레드는 독립적인 스택을 가짐.

 - 컨텍스트 스위칭이 빠름

   프로세스 간 전환보다 오버헤드가 적음.

 - 같은 프로세스의 스레드끼리는 IPC 없이도 쉽게 데이터 공유 가능

 

비교 항목 프로세스 (Process) 스레드 (Thread)
독립성 독립적인 실행 단위 프로세스 내에서 실행
메모리 각 프로세스는 독립적인 메모리 공간을 가짐 같은 프로세스 내에서 코드, 데이터, 힙을 공유
스택 각 프로세스마다 별도 존재 각 스레드마다 개별적인 스택 존재
데이터 공유 IPC (파이프, 메시지 큐 등) 필요 힙 영역을 통해 바로 공유 가능
컨텍스트 스위칭 속도 느림 (PCB 저장/복원 필요) 빠름 (같은 메모리 공간 사용)
생성 비용 높음 (새로운 메모리 공간 필요) 낮음 (메모리 공유)
예제 크롬 브라우저의 각 탭, 실행 중인 터미널 크롬 브라우저 내에서 여러 탭, 멀티스레드 서버

 

 

 위와 같은 차이가 있지만 커널 입장에서는 스레드를 다른 프로세스와 동등하게 관리합니다. 대신 각 프로세스 식별자인 태스크 디스크립터(task_struct)에서 스레드 그룹은 여부를 점검합니다.

 

 

4. 라즈베리 파이에서 프로세스 확인

 

 위에서 설명에 대한 것을 보았다면 실제로 프로세스를 확인해 보면 더 와닿을 수 있을 것입니다.

 

1) PS(Process Status) 확인

 

 ps 명령어로 실행 중인 프로세스를 확인할 수 있습니다. 추가 옵션 -ely를 통해 상세하게 볼 수 있습니다.

 

-e: 사용하면 시스템 전체의 모든 프로세스를 표시합니다.

-l: 일반 형식보다 더 많은 필드를 포함하여 프로세스의 상세한 정보를 제공합니다.

-y: RSS (Resident Set Size)와 TTY 정보를 출력하지 않습니다. 이는 메모리 사용량에 대한 정보를 줄이고, 프로세스의 실행 터미널 정보를 제외시킵니다.

 

$ ps -ely

 

 

 

 책 저자는 질문을 던집니다. "ps 명령어를 입력하면 어떻게 리눅스 커널 내부의 어떤 자료 구조에 접근해서 전체 프로세스 정보를 출력할까?"

 

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

 

추가로 다른 옵션을 지정해서 확인해 봅니다. 프로세스를 부모 자식 프로세스 관계 토대로 출력합니다. 예를 들면 kthreadd 부모 프로세스 밑에 rcu_gp, rcu_par_gp 등이 있고, systemd 부모 프로세스 밑에 systemd-journal 등 자식 프로세서가 있는 것을 보여줍니다.

 

 kthreadd는 커널 공간에서만 실행하는 프로세스로 커널 프로세스를 생성합니다. 그래서 아래(자식)에 보이는 것을 커널 스레드, 커널 프로세스라고 하며 커널 공간에서만 실행됩니다.

 

$ ps -ejH

 

 

-j: 작업(job) 형식으로 출력합니다. 이 형식은 프로세스 ID(PID), 부모 프로세스 ID(PPID), 세션 ID(SID), TTY, 상태(STAT), 사용자 시간(TIME), 명령어(COMMAND)를 포함한 정보를 보여줍니다.

 

-H: 트리 형식으로 출력합니다. 이 형식은 각 프로세스의 부모/자식 관계를 계층적으로 나타내어 보다 시각적으로 보기 쉽게 합니다.

 

 

 

 ps 출력결과 맨 앞에 나오고, 소스코드 분석할 때도 나오는 PID는 어떻게 정의되어 있는가 찾아볼 수 있습니다.

 여기서는 PID는 Process IDentifier라고 하며 커널이 프로세서를 생성할 때 프로세스에 고유의 정수형 값을 순서번호처럼 증가시키며 부여합니다.

  

리눅스 커널의 PID는 pid_t으로 되어 있고, pid_t는 __kernel_pid_t 형식으로 정의되어 있고 이것은 int형으로 되어 있는 것을 알 수 있습니다.

 

rpi_kernel_src/linux $ vim include/linux/sched.h

 

rpi_kernel_src/linux $ vim include/linux/types.h

 

 

 

rpi_kernel_src/linux $ vim include/uapi/asm-generic/posix_types.h

 

 

 

2) 커널이 초기 생성하는 프로세스

 

 아래 3가지 공통적인 프로세스이며, 나머지는 프로세스는 부팅 시마다 다른 PID를 가질 수 있습니다.

 

(1) swapper 프로세스 : PID 0

 

 리눅스 커널이 가장 먼저 생성하는 프로세스

 CPU가 아무 작업도 하지 않을 때 실행됨 (즉, Idle 상태)

 컨텍스트 스위칭 시 대기하는 동안 CPU를 절전 모드로 진입시키는 역할 수행

 종료되지 않으며, 시스템이 꺼질 때까지 계속 존재함

 

(2) init(systemd) 프로세스 : PID 1

 

 swapper가 kernel_init()을 호출한 후 실행됨

 예전에는 init 프로세스였지만, 최신 리눅스에서는 systemd가 이를 대신함

 모든 유저 프로세스의 부모 프로세스가 됨 (fork()로 새로운 프로세스 생성)

 시스템 서비스(예: 네트워크, 로그인, 데몬 프로세스 등)를 초기화하고 관리함

 

(3) kthread 프로세스 : PID 2

 

 커널 내부에서 실행되는 백그라운드 작업을 담당하는 프로세스

 예를 들어, 디스크 I/O, 네트워크 패킷 처리, 메모리 관리 등을 수행하는 데 사용됨

 새로운 커널 스레드를 생성할 때 kthread_create() 함수가 사용됨

 이후 생성된 모든 커널 스레드의 부모 프로세스 역할을 함

 

 

3) PID를 어떻게 확인하는가?

 

 유저 공간에서 PID를 확인하는 방법 중 시스템 프로그램을 할 때 getpid() 함수를 사용할 수 있습니다. 이 getpid() 함수를 호출하면 이에 대응하는 sys_getpid() 함수가 호출됩니다.

 

rpi_kernel_src/linux $ vim kernel/sys.c

 

 

 

 각 프로세스마다 부모프로세스가 있고 자식프로세스가 종료할 때 부모 프로세스에게 신호로 알립니다. 그리고 예외 상황으로 부모프로세스가 먼저 사라지면 그 상위 조부모가 부모프로세스가 되며 대부분 init프로세스가 그 역할을 하게 됩니다.

 

 이번장의 프로세스 구조가 인간 세상과 유사하게 만들었다고 표현해야 할지 그렇게 만들어질 수밖에 없다고 표현해야 할지, 비슷하게 느껴집니다.^^

 

 

감사합니다.

 

 

<참고 자료>

1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p128~p137, wikibook

 

 

 

반응형