컴퓨터 시스템은 하드웨어와 시스템 소프트웨어로 구성, 이들이 함께 작동하여 응용 프로그램을 실행한다.
<hello.c>
#include <stdio.h>
int main(){
printf("hello, world\n");
return 0;
}
=> hello 프로그램을 실행하면 무슨 일이 일어나고, 그 이유를 이해하는 것이 목적이다.
hello 프로그램은 프로그래머가 에디터로 작성한 소스 프로그램으로 시작
hello.c 라는 텍스트 파일로 저장
hello.c 프로그램은 연속된 바이트들로 파일에 저장
hello.c 의 표시 방법으은 기본개념을 보여준다.
모든 시스템의 내부 정보는 비트들로 표시된다.서 로 다른 객체들을 구분하는 유일한 방법은 이들을 바라보는 컨텍스트에 의해서다.
컴퓨터 내 숫자들의 표현이 우리가 알던 정수나 실수와 같지 않기 때문에 예상하지 못화는 방식으로 움직일 수 있는 유한한 근사값이다.
hello 프로그램은 인간이 그 형태를 바로 이해하고 읽을 수 있기 때문에 고급 C프로그램으로 시작된다.
hello.c 를 시스템에서 실행시키려면 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 변역되어야한다.
인스트럭션들은 실행가능 목적 프로그램이라고 하는 형태로 합쳐져서 바이너리 디스크 파일로 저장된다.
컴파일러 드라이버는 유닉스 시스템에서 소스파일에서 오브젝트 파일로 변역한다.
linux> gcc -o hello hello.c
여기서 GCC 컴파일러 드라이버는 실행파일로 번역한다.
=> 번역은 4단계를 거쳐 실행됨 , 이 단계를 실행하는 프로그램을 합쳐 컴파일 시스템이라고 부름
1. 전처리 단계 : 전처리기 (cpp)는 C프로그램을 #문자로 시작하는 디렉티브에 따라 수정 , 일반적으로 .i로 끝나는 새로운 C프로그램이 생성한다.
2. 컴파일 단계 : 컴파일러 (cc1)는 텍스트파일 hello.i 를 텍스트파일 hello.s로 번역하며, 이 파일에는 어셈블리어 프로그램이 저장된다.
3. 어셈블리 단계 : 어셈블러 (as)가 hello.s를 기계어 인스트럭션으로 번역하고 재배치가능 목적프로그램의 형태로 묶어서 hello.o라는 목적 프로그램에 저장한다.
4. 링크 단계 : 링커 프로그램 (ld)이 hello.o파일과 C 컴파일러에서 제공하는 표준 C라이브러리에 들어있는 printf.o 파일과 동합작업을 수행한다. hello 파일은 실행가능 목적파일 (실행파일)로 메모리에 적재되어 시스템에 의해 실행된다.
1. 프로그램 성능 최적화 하기 : 효율적인 코드를 작성하기 위해서 컴파일러의 내부 동작을 알 필요는 없지만, C 프로그램 작성 시 올바른 판단을 하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있으며, 어떻게 컴파일러가 C문장들을 기계어 코드로 번역하는지 알 필요가 있다.
2. 링크 에러 이해하기 : 가장 당혹스러운 프로그래밍 에러는 링커의 동작과 관련되어 있다.
3. 보안 약점 피하기 : 안전한 프로그래밍을 배우는 첫 단계는 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 떄문에 생겨나는 영향을 이해하는 것 이다.
인스트럭션이란 컴퓨터에서 명령어를 나타내는 단위로, CPU에서 실행되는 각각의 명령어를 의미합니다. 인스트럭션은 이진수로 인코딩되어 CPU가 처리할 수 있는 형식으로 되어 있으며, CPU는 인스트럭션을 해독하여 해당 명령을 수행합니다.
인스트럭션은 CPU의 아키텍처와 관련이 있으며, CPU의 종류에 따라 인스트럭션 세트가 다르게 구성됩니다. 각 CPU는 명령어 세트 아키텍처(Instruction Set Architecture, ISA)라는 명령어 집합을 가지고 있으며, ISA는 해당 CPU에서 지원하는 명령어의 종류와 형식을 정의합니다.
인스트럭션은 보통 다음과 같은 구성요소를 가지고 있습니다.
-오퍼랜드(Operand): 인스트럭션에서 처리할 데이터나 주소 등의 값을 나타냅니다.
-오퍼레이션(Operation): 인스트럭션이 수행할 동작을 나타냅니다.
-플래그(Flag): 인스트럭션이 수행된 결과를 저장하는 데 사용되는 값입니다.
실행파일을 유닉스 시스템에서 실행하기 위해서 쉘이라는 응용프로그램에 이름을 입력한다.
linux> ./hello 쉘은 hello 프로그램을 로딩하고 실행한 뒤에 종료를 기다린다.
hello, wolrd hello 프로그램은 메시지를 화면에 출력하고 종료한다.
linux> 쉘은 프롬프트를 출력해 주고 다음 입력 명령어 라인을 기다린다.
hello 프로그램을 실행할 때 무슨 일이 일어나는지 설명하기 위해서는 전형적인 시스템에서의 하드웨어 조직을 이해할 필요가 있다.
버스
시스템 내를 관통하는 전기적 배선군을 버스라고 하며, 컴포넌트들 간에 바이트 정보들을 전송한다. 버스는 일반적으로 word(32비트 : 4바이트 /64비트 : 8바이트)단위로 데이터를 전송하도록 설계된다.
입출력 장치
입출력 장치는 시스템과 외부세계와의 연결을 담당한다. 예제 시스템은 네 개의 입출력 장치를 가지고 있다. 마우스, 키보드, 디스플레이, 디스크 드라이브. hello 실행파일은 디스크에 저장되어 있다. 각 입출력 장치는 입출력 버스와 컨트롤러나 어댑터를 통해 연결된다. 이 두 장치의 차이는 패키징에 있다. 컨트롤러는 디바이스 자체가 칩셋이거나 메인보드에 장착된다. 어댑터는 메인보드의 슬롯에 장착되는 카드이다.
메인 메모리
메인 메모리는 프로세서가 프로그램을 실행하는 동안 데이터와 프로그램을 모두 저장하는 임시 저장장치다. 물리적으로 메인 메모리는 DRAM 칩들로 구성되어 있다. 논리적으로 메모리는 연속적인 바이트들의 배열로 0부터 시작해서 각 고유의 주소를 가진다.
프로세서
주처리장치(CPU) or 프로세서는 메인 메모리에 저장된 인스트럭션들을 해독하는 엔진이다. 프로세서의 중심에는 word 크기의 저장장치(Register)인 프로그램 카운터(PC)가 있다. 프로세서는 PC가 가리키는 곳의 메모리로부터 인스트럭션을 읽어오고, 비트들을 해석하여 지정된 동작을 실행한다. 그리고 PC를 다음 인스트럭션 위치로 업데이트 한다.(이 새로운 위치는 이전의 인스트럭션과 메모리 상에서 연속적일 수도 있고, 그렇지 않을 수도 있다.) 프로세서는 메인 메모리, 레지스터 파일, ALU(수식/논리 처리기) 주위를 순환한다. 레지스터 파일은 각각 고유의 이름을 갖는 word 크기의 레지스터 집합으로 구성되어 있다. ALU는 새 데이터와 주소 값을 계산한다.
적재(Load) : 메인 메모리에서 레지스터에 1 byte or 1 word를 이전 값에 덮어쓰는 방식으로 복사한다.
저장(Store) : 레지스터에서 메인 메모리로 1 byte or 1 word를 이전 값을 덮어쓰는 방식으로 복사한다.
작업(Operate) : 두 레지스터의 값을 ALU로 복사하고 두 개의 워드로 수식연산을 수행한 뒤, 결과를 덮어쓰기 방식으로 레지스터에 저장한다.
점프(Jump) : 인스트럭션 자신으로부터 1 word를 추출하고 이를 PC에 덮어쓰기 방식으로 복사한다.
앞 서 작성했던 hello 프로그램을 실행시켰을 때 무슨 일이 일어나는지 자세히 살펴보자.
Step 1
쉘 프로그램을 실행시키고 “./hello”를 입력한다.
메인 메모리에 “./hello”가 저장된다. USB Controller - Keyboard » I/O bridge » Bus Interface » Register file » Bus Interface » I/O bridge » Main Memory
Step 2
엔터를 누른다. 파일의 코드와 데이터를 복사하는 인스트럭션을 실행한다.
hello를 디스크에서 메인 메모리로 로딩한다.
출력 문자열인 “hello, world\n”이 메인 메모리에 포함된다. Disk » Disk Controller » I/O bridge » Main Memory
Step 3
hello 프로그램의 main 루틴의 인스트럭션을 실행한다.
“hello, world\n” 문자열을 메인 메모리로부터 레지스터로 복사한다.
문쟈열을 디스플레이 장치로 전송하여 화면에 글자들이 표시된다. Main memory » I/O bridge » Bus Interface » Register file » Bus Interface » I/O bridge » Graphics adapter
hello 예제로부터 얻게 되는 중요한 교훈은 시스템이 정보를 이동시키는 일에 매우 많은 시간을 보낸다는 것이다. 이러한 여러 복사과정들이 프로그램의 실제 작업을 느리게 하는 오버헤드이다. 그래서 시스템 설계자들은 이러한 복사과정을 가능한 빠르게 동작하도록 설계하려고 한다. 물리학의 법칙 때문에 더 큰 저장장치들은 더 작은 저장장치들보다 느린 속도를 갖는다. 하지만 더 빠른 장치를 만드는 것은 더 많은 비용이 든다.
시스템 드라이브는 메인 메모리보다 1,000 배 크기가 크지만 프로세서가 디스크에서 1 word의 데이터를 읽어드리는 데는 천만 배 더 오래걸릴 수 있다. 레지스터 파일은 수백 바이트를 저장하지만 메인 메모리는 십억 바이트를 저장한다. 프로세서는 레지스터 파일의 데이터를 읽는데 메모리보다 100배 이상 빨리 읽을 수 있다.
프로세서-메모리 간 격차에 대응하기 위해 시스템 설계자는 보다 작고 빠른 캐시 메모리라고 부르는 저장장치를 고안하여 프로세서가 단기간에 필요로 할 가능성이 높은 정보를 임시로 저장할 목적으로 사용한다.
자주 액세스할 가능성이 높은 데이터를 캐시가 보관하도록 설정하면 빠른 캐시를 이용해서 대부분의 메모리 작업을 수행할 수 있게 된다.
=> 캐시 메모리를 이해하는 응용 프로그래머는 캐시를 활용허여 자신의 프로그램 성능을 10배 이상 개선할 수 있다
모든 컴퓨터 시스템의 저장장치들은 다음과 같은 메모리 계층구조로 구성되어 있다. 계층의 곡대기에서부터 맨 밑바닥까지 이동할수록 저장장치들을 더 느리고, 크고, 비용이 싸진다.
레지스터 파일은 가장 최상위인 레벨 0에 해당하며 L1-L3의 캐시를 가진다. 메인 메모리는 다음 계층에 속한다. 이러한 메모리 계층 구조의 주요 아이디어는 한 레벨의 저장장치가 다음 하위레벨 저장장치의 캐시 역할을 안하는 것이다. L1과 L2의 캐시는 L2와 L3이며, 디스크의 캐시는 메인 메모리다.
운영체제(Operating System)는 하드웨어와 소프트웨어 사이에 위치한 소프트웨어 계층으로 생각할 수 있다. 응용프로그램이 하드웨어를 제어하려면 언제나 운영체제를 통해야만 한다.
- 제멋대로 동작하는 응용프로그램들이 하드웨어를 잘못 사용하는 것을 막기 위해
- 응용프로그램들이 단순하고 균일한 메커니즘을 사용하여 복잡하고 매우 다른 저수준 하드웨어 장치들을 조작할 수 있도록 하기 위해
프로세스는 실행 중인 프로그램에 대한 운영체제의 추상화다. 다수의 프로세스는 동일한 시스템에서 동시에(concurrently) 실행될 수 있으며, 각 프로세스는 하드웨어를 배타적으로 사용 것처럼 느낀다. 즉 동시에란 한 프로세스의 인스트럭션들이 다른 프로세스의 인스트럭션들과 섞일 수 있다는 것을 의미한다.
이는 한 개의 CPU가 다수의 프로세스를 동시에 실행하는 것처럼 보이게 해준다.
운영체제는 문맥 전환(context switching)이라는 방법을 사용해서 이러한 교차실행을 수행한다.
운영체제는 프로세스가 실행하는 데 필요한 모든 상태정보의 변화를 추적한다. 이 컨텍스트라고 부르는 상태정보는 PC, 레지스터 파일, 메인 메모리의 현재 값을 포함하고 있다.
운영체제는 현재 프로세스에서 다른 새로운 프로세스로 제어를 옮기려고 할 때 현재 프로세스의 컨텍스트를 저장하고 새 프로세스의 컨텍스트를 복원시키는 문맥전환을 실행하여 제어권을 새 프로세스로 넘겨준다.
리눅스 환경에서 쉘(bash.sh)이 실행되고 있을 때, 응용 프로그램(hello.bin)이 실행됐을 때의 예시다. 응용 프로그램이 운영체제에 의한 어떤 작업을 요청하면, 컴퓨터는 파일 읽기나 쓰기와 같은 특정 시스템 콜(system call)을 실행해서 커널에 제어를 넘겨준다. 그러면 커널은 요청된 작업을 수행하고 응용프로그램으로 리턴한다.
단, 커널은 별도의 프로세스가 아니다. 모든 프로세스를 관리하기 위해 시스템이 이용하는 코드와 자료구조의 집합이다. 이처럼 하나의 프로세스에서 다른 프로세스로의 전환은 커널(Kernel)에 의해 관리된다. 커널은 운영체제 코드의 일부분으로 메모리에 상주한다.
쓰레드(Thread)는 다수의 실행 유닛으로 구성되어 있으며, 각각의 쓰레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역 데이터를 공유한다. 다수의 프로세스들에서보다 데이터의 공유가 더 쉽고, 프로세스보다 더 효율적이라는 장점을 가지고 있다.
가상 메모리는 각 프로세스들이 메인 메모리 전체를 독점적으로 사용하고 있는 것 같은 환상을 제공하는 추상화이다. 각 프로세스는 가상주소 공간이라고 하는 균일한 메모리의 모습을 갖게 된다.
주소공간의 최상위 영역은 모든 프로세스들이 공통으로 사용하는 운영체제의 코드와 데이터를 위한 공간이다. 주소공간의 하위 영역은 사용자의 프로세스의 코드와 데이터를 저장한다.
프로그램 코드와 데이터 : 코드는 모든 프로세스들이 같은 고정 주소에서 시작하며, 다음에 C 전역변수에 대응되는 데이터 위치들이 따라온다.
힙(Heap) : 힙은 프로세스가 실행되면서 C 표준함수인 malloc이나 free를 호출하면서 런타임에 동적으로 그 크기가 늘었다 줄었다 한다.
공유 라이브러리 : 주소공간의 중간 부근에 위치하며 공유 라이브러리의 코드와 데이터를 저장하는 영역이다.
스택(Stack) : 사용자가 가상메모리 공간의 맨 위에 컴파일러 함수 호출을 구현하기 위해 사용하며, 힙처럼 프로그램이 실행되는 동안에 동적으로 늘어났다 줄어들었다 한다. 특히, 함수를 호출할 때마다 스택이 커지며, 함수에서 리턴될 때는 줄어든다.
커널 가상메모리 : 주소공간의 맨 윗부분은 커널을 위해 예약되어 있다. 이 영역의 내용을 읽거나 쓰는 것이 금지되어 있다. 대신, 커널을 호출하면 된다.
파일(file)은 그저 연속된 바이트들이다.시스템의 모든 입출력은 유닉스 I/O라는 시스템 콜들을 이용하여 파일을 읽고 쓰는 형태로 이루어진다.
응용 프로그램에 시스템에 들어 있는 다양한 입출력장치들의 통일된 관점을 제공한다.
지금까진 시스템으로의 여행을 통하서 시스템을 하드웨어와 스프트웨어의 분리된 집합체로 취급하였다. 개별 시스템의 관점에서 볼 때, 네트워크는 또 다른 입출력 장치로 볼 수 있다.
시스템이 메인 메모리로부터 네트워크 어댑터로 일련의 바이트를 복사할 때, 데이터는 로컬디스크 드라이브 대신에 네트워크를 통해서 다른 컴퓨터로 이동된다. 마찬가지로 시스템은 다른 컴퓨터로부터 받은 데이터를 읽어서 메인 메모리에 복사 할 수 있다.
hello를 telnet 응용으로 다른 곳에 위치한 컴퓨터에서 실행
"hello" 스트링을 telnet 클라이언트에 입력하고 엔터 키를 누른 후, 클라이언트 프로그램은 이 스트링을 telnet 서버로 보낸다.
telnet 서버가 네트워크에서 스트링을 받은 후, 원격 쉘 프로그램에 이들을 전달한다.
원격 쉘은 hello 프로그램을 실행하고 출력을 다시 telnet 서버로 전달한다.
telnet 서버는 네트워크를 거쳐 출력 스트링을 telnet 클라이언트로 전달하고, 클라이언트 프로그램은 출력 스트링을 자신의 로컬 터미널에 표시한다.
클라이언트와 서버 간의 데이터 교환은 모든 네트워크 응용의 전형적인 사례이다.
- 시스템이라는 것이 단지 하드웨어 그 이상의 것이라는 점
- 응용프로그램의 실행이라는 궁극의 목적을 달성하기 위해 협력해야 하는 하드웨어와 시스템 소프트웨거가 서로 연결된 것이라는 점
계산학 분야의 초창기 개척자인 Gene Amdahl은 시스템의 일부 성능 개선의 효율성에 대해 간단하지만 직관적인 관찰을 하였다.
우리가 어떤 시스템의 한 부분의 성능을 개선할 때, 전체 시스템 성능에 대한 효과는 그 부분이 얼마나 중요한가 이 부분이 얼마나 빨라 졌는가에 관계된다.
비록 시스템의 주요 부분에 대해 실질적인 개선을 하였지만, 총 속도향상은 매우 적다. 이것이 이 법칙의 주요 통찰이다.
전체 시스템을 상당히 빠르게 하기 위해서는 전체 시스템의 매우 큰 부분의 성능을 개선해야 한다.
동시성과 병렬성 두 개의 요구가 지속적으로 디지털 컴퓨터의 성능개선을 주도해왔다. 컴퓨터가 더 많은 일을 해내고, 더 빨리 실행되기를 원한다. 이런 요구 모두 프로세서가 한번에 더 많은 일을 할 떄 개선되는 특징이 있다.
동시성
다수의 동시에 벌어지는 일을 갖는 시스템에 관한 일반적인 개념
벙렬성
동시성을 사용해서 시스템을 보다 빠르게 동작하도록 하는 것
프로세스의 추상화 개념을 이용하면 다수의 프로그램이 동시에 실행되는 시스템을 생각해 볼 수 있으며, 이것은 결국 동시성으로 이어지게 된다.
쓰레드를 이용하면 한 개의 프로세스 내에서 실행되는 다수의 제어흐름을 가질 수도 있다.
시간공유(time-sharing)
한 개의 컴퓨터가 실행하는 프로세스를 빠르게 전환하는 방법으로, 한 명의 사용자가 다수의 태스크에 동시에 연관될 수 있게 해준다.
단일 프로세서 시스템
실질적인 계산을 한 개의 프로세서에서 이루어지는 시스템
멀티프로세서 시스템
여러 개의 프로세서를 가지고 하나의 운영체제 커널의 제어 하에 동작하는 시스템으로, 멀티코어 프로세서와 하이퍼쓰레딩(Hyperthreading) 기법의 출현으로 일반적인 환경이 되었다.
멀티 프로세싱의 이용은 시스템 성능을 두 가지 방법으로 개선할 수 있다.
- 다수의 태스크를 실행할 때, 동시성을 시뮬레이션할 필요를 줄여준다,
- 멀티 프로세싱으로 한 개의 응용프로그램을 빠르게 실행할 수 있지만, 프로그램이 병렬로 효율적으로 싱행할 수 있는 멀티쓰레드의 형태로 표현되었을 때에만 가능하다.
최근의 프로세서들은 훨씬 낮은 수준에서의 추상화로 여러개의 인스트럭션을 한번에 실행 할 수 있다. 이리한 특성을 인스트럭션 수준 병렬성이라고 한다.
사이클 당 한 개 이상의 인스트럭션을 실행할 수 있는 프로세서를 슈퍼스케일러(super-scalar)라고 한다.
많은 최신 프로세서들은 최하위 수준에서 싱글 인스트럭션, 다중 데이터, 즉 SIMD 병렬성이라는 모드로 한 개의 인스트럭션이 병렬로 다수의 연산을 수행할 수 있는 특수 하드웨어를 가지고 있다.
추상화의 사용은 전산학에서 가장 중요한 개념이다. 대표적으로 함수들을 간단한 응용프로그램 인터페이스 API로 정형화하는 것, 프로그래머가 그 내부의 동작을 고려하지 않으면서 코드를 사용할 수 있도록 해주는 것, JAVA의 경우는 클래스 선언, c에서는 함수 프로토타입을 예를 들 수 있다.
프로세서 측면에서 인스트럭션 집합구조는 실제 프로세서 하드웨어의 추상화를 제공한다. 이러한 추상화로 인해 기계어 코드 프로그램은 마치 한 번에 하나으 인스트럭션을 실행하는 프로세서에서 실행되는 것 처럼 동작한다. 실제 하드웨어는 훨씬 더 정교해서 여러 개의 인스트럭션을 병렬로, 그러나 항상 간단한 순차적인 모델에 의거한 방식으로 실행한다.