컴퓨터 아키텍처와 운영체제
기본적인 구조 요소들
폰 노이만 구조와 하버드 구조
가장 흔한 컴퓨터의 구조 2가지를 꼽으라면 ‘폰 노이만 구조’와 ‘하버드 구조’일 것이다.
유명한 과학자 ‘폰 노이만’과 컴퓨터 ‘하버드 마크 I’의 이름을 딴 구조이름인데, 차이점은 하버드는 데이터 메모리와 프로그램 메모리 버스가 따로 있지만 폰 노이만은 데이터와 프로그램 메모리 버스가 함께 있다. 그래서 폰 노이만 구조는 속도가 느리고 하버드 구조는 두 번째 메모리를 처리하기 위한 버스가 더 필요하다
프로세서 코어
한 명이 일을 하는것보다 두 명이 일을 분업하는게 더 효과적이고 빠르다. 그래서 단일 CPU보다 더 좋은 성능을 얻기 위해 멀티 프로세서 시스템을 만들었다. 병렬처리 시스템으로 2개 이상의 CPU를 갖는 시스템이다. 그리고 반도체 회로의 크기가 줄어들면서 CPU를 더 빠르게 만듦으로써 더 나은 성능을 가지게 만들었다. 하지만 CPU가 더 빨라지고(열이 많아짐) 더 작아지면서 단위 면적당 열 발생이 많아졌다 그래서 2000년경 프로세서는 전력 장벽에 부딪혀서 또 다른 해결책을 찾아야했다.
그래서 작아진 회로의 크기를 활용해 새로운 해결책을 찾아냈다. 프로세서 코어가 여러개 들어간 멀티코어 프로세서를 사용하는것이다.
마이크로프로세서, 마이크로컨트롤러
마이크로프로세서(MPU)는 내부에 메모리와 I/O가 포함되어있지 않은 프로세서를 뜻하고, 마이크로컨트롤러(MCU)는 내부에 메모리와 I/O가 포함되어있는 프로세서를 뜻한다.
컴퓨터의 CPU를 마이크로프로세서라고 부른다. 마이크로컨트롤러에는 메모리와 입력장치가 함께 집약되어있기 때문에 MCU만으로도 여러가지 일들을 할 수 있지만, MPU는 할 수 있는 일이 제한되는 대신 물리적으로 칩 안에서 메모리가 차지하는 영역이 크기 때문에 일반적으로 MPU는 MCU보다 성능이 좋다.
보통 MPU는 큰 시스템의 부품으로, MCU는 식기세척기같은 단일 칩으로 된 작은 컴퓨터에 쓰인다.
프로시저, 서브루틴, 함수
프로시저, 서브루틴, 함수는 전부 한 가지를 말한다(언어에 따라 달라지는거라고 보면 된다).
개발자들은 ‘귀찮은 일을 하기 싫어서 귀찮게 프로그램을 만든다’라는 모순을 가지고 있는데, 1번만 타이핑하면 되는 일을 2번 3번 타이핑하기 싫어서 ‘함수’라는 수단을 사용해서 코드를 재사용한다.
중학교때 즈음에 배우는 y=x+3 같은 수학적인 ‘함수’와 기능이 똑같다. x나 y에 값을 넣으면 결과가 바뀌는 그런 기능을 가지고있다.
1
2
3
4
5
def add(x, y):
print(x+y)
add(3, 4)
add(5, 99)
아주 간단한 파이썬의 덧셈 함수를 만들어보았다. 중요한 건 ‘이 정도면 그냥 타이핑쳐도 되는거 아니야?’가 아니라 ‘이게 만약에 어려운 기능이었다면 코드 진행이 쉬워지겠구나’로 생각해야한다. 만든 함수를 호출한다고 하는데, 함수를 호출할 때에 진행되던 자리 → 함수 → 진행되던 자리의 식으로 진행된다.
스택
함수내에는 또 다른 함수를 집어넣을 수 있는데, 함수안에 자기 자신(함수)을 넣을 수도 있다. 이걸 재귀라고 하는데, 만약 이 재귀함수를 빠져나가는 구문을 넣어주지 않는다면 무한 루프에 빠지게 된다. 그리고 함수 호출을 하게 되면 메모리를 사용하는데, 무한 루프상태에서 무한으로 함수를 호출하다보면 메모리의 크기가 부족해지고, 스택 오버플로가 발생해서 프로그램이 종료된다.
스택 오버플로란 스택에 데이터를 푸시하는데 더 이상 들어갈 공간이 없을 때 발생하는 에러인데(반대의 경우 즉, 빈 스택에서 데이터를 가져오려고 하면 스택 언더플로가 발생한다), 여기서 스택이란 식당에 쌓아둔 접시 더미같은 녀석이다. 함수를 호출할 때 반환 주소를 접시에 넣어서 접시 더미 맨 위에 넣고, 함수 호출에서 돌아올 때 맨 위의 접시를 보고 반환 주소를 결정하고 접시를 제거한다(LIFOLast In First Out 구조라고도 함). 스택에 데이터를 푸시하고, 스택에서 데이터를 팝해서 제거한다.
각각의 함수에는 지역변수가 있을테고, 지역변수에 들어있는 값을 사용해야하는데 모두 독립적이어야한다. 그래서 스택 프레임이라고 하는 스택에 저장되는 데이터의 모음에는 반환주소와 스택이 같이 포함되어있다.
인터럽트
집에서 코딩을 한다고 가정해보자. 집에는 배달부가 나타날 수도, 경비원 아저씨가 나타나서 초인종을 눌러댈 수 있다.
1
2
3
4
5
6
7
시작
▽
코딩을 한다
▽
집에 누군가 왔는가? ▷예 방문자에게 대답한다
▽아니오
다음 일을 한다
방문자가 왔다고 생각했을 때, 코딩을 다 하고 방문자에게 대답하기 때문에 이 방문자가 문 앞에 얼마나 서있을지 알 수가 없다. 방문자의 참을성이 좋지 않은 이상 터져버릴 것이다.
그렇다면 이건 어떨까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
시작
▽
집에 누군가 왔는가? ▷예 방문자에게 대답한다
▽아니오
컴퓨터를 킨다
▽
집에 누군가 왔는가? ▷예 방문자에게 대답한다
▽아니오
텍스트 에디터를 킨다
▽
집에 누군가 왔는가? ▷예 방문자에게 대답한다
▽아니오
프로그램을 작성한다
▽
집에 누군가 왔는가? ▷예 방문자에게 대답한다
▽아니오
버그를 수정한다.
방문자가 기다리는 시간은 짧아지겠지만 계속해서 인터폰을 쳐다보고 있는 개발자는 개발에 집중하지 못할것이다. 이런 방법을 폴링이라고 하는데, 작동은 잘 하지만 검사하는데 너무 많은 시간을 소요한다.
그냥 코딩하다가 벨 울리면 나가면 되는거 아닌가요? — 맞다.
실행 중인 프로그램을 잠시 중단interrupt해서 다른 일을 진행하고 돌아오면 된다. CPU가 일을 하고 있을 때, 다른 주변 장치에서 인터럽트 요청을 생성한다. 프로세서는 현재 실행중인 명령어를 끝까지 수행하고(보통은, 일을 다 하는게 아니라 ‘명령어를 수행하는 것’임) 그 후에 현재 실행중인 프로그램을 잠시 중단하고 인터럽트 핸들러라는 전혀 다른 프로그램을 실행한다. 인터럽트 핸들러가 일을 다 마치고나면 원래 실행중이던 프로그램의 중단된 위치부터 다시 실행한다(인터럽트 핸들러는 함수다).
인터폰(초인종)을 인터럽트라고 생각하면된다. 인터럽트를 추가하기 위해서는 몇 가지 고려해야할 사항이 있는데
- 인터럽트에 대한 응답시간
- 인터럽트를 서비스(대응)하고 원래 프로그램의 위치로 가기위한 상태를 저장할 방법
이 때 인터럽트 시스템이라는 녀석이 서비스하고 나서 원래 프로그램의 위치를 스택에 저장한다.
상대 주소 지정
우리들이 컴퓨터를 키면 유튜브도 보고, 블로그도 보고, 게임도 하고 컴퓨터는 한 번에 여러 가지 일을 수행한다. 어떻게 여러 가지 프로그램을 동시에 실행할 수 있을까? 위에서 설명했듯이 프로그램을 서로 전환시켜주며 진행하면된다. 빠르게 전환시켜주면 동시에 실행하는 듯한 착각이 생길 것이다. 이런 전환시켜주는 관리자 프로그램을 OS(운영체제, 커널)라고 부르고, OS와 OS를 관리하는 프로그램은 시스템 프로그램이라고 부르고, 이하 다른 모든 프로그램은 유저(사용자) 프로그램이나 프로세스라고 부른다.
OS는 타이머 인터럽트를 실행시켜 프로그램을 전환해줄 때인지 판단한다. 이런 식으로 사용자 프로그램의 실행 시간을 조절하는 방식을 시분할 스케줄링 기법이라고 한다. 시간을 정해진 간격으로 나누고, 정해진 시간 간격 동안 사용자 프로그램을 실행하는 방식이다. 메모리와 프로그램을 왔다갔다해서 비효율적이라하면 비효율적이라, 프로그램들을 독립된 메모리 공간에 집어넣어서 독립된 공간을 마련해주면 꽤나 성능을 향상시킬 수 있다. 100번대는 1프로그램, 200번대는 2프로그램 식으로 말이다. 이런 예는 절대 주소 지정이라고 하는데, 명령어 주소가 특정 메모리 주소를 가리킨다는 뜻이다. 만약 1프로그램을 200번대에서 실행하려고하면 제대로 실행되지 않을 것이다.
인덱스 레지스터를 추가하면 위의 문제를(제대로 실행되지 않는) 해결할 수 있는데, 인덱스 레지스터의 값과 명령어에 들어있는 주소를 더해서 유효 주소를 계산(만약 1프로그램을 200번대에서 실행한다면 100(원래 주소)+100(인덱스 레지스터)로 유효 주소를 만듦)하는 식이다.
또 다른 방법으로는 상대 주소 지정이 있는데, 명령어에 들어 있는 주소를 0(보통 시작하는 위치)으로 해석하지 않고 명령어의 주소를 기준으로 하는 상대적인 주소로 해석한다. 예를 들어 명령어가 들어있는 주소가 1이라면 100이라는 주소는 1로부터 99만큼 떨어져있으니 상대 주소 지정의 값은 99가 되는 방식이다.
메모리 관리 장치
멀티태스킹은 현 세대의 컴퓨터를 사용하는 우리들은 당연하게 사용하고있는 방식이다. 상대 주소 지정과 인덱스 레지스터로 멀티태스킹에 도움을 주지만, 만약에 프로그램에 버그가 나서 1프로그램의 메모리를 2프로그램이 덮어씌우고, OS 메모리까지 덮어씌운다면? 만약 의도적으로 악성 프로그램으로 다른 사람의 프로그램을 훔쳐본다면? 각 프로그램을 분리해서 이런 시나리오를 없애기 위해 오늘날의 대부분의 마이크로프로세서에는 메모리 관리 장치, MMU가 들어가 있다.
MMU는 가상 주소와 물리 주소를 구분하는데, 프로그램은 가상 주소로 작성되고 MMU가 가상 주소를 물리 주소로 변환해준다.
이 MMU로 인해 폰 노이만 구조와 하버드 구조의 구분이 의미가 없어졌다. 단일 메모리 버스의 폰 노이만 구조 시스템도 MMU를 사용하면 명령어와 데이터 메모리를 분리해 사용할 수 있기 때문이다.
가상 메모리
MMU는 프로그램의 가상 주소를 물리 주소로 변환해준다는 사실을 알았다. OS는 MMU를 사용해 사용자 프로그램에게 가상 메모리 또한 제공한다.
만약 사용자 프로그램에서 요청받은 메모리가 컴퓨터가 가지고 있는 메모리보다(사용가능한 메모리보다) 크다면 OS는 현재 필요치않은 메모리 페이지를 더 큰 저장장치로 옮긴다(이를 스왑 아웃이라고 함). 이 스왑 아웃한 페이지에 프로그램이 접근하면 OS는 메모리 공간을 확보하고 이를 다시 메모리로 옮긴다(이를 스왑 인이라고 함). 이런 식으로 처리되는 것을 요구불 페이징, Demand Paging이라고 한다. 이렇게 스와핑이 일어나면 시스템의 성능이 저하되는데, 최소 최근 사용 알고리즘을 사용해서 페이지 접근을 추적해 스왑 아웃할 페이지를 결정해주어서 시스템 성능 저하를 최소화시킨다.
시스템 공간과 사용자 공간
멀티태스킹, MMU는 마치 여러 프로그램이 동시에 실행된다는 착각을 만드는데, 앞서 말했다시피 여러 개의 프로그램을 바꿔가며 사용하고 있는것이다. OS는 타이머 인터럽트로 프로그램을 전환할 시점을 알아내는데, 유저 프로그램이 이 타이머 인터럽트의 대기 시간을 바꿔버린다면 MMU는 프로그램을 서로 격리시키지 못할 것이다.
그래서 CPU는 시스템 공간과 사용자 공간을 분리시켜 이런 짓을 하지 못하게 막았다. CPU에는 컴퓨터가 시스템 모드에 있는지 유저 모드에 있는지 결정하는 비트가 어떤 레지스터 안에 들어있고, 중요한 기능을 건드리는 명령어는 시스템 공간에서만 작동하도록 설저오디어있다.
메모리 계층과 성능
세대가 지나감에 따라 CPU의 성능은 비약적으로 발전했고 메모리 또한 발전했으나 CPU의 발전속도를 따라가지는 못했다. CPU의 속도와 메모리의 속도의 차이가 날이 갈수록 심해져만 갔다.
이 때 캐시라는 하드웨어를 CPU에 추가해줘서 메모리를 기다리는 시간을 단축시켜주었다. CPU에서 멀어질수록 캐시는 더 느려지고 더 커지는데, 이걸 레벨로 구분해줘서 L1, L2, L3 캐시로 구분한다.
CPU를 사람이라고 하면 레지스터는 주머니, 캐시는 수납장, 메모리는 가게, 대용량 저장장치는 창고라고 보면 된다.
레지스터는 바로바로 접근할 수 있지만 용량이 적고, 메모리는 공간은 더 많지만 가져오려면 시간이 걸린다. 창고는 더 멀리 떨어져 있지만 공간이 아주 큰 곳이라고 생각하면 된다.
Expensive, but fast | CPU |
---|---|
↑ | Register |
│ | L1 Cache |
│ | L2 Cache |
│ | L3 Cache |
↓ | Memory |
Cheap, but slow | Storage device |
코프로세서
코프로세서는 프로세서 코어가 더 많은 일을 할 수 있게끔 단순한 연산을 도와주는 회로이다. 예를 들어 그래픽 처리를 담당하는 코프로세서, 부동소수점 수를 계산하는 코프로세서, 단순히 데이터 복사를 하는 코프로세서 등으로 말이다. 이 때 단순히 데이터 복사만을 하는 코프로세서를 DMA라고 부르는데, CPU는 DMA 장치에 귀찮은 일을 맡기기 때문에 더 유용한 연산을 많이 처리할 수 있다.
메모리상의 데이터 배치
메모리에는 명령어만 담기는게 아니라 데이터도 담긴다. 이 때 데이터는 정적static 데이터이고, 정적이라는 말은 프로그램을 작성할 때 얼만큼의 메모리가 필요한지 안다는 뜻이다. 하지만 채팅 프로그램이나, 유저의 개입에 따라 데이터의 크기가 달라지는 프로그램들은 정적이지 않다. 이 때 우리들은 이 데이터들을 동적 데이터라고 부르기로 했다. 대부분의 프로그램은 동적dynamic 데이터를 다뤄야하는데, 동적 데이터는 정적 데이터가 차지하는 영역의 바로 위 영역에 쌓이고, 이를 힙이라고 부른다. 보통 스택 ↔ 힙 → 정적 데이터 → 명령어인데 스택과 힙이 서로 ↔인 것은 스택은 아래로 뿌리를 내려가고 힙은 위로 뿌리를 올라가기 때문이다. 그렇기에 힙과 스택은 서로 충돌할 가능성이 있어서 힙과 스택을 서로 충돌하지 않게 하는 것이 중요하다.
프로그램 실행
프로그래머들은 함수를 사용해 재사용한다. 그리고 수 많은 프로그래머들은 공통된 함수를 재사용한다(복잡한 수학 계산, 파일을 여는 함수). 이럴 때는 매번 새로 함수를 작성하는 것보다 누군가 작성한 함수를 활용할 수 있으면 편할 것이다. 이걸 달성하는 방법 중 하나는 라이브러리를 활용하는 것인데, 관련 함수들을 한데 모아논 것을 말한다. 본격적인 프로그램은 라이브러리뿐 아니라 여러 조각으로 이루어지는데, 여러 파일들로 나누어놓으면 여러 사람들이 한 프로그램의 기능을 동시에 개발하기에 좋아서다.
이 때 모든 조각들을 하나로 엮는 걸 링크한다고 하고, 링커라는 프로그램을 통해 모든 조각들을 하나로 엮어 실행한다. 현재 가장 유명한 매개 파일 형식인 ELF(실행과 링크가 가능한 형식)은 ‘나는 함수가 필요해’친구들과 ‘나는 함수를 제공해’친구들을 한 데 모아서 엮어주고(해소해주고) 실제로 실행하는 프로그램을 만들어준다.
라이브러리를 단순히 함수들의 파일로 간주해서 프로그램의 나머지 부분과 연결하는 방식을 정적 링크라고 하고, 공유 라이브러리를 사용해 여러 프로그램이 똑같은 라이브러리를 사용해 메모리의 낭비를 최소화시킨 동적 링크가 존재한다.
공유 라이브러리를 사용할 때에는 스택과 힙을 사용하도록 함수를 설계해야 한다
프로그램에는 진입점이 있고, 프로그램의 첫 번째 명령어가 위치한 주소를 뜻한다. 보통 우리들은 이 진입점이 가장 먼저 실행된다고 생각할 것이다. 하지만 프로그램은 가장 먼저 런타임 라이브러리라는 녀석을 추가하고, 이 안의 명령어들을 먼저 실행한 후에 우리들이 생각하는 진입점의 명령어가 실행된다. 런타임 라이브러리는 메모리 설정을 책임진다(스택과 힙 영역도 설정해준다)
메모리 전력 소비
공부할 때 전력을 생각하지 않고 기능만을 고려했는데, 사실 전력도 매우 중요한 고민 요소중 하나이다. 컴퓨터가 엄청나게 많은 통신 회사의 경우 프로그램 하나의 전력 소모량도 큰 값이 될 수 있고, 모바일 장치에서는 배터리와 관련해서 전력 소모가 중요하기 때문이다. 전력 소비와 성능 사이의 균형을 잡는 것이 프로그래머로써 해야할 일이라고 생각한다.
댓글남기기