📖 본 글은 모든 내용을 "Operating System Concepts Ed.10"에서 인용합니다.
💡 An operating system provides an environment for the execution of programs.
유저 입장에서는 편의성, 시스템 입장에서는 관리자로서 역할했다면,
앱 입장에서는 구동 환경을 제공해주는 것이 운영체제이다.
그렇다면 어떻게 생겼길래 구동환경을 만들어준다는 것일까?
운영체제마다 구조가 다 다르기 때문에 정확히 어떻게 생겼는지는 모르지만,
전체적인 그림은 다음과 같다.

우선 나중에 알아볼 "시스템콜"을 기점으로 두 진영으로 나뉜다.
유저가 조작할 수 있게 해주는 인터페이스 공간과,
하드웨어를 동작시키는 기능들이 모여있는 service 공간이 있다.
이 service 내의 기능들을 다시 두 분류로 나눠서 알아보자.
프로그램 실행
I/O 작업
파일시스템 조작
커뮤니케이션
오류 감지
자원 할당
로깅 (Logging)
보호와 보안
거의 모든 운영체제는 유저 인터페이스를 가지고 있다.
일부 임베디드 시스템 OS를 제외하면 크게 3가지 인터페이스 종류로 볼 수 있다.
💡 Most operating systems, include Linux, UNIX, and Windows, treat the command interpreter as a special program that is running when a process is initiated or when a user first logs on.
대부분의 운영체제에서 CLI를 항시 가동하는 특수한 프로그램으로 간주한다.
Command Interpreter는 약속된 명령어(ex. rm, cd)들을 사용하면,
OS가 이를 해석하여 요청한 업무를 처리해주는 방식이다.
우리가 흔히 아는 CMD나 terminal 등이 이에 해당하는데,
OS와 통신할 수 있는 가장 기본적인 인터페이스이다.
이들은 보통 shell이라는 명칭으로 불리기도 한다.
사실 최근의 디바이스들은 그래픽 요소들을 기본적으로 탑재하고있다.
바탕화면, 아이콘, 제어판 등 모든 것들이 여기에 해당한다.
일반 사용자들 입장에서 CLI는 명령어를 모르면 사용하기 불편하기 때문에
직관적으로 사용할 수 있게 해주는 편의성 제공 차원에서 생기게 되었다.
그러나 모바일로 넘어오면서 키보드나 마우스와 같은 장치를 연결하기 힘들어서
터치방식으로 작동할 수 있는 인터페이스도 새로 생기게 되었다.
💡 System calls provide an interface to the services made available by an operating system.
시스템콜도 결국 OS가 제공하는 인터페이스의 일종이다.
그리고 syscall은 기본적으로 C나 C++과 같은 low-level 언어로 구성되어있다.
(최근 들어서는 Rust를 주로 사용하기도 한다.)
그렇다면 syscall은 어떤 것들일까?
사용자가 파일을 복사하는 명령어(cp fileA fileB)를 실행한다고 가정하자.
위 과정에서 실제로는 open, close, read, write 등의 수많은 기능들이 수행된다.
방금 언급한 기능들이 전부 syscall인데, 이들을 OS가 제공하는 것이다.
syscall 내부적으로는 하드웨어 접근 로직과 예외처리가 되어있다.
하지만 이렇게 사용하기 편하게 만든 syscall도 불편하다.
실제로 사용한다고 생각하면 코드가 굉장히 길어질 것이다.
그래서 이들을 한번 더 감싸서 만들어준 것이 Application Programming Interface이다.
윈도우의 Windows API, UNIX 계열의 POSIX 등이 대표적이다.
UNIX 및 Linux에서 가장 대표적인 API인 read 함수를 알아보자.
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count)
이제 일반적인 프로그래밍 언어와 많이 닮아졌다는 생각이 든다.
사실 중요한 예외처리는 어차피 syscall 선에서 잘 구현됐을텐데,
왜 우리는 자연스럽게 API를 쓰고있을까?
💡 One benefit concerns program portability. An application programmer designing a program using an API can expect her program to compile and run on any system that supports the same API.
첫번재 이유는 이식성이다.
이식성이라 함은, 같은 API(보통 운영체제 단위)에서 코드가 동작할 수 있음을 얘기한다.
💡 Furthermore, actual system calls can often be more detailed and difficult to work with than the API available to an application programmer.
두번째 이유는, 생각 이상으로 syscall을 직접 사용하는게 어렵다는 것이다.
이미 잘 짜여진 API를 냅두고 굳이 코드를 길게 쓰면서 실수를 만들 필요가 없다.
API를 사용한다고 쳤을 때, 어떤 syscall이 호출되어야 하는지는 어떻게 알까?
그걸 해주는 것이 바로 RTE다.
💡 Another important factor in handling system calls is the run-time environment (RTE) - the full suite of software needed to execute applications written in a given programming languages, including its compilers or interpreters as well as other software, such as libraries and loaders.
어떤 app을 실행한다고 했을 때, 그 app을 구성하는 언어와 라이브러리 등에 해당하는 syscall을 찾아서 호출해주는 역할을 해준다.

위 그림을 보면, open 함수를 유저 앱에서 실행하고 있다.
system call interface 안에는 syscall table이라는 것이 있어서,
해당 syscall의 number을 찾은 뒤 실제 syscall을 호출하는 구조이다.
운영체제를 공부할 때 많이 건너뛰는 부분이지만, 사실 굉장히 중요하다.
어떻게 단순 비트 집합인 프로그램이 실행까지 연결되는지 살펴보자.
💡 Usually, a program resides on disk as a binary executable file. To run on a CPU, the program must be brought into memory and placed in the context of a process.
여태 공부한 내용을 바탕으로 생각해보면, CPU는 메모리로부터 data를 읽기 때문에,
실행하고자 하는 파일을 임의의 메모리 주소로 올려야 한다는 사실을 알 수 있다.
이를 위해 linker와 loader가 존재하는 것이다.
💡 Source files are compiled into object files that are designed to be loaded into any physical memory location, a format known as an relocatable object file.
우선 모든 파일은 컴파일을 통해 object 파일(확장자 .o)로 변환이 돼야한다.
이는 relocatable object file이라고도 알려져있다.
[참고]
오브젝트 파일은 컴퓨터가 읽을 수 있는 기계어로 번역된 파일을 얘기한다.
.c나 .java와 같은 언어로 된 소스파일을 컴파일러가 오브젝트 파일로 변환해준다.
💡 Next, the linker combines there relocatable object files into a single binary file. During the linking phase, oither object files or libraries may be included as well, such as the standard C or math library.
오브젝트 파일로 변환이 끝났다고 해서 바로 메모리에 올릴 수 있는 것은 아니다.
기계어를 완전한 바이너리 파일로 바꿔야 메모리에 그대로 들어갈 수 있다.
그리고 해당 파일에 연관된 모든 파일도 (ex. 라이브러리) 함께 처리해줘야 한다.
이 역할을 수행하는 친구가 "Linker"이다.
💡 A loader is used to load the binary executable file into memory, where it is eligible to run on a CPU core. An activity associated with linking and loading is relocation, which assigns final addresses to the program parts and adjusts code and data in the program to match those addresses.
이제 빈 메모리 공간에 위에서 준비된 파일을 올려주는 것이 "Loader"의 역할이다.
이렇게만 하면 이해하기 힘드니 아래의 그림을 보면서 이해해보자.

GCC라는 컴파일러를 사용해 main.c를 실행시키는 과정을 살펴보자.
Compiling
Linking
Loading
[참고]
GCC는 GNU Compiler Collection의 줄임말로, 유명한 C 컴파일러이다.
원래는 GNU OS를 위한 컴파일러였는데, 지금은 C를 컴파일 할 때 많이 사용된다.
이제 다 알아봤는데, 마지막에 dynamically linked libraries는 무엇일까?
💡 In reality, most system allow a program to dynamically link libraries as the program is loaded. The benefit of this approach is that it aboids linking and loading libraries that may end up not being used into an executable file. Instead, the library is conditionally linked and is loaded if it is required during program run time.
DLL(Windows 기준)은 동적으로 라이브러리를 할당하는 것이다.
사용할지 안할지 모르는 라이브러리를 통째로 위의 과정을 같이 하면,
쓸데없는 메모리를 괜히 먹고 있는 거니까 이를 방지하기 위함이다.
그래서 런타임 중에 필요한 상황이 오면 그때 바로 link & load를 진행한다.
앱을 만들었을 때, OS와 상관없이 돌아간다는 너무 좋겠지만, 현실은 아니다.
💡 Fundamentally, applications compiled on one operating system are not executable on other operating systems. Each operating system provides a unique set of system calls.
근본적으로 앱은 또다른 OS에서 작동하지 않는다.
모든 OS가 독자적인 system call을 가지고 있기 때문이다.
그럼에도 불구하고 3가지 방법으로 가능하게 할 수는 있다.
App을 만들 때 interpreted language로 만드는 것이다. Python이나 Ruby가 대표적인 언어인데, 다양한 OS에서 동작 가능한 인터프리터가 내장되어 있다.
VM (virtual machine)을 보유한 언어로 만드는 것이다. JAVA가 대표적인 언어로서, JVM은 full RTE이기 때문에 프로그램이 동작 가능한 전체 환경을 제공해준다.
OS에서 제공하는 API를 잘 활용해서 개발하는 것이다. 물론 이 방법을 쓰게 되면 새로운 version이 나올때마다 porting을 해줘야 한다는 단점이 있지만, 특정 언어에 종속적이지 않게 제작할 수 있다는 장점은 있다. 대표적으로는 POSIX API를 통해 대부분의 UNIX 계열 운영체제에서 프로그램을 실행시킬 수 있다.
개발 단계에서 말고도, 근본적으로 OS 종속성을 해결하기 위한 노력도 있다.
💡 Object files and executable files typically have standard formats that include the compiled machine code and a symbol table contating metadata about functions and variables that are referenced in the program.
우선 오브젝트 파일과 실행 파일에 기본 포맷을 두는 것이다.
기계어에 대한 해석이나, 심볼 테이블에 대한 기준을 두는 것을 얘기하는데,
UNIX의 ELF, Windows의 PE, 그리고 macOS의 Mach-O 등이 있다.
또다른 해결방법으로서, ABI가 있다.
💡 At the architecture level, an application bincary interface (ABI) is used to define how different components of binary code can interface for a given operating system on a given architecture.
...
Thus, an ABI is the architecture-level equivalent of an API. If a binary executable file has been compiled and linked according to a particular ABI, it should be able to run on different systems that support that ABI.
ABI는 쉽게 얘기해서 API의 시스템(하드웨어) 버전이다.
주소 길이, syscall 인자, 런타임 스택 등을 규정해서 호환 가능하게 만든 것이다.
그러나 ABI는 결국 특정 OS가 기반으로 하는 아키텍처에 제한되기 때문에,
완전히 다른 OS (ex. Windows vs UNIX) 등에서는 큰 의미를 갖지는 못한다.
운영체제 디자인은 굉장히 많은 고민이 담겨있는 것 같다.
굉장히 흥미로운 학문이다.
Abraham Silberschatz, 『Operating System Concepts Ed.10』