최근에 OS 스터디를 하게 되었는데, 좋은 기회라고 생각하고 학습한 내용을 정리해 보았다.
노션에서 티스토리로 바로 업로드 해주는 기능이 있을 줄은 몰랐는데 써보니까 좋네...
2.1 Operating System Servies
OS는 프로그램 실행 환경을 제공한다. 즉, 프로그램과 사용자에게 특정 서비스를 제공함으로써 프로그램이 정상적으로 실행될 수 있는 환경을 제공한다고 볼 수 있다. OS의 종류와 프로그램의 종류에 따라 제공하는 서비스는 달라질 수 있지만, 크게 다음과 같이 볼 수 있다.
- User Interface: 크게 CLI/GUI 구분.
- Program Execution: 프로그램을 메모리에 적재해 실행할 수 있도록 함.
- I/O Operation: 보안 문제 때문에 사용자나 프로그램은 직접 입출력 장치를 제어할 수 없고, OS가 이를 담당함. (이후에 나올 인터럽트 방식!!)
- File System Manipulation: 프로그램이 요구하는 파일 읽기/쓰기/생성을 돕는다.
- Communication: 특정 프로세스가 다른 프로세스 (이는 같은 시스템 내부일수도 있고, 다른 시스템일수도 있음. → 네트워크!)와 정보를 교환하는 것. 공유 메모리나 메시지 전달 기법을 활용함.
- Error Detection: 다양한 유형의 오류에 대해 적합한 조치를 취해야 함.
- Resource Allocation: 다수의 사용자나 다수의 작업들이 실행될 때, 적합하게 자원을 분배해야 한다.
- Accounting: 컴퓨터 자원을 어떤 프로그램이나 어떤 사용자가 얼마나 사용했는지를 확인하기 위해 기록 하는 것.
- Protection/Secutiry: 프로세스의 정보나, 시스템의 정보를 필요에 따라 접근이 통제되도록 보장하는 것을 의미함.
2.2 User Operating-System Interface
CLI
Command-Interpreter를 사용하며, 특히 선택할 수 있는 다양한 Command-Interpreter를 제공하는 시스템에서는 이를 Shell 이라고 호칭하기도 함.
Command-Interpreter의 주된 기능은 사용자가 요청한 명령을 실행하는 것임. 흔히 떠올릴 수 있는 파일 생성, 삭제, 리스트 출력, 실행, 복사 및 이동 등등... 이러한 명령어를 실행하는 방식은 두 가지 방법으로 나뉜다.
- Command-Interpreter 자체가 실행할 코드를 내장하는 경우.
- Command-Interpreter와 별개의 시스템 프로그램에게 실행을 위임하는 경우. (MS-DOS, UNIX)
2번을 대부분 채택하는 이유는 유연성 때문. Interpreter의 크기를 최소화 시킬 수 있으며, 그러면서도 시스템의 기능을 쉽게 추가할 수 있다.
GUI
데스크톱이라는 시스템을 주로 사용 (마우스 기반, 윈도우 메뉴 시스템)
현대 대부분의 OS가 GUI를 채택하고 있고, UNIX 시스템 또한 KDE/GNOME 등이 탑재되기 시작하면서 주류가 되고 있음. (물론 CLI가 기능 추가 측면에서 더 유연하기 때문에 GUI만 쓰긴 좀...)
2.3 System Calls
운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공함.
말 자체는 어려울 수 있으나, 이렇게 생각해보자. 2.1에서 프로그램은 직접 입/출력 장치에 접근할 수 없다고 되어 있음. 입출력 뿐 만 아니라, 일반적인 프로세스가 접근할 수 없는 시스템 내 장치/영역이 존재하는데, 이를 접근할 필요가 있을 때는 OS에 대신해서 요청을 해야 함. 이것이 시스템 콜!
일반적으로 C와 C++언어로 작성된 루틴 형태로 제공 됨.
그렇다면 프로그램들은 어떻게 시스템 콜을 호출할까? 일반적으론 API(Application Programming Interface)에 따라 프로그램을 설계 함... (ex. Windows API, POSIX API 등등...)
#include <unistd.h> // POSIX Standard API
ssize_t read(int fd, void *buf, size_t count); // 파일을 읽어들임.
근데 왜 간접적으로 API를 사용할까? → 같은 OS를 사용하는 경우의 호환성을 확보할 수 있고, 시스템 콜을 직접 호출하는 것 보다 더 간단하기 때문.
시스템 콜을 호출하는 과정에서, OS에 Parameter를 전달해야 하는 경우가 있음.
- Register 내부로 전달함.
- (Register 보다 더 많은 Parameter가 있으면?) 메모리 내부 블록/테이블에 저장
- 또는 스택에 넣어버리고 OS가 그것을 꺼내감.
그래서 시스템 콜은 언제 발생할까? 대표적인 예시 몇 가지만 알아보자.
Process Control
실행중인 프로그램은 정상적으로 종료되거나, 비정상적으로 종료됨. 만약 비정상적으로 종료되기 위해 시스템 콜이 호출되었다면, 메모리 덤프가 행해지고 디버거로 검사하곤 함.
종료되는 것 이외에도, 현재 실행되고 있는 프로세스가 다른 프로그램을 로드하고, 실행하길 원할 수 있는데, 이럴 때도 당연히 시스템 콜이 호출 됨.
또한, 여러 프로세스는 데이터를 공유하곤 하는데, 일관성이 흐트러질 수 있으니 종종 OS는 데이터를 시스템 콜을 통해 잠궈버릴 수 있음. (이런 경우 어떤 프로세스도 데이터에 접근할 수 없음!!)
- Single-Tasking System (Arduino, MS-DOS): 프로그램을 실행하기 위해 새로운 프로세스를 생성하지 않으며, 자신의 메모리를 덮어쓰는 한이 있더라도 프로그램에 최대한 많은 메모리를 할당함. → 당연히 프로그램 끝나고 시스템 콜이 호출되고, 이후에 덮어 쓰이지 않은 Interpreter의 일정 부분이 다시 실행되어 나머지를 다시 메모리에 적재함.
- Multi-Tasking System (FreeBSD): 로그인 후 쉘 실행 → 프로세스가 호출되어도 기존의 Interpreter는 실행을 계속 함. → 당연히 여러 프로세스가 백그라운드에서 계속 실행될 수 있다. (이는 fork()와 exec()로 구현 됨.)
File Management
파일을 생성하고, 삭제하고, 열고, 읽고, 쓰고, 위치 변경, 닫기 등등...
만약 파일 시스템이 디렉토리 구조를 채택하고 있다면 당연히 이와 관련된 시스템 콜도 존재할 것임.
Device Management
프로세스가 실행 과정에서 추가적인 자원을 요구할 수 있음 (주기억장치, 디스크, 파일 등등...) 이런 경우에 자원을 제공해야 하는데, 이것 또한 시스템 콜이 요구됨. (자원을 제공받지 못하면 무한히 대기해야 함!!)
다만, 실제 물리적인 장치가 아니더라도 OS에 의해 제어되는 자원들 또한 장치로 볼 수 있음.
만약 다수가 이용하는 시스템이라면 어떨까? 자원이 동시에 접근되고 사용되면 의도치 않은 결과가 나타날 수 있으니, 프로세스가 요청을 하면 독점적으로 제공함.
Information Mainternance
일반적인 정보 전달을 위한 시스템 콜.
시간이나 날짜 같은 기본적인 정보부터 시작해 현재 실행되는 프로세스 목록, 남은 메모리 및 디스크, 운영체제의 버전 같은 대부분의 시스템 정보를 시스템 콜을 통해 요청하면 획득할 수 있음.
Communication
일반적으로 두 가지 모델을 사용함.
- 메시지 전달 모델: 통신하는 두 프로세스가 정보를 교환하기 위해 메시지를 전달하는 방법. 통신 전 연결이 활성화 되어야 하며, 이후 식별자 (컴퓨터의 호스트 이름, 프로세스의 이름으로 구별함.)를 통해 서로를 식별하고, 각자의 open() 및 close() 호출을 진행하여 서로의 정보를 획득함.
- 공유 메모리 모델: 다른 프로세스가 소유한 메모리 영역에 접근을 함. (당연히 일반적으론 다른 프로세스가 메모리 영역에 접근해선 안되지만, 이런 경우에 한해 허용할 수 있도록 해야 함.)
전자는 충돌이 적다는 장점이 있고, 후자는 같은 컴퓨터 내에서 빠른 속도로 접근할 수 있다는 장점이 있음.
Protection
자원에 대한 접근을 제어하는 기법과 관련된 시스템 콜.
2.4 System Services
결국 현대 시스템은 시스템 프로그램의 집합체임.
시스템 서비스?: 프로그램의 개발과 실행을 위해 편리한 환경을 제공하는 것으로, 일부는 시스템 콜에 대한 인터페이스이다.
- File Management: 파일을 조작함.
- Status Information: 시스템에게 정보를 획득함. (System Call에서 이야기 한 Information Mainternance Call을 주로 활용함.)
- File Modification: 파일의 내용을 수정. (File Editor의 기능)
- Programming Language Support: 일반적인 프로그래밍 언어에 대한 컴파일러, 어셈블러, 디버거 등등을 제공함.
- Program Loading and Execution: 컴파일 된 이후, 프로그램은 메모리에 적재되어야 실행될 수 있다. 즉, 시스템은 Loader를 제공해야 한다.
- Communications: 다른 프로세스나 사용자, 또는 다른 컴퓨터 시스템들 사이에 접속할 수 있도록 하는 기법을 제공함.
- Background Services: 현대 시스템은 부팅 과정에서 특정 시스템 프로그램을 실행시킨다. 이들 중 일부는 자신의 일을 마치고 종료하지만 그렇지 않은 경우도 있다. 후자의 경우는 Service/Daemon 이라고 부른다.
2.5 Linker and Loader
프로그래밍 언어를 통해 프로그램을 짜게 되면, 이후 실행 가능한 파일로 변환하고, 실행하게 된다.
우선 컴파일러를 통해 소스파일을 목적 파일 (C/C++에서는 .o)로 변환하게 된다. 이를 relocatable object file 이라고도 부른다. (메모리에 적재될 수 있기 때문에) 이후 링커를 통해 목적 프로그램들을 묶어 하나의 실행 가능한 파일로 변환하게 된다. 이후 로더를 통해 메모리에 적재하게 된다.
UNIX의 경우, 파일을 실행하게 되면 일단 fork()을 통해 프로세스를 생성하고, exec()을 통해 로더를 호출하게 된다. 그렇게 되면 로더는 fork를 통해 생성한 프로세스 공간에 실행하려는 파일을 할당하게 된다.
그러나, 최근 프로그램들은 동적 링크 파일들을 실행 과정에서 같이 메모리에 넣는다. 대표적인게 윈도우의 .dll 파일.
그냥 처음에 링커로 묶을때 넣으면 되는걸 왜 따로 뺄까? 일단 하나의 코드를 여러 프로그램이 공유하니 메모리와 디스크 공간을 절약할 수 있고, DLL 내부의 코드를 수정할 필요가 있을때 관련된 모든 프로그램들을 일일히 재컴파일 할 필요가 없다는 이점이 있기 때문이다.
2.6 Why Applications Are Operating System Specific
생략.
2.7 Operating System Design and Implementation
Design Goals
시스템의 목표와 명세를 정의함.
Mechanisms and Policies
Mechanism: 어떤 일을 어떻게 할 것인가?
Policy: 무엇을 할 것인가?
이런걸 잘 고민해서 설계 함.
Implementation
초창기 OS는 어셈블리어로 구현되었으나, 최근엔 C나 C++로 작업하곤 함. 물론 커널단은 어셈블리어, 시스템 서비스는 C/C++로 작업하는 경우도 있음.
왜 고급 언어를 사용해서 OS를 구현하려고 할까? 당연히 어셈블리어 보다 짜기 쉽고, 이해하기 쉽고, 디버그 하기 쉬우니까. 또한, 이식성도 좋다. 이전에는 속도 문제 때문에 어셈블리어로 많이 짰지만, 현대에 와서 컴퓨터의 발전과 컴파일러의 발전으로 인하여 그런 성능 차이는 무시할 정도로 미미하다.
2.8 Operating System Structure
Monolithic Structure
초창기 OS가 이에 해당. MS-DOS가 이런 구조를 하고 있음.
인터페이스와 다른 기능 계층이 잘 분리가 되지 않음. 사용자 프로그램이 시스템적인 부분에 접근하기 쉬웠고, 그에 따라 사용자 프로그램의 문제가 시스템으로 퍼질 수 있다는 단점이 있음.
초창기 UNIX도 Monolithic 했다.
UNIX는 커널과 시스템 프로그램으로 구분되어 있다.
커널은 여러 인터페이스와 드라이버로 다시 분리되지만, 이는 UNIX가 발전하면서 추가된 것들이고, 이전에는 그림처럼 계층 구도라고 보면 된다.
보시다시피 커널이 엄청나게 많은 기능을 담당했기 때문에, 구현하기도 어려웠고 유지보수하기도 매우 어려웠다.
단점만 있어 보이지만, 성능 측면에선 오버헤드가 거의 없기 때문에 이득이다.
Layered Approach
적절한 하드웨어 지원을 끼면, OS를 적절하게 분리할 수 있다. 우리는 이를 '모듈'로 분리한다고 하는데, 그중에서 계층적 접근이란 모듈을 top-down 방식으로 설계한다는 이야기이다.
이런 방식의 장점은 구현과 디버깅의 간단함이다. top-down 방식의 특성상 특정 계층은 하위 계층의 연산만을 사용할 수 있으므로, 첫번째 층을 제대로 구현했으면 이후 층을 구현할 때 생기는 문제는 자신의 층에서 발생한다고 볼 수 있기 때문에, 디버깅이 용이하다는 장점이 있다.
다만 층을 적절히 정의하는 것은 쉽지 않다. 말했듯이 하위 계층의 연산만 사용할 수 있기 때문에, 정계획을 제대로 세우지 못하면 이후에 문제가 발생할 수 있다.
또한, 층을 쪼개다 보니 오버헤드가 많을 수 밖에 없다. 제일 외부의 층에서 내부의 연산을 활용하게 된다면, 자연스럽게 오버헤드가 발생할 수 밖에 없다.
MicroKernals
위에서 언급했던 UNIX의 초기 버전을 떠올려 보자. 커널에 모든 기능을 다 넣다보니, 유지보수가 어려워졌다고 말했었다.
이를 개선하기 위해, 상대적으로 덜 중요한 부분을 커널에서 제거하고, 그들을 시스템 소프트웨어/사용자 소프트웨어로 구현하여 OS를 구현하는 방법을 찾았다.
그림에서 보다시피, 메시지 전달 방식으로 통신을 구현하고 있다. 우리 눈에선 특정 서비스를 요청하게 될 때, 직접적으로 통신을 할거라고 생각하겠지만, 실제로는 마이크로커널을 매개로 통신을 하는 것을 볼 수 있다.
운영체제의 확장성 측면에서 마이크로커널 방식은 우수하다. 새로운 서비스를 구현해야 할 때는 커널이 아닌 사용자 공간에 구현하면 되기 때문이다. 또한, 이식성 측면에서도 커널이 가볍기 때문에 이식이 어렵지 않다. 마지막으로, 서비스에 문제가 생겨도 커널에 영향을 주지 않는다.
다만, 마이크로커널은 오버헤드가 잦기 때문에 성능이 상대적으로 떨어진다.
Hybrid System
현대의 OS는 다양한 구조를 결합하여 장점만을 취하려고 한다.
예시는 쓸데 없을 것 같으니 생략...
2.9 Building and Booting an Operating System
시스템을 Booting 하게 되면, 우선 Bootstrap Loader라고 하는 작은 크기의 코드가 커널을 찾고, 메모리에 적재하고 실행한다. (일부 시스템의 경우 작은 Bootstrap이 큰 Bootstrap을 로드 하고, 그것이 커널을 실행시킨다.)
컴퓨터의 전원을 올리게 되면, IR이 미리 지정한 메모리에서부터 실행을 시작하게 되는데, 이 자리에는 최초의 Bootstrap이 존재한다.
Boot하게 되면, 우선 시스템의 상태를 진단하고, 문제가 없으면 레지스터, 메모리등을 초기화 하고 OS를 시작시킨다.
2.10 Operating System Debugging
“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.” - Kernighan's Law
시스템의 오류를 찾아내어 수정하는 작업이다. 당연히 병목 현상 같은 성능 저하 문제도 버그라고 간주할 수 있으므로, 이러한 현상을 제거하는 것도 디버깅의 일종이라고 볼 수 있을 것.
Uploaded by Notion2Tistory v1.1.0