컴퓨터 시스템
- 컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성된다. 응용프로그램 실행이라는 궁극적인 목적을 달성하기 위해 두 시스템이 함께 작동된다.
- 아래는 hello 파일이 어떻게 저장되는지의 라이프 사이클을 정리했다.
- 버스
- 입출력 장치
- 메인 메모리
- 프로세서
- 캐시
- 운영 체제
- 쓰레드(Thread)
- 가상 메모리
- 파일
- 네트워크
- Amdahl의 법칙
- 동시성과 병렬성
- 시스템 계층 구조의 높은 계층에서 낮은 계층까지 중요한 세 개의 수준
- 인스트럭션 수준 병렬성
- 싱글 인스트럭션, 다중 데이터 병렬성(Single Instruction Multiple Data)
- 컴퓨터 시스템 추상화의 중요성
컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성된다. 응용프로그램 실행이라는 궁극적인 목적을 달성하기 위해 두 시스템이 함께 작동된다.
먼저 시스템에 관한 공부의 시작은 printf(“Hello, world\n”); 를 저장하는 프로그램이 프로그래머에 의해 만들어지고, 시스템에서 실행되고, 단순한 메시지를 출력하고, 종료될 때까지의 수명주기를 추적하는 것으로 시작이 된다.
아래는 모르는 용어 정리이다.
| 소스 프로그램 | 원시 코드 | 사람이 읽을 수 있음 | hello.c라는 txt파일로 저장 |
|---|---|---|---|
| 목적 프로그램 | 컴퓨터가 실행할 수 있는 코드 | 기계어 | hello.o라는 binary파일로 저장 |
바이트는 8비트 단위로, 1비트는 0 또는 1로 표시된다.
각 바이트는 프로그램의 텍스트 문자를 나타낸다고 하는데, 모든 텍스트를 담을 수 있을까 궁금했다. 1비트는 2^0 으로 2^8승은 256이기 때문에 충분히 담을 수 있을거 같다.
아스키 문자들로만 이루어진 파일은 텍스트(Text) 파일이고 다른 모든 파일들은 바이너리(Binary) 파일이라고 한다.
hello 프로그램은 인간이 이해하고 읽을 수 있기 때문에 고급 C프로그램으로 일생이 시작되고 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 변역이 돼. 그 후 인스트럭션들은 실행가능 목적 프로그램이라고 하는 형태로 합쳐져서 바이너리 디스크 파일로 저장된다.
아래는 hello 파일이 어떻게 저장되는지의 라이프 사이클을 정리했다.
각 단계는 더 세부적인 과정들이 있는데, 일단은 생략
| hello.c | 전처리 단계(pre-processing) |
|---|---|
| Source program(text) ↓ | 컴파일하기 전에 처리하는 단계로 C언어의 중요한 역할, 결과는 소스 코드는 컴파일러가 이해할 수 있는 순수한 C 코드 형태로 정리됨 |
| hello.i | 컴파일 단계 |
|---|---|
| Modified source program(text) ↓ | 컴파일러(ccl)는 텍스트 파일 .i를 hello.s로 번역하며 이 파일에는 어셈블리어 프로그램이 저장됨 |
| hello.s | 어셈블리 단계 |
|---|---|
| Assembly program(text) ↓ | 어셈블러(as)가 hello.s를 기계어 인스트럭션으로 번역하고 재배치가능 목적프로그램의 형태로 묶어서 hell.o로 라는 목적파일에 결과 저장 |
| hello.o | 링크 단계 | hello |
|---|---|---|
| Relocatable object → programs(binary) → | printf 함수는 C 컴파일러에서 제공하는 표준 C 라이브러리에 있다. 그래서 별도의 목적파일에 있으며, hello.o 파일과 어떤 형태로 결햅되어야 하기 때문에 링커(ld)가 통합작업을 수행한다. 그 결과로 hello 파일은 실행가능 목적파일(실행파일)로 메모리에 적재된다. | Executable object program(binary) |

버스
시스템 내를 관통하는 전기적 배선군. 그냥 통로라고 생각하면 좋을거 같다. 바이트 단위로 데이터를 전송하며 오늘날 대부분의 컴퓨터들은 4바이트 또는 8바이트 워드 크기를 갖는다.
입출력 장치
시스템과 외부세계의 연결을 담당. 키보드나 모니터 같은거라고 생각하면 됨. I/O(input/output) 버스를 통해 연결됨
메인 메모리
프로그램을 실행하는 동안의 데이터와 프로그램을 모두 저장하는 임시 저장 장치. DRAM으로 구성됨. 연속적인 바이트들의 배열을 이루며 0부터 시작해 고유의 인덱스를 갖고 있다.
프로세서
메인 메모리에 저장된 instruction들을 해독(실행)하는 엔진. Program counter가 가리키는 메모리로 부터 instruction을 읽어오고, 이 instruction에서 비트들을 해석하여 instruction이 지정하는 간단한 동작을 실행하고, PC를 다음 instruction위치로 update 한다. 새로운 instruction은 같을수도 있다.
메인 메모리, 레지스터 파일, 수식/논리 처리기(ALU) 주위를 순환한다.
instruction 요청에 따라 CPU가 실행하는 작업의 예를 아래 나열하였다
적재(Load): 메인 메모리에서 레지스터에 한 바이트 또는 워드를 이전 값에 덮어씌우는 방식으로 복사한다
저장(Store): 레지스터에서 메인 메모리로 한 바이트 또는 워드를 이전 값에 덮어씌우는 방식으로 복사한다
작업(Operarte): 두 레지스터의 값을 수식/논리 처리기(ALU)로 복사하고 두 개의 워드로 수식연산을 수행한 뒤 , 결과를 덮어쓰기 방식으로 레지스터에 저장한다
점프(Jump): instruction 자신으로부터 한개의 워드를 추출하고 이걸을 PC에 덮어쓰기 방식으로 복사한다
컴퓨터 특성상 저장을 많이 할수 있는 디스크나 메인 메모리, 그에 반해 많이 저장할수 없지만 빠르게 읽을 수 있는 레지스터나 캐시가 존재한다. 캐시 중 L1 캐시와 L2 캐시가 있는데, 이들은 SRAM(Static Random Access Memory)라는 하드웨 기술을 이용해 구현된다.
캐시
캐시의 지역성 개념은 프로그램이 메모리를 접근할 때 특정 부분을 집중적으로 사용하는 경향을 나타낸다. 지역성은 두가지 형태로 나타나며, 아래와 같다.
시간적 지역성(Temporal locality) : 한번 접근된 데이터는 가까운 미래에 다시 접근될 가능성이 높다. 예를 들어 루프 내에서 반복적으로 사용되는 변수는 시간적 지역성의 예임
공간적 지역성(Spatial locality) : 메모리의 특정 주소에 접근한 후, 그 주변 주소에 있는 데이터에 접근될 가능성이 높다는 원리. 배열이나 연속적인 메모리 블록에 접근할 때 종종 발생
캐시 메모리는 이러한 지역성 원리를 활용하여 자주 사용되거나 연속적으로 사용될 가능성이 높은 데이터를 미리 캐시에 저장하고 이로 인해 CPU는 필요한 데이터를 캐시에서 빠르게 찾을 수 있게 된다.
메모리 계층구조라는것이 존재하는데, 이것의 주요 아이디어는 한 레벨의 저장장치가 다음 하위레벨 저장장치의 캐시 역할을 한다는 것이다
운영 체제
운영 체제는 응용 프로그램과 하드웨어 사이에 있어서 응용 프로그램대로 하드웨어가 작동하도록 한다.
컨텍스트(Context) : 운영체제는 프로세스가 실행하는 데 필요한 모든 상태정보의 변화를 추적한다. 컨텍스트라고 부르는 상태정보는 PC, 레지스터 파일, 메인 메모리의 현재 값을 포함
문맥전환(context switching) : 현재 프로세스에서 다른 새로운 프로세스로 제어를 옮기려고 할 때 현재 프로세스의 컨텍스트를 저장하고 새 프로세스의 컨텍스트를 복원시킴. 이런 과정을 실행하여 제어권을 새 프로세스로 넘겨줌
시스템 콜 : 제어권을 운영체제로 넘기는 것
커널 : 하나의 프로세스에서 다른 프로세스로의 전환은 운영체제 커널에 의해 관리되고 메모리에 상주함, 코드와 자료구조의 집합이라 하는데 무슨 소리지…
쓰레드(Thread)
프로세스가 마치 한 개의 제어흐름을 갖고 있다고 생각할 수 있는데, 최근의 시스템에서는 프로세스가 쓰레드라고 하는 다수의 실행 유닛으로 구성돼 있다.
쓰레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역 데이터를 공유한다.
다수의 프로세스들보다 데이터의 공유가 더 쉬운점, 프로세스보다 더 효율적인 점 떄문에 프로그래밍 모델로써 중요성이 커지고 있다.
그렇다면, 프로세스와 쓰레드의 차이점에 대해 알아보자
| 정의 | 자원 공유 | |
|---|---|---|
| 프로세스 | 독립적으로 실행되는 프로그램의 인스턴스로 자체적인 주소 공간, 메모리, 데이터 스택 및 다른 시스템 자원을 보유 | 각 프로세스는 독립적인 메모리 공간과 세스템 자원을 가지므로, 프로세스간 자원 공유는 IPC(Inter-Process Communication) 메커니즘을 통해 이루어짐 |
| 쓰레드 | 프로세스 내부의 실행 흐름 단위로, 프로세스의 자원과 주소 공간을 공유하면 실행 | 같은 프로세스 내의 쓰레드들은 코드, 데이터 및 시스템 자원을 공유 |
가상 메모리
가상 메모리는 각 프로세스들이 메인 메모리 전체를 독점적으로 사용하고 있는 것 같은 환상을 제공하는 추상화이다. 각 프로세스는 가상주소 공간이라고 하는 균일한 메모리의 모습을 갖게 된다.
가상주소 공간의 취상위 영역은 모든 프로세스들이 공통으로 사용하는 운영체제의 코드와 데이터를 위한공간
커널 가상 메모리 : 주소공간의 맨 윗부분은 커널을 위해 예약되어 있다. 응용프로그램들은 이 영역의 내용을 읽거나 쓰는 것이 금지되어 있으며, 마찬가지로 커널 코드 내에 정의된 함수를 집접 호출하는 것도 금지되어 있다.
스택(Stack) : 사용자 가상메모리 공간의 맨 위에 컴파일러가 함수 호출을 구현하기 위해 사용하는 사용자 스택이 위치한다. 힙과 마찬가지로 사용자 스택은 프로그램이 실행되는 동안에 동적으로 늘어났다 줄어들었다 한다. 특히, 함수를 호출할 때마다 스택이 커지며, 함수에서 리턴될 떄는 줄어든다.
공유 라이브러리 : 주소공간의 중간 부근에 C 표준 라이브러리나 수학 라이브러리와 같은 공유 라이브러의 코드와 데이터를 저장하는 영역이 있다. 공유 라이브러리 개념은 강력하지만 다소 어렵다.
힙(Heap) : 코드와 데이터 영역 다음으로 런타입 힙이 따라온다. 크기(주소)가 고정되어 있는 코드, 데이터 영역과 달리, 힙은 프로세스가 실행되면서 C 표준함수인 malloc이나 free를 호출하면서 런타임에 동적으로 그 크기가 늘었다 줄었다 한다.
프로그램 코드와 데이터 : 코드는 모든 프로세스들이 같은 고정 주소에서 시작하며, 다음에 C 전역변수에 대응되는 데이터 위치들이 따라온다. 코드와 데이터 영역은 실행가능 목적파일인 hello로부터 직접 초기화된다.
가상주소 하위 영역은 사용자 프로세스의 코드와 데이터를 저장
가상주소 위쪽으로 갈수록 주소가 증가한다
가상메모리가 작동하기 위해서는 프로세스가 만들어내는 모든 주소를 하드웨어로 변역하는 등의 하드웨어와 운영체제 소프트웨어 간의 복잡한 상효작용이 필요하다. 기본적인 아이디어는 프로세스의 가상메모리의 내용을 디스크에 저장하고 메인 메모리를 디스크의 캐시로 사용하는 것이다.
여기서 왜 스택이 공유 라이브러리보다 높고 힙이 더 낮을까 궁금하지 않나? 가장 큰 이유는 메모리의 성장 방향과 충돌 방지를 고려한 설계 때문인데, 스택은 함수 호출이 많아질수록 위에서 아래로(높은 주소 -> 낮은 주소) 성장, 힙은 반대로 메모리를 할당할수록 아래에서 위로(낮은 주소 -> 높은 주소) 성장하기 때문에 충돌 가능성을 줄이고자 이렇게 만들었대
공유 라이브러리는 여러 프로세스에서 공유되기 때문에, 중간쯤에 위치해서 재사용성과 메모리 보호를 높혔어
파일
파일은 연속된 바이트들이다. 모든 입출력장치는 파일로 모델링하고 시스템의 모든 입출력은 시스템 콜들을 이용해 파일을 읽고 쓰는 형태로 이루어진다. 신기한 것은 파일 개념이 있어서 사용하고 있는 특정 디스크의 기술에 대해서 몰라도 사용이 가능하다.
네트워크
네트워크는 일종의 입출력장치로 여러 대의 컴퓨터가 서로 데이터를 주고받을 수 있도록 연결된것을 이야기함
원격 컴퓨팅에 대해서 한번 다뤄보자.
| telnet(원격 컴퓨팅) 클라이언트 사용해서 로컬/원격 컴퓨터 연결 |
|---|
| ↓ |
| 원격지 컴퓨터에 로그인 후 쉘 실행 |
| ↓ |
| “hello” 스트링을 telnet 클라이언트에 입력 후 Enter |
| ↓ |
| 클라이언트 프로그램은 스트링을 telnet 서버로 보냄 |
| ↓ |
| telnet 서버가 네트워크에서 스트링을 받은 후에,원격 쉘 프로그램에 전달 |
| ↓ |
| 원격 쉘은 hello 프로그램을 실행하고 출력 스트링을 telnet 클라이언트로 전달 |
| ↓ |
| 클라이언트 프로그램은 출력 스트링을 로컬(여기서 원격 컴퓨터) 터미널에 표시 |
Amdahl의 법칙
시스템을 개선하는것은 개발자로써의 필수 요소이다. 그렇다면 시스템 개선 후 속도향상에 대한 식에 대해 배워보자 \[\begin{aligned} T_{\text{new}} &= (1 - \alpha) T_{\text{old}} + \left( \alpha T_{\text{old}} \right)/k \\[1em] &= T_{\text{old}} \left[ (1 - \alpha) + \frac{\alpha}{k} \right] \end{aligned}\]
어떤 응용 프로그램 실행하는데 걸리는 시간이(Told), 해당 응용프로그램 외 Told의 a비율만큼 소모하고, 이것의 성능을 k배 개선한다고 했을때 위의 식이 나와.
여기서 a는 병렬화에 대해서 생각을 해야해 전체 작업량이 1이라고 가정했을때, 아래와 같이 나와
-병렬화할 수 없는 부분 = 1 - a
-병렬화 가능한 부분 = a
병렬화가 안되면 직렬밖에 안되겠지? 그러면 직렬 부분은 (1 - a), 병렬이 가능한 부분은 k개의 코어로 나눠 실행돼 a/k. 그래서 위의 식이 나왔다고 보면 돼 \[\begin{aligned} S &= \frac{T_{\text{old}}}{T_{\text{new}}} \\[1em] &= \frac{1}{(1 - \alpha) + \frac{\alpha}{k}} \end{aligned}\]
Told/Tnew 했을때 개선된 속도(S)를 산출할 수 있는데, 위의 식으로 풀수가 있어.
그렇다면 만약 k가 무한대가 되었을때가 궁금하지 않아? k가 무한대로 되었을때는 아래와 같은 식이 나와 \[S_{\infty} = \frac{1}{1 - \alpha}\]
개선할 수 있는 비율 a가 1에 가까울수록 S가 무한대로 가겠지만 만약 한 60%밖에 되지 않는다면 속도 개선율은 2.5배밖에 안되겠지. 그러니 k만 높일생각 말고 a값도 높일생각을 해야해.
동시성과 병렬성
동시성(concurrency) 이라는 용어는 다수의 동시에 벌어지는 일을 갖는 시스템에 관한 일반적인 개념
병렬성(parallelism) 이라는 용어는 동시성을 사용해서 시스템을 보다 빠르게 동작하도록 하는 것의 개념
시스템 계층 구조의 높은 계층에서 낮은 계층까지 중요한 세 개의 수준
쓰레드 수준 동시성
곡예사가 여러 개의 공을 공중에 던지는 것을 생각해보면, 한 개의 컴퓨터가 프로세스를 빠르게 전환하는것과 비슷하다.
프로세스 추상화 개념을 이용해, 다수의 프로그램이 동시에 실행되는 시스템을 생각하면 동시성으로 이어진다. 위에서 언급했었는데, 쓰레드를 이용하면 한개의 프로세스 내에서 실행되는 다수의 제어흐름을 가질 수 있다.
단일 프로세서 시스템 : 단일의 프로세서로 시스템이 하나의 운영체제 커널의 제어 하에 동작
멀티 프로세서 시스템 : 다수의 프로세서로 시스템이 하나의 운영체제 커널의 제어 하에 동작하는데, 하위 개념으로 Multi-core / Hyper-threaded 개념으로 나뉜다. 장점으로는, 다수의 태스크를 실행할 때, 동시성을 시뮬레이션할 필요 줄어듬. 응용 프로그램을 빠르게 실행 가능하지만, 프로그램이 병렬로 효율적으로 실행할 수 있는 멀티쓰레드의 형태로 표현되었을 때에만 적용된다. 고로 프로그래머로써 이 멀티쓰레드의 형태 구조 작성 방법에 대해서 알아야 될 이유를 느꼈다.
멀티 코어
멀티 코어 프로세서인 인텔 i7 프로세서를 예로 들어 설명을 해보겠다.
사진?
총 4개의 코어가 보인다. L1/L2 캐시같은 경우 각각 코어별로 나뉘지만, 메인 메모리/인터페이스 등 상위 수준 캐시 같은 경우에는 공유하고 있다.
멀티 쓰레딩
하나의 CPU가 여러 개의 제어 흐름을 실행할 수 있게 해주는 기술. 여러개의 동일한 프로그램 카운터나 레지스터 파일을 보유하고 있지만, 부동소수 연산기를 포함한 외적인 것들은 한개만 갖고 있다.
기존 프로세서 같은 경우 쓰레드의 전환에 2만 클럭 사이클이 요구 됨
하이퍼 쓰레드 프로세서는 매 사이클마다 실행할 쓰레드 결정
인스트럭션 수준 병렬성
먼저 추상화에 대한 용어 정의를 먼저 할게. “추상화는 복잡한 시스템에서의 필요한 부분만 뽑아서 단순하게 표현” 의 뜻이 있고 고수준 언어(Python, Java) 같은 경우에 추상화 수준이 높고 저수준(C, 어셈블리어)는 추상화 수준이 낮아
최근의 프로세서들은 낮은 수준에서의 추상화로 여러 개의 인스트럭션들을 한 번에 실행할 수 있는데, 이런 특성을 인스트럭션 수준 병렬성 이라고 해.
예전에는 한 개의 인스트럭션을 수행하는데 여러 클럭 사이클이 필요했대. 지금은 매 클럭마다 2.4개의 인스트럭션을 수행할 수 있고
용어 2개에 대한 정리
파이프라이닝 : 하나의 인스트럭션을 실행하기 위해 요구되는 일들을 여러 단계로 나누고 프로세서 하드웨어가 일련의 단계로 구성되어 이들 단계를 하나씩 수행.
슈퍼스케일러 : 사이클당 한 개 이상의 인스트럭션을 실행할 수 있는 프로세서로, 이 모델을 통해 작성한 프로그램의 성능을 구체화할 수 있다.
싱글 인스트럭션, 다중 데이터 병렬성(Single Instruction Multiple Data)
최신 프로세서 같은 경우 최하위 수준에서 싱글 인스트럭션, 다중 데이터, 즉 SIMD 병렬성이라는 모드로 한 개의 인스트럭션이 병렬로 다수의 연산을 수행할 수 있는 특수 하드웨어를 갖고 있다.
SIMD 인스트럭션들은 영상, 소리, 동영상 데이터 처리를 위해 응용프로그램 속도 개선용으로 제공된다.??
컴퓨터 시스템 추상화의 중요성

복잡한 걸 단순하게 표현, 핵심만 보여주기는 추상화에 대한 정의로써, 사용자가 내부 동작 이해 없이도 Python에서 프로그래밍 연습을 할수 있게 해준다.
보통 사람들이 기계어 코드 프로그램은 한 번에 하나의 인스트럭션을 실행하는 프로세서에서 실행되는 것처럼 동작한다고 느낀다. 하지만 실제 하드웨어는 여러 개의 인스트럭션을 병렬로, 순차적 방식으로 실행된다.