서론
지난 SSR에서는 Kernel의 개념 그리고 System call의 기본적인 내용을 다루었다. 이번 주차도 저번 주차와 비슷한 개념들을 조금 더 깊이있게 살펴보려고 한다. 그리고 간단한 커널 모듈도 개발해보며 커널이 동작하는 과정을 조금더 알아보려고 한다.
본론
내용 정리
시작하기에 앞서 지난주차에 공부했던 개념들을 조금 정리하고 넘어가려고 한다.
<Kernel>
먼저 Kernel이다. 리눅스는 효율성을 위해 OS를 하나의 커다란 프로그램이 아닌 작은 프로그램으로 분할해놓았다. Kernel은 이 분할된 작은 프로그램 중 하나이다(shell, utility 또한). Kernel은 memory resident한 프로그램으로, 컴퓨터가 부팅되고 난 이후 꺼질때까지 항상 메모리에 상주하며, 사용자와 하드웨어 자원 사이에서 인터페이스 역할을 해준다.
<System call>
리눅스는 멀티 유저 시스템이다. 따라서 메모리 자원에 대한 보호가 필요하다. 이러한 문제점을 해결하기 위해 고안된 방법이 바로 '특정 사용자의 쉘에게 CPU를 양도해도, 연산할 시 I/O instruction을 못하게 막는다'이다. 이 방법에 의해 사용자가 I/O 작업을 할 때는 반드시 커널에게 요청하고, 커널은 요청된 I/O가 정상인지 확인한 뒤 커널이 가지고 있는 function으로 I/O를 진행해준다. 즉, 커널에게 요청이 들어오면 커널 내의 function을 이용해 I/O를 진행하며 이러한 요청을 바로 System call이라고 부른다.
<mode bit & chmodk>
현재 OS kernel이 돌아가고 있으면, CPU의 mode bit는 kernel mode로 세팅된다. kernel mode 상태에서는 어더한 수행의 제한이 없다. 반면 user mode 상태에서는 I/O 연산, 특별한 레지스터 접근, 현재 프로세스의 범위 밖 주소에 접근 불가하다.
하지만 우리가 a.out이라는 파일에 read로 '어떤 파일을 읽어와'라고 코딩을 해놓으면? 우리는 user mode일텐데 어떻게 I/O이 수행되는걸까? 사실 이 경우, 우리가 소스코드에 써놓은 I/O 관련 코드는 우리 눈에만 그렇게 보인다. 실제 컴파일 과정을 거친 바이너리 코드 안에는 I/O statement가 없다. 소스 코드에 존재하는 read같은 I/O가 필요한 함수들은 컴파일이 될 때 컴파일러에 의해 전부 chmodk 명령을 수행하게끔 변경된다. chmodk(change mode protection kernel)는 privileged 명령이다. 따라서 일반적인 user 프로그램은 이 수행을 하지 못하므로 chmodk 부분부터 CPU 사용권한을 뺏기며 trap에 걸린 후 이때부터 trap handler 루틴이 수행된다.
<System call 시에 수행되는 전체 로직 정리>
1. 내가 작성한 소스코드에 read write 같은 I/O 관련 statement가 있는 상황이다
2. 이 소스코드를 컴파일하면 read, write 같은 I/O statement 부분이 chmodk로 변경된다
3. 이때까지 CPU의 mode bit는 user mode였다. 하지만 런타임이 실행되면 chmodk가 실행이 되면서 우리는 trap에 걸린다
4. trap에 걸리면 trap handler 루틴을 수행하기 위해 하드웨어가 자동적으로 mode bit를 kernel mode로 변경한다
5. 현재 trap은 커널 프로그램 안에 존재하는 상태이다. trap handler에서 왜 요청이 왔는지를 보고, 요청에 해당하는 커널 함수 즉 I/O와 관련된 함수를 호출한다
6. 해당 함수를 호출하면서 권한 체크를 우선 실행하고, 문제가 없으면 실제 실행이 시작된다
7. 실행이 완료되면 mode bit를 다시 user mode로 변경한다
->이렇게 모든 프로그램은 user mode, kernel mode를 반복하게 되어있다

System call wrapper routine
현재 main() 함수 내에 add, sub 그리고 printf() 함수가 있다. 이 중 printf() 함수는 내가 만든 함수가 아니라(def printf X) 라이브러리에 이미 존재하고 있는 함수이다. 라이브러리 내부에 구현되어있는 이 printf 함수의 주 목족은 I/O이다. 하지만 멀티 유저 시스템인 리눅스의 user mode에서는 I/O 작업이 불가능 하므로 System call을 호출하여 커널에게 작업을 요청해야 한다. 아래 사진에서 printf 옆에 (3)을 볼 수 있는데, 이는 라이브러리 함수를 의미한다. 또한 (2)는 system call이다.
printf(3) 부분을 보면 printf 함수 구현 로직 내부에서(write()) 결국은 I/O를 수행하기 위해 write라는 wrapper routine을 호출하게 된다. 이는 chmodk가 호출됨과 동시에 kernel mode로 넘어가 커널 내부에 존재하는 sys_write 함수를 호출한다.
-> 즉, printf 함수 내부의 write 함수가 System call을 호출하기 위한 wrapper script로 구현되어 있다.

Process Management
커널의 가장 중요한 임무 중 하나는 바로 process management이다. 커널은 아래로 하드웨어 자원들, 위로 유저 프로그램들을 지원하는 역할을 하는데 이를 조금 더 효율적으로 관리하기 위해 각각의 하드웨어들마다 (Hardware) Data Structure를 가진다. 여기서 말하는 Data Structure란, 실제 메모리, 디스크, 디바이스 등의 크기가 어느 정도인지, 어디서부터 어디까지 사용되고 있는지와 같은 정보들을 저장하고 있는 구조라고 생각하면 된다.
또한 현재 돌아가고 있는 프로세스들, 예를 들어 shell, ppt 같은 프로그램이 실행중이라면 이런 프로세스들을 관리하기 위한 Data Structure 또한 필요할 것이다. 우리는 이를 PCB(Process Control Block)이라고 부른다.
OS에서는 이러한 HW, SW(process)를 관리하기 위해 필요한 정보를 가지고 있어야 하는데, 이를 메타 데이터(metadata)라고 부르며, 위에서 설명한 Hardwate Data Structure, PCB가 메타데이터에 속한다.

PCB
프로세스를 관리하기 위해 필요한 PCB같은 메타데이터에는 어떠한 내용이 들어갈까? PCB에는 다음과 같은 정보들이 들어있다.
- 해당 프로세스의 PID
- proiority
- 프로세스 상태
- 프로세스가 메모리 그리고 disk의 어디에 올라와 있는지에 대한 정보
- directory(현재 실행중인 환경이 어디인지)
- 터미널
- open files(리눅스에서는 모든게 파일. I/O, 키보드, 모니터 등)
- state vector save area
PCB 중 중요한 것들 중 하나가 바로 state vector save area이다. 리눅스는 멀티 유저 시스템이고, 한 유저가 CPU를 사용하고 있다면 다른 유저들은 대기 상태에서 기다려야한다. 그리고 CPU를 다 사용했으면 다른 유저에게 CPU를 양도하는데, 양도하기 전 현재 CPU 안의 모든 데이터 값을 저장하고 넘겨줘야 할 것이다. 이러한 갓을 저장하는게 바로 PCB 안의 state vector save area이다. 이 공간은 여러 레지스터들을 저장하고 있는 곳이라고 보면 된다.

User Stack & Kernel Stack
<User Stack>
User Stack은 user mode 프로세스의 작업 공간이다. User Stack은 user mode에서 실행되는 각 프로세스에 할당된 개별 메모리 영역이다. user mode 코드 실행에 필수적인 다양한 요소를 위한 동적 저장 공간 역할을 한다.
사용자 스택의 주요 구성 요소에는 무엇이 있는지 살펴보자
- 지역 변수: 함수 내에서 선언된 변수와 해당 값은 사용자 스택에 저장된다.
- 함수 매개 변수: 함수 호출 전에 함수에 전달되는 인자가 사용자 스택에 push된다
- 프레임 포인터: 스택 프레임 구조를 유지하여 중첩된 함수 호출에서 지역 변수 및 함수 매개 변수를 식별할 수 있도록 한다(*)
- 반환 주소: 함수실행이 끝나면 호출자의 위치를 나타내는 반환 주소가 User Stack에 push 된다
- 자동 변수: 함수 매개 변수 및 지역 변수를 포함하여 블록 범위 내에서 선언된 변수가 사용자 스택에 저장된다
- 임시값: 중간 결과 또는 함수 호출 간에 전달되는 값은 임시 데이터가 User stack에 저장될 수 있다
그렇다면 Use Stack 작업은 어떻게 이루어질까? Stack 작업을 하려면 Stack Pointer가 필요하다.
- 스택 포인터(rsp): Stack Pointer는 특수 레지스터로서 User Stack의 현재 상단을 추적하여 스택 요소에 대한 효율적인 액세스를 허용한다
- 스택 Push: 새로운 데이터가 스택 포이터를 감소시키고(-) 데이터를 새 주소에 저장하여 스택의 상단(top)에 추가된다
- 스택 Pop: 데이터가 스택 포인터 주소의 값을 읽고 스택 포인터를 증가시켜 스택의 상단에서 검색된다(해제된다?)
-> 리버싱을 한 번 공부하고 보니 이해하기 훨씬 수월했다
<Kernel Stack>
Kernel Stack은 OS에 의해 독점저으로 사용되는 권한이 부여된 메모리 영역이다. 커널 코드 실행고 시스템 상호 작용을 위한 자업 공간 역할을 한다
커널 스택의 주요 구성요소를 살펴보자
- 커널 코드 변수: 커널 함수 내에서 선언된 변수와 해당 값이 커널 스택에 저장된다
- 시스템 호출 매개 변수 및 반환 값: 시스템 호출에 전달되는 인수와 해당 반환 값이 커널 스택에 저장된다
- 커널 데이터 포인터: 프로세스 디스크립터 및 메모리 관리 정보와 같은 커널 데이터 구조에 대한 포인터가 커널 스택에 저장된다
- 하드웨어 컨텍스트 정보: 인터럽트 발생 시점의 하드웨어 컨텍스트, CPU 레지스터, 프로그래머 카운터 등이 커널 스택에 저장된다.
커널 스택 작업도 간단하게 살펴보자
- 스택전환: 프로세스가 User mode에서 Kernel mode로 전환될 때, Kernel Stack Pointer로 전환되여 User mode 데이터에 대한 무단 엑세스를 방지하고 격리한다.
-> 과정: System call이나 인터럽트가 발생하면, CPU는 현재 사용자의 rsp를 저장하고, 커널 스택 포인터로 전환한다. 이때 커널 스택은 프로세스 컨텍스트와 하드웨어 상태(레지스터 값)을 저장한다
- 인터럽트 처리: 인터럽트가 발생하면 CPU는 현재 실행 중인 코드의 상태를 저장하고 인터럽트 핸들러를 호출한다. 이 저장 작업은 커널 스택을 통해 이루어진다.
Creating a child process
컴퓨터를 부팅하면 제일 먼저 커널 프로세스가 올라온다. 그리고 만약 터미널이 10개이면, 각 터미널마다 하위에 쉘을 만들어준다. 그러면 커널 밑에 쉘이 10개가 생성되는데, 그 중 하나의 쉘에서 ppt를 실행하면 해당 쉘 아래에 ppt가 실행된다. 이런 식으로 부모-자식 관계의 계층관계가 생성된다. 즉, 컴퓨터를 사용하는 과정에서는 반드시 child process를 생성하는 일이 생기게 된다.
하나의 프로세스가 실행되면, 그 프로세스 안에는 user stack이 존재하고, 해당 프로세스 정보를 가지고 있는 PCB 그리고 kernel stack이 존재한다. 만약 trap이 걸려 system call이 호출되면, 커널 프로세스의 함수를 호출한다.
이제 Child Process를 만드는 순서에 대해 살펴보자.
step1 (PCB)
자식 PCB를 위한 공간을 만들고, 부모 PCB의 정보를 복사해온다. 이로써 작업 공간이 자식의 작업 공간이 된다. 다시 말해, 부모가 쓰던 리소스들을 자식도 공유하게 된다는 소리이다
step2 (Image)
1. child process가 올라올 메모리 공간을 확보한다.
2. 하드웨어 메모리 리소스마다 존재하는 메타데이더(앞에서 살펴본) 내부에는 메모리 전체 크기는 얼마인지 등의 정보가 들어있다. 이 메타데이터를 참고하여 자식 프로세스가 들어올 공간을 확보한다.
3. 공간을 확보했다면, 그 후에 부모의 image를 그대로 복사해온다. 이로써 부모와 자식은 동일한 코드를 가지게 된다.
step3
이후 디스크로부터 새로운 image를 로드한다. 즉, 자식 프로세스를 생성하기 위해 2단계에서 메모리 공간을 확보하고, 부모 image(초기값)을 세팅한 뒤에 실제 디스크에서 원하는 프로그램을 가져오는 로직...정도로 이해하면 된다.
-> 예를 들어 사용자가 ppt 프로램을 실행하려고 할 때, 1~2 단계에서 자식 프로세스를 위한 공간 확보&초기 세팅이 완료되었다고 하자. 이후에 실제 ppt 프로그램을 디스크에서 가져오는게 step3이다.
step4
이제 새로 생성된 child process의 PCB를 cpu ready 큐에 넣어놓고,CPU를 사용하기 위해 대기시킨다. 대기시키는 이유는 뭘까? 바로 아직 부모 프로세스가 CPU를 사용하고 있기 때문이다.
이러한 4단계의 step을 2개의 system call로 나누어서 표현할 수 있다.
1. fork => 1,2단계를 통틀어 fork라고 한다 = 부모의 복제본을 만든다
2. exec => 3,4단계를 통틀어 exec라고 한다 = 디스크로부터 자식의 (새로운)image를 가져온다.
fork
fork에 대해 조금 더 자세히 살펴보자. 위에서 fork는 step1,2 과정을 의미한다고 했다. 부모 프로세스의 PCB, image를 자식 프로세스에게 그대로 복사해주는 과정말이다. 여기서 중요한 사실이 있다. 자식 프로세스를 생성하기 위해 fork가 호출되고 나면 반환되는 값이 2개라는 점이다! 이게 무슨 말일까? 그림과 함께 살펴보자.

위의 그림을 보자. 부모가 fork() 함수를 호출함으로써 자식(프로세스)이 생성된다. 여기서 자식은 부모가 생성한 프로세스이기 때문에 부모의 모든 정보가 복사되어 리소스들을 서로 공유하게 된다.
fork()의 호출이 끝나면 부모 프로세스에서는 fork() 아래에 위치한 코드가 수행될 것이다. 이것이 첫 번째 return이다. 이후 fork()에 의해 자식이 생성되면, read 큐에 CPU 점유를 기다리게 된다(부모의 CPU 사용이 끝날 때 까지). 그러다가 자식 프로세스가 CPU를 점유하게 되면 실행이 시작되느데, 여기서 어떤것이 실행되는지가 중요하다. 뭐가 실행될까? 자식 프로세스는 부모 프로세스의 코드가 그대로 복사된 사본이기 때문에, 부모 프로세스와 똑같은 코드(fork() 아래에 위치한 코드)가 실행된다. 이것이 두 번째 return이다.
또한 자식은 부모의 PCB도 복사해왔기 때문에, CPU의 state vector도 전부 자식도 가지게 된다. 그렇기 때문에 위에서 부모가 fork한 뒤 return을 하면, 자식 역시도 return하게 된다. 계속 말하지만 state vecto들을 부모, 자식이 둘다 동일하게 가지고 있기 때문이다.
<정리>
1. 부모 프로세스가 fork()를 호출
2. 호출된 fork()가 실행되며 새로운 자식 프로세스가 생성
-> 이때 자식 프로세스는 부모 프로세스의 메모리 공간을 그대로 복사
3. 자식 프로세스가 생성되며, ready 큐에서 대기
4. 부모 프로세스의 CPU 점유가 끝나면 자식 프로세스가 CPU를 점유
-> 이때 부모 프로세스는 코드의 처음으로 돌아가는 것이 아니라 fork() 이후의 코드를 실행
다만, OS가 한 번의 fork로 두번 return을 하게 되면 혼동을할 수 있는데, 이를 막기 위해 '서로 다른 return값을 전달'해주는 방법이 사용된다. 아래 그림과 함께 살펴보자.

pid = fork() 부분처럼, fork()가 호출되고 나면 pid를 return한다. 그리고 fork()는 총 2번 return한다고 했었다(부모, 자식). 이때 반환되는 pid 값에 따라 이 반환이 부모에서부터 왔는지, 자식에서부터 왔는지 알 수 있다.
- pid == 0: child 프로세스에서 return
- pid != 0: 부모 프로세스에서 return
-> 이렇게 한 번의 fork()로 2개의 return 값이 반환되게 된다.
출처
[1] 까망눈 연구소, (2022), kernel of linux - system call(1)
https://jeongzero.oopy.io/ce68cf2c-4c14-431b-950f-e32eeae42e30
[2] olc, kernel of linux
https://olc.kr/course/course_online_view.jsp?id=35&cid=51
[3] junhyeok_kim.log, (2024), [CS-KEYWORD] User Stack and Kernel Stack
https://velog.io/@junhyeok_kim/CS-KEYWORD-User-Stackand-Kernel-Stack
'Cyber_Sec > Linux Kernel' 카테고리의 다른 글
| Kernel of Linux - Introduction (0) | 2025.03.15 |
|---|