본문 바로가기
공뷰/운영체제

OSTEP - 0. 개요

by WOOSERK 2023. 4. 1.

OSTEP 한글 번역본을 읽으면서 공부한 내용을 정리한 글입니다. (https://pages.cs.wisc.edu/~remzi/OSTEP/Korean/)

 

운영체제의 3가지 원칙은 가상화(virtualization), 병행성(concurrency), 영속성(persitence)이다.

폰 노이만 구조

프로그램이 하는 일은 명령어를 실행하는 것이다. 명령어를 초당 수 억번 메모리로부터 fetch하고, decode하고, execute한다. 간략화했지만, 아래 동작을 프로그램이 끝날 때까지 반복한다.

  1. PC(Program Counter)에 저장된 주소를 읽어서 그 주소가 가리키는 메모리 공간에 접근하여 명령어를 읽어 들이고 레지스터에 저장한다.
  2. 레지스터에 저장된 명령어를 CPU 내부의 decoder로 해석하여 어떤 명령어인지 파악한다. 이 해석은 ISA(Instruction Set Architecture)에 따라 다르다. (ex. Intel과 AMD)
  3. 명령어에 따라 필요한 데이터를 가져오고, 연산을 수행한다.
  4. 연산 결과를 메모리 또는 레지스터에 저장한다.

 

운영체제(Operating system)

프로그램을 쉽게 실행하고, 프로그램 간의 메모리 공유를 가능하게 하고, 장치와 상호작용을 가능하게 하는 등 다양한 일을 할 수 있게 하는 소프트웨어이다.

 

운영체제는 이를 위해 가상화라는 기법을 사용한다. 운영체제는 프로세서, 메모리, 디스크와 같은 물리적인 자원을 이용하여 일반적이고, 강력하고, 사용이 편리한 가상 형태의 자원을 생성한다. 때문에 운영체제를 때로는 가상 머신(virtual machine)이라고 부른다.

 

시스템 콜(System call)

사용자 프로그램의 프로그램 실행, 메모리 할당, 파일 접근과 같은 가상 머신과 관련된 기능들을 운영체제에게 요청할 수 있도록, 운영체제가 사용자에게 제공하는 API이다. 보통 운영체제는 응용 프로그램이 사용 가능한 수백 개의 시스템 콜을 제공한다. 때문에 우리는 운영체제가 표준 라이브러리를 제공한다고 일컫기도 한다.

 

운영체제 역할 = 자원 관리

가상화는 많은 프로그램들이 CPU를 공유하여 동시에 실행될 수 있도록 한다. 그리고 프로그램들이 각자 명령어와 데이터를 접근할 수 있게 한다. 프로그램들이 디스크 등의 장치를 공유할 수 있게 한다. 이러한 이유로 운영체제는 자원 관리자(resource manager)라고도 불린다. CPU, 메모리, 디스크는 시스템의 자원이다. 이들을 효율적이고 공정하게 관리하는 것이 운영체제의 역할이다.

 

자원을 어떻게 가상화시키는가
운영체제가 자원을 가상화시키는 이유는 시스템을 사용하기 편리하게 만들기 때문이다. 가상화 효과를 얻기 위해 운영체제가 구현하는 기법과 정책은 무엇인가? 운영체제는 이들을 어떻게 효율적으로 구현하는가? 어떤 하드웨어 자원이 필요한가? 이제부터 알아볼 것이다.

 

CPU 가상화

인자로 받은 문자를 1초마다 반복해서 출력하는 코드를 작성하고, 4개의 인스턴스를 동시에 실행시킨다.

프로세서가 하나밖에 없음에도 프로그램 4개 모두 동시에 실행되는 것처럼 보인다. 어떻게 이런 일이 일어날까?

하드웨어의 도움을 받아 운영체제가 시스템에 매우 많은 수의 가상 CPU가 존재하는 듯한 환상을 만들어 낸 것이다. 하나의 CPU 또는 소규모 CPU 집합을 무한 개의 CPU가 존재하는 것처럼 변환하여 동시에 많은 수의 프로그램을 실행시키는 것을 CPU 가상화라고 한다.

 

프로그램을 실행하고, 멈추고, 어떤 프로그램을 실행시킬 것인가를 운영체제에게 알려주기 위해서는 원하는 바를 운영체제에 전달할 수 있는 인터페이스(API)가 필요하다. API는 운영체제와 사용자가 상호작용할 수 있는 주된 방법이다.

 

다수의 프로그램을 동시에 실행시키는 기능은 새로운 종류의 문제를 발생시킨다. 예를 들어, 특정 순간에 두 개의 프로그램이 실행되기를 원한다면 누가 실행되어야 하는가? 이는 운영체제의 정책에 달려있다. 운영체제 여러 부분에서 이러한 문제에 답하기 위한 정책들이 사용된다. 운영체제가 구현한 동시에 다수의 프로그램을 실행시키는 기본적인 기법, 자원 관리자로서의 운영체제의 역할을 다룰 것이다.

 

메모리 가상화

malloc()을 호출하여 메모리를 할당하는 메모리 접근 프로그램

현재 우리가 사용하고 있는 컴퓨터의 물리 메모리 모델은 바이트의 배열이다. 메모리를 읽기 위해서는 데이터에 주소를 명시해야 한다.메모리에 쓰기(또는 갱신) 위해서는 주소와 데이터를 명시해야 한다.

 

메모리는 프로그램이 실행되는 동안 항상 접근된다. 프로그램은 실행 중에 자신의 모든 자료 구조를 메모리에 유지하고 load, store 등의 메모리 접근 명령어를 통해 자료 구조에 접근한다. 명령어 역시 메모리에 존재한다. 명령어를 반입(fetch)할 때마다 메모리에 접근한다.

 

위 프로그램은 아래 순서로 진행된다.

  1. 메모리를 할당받는다.
  2. 할당받은 메모리 주소를 출력한다. 이 때, 실행 중인 프로그램의 프로세스 식별자(PID)가 함께 출력된다.
  3. 할당받은 메모리의 첫 슬롯에 0을 넣는다.
  4. 루프를 돌며 1초마다 변수 p가 가리키는 주소의 값을 1 증가시킨다.
여러 번 실행시킨다.

프로그램들은 같은 주소에 메모리를 할당받지만(00200000), 각각 독립적으로 00200000 번지의 값을 갱신한다. 각 프로그램은 물리 메모리를 다른 프로그램과 공유하는 것이 아니라, 각자 자신의 메모리를 가지고 있는 것처럼 보인다.(이 코드가 동작하려면 보안을 위한 주소 공간 난수화 기능을 꺼야 한다.)

 

운영체제가 메모리 가상화를 하기 때문에 이런 현상이 생긴다. 각 프로세스는 자신만의 가상 주소 공간(또는 주소 공간)을 갖는다. 운영체제는 이 가상 주소 공간을 컴퓨터의 물리 메모리로 매핑한다. 하나의 프로그램이 수행하는 각종 메모리 연산은 다른 프로그램의 주소 공간에 영향을 주지 않는다. 실행 중인 프로그램의 입장에서는 자기 자신만의 물리 메모리를 갖는 셈이다. 실제로는 물리 메모리는 공유 자원이고, 운영체제에 의해 관리된다.

 

병행성

프로그램이 한 번에 많은 일을 하려 할 때(즉. 동시에) 발생하는, 그리고 반드시 해결해야 하는 문제들을 가리킬 때 이 용어를 사한다. 병행성 문제는 운영체제 자체에서 발생한다. 운영체제는 한 프로세스 실행, 다음 프로세스, 또 다음 프로세스 등의 순서로 여러 프로세스를 실행시켜 한 번에 많은 일을 한다. 이러한 방식은 심각하고 흥미로운 문제를 발생시킨다.

 

병행성 문제는 운영체제만의 문제는 아니다. 멀티 쓰레드 프로그램도 동일한 문제를 드러낸다.

멀티 쓰레드 프로그램.
메인 프로그램은 Pthread_create()로 두 개의 쓰레드를 생성한다. 쓰레드를 동일한 메모리 공간에서 함께 실행 중인 여러 개의 함수라고 생각할 수 있다. 각 쓰레드는 worker()라는 루틴을 실행한다. 이는 loops번 만큼 루프를 반복하면서 카운터 값을 증가시킨다.

loops 값을 1000으로 지정한 후 프로그램을 실행시키면 counter 변수의 최종 값은 얼마가 될까? 

 

예상했겠지만 각 쓰레드가 1000번씩 증가시켰기 때문에 2000이 된다. 사실 그리 간단하지 않다. 이번에는 loops의 값을 더 큰 값으로 지정하자.

2번 실행했을 때 각각 143012, 137298 반환

최종 값이 200,000이 아니다. 심지어 실행할 때마다 다른 값이 나온다. 왜 이럴까?

그 원인은 명령어가 한 번에 하나씩만 실행된다는 것과 관련이 있다. counter를 증가시키는 부분은 세 개의 명령어로 이루어진다.

  1. counter 값을 메모리에서 레지스터로 탑재
  2. 레지스터를 1 증가
  3. 레지스터의 값을 다시 메모리에 저장

이 세 개의 명령어가 원자적으로 실행되지 않기 때문에 이상한 일이 발생할 수 있다. 이것이 병행성 문제이다.

 

올바르게 동작하는 병행 프로그램은 어떻게 작성해야 하는가
같은 메모리 공간에 다수의 쓰레드가 동시에 실행된다고 할 때, 올바르게 동작하는 프로그램을 어떻게 작성할 수 있는가? 운영체제로부터 어떤 기본 기법들을 제공받아야 하는가? 하드웨어는 어떤 기능을 제공해야 하는가? 병행성 문제를 해결하기 위하여 기본 기법들과 하드웨어 기능을 어떻게 이용할 수 있는가?

 

영속성

이 책의 세번째 주제는 영속성(persistence)이다. DRAM과 같은 장치는 데이터를 휘발성 방식으로 저장하기 때문에 메모리의 데이터는 쉽게 손실될 수 있다. 데이터를 영속적으로 저장할 수 있는 하드웨어와 소프트웨어가 필요하다.

 

하드웨어는 입력/출력 혹은 I/O 장치 형태로 제공된다. 요즘에는 solid-state drives(SSD)가 많이 사용되고 있지만 장기간 보존할 정보를 저장하는 장치로는 일반적으로 하드 드라이브가 사용된다.

 

디스크를 관리하는 운영체제 소프트웨어를 파일 시스템(file system)이라고 부른다. 파일 시스템은 사용자가 생성한 파일을 시스템의 디스크에 안전하고 효율적인 방식으로 저장할 책임이 있다.

 

CPU나 메모리 가상화와는 달리 운영체제는 프로그램 별로 가상 디스크를 따로 생성하지 않는다. 오히려 사용자들이 종종 파일 정보를 공유하기 원한다고 가정한다.

데이터를 영속적으로 저장하는 방법은 무엇인가
파일 시스템은 데이터를 영속적으로 관리하는 운영체제의 일부분이다. 올바르게 일하기 위해서는 어떤 기법이 필요할까? 이러한 작업의 성능을 높이기 위해서 어떤 기법과 정책이 필요한가? 하드웨어와 소프트웨어가 실패하더라도 올바르게 동작하려면 어떻게 해야 하는가? 

 

파일을 생성하는 코드

이 프로그램은 운영체제를 세 번 호출한다.
첫째, open() 콜은 파일을 생성하고 연다.
둘째, write() 콜은 파일에 데이터를 쓴다.
셋째, close() 콜은 단순히 파일을 닫는데, 프로그램이 더 이상 해당 파일을 사용하지 않는다는 것을 나타낸다.

이들 시스템 콜은 운영체제에서 파일 시스템이라 불리는 부분에서 전달된다. 파일 시스템은 요청을 처리하고 경우에 따라 사용자에게 에러 코드를 반환한다.

 

데이터를 디스크에 쓰기 위해서 운영체제가 실제로 하는 일은 간단하지 않다. 파일 시스템은 많은 작업을 해야 한다. 먼저, 새 데이터가 디스크의 어디에 저장될지 결정해야 하고, 파일 시스템이 관리하는 다양한 자료 구조를 통하여 데이터의 상태를 추적해야 한다. 이런 작업을 하기 위해서는 저장 장치로부터 기존 자료 구조를 읽거나 갱신해야 한다. 운영체제는 시스템 콜이라는 표준화된 방법으로 장치들을 접근할 수 있게 한다.

 

성능 향상을 위해서 대부분의 파일 시스템은 쓰기 요청을 지연시켜 취합된 요청들을 한 번에 처리한다. 쓰기 중에 시스템의 갑작스러운 고장에 대비하여 많은 파일 시스템들이 저널링(journaling)이나 쓰기 시 복사(Copy-On-Write)와 같은 복잡한 쓰기 기법을 사용한다. 이러한 기법들은 고장이 발생하더라도 정상적인 상태로 복구될 수 있게 한다. 효율적인 디스크 작업을 위해 단순 리스트에서 복잡한 B-트리까지 다양한 종류의 자료 구조를 사용한다.

 

설계 목표

운영체제는 CPU, 메모리, 디스크와 같은 물리 자원을 가상화한다. 운영체제는 병행성과 관련된 복잡한 문제를 처리한다. 파일을 영속적으로 저장하여 오랫동안 안전한 상태에 있게 한다. 그런 시스템을 구현하려면 몇 가지 목표를 세워야 한다.

 

가장 기본적인 목표는 시스템을 편리하고 사용하기 쉽게 만드는 데 필요한 개념들을 정리하는 것이다. 컴퓨터 과학에서 추상화는 모든 일의 근간이다. 추상화를 통해 큰 프로그램을 이해하기 쉬운 작은 부분들로 나누어 구현할 수 있다. 추상화는 논리 게이트를 고려하지 않고도 어셈블리 코드를 작성할 수 있게 하며, 트랜지스터에 대한 지식 없이도 게이트를 이용하여 프로세서를 만들 수 있게 한다.

 

운영체제의 설계와 구현의 중요한 목표는 성능이다. 다른 말로, 오버헤드를 최소화하는 것이다. 오버헤드는 시간(더 많은 명령어)과 공간(메모리 또는 디스크)의 형태로 나타난다.

 

또 다른 목표는 응용 프로그램 간의 보호, 그리고 운영체제와 응용 프로그램 간의 보호이다. 다수의 프로그램들이 동시에 실행되기 때문에 운영체제는 한 프로그램의 악의적인 또는 의도치 않은 행위가 다른 프로그램에게 피해를 주지 않는다는 것을 보장해야 한다. 보호는 운영체제의 원칙 중 하나인 고립(isolation) 원칙의 핵심이다. 프로세스를 다른 프로세스로부터 고립시키는 일은 보호의 핵심이고 운영체제가 해야 하는 일 중 많은 부분의 근간이 된다.

 

운영체제는 계속 실행되어야 한다. 운영체제가 실패하면 그 위에서 실행되는 모든 응용 프로그램도 실패하게 된다. 이러한 종속성 때문에 운영체제는 높은 수준의 신뢰성을 제공해야 한다. 

 

다른 목표들도 존재한다. 에너지 효율성, 보안, 이동성 등 시스템의 목적에 따라 운영체제는 다른 목표를 지향하게 되고, 구현이 달라진다.

 

역사

초창기 운영체제: 단순 라이브러리

초창기 운영체제는 많은 일을 하지 않고, 자주 사용되는 함수들을 모아 놓은 라이브러리에 불과했다. 현대 운영체제가 하는 많은 작업, 예를 들어 작업의 순서를 정하는 것과 같은 일을 컴퓨터 관리자(인간)가 담당했다.

 

작업들이 준비되면 컴퓨터 관리자가 일괄적으로 처리한다. 이러한 방식의 컴퓨팅을 일괄 처리(batch)라고 부른다.

 

라이브러리를 넘어서: 보호

단순한 라이브러리를 넘어서 컴퓨터 관리 면에서 더 중심적인 역할을 하게 된다. 운영체제 코드는 장치를 제어하였기 때문에 일반 응용 프로그램 코드와는 다르게 취급되어야 한다. 모든 응용 프로그램이 디스크에서 원하는 지점을 읽을 수 있게 된다면, 어떤 프로그램이든 원하는 모든 파일을 읽을 수 있기 때문에 사생활이 사라진다. 때문에, 라이브러리 형태로 파일 시스템을 구현하는 것은 의미가 없다.

 

시스템 콜이라는 아이디어가 발명되었다. 운영체제를 라이브러리가 아니라 특별한 하드웨어 명령어와 하드웨어 상태를 결합하여, 운영체제로 전환하기 위해서는 정해진 규칙에 따라 제어 가능한 과정을 거치도록 만들었다.

 

시스템 콜과 프로시저 호출의 결정적인 차이는 시스템 콜은 제어를 운영체제에게 넘길 때 하드웨어 권한 수준을 상향 조정한다. 사용자 응용 프로그램은 사용자 모드(user mode)라고 불리는 상태에서 실행된다. 사용자 모드에서는 응용 프로그램이 할 수 있는 일을 하드웨어적으로 제한한다. 예를 들어, 사용자 모드에서 실행 중인 응용 프로그램은 디스크 입출력, 물리 메모리 페이지 접근 또는 네트워크 패킷 송신 등의 작업을 할 수 없다.

 

시스템 콜은 보통 trap이라고 불리는 특별한 하드웨어 명령어를 이용하여 호출된다. 시스템 콜 시작 시, 하드웨어는 미리 지정된 트랩 핸들러(trap handler) 함수에게 제어권을 넘기고 특권 수준을 커널 모드(kernel mode)로 격상시킨다. 트랩 핸들러 함수는 운영체제가 미리 구현해 놓는다.

 

커널 모드에서 운영체제는 시스템의 하드웨어를 자유롭게 접근할 수 있으며, 입출력 또는 메모리 할당 등과 같은 작업을 할 수 있다. 운영체제가 서비스를 완료하면 return-from-trap 특수 명령어를 사용하여 제어권을 다시 사용자에게 넘기는 동시에 사용자 모드로 전환한다.

 

멀티 프로그래밍 시대

컴퓨터 자원의 효율적 활용을 위해 멀티프로그래밍(multiprogramming) 기법이 대중으로 사용되었다. 한 번에 하나의 프로그램만 실행시키는 대신 운영체제는 여러 작업을 메모리에 탑재하고 작업들을 빠르게 번갈아가며 실행하여 CPU 사용률을 향상시킨다. 입출력 장치가 느리기 때문에 전환(switching) 능력이 특히 중요하였다. 입출력 요청이 서비스되는 동안 CPU가 대기하는 것은 CPU 시간 낭비를 초래한다. 

 

멀티프로그래밍 지원의 필요와 인터럽트를 통한 입출력 작업 처리 등이 운영체제의 발전에 여러 가지 혁신을 가져왔다. 메모리 보호(memory protection)와 같은 주제가 중요하게 되었다. 한 프로그램이 다른 프로그램의 메모리에 접근하는 것을 원치 않는다. 

 

병행성(concurrency) 문제에 대한 이해도 중요하다. 인터럽트가 발생하더라도 운영체제가 올바르게 동작한다는 것을 보장하는 것도 어려운 일이다.

 

현대

PC가 등장하였다. 불행하게도 운영체제의 입장에서는 PC의 등장이 퇴보를 의미했다. 예를 들어, DOS같은 초기 운영체제는 메모리 보호가 중요하다는 생각조차 하지 않았다. 

 

다행히 암흑기가 지난 뒤 예전 미니컴퓨터 운영체제 기법들이 데스크톱 컴퓨터용 운영체제에 등장하기 시작했다.

 

UNIX의 중요성
운영체제의 역사에서 UNIX의 중요성은 아무리 강조해도 지나치지 않다.
"Bell Labs"에서 시작된 UNIX의 핵심은 서로 통합 사용이 가능한 작고 강력한 프로그램들이다. 명령어를 입력하는 쉘(shell)은 그러한 프로그래밍을 할 수 있는 pipes와 같은 기법을 제공한다.
저자들이 소스 코드를 요청한 누구에게나 공짜로 배포한 것도 크다. 이는 공개 소스 소프트웨어(open source software)의 초창기 형태라고 할 수 있다.

그리고 LINUX가 나왔다
UNIX가 여러 회사들의 소유권 분쟁으로 인해 느려지는 동안 LINUX가 나왔다. 동시에 현재의 공개 소스 소프트웨어 운동도 시작되었다. 여러 회사가 공짜이면서 자신들의 입맛대로 수정할 수 있는 LINUX를 사용하기로 결정했다.