프로세스라는 용어는 추상적일 수 있다.
프로세스란 간단하게 말해서 리눅스 시스템 메모리에서 실행 중인 프로그램을 말한다
스케쥴링 대상인 타스크 와 유사한 의미로도 쓰인다.
다수의 프로세스를 실시간으로 사용하는 기법을 멀티프로세싱이라고하고, 같은 시간에 여러 프로그램을 실행하는 방식을
멀티타스킹이라고 한다.
리눅스 개발자 입장에서 프로세스는 무엇일까?
리눅스 시스템 메모리에 적재되어 실행을 대기하거나 실행하는 실행 흐름을 의미
- 프로세스가 실행을 대기한다면 실행할 때 어떤 과정을 거칠까?
- 프로세스는 어떤 구조체로 식별할까?
프로세스를 관리하는 자료구조이자 객체를 태스크 디스크립터 (Task descriptor) 라고 부르고 task_struct 구조체로 표현
이 구조체에 프로세스가 쓰는 메모리 리소스 , 프로세스 이름, 실행 시각, 프로세스 아이디(PID), 프로세스 스택의 최상단 주소와 같은 속성 정보 저장
프로세스의 실행흐름을 표현하는 중요한 공간은 프로세스 스택 공간이며, 이 프로세스 스택의 최상단 주소에 thread_info 구조체가 있다.
-000|__schedule()
-001|schedule_timeout()
-002|do_sigtimedwait()
-003|sys_rt_sigtimewait()
-004|ret_fast_syscall(asm)
위와 같은 함수 목록을 처음보면 낯설겠지만 어렵지 않다.
- 함수의 호출방향은 004번째 줄에서 000번째 줄 방향
- 유저 공간 프로그램에서 sigtimewait() 함수를 호출하면 이에 대응하는 시스템 콜 핸들러 함수인 sys_rt_sigtimewait()함수가 호출
- 000번째 줄의 __schedule()함수가 호출돼 스케쥴링
프로세스는 함수를 호출하면서 함수를 실행한다.
함수를 호출하고 실행할 때 어떤 리소스를 쓸까?
프로세스 스택 메모리 공간을 쓰고, 모든 프로세스들은 커널 공간에서 실행 될 때 각자 스택 공간을 할당받으며 스택 공간 내에서 함수 실행
앞에서 본 프로세스가 스케쥴러에 의해 다시 실행된다고 가정해봅시다. 이후 어떻게 실행될까?
-000|__schedule()
-001|schedule_timeout()
-002|do_sigtimedwait()
-003|sys_rt_sigtimewait()
-004|ret_fast_syscall(asm)
000번째 줄함수에서 004번째 줄 함수 방향으로 되돌아올 것
어떤 정보를 참고해서 이전에 실행됐던 함수로 되돌아갈까?
프로세스가 마지막에 실행했던 레지스터 세트와 실행흐름은 프로세스 스택공간에 저장돼있었습니다.
A() 함수에서 B()함수를 호출하고, B()함수에서 C()함수를 호출하는 코드를 작성했다고 가정
C()함수가 실행을 마무리하면 B()함수에서 A()함수로 되돌아갑니다. 이 같은 원리입니다.
정리하면 프로세스는 어느정도 추상적인 개념을 내포합니다. 하지만 리눅스 커널에서 프로세스를 표현할 수 있는 다음과 같은 자료구조가 있습니다.
- task_struct 구조체 : 태스크 디스크립터
- thread_info 구조체 : 프로세스 스레드 정보
프로세스 속성 정보와 실행 흐름을 저장하는 구조체를 잘 알아야 합니다.
TASK 란?
리눅스 커널 코드를 볼 때 리눅스 커널함수의 이름에 'task' 가 왜 보일까?
타스크는 리눅스 외의 다른 운영체제에서 예전부터 많이 쓰이는 용어
타스크는 실행 (Execution)이라고 말할 수 있습니다.
운영체제 책을 보면 첫장에서 타스크 에 대한 설명을 볼 수 있다. 예전에는 특정 코드나 프로그램 실행을 일괄처리했습니다. 이러한 실행 및 작업단위를 타스크라고 불렀습니다.
이전에 썼던 타스크라는 용어를 리눅스 커널 소스코드에서 그대로 쓰고있다.
대표적인 예로 프로세스 속성을 표시하는 구조체 이름을 process_struct 대신 task_struct 으로 쓰고있다.
이처럼 리눅스 커널 함수 이름이나 변수 중에 task 란 단어를 보면 프로세스 관련 코드 라고 생각해도 좋다.
다음 함수는 모두 프로세스 관리 및 제어하는 역할을 수행
- dump_task_regs()
- get_task_mm()
- get_task_pid()
- idel_task()
- task_tick_stop()
리눅스 커널에서 타스크는 프로세스와 같은 개념으로 쓰이는 용어
소스코드나 프로세스에 대한 설명을 읽을 때 타스크란 단어를 보면 프로세스와 같은 뜻으로 이해하자.
THREAD 란?
쓰레드란 무엇일까 유저레벨에서 생성된 가벼운 프로세스라 할 수 있다.
스레드는 일반 프로세스에 비해 컨텍스트스위칭을 수행할 때 시간이 적게 걸린다.
그이유는 스레드는 자신이 속한 프로세스 내 다른 스레드와 파일 디스크립터, 파일 및 시그널 정보에 대한 주소공간을 공유하기 때문이다.
프로세스가 자신만의 주소공간을 갖는 것과 달리 스레드는 스레드 그룹안의 다른 스레드와 주소공간을 공유한다.
하지만 커널입장에서 스레드를 다른 프로스스와 동등하게 관리
대신 각 프로세스 식별자인 타스크 디스크립터(task_struct)에서 스레드 그룹 여부를 점검할 뿐이다.
프로세스 확인하기
ps 명령어로 프로세스 목록 확인
라즈베리파이에서 터미널을 실행한 후 다음과 같이 "ps -ely" 명령어 입력해보자
ps 명령어를 입력하면 리눅스 커널 내부의 어떤 자료구조에 접근해서 전체 프로세스 정보를 출력할까?
init_task 전역변수를 통해 전체 프로세스 목록을 출력
리눅스 시스템에서 생성된 모든 프로세스 (유저레벨, 커널 스레드) 는 init 프로세스를 표현하는 자료구조인 init_task 전역변수의 tasks 필드에 연결 리스트로 등록돼 있습니다.
이 연결리스트를 순회하면서 프로세스 정보인 task_struct 구조체 주소를 계산해 프로세스 정보를 출력
PID가 1인 systemd 프로세스가 보입니다.
sytstemd는 라즈비안에서 모든 프로세스들을 관리하는 init 시스템 및 프로세스 입니다.
대부분의 리눅스에서는 PID 1인 프로세스를 init 라고합니다.
이번에는 다른방식으로 프로세스 목록을 확인 ps -ejH
출력결과를 보면 부모자식 프로세스 관계를 토대로 출력
프로세스는 어떻게 만들어지나
프로세스 생성과정은 왜 알아야할까?
리눅스 커널에서 프로세스를 생성하는 함수를 분석하면 자연히 다음과 같은 내용을 알게 된다.
- 프로세스가 부모 프로세스로부터 어떻게 복제 되는가?
- 생성된 프로세스가 어떻게 실행을 시작하는가?
- 프로세스 자료구조는 어떻게 처리하는가?
리눅스에서 구동되는 프로세스는 크게 유저레벨에서 생성된 프로세스와 커널 레벨에서 생성된 프로세스로 분류할 수 있다.
각 프로세스 별로 생성과정이 상이하다
- 유저프로세스 : 유저공간에서 프로세스를 생성하는 라이브러리 (GNU C : glibc)의 도움을 받아 커널에게 프로세스 생성 요청
- 커널프로세스 : 커널 내부의 kthread_create()함수를 호출해서 생성
하지만 공통점이있다 바로 프로세스 생성할 때 _do_fork()를 호출
_do_fork() 함수 소개
모든 프로세스는 이함수가 실행할 때 생성
프로세스는 누가 생성할까? 리눅스에서 프로세스 생성을 전담하는 프로세스가 있다.
주인공은 바로 init 와 kthread 프로세스
특히 init 프로세스는 부팅과정에서 유저 프로세스 생성하는 역할을 담당
유저레벨 프로세스는 init 프로세스, 커널레벨 프로세스(커널스레드)는 kthread 프로세스가 생성
그런데 프로세스는 생성이아니라 복제된다고 말할 수 있다.
그럼 프로세스 생성할 때 부모 프로세스를 복제하는 이유는 무엇일까?
프로세스에게 필요한 리소스를 각각할당받으면 시간이 오래걸린다 그래서 이미 생성된 프로세스에게서 리소스를 물려받는 것이다.
커널 메모리할당자인 슬럽 메모리 할당자 (Slub Memory Allocator) 도 유사한 기법을 활용
_do_fork()함수 선언부와 반환값 확인
먼저 _do_fork함수의 선언부를 보면서 이 함수에 전달되는 인자와 반환값을 확인해보겠습니다.
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls);
먼저 반환값을 long 타입인데 바로 프로세스 PID를 반환합니다.
프로세스 생성시 에러가 발생하면 PID대신 에러값을 반환
전달인자 확인해보자
- clone_flags : 프로세스 생성할 때 지정하는 옵션 정보를 저장 , 프로세스를 생성할 때 부모 프로세스로부터 복제될 리소스 정보를 의미
- stack_start : 스레드를 생성할 때 복사하려는 스택의 주소 , 스택 주소는 유저공간에서 실행중인 프로세스 스택의 주소
- stack_size : 유저 영역에서 실행중인 스택 크기 , 보통 유저 영역에서 스레드 생성할 때 복사
- parent_tidptr , child_tidptr : 부모와 자식 스레드 그룹을 관리하는 핸들러 정보
유저모드와 커널 모드 ?
유저레벨 프로세스에 대해 알아보기 앞서 유저모드가 무엇일까?
: 바탕화면에있는 임의의 프로그램을 실행중이라고 가정해보자, 이때 프로그램은 유저모드 또는 커널모드로 프로그램을 실행합니다. ftrace 로고를 보면 시스템 콜로 유저모드와 커널 모드를 자주 스위칭하는 동작을 확인할 수 있다.
일반적인 프로그램들은 사용자 모드에서 실행되므로 커널 모드에 대한 직접적인 접근이 불가능하다. 하지만 커널에 접근할 수 없으면 사용자 모드의 프로세스들이 파일을 쓰거나 불러올 수 없고 그래픽 처리와 같은 거의 모든 작업을 할 수 없다. 따라서 커널에 요청하여 커널 모드에서 처리하고 그 결과를 사용자 모드의 프로그램에게 전달하는 것이 바로 시스템 콜이다.
유저모드와 커널모드 나누는 기준
: 메모리 접근과 실행 권한 기준으로 두 모드를 구분한다.
: 일반적인 어플리케이션 개발자가 메모리 관리를 제대로 못하면 LockUP 이 발생할 수 있기떄문에 직접 접근하지 못하게 하고, 시스템 콜을 통해 특정 서비스를 커널에게 요청합니다.
커널 입장에서 시스템 콜을 통해 유저모드에서 요청한 서비스를 어떻게 처리?
시스템 콜의 종류는 다양하지만 커널의 시스템 콜 핸들러는 다음과 같이 동작
1. 시스템 콜에 전달한 인자의 오류 점검
2. 커널 내부 함수 호출
3. 유저 어플리케이션에서 요청한 정보를 알려줌
유저모드에 해당하는 파일은 무엇일까? 라즈베리파이의 경우 다음 경로에 리눅스 시스템 구동에 필요한 라이브러리가 있습니다.
이 라이브러리 파일이 유저 모드 코드라고 볼 수 있다.
리눅스에서 실행중인 유저 어플리케이션은 이 라이브러리와 링킹되어 메모리에 적재돼 실행
유저레벨 프로세스는 어떻게 생성될까?
바탕화면의 임의 프로그램을 실행할 수 있다고 해봅니다.
유저모드에서는 스스로 프로세스를 생성하지 못한다.
대신 리눅스에서 제공하는 라이브러리 (GNU) 의 도움으로 프로세스 생성 요청이 가능
유저 애플리케이션과 유저레벨 프로세스는 커널에게 어떤 의미인가?
유저 애플리케이션은 일반적인 프로그램파일을 의미하고
유저 레벨 프로세스는 이 애플리케이션을 실행하는 주체를 의미
유저레벨 프로세스 와 커널레벨프로세스의 가장 큰 차이점
실행출발점이 다르다
유저레벨 프로세스는 유저모드에서 fork() 함수나 pthread_create()함수를 호출
다음 경로에 있는 glibc 리눅스 라이브러리 파일의 도움으로 커널에 서비스를 요청해 생성
이처럼 유저레벨 프로세스가 커널에 어떤 서비스를 요청하려면 시스템 콜을 실행해야한다.
하지만 커널 레벨 프로세스는 커널모드에서 실행합니다.
커널의 kthread_create()함수를 호출해 프로세스 생성
유저 레벨 프로세스를 생성할 때 _do_fork()함수 처리 흐름
sys_clone() 함수 분석
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
sys_clone 이라는 함수 이름이 보이지 않는데
커널에서 제공하는 SYSCALL_DEFINES 매크로와 함께 함수 이름을 지정하면 커널 소스를 빌드하는 과정에서 지정한 함수 이름 앞에
'sys_' 접두사를 붙여서 심벌을 생성합니다.
즉 다음과 같이 시스템 콜 함수를 정의하면 sys_clone() 시스템 콜 함수가 생성합니다.
sys_clone() 함수는 _do_fork() 함수를 그대로 호출합니다.
유저레벨에서 생성한 프로세스와 스레드를 커널은 동등하게 처리합니다.
유저레벨프로세스 생성할 때 처리흐름은 아래와 같다.
- 유저 공간에서 fork()함수 호출하면 시스템 콜을 발생
- 커널 공간에서 sys_clone()함수 호출
- sys_clone() 함수는 _do_fork()함수를 호출해 프로세스를 생성
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
//자식 프로세스가 종료될 때 부모 프로세스에게 SIGCHILD 시그널을 보내라
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL, 0);
//CLONE_VM => 생성되는 자식과 부모간의 메모리를 공유하겠다는 옵션
}
커널 프로세스 생성 시 _do_fork() 함수의 흐름
커널 프로세스 생성할 때도 _do_fork()함수를 호출
커널 프로세스란 무엇일까?
: 시스템 콜 없이 커널 함수로 생성되어 커널 공간에서만 실행되는 프로세스 의미
: 커널 프로세스의 대표적인 예는 커널 스레드 , 커널 스레드는 커널 공간에서 시스템 리소스 (메모리,전원) 관리 및 수행
커널 스레드와 같은 커널 프로세스를 어떻게 생성하는지
보다시피 커널스레드 생성은 2단계로 나눌 수 있다.
- 1단계 : kthreadd 프로세스에게 커널 프로세스 생성 요청
: ktrhead_create() 함수를 호출해 kthreadd 프로세스에게 커널 프로세스 생성 요청하고 kthreadd 프로세스를 깨운다. - 2단계 : 커널 프로세스 생성
: kthreadd 프로세스는 깨어나 자신에게 커널 프로세스 생성 요청했는지 점검합니다., 프로세스 생성해달라는 요청이 있으면 프로세스 생성합니다.
이처럼 커널스레드 도 프로세스의한종류로 볼 수 있다.
그런데 커널 스레드는 언제 실행을 시작할까요?
커널은 시스템 부팅과정에서 대부분의 커널스레드를 생성합니다.
커널 스레드는 생성된 후 바로 일을 시작하며, 이후 배경작업으로 주기적으로 실행, 대표적으로 워커 스레드를 들 수 있다.
(반드시 부팅과정에서만 생성되는 것은 아니다)
- 리눅스 드라이버에서 많은 워크를 워크 큐에 큐잉하면 커널은 커널 스레드의 종류인 워커 스레드를 더 생성한다.
- 커널에서 메모리가 부족하면 페이지를 확보하는 일을 하는 kswapd 스레드를 깨워 실행한다.
유저 레벨 프로세스 실행 실습
기본 유저 레벨 프로세스 실행 실습 및 ftrace 로그 분석
ps 명령어를 이용한 프로세스 목록 확인
"ps -ely | grep bash "
터미널을 하나 더 열고 다시 명령어 입력하면
새로 생성된 프로세스를 볼 수 있다.
즉 새로운 프로그램을 실행하면 이에 해당하는 프로세스가 실행 된다는 사실을 알 수 있다. 이쯤은 기본
라즈베리파이 프로그램 중 geany 프로그램을 실행하고 ' ps -ely | grep geany '
14411 PID 프로세스로 확인된다.
'Unix & Linux > Kernel' 카테고리의 다른 글
Kernel, menuconfig <raspberrypi 3> (0) | 2022.10.27 |
---|---|
Kernel log (커널로그) 보는 방법 , domes (0) | 2022.07.12 |
ftrace 디버깅 노트(1), 샘플 (0) | 2022.07.11 |
objdump 바이너리 유틸리티 (0) | 2022.07.07 |
리눅스 커널 소스의 구조 ( Linux Kernel Structure ) (1) | 2022.07.07 |