Search
🤖

시스템 구조 & 프로그램 실행

컴퓨터 시스템 구조

1.
CPU + Memory를 (좁은 의미의) 컴퓨터라고 한다.
2.
나머지는 IO 디바이스로 이루어져 있다. Input은 입력, Output은 출력이다.
3.
CPU는 매 클럭 사이클 마다 인터럽션interruption을 수행해 input을 읽어오고, output을 수행한다.
조금 더 자세히 살펴보자.

디바이스 컨트롤러

각 I/O 디바이스마다 디바이스 컨트롤러가 있다. 해당 장치를 관리하는 일종의 작은 CPU이다. 제어 정보를 위해 control register, status register를 가진다.
이 디바이스 컨트롤러는 작업 공간이 각각 필요하기 때문에, 로컬 버퍼local buffer라고 하는 메모리 공간을 가지고 있다.

디바이스 컨트롤러의 필요성

CPU가 직접 입출력을 처리하려고 하면 느리기 때문에 디바이스 컨트롤러가 필요하다. 디스크를 예로 들면 CPU의 작업속도가 디바이스 컨트롤러에 비해 100만배는 느리다.
그렇다면 CPU에서 하는 일은 무엇인가? 여러 IO 장치에서 오는 인터럽션을 차례대로 읽어 수행하는 일이다.

device driver(장치구동기)

1.
device driver(장치구동기) - OS 코드 중 각 장치별 처리루틴 → software
2.
device controller(장치제어기) - 각 장치를 통제하는 일종의 작은 CPU → hardware

레지스터

CPU 안에는 메모리보다 빠르면서 정보를 저장할 수 있는 레지스터가 있다.

프로그램 카운터

이 중 프로그램 카운터(Program Counter)는 실행해야할 메모리 주소를 담고 있는 정보 저장소이다. CPU는 켜지면 꺼질 때 까지 프로그램 카운터에 있는 프로그램 인스트럭션을 실행을 반복적으로 수행하는 단순 반복 기계이다. 인스트럭션은 4Byte이기 때문에 프로그램 카운터는 인스트럭션을 수행하고나면 프로그램 카운터에 있는 memory 주소값을 4바이트 증가시키는 방식으로 다음 인스트럭션을 수행한다.

DMA controller

Direct Memory Access Controller의 준말.

역할

모든 I/O 작업이 일어날 때마다 인터럽트가 일어나면 CPU에 인터럽트가 너무 자주 걸리게 된다. 해당 오버헤드가 작지 않으므로, CPU와 디바이스 컨트롤러 로컬버퍼의 중간에서 인터럽트 처리 과정에 필요한 전작업들을 처리하여 CPU가 인터럽트에 시달리지 않도록 해주는 장치이다.

동작원리

빠른 입출력 장치를 메모리에 가까운 속도로 처리하기 위해 사용한다.
CPU의 중재 없이 device controller가 device의 local buffer storage의 내용을 메모리에 block 단위로 직접 write한다.
CPU에게는 block 단위로 인터럽트를 한번만 건다.

메모리 컨트롤러

CPU에 의한 메모리 접근과, DMA Controller로 인한 메모리 접근 순서를 정하는 장치.
메모리에 접근할 수 있는 하드웨어가 두 가지 (CPU와 DMA Controller)이기 때문에, 데이터의 일관성을 위해 메모리 컨트롤러가 메모리 접근을 중재를 한다.

모드 비트

정의와 역할

모드 비트(Mode bit)도 있는데, 현재 CPU를 사용하는 것이 사용자 프로그램인지 OS인지 구분을 해주는 역할이다.
사용자 프로그램의 잘못된 수행으로 다른 프로그램 및 운영체제에 피해가 가지 않도록 하는 보호장치이다.
코드
역할
1
사용자 모드, 사용자 프로그램 수행
0
모니터 모드, OS 코드 수행
보안을 해칠 수 있는 중요한 명령어는 모니터 모드에서만 수행가능한 특권명령(privileged instruction)으로 규정된다. 모니터 모드는 커널모드 혹은 시스템모드로도 불린다.
모드비트가 있음으로 해서, 권한이 없는(모드 비트가 1인) 사용자 어플리케이션은 자기 메모리 영역 내에 있는 주소에만 접근이 가능하다.

동작 방식

interrupt나 exception이 발생하면 하드웨어가 모드 비트를 0으로 바꾼다.
사용자 프로그램에게 CPU를 넘기기 전 모드 비트를 1로 세팅한다.

인터럽트 요청 라인

CPU 하드웨어에는 인터럽트 요청 라인(interrupt request line)이 있다. 이것이 존재하는 이유는 인터럽트가 어떤 순서로 이루어지는지, 인터럽트가 완료되었는지 확인하는 역할이다. 다음과 같은 순서로 수행이 된다.
1.
발생(raise) : 디바이스 컨트롤러가 인터럽트 요청 라인에 신호를 보낸다.
2.
포착(catch) : 앞선 인터럽트 요청을 수행하던 CPU가 앞 요청을 다 수행하면, 인터럽트 요청 라인을 읽어 다음 인터럽트 번호를 알아낸다.
3.
디스패치(dispatch) : 이 번호를 통해 인터럽트 벡터의 인덱스로 활용하여 인터럽트 핸들러 루틴(interrupt_handler routine)으로 점프한다.
4.
정리(clear) : 해당 인덱스와 관련된 주소에서 실행한다. 인터럽트 처리기는 작업 중에 변경될 상태를 저장하고, 인터럽트 원인을 확인하고, 필요한 처리를 수행하고, 상태 복원을 수행하고, return_from_interrupt 명령어를 실행하여 CPU를 인터럽트 전 실행 상태로 되돌린다.
인터럽션이 수행, 인터럽션 주소값을 추가하여 다음 인터럽션을 수행한다.

인터럽트

인터럽트 당한 시점에 레지스터와 program counter를 save 한 후 cpu의 제어를 인터럽트 처리 루틴에 넘기는 것을 말한다.
소프트웨어로부터 발생된 것을 Trap, 하드웨어(타이머, I/O 디바이스)로부터 발생된 것을 Interrupt라고 하는데, 둘을 합쳐 (넓은 의미의) Interrupt라고 부른다.

관련 용어

인터럽트 벡터
해당 인터럽트의 처리 루틴 주소를 가지고 있음
인터럽트 처리 루틴 (== Interrupt Service Routine, 인터럽트 핸들러)
해당 인터럽트를 처리하는 커널 함수

타이머

정의

하드웨어에는 타이머도 있는데, 특정 프로그램이 CPU를 독점하는 것을 막기 위한 장치이다. time sharing을 가능하도록 하기 위해 널리 이용된다.

동작 원리

보통 프로그램은 작업이 끝나면 프로그램을 종료하고 CPU를 반환한다. 하지만 만약 특정 프로그램이 CPU를 완전 독점하고 종료되지 않는 인스트럭션을 수행하고 있다면, OS 입장에서도 해당 프로그램을 종료할 방도가 없다.
그래서 OS는 항상 시분할성(동시에 여러 프로그램이 돌아가는 것)을 확보하기 위해 타이머를 두었다. 이 타이머는 사용자 프로그램에 CPU가 넘어갈 때 timeout 시간(100ms 라던지)을 설정한다. 이 타임아웃 시간은 매 클럭 틱 때마다 1씩 감소한다.
만약 설정한 시간이 지나도(타이머 값이 0이 되어도) 프로그램 인스트럭션이 끝나지 않았다면, 타이머 인터럽트를 발생시켜 OS가 제어권을 가져오도록 한다.
현재 시간을 구하기 위해서도 사용한다.

CPU의 IO 디바이스 작동 방식

CPU는 memory에서 읽을 뿐이다. 만약 메모리에서 필요한 데이터가 없고 디스크에 있다면, CPU는 디스크에서 직접 읽지 않고 디스크 디바이스 컨트롤러에게 읽어오라고 명령한다.
현재 CPU에서 실행중인 프로그램의 인스트럭션이 키보드 입력을 요청한다면? 보안상의 이유로 사용자 프로그램은 직접 IO에 접근할 수 없고 CPU를 거쳐야 한다. 그런 이유로 IO 작업을 해야 하면 사용자 프로그램은 운영체제에게 CPU를 넘겨준다(시스템 콜). CPU는 키보드 디바이스 컨트롤러에게 해당 일을 맡기고, 끝날 때 까지 기다리지 않고 다음 일을 처리한다. (IO 작업은 오래 걸리기 때문이다)
하지만 그 ‘다음 일’은 CPU를 넘겨줬던 어플리케이션에 제어권을 돌려주는 것이 아니다. IO 작업의 결과물이 있어야 작업을 처리할 수 있을테니!
CPU 스케줄링 상 안배되어있는 바로 다음 작업으로 넘어간다.
그렇다면 IO를 작업했던 프로그램은 어떻게 제어권을 되찾는가? 디바이스 컨트롤러에서 키보드 입력이 완료되었다는 인터럽트(하드웨어 인터럽트)가 들어오면, cpu는 디바이스 컨트롤러 로컬 버퍼에 있는 내용을 메모리에 copy 해준다. 이후 프로그램은 ‘작업 가능한 상태’가 되고, 스케줄링에 따라 다음 차례 때 CPU 제어권을 얻어올 수 있다.

시스템 콜

시스템콜(System call)은 사용자 프로그램이 운영체제에게 I/O 요청을 하는 것을 의미한다. 이것은 인터럽트의 일종이기 때문에 인터럽트 요청 라인 에서 서술한 발생, 포착, 디스패치, 정리의 순서를 따른다.
1.
trap을 사용하여 인터럽트 벡터의 특정위치로 이동한다.
2.
제어권이 인터럽트 벡터가 가리키는 인터럽트 서비스 루틴으로 이동한다.
3.
올바른 I/O 요청인지 확인 후 I/O를 수행한다.
4.
I/O 완료 시 제어권을 시스템 콜 다음 명령으로 옮긴다.
모드비트가 1인 어플리케이션이 OS의 메모리에 직접 접근하는 것은 안 되기 때문에 시스템 콜은 인터럽트 라인에 IO 인터럽트를 요청하는 커널함수를 호출(트랩)을 하는 방식으로 이루어진다.

동기식 입출력과 비동기식 입출력

동기식 입출력 (synchronous I/O)
I/O 요청 후 입출력 작업이 완료된 후에 제어가 사용자 프로그램에게 넘어감
구현법 (동기 / 블락킹)
I/O가 완료될 때까지 해당 프로그램에게서 CPU를 빼았음
I/O 처리를 기다리는 줄에 그 프로그램을 줄 세움
다른 프로그램에게 CPU를 줌
비동기식 입출력 (asynchronous I/O)
I/O가 시작된 후 입출력 작업이 끝나기를 기다리지 않고 제어가 사용자 프로그램에 즉시 넘어감
a느 하드웨어의 인터럽트가 올 때 까지 CPU를 받지 않는다. b는 바로 cpu 제어권을 받아와 로직을 수행하고, I/O작업이 끝나면 인터럽트로 부터 데이터를 받을 수 있다.

서로 다른 입출력 명령어

I/O를 수행하는 별도의 인스트럭션이 있는 포트 맵 입출력(Port Mapped I/O) 방식과, 메모리 맵 입출력 (Memory Mapped I/O) 방식이 있다.
왼쪽이 포트맵, 오른쪽이 메모리맵
각각 장단점이 있다. 링크

포트 맵 입출력

어드레싱 능력이 제한된 CPU를 사용할 때 장점이 있다. 입출력 인스트럭션이 메모리 접근과 분리되어 있기 때문에 메모리용으로 주소 영역 전체를 사용할 수 있다. 또 다른 장점은 어셈블리어로 된 소스를 볼 때, 언제 입출력이 수행되는지 알아보기 쉽다. 입출력 전용 명령어를 써서 입출력을 수행하기 때문이다.

메모리 맵 입출력

포트 입출력을 구현할 때 필요한 부수적인 복잡성(메모리 접근과 입출력 장치 접근을 분리해야 하는)이 없어진다. CPU 내부 로직이 덜 필요하므로(RISC) 더 저렴하고 더 빠르고 더 쉽게 CPU를 만들 수 있다. 장치를 어드레싱하기 위해 일반적인 메모리 명령어를 사용한다는 것은 메모리 뿐 아니라 입출력을 위해서도 모든 CPU 어드레싱 모드가 지원된다는 것을 의미한다.
하지만 메모리 맵 입출력은 주소와 데이터 버스를 많이 사용하게 된다. 그래서 보통 메인 메모리에 접근하는 것보다 매핑된 장치에 접근하는 것이 포트맵보다 더 느리다.

저장장치 계층 구조

올라갈수록 접근 속도가 빠르고, 비싸다. Secondary Disk에 SSD(반도체 기반)가 들어왔음을 인지하자
Primary는 CPU가 직접 접근할 수 있는 메모리를 의미한다. 바이트 단위 접근이 가능하다. 값이 Secondary에 비하여 매우 비싸며, 휘발성이 있다. 계층 내에서도 register가 가장 비싸며 가장 빠르고, Main Memory(D-RAM)가 Primary 중 가장 느리다. register와의 메인 메모리간의 속도차이가 크기 때문에, Cache Memory를 중간에 두어 메인 메모리에서 읽어온 값을 저장한다. CPU는 메인메모리에 들리기 전 캐시 메모리에 들려 캐시 여부를 확인한다.
Secondary는 CPU가 직접 접근하지 못하고 섹터 단위의 접근을 해야 한다. 비교적 저렴하며, 영구적으로 저장된다. 계층 내에서도 이미지엔 없지만 SSD가 가장 빠르고 비싸며, 자기 테이프가 가장 저렴하다.

프로그램의 실행

프로그램은 어떻게 실행이 되는가? 프로그램은 실행파일 형태로 하드디스크에 저장이 되어있다. (위에서 서술한 영구적 저장장치) 이후 물리적 메모리에 실행파일을 올려(복사해 와서) CPU에서 직접 접근하고, 해당 메모리 주소에 적혀있는 CPU가 실행함으로써 프로그램이 작동하는 것이다.
의문이 들지 않을 수 없다. 메모리(램)가 하드디스크보다 훨씬 작다. 100GB에 육박하는 GTA5 게임을 어떻게 램이 16GB인 컴퓨터에서 실행(Copy 후 인스트럭션 실행) 시킬 수 있는가?

가상메모리

위 문제를 해결하기 위해 가상 메모리라는 개념을 활용한다. 커널(OS)은 메모리에 전체 내용을 상주시키지만, 사용자 어플리케이션은 가상 메모리를 활용해 메인 메모리를 사용한다. 가상 메모리는 물리적으로 존재하지 않는 , 각 프로세스마다 독자적으로 가지고 있는 메모리 주소 공간이다.
프로그램을 실행할 때 자주 사용하는 부분만 메인 메모리에 올리고 나머지는 내용은 하드디스크의 가상메모리 기법을 위해 안배된 공간인 Swap area에 배치한다. CPU는 가상메모리에 접근함으로써 마치 프로그램이 모두 메인 메모리에 올라와 있는 것처럼 활용하지만, 사실은 가상 메모리 주소값이 가리키는 곳에 따라 일부는 메인 메모리에, 일부는 Swap Area에 있는 메모리에 접근하는 방식으로 활용된다.
// to be
가상 메모리 주소가 어떤 물리 메모리 공간에 가리키는지 맵핑해주는 기법이 있는데, 추후 서술

커널 주소 공간의 내용

1.
커널 코드 : 시스템 콜이 일어났을 때 어떤 함수를 호출해야 되는지 코딩되어 있는 부분
2.
데이터 영역 : 운영체제에서 활용하는 자료구조들이 있는 부분. CPU, 메모리, 디스크 같은 물리 메모리를 관리하는 자료구조와 프로세스가 활용하는 독자적 주소공간을 관리하는 자료구조(PCB, Process Control Block)가 있다.
3.
커널 스택 : 프로세스마다 하나씩 생성되는 커널 스택. 사용자 어플리케이션(프로세스)가 커널의 함수를 호출할 때 활용하는 스택이다. 아래 도식을 보자.
A라는 프로그램이 시작되면 유저모드를 통해 실행이 된다. 시스템 콜을 하게 되면 커널 모드로 전환되는데, 커널 모드의 실행이 끝나면 돌아가야할 프로세스 주소값을 알아야 할텐데 이 때 활용되는 것이 커널 스택이다. 흔히 알고 있는 메인문으로 시작되는 프로그램 내부 스택과는 다르다.

프로그램 인스트럭션?

인스트럭션은 컴퓨터에게 일을 시키는 단위로서, 컴퓨터가 알 수 있는 기계어로 이루어진 명령어이다. 지시 또는 명령어 라고 부른다.
명령부와 처리부로 나뉜다.
1.
명령부는 실제 수행해야 할 동작을 나타내는 부분. 산술 및 논리 연산, 데이터 이동, 분기, 입출력, 그 외의 제어명령을 의미한다.
2.
처리부는 동작의 대상이 되는 데이터를 지정한다. 처리부는 피연산자(데이터)를 직접 나타내거나 피연산자가 기억된 레지스터 또는 기억장치의 주소이다.

깨달음

내 자바 어플리케이션이 디스크에 접근하려면 시스템 콜이 필요하다. heap 영역 내에서의 이동은 자유롭지만(객체 생성 등 자바 어플리케이션 내에서 일어나는 일), File IO등을 수행하려면 시스템 콜이 일어나 CPU로 제어권을 넘겨야겠지!

레퍼런스

운영체제 (공룡책)