운영체제
운영체제(Operating System, OS)는 컴퓨터의 하드웨어 자원을 관리하고 응용 프로그램이 실행될 수 있는 공통 환경과 서비스를 제공하는 시스템 소프트웨어이다. 중앙처리장치의 실행 시간과 메모리, 저장 장치, 입출력 장치, 파일, 네트워크 연결 등의 자원을 여러 프로그램에 배분...
운영체제(Operating System, OS)는 컴퓨터의 하드웨어 자원을 관리하고 응용 프로그램이 실행될 수 있는 공통 환경과 서비스를 제공하는 시스템 소프트웨어이다. 중앙처리장치의 실행 시간과 메모리, 저장 장치, 입출력 장치, 파일, 네트워크 연결 등의 자원을 여러 프로그램에 배분하며, 응용 프로그램이 하드웨어의 구체적인 동작을 직접 제어하지 않고도 파일을 읽고 프로세스를 생성하며 화면과 네트워크, 주변 장치를 사용할 수 있도록 추상화된 인터페이스를 제공한다. 운영체제의 핵심 구성 요소인 커널은 높은 권한으로 실행되면서 프로세스와 스레드의 스케줄링, 가상 메모리, 장치 드라이버, 입출력과 보호 기능을 담당하고, 응용 프로그램은 일반적으로 시스템 호출과 시스템 라이브러리를 통해 커널의 기능을 요청한다. 응용 프로그램을 제한된 사용자 모드에서 실행하고 각 프로세스에 분리된 주소 공간과 접근 권한을 부여함으로써 한 프로그램의 오류나 무단 접근이 다른 프로그램과 운영체제 전체에 미치는 영향을 줄이는 것도 주요 역할이다.
운영체제는 커널 하나만이 아니고 실제 운영 환경은 커널을 중심으로 장치 드라이버와 시스템 라이브러리, 백그라운드 서비스, 초기화 시스템, 사용자 계정과 보안 체계, 명령행 셸 또는 그래픽 사용자 인터페이스, 관리 도구 등이 결합되어 구성된다. 다만 운영체제와 커널의 경계는 체계와 문맥에 따라 다르게 사용되며, 커널 기능을 사용자 공간의 별도 서버로 분리하는 구조도 있고 많은 기능을 커널 내부에 통합하는 구조도 있다. 운영체제는 개인용 컴퓨터와 서버를 위한 범용 체계뿐 아니라 스마트폰과 게임기, 네트워크 장비, 자동차, 산업 제어기와 소형 임베디드 장치를 위한 전용 또는 실시간 체계로도 구현되며, 요구되는 응답 시간과 안전성, 전력, 메모리, 사용자 인터페이스에 따라 서로 다른 구조와 정책을 사용한다.
역사
초기의 전자식 컴퓨터에는 오늘날과 같은 운영체제가 존재하지 않았다. 컴퓨터는 하드웨어와 기본적인 조작 설명서의 형태로 제공되었고, 프로그래머가 프로그램을 기계에 직접 적재하고 실행 환경을 구성해야 했다. 프로그램과 데이터는 천공 카드와 종이테이프, 자기테이프 등에 기록되었으며, 한 작업이 끝나면 다음 작업을 실행하기 위해 사람이 장비를 다시 설정했다. 이 시기에는 컴퓨터가 하나의 프로그램을 실행하는 동안 다른 사용자가 시스템을 이용하기 어려웠고, 프로그램 준비와 장비 전환에 상당한 시간이 소요되었다.
운영체제의 역사는 컴퓨터 자원을 더 효율적으로 사용하고, 하드웨어의 복잡한 조작을 소프트웨어로 자동화하려는 과정에서 시작되었다. 초기의 단순한 제어 프로그램은 작업의 적재와 실행 순서를 관리하는 수준이었지만, 이후 중앙처리장치와 메모리, 저장 장치, 입출력 장치, 사용자와 프로그램을 동시에 관리하는 복합적인 시스템으로 발전하였다.
초기 컴퓨터와 수동 운용
1940년대와 1950년대 초의 컴퓨터에서는 한 번에 하나의 프로그램을 실행하는 방식이 일반적이었다. 사용자는 사용할 시간을 예약하고 프로그램과 데이터를 직접 기계에 입력했으며, 실행 오류가 발생하면 프로그램을 수정한 뒤 처음부터 다시 준비해야 했다.
초기 컴퓨터는 매우 비쌌지만 프로그램을 준비하고 장비를 전환하는 동안 중앙처리장치가 사용되지 않는 시간이 많았다. 프로그램이 카드 판독기나 프린터 같은 느린 입출력 장치를 기다리는 동안에도 다른 작업을 실행할 수 없었다. 이에 따라 컴퓨터 자체가 작업의 실행 순서와 장치 조작을 맡도록 하는 상주 모니터와 제어 프로그램이 등장했다.
초기 운영체제의 선구적인 사례로는 1950년대 중반의 GM-NAA I/O가 언급된다. 이 시스템은 제너럴 모터스와 노스아메리칸 항공의 사용자 그룹이 IBM 704를 위해 개발했으며, 작업을 자동으로 연속 실행하고 입출력 처리를 공통화하는 기능을 제공했다. Computer History Museum의 운영체제 보존 자료는 GM-NAA 계열 시스템을 초기 메인프레임 운영체제 발전의 출발점 가운데 하나로 설명한다.
배치 처리
배치 처리는 여러 프로그램과 데이터를 하나의 작업 묶음으로 준비한 뒤, 운영자가 이를 컴퓨터에 순서대로 입력하여 자동 실행하는 방식이다. 사용자는 작업을 제출한 후 즉시 결과를 받지 못하고, 시스템이 앞선 작업을 모두 처리한 뒤 출력물을 받을 수 있었다.
배치 시스템은 사람이 작업마다 장비를 다시 설정해야 하는 시간을 줄였으며, 작업 제어 언어와 상주 모니터를 사용하여 프로그램 적재와 실행, 오류 처리와 출력 작업을 자동화했다. 초기 운영체제는 이러한 배치 작업을 순차적으로 실행하면서 중앙처리장치와 입출력 장치를 관리하는 역할을 맡았다.
그러나 단순한 배치 처리에서는 실행 중인 프로그램이 입출력을 기다리는 동안 중앙처리장치가 유휴 상태가 되는 문제가 남아 있었다. 이를 줄이기 위해 입출력 작업을 별도의 장치와 채널에서 처리하고, 여러 프로그램을 메모리에 함께 적재하여 하나의 프로그램이 기다리는 동안 다른 프로그램을 실행하는 다중 프로그래밍이 발전하였다.
Computer History Museum은 초기 배치 처리를 사용자가 작업을 제출한 후 결과를 기다리는 비대화형 방식으로 설명하며, 이후 시분할이 컴퓨터 사용을 대화형 방식으로 변화시켰다고 설명한다.
다중 프로그래밍
다중 프로그래밍은 여러 프로그램을 메모리에 적재하고, 한 프로그램이 입출력을 기다릴 때 중앙처리장치를 다른 프로그램에 할당하는 방식이다. 이를 통해 중앙처리장치와 입출력 장치를 동시에 활용하고 시스템 전체의 처리량을 높일 수 있었다.
다중 프로그래밍을 구현하려면 운영체제가 다음과 같은 기능을 제공해야 했다.
- 여러 작업을 메모리에 배치하는 기능
- 프로그램 사이의 메모리 보호
- 실행할 작업을 선택하는 스케줄링
- 입출력 완료를 알리는 인터럽트 처리
- 작업별 파일과 장치 상태 관리
- 오류가 다른 작업으로 확산되지 않도록 하는 보호 기능
이 과정에서 운영체제는 단순한 작업 실행 도구에서 자원 할당자와 보호 관리자로 발전하였다. CPU 스케줄링과 메모리 관리, 인터럽트, 장치 드라이버와 파일 시스템 같은 현대 운영체제의 핵심 개념이 이 시기에 본격적으로 형성되었다.
시분할 시스템
시분할 시스템은 중앙처리장치의 실행 시간을 짧은 단위로 나누어 여러 사용자의 프로그램에 번갈아 할당하는 방식이다. 각 사용자는 단말기를 통해 컴퓨터와 상호 작용하며, 시스템이 빠르게 사용자를 전환하기 때문에 자신이 컴퓨터를 독점적으로 사용하는 것과 비슷한 경험을 얻을 수 있었다.
시분할은 사용자가 프로그램을 제출하고 오랜 시간 뒤 결과를 받던 배치 처리와 달리, 명령을 입력하고 즉시 결과를 확인하며 프로그램을 수정할 수 있게 했다. Computer History Museum은 시분할이 비싼 메인프레임 자원을 여러 사용자가 공유하고 배치 처리의 불편을 해결하기 위한 방식으로 1960년대 초에 발전했다고 설명한다.
MIT에서 개발된 Compatible Time-Sharing System(CTSS)은 초기의 중요한 시분할 시스템 가운데 하나였다. CTSS 개발은 1961년에 시작되었고, 1963년 무렵에는 대규모로 안정적으로 운용되면서 시분할의 실현 가능성을 보여주었다. 사용자는 원격 단말기를 통해 프로그램을 편집하고 실행했으며, 파일과 사용자 계정을 사용할 수 있었다.
시분할 시스템은 일반적인 CPU 공유를 넘어 사용자 인증과 파일 보호, 대화형 셸, 프로세스 분리와 자원 사용량 계산 등의 기능을 요구했다. 이 과정에서 현대적인 다중 사용자 운영체제의 구조가 형성되었다.
Multics
CTSS의 경험을 바탕으로 MIT와 제너럴 일렉트릭, 벨 연구소는 1960년대 중반 Multics를 개발하기 시작했다. Multics는 컴퓨팅 자원을 전기나 전화와 같은 공공 서비스처럼 여러 사용자가 안정적으로 이용할 수 있는 컴퓨터 유틸리티를 목표로 했다. 개발 계획은 1964년부터 구체화되었고, 1965년 학술 회의에서 시스템의 설계 목표가 공개되었다.
Multics는 당시로서는 매우 선진적인 기능을 도입했다.
- 계층형 파일 시스템
- 파일과 가상 메모리를 통합한 저장 구조
- 동적 링크
- 세분화된 접근 권한
- 보호 링
- 다중 프로세서 지원
- 온라인 시스템 재구성
- 고급 언어를 이용한 운영체제 구현
- 여러 사용자를 위한 지속적인 시분할 서비스
Multics는 복잡성과 개발 지연으로 어려움을 겪었지만 운영체제의 보안과 파일 시스템, 가상 메모리, 동적 연결 및 시스템 구조에 장기적인 영향을 주었다. Multics는 1965년에 시작되어 상용 시스템으로 사용되었고, 마지막 운영 시스템은 2000년에 종료되었다.
IBM System/360과 OS/360
1964년 IBM은 서로 다른 성능과 가격대의 컴퓨터가 하나의 호환 가능한 명령어 체계와 소프트웨어를 공유하도록 설계된 IBM System/360 제품군을 발표했다. 이전에는 용도와 크기가 다른 컴퓨터 제품군이 서로 다른 명령어와 소프트웨어를 사용하는 경우가 많았지만, System/360은 하나의 호환 가능한 아키텍처를 여러 모델에 적용하려 했다.
System/360을 위해 개발된 OS/360은 다양한 크기의 시스템과 상업·과학 계산 작업을 하나의 운영체제 계열로 지원하려는 대규모 프로젝트였다. OS/360은 배치 처리와 다중 프로그래밍, 주변 장치 관리와 공통 소프트웨어 환경을 제공했다.
OS/360 개발은 예상보다 훨씬 복잡하고 어려운 프로젝트가 되었으며, 소프트웨어 공학의 역사에서도 중요한 사례가 되었다. 방대한 기능과 여러 하드웨어 구성을 동시에 지원하면서 개발 일정과 품질 관리에 어려움을 겪었고, 이는 대규모 소프트웨어 프로젝트의 조직과 설계 방법에 관한 논의를 촉진했다.
System/360과 OS/360의 호환성 전략은 사용자가 더 높은 성능의 컴퓨터로 교체하더라도 기존 소프트웨어와 데이터를 계속 사용할 수 있게 하는 방향을 제시했다. 이는 이후 운영체제와 하드웨어 플랫폼에서 하위 호환성이 중요한 가치로 자리 잡는 데 영향을 주었다.
가상 머신의 등장
1960년대 IBM은 하나의 물리적 컴퓨터를 여러 개의 독립적인 가상 컴퓨터처럼 사용하는 가상화 기술을 연구했다. CP-40과 CP-67은 System/360 계열에서 여러 가상 머신을 생성하고, 각 가상 머신에서 별도의 운영 환경을 실행하는 구조를 발전시켰다.
IBM의 CP/CMS 연구는 1964년 CP-40에서 시작되어 CP-67로 발전했고, 1972년 VM/370으로 공식 발표되었다. 각 사용자는 독립된 가상 머신과 대화형 환경을 사용할 수 있었으며, 운영체제 개발과 시험, 사용자 격리에 활용할 수 있었다.
가상 머신은 이후 오랫동안 메인프레임 환경에서 사용되었으며, 2000년대 서버 통합과 클라우드 컴퓨팅이 확산되면서 다시 핵심 기술로 부상했다.
UNIX의 탄생
벨 연구소는 Multics 프로젝트에서 철수한 뒤, 켄 톰프슨과 데니스 리치 등을 중심으로 더 작고 단순한 운영체제를 개발했다. 초기 UNIX는 1969년 무렵 PDP-7 컴퓨터에서 개발되었으며, 이후 PDP-11로 이전되었다.
UNIX는 처음에는 어셈블리어로 작성되었지만 이후 데니스 리치가 개발한 C (프로그래밍 언어)로 다시 작성되었다. 운영체제의 상당 부분을 고급 언어로 구현함으로써 다른 하드웨어로 이식하기 쉬워졌고, C와 UNIX는 함께 확산되었다.
리치와 톰프슨이 발표한 1974년 논문은 UNIX의 파일 시스템과 프로세스, 명령 인터페이스를 설명했다. UNIX는 계층형 파일 시스템과 프로세스 생성, 파이프, 장치의 파일 추상화, 작은 프로그램을 조합하는 명령 환경을 제공했다.
UNIX의 주요 설계 특성은 다음과 같다.
- 단순한 파일 중심 인터페이스
- 프로세스 생성과 프로그램 실행의 분리
- 작은 도구를 파이프로 연결하는 방식
- 텍스트 기반 설정과 데이터 처리
- 다중 사용자와 시분할 지원
- C를 통한 높은 이식성
- 운영체제와 사용자 도구의 비교적 명확한 구분
최초의 UNIX 프로그래머 설명서는 1971년에 작성되었고, 1970년대 대학과 연구기관에 소스 코드와 함께 보급되면서 다양한 변형이 개발되었다.
BSD와 상용 UNIX
캘리포니아 대학교 버클리에서는 UNIX를 기반으로 BSD를 개발했다. BSD는 가상 메모리와 작업 제어, Fast File System, TCP/IP 네트워크 구현과 소켓 API 등을 발전시켰다. BSD의 네트워크 기술은 인터넷과 여러 운영체제에 영향을 주었다.
동시에 AT&T 계열 UNIX와 SunOS, HP-UX, AIX와 같은 여러 상용 UNIX가 등장했다. 각 시스템은 서로 다른 하드웨어와 기능을 제공했지만 호환되지 않는 확장과 도구가 늘어나면서 UNIX 생태계가 여러 계열로 분리되었다.
이러한 차이를 줄이기 위해 POSIX와 Single UNIX Specification 같은 표준화 작업이 추진되었다. 이 표준들은 프로세스와 파일, 셸, 시스템 API의 공통 인터페이스를 정의하여 UNIX 계열 시스템 사이에서 소스 코드의 이식성을 높이는 데 목적이 있었다.
실시간 운영체제
일반적인 메인프레임과 시분할 운영체제가 처리량과 사용자 응답성을 중심으로 발전하는 동안, 산업 제어와 군사·항공 시스템에서는 정해진 시간 안에 작업을 완료해야 하는 실시간 운영체제가 발전했다.
실시간 운영체제는 평균적으로 빠른 것보다 최악의 상황에서도 응답 시간을 예측할 수 있는 것이 중요하다. 우선순위 기반 스케줄링과 빠른 인터럽트 처리, 제한된 메모리 할당, 우선순위 역전 방지와 같은 기능이 중요하게 다뤄졌다.
이후 실시간 운영체제는 공장 자동화와 자동차, 항공기, 통신 장비, 의료기기, 로봇과 소형 임베디드 장치에 폭넓게 사용되었다.
개인용 컴퓨터 운영체제
1970년대 마이크로프로세서와 개인용 컴퓨터가 등장하면서 운영체제는 대형 메인프레임의 다중 사용자 환경에서 개인이 사용하는 소형 컴퓨터로 확장되었다.
게리 킬달이 개발한 CP/M은 초기 마이크로컴퓨터에서 상업적으로 성공한 대표적인 운영체제였다. CP/M은 1974년 무렵 개발되었으며, 여러 제조사의 8비트 컴퓨터에서 동일한 응용 프로그램을 사용할 수 있는 공통 환경을 제공했다. Computer History Museum은 CP/M을 최초로 상업적 성공을 거둔 개인용 컴퓨터 운영체제로 평가한다.
CP/M은 하드웨어별 입출력 부분을 BIOS로 분리하여 비교적 쉽게 다른 컴퓨터로 이식할 수 있었다. 1976년의 CP/M 1.3은 기계별 입출력 코드를 수정하기 쉬운 BIOS 구조를 포함했고, 이 구조는 여러 제조사의 컴퓨터가 공통 소프트웨어 생태계를 공유하는 데 기여했다.
1981년 IBM PC가 등장하면서 Microsoft가 제공한 PC DOS와 MS-DOS가 빠르게 확산되었다. MS-DOS는 초기에는 단일 사용자와 단일 작업 중심의 비교적 단순한 명령행 운영체제였지만, IBM PC 호환 컴퓨터의 성장과 함께 사실상의 표준 환경이 되었다. IBM PC는 Intel 8088 프로세서와 Microsoft의 DOS를 사용했으며 기업과 가정의 개인용 컴퓨터 보급을 크게 촉진했다.
그래픽 사용자 인터페이스
초기의 개인용 컴퓨터 운영체제는 주로 명령행을 사용했지만, 1970년대 Xerox PARC의 연구와 1980년대 상용 제품을 통해 창과 아이콘, 메뉴, 포인터를 사용하는 그래픽 사용자 인터페이스가 확산되었다.
Apple Lisa와 Macintosh는 그래픽 인터페이스와 마우스를 일반 사용자에게 제공한 초기 상용 컴퓨터였다. Macintosh는 파일과 프로그램을 아이콘과 창으로 조작하는 환경을 대중화했다.
Microsoft는 1983년 Windows를 MS-DOS 위에서 동작하는 그래픽 운영 환경으로 발표했고, 첫 제품은 1985년에 출시되었다. 초기 Windows는 DOS에 의존했지만 이후 Windows NT 계열을 중심으로 독립적인 운영체제 구조로 전환되었다.
그래픽 인터페이스의 확산은 운영체제의 역할을 넓혔다. 운영체제는 프로세스와 파일만 관리하는 것이 아니라 창 관리와 그래픽, 글꼴, 인쇄, 멀티미디어, 입력 장치, 접근성과 일관된 애플리케이션 인터페이스를 제공하게 되었다.
네트워크 운영체제
개인용 컴퓨터와 워크스테이션이 네트워크로 연결되면서 운영체제는 원격 파일과 프린터 공유, 사용자 인증, 전자우편, 원격 로그인과 분산 자원 접근 기능을 제공하기 시작했다.
UNIX와 BSD는 TCP/IP를 폭넓게 지원하면서 인터넷 서버와 워크스테이션의 주요 기반이 되었다. Novell NetWare와 Windows Server 계열은 기업 내부의 파일·인쇄·계정 서비스를 제공했으며, 여러 운영체제가 네트워크 프로토콜과 디렉터리 서비스를 중심으로 상호 연결되었다.
네트워크가 운영체제의 선택 기능에서 기본 기능으로 변화하면서 보안 모델도 변했다. 이전에는 같은 컴퓨터 안의 사용자와 프로세스를 분리하는 것이 중심이었다면, 이후에는 원격 공격과 네트워크 인증, 암호화, 방화벽과 자동 보안 업데이트가 운영체제의 핵심 기능이 되었다.
마이크로커널 연구
1980년대에는 운영체제의 기능을 작은 커널과 사용자 공간의 서버로 분리하는 마이크로커널 구조가 활발하게 연구되었다. 마이크로커널은 커널 내부에 최소한의 주소 공간과 스레드, 프로세스 간 통신 기능만 두고, 파일 시스템과 장치 드라이버, 네트워크 같은 서비스를 별도의 프로세스로 실행하려 했다.
Mach와 MINIX, QNX 등이 대표적인 사례이다. 마이크로커널 구조는 서비스의 격리와 교체, 시스템의 모듈화에 장점이 있지만, 프로세스 간 통신과 문맥 전환 비용이 성능 문제로 이어질 수 있었다.
마이크로커널의 개념은 이후 순수한 형태뿐 아니라 혼합형 커널과 하이브리드 구조로 여러 운영체제에 반영되었다. Windows NT와 Apple의 XNU는 전통적인 단일형 커널과 마이크로커널의 요소를 결합한 구조로 발전했다.
Linux와 공개 개발
1991년 리누스 토르발스는 Intel 80386 컴퓨터를 위한 새로운 커널을 개발하여 공개했다. Linux 0.01의 릴리스 문서는 커널이 386 프로세서의 기능을 활용하고 MINIX와 유사한 환경에서 개발되었음을 설명한다.
Linux는 GNU 프로젝트의 컴파일러와 셸, 라이브러리, 도구와 결합되어 완전한 UNIX 계열 운영 환경을 구성했다. 소스 코드가 공개되고 인터넷을 통해 여러 개발자가 참여하면서 분산된 공개 개발 모델의 대표적인 사례가 되었다.
Linux는 처음에는 개인용 386 컴퓨터를 위한 실험적인 커널이었지만, 이후 여러 CPU 아키텍처와 서버, 슈퍼컴퓨터, 네트워크 장비, 임베디드 장치와 스마트폰에 이식되었다. Linux 개발은 1991년 시작된 이후 공개 메일링 리스트와 버전 관리 체계를 통해 지속되고 있다.
Linux의 발전은 운영체제가 반드시 하나의 기업 내부에서만 개발되어야 한다는 기존 관념을 변화시켰다. 여러 기업과 개인, 연구기관이 하나의 커널에 공동으로 기여하면서도 각 조직이 서로 다른 배포판과 제품을 구성하는 생태계가 형성되었다.
Windows NT
Microsoft는 MS-DOS 계열과 별도로 보안과 이식성, 다중 사용자 및 선점형 멀티태스킹을 고려한 Windows NT를 개발했다. Windows NT 3.1은 1993년에 출시되었으며, 이후 Windows 2000과 Windows XP, 현대 Windows 계열의 기반이 되었다.
Windows NT는 사용자 모드와 커널 모드의 분리, 가상 메모리, 보안 식별자와 접근 제어 목록, 하드웨어 추상화 계층, 여러 실행 환경을 지원하는 구조를 제공했다. 이후 소비자용 Windows도 NT 계열로 통합되면서 DOS 기반 Windows 계열은 종료되었다.
Windows는 개인용 컴퓨터뿐 아니라 기업 서버와 워크스테이션, 게임과 각종 산업용 장치에서 사용되었으며, 그래픽 사용자 인터페이스와 하위 호환성, 광범위한 하드웨어와 응용 프로그램 생태계를 중심으로 발전했다.
NeXTSTEP과 macOS
1980년대 말 NeXT는 UNIX 계열 기반과 객체 지향 개발 환경, Display PostScript 그래픽 시스템을 결합한 NeXTSTEP을 개발했다. NeXTSTEP은 Mach와 BSD 기술을 바탕으로 했으며, 이후 Apple이 NeXT를 인수하면서 차세대 Mac 운영체제의 기반이 되었다.
Apple은 기존 Classic Mac OS를 대체하기 위해 NeXTSTEP 계열 기술을 사용하여 Mac OS X를 개발했다. Mac OS X는 Mach 기반 커널과 BSD 사용자 환경을 결합한 XNU 커널, 새로운 그래픽과 애플리케이션 프레임워크를 제공했다. 이후 명칭은 OS X를 거쳐 macOS로 변경되었다.
이 계열의 운영체제 기술은 macOS뿐 아니라 iOS와 iPadOS, watchOS, tvOS와 visionOS의 공통 기반으로 확장되었다. Apple의 현대 운영체제는 서로 다른 장치에 맞는 사용자 인터페이스를 제공하면서 커널과 보안, 개발 도구와 여러 시스템 기술을 공유한다.
모바일 운영체제
휴대전화가 통화 중심 장치에서 범용 컴퓨터로 발전하면서 모바일 운영체제는 별도의 중요한 분야가 되었다. 초기 스마트폰과 PDA에서는 Palm OS와 Symbian, Windows Mobile, BlackBerry OS 등이 사용되었다.
2007년 Apple은 iPhone을 발표했고, 이후 해당 운영체제는 iOS라는 이름으로 발전했다. iOS는 데스크톱 운영체제 계열의 커널과 보안 구조를 모바일 환경에 적용하면서 터치 인터페이스와 전력 관리, 센서, 이동통신, 앱 배포와 권한 제어를 중심으로 설계되었다.
Google이 주도한 Android는 Linux 커널을 기반으로 모바일 장치용 프레임워크와 애플리케이션 실행 환경을 결합했다. Android는 여러 제조사와 반도체, 이동통신사에 채택되면서 다양한 스마트폰과 태블릿, TV, 자동차 및 임베디드 장치로 확산되었다.
모바일 운영체제는 데스크톱 운영체제보다 애플리케이션 격리와 코드 서명, 권한 요청, 중앙화된 앱 배포, 절전과 백그라운드 실행 제한을 더 강하게 적용하는 경향을 보였다. 이러한 보안과 배포 모델은 이후 데스크톱 운영체제에도 영향을 주었다.
가상화의 대중화
가상화는 메인프레임 시대부터 존재했지만, 1990년대 말과 2000년대에 x86 서버를 대상으로 다시 크게 확산되었다. 소프트웨어 기반 가상화와 이후 CPU의 하드웨어 가상화 지원을 통해 하나의 물리 서버에서 여러 운영체제를 동시에 실행할 수 있게 되었다.
가상화는 다음과 같은 변화를 가져왔다.
- 서버 통합
- 운영체제와 응용 프로그램 격리
- 가상 머신의 복제와 이동
- 개발 및 시험 환경의 자동화
- 장애 복구와 스냅샷
- 클라우드 컴퓨팅의 자원 분할
가상 머신 모니터 또는 하이퍼바이저가 하드웨어 자원을 여러 게스트 운영체제에 배분하면서, 운영체제는 실제 하드웨어뿐 아니라 가상 하드웨어 위에서도 실행되는 계층으로 자리 잡았다.
컨테이너와 운영체제 수준 가상화
가상 머신이 각각 별도의 커널을 실행하는 것과 달리, 컨테이너는 하나의 운영체제 커널을 공유하면서 프로세스와 파일 시스템, 네트워크, 사용자와 자원 사용량을 격리한다.
UNIX의 chroot와 BSD jail, Solaris Zones 같은 기술을 거쳐 Linux의 namespace와 cgroup이 결합되면서 현대적인 컨테이너 환경이 발전했다. Docker는 컨테이너 이미지를 제작하고 배포하는 사용자 경험을 단순화했고, Kubernetes는 여러 서버에서 컨테이너를 배치하고 관리하는 기반을 제공했다.
컨테이너의 확산으로 운영체제의 역할은 하나의 물리 컴퓨터를 관리하는 데서 더 나아가, 수많은 격리된 응용 프로그램 환경과 자원 제한, 네트워크 이름 공간 및 이미지 실행을 지원하는 방향으로 확장되었다.
클라우드와 대규모 데이터 센터
클라우드 컴퓨팅에서는 운영체제가 물리 서버와 가상 머신, 컨테이너, 네트워크와 저장소를 함께 구성하는 기반이 되었다. Linux와 Windows Server는 대규모 데이터 센터에서 사용되며, 하이퍼바이저와 컨테이너 런타임, 오케스트레이션 시스템과 결합된다.
클라우드 환경은 운영체제에 다음과 같은 요구를 강화했다.
- 대규모 다중 코어 지원
- 가상화 성능
- 네트워크와 저장소 처리량
- 자동화 가능한 관리 인터페이스
- 관찰 가능성과 원격 진단
- 실시간 보안 업데이트
- 자원 격리와 사용량 측정
- 장애 상황에서의 자동 복구
운영체제와 분산 시스템의 경계도 점차 흐려졌다. 단일 서버의 운영체제가 로컬 자원을 관리하는 동안, 클러스터 관리 시스템은 여러 서버의 CPU와 메모리, 저장소를 하나의 자원 풀처럼 배분한다.
보안 중심의 발전
초기 운영체제의 보안은 주로 여러 사용자의 파일과 프로세스를 분리하는 데 초점을 맞췄다. 컴퓨터가 인터넷과 연결되고 개인 정보와 금융, 산업 기반 시설을 처리하면서 운영체제 보안의 범위는 크게 확대되었다.
현대 운영체제는 다음과 같은 보안 기능을 발전시켰다.
- 사용자 모드와 커널 모드 분리
- 가상 주소 공간
- 접근 제어 목록
- 권한과 역할 기반 접근 제어
- 실행 방지와 주소 공간 배치 난수화
- 코드 서명과 보안 부팅
- 샌드박스
- 응용 프로그램 권한
- 디스크와 파일 암호화
- 무결성 검사
- 자동 보안 업데이트
- 하드웨어 보안 모듈과 신뢰 실행 환경
보안 취약점이 운영체제 전체의 권한과 연결될 수 있기 때문에 커널 코드와 장치 드라이버의 크기, 메모리 안전성, 공격 표면을 줄이려는 연구도 계속되었다.
현대 운영체제
현대의 운영체제는 과거의 여러 계보와 설계가 결합된 결과이다. 메인프레임 운영체제는 여전히 대규모 거래와 기업 데이터를 처리하며, UNIX와 BSD의 개념은 Linux와 macOS, 여러 서버 및 모바일 운영체제에 이어지고 있다. Windows NT 계열은 개인용 컴퓨터와 기업 환경을 폭넓게 지원하고, Linux는 서버와 클라우드, 슈퍼컴퓨터, 임베디드와 Android 장치의 기반으로 사용된다.
오늘날 운영체제는 다음 환경에서 서로 다른 형태로 사용된다.
- 개인용 컴퓨터
- 스마트폰과 태블릿
- 서버와 클라우드
- 메인프레임
- 슈퍼컴퓨터
- 게임 콘솔
- 자동차와 로봇
- 네트워크 장비
- 스마트 가전
- 산업 제어기
- 센서와 마이크로컨트롤러
- 가상 머신과 컨테이너
운영체제의 역사는 단순히 새로운 제품이 이전 제품을 대체한 과정이 아니다. 초기 배치 시스템에서 개발된 작업 관리, 시분할 시스템의 다중 사용자와 대화형 실행, Multics의 보호와 가상 메모리, UNIX의 단순한 인터페이스와 이식성, 개인용 컴퓨터의 그래픽 사용자 환경, 메인프레임의 가상화, 모바일의 샌드박스와 전력 관리가 현대 운영체제 안에 함께 남아 있다.
운영체제는 하드웨어의 발전과 함께 지속적으로 변화해 왔다. 중앙처리장치와 메모리, 저장 장치의 성능이 향상될수록 운영체제는 더 많은 추상화와 보호 기능을 제공했고, 컴퓨터가 네트워크와 일상생활에 깊이 연결될수록 보안과 격리, 전력 관리와 자동 업데이트의 중요성이 커졌다. 앞으로도 새로운 하드웨어 구조와 가상화, 분산 컴퓨팅, 보안 및 실시간 요구에 대응하면서 운영체제의 구조와 역할은 계속 변화할 것이다.
구조
운영체제는 하드웨어와 응용 프로그램 사이에서 자원을 관리하고 공통 실행 환경을 제공하는 여러 구성 요소의 집합이다. 운영체제의 중심에는 커널이 있으며, 그 주변에 시스템 라이브러리와 백그라운드 서비스, 파일 시스템, 장치 드라이버, 명령행 셸, 그래픽 사용자 인터페이스 및 관리 도구가 결합된다. 운영체제의 구체적인 구성과 각 요소가 실행되는 권한 수준은 운영체제의 설계에 따라 달라진다.
현대의 범용 운영체제는 일반적으로 실행 환경을 사용자 모드와 커널 모드로 분리한다. 응용 프로그램은 제한된 권한을 가진 사용자 모드에서 실행되고, 커널과 주요 장치 드라이버는 하드웨어와 전체 메모리에 접근할 수 있는 커널 모드에서 실행된다. Windows에서도 응용 프로그램은 사용자 모드에서, 핵심 운영체제 구성 요소는 커널 모드에서 실행되며, 일부 드라이버는 사용자 모드에서도 실행할 수 있다. [1]
응용 프로그램
↓
시스템 라이브러리와 런타임
↓
시스템 호출 인터페이스
↓
커널
├── 프로세스와 스레드 관리
├── CPU 스케줄링
├── 메모리 관리
├── 파일 시스템
├── 네트워크
├── 보안과 접근 제어
└── 장치 드라이버
↓
컴퓨터 하드웨어
사용자 모드와 커널 모드의 분리는 응용 프로그램이 임의로 물리 메모리를 수정하거나 하드웨어 장치를 직접 제어하지 못하도록 한다. 응용 프로그램이 파일을 열거나 메모리를 할당하고, 프로세스를 생성하거나 네트워크 데이터를 보내려면 운영체제가 제공하는 인터페이스를 통해 커널에 요청해야 한다. 커널은 요청의 유효성과 접근 권한을 검사한 뒤 필요한 작업을 수행한다.
운영체제의 모든 기능이 반드시 커널 내부에서 실행되는 것은 아니다. 파일 색인과 네트워크 설정, 로그인 관리, 인쇄, 그래픽 세션과 같은 기능은 사용자 공간에서 실행되는 시스템 서비스가 담당할 수 있다. 일부 운영체제는 장치 드라이버와 파일 시스템의 일부도 사용자 공간에서 실행하여 결함이 커널 전체에 미치는 영향을 줄인다.
운영체제의 구성 요소
운영체제는 일반적으로 다음과 같은 구성 요소로 이루어진다.
- 커널은 프로세서와 메모리, 인터럽트 및 하드웨어 자원을 관리한다.
- 시스템 호출 인터페이스는 사용자 프로그램이 커널의 기능을 요청하는 통로를 제공한다.
- 프로세스와 스레드 관리 체계는 실행 단위를 생성하고 종료하며 CPU 시간을 배분한다.
- 메모리 관리 체계는 물리 메모리와 가상 주소 공간을 관리한다.
- 파일 시스템은 파일과 디렉터리, 저장 장치의 데이터를 조직하고 접근 권한을 관리한다.
- 입출력 체계는 장치에 대한 요청을 버퍼링하고 예약하며 완료 결과를 전달한다.
- 장치 드라이버는 운영체제의 일반적인 입출력 요청을 장치별 명령으로 변환한다.
- 네트워크 스택은 네트워크 인터페이스와 통신 프로토콜, 소켓을 관리한다.
- 보안 체계는 사용자와 프로세스의 권한, 인증 및 자원 접근을 통제한다.
- 시스템 서비스는 커널 밖에서 로그인과 네트워크 설정, 장치 탐지 등의 운영 기능을 제공한다.
- 사용자 인터페이스는 명령행이나 그래픽 환경을 통해 사용자가 시스템을 조작할 수 있게 한다.
이러한 구성 요소의 경계는 운영체제마다 다르다. Windows의 커널 모드에는 낮은 수준의 스케줄링과 인터럽트 처리를 담당하는 커널뿐 아니라 객체, 메모리, 프로세스와 스레드, 입출력 및 설정을 관리하는 Executive 계층이 포함된다. [2]
Apple의 XNU 기반 운영체제에서는 Mach와 BSD, I/O Kit, 파일 시스템과 네트워크 구성 요소가 함께 커널 환경을 이룬다. 따라서 여기서 말하는 커널은 Mach 핵심 부분 하나만이 아니라 여러 커널 구성 요소의 결합을 가리킨다. [3]
커널 구조의 종류
운영체제는 기능을 커널 내부와 사용자 공간에 어떻게 배치하는지에 따라 여러 구조로 분류할 수 있다. 실제 운영체제는 하나의 구조를 순수하게 따르기보다 여러 방식의 특성을 결합하는 경우가 많다.
단일형 커널
단일형 커널은 프로세스 관리와 메모리 관리, 파일 시스템, 네트워크 및 많은 장치 드라이버를 하나의 커널 주소 공간에서 실행하는 구조이다.
각 구성 요소가 같은 권한과 주소 공간에서 직접 함수를 호출할 수 있으므로 구성 요소 사이의 통신 비용이 비교적 작다. 그러나 하나의 드라이버나 커널 구성 요소에서 발생한 메모리 오류가 다른 부분과 운영체제 전체에 영향을 미칠 수 있다.
Linux는 일반적으로 단일형 커널로 분류되지만, 실행 중 커널 모듈을 추가하거나 제거할 수 있는 모듈식 구조도 제공한다. 커널 모듈을 사용하면 모든 드라이버와 기능을 기본 커널 이미지에 포함하지 않고 필요에 따라 적재할 수 있다.
마이크로커널
마이크로커널은 커널 내부에 스케줄링과 주소 공간, 인터럽트, 프로세스 간 통신 같은 최소 기능만 두고, 파일 시스템과 네트워크, 장치 드라이버 등의 서비스를 사용자 공간 프로세스로 분리하는 구조이다.
서비스가 서로 다른 주소 공간에서 실행되므로 하나의 서비스가 실패하더라도 커널과 다른 서비스에 미치는 영향을 줄일 수 있다. 서비스의 교체와 재시작, 권한 분리에도 유리하다. 반면 서비스를 호출할 때 프로세스 간 메시지 전달과 주소 공간 전환이 필요하므로 설계와 구현에 따라 추가 비용이 발생할 수 있다.
MINIX 3와 QNX 등이 마이크로커널 계열의 대표적인 사례로 분류된다.
혼합형 커널
혼합형 커널은 마이크로커널의 계층적 구조를 일부 사용하면서도 성능이나 호환성을 위해 여러 운영체제 서비스를 커널 공간에서 실행하는 구조이다.
Windows NT 계열과 Apple의 XNU는 흔히 혼합형 커널로 분류된다. XNU는 Mach의 기능과 BSD 구성 요소, I/O Kit와 파일 시스템 및 네트워크 기능을 하나의 커널 환경 안에서 결합한다. [4]
혼합형이라는 명칭은 하나의 엄격한 구현 방식을 뜻하지 않는다. 어떤 기능을 커널에 넣고 어떤 기능을 사용자 공간에 둘지는 운영체제마다 다르다.
모듈형 커널
모듈형 커널은 커널의 핵심 기능과 추가 기능을 분리하고, 장치 드라이버와 파일 시스템 등의 기능을 실행 중 적재할 수 있는 모듈로 제공하는 구조이다.
모듈은 필요할 때만 메모리에 적재할 수 있으며, 새로운 하드웨어나 파일 시스템을 지원하기 위해 커널 전체를 다시 빌드하지 않아도 된다. 다만 커널 모드 모듈은 일반적으로 커널과 같은 권한으로 실행되므로 잘못된 모듈이 시스템 전체에 영향을 줄 수 있다.
엑소커널
엑소커널은 커널이 하드웨어 자원의 보호와 할당만 담당하고, 파일 시스템이나 네트워크 같은 고수준 추상화를 응용 프로그램 또는 라이브러리 운영체제에 맡기는 연구적 구조이다.
응용 프로그램은 자신의 목적에 맞는 자원 관리 방식을 선택할 수 있지만, 일반적인 응용 프로그램에 공통 환경을 제공하려면 별도의 라이브러리와 실행 체계가 필요하다. 엑소커널은 범용 상용 운영체제의 주류 구조라기보다 운영체제 추상화와 자원 관리에 관한 연구에서 중요한 의미를 가진다.
커널
커널은 운영체제에서 가장 높은 권한으로 실행되며 시스템의 핵심 자원을 관리하는 구성 요소이다. 프로세서의 동작 모드와 메모리, 인터럽트, 프로세스와 스레드, 입출력 장치 및 접근 권한을 통제한다.
Windows 문서에서는 커널이 스레드 스케줄링과 하드웨어 인터럽트 전달 같은 낮은 수준의 기본 연산을 제공하며, 운영체제의 다른 부분이 의존하는 중심 기능이라고 설명한다. [5]
커널의 주요 역할은 다음과 같다.
- 프로세스와 스레드를 생성하고 종료한다.
- 실행 가능한 스레드 가운데 CPU를 사용할 대상을 선택한다.
- 각 프로세스의 가상 주소 공간을 구성한다.
- 물리 메모리와 페이지 교체를 관리한다.
- 하드웨어 인터럽트와 예외를 처리한다.
- 파일과 소켓, 장치 등의 커널 객체를 관리한다.
- 사용자와 프로세스의 접근 권한을 검사한다.
- 프로세스 간 통신과 동기화 기능을 제공한다.
- 파일 시스템과 네트워크 스택을 실행한다.
- 장치 드라이버에 공통 입출력 기반을 제공한다.
커널은 부팅 과정에서 메모리 관리와 인터럽트, 프로세서와 장치를 초기화한 뒤 최초의 사용자 공간 프로세스를 시작한다. 이후 대부분의 응용 프로그램 코드는 사용자 모드에서 실행되며, 필요한 경우에만 시스템 호출과 인터럽트 등을 통해 커널로 제어가 이동한다.
커널 내부의 오류는 일반 응용 프로그램 오류보다 영향이 크다. 커널은 전체 주소 공간과 장치에 접근할 수 있으므로 잘못된 포인터와 동기화 오류, 드라이버 결함이 시스템 정지와 데이터 손상, 보안 취약점으로 이어질 수 있다. 이 때문에 커널 코드는 제한된 실행 환경과 엄격한 동기화, 입력 검증을 필요로 한다.
시스템 호출
시스템 호출은 사용자 공간의 프로그램이 커널이 제공하는 기능을 요청하는 공식적인 진입점이다. Linux 매뉴얼은 시스템 호출을 Linux 커널로 들어가는 진입점으로 정의하며, 일반적으로 응용 프로그램이 시스템 호출을 직접 실행하기보다 C 표준 라이브러리와 같은 라이브러리의 래퍼 함수를 통해 호출한다고 설명한다. [6]
대표적인 시스템 호출은 다음과 같은 기능을 제공한다.
- 파일 열기와 닫기
- 데이터 읽기와 쓰기
- 프로세스와 스레드 생성
- 프로그램 이미지 교체
- 가상 메모리 매핑
- 소켓 생성과 네트워크 통신
- 시간과 시스템 정보 조회
- 프로세스 간 통신
- 권한과 사용자 정보 변경
- 장치별 제어 요청
일반적인 시스템 호출 흐름은 다음과 같다.
응용 프로그램
↓
시스템 라이브러리 함수
↓
시스템 호출 번호와 인수 준비
↓
특수 명령으로 커널 모드 진입
↓
커널의 시스템 호출 처리기
↓
권한과 인수 검사
↓
커널 하위 시스템 실행
↓
결과 또는 오류 반환
↓
사용자 모드 복귀
각 시스템 호출에는 호출을 구별하는 번호나 내부 식별자가 있으며, 인수는 CPU 레지스터나 메모리를 통해 전달된다. 정확한 호출 규약은 CPU 아키텍처와 운영체제 ABI에 따라 다르다. Linux의 syscall() 함수는 시스템 호출 번호와 인수를 사용하여 대응하는 커널 호출을 실행할 수 있지만, 일반적인 프로그램은 각 시스템 호출에 대응하는 라이브러리 함수를 사용한다. [7]
시스템 호출과 일반 함수 호출은 역할이 다르다. 일반 함수 호출은 같은 권한 수준과 주소 공간 안에서 실행될 수 있지만, 시스템 호출은 사용자 공간과 커널 공간의 보호 경계를 넘는다. 이 과정에서는 실행 모드 전환과 인수 검사, 사용자 메모리 접근 검증이 필요하다.
Apple의 커널 문서도 사용자 공간과 커널 사이의 경계를 넘는 방식으로 BSD 시스템 호출과 ioctl, Mach 메시지, 메모리 매핑 등을 설명한다. 이 경계를 통과할 때는 성능 비용뿐 아니라 사용자 공간에서 전달된 데이터를 검증해야 하는 보안 문제가 발생한다. [8]
시스템 호출은 운영체제의 안정적인 사용자 공간 인터페이스를 구성한다. 내부 커널 구현이 변경되더라도 기존 응용 프로그램이 계속 실행되려면 시스템 호출의 번호와 인수, 결과 및 오류 규칙과 같은 ABI가 유지되어야 한다.
파일 시스템
파일 시스템은 저장 장치나 메모리, 네트워크 등의 데이터를 파일과 디렉터리 형태로 조직하고 이름, 위치, 크기, 소유자, 접근 권한과 시간 정보 등을 관리하는 체계이다.
응용 프로그램은 일반적으로 저장 장치의 물리적 섹터 위치를 직접 지정하지 않는다. 경로와 파일 이름을 사용하여 파일을 열고, 파일 내부의 논리적인 위치에서 데이터를 읽고 쓴다. 운영체제와 파일 시스템은 이러한 요청을 저장 장치의 블록과 입출력 명령으로 변환한다.
파일 시스템의 주요 기능은 다음과 같다.
- 파일과 디렉터리 생성 및 삭제
- 파일 이름과 경로 해석
- 파일 데이터 읽기와 쓰기
- 파일 크기와 위치 관리
- 사용자와 그룹별 접근 권한 관리
- 저장 공간 할당과 회수
- 파일과 디렉터리의 메타데이터 관리
- 저장 장치 연결과 마운트
- 캐시와 버퍼 관리
- 장애 이후 파일 시스템 복구
- 심벌릭 링크와 하드 링크 처리
- 메모리 매핑과 파일 잠금 지원
운영체제는 여러 종류의 파일 시스템을 하나의 공통 인터페이스로 제공할 수 있다. Linux의 가상 파일 시스템은 사용자 프로그램에 파일 시스템 인터페이스를 제공하고, 서로 다른 파일 시스템 구현이 커널 안에서 함께 사용될 수 있도록 추상화 계층을 형성한다. open, read, write, stat 등의 시스템 호출은 VFS를 거쳐 실제 파일 시스템 구현으로 전달된다. [9]
응용 프로그램
↓
open / read / write 등의 시스템 호출
↓
가상 파일 시스템
↓
실제 파일 시스템
├── ext4
├── XFS
├── Btrfs
├── FAT
├── NTFS
├── APFS
└── 네트워크 파일 시스템
↓
블록 장치 드라이버
↓
저장 장치
가상 파일 시스템 계층은 파일과 디렉터리, 마운트 지점 및 열린 파일을 공통 객체로 표현한다. 실제 파일 시스템은 공통 인터페이스에 맞는 연산을 구현하여 디스크 기반 파일 시스템과 메모리 파일 시스템, 네트워크 파일 시스템을 동일한 파일 API로 사용할 수 있게 한다.
파일 시스템은 영구 저장 장치만을 대상으로 하지 않는다. 운영체제는 메모리에만 존재하는 임시 파일 시스템과 프로세스 및 장치 정보를 파일처럼 보여주는 가상 파일 시스템도 제공할 수 있다. UNIX 계열 운영체제에서는 장치와 파이프, 소켓의 일부도 파일 서술자 인터페이스를 통해 다룰 수 있다.
파일 시스템은 성능과 데이터 안정성 사이에서 여러 정책을 사용한다. 쓰기 데이터를 즉시 장치에 반영하지 않고 메모리에 캐시하면 성능을 높일 수 있지만, 전원이 끊기기 전에 기록되지 않은 데이터가 손실될 수 있다. 저널링과 쓰기 순서 제어, 복사 후 쓰기 등의 기술은 비정상 종료 이후 파일 시스템의 일관성과 복구 가능성을 높이는 데 사용된다.
장치 드라이버
장치 드라이버는 운영체제와 특정 하드웨어 장치 사이의 통신을 담당하는 소프트웨어이다. 운영체제가 사용하는 일반적인 입출력 요청을 장치가 이해하는 레지스터 접근과 명령, 메모리 전송으로 변환하고, 장치에서 발생한 인터럽트와 완료 상태를 운영체제에 전달한다.
드라이버가 관리하는 장치에는 다음과 같은 것이 있다.
- 저장 장치
- 키보드와 마우스
- 그래픽 처리 장치
- 네트워크 인터페이스
- 오디오 장치
- USB 장치
- 카메라와 센서
- 프린터
- 가상 장치
- 시스템 버스와 전원 관리 장치
일반적인 장치 입출력 흐름은 다음과 같다.
응용 프로그램
↓
파일·소켓·그래픽 API
↓
시스템 호출
↓
운영체제의 입출력 관리자
↓
장치 드라이버
↓
장치 제어기와 하드웨어
↓
인터럽트 또는 완료 신호
↓
장치 드라이버
↓
요청한 프로세스에 결과 전달
드라이버는 장치의 초기화와 종료, 입출력 요청 큐, 인터럽트 처리, 직접 메모리 접근, 전원 관리, 오류 복구 및 장치 연결과 제거를 처리할 수 있다.
운영체제는 장치 종류마다 공통 드라이버 인터페이스를 제공할 수 있다. 예를 들어 저장 장치 드라이버는 파일 시스템과 블록 입출력 계층이 요구하는 공통 연산을 구현하고, 네트워크 드라이버는 네트워크 스택과 패킷을 주고받는 공통 인터페이스를 구현한다. 이를 통해 상위 운영체제 구성 요소가 각 장치의 모든 하드웨어 세부 사항을 알 필요가 없어진다.
Windows의 커널 모드 드라이버는 객체와 메모리, 프로세스와 스레드, 입출력 및 설정 관리 기능을 포함한 Executive 서비스를 사용할 수 있다. Windows는 장치와 드라이버를 계층적으로 연결하고 입출력 요청을 드라이버 스택에 전달하는 구조를 사용한다. [10]
드라이버가 반드시 커널 모드에서 실행되는 것은 아니다. 사용자 모드 드라이버는 제한된 권한에서 실행되므로 결함이 발생해도 운영체제 전체가 중단될 가능성을 줄일 수 있다. Windows는 일부 장치 드라이버를 사용자 모드에서 실행할 수 있으며, Apple은 macOS와 iPadOS에서 사용자 공간 장치 드라이버를 작성하기 위한 DriverKit을 제공한다. [11] [12]
그러나 인터럽트와 매우 낮은 지연 시간이 필요한 장치, 초기 부팅에 필요한 장치와 커널 내부 기능에 직접 연결되는 장치는 여전히 커널 모드 드라이버를 필요로 할 수 있다.
장치 드라이버는 커널과 하드웨어에 가까운 위치에서 실행되기 때문에 운영체제의 안정성과 보안에 큰 영향을 준다. 커널 모드 드라이버에서 발생한 잘못된 메모리 접근과 동기화 오류는 시스템 전체를 중단시킬 수 있다. 따라서 운영체제는 드라이버 서명과 권한 제한, 사용자 모드 드라이버, 입출력 메모리 관리 장치와 같은 방법으로 드라이버의 신뢰 경계를 강화한다.
구조 사이의 관계
커널과 시스템 호출, 파일 시스템 및 장치 드라이버는 독립된 기능이 아니라 하나의 요청을 처리하기 위해 연속적으로 동작한다.
응용 프로그램이 파일에서 데이터를 읽는 과정은 다음과 같이 진행될 수 있다.
1. 응용 프로그램이 read 함수를 호출한다.
2. 시스템 라이브러리가 시스템 호출을 실행한다.
3. CPU가 사용자 모드에서 커널 모드로 전환한다.
4. 커널이 파일 서술자와 접근 권한을 검사한다.
5. 가상 파일 시스템이 열린 파일과 실제 파일 시스템을 찾는다.
6. 실제 파일 시스템이 데이터가 저장된 블록을 계산한다.
7. 데이터가 캐시에 없으면 블록 입출력 요청을 생성한다.
8. 저장 장치 드라이버가 요청을 장치 명령으로 변환한다.
9. 장치가 데이터를 전송하고 완료 인터럽트를 발생시킨다.
10. 커널이 요청을 완료하고 데이터를 응용 프로그램에 반환한다.
11. CPU가 사용자 모드로 돌아간다.
Linux에서 read()는 열린 파일 서술자가 나타내는 파일로부터 데이터를 읽는 시스템 호출이며, 실제 전송 결과를 호출자에게 반환한다. [13]
이처럼 운영체제의 구조는 보호 경계와 공통 인터페이스를 통해 여러 하위 시스템을 연결하는 계층적 체계이다. 시스템 호출은 사용자 프로그램과 커널의 경계를 형성하고, 파일 시스템은 데이터를 파일이라는 공통 추상화로 제공하며, 장치 드라이버는 운영체제의 논리적인 요청을 실제 하드웨어 동작으로 변환한다.
운영체제마다 내부 계층과 명칭은 다르지만, 복잡한 하드웨어를 공통 인터페이스로 추상화하고 여러 프로그램이 안전하게 자원을 공유하도록 한다는 기본 목적은 공통적이다.
실행과 자원 관리
운영체제는 실행 중인 프로그램에 중앙처리장치와 메모리 등의 자원을 직접 넘겨주는 대신, 프로세스와 스레드, 가상 메모리 같은 추상화를 제공한다. 각 프로그램은 자신이 프로세서와 메모리를 독점적으로 사용하는 것처럼 동작하지만, 실제로는 운영체제가 제한된 물리 자원을 여러 실행 단위 사이에 분배하고 보호한다. 운영체제는 이러한 자원 가상화를 통해 여러 프로그램을 동시에 실행하고, 한 프로그램의 오류나 과도한 자원 사용이 다른 프로그램과 시스템 전체에 미치는 영향을 제한한다. 운영체제 이론에서도 CPU와 메모리의 가상화는 동시성 및 영속적 저장과 함께 핵심적인 구성 영역으로 다뤄진다.
프로그램 파일은 저장 장치에 기록된 정적인 코드와 데이터이지만, 프로세스는 해당 프로그램이 실제로 실행되고 있는 동적인 상태를 나타낸다. 운영체제는 프로그램을 메모리에 적재하고 실행 문맥과 주소 공간, 열린 파일, 접근 권한 등의 정보를 구성하여 프로세스를 생성한다. 하나의 프로그램 파일에서 여러 프로세스가 만들어질 수 있으며, 각 프로세스는 일반적으로 다른 프로세스와 구분되는 실행 상태와 자원을 가진다.
프로그램
↓ 실행
프로세스
├── 가상 주소 공간
├── 하나 이상의 스레드
├── 열린 파일과 입출력 상태
├── 사용자와 접근 권한
├── 신호와 예외 상태
└── 운영체제가 관리하는 각종 자원
프로세스와 스레드
프로세스는 운영체제가 실행 중인 프로그램과 그 자원을 관리하기 위해 사용하는 기본적인 격리 단위이다. 프로세스에는 실행할 코드뿐 아니라 전역 데이터와 힙, 스택, 메모리 매핑, 열린 파일, 현재 작업 디렉터리, 사용자 권한과 각종 커널 객체에 대한 참조가 포함될 수 있다.
운영체제는 각 프로세스를 식별하기 위한 번호와 상태 정보를 관리한다. 프로세스를 나타내는 구체적인 내부 자료 구조는 운영체제마다 다르지만 일반적으로 다음과 같은 정보가 필요하다.
- 프로세스 식별자
- 부모와 자식 프로세스 관계
- 프로세스의 실행 상태
- 가상 주소 공간 정보
- 소속된 스레드
- 열린 파일과 입출력 자원
- 사용자와 보안 자격
- 사용한 CPU 시간과 메모리
- 신호와 예외 처리 상태
- 스케줄링 및 자원 제한 정보
프로세스는 하나 이상의 스레드를 포함한다. 스레드는 프로세스 내부에서 실제 명령을 실행하며 운영체제의 스케줄러가 CPU 실행 시간을 배분하는 단위이다. 같은 프로세스에 속한 스레드는 일반적으로 코드와 전역 데이터, 힙, 열린 파일과 가상 주소 공간을 공유하지만, 각 스레드는 별도의 실행 문맥과 스택, 레지스터 상태를 가진다. Windows도 스레드를 프로세스 안에서 스케줄될 수 있는 실행 단위로 정의하며, 같은 프로세스의 스레드가 가상 주소 공간과 시스템 자원을 공유한다고 설명한다.
프로세스
├── 공유 코드
├── 공유 전역 데이터
├── 공유 힙
├── 공유 열린 파일
├── 공유 가상 주소 공간
│
├── 스레드 1
│ ├── 명령어 위치
│ ├── CPU 레지스터
│ └── 스택
│
├── 스레드 2
│ ├── 명령어 위치
│ ├── CPU 레지스터
│ └── 스택
│
└── 스레드 3
├── 명령어 위치
├── CPU 레지스터
└── 스택
하나의 프로세스 안에 여러 스레드를 사용하면 동일한 데이터와 자원을 공유하면서 여러 작업을 동시에 진행할 수 있다. 사용자 인터페이스를 처리하는 스레드와 파일을 읽는 스레드, 네트워크 요청을 처리하는 스레드를 분리할 수 있으며, 다중 코어 프로세서에서는 여러 스레드가 실제로 병렬 실행될 수도 있다.
스레드는 프로세스보다 많은 자원을 공유하므로 일반적으로 새로운 프로세스를 만드는 것보다 생성과 전환 비용이 작을 수 있다. 그러나 공유 메모리에 동시에 접근할 수 있기 때문에 경쟁 상태와 데이터 손상, 교착 상태가 발생할 수 있다. 이러한 문제를 제어하는 뮤텍스, 세마포어, 조건 변수와 원자적 연산은 ## 동시성과 통신에서 다룬다.
운영체제가 스레드를 구현하는 방법은 하나로 고정되지 않는다. 사용자 공간의 런타임이 여러 사용자 스레드를 소수의 커널 실행 단위에 대응시킬 수도 있고, 각 사용자 스레드를 하나의 커널 스레드와 대응시킬 수도 있다. 현대 Linux의 일반적인 POSIX 스레드 구현인 NPTL은 각 스레드를 커널이 스케줄할 수 있는 실행 단위에 일대일로 대응시키며, 스레드 생성에는 Linux의 clone 계열 기능을 이용한다.
프로세스와 스레드를 구분하는 정확한 경계도 운영체제마다 다를 수 있다. 일부 커널은 프로세스와 스레드를 완전히 다른 객체로 관리하지만, 다른 커널은 주소 공간과 파일 등의 자원을 얼마나 공유하는지에 따라 같은 실행 단위 구조를 다르게 구성하기도 한다. 따라서 프로세스는 자원과 보호의 단위이고 스레드는 실행과 스케줄링의 단위라는 설명은 일반적인 개념적 구분으로 이해해야 한다.
프로세스의 생성과 종료
새로운 프로세스는 기존 프로세스의 요청이나 시스템 초기화 과정에 따라 생성된다. 운영체제는 프로세스를 생성할 때 새로운 식별자와 주소 공간, 초기 스레드, 보안 정보와 커널 관리 구조를 준비한다.
UNIX 계열 운영체제에서는 전통적으로 기존 프로세스를 복제하는 fork와, 현재 프로세스가 실행할 프로그램을 교체하는 exec 계열 기능을 조합한다.
기존 프로세스
↓ fork
부모 프로세스 + 자식 프로세스
↓ exec
새로운 프로그램 실행
fork가 호출되면 자식 프로세스는 부모의 주소 공간과 여러 실행 속성을 논리적으로 물려받는다. 실제 메모리 전체를 즉시 복사하면 비용이 크므로, 현대 운영체제는 일반적으로 부모와 자식이 기존 물리 페이지를 공유하다가 어느 한쪽이 데이터를 수정할 때 해당 페이지를 복사하는 쓰기 시 복사를 사용할 수 있다. 다중 스레드 프로세스가 fork를 호출하면 새 프로세스에는 호출한 스레드만 복제되며, 기존 주소 공간과 동기화 객체의 상태를 물려받기 때문에 이후 사용할 수 있는 함수와 실행 순서에 주의해야 한다.
Windows 계열에서는 프로세스 생성 요청에서 실행할 프로그램과 초기 주소 공간, 프로세스 객체와 최초 스레드를 함께 구성하는 방식이 일반적이다. 이러한 API상의 차이에도 불구하고 운영체제가 새 주소 공간과 실행 문맥, 자원 정보를 구성한다는 핵심 역할은 같다.
프로세스가 정상적으로 작업을 마치거나 오류로 종료되면 운영체제는 해당 프로세스의 종료 상태를 기록하고, 메모리와 열린 파일, 커널 객체 등의 자원을 정리한다. 부모 프로세스가 자식의 종료 결과를 확인할 수 있는 체계도 제공될 수 있다.
프로세스가 종료된 직후에도 부모가 종료 상태를 회수할 때까지 최소한의 관리 정보가 남는 운영체제가 있다. UNIX 계열에서는 실행은 끝났지만 부모가 종료 상태를 아직 확인하지 않은 프로세스를 좀비 프로세스라고 한다. 반대로 부모가 먼저 종료된 자식 프로세스는 다른 시스템 프로세스가 관리 관계를 이어받을 수 있다.
프로세스와 스레드의 상태
운영체제는 프로세스와 스레드가 현재 실행 가능한지, 어떤 자원을 기다리는지를 추적한다. 세부 상태와 명칭은 운영체제마다 다르지만 일반적으로 다음과 같이 구분할 수 있다.
| 상태 | 의미 | | -- | ------------------------------ | | 생성 | 실행에 필요한 자료 구조와 자원을 준비하고 있음 | | 준비 | 실행할 수 있지만 CPU 할당을 기다리고 있음 | | 실행 | 현재 CPU에서 명령을 실행하고 있음 | | 대기 | 입출력이나 잠금, 시간 만료 등의 사건을 기다리고 있음 | | 중지 | 디버깅이나 작업 제어 등에 의해 실행이 정지됨 | | 종료 | 실행을 끝내고 자원 정리 또는 종료 상태 회수를 기다림 |
실행 중인 스레드는 시스템 호출이나 페이지 폴트, 입출력 요청, 동기화 객체의 대기 때문에 CPU를 계속 사용할 수 없는 상태가 될 수 있다. 이때 운영체제는 해당 스레드를 대기 상태로 바꾸고 다른 준비된 스레드에 CPU를 할당한다. 기다리던 사건이 완료되면 스레드는 다시 준비 상태로 이동한다.
생성
↓
준비 ───────────────┐
↓ 스케줄링 │ 선점
실행 ────────────────┘
│
├── 종료 → 종료 상태
│
└── 입출력·사건 대기
↓
대기
↓ 사건 완료
준비
프로세스 전체가 실행 또는 대기한다고 표현하기도 하지만, 다중 스레드 프로세스에서는 각 스레드의 상태가 서로 다를 수 있다. 한 스레드는 CPU에서 실행되고 다른 스레드는 네트워크 입력을 기다리며 또 다른 스레드는 잠금을 기다릴 수 있다.
CPU 자원의 분배
물리적인 CPU 코어는 한 시점에 제한된 수의 명령 흐름만 실행할 수 있다. 운영체제는 실행 가능한 스레드가 CPU 코어보다 많을 때 CPU 스케줄링을 통해 어떤 스레드를 언제 어느 코어에서 실행할지 결정한다.
운영체제는 짧은 시간 동안 여러 스레드를 번갈아 실행함으로써 각 프로그램이 지속적으로 진행되는 것처럼 보이게 한다. 다중 코어 환경에서는 일부 스레드가 서로 다른 코어에서 실제로 동시에 실행될 수 있다.
스케줄링은 다음과 같은 목표를 고려할 수 있다.
- CPU가 유휴 상태로 남는 시간을 줄인다.
- 단위 시간 동안 완료되는 작업 수를 늘린다.
- 대화형 프로그램의 응답 시간을 줄인다.
- 각 사용자와 작업에 공정하게 CPU 시간을 배분한다.
- 높은 우선순위 작업을 먼저 처리한다.
- 실시간 작업의 시간 제한을 만족시킨다.
- CPU 캐시와 메모리 접근의 지역성을 유지한다.
- 전력 소비와 발열을 조절한다.
- 여러 CPU 코어의 부하를 균형 있게 분배한다.
이러한 목표는 서로 충돌할 수 있다. 긴 계산 작업을 계속 실행하면 문맥 전환 비용은 줄어들지만 대화형 프로그램의 응답이 늦어질 수 있다. 반대로 실행 시간을 지나치게 짧게 나누면 응답성은 좋아질 수 있지만 문맥 전환이 자주 발생하여 실제 작업에 사용하는 CPU 시간이 줄어들 수 있다.
선점형 스케줄링
선점형 스케줄링에서는 실행 중인 스레드가 스스로 CPU를 반환하지 않아도 운영체제가 실행을 중단하고 다른 스레드로 전환할 수 있다. 일반적으로 하드웨어 타이머 인터럽트와 우선순위, 더 중요한 작업의 준비 상태 등에 따라 선점이 일어난다.
선점형 구조는 하나의 프로그램이 무한 반복하거나 장시간 계산하더라도 다른 프로그램이 CPU를 사용할 수 있게 한다. 현대의 범용 데스크톱과 서버 운영체제는 일반적으로 선점형 스케줄링을 사용한다.
비선점형 스케줄링에서는 현재 작업이 종료되거나 대기 상태로 들어가고, 또는 명시적으로 CPU를 양보할 때까지 계속 실행될 수 있다. 구현이 비교적 단순하지만 잘못된 작업 하나가 다른 작업의 실행을 오래 지연시킬 수 있다. 일부 제한된 임베디드 환경과 협력적 런타임에서는 이러한 방식이 사용될 수 있다.
시간 할당량과 우선순위
운영체제는 각 스레드에 일정한 시간 할당량을 주고, 할당량이 끝나면 다른 스레드로 전환할 수 있다. 모든 스레드가 같은 양의 CPU 시간을 받는 것은 아니며, 작업의 우선순위와 과거 사용량, 대기 시간 및 시스템 정책에 따라 실행 순서와 할당 시간이 달라질 수 있다.
높은 우선순위는 중요한 작업을 빠르게 실행하는 데 도움이 되지만, 높은 우선순위 작업이 계속 준비 상태로 존재하면 낮은 우선순위 작업이 거의 실행되지 못하는 기아 상태가 발생할 수 있다. 운영체제는 오래 기다린 작업의 우선순위를 높이거나 CPU 사용량에 따라 동적 우선순위를 조정하여 이를 완화할 수 있다.
실시간 스케줄링에서는 단순한 공정성보다 작업의 마감 시간이 중요하다. Linux의 SCHED_DEADLINE 정책은 실행 시간과 주기, 마감 시간에 관한 매개변수를 사용해 작업에 CPU 대역폭을 제공한다.
문맥 전환
운영체제가 실행 중인 스레드를 멈추고 다른 스레드를 실행하는 과정을 문맥 전환이라고 한다. 운영체제는 현재 스레드의 명령어 위치와 스택 포인터, CPU 레지스터 등의 실행 상태를 저장하고, 다음 스레드의 상태를 복원한다.
스레드 A 실행
↓ 상태 저장
스케줄러 실행
↓ 다음 스레드 선택
스레드 B 상태 복원
↓
스레드 B 실행
서로 다른 프로세스 사이를 전환할 때는 가상 주소 공간과 메모리 변환 정보도 바뀔 수 있다. 현대 CPU는 주소 변환 결과와 캐시 데이터를 내부에 저장하므로, 전환은 단순히 레지스터 몇 개를 바꾸는 것보다 큰 비용을 발생시킬 수 있다.
문맥 전환 자체는 프로그램의 실제 계산을 수행하지 않는다. 따라서 지나치게 잦은 전환은 전체 처리량을 낮출 수 있다. 그러나 여러 프로그램의 응답성과 공정성을 유지하기 위해 반드시 필요한 비용이기도 하다.
다중 코어와 프로세서 친화도
다중 코어 시스템에서는 스케줄러가 어떤 스레드를 실행할지만 아니라 어느 코어에서 실행할지도 결정해야 한다. 스레드를 이전에 실행하던 코어에 계속 배치하면 해당 코어의 캐시에 남아 있는 코드와 데이터를 재사용할 가능성이 높아진다. 이를 프로세서 친화도라고 한다.
반면 특정 코어에 작업이 몰리면 전체 CPU를 고르게 사용하지 못하므로 스케줄러는 부하를 다른 코어로 이동시킬 수 있다. NUMA 시스템에서는 메모리에 접근하는 비용이 CPU와 메모리의 물리적인 위치에 따라 달라지므로, 운영체제는 스레드를 해당 스레드가 사용하는 메모리와 가까운 프로세서에 배치하려 할 수 있다. Windows도 NUMA 환경에서 사용 중인 메모리와 같은 노드의 프로세서에 스레드를 배치하여 성능을 개선하려는 정책을 사용한다.
실행 문맥과 주소 공간
프로그램이 사용하는 주소는 일반적으로 물리 RAM의 실제 위치를 그대로 나타내지 않는다. 운영체제는 각 프로세스에 독립적인 가상 주소 공간을 제공하고, CPU의 메모리 관리 장치와 페이지 테이블을 이용해 가상 주소를 물리 주소로 변환한다.
프로세스의 가상 주소
↓
페이지 테이블
↓
물리 메모리의 실제 위치
각 프로세스는 자신만의 연속된 주소 공간을 가진 것처럼 동작할 수 있지만 실제 물리 메모리에서는 여러 위치에 분산되어 저장될 수 있다. Linux의 페이지 테이블도 CPU가 보는 가상 주소를 메모리 버스에서 사용하는 물리 주소로 대응시키는 계층적인 구조로 설명된다.
가상 주소 공간은 일반적으로 다음과 같은 영역으로 구성된다.
- 실행할 기계어 코드
- 읽기 전용 상수
- 초기화된 전역 데이터
- 초기화되지 않은 전역 데이터
- 동적으로 할당되는 힙
- 공유 라이브러리와 메모리 매핑 영역
- 각 스레드의 스택
- 운영체제와 아키텍처가 사용하는 예약 영역
높은 주소
┌──────────────────────┐
│ 스택 │
│ ↓ │
├──────────────────────┤
│ 메모리 매핑 영역 │
│ 공유 라이브러리 │
├──────────────────────┤
│ ↑ │
│ 힙 │
├──────────────────────┤
│ 전역·정적 데이터 │
├──────────────────────┤
│ 프로그램 코드 │
└──────────────────────┘
낮은 주소
이 그림은 일반적인 개념을 단순화한 것이며 실제 배치 방향과 주소 범위, 영역의 위치는 운영체제와 실행 파일 형식, 아키텍처 및 보안 정책에 따라 다르다.
가상 주소 공간의 중요한 목적은 프로세스 격리이다. 한 프로세스가 사용하는 가상 주소가 다른 프로세스의 같은 숫자 주소와 같더라도, 서로 다른 물리 페이지에 연결될 수 있다. 프로세스는 권한이 없는 다른 프로세스의 메모리에 일반적으로 직접 접근할 수 없다.
가상 주소는 격리 외에도 다음 기능을 지원한다.
- 실제 물리 메모리보다 큰 논리 주소 공간 제공
- 물리적으로 떨어진 메모리를 연속된 영역처럼 사용
- 파일을 주소 공간에 직접 매핑
- 여러 프로세스가 같은 코드와 데이터를 공유
- 메모리 영역별 읽기·쓰기·실행 권한 설정
- 쓰기 시 복사
- 필요할 때만 물리 메모리를 할당하는 지연 할당
- 주소 공간 배치 난수화를 통한 보안 강화
Windows에서도 각 프로세스는 독립적인 가상 주소 공간을 가지며 같은 프로세스의 모든 스레드가 해당 주소 공간에 접근할 수 있다.
가상 메모리와 페이징
가상 메모리는 프로세스가 사용하는 가상 주소와 실제 물리 메모리를 분리하는 관리 체계이다. 운영체제는 메모리를 일정한 크기의 페이지 단위로 관리하고, 가상 페이지를 물리 페이지 프레임에 대응시킨다.
프로세스가 어떤 가상 주소를 읽거나 쓰면 CPU는 페이지 테이블을 이용해 해당 주소가 연결된 물리 페이지를 찾는다. 주소 변환 속도를 높이기 위해 CPU는 최근 사용한 변환 결과를 변환 색인 버퍼에 저장할 수 있다.
가상 주소
↓
가상 페이지 번호 + 페이지 내부 위치
↓
페이지 테이블 조회
↓
물리 페이지 프레임 + 페이지 내부 위치
↓
물리 주소
페이지 테이블 항목에는 물리 페이지 위치 외에도 읽기와 쓰기, 실행 권한, 사용자 모드 접근 가능 여부, 현재 물리 메모리에 존재하는지 등의 상태가 포함될 수 있다.
운영체제는 프로세스가 요청한 가상 메모리 전체에 물리 메모리를 즉시 배정하지 않을 수 있다. 실제로 페이지에 처음 접근할 때 물리 메모리를 준비하는 방식을 요구 페이징이라고 한다. Linux의 메모리 관리 하위 시스템도 가상 메모리와 요구 페이징, 사용자 공간 및 커널 내부의 메모리 할당과 파일 매핑을 담당한다.
페이지 폴트
프로세스가 접근한 가상 페이지에 현재 유효한 물리 매핑이 없거나 접근 권한에 맞지 않으면 CPU는 페이지 폴트를 발생시킨다. 페이지 폴트는 항상 프로그램 오류를 뜻하지 않는다.
정상적인 페이지 폴트의 예는 다음과 같다.
- 처음 접근한 익명 메모리에 물리 페이지를 할당함
- 실행 파일이나 공유 라이브러리의 필요한 부분을 불러옴
- 메모리에 없는 파일 매핑 페이지를 읽음
- 쓰기 시 복사 페이지를 수정하려고 하여 별도 페이지를 만듦
- 저장 장치로 옮겨 둔 페이지를 다시 메모리로 불러옴
운영체제가 요청을 해결할 수 있으면 페이지 테이블을 갱신한 뒤 중단되었던 명령을 다시 실행한다. 존재하지 않는 주소나 쓰기 권한이 없는 읽기 전용 페이지에 접근하는 등 요청을 해결할 수 없다면 운영체제는 프로세스에 오류 신호나 예외를 전달하고 프로그램을 종료할 수 있다.
물리 메모리와 작업 집합
가상 주소 공간의 모든 페이지가 항상 물리 메모리에 존재하는 것은 아니다. 프로세스가 최근 실제로 사용하여 물리 메모리에 올라와 있는 페이지들의 집합을 작업 집합이라고 부를 수 있다. Windows에서도 작업 집합은 프로세스의 가상 주소 공간 중 최근 참조된 페이지의 집합이며, 공유 데이터와 비공유 데이터를 포함할 수 있다.
물리 메모리가 충분하면 자주 사용하는 페이지를 RAM에 유지하여 빠르게 접근할 수 있다. 메모리 압박이 커지면 운영체제는 덜 사용되는 페이지를 회수하거나 파일에서 다시 불러올 수 있는 페이지를 버리고, 필요하면 일부 익명 메모리를 저장 장치의 스왑 공간으로 옮길 수 있다.
저장 장치는 RAM보다 훨씬 느리므로 필요한 페이지가 계속 물리 메모리와 저장 장치 사이를 오가면 시스템 성능이 급격히 낮아질 수 있다. 작업에 필요한 페이지 집합이 실제 메모리보다 커서 페이지 교체가 반복되는 현상을 스래싱이라고 한다.
페이지 교체
물리 메모리가 부족할 때 운영체제는 어떤 페이지를 회수할지 결정해야 한다. 이론적인 페이지 교체 알고리즘에는 다음과 같은 방식이 있다.
- 먼저 들어온 페이지부터 교체하는 FIFO
- 가장 오랫동안 사용되지 않은 페이지를 선택하는 LRU
- 최근 접근 여부를 이용하는 Clock 계열 방식
- 앞으로 가장 늦게 사용될 페이지를 선택하는 최적 알고리즘
미래의 메모리 접근을 정확히 알 수 없으므로 최적 알고리즘은 주로 비교 기준으로 사용된다. 실제 운영체제는 페이지의 최근 접근과 변경 여부, 파일 기반인지 익명 메모리인지, 프로세스별 우선순위와 메모리 압박 등을 조합한 복잡한 정책을 사용한다.
운영체제가 파일에서 다시 읽어올 수 있는 변경되지 않은 페이지는 저장 장치에 별도로 기록하지 않고 버릴 수 있다. 반면 프로그램이 수정한 익명 페이지는 회수 전에 스왑 공간 등에 기록해야 할 수 있다.
메모리 할당
운영체제의 메모리 관리는 프로세스에 가상 주소 공간을 제공하는 작업과 실제 물리 페이지를 배정하는 작업으로 나뉜다. 프로그램 내부의 malloc, new 또는 언어 런타임 할당기는 운영체제로부터 큰 메모리 영역을 확보한 뒤 이를 더 작은 객체 단위로 나누어 사용할 수 있다.
응용 프로그램의 객체 할당 요청
↓
언어 런타임·메모리 할당기
↓ 필요할 때
운영체제에 가상 메모리 요청
↓
가상 주소 영역 예약
↓ 접근 시
물리 페이지 할당
따라서 프로그램에서 객체 하나를 만들 때마다 반드시 시스템 호출이 한 번씩 발생하는 것은 아니다. 일반적인 메모리 할당기는 운영체제에서 받은 영역을 재사용하고, 큰 할당이나 메모리 부족 상황에서만 운영체제에 추가 공간을 요청한다.
가상 주소 영역은 사용할 주소 범위만 예약한 상태와 실제 물리 저장 공간이 연결된 상태를 구분할 수 있다. 운영체제와 플랫폼에 따라 예약과 확정의 구체적인 의미는 다르지만, 이 구분을 통해 큰 연속 주소 공간을 확보하면서 실제 사용한 부분에만 물리 자원을 배정할 수 있다.
메모리 영역은 익명 메모리뿐 아니라 파일과 장치에도 연결될 수 있다. Linux의 mmap은 호출한 프로세스의 가상 주소 공간에 새로운 매핑을 생성한다. 파일을 매핑하면 프로그램은 일반적인 메모리 접근 명령을 통해 파일 데이터를 읽고 쓸 수 있으며, 운영체제가 페이지 캐시와 페이지 폴트를 이용해 실제 입출력을 처리한다.
공유 메모리
프로세스의 주소 공간은 기본적으로 분리되지만 운영체제는 명시적으로 같은 물리 페이지를 여러 프로세스의 가상 주소 공간에 연결할 수 있다. 이를 공유 메모리라고 한다.
프로세스 A 가상 주소 ─┐
├── 같은 물리 메모리
프로세스 B 가상 주소 ─┘
공유 메모리는 데이터를 별도의 복사 없이 여러 프로세스가 접근할 수 있어 대량 데이터 교환에 효율적이다. POSIX 공유 메모리도 공유 메모리 객체를 프로세스의 가상 주소 공간에 매핑하여 프로세스들이 같은 메모리 영역으로 정보를 주고받게 한다.
그러나 여러 프로세스가 같은 데이터를 동시에 변경하면 스레드 공유 메모리와 마찬가지로 경쟁 상태가 발생할 수 있다. 따라서 뮤텍스와 세마포어, 원자적 연산이나 명확한 단일 작성자 구조 등의 동기화가 필요하다.
프로그램 코드와 공유 라이브러리의 읽기 전용 페이지도 여러 프로세스가 물리적으로 공유할 수 있다. 각 프로세스에는 독립된 가상 주소로 보이지만 동일한 물리 페이지를 연결하면 메모리 사용량을 줄일 수 있다.
자원 제한과 사용량 관리
운영체제는 CPU와 메모리를 단순히 할당하는 것뿐 아니라 특정 사용자와 프로세스가 사용할 수 있는 자원의 양을 제한하고 측정할 수 있다.
관리할 수 있는 자원에는 다음이 포함될 수 있다.
- CPU 시간과 CPU 사용 비율
- 물리 및 가상 메모리
- 생성 가능한 프로세스와 스레드 수
- 열 수 있는 파일 수
- 디스크 사용량
- 입출력 대역폭
- 네트워크 대역폭
- 특정 CPU와 NUMA 노드 접근
- 실시간 스케줄링 권한
자원 제한은 한 프로그램이 메모리와 CPU를 독점하여 다른 프로그램을 방해하는 상황을 줄인다. 다중 사용자 서버와 컨테이너 환경에서는 사용자 또는 서비스별로 사용 가능한 자원을 분리하고 사용량을 측정하는 데 중요하다.
Linux의 제어 그룹은 프로세스를 계층적인 그룹으로 구성하고 CPU와 메모리, 입출력 등의 자원 사용을 제한하고 측정할 수 있도록 하는 커널 기능이다.
자원 제한을 초과했을 때의 동작은 자원 종류와 정책에 따라 다르다. 추가 메모리 할당이 실패하거나 작업의 CPU 실행 비율이 낮아질 수 있으며, 심각한 메모리 부족 상황에서는 운영체제가 일부 프로세스를 종료할 수도 있다.
실행과 자원 관리의 관계
프로세스와 스레드, CPU 스케줄링 및 메모리 관리는 서로 분리된 기능이 아니다. 운영체제는 하나의 스레드를 실행하기 위해 해당 스레드의 CPU 문맥과 프로세스 주소 공간을 준비해야 하며, 실행 중 접근한 메모리 페이지가 없으면 페이지 폴트를 처리해야 한다. 스레드가 입출력이나 메모리를 기다리면 다른 스레드에 CPU를 할당한다.
프로그램 실행 요청
↓
프로세스와 가상 주소 공간 생성
↓
초기 스레드 생성
↓
스케줄러의 준비 큐에 등록
↓
CPU 할당
↓
명령 실행과 가상 주소 변환
↓
페이지 폴트·입출력·시스템 호출 발생 가능
↓
필요하면 스레드 대기
↓
다른 스레드에 CPU 할당
↓
사건 완료 후 다시 준비 상태
운영체제는 이 과정을 반복하면서 제한된 CPU 코어와 물리 메모리를 여러 프로그램이 공유하도록 한다. 프로세스는 각 프로그램에 독립된 실행 환경을 제공하고, 스레드는 실제 실행 흐름을 나타내며, 스케줄러는 CPU 시간을 분배한다. 가상 메모리는 프로세스의 주소 공간을 서로 분리하면서 물리 메모리를 유연하게 배치하고 공유할 수 있게 한다.
이러한 추상화는 프로그램이 CPU 코어의 수와 실제 물리 페이지 위치, 다른 프로그램의 실행 상태를 일일이 제어하지 않고도 동작할 수 있게 한다. 반면 운영체제는 공정성과 응답성, 처리량, 메모리 사용량과 격리 사이에서 계속 정책적인 결정을 내려야 한다.
CPU 스케줄링 알고리즘과 페이지 교체 정책은 모든 환경에서 하나의 최적 해답을 가지지 않는다. 대화형 개인용 컴퓨터와 대규모 서버, 배터리로 동작하는 모바일 장치, 실시간 제어 시스템은 서로 다른 목표를 가진다. 따라서 실제 운영체제는 작업의 우선순위와 실행 특성, 하드웨어 구조 및 시스템 부하에 따라 여러 정책을 결합하여 실행과 자원을 관리한다.
동시성과 통신
운영체제는 여러 프로세스와 스레드가 같은 시간대에 실행될 수 있는 환경을 제공한다. 하나의 CPU 코어에서도 운영체제가 실행 대상을 빠르게 전환하면 여러 작업이 번갈아 진행될 수 있으며, 다중 코어 시스템에서는 여러 실행 흐름이 실제로 동시에 명령을 수행할 수 있다. 이러한 실행 관계를 동시성이라고 하며, 같은 순간에 여러 계산이 물리적으로 실행되는 경우는 병렬성이라고 구분할 수 있다.
동시성은 CPU와 입출력 장치를 효율적으로 사용하고 프로그램의 응답성을 높이는 데 필요하다. 웹 서버는 여러 클라이언트 요청을 함께 처리하고, 그래픽 프로그램은 화면 입력과 렌더링, 파일 입출력을 나누어 실행할 수 있다. 운영체제 내부에서도 여러 CPU와 장치, 인터럽트 처리기가 동시에 커널 자료 구조에 접근할 수 있다.
그러나 여러 실행 흐름이 데이터와 장치를 공유하면 실행 순서에 따라 결과가 달라지는 문제가 발생한다. 운영체제와 프로그램은 뮤텍스, 세마포어, 조건 변수, 원자적 연산과 메시지 전달 등의 수단을 이용하여 공유 자원에 대한 접근 순서를 조정한다. 프로세스가 서로 다른 주소 공간에 존재할 때는 프로세스 간 통신을 통해 데이터를 교환하고 실행 상태를 알린다. POSIX도 여러 실행 흐름이 같은 메모리 위치에 접근할 때 동기화 연산을 사용하여 읽기와 쓰기의 관계를 정하도록 요구한다.
동시에 실행되는 작업
↓
공유 자원 접근
↓
실행 순서와 가시성 조정
├── 잠금과 조건 변수
├── 세마포어와 이벤트
├── 원자적 연산
└── 메시지 전달
↓
안전한 데이터 공유와 통신
동시성과 병렬성
동시성은 둘 이상의 작업이 서로 겹치는 시간 동안 진행되는 구조이다. 작업이 반드시 같은 순간에 실행될 필요는 없다. 단일 코어에서도 운영체제가 작업을 번갈아 실행하면 한 작업이 입출력을 기다리는 동안 다른 작업이 계산을 수행할 수 있다.
병렬성은 둘 이상의 작업이 여러 CPU 코어나 연산 장치에서 실제로 같은 순간에 실행되는 상태이다.
동시성
CPU 1: 작업 A → 작업 B → 작업 A → 작업 C
병렬성
CPU 1: 작업 A ───────────────
CPU 2: 작업 B ───────────────
CPU 3: 작업 C ───────────────
동시성이 있다고 항상 병렬 실행되는 것은 아니며, 병렬 프로그램은 여러 작업의 동시성을 전제로 한다. 운영체제는 스케줄링을 통해 실행 가능한 스레드를 CPU에 배치하지만, 프로그램이 작업을 어떤 단위로 나누고 어떤 데이터를 공유하는지는 프로그램과 런타임이 결정한다.
운영체제 분야에서 동시성은 CPU 가상화와 영속적 저장과 함께 핵심적인 주제로 다뤄진다. 여러 실행 흐름이 공유 상태에 접근할 때 발생하는 오류는 단순한 스케줄링 문제가 아니라 프로그램의 정확성과 메모리 모델, 동기화 규칙의 문제이기도 하다.
공유 상태
같은 프로세스의 스레드는 일반적으로 가상 주소 공간과 전역 변수, 힙 객체와 열린 파일을 공유한다. 여러 프로세스도 공유 메모리나 파일, 커널 객체, 장치와 네트워크 연결을 통해 같은 상태에 접근할 수 있다.
공유 상태 자체가 잘못된 것은 아니다. 문제는 하나의 실행 흐름이 값을 읽거나 변경하는 동안 다른 실행 흐름이 같은 값에 접근할 수 있다는 점이다.
다음과 같은 증가 연산을 가정할 수 있다.
counter = counter + 1
소스 코드에서는 하나의 연산처럼 보이지만 실제 실행은 다음과 같은 여러 단계로 나뉠 수 있다.
1. counter의 값을 읽는다.
2. 읽은 값에 1을 더한다.
3. 계산 결과를 counter에 저장한다.
두 스레드가 counter의 값이 10인 상태에서 동시에 이 연산을 수행하면 다음과 같은 순서가 가능하다.
스레드 A: counter에서 10을 읽음
스레드 B: counter에서 10을 읽음
스레드 A: 11을 저장
스레드 B: 11을 저장
두 번 증가했지만 최종 결과는 12가 아니라 11이 된다. 각 스레드의 개별 연산은 정상적으로 실행되었지만 연산들이 서로 끼어들면서 전체 결과가 잘못된 것이다.
경쟁 상태
경쟁 상태는 여러 실행 흐름이 공유 상태에 접근하며, 구체적인 실행 순서나 시점에 따라 프로그램의 결과가 달라지는 상황이다.
경쟁 상태는 다음과 같은 자원에서 발생할 수 있다.
- 공유 변수와 자료 구조
- 파일의 현재 위치와 내용
- 메모리 할당기
- 프로세스와 스레드 관리 목록
- 장치의 명령 큐
- 네트워크 연결 상태
- 데이터베이스와 트랜잭션
- 파일 시스템의 메타데이터
- 커널의 스케줄링 자료 구조
경쟁 상태가 항상 눈에 보이는 오류를 즉시 일으키는 것은 아니다. 대부분의 실행에서는 예상대로 동작하다가 특정한 타이밍과 시스템 부하, CPU 수, 컴파일러 최적화에서만 나타날 수 있다. 이 때문에 동시성 오류는 재현과 진단이 어려운 경우가 많다.
공유 상태를 변경하는 모든 코드를 무조건 하나의 전역 잠금으로 보호하면 일부 경쟁 상태를 막을 수 있지만, 동시 실행의 장점을 잃고 잠금 경합과 교착 상태의 위험이 커진다. 따라서 어떤 상태를 공유해야 하는지, 어느 범위까지 하나의 원자적 작업으로 처리해야 하는지를 먼저 설계해야 한다.
임계 구역
임계 구역은 공유 자원의 일관성을 유지하기 위해 한 번에 제한된 수의 실행 흐름만 들어갈 수 있도록 보호해야 하는 코드 영역이다.
잠금 획득
↓
공유 데이터 읽기·수정
↓
잠금 해제
한 번에 하나의 스레드만 임계 구역에 들어갈 수 있도록 하는 성질을 상호 배제라고 한다. 임계 구역의 여러 명령이 다른 실행 흐름의 관점에서 중간 상태 없이 하나의 작업처럼 수행되도록 만드는 것이 목적이다.
상호 배제 알고리즘과 동기화 수단은 일반적으로 다음 조건을 고려한다.
- 동시에 둘 이상의 실행 흐름이 보호 영역에 들어가지 않아야 한다.
- 아무도 임계 구역을 사용하지 않을 때 진입 가능한 작업이 불필요하게 영원히 기다려서는 안 된다.
- 특정 실행 흐름이 계속 선택되지 못하는 기아 상태를 가능한 한 방지해야 한다.
- CPU를 낭비하는 대기 시간을 줄여야 한다.
- 인터럽트와 다중 코어 환경에서도 올바르게 동작해야 한다.
임계 구역을 너무 크게 잡으면 공유 자원의 안전성은 단순하게 확보할 수 있지만 다른 작업이 오래 기다려야 한다. 반대로 너무 작게 나누면 잠금 수가 늘고 여러 상태를 함께 변경해야 하는 연산의 정확성을 유지하기 어려워질 수 있다.
원자성
원자성은 하나의 작업이 외부에서 볼 때 더 작은 중간 단계로 나뉘지 않고 완전히 수행되거나 전혀 수행되지 않은 것처럼 보이는 성질이다.
일부 간단한 읽기와 쓰기는 하드웨어에서 원자적으로 수행될 수 있지만, 모든 변수 접근이 자동으로 원자적인 것은 아니다. 값의 크기와 정렬, 프로세서 아키텍처와 언어의 메모리 모델에 따라 달라질 수 있다. 또한 하나의 읽기와 하나의 쓰기가 각각 원자적이더라도 여러 연산을 조합한 전체 작업은 원자적이지 않을 수 있다.
CPU는 다음과 같은 원자적 연산을 제공할 수 있다.
- 값을 교환하는 연산
- 비교한 뒤 조건부로 값을 바꾸는 연산
- 원자적으로 값을 증가하거나 감소하는 연산
- 특정 비트를 설정하거나 제거하는 연산
운영체제와 런타임은 이러한 하드웨어 연산을 이용해 잠금과 세마포어, 참조 계수와 잠금 없는 자료 구조를 구현할 수 있다.
원자적 연산을 사용한다고 해서 동기화 문제가 모두 해결되는 것은 아니다. 서로 관련된 여러 변수의 일관성을 유지하거나 복잡한 자료 구조를 변경하려면 더 넓은 임계 구역과 메모리 순서 규칙이 필요할 수 있다.
메모리 가시성과 순서
다중 코어 프로세서에서는 각 코어가 캐시와 쓰기 버퍼를 사용하며, 컴파일러와 CPU는 성능을 위해 서로 독립적이라고 판단한 명령의 실행 순서를 바꿀 수 있다. 따라서 한 스레드가 메모리에 값을 쓴 직후 다른 스레드가 반드시 그 값을 같은 순서로 관찰한다고 단순하게 가정할 수 없다.
메모리 모델은 다음과 같은 사항을 정의한다.
- 어떤 읽기와 쓰기가 원자적인가
- 한 실행 흐름의 쓰기가 다른 실행 흐름에 언제 보이는가
- 컴파일러와 CPU가 연산을 어느 범위까지 재배치할 수 있는가
- 잠금과 원자적 연산이 어떤 순서 관계를 형성하는가
- 데이터 경쟁이 발생했을 때 프로그램의 의미가 무엇인가
잠금을 해제한 스레드가 변경한 내용은 같은 잠금을 이후 획득한 스레드에서 관찰될 수 있어야 한다. 이러한 관계는 상호 배제뿐 아니라 메모리 가시성을 제공한다. POSIX 동기화 규칙도 뮤텍스 연산 등의 동기화 작업을 통해 한 실행 흐름의 메모리 쓰기와 다른 실행 흐름의 읽기 사이에 순서를 형성한다.
단순히 변수를 반복해서 읽는 것만으로 다른 스레드의 변경을 안전하게 관찰할 수 있다고 보장할 수는 없다. 공유 상태에는 언어와 운영체제가 제공하는 원자적 연산이나 동기화 객체를 사용해야 한다.
뮤텍스
뮤텍스는 한 시점에 하나의 스레드만 소유할 수 있는 상호 배제 동기화 객체이다.
스레드 A: 잠금 획득 → 공유 자원 사용 → 잠금 해제
스레드 B: 대기 → 잠금 획득
뮤텍스를 획득하려는 시점에 다른 스레드가 이미 소유하고 있으면 호출한 스레드는 잠금이 해제될 때까지 대기할 수 있다. 구현에 따라 짧은 시간 동안 반복 검사한 뒤 잠들거나 처음부터 운영체제의 대기 상태로 들어갈 수 있다.
뮤텍스에는 일반적으로 다음 규칙이 적용된다.
- 잠금을 획득한 스레드가 소유자가 된다.
- 다른 스레드는 잠금이 해제될 때까지 보호 자원에 접근하지 않는다.
- 작업이 끝나면 소유자가 잠금을 해제한다.
- 모든 접근 경로가 같은 잠금 규칙을 따라야 한다.
Windows의 뮤텍스도 한 번에 하나의 스레드만 소유할 수 있어 공유 자원에 대한 상호 배제를 제공한다. 같은 이름의 동기화 객체를 여러 프로세스가 열면 프로세스 사이의 동기화에도 사용할 수 있다.
POSIX의 뮤텍스는 주로 같은 프로세스 안에서 메모리를 공유하는 스레드의 동기화에 사용되지만, 프로세스 공유 속성을 지정하고 해당 뮤텍스를 공유 메모리에 배치하면 여러 프로세스의 스레드가 함께 사용할 수도 있다.
잠금을 획득한 상태에서 오류나 조기 반환이 발생하면 잠금이 영원히 해제되지 않을 수 있다. 이를 방지하기 위해 언어와 라이브러리는 범위가 끝날 때 자동으로 잠금을 해제하는 구조를 제공할 수 있다.
스핀락
스핀락은 잠금을 얻을 수 있을 때까지 실행 흐름이 반복적으로 잠금 상태를 확인하는 동기화 수단이다.
while 잠금 획득 실패:
계속 확인
대기 중에도 CPU를 사용하므로 긴 시간 동안 유지되는 잠금에는 비효율적이다. 반면 잠금 보유 시간이 매우 짧고 현재 스레드를 잠들게 했다가 다시 깨우는 비용이 더 큰 경우에는 유리할 수 있다.
스핀락은 운영체제 커널과 다중 코어의 매우 짧은 임계 구역에서 사용될 수 있다. 인터럽트 문맥이나 스케줄링이 불가능한 상황에서는 일반적인 수면형 뮤텍스를 사용할 수 없기 때문이다.
단일 코어에서 현재 실행 중인 스레드가 잠금을 가진 다른 스레드를 기다리며 계속 회전하면, 잠금 소유자가 실행될 기회를 얻지 못할 수 있다. 따라서 스핀락의 사용 가능 여부는 선점과 인터럽트, CPU 수와 커널 실행 문맥을 고려해야 한다.
읽기-쓰기 잠금
읽기-쓰기 잠금은 여러 읽기 작업은 동시에 허용하지만 쓰기 작업은 배타적으로 수행하도록 하는 동기화 객체이다.
읽기 잠금
- 여러 스레드가 함께 획득 가능
쓰기 잠금
- 한 스레드만 획득 가능
- 읽기 잠금과 동시에 유지할 수 없음
자료를 읽는 작업이 대부분이고 변경이 드문 경우 일반 뮤텍스보다 동시성을 높일 수 있다. 그러나 잠금 관리가 더 복잡하고 읽기 작업이 계속 들어오면 쓰기 작업이 오래 기다리는 기아 상태가 발생할 수 있다. 반대로 쓰기 우선 정책은 읽기 작업의 지연을 늘릴 수 있다.
읽기 작업이 매우 짧거나 쓰기 비율이 높으면 읽기-쓰기 잠금의 추가 관리 비용 때문에 일반 뮤텍스보다 불리할 수 있다.
세마포어
세마포어는 정수 값을 이용하여 사용 가능한 자원의 수나 발생한 사건의 수를 나타내는 동기화 수단이다.
세마포어의 기본 연산은 다음과 같이 설명할 수 있다.
대기 연산
값이 0보다 크면 값을 1 감소하고 진행
값이 0이면 진행할 수 있을 때까지 대기
신호 연산
값을 1 증가
필요하면 대기 중인 실행 흐름을 깨움
초기값이 1인 세마포어는 하나의 자원에 대한 접근을 조정할 수 있지만, 뮤텍스와 완전히 같은 의미를 가지는 것은 아니다. 뮤텍스에는 소유자 개념이 있지만 세마포어는 일반적으로 대기한 실행 흐름과 신호를 보내는 실행 흐름이 달라도 된다.
초기값이 여러 개인 계수 세마포어는 동시에 사용할 수 있는 동일한 자원의 수를 제한할 수 있다.
사용 가능한 연결 4개
↓
세마포어 초기값 4
작업이 연결 사용
↓
값 감소
작업이 연결 반환
↓
값 증가
POSIX는 세마포어를 프로세스 또는 스레드 사이의 동기화에 사용할 수 있도록 정의한다. 이름 없는 세마포어는 공유 메모리에 배치할 수 있고, 이름 있는 세마포어는 독립적인 이름을 가진 운영체제 객체로 여러 프로세스에서 열 수 있다.
Windows의 세마포어도 0부터 정해진 최대값 사이의 계수를 유지하며, 동시에 공유 자원에 접근할 수 있는 스레드 수를 제한한다.
조건 변수
조건 변수는 특정한 상태 조건이 참이 될 때까지 스레드를 대기시키는 동기화 객체이다. 조건 변수는 일반적으로 공유 상태를 보호하는 뮤텍스와 함께 사용한다.
뮤텍스 획득
while 조건이 거짓:
조건 변수에서 대기
└── 대기 중 뮤텍스를 해제
└── 깨어난 뒤 뮤텍스를 다시 획득
공유 상태 사용
뮤텍스 해제
조건 변수에서 대기할 때는 상태를 확인하는 작업과 대기 상태로 들어가는 작업 사이에 신호를 놓치지 않도록 원자적인 전환이 필요하다. 대기 함수는 뮤텍스를 해제하면서 스레드를 잠들게 하고, 깨어날 때 다시 뮤텍스를 획득한다.
조건 변수의 신호는 상태 자체가 아니다. 스레드는 깨어난 뒤 보호된 조건을 다시 확인해야 한다. 다른 스레드가 먼저 상태를 소비했거나 조건과 무관하게 깨어나는 일이 발생할 수 있기 때문이다. POSIX도 조건 변수를 관련된 조건이 참이 될 때까지 스레드가 반복적으로 실행을 중단할 수 있는 동기화 객체로 정의한다.
POSIX 설계에서는 뮤텍스를 짧은 상호 배제에, 조건 변수를 비교적 길거나 불확정적인 조건 대기에 대응시키며, 두 요소를 함께 사용하는 방식을 기본적인 스레드 동기화 구조로 설명한다.
이벤트와 신호 객체
이벤트는 특정한 사건이 발생했음을 하나 이상의 대기 중인 실행 흐름에 알리는 동기화 객체이다.
이벤트는 상태가 다음 두 종류 가운데 하나일 수 있다.
비신호 상태
- 대기하는 스레드는 계속 기다림
신호 상태
- 대기하는 스레드가 실행 가능해짐
Windows는 수동 재설정 이벤트와 자동 재설정 이벤트를 제공한다. 수동 재설정 이벤트는 명시적으로 다시 비신호 상태로 바꿀 때까지 여러 대기 스레드를 깨울 수 있고, 자동 재설정 이벤트는 일반적으로 한 스레드를 깨운 뒤 자동으로 비신호 상태로 돌아간다.
이벤트는 다음과 같은 상황에서 사용할 수 있다.
- 비동기 입출력 완료 알림
- 작업 시작과 종료 알림
- 여러 작업의 초기화 완료 대기
- 종료 요청 전달
- 생산자와 소비자 사이의 상태 변경 알림
이벤트가 데이터 자체를 전달하는 것은 아니다. 실제 데이터는 공유 메모리나 큐 등에 저장하고, 이벤트는 해당 데이터를 사용할 수 있다는 사실만 알릴 수 있다.
장벽과 일회성 초기화
장벽은 여러 실행 흐름이 특정 지점에 모두 도착할 때까지 각각을 대기시키는 동기화 수단이다.
스레드 A ─── 장벽 도착 ─── 대기
스레드 B ───────── 장벽 도착 ─── 대기
스레드 C ───────────── 장벽 도착
↓
모두 다음 단계로 진행
병렬 계산을 여러 단계로 나누고, 이전 단계의 모든 작업이 끝난 뒤 다음 계산을 시작해야 할 때 사용할 수 있다.
일회성 초기화는 공유 객체나 전역 상태의 초기화가 프로그램 전체에서 정확히 한 번만 실행되도록 한다. 여러 스레드가 동시에 초기화 함수를 호출해도 하나의 스레드만 초기화를 수행하고 나머지는 완료를 기다린다.
단순히 초기화 완료 여부를 나타내는 논리값만 검사하면 검사와 초기화 사이에 경쟁 상태가 발생할 수 있으므로, 운영체제나 런타임이 제공하는 일회성 초기화 기능을 사용해야 한다.
생산자와 소비자
생산자-소비자 문제는 하나 이상의 생산자가 데이터를 만들고, 하나 이상의 소비자가 이를 처리하는 대표적인 동기화 문제이다.
생산자
↓ 데이터 추가
공유 버퍼
↓ 데이터 제거
소비자
버퍼의 크기가 제한되어 있다면 다음 조건을 조정해야 한다.
- 버퍼가 가득 차면 생산자가 기다린다.
- 버퍼가 비어 있으면 소비자가 기다린다.
- 여러 생산자와 소비자가 버퍼 구조를 동시에 손상시키지 않도록 한다.
일반적으로 뮤텍스로 버퍼 자체를 보호하고, 비어 있지 않음과 가득 차지 않음이라는 조건을 조건 변수로 기다릴 수 있다. 또는 빈 슬롯 수와 데이터 수를 나타내는 두 개의 세마포어를 사용할 수 있다. POSIX의 계수 세마포어도 생산자-소비자와 같은 자원 수 관리 문제에 잘 대응하는 동기화 수단으로 설명된다.
운영체제의 입출력 큐와 네트워크 패킷 큐, 작업 스케줄러와 로그 시스템에도 이와 유사한 구조가 사용된다.
교착 상태
교착 상태는 둘 이상의 실행 흐름이 서로가 보유한 자원을 기다리면서 누구도 더 진행할 수 없는 상태이다.
다음과 같은 상황을 예로 들 수 있다.
스레드 A
잠금 1 획득
잠금 2 대기
스레드 B
잠금 2 획득
잠금 1 대기
두 스레드는 상대방이 잠금을 해제하기를 기다리지만, 어느 쪽도 자신이 가진 잠금을 해제할 위치까지 진행할 수 없다.
교착 상태가 성립하는 조건은 일반적으로 다음과 같이 설명된다.
- 상호 배제: 자원을 한 번에 하나의 실행 흐름만 사용한다.
- 점유 대기: 자원을 가진 상태로 다른 자원을 기다린다.
- 비선점: 다른 실행 흐름이 가진 자원을 강제로 빼앗을 수 없다.
- 순환 대기: 여러 실행 흐름이 원형으로 서로의 자원을 기다린다.
네 조건 가운데 하나 이상을 제거하면 교착 상태를 방지할 수 있다.
대표적인 방지 방법은 다음과 같다.
- 모든 잠금을 정해진 전역 순서로 획득한다.
- 여러 자원을 한 번에 요청한다.
- 잠금 획득에 실패하면 이미 얻은 잠금을 해제하고 다시 시도한다.
- 잠금 대기에 시간 제한을 둔다.
- 공유 상태를 줄이고 메시지 전달을 사용한다.
- 하나의 잠금으로 관련 상태를 묶는다.
- 잠금 없는 자료 구조나 트랜잭션을 사용한다.
잠금 순서를 통일하는 방식은 순환 대기를 제거하는 실용적인 방법이다. OSTEP의 동시성 오류 분석도 여러 잠금을 서로 다른 순서로 획득하는 구조를 대표적인 교착 상태 원인으로 설명한다.
운영체제는 모든 사용자 프로그램의 교착 상태를 자동으로 해결하지 않는다. 데이터베이스나 일부 자원 관리 시스템은 대기 관계를 분석하여 교착 상태를 감지하고 작업 하나를 취소할 수 있지만, 일반적인 스레드 잠금에서는 프로그램이 예방과 회복 정책을 직접 설계해야 한다.
기아와 라이브락
기아 상태는 실행 가능한 작업이 존재하지만 다른 작업이 계속 우선되어 필요한 자원이나 CPU 시간을 사실상 얻지 못하는 상태이다.
높은 우선순위 작업이 지속적으로 들어오거나, 읽기 잠금이 계속 획득되어 쓰기 작업이 진행하지 못할 때 발생할 수 있다. 공정한 대기 순서와 우선순위 조정, 대기 시간이 긴 작업의 우선순위를 높이는 방식으로 완화할 수 있다.
라이브락은 여러 실행 흐름이 중단되지는 않았지만 서로의 동작에 반응하여 상태를 계속 변경할 뿐 실제 작업을 완료하지 못하는 상태이다.
스레드 A: 충돌을 피하려고 자원 양보
스레드 B: 동시에 자원 양보
스레드 A와 B: 다시 시도
같은 과정 반복
재시도 시간에 무작위 지연을 추가하거나 한쪽에 우선권을 부여하여 반복되는 충돌을 줄일 수 있다.
교착 상태에서는 실행 흐름이 기다리며 멈춰 있지만, 라이브락에서는 계속 실행하면서도 진전하지 못한다는 차이가 있다.
우선순위 역전
우선순위 역전은 높은 우선순위 작업이 낮은 우선순위 작업이 보유한 자원을 기다리고, 그 사이 중간 우선순위 작업이 낮은 우선순위 작업의 실행을 방해하여 높은 우선순위 작업이 오래 지연되는 상황이다.
낮은 우선순위 작업
잠금 보유
높은 우선순위 작업
같은 잠금을 기다림
중간 우선순위 작업
낮은 우선순위 작업보다 먼저 계속 실행
이를 완화하기 위해 낮은 우선순위 잠금 소유자에게 일시적으로 높은 우선순위를 부여하는 우선순위 상속이나, 잠금을 획득했을 때 미리 정해진 우선순위로 올리는 우선순위 상한 방식이 사용될 수 있다. POSIX 뮤텍스 속성에도 우선순위 상속과 상한을 위한 동기화 특성이 포함될 수 있다.
실시간 운영체제에서는 우선순위 역전이 작업의 마감 시간을 위반할 수 있으므로 특히 중요하다.
잠금 경합과 확장성
여러 실행 흐름이 같은 잠금을 자주 획득하려 하면 잠금 경합이 발생한다. 한 번에 하나만 임계 구역을 사용할 수 있으므로 CPU 코어를 늘려도 성능이 더 이상 향상되지 않거나 오히려 감소할 수 있다.
경합을 줄이는 방법은 다음과 같다.
- 공유 상태를 여러 부분으로 나누고 각각 별도 잠금을 사용한다.
- 읽기 전용 데이터와 불변 객체를 활용한다.
- 스레드별 또는 CPU별 데이터를 사용한 뒤 나중에 결합한다.
- 임계 구역 안에서 입출력이나 긴 계산을 수행하지 않는다.
- 자료 구조의 소유자를 하나로 정하고 메시지로 작업을 요청한다.
- 원자적 연산이나 잠금 없는 알고리즘을 사용한다.
- 복사 후 쓰기와 버전 기반 읽기를 사용한다.
잠금 수를 늘리면 경합은 줄어들 수 있지만 잠금 순서와 데이터 일관성 관리가 복잡해진다. 따라서 잠금의 세분성은 성능과 정확성 사이의 설계 선택이다.
잠금 없는 구조
잠금 없는 알고리즘은 전통적인 뮤텍스로 모든 접근을 직렬화하지 않고 원자적 연산을 이용해 여러 실행 흐름이 자료 구조를 변경할 수 있게 한다.
잠금 없는 구조는 한 작업이 중단되더라도 다른 작업이 계속 진행할 수 있는 성질을 목표로 할 수 있다. 더 강한 보장으로 모든 작업이 제한된 단계 안에 완료되는 대기 없는 구조도 연구된다.
잠금 없는 방식은 다음과 같은 장점을 가질 수 있다.
- 잠금 소유자가 중단되어 전체가 멈추는 상황을 줄인다.
- 일부 높은 경합 환경에서 확장성을 높일 수 있다.
- 우선순위 역전과 전통적인 교착 상태를 피할 수 있다.
반면 구현과 검증이 매우 어렵고 다음 문제를 고려해야 한다.
- 메모리 순서
- 제거된 객체의 안전한 회수
- 같은 값이 변경되었다가 돌아오는 ABA 문제
- 재시도에 따른 기아
- 아키텍처별 원자적 명령의 차이
따라서 일반적인 프로그램에서는 검증된 동시성 자료 구조와 운영체제·런타임 라이브러리를 사용하는 편이 안전하다.
프로세스 간 통신
프로세스는 서로 분리된 주소 공간을 가지므로 한 프로세스가 다른 프로세스의 일반 메모리를 직접 읽고 쓸 수 없다. 프로세스가 데이터를 교환하거나 작업의 시작과 완료를 알리려면 운영체제가 제공하는 프로세스 간 통신 수단을 사용해야 한다. 이를 IPC라고도 한다.
프로세스 간 통신은 다음 기능 가운데 하나 이상을 제공한다.
- 바이트 스트림 전송
- 메시지 단위 전송
- 메모리 영역 공유
- 사건과 상태 알림
- 자원 접근의 동기화
- 프로세스 사이의 권한과 신원 전달
- 같은 컴퓨터 또는 네트워크의 원격 시스템과 통신
대표적인 통신 수단은 다음과 같다.
| 방식 | 주요 특성 |
|---|---|
| 파이프 | 일반적으로 단방향 바이트 스트림 |
| 이름 있는 파이프 | 이름을 통해 관계없는 프로세스도 연결 가능 |
| 메시지 큐 | 메시지 경계를 유지하여 커널 큐에 저장 |
| 공유 메모리 | 같은 물리 메모리를 여러 프로세스에 매핑 |
| 소켓 | 로컬 또는 네트워크 통신 |
| 신호 | 제한된 사건 알림 |
| 파일 | 영속적이고 단순하지만 동기화와 성능 고려 필요 |
| 메모리 매핑 | 파일 또는 공유 객체를 주소 공간에 연결 |
| 운영체제별 객체 | 포트, 이벤트, 메일슬롯 등의 플랫폼 기능 |
어떤 방식을 사용할지는 전송할 데이터의 크기와 구조, 지연 시간, 양방향성, 보안, 프로세스 수명과 네트워크 확장 가능성에 따라 달라진다.
파이프
파이프는 한 프로세스가 쓴 바이트를 다른 프로세스가 순서대로 읽을 수 있게 하는 커널 통신 객체이다.
프로세스 A
↓ 쓰기
파이프 버퍼
↓ 읽기
프로세스 B
전통적인 익명 파이프는 일반적으로 부모와 자식처럼 파일 서술자를 상속하거나 전달할 수 있는 관계의 프로세스 사이에서 사용된다. 셸의 파이프라인도 한 프로그램의 표준 출력을 다음 프로그램의 표준 입력에 연결한다.
프로그램 A | 프로그램 B | 프로그램 C
파이프는 일반적으로 바이트 스트림이므로 여러 번 쓴 데이터의 경계가 읽을 때 그대로 보존된다고 가정해서는 안 된다. 통신 프로토콜이 필요하면 길이 정보나 구분자를 직접 포함해야 한다.
파이프 버퍼가 가득 차면 쓰기 작업이 기다릴 수 있고, 데이터가 없으면 읽기 작업이 기다릴 수 있다. 모든 쓰기 끝이 닫히면 읽는 쪽은 파일 끝을 확인할 수 있으며, 읽는 프로세스가 모두 사라진 파이프에 쓰려고 하면 오류나 신호가 발생할 수 있다.
이름 있는 파이프
이름 있는 파이프는 파일 시스템 경로나 운영체제 이름 공간에 이름을 가지므로 부모·자식 관계가 없는 프로세스도 같은 이름을 통해 연결할 수 있다. UNIX 계열에서는 FIFO라고도 한다.
이름은 연결 지점을 제공하지만 실제 데이터는 일반 파일처럼 영구 저장되지 않고 커널의 파이프 버퍼를 통해 전달된다.
Windows의 이름 있는 파이프는 로컬 프로세스뿐 아니라 설정에 따라 네트워크상의 클라이언트와 서버 통신에도 사용할 수 있고, 양방향 메시지 또는 바이트 기반 통신을 구성할 수 있다. Windows IPC 체계는 파이프와 파일 매핑, 메일슬롯 등 여러 통신 수단을 제공한다.
메시지 큐
메시지 큐는 송신자가 보낸 데이터를 메시지 단위로 운영체제의 큐에 저장하고 수신자가 이를 꺼내는 방식이다.
송신 프로세스
↓ 메시지
┌───────────────────────┐
│ 메시지 1 │
│ 메시지 2 │
│ 메시지 3 │
└───────────────────────┘
↓
수신 프로세스
파이프의 바이트 스트림과 달리 각 메시지의 경계를 유지할 수 있다. 일부 구현에서는 메시지 우선순위와 비동기 알림, 시간 제한을 지원한다.
송신자와 수신자가 같은 시점에 실행 중일 필요가 없다는 장점이 있지만, 큐의 저장 용량은 제한되어 있고 큰 데이터를 전송하면 사용자 공간과 커널 공간 사이의 복사 비용이 발생할 수 있다.
Linux에서는 POSIX 메시지 큐와 System V 메시지 큐 등의 IPC 체계가 제공되며, POSIX 메시지 큐 서술자는 파일 서술자 기반 감시 체계와 결합할 수 있는 구현 특성을 가진다.
공유 메모리
공유 메모리는 같은 물리 메모리 영역을 여러 프로세스의 가상 주소 공간에 매핑하는 통신 방식이다.
프로세스 A 주소 공간 ─┐
├── 공유 물리 페이지
프로세스 B 주소 공간 ─┘
데이터를 파이프나 메시지 큐를 통해 반복해서 복사하지 않아도 되므로 대량 데이터와 낮은 지연 시간이 필요한 통신에 유리하다.
그러나 운영체제가 메모리 영역을 공유하도록 연결해 줄 뿐, 그 안의 자료 구조가 자동으로 안전해지는 것은 아니다. 여러 프로세스가 같은 데이터를 변경한다면 프로세스 공유 뮤텍스와 세마포어, 원자적 연산 등을 사용해야 한다.
Windows의 파일 매핑도 같은 파일 또는 페이지 파일 기반 영역을 여러 프로세스의 주소 공간에 연결하여 공유 메모리로 사용할 수 있다. Microsoft 문서는 파일 매핑이 효율적인 로컬 IPC 방식이지만 데이터 손상을 방지하려면 프로세스 사이에서 세마포어 등의 동기화가 필요하다고 설명한다.
공유 메모리 통신에서는 다음 사항을 별도로 정해야 한다.
- 메모리 영역의 자료 구조
- 초기화 담당 프로세스
- 데이터의 소유권
- 읽기와 쓰기 순서
- 프로세스가 비정상 종료했을 때의 복구
- 구조 버전과 크기 호환성
- 접근 권한
- 캐시와 메모리 순서
프로세스 하나가 공유 잠금을 가진 채 종료하면 다른 프로세스가 영원히 기다릴 수 있다. 일부 운영체제와 동기화 API는 소유자 종료를 감지할 수 있는 강건한 뮤텍스 등의 기능을 제공하지만, 공유 자료 구조 자체의 일관성을 복원하는 책임은 프로그램에 남는다. POSIX도 뮤텍스 소유 스레드가 종료되는 경우를 뮤텍스 종류와 복구 정책에 따라 별도로 다룬다.
소켓
소켓은 두 통신 끝점 사이에서 데이터를 주고받기 위한 인터페이스이다. 같은 컴퓨터의 프로세스뿐 아니라 네트워크를 통해 다른 컴퓨터의 프로세스와도 통신할 수 있다.
주요 소켓 유형은 다음과 같다.
- 연결 지향 바이트 스트림
- 연결 없는 데이터그램
- 로컬 도메인 소켓
- 네트워크 도메인 소켓
로컬 통신
프로세스 A
↓
로컬 소켓
↓
프로세스 B
네트워크 통신
클라이언트
↓ 네트워크
서버 소켓
↓
서버 프로세스
로컬 소켓은 같은 시스템 안에서 통신하면서 프로세스의 자격 정보나 파일 서술자와 같은 운영체제 자원을 전달할 수 있는 플랫폼도 있다. 네트워크 소켓은 TCP와 UDP 등의 프로토콜을 통해 원격 통신을 제공한다.
소켓은 범용성이 높지만 응용 프로그램이 메시지 형식과 직렬화, 부분 읽기와 쓰기, 연결 종료, 오류와 재연결을 처리해야 한다.
신호
신호는 운영체제나 다른 프로세스가 프로세스에 비동기적인 사건을 알리는 수단이다.
신호는 다음과 같은 상황을 나타낼 수 있다.
- 사용자의 중단 요청
- 잘못된 메모리 접근
- 자식 프로세스 종료
- 타이머 만료
- 터미널 연결 상태 변화
- 프로세스 종료 요청
신호는 전달할 수 있는 정보가 제한적이고, 신호 처리기는 프로그램의 임의 지점에서 실행될 수 있으므로 일반적인 함수와 자료 구조를 자유롭게 사용할 수 없다. 복잡한 데이터를 전달하는 통신 수단보다는 사건 알림과 제어에 적합하다.
안전한 구조에서는 신호 처리기가 최소한의 상태만 변경하거나 파일 서술자 기반 이벤트 루프에 사건을 전달하고, 실제 처리는 일반 실행 문맥에서 수행하도록 할 수 있다.
파일과 메모리 매핑
일반 파일도 프로세스 사이의 통신 수단으로 사용할 수 있다. 한 프로세스가 데이터를 기록하고 다른 프로세스가 이를 읽는 방식은 단순하고 프로그램이 종료된 뒤에도 데이터가 남는다는 장점이 있다.
그러나 여러 프로세스가 같은 파일을 변경한다면 다음 사항을 고려해야 한다.
- 파일 잠금
- 원자적인 이름 변경
- 쓰기 순서와 버퍼 플러시
- 부분적으로 기록된 데이터
- 파일 형식 버전
- 장애 복구
메모리 매핑을 사용하면 파일 내용을 프로세스의 주소 공간에 연결하여 일반적인 메모리 접근으로 읽고 쓸 수 있다. 여러 프로세스가 같은 파일 영역을 공유 매핑하면 공유 메모리와 비슷한 방식으로 통신할 수 있다.
파일 기반 방식은 영속성이 필요하거나 프로세스가 동시에 실행되지 않아도 되는 통신에 적합하지만, 실시간으로 작은 메시지를 교환하는 데는 파이프와 소켓보다 복잡할 수 있다.
원격 프로시저 호출
원격 프로시저 호출은 다른 프로세스나 컴퓨터에서 실행되는 함수를 로컬 함수처럼 호출할 수 있도록 통신을 추상화하는 방식이다.
클라이언트 함수 호출
↓
인수 직렬화
↓
프로세스 또는 네트워크 전송
↓
서버 함수 실행
↓
결과 직렬화
↓
클라이언트에 반환
실제로는 네트워크 지연과 연결 실패, 서버 종료, 메시지 중복과 시간 초과가 발생할 수 있으므로 일반적인 로컬 함수 호출과 같은 의미를 가질 수 없다.
운영체제와 시스템 서비스는 내부 구성 요소 사이의 통신에 RPC와 유사한 메시지 기반 인터페이스를 사용할 수 있다. 마이크로커널 구조에서는 파일 시스템과 장치 서비스가 별도 프로세스로 실행되므로 효율적인 IPC가 운영체제 성능에 중요한 영향을 준다.
통신과 동기화의 관계
통신과 동기화는 서로 별개의 기능처럼 보이지만 실제로는 밀접하게 연결된다.
공유 메모리는 빠르게 데이터를 교환할 수 있지만 데이터 접근을 조정하는 동기화 객체가 필요하다. 반대로 파이프와 메시지 큐는 데이터 전달 자체가 일정한 동기화 효과를 가진다. 수신자는 데이터가 도착할 때까지 기다릴 수 있고, 송신자는 큐에 공간이 생길 때까지 기다릴 수 있다.
공유 메모리 방식
데이터 공유
+
뮤텍스·세마포어·이벤트
메시지 전달 방식
메시지 전송
=
데이터 이동 + 사건 알림
공유 메모리는 높은 성능과 유연성을 제공하지만 별칭과 수명, 동기화를 직접 관리해야 한다. 메시지 전달은 각 작업의 상태를 더 명확하게 분리할 수 있지만 데이터 복사와 직렬화, 큐 관리 비용이 발생할 수 있다.
일부 시스템은 실제 데이터는 공유 메모리에 두고 파이프나 이벤트, 소켓으로 새 데이터의 위치와 준비 상태만 전달하는 혼합 구조를 사용한다.
블로킹과 비블로킹
동기화와 통신 연산은 작업을 완료할 수 없을 때 호출한 실행 흐름을 기다리게 할 수 있다. 이를 블로킹 방식이라고 한다.
읽을 데이터 없음
↓
스레드를 대기 상태로 전환
↓
데이터 도착
↓
스레드 다시 실행
비블로킹 연산은 즉시 처리할 수 없으면 대기하지 않고 오류나 현재 상태를 반환한다. 프로그램은 나중에 다시 시도하거나 여러 통신 객체의 준비 상태를 함께 감시할 수 있다.
비블로킹 방식은 하나의 스레드가 많은 연결을 관리하는 이벤트 기반 프로그램에 유용하지만, 상태 기계와 부분 완료, 오류 처리가 복잡해질 수 있다. async와 await 같은 비동기 문법은 비블로킹 입출력의 상태 전환을 순차 코드와 비슷한 형태로 표현하도록 돕는다.
블로킹은 반드시 CPU를 계속 소비한다는 뜻이 아니다. 일반적인 운영체제 대기는 해당 스레드를 스케줄링 대상에서 제외하고 사건이 발생하면 다시 깨운다. 반대로 스핀 대기는 실행 흐름이 CPU에서 상태를 계속 확인한다.
이벤트 기반 동시성
이벤트 기반 프로그래밍에서는 하나 또는 소수의 스레드가 여러 입출력 객체의 사건을 기다리고, 준비된 사건에 대응하는 처리기를 실행한다.
이벤트 루프
↓
입출력 준비 상태 대기
↓
준비된 연결 확인
↓
해당 처리기 실행
↓
다시 대기
많은 네트워크 연결이 대부분 대기 상태에 있을 때 연결마다 스레드를 하나씩 만드는 비용을 줄일 수 있다. 그러나 하나의 처리기가 오랫동안 CPU를 사용하면 다른 사건의 처리가 지연되므로 긴 계산은 별도 작업자에게 넘겨야 한다.
이벤트 기반 구조는 다중 스레드에서 발생하는 공유 상태와 잠금 문제를 줄일 수 있지만, 작업의 상태를 여러 콜백과 사건에 걸쳐 관리해야 하는 어려움이 있다. OSTEP도 이벤트 기반 동시성을 스레드와 잠금 중심 방식에서 발생하는 경쟁 상태와 교착 상태의 일부를 피하기 위한 대안으로 다룬다.
운영체제 내부의 동시성
동시성 문제는 사용자 프로그램에만 존재하지 않는다. 운영체제 커널은 다음과 같은 여러 실행 문맥에서 동시에 실행될 수 있다.
- 서로 다른 CPU에서 실행되는 시스템 호출
- 커널 스레드
- 하드웨어 인터럽트 처리기
- 지연 처리 작업
- 장치 완료 콜백
- 프로세스 종료와 자원 정리
- 타이머와 스케줄러
커널은 프로세스 목록과 페이지 테이블, 파일 시스템 캐시, 네트워크 큐, 장치 상태 등의 공유 자료 구조를 보호해야 한다. 커널 문맥에서는 잠들 수 있는지, 인터럽트를 허용하는지, 현재 CPU에서 선점될 수 있는지에 따라 사용할 수 있는 동기화 수단이 달라진다.
인터럽트 처리기가 접근하는 데이터를 일반 스레드의 뮤텍스로 보호하면, 인터럽트 문맥에서는 잠들 수 없어 문제가 될 수 있다. 따라서 짧은 스핀락과 인터럽트 비활성화, 원자적 연산 및 작업의 지연 처리 같은 커널 전용 방식이 사용된다.
잠금을 가진 상태에서 사용자 공간의 데이터나 느린 장치 응답을 기다리면 다른 커널 작업을 장시간 막을 수 있으므로, 커널 동기화에서는 잠금 범위와 실행 문맥이 특히 중요하다.
프로세스 간 통신의 보호
IPC 객체도 운영체제가 관리하는 자원이므로 아무 프로세스나 접근할 수 있어서는 안 된다. 운영체제는 사용자와 그룹, 접근 제어 목록, 이름 공간과 보안 식별자를 통해 IPC 객체의 접근 권한을 제한할 수 있다.
검사 대상에는 다음이 포함될 수 있다.
- 공유 메모리를 열 수 있는 프로세스
- 이름 있는 파이프에 연결할 수 있는 사용자
- 소켓 주소에 접근할 수 있는 프로세스
- 메시지 큐에 쓰거나 읽을 수 있는 권한
- 동기화 객체를 열거나 상태를 바꿀 수 있는 권한
- 다른 프로세스의 자격 정보 확인
Windows의 이름 있는 동기화 객체와 파일 매핑 객체도 보안 설명자와 접근 제어 목록을 사용하여 다른 프로세스의 접근을 제한할 수 있다. 여러 프로세스가 같은 이름의 객체를 공유할 때는 객체의 이름뿐 아니라 권한 설정도 올바르게 구성해야 한다.
Linux의 System V IPC와 POSIX IPC 객체도 사용자와 그룹, 권한 비트 또는 파일 시스템 이름 공간을 이용해 접근을 통제할 수 있다.
IPC로 받은 데이터는 같은 컴퓨터의 다른 프로세스에서 왔더라도 신뢰할 수 없는 입력으로 취급해야 한다. 메시지 크기와 형식, 권한과 상태 전이를 검증하지 않으면 프로세스 경계를 통한 보안 취약점이 발생할 수 있다.
동기화 수단의 선택
동기화와 통신 수단은 다음 기준을 고려하여 선택할 수 있다.
| 요구 | 적합할 수 있는 방식 |
|---|---|
| 짧은 공유 자료 수정 | 뮤텍스 또는 원자적 연산 |
| 특정 상태가 될 때까지 대기 | 뮤텍스와 조건 변수 |
| 제한된 수의 동일 자원 관리 | 세마포어 |
| 작업 완료 알림 | 이벤트 |
| 여러 단계의 병렬 작업 합류 | 장벽 |
| 대량의 로컬 데이터 공유 | 공유 메모리와 동기화 객체 |
| 단순한 바이트 흐름 | 파이프 |
| 메시지 경계 유지 | 메시지 큐 또는 데이터그램 소켓 |
| 로컬·원격 공통 통신 | 소켓 |
| 비동기 사건 알림 | 이벤트, 신호, 파일 서술자 기반 알림 |
| 프로세스 종료 뒤에도 데이터 유지 | 파일 또는 데이터베이스 |
하나의 방법이 모든 상황에서 가장 좋은 것은 아니다. 공유 메모리는 빠르지만 복잡하고, 메시지 전달은 분리를 강화하지만 복사와 큐 비용이 발생한다. 스핀락은 매우 짧은 커널 임계 구역에 적합할 수 있지만 사용자 프로그램의 긴 대기에는 부적합하다. 읽기-쓰기 잠금은 읽기가 많은 경우에만 유리할 수 있다.
동시성 설계 원칙
동시성 오류를 줄이기 위해서는 동기화 객체를 나중에 추가하기보다 데이터의 소유권과 통신 구조를 먼저 설계하는 것이 중요하다.
주요 원칙은 다음과 같다.
- 가능한 한 공유 가변 상태를 줄인다.
- 불변 데이터와 값 복사를 활용한다.
- 각 공유 자원을 보호하는 잠금을 명확하게 정한다.
- 잠금 획득 순서를 일관되게 유지한다.
- 잠금을 가진 상태에서 긴 계산과 입출력을 피한다.
- 조건 변수는 조건을 반복 검사하는 구조로 사용한다.
- 운영체제가 제공하는 검증된 동기화 원시 요소를 사용한다.
- 프로세스와 스레드의 비정상 종료를 고려한다.
- 타임아웃과 취소, 부분 실패를 처리한다.
- IPC 메시지의 형식과 버전을 명확하게 정의한다.
- 받은 데이터와 객체 권한을 검증한다.
- 성능 최적화 전에 경쟁 상태가 없는지 확인한다.
동시성을 제거할 수 있는 구조가 복잡한 동기화보다 나을 수 있다. 특정 자료를 하나의 스레드나 프로세스만 수정하고 다른 작업은 메시지로 요청하게 하면 공유 메모리 잠금의 필요성을 줄일 수 있다.
그러나 메시지 큐와 이벤트 루프도 내부적으로 운영체제의 동기화와 메모리 관리에 의존한다. 동시성 문제를 완전히 없애는 것이 아니라 더 명확한 경계와 통신 규칙으로 이동시키는 것이다.
종합
동시성은 운영체제가 여러 실행 흐름을 겹쳐 진행하고 다중 코어를 활용할 수 있게 하는 기본 능력이다. 프로세스와 스레드는 CPU와 입출력 장치를 효율적으로 사용할 수 있지만, 공유 상태에 대한 접근 순서가 달라지면 경쟁 상태와 데이터 손상, 교착 상태가 발생할 수 있다.
운영체제는 뮤텍스와 세마포어, 조건 변수, 이벤트, 원자적 연산 등의 동기화 기능을 제공하여 공유 자원에 대한 접근과 사건 대기를 조정한다. 프로세스는 파이프와 메시지 큐, 공유 메모리, 소켓, 신호와 파일 매핑 등을 통해 격리된 주소 공간 사이에서 정보를 교환한다.
동기화는 단순히 한 번에 하나만 실행하도록 막는 기능이 아니다. 실행 순서와 메모리 가시성을 정하고, 자원의 수량과 상태 변화를 표현하며, 여러 작업이 안전하게 협력할 수 있도록 하는 체계이다. 프로세스 간 통신 역시 데이터 전달뿐 아니라 작업의 시작과 완료, 오류와 수명 관계를 구성한다.
운영체제는 이러한 기본 수단을 제공하지만 어떤 데이터를 공유하고 어떤 순서로 잠금을 획득하며 실패를 어떻게 복구할지는 프로그램과 시스템 설계가 결정해야 한다. 따라서 동시성과 통신의 핵심은 가능한 실행 순서를 통제하면서도 불필요한 직렬화를 줄이고, 서로 독립적인 작업이 안전하게 진행되도록 만드는 데 있다.
보안과 격리
운영체제는 여러 사용자와 프로그램이 같은 하드웨어를 공유하면서도 서로의 데이터와 실행 상태를 임의로 변경하지 못하도록 보호해야 한다. 이를 위해 프로세서의 권한 수준과 가상 주소 공간, 사용자 계정, 접근 권한, 시스템 호출, 샌드박스와 가상화 등의 기능을 결합하여 신뢰 경계를 구성한다.
운영체제 보안의 기본 목표는 허가받지 않은 주체가 데이터와 시스템 자원을 읽거나 변경하거나 사용할 수 없게 하는 것이다. NIST는 신뢰할 수 있는 운영체제를 적절한 권한을 가진 주체만 데이터에 접근하거나 이를 변경·이동할 수 있도록 관리하는 운영체제로 정의한다.
운영체제의 보호 대상에는 다음과 같은 자원이 포함된다.
- 프로세스와 스레드
- 가상 메모리와 물리 메모리
- 파일과 디렉터리
- 장치와 장치 드라이버
- 네트워크 연결과 포트
- 사용자 계정과 인증 정보
- 프로세스 간 통신 객체
- 커널과 시스템 서비스
- 암호화 키와 보안 자격
- 가상 머신과 컨테이너
운영체제는 모든 프로그램을 동일하게 신뢰하지 않는다. 일반 응용 프로그램은 제한된 권한으로 실행하고, 커널과 일부 시스템 구성 요소만 하드웨어와 전체 시스템 자원에 접근할 수 있게 한다. 프로그램이 추가 권한을 필요로 하면 운영체제가 제공하는 시스템 호출과 권한 검사 절차를 거쳐야 한다.
사용자와 응용 프로그램
↓ 인증과 권한
사용자 공간
↓ 시스템 호출
커널의 권한 검사
↓
파일·메모리·장치·네트워크
보호와 보안
보호는 운영체제 안의 주체가 어떤 자원에 어떤 방식으로 접근할 수 있는지를 통제하는 체계이다. 보안은 보호 기능을 포함하여 인증과 암호화, 무결성 검증, 감사, 업데이트 및 공격 대응까지 포함하는 더 넓은 개념이다.
운영체제 보호에서는 행동을 수행하는 대상을 주체로, 접근되는 대상을 객체로 구분할 수 있다.
| 구분 | 예시 | | -- | ------------------------------- | | 주체 | 사용자, 프로세스, 스레드, 시스템 서비스 | | 객체 | 파일, 메모리, 장치, 소켓, 프로세스, IPC 객체 | | 권한 | 읽기, 쓰기, 실행, 생성, 삭제, 제어 | | 정책 | 어떤 주체가 어떤 객체에 어떤 권한으로 접근할 수 있는가 |
프로세스는 사용자를 대신해 동작하는 주체이면서 다른 프로세스가 접근할 수 있는 객체가 되기도 한다. 초기의 운영체제 보호 연구에서도 사용자와 프로세스 같은 능동적 주체와 파일·장치·다른 프로세스 같은 보호 객체를 구분하고, 허가받지 않은 접근을 방지하는 구조를 다뤘다.
보호 기능이 존재한다고 해서 운영체제가 자동으로 안전해지는 것은 아니다. 잘못된 권한 설정과 취약한 시스템 서비스, 커널 결함, 안전하지 않은 장치 드라이버와 사용자의 판단 오류가 보안 문제를 일으킬 수 있다. 운영체제는 이러한 위험을 줄이기 위해 하나의 방어 수단에만 의존하지 않고 여러 계층을 결합한다.
사용자 모드와 커널 모드
현대 프로세서는 서로 다른 권한 수준에서 코드를 실행할 수 있다. 운영체제는 이를 이용해 일반 프로그램과 커널을 분리한다.
Windows에서도 응용 프로그램은 사용자 모드에서 실행되고 핵심 운영체제 구성 요소는 커널 모드에서 실행된다. 많은 드라이버가 커널 모드에서 동작하지만 일부 드라이버는 사용자 모드에서 실행할 수 있다.
사용자 모드
- 일반 응용 프로그램
- 제한된 주소 공간
- 직접 하드웨어 접근 제한
- 특권 명령 사용 불가
↓ 시스템 호출
커널 모드
- 커널
- 메모리 관리자
- 스케줄러
- 파일 시스템
- 네트워크 스택
- 일부 장치 드라이버
사용자 모드 프로그램이 직접 페이지 테이블을 변경하거나 인터럽트를 비활성화하고 임의의 장치 레지스터를 조작할 수 있다면 하나의 프로그램이 시스템 전체를 손상시킬 수 있다. 따라서 이러한 작업은 특권 명령으로 제한되고 커널이 대신 수행한다.
시스템 호출이 발생하면 프로세서는 통제된 진입점을 통해 커널 모드로 전환한다. 커널은 호출 번호와 인수, 사용자 메모리 주소와 접근 권한을 검사한 뒤 요청을 처리하고 다시 사용자 모드로 돌아간다.
커널 모드의 결함은 사용자 모드의 오류보다 영향이 크다. 커널 코드와 드라이버는 다른 프로세스의 메모리와 시스템 전체 자원에 접근할 수 있으므로, 잘못된 메모리 접근이 시스템 중단이나 권한 상승으로 이어질 수 있다.
주소 공간 격리
운영체제는 각 사용자 프로세스에 독립된 가상 주소 공간을 제공한다. 서로 다른 프로세스에서 같은 숫자의 가상 주소를 사용하더라도 서로 다른 물리 메모리에 연결될 수 있다.
Windows에서도 각 사용자 모드 프로세스는 독립적인 개인 가상 주소 공간을 가지며, 커널 모드 코드는 별도의 공통 시스템 주소 공간을 사용한다.
프로세스 A
가상 주소 0x1000
↓
물리 페이지 A
프로세스 B
가상 주소 0x1000
↓
물리 페이지 B
페이지 테이블 항목에는 다음과 같은 보호 속성이 포함될 수 있다.
- 사용자 모드 접근 가능 여부
- 읽기 권한
- 쓰기 권한
- 실행 권한
- 현재 유효한 페이지인지
- 공유 페이지인지
- 변경되었는지
코드 영역을 읽기 전용과 실행 가능으로 설정하고, 일반 데이터 영역을 쓰기 가능하지만 실행 불가능하게 설정할 수 있다. 이를 통해 데이터에 삽입된 값을 기계어 코드로 실행하는 공격을 어렵게 만들 수 있다.
프로세스가 허용되지 않은 주소에 접근하거나 읽기 전용 영역을 수정하려 하면 프로세서가 예외를 발생시킨다. 커널은 해당 접근이 요구 페이징과 쓰기 시 복사처럼 정상적으로 해결할 수 있는지 판단하고, 해결할 수 없으면 프로세스에 오류를 전달하거나 종료한다.
주소 공간 격리는 프로그램 사이의 우발적인 침범과 일부 공격을 차단하지만 완전한 보안 경계는 아니다. 커널이나 장치 드라이버에 결함이 있으면 다른 프로세스의 메모리에 접근할 수 있으며, 프로세스가 명시적으로 공유 메모리를 연결하면 해당 영역은 공동으로 접근된다.
사용자와 계정
사용자 계정은 운영체제가 사람과 서비스, 시스템 작업의 신원을 구분하기 위해 사용하는 논리적 주체이다. 프로세스는 일반적으로 자신을 시작한 사용자의 보안 자격을 물려받는다.
운영체제는 사용자마다 다음 정보를 관리할 수 있다.
- 사용자 식별자
- 소속 그룹
- 로그인 자격
- 기본 디렉터리
- 허용된 권한
- 자원 제한
- 보안 정책
- 감사 정보
하나의 사람이 여러 계정을 사용할 수 있고, 사람이 직접 로그인하지 않는 서비스용 계정도 존재한다. 시스템 서비스에 별도 계정을 부여하면 서비스가 침해되어도 해당 계정에 허용된 자원만 접근하도록 제한할 수 있다.
그룹은 여러 사용자에게 공통 권한을 부여하기 위한 단위이다. 개별 사용자마다 같은 권한을 반복해서 설정하지 않고 역할과 조직에 따라 그룹에 권한을 배정할 수 있다.
운영체제는 현재 로그인한 사용자가 누구인지 확인하는 인증과, 인증된 사용자가 특정 동작을 수행할 수 있는지 판단하는 인가를 구분한다.
인증
누가 요청했는가
인가
해당 주체가 요청한 작업을 수행할 수 있는가
비밀번호와 보안 키, 생체 정보, 스마트 카드와 다중 요소 인증은 사용자 인증에 사용될 수 있다. 인증이 성공했더라도 모든 자원에 접근할 수 있는 것은 아니며, 각 객체의 접근 정책에 따라 다시 권한 검사를 거친다.
접근 제어
접근 제어는 주체가 객체에 수행할 수 있는 동작을 결정한다. 운영체제는 파일을 열거나 프로세스를 제어하고 장치에 접근할 때 현재 프로세스의 자격과 객체의 보안 정보를 비교한다.
대표적인 접근 권한은 다음과 같다.
- 읽기
- 쓰기
- 실행
- 생성
- 삭제
- 소유권 변경
- 권한 변경
- 다른 프로세스 제어
- 장치 사용
- 네트워크 포트 사용
임의적 접근 제어
임의적 접근 제어에서는 객체의 소유자가 다른 사용자와 그룹에 허용할 권한을 일정 범위 안에서 정할 수 있다. UNIX 계열의 소유자·그룹·기타 사용자 권한과 Windows의 접근 제어 목록이 대표적인 예이다.
UNIX 계열의 전통적인 파일 권한은 다음과 같이 구분된다.
소유자 권한
그룹 권한
기타 사용자 권한
각 권한
- 읽기
- 쓰기
- 실행
디렉터리에서 읽기와 쓰기, 실행 권한은 일반 파일과 다른 의미를 가질 수 있다. 실행 권한은 디렉터리 내부 경로를 탐색할 수 있는 권한을 나타낼 수 있다.
접근 제어 목록
접근 제어 목록은 한 객체에 여러 사용자와 그룹의 구체적인 허용 또는 거부 권한을 기록하는 방식이다.
문서 파일
사용자 A: 읽기, 쓰기
사용자 B: 읽기
그룹 C: 읽기
사용자 D: 접근 거부
단순한 소유자·그룹·기타 사용자 구분보다 세밀한 정책을 표현할 수 있다. 그러나 상속된 규칙과 허용·거부 순서가 복잡해지면 실제 유효 권한을 이해하기 어려워질 수 있다.
강제적 접근 제어
강제적 접근 제어에서는 사용자가 객체의 권한을 자유롭게 변경하는 것보다 시스템 전체의 보안 정책이 우선한다. 프로세스와 객체에 보안 등급이나 역할, 유형을 부여하고 허용된 관계만 접근하게 할 수 있다.
Linux에서는 Linux Security Modules 구조를 통해 SELinux와 AppArmor 같은 보안 정책 구현을 커널에 연결할 수 있다. Apple의 운영체제도 커널 수준의 강제적 접근 제어 기반 기능을 사용해 응용 프로그램 샌드박스를 구성해 왔다. Apple의 과거 운영체제 구조 문서도 MAC을 통해 프로세스의 기능을 제한하고 응용 프로그램 샌드박스를 제공한다고 설명한다.
강제적 접근 제어는 관리자 권한을 가진 프로세스에도 추가 제약을 적용할 수 있다. 다만 정책이 복잡하면 정상 프로그램의 동작이 차단될 수 있어 정책 설계와 진단 도구가 중요하다.
최소 권한 원칙
최소 권한 원칙은 프로그램과 사용자에게 작업 수행에 필요한 최소한의 권한만 부여하는 원칙이다.
예를 들어 단순한 웹 서버가 다음 권한을 모두 가질 필요는 없다.
- 모든 사용자 파일 읽기
- 임의 장치 접근
- 커널 모듈 적재
- 사용자 계정 생성
- 시스템 설정 변경
웹 콘텐츠 디렉터리 읽기와 제한된 네트워크 포트 사용만 허용하면, 서버가 침해되더라도 공격자가 이용할 수 있는 자원의 범위를 줄일 수 있다.
최소 권한은 다음 방법으로 적용할 수 있다.
- 일반 사용자 계정으로 프로그램 실행
- 서비스마다 별도 계정 사용
- 필요한 파일과 장치만 허용
- 관리자 권한을 일시적으로만 사용
- 기능별 권한 분리
- 샌드박스와 컨테이너 사용
- 자식 프로세스에서 불필요한 권한 제거
- 특권 기능을 작은 별도 서비스로 분리
권한을 한 번 부여한 뒤 프로그램 전체에서 계속 유지하기보다, 필요한 작업을 완료한 뒤 권한을 제거할 수 있다. 네트워크 포트를 연 뒤 관리자 권한을 내려놓거나, 특권이 필요한 작업을 작은 보조 프로세스에 맡기는 방식이 이에 해당한다.
관리자와 권한 상승
운영체제에는 시스템 전체 설정과 사용자, 장치 및 보안 정책을 관리할 수 있는 높은 권한의 계정이나 보안 주체가 존재한다. UNIX 계열에서는 전통적으로 root, Windows에서는 관리자 계정과 높은 무결성 수준의 프로세스가 이에 해당한다.
일반 사용자가 항상 관리자 권한으로 프로그램을 실행하면 응용 프로그램의 결함과 악성 코드가 시스템 전체에 영향을 줄 수 있다. 현대 운영체제는 일반 작업을 제한된 권한으로 수행하고, 시스템 설정 변경처럼 필요한 순간에만 별도의 인증과 승인을 요구하는 방식을 사용한다.
권한 상승은 프로세스가 더 높은 권한을 얻는 과정이다. 정당한 관리 작업에도 사용되지만, 취약점을 이용해 허가받지 않은 권한을 획득하는 공격도 같은 표현으로 부른다.
안전한 권한 상승 구조는 다음 요소를 필요로 한다.
- 요청한 사용자의 인증
- 수행할 작업의 명확한 표시
- 필요한 범위로 제한된 권한
- 사용자 입력 검증
- 실행 기록과 감사
- 작업 완료 후 권한 회수
권한이 높은 구성 요소는 가능한 한 작게 유지해야 한다. 전체 사용자 인터페이스를 관리자 권한으로 실행하기보다, 실제 시스템 변경만 작은 특권 서비스가 수행하도록 분리할 수 있다.
보안 경계
보안 경계는 한 영역의 주체가 다른 영역의 자원에 직접 접근하지 못하도록 하는 격리 지점이다.
운영체제의 대표적인 보안 경계는 다음과 같다.
- 사용자 모드와 커널 모드
- 서로 다른 프로세스의 주소 공간
- 일반 사용자와 관리자
- 응용 프로그램과 샌드박스 외부
- 게스트 운영체제와 하이퍼바이저
- 가상 머신과 호스트
- 서로 다른 보안 영역
- 신뢰 실행 환경과 일반 운영체제
모든 논리적 분리가 동일한 강도의 보안 경계인 것은 아니다. 프로세스 이름이나 디렉터리 구조를 구분하는 것만으로는 메모리와 권한이 격리되지 않는다. 보안 경계라고 부르려면 해당 경계를 넘어가는 접근을 신뢰할 수 있는 구성 요소가 강제해야 한다.
Windows의 가상 보안 모드는 하이퍼바이저를 이용해 여러 Virtual Trust Level을 만들고, 한 신뢰 수준의 프로세스가 다른 수준의 메모리에 접근하지 못하도록 별도의 보안 경계를 형성한다. 이를 통해 암호와 Kerberos 키 같은 자산을 일반 커널 영역과 분리할 수 있다.
샌드박스
샌드박스는 프로그램이 접근할 수 있는 파일과 장치, 네트워크, 프로세스와 시스템 기능을 제한하여 프로그램이 침해되었을 때 피해 범위를 줄이는 격리 환경이다.
샌드박스는 프로그램이 신뢰할 수 없다는 가정에서 출발한다. 프로그램이 정상적으로 필요한 작업은 수행할 수 있게 하면서, 그 밖의 자원에는 접근하지 못하도록 정책을 적용한다.
응용 프로그램
↓
샌드박스 정책
├── 지정된 파일만 접근
├── 허용된 네트워크만 사용
├── 제한된 장치만 사용
├── 다른 프로세스 접근 제한
└── 커널 기능 제한
Apple의 App Sandbox는 응용 프로그램이 선언한 entitlement를 기준으로 시스템 자원과 사용자 데이터에 대한 접근을 제한하고, 응용 프로그램이 침해되었을 때 피해를 억제하는 것을 목적으로 한다.
샌드박스 정책은 다음 요소를 제한할 수 있다.
- 파일 시스템 경로
- 카메라와 마이크
- 위치와 연락처
- 네트워크 연결
- 다른 프로세스와의 통신
- 자식 프로세스 생성
- 시스템 호출
- 장치 접근
- 사용자 인터페이스 제어
- 디버깅과 프로세스 검사
응용 프로그램은 사용자에게 파일을 선택하게 하거나 운영체제가 제공하는 중개 서비스를 통해 제한된 자원에 접근할 수 있다. macOS App Sandbox도 기본적으로 보호된 자원에 대한 접근을 제한하고, 사용자가 선택한 문서와 앱 그룹 등의 명시적인 경로로 접근 범위를 확장한다.
샌드박스는 별도의 가상 머신과 달리 대개 같은 커널을 공유한다. 따라서 커널 취약점이 존재하면 샌드박스 경계를 벗어날 가능성이 있으며, 샌드박스 자체도 정확한 정책 설정이 필요하다.
기능 기반 보안
전통적인 접근 제어가 사용자와 객체를 중심으로 권한을 관리한다면, 기능 기반 보안은 특정 객체에 특정 작업을 수행할 수 있는 위조하기 어려운 참조나 토큰을 권한으로 사용한다.
프로세스가 파일과 소켓을 열어 얻은 파일 서술자도 제한적인 기능 참조로 볼 수 있다. 한 번 열린 파일 서술자를 자식 프로세스나 다른 프로세스에 전달하면 전체 파일 시스템 접근 권한을 주지 않고도 특정 파일에 대한 접근만 위임할 수 있다.
기능 기반 구조는 다음과 같은 장점이 있다.
- 권한을 특정 객체에 한정할 수 있다.
- 별도 이름 탐색 없이 이미 승인된 객체를 사용할 수 있다.
- 필요한 권한만 다른 프로세스에 위임할 수 있다.
- 전역적인 관리자 권한을 줄일 수 있다.
다만 기능 참조가 의도하지 않은 프로세스에 유출되지 않도록 전달과 상속을 관리해야 한다.
시스템 호출 제한
사용자 모드 프로그램은 시스템 호출을 통해 커널 기능을 사용한다. 프로그램이 사용할 필요가 없는 시스템 호출을 차단하면 공격자가 취약점을 악용해 수행할 수 있는 행동을 줄일 수 있다.
시스템 호출 제한 정책은 다음과 같은 방식으로 구성할 수 있다.
- 허용된 시스템 호출만 실행
- 특정 인수와 동작만 허용
- 프로세스 생성과 디버깅 제한
- 파일 시스템 이름 공간 제한
- 네트워크 주소 체계 제한
- 장치 제어 호출 제한
Linux의 seccomp와 같은 기능은 프로세스가 호출할 수 있는 시스템 호출을 제한하는 샌드박스 구성 요소로 사용될 수 있다. 그러나 필요한 시스템 호출 목록을 정확히 구성하지 못하면 프로그램이 정상적으로 동작하지 않거나, 너무 넓게 허용하면 보호 효과가 낮아질 수 있다.
시스템 호출 제한은 파일 권한과 주소 공간 격리, 네트워크 정책을 대체하지 않는다. 하나의 계층에서 허용된 작업을 다른 보호 계층이 다시 제한할 수 있도록 조합하는 것이 일반적이다.
코드 서명과 실행 신뢰
코드 서명은 실행 파일과 라이브러리, 드라이버가 특정 서명 주체에 의해 배포되었으며 서명 이후 변경되지 않았음을 확인하는 기술이다.
Apple은 코드 서명을 응용 프로그램의 제작자를 인증하고 서명 이후 프로그램이 우발적 또는 악의적으로 변경되었는지를 확인할 수 있는 macOS 보안 기술로 설명한다.
코드 서명은 다음을 확인하는 데 사용될 수 있다.
- 코드의 무결성
- 배포 주체
- 허용된 권한과 entitlement
- 신뢰할 수 있는 드라이버인지
- 업데이트가 정당한 배포자에게서 왔는지
- 실행 정책을 통과할 수 있는지
서명이 있다고 프로그램 자체가 안전하다는 뜻은 아니다. 서명된 프로그램에도 취약점이나 악성 동작이 존재할 수 있다. 코드 서명은 주로 코드의 출처와 변경 여부를 검증하며, 실제 동작의 안전성은 별도로 평가해야 한다.
Linux 커널도 커널 모듈을 암호학적으로 서명하고 적재 시 서명을 확인하는 기능을 제공한다. 이를 통해 정책에 따라 서명되지 않았거나 신뢰할 수 없는 모듈의 적재를 제한할 수 있다.
보안 부팅
보안 부팅은 컴퓨터가 시작될 때 펌웨어와 부트로더, 커널 및 초기 시스템 구성 요소의 무결성과 신뢰를 단계적으로 검증하는 체계이다.
하드웨어 신뢰 기반
↓ 펌웨어 검증
부트로더
↓ 커널 검증
운영체제 커널
↓ 시스템 구성 요소 검증
사용자 공간 시작
부팅 과정의 초기 코드가 공격자에 의해 바뀌면 이후 운영체제가 수행하는 보안 검사도 우회될 수 있다. 보안 부팅은 이전 단계가 다음 단계의 서명이나 해시를 확인하는 신뢰 사슬을 구성한다.
Apple의 플랫폼 보안 문서도 보안 부팅 체인과 시스템 보안, 응용 프로그램 보안 기능이 신뢰할 수 있는 코드와 앱만 실행되도록 검증하는 데 함께 사용된다고 설명한다.
보안 부팅은 시스템이 승인한 코드만 실행하도록 제한할 수 있지만, 사용자가 직접 운영체제와 키를 관리하는 장치에서는 소유자가 신뢰 기준을 변경할 수 있는 구조가 필요할 수 있다.
무결성과 실행 보호
운영체제는 실행 중인 코드와 제어 흐름을 보호하기 위해 다양한 하드웨어 및 소프트웨어 기능을 사용할 수 있다.
대표적인 기능은 다음과 같다.
- 쓰기 가능한 메모리의 실행 제한
- 주소 공간 배치 난수화
- 스택 보호 값
- 제어 흐름 보호
- 읽기 전용 커널 영역
- 커널 모듈 서명
- 시스템 파일 무결성 검사
- 하드웨어 기반 스택 보호
- 커널 페이지 테이블 격리
주소 공간 배치 난수화는 프로그램과 라이브러리, 스택과 힙의 배치 위치를 실행마다 바꾸어 공격자가 코드와 데이터의 주소를 예측하기 어렵게 한다.
실행 방지 기능은 일반 데이터 페이지에서 코드 실행을 제한한다. 다만 JIT 컴파일러처럼 실행 중 코드를 생성해야 하는 프로그램은 쓰기 가능한 상태와 실행 가능한 상태를 엄격히 전환해야 한다.
Windows는 하드웨어가 지원하는 커널 모드 스택 보호와 같은 기능을 통해 반환 주소의 손상을 탐지하거나 차단할 수 있다.
이러한 완화 기법은 취약점 자체를 제거하지는 않지만 취약점을 실제 코드 실행과 권한 상승으로 연결하기 어렵게 한다.
파일과 데이터 보호
운영체제는 파일과 저장 장치에 다음과 같은 보호 기능을 제공할 수 있다.
- 파일 소유자와 접근 권한
- 접근 제어 목록
- 파일 암호화
- 전체 디스크 암호화
- 보안 삭제
- 무결성 검사
- 변경 기록
- 백업과 복구
- 읽기 전용 파일 시스템
- 스냅샷
암호화는 저장 장치가 분리되거나 장치를 분실했을 때 권한 없는 사용자가 데이터 내용을 읽기 어렵게 한다. 그러나 사용자가 로그인하여 암호화 키가 활성화된 상태에서는 운영체제와 응용 프로그램의 권한 관리가 여전히 중요하다.
파일 암호화와 접근 제어는 서로 다른 문제를 해결한다.
접근 제어
실행 중인 시스템에서
누가 파일을 사용할 수 있는가
암호화
저장 장치나 데이터가 유출되었을 때
내용을 읽을 수 있는가
악성 코드가 이미 사용자의 권한으로 실행되고 있다면 사용자가 읽을 수 있는 암호화 파일을 함께 읽을 수 있다. 따라서 암호화만으로 응용 프로그램 격리를 대체할 수 없다.
감사와 기록
보안 감사는 사용자와 프로그램이 수행한 보안 관련 행동을 기록하고 분석하는 체계이다.
감사 기록에는 다음과 같은 내용이 포함될 수 있다.
- 로그인과 로그아웃
- 인증 실패
- 관리자 권한 사용
- 파일 접근과 권한 변경
- 시스템 설정 변경
- 서비스 시작과 종료
- 보안 정책 위반
- 장치 연결
- 프로그램 실행
- 네트워크 연결
감사 로그는 사고가 발생한 뒤 원인을 조사하고, 규정 준수를 확인하며, 진행 중인 공격을 탐지하는 데 사용된다.
그러나 로그에 비밀번호와 암호화 키, 개인 데이터가 그대로 남으면 로그 자체가 보안 위험이 될 수 있다. 로그 접근 권한과 보존 기간, 무결성 및 원격 전송을 함께 관리해야 한다.
공격자가 관리자 권한을 얻으면 로컬 로그를 변경할 수 있으므로 중요한 시스템은 로그를 별도 서버나 변경하기 어려운 저장소로 전달할 수 있다.
업데이트와 취약점 대응
운영체제는 수많은 하드웨어와 네트워크 입력, 복잡한 파일 형식과 응용 프로그램을 처리하므로 시간이 지나면서 보안 취약점이 발견될 수 있다. 따라서 안전한 운영체제는 취약점이 전혀 없는 체계라기보다 취약점을 발견하고 수정하며 업데이트를 안전하게 배포할 수 있는 체계를 포함한다.
업데이트 체계는 다음 조건을 만족해야 한다.
- 배포자의 서명 검증
- 전송 중 변조 방지
- 실패 시 복구
- 부팅 불가능 상태 방지
- 버전과 의존성 관리
- 보안 수정의 신속한 배포
- 이전 버전으로의 악의적 되돌리기 방지
커널과 장치 드라이버, 시스템 라이브러리는 높은 권한으로 실행되므로 보안 업데이트가 특히 중요하다. 지원이 종료된 운영체제는 새로 발견된 취약점에 대한 수정을 받지 못할 수 있다.
자동 업데이트는 많은 장치를 빠르게 보호할 수 있지만, 중요한 산업 시스템과 서버에서는 호환성 시험과 단계적 배포, 복구 계획이 필요하다.
가상화
가상화는 하나의 물리적 컴퓨터 자원을 여러 개의 논리적인 실행 환경으로 나누거나, 실제 하드웨어와 다른 추상화된 하드웨어 환경을 제공하는 기술이다.
운영체제 가상화에서는 하이퍼바이저가 CPU와 메모리, 인터럽트, 장치를 가상 머신에 배분한다. 각 가상 머신은 자신에게 독립된 컴퓨터가 제공되는 것처럼 동작하고, 별도의 게스트 운영체제를 실행할 수 있다.
물리 하드웨어
↓
하이퍼바이저
├── 가상 머신 A
│ └── 게스트 운영체제 A
├── 가상 머신 B
│ └── 게스트 운영체제 B
└── 가상 머신 C
└── 게스트 운영체제 C
가상 머신은 다음 자원을 가상화할 수 있다.
- 가상 CPU
- 가상 메모리
- 가상 디스크
- 가상 네트워크 장치
- 가상 인터럽트
- 가상 펌웨어
- 가상 그래픽과 주변 장치
가상 머신의 게스트 커널은 다른 가상 머신과 분리된 주소 공간과 커널 상태를 가진다. 하나의 게스트 운영체제가 손상되더라도 하이퍼바이저의 격리가 올바르게 동작하면 다른 게스트와 호스트에 직접 접근할 수 없다.
그러나 가상 머신이 절대적인 격리를 보장하는 것은 아니다. 하이퍼바이저와 가상 장치 구현에 취약점이 있거나 관리 인터페이스의 권한이 잘못 설정되면 경계가 무너질 수 있다.
하이퍼바이저
하이퍼바이저는 일반적으로 다음 두 형태로 분류한다.
베어메탈 하이퍼바이저
하드웨어 위에서 직접 실행되며 가상 머신을 관리한다.
하드웨어
↓
하이퍼바이저
↓
여러 게스트 운영체제
데이터 센터와 서버 가상화에서 주로 사용되며, 호스트 범용 운영체제에 대한 의존을 줄일 수 있다.
호스트형 하이퍼바이저
일반 운영체제의 응용 프로그램이나 커널 구성 요소로 실행된다.
하드웨어
↓
호스트 운영체제
↓
가상화 프로그램
↓
게스트 운영체제
개발과 시험, 데스크톱 환경에서 사용하기 쉽지만 호스트 운영체제의 드라이버와 보안, 자원 관리에 더 많이 의존한다.
실제 제품은 이러한 구분에 완전히 맞지 않는 혼합 구조를 가질 수 있다. 중요한 차이는 가상 머신 사이의 격리를 어느 계층이 강제하고, 장치와 관리 서비스를 어디에서 실행하는지에 있다.
하드웨어 지원 가상화
현대 CPU는 하이퍼바이저가 게스트 운영체제를 효율적으로 격리할 수 있도록 가상화 명령과 별도의 실행 모드를 제공한다.
하드웨어 지원 기능에는 다음이 포함될 수 있다.
- 게스트와 하이퍼바이저 실행 모드 분리
- 2단계 주소 변환
- 가상 인터럽트
- 장치의 직접 할당
- 입출력 메모리 관리 장치
- 가상 신뢰 수준
- 메모리 암호화
2단계 주소 변환에서는 게스트 운영체제의 가상 주소를 게스트 물리 주소로 변환한 뒤, 하이퍼바이저가 다시 실제 호스트 물리 주소로 변환한다.
게스트 가상 주소
↓ 게스트 페이지 테이블
게스트 물리 주소
↓ 하이퍼바이저 주소 변환
호스트 물리 주소
하드웨어 지원이 없던 초기 가상화에서는 특권 명령을 가로채거나 코드를 동적으로 변환해야 했지만, 현대 CPU는 많은 동작을 직접 지원하여 가상화 비용을 줄인다.
가상 머신의 격리
가상 머신의 주요 보안 장점은 각 게스트가 별도의 커널을 사용한다는 것이다. 프로세스 기반 샌드박스와 컨테이너는 호스트 커널을 공유하지만, 가상 머신은 일반적으로 게스트마다 독립적인 커널과 시스템 상태를 가진다.
가상 머신 격리는 다음 경계를 제공할 수 있다.
- 별도 커널
- 별도 페이지 테이블
- 가상 장치
- 별도 부팅 과정
- 별도 사용자와 파일 시스템
- 가상 네트워크 경계
하이퍼바이저 기반 격리는 보안에 민감한 자산을 일반 운영체제 커널과도 분리하는 데 사용할 수 있다. Windows의 VSM도 하이퍼바이저와 가상 신뢰 수준을 이용해 메모리 보호 경계를 구성한다.
가상 머신의 관리 인터페이스는 매우 높은 권한을 가진다. 가상 디스크와 메모리 스냅샷, 가상 네트워크를 읽을 수 있으므로 일반 사용자에게 관리 권한을 무분별하게 제공해서는 안 된다.
컨테이너
컨테이너는 별도의 완전한 게스트 커널을 실행하는 대신 호스트 운영체제의 커널을 공유하면서 프로세스와 자원에 격리된 관점을 제공하는 운영체제 수준 가상화 방식이다.
물리 하드웨어
↓
호스트 운영체제 커널
├── 컨테이너 A
│ ├── 프로세스
│ └── 파일 시스템 환경
├── 컨테이너 B
│ ├── 프로세스
│ └── 파일 시스템 환경
└── 컨테이너 C
├── 프로세스
└── 파일 시스템 환경
컨테이너는 일반적으로 다음 기능을 조합한다.
Linux 네임스페이스는 전역 시스템 자원을 추상화하여 그 안의 프로세스가 독립된 자원을 가진 것처럼 보이게 하며, 한 네임스페이스의 변경을 다른 네임스페이스의 프로세스에서 보이지 않게 할 수 있다. 컨테이너는 이러한 네임스페이스의 대표적인 활용 사례이다.
Linux 네임스페이스
Linux의 네임스페이스는 다음과 같은 자원을 분리할 수 있다.
- 프로세스 식별자
- 마운트 지점
- 네트워크 장치와 주소
- 호스트 이름
- 사용자와 그룹 식별자
- 프로세스 간 통신 객체
- 제어 그룹 관점
- 시간 정보의 일부
호스트
├── PID 네임스페이스 A
│ └── 내부에서는 프로세스 번호 1부터 시작
├── 네트워크 네임스페이스 A
│ └── 독립된 인터페이스와 라우팅
└── 마운트 네임스페이스 A
└── 독립된 파일 시스템 마운트 관점
사용자 네임스페이스를 사용하면 컨테이너 내부의 관리자 사용자를 호스트의 일반 사용자 식별자에 대응시킬 수 있다. 이를 통해 컨테이너 안에서 높은 권한처럼 보이더라도 호스트에서는 제한된 권한만 갖게 할 수 있다.
네임스페이스는 자원을 숨기거나 별도의 관점을 제공하지만 자원의 사용량을 자동으로 제한하지는 않는다. CPU와 메모리, 입출력 사용량은 제어 그룹과 함께 관리해야 한다.
제어 그룹
제어 그룹은 여러 프로세스를 그룹으로 묶고 CPU와 메모리, 입출력 등의 자원 사용량을 제한하고 측정하는 Linux 커널 기능이다.
제어 그룹
├── CPU 사용 한도
├── 메모리 한도
├── 입출력 우선순위
├── 프로세스 수 제한
└── 자원 사용량 통계
Linux 커널 문서는 cgroup을 작업 집합과 여러 하위 시스템의 자원 매개변수를 연결하는 구조로 설명한다.
cgroup v2는 하나의 통합된 계층을 중심으로 자원 제어와 위임을 구성한다. 커널 문서는 cgroup 네임스페이스를 위임 경계로 사용할 수 있음을 설명한다.
제어 그룹은 자원 부족으로 인한 서비스 간 간섭을 줄이지만, CPU 시간과 메모리 대역폭, 캐시와 디스크 같은 모든 하드웨어 자원을 완벽하게 분리하는 것은 아니다. 같은 호스트에서 실행되는 작업 사이에 성능 간섭이 남을 수 있다.
컨테이너와 가상 머신의 차이
| 구분 | 컨테이너 | 가상 머신 |
|---|---|---|
| 커널 | 호스트 커널 공유 | 게스트별 커널 |
| 시작 | 일반적으로 빠름 | 게스트 부팅 필요 |
| 자원 사용 | 비교적 작음 | 운영체제 전체 자원 필요 |
| 운영체제 종류 | 호스트 커널과 호환 필요 | 서로 다른 게스트 운영체제 가능 |
| 격리 경계 | 프로세스·커널 기능 기반 | 하이퍼바이저·하드웨어 기반 |
| 주요 용도 | 응용 프로그램 패키징과 배포 | 운영체제 전체 격리와 시험 |
| 취약점 영향 | 호스트 커널 취약점 공유 | 하이퍼바이저 취약점이 주요 경계 |
컨테이너는 별도 커널을 실행하지 않으므로 가볍고 빠르게 시작할 수 있다. 반면 모든 컨테이너가 같은 호스트 커널을 공유하므로 커널 취약점이나 과도한 권한 설정이 여러 컨테이너와 호스트에 영향을 줄 수 있다.
가상 머신은 게스트별 커널을 사용하여 더 강한 격리를 제공할 수 있지만 자원 사용과 시작 비용이 더 크다.
Windows도 프로세스 격리 컨테이너와 하이퍼바이저 격리 컨테이너를 구분한다. Microsoft는 초경량 가상 머신으로 감싼 하이퍼바이저 격리 컨테이너가 프로세스 격리 컨테이너보다 강한 하드웨어 수준의 보안 경계를 제공한다고 설명한다.
컨테이너 보안
컨테이너를 사용한다고 프로그램이 자동으로 안전해지는 것은 아니다. 안전한 컨테이너 실행에는 다음과 같은 설정이 필요하다.
- 관리자 권한이 아닌 사용자로 실행
- 불필요한 Linux capability 제거
- 호스트 장치 접근 제한
- 읽기 전용 루트 파일 시스템 사용
- 호스트 경로 마운트 최소화
- 시스템 호출 제한
- 네트워크 정책 적용
- 자원 한도 설정
- 이미지 서명과 출처 검증
- 최신 커널과 런타임 유지
- 비밀 정보를 이미지에 포함하지 않음
컨테이너에 호스트의 전체 파일 시스템과 장치, 컨테이너 런타임 소켓을 제공하면 격리의 의미가 크게 약해질 수 있다.
컨테이너 내부의 root가 호스트에서도 항상 동일한 권한을 가지는 것은 아니지만, 사용자 네임스페이스를 사용하지 않고 많은 기능을 부여한 컨테이너는 호스트 공격에 더 큰 위험을 줄 수 있다.
프로세스 격리와 자원 격리
보안 격리와 자원 격리는 서로 관련되지만 같은 개념은 아니다.
보안 격리
허가되지 않은 데이터와 기능에 접근하지 못하게 함
자원 격리
CPU와 메모리, 입출력을 과도하게 사용하지 못하게 함
파일 접근이 완전히 차단된 프로세스라도 CPU를 무한히 사용하면 다른 프로그램을 방해할 수 있다. 반대로 CPU 사용량이 제한된 프로세스라도 권한 설정이 잘못되면 중요한 파일을 읽을 수 있다.
따라서 샌드박스와 컨테이너에는 접근 권한 제한과 함께 CPU·메모리·프로세스 수와 입출력 한도를 적용해야 한다.
Linux의 장치 cgroup은 그룹별로 장치 파일을 열거나 생성하는 동작을 허용 목록에 따라 제한할 수 있다.
장치 격리
장치는 CPU와 독립적으로 직접 메모리 접근을 수행할 수 있으므로, 잘못된 장치나 드라이버가 임의의 메모리를 읽고 쓰지 못하도록 제한해야 한다.
입출력 메모리 관리 장치는 장치가 접근할 수 있는 물리 메모리 범위를 별도의 주소 변환 표로 제한할 수 있다.
장치의 DMA 주소
↓
IOMMU 변환
↓
허용된 물리 메모리 영역
가상 머신에 물리 장치를 직접 할당할 때도 IOMMU를 사용하여 해당 장치가 다른 가상 머신과 하이퍼바이저 메모리에 접근하지 못하도록 할 수 있다.
장치 드라이버는 커널과 장치 사이에서 높은 권한으로 동작하는 경우가 많기 때문에 공격 표면이 될 수 있다. 가능한 장치를 사용자 모드 드라이버로 이동하거나, 드라이버의 메모리 접근을 제한하고, 서명된 드라이버만 허용하는 방식이 사용된다.
네트워크 격리
운영체제는 프로세스와 컨테이너, 가상 머신에 별도의 네트워크 관점을 제공할 수 있다.
네트워크 격리에는 다음 요소가 포함된다.
- 가상 네트워크 인터페이스
- 별도 IP 주소와 라우팅 표
- 방화벽 정책
- 네트워크 이름 공간
- 포트 접근 제한
- 가상 스위치
- 네트워크 주소 변환
- 서비스 간 통신 정책
컨테이너가 별도 네트워크 네임스페이스를 사용하면 호스트 및 다른 컨테이너와 다른 인터페이스와 주소, 라우팅 정보를 가질 수 있다. 그러나 가상 네트워크가 연결되어 있다면 방화벽과 서비스 인증이 필요하다.
같은 호스트에서 실행된다는 이유만으로 다른 프로세스나 컨테이너의 네트워크 요청을 신뢰해서는 안 된다. 로컬 IPC와 네트워크 통신에도 인증과 입력 검증이 필요할 수 있다.
신뢰 실행 환경
신뢰 실행 환경은 일반 운영체제와 분리된 실행 영역에서 암호화 키와 인증 정보, 보안 작업을 처리하는 구조이다.
이러한 영역은 다음 기능을 제공할 수 있다.
- 격리된 메모리
- 별도 권한 수준
- 보안 부팅과 측정
- 암호화 키 보호
- 일반 커널로부터의 접근 제한
- 보안 연산 실행
하드웨어 보안 모듈과 TPM, 보안 영역, 하이퍼바이저 기반 보안 영역 등이 이러한 목적으로 사용될 수 있다.
다만 신뢰 실행 환경은 가능한 한 작아야 한다. 높은 신뢰를 요구하는 코드의 규모가 커질수록 검증해야 할 공격 표면도 커진다.
격리의 한계
운영체제의 격리는 여러 프로그램이 같은 시스템을 안전하게 공유하게 하지만 완전한 분리를 보장하지는 않는다.
격리 경계를 약화할 수 있는 요인은 다음과 같다.
- 커널과 하이퍼바이저 취약점
- 잘못된 접근 권한
- 과도한 관리자 권한
- 안전하지 않은 장치 드라이버
- 공유 메모리와 공유 파일
- CPU 캐시와 실행 시간 등의 부채널
- 잘못된 IPC 입력 검증
- 컨테이너의 과도한 capability
- 호스트 경로와 장치 직접 연결
- 관리 인터페이스 노출
- 오래된 운영체제와 라이브러리
부채널 공격은 보호된 메모리를 직접 읽지 않고도 실행 시간과 캐시, 전력 사용과 공유 하드웨어 상태를 관찰하여 정보를 추론할 수 있다. 따라서 주소 공간과 가상 머신의 논리적 격리만으로 모든 정보 흐름을 차단할 수는 없다.
같은 물리 CPU와 캐시, 메모리 대역폭을 공유하는 가상 머신과 컨테이너 사이에는 성능 간섭과 일부 부채널 가능성이 남을 수 있다. 높은 보안 수준이 필요한 작업은 전용 하드웨어와 추가 격리 정책을 사용할 수 있다.
다층 방어
운영체제 보안은 하나의 기능이 실패해도 다른 계층이 피해를 제한하도록 여러 방어 수단을 겹쳐 구성한다. 이를 다층 방어라고 한다.
보안 부팅
↓
서명된 커널과 드라이버
↓
사용자 모드와 커널 모드 분리
↓
프로세스 주소 공간 격리
↓
사용자와 접근 권한
↓
샌드박스와 시스템 호출 제한
↓
가상화와 컨테이너
↓
암호화와 감사
↓
업데이트와 취약점 대응
예를 들어 응용 프로그램에 취약점이 있더라도 샌드박스가 파일 접근을 제한하고, 사용자 권한이 시스템 설정 변경을 막으며, 주소 공간 격리가 다른 프로세스의 메모리 접근을 차단할 수 있다. 공격자가 샌드박스를 탈출하더라도 커널 권한을 얻으려면 별도의 취약점이 필요하도록 경계를 겹칠 수 있다.
다층 방어는 모든 계층을 무조건 많이 추가한다는 뜻은 아니다. 복잡한 보안 설정은 잘못 구성될 가능성도 높이므로, 각 경계의 목적과 신뢰 대상을 명확하게 해야 한다.
신뢰 컴퓨팅 기반
신뢰 컴퓨팅 기반은 시스템의 보안 정책을 강제하기 위해 반드시 신뢰해야 하는 하드웨어와 소프트웨어의 집합이다.
신뢰 컴퓨팅 기반에는 다음 요소가 포함될 수 있다.
- CPU와 메모리 보호 기능
- 펌웨어와 부트로더
- 하이퍼바이저
- 운영체제 커널
- 보안 정책 관리자
- 인증 서비스
- 특권 시스템 서비스
- 암호화 키 관리 구성 요소
일반적으로 신뢰 컴퓨팅 기반은 작고 단순할수록 검증하기 쉽다. 파일 시스템과 네트워크 서비스, 장치 드라이버를 사용자 공간에 분리하는 마이크로커널 구조도 커널의 신뢰 범위를 줄이려는 목적을 가질 수 있다.
그러나 사용자 공간 서비스가 높은 권한을 가지거나 보안 정책에 필수적이라면 해당 서비스 역시 실질적인 신뢰 기반에 포함된다. 커널 코드의 크기만으로 전체 신뢰 범위를 판단할 수는 없다.
종합
운영체제의 보안과 격리는 여러 사용자와 프로그램이 하나의 컴퓨터를 공유할 수 있게 하는 기본 조건이다. 사용자 모드와 커널 모드는 일반 프로그램과 운영체제 핵심을 분리하고, 가상 주소 공간은 프로세스의 메모리를 서로 격리한다. 사용자 계정과 접근 제어는 파일과 장치, 프로세스와 시스템 기능을 사용할 수 있는 주체를 제한한다.
샌드박스는 응용 프로그램에 필요한 기능만 허용하여 침해 시 피해 범위를 줄이고, 코드 서명과 보안 부팅은 실행되는 코드의 출처와 무결성을 확인한다. 암호화와 감사, 업데이트 체계는 데이터 유출과 사고 조사, 새로 발견된 취약점에 대응하는 기반을 제공한다.
가상 머신은 하이퍼바이저를 통해 서로 다른 게스트 커널을 분리하고, 컨테이너는 같은 호스트 커널을 공유하면서 네임스페이스와 제어 그룹, 권한 제한을 이용해 프로세스와 자원을 격리한다. 가상 머신은 일반적으로 더 강한 커널 경계를 제공하고, 컨테이너는 더 작은 자원 사용과 빠른 배포를 제공한다.
어떤 격리 방식도 단독으로 완전한 보안을 보장하지 않는다. 커널과 하이퍼바이저, 드라이버와 관리 서비스의 취약점, 잘못된 권한과 공유 자원은 보호 경계를 약화할 수 있다. 따라서 운영체제는 최소 권한과 접근 제어, 메모리 보호, 샌드박스, 가상화, 무결성 검증과 지속적인 업데이트를 결합하여 여러 단계의 신뢰 경계를 구성한다.
운영체제의 종류와 계열
운영체제는 목적과 실행 환경, 응답 시간, 사용자 수, 하드웨어 자원과 시스템 구조에 따라 여러 종류로 구분할 수 있다. 하나의 운영체제가 여러 종류의 특성을 동시에 가질 수도 있다. 예를 들어 Linux 기반 운영체제는 개인용 컴퓨터와 서버, 스마트폰, 네트워크 장비와 임베디드 시스템에 모두 사용될 수 있으며, QNX는 임베디드 운영체제이면서 실시간 운영체제이기도 하다.
운영체제의 종류는 주로 사용 목적과 동작 특성을 기준으로 나누고, 운영체제의 계열은 공통된 커널과 인터페이스, 코드와 역사적 기원을 기준으로 구분한다. 같은 계열에 속한 운영체제라도 서로 다른 용도로 사용될 수 있고, 서로 다른 계열의 운영체제가 같은 시장과 환경에서 경쟁할 수도 있다.
용도와 환경에 따른 종류
| 종류 | 주요 특징 | 대표 사례 |
|---|---|---|
| 범용 운영체제 | 다양한 응용 프로그램과 장치를 지원하며 여러 사용 목적을 처리함 | Windows, macOS, Linux, FreeBSD |
| 서버 운영체제 | 네트워크 서비스와 다중 사용자, 원격 관리, 높은 가용성과 대규모 자원을 중시함 | Linux, Windows Server, FreeBSD |
| 모바일 운영체제 | 터치 입력과 이동통신, 센서, 전력 관리, 앱 권한과 샌드박스를 중심으로 함 | Android, iOS, iPadOS |
| 임베디드 운영체제 | 특정 장치와 제한된 하드웨어 자원을 대상으로 작은 크기와 장치 제어를 중시함 | Zephyr, FreeRTOS, 임베디드 Linux |
| 실시간 운영체제 | 작업을 단순히 빠르게 처리하는 것이 아니라 정해진 시간 안에 완료할 수 있는 예측 가능성을 중시함 | QNX, FreeRTOS, Zephyr, VxWorks |
| 메인프레임 운영체제 | 매우 많은 동시 작업과 입출력, 대규모 거래, 가상화와 지속적인 가용성을 중시함 | z/OS, z/VM, z/VSE |
| 분산 운영체제 | 여러 컴퓨터의 자원을 하나의 통합된 시스템처럼 관리하려 함 | Plan 9, Amoeba, 연구용 분산 운영체제 |
범용 운영체제는 문서 작성과 웹 탐색, 소프트웨어 개발, 게임, 미디어와 업무 프로그램처럼 서로 다른 종류의 응용 프로그램을 같은 환경에서 실행한다. 일반적으로 그래픽 사용자 인터페이스와 파일 시스템, 네트워크, 사용자 계정, 멀티태스킹과 폭넓은 장치 지원을 제공한다.
서버 운영체제는 대화형 개인 사용보다 여러 사용자의 요청과 네트워크 서비스를 안정적으로 처리하는 데 초점을 둔다. 웹 서버와 데이터베이스, 파일 서버, 디렉터리 서비스, 가상 머신과 컨테이너를 장기간 실행하며 원격 관리와 자동화, 자원 제한 및 장애 복구 기능이 중요하다. Microsoft는 Windows Server를 온프레미스와 하이브리드, 클라우드 환경에서 응용 프로그램과 서비스 및 작업 부하를 실행하는 엔터프라이즈 서버 플랫폼으로 설명한다.
모바일 운영체제는 스마트폰과 태블릿의 제한된 배터리, 이동통신과 무선 연결, 카메라와 위치 센서, 터치 기반 사용자 인터페이스를 관리한다. 응용 프로그램을 서로 다른 프로세스와 샌드박스에 격리하고, 카메라와 마이크, 연락처와 위치 같은 민감한 자원은 권한을 받은 앱에만 제공하는 구조가 일반적이다. Android는 Linux LTS 커널을 기반으로 Android 전용 변경 사항을 결합하며, 운영체제 수준에서 Linux 커널의 보안과 프로세스 간 통신, 앱 샌드박스를 사용한다.
임베디드 운영체제는 자동차의 제어 장치와 센서, 가전제품, 네트워크 장비, 웨어러블 기기와 산업용 제어기처럼 특정한 목적을 가진 장치에 포함된다. 범용 운영체제보다 작은 메모리와 저장 공간에서 동작해야 할 수 있으며, 화면이나 파일 시스템, 동적 프로세스 생성 같은 기능을 일부 제공하지 않을 수도 있다. Zephyr는 제한된 자원을 가진 센서와 제어기, 스마트워치 및 IoT 장치를 대상으로 작은 커널을 제공한다.
실시간 운영체제는 평균 처리 속도보다 최악의 조건에서도 정해진 시간 안에 작업을 시작하거나 완료할 수 있는 결정성과 예측 가능성을 중요하게 다룬다. 높은 우선순위 작업의 빠른 선점과 정밀한 타이머, 우선순위 역전 방지, 제한된 지연 시간과 예측 가능한 메모리 관리 기능을 제공할 수 있다. FreeRTOS는 마이크로컨트롤러와 소형 프로세서를 위한 작은 실시간 커널이며, QNX는 임베디드 시스템에서 사용할 수 있는 POSIX 기반 실시간 운영체제 환경을 제공한다.
실시간 시스템이라고 해서 모든 작업이 항상 빠른 것은 아니다. 중요하지 않은 작업은 늦게 실행될 수 있지만, 시간 제한이 있는 작업은 정해진 우선순위와 스케줄링 규칙에 따라 요구된 시간 안에 처리되어야 한다. 실시간 운영체제는 자동차와 로봇, 항공·철도 장비, 산업 제어, 의료기기와 통신 장비 등에 사용될 수 있다.
메인프레임 운영체제는 매우 많은 동시 사용자와 배치 작업, 거래 처리 및 입출력을 지속적으로 처리하는 환경을 대상으로 한다. IBM은 z/OS를 안정성과 보안, 지속적인 가용성이 필요한 메인프레임 응용 프로그램 환경으로 설명하며, 수천 개의 프로그램과 사용자를 동시에 처리할 수 있도록 발전해 왔다고 설명한다.
배치 운영체제와 시분할 운영체제는 운영체제 역사에서 중요한 분류이지만, 현대 운영체제에서는 서로 완전히 분리된 제품 종류라기보다 함께 제공되는 실행 방식으로 남아 있다. 하나의 현대 서버 운영체제는 대화형 사용자와 네트워크 서비스를 처리하면서 예약된 배치 작업도 동시에 실행할 수 있다.
네트워크 운영체제 역시 과거에는 네트워크 파일과 프린터, 사용자 계정을 관리하는 서버 중심 운영체제를 가리키는 별도의 분류로 사용되었다. 현재는 대부분의 범용 운영체제가 TCP/IP와 원격 관리, 파일 공유와 인증 기능을 기본적으로 제공하기 때문에 독립된 분류로서의 경계가 약해졌다.
분산 운영체제는 네트워크로 연결된 여러 컴퓨터의 프로세서와 메모리, 파일 및 장치를 하나의 운영체제 환경처럼 제공하려는 구조이다. 현대의 대규모 분산 서비스는 하나의 분산 운영체제보다 Linux나 Windows Server 같은 개별 운영체제 위에 클러스터 관리자와 분산 저장소, 컨테이너 오케스트레이션 체계를 구성하는 경우가 많다.
주요 운영체제 계열
UNIX와 Unix 계열
UNIX는 단순히 비슷한 명령행과 파일 구조를 가진 모든 운영체제를 뜻하는 명칭이 아니다. UNIX 상표는 The Open Group이 관리하며, Single UNIX Specification의 요구 사항을 충족하고 인증받은 시스템만 공식적으로 UNIX 상표를 사용할 수 있다. 이 사양은 운영체제 인터페이스와 셸, 공통 유틸리티를 정의하며 핵심 부분은 POSIX 표준과 연결된다.
Unix 계열 운영체제 또는 Unix-like 운영체제는 공식 UNIX 인증 여부와 관계없이 UNIX의 프로세스와 파일, 권한, 셸과 시스템 인터페이스의 영향을 받은 운영체제를 넓게 가리킨다. Linux와 BSD 운영체제는 일반적으로 Unix 계열에 포함되지만, 개별 배포판과 시스템이 모두 UNIX 인증을 받은 것은 아니다.
UNIX 계열의 공통적인 특성에는 계층형 파일 시스템과 다중 사용자, 프로세스 기반 실행, 파일 서술자, 셸과 작은 명령행 도구, 파이프를 통한 프로그램 조합 등이 있다. POSIX는 이러한 환경의 공통 인터페이스를 정의하여 서로 다른 운영체제 사이에서 소스 코드 이식성을 높인다.
BSD 계열
BSD는 캘리포니아 대학교 버클리에서 UNIX를 기반으로 발전한 운영체제와 소프트웨어 계열이다. 현대의 대표적인 BSD 운영체제에는 FreeBSD, OpenBSD, NetBSD가 있다.
FreeBSD는 커널과 기본 사용자 공간, 시스템 도구와 문서를 하나의 운영체제 프로젝트에서 함께 개발한다. Linux 배포판이 Linux 커널과 여러 독립 프로젝트의 사용자 공간을 조합하는 것과 달리, FreeBSD는 기본 시스템을 하나의 통합된 프로젝트로 관리한다. FreeBSD 공식 문서는 FreeBSD의 역사와 다른 운영체제와의 관계, 프로젝트의 개발 모델을 별도로 설명한다.
BSD에서 발전한 네트워크와 파일 시스템, 시스템 인터페이스는 다른 여러 운영체제에 영향을 주었다. Apple의 Darwin과 XNU에도 FreeBSD를 비롯한 BSD 계열 기술이 포함되어 있다. Apple은 XNU를 Mach 커널과 FreeBSD 구성 요소, I/O Kit를 결합한 혼합형 커널로 설명한다.
Linux 계열
Linux는 완전한 운영체제 제품 하나의 이름이라기보다 운영체제의 핵심인 커널을 가리킨다. 실제 사용자 환경은 Linux 커널에 시스템 라이브러리와 셸, 초기화 체계, 패키지 관리자, 그래픽 환경과 응용 프로그램을 결합하여 구성된다. 이러한 완성된 체계를 Linux 배포판이라고 한다.
대표적인 Linux 배포판에는 Debian, Ubuntu, Fedora, Red Hat Enterprise Linux, SUSE Linux Enterprise, Arch Linux 등이 있다. 배포판은 같은 Linux 커널 계열을 사용하면서도 패키지 형식과 업데이트 정책, 기본 도구와 지원 기간, 대상 사용자와 시스템 설정에서 차이를 가진다.
Linux 커널은 서버와 개인용 컴퓨터뿐 아니라 슈퍼컴퓨터와 네트워크 장비, 임베디드 장치와 스마트폰의 기반으로 사용될 수 있다. Linux 커널 공식 문서는 사용자 공간 API와 파일 시스템, 메모리 관리, 드라이버, 관리 및 여러 아키텍처에 대한 문서를 제공한다.
Android는 Linux 커널을 사용하지만 일반적인 데스크톱 Linux 배포판과 같은 사용자 공간과 응용 프로그램 환경을 제공하지 않는다. Android는 Android Common Kernel과 하드웨어 추상화, 네이티브 라이브러리, Android Runtime, 시스템 서비스와 앱 프레임워크를 결합한 별도의 운영체제 플랫폼이다.
Windows NT 계열
Windows NT는 Microsoft가 개발한 운영체제 계열로, 현대의 Windows 클라이언트와 Windows Server 제품의 구조적 기반이 되었다. Windows 운영체제는 사용자 모드와 커널 모드 구성 요소를 분리하고, 프로세스와 가상 메모리, 객체 관리자, 입출력 관리자, 보안 참조 모니터와 장치 드라이버 체계를 제공한다.
과거의 Windows 95와 Windows 98, Windows Me는 MS-DOS 계열의 기반을 이어받았지만, Windows XP 이후의 소비자용 Windows는 NT 계열로 통합되었다. 현재 Windows와 Windows Server는 용도와 제공 기능, 지원 정책에 차이가 있지만 공통된 Windows 커널 구조와 시스템 구성 요소를 기반으로 한다.
Windows 클라이언트는 개인용 컴퓨터와 워크스테이션을 대상으로 하고, Windows Server는 파일과 웹 서비스, 디렉터리, 가상화, 원격 데스크톱과 기업 인프라를 대상으로 한다. Windows Server는 그래픽 환경을 포함하는 설치와 더 작은 구성 요소만 설치하는 Server Core 형태를 제공할 수 있다.
Darwin과 Apple 운영체제 계열
Darwin은 Apple 운영체제의 핵심을 구성하는 공개 기반 시스템이다. Darwin의 XNU 커널은 Mach와 BSD 계열 구성 요소, I/O Kit를 결합하며, macOS와 iOS에서 사용된다.
Apple 운영체제 계열에는 macOS, iOS, iPadOS, watchOS, tvOS, visionOS 등이 있다. 이들은 장치와 사용자 인터페이스, 응용 프로그램 실행 정책이 서로 다르지만 커널과 파일 시스템, 보안 부팅, 코드 서명 및 여러 시스템 기술을 공유한다. Apple은 운영체제와 개발 도구의 일부 공개 소스 코드를 macOS와 iOS 등의 릴리스별로 제공한다.
macOS는 범용 데스크톱 운영체제이며 UNIX 환경과 그래픽 응용 프로그램 생태계를 함께 제공한다. iOS와 iPadOS 등은 모바일 및 전용 장치 환경을 대상으로 더 강한 앱 격리와 제한된 배포·권한 체계를 사용한다.
메인프레임 계열
IBM 메인프레임 환경에는 z/OS, z/VM, z/VSE와 같은 서로 다른 운영체제가 존재한다.
- z/OS는 대규모 거래와 배치 처리, 많은 동시 사용자와 높은 가용성이 필요한 환경을 대상으로 한다.
- z/VM은 여러 가상 머신을 생성하고 다른 운영체제를 실행하는 제어 프로그램이자 하이퍼바이저 역할을 한다.
- z/VSE는 비교적 작은 구성으로 전통적인 배치와 거래 처리 작업을 지원한다.
IBM은 z/OS를 지속적인 가용성과 대규모 작업 처리를 위한 주력 메인프레임 운영체제로, z/VM을 다른 운영체제를 가상 머신에서 실행하는 가상화 운영 환경으로 설명한다.
z/OS는 전통적인 메인프레임 작업과 함께 POSIX API와 셸을 제공하는 z/OS UNIX System Services 환경도 포함한다. 이를 통해 기존 메인프레임 프로그램과 UNIX 계열 응용 프로그램을 하나의 운영체제 안에서 함께 사용할 수 있다.
실시간 운영체제 계열
실시간 운영체제는 하나의 공통 조상에서 나온 단일 계열이 아니라, 실시간 제약을 해결하기 위해 개발된 여러 독립적인 운영체제와 커널을 묶는 분류이다.
QNX는 마이크로커널과 메시지 전달을 중심으로 하는 POSIX 계열 실시간 운영체제이다. 작은 임베디드 장치부터 복잡한 분산·제어 시스템까지 확장할 수 있도록 프로세스와 스레드, 타이머, 인터럽트와 자원 관리자를 제공한다.
FreeRTOS는 마이크로컨트롤러와 소형 프로세서를 위한 실시간 커널로, 작업 스케줄링과 큐, 타이머 및 동기화 기능을 중심으로 제공한다. 완전한 데스크톱 운영체제보다 응용 프로그램에 포함되는 작은 커널에 가깝다.
Zephyr는 작은 실시간 커널에 장치 드라이버와 네트워크, 파일 시스템, 보안 및 여러 임베디드 서비스를 결합한 공개 운영체제 프로젝트이다. 다양한 프로세서 아키텍처와 제한된 자원의 임베디드 장치를 지원한다.
활용 분야
운영체제는 프로그램을 실행하는 거의 모든 범용 컴퓨터와 전자 장치에 사용된다. 다만 시스템의 목적에 따라 제공해야 하는 사용자 인터페이스와 장치 지원, 응답 시간, 보안과 자원 관리 정책이 달라진다.
개인용 컴퓨터와 업무 환경
개인용 컴퓨터 운영체제는 그래픽 사용자 인터페이스와 파일 관리, 웹 브라우저, 미디어, 게임, 문서 작성과 개발 도구를 함께 실행하는 환경을 제공한다. Windows, macOS와 데스크톱용 Linux 배포판이 대표적이다.
이 환경에서는 다음 요소가 중요하다.
- 폭넓은 하드웨어와 주변 장치 지원
- 그래픽 사용자 인터페이스
- 사용자 계정과 파일 보호
- 응용 프로그램 설치와 업데이트
- 절전과 배터리 관리
- 오디오와 영상, 그래픽 가속
- 접근성과 국제화
- 기존 응용 프로그램과의 호환성
개인용 컴퓨터는 한 사용자가 중심이 되지만, 운영체제 내부에서는 다수의 사용자 계정과 시스템 서비스, 백그라운드 프로세스가 동시에 실행된다.
서버와 클라우드
서버 운영체제는 웹 서비스와 데이터베이스, 파일 공유, 인증, 전자우편, 가상 머신과 컨테이너를 장시간 실행한다. 원격 관리와 자동화, 다중 사용자, 높은 네트워크 처리량과 장애 복구가 중요하다.
클라우드 환경에서는 물리 서버 위의 호스트 운영체제와 하이퍼바이저, 가상 머신 내부의 게스트 운영체제, 컨테이너의 호스트 커널이 여러 계층으로 결합된다.
물리 서버
↓
호스트 운영체제 또는 하이퍼바이저
↓
가상 머신
↓
게스트 운영체제
↓
컨테이너와 응용 프로그램
주요 클라우드 플랫폼은 Linux와 Windows Server를 포함한 여러 운영체제 이미지를 제공한다. Google Compute Engine은 Linux와 Windows 운영체제의 사전 구성 이미지를 제공하고, AWS Systems Manager도 Linux와 Windows Server를 관리 대상 운영체제로 지원한다.
서버에서는 화면과 로컬 입력 장치가 필요하지 않을 수 있으므로 그래픽 사용자 환경을 제외하고 명령행과 원격 관리 서비스만 설치할 수 있다. 불필요한 구성 요소를 줄이면 저장 공간과 메모리 사용량, 업데이트 대상과 공격 표면을 줄일 수 있다.
스마트폰과 개인용 장치
스마트폰과 태블릿 운영체제는 전화와 메시지, 카메라, 위치 정보와 이동통신을 관리하고, 앱이 민감한 장치 기능을 사용할 때 사용자 권한을 요구한다.
모바일 환경에서는 다음 요소가 중요하다.
- 배터리와 발열 관리
- 터치와 제스처 입력
- 셀룰러와 무선 네트워크
- 카메라와 마이크, 위치 센서
- 앱 샌드박스
- 백그라운드 실행 제한
- 앱 서명과 배포
- 장치 암호화
- 빠른 절전과 복귀
Android는 여러 제조사와 하드웨어에서 사용할 수 있는 공개 기반 플랫폼이며, iOS와 iPadOS는 Apple 하드웨어와 소프트웨어가 결합된 환경을 제공한다. 두 계열 모두 앱 프로세스와 사용자 데이터, 장치 권한을 강하게 분리한다. Android 공식 문서는 Linux 커널 보안과 응용 프로그램 샌드박스가 네이티브 코드를 포함한 앱의 동작을 제한한다고 설명한다.
임베디드 장치와 사물 인터넷
임베디드 운영체제는 센서와 가전제품, 공유기, 카메라, 웨어러블 장치, 드론과 산업 장비 안에서 특정 기능을 수행한다.
이 환경에서는 다음 조건이 중요할 수 있다.
- 매우 작은 메모리와 저장 공간
- 낮은 전력 소비
- 빠른 부팅
- 장기간 무중단 실행
- 하드웨어 레지스터와 인터럽트 제어
- 원격 업데이트
- 제한된 네트워크 연결
- 물리적 접근에 대한 보안
- 오류 발생 시 안전한 복구
작은 마이크로컨트롤러에는 FreeRTOS나 Zephyr 같은 작은 실시간 운영체제가 사용될 수 있고, 더 강력한 장치에는 임베디드 Linux와 Android 기반 체계가 사용될 수 있다. FreeRTOS는 마이크로컨트롤러와 소형 프로세서를, Zephyr는 센서부터 복잡한 임베디드 제어기와 IoT 장치를 대상으로 한다.
자동차와 산업 제어
자동차와 공장 설비, 로봇과 의료기기에서는 프로그램이 물리적인 장치를 직접 제어한다. 잘못된 명령이나 긴 지연이 장치 손상과 안전 문제로 이어질 수 있으므로 실시간성과 신뢰성, 오류 격리가 중요하다.
이 환경의 운영체제는 다음 기능을 제공할 수 있다.
- 우선순위 기반 선점 스케줄링
- 예측 가능한 인터럽트 지연
- 작업 간 메모리 보호
- 오류가 발생한 서비스의 재시작
- 감시 타이머
- 안전한 통신
- 이중화와 장애 감지
- 인증과 안전 표준을 위한 개발 지원
QNX는 자동차와 산업·제어 시스템을 포함한 임베디드 환경을 위한 실시간 운영체제와 하이퍼바이저를 제공하며, FreeRTOS와 Zephyr도 여러 소형 제어 장치에 사용될 수 있다.
네트워크와 통신 장비
라우터와 스위치, 방화벽, 무선 기지국과 저장 장치는 많은 패킷과 입출력 요청을 낮은 지연 시간으로 처리해야 한다.
네트워크 장비 운영체제는 다음을 중시한다.
- 높은 패킷 처리량
- 네트워크 인터페이스와 가속기 지원
- 원격 설정과 관리
- 장애 없는 업데이트
- 사용자와 관리 영역 분리
- 라우팅과 방화벽 정책
- 여러 제어 프로세스의 격리
- 하드웨어 이중화
Linux와 BSD 계열은 성숙한 TCP/IP 네트워크 스택과 장치 드라이버를 기반으로 네트워크 장비에 사용될 수 있으며, 실시간 제약이 있는 통신 장비에는 QNX와 전용 운영체제가 사용될 수 있다.
메인프레임과 대규모 거래
메인프레임 운영체제는 은행과 보험, 항공 예약, 정부와 대기업의 핵심 거래 및 배치 작업을 처리한다. 매우 많은 입출력과 동시 사용자를 지원하고, 장시간 서비스를 중단하지 않는 것이 중요하다.
z/OS는 대규모 가상·물리 저장 공간과 입출력 작업을 관리하고 수천 개의 프로그램과 사용자를 함께 처리하는 환경을 제공한다.
메인프레임 환경에서는 하나의 물리 시스템 안에 여러 논리 파티션과 가상 머신을 구성하여 서로 다른 운영체제와 작업 부하를 격리할 수 있다. 배치 처리와 온라인 거래, 데이터베이스와 UNIX 응용 프로그램이 하나의 시스템 안에서 함께 실행될 수도 있다.
고성능 컴퓨팅과 연구
슈퍼컴퓨터와 계산 클러스터는 수천 개 이상의 프로세서와 가속기, 고속 네트워크와 분산 저장소를 사용해 과학 계산과 기상 예측, 물리 시뮬레이션과 인공지능 학습을 수행한다.
이 환경에서는 다음 요소가 중요하다.
- 대규모 병렬 프로세서 지원
- NUMA 메모리 관리
- GPU와 계산 가속기
- 고속 네트워크와 원격 직접 메모리 접근
- 대규모 파일 시스템
- 배치 작업 스케줄러
- 장애가 발생한 노드의 복구
- 성능 측정과 자원 할당
TOP500은 고성능 컴퓨터의 운영체제와 운영체제 계열을 별도의 분류 항목으로 기록하며, 현재 목록의 여러 시스템은 Ubuntu와 Red Hat Enterprise Linux 등의 Linux 계열 운영체제를 사용한다.
운영체제만으로 전체 클러스터를 관리하는 것은 아니며, 운영체제 위에 작업 스케줄러와 메시지 전달 라이브러리, 분산 파일 시스템과 클러스터 관리 도구가 추가된다.
게임 콘솔과 전용 장치
게임 콘솔과 셋톱박스, 스마트 TV, 가상현실 장치와 키오스크는 범용 컴퓨터와 유사한 하드웨어를 사용하지만 특정한 사용자 경험과 보안, 배포 체계를 제공한다.
이 환경의 운영체제는 다음을 중시할 수 있다.
- 그래픽과 오디오의 낮은 지연 시간
- 제한된 하드웨어 구성에 대한 최적화
- 응용 프로그램과 게임의 격리
- 코드 서명과 무결성 검증
- 절전과 빠른 대기 모드 복귀
- 컨트롤러와 센서
- 콘텐츠 보호
- 중앙화된 업데이트
일부 전용 장치는 BSD나 Linux, Windows와 같은 기존 운영체제 계열을 기반으로 하면서 사용자 인터페이스와 보안 정책, 개발 API를 장치 목적에 맞게 변경한다.
소프트웨어 개발과 시험
운영체제는 응용 프로그램의 실행 대상이면서 동시에 소프트웨어 개발 도구의 기반이다. 컴파일러와 디버거, 빌드 시스템, 에뮬레이터와 프로파일러는 운영체제의 프로세스와 파일, 메모리와 디버깅 인터페이스를 사용한다.
개발자는 여러 운영체제와 버전을 시험하기 위해 다음 기능을 사용할 수 있다.
- 가상 머신
- 컨테이너
- 운영체제 에뮬레이터
- 호환 계층
- 교차 컴파일
- 원격 장치 디버깅
- 시스템 호출 추적
- 커널과 성능 프로파일링
가상 머신은 다른 커널과 전체 운영체제 환경을 시험하는 데 적합하고, 컨테이너는 같은 커널 위에서 응용 프로그램의 사용자 공간과 의존성을 격리하는 데 적합하다.