서로 다른 프로세스가 병행하게 수행될 때, 한 프로세스가 다른 프로세스나 운영체제 자체를 방해해서는 안 된다.
보호는 시스템 자원에 대한 모든 접근이 통제되도록 보장하는 것을 필요로 한다.
보안은 통상 패스워드를 사용해서 자신을 인증하는 것으로부터 시작된다. 보안은 네트워크 어댑터 등과 같은 외부 입출력 장치들을 부적합한 시도로부터 지키고, 침입의 탐지를 위해 모든 접속을 기록하는 것으로 범위를 넓힌다(보안 로그?). 시스템의 보호와 보안이 유지되려면, 시스템 전체에 걸친 예방책이 필요하다.
하드웨어를 직접 접근하는 등의 특정 저수준 작업 외에 일반적인 시스템 콜의 호출은 C와 C++언어로 작성된 함수 형태로 이루어진다.
cp in.txt out.txt
위 명령은 source파일 in.txt를 dest파일 out.txt파일에 복사하라는 명령이다.
이 명령을 수행하기 위해서 무수히 많은 시스템 콜이 호출된다.
우선 in.txt를 오픈하고(시스템 콜), 출력파일을 생성(시스템 콜)하고 오픈(시스템 콜)한다. 만약 입력 파일을 오픈하려고 할 때, 그 이름을 갖는 파일이 없거나 파일에 대한 접근 권한이 없으면 각각 에러메시지(각각 시스템 콜)를 출력한다. 그리고 비정상으로 종료되거나(시스템 콜), 하드웨어 오류(패리티오류)또는 프로그램이 파일의 끝에 도달하는 경우, 디스크 공간이 부족한 경우 모두 각자의 시스템 콜이 호출되어 오류를 처리하게 된다.
자신의 프로그램이 같은 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이 아닌 정수값을 갖는다.
레지스터 + 메모리 블록 레지스터의 개수보다 많은 매개변수가 전달되는 경우. 테이블 형태로 매개변수를 전달
매개변수는 메모리 내의 블록이나 테이블에 저장되고, 블록의 주소가 레지스터 내에 매개변수로 전달된다. ( Linux는 이러한 접근법을 조합하여 5개 이하의 매개변수가 있으면 레지스터가 사용되고, 5개를 넘으면 블록 방법이 사용된다. )
모두 블록이나 스택 매개변수는 프로그램에 의해 스택에 넣어질 수 있고, 운영체제에 의해 꺼내질 수 있다. 전달되는 매개변수들의 개수나 길이를 제한하지 않는 장점이 있다.
응용 프로그램은 운영체제마다 인터프리터가 제공되는 인터프리터 언어(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 또는 이진 실행 파일을 작성하고 컴파일하지 않으면 응용 프로그램이 실행되지 않는다는 것을 의미한다.
기능에 따라 시스템을 구분하고, 한 구성요소의 변경이 다른 구성요소에 영향을 미치지 않는다면 이를 느슨하게 결합된 시스템이라고 부른다.
다양한 모듈화 방식 중 계층적 접근 방식을 알아보자
계층적 접근 방식은 가장 하위 계층인 하드웨어 계층부터 가장 상위계층인 사용자 인터페이스 계층으로 구성되어있다.
이 방식의 핵심은 다음과 같다.
임의의 M층은 자료구조와 자신의 상위 층에서 호출할 수 있는 루틴의 집합으로 구성된다.
그리고 M층은 자신의 하위층들의 서비스만 사용할 수 있다.
이러한 구성은 테스트와 디버깅을 단순화 할 수 있다는 장점이 있다.
첫 번째 층의 디버깅이 끝나면, 두 번째 층을 디버깅 하는 동안 첫 번째 층이 제대로 동작한다는 사실을 가정할 수 있고, 이러한 과정의 반복을 통해 어느 층의 오류가 발견되면 그 하위 층들은 모두 디버깅 되었기 때문에 오류는 현재 테스트중인 층에서 나타났음을 확실시 할 수 있다.
각 층은 자신의 하위 층에 의해 제공된 연산들만 사용해서 서비스를 구현한다. 하위 층의 자세한 구현 방식은 알 필요없고 이러한 연산에 필요한 값과 결과 값만 알면 된다.
이러한 계층적 구조는 이미 OSI 7계층, 5계층 같이 네트워크 및 웹 응용 프로그램에서 성공적으로 사용되었다.
하지만 운영체제에서의 사용은 적은 편이다. 왜냐하면 각 계층의 기능을 확실히 나눠야 한다는 이유 때문이다. (계층을 뚜렷하게 나누기 힘듦. 너무나 많은 연산이 서로 얽히고 얽혀있기 때문에) 그리고 여러 계층을 통과해야 하는 계층적 구조의 특징 때문에 오버헤드가 높아서 성능이 좋지 못하다.
이 기법은 커널은 핵심적인 구성요소를 가지고 있고, 부팅 또는 실행 중에 필요한 부가적인 서비스를 모듈을 통하여 링크해 사용하는 방식이다. 대표적으로 Linux, Mac OS X, Solaris, Windows등이 사용한다.
설계의 주안점은 커널은 핵심 서비스를 제공하고, 다른 서비스들은 커널이 실행되는 동안 동적으로 구현하는 것이다.
예를 들어, CPU 스케줄링과 메모리 관리 알고리즘은 커널에 직접 구현하고, 다양한 파일 시스템을 지원하는 것은 적재가능 모듈을 통하여 구현할 수 있다. 만약 파일 시스템을 커널에 직접 구현한다면 파일 시스템의 수정이 생길 때마다 커널을 다시 컴파일 해야 할 것이다.
전체적으로 계층적 구조와 유사하다. 하지만 모듈에서 다른 임의의 모듈을 호출할 수 있다는 점에서 계층 구조보다 훨씬 유연하다.(계층 구조는 자신의 하위 계층만 호출 가능)
그리고 마이크로커널과도 유사하다(중심 모듈은 핵심기능만을 가지고 있고, 다른 모듈의 적재방법과 모듈들과 어떻게 통신하는지 안다는 점에서). 하지만 모듈간의 통신을 위해 메시지 전달 시스템 콜을 호출할 필요가 없기 때문에 더 효율적이다.
대표적으로 Linux는 장치 드라이버와 파일 시스템을 지원하기 위해 적재가능 커널 모듈(LKM)을 사용한다. LKM은 부팅중에 삽입될 수 있고, Linux 커널에 필요한 드라이버가 없으면 동적으로 적재할 수도 있다. 그리고 LKM은 런타임 중에 커널에서 제거될 수도 있다. 결과적으로 Linux는 모놀리식 시스템의 이점을 유지하면서 동적 모듈 커널은 허용하는 방법을 사용한다.
사용자 경험 층 사용자가 컴퓨팅 장치화 상호 작용할 수 있는 소프트웨어 인터페이스를 정의한다. 마우스 또는 트랙패드 용의 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내의 메시지 전달은 더 이상 복사가 필요가 없기 때문에 효율적인 통신이 가능해진다.
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를 추가했다.