공룡책(운영체제)을 읽고 정리한 글입니다.
운영체제를 이해하는 관점
- 운영체제가 제공하는 서비스에 초점
- 운영체제가 사용자와 프로그래머에게 제공하는 인터페이스에 초점
- 시스템의 구성요소와 그들의 상호 연결에 초점
운영체제 서비스
위 그림은 운영체제의 서비스에 대한 관점에서 본 운영체제의 구성요소들과 그들의 상호관계를 보여준다.
1. 사용자에 도움을 주는 것이 목적인 운영체제의 기능
사용자 인터페이스
말 그대로 사용자가 I/O를 지시하고, 메뉴에서 작업을 선택하고, 화면을 선택할 수 있게 해주는 역할의 인터페이스를 제공한다.
PC에서 주로 사용하는 그래픽 사용자 인터페이스(GUI)가 있고, 휴대폰이나 태블릿에서 사용하는 터치 스크린 인터페이스가 있고, 터미널과 같은 명령어 라인 인터페이스(CLI)가 있다.
프로그램 수행
프로그램을 메모리에 적재해 실행할 수 있어야하고, 종료할 수 있어야 한다.(비정상 종료 포함)
입출력 연산
프로그램은 입출력을 요구할 수 있기 때문에 파일 혹은, 입출력 장치가 연관될 수 있다. 따라서 입출력 수행의 수단을 제공해야 한다.
(책에서 '사용자들은 통상 입출력 장치를 직접 제어할 수 없다.' 라고 나와있던데 키보드나 마우스는 우리가 직접 제어하는게 아닌가? 입출력 장치의 의미가 다른건지?)
파일 시스템 조작
파일을 CRUD할 수 있어야 한다. 파일이나 디렉토리에 따른 접근 권한을 설정할 수 있어야 한다.
그리고 운영체제는 다양한 파일 시스템을 제공한다. ex) FAT16/32 ... NTFS 등
통신
한 컴퓨터 내에서 프로세스간의 통신이 있고, 네트워크에 의해 묶어있는 서로 다른 컴퓨터 간의 통신이 존재한다.
통신은 공유메모리나 메시지 전달 기법을 사용하여 구현할 수 있다.
여기서 메시지 전달 기법은 정보를 패킷으로 만들어서 운영체제에 의해 프로세스들 사이를 이동하는 기법이라고 한다.
오류 탐지
운영체제는 모든 가능한 오류를 항상 의식하고 있어야 한다.
오류로는 CPU, 메모리오류, 정전, 패리티 오류, 네트워크 접속실패, 프린터 종이부족, 연산 오버플로우, 불법적인 메모리 위치 접근 등 정말 많은 종류가 있다.
운영체제는 올바르고 일관성있는 계산을 보장하기 위해서 각 유형의 오류에 대해 적당한 조치를 할 수 있어야 한다.
2. 시스템 자체의 효율적인 동작을 보장하기 위한 기능
자원 할당
다수의 프로세스나 다수의 태스크가 수행될 때, 그들 각각에 알맞은 자원을 할당해 주어야 한다.
CPU를 최대한 효율적으로 이용하기 위해 운영체제는 CPU 스케줄링 루틴이 CPU의 속도, 반드시 실행해야 할 프로세스들, CPU의 처리 코어의 개수와 다른 요인들을 고려해 자원을 할당하게 된다.
따라서 효율적인 시스템 사용을 위해 자원을 할당하는 다양한 루틴이 존재한다.
기록 작성
logging기능을 통해 컴퓨팅 서비스를 개선할 수 있다.
보호와 보안
서로 다른 프로세스가 병행하게 수행될 때, 한 프로세스가 다른 프로세스나 운영체제 자체를 방해해서는 안 된다.
보호는 시스템 자원에 대한 모든 접근이 통제되도록 보장하는 것을 필요로 한다.
보안은 통상 패스워드를 사용해서 자신을 인증하는 것으로부터 시작된다. 보안은 네트워크 어댑터 등과 같은 외부 입출력 장치들을 부적합한 시도로부터 지키고, 침입의 탐지를 위해 모든 접속을 기록하는 것으로 범위를 넓힌다(보안 로그?). 시스템의 보호와 보안이 유지되려면, 시스템 전체에 걸친 예방책이 필요하다.
시스템 콜
운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공한다.
하드웨어를 직접 접근하는 등의 특정 저수준 작업 외에 일반적인 시스템 콜의 호출은 C와 C++언어로 작성된 함수 형태로 이루어진다.
cp in.txt out.txt
위 명령은 source파일 in.txt를 dest파일 out.txt파일에 복사하라는 명령이다.
이 명령을 수행하기 위해서 무수히 많은 시스템 콜이 호출된다.
우선 in.txt를 오픈하고(시스템 콜), 출력파일을 생성(시스템 콜)하고 오픈(시스템 콜)한다. 만약 입력 파일을 오픈하려고 할 때, 그 이름을 갖는 파일이 없거나 파일에 대한 접근 권한이 없으면 각각 에러메시지(각각 시스템 콜)를 출력한다. 그리고 비정상으로 종료되거나(시스템 콜), 하드웨어 오류(패리티오류)또는 프로그램이 파일의 끝에 도달하는 경우, 디스크 공간이 부족한 경우 모두 각자의 시스템 콜이 호출되어 오류를 처리하게 된다.
API
위에서 볼 수 있듯이, 간단한 프로그램이라도 무수히 많은 시스템 콜을 호출하게 된다(초당 수천 개). 사용자 대부분은 이정도의 디테일을 알지 못하기 때문에 API(응용 프로그래밍 인터페이스)에 따라 프로그램을 설계한다.
API는 각 함수에 전달되어야 할 매개변수들과 프로그래머가 기대할 수 있는 반환 값을 포함하여 응용 프로그래머가 사용 가능한 함수의 집합을 명시한다.(일종의 명세서 같은 느낌)
가장 대표적인 API 3가지로는 Windows API, POSIX API(UNIX, Linux, Mac OS), Java API가 있다.
특히 UNIX와 Linux 시스템에서 C 언어로 작성 된 프로그램을 위해서 제공되는 라이브러리는 libc로 불린다.
API를 사용하는 이유
호환성 때문이다.
자신의 프로그램이 같은 API를 지원하는 어느 시스템에서건 컴파일되고 실행됨을 기대할 수 있기 때문이다.
표준 API의 예
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count)
int fd : 읽으려는 파일 디스크립터
void *buf : 데이터를 읽어 들일 버퍼
size_t count : 버퍼로 읽어 들일 수 있는 최대 바이트 수
읽기가 성공한 경우 읽어 들인 바이트 수가 반환된다. 반환 값이 0인 경우는 파일의 끝에 도달했다는 것을 의미한다.
오류가 발생한 경우 -1을 리턴한다.
파일 디스크립터(File Descriptor)란 리눅스 혹은 유닉스 계열의 시스템에서 프로세스(process)가 파일(file)을 다룰 때 사용하는 개념으로, 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값이다.
파일 디스크럽터는 일반적으로 0이 아닌 정수값을 갖는다.
RTE(실행시간 환경 : Run Time Environment)
운영체제가 제공하는 시스템 콜에 대한 연결고리 역할을 하는 시스템 콜 인터페이스를 제공한다.
시스템 콜 인터페이스는 API 함수의 호출을 가로채어 알맞은 시스템 콜을 부른다.
시스템 콜은 각자의 번호가 할당되기 때문에 시스템 콜 인터페이스는 이 번호에 따라 색인되는 테이블을 유지하게 된다.
따라서 운영체제 인터페이스에 대한 대부분의 자세한 내용은 API에 의해 프로그래머로부터 숨겨지고 RTE에 의해 관리된다.
운영체제에 매개변수를 전달하는 방법
- 레지스터를 통해 전달한다.
- 레지스터 + 메모리 블록
레지스터의 개수보다 많은 매개변수가 전달되는 경우.
매개변수는 메모리 내의 블록이나 테이블에 저장되고, 블록의 주소가 레지스터 내에 매개변수로 전달된다.
( Linux는 이러한 접근법을 조합하여 5개 이하의 매개변수가 있으면 레지스터가 사용되고, 5개를 넘으면 블록 방법이 사용된다. ) - 모두 블록이나 스택
매개변수는 프로그램에 의해 스택에 넣어질 수 있고, 운영체제에 의해 꺼내질 수 있다.
전달되는 매개변수들의 개수나 길이를 제한하지 않는 장점이 있다.
시스템 콜의 유형
시스템 콜은 다섯 가지의 중요한 범주가 있다.
1. 프로세스 제어(Process Control)
실행 중인 프로그램을 정상적으로(end()) 또는 비정상적으로(abort()) 멈출 수 있어야 한다.
만약 프로그램에 문제가 발생해 오류 트랩을 유발할 경우, 메모리 덤프가 일어나고 오류메시지가 생성된다.
- 끝내기(end, exit()), 중지(abort)
- 적재(load), 실행(execute, exec())
- 프로세스 생성(create process, fork())
- 프로세스 속성 획득과 설정(get process attribute and set process attribute)
- 시간 대기(wait time)
- 사건 대기(wait event)
- 사건을 알림(signal event)
- 메모리 할당 및 해제 : malloc, free
- 프로세스간 데이터 공유 제한 : acquire_lock(), release_lock()
2. 파일 조작(File Manipulation)
- 파일 생성(create file), 파일 삭제(delete file)
- 열기(open), 닫기(close)
- 읽기(read), 쓰기(write), 위치 변경(reposition, rewind())
- 파일 속성 획득 및 설정(get file attribute and set file attribute)
3. 장치 관리(Devide Management)
프로세스는 작업을 계속 수행하기 위해 추가 자원이 필요할 수 있다. 이러한 추가 자원은 주 기억장치, 디스크, 파일의 접근 등이 될 수 있다. 이러한 자원들은 장치로 간주될 수 있다. 운영체제는 이러한 장치를 관리해야 한다.
- 장치를 요구(request devices), 장치를 방출(release device)
- 읽기, 쓰기, 위치 변경(read, write, reposition, open, close)
- 장치 속성 획득, 장치 속성 설정
- 장치의 논리적 부착(attach) 또는 분리(detach)
4. 정보 유지(Information Maintenance)
- 시간과 날짜의 설정과 획득(time)
- 시스템 데이터의 설정과 획득(date)
- 디버깅에 도움을 주는 메모리 덤프(dump)
일반적으로 트랩(trap)은 디버거에 의해 포착된다. - 프로세스 파일, 장치 속성의 획득 및 설정(get_process_attributes, set_process_attributes)
5.1 통신(Communication)
PC끼리의 통신이라면 각 컴퓨터는 호스트 이름을 가지며, 이 이름은 운영체제에 의해 동등한 식별자로 반환된다.
마찬가지로 하나의 컴퓨터 내부의 프로세스간의 통신이라면 프로세스 이름이 식별자로 반환되어 프로세스를 가리키는데 사용된다.
Client와 Server의 메시지들은 read_message, write_message 시스템 콜에 의해서 교환된다.
- 통신 연결의 생성, 제거
- 메시지의 송신, 수신
- 상태 정보 전달
- 원격 장치의 부착(attach) 및 분리(detach)
5.2 보호(Protection)
파일과 디스크와 같은 자원의 허가 권한을 설정하는데 이용된다.
특정 사용자가 지정된 자원에 대해 접근이 허가 혹은 불허되었는지를 명시한다.
- get file permissions
- set file permissions
- alow_user, deny_user
시스템 서비스
현대 시스템은 시스템 서비스의 집합체라고 부를 수 있다.
최하위 수준은 하드웨어이고, 다음은 운영체제, 그다음은 시스템 서비스, 그리고 마지막으로 응용 프로그램이다.
시스템 서비스는 시스템 유틸리티로도 알려진, 프로그램 개발과 실행을 위해 더 편리한 환경을 제공한다.
파일관리
- 파일과 디렉터리 생성, 삭제, 복사, 개명, 인쇄, 열거, 조작
상태 정보
- 사용가능한 메모리, 디스크 양, 사용자 수, 날짜, 시간 등
- 상세 성능, 로깅 및 디버깅 정보
- 일반적으로 GUI에 표시
파일 변경
- 파일의 내용을 생성하고 변경하기 위해 text editor를 사용할 수 있다.
프로그래밍 언어 지원
- C, C++, Java, Python등에 대한 컴파일러, 어셈블러, 디버거 및 해석기가 운영체제와 함께 제공될 수 있다.
프로그램 적재와 수행
- 프로그램이 컴파일 된 후, 수행되려면 반드시 메모리에 적재되어야 한다.
- 절대 로더, 재배치 가능 로더, 링키지 에디터, 중첩 로더를 제공한다.
- 디버깅 시스템 필요
통신
- 프로세스, 사용자, 다른 컴퓨터들 사이에 가상 접속을 이루기 위한 기법을 제공
백그라운드 서비스
- 시스템이 정지될 때까지 계속해서 실행되는 프로세스.
- 예를 들어 서비스, 서브시스템, 디먼 등
네트워크 디먼 : 연결 요청을 올바른 프로세스로 연결해주기 위해 네트워크 연결을 청취하는 일을 수행한다.
링커와 로더
일반적으로 프로그램은 디스크에 이진 실행 파일로 존재한다.
CPU에서 실행하려면, 프로그램을 메모리로 가져와 프로세스 형태로 배치되어야 한다.
'프로그램 컴파일 -> 메모리에 배치 -> CPU 코어에서 실행'
소스 파일은 물리 메모리 위치에 로드되도록 설계된 오브젝트 파일로 컴파일된다. (재배치 가능 오브젝트 파일)
링커는 재배치 가능 오브젝트 파일을 하나의 이진 실행 파일로 결합한다. 이때 다른 오브젝트 파일 또는 라이브러리도 포함된다.
로더는 이진 실행 파일을 메모리에 로드하는 데 사용되며, CPU 코어에서 실행할 수 있는 상태가 된다.
링크 및 로드와 관련된 활동은 재배치라고 부른다.
프로그램 부분에 최종 주소를 할당하고, 프로그램 코드와 데이터를 해당 주소와 일치하도록 조정한다.
런타임에 코드가 라이브러리 함수를 호출하고 변수에 접근할 수 있게 한다.
이 작업이 끝나고 로더를 실행하려면 명령어 라인에 실행 파일의 이름을 입력하기만 하면 된다.
컴파일된 object 파일들을 linking해서 exe파일로 만드는 과정에서 어떤 일들이 일어나는지 알아보자
// main.c
int sum(int *a, int n)
int array[2] = {1, 2}
int main(){
int val = sum(array, 2);
return val;
}
// sum.c
int sum(int *a, int n){
int i, s = 0;
for(i = 0; i < n; i++){
s += a[i];
}
return s;
}
main.c의 sum 함수는 어떤 과정을 통해서 sum.c의 sum 함수를 불러와서 기능을 수행할 수 있을까?
위와 같이 여러 개로 이루어진 파일들을 연결하는 과정을 'Linking' 이라고 한다.
- 먼저 cpp(C Pre-Processor 전처리) 과정을 거친다.
#include, #define 같은 과정을 통해 code에서 해당하는 부분들을 전처리해준다. - cc1이라는 과정을 통해서 어셈블러 코드로 변환된다 C->assembly
- 마지막으로 어셈블러 코드를 통해 .o파일(바이너리 파일)을 만들어 준다.
위 과정을 거치면 main.c, sum.c 파일 모두. o파일이 생기게 된다.
이때 생성된 .o파일들은 Linker를 통해서 실행 가능한 object파일로 변환된다.
응용 프로그램이 운영체제마다 다른 이유
기본적으로 한 운영체제에서 컴파일된 응용 프로그램은 다른 운영체제에서 실행할 수 없다.
그러나 동일한 프로그램을 다른 운영체제에서 사용한 적이 있을 것이다.
이를 가능하게 하는 세 가지 방법을 알아보자
- 응용 프로그램은 운영체제마다 인터프리터가 제공되는 인터프리터 언어(Python, Ruby 등)로 작성될 수 있다.
인터프리터는 소스 프로그램의 각 라인을 읽고, 상응하는 기계어 명령을 실행하고, 해당 운영체제의 시스템 콜을 호출한다.
하지만 기계어 코드로 구성된 응용 프로그램에 비해 성능이 떨어지고, 인터프리터는 각 운영체제 기능의 일부만 제공하므로 관련 응용 프로그램의 기능도 제한될 수 있다. - 프로그램은 실행 중인 응용프로그램을 포함하고있는 가상 머신을 가진 언어로 작성될 수 있다.
가상 머신은 언어의 RTE중 일부이다. 대표적인 예로 Java가 있다.
따라서 이론적으로 Java프로그램은 RTE가 제공되는 어디에서나 실행될 수 있다.
하지만 이러한 시스템은 인터프리터 언어의 단점과 비슷한 단점을 가진다 - 컴파일러가 기기 및 운영체제 고유의 이진 파일을 생성하는 표준 언어 또는 API를 사용할 수 있다.
프로그램이 실행될 운영체제로 이식되어야 한다는 뜻이다.
많은 시간이 소요될 수 있고 많은 시험과 디버깅이 응용 프로그램의 새 버전마다 수행되어야 한다.
위 방법처럼 다양한 운영체제에서 실행될 수 있는 프로그램을 개발하기 위해서 제공되는 다양한 솔루션이 있다.
하지만 시스템의 Low Level에는 다른 어려운 점이 존재한다.
- 각 운영체제는 헤더, 명령어 및 변수의 배치를 강제하는 응용 프로그램 이진 형식이 있다.
이러한 구성요소는 명시된 구조 형태로 실행 파일 내의 특정 위치에 있어야 운영체제가 파일을 열고 응용 프로그램을 적재하여 올바르게 실행할 수 있다. - CPU는 다양한 명령어 집합을 가지며 해당 명령어가 포함된 응용 프로그램만 올바르게 실행할 수 있다.
- 운영체제는 응용 프로그램이 파일 생성과 네트워크 연결 열기와 같은 다양한 활동을 요청할 수 있는 시스템 콜을 제공한다. 이러한 시스템 콜은 사용되는 피연산자, 피연산자 순서, 프로그램이 시스템 콜을 호출할 수 있는 방법, 시스템 콜 번호, 의미 및 반환 결과를 포함하여 여러 측면에서 운영체제마다 다르다.
이러한 구조적 차이점을 완전히 해결하지는 못했지만 해결하는데 도움이 되는 몇 가지 방법이 있다.
- 대부분의 UNIX 시스템에서 이진 파일은 ELF 형식을 채택하여 UNIX 시스템 간의 호환성을 높였다.
- API와 비슷하게 ABI(Application Binary Interface)를 사용한다.
ABI는 주소 길이, 시스템 콜에 매개변수를 전달하는 방법, 런타임 스택 구성, 시스템 라이브러리의 이진 형식 및 데이터 유형의 크기 등의 하위 수준의 세부 정보를 명시한다.
일반적으로 ABI는 특정 아키텍처에 대해 명시된다. 따라서 ABI는 아키텍처 수준의 API이다.
요약하자면, 이러한 모든 차이점은 특정 CPU유형(Intel x86, ARMv8)의 특정 운영체제에서 인터프리터, RTE 또는 이진 실행 파일을 작성하고 컴파일하지 않으면 응용 프로그램이 실행되지 않는다는 것을 의미한다.
운영체제 설계 및 구현
운영제체를 설계하기 위한 성공적인 접근 방법
1. 설계 목표
첫 번째 문제는 시스템의 목표와 명세를 정의하는 일이다.
이는 근본적으로 사용자 목적과 시스템 목적의 두 가지 기본 그룹으로 나눌 수 있다.
사용자 입장에서는 시스템이 사용하기 쉽고 편리하며, 배우기 쉽고, 믿을 수 있고, 안전하고, 신속해야 한다.
하지만 이러한 사항은 일반적으로 약속된 공통된 방법이 없기 때문에 명세가 쓸모있지는 않다.
이와 유사하게 시스템을 설계, 생성, 유지보수하는 사람들을 위한 요구 조건이 있다.
설계, 구현, 유지보수가 쉬워야 하며, 적응성, 신뢰성, 무오류, 효율성을 가져야 한다.
위 모든 조건들은 애매하며 다양하게 해석될 수 있다.
따라서 운영체제에 대한 요구를 정의하는 문제를 해결하는 유일한 해법은 없다고 볼 수 있다.
하지만 소프트웨어 공학 분야에 의해 개발된 운영체제에 적용 가능한 일반적인 원칙들이 존재한다.
2. 기법과 정책
기법으로부터 정책을 분리하라.
무슨말일까?
기법은 어떤 일을 어떻게 할 것인가를 결정하는 것이고, 정책은 무엇을 할 것인가를 결정하는 것이다.
예를 들어 타이머 구조는 CPU 보호를 보장하기 위한 기법이지만(CPU 보호를 어떻게 할 것인가? -> 타이머를 사용해서),
특정 사용자를 위해 타이머를 얼마나 오랫동안 설정할지를 결정하는 것은 정책적 결정이다(무엇? -> '타이머를 얼마나 오랫동안 설정할지').
정책과 기법의 분리는 융통성을 위해 아주 중요하다. 정책은 장소가 바뀌거나 시간이 흐름에 따라 변경될 수 있다.
최악의 경우는 정책의 변경이 베이스로 깔린 기법의 변경을 요구하는 경우이다.
따라서 바람직한 기법은 여러 정책에서 사용되기에 충분히 융통성이 있어야한다.
예를 들어, 한 유형의 프로그램이 다른 유형의 프로그램보다 높은 우선순위를 가지도록 하는 기법을 생각해보자.
만약 기법이 정책으로부터 적절하게 분리되면, 입출력 중심 프로그램이 CPU 중심 프로그램보다 높은 우선순위를 가지도록 하는 정책을 지원할 수 있다.
실제로 표준 Linux 커널에는 특정 CPU 스케줄링 알고리즘이 있고, 이 알고리즘은 특정 정책을 지원하는 기법이다. 그러나 누구나 다른 정책을 지원하기 위해 스케줄러를 자유롭게 수정하거나 교체할 수 있다.
특히 자원 할당 문제에 있어서 정책의 결정은 매우 중요하다.
자원 할당 여부를 결정할 때마다 정책을 결정해야한다. (자원(무엇)에 대한 결정)
여기서 자원을 어떻게 할당할 지를 결정하는건 기법이다.
3. 구현
설계가 완료되면 구현할 차례이다.
대부분은 C, C++같은 고급 언어로 작성되며, 극히 일부의 시스템이 어셈블리어로 작성된다. (커널의 최하위 레벨은 어셈블리, C로 작성될 수 있다.) 시스템 라이브러리는 C++ 이상 레벨의 언어로 작성될 수 있으며 대표적인 예로 Android가 있다.
Android의 커널은 어셈블리어 + C로 작성되었고, 시스템 라이브러리는 C, C++로 작성되었고, 시스템에 개발자 인터페이스를 제공하는 응용 프로그램 프레임워크는 Java로 작성되었다.
운영체제를 구현하기 위해서 고급 언어나 시스템 구현 언어(C, C++..)를 사용함으로써 얻는 장점
- 코드를 빨리 작성할 수 있다.
- 간결해서 이해하기 쉽고, 디버깅하기 쉽다.
- 추가적으로 컴파일러 기술이 향상되면 단순히 고급 언어를 재컴파일 함으로써 더 빠른 속도를 얻는다.
- 다른 하드웨어로 이식하는 것이 훨씬 쉽다. 이는 여러 하드웨어 시스템에서 실행되어야 하는 운영체제에 특히 중요하다.
단점
- 속도가 느리고 저장 장치가 많이 소요된다.
하지만 현대의 컴파일러는 대규모 프로그램을 위해 복잡한 분석을 수행하고 정교한 최적화를 적용하여 우수한 코드를 생산할 수 있다. 깊은 파이프라이닝과 다수의 기능장치 덕분에 인간이 할 수 있는 것보다 훨씬 쉽게 복잡한 의존성의 상세 사항들을 처리할 수 있다.
추가적으로 운영체제의 주요 성능 향상은 우수한 어셈블리어 코드보다 좋은 자료구조와 알고리즘의 결과일 가능성이 높다. 그리고 운영체제가 크지만, 소량의 코드만이 성능이 중요하다. 인터럽트 핸들러, 입출력 관리자, 메모리 관리자와 CPU 스케줄러등.
운영체제 구조
1. 모놀리식 구조
커널의 모든 기능을 단일 주소 공간에서 실행되는 단일 정적 이진 파일에 넣는 구조.
모놀리식 구조의 대표적인 운영체제는 최초의 UNIX이다.
UNIX는 커널과 시스템 프로그램의 두 부분으로 구성된다.
위 그림에서 시스템 콜과 하드웨어 위의 모든 것이 커널이다.
UNIX를 기반으로 만들어진 Linux 운영체제는 아래와 유사하게 구성된다.
응용 프로그램은 커널에 대한 시스템 콜 인터페이스와 통신할 때 glibc 표준 C 라이브러리를 사용한다.
Linux 커널은 단일 주소 공간에서 커널 모드로 전부 실행된다는 점에서 모놀리식이지만, 런타임 중에 커널을 수정할 수 있다는 모듈식 설계를 갖추고 있다.
모놀리식 커널은 매우 단순하지만 이 구조는 구현 및 확장하기 어렵다.
하지만 성능면에서는 뚜렷한 이점이 있다.
시스템 콜 인터페이스는 오버헤드가 거의 없고, 커널 내부의 통신 속도가 빠르다.
이러한 장점 때문에 Linux, Windows는 여전히 모놀리식 운영체제를 사용중이다.
2. 계층적 접근
기능에 따라 시스템을 구분하고, 한 구성요소의 변경이 다른 구성요소에 영향을 미치지 않는다면 이를 느슨하게 결합된 시스템이라고 부른다.
다양한 모듈화 방식 중 계층적 접근 방식을 알아보자
계층적 접근 방식은 가장 하위 계층인 하드웨어 계층부터 가장 상위계층인 사용자 인터페이스 계층으로 구성되어있다.
이 방식의 핵심은 다음과 같다.
임의의 M층은 자료구조와 자신의 상위 층에서 호출할 수 있는 루틴의 집합으로 구성된다.
그리고 M층은 자신의 하위층들의 서비스만 사용할 수 있다.
이러한 구성은 테스트와 디버깅을 단순화 할 수 있다는 장점이 있다.
첫 번째 층의 디버깅이 끝나면, 두 번째 층을 디버깅 하는 동안 첫 번째 층이 제대로 동작한다는 사실을 가정할 수 있고, 이러한 과정의 반복을 통해 어느 층의 오류가 발견되면 그 하위 층들은 모두 디버깅 되었기 때문에 오류는 현재 테스트중인 층에서 나타났음을 확실시 할 수 있다.
각 층은 자신의 하위 층에 의해 제공된 연산들만 사용해서 서비스를 구현한다. 하위 층의 자세한 구현 방식은 알 필요없고 이러한 연산에 필요한 값과 결과 값만 알면 된다.
이러한 계층적 구조는 이미 OSI 7계층, 5계층 같이 네트워크 및 웹 응용 프로그램에서 성공적으로 사용되었다.
하지만 운영체제에서의 사용은 적은 편이다. 왜냐하면 각 계층의 기능을 확실히 나눠야 한다는 이유 때문이다. (계층을 뚜렷하게 나누기 힘듦. 너무나 많은 연산이 서로 얽히고 얽혀있기 때문에) 그리고 여러 계층을 통과해야 하는 계층적 구조의 특징 때문에 오버헤드가 높아서 성능이 좋지 못하다.
3. 마이크로 커널
마이크로 커널 방식은 모든 중요하지 않은 구성요소를 커널로 부터 제거하고, 이들을 별도의 주소공간에 분리해 사용자 프로그램을 따로 만드는 방식이다. (이때 어떤 서비스가 기존 커널에 남아있어야 하는지는 의견이 갈린다.)
만약 프로그램이 사용자의 파일에 접근하기를 원한다고 가정하자.
이때 프로그램은 파일 시스템 서버와 반드시 상호작용 해야할 것이다. 하지만 마이크로 커널에서 프로그램은 절대로 파일 시스템과 직접적으로 상호작용하지 않는다. 커널을 통해서 파일 시스템을 전달하고, 커널을 통해서 파일 시스템의 응답을 전달 받을 것이다.
마이크로커널의 장점은 운영체제의 확장이 쉽다는 것이다. 모든 새로운 서비스는 사용자 공간에 따로 추가되며, 커널을 변경할 필요가 없다. 결과적으로 운영체제는 하드웨어에서 하드웨어로의 이식이 쉽다.
그리고 마이크로 커널은 서비스 대부분이 커널이 아니라 사용자 프로세스로 수행되기 때문에 더욱 높은 보안과 신뢰성을 제공한다는 장점이 있다. 만일 한 서비스가 잘못되더라도, 운영체제의 다른 부분은 아무런 영향을 받지 않는다.
단점으로는 시스템 기능 오버헤드 때문에 성능이 나빠진다는 점이다. 두 개의 사용자 프로세스가 통신하는 경우 별도의 주소 공간에 서비스가 존재하기 때문에 프로세스의 메시지가 복사되어야 한다. 그리고 메시지를 전달하기 위해서 프로세스간의 전환이 필요할 것이다.
이러한 메시지 복사와 프로세스 전환에 걸리는 오버헤드는 마이크로커널 기반 운영체제의 성장에 가장 큰 장애이다.
마이크로커널을 사용하는 대표적인 운영체제로는 MacOS및 IOS가 있다.
4. 모듈
운영체제 설계의 최근 최선책은 적재가능 커널 모듈(Loadable Kernel Modules, LKM) 기법의 사용이다.
이 기법은 커널은 핵심적인 구성요소를 가지고 있고, 부팅 또는 실행 중에 필요한 부가적인 서비스를 모듈을 통하여 링크해 사용하는 방식이다. 대표적으로 Linux, Mac OS X, Solaris, Windows등이 사용한다.
설계의 주안점은 커널은 핵심 서비스를 제공하고, 다른 서비스들은 커널이 실행되는 동안 동적으로 구현하는 것이다.
예를 들어, CPU 스케줄링과 메모리 관리 알고리즘은 커널에 직접 구현하고, 다양한 파일 시스템을 지원하는 것은 적재가능 모듈을 통하여 구현할 수 있다. 만약 파일 시스템을 커널에 직접 구현한다면 파일 시스템의 수정이 생길 때마다 커널을 다시 컴파일 해야 할 것이다.
전체적으로 계층적 구조와 유사하다. 하지만 모듈에서 다른 임의의 모듈을 호출할 수 있다는 점에서 계층 구조보다 훨씬 유연하다.(계층 구조는 자신의 하위 계층만 호출 가능)
그리고 마이크로커널과도 유사하다(중심 모듈은 핵심기능만을 가지고 있고, 다른 모듈의 적재방법과 모듈들과 어떻게 통신하는지 안다는 점에서). 하지만 모듈간의 통신을 위해 메시지 전달 시스템 콜을 호출할 필요가 없기 때문에 더 효율적이다.
대표적으로 Linux는 장치 드라이버와 파일 시스템을 지원하기 위해 적재가능 커널 모듈(LKM)을 사용한다. LKM은 부팅중에 삽입될 수 있고, Linux 커널에 필요한 드라이버가 없으면 동적으로 적재할 수도 있다. 그리고 LKM은 런타임 중에 커널에서 제거될 수도 있다. 결과적으로 Linux는 모놀리식 시스템의 이점을 유지하면서 동적 모듈 커널은 허용하는 방법을 사용한다.
5. 하이브리드 시스템
엄격하게 정의된 하나의 구조를 선택한 운영체제는 없다.
대신에 다양한 구조를 결합하여 성능, 보안 및 편리성 문제를 해결하려는 혼용 구조로 구성된다.
macOS 와 IOS
macOS와 IOS의 일반적인 아키텍처이다.
- 사용자 경험 층
사용자가 컴퓨팅 장치화 상호 작용할 수 있는 소프트웨어 인터페이스를 정의한다.
마우스 또는 트랙패드 용의 Aqua, 터치용의 Springboard - 응용 프로그램 프레임워크
Cocoa, Cocoa Touch 프레임워크(macOS)와 Objective-C, Swift(IOS)에 대한 API를 제공한다. - 핵심 프레임워크
Quicktime 및 OpenGL을 포함한 그래픽 및 미디어를 지원하는 프레임워크이다. - 커널 환경(Darwin)
Mach 마이크로커널과 BSD UNIX 커널이 포함된다.
예전에는 macOS와 IOS는 많은 차이가 있었다. macOS는 Intel기반의 실행 운영체제였고 IOS는 ARM 기반의 운영체제였다. 2020년 하반기 M1칩의 출시이후 macOS가 ARM 기반의 운영체제가 되었다. 그래서 기존 Intel 아키텍처에서 사용하던 프로그램의 호환성을 위해서 로제타2라는 동적 바이너리 기술이 탑재되었다.
이제 macOS 커널 Darwin에 대해서 알아보자
Mac의 터미널에서 uname 을 입력하면 Darwin이 출력될 것이다.
Darwin은 Mach 시스템 콜(트랩이라고 한다)과 BSD(POSIX 기능 제공) 시스템 콜 두 개의 시스템 콜 인터페이스를 제공한다. 이들은 표준 C 라이브러리, 네트워킹, 보안 및 프로그래밍 언어 지원을 제공하는 라이브러리를 포함한 풍부한 라이브러리 집합이다.
Mach는 메모리 관리, CPU 스케줄링 및 메시지 전달 및 원격 프로시저 호출과 같은 프로세스 간 통신(IPC) 기능을 포함한 기본 운영체제 서비스를 제공한다. Mach는 커널 추상화 기능을 사용해서 대부분의 기능을 제공한다.
(이 추상화에는 Mach프로세스, 스레드, 메모리 객체 및 포트가 포함된다.) 예를 들어, 응용 프로그램은 BSD운영체제의 POSIX규약의 fork() 시스템 콜을 호출하여 새 프로세스를 생성할 수 있다.
Mach 및 BSD 외에도 커널은 장치 드라이버 및 동적 적재가능 모듈개발을 위한 iokit를 제공한다.
이러한 마이크로 서비스의 단점은 프로세스간 통신의 오버헤드가 크고 통신에 사용되는 메시지의 복사로 인해 자원이 낭비된다는 점이 있었다. 하지만 Darwin에서는 Mach, BSD, iokit 및 모든 커널을 하나의 주소 공간으로 결합한다.
그래서 순수한 마이크로커널로 볼 수 없지만 Mach내의 메시지 전달은 더 이상 복사가 필요가 없기 때문에 효율적인 통신이 가능해진다.
Android
Google의 주도 하에 Android 스마트폰과 태블릿을 위해서 개발되었다.
IOS는 오픈소스가 아니지만 Android는 오픈소스이다. 이는 안드로이드의 빠른 성장의 이유 중 큰 부분을 차지한다.
안드로이드 장치의 소프트웨어는 Java로 개발하지만 일반적으로 표준 Java API를 사용하지 않는다.
Google은 Java 개발을 위해 별도의 안드로이드 API를 설계했다. 따라서 Java 응용 프로그램은 Android RunTime(ART)에서 실행하도록 컴파일 된다.
ART는 안드로이드용으로 설계되어 메모리와 CPU 처리 능력이 제한적인 모바일 장치에 최적화된 가상 머신이다.
대부분의 Java 가상 머신이 프로그램 효율을 향상시키기 위해 JIT(Just In Time) 컴파일을 수행하지만 ART는 AOT(Ahead Of Time) 컴파일을 수행한다.
JIT
바이트 코드를 실제 기계어로 컴파일하는 것을 실행 시점에 수행한다.
실행 속도가 상대적으로 느리지만, 앱 설치 속도, 설치 용량 등이 AOT에 비해 빠르고 적게 차지한다.
AOT
서버에서 미리 컴파일을 해놓는다.
실제 실행 시 별도 컴파일 과정이 없어서 속도가 상대적으로 빠르다.
미리 컴파일을 해놓기 때문에 설치 속도가 느리고, 용량이 좀 더 큰 편이다.
AOT 컴파일은 모바일 시스템에 중요한 기능인 전력 소비를 줄이면서 더 효율적인 응용 프로그램 실행을 가능하게 한다.
JNI(Java Native Interface)를 사용해서 자바 가상 머신을 우회해 특정 하드웨어 기능에 엑세스할 수 있는 프로그램을 개발할 수 있다. 이렇게 개발된 프로그램은 하드웨어간의 이식이 불가능하다.
안드로이드 스택의 맨 아래에는 Linux 커널이 존재한다. Google은 전원 관리와 같은 모바일 환경의 특수한 요구를 지원하기 위해 많은 부분의 Linux 커널을 수정했다. 그리고 메모리 관리 및 할당 방식을 변경했으며, Binder로 알려진 새로운 IPC를 추가했다.
운영체제 빌딩과 부팅
운영체제 디버깅
연습 문제
2.1 시스템 콜의 목적은 무엇인가?
유저 시스템에서 운영체제 서비스의 기능을 요청하기 위한 인터페이스 역할을 한다.
2.2 명령 인터프리터의 목적은 무엇인가? 통상 커널에 포함되지 않는 이유는 무엇인가?
인터프리터는 소스 프로그램의 각 라인을 읽고, 상응하는 기계어 명령을 실행하고, 해당 운영체제의 시스템 콜을 호출한다.
인터프리터는 온전히 사용자를 위한 프로세스이고 언제든지 변할 수 있는 대상이므로 커널에 포함되지 않는다.
2.3 UNIX 시스템에서 새 프로세스를 시작하기 위해 명령 인터프리터나 셸에서 어떤 시스템 콜이 실행되어야 하는가?
1. 사용자가 셸에 실행파일을 입력한다.
2. 셸은 fork() 명령어를 사용해서 현재 프로세스를 계승하는 새로운 프로세스를 복제해서 만든다.
3. exec() 명령어를 호출해서 프로세스를 실행시킨다.
2.4 시스템 프로그램의 목적은 무엇인가?
시스템 프로그램은 사용자가 사용가능한 시스템 콜의 집합이라고 볼 수 있다.
대부분의 사용자가 일반적으로 해결해야하는 문제를 직접 해결할 필요가 없게 도와준다.
2.5 시스템 설계시 계층화된 접근 방식의 주요 장점은 무엇인가? 단점은 무엇인가?
장점 : 자신의 하위 계층의 서비스만 사용할 수 있기 때문에 테스트와 디버깅이 수월하다
단점 : 사용자의 메시지가 많은 계층을 거쳐야 하므로 오버헤드가 크다.
2.6 운영체제에서 제공하는 5가지 서비스를 나열하고 각 서비스가 사용자에게 편의를 제공하는 방법을 설명하라. 사용자 수준 프로그램이 이러한 서비스를 제공할 수 없는 경우는 언제인가?
1. 프로그램 실행 : 운영체제는 프로그램을 메모리에 적재하고 실행시킨다. 사용자 입장에서 마치 프로그램이 스스로 프로세스를 만드는 것 같지만, 프로그램이 호출하는 시스템 콜에 의해서 프로그램의 실행이 수행된다.
2. I/O 연산 : 프로그램은 입출력을 요구할 수 있기 때문에 파일 혹은, 입출력 장치가 연관될 수 있다. 따라서 입출력 수행의 수단을 제공해준다.
3. 파일 시스템 조작 : 파일의 CRUD를 수행한다. 파일이나 디렉토리에 따른 접근 권한을 설정할 수 있어야 한다.
그리고 운영체제는 다양한 파일 시스템을 제공한다.
4. 사용자 인터페이스 : 말 그대로 사용자가 I/O를 지시하고, 메뉴에서 작업을 선택하고, 화면을 선택할 수 있게 해주는 역할의 인터페이스를 제공한다.
5. 통신 : 한 컴퓨터 내에서 프로세스간의 통신이 있고, 네트워크에 의해 묶어있는 서로 다른 컴퓨터 간의 통신을 수행한다.
'CS > OS' 카테고리의 다른 글
[운영체제] Ch4. 스레드와 병행성 (0) | 2022.02.11 |
---|---|
[운영체제] Ch3. 프로세스 (0) | 2022.02.09 |
[운영체제] Ch1. 서론 (0) | 2022.02.01 |
[OS] Virtual Memory #1 (0) | 2021.07.21 |
[OS] Scheduling #3 (0) | 2021.07.12 |