이 글은 "운영체제 아주 쉬운 세 가지 이야기" 책을 읽고 공부한 내용들을 두고두고 보기 위해 정리하는 글이다.
읽을 때마다 그 날의 내용들을 꾸준히 이어서 업데이트 할 예정이다.
🗓️ 2023.08.10 작성 ▽
운영체제의 세 주제중 첫 번째인 가상화
직접 실행의 두 번째 문제점은 프로세스 전환이 가능해야 하고 간단해야 한다는 점
실행 중인 프로세스를 멈추고 다른 프로세스를 실행하는 것은 매우 까다로운 문제
프로세스가 실행 중이라는 것은 운영체제는 실행 중이지 않다는 것을 의미
운영체제가 실행 중이 아니라면 어떻게 프로세스를 전환할 수 있을까?
⭕ 팁: 보안상 사용자의 입력값을 조심하라
- 시스템 콜 호출 과정에서 발생할 수 있는 다양한 문제점으로 부터 운영체제를 보호하는 방법들
- trap 기법
- 모든 시스템 콜은 반드시 trap을 통해서 호출되어야 한다는 제약
- 시스템 콜의 인자값이 제대로 된 값인지 검사도 해야함
- 유효하지 않은 인자값을 전달한 시스템 콜은 거부
- ex) write() 시스템 콜에서는 사용자가 버퍼의 주소를 인자값으로 전달 -> 올바르지 않은 주소를 전달한다면 운영체제는 이를 파악하고 시스템 콜 중지
❓ 핵심 질문: CPU를 어떻게 다시 획득할 수 있는가
- 운영체제는 어떻게 CPU를 다시 획득하여 프로세스를 전환할 수 있는가
❗ 협조 방식: 시스템 콜 호출시 까지 대기
협조 방식은 각 사용자 프로세스가 비정상적인 행동은 하지 않을 것으로 가정
-> CPU를 장기간 사용해야 하는 프로세스들은 다른 프로세스들이 CPU를 사용할 수 있도록 주기적으로 CPU를 반납할 것이라 믿음
프로세스가 CPU를 반납하기 위해서는 운영체제가 해당 프로세스의 실행 상태(각 레지스터값들)를 저장해주어야 함
-> 이렇게 하면 CPU를 반납했던 프로세스가 추후에 다시 실행을 계속할 수 있음
CPU 반납 문제의 핵심은 "응용 프로세스가 어떻게 제어권을 운영체제에게 넘기느냐"
대부분의 프로세스는 시스템 콜을 자주 호출하는 것으로 알려짐
시스템 콜(파일 열기, 읽기, 다른 컴퓨터에 메시지 송신, 프로세스 생성 등)을 호출하면 자연스럽게 운영체제의 코드가 실행되고 제어권이 운영체제로 넘어감
협조 방식을 사용하는 운영체제는 yield 시스템 콜을 제공
운영체제에게 제어를 넘겨 운영체제가 CPU를 다른 프로세스에게 할당할 수 있는 기회를 제공
응용 프로그램이 비정상적인 행위를 하게 되면 운영체제에게 제어가 넘어감
ex) 어떤 수를 0으로 나누는 연산시 운영체제로의 트랩이 일어남
협조 방식의 스케줄링 시스템은 근본적으로 수동적
CPU 제어권 획을을 위해 운영체제는 시스템 콜이 호출되기를 기다리거나 불법적인 연산이 일어나기를 대기
프로세스가 무한 루프에 빠져 시스템 콜을 호출할 수 없다면 문제가 발생할 수 있음
⭕ 팁: 응용 프로그램의 오작동 처리하기
- 오작동 하도록 의도적으로 설계되었거나 의도치 않은 버그로 인해 해서는 안 될 행위를 하려는 프로세스를 처리하기 위해 운영체제는 해당 프로세스를 종료시킴
- 종료시키는 것 외에 다른 방법이 특별히 존재하지 않음
❗ 비협조 방식: 운영체제가 제어권 확보
프로세스가 시스템 콜을 호출하지 않을 경우, 하드웨어의 도움을 받아 운영체제로 제어권을 넘길 수 있음
협조 방식 운영체제의 경우 프로세스가 무한 루프에 빠졌을 때 해결할 수 있는 방법은 재부팅 밖에 없음
⭕ 팁: 재부팅의 유용함
- 협조적 선점 모드에서는 재부팅이 무한 루프와 같은 상황을 해결하는 유일한 방법
- 재부팅은 견고한 시스템을 구축하는 데 매우 유용함
- 소프트웨어를 잘 정의된 그리고 검증된 상태로 되돌리기 때문에 매우 유용
- 재부팅은 오래되었거나 유출된 자원을 시스템에 반환
- 이러한 자원은 재부팅을 통하지 않고는 반환받을 방법이 없음
- 또한 재부팅은 자동화하기 쉬움
❓ 핵심 질문: 협조 없이 제어를 얻는 방법
- 프로세스가 비협조적인 상황에서도 CPU의 할당을 위한 제어권을 어떻게 확보할 수 있는가
- 어떻게 하면 악의적인 프로세스가 컴퓨터를 장악하는 것을 방지할 수 있는가
시스템 콜의 호출이 없더라도 운영체제에게 제어권을 넘길 수 있는 방법으로 타이머 인터럽트(timer interrupt) 방법이 있음
타이머는 수 밀리 초마다 인터럽트라 불리는 하드웨어 신호를 발생시키도록 하는 것이 가능
인터럽트가 발생했을 때 이를 처리하는 것이 운영체제의 가장 중요한 역할 중 하나
인터럽트가 발생하면 운영체제는 현재 수행 중인 프로세스를 중단시키고 해당 인터럽트에 대한 인터럽트 핸들러(interrupt handler)를 실행
인터럽트 핸들러는 운영체제의 일부이며 인터럽트를 처리하는 과정에서 제어권이 자연스럽게 운영체제로 넘어감
-> 운영체제는 현재 실행중인 프로세스를 중단하고 다른 프로세스를 실행시킬 수 있는 기회를 갖게되는 것
⭕ 팁: 타이머 인터럽트를 이용한 제어권 확보
- 타이머 인터럽트 기능을 사용하면 프로세스가 비협조적으로 행동하는 상황에서도 운영체제가 실행될 수 있음
- 타이머 인터럽트는 운영체제가 컴퓨터를 제어하는 데 있어 근간이 되는 핵심 기능
운영체제는 타이머 인터럽트가 발생 시 실행해야 할 코드의 주소를 기록해두어야 함
컴퓨터 부팅 시 운영체제는 컴퓨터에서 정의된 각 인터럽트에 대해 관련 인터럽트 핸들러의 위치를 테이블 형태로 메모리에 초기화 시킴
또한 컴퓨터 부팅 시 운영체제는 타이머를 시작하며 타이머 인터럽트가 발생할 때마다 제어권이 운영체제로 넘어감
타이머는 인터럽트를 주기적으로 발생시킴
-> 특정 주기로 운영체제에게 제어권이 넘어가 운영체제는 사용자 프로그램이 비정상적으로 작동하는 경우가 발생하더라도 언제든지 해당 프로그램을 적절히 처리할 수 있는 기회를 가짐
인터럽트 발생 시에는 시스템 콜 호출과 동일하게 실행 중이던 프로그램의 상태를 저장
-> 추후 return-from-trap 명령어가 프로그램을 다시 시작할 수 있게 하기 위해
다양한 레지스터가 커널 스택에 저장되고, return-from-trap 명령어를 통하여 복원
🗓️ 2023.08.16 작성 ▽
❗ 문맥의 저장과 복원
시스템 콜 혹은 타이머 인터럽트를 통해 운영체제가 제어권을 다시 획득하면 현재 실행중인 프로세스를 계속 실행할 것인지 아니면 다른 프로세스로 전환할 것인지를 결정해야 함
이 결정은 운영체제의 스케줄러(scheduler)라는 부분에 의해 내려짐
현재 프로세스를 중단하고 다른 프로세스를 실행하기로 결정을 하면 운영체제는 문맥 교환(context switch)이라 불리는 코드를 실행
문맥 교환은 현재 실행 중인 프로세스의 레지스터 값들을 커널 스택 같은 곳에 저장하고 새로이 실행될 프로세스의 커널 스택으로부터 레지스터 값을 복원함
그렇게 함으로써 운영체제는 return-from-trap 명령어가 마지막으로 실행될 때 현재 실행중이던 프로세스로 리턴하는 것이 아니라 다른 프로세스로 리턴하여 실행을 다시 시작
문맥을 메모리에 저장하고 새로 실행될 프로세스의 문맥을 CPU로 읽어들이는 작업은 빠른 실행 속도를 위해 주로 어셈블리 코드를 사용하여 작성
운영체제는 현재 실행 중인 프로세스의 범용 레지스터, PC(프로그램 카운터)뿐 아니라 현재 커널 스택 포인터를 저장
그 후 새롭게 실행될 프로세스의 범용 레지스터, PC를 CPU로 읽어옴
마지막으로 현재 커널 스택을 새로이 시작될 프로세스의 커널 스택으로 전환 후 return-from-trap 명령어로 새로운 프로세스를 실행
운영체제 @부트(커널 모드) 하드웨어
--------------------------------------------------------
1. 트랩 테이블을 초기화
2. syscall 핸들러, 타이머 핸들러의 주소 기억
3. 인터럽트 타이머 시작
4. 타이머 시작
5. X msec 지난 후 CPU를 인터럽트
------------------------------------------------------------------------
운영체제 @실행(커널 모드) 하드웨어 프로그램(사용자 모드)
------------------------------------------------------------------------
1. 프로세스 A
...
2. 타이머 인터럽트
3. A의 레지스터를 A의 커널 스택에 저장
4. 커널 모드로 이동
5. 트랩 핸들러로 분기
6. 트랩 처리
7. switch() 루틴 호출
8. A의 레지스터를 A의 proc 구조에 저장
9. B의 proc 구조로부터 B의 레지스터를 복원
10. B를 커널 스택으로 전환
11. return-from-trap(B 프로세스로)
12. B의 커널 스택을 B의 레지스터로 저장
13. 사용자 모드로 이동
14. B의 PC로 분기
15. 프로세스 B
...
위는 앞에서 설명한 문맥 교환의 대략적인 과정
프로세스 A의 실행이 타이머 인터럽트에 의해 중단되고 하드웨어는 A의 레지스터를 커널 스택에 저장하고 커널 모드로 진입
운영체제의 스케줄링에 따라 B 프로세스로 전환하기로 결정하고 switch() 루틴 호출
A가 사용하는 레지스터 값들을 A의 프로세스 구조체에 저장하고 B의 프로세스 구조체에 저장되어있던 레지스터 값들을 복원
그 후 스택포인터 레지스터의 값을 A의 커널 스택이 아닌 B의 커널 스택으로 설정
마지막으로 return-from-trap을 수행하여 B의 레지스터 값들을 복원하고 실행 시작
문맥 교환 과정에서 서로 다른 두 가지 종류의 레지스터의 저장/복원이 발생
1. 타이머 인터럽트가 발생했을 때
-> 실행 중인 프로세스의 사용자 레지스터가 하드웨어에 의해 저장되고 저장 장소로 해당 프로세스의 커널 스택이 사용
2. 운영체제가 A에서 B로 전환하기로 결정했을 때
-> 커널 레지스터가 운영체제에 의하여 해당 프로세스의 프로세스 구조체에 저장, 실제로는 프로세스 A 실행중에 커널로 진입했지만 프로세스 구조체로부터 레지스터 값을 복원하는 작업으로 인해 운영체제가 프로세스 B의 실행중에 커널로 트랩된 것처럼 보이게 됨
# void switch(struct context **old, struct context *new);
#
# Save current register context in old
# and then load register context from new.
.global switch
switch:
# Save old registers
movl 4 (%esp) , %eax # old 포인터를 eax에 넣음
popl 0 (%eax) # old IP를 저장
movl %esp, 4 (%eax) # 스택
movl %ebx, 8 (%eax) # 다른 레지스터
movl %ecx, 12 (%eax)
movl %edx, 16 (%eax)
movl %esi, 20 (%eax)
movl %edi, 24 (%eax)
movl %ebp, 28 (%eax)
# Load new registers
movl 4(%esp), %eax # new포인터를 eax에 넣음
movl 28 (%eax), %ebp # 다른 레지스터를 복원
movl 24 (%eax), %edi
movl 20 (%eax), %esi
movl 16 (%eax), %edx
movl 12 (%eax), %ecx
movl 8 (%eax), %ebx
movl 4 (%eax), %esp # 스택은 디 지점에서 전환
pushl 0 (%eax) # 리턴 주소를 지정된 장소에 넣음
ret # 마지막으로 new문맥으로 리턴
위는 xv6에서의 문맥 교환 코드
🗓️ 2023.09.11 작성 ▽
다음과 같은 질문이 생길 수 있음
위와 같은 문제들은 실제로 발생
인터럽트나 트랩을 처리하는 도중에 다른 인터럽트가 발생할 때는 주의가 필요
간단한 해법은 인터럽트를 처리하는 동안에는 인터럽트를 불능화시키는 것
그러나 인터럽트를 장기간 불능화하면, 손실되는 인터럽트가 생기게 되므로 신중하게 사용해야 함
운영체제는 내부 자료 구조가 동시에 접근되는 것을 방지하기 위해 다양한 락(lock)기법을 개발해 옴
커널 내부의 각종 자료 구조들이 락으로 보호되어 커널 내부에서 다수의 작업들이 동시에 진행되는 것이 가능
이러한 락의 사용으로 인해 운영체제 전체의 구성과 작동이 매우 복잡해질 수 있음
-> 락 때문에 많은 문제접과 버그들이 발생하기도 함
⭕ 여담: 문맥 교환에 걸리는 시간
- 문맥 교환 작업이 걸리는 시간 또는 시스템 콜의 처리 소요 시간등을 측정하는 lmbench[MS96]이라는 도구가 있음
- 문맥전환 소요 시간, 시스템 콜 처리 소요 시간 등을 정확히 측정하고 연관된 다른 성능 수치도 측정
- 문맥 교환 작업 소요 시간은 시간이 지나면서 프로세서의 성능개선 추이와 비슷하게 점점 좋아졌음
- 운영체제의 모든 동작이 CPU 성능에 따라 좋아지는 것은 아님
- 운영체제의 많은 연산이 주로 메모리를 접근하는 연산이며 메모리의 대역폭은 프로세서 속도가 발전하는 것만큼 극적으로 향상되지는 않았음
이번 장에서 다룬 CPU 가상화를 실현하는 핵심 기법들을 제한적 직접 실행이라고 통칭
원하는 프로그램을 실행하면 하드웨어를 적절히 설정하여 프로세스가 할 수 있는 작업을 제한하고, 중요한 작업을 실행할 때는 반드시 운영체제를 거치도록 하는 기법
CPU 사용에 대한 적절한 안전 장치를 제공
부팅할 때 트랩 핸들러 함수를 셋업하고 인터럽트 타이머를 시작시키며 그 후 제한된 모드에서만 프로세스가 실행되도록 함
이러한 방식으로 운영체제는 프로세스를 효율적으로 실행되는 것을 보장
운영체제는 이 과정에서 프로그램이 특별연산을 수행할 때 혹은 어떤 프로세스가 CPU를 독점하고 있어 CPU를 강제로 다른 프로세스에게 전환할 때에만 개입
⭕ 여담: CPU 가상화의 핵심 개념
- CPU는 최소한 두 가지 실행 모드를 지원해야 함
- 제한적인 사용자 모드
- 특권을 가진 (제한이 없는) 커널 모드
- 일반적인 응용 프로그램은 사용자 모드에서 실행되며 시스템 콜을 사용하여 커널로 트랩해 운영체제의 서비스를 요청
- 트랩 인스트럭션은 레지스터 상태를 저장하고, 하드웨어 상태를 커널 모드로 변경하며 운영체제 내의 트랩 테이블로 이동
- 운영체제가 시스템 콜 서비스를 마치면 다른 특수한 명령어 return-from-trap을 통해 사용자 프로그램으로 돌아감
- 그 과정에서 권한을 줄이고 운영체제로 들어가게 만든 트랩 이후의 인스트럭션으로 제어권 반환
- 트랩 테이블은 부팅 시 운영체제에 의해 설정되어야 하며 사용자 프로그램에 의해 쉽게 수정될 수 없는지 확인해야 함
- 이 모든 것은 프로그램을 효율적으로 실행하지만 운영체제 제어를 잃지 않는 제한적 직접 실행 방식의 일부
- 일단 프로그램이 실행되면 운영체제는 사용자 프로그램이 영원히 실행되는 것을 막기 위해 하드웨어 메커니즘인 타이머 인터럽트를 사용
- 이것은 비협조 방식의 CPU 스케줄링
- 때때로 운영체제는 타이머 인터럽트나 시스템 콜 실행 중에 현재 프로세스에서 다른 프로세스로 전환할 수 있음 -> 컨텍스트 스위치(문맥 전환)