Fundamental of CS/: : CSAPP

[CSAPP] Ch 1. Prologue : A Tour of Computer System

Jay.P Morgan 2023. 10. 18. 00:03

 

. 컴퓨터 시스템으로의 여행 (A Tour of Computer Systems)

 

  1.0  Intro

 

   컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성되며, 이들과 함께 작동하여 응용 프로그램을 실행한다. 시간이 지남에 따라 시스템의 구현 방법은 바뀔 수 있지만, 근본적인 개념들은 변하지 않는다. 모든 컴퓨터 시스템들은 유사한 기능을 수행하는 유사한 하드웨어와 소프트웨어 컴포턴트를 가지고 있다.

   여러분이 컴퓨터 시스템, 그리고 시스템이 응용프로그램에 미치는 영향을 이해한다면 보기 드문 "파워 프로그래머"가 되는 길로 향하게 될 것이다.

 

   시스템에 관한 공부의 시작은 hello 프로그램이 시스템에서 실행되고, 단순한 메시지를 출력하고, 종료될 때까지의 수명주기를 추적하는 것으로 시작한다.

 

#include <stdio.h>

int main()
{
	printf("hello, world\n");
    return 0;
}

 

 

  1.1  정보는 비트와 컨텍스트로 이루어진다

 

   hello 프로그램은 hello.c라는 텍스트 파일로 저장되며, 대부분의 컴퓨터 시스템은 텍스트 문자를 아스키(ASCII) 표준을 사용하여 표시한다. 아스키 코드는 각 문자를 바이트 길이의 정수 값으로 나타낸다.

※ newline 문자 '\n' 역시 정수 값 10으로 표시한다.

 

  hello.c처럼 오직 아스키 코드들로만 이루어진 파일들을 '텍스트 파일'이라 부른다. (다른 파일들은 바이너리 파일)

 

  모든 시스템 내부의 정보는 0과 1의 비트들로 표시되는데, 오직 이들을 바라보는 컨텍스트(Context)에 의해서만이 서로 다른 객체들을 구분하는 유일한 방법이다. 다른 컨텍스트에서는 동일한 일련의 바이트가 정수, 부동소수, 문자열 혹은 기계어 명령을 의미할 수 있다.

 

 

  1.2  프로그램은 다른 프로그램에 의해 다른 형태로 번역된다

 

  hello.c를 시스템에서 실행하려면, 각 C 문장들은 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 번역되어야 한다.

 전처리 단계

   전처리기(Pre-processor, cpp)는 #으로 시작하는 지시(Directive)에 따라 C 프로그램을 수정한다.

   예를들어 #include 가 있다면 전처리기는 stdio.h를 프로그램에 직접 삽입한다.

 

 컴파일 단계

   Complier(cc1)는 hello.i를 어셈블리어 텍스트파일 hello.s로 번역한다. 이때 '어셈블리어' 프로그램이 저장된다.

 

 어셈블리 단계

   어셈블러(as)가 hello.s를 기계어 인스트럭션으로 번역하여, hello.o라는 목적파일에 저장한다. 이 파일은 main 함수의 인스트럭션을 인코딩하기 위한 바이너리 파일이다.

 

 링크 단계

   링커 프로그램(ld)이 hello.o파일과 표준 C 라이브러리에 있는 함수들을 결합시킨다.

   그 결과 hello 파일은 실행가능 목적파일(Executable object program, 실행파일)로 메모리에 실려 시스템이 실행한다.

 

 

  1.3  컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다

 

 프로그램 성능 최적화하기

   올바른 판단을 하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있으며, 컴파일러가 C 문장들을 어떻게 어셈블리 코드로 번역하는지 알 필요가 있다. 

 

 링크 에러 이해하기

   굉장히 당혹스러운 프로그래밍 에러는 링커의 동작과 관련이 있다.

 

 보안 약점(security hole) 피하기

   안전한 프로그래밍의 시작은 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 때문에 생겨나는 영향을 이해하는것이다.

 

 

  1.4  프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다

 

  쉘은 커멘드라인 인터프리터로 프롬프트를 출력하고 명령어 라인을 입력받아 그 명령을 실행한다. 만일 명령어 라인이 내장 쉘 명령어가 아니면 쉘은 실행파일의 이름으로 판단하고 그 파일을 로딩해서 실행해 준다.

 

  1.4.1 시스템의 하드웨어 조직

   hello 프로그램을 실행할 때 무슨 일이 일어나는지 설명하기 위해서는 아래와 같은 전형적인 시스템에서의 하드웨어 조직을 이해할 필요가 있다.

※ 전형적인 시스템의 하드웨어 구성. Controller : 칩셋 혹은 메인보드, Adapter : 메인보드에 장착하는 카드

 

   버스(Buses)

    - 시스템 내부를 관통하여 컴포넌트들 간에 바이트 정보를 전송하는 배선군

    - 일반적으로 워드(word)라고 하는 고정 크기의 바이트 단위로 데이터를 전송

 

   입출력 장치

    - 시스템과 외부세계와의 연결을 담당한다. 위의 시스템에는 입력용 키보드와 마우스, 출력용 디스플레이, 디스크 드라이브 이렇게 4개의 입출력장치가 있다.

 

   메인 메모리

    - 프로세서가 프로그램을 실행하는 동안 데이터와 프로그램을 모두 저장하는 임시 저장장치

    - 논리적으로 메모리는 연속적인 바이트들의 배열로 구성

 

   프로세서

    - 메인 메모리에 저장된 인스트럭션들을 실행(해독)하는 엔진이다.

    - 중심에는 워드 크기의 레지스터(저장장치)인 프로그램 카운터(PC)가 있다.

    - 프로세서는 프로그램 카운터가 가리키는 곳의 인스트럭션을 반복적으로 실행하고, PC값이 다음 인스트럭션의 위치를 가리키도록 업데이트 한다. 프로세서는 자신의 인스트럭션 집합 구조(Instruction Set Architecture)로 정의되는 매우 단순한 인스트럭션 실행 모델을 따라 작동하는 것처럼 보인다.

 

   CPU가 실행하는 단순 작업 예

    - 적재(Load) : 메인 메모리에서 레지스터에 한 바이트 혹은 워드를 이전 값에 덮어쓰는 방식으로 복사

    - 저장(Store) : 레지스터에서 메인 메모리로 한 바이트 혹은 워드를 이전 값을 덮어쓰는 방식으로 복사

    - 작업(Operate) : 두 레지스터의 값을 ALU로 복사하고 두 개의 워드로 수식연산을 수행한 뒤, 결과를 덮어쓰기 방식으로 레지스터에 저장

    - 점프(Jump) : 인스트럭션 자신으로부터 한 개의 워드를 추출하고, 이것을 PC에 덮어쓰기 방식으로 복사

 

 

   1.4.2 hello 프로그램의 실행

 

   쉘 프로그램은 사용자가 ".\hello"를 입력하기를 기다리다가, 사용자가 입력하면 각각의 문자를 레지스터에 읽어들인 후 아래와 같이 메모리에 저장한다.

※ hello 명령을 키보드에서 읽어들린다

 

   쉘은 파일 내의 코드와 데이터를 복사하는 인스트럭션을 실행하여, 실행파일 hello를 디스크에서 메인 메모리로 로딩한다.

※ 실행파일을 디스크에서 메인 메모리로 로딩한다 (DMA)

 

   hello 목적파일의 코드와 데이터가 메모리에 적재된 후, 프로세서는 hello 프로그램의 main 루틴 기계어 인스트럭션들을 실행한다.  이 인스트럭션들은 "hello, world\n" 스트링을 메모리로부터 레지스터 파일로 복사하고, 디스플레이장치로 전송하여 화면에 글자들이 표기된다.

※ 출력 스트링을 메모리에서 화면으로 기록

 

 

  1.5  캐시가 중요하다

 

  이와 같은 예제로부터 알 수 있는 것은, 시스템이 정보를 한 곳에서 다른 곳으로 옮기는 일에 많은 시간을 할애한다는 것이다. HDD에서 메모리로, RAM에서 Processor로 ..  이 여러 복사과정들이 프로그램의 "실제 작업"을 느리게 하는 오버헤드다. 그래서, 시스템 설계자들의 주요 목적은 이러한 복사과정들을 가능한 한 빠르게 동작하도록 하는 것이다.

  난감한 것은, 반도체 기술이 발달함에 따라 프로세서-메모리 간 격차가 지속적으로 증가하고 있다는 것이다. 이 격차에 대응하여, 보다 작고 빠른 캐시 메모리를 고안하여 프로세서가 단기간에 필요로 할 가능성이 높은 정보를 임시로 저장할 목적으로 사용한다.

   L1, L2, L3 캐시 시스탬의 배경으로는, 프로그램이 지엽적인 영역의 코드와 데이터를 액세스하는 경향인 지역성(locality)을 활용하여 시스템이 매우 크고 빠른 메모리 효과를 얻을 수 있다는 것이다. 자주 액세스할 가능성이 높은 데이터를 캐시가 보관하도록 하면 빠른 캐시를 이용해서 대부분의 메모리 작업을 수행할 수 있게 된다.

※ CPU chip 안에 들어있는 캐시 메모리

 

 

  1.6  저장장치들은 계층구조를 이룬다

 

  모든 컴퓨터 시스템의 저장장치들은 아래와 같은 메모리 계층구조로 구성된다.

※ 메모리 계층구조의 예시

 

   메모리 계층구조의 특징으로는, 한 레벨의 저장장치가 다음 하위레벨 저장장치의 캐시 역할을 한다는 것이다.

 

 

  1.7  운영체제는 하드웨어를 관리한다

※ 응용프로그램이 하드웨어를 제어하려면 언제나 운영체제를 통해서 해야 한다.

 

  운영체제는 두 가지 주요 목적을 가지고 있다.

(1) 제멋대로 동작하는 응용프로그램들이 하드웨어를 잘못 사용하는 것을 막기 위해

(2) 응용프로그램들이 단순하고 균인ㄹ한 매커니즘을 사용하여 복잡하고 매우 다른 저수준 하드웨어 장치들을 조작할 수 있도록 하기 위해

 

※ 운영체제는 위의 두 가지 목적을 위해, 세 가지 추상화를 사용하였다.

 

 

  1.7.1 프로세스 (Processes)

 

    프로세스는 실행중인 프로그램에 대한 운영체제의 추상화다.

 

    hello같은 프로그램이 실행될 때, 운영체제는 이 한개의 프로그램만 실행되는 것 같은 착각에 빠지도록 해준다. 프로그램이 프로세서, 메인 메모리, I/O 장치 모두 독차지 하고있는 것처럼 보이며, 프로세서는 프로그램 내의 인스트럭션들을 다른 방해 없이 순차적으로 실행하는 것처럼 보인다. (프로세스라고 하는 개념에 의해)

 

   다수의 프로세스들은 동일한 시스템에서 동시에 실행될 수 있으며, 각 프로세스는 하드웨어를 배타적으로 사용하는 것처럼 느낀다. 이는 싱글코어건 멀티코어 프로세서건, 프로세서가 프로세스를 바꿔주는 방식으로 한 개의 CPU가 다수의 프로세스를 동시에 실행하는 것 처럼 보이게 해준다. (통신에서의 Time Division -, 시분할 -)

 

  운영체제는 문맥 전환(Context Switching, 문맥 교환)이라는 방법을 통해 이러한 교차 실행을 수행한다.

  (컨텍스트라 부르는 상태정보는 PC, 레지스터 파일, 메인 메모리의 현재 값을 포함하고 있다.)

 

  간단히 나타내기 위해 단일 CPU를 가지는 단일 프로세서 시스템에서 설명을 해 보자.

  운영체제는 프로세스가 실행하는 데 필요한 모든 컨텍스트의 변화를 추적한다.  어느 한 순간에 단일 프로세서 시스템은 한 개의 프로세서의 코드만 실행할 수 있다. 운영체제는 현재 프로세스에서 다른 새로운 프로세스로 제어를 옮기려고 할 때, 현재 프로세스의 컨텍스트를 저장하고 새 프로세스의 컨텍스트를 복원시키는 문맥전환을 실행하여 제어권을 새 프로세스로 넘겨준다.

  쉘 프로세스와 hello 프로세스를 예로 들면, 처음에는 쉘 프로세스가 혼자서 동작을 기다가 명령줄에서 입력을 기다린다.

  hello 프로그램의 실행 명령을 받으면,  쉘은 시스템 콜(System Call) 함수를 호출하여 운영체제로 제어권을 넘겨준다.

  운영체제는 쉘의 컨텍스트를 저장하고, 새로운 hello 프로세스와 컨텍스트를 생성한 뒤 제어권을 새 hello 프로세스로 넘겨준다.

  그러다, hello가 종료되면 운영체제는 기존의 쉘 프로세스의 컨텍스트를 복구시키고 제어권을 넘겨주면서 다음 명령줄 입력을 기다린다.

 

  문맥 전환은 운영체제 커널(Kernel)에 의해서 진행된다. 커널은 운영체제 코드의 일부로 메모리에 상주한다.

  응용프로그램이 운영체제에 의한 작업을 요청하면, 컴퓨터는 파일 읽기나 쓰기와 같은 특성 시스템 콜을 실행하여 커널에 제어를 넘긴다. 그리고 커널은 요청된 작업을 수행하고 응용프로그램으로 리턴한다.

  단, 커널은 별도의 프로세스가 아니라, 모든 프로세스를 관리하기 위해 시스템이 이용하는 코드와 자료구조의 집합이다.

 

 

  1.7.2 쓰레드 (Thread)

 

   오늘날에는 쓰레드(Thread)라고 하는 다수의 실행 유닛으로 프로세스가 구성되어있다. 각각의 쓰레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역데이터(메모리 영역)를 공유한다.

   쓰레드간의 데이터 공유가는 프로세서간의 데이터 공유가 더 쉽고 효율적이라는 점에서 중요성이 커지고있다.

 

 

 

  1.7.3 가상메모리 (Virtual Memory)

 

   가상메모리는 각 프로세스들이 메인 메모리 전체를 독점적으로 사용하고 있는 것 같은 환상을 제공하는 추상화다.

   각 프로세스는 가상주소 공간이라고 하는 균일한 메모리의 모습을 갖게 된다.

  프로그램 코드와 데이터 : 코드는 모든 프로세스들이 같은 고정 주소에서 시작하며, 다음에 C 전역변수에 대응되는 데이터 위치들이 따라온다.

  힙(Heap) : 코드와 데이터 영역 다음으로 런타임 힙이 따라온다. 동적 메모리 할당으로 크기가 가변적이다.

  공유 라이브러리 : 주소공간의 중간 부근에 C 표준 라이브러리와 같은 공유 라이브러리의 코드와 데이터를 저장하는 영역

   스택(Stack) : 사용자 가상메모리 공간의 맨 위에 컴파일러가 함수 호출을 구현하기 위해 사용하는 사용자 스택이 위치한다. 함수를 호출할 때마다 스택이 커지며, 리턴될 때에는 줄어들며 동적으로 늘어났다 줄어들었다 한다.

   커널 가상메모리 : 주소공간의 맨 윗부분이 커널을 위해 예약받는다.

 

 

  1.7.4 파일 (Files)

 

   파일은 더도 말고 덜도 말고 그저 연속된 바이트들이다. 심지어 리눅스에선 디스크, 키보드, 디스플레이, 네트워크까지 포함하는 모든 입출력장치는 파일로 모델링한다. 모든 입출력은 유닉스 I/O라는 시스템 콜들을 이용하여 파일을 읽고 쓰는 형태로 이루어진다.

   이러한 파일 개념은 응용프로그램에게 다양한 입출력장치들의 통일된 관점을 제공한다.

 

 

  1.8  시스템은 네트워크를 사용하여 다른 시스템과 통신한다

 

  데이터가 로컬디스크 드라이브 대신에 네트워크를 통해서 다른 컴퓨터로 이동될 수 있는 것처럼, 네트워크는 하나의 또 다른 입출력장치라고 볼 수 있다.

 

 

 

  1.9  중요한 주제들

 

 

  9.1 암달의 법칙 (Amdahl's Law)

 

  - 시스템의 일부 성능 개선의 효율성에 대한 간단하지만 직관적인 관찰

  - 우리가 어떤 시스템의 한 부분의 성능을 개선 할 때, 전체 시스템 성능에 대한 효과는 얼마나 중요한 부분인가와 이 부분이 얼마나 빨라졌는가에 연관이 있다는 것이다.

 

  - 시스템의 특정 부분이 실행하는데 걸리는 시간의 α 비율만큼 소모하고, 이것의성능을 k배 개선하려 한다고 하자. 

. . . 유도 생략 . . .

    향상된 속도 S = Told/Tnew 를 다음과 같이 구한다.

    ex) 조기에는 전체 시간의 60%( α = 0.6)만 소모한 이 시스템의 일부분이 3배(k = 3) 속도가 빨라진다고 하면,

          속도향상은 1/[0.4 + 0.6/3] = 1.67을 얻는다. 주요 부분에 대해 상당한 개선을 하였지만, 총 속도향상은 매우 적다.

          전체 시스템을 상당히 빠르게 하기 위해서는 전체 시스템의 매우 큰 부분의 성능을 개선해야 한다.

 

  -  특별한 경우는 k로 설정하는 효과에 대해 생각하는 것이다. 즉, 시스템의 일부를 택해서 그 성능을 무시할 수 있을 정도의 시간이 걸리는 지점까지 속도를 올리면

    와 같은 결과를 얻는다.  

   ex) 시스템의 60%를 시간이 거의 걸리지 않는 지점까지 속도를 올려도, 총 속도개선율은 1/0.4 = 2.5배밖에 되지 않는다.

 

 

  9.2 동시성과 병렬성

 

  동시성(concurrency) : 다수의 동시에 벌어지는일,  병렬성(parallelism) : 동시성을 사용해서 시스템을 보다 빠르게 동작하도록 하는 것

 

  우리는 컴퓨터가 더 많은 일을 해내고, 더 빨리 실행되길 원한다. 이는 프로세서가 한번에 더 많은 일을 할 때 개선되는 특징을 갖고있다.

 

  ① 쓰레드 수준 동시성

   프로세스 추상화 개념을 이용하면 다수의 프로그램이 동시에 실행되는 시스템을 생각해낼 수 있다(동시성). 쓰레드를 사용하면 한개의 프로세스 내에서 실행되는 다수의 제어흐름을 가질 수 있다. 1960년대 초반 시간공유기법의 출현으로, 동시실행이 가능해졌다. 이는 한개의 컴퓨터가 실행하는 프로세스를 매우빠른 속도로 전환하는 방법을 사용한다. 이러면 사용자는 여러개의 프로세스가 한번에 돌아가고 있다고 느낀다. 한개의 프로세서에 의해서만 프로세스가 진행될때 이것을 '단일 프로세서 시스템' 이라고 한다.

 

   하나의 운영체제 커널의 통제 하에 여러개의 프로세서가 동작하면 '멀티프로세서 시스템'이라고 한다. 최근에는 '멀티코어 프로세서'들과 하이퍼쓰레딩 기법으로 일반적인 환경이 되었다.

 

 

  ② 인스트럭션 수준 병렬성

   최근의 프로세서들은 훨씬 낮은 수준에서의 추상화로 여러개의 인스트럭션을 한번에 수행할 수 있다. 인스트럭션들은 시작부터 종료까지 2.4보다 훨씬 긴 클럭사이클이 힐요하지만, 프로세서는 다양한 교묘한 기법을 활용하여 한 번에 100개의 인스트럭션까지 처리할 수 있다.

   사이클당 한개 이상의 인스트럭션을 실행할 수 있는 프로세스를 '슈퍼스케일러'라고 하며, 최근 프로세서들은 대부분 지원한다.

 

  ③ 싱글 인스트럭션, 다중 데이터 병렬성(SIMD)

   오늘날 프로세서들은 최하위 수준에서 싱글인스트럭션, 다중 데이터(SIMD) 병렬성이라는 모드로 한 개의 인스트럭션이 병렬로 다수의 연산을 수행할 수 있는 특수한 하드웨어를 지니고 있다. 대개 멀티미디어 데이터 처리를 위한 응용프로그램의 속도개선을 위해 제공된다.

 

 

  9.3 컴퓨터 시스템에서 추상화의 중요성 

 

   함수들을 간단한응용프로그램 인터페이스 API로 저형화 하여 그 내부의 동작을 고려하지 않으면서 코드를 사용할 수 있도록 해준다. (C언어의 함수 프로토타입, 자바의 클래스 선언 등)

   하드웨어 추상화로 다른 프로세서에 구현될 때도 동일한 기계어 코드를 실행할 수있게 된다. 

   운영체제 측면에서의 가상 머신은 컴퓨터 전체의 추상화를 제공하여, 여러 종류의 OS나 동일한 OS의 이전 버전에서 프로그램을 실행할 수 있도록 해야 하는 컴퓨터들을 관리하는 방법으로 더욱 두드러지게 나타나고 있다.