운영체제의 서비스는 다음과 같이 요약할 수 있다.
- 사용자 인터페이스
- 프로그램 수행
- 입출력 연산
- 파일 시스템 조작
- 통신
- 오류 탐지
- 자원할당
- 기록 작성
- 보호(protection)과 보안(security)
명령 인터프리터는 해석기라고 할 수 있으며, 이 해석기는 셸(shell) 이라고 부른다. 이 셸은 굉장히 많은 종류가 있다. 명령 인터프리터가 명령어를 입력받은 경우 작동하는 방식은 두 가지가 있다. (1) 인터프리터 자체가 명령을 실행할 코드를 가지고, 적절한 시스템 콜을 수행하거나, (2) 시스템 프로그램에 의해 명령을 구현하는것, 즉 명령어 코드가 담긴 별도의 파일이 존재하고 이를 메모리에 적재하는 방식이다.
지금 개인이 사용하는 대부분의 운영체제는 그래픽 기반 사용자 인터페이스다. 즉 시각적 이미지와 마우스 및 키보드를 통한 상호작용을 통해 사용자는 운영체제를 사용할 수 있다.
모든 기기가 마우스와 키보드를 가지는 것은 아니다. 모바일 기기가 그렇다. 떄문에 터치스크린 위에서의 제스처를 통해 상호 작용이 이뤄진다. iPad와 iPhone 모두 Springboard 터치스크린 인터페이스를 사용한다.
명령어 라인 인터페이스는 여전히 시스템 관리자와 파워 유저들에 의해 사용된다. GUI에 비해 명령어 라인 인터페이스는 더 빠르고, 지원 기능이 더 많다. 특히 반복작업을 셸 스크립트를 통해서 간편하게 만들 수 있다.
시스템 콜은 운영체제에 의해 사용 가능하게 된 서비스에 대한 인터페이스를 제공한다.
단순히 파일을 복사하여 붙여넣는 것에서는 입력/출력 파일 이름을 획득하는 것, 오류 탐지와 비정상적인 종료, 파일 닫기, 정상 종료 등 무수히 많은 시스템 콜로 구성되어 있다.
하지만 이 많은 시스템 콜을 통해 직접 프로그램을 만드는 것은 자원이 너무 많이 필요하다. 그래서 응용 프로그래밍 인터페이스(Application programming Interface, API) 가 나왔다. API는 각 함수에 전달되어야 할 매개변수들과 프로그래머가 기대할 수 있는 반환 값을 포함하여 응용 프로그래머가 사용 가능한 함수의 집합을 명시한다. 프로그래머는 운영체제가 제공하는 라이브러리를 통해서 API를 활용한다.
API 사용의 이점은 같은 API를 지원하는 시스템간의 가용성과 용이성이다.
실행시간 환경(RTE)는 응용 프로그램을 실행하는데 필요한 전체 소프트웨어 제품군과 라이브러리 또는 로더와 같은 다른 소프트웨어를 가리킨다. RTE는 시스템 콜 인터페이스를 제공하며, 이는 API의 시스템 콜 함수 호출을 가로채고 대신 호출하며 상태와 반환값을 전달해 준다. 이로인해 시스템 콜의 상세한 내용은 프로그래머로부터 숨겨진다.
운영체제에 매개변수가 전해지는 방법은 레지스터 저장을 통하는 방법이다. 그러나 레지스터 갯수가 모자라는 경우에는 메모리 내의 블록 혹은 테이블에 저장되고 그 주소를 레지스터에 저장한다.
- 프로세스 제어
- 파일 조작
- 장치 조작
- 정보 유지 보수
- 통신
- 보호
실행중인 프로그램에서 오류 트랩이 발생, 메모리 덤프가 행해지고, 오류메시지가 실행된다. 명령인터프리터는 제어를 전달받아 다음 명령을 읽는다. 대화식에서는 다음 명령어를 계속 수행하고, GUI에서는 지시를 기다린다. 일부는 복구행위를 지시하는 제어카드를 쓴다. 이 오류수준은 정의될 수도 있다.
한 프로그램을 실행중인 프로세스는 다른 프로그램을 적재/실행 할 수 있다. 여기서 살펴봐야할 부분은, 적재된 프로그램이 종료되었을때, 제어가 어디로 넘어가는지다.
[프로세서 제어를 위한 시스템 콜 예시]
- create_process()
- get_process_attributes(), set_process_attributes()
- terminates_process()
- wait_time(), wait_event(), signal_event()
- acquire_lock(), require_lock()
아두이노는 단일 태스킹의 대표적인 예시다. 아두이노에서 스케치라는 사용자프로그램은 메모리 특정 위치에 적재되고 실행된다. 다른 스케치가 적재되면, 기존 스케치는 대체된다.
FreeBSD는 다중 태스킹의 예시다. fork(), exec()로 새로운 프로세스를 시작하고, 새로운 프로그램을 적재하고 수행한다. 그리고 프로세스를 exit()로 종료한다.
create()로 파일을 생성하고 delete()로 삭제한다. 또 우리는 파일을 열고open(), 읽고read(), 쓰고write(), 위치변경reposition(), 되감기rewind, 파일닫기close()가 필요하다.
파일시스템이 디렉터리 구조를 가지는 경우 이에 대해 연산 집합이 필요하다. 거기에 파일 혹은 디렉터리의 속성 값을 결정할 수 있어야한다.(get_file_attribute(),set_file_attribute()). 몇몇 운영체제는 이동과 복사 등의 더 많은 시스템 콜을 지원한다. API에 의해 동일한 작업을 수행할 수도 있다.
프로세스는 작업 수행을 위해 추가 자원을 요청할 수있다. 장치는 물리 장치, 추상/가상 장치로 나뉜다. 다수 사용자가 동시 사용하는 시스템은 독점 장치 사용을 보장받기 위해 그 장치를 요청request()하고 반드시 마지막에는 방출release()한다.
일단 장치를 할당받으면, 읽고, 쓰고, 위치변경할 수 있다. 입출력 장치와 파일 간에는 유사성이 매우 많기 때문에, UNIX를 포함한 많은 운영체제가 이들 둘을 통합된 파일-장치 구조로 결합했다. 이 경우, 같은 시스템 콜이 파일과 장치에 대해 ㅏㅅ용된다. 떄로 입출력 장치들은 특별한 파일 이름, 디렉터리 배치 또는 파일 속성으로 식별된다.
많은 시스템 콜은 단순히 사용자 프로그램과 운영체제 간의 정보 전달을 위해 존재한다.(time(), date())
다른 시스템 콜 집합은 프로그램 디버깅에 도움이 된다.(dump()) 마이크로프로세서에서도 한 명령어 실행(single step) 라는 CPU모드를 제공한다. 많은 운영체제는 프로그램의 시간 프로파일(time profile)을 제공한다. 이 프로파일은 추적 설비(tracing facility)나 정규 타이머 인터럽트가 필요하다.
더불어 운영체제는 현재 운영되는 모든 프로세스에 관한 정보를 가지고 있다. 이 정보에 접근할 수도 있다.(get_process_attributes(), set_process_attributes())
(1) 메시지 전달 모델
두 프로세스는 반드시 이름을 알고 있어야 하며, 통신이 이루어지기 전에 연결이 반드시 열려야 한다. 네트워크의 각 컴퓨터는 호스트 이름을 가지며 각 프로세스는 프로세스 이름을 가지고 있다. 이 이름들은 호스트에 의해 동등한 식별자로 변환된다.(get_hostid(), get_processid()) 그 후 시스템의 통신 모델에 따라 파일 시스템에 의해 제공되는 범용의 open, close 호출에 전달되거나, 특정 open_connection(), close_connection() 시스템 콜에 전달된다. 수신 프로세스는 통상 통신이 일어날 수 있도록 acquire_connection() 호출에 자신의 permission을 제공한다.
연결을 받을 프로세스들은 대부분 특수목적의 데몬이다. 따라서 그들은 wait_for_connection() 호출을 수행한다. 연결이 이뤄지면 read_message(), write_message() 시스템 콜에 의해 메시지를 교환한다. 이후 close_connection() 호출로 통신을 종료한다.
(2) 공유 메모리 모델
공유 메모리 모델에서, 프로세스는 다른 프로세스가 소유한 메모리 영역에 대한 접근을 위해 shared_memory_creates(), shared_memory_attach() 시스템 콜을 사용한다. 정상적인 운영체제는 한 프로세스가 다른 프로세스 메모리에 접근하는 것을 막겠지만, 여기서의 프로세스들은 이 제한을 제거하는데 동의한다. 이후 공유 영역에 데이터를 읽고 씀으로써 정보를 교환한다.
보호는 컴퓨터 시스템이 제공하는 자원에 대한 접근을 제어하기 위한 기법을 지원한다. set_permission(), get_permission() 등의 시스템 콜이 보호를 지원하는 시스템 콜에 포함된다. allow_user(), deny_user()는 특정 사용자가 지정된 자원에 대해 접근이 허가 혹은 불허되었는지를 명시한다.
시스템 서비스는 시스템 유틸리티로도 알려진, 프로그램 개발과 실행을 위해 더 편리한 환경을 제공한다.
시스템 유틸리티의 범주
- 파일 관리
- 상태 정보
- 파일 변경
- 프로그래밍 언어지원
- 프로그램 적재와 수행
- 통신
- 백그라운드 서비스
운영체제 대부분은 시스템 프로그램과 함꼐 일반적인 문제점을 해결하거나 일반적인 연산을 수행하는데 유용한 프로그램들도 제공한다. 이러한 응용 프로그램에는 웹브라우저, 워드프로세서와 텍스트 포맷터, 스프레드시트, 데이터베이스 시스템, 컴파일러 등이 있다.
(1) 일반적인 재배치 과정
소스파일은 오브젝트 파일로 컴파일 됨ㄴ다. 이러한 형식을 재배치 가능 오브젝트 파일이라고 한다. 이후, 링커는 이 파일을 하나의 이진 실행 파일로 결합한다. 이 단계에서 표준 C 또는 수학 라이브러리와 같은 다른 오브젝트 파일 또는 라이브러리도 포함될 수 있다.
로더는 이진 실행 파일을 메모리에 적재하는데 사용되며, CPU 코어에서 실행할 수 있는상태가 된다. 링크, 로드와 관련된 활동은 재배치로, 프로그램 부분에 최종 주소를 할당하고 프로그램 코드와 데이터를 해당 주소와 일치하도록 조정하여 프로그램이 실행될 때 코드가 라이브러리 함수를 호출하고 변수에 접근할 수 있게 만든다.
(2) 동적 링킹 라이브러리(DLL)
실제 시스템 대부분에서는 프로그램이 적재될 때 라이브러리를 동적으로 링크할 수 있게 한다. 윈도우는 이를 위해 DLL을 사용한다. 여기서는 링커가 프로그램이 적재될 때 동적으로 링크되고 적재될 수 있도록 재배치 정보를 삽입한다.
(3) 표준 형식
UNIX, LINUX 시스템의 경우 ELF(Excutable and Linkable Format) 표준형식을 가진다. Window 시스템은 PE(Portable Excutable) 표준형식을 가지며 macOS는 Mach-O 형식을 사용한다.
한 운영체제에서 컴파일된 응용프로그램은 다른 운영체제에서 실행할 수 없다. 그런데 동일한 응용프로그램을 다른 운영체제에서 사용한 적이 있을거다. 이를 위한 방법이 세 가지가 있다.
하지만 운영체제마다 지원하는 라이브러리, API, 응용 프로그램 이진 형식, 명령어 집합, 시스템 콜에서 차이를 가지기 때문에 크로스 플랫폼 개발은 어렵다.
아키텍처 수준에서 이진 코드의 여러 구성요소가 주어진 아키텍처에서 특정 운영체제와 상호 작용할수 싱ㅆ는 방법을 정의하는데 ABI(application binary interface)가 사용될 수 있다. ABI는 아키텍쳐 수준의 API라고 이해하면 편리하다. 특정 아키텍처에서 실행되는 특정 운영체제에 대해 ABI가 정의되기 때문에, ABI는 플랫폼간 호환성을 거의 제공하지 않는다.
이 절에서는 운영체제를 설계하고 구현할 때 우리가 당면하는 문제점을 논의한다. 물론 이들 설계 문제점에 대한 완전한 해결책은 없지만 성공적인 접근 방법들이 있다.
시스템의 목표와 명세를 정의하는 것이 첫 번쨰 문제점이다. 하드웨어와 시스템 유형의 선택은 시스템 설계에 영향을 미친다. 이 설계 수준을 넘어서는 요구조건은 사용자 목적과 시스템 목적 두 그룹으로 나눌 수 있다.
하지만 사용자들이 기대하는 특징들을 구현하는 일반적인 합의 사항은 없으며, 시스템 목적 상의 요구조건들도 애매하고, 다양하게 해석 될 수 있다. 따라서 운영체제에 대한 요구를 정의하는 문제를 해결하는 해법은 다양하다. 그러나 소프트웨어 공학 분야에 의해 개발된, 특별히 운영체제에 적용 가능한 일반적인 원칙들이 존재한다.
그 중 하나는 기법(mechanism) 으로부터 정책을 분리하는 것이다.기법은 일을 어떻게 할 것인가, 정책은 무엇을 할 것인가를 의미한다. 정책과 기법을 분리하지 않으면 융통성이 매우 떨어진다. (Window <-> 마이크로커널 기반 운영체제) 정책 결정은 모든 자원 할당 여부 결정에 필요하다. 무엇이 아니라 어떻게일 때마다, 반드시 결정되어야 하는 것은 기법이다.
설계가 완료되면 이제 구현을 할 차례다. 이에 대한 일반적인 언급은 매우 어렵다.
과거와 달리 대부분은 C 또는 C++로 운영체제가 작성되며 소수만이 어셈블리 언어로 작성된다. 커널의 최하위 레벨은 어셈블리어 또는 C로 작성 될 수 있다. 상위 레벨 루틴은 C 및 C++로 작성될 수 있으며, 시스템 라이브러리는 C++ 또는 상위 레벨 언어로 작성 될 수 있다.
운영체제를 고급 언어로 구현하면 디버깅, 이식 등에서 이점을 가지나, 속도와 저장공간 측면에서 불리하다.
운영체제와 같이 매우 크고 복잡한 시스템은 그 태스크를 작게 나누고, 그 작은 부분은 신중하게 정의되어있어야한다.
가장 간단한 구조는 구조가 아예 없는 것이다. 커널의 모든 기능ㅇ을 단일 주소 공간에서 실행되는 단일 정적 이진 파일에 넣는 것이다.(상남자 스타일 인정합니다.)
최초의 UNIX 운영체제가 제한적인 구조를 가진 운영체제다. 하지만 커널은 점차 하나의 주소공간으로 결합하기에는 엄청나게 많은 기능이 추가되었다.
Linux는 Unix 기반이다. Linux 커널은 단일 주소 공간에서 커널 모드로 전부 실행된다는 점에서 모놀리식이지만, 런타임 중에 커널을 수정할 수 있는 모듈식 설계를 가지고 있다.
모놀리식은 단순하지만 확장이 어렵다. 그럼에도 불구하고 성능과 속도가 좋기 때문에 UNIX, Linux, Windows에서 발견된다.
모놀리식 접근은 밀접하게 결합된 시스템이라고 불리고, 그 대안은 느슨하게 결합된 시스템이다. 이 대안에서는 시스템의 기능이 특정/한정된 기능으로 나뉘고 한 구성요소의 변경이 다른 구성요소에는 영향을 미치지 않는다. 이는 모듈화 중 하나다.
또다른 모듈화 방식이 계층적 접근 방식이다. 운영체제는 여러 층으로 나뉘며, 최하위 층은 하드웨어, 최상위 층은 사용자 인터페이스다. 운영체제 층을 M이라고 할 때, M은 자료구조와 상위층에서 호출할 수 있는 루틴의 집합이며, M은 하위층에 대한 연산을 호출 할 수 있다.
이 접근 방식은 구현과 디버깅이 간단하다. 왜냐하면 각 층들은 단지 자신의 하위층들의 서비스와 기능(연산)만을 사용하도록 선택되기 때문이다. 이 시스템은 컴퓨터 네트워크 와 웹 응용 프로그램에서 성공적으로 사용되었다. 하지만 각 층의 기능을 적절히 정의해야하고, 오버헤드가 발생해서 일반적인 운영체제에서 찾아보기 힘들다. 하지만 현대 운영체제는 어느정도 수준으로 계층화가 이뤄져 있다.
최초의 마이크로커널 접근 방식 사용은 커널을 모듈화 한 Mach라는 운영체제였다. 잘 사용하지 않는 기능을 별도의 주소 공간에 사용자 수준 프로그램으로 구현한 것이다. 통상 마이크로커널은 통신 설비 외에 최소한의 프로세스와 메모리 관리를 제공한다.
마이크로커널의 주 기능은 클라이언트 프로그램과 사용자 공간에서 수행되는 다양한 서비스 간에 통신을 제공한다. 여기서의 통신은 마이크로커널을 통한 간접적인 통신이다.
이 접근법의 장점은 운영체제의 확장이 쉽다는 것이다. 그리고 다른 하드웨어로의 이식이 쉽다. 보안과 신뢰성도 높다. 마이크로커널 운영체제의 대표적인 예시는 macOS, iOS의 커널 구성요소 Darwin이다. Darwin은 두 개의 커널로 구성되며 하나는 Mach 커널이다.
하지만 마이크로커널은 시스템 기능 오버헤드로 성능이 나빠진다. 메시지 복사 및 프로세스 전환 관련 오버헤드는 마이크로커널 기반 운영체제의 성장에 방해가 되었다.
운영체제 설계의 최근 기술 중 최선책은 적재가능 커널 모듈이다.(aka. LKM) 여기서 커널은 핵심 구성요소의 집합을 가지고 부팅 혹은 실행 중에 부가 서비스를 모듈을 통해 링크할 수 있다.
이 설계의 주안점은 커널이 핵심 서비스를 제공하는 동안, 다른 서비스들을 동적으로 구현하는 것이다. 이는 수정 사항이 생길 때마다 커널을 다시 컴파일할 필요를 줄였다.
사실 엄격하게 정의된 하나의 구조를 채택한 운영체제는 거의 존재하지 않는다. 대신 다양한 구조를 결합하여 성능, 보안 및 편리성 문제를 해결하려는 혼용 구조로 구성된다.