본문 바로가기

CS ﹒ Algorithm/Computer architecture

컴퓨터 구조 (8) CPU 형태/ 운영체제 기반 지식(메모리, 프로세스 관리)

 

Big sur 이후로는 맥 유저도 윈도우 유저랑 같았다고 합니다 ^^~~~~~

 

 

 

1.   CPU 구조

 

1-1. 폰 노이만, 하버드

 

 

 

(1) 폰 노이만 구조

 

- 최초의 프로그램 내장 방식 컴퓨터로, 저장된 프로그램이라는 개념이 최초 도입되었다.

- 현대의 PC도 외적으로는 모두 폰 노이만 구조를 따르고 있다.

- 데이터 메모리와 프로그램 메모리가 구분되어 있지 않고 하나의 버스로 모두 읽고 쓴다.

- 명령과 데이터가 같은 신호와 버스 메모리를 사용하기 때문에 하버드 구조보다 항상 한 번의 과정을 더 거쳐야 한다.

 

* 폰 노이만 병목 현상 : 메모리의 값을 읽고 쓰는 구조이기 때문에 CPU보다 기억장치가 훨씬 느려서 병목 현상이 생기는 것

 

(2) 하버드 구조

 

- 시초는 릴레이 컴퓨터였던 Havard Mark I에서 유래된 것이나, 그 때부터 계속해서 이어져온 구조는 아니다.

- CPU 성능이 기억 장치(메모리)의 속도에 비해 크게 향상하게 되며 CPU가 Memory를 기다리는 "폰 노이만 병목 현상"이 생김에 따라 데이터 메모리용 버스와 명령어 메모리용 버스를 분리해서 사용하게 되었고, 이것을 하버드 구조라고 한다.

- 명령어와 데이터를 동시에 처리할 수 있다.

- 회로가 복잡하고 비용이 증가하지만 속도는 폰 노이만 구조보다 유리하다.

 

 

(3) 수정된 하버드 구조

 

- 기존의 통합 캐시 메모리를 분리하여 하나의 블록 사이클에서 적재(Load)와 저장(Store) 명령어를 동시에 실행할 수 있게 변경되었다.

- 주 기억장치 내부에서는 하버드 구조(캐시메모리), 외부적으로는 폰 노이만 구조(DRAM)이 적용되었다.

- 현대의 범용 CPU는 대부분 이런 구조를 차용하고 있다.

 

 

 

 

1-2. MCU, MPU, SOC

 

 

 

(1) MCU (Micro Controller Unit)

 

- CPU, 메모리, I/O(입출력 제어), 주변 장치 컨트롤러가 모두 하나의 칩셋에 패키징되어 있는 형태.

- 일반적인 가전제품 (전기밥솥, 식기 세척기) 등에서 찾아볼 수 있으며, 컴퓨터 관련으로는 "아두이노"가 가장 찾기 쉬운 예시.

- 현대에는 SOC와 혼용되어 사용되기도 한다.

 

(2) MPU (Micro Processor Unit)

 

- CPU를 하나의 단일 IC칩에 집적시켜 만든 반도체 소자를 의미.

- MCU와 달리 I/O Interface, Memory 등 주변 다른 부품들이 필요하며 MPU는 연산 처리가 주요 역할이다.

- 범용 CPU에 많이 채택되는 방식이라 일반적으로 MPU를 CPU라고 부르는 경우가 많다.

 

(3) SOC (System On Chip)

 

- 하나의 칩에 시스템이 모두 포함되어 있다는 의미.

- ARM, GPU, ROM, RAM, System Controller 등 모든 것을 포함하고 있는 초소형 컴퓨터의 개념.

- MCU처럼 특정 목적을 위해 만들어진 것이 아닌 MPU처럼 사용 가능.

- 스마트폰, 라즈베리파이, 애플의 ARM 맥북 시리즈에서 찾아볼 수 있다.

 

 

 

1-3. MMU

 

 

MMU는 Memory Management Unit, 즉 메모리 관리 장치이다.

메모리 관리의 핵심적인 역할을 담당한다. 자세한 내용은 하단의 가상 메모리에서 마저 다룬다.

 

- 가상 메모리 주소를 물리 주소로 변환해준다.

- 사용자 프로그램이 OS 영역을 침범하는 것을 차단한다.

- 캐시 가능 영역과 불가 영역을 설정한다.

- 실행 중인 프로그램에 임의로 읽기/쓰기 접근하는 것을 방지한다.

- 각 프로세스 별 할당된 메모리 영역만 접근할 수 있도록 통제한다.

 

 

 

 

 

 

2. OS 구조

 

 

 

2-1. 프로시저, 서브루틴, 함수

 

함수 (Function) : 자신의 이름이 있으며 하나의 반환값을 가진다.

서브루틴 (Subroutine) : 자신의 이름이 있으면 반환 값이 없다.

프로시저 (Procedure) : 함수와 서브루틴을 통틀어 프로시저라고 한다.

 

사실 위의 셋은 현 시점에서 구별해서 사용하지 않기 때문에 동의어나 마찬가지이다.

(RDBMS의 프로시저를 떠올리지 마라.)

 

 

함수는 위와 같이 작동한다.

- 프로그램 함수의 실행 후 돌아올 메모리 주소를 예측해서 간접 주소로 저장.

- 누산기에서 함수 연산.

- 모든 코드의 값을 실행하면 미리 저장해놓은 간접 분기를 이용해 원래 위치로 돌아온다. 

 

* 연산 과정을 줄이기 위해 함수 반환 위치를 저장하는 레지스터를 탑재하는 경우도 있다. 호출 스택에 저장하지 않기 때문에 효율적이다.

 

 

 

2-2. 인터럽트

 

 

 

만약 CPU가 당장 처리해야할 문제가 생겼는데, 프로그램이 끝날 때까지 대기할 수는 없을 것이다.

따라서 프로그램 실행 도중 Interrupt(방해하다)하고 문제를 처리해야 하는데, 이를 단어 그대로 인터럽트라고 한다.

 

과거의 컴퓨터는 입출력이나 장치 예외 등의 문제가 생겼을 때를 대비해 프로그램 실행 중 일정 간격으로 계속해서 체크하는 방식을 채택했다.

이를 폴링(polling) 방식이라고 하는데, 당연히 계속해서 체크하는 것으로 인해 성능 저하가 발생하여 이제는 사용하지 않는다.

하지만 소프트웨어 내부적으로는 더이상 효율적으로 처리할 방법이 없다.

 

따라서 현재는 하드웨어적 방법인 벡터 인터럽트(Vector Interrupt) 방법을 사용한다.

이는 인터럽트를 요청할 수 있는 장치와 CPU를 버스로 연결해 인터럽트를 요청한 요소의 번호를 CPU의 Pin(접점)에 전기 신호로 알리는 방식이다. ( 단, SOC의 경우 CPU 내부에 인터럽트가 탑재되어 있다. )

 

- (일반적으로) 기존 수행 중이던 명령어를 끝까지 실행한다.

- 현재 소프트웨어, 하드웨어 상태 등을 프로세스 제어 블록(Process Controll block)에 저장한다.

- Inturrpt Handler를 실행하여 적절하게 처리한다.

- 저장시킨 상태를 다시 불러온다.

 

참고로 인터럽트에는 전원이나 CPU의 기계적 결함 등 심각한 문제도 있으나, 하드웨어에서 자체적으로 인터럽트를 (우리가 눈치채지 못할 정도의 시간 동안) 타이머로 발생시키거나 정말 단순하게는 볼륨 조절, USB 연결, 접근 권한 요청 등도 모두 인터럽트 대상에 해당된다.

 

즉, 현재 프로세서에서 내부적으로 실행 중인 프로그램과 관계 없이 외부 H/W에서 입력되는 모든 정보는 전부 인터럽트 대상이다.

(만약 인터럽트 도중 다른 인터럽트가 발생할 경우 우선순위에 따라 처리 순서가 결정된다 => 볼륨 조절을 처리하다가 전원 문제가 생겼을 경우 전원 인터럽트 우선 처리)

 

 

 

2-3. 시분할 시스템 (TSS : Time-Sharing System)

여러 프로그램을 동시에 실행시켜줄 때 각 프로그램을 전환시켜줄 수 있는 관리자 프로그램을 커널(Kernel)이라고 부르며, OS를 시스템 프로그램, 다른 모든 프로그램은 사용자 프로그램(User Program)혹은 프로세스(Process)라고 부른다.

 

우리는 프로그램을 사용할 때 모든 것이 동시에 연속적으로 실행되고 있다고 느끼지만 실은 OS는 각 프로세스를 번갈아가면서 실행하고 있는 것이다. 

이운영체제에서 프로그램을 동시에 실행시킬 때 주로 사용하는 방식이 시분할 시스템인데, 이는 커널에게 전적으로 자원 제어에 대한 권한을 위임함으로써 프로세스의 유휴 시간을 극단적으로 줄여 사용자에게 프로세스가 계속해서 실행되고 있는 것처럼 느껴지게 한다. (물론 뛰어난 기억 장치 관리 기법 및 디스크 스케줄링 정책이 필요하다.)

 

왜 멀티 코어가 아닌 멀티 쓰레드 방식을 채택하는지 궁금할 수 있는데, 오히려 코어간에 프로세스가 이동하는 방식이 오버헤드를 발생시키기 때문에 더욱 비효율적이다.

또한, 시분할 시스템에서도 기본 방식이 멀티 쓰레드일 뿐 멀티 코어를 지원하는 프로세스는 멀티 코어로 실행된다.

* 오버헤드(overhead)란 처리를 위해 필요한 간접적인 처리 시간 및 메모리를 의미한다.

 

이 때 각 프로세스는 해당 프로세스가 배정된 메모리 공간에 스택 형태로 저장(Store)되고 불러와지는데 상대 주소 지정(relative addressing)으로 메모리에 저장되기 때문에 일정 메모리 영역을 사용할 뿐 계속해서 같은 메모리 주소에서 실행되는 것이 아니다.

 

어떤 프로그램이 메모리의 특정 주소를 계속해서 점거하고 있다면 얼마나 비효율적인지에 생각해본 적이 있는 사람이라면 이는 아주 놀랍거나 재미있는 사실일 것이다. 

(단, 상대 주소 지정 기법이 아닌 인덱스 레지스터라는 부품을 통해 유효 주소를 계산하는 경우도 있다. 그러나 이 때에도 메모리 주소가 변한다는 사실은 변함 없다.)

 

 

2-4. 가상 메모리

 

 

 

가상 메모리의 기본 개념은 프로세스는 가상 주소를 사용하고, 실질적으로 데이터를 Read/Write할 때만 물리주소로 변환해주면 된다는 것이다. 따라서 프로세스의 실제 메모리인 Physical Address와 Virtual Address가 필요한데, 이 때 둘을 매핑(Mapping)해주는 역할을 위에서 언급했던 MMU(메모리 관리 장치)가 담당한다.

 

우선 왜 페이징(Paging)이 필요한지에 대해 간단한 개념부터 설명해야 이해가 될 것이다.

만약 우리가 프로세스의 실행파일 전체를 DRAM에 올리고 사용한다면 로드 시간도 길어질 뿐더러 메모리 용량이 금방 가득차게 될 것이다, 따라서 프로세스의 코드를 page단위(약 4kb)로 나눠 프로세스가 요구할 때마다 이를 보내준다.

그런데 문제는 이런 과정에서 메모리 공간을 서로 다른 프로세스들이 덮어쓰기 때문에 메모리 주소가 엉망으로 뒤섞인다는 것이다.

이를 해결하기 위해 MMU는 가상 메모리와 물리 메모리를 매핑해주기 위해 (가상 주소 : 페이지가 위치한 실제 주소)의 형태로 Page table에 연속적으로 배치하여 프로세스 입장에서는 연속적인 메모리상에서 코드가 실행되고 있는 것처럼 보이게 만들어준다. 

 

두번째 사용처가 있는데, 바로 Disk Swap이다. 요청 받은 메모리가 사용 가능한 물리적 메모리 크기보다 더 클 경우 현재 필요하지 않은 페이지를 디스크에 옮기는 것이다(Swap out).  그리고 프로세스가 해당 페이지를 요청할 경우 메모리로 불러들인다.(Swap in)

이렇게 페이징 하는 것을 Demand Paging이라고 하는데, 당연히 SSD를 사용한다고 해도 주 메모리(DRAM)에 비해 디스크는 엄청나게 느리기 때문에 시스템 성능이 크게 저하된다. 그래도 아예 실행하지 못하는 것보다는 나을 것이다.

자주 사용되는 page는 물리 메모리에 남겨두고 자주 사용되지 않는 page는 디스크에 저장하는 기법도 있다.

 

(여담이지만 나의 어린 시절, HDD를 이용하던 당시에는 게임할 때 가상 메모리를 사용하면 너무너무 느려서 실행이 안되는 것만 못했기에 일부러 가상 메모리 공간을 완전히 없에고 사용하는 방법이 유행이였다.)

 

 

 

 

2-5. 시스템(Kernal) 영역과 사용자 영역

 

운영 체제를 보호하기 위한 기법으로, 커널 모드와 사용자 모드를 두어 일반 유저가 무분별하게 시스템 자원으로 접근할 수 없도록 하는 것을 Dual Mode라고 한다.

 

이중 모드는 커널 모드와 사용자 모드가 있으며, 커널 모드는 시스템 모드, 특권 모드라고도 불린다.

"특건 명령"은 오직 커널 모드만 사용할 수 있는 명령으로 유저가 사용했을 때 하드웨어에 문제를 발생시킬 수 있는 명령을 사용할 수 없도록 강제한다.

커널 모드는 일반적으로 운영체제가 실행 될 때, 인터럽트가 발생했을 때 실행되며 일반적인 프로그램 실행 환경은 유저 환경으로 구별할 수 있다.

 

일반적으로 커널은 입출력장치, 메모리, cpu의 보호를 위해 동작한다.

이 중 현재 배우고 있는 내용과 가장 관련이 있는 것은 메모리 보호로써, 프로세서가 특정 프로그램을 실행 중일 때 만약 해당 프로세스의 영역이 아닌 다른 프로세스의 메모리 영역의 주소 값을 가져왔다면 MMU가 이를 감지하고 인터럽트를 발생시킨다.

 

이외 입출력 장치 보호는 일반적으로 허락되지 않은 H/W로의 접근을 막으며 CPU 보호는 무한 루프 등 특정 프로세스가 CPU를 점유하는 것을 timer를 이용하여 체크하고 강제로 취소시킨다.

 

이런 방식으로 인해 사용자 프로그램으로부터 운영체제를 보호하고, 사용자 프로그램이 MMU 등의 요소에 영향을 줄 수 없기 때문에 운형체제가 프로그램에 대한 자원 할당을 엄격하게 제어할 수 있다.

 

 

 

 

2-6. 메모리 계층과 성능 관리

 

 

 

가상 메모리 부분을 읽으며 " 어.. 그런데 이거 너무 느려지는 거 아닌가? 이 중으로 탐색해야 하는데.. "라고 생각한 사람도 있을 것이다.

그것은 사실이며, 그 부분을 완화하기 위해 MMU에 TLB(Translation Lookaside Buffer)를 탑재하고 있다.

TLB는 가상 메모리 주소를 물리적 주소로 변환하는 속도를 높여주기 위한 캐시로 가상 주소를 물리 주소로 변환해야 할 때 TLB에서 우선 검색하고, 찾지 못했다면 페이지 테이블에서 찾는다.

만약 존재할 경우 해당 값은 다시 TLB에 쓰이고 해당 주소를 물리 주소로 변환한 후 메모리에 접근한다.

존재하지 않을 경우 Disk에서 찾아 DiskSwaping 후 Page table에 작성한 뒤 TLB에 작성하고 메모리에 접근한다.(결과적으로 늘 메모리에 접근하는 것은 TLB라는 캐시이다.)

 

이 때 만약 처음부터 TLB에서 물리적 주소 탐색에 성공하는 것을 TLB Hit라고하고, 탐색에 실패하면 TLB Miss라고 한다.

이 부분은 굉장히 중요한데, CPU Memory Controller는 연속된 열에 있는 데이터를 한 번에 가져온다.

일반적으로 연속된 위치에 있는 데이터가 사용되기 때문이다.

 

이런 이유로 List와 LinkedList 중, List를 사용할 수 있는 상황이면 List를 권하게 되는 것이다.

현대의 CPU는 굉장히 빠르기 때문에 연산 속도나 메모리 공간을 효율적으로 활용하는 것보다 캐시 메모리를 효율적으로 활용하여 메모리에서 얼마나 빨리 가져올 수 있는지가 중요하다. 그러나 Linked List는 데이터가 연속성 없이 저장되기 때문에 Cache Miss를 발생시킬 확률이 높다. 

 

 

 

2-7. Coprocessor

 

코 프로세서는 CPU의 기능을 보충하기 위해 사용되는 프로세서로써, 과거에는 부동 소수점 연산, I/O 등의 간단한 연산을 처리했으나 현재는 그래픽, 신호 처리, 암호화, I/O등 다양한 기능을 종합적으로 수행하고 있다.

 

 

 

2-8. 운영체제의 데이터 배치

 

 

 

(1) 코드 영역

: 실행할 프로그램의 코드가 저장되는 영역으로 프로세스의 시작부터 종료 시점까지 메모리를 점유한다.

 

(2) 데이터 영역

: 전역 변수 및 정적 변수가 저장되는 영역으로 프로세스의 시작부터 종료 시점까지 메모리를 점유한다.

(자바스크립트에서 전역 변수 사용을 지양하라고 하거나, 자바에서 자주 사용하지 않는 static 클래스 생성 대신 다른 방법을 찾아보라고 하는 것도 이런 이유다.)

 

(3) Heap 영역

: 대다수의 프로그램은 동적인 데이터를 다루는데, 이런 동적인 데이터는 Heap 영역에 저장된다.

메모리의 낮은 주소부터 높은 주소로 순차적으로 할당되며 LIFO(Last In Last OUT) 방식으로 동작한다.

자바와 C에서 new로 생성하는 인스턴스가 Heap 영역에 저장되며 일정량 이상 데이터가 쌓일 경우 C에서는 직접 메모리를 해제하고 Java는 Garbage Collector가 메모리를 해제시킨다.

 

(4) Stack 영역

: 스택 영역은 컴파일 시 크기가 결정되기 때문에 첫 로드 이후 더 이상 크기가 커지지 않는다.

지역 변수와 매개 변수, 등 함수 호출에 대한 정보가 들어간다. 또한 힙 영역에 비해 상대적으로 접근이 빠르며 변수를 명시적으로 할당 해제할 필요가 없고 메모리 공간의 낭비가 없다.(연속적 메모리)

 

=> 단 메모리 용량을 차지하는 부분에 대한 설명은 모두 원론적인 이야기이며, 현 시점의 PC는 예전에는 상상도 할 수 없을 만큼의 고용량의 메모리가 보편적으로 장착되어 있다.

따라서 상황에 따라 오히려 어느 정도의 데이터는 일부러 메모리에 쌓아놓고 쓰는 것으로 추세가 변했다. 데이터를 읽고 쓰는데 캐시메모리가 낭비되는 것이 성능에 더 큰 영향을 미치기 때문이다.

대표적으로 자바의 Heap 공간이 줄어들고 Native Memory로 대체된 것을 예로 들 수 있겠다.

 

 

 

 

 

2-9. Static Linking(정적 링킹)과 Dynamic Linking(동적 링킹)

 

 

(1) 정적 링킹

: 파편화된 파일을 모아 실행 가능한 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사한다.

만약 파편화된 프로그램 중 5개에서 동일한 A라는 외부 함수를 이용하는 데, 이 때 정적 링킹 방식으로는 5개의 실행 가능한 파일 각각에 A의 정보가 담긴다.

파일 내부에 함수가 존재하기 때문에 함수를 변경하려면 실행 파일도 다시 생성해야 한다.

메모리 용량을 많이 사용한다는 단점이 있으나 내부에서 실행하기 때문에 속도가 빠르다.

 

(2) 동적 링킹

: 실행 가능한 파일을 만들 때 라이브러리 모듈을 복사하지 않고 주소만 가지고 있다가 런타임 시 라이브러리가 위치한 메모리 주소에서 필요한 함수를 가져오는 방식으로, 운영체제에 의해 작동한다.

당연히 메모리와 디스크 공간을 아낄 수 있으며 함수가 외부에 존재하기 때문에 다시 컴파일할 필요가 없다.

그러나 매번 주소를 따라가 함수를 가져오기 때문에 정적 링킹 방식보다 느리다.

또한 함수가 외부에 존재하기 때문에 프로그램이 함수가 삭제되었음에도 예외 처리 없이 실행될 경우 의도와 다르게 작동할 수 있다.

* 자바는 동적 링킹 방식을 따른다.

* .ddl 파일이 바로 함수들이 저장되어 있는 라이브러리 파일이다.