3장 컴퓨터 시스템의 동작 원리
컴퓨터 시스템의 구조
컴퓨터 시스템은 내부 시스템과 외부 시스템으로 구분하며, 각 하드웨어 장치에는 컨트롤러라는 작은 CPU가 있다.
외부 시스템에서 입력을 하면 내부 시스템에서 출력을 해준다.
운영체제 전체가 메모리에 적재되면 비효율적이므로 핵심 부분만 항상 적재를 하며 이 부분을 커널이라 한다.
CPU 연산과 I/O 연산
입출력 장치들의 I/O 연산은 입출력 컨트롤러가, 컴퓨터 내부 연산은 내부 CPU가 담당한다.
두 연산 작업은 동시에 수행 가능하며, 각각의 장치 컨트롤러에는 오고 가는 데이터들을 저장하기 위한 로컬 버퍼가 있다.
컨트롤러가 장치로부터 데이터를 읽어와서 로컬버퍼에 저장된 뒤에 로컬버퍼에서 메모리로 전달된다.
이 때, 로컬버퍼에 데이터를 저장하는 작업이 끝났다면 컨트롤러가 인터럽트를 발생시켜 CPU에게 보고한다.
그럼 CPU는 하던 작업을 잠시 중단하고 발생한 인터럽트를 작업한 다음 원래 하던 작업을 다시 재개한다.
인터럽트의 일반적 기능
운영체제에는 미리 인터럽트 루틴 코드가 작성되어 있으며, 각각의 인터럽트에는 번호가 부여되어 있다.
이 번호를 인터럽트 벡터라고 하고, 번호에 따라 처리해야 할 코드가 위치한 부분을 가리키고 있는 자료구조를 뜻한다.
인터럽트에는 하드웨어 인터럽트와 소프트웨어 인터럽트가 있다.
일반적으로는 하드웨어 인터럽트를 뜻하며, 두 방식은 CPU의 서비스가 필요한 경우 인터럽트로 요청을 보내는 방식까지는 동일하다.
하드웨어 인터럽트는 컨트롤러 등 하드웨어 장치가 CPU의 인터럽트 라인을 세팅하는 반면, 소프트웨어 인터럽트는 소프트웨어가 그 일을 수행한다는 차이점이 있다.
소프트웨어 인터럽트는 트랩(trap)이라 불리며, 그 예로는 예외상황(exception)과 시스템 콜(system call)이 있다.
예외상황은 프로그램이 비정상적인 작업 시도나 영역 외의 권한이 없는 작업을 시도할 때를 뜻한다.
시스템 콜은 사용자 프로그램이 운영체제 내부에 정의된 코드를 실행하고 싶을 때, 운영체제에 서비스를 요청하는 방법이다.
인터럽트 핸들링
인터럽트 핸들링은 운영체제의 일종의 인터럽트 발생 시 처리할 일의 절차를 뜻한다.
프로그램 실행 중에 인터럽트가 발생하면 현재 상태를 저장을 해야 한다.
CPU에서 명령이 실행될 때, 레지스터에 데이터를 읽고 쓰는데 인터럽트가 발생하여 처리하러 가면 새로운 명령으로 인해 기존의 레지스터의 값이 지워지므로 현재 상태를 저장한 뒤 인터럽트를 처리해야 한다.
운영체제에서는 이 상태를 저장할 수 있는 공간이 있는데, 프로세스 제어블록(process control block, PCB)라고 하며 프로그램마다 하나씩 존재한다.
인터럽트가 발생하면 PCB에 현재 상태, 프로그램이 현재 어느 부분을 실행 중이었는지와 메모리 주소, 레지스터 값, 하드웨어 상태 등이 저장된다.
오늘날의 컴퓨터에서 운영체제는 인터럽트가 발생 시에만 실행되어 사용자 프로그램이 CPU를 원하는 만큼 점유할 수 있다.
결론적으로, 운영체제가 CPU를 점유하는 경우는 인터럽트가 발생했을 때 뿐이다.
입출력 구조
입출력 방식에는 동기식 입출력과 비동기식 입출력이 있다.
동기식 입출력은 입출력 연산을 수행할 때, 입출력 연산이 끝난 후에 그 프로그램이 후속 작업을 할 수 있다.
따라서 동기식 입출력에서 CPU는 입출력 연산이 끝난 후에야 프로그램에게 제어권이 넘어가서 다음 명령을 수행할 수 있게 되므로, 입출력 연산이 완료되기 전까지 CPU의 낭비가 발생하게 된다.
CPU의 명령 수행 속도는 빠르지만, 입출력 연산은 상대적으로 느리다는 점 또한 CPU의 낭비를 초래할 수 있다.
그래서 입출력 연산이 수행될 때, CPU의 제어권을 바로 사용 가능한 다른 프로그램에 넘긴다.
예를 들면, 프로그램 A가 입출력 연산을 수행하면 CPU 제어권은 프로그램 B에게 넘기고 A가 입출력 연산이 끝나기 전까지 제어권을 넘기지 않는다.
운영체제는 이를 관리하기 위해 몇 가지 상태로 나누고 입출력 중인 프로그램의 경우 봉쇄 상태로 전환시킨다.
봉쇄 상태의 프로그램에게는 CPU를 할당하지 않는다.
프로그램이 동기식 입출력을 수행중일 때, CPU를 다른 프로그램에 할당하지 않는 경우에 동기화는 자동적으로 이루어질 수 있다.
하지만 넘기게 되었을 때를 생각해본다면 발생할 수 있는 문제가 있다.
만약 프로그램 A가 원래 1이던 파일의 내용을 3으로 바꾸는 입출력 연산을 수행한다고 할 때, CPU의 제어권을 프로그램 B에 넘긴다고 하자.
프로그램 B도 공교롭게도 같은 위치의 파일의 내용을 1 증가시키는 입출력 연산을 수행하게 된다고 했을 때, 2개 이상의 입출력 연산을 수행할 수 있다면, 컨트롤러가 B와 A의 연산 순서를 바꿔서 진행할 수도 있다.
즉, 원래는 1 -> 3 -> 4
가 되어야 하는데 1 -> 2 -> 3
이 될 수 있으므로 의도하지 않은 결과를 초래할 수 있다.
이를 위해서 동기식 입출력에서는 입출력 요청의 동기화를 위해 장치별로 큐를 두어 요청한 순서대로 처리할 수 있게 한다.
비동기식 입출력은 CPU의 제어권을 입출력 연산을 수행하는 프로그램에게 곧바로 다시 부여해주는 방식이다.
입출력 연산의 결과를 이용해서 다음 연산을 수행하지만 경우에 따라서는 그 데이터와 관련 없이 수행할 수 있는 명령들이 있기 때문에, 그러한 명령들을 먼저 처리할 수 있도록 CPU의 제어권을 곧바로 부여해주는 것이다.
일반적으로는 운영체제에게 입출력 요청을 할 경우 동기식 입출력 방식을 사용한다.
DMA
DMA(Direct Memory Access)는 CPU 대신에 로컬버퍼에서 메모리로 데이터를 읽어오는 작업을 하여, CPU에 발생하는 인터럽트의 빈도를 줄여서 CPU를 좀 더 효율적으로 관리할 수 있게 해주는 컨트롤러이다.
원칙적으로 메모리는 CPU에 의해서만 접근이 가능해서, CPU 이외의 장치가 접근하려면 인터럽트를 발생시켜서 CPU에게 작업을 요청해야 한다.
하지만 잦은 인터럽트로 인해 CPU의 업무가 방해받게 되어 CPU의 효율성을 떨어뜨리게 된다.
이러한 비효율성을 극복하기 위해 CPU 외에 메모리에 접근 가능한 장치인 DMA를 둔 것이다.
DMA는 데이터를 읽어올 때, byte 단위가 아닌 블록이라는 큰 단위로 읽어온 후, 다 읽어오면 인터럽트로 CPU에 알린다.
저장장치의 구조
저장장치에는 주기억장치와 보조기억장치가 있다.
주기억장치는 메모리가 있는데 전원이 꺼지면 데이터가 날라가는 휘발성의 RAM을 매체로 사용하는 경우가 대부분이다.
보조기억장치로는 디스크, CD, 플래시 메모리 등의 전원이 나가도 데이터가 날라가지 않는 비휘발성의 마그네틱 디스크를 주로 사용한다.
보조기억장치의 용도는 크게 두 가지로, 파일 시스템용과 스왑 영역용으로 구분된다.
전원이 나가도 유지해야 하는 정보들을 파일 형태로 보조기억장치에 저장하게 되므로 비휘발성의 디스크를 파일 시스템용으로 사용하는 것이다.
또한 스왑 영역용으로 앞서 배웠듯이, 프로그램의 필수 부분만 메모리에 올리고 안쓰는 부분은 스왑 영역에 저장해둔 뒤, 필요할 때 메모리에 올린다.
스왑 영역에 내려놓는 것을 스왑 아웃시킨다고 말한다.
스왑 영역으로는 하드디스크가 가장 널리 사용되며, 파일 시스템처럼 비휘발성 용도로 사용하는 것과는 다르게 프로그램 종료 시 삭제하는 메모리의 연장 공간으로서의 역할을 담당하는 점에서 구분된다.
저장장치의 계층 구조
저장장치의 계층은 빠른 저장장치부터 느린 저장장치까지의 단계적인 계층 구조로 이루어진다.
빠른 저정장치는 휘발성의, 수행 능력이 좋은 CPU 내부에 존재하는 레지스터, 메모리를 뜻하는데 단점은 단위 공간 당 가격이 높아 적은 용량을 사용한다.
느린 저장장치는 비휘발성의 디스크 등을 뜻하는데 대용량이긴 하지만 접근 속도가 느리다는 단점이 있다.
따라서 당장 필요한 정보는 빠른 저장장치에 넣어두어 수행 속도를 높이고, 그렇지 않은 정보는 느린 저장장치에 넣어둔다.
빠른 저장장치에서는 캐싱 기법을 통해 적은 용량으로도 효과적으로 전체 시스템의 평균적인 성능을 향상시킬 수 있다.
하드웨어의 보안
흔히 사용되는 운영체제는 다중 프로그래밍 환경 즉, 여러 프로그램이 동시에 실행될 수 있는 환경으로 프로그램이 다른 프로그램의 실행을 방해하거나 충돌하는 등의 문제가 발생할 수 있다.
따라서 하드웨어의 보안 기법이 필요한데, 하드웨어적 보안 유지를 위해 운영체제는 2가지의 모드를 지원한다.
첫 번째는 커널 모드로, 커널 모드에서는 위험을 초래할 수 있는 중요한 명령(연산)을 수행할 때 실행된다.
두 번째는 사용자 모드로, 일반적인 연산만 가능하도록 사용자 프로그램을 통제하여 보안성을 확보한다.
단, 사용자 프로그램에서 중요하고 위험을 초래할 수 있는 연산을 수행하면, 다른 프로그램이 실행 중일 땐 운영체제는 CPU 제어권이 없으므로 감시할 방법이 없는 문제가 발생한다.
따라서 하드웨어적 지원이 필요하였고, CPU 내부에 모드비트(mode bit)를 두어 사용자 프로그램을 감시한다.
모드비트는 0이면 커널모드, 1이면 사용자모드이며, CPU는 보안과 관련된 명령을 수행하기 전에 항상 모드비트를 확인한다.
운영체제에서 사용자 프로그램으로 CPU 제어권을 넘길 때, 모드비트를 1로 설정하여 사용자 프로그램에 제약을 걸 수 있다.
사용자 프로그램에서 보안이 필요한 중요한 명령을 수행해야 할 경우엔, 시스템 콜을 통해 운영체제에 요청하면 인터럽트 발생 시 모드비트는 자동으로 0이 세팅된다.
이러한 시스템 보안과 관련된 명령들을 특권명령이라 하며, 특권명령은 모드비트가 0일 때에만 수행할 수 있으며 즉, 커널모드에서 운영체제에 의해서만 수행할 수 있는 것이다.
각종 하드웨어 장치에서 보안이 유지되는 방식은 모든 입출력 명령은 특권명령으로 규정하여 사용자 프로그램이 직접 입출력을 하는 것을 차단한다.
사용자 프로그램이 입출력이 필요하면 운영체제에 요청하여 운영체제가 대신 수행하게 한다.
운영체제는 입출력 요청이 올바른 요청인지 확인한 후 입출력을 실행하므로 파일에 대한 보안을 유지할 수 있다.
메모리 보안
메모리도 같은 맥락에서 보안이 필요하므로 2개의 레지스터를 이용해 프로그램이 접근하려는 메모리 부분을 검증하여 메모리를 보호한다.
base register와 limit register이며, base register는 어떤 프로그램이 수행되는 동안 그 프로그램이 합법적으로 접근할 수 있는 가장 작은 메모리 주소를 저장한다.
limit register는 기준 레지스터로부터 접근할 수 있는 메모리의 범위를 저장한다.
사용자 프로그램의 불법적인 메모리 접근이 발생하면 예외상황을 일으켜 소프트웨어 인터럽트를 통해 CPU 제어권을 운영체제로 이양시키고, 운영체제는 해당 프로그램을 강제 종료시킨다.
위의 메모리 보호 기법은 하나의 프로그램이 메모리의 한 영역에 연속적으로 위치하는 단순화된 메모리 관리 기법을 사용하는 경우에 한정된다.
7장에서 다양한 메모리 기법을 배울 것이다.
메모리 접근 연산은 특권명령이 아니다.
다만, 프로그램이 메모리에 접근하기 전에 하드웨어 단에서 먼저 접근에 대한 검증을 한다.
사용자 모드에서는 2개의 레지스터로 검증하지만, 커널 모드에서는 메모리에 무한정 접근이 가능하다.
메모리 접근 명령은 특권명령이 아니지만, 레지스터에 값을 정하는 연산은 사용자 프로그램이 악의적으로 정할 수 있는 등의 보안적인 문제가 발생할 수 있으므로 특권명령으로 규정해야 한다.
CPU 보호
CPU는 하나밖에 없으므로 독점을 하게 되면 빼앗을 방법이 없다.
따라서 CPU는 타이머라는 하드웨어을 통해 관리를 한다.
타이머는 정해진 시간이 지나면 인터럽트를 발생시켜 운영체제가 해당 프로그램으로부터 CPU를 빼앗아 다른 프로그램에 이양한다.
타이머는 일정한 시간 단위로 세팅될 수 있으며 매 클럭 틱(clock tick) 때마다 1씩 감소하고, 0이 되는 순간 인터럽트가 발생하게 된다.
타이머를 설정하는 명령은 특권명령이며, 타이머는 시분할 시스템에서 현재 시간을 계산하기 위해서도 사용된다.
시분할 시스템은 CPU를 여러 프로그램이 분할하여 사용하는 시스템이다.
시스템 콜을 이용한 입출력 수행
하드웨어 보안에서 설명했듯이, 입출력 연산은 특권명령으로 사용자 프로그램이 입출력 연산을 하려면 시스템 콜을 통해 운영체제에게 인터럽트를 보내 요청해야 한다.
시스템 콜은 일종의 소프트웨어 인터럽트로서, 시스템 콜을 할 경우 트랩이 발생해 CPU의 제어권이 운영체제에 넘어가고 운영체제는 해당 시스템 콜을 처리하기 위한 루틴으로 가서 정의된 명령을 수행한다.
예를 들어, 시스템 콜이 디스크 입출력 요청이면 디스크 컨트롤러에게 입출력 요청을 수행하도록 명령하고, 일을 마치면 CPU에 인터럽트를 보내 완료됬음을 알림으로써 해당 프로그램이 다시 CPU를 할당받을 수 있도록 한다.