문제 출처: https://docs.google.com/document/d/1ebOe9TvEFWVpae8cszHVoptYxwn8QACE/edit?tab=t.0
수정한 커널을 컴파일하고 설치한 후, 시스템이 정상적으로 부팅되는지 확인하는 부팅 테스트를 진행하고, dmesg 명령어를 통해 부팅 과정에서 발생한 커널 로그를 확인한다. 만약 커널 내부의 특정 기능만 수정되었다면, kselftest와 같은 명령어를 통해 유닛테스트를 진행한다. 또한, 모듈 및 드라이버 테스트, 스트레스 테스트, 회귀 테스트, 보안 테스트 등을 진행해야 한다.
Interrupt란 CPU가 실행 중인 작업을 잠시 멈추고 더 중요한 작업(예: 입출력, 예외 처리)을 처리한 후 다시 원래 작업으로 돌아가는 메커니즘이다. interrupt에는 두 종류가 있는데 먼저, software interrupt는 프로그램이 의도적으로 명령어를 통해 발생시키는 interrupt다. 보통 Systeml Call이나 Exception 상황에서 발생한다. 반면에 hardware interrupt는 외부 하드웨어 장치(예: 키보드, 마우스)가 CPU에 신호를 보내어 발생하는 interrupt다. 보통 장치의 입력시 발생하며, 이때 Interrupt Controller를 통해 CPU가 interrupt를 수신한다. 두 interrupt는 성질이 달라 서로 구분해서 구현해야 한다. 예를 들어, 소프트웨어 interrupt는 프로세스의 요청으로 발생하고 하드웨어 interrupt는 외부 장치로부터 발생한다. 따라서 이를 각각 처리하는 handler가 필요하며, 전자의 경우 System Call Handler에 의해, 그리고 후자의 경우 Interrupt Controller에 의해 처리가 된다. 또한, 보통 하드웨어 interrupt가 우선순위가 더 높기에 이를 구분할 필요도 있다.
시스템 콜은 프로세스가 커널의 기능을 이용하기 위해 의도적으로 호출하지만, 인터럽트는 의도와 관계없이 예상치 못한 외부 상황이나 예와 처리 등에 의해서 발생된다. 인터럽트가 걸리면 먼저, 현재 실행 중이던 프로그램의 상태(레지스터, 프로그램 카운터 등)를 저장한다. 그러고 나서 다른 작업을 처리한 후, 저장했던 프로그램의 상태를 복원하여 이전 상태로 돌아간다.
thread는 프로세스 내에서 실행되는 가장 작은 실행 단위이며, process는 실행 프로그램을 의미한다. thread의 경우 같은 프로세스의 메모리 및 자원을 공유하지만, process는 각 프로세스마다 별도의 메모리를 갖는다는 차이가 있다.
IPC란 Inter Process Communication의 약자로, 프로세스 간 데이터를 주고 받는 기술을 의미한다. 왜냐하면 프로세스는 독립적인 메모리를 할당받기 때문에 프로세스 간 직접 데이터를 주고받을 수 없다. 따라서 IPC를 통해 데이터를 주고 받는다.
thread 간의 context switching은 동일 프로세스 내 혹은 다른 프로세스 간의 방식으로 두 가지가 있다. 우선 같은 프로세스 내부에서의 context switching은 현재 실행 중이던 thread의 스택과 레지스터만 변경하여 전환이 가능하지만, 다른 프로세스 간의 thread context switching은 현재 프로세스를 다른 프로세스로 바꾸고 해당 프로세스의 thread로 변경하여야 하기 때문에 전자와 달리 스택 및 레지스터와 더불어 page 테이블도 변경해야 한다는 점이 다르다.
Non-preemptive sceduling이란, CPU가 한 프로세스에 할당되면, 그 프로세스가 스스로 종료하거나 I/O에 의해 대기 상태로 전환되기 전까지 CPU를 계속 사용하는 방식의 스케쥴링 기법이다.
각 앱을 하나의 프로세스로 독립해서 실행하는 것이 더 낫다. 왜냐하면 한 앱에서 크래시가 발생하더라도 다른 앱에 영향을 미치지 않아야 하고, OS는 프로세스 단위로 메모리 사용량을 제한하고 백그라운드 프로세스를 효율적으로 관리할 수 있기 때문이다. 또한 한 앱을 백그라운드에서 실행하면서 다른 앱을 실행하는 멀티테스킹 측면에서도 이점이 있다. 예를 들어, 음악 앱을 백그라운드에서 실행하면서 다른 앱을 실행하는 경우를 들 수 있다.
우선 sequential program은 단일 thread에서 실행되므로 오류 탐지가 비교적 더 쉽고, 동일한 실행 흐름을 보장하기에 디버깅이 더 쉽다. 반면에 multihtread program은 실행 순서가 매번 달라질 수 있으며 이에 따라 오류가 재현되지 않아 오류 탐지가 어려울 수 있다. 즉, 동기화 문제로 디버깅이 더 어렵다. 따라서 multithread program은 오류 탐지를 위해 별도의 semaphore와 같은 별도의 동기화 도구가 필요하다.
여러 스레드나 프로세스가 동시에 접근할 수 있는 공유 자원에 대해, 한 번에 하나의 스레드만 접근 가능하도록 보장하는 특정 구간을 의미한다. 만약 동시 접근이 발생할 경우 race condition과 같은 문제가 발생할 수 있으며, 이를 방지하기 위해 semaphore와 같은 동기화 기법이 필요하다.
공유 자원에 대한 접근을 제어하고 동기화를 관리하는 동기화 기법이다. 동시 접근의 경우 발생 가능한 race condition 문제 등을 방지하기 위해 필요하다. 주요 연산은 Wait과 Signal 연산이다. 세마포어에는 카운트 값이 존재하는데, 이 카운트가 0이면 해당 스레드 혹은 프로세스는 대기하여 대기 큐에 들어가게 되고, 0이 아니면 카운트 값을 1 감소시킨 후 자원을 할당한다. 이 과정이 Wait이다. 반면에 Signal은 다음과 같다. 카운트 값을 1 증가시킨 후 만약 대기 큐에 스레드 혹은 프로세스가 존재한다면 대기 중이던 스레드 혹은 프로세스를 깨운다. 코드로 설명하면 다음과 같이 간단히 구현할 수 있다.
def wait(semaphore_cnt): if semaphore_cnt == 0: add_to_wait_queue() else: semaphore_cnt -= 1 # def signal(semaphore_cnt): semaphore_cnt += 1 if is_not_empty_queue(): wake_up_wait()
가상 주소(Virtual Address)는 프로그램이 실행 중일 때 사용되는 논리적 주소로, 프로그램은 자신에게 할당된 가상 주소를 통해 메모리에 접근하게 되고, OS는 이 가상 주소를 실제 주소로 변환하여 처리한다. 가상 메모리(Virtual Memory)는 물리적 메모리가 부족할 때, 하드디스크와 같은 보조 기억 장치를 활용하여 프로그램이 사용할 수 있는 주소 공간을 확장하는 메커니즘이다. OS는 물리적 메모리의 한계를 넘는 메모리 공간을 제공할 수 있으며, 프로그램은 실제 물리적 메모리가 부족하더라도 마치 더 많은 메모리를 가진 것처럼 동작할 수 있다.
기본적으로 각 프로세스는 독립된 가상 메모리 공간을 갖기 때문에, 1, 2, 3, 4 주소에 있는 변수를 바꿔도 서로 영향을 주지 않는다. 하지만, 만약 공유 메모리를 사용할 경우, 한 프로세스가 해당 메모리의 값을 변경하면 다른 프로세스에도 영향을 끼칠 것이다.
현대 운영 체제에서 사용하는 가상 메모리 시스템은 각 프로세스에게 독립적인 주소 공간을 할당한다. 각 프로세스는 자신만의 가상 주소 공간을 가지게 되며, 운영 체제가 이를 물리적 주소로 변환해준다. 이렇게 하면 각 프로세스는 자신에게 할당된 메모리 공간을 사용한다고 생각하지만, 실제 메모리의 서로 다른 주소를 참조하게 된다.
논리 주소(logical address)는 프로그램이나 프로세스가 사용하는 주소로, 실제 메모리에서 사용되는 주소가 아니라 가상 시스템에서 사용되는 주소이며, 운영 체제에 의해 할당된다. 반면에 물리 주소(physical address)는 실제 메모리의 물리적인 위치를 가리키는 주소이며, 하드웨어가 사용하는 주소다. 실제 메모리에 저장된 데이터의 물리적인 위치를 나타낸다.
MMU(Memory Management Unit)
먼저, 가상 주소에서 페이지 번호와 페이지 오프셋을 추출한다. 그리고 MMU는 해당 페이지 번호를 활용해 페이지 테이블을 조회한 후, 페이지 테이블에서 물리적 페이지 번호를 찾는다. 그리고 물리적 페이지 번호에 페이지 오프셋을 더하여 실제 메모리 주소를 찾는다.
캐시 메모리의 사이즈를 구하기 위한 프로그램을 구현하려면, 캐시의 히트율, 미스율, 접근 시간 등을 측정하는 방법을 활용하여 구현해야 한다.
페이징은 프로그램의 가상 주소 공간을 고정된 크기로 나눠, 이를 물리 메모리의 고정된 크기로 나눠진 블록에 대응시켜, 가상 메모리를 관리하는 기법이다.
보통 가상 주소 공간에 페이지 번호와 페이지 오프셋이 저장되는데, 페이지 번호를 통해 페이지 테이블에서 실제 메모리의 블록 주소를 찾은 후 여기에 페이지 오프셋을 더하여 실제 메모리 주소로 데이터를 전송할 수 있다.
페이지 테이블에는 블록 번호, 유효 비트, 수정 비트, 읽기/쓰기 비트, 접근 비트 등이 저장된다.
페이지 테이블은 물리적 메모리에 저장된다.
블록 번호를 찾을 때와 실제 메모리 주소를 찾을 때로 메모리를 두 번 참조하게 되는데 이를 빨리 하기 위해서 TLB(Translation Lookaside Buffer)가 사용된다. TLB는 페이지 테이블의 일부 정보를 캐시하여, 가상 주소에서 물리 주소로 변환되는 시간을 단축시킨다.
TLB에는 페이지 번호, 블록 번호, 유효 비트, 접근 비트 등이 저장된다.
page replacement policy에는 FIFO(First-In First-Out), LRU(Least Recently Used), Clock, OPT, NRU(Not Recently Used), Second-Chance 등이 있다. LRU는 언제 페이지가 마지막으로 사용되었는지 추적해야 하므로 구현 비용이 높고 효율성이 떨어진다는 단점이 있다. 또한, 페이지 교체가 발생할 경우 비교적 많은 연산이 필요하기에 성능이 저하된다는 단점이 있다.
페이징 기법을 통해 각 페이지가 물리 메모리의 어떤 위치에 배치되더라도 상관이 없어 외부 단편화를 방지할 수 있으며, 실제 메모리보다 더 큰 가상 메모리 공간을 사용할 수 있다는 장점이 있다. 또한, 각 페이지는 독립적으로 관리되어 메모리 보호 및 관리 측면에서 이점이 있으며, 페이지 크기가 일정하여 효율적인 메모리 사용이 가능하다는 장점이 있다.
반면에 페이지는 고정된 크기를 갖기에 각 페이지에 여유 공간이 생길 수 있어 내부 간편화 문제점이 있을 수 있으며, 페이지 테이블 관리 비용이 많이 든다는 단점이 존재한다. 또한 페이지 교체 시 큰 오버헤드가 발생할 수 있으며, TLB 미스가 발생할 경우 페이지 테이블을 참조하는 시간이 길어져 성능 저하 이슈가 있을 수 있다는 단점이 있다.
hierarchical paging은 페이지 테이블을 여러 라벨로 나누어 관리하는 방법으로, 큰 가상 공간 메모리 공간을 다루는 환경에서 유리하다. 반면에 inverted page table은 물리 메모리에 대해 하나의 엔트리만 유지하며 가상 주소를 물리 주소로 매핑하는 방법으로, 작은 가상 메모리 공간을 다루는 환경에서 유리하다.