운영체제는 그 속에서 프로그램이 실행되는 환경을 제공해 준다.
운영체제를 살펴보기 위한 몇 가지 유리한 관점이 있다.
운영체제가 제공하는 서비스는 무엇이며, 이 서비스는 어떤 방식으로 제공되는지, 이 서비스들이 어떻게 디버깅되며, 이러한 시스템을 설계하기 위해서는 어떤 방법들이 사용되는지 고려한다.
마지막으로 운영체제가 어떻게 만들어지고 컴퓨터가 운영체제를 구동시키는 방법에 관해 설명한다.
운영체제는 프로그램 실행 환경을 제공한다.
user interface
거의 모든 운영체제가 ui를 제공하는데 가장 일반적으로 gui가 사용된다.
또 다른 옵션은 CLI이다.
프로그램 수행
시스템은 프로그램을 메모리에 적재해 실행할 수 있어야 한다. 또한 실행을 끝낼 수 있어야 한다.
입출력 연산
수행 중인 프로그램은 입출력을 요구할 수 있다. 운영체제가 입출력 수행의 수단을 제공해야 한다.
파일 시스템 조작
프로그램은 파일을 읽고 쓸 필요가 있다.
또한 이름에 의해 파일을 생성하고 삭제, 조회할 수 있어야 한다.
몇몇 프로그램은 파일 소유권에 기반을 둔 권한 관리를 이용하여 접근을 제어할 수 있게 한다.
운영체제는 다양한 파일 시스템을 제공한다.
통신
프로세스간에 정보를 교환해야 할 상황이 있다.
같은 로컬에서 돌아가는 프로세스일 수도 있고, 네트워크로 묶여 있는 프로세스일 수 있다.
통신은 공유 메모리를 통해서 구현될 수도 있고, 메시지 전달 기법을 사용하여 구현될 수도 있는데, 후자의 경우 정보 패킷들이 운영체제에 의해 프로세스들 사이를 이동한다.
오류 탐지
운영체제는 항상 모든 가능한 오류를 의식하고 있어야 한다.
운영체제는 올바르고 일관성 있는 계산을 보장하기 위해 오류에 대한 조처를 해야 한다.
사용자에게 도움을 주는 목적이 아니라 시스템 자체의 효율적인 동작을 보장하기 위한 운영체제 기능들도 존재한다.
다수의 프로세스가 사용하는 시스템에서는 프로세스들 간에 컴퓨터 자원을 공유함으로써 효율성을 얻을 수 있다.
resource allocation
다수의 프로세스나 작업이 동시에 실행될 때, 각각 자원을 할당해 주어야 한다.
운영체제는 여러 자원을 관리한다.(ex. cpu사이클, 메모리, I/O 장치)
logging
어떤 프로그램이 어떤 자원을 얼마나 많이 사용하는지 추적할 수 있다.
protection & security
다중 사용자 컴퓨터 시스템 혹은 네트워크로 연결된 컴퓨터 시스템에 저장된 정보는 보안이 필요하다.
보호는 시스템 자원에 대한 모든 접근이 통제되도록 보장하는 것을 필요로 한다.
보안은 부적합한 접근을 차단하고, 침입을 탐지하기 위해 접속을 기록한다.
운영체제와 접촉하는 방식.
운영체제 대부분은 명령 인터프리터를 특수 프로그램으로 취급한다.
이는 shell이라고 불린다.
셸의 중요한 기능은 사용자가 지정한 명령을 수행하는 것이다.
이는 두 가지 일반적인 방식으로 구현될 수 있는데
1. 명령 인터프리터가 명령을 실행할 코드를 직접 가지는 경우
2. 시스템 프로그램에 의해 대부분의 명령을 구현하는 경우
2번의 예시로 rm file.txt
를 입력하면 rm이라는 파일을 찾아서, 그 파일을 메모리에 적재하고, 매개변수로 file.txt를 넘겨준다.
보통 마우스를 이용해 그래픽적으로 상호작용한다.
일반적으로 모바일 시스템에서 많이 사용한다.
자주 사용되는 작업의 명령어 절차를 파일로 저장한다.
이 프로그램은 기계어로 컴파일 되지는 않지만 CLI에 의해서 번역되면서 실행될 수 있다.
이것을 shell script라고 부른다.
시스템 콜은 운영체제에 의해 사용 가능한 서비스에 대한 인터페이스를 제공한다.
간단한 프로그램이라도 운영체제의 기능을 아주 많이 사용하게 된다.
종종 초당 수천 개의 시스템 콜을 수행하게 된다 .
대부분의 응용 개발자들은 API에 따라 프로그램을 설계한다.
API는 각 함수에 전달되어야 할 매개변수들과 기대 반환 값을 포함한, 함수의 집합을 명시한다.
프로그래머는 운영체제가 제공하는 코드의 라이브러리를 통하여 API를 활용한다.
API를 구성하는 함수들은 통상 시스템 콜을 호출한다. (ex. Windows 함수 createProcess()는 Windows 커널의 NTCreateProcess() 시스템 콜을 호출한다.)
왜 프로그래머가 직접 호출하지 않고 API를 통해 호출할까?
이렇게 하는 데에는 첫 째, 프로그램의 호환성에 있다.
API에 따라 설계하면 같은 API를 지원하는 시스템에서 호환될 수 있다.
또한 보통 시스템 콜은 API보다 어렵다.
또 다른 중요한 요소는 RTE(런타임 환경)이다.
컴파일러를 포함하여 필요한 전체 소프트웨어를 가리킨다.
RTE는 시스템 콜에 대한 인터페이스, 시스템 콜 인터페이스를 제공한다.
이 시스템 콜 인터페이스는 API 함수의 호출을 가로채어 필요한 시스템 콜을 호출한다.
따라서 운영체제 인터페이스에 대한 자세한 내용은 API에 의해 숨겨지고 RTE에 의해 관리된다.
운영체제에 매개변수를 전달하기 위해서 세 가지 일반적인 방법을 사용한다.
시스템 콜은 여섯 가지의 중요한 범주로 묶을 수 있다.
프로세스 제어
실행중인 프로그램은 정상종료(end()) 또는 비정상 종료(abort())되어야 한다.
프로세스 제어는 너무 많은 측면과 다양성이 있다.
파일 관리
create(), delete(), open(), read(), write() 등이 있다.
장치 관리
장치를 request()하고 release()한다.
입출력 장치와 파일 간에는 유사성이 매우 높아 많은 운영체제에서 이들을 통합한 파일 - 장치 구조로 결합하였다.
정보 유지 관리
운영체제는 현재 운영되고 있는 모든 프로세스에 관한 정보를 가지고 있으며, 이에 접근하기 위한 시스템 콜이 있다.
통신
통신모델에는 공유 메모리 모델과 메시지 전달 모델이 있는데,
메시지 전달 모델에서는 서로간의 호스트 이름이나 프로세스 이름을 알아야 한다.
get_hostid(), get_processid()을 통해 알아내고 open_connection()이나 close_connection() 시스템 콜에 전달된다.
공유 메모리 모델에서 프로세스는 다른 프로세스가 소유한 메모리 영역에 접근하기 위해 shared_momory_create() 등을 사용한다.
이러한 두 가지 방법은 보편적이며 시스템 대부분은 둘 다 구현한다.
protection
자원에 대한 접근을 제어하기 위한 기법을 지원한다.
set_permission()과 get_permission() 등이 있다.
현대 시스템의 또 다른 면은 시스템 서비스의 집합체이다.
시스템 서비스는 시스템 유틸리티로도 알려진, 프로그램 개발과 실행을 위해 더 편리한 환경을 제공한다.
그들 중 몇몇은 단순히 시스템 콜에 대한 사용자 인터페이스이며, 반면에 나머지는 훨씬 복잡하다.
이들은 몇 가지 범주로 분류할 수 있다.
운영체제 대부분은 시스템 프로그램과 함께 일반적인 연산을 수행하는 데 유용한 프로그램들도 제공한다.
사용자 대부분이 보는 운영체제의 관점은 실제의 시스템 콜에 의해서보다는, 시스템 프로그램과 응용에 의해 정의된다.
쉽게 말해, pc를 사용할 때 동일한 시스템 콜을 사용할 때도 gui로 하냐, shell로 하냐에 따라 다르게 보인다.
일반적으로 프로그램은 디스크에 이진 실행 파일(ex. a.out || prog.exe)로 존재한다.
cpu에서 실행하려면 프로그램을 메모리로 가져와 프로세스 형태로 배치되어야 한다.
여기선 프로그램을 컴파일하고 메모리에 배치하여 실행하기 까지의 절차를 설명한다.
소스 파일은 임의의 물리 메모리 위치에 적재되도록 설계된 오브젝트 파일로 컴파일 된다.
이러한 형식을 재배치 가능 오브젝트 파일이라고 한다.
링커는 재배치 가능 오브젝트 파일을 하나의 이진 실행 파일로 결합한다.
링킹 단계에서 라이브러리도 포함될 수 있다.
로더는 이진 실행 파일을 메모리에 적재하는 데 사용된다.
프로그램 부분에 최종 주소를 할당하고 프로그램 코드와 데이터를 해당 주소와 일치하도록 조정하여 프로그램이 라이브러리 호출이나 변수에 접근할 수 있게 한다.
./a.out
형태처럼 ./ 뒤에 실행파일의 이름을 입력하면 로더가 실행된다.
UNIX 시스템에서 ./a.out
을 하면 셸은 fork() 시스템 콜을 사용하여 새 프로세스릉 생성한다.
그 다음 exec() 시스템 콜로 로더를 호출하고 exec()에 실행 파일 이름을 전달한다.
로더는 생성된 프로세스의 주소공간을 사용하여 지정된 프로그램을 메모리에 적재한다.
시스템 대부분에서는 프로그램이 적재될 때 라이브러리를 동적으로 링크할 수 있게 한다.
링커는 프로그램이 적재될 때 동적으로 링크되고 적재될 수 있도록 재배치 정보를 삽입한다.
또한 여러 프로세스가 동적으로 링크된 라이브러리를 공유할 수 있다.
오브젝트 파일 및 실행파일은 일반적으로 표준화된 형식을 가진다.
UNIX 및 Linux 시스템에선 이를 ELF(Executable & Linkable Format)라고 한다.
실행가능 파일의 ELF 파일의 정보 중에는 프로그램의 시작점이 포함되며, 프로그램을 실행할 첫 번째 명령어의 주소가 저장되어 있다.
Windows는 PE(Portable Executable), macOS는 Mach-O형식을 사용한다.
기본적으로 한 운영체제에서 컴파일된 응용 프로그램은 다른 운영체제에서 실행할 수 없다.
하지만 이를 가능하게 만드는 여러 방법이 있다.
하지만 응용 프로그램의 운영체제 이동성이 부족한 데에는 여러가지 원인이 있어, 크로스 플랫폼 개발은 여전히 어렵다.
시스템을 설계하는 데에 첫째 문제점은 시스템의 목표와 명세를 정의하는 일이다.
운영체제에 대한 요구를 정의하는 문제를 해결하는 유일한 해법은 없다.
운영체제의 명세와 설계는 매우 창조적인 일이나, 소프트웨어 공학 분야에 의해 개발된, 일반적인 원칙들이 존재한다.
한 가지 중요한 원칙은 기법으로부터 정책을 분리하는 것이다.
기법은 어떤 일을 어떻게 할 것인가를 결정하는 것이고,
정책은 무엇을 할 것인가를 결정하는 것이다.
운영체제는 많은 사람에 의해 오랫동안 개발된 많은 프로그램의 집합체이기 때문에 구현 방법에 대해 일반적인 언급을 하는 것은 어렵다.
초기 운영체제는 어셈블리 언어로 작성되었으나, 현대에는 대부분 고급언어로 작성된다.
운영체제를 고급언어로 구현하는 것은 속도가 느리고 저장 장치가 많이 소요되는 것인데, 현대의 시스템에서는 크게 문제가 안된다.
운영체제의 주요 성능 향상은 어떤 언어로 작성하였느냐보다는 좋은 자료구조와 알고리즘에 의해 결정된다.
운영체제같이 복잡한 시스템은 신중히 제작되어야 한다.
일반적인 접근 방법은 태스크를 작은 구성요소로 분할하는 것이다.
커널의 모든 기능을 단일 정적 이진 파일에 넣는 것이다.
모놀리식 구조는 운영체제를 설계하는 일반적인 기술이다.
시스템 콜 인터페이스 아래와 물리적 하드웨어 위의 모든 것이 커널이다.
모놀리식 구조는 구현 및 확장이 어렵지만, 시스템 콜에 대한 오버헤드가 거의 없고 커널 안에서의 통신 속도가 빠르다.
모놀리식의 밀접하게 결합된 시스템이 아닌, 느슨하게 결합된 시스템이다.
이러한 시스템은 기능이 작은 구성요소로 나뉜다. 이 모든 구성요소가 합쳐져 커널을 구성한다.
이러한 구조의 장점은 시스템의 내부 작동을 더 자유롭게 생성하고 변경할 수 있다는 것이다.
시스템은 다양한 방식으로 모듈화 될 수 있는데 그 중 한 가지 방식이 계층적 접근 방식이다.
이 방식에선 운영체제가 여러 개의 층으로 나누어진다.
계층적 접근 방식의 장점은 구현과 디버깅의 간단함에 있다.
각 층은 자신보다 하위 층에서 제공된 연산들만 사용해 구현한다.
계층화된 시스템은 네트워크(ex. TCP/IP) 및 웹 응용 프로그램에서 성공적으로 사용됐다.
하지만 이러한 방식은 각 계층의 기능을 적절히 정의해야 하고,
사용자 프로그램이 여러 계층을 통과해야 하는 오버헤드로 인해 성능이 열악하다.
따라서 어느 정도만 계층화를 하는게 좋다.
모든 중요치 않은 구성요소를 커널로부터 제거하여 구성하는 방법이다.
통상 마이크로커널은 최소한의 프로세스와 메모리 관리를 제공한다.
이 접근법의 장점은 운영체제의 확장이 쉽다는 것이다.
모든 새로운 서비스는 사용자 공간에 추가되며, 커널을 변경할 필요가 없다.
또한 서비스 규모가 작은만큼 높은 보안성과 신뢰성을 제공한다.
마이크로 커널의 잘 알려진 실례는 Darwin이 있다.
Darwin은 두 개의 커널로 구성되며 그 중 하나가 Mach 마이크로 커널이다.
하지만 마이크로커널은 가중된 시스템 기능 오버헤드 때문에 성능이 나빠진다.
사용자 수준의 서비스들은 별도의 주소 공간에 존재하기 때문에, 메세지 복사 및 프로세스 전환과 관련된 오버헤드가 있다.
운영체제를 설계하는 데 최선책은 아마 LKM(Loadable Kernel Modules)기법의 사용일 것이다.
이 접근법에서 커널은 핵심적인 구성요소의 집합을 가지고, 부팅 때 또는 실행 중에 부가 서비스들을 모듈을 통하여 링크할 수 있다.
커널은 핵심 서비스를 제공하고 다른 서비스들은 커널이 실행되는 동안 동적으로 구현하는 것이다.
서비스를 동적으로 링크하는 것은 새로운 기능을 직접 커널에 추가하는 것보다 바람직하다.
계층 구조와 닮았지만, 모듈에서 임의의 다른 모듈을 호출할 수 있다는 점에서 유연하다.
또한 통신하기 위해 메시지 전달을 호출할 필요가 없기 때문에 효율적이다.
Linux는 주로 장치 드라이버와 파일 시스템을 지원하기 위해 LKM을 사용한다.
사실 엄격하게 정의된 하나의 구조를 채택한 운영체제는 거의 존재하지 않는다.
일반적으로 운영체제는 다양한 주변장치 구성을 가진 모든 컴퓨터에서 실행되도록 설계된다.
운영체제를 처음부터 생성하는 경우 다음 절차를 밟아야 한다.
1. 운영체제 소스코드를 작성한다(또는 작성된 소스코드를 구한다)
2. 운영체제가 실행될 시스템의 운영체제를 구성한다.
3. 운영체제를 컴파일한다.
4. 운영체제를 설치한다.
5. 컴퓨터와 새 운영체제를 부팅한다.
Linux 시스템을 처음부터 빌드하는 방법에 관해 설명한다.
1. http://www.kernel.org
에서 Linux 소스코드를 다운한다.
2. make menuconfig
를 통해 커널을 구성한다. .config
구성파일이 생성된다.
3. make
명령을 사용하여 메인 커널을 컴파일한다. make 명령은 .config 파일에서 식별된 구성 매개변수를 기반으로 커널을 컴파일하여, 커널 이미지인 vmlinuz 파일을 생성한다.
4. make modules
로 커널 모듈을 컴파일한다. 커널 컴파일과 마찬가지로 모듈 컴파일은 .config 파일에 지정된 구성 매개변수에 따라 다르다.
5. make modules install
로 커널 모듈을 vmlinuz에 설치한다.
6. make install
로 시스템에 새 커널을 설치한다.
하드웨어는 커널의 위치 또는 커널을 적재하는 방법을 어떻게 알 수 있을까?
커널을 적재하여 컴퓨터를 시작하는 과정을 Booting이라고 한다.
대부분의 시스템에서의 부팅과정은 다음과 같다.
일부 컴퓨터 시스템은 다단계 부팅 과정을 사용한다.
컴퓨터를 켜면 BIOS라고 하는 비휘발성 펌웨어 에 있는 소형 부트로더가 실행된다.
이 초기 부트로더는 부트 블록이라고 하는 디스크의 정해진 위치에 있는 두 번째 부트 로더를 적재하는 작업만 한다.
부트 블록에 저장된 프로그램은 전체 운영체제를 메모리에 적재하고 실행을 시작하기에 충분히 정교할 수도 있다.
일반적으로 이 부트로더는 간단한 코드로서(하나의 블록에 저장되어야 하기 때문에) 디스크의 주소와 부트스트랩 프로그램 나머지의 길이만 알고 있다.
많은 최신 컴퓨터 시스템이 BIOS 기반 부팅 과정을 UEFI(Unified Extensible Firmware Interface)로 대체하였다.
UEFI는 64비트 시스템과 용량이 큰 디스크를 더 잘 지원하고, BIOS보다 부팅이 빠르다.(하나의 완전한 부팅 관리자라 다단계인 BIOS보다 빠르다)
부트스트랩 프로그램은 커널을 메모리에 적재하는 것 외에도, 메모리와 cpu를 점검하고 시스템 상태를 확인한다.
진단을 통과해야 부팅을 계속 진행할 수 있다.
부트스트랩은 cpu 레지스터에서 장치 컨트롤러, 메모리의 내용에 이르기까지, 시스템의 모든 측면을 초기화 할 수 있다.
운영체제를 시작하고 루트 팡리 시스템을 마운트한다.
바로 이 시점에서 시스템이 실행 중이라고 말할 수 있다.
GRUB는 Linux 및 UNIX 시스템을 위한 오픈소스 부트스트랩 프로그램이다.
만일 프로세스가 실패한다면, 문제가 발생했다는 것을 경고하기 위해 오류 정보를 로그 파일에 기록한다.
운영체제는 또한 프로세스가 사용하던 메모리를 캡처한 core dump를 취하고, 차후 분석을 위해 파일로 저장한다. (초창기 시절에 메모리를 코어라고 칭했다.)
실행중인 프로그램과 코어덤프는 디버거에 의해 검사될 수 있으며, 이는 프로그래머가 프로세스의 코드와 메모리를 분석할 수 있도록 한다.
커널 디버깅은 정말로 복잡한 작업입니다. 이것은 컴퓨터의 운영 체제인 커널이 오류를 일으킬 때 발생하는 문제를 찾아 해결하기 위한 과정입니다.
우선, 커널에서 발생하는 장애는 crash라고 합니다. 이는 일반적으로 프로그램이 예기치 못한 상황에서 중단되는 경우를 말합니다. 이런 상황에서 오류 정보는 로그 파일에 저장되고, 커널의 메모리 상태는 crash dump라는 특별한 공간에 저장됩니다.
그런데 문제는 파일 시스템 코드가 장애를 일으킨 경우입니다. 이때 커널의 상태를 파일 시스템에 저장하려는 시도는 굉장히 위험합니다. 왜냐하면, 이미 문제가 발생한 파일 시스템 코드 때문에 추가적인 오류가 발생할 수 있기 때문입니다.
그래서 보통 커널의 메모리 상태는 파일 시스템이 아닌 디스크의 특정 부분, 즉 파일 시스템에서 분리된 공간에 저장됩니다. 이렇게 하면, 파일 시스템의 오류 때문에 추가적인 문제를 일으키는 것을 방지할 수 있습니다.
이렇게 저장된 메모리 상태는 커널이 복구 불가능한 오류를 감지했을 때 작성됩니다. 이때 시스템 메모리의 전체 내용 또는 최소한 커널이 소유한 부분의 내용이 이 디스크 영역에 저장됩니다.
그리고 나서 시스템이 재부팅되면, 이 디스크 영역에서 데이터를 수집합니다. 그리고 이 데이터를 분석하기 위해 파일 시스템의 "크래시 덤프" 파일에 기록합니다. 이렇게 저장된 데이터를 분석하여 원래의 오류를 찾아내고 해결하는 것이 커널 디버깅의 목표입니다.
시스템 성능을 감시하기 위해 시스템 동작을 측정하고 표시할 수 있는 방법을 가지고 있어야 한다.
이러한 관찰을 위해 도구는 카운터 또는 추적의 두 가지 접근 방식 중 하나를 사용할 수 있다.
카운터
운영체제는 카운터를 통해 호출된 시스템 콜 횟수 또는 네트워크 장치 또는 디스크에 수행된 작업 수와 같은 시스템 활동을 추적한다.
ex. ps
: 프로세스에 대한 정보
netstat
: 네트워크 인터페이스에 대한 통계
Linux 시스템의 카운터 기반 도구의 대부분은 /proc 파일 시스템에서 통계를 읽는다.
/proc은 커널 메모리에만 존재하는 pseudo 파일 시스템으로, 주로 프로세스별 통계와 커널 통계를 질의하는 데 사용된다.
프로세스가 /proc 아래의 하위 디렉터리로 표시된다. (ex. /proc/2155)
Windows에서는 Windows 작업 관리자를 제공한다.
추적
카운터 기반 도구는 커널에서 유지 관리하는 특정 통계의 현재 값에 대해 간단히 문의하는 반면,
추적 도구는 시스템 콜과 관련된 단계와 같은 특정 이벤트에 대한 데이터를 수집한다.
다음으로 Linux에서 동적 커널 추적을 위한 툴킷인 BCC에 대해 설명한다.
도구 집합은 시스템의 어느 부분도 디버깅할 수 있어야 하며, 시스템의 안정성을 해치지 않아야 한다.
BCC(BPF Compiler Collection)는 이러한 것을 만족하면서 동적이고, 낮은 영향력을 미치는 디버깅 환경을 제공한다.
BCC는 Linux 시스템을 위한 추적 기능을 제공하는 풍부한 툴킷이다.
BCCsms eBPF 도구에 대한 프론트 엔드 인터페이스이다.