total_activ
[커널 디버깅과 코드 학습] 프로세스 정의 및 생성과정 본문
프로세스
프로세스 소개
1. 프로세스란?
프로세스는 리눅스 시스템 메모리에서 실행 중인 프로그램을 뜻한다.
- 스케줄링 대상인 태스크와 유사한 의미로도 사용된다.
- 다수의 프로세스를 실시간으로 사용하는 기법의 용어로 '멀티 프로세싱'이라고 한다.
리눅스 개발자 입장에서의 프로세스는 리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 뜻한다.
- 프로세스가 실행 대기한다면 실행 할때 거치는 과정?
- 프로세스를 식별하는 구조체 모습? : task_struct 구조체
테스크 디스크럽터는 프로세스를 관리하는 자료 구조이자 객체이다.
테스크 디스크럽터는 task_struct 구조체로 표현된다.
해당 구조체에 프로세스가 쓰는 메모리 리소스, 프로세스 이름, 실행 시각, PID, 프로세스 스택의 최상단 주소같은 속성 정보가 저장되어 있다.
또한, 그중에서 프로세스 스택 공간은 프로세스 실행 흐름을 표현하는 공간이다.
이 프로세스 스택의 최상단 주소에 thread_info 구조체가 있다.
- task_struct 구조체 : 테스크 디스크럽트
- thread_info 구조체 : 프로세스 스레드 정보
2. 태스크란?
윈도우 운영체제에서의 태스크는 실행 및 작업 단위를 뜻한다.
이러한 태스크 용어가 리눅스로 넘어와 process_struct 대신 task_struct를 쓰고 있다.
즉, 예전의 용어를 현재까지 그대로 사용하고 있는 셈이다. (거의 task = process)
스레드란?
스레드는 유저 레벨에서 생성된 가벼운 프로세스라 할수 있다.
스레드는 일반 프로세스에 비해 컨텍스 스위칭을 수행 할때 시간이 적게 소모되는 것만큼 가볍다.
그 이유는 스레드는 자신이 속한 프로세스 내의 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소 공간을 공유하기 때문이다.
- 프로세스는 자신만의 주소 공간 소유
- 스레드는 스레드 그룹 안에 다른 스레드와 주소 공간 공유
커널 입장에서의 스레드는 다른 프로세스와 동이랗게 관리한다. 다만, 각 프로세스의 식별자인 테스크 디스크립터(tack_struct에서 스레드 그룹 여부를 점검 할 뿐이다.
프로세스 확인하기
프로세스에 익숙해지기 위해서는 ftrace 로그에서 프로세스 관련 정보를 자주 봐야한다.
1. ps 명령어로 프로세스 목록 확인
#[ 라즈베리 파이에서의 명령어]
ps -ely
#[리눅스에서의 명령어]
ps
출력 결과 : UID, PID, PPID, C, PRI, NI, RSS, SZ, WCHAN, TTY, TIME, CMD
ps 명령어를 입력하면 리눅스 커널 내부의 어떤 자료구조에 접근하여 전체 프로세스 정보를 출력할까? (info ps)
- 답 : init_task 전역 변수
- 리눅스 시스템에서 생성된 모든 프로세스는 init 프로세스를 표현하는 자료구조, init_task 전역변수의 tasks 필드에 연결 리스트로 등록되어 있다.
- 따라서 해당 연결 리스트를 순회하면서 프로세스 정보인 task_struct 구조체의 주소를 계산하여 프로세스 정보를 출력한다.
ps -ejH
출력 결과 : PID, PGID, SID, TTY, TIME, CMD
"ps -ely"와는 달리 부모 자식 프로세스 관계를 토대로 프로세스를 출력한다.
부모 프로세스와 자식 프로세스는 CMD 값이 다른 열에 정렬되어 있다.
(ex) 부모 프로세스 : kthreadd
자식 프로세스 : kworker/0:0H, mm_percpu_wq, ksoftirqd/0
"ps -ejH" 결과 목록들을 커널 스레드, 커널 프로세스라고 하며 커널 공간에서만 실행된다.
(더보기) PID 란?
커널이 프로세스를 생성할 때 프로세스에 고유 정수형 ID 값을 부여한다.
이를 PID(Process IDentifier)라고 한다.
PID는 리눅스 커널에서 pid_t라는 타입으로 <sys.types.h> 헤더 파일에 저장되어 있다.
- typedef in __kernel_pid_t pid_t;
- __kernel_pid_t는 int 타입임
커널이 프로세스에게 부여하는 PID의 기준
답: PID를 증가하면서 프로세스에게 부여
- 즉, 대기번호와 같이 새로운 프로세스를 생성하면 PID 정수값을 증가시켜 제공
- 프로세스의 생성 순서를 파악 가능
- 공통적으로 커널을 생성하는 프로세스 (대부분은 리눅스 시스템마다 다름)
swapper : 0
init : 1
kthreadd : 2
PID를 유저공간에서 확인 하는 방법
답 : getpid() 함수 호출
getpid() 함수 호출시 커널에서 호출되는 함수
답 : 시스템 콜 핸들러인 sys_getpid() 함수
#sys_getpid()
SYSCALL_DEFINE0(getpid)
{
reutrn task_tpid_vnr(currnet);
}
task_tpid_vrn()함수에서 인자로 받는 current : 현재 실행 중인 프로세스의 태스크 디스크럽트주소가 담긴 인자, 해당 current 인자를 통해 호출하여 PID를 읽음
systemd, init 프로세스
PID PGID SID TTY TIME CMD
1 1 1 ? 00:00:03 systemd
systmed(=init) : pid가 1인 프로세스는 init 프로세스라고 하며 모든 유저 공간에서 생성되는 프로세스의 부모 프로세스 역활을 수행한다.
- 프로세스마다 부모 프로세스가 존재한다
- 하지만 부모 프로세스가 종료되면 자식 프로세스에 대한 조부모 역활을 대부분 init 프로세스가 수행한다.
2. ftrace에서 프로세스 확인하기
ftrace에서도 맨 왼쪽에 프로세스이름-[pid] 형식으로 프로세스의 정보가 등장한다.
프로세스 생성 과정
생성 과정을 알면 알 수 있는 내용
- 프로세스가 부모 프로세스로부터 어떻게 복제되는가?
- 생성된 프로세스가 어떻게 실행을 시작하는가?
- 프로세스 자료구조는 어떻게 처리하는가?
리눅스에서 구동되는 프로세스 : 유저레벨에서 생성된 프로세스 / 커널 레벨에서 생성된 프로세스
- 유저 프로세스 : 유저 공간에서 프로세스를 생성하는 라이브러리 도움을 받아 커널에게 프로세스 생성 요청을 진행
- 커널 프로세스 : 커널 내부의 kthread_create() 함수를 호출 받아 커널 프로세스를 생성
여기서 둘의 공통점은 프로세스 생성시 _do_fork() 함수 호출 한다는 것이다.
1. _do_fork() 함수 소개
리눅스에서 구동중인 모든 프로세스는 _do_fork() 함수가 실행할 때 생성된다.
이때 init와 kthreadd 프로세스가 프로세스를 생성한다.
- init 프로세스 : 부팅 과정에서 유저 프로세스를 생성하는 역활을 담당
이때 프로세스는 "생성" 보다 "복제"에 가까운 행위이다.
- 프로세스를 생성할때 부모 프로세스를 복제한다.
- 그 이유는 프로세스에게 필요한 리소스를 각각 할당 받을 경우 장시간 소요되기 때문이다.
비슷한 행위로 슬럽 메모리 할당자가 존재한다.
- 속도 개선을 위해 반복 실행되는 코드를 줄이는 기법으로 사용된다.
- 드라이버에 메모리 할당을 요청 할때 자주 사용되는 구조체를 정의하여 미리 메모리를 확보한다.
- 그렇게 되면, 메모리 할당 요청시 이미 확보한 메모리 주소를 제공한다.
_do_fork() 함수 선언부와 반환값 확인
#선언부 인자값 확인
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
init __user *parent_tidptr,
init __user *child_tidptr,
unsigned long tls);
반환값 : long 타입의 PID 반환
- 에러 발생시, PTR_ERR() 메크로로 지정된 에러 값 반환
인자값
- clone_flags : 프로세스를 생성할 때 지정하는 옵션 정보 저장 (옵션 정보는 생성시 부모 프로세스로부터 복제될 리소스 정보 의미)
- stack_start : 유저 영역에서 스레드를 생성할 때 복사하려는 스택 주소 (= 유저공간에서 실행 중인 프로세스 스택 주소)
- stack_size : 유저 영역에서 실행중인 스택 크기 (유저 영역에서 스레드를 생성 할때 복사)
- *parent_tidptr, *child_tidptr : 부모와 자식 스레드 그룹을 관리하는 핸들러 정보
_do_fork()함수 호출 시기
프로세스의 유형마다 호출되는 시기,흐름은 다르다.
- 유저 레벨 프로세스(유저에서 생성) : sys_clone() 시스템 콜 핸들러 함수
- 커널 레벨 프로세스 (커널에서 생성): kernel_thread() 함수
유저 모드와 커널 모드 차이
분류 기준 : 메모리 접근과 실행 권한 기준 두 모드로 분류됨
1. 메모리 접근
메모리의 잘못된 접근으로 인해 커널의 오작동 유발 가능성 때문에 메모리에 접근하지 못하도록 관리형 언어를 설계함
대표적으로 자바가 존재하며, 반대로 직접 접근이 가능한 언어는 C와 C++이 존재함
- 0~3GB의 가상 메모리 공간까지는 유저모드로 접근 가능
- 0~4GB의 메모리까지는 접근 제한 존재
2. 실행 권한 기준
유저 모드에서 커널에게 요청할때 : 파일을 읽고 쓰거나 현재 실행중인 프로세스 정보 획득을 원할때 시스템 콜을 통해 커널에게 요청한다.
시스템 콜은 커널의 시스템 콜 핸들러로 다음 3단계를 통해 처리한다.
- 시스템 콜에 전달한 인자의 오류 검사
- 커널 내부 함수 호출
- 유저 애플리케이션에 요청한 정보를 알려줌
라이브러리 파일은 유저모드 코드이다.
실행중인 유저 어플리케이션은 라이브러리와 링킹되어 메모리에 적재되어 생성되기 때문이다.
유저 레벨 프로세스 생성 방식
- 더블클릭 OR 컴파일한 프로그램 실행 (유저모드에서는 스스로 프로세스 생성이 안됨. 이때는 GNU와 같은 라이브러리 필요)
유저 어플리케이션? 유저 레벨 프로세스?
- 유저 애플리케이션은 프로그램을 의미
- 유저 레벨 프로세스는 애플리케이션을 실행하는 주체 (즉, 커널은 애플리케이션을 모르고 단순히 프로세스를 실행함)
유저 레벨 프로세스와 커널 레벨 프로세스 차이
유저 레벨 프로세스 : fork() 함수나 phtread_create() 함수를 호출함. 즉, 시슷템 콜을 실행해야만함
커널 레벨 프로세스 : kthread_create() ㅎ마수를 호출함. 커널 모드에서 실행함
p. 164~168 읽고 정리 안했기 때문에 다음 (8) 페이지에서 162부터 정리하기
'리눅스 커널' 카테고리의 다른 글
리눅스 커널 최신 버전 설치 및 빌드 (Ubuntu) - Printk() 코드 삽입 (0) | 2023.11.03 |
---|---|
[커널 디버깅과 코드 학습] 프로세스 생성 과정 (0) | 2023.10.31 |
[커널 디버깅과 코드 학습] TRACE32 (0) | 2023.10.12 |
[커널 디버깅과 코드 학습] ftrace 함수 (0) | 2023.10.06 |
[커널 디버깅과 코드 학습] printk() dump_stack() 함수 (0) | 2023.10.06 |