CSAPP : 1장 컴퓨터 시스템으로의 여행
CSAPP
hello world 출력하는 간단한 프로그램조차도 실행되고 종료되기 위해서 시스템 주요 부분들이 조화롭게 동작해야 함.
이게 어떤 과정으로 일어나는지 알아보는 것이 이 책의 목적.
1.2 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다
linux> gcc -o hello hello.c
이거 유닉스 시스템에서 입력하면 hello.c를 읽어서 실행파일인 hello로 번역한다.
번역은 전처리기, 컴파일러, 어셈블러, 링커 이 4가지의 컴파일 시스템을 거친다.
- 전처리 단계 : 전처리기(cpp)는 C 프로그램을 #문자로 시작하는 디렉티브에 따라 수정한다.
#include<stdio.h>
<< 이거 프로그램 문장에 삽입한다는 뜻. .i로 끝나는 새로운 C 프로그램이 생성됨. - 컴파일 단계 : 컴파일러(ccl)는 텍스트파일 hello.i를 텍스트 파일인 hello.s로 번역하며, 이 파일에는 어셈블리어 프로그램이 저장됨.
- 어셈블리 단계 : 어셈블러(as)가 hello.s를 기계어 인스트럭션으로 번역해서 재배치가능 목적프로그램 의 형태로 묶어 hello.o라는 목적파일에 그 결과를 저장. (이제 열어보면 해석불가)
- 링크 단계 : 링커 프로그램(ld)이 표준 C 라이브러리인 printf.o에 들어있는 printf 함수를 hello.o 파일과 결합시킴. 그 결과인 hello 파일은 실행파일로 메모리에 적재되어 시스템에 의해 실행됨
1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다
hello.c처럼 간단한 프로그램은 컴파일 시스템이 알아서 잘 해주겠지만...
프로그램 성능 최적화하기
: c 프로그램 작성 시 올바른 판단을 하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있음
- switch 문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
- 함수 호출 시 발생하는 오버헤드는 얼마나 되는가? (오버헤드 : 프로그램의 실행흐름 도중에 동떨어진 위치의 코드를 실행시켜야 할 때 , 추가적으로 시간, 메모리, 자원이 사용되는 현상)
- while 루프는 for 루프보다 더 효율적일까?
- 포인터 참조가 배열 인덱스보다 더 효율적인가?
- 합계를 지역변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가?
- 수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유는 무엇인가?
프로그램 성능 최적화하기 : GPT 답변
1. switch 문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
switch 문은 컴파일러가 효율적으로 최적화할 수 있는 경우가 많습니다. 특히, switch 문이 많은 case를 가지며 이들이 정수형 상수일 경우, 컴파일러는 이를 "jump table"로 최적화할 수 있습니다. 이렇게 하면 상수 값에 따라 직접적인 점프가 가능하여 효율적입니다.
if-else 문은 연속적으로 조건을 평가합니다. 각 조건이 평가되어야 하므로, 많은 조건이 있는 경우 시간이 더 많이 소요될 수 있습니다.
하지만, switch 문이 항상 if-else 문보다 효율적인 것은 아닙니다. 조건이 복잡하거나 비정수형 데이터인 경우, if-else 문이 더 적합할 수 있습니다.
결론적으로, switch 문이 정수형 상수 값을 비교하는 경우에는 일반적으로 더 효율적입니다.
2. 함수 호출 시 발생하는 오버헤드는 얼마나 되는가?
함수 호출 시 발생하는 오버헤드는 다음과 같은 요소들로 구성됩니다:
스택 프레임 생성: 함수 호출 시 스택에 새로운 프레임이 생성되어야 합니다.
인자 전달: 함수 인자들이 스택에 push되거나 레지스터를 통해 전달되어야 합니다.
리턴 주소 저장: 호출된 함수가 끝난 후 돌아갈 주소를 스택에 저장해야 합니다.
함수 호출 및 리턴: 함수 진입 및 반환을 위한 명령어가 실행됩니다.
이 오버헤드는 함수의 인자 수, 인자 전달 방식(레지스터 vs 스택), 함수의 호출 빈도 등에 따라 달라집니다.
일반적으로 함수 호출 오버헤드는 몇 개의 명령어 사이클을 추가로 소비합니다. 하지만, 인라인 함수로 최적화할 수 있는 경우, 이 오버헤드를 줄일 수 있습니다.
3. while 루프는 for 루프보다 더 효율적일까?
while 루프와 for 루프는 거의 동일한 방식으로 동작합니다. 컴파일러는 두 루프를 거의 같은 기계어 코드로 변환합니다.
for 루프는 초기화, 조건 검사, 증감 부분을 한 곳에서 선언할 수 있어 코드가 더 깔끔하고 가독성이 좋습니다.
while 루프는 조건만 검사하며, 초기화와 증감 부분은 루프 외부에 명시해야 합니다.
따라서, 성능 측면에서 while 루프와 for 루프의 차이는 거의 없으며, 주로 가독성과 코드 스타일에 따라 선택됩니다.
4. 포인터 참조가 배열 인덱스보다 더 효율적인가?
포인터 참조는 배열 인덱스 연산과 동일한 기계어 명령어로 컴파일될 수 있습니다. 즉, 배열 인덱스는 포인터 연산으로 변환됩니다.
예를 들어, array[i]는 내부적으로 *(array + i)로 변환됩니다.
그러나, 포인터를 직접 사용하는 경우, 인덱스 연산을 명시적으로 하지 않으므로 코드가 더 직관적일 수 있습니다.
특정 경우, 포인터 연산이 더 효율적일 수 있지만, 현대의 최적화 컴파일러는 배열 인덱스와 포인터 참조 간의 성능 차이를 거의 없앱니다.
5. 합계를 지역변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가?
지역 변수는 스택에 저장되며, 접근이 매우 빠릅니다.
반면, **참조 형태(포인터)**로 넘겨받은 인자는 메모리의 다른 위치(힙 또는 다른 스택 프레임)에 위치할 수 있으며, 접근 시 메모리 지연이 발생할 수 있습니다.
캐시 효율성도 큰 차이를 만듭니다. 지역 변수는 일반적으로 캐시에 잘 들어맞지만, 참조된 데이터는 그렇지 않을 수 있습니다.
따라서, 루프 내에서 빈번히 접근하는 데이터는 지역 변수에 저장하는 것이 더 효율적입니다.
6. 수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유는 무엇인가?
수식 연산에서 괄호를 재배치하면 연산 순서가 변경됩니다. 이는 연산의 중간 결과를 저장하고 재사용할 수 있도록 하여 효율성을 높일 수 있습니다.
예를 들어, (a + b) + c와 a + (b + c)는 중간 결과를 달리 생성합니다.
(a + b) + c는 먼저 a + b를 계산한 후 그 결과에 c를 더합니다.
a + (b + c)는 먼저 b + c를 계산한 후 그 결과에 a를 더합니다.
연산 순서를 바꾸면 CPU 레지스터와 캐시 사용이 최적화될 수 있습니다.
특히, 컴파일러는 상수 식을 미리 계산하거나, 공통 부분을 미리 계산하여 반복적으로 사용되는 값을 줄임으로써 성능을 최적화할 수 있습니다.
링크 에러 이해하기
: 가장 당혹스러운 프로그래밍 에러는 링커의 동작과 관련되어 있으며, 큰 규모 소프트웨어 빌드할 때 더욱 그럼.
- 링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가?
- 정적변수와 전역변수의 차이는 무엇인가?
- 만일 각기 다른 파일에 동일한 이름의 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가?
- 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
- 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
- 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?
링크 에러 이해하기 : GPT 답변
1. 링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가?
링커가 참조를 풀어낼 수 없다고 하는 것은 프로그램에서 사용된 심볼(변수, 함수 등)의 정의를 찾을 수 없음을 의미합니다. 이는 여러 가지 이유로 발생할 수 있습니다:
함수나 변수가 선언만 되고 정의되지 않았을 때.
링킹 시 필요한 라이브러리가 누락되었을 때.
오타나 잘못된 이름 사용으로 인해 참조가 실패할 때.
필요한 객체 파일이나 라이브러리가 컴파일 또는 링크 과정에서 포함되지 않았을 때.
2. 정적변수와 전역변수의 차이는 무엇인가?
정적 변수 (static variable):
파일 내 정적 변수: 함수 외부에서 선언되었지만 static 키워드가 붙어있는 변수입니다. 해당 파일 내에서만 접근 가능하며, 다른 파일에서는 접근할 수 없습니다.
함수 내 정적 변수: 함수 내부에서 선언된 static 변수로, 함수가 호출될 때마다 초기화되지 않고, 프로그램이 종료될 때까지 값을 유지합니다.
전역 변수 (global variable):
파일 내 어디서나 접근할 수 있는 변수입니다.
static 키워드가 붙지 않은 변수로, 다른 파일에서 extern 키워드를 사용하여 참조할 수 있습니다.
3. 만일 각기 다른 파일에 동일한 이름의 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가?
동일한 이름의 전역 변수를 각기 다른 파일에서 정의하면 링킹 과정에서 충돌이 발생합니다. 링커는 두 개의 변수를 구분할 수 없으므로 multiple definition 에러가 발생합니다.
이러한 충돌을 피하기 위해 헤더 파일에는 전역 변수를 extern 키워드로 선언하고, 하나의 소스 파일에서만 해당 변수를 정의하는 것이 좋습니다.
4. 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
정적 라이브러리 (Static Library):
.a 또는 .lib 확장자를 가집니다.
컴파일 시에 라이브러리의 코드가 프로그램의 실행 파일에 포함됩니다.
실행 파일이 더 커지지만, 배포 시 별도의 라이브러리가 필요하지 않습니다.
동적 라이브러리 (Dynamic Library):
.so (Shared Object) 또는 .dll (Dynamic Link Library) 확장자를 가집니다.
실행 파일이 실행될 때 라이브러리가 동적으로 로드됩니다.
실행 파일 크기가 작아지지만, 실행 시 라이브러리가 필요합니다.
5. 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
컴파일 시 명령어 라인에서 라이브러리의 순서는 중요합니다.
링커는 왼쪽에서 오른쪽으로 참조를 해결합니다. 즉, 어떤 라이브러리가 다른 라이브러리를 필요로 하는 경우, 참조되는 라이브러리가 참조하는 라이브러리보다 앞에 와야 합니다.
잘못된 순서로 인해 참조가 해결되지 않아 링킹 오류가 발생할 수 있습니다.
예를 들어, gcc main.o -lmylib1 -lmylib2에서 mylib1이 mylib2를 참조한다면 순서가 맞지만, 반대의 경우 참조를 해결할 수 없어 오류가 발생합니다.
6. 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?
링커는 컴파일된 객체 파일들을 결합하여 실행 파일을 생성하는 역할을 합니다.
컴파일러는 각 소스 파일을 독립적으로 컴파일하여 객체 파일을 생성합니다. 이 과정에서 변수나 함수의 선언은 확인하지만, 정의는 확인하지 않습니다.
링커는 이 객체 파일들을 결합하면서 참조된 모든 심볼의 정의를 찾아야 합니다. 만약 정의를 찾을 수 없으면 링킹 에러가 발생합니다.
따라서, 링커와 관련된 에러는 컴파일 시점이 아니라 링킹 시점에 나타납니다.
동적 라이브러리의 경우, 런타임에 로드될 때 참조를 해결하므로, 이 때 에러가 발생할 수도 있습니다.
보안 약점 피하기
: 오랫동안 버퍼 오버플로우 취약성이 인터넷과 네트워크 상의 보안 약점의 주요 원인으로 설명되었다. 이 취약성은 프로그래머들이 신뢰할 수 없는 곳에서 획득한 데이터의 양과 형태를 주의 깊게 제한해야 할 필요를 거의 인식하지 못하기 때문에 생겨난다.
3장에서는 스택체제, 버퍼 오버플로우 취약성, 공격의 위협을 줄이기 위한 프로그래머, 컴파일러, 운영체제에서 사용할 수 있는 방법들을 학습함.
보안 약점 피하기 : GPT 답변
버퍼 오버플로우의 원리
버퍼 오버플로우는 프로그램이 버퍼(메모리 블록)에 예상보다 더 많은 데이터를 쓰려고 할 때 발생합니다. 일반적으로, 버퍼는 고정된 크기를 가지며, 이 크기를 초과하는 데이터를 저장하려고 하면 메모리의 다른 영역을 덮어쓰게 됩니다. 이는 메모리 손상을 초래하며, 악의적인 공격자는 이를 이용해 코드를 주입하거나 프로그램의 흐름을 조작할 수 있습니다.
버퍼 오버플로우를 방지하는 방법
입력 데이터 검증 및 제한:
모든 입력 데이터를 검증하고, 버퍼 크기를 초과하지 않도록 제한해야 합니다.
예를 들어, strncpy 함수는 복사할 데이터의 최대 크기를 지정할 수 있습니다.
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // null-terminate the buffer
안전한 라이브러리 함수 사용:
gets 함수 대신 fgets와 같은 안전한 입력 함수를 사용합니다.
fgets(input, sizeof(input), stdin);
컴파일러 보호 기능 사용:
현대 컴파일러는 버퍼 오버플로우를 방지하는 다양한 보호 기능을 제공합니다.
예를 들어, 스택 보호(stack protection), 주소 공간 배치 랜덤화(Address Space Layout Randomization, ASLR) 등을 사용합니다.
정적 분석 도구 사용:
정적 분석 도구를 사용해 코드의 잠재적 버퍼 오버플로우 취약성을 사전에 발견하고 수정할 수 있습니다.
메모리 안전 언어 사용:
C와 같은 메모리 관리가 어려운 언어 대신, 메모리 안전성이 높은 언어(예: Java, Python, Rust)를 사용합니다.
1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다
hello.c 소스 프로그램을 컴파일 시스템이 번역하면 실행파일이 생기고,
이것을 유닉스 시스템에서 실행하기 위해 쉘이라는 응용 프로그램에 그 이름을 입력한다.
linux> ./hello
hello, world
linux>
쉘 : 커맨드라인 인터프리터, 프롬프트 출력하고 명령어 입력받아서 실행한다. 명령어가 내장 쉘 명령어가 아니면 실행파일 이름으로 판단하고 그 파일을 로딩해서 실행해줌.
1.4.1 시스템의 하드웨어 조직
버스(Buses)
- 버스(bus) : 시스템 내를 관통하는 전기적 배선군. 워드(word)라는 고정 크기의 바이트 단위로 데이터를 전송하도록 설계됨.
- 한 개의 워드를 구성하는 바이트 수는 시스템마다 보유하는 기본 시스템 변수
- 오늘날 대부분의 컴퓨터들은 4바이트(32비트) 또는 8바이트(64비트) 워드 크기를 갖는다
입출력 장치
- 각 입출력 장치는 입축력 버스와 컨트롤러 나 어댑터 를 통해 연결됨
- 컨트롤러와 어댑터의 차이는 패키징에 있음
- 컨트롤러 : 디바이스 자체가 칩셋이거나 시스템의 인쇄기판 (마더보드)에 장착된다
- 어댑터 : 마더보드의 슬롯에 장착되는 카드
메인 메모리
- 메인 메모리 : 프로세서가 프로그램을 실행하는 동안 데이터와 프로그램을 모두 저장하는 임시 저장장치
- 물리적으로 메인 메모리는 DRAM (Dynamic Random Access Memory) 칩들로 구성되어 있음
- 논리적으로 메모리는 연속적인 바이트들의 배열로, 각각 0부터 시작해서 고유의 주소(배열의 인덱스)를 가지고 있다.
프로세서
- 주처리장치(CPU) (=프로세서) : 메인 메모리에 저장된 인스트럭션들을 해독(실행)하는 엔진
- 프로세서의 중심에는 워드 크기의 저장장치(또는 레지스터)인 프로그램 카운터(PC)가 있다.
- 전원 공급부터 끊어질 때까지 프로세서는 프로그램 카운터가 가리키는 곳의 인스트럭션을 반복적으로 실행하고 PC값이 다음 인스트럭션의 위치를 가리키도록 업데이트함.
인스트럭션의 요청에 의해 CPU가 실행하는 단순한 작업의 예
- 적재(Load): 메인 메모리에서 레지스터에 한 바이트 또는 워드를 이전 값에 덮어쓰는 방식으로 복사한다.
- 저장(Store): 레지스터에서 메인 메모리로 한 바이트 또는 워드를 이전 값을 덮어쓰는 방식으로 복사한다.
- 작업(Operate): 두 레지스터의 값을 ALU로 복사하고 두 개의 워드로 수식연산을 수행한 뒤, 결과를 덮어쓰기 방식으로 레지스터에 저장한다.
- 점프(Jump): 인스트럭션 자신으로부터 한 개의 워드를 추출하고, 이것을 PC에 덮어쓰기 방식을 복사한다.
적재와 저장의 차이(GPT답변)
적재 (Load):
- 적재는 메모리에서 데이터를 읽어와서 레지스터에 저장하는 작업입니다.
- 예를 들어, RAM에 저장된 값을 CPU의 레지스터로 가져오는 작업입니다.
- 이는 메모리에서 레지스터로 데이터를 "적재"하는 것이라고 할 수 있습니다.
저장 (Store):
- 저장은 레지스터에 있는 데이터를 메모리에 기록하는 작업입니다.
- 예를 들어, CPU의 레지스터에 있는 값을 RAM에 저장하는 작업입니다.
- 이는 레지스터에서 메모리로 데이터를 "저장"하는 것이라고 할 수 있습니다.
간단히 말하면:
- 적재 (Load): 메모리 → 레지스터### ALU (산술 논리 장치)
- 저장 (Store): 레지스터 → 메모리
ALU (산술 논리 장치)
- ALU는 컴퓨터 프로세서의 핵심 구성 요소 중 하나로, 산술 및 논리 연산을 수행합니다.
- 산술 연산에는 덧셈, 뺄셈, 곱셈, 나눗셈이 포함됩니다.
- 논리 연산에는 AND, OR, NOT, XOR와 같은 비트 수준의 연산이 포함됩니다.
- ALU는 데이터를 처리하고 결과를 레지스터에 저장합니다.
레지스터 (Register)
- 레지스터는 CPU 내부에 있는 고속 메모리 저장소입니다.
- 레지스터는 매우 빠른 속도로 데이터를 읽고 쓸 수 있으며, 주로 연산 중간 결과를 저장하거나, 데이터를 임시로 저장하는 데 사용됩니다.
- 레지스터는 메모리보다 용량이 작지만 훨씬 빠릅니다.
1.5 캐시가 중요하다
이전의 내용이 의미하는 것은, 시스템은 정보를 한 곳에서 다른 곳으로 이동시키는 일에 많은 시간을 보낸다는 것이다. 그래서 시스템 설계자들의 주요 목적은 이러한 복사과정들을 가능한 한 빠르게 동작하도록 하는 것이다.
물리학의 법칙 때문에 더 큰 저장장치들은 보다 작은 저장장치들보다 느린 속도를 갖는다. 그리고 더 빠른 장치들은 더 느린 장치들보다 만드는 데 더 많은 비용이 든다.
일반적인 레지스터 파일은 수백 바이트의 정보를 저장하는 반면, 메인 메모리의 경우는 십억 개의 바이트를 저장한다. 그러나 프로세서는 레지스터 파일의 데이터를 읽는데 메로리의 경우보다 거의 100배 더 빨리 읽을 수 있다. 이 격차는 기술이 발달하며 매년 커지고 있다.
이에 대응하기 위해 캐시가 고안되었다. 캐시는 프로세서가 단기간에 필요로 할 가능성이 높은 정보를 임시로 저장할 목적으로 사용한다.
1.6 저장장치들은 계층구조를 이룬다
빠른 것도 있고 느린 것도 있다. L0, L1, L2 ... 이런식으로 부른다. 숫자가 클수록 느리고 저장용량 크다.
메모리 계층구조의 주요 아이디어는 한 레벨의 저장장치가 다음 하위레벨 저장장치의 캐시 역할을 한다는 것이다. L1과 L2의 캐시는 각각 L2와 L3의 캐시다. 로컬 디스크들은 원격 네트워크 서버에서 파일들을 가져와 보관한다.
1.7 운영체제는 하드웨어를 관리한다.
운영체제는 하드웨어와 소프트웨어 사이에 위치한 소프트웨어 계층으로 생각할 수 있다. 응용프로그램이 하드웨어를 제어하려면 언제나 운영체제를 통해서 해야 한다.
운영체제의 두 가지 주요 목적
- 제멋대로 동작하는 응용프로그램들이 하드웨어를 잘못 사용하는 것을 막는다.
- 응용 프로그램들이 단순한 매커니즘을 사용해 복잡하고 다양한 하드웨어를 조작할 수 있게 한다.
목적 달성을 위해서 3가지로 추상화를 한다
- 파일 : 입출력장치의 추상화
- 가상메모리 : 메인 메모리, 디스크 입출력 장치의 추상화
- 프로세스 : 프로세서, 메인 메모리, 입출력장치 모두의 추상화
1.7.1 프로세스
프로세스 : 실행 중인 프로그램에 대한 운영체제의 추상화
다수의 프로세스들은 동일한 시스템에서 동시에 실행될 수 있으며, 각 프로세스는 하드웨어를 배타적으로 사용하는 것처럼 느낀다. 운영체제는 문맥 전환(context switching)이라는 방법을 사용해서 이러한 교차실행을 수행한다.
운영체제는 프로세스가 실행하는 데 필요한 모든 상태정보의 변화를 추적한다. 이 '컨텍스트'라고 부르는 PC, 레지스터 파일, 메인 메모리의 현재 값을 포함하고 있다. 운영체제는 현재 프로세스에서 다른 프로세스로 제어를 옮기려고 할 때 현재 프로세스의 컨텍스트를 저장하고 새 프로세스의 컨텍스트를 복원시키는 문맥 전환을 실행하여 제어권을 새 프로세스로 넘겨준다.
문맥 전환은 운영체제 커널(kernal)에 의해 관리된다. 커널은 운영체제 코드의 일부분으로 메모리에 상주한다. 응용프로그램이 운영체제에 의한 어떤 작업을 요청하면, 컴퓨터는 특정 파읽 읽기나 쓰기와 같은 특정 시스템 콜(system call)을 실행해서 커널에 제어를 넘겨준다. 그러면 커널은 요청된 작업을 수행하고 응용프로그램으로 리턴한다.
1.7.2 쓰레드(Thread)
프로세스는 쓰레드라고 하는 다수의 실행 유닛으로 구성되어 있다. 각각의 쓰레드는 해당 프로세스의 컨텍스트에서 실행되며 동일한 코드와 전역 데이터를 공유한다. 다중 쓰레딩은 다중 프로세서를 활용할 수 있다면 프로그램의 실행 속도를 빠르게 할 수 있다.
1.7.3 가상메모리
가상메모리 : 각 프로세스들이 메인 메모리 전체를 독점적으로 사용하고 있는 것처럼 만들어주는 추상화
각 프로세스는 가상주소 공간이라고 하는 균일한 메모리의 모습을 갖게 된다. 리눅스에서, 주소공간의 최상위 영역은 모든 프로세스들이 공통으로 사용하는 운영체제의 코드와 데이터를 위한 공간이다. 주소공간의 하위 영역은 사용자 프로세스의 코드와 데이터를 저장한다.
각 프로세스들에게 보여지는 가상주소공간들
- 프로그램 코드와 데이터 : 코드는 모든 프로세스들이 같은 고정 주소에서 시작하며, 다음에 C 전역변수에 대응되는 데이터 위치들이 따라온다. 코드와 데이터 영역은 실행가능 목적파일인 hello로부터 직접 초기화된다.
- 힙(Heap) : 코드와 데이터 영역 다음으로 런타임 힙이 따라온다. 힙은 프로세스가 실행되면서 C 표준함수인 malloc이나 free를 호출하면서 런타임에 동적으로 그 크기가 늘었다 줄었다 한다.
- 공유 라이브러리 : 주소공간의 중간 부근에 C 표준 라이브러리나 수학 라이브러리와 같은 공유 라이브러리의 코드와 데이터를 저장하는 영역.
- 스택(Stack) : 사용자 가상메모리 공간의 맨 위에 컴파일러가 함수 호출을 구현하기 위해 사용하는 사용자 스택이 위치한다. 힙과 마찬가지로 늘었다 줄었다 한다. 특히, 함수를 호출할 때마다 스택이 커지며, 함수에서 리턴될 때는 줄어든다.
- 커널 가상메모리 : 주소공간의 맨 윗부분은 커널을 위해 예약되어 있다. 응용프로그램들은 이 영역의 내용을 읽거나 쓰는 것이 금지되어 있으며, 마찬가지로 커널 코드 내에 정의된 함수를 직접 호출하는 것도 금지되어 있다.
가상메모리가 작동하기 위해서는 프로세서가 만들어내는 모든 주소를 하드웨어로 번역하는 등의 하드웨어와 운영체제 소프트웨어 간의 복잡한 상호작용이 필요하다. 기본적인 아이디어는 프로세스의 가상메모리의 내용을 디스크에 저장하고 메인 메모리를 디스크의 캐시로 사용하는 것이다.
1.7.4 파일
파일 : 연속된 바이트
1.8 시스템은 네트워크를 사용하여 다른 시스템과 통신한다
네트워크를 단지 또 다른 입출력 장치로도 볼 수 있다.
1.9 중요한 주제들
1.9.1 Amdahl의 법칙
일부 성능 개선 해도 총 속도 향상은 적다
1.9.2 동시성과 병렬성
쓰레드 수준 동시성
여러 스레드가 동시에 실행되면서 하나의 프로세스 내에서 작업을 병렬로 수행
멀티프로세서 시스템
어떤 시스템이 여러 개의 프로세서를 가지고 하나의 운영체제 커널의 제어 하에 동작
하이퍼쓰레딩(멀티쓰레딩)
하나의 CPU가 여러 개의 제어 흐름을 실행하는 기술
인스트럭션 수준 병렬성
프로세서가 여러 명령어를 동시에 실행하는 기술
파이프라이닝 : 하나의 인스트럭션을 실행하기 위해 요구되는 일들을 여러 단계로 나누고 프로세서 하드웨어가 일련의 단계로 구성되어 이들 단계를 하나씩 각각 수행한다.
슈퍼스케일러 : 사이클당 한 개 이상의 인스트럭션을 실행할 수 있는 프로세서
싱글 인스트럭션, 다중 데이터 병렬성(SIMD)
한 개의 인스트럭션이 병렬로 다수의 연산을 수행하는 기술
1.9.3 컴퓨터 시스템에서 추상화의 중요성
추상화의 사용은 전산학에서 가장 중요한 개념. 예를 들면, 좋은 프로그래밍 연습의 한 가지 측면은 함수들을 간단한 응용프로그램 인터페이스 API로 정형화하는 것으로, 프로그래머가 그 내부의 동작을 고려하지 않으면서 코드를 사용할 수 있도록 해준다.
운영체제 측면 세 가지 추상화를 소개했었다. 파일을 입출력 장치의 추상화로, 가상메모리는 프로그램 메모리의 추상화로, 프로세스는 실행 중인 프로그램의 추상화로. 여기에 한 가지를 더하면, 가상머신이 있다. 가상머신은 운영체제, 프로세서, 프로그램 모두를 포함하는 컴퓨터 전체의 추상화를 제공한다.
1.10 요약
컴퓨터 시스템은 응용프로그램을 실행하기 위해 함께 동작하는 하드웨어와 시스템 스포트웨어로 구성된다. 컴퓨터 내의 정보는 상황에 따라 다르게 해석되는 비트들의 그룹으로 표시된다.
프로그램은 ASCII 문자로 시작해서 컴파일러와 링커에 의해 바이너리 실행파일들로 번역되는 방식으로, 다른 프로그램들에 의해 다른 형태로 번역된다.
프로세서는 메인 메모리에 저장된 바이너리 인스트럭션을 읽고 해석한다.
계층구조 상부의 저장장치들은 하부의 장치들을 위한 캐시 역할을 수행한다.
운영체제 커널은 응용프로그램과 하드웨어 사이에서 중간자의 역할을 수행한다. 운영체제는 세 가지 근본적인 추상화 (파일, 가상메모리, 프로세스) 를 제공한다.
네트워크는 특정 시스템의 관점으로 볼 때, 단지 또 하나의 입출력장치다.