Kernel/Theory

[Kernel of Linux] 1. Introduction

Karatus 2021. 11. 16. 00:14
반응형

계속 커널 공부를 하고는 있지만 뭔가 기본기가 부족하다는 느낌을 많이 받고 있다. 그래서 예전부터 알고 있던 까망눈연구소 님의 블로그 글 중에 커널과 관련된 글을 강의를 통해 잘 정리하고 계신 것을 발견하고 나도 똑같이 해당 강의를 통해 기본기를 다지려고 한다.

https://olc.kr/course/course_online_view.jsp?id=35&s_keyword=Kernel&x=0&y=0 

 

OLC CENTER

 

olc.kr

 

1. What is an Operating System?

프로그램은 user와 hardware 사이의 중개인 역할을 수행한다. 이때 OS가 관여를 하는데 이 중개인 역할이 바로 OS다. 아래(lowlevel)로는 hardware 자원을 관리하고, 위(highlevel)로는 프로그램들을 support 한다. 다른 말로는 하드웨어를 감추고 프로그램을 지원한다고 할 수 있다.

 

2. What is a Kernel?

커널이 뭔지 보기 전에 먼저 다른 예시를 통해 큰 틀을 이해해보자. MS에서 개발한 Office 프로그램은 왜 하나의 프로그램, 즉 word, excel, ppt 등을 하나의 Office라는 프로그램으로 만들지 않고 다 따로 만든 것인지를 생각해보자. 그 이유 중 하나로 생각해볼 수 있는 것은 바로 비효율적이기 때문이라는 것을 떠올릴 수 있다. 예를 들어 word만 사용할 건데 word를 실행시키려면 다 같이 실행되는 상황을 생각해보면 될 것 같다.

마찬가지로 OS 또한 하나의 거대한 덩어리로 만들게 된다면 무척이나 무겁고 부팅 시 메모리에 올라가는 데에도 상당한 시간이 소요될 것이다. 그래서 OS도 여러 프로그램으로 나뉘어 동작하게 되는데 여러 프로그램 중 하나가 바로 kernel이다.

kernel은 독립된 프로그램이면서 memory resident 하다. memory resident 하다는 것은 메모리에 항상 상주하고 있다는 말이다. 여타 다른 OS 프로그램들은 메모리에 올라가 있을 때도 있고 아닐 때도 있기 때문에 memory resident 하다고 할 수 없으며 이를 disk resident라고 한다. 커널과 다른 프로그램들과의 가장 큰 특징 중 하나다.

  • kernel
    - memory resident 함. 단순한 C 프로그램이다.
  • utility
    - command <- disk resident 함(loaded on demand). disk resident "program"이다.
    - job 이라고도 불린다.
  • shell
    - 특별한 utility이다. Job(utility)을 컨트롤하는 임무를 맡았다.
    - 여러 utility를 관리해주는 프로그램의 필요성에 의해 생겨났다.
  • file
    - UNIX에서는 "sequence of bytes"를 file이라고 정의한다.
    - 해당 정의에 제한은 없으며 record, block도 file이지만 I/O devices 또한 special files로서 다뤄진다.
  • standard file
    - special files 중 terminal은 특별히 standard file이라고 불린다.
    - standard output: screen
      standard error  : error message
      standard input  : keyboard

 

3. How Kernel-Shell-Utilities are Related

booting을 시작하면 kernel 프로그램이 메모리에 올라오는 과정을 거친다.

이후 shell이 메모리에 올라오면서 user가 input을 줄 때까지 대기하고 있다가 입력을 감지하면 입력된 command를 처리해주게 된다. 리눅스는 multi-user system이기 때문에 여러 user가 각기 다른 환경에서 각자의 command를 실행시켜주는 게 가능하다.

한 가지 상황을 예로 들면 B user가 ppt를 실행하게 되면 shell은 child process를 생성해서 실행시켜 준다. 이는 fork() syscall을 통해 parent process를 복제한 뒤 PCB(혹은 TCB)를 수정해주는 식으로 동작한다.

[요약]
- kernel은 항상 메모리에 상주해있다. (memory resident)
- kernel을 제외한 나머지는 모두 utility(command, job)다. 필요할 때만 메모리에 올라와 실행되고 내려간다. (disk resident)
- shell은 utility를 control 하는 일을 한다.

 

4. Linux vs. Windows

리눅스와 윈도우 OS의 가장 큰 차이점 중 하나는 바로 하나의 시스템에 몇 명의 user가 동시에 사용할 수 있는지다. 리눅스는 multi-user system으로 resource를 최소화(minimize)하는 게 필요하다. 나 말고도 다른 user도 존재하기 때문에 자원을 쪼개야 하기 때문이다. 반대로 윈도우는 라이선스를 사서 나만이 하드웨어에 올려놓고 쓰기 때문에 딱히 resource에 대한 한계를 정할 필요가 없다.

윈도우는 자원을 자유롭게 사용할 수 있기 때문에 자원의 분배 혹은 공유를 생각지 않고 GUI(Graphical User Interface)를 이용해서 정보들을 자유롭게 표시하는 환경을 갖추고 있다. 반대로 리눅스는 그렇지 않기 때문에 내가 미리 어떤 정보가 있는지 알기 위해 command들을 숙지하고 있어야 한다. 그래서 간단히 정보만을 표시해주는 CUI(Character User Interface) 환경을 사용한다.

물론... 이는 옛날 얘기라는 것을 누구나 알고 있다. 하지만 막 computer가 발달하기 시작한 시점, 예를 들어 DOS, UNIX가 메인으로 쓰였던 시절에는 CUI가 당연한 것이었다. 그렇기 때문에 옛날 상황을 고려해서 해당 강의를 보면 될 것 같다.

 

5. Protection

리눅스 환경에서는 다음과 같은 일이 발생할 수 있다. process P1이 불법적으로 P2의 정보를 read or write 하려는 상황을 생각해보자. 이는 있어서는 안 되는 일임이 분명하다. 또한 정보가 탈취되고 나서 detect 하거나 사후처리를 한다는 것은 불가능하다. 그렇기 때문에 protection, 즉 예방을 해야 한다.

보통 그런 정보들은 memory나 disk에 저장되어 있다. 

cpu마다 실행 가능한 프로세스는 하나다. 여기서 생기는 문제는 어떤 프로세스가 cpu 하나를 점유하고 나서 I/O를 하려고 할 때 자신의 영역을 벗어나는 곳에 access 할 수도 있다는 것이다. 이미 cpu는 해당 프로세스가 점유하고 있으므로 다른 프로세스들은 그 프로세스가 하려는 동작을 막을 수는 없다.

그래서 이를 해결할 수 있는 방법이 바로 모든 I/O instructions는 kernel을 통해서만 가능하게끔 만드는 것이다. kernel은 해당 I/O가 적합한 access right를 가지는지를 체크한 뒤 이상이 없을 때에만 I/O를 수행하는 식으로 위와 같은 문제를 prevention(예방)할 수 있게 되는 것이다.

정리를 하자면 user mode에서 동작하는 process가 I/O를 수행하기 위해서는 무조건 kernel에게 I/O를 해달라고 요청해야만 가능하도록 한다. 이렇게 kernel에게 I/O를 요청하는 행위를 바로 system call이라고 부른다.

위의 도식은 instruction을 실행하는 단계를 나타내고 있다. 여기서 system call을 도와주기 위해 하드웨어적인 support가 추가되었는데 그것이 바로 mode bit다. 간단히 말해 user mode와 kernel mode를 표시, 그리고 구분해주는 역할을 한다. 1로 세팅되어 있다면 user mode, 0으로 세팅되어 있다면 kernel mode를 의미한다.

이 mode bit에 세팅된 값에 따라 access 할 수 있는 섹션의 범위와 기능이 달라진다. kernel mode에서는 어떠한 영역이든지 접근이 가능하고 어떤 기능이든지 사용이 가능하다. 반대로 user mode에서는 자신이 가진 address space에만 접근이 가능하고 한정된 기능만 사용할 수 있다. I/O라든지 특정 register로의 접근은 거부되는 식으로 말이다.

<mode bit에 따른 차이 정리>

  • kernel mode
    - memory: 모든 영역 access 가능
    - op-code: 어떤 명령이든 처리 가능
  • user mode
    - memory: 자신이 가진 영역만 access 가능
    - op-code: privileged op-code와 피해를 줄 가능성이 있는 op-code를 제외한 나머지 명령 처리 가능

그렇다면 언제 mode bit를 사용하여 access control을 수행하는지 살펴보자.

cpu는 자신이 가지고 있는 address를 기준으로 하나의 instruction을 fetch 하고 decode 한 뒤 execute 해주는 역할을 한다. 그 과정에서 2번 mode bit을 체크하여 prevention을 수행하는 과정이 있다. 해당 검사 루틴은 MMU(Memory Management Unit)이 수행한다.

첫 번째로 pc(program counter)가 가진 주소를 통해 memory에 access 하려 할 때 MMU가 검사한다. kernel mode라면 그냥 통과하지만 user mode라면 그 프로세스가 가진 address space 안에 들어오는지 검사한다. 만약 범위를 벗어났다면 그 즉시 page-fault를 일으켜 해당 프로세스를 중지시킨다. cpu가 뺏겨버리는 것이다.

두 번째로 op-code를 decode 하는 과정에서 해당 op-code가 현재 mode에서 실행 가능한지 판별한다. kernel mode라면 어떤 op-code라도 통과하지만 user mode라면 privileged op-code인지 검사를 받는다.

정리하자면 OS kernel이 돌아갈 때는 kernel mode, 이외의 다른 프로그램이 돌아갈 때는 user mode로 mode bit이 세팅되어 있다는 것이다.

하지만 여기서 한 가지 의문이 든다. 내가 프로그램 짤 때 쓰는 printf() 같은 함수들은 다 I/O를 수행하는 함수들인데 그럼 어떻게 되는 거지?

실제로 소스코드를 컴파일해서 바이너리로 만든 후에 그 바이너리를 까 보면 그곳에는 I/O와 관련된 동작이 하나도 없다. 있는 것처럼 보여도 실제로는 없다. 이유는 컴파일러가 I/O statement가 있는 곳마다 chmodk(change CPU protection mode to Kernel) 명령어를 실행시키도록 한다.

그리고 chmodk 명령어를 집어넣기 이전에 미리 system call의 종류 및 parameter를 준비해놓는다. 이것으로 kernel에게 system call을 이용하기 위한 정보를 알려준다.

chmodk 명령어는 privileged instruction이다. privileged이다 보니 user mode에서 바이너리가 실행되면 cpu를 뺏기게 된다. 즉 hardware trap(interrupt)이 걸린다.

trap이 걸리게 되면 cpu state vector(including return address)를 저장하고, cpu_mode_bit을 kernel mode로 바꾸고, trap handling routine으로 분기하여 trap handler를 실행하게 된다.

trap handler는 제일 먼저 read/write permission check를 해준다. 만약 문제가 없다면 요청한 system call을 수행한 후 mode bit를 user mode로 돌리고 state vector를 복구시킨 뒤 다시 프로세스로 돌아가게 된다.

  1. 소스코드 내에 I/O operation이 존재
  2. 컴파일러가 I/O statement가 존재하는 부분을 [system call prepare + chmodk 명령어]로 바꿈
  3. 바이너리가 실행된 후 chmodk 명령어를 만나면 user mode이기 때문에 trap이 걸려 cpu를 뺏기게 되고 trap handler가 실행됨
  4. trap handler는 system call prepare 정보를 보고 요청한 I/O 관련 function을 호출함
  5. I/O를 수행하기 전, 해당 프로세스가 access 가능한 disk 영역에 접근하려는지 permission check를 수행함
  6. 문제가 없다면 I/O를 수행한 뒤에 다시 user mode로 복귀함

kernel이 memory resident였던 이유는 프로세스에서 사용된 library functions로부터 어떤 system call이 올지 모르기 때문에 항시 대기하면서 체크해야 하기 때문이다.

library function과 system call은 아주 다른 것이니 유의해야 한다. library function은 단순히 programming library로부터 제공되는 함수일 뿐이다. mode switching 기능도 없고 I/O도 못하니 당연히 system call과 구분되어야 할 것이다. 정확한 차이점은 아래 링크에서 보면 된다. 꼭 기억하자!

https://pediaa.com/what-is-the-difference-between-system-call-and-library-call/

 

What is the Difference Between System Call and Library Call - Pediaa.Com

The main difference between System Call and Library Call is that System call is a request to the kernel to access a resource while library call is a ....

pediaa.com

결국 내가 만든 프로그램은 실행되고 I/O operation을 수행할 때마다 user mode와 kernel mode를 왔다 갔다 하게 된다. 그렇기 때문에 user mode에서 함수를 사용하기 위해 user stack이 필요하고 마찬가지로 kernel mode에서 사용하는 함수들에게 필요한 kernel stack이 따로 필요하다. 결론은 모든 프로세스는 각각 2개의 stack을 가지게 된다.

man command를 이용해서 여러 명령어들의 매뉴얼을 확인할 수 있다. 옆에 붙은 번호에 따라 분류가 되어 있다.

반응형