CPU는 가산 명령어, 점프 명령어 등 겨우 몇 가지 명령어만 실행할 수 있다는 사실을 발견합니다. 따라서 기계어와 해당 특정 작업을 간단하게 대응시켜 기계어를 인간이 읽고 이해할 수 있는 단어와 대응시켰습니다. -55p.
인간은 천성적으로 추상적 표현에 익숙합니다. -61p.
세상의 모든 코드는 아무리 복잡하더라도 결과적으로는 모두 구문으로 귀결됩니다. 이것이 가능한 이유는 매우 간단한데 모든 코드는 구문에 기초하여 작성되기 때문입니다. -77p.
재귀 구문에 따라 작성된 코드를 트리구조로 표현할 수 있습니다. -81p.
컴파일러는 고수준 언어를 저수준 언어로 번역하는 프로그램입니다. -96p.
어휘 분석: 소스 코드를 토큰으로 변환.
구문 분석: 토큰으로 문법적 구조를 확인하고, 구문 트리 또는 AST 생성.
의미 분석: 의미적 유효성 검사 (타입, 변수 사용 등).
중간 코드 생성: 플랫폼 독립적인 중간 코드 생성.
중간 코드 최적화: 불필요한 코드 제거 및 최적화.
목적 코드 생성: 중간 코드를 기계어로 변환.
코드 최적화: 기계어 코드를 최적화.
어셈블 및 링크: 최종 실행 파일 생성.
대상 파일을 병합하는 이 작업은 링크라는 매우 직관적인 이름이 있습니다. 컴파일을 담당하는 프로그램을 컴파일러라고 하는 것과 마찬가지로, 링크를 담당하는 프로그램을 링커라고 합니다. -115p.
우리가 참고하고 있는 외부 심벌에 대한 실제 구현이 어느 모듈이든지 단 하나만 있어야 합니다. 그러면 링커는 이를 찾아내 연결하는 작업을 하는데, 이 과정을 심벌 해석이라고 합니다. -121p.
팀의 코드를 별도로 컴파일한 후 패키지로 묶고, 구현된 모든 함수의 선언을 포함하는 헤더 파일을 제공하는 것은 어떨까요? 물론 가능합니다. 이것을 바로 정적 라이브러리라고 합니다. -141p.
정적 라이브러리의 코드가 변경될 때마다 해당 정적 라이브러리에 종속된 프로그램 역시 매번 다시 컴파일해야 합니다. 그렇다면 이 문제를 어떻게 해결 할 수 있을까요? 정답은 바로 동적 라이브러리를 사용하는 것입니다. -147p.
동적 라이브러리를 사용하면 정적 라이브러리가 실행 파일에 라이브러리 내용을 모두 복사했던 것과 달리, 참조된 동적 라이브러리, 이름, 심벌 테이블, 재배치 정보 등 필수 정보만을 실행 파일에 포함됩니다. -151p.
정적 라이브러리는 컴파일 단계에서 실행 파일이 함께 복사되기 때문에 실행 파일에는 정적 라이브러리의 전체 내용이 포함됩니다. 하지만 동적 라이브러리에 의존하는 실행 파일에는 컴파일 단계에서 필수 정보만 저장되기 때문에 동적 링크는 실제 프로그램 실행 시점까지 미룹니다. -153p.
정적 라이브러리는 컴파일 타임에 프로그램에 통합되는 라이브러리 like “.lib, .a”
동적 라이브러리는 프로그램이 실행되는 동안 런타임에 로드되는 라이브러리 like “.dll, .so”
가상 메모리의 기본 원리
모든 프로세스의 가상 메모리는 표준화되어 있고 크기가 동일합니다. 프로세스마다 각 영역의 크기가 다를 수는 있지만 영역이 배치되는 순서는 동일합니다.
실제 물리 메모리의 크기는 가상 메모리의 크기와는 무관하며 물리 메모리에는 힙 영역, 스택 영역 등 영역 구분 조차 존재하지 않습니다. 단, 운영 체제마다 이는 조금씩 다를 수 있습니다.
모든 프로세스는 자신만의 페이지 테이블을 가지고 있으며, 같은 가상 메모리 주소라도 페이지 테이블을 확인하여 서로 다른 물리 메모리 주소 획득합니다. 이런 이유로 CPU는 동일한 가상 메모리 주소에서 서로 다른 내용을 가져올 수 있습니다. -191p.
모든 프로그래밍 언어는 추상화를 지원하기 위해 각자 자신만의 작동 방식을 제공합니다. 예를 들어 객체 지향 언어의 주요 장점은 바로 다형성과 추상 클래스등을 이용하여 프로그래머가 손쉽게 이해할 수 있다는 것입니다. -199p.
저수준 계층에 대한 철저한 이해는 고급 프로그래머를 남들과 구분 짓는 특징 중 하나입니다. -207p.
내 생각 정리
프로그래밍 언어가 왜 생겼는지, 어떻게 동작하는지 설명해주는 챕터로 가볍지만 깊은 지식을 잘 설명주는 챕터이다.
추상적인 표현에 익숙한 인간에게는 컴퓨터의 정확한 이진법 코드를 이해하기 힘들기 때문에 인간에게 가까운 고수준 언어인 프로그래밍 언어를 이용하여 컴퓨터에게 명령하기 위함이다.
모든 것은 CPU에서 시작된다. 아마도 왜 CPU부터 시작해야 하는지 궁금하겠지만, 사실 그 이유는 간단합니다. 여기에는 혼란스러운 개념이 없고 모든 것이 너무나도 단순해서 문제 본질을 보다 명확하게 들여다볼 수 있기 때문입니다. -217p.
CPU는 단지 다음 두 가지 사항만을 알고 있습니다. -218p.
메모리에서 명령어(instruction)를 하나 가져옵니다.(dispatch)
이 명령어를 실행(execute)한 후 1.로 돌아갑니다.
CPU는 어떤 기준으로 메모리에서 명령어를 가져올까요? 이 질문에 대한 답은 프로그램 카운터(program counter), 줄여서 PC라고 불리는 레지스터(register)에서 찾을 수 있습니다. -219p.
CPU가 프로그램을 실행하게 하려면 실행 파일을 수동으로 메모리에 복사한 후 main 함수에 해당하는 첫 번째 기계 명령어를 메모리에서 찾아 그 주소를 PC 레지스터에 적재하면 된다는 것을 알고 있습니다. -225p.
저장되는 상태를 상황 정보(context)라고 합니다. 프로그램 실행 역시 농구 경기와 비슷합니다. CPU가 어떤 기계 명령어를 실행했는지와 CPU 내부의 기타 레지스터 값 등 상태 값이 있습니다. -233p.
실행 중인 모든 프로그램은 필요한 정보를 기록할 수 있는 이런 형태의 구조체를 가지고 있어야 합니다. 이제 ‘이해 불가능’원칙에 따라 이 구조체에 듣기에 매우 신비한 단어 프로세스(process)라는 이름을 붙입니다. -235p.
기반 기능의 프로그램을 모아 둔 도구에도 이름이 필요하게 되었고, ‘이해 불가능’원칙에 따라 이 ‘간단한’프로그램에 운영체제라는 이름을 붙였습니다. -237p.
프로세스 주소 공간은 아래에서 위의 방향을 기준으로 각각 다음과 같습니다. -243p.
코드 영역(code segment): 코드를 컴파일하여 생성된 기계 명령어가 저장됩니다.
데이터 영역(data segment): 전역 변수 등이 저장됩니다.
힙 영역(heap segment): malloc 함수가 요청을 반환한 메모리가 여기에 할당됩니다.
스택 영역(stack segment): 함수의 실행 시간 스택입니다.
다중 프로세스 프로그래밍에는 다음과 같은 단점이 있습니다. -248p.
프로세스를 생성할 때 비교적 큰 부담(overhead)이 걸립니다.
프로세스마다 자체적인 주소 공간을 가지고 있기 때문에 프로세스 간 통신은 프로그래밍하기에 더 복잡합니다.
CPU 여러 개가 한 지붕 아래에 있는 것과 마찬가지로 공유 프로세스 주소 공간에서 동일한 프로세스에 속한 명령어를 동시에 실행할 수 있습니다. 다시 말해 하나의 프로세스 안에 여러 실행 흐름이 존재할 수 있습니다. 실행 흐름이라는 용어는 너무 이해하기 쉬우니 다시 한번 ‘이해 불가능’원칙을 적용하여 여기에 이해하기 힘든 스레드(thread)라는 이름을 붙였습니다. -253p.
스레드가 자신이 속해 있는 프로세스의 주소 공간을 공유한다는 의미이며, 이는 스레드가 프로세스보다 훨씬 가볍고 생성 속도가 빠른 이유이기도 합니다. 이런 이유로 스레드를 경량 프로세스라고 합니다. -257p.
함수가 실행될 때 필요한 정보에는 함수 매개변수(parameter), 지역 변수, 반환 주소(return address)등이 있습니다. 이런 정보는 대응하는 스택 프레임(stack frame)에 저장되며, 모든 함수는 실행 시에 자신만의 실행 시간 스택 프레임(runtime stack frame)을 가집니다. -262p.
매번 스레드가 생성된다는 의미로, 긴 작업 대상으로는 매우 잘 동작합니다. 하지만 대량의 짧은 작업에서는 구현이 간단한 장점이 있는 동시에 다음 몇 가지 단점이 있습니다. -269p.
스레드 생성과 종료에 많은 시간을 허비합니다.
스레드마다 각자 독립적인 스택 영역이 필요한데, 많은 수의 스레드를 생성하면 메모리와 기타 시스템 리소스를 너무 많이 소비하게 됩니다.
스레드 수가 많으면 스레드 간 전환에 따른 부담이 증가합니다.
스레드 풀의 개념은 매우 간단합니다. 단지 스레드 여러 개를 미리 생성해 두고, 스레드가 처리할 작업이 생기면 해당 스레드에 처리를 요청하는 것입니다. 스레드 여러 개가 미리 생성되어 있기 때문에 스레드의 생성과 종료 작업이 빈번하게 발생하지 않으며, 이와 동시에 스레드 풀 내에 있는 스레드 수도 일반적으로 일정하게 관리되기 때문에 불필요하게 많은 메모리를 소비하지 않습니다. 이 개념에서 중요한 점은 스레드를 재사용하는 것입니다. -272p.
스레드는 프로세스 주소 공간에서 스택 영역을 제외한 나머지 영역을 모두 공유합니다. -292p.
함수의 지역변수, 스레드의 스택 영역, 스레드 전용 저장소는 스레드 전용 리소스이며, 그 외 영역은 공유 리소스로 다음과 같이 구성됩니다. -332p.
힙 영역: 메모리의 동적 할당에 사용되는 영역으로, C/C++언어의 malloc 함수와 new 예약어가 요청하는 메모리는 이 영역에 할당됩니다.
데이터 영역: 전역 변수가 저장되는 영역입니다.
코드 영역: 이 영역은 읽기 전용으로, 프로그램이 실행되는 동안 코드를 수정할 방법이 없으므로 이 부분은 신경 쓸 필요가 없습니다.
공유 리소스를 사용하는 스레드는 반드시 순서를 따라야 하며, 이 순서 핵심은 공유 리소스를 사용하는 작업이 다른 스레드를 방해할 수 없다는 것입니다. 그리고 이를 위해 각종 잠금(lock)이나 세마포어(semaphore)같은 장치를 사용할 수 있습니다. 이 규칙의 목적은 공유 리소스 순서를 유지하는 것입니다. -334p.
공유 리소스가 어느 영역에 저장되어 있든 관계없이 다중 스레드 프로그래밍 중에는 어떤 리소스라도 최대한 공유하지 않는 것이 원칙입니다. -362p.
‘스레드 안전 구현은 스레드 전용 리소스와 스레드 공유 리소스를 중심으로 진행됩니다. 먼저 어떤 것이 스레드 전용 리소스고 어떤 것이 스레드 공유 리소스인지 파악하고, 이어 각 증상에 맞는 약을 처방하면 됩니다.’ -336p.
스레드 전용 저장소
읽기 전용
원자성 연산: 중간에 중단되지 않음
동기화 시 상호배제: 뮤텍스(mutex), 스핀 잠금(spin lock), 세마포어(semaphore) …
컴퓨터 시스템은 주기적으로 타이머 인터럽트(timer interrupt)를 생성하고, 인터럽트가 처리될 때마다 운영 체제는 현재 스레드의 일시 중지 여부를 결정할 기회를 가집니다. 이것이 바로 프로그래머가 명시적으로 스레드를 언제 일시 중지시키고 CPU의 리소스를 내어 줄지 지정할 필요가 없는 이유입니다. 그러나 사용자 상태에서는 타이머 인터럽트를 위한 작동 방식이 없기 때문에 여러분은 코루틴에서 반드시 yield와 같은 예약어를 사용하여 어디에서 일시 중지하고 CPU의 리소스를 내어 줄 것인지 명시적으로 지정해야 합니다. -387p.
함수 A가 반환되기 전에 콜백 함수가 실행됩니다. 바로 이것이 동기 콜백입니다. -434p.
주 프로그램과 콜백 함수의 실행이 동시에 진행될 수 있기에 보통 주 프로그램과 콜백 함수는 서로 다른 스레드 또는 프로세스에서 실행됩니다. 이것이 바로 비동기 콜백입니다. -435p.
‘의존성’, ‘연관된’, ‘기다림’등 단어를 떠올릴 것입니다. 전화 통화와 같은 소통 방식은 동기에 해당합니다. -449p.
‘기다릴 필요 없음’이라는 단어를 떠올릴 수 있으므로 이메일 같은 소통 방식은 바로 비동기입니다. -452p.
여러분은 피자가 오기 전까지 다른 일을 할 수 있는 것이죠. 이렇게 전화 주문 방식으로 피자를 주문하는 것이 바로 논블로킹 호출입니다. -502p.
논블로킹이 반드시 비동기를 의미하지 않는다. -504p.
프로그래밍 관점에서 보면, 동기 호출은 반드시 블로킹이 아닌 반면에 블로킹 호출은 모두 확실한 동기 호출입니다. -505p.
다중 프로세스 병행 처리의 장점은 분명하지만, 단점 역시 그에 못지 않게 분명합니다. -517p.
각 프로세스의 주소 공간이 서로 격리되어 있다는 것이 장점이지만, 반대로 이는 단점이 될 수 있습니다. 프로세스 간에 서로 통신이 필요할 때 난이도가 더 올라가며, 프로세스의 통신 작동 방식을 사용해야 합니다.
프로세스를 생성할 때 부담이 상대적으로 크고, 프로세스의 빈번한 생성과 종료는 의심의 여지없이 시스템 부담을 증가시킵니다.
이벤트 기반 프로그래밍 기술에는 두 가지 요소가 필요합니다. -526p.
이벤트(event): 계속해서 이벤트 기반이라고 말해 왔듯이 이벤트는 당연히 필요합니다. 이 절에서는 주로 서버를 다루고 있기 때문에 여기에서 말하는 이벤트는 대부분 입출력에 관계된 것입니다. 예를 들어 네트워크 데이터의 수신 여부, 파일의 읽기 및 쓰기 가능 여부등이 관심 대상인 이벤트에 해당합니다.
이벤트를 처리하는 함수: 이 함수를 일반적으로 이벤트 핸들러(event handler)라고 합니다.
나머지 내용은 간단합니다. 여러분은 이벤트가 도착할 때까지 조용히 기다렸다 이벤트가 도착하면 이벤트 유형을 확인합니다. 이어서 해당 유형에 대응하는 이벤트 처리 함수인 이벤트 핸들러를 찾은 후 직접 이벤트 핸들러를 호출하기만 하면 됩니다.
리눅스와 유닉스 세계에서는 모든 것이 파일로 취급됩니다. 프로그램은 모두 파일 서술자를 사용하여 입출력 작업을 실행하며, 소켓도 예외는 아닙니다. -531p.
사실 전체적인 프레임워크만 확정되어 있다면 개발자는 handler함수에 대한 프로그래밍만 하면 됩니다. -547p.
서버는 일반적으로 원격 프로시저 호출(remote procedure call), 즉 RPC를 통해 통신합니다. RPC는 네트워크 설정, 데이터 전송, 데이터 분석 등 지루한 작업을 담아 프로그래머가 일반 함수를 호출하는 것처럼 네트워크로 통신할 수 있도록 합니다. -550p.
내 생각 정리
“프로그램이 어떻게 실행되는지?”에 대해서 자세하게 설명하는 파트이다. CPU에서 시작하여 운영체제에서 프로세스가 어떻게 올라가고 스레드, 코루틴 그리고 동기, 비동기까지 모두 설명해준다.
현대 컴퓨터는 존 폰 노이만 구조가 기본이다. CPU가 핵심이라고 볼 수 있으며 구조는 간단하다. 메모리에서 명령어를 가져오고 실행하는 반복되는 구조이다. 명령어를 가져오는 기준은 PC이다.
고로 프로그램이 실행되면 PC에서 첫 번째 명령어를 메모리에서 실행한다.(진입점)
단순하게 위와 같이 실행되면 프로그램이 실행되는 동안 완료가 되는 동안 아무것도 할 수 없고 진입점을 계속 찾아서 올리기에는 어렵기 때문에 이를 추상화하여 관리할 수 있는 운영체제가 만들어졌다.
프로그램 실행마다 CPU가 어떤 명령어를 실행했는지에 대해서 관리하기 위해서 프로세스가 생겼다. 프로세스가 가지고 있는 주요 정보는 다음과 같다.
프로세스 ID (PID)
프로세스 상태
프로세스 우선순위
프로그램 카운터
레지스터 상태
메모리 정보
코드, 데이터, 스택, 힙
파일 디스크립터 테이블
프로세스가 열어 놓은 파일, 소캣 등 자원에 대한 정보
계정 정보
리소스 사용 정보
입출력 상태 정보
프로세스 소유자 정보
시그널 정보
다중 프로세스 프로그래밍은 부담이 크고 복잡하기 때문에 프로세스 주소 공간을 공유하는 스레드를 사용한다.
스레드를 매번 생성하고 제거하면 속도가 지연되기 때문에 스레드 풀을 이용한다.
스레드를 추상화하여 코루틴이라는 이름으로 사용한다. 일시정지 할 수 있고 마지막으로 실행된 위치에서 실행되는 함수라고 봐도 무방하긴 하다.
동기, 비동기로 콜백 함수로 나눌 수 있고 이를 전화 통화와 이메일로 분리해서 생각하게 한 것이 매우 쉽게 이해가 되었다.
아무래도 다중 스레드 프로그래밍은 동시성의 위험도가 높기 때문에 이를 방지하기 위해서 블로킹을 사용하여 방지한다는 점 그리고 전략들도 소개하였다.
node.js의 이벤트 기반 프로그래밍도 나름 쉽게 깔끔하게 설명해주었다. 추후 nodejs를 더 깊게 판다면 이벤트 기반 프로그래밍에 대해서 더 생각해봐도 좋을 것 같다.
Chapter3. 저수준 계층? 메모리는 사물함에서부터 시작해 보자
프로그래밍 언어의 우열에 대해서는 격렬한 토론을 하는 프로그래머들도 메모리에 대해서는 만장일치로 단결합니다. 어떤 프로그래밍 언어로 프로그램을 작성하든지 모든 프로그램은 메모리에서 실행되어야 하며, 그렇기에 여기에서 마주치는 문제들은 모두 매한가지입니다. - 456p.
사물함에 보관되는 0 도는 1을 가리켜 1비트(bit)라고 합니다. - 459p.
더 많은 정보를 표현하려면 더 많은 비트가 필요하므로 비트 여덟개를 묶어 정보를 나타내는 하나의 단위인 1바이트(byte)를 사용합니다. - 460p.
모든 바이트는 메모리 내 자신의 주소를 가지고 있으며 우리는 이 주소를 일반적으로 메모리 주소(memory address)라고 합니다. - 460p.
12바이트를 사용해서 정보를 조합하여 표시하는 것을 프로그래밍 언어에서는 구조체(structure) 또는 객체 (object)라고 표현합니다. - 462p.
원시형 데이터는 숫자, 문자, 불리언, 포인터만 존재한다. 문자열은 구조체 또는 객체로 볼 수 있다. null은 유효한 값을 가리키지 않은 상태를 나타내는 특수 값이다.
“주소1 > 주소3 > 데이터” 이를 간접 주소 지정(indirect addressing)이라고 하며, 어셈블리어에는 변수라는 개념이 없기 때문에 어셈블리어를 사용한다면 반드시 이 간접 주소 지정 계층을 알고 있어야 합니다. - 479p.
포인터는 여러분에게 메모리를 직접 조작할 수 있는 능력을 부여함과 동시에 포인터를 조작할 때 실수하지 않아야 한다는 더 높은 기준을 요구합니다. 포인터를 잘못 다루면 프로그램 실행 시 오류가 쉽게 발생할 수 있으며, 이것이 프로그래머가 포인터를 기피하는 이유 중 하나입니다. - 486p.
포인터는 메모리 주소를 추상화한 것이고 참조 포인터를 한 번 더 추상화한 것이라고 할 수 있습니다. - 488p.
프로세스는 동일한 크기의 조각(chunk)으로 나뉘어 물리 메모리에 저장됩니다. 그림에서 이 프로세스의 힙 영역은 동일한 크기의 조각 세 개로 나뉘어 있습니다. 2. 모든 조각은 물리 메모리 전체에 무작위로 흩어져 있습니다. - 495p.
함수는 가장 기초적이고 간단한 코드 재사용 방식입니다. ‘자기 자신을 반복하지 마세요(Don’t Repeat Yourself)‘역시 함수의 가장 중요한 역할 중 하나 입니다. 이외에도 함수는 프로그래머가 구현의 세부 사항을 감출 수 있게 하므로 여러번이 함수를 호출할 때는 함수 이름, 매개변수, 반환값을 알면 됩니다. 함수가 어떻게 구현되어 있는지 신경 쓸 필요가 없으며, 이 역시 일종의 추상화에 해당합니다. - 504p.
함수 A가 함수 B를 호출하면, 제어권이 함수 A에서 함수 B로 옮겨집니다. 여기에서 제어권은 실제로 CPU가 어떤 함수에 속하는 기계 명령어를 실행하는지 의미합니다. CPU가 함수 A의 명령어를 실행하다가 함수 B의 명령어로 점프하는 것을 제어권이 함수A에서 함수B로 이전되었다고 이야기합니다. - 513p.
제어권이 이전될 때는 다음 두 가지 정보가 필요합니다.
반환(return): 어디에서 왔는지에 대한 정보
점프(jump): 어디로 가는지에 대한 정보
스택 프레임은 우리가 흔히 스택 영역이라고 하는 곳에 위치해 있습니다. 스택 영역은 프로세스 주소 공간의 일부입니다. - 528p.
스택 영역의 크기에는 제한이 있으며, 이 제한을 초과하면 바로 그 유명한 스택 넘침(stack overflow)오류가 발생합니다. 그리고 앞의 코드는 분명히 문제를 일으킬 것입니다. 따라서 프로그래머는 다음 것들을 주의해야 합니다. 1. 너무 큰 지역 변수를 만들면 안됩니다. 2. 함수 호출 단계가 너무 많으면 안됩니다. 함수 호출 원리를 이해하면 수많은 문제를 회피할 수 있습니다. - 531p.
메모리는 함수 호출 횟수와 관계없이 프로그래머가 해당 메모리 영역의 사용이 완료되었다고 확신할 때까지 유효하게 유지됩니다. 이후 해당 메모리는 무효화되며, 이 과정을 동적 메모리 할당과 해제라고 합니다. 이와 같은 이유로 메모리 수명 주기에는 프로그래머가 완전히 제어할 수 있는 매우 큰 메모리 영역이 필요하며, 이 영역을 바로 힙 영역(heap segment)이라고 합니다. - 536p.
최초 적합 방식은 항상 제일 앞부터 요구 사항을 충족하는 첫 번째 여유 메모리 조각을 찾는다. - 558p.
적합 방식은 메모리를 요청할 때 처음부터 검색하는 대신 적합한 여유 메모리 조각이 마지막으로 발견된 위치에서 시작한다는 점이 다릅니다. - 559p.
최적 적합 방식은 가장 적합한 크기의 여유 메모리 조각을 반환한다. - 562p.
시스템 호출을 이용하며 운영 체제가 파일의 읽기와 쓰기, 네트워크 데이터 통신 같은 작업을 응용 프로그램 대신 처리해 줍니다. - 588p.
여러분이 시스템 호출을 직접 사용하면 리눅스의 프로그램은 윈도에서 직접 실행할 수 없게 됩니다. 따라서 우리는 사용자에게서 저수준 계층 간 차이를 감추는 일종의 표준이 필요합니다. 이것으로 프로그래머가 작성한 프로그램을 추가적인 수정 없이 서로 다른 운영체제에서 실행할 수 있습니다. C언어에서 이 일을 하는 것이 바로 표준 라이브러리입니다. - 590p.
실제로 할당한 메모리가 사용되는 순간에 물리 메모리를 할당하게 됩니다. 이때 가상 메모리가 아직 실제 물리 메모리와 연결되어 있지 않으면 내부적으로 페이지 누락 오류(page fault)가 발생할 수 있습니다. 운영 체제가 이 오류를 감지하면 페이지 테이블을 수정하여 가상 메모리와 실제 물리 메모리의 사상 관계를 설정하며, 이것으로 실제 물리 메모리가 할당됩니다. 이 과정이 완료되면 프로그램에서 할당받은 메모리를 사용할 수 있으며, 프로그래머 입장에서 마치 메모리가 할당되어 있었던 것처럼 보입니다. - 602p.
메모리 풀 기술은 그림3-71과 같이 한 번에 큰 메모리 조각을 요청하고 그 위에서 자체적으로 메모리 할당과 해제를 관리하는 방식으로 표준 라이브러리와 운영 체제를 우회합니다. - 611p.
다양한 메모리 영역을 파괴한 대가 - 632p.
스택 영역: 이곳을 덮어쓰면 스택 프레임이 파괴됩니다.
유휴 영역: 이곳을 덮어쓰면 프로세스가 강제 종료됩니다.
힙 영역: 이곳을 덮어쓰면 힙 영역이 파괴됩니다.
데이터 영역: 이곳을 덮어쓰면 데이터 영역이 파괴됩니다.
코드 영역: 이곳을 덮어쓰면 프로세스가 강제 종료됩니다.
자신의 개발 환경에 적합한 메모리 분석 도구를 찾아 이를 제대로 사용하는 방법을 익히면 문제를 해결할 때 절반의 노력으로 두 배의 효과를 거둘 수 있습니다. - 647p.
SSD는 조각 단위로 데이터를 관리하며, 이 조각 크기는 매우 다양합니다. 여기에서 중요한 점은 CPU가 파일의 특정 바이트에 직접 접근할 수 있는 방법이 없다는 것입니다. 다시 말해 바이트 단위 주소 지정이 지원되지 않는다는 의미입니다.
메모리는 사실 매우 단순한데, 미시적 수준에서 보면 개별 사물함으로 구성되어 있고, 그 안에는 0 아니면 1을 저장하고 있을 뿐입니다. 그러나 거시적 수준에서 보면 메모리는 매우 복잡합니다. 메모리에서 함수 실행 시 정보를 저장하고, 함수의 호출과 반환이 되는 스택 영역이 있으며, 할당 요청된 메모리의 수명 주기를 프로그래머가 직접 관리해야 하는 힙 영역이 있습니다. 또 메모리 할당자는 어떻게 구현되는지와 메모리를 할당할 대 저수준 계층에서 무슨 일이 일어나는지도 살펴보았습니다. 물리 메모리 위에 가상 메모리를 추상화하여 최신 운영체제가 각 프로세스에 메모리를 독점적으로 부여하는 것처럼 만들기도 합니다. 이것으로 프로그래머는 연속된 주소 공간에서 프로그래밍을 할 수 있게 되었고, 이는 엄청난 편리함을 제공합니다.
내 생각 정리
프로그래밍에 있어서 메모리 관리는 굉장히 중요하다. 최근 언어는 가비지 컬렉터가 있어서 크게 신경을 안써도 되긴 하지만 그래도 알고 쓰는 것과 모르고 쓰는건 엄청난 차이가 있다고 본다.
미시적으로 보면 그저 0,1을 담은 공간 1bit만 있지만 크게 보면 바이트, 스택, 유휴, 힙, 데이터, 코드 등등 영역이 존재하고 물리 메모리와 가상 메모리에 대한 내용도 있다.
가상 메모리는 실제 물리 메모리가 할당되어 있지 않고 실제 메모리를 사용할 때 할당된다. 이 대 페이지 누락이 발생할 수 있기 때문에 메모리 관련에 신경을 써야 한다.
Chapter4. 트랜지스터에서 CPU로, 이보다 더 중요한 것은 없다.
회로 세 개는 의외로 특성이 매우 매혹적인데 바로 논리곱 게이트, 논리합 게이트, 논리부정 게이트로 모든 논리 함수를 표현할 수 있다는 것입니다. 그리고 놀랍게도 이것을 논리적 완전성이라고 합니다.
명령어 집합(instruction set)은 CPU가 실행할 수 있는 명령어(opcode)와 각 명령어에 필요한 피연산자(operand)를 묶은 것 입니다. 서로 다른 유형의 CPU는 서로 다른 명령어 집합을 가지고 있습니다.
CPU의 클럭 주파수(clock rate)가 무엇을 의미하는지 알아야겠죠? 클럭 주파수는 1초 동안 지휘봉을 몇 번 흔드는가를 의미하여, 클럭 주파수가 높을수록 CPU가 1초에 더 많은 작업을 할 수 있음은 자명합니다.
프로그램에 무한 순환이 있더라도 운영체제는 여전히 타이머 인터럽트를 통해 프로세스의 스케줄링을 제어할 수 있으며, 무한 순환이 있다고 운영체제가 실행하지 못하는 문제는 발생하지 않습니다.
하나의 기계 명령어를 처리하는 과정 역시나 대체로 명령어 인출(instruction fetch), 명령어 해독(instruction decode), 실행(execute), 다시 쓰기(writeback) 네 단계로 구분할 수 있습니다.
모든 컴퓨팅 장치의 원조인 폰 노이만 구조를 보여줍니다. 모든 것의 본질은 이 간단한 그림에 담겨 있습니다. 그야말로 모든 컴퓨터 장치의 기원인 것입니다.
축소 명령어 집합의 특징 중 하나는 복잡한 명령어를 제거하고 대신 간단한 명령어 여러 개로 대체하는 매우 단순한 아이디어라는 것입니다. 이 사상으로 CPU 내부 마이크로코드 설계가 필요하지 않으며, 마이크로코드가 없으면 컴파일러에서 생성된 기계 명령어의 CPU 제어 능력이 크게 향상됩니다.
복잡 명령어 집합 진영은 복잡 명령어 집합 축소 명령어 집합처럼 보이게 하는 것 외에 또 다른 기술을 추가로 개발했는데, 바로 하이퍼스레딩입니다.
CPU는 컴퓨터에서 가장 핵심적인 하드웨어로, CPU가 하는 작업을 매우 간단해서 메모리에서 명령어를 가져와 실행하는 것이 전부입니다. 그러나 소프트웨어 관점에서 보면, 코드는 하나하나 실행되지 않고 명령어의 순차적인 실행은 함수 호출, 시스템 호출, 시스템 전환, 인터럽트 처리 등으로 끊어집니다. 하지만 이것 역시 컴퓨터 시스템에서 매우 중요한 실행 흐름 전환 구조에 해당합니다.
함수 호출로 프로그래머는 코드 재사용성을 개선할 수 있고, 시스템 호출로 프로그래머는 운영체제에 요청을 보낼 수 있습니다. 프로세스와 스레드 전환으로 다중 작업이 가능할 뿐만 아니라 인터럽트 처리로 운영 체제가 외부 장치를 관리하게 할 수 있습니다. 이런 구조는 컴퓨터 시스템의 기반이 됩니다.
프로그램 실행 시 상황 정보를 가져오고 저장할 수 있다면 언제든지 프로그램의 실행을 일시 중지할 수 있으며, 반대로 이 정보를 이용하여 언제든지 프로그램의 실행을 재개할 수도 있습니다. 그렇다면 이 상황 정보를 저장하고 복원해야 하는 이유는 무엇일까요? 근본적인 원인은 CPU가 엄격한 오름차순으로 기계 명령어를 실행하지 않기 때문입니다.
CPU는 함수 A에서 함수B로 점프할 수 있습니다.
CPU는 커널 코드를 실행하기 위해 사용자 상태에서 커널 상태로 전환할 수 있습니다.
CPU는 프로그램A의 기계 명령어 실행 상태에서 프로그램 B의 기계 명령어 실행 상태로 전환할 수 있습니다.
CPU는 인터럽트를 처리하기 위해 실행하던 프로그램을 중지시킬 수 있습니다.
이 상황 중 어느 것도 CPU에 의한 기계 명령어의 순차적 실행을 방해할 수 없으며, 이때 CPU는 이후 복구를 대비해서 중단되기 전 상태를 저장해야 합니다.
내 생각 정리
논리 게이트 부분은 굳이 필요 없다고 생각이 들었다. (너무 하드웨어 기반이라서 소프트웨어 중점인 나에게는 굳이?)
하나의 기계 명령어를 처리하는 4단계를 알았다.
명령어 인출(instruction fetch)
명령어 해독(instruction decode)
실행(execute)
다시 쓰기(writeback)
존 폰 노이만 구조가 역시 컴퓨터의 근본이라고 볼 수 있다는걸 또 다시 보았다.
Chapter5. 작은 것으로 큰 성과 이루기, 캐시
캐시는 메모리의 데이터를 저장하고, 메모리는 디스크 데이터를 저장한다.
우리는 컴퓨터 저장 체계의 각 계층이 다음 계층에 대한 캐시 역할을 한다는 것을 알았습니다. 여기에서 반드시 주의해야 할 점이 하나 있는데, 각 계층의 저장 용량은 반드시 다음 계층보다 작아야 한다는 것입니다. 예를 들어 L3 캐시는 반드시 메모리 용량보다 작아야 합니다. 그렇지 않으면 L3 캐시를 직접 메모리로 사용하면 되기 때문입니다. 이에 근거하여 전체 저장 체계가 최대 성능을 발휘하려면 프로그램이 매우 캐시 친화적이어 야합니다.
캐시 용량은 제한되어 있으므로 프로그램에 필요한 데이터에 더 집중하는 것이 좋습니다.
여러 스레드 사이에 캐시 일관성을 유지할 필요가 있다면 다중 스레드 프로그래밍을 할 때 캐시 튕김 문제를 경계해야 합니다.
Chapter6. 입출력이 없는 컴퓨터가 있을까?
여기에서 관건은 CPU는 특정한 방법을 사용하여 장치의 작업 상태를 얻어야 한다는 것입니다. 예를 들어 키보드의 키 데이터가 들어오고 있는지, 마우스의 데이터가 들어오고 있는지 알 수 있는 방법이 필요합니다. 그렇다면 CPU는 어떻게 장치의 작업 상태를 알 수 있을까요? 이것이 바로 장치 상태 레지스트의 역할입니다.
본질적으로 폴링은 일종의 동기식 설계 방식입니다. CPU는 누군가가 키를 누를 때까지 계속 대기하므로, 이를 자연스럽게 개선할 수 있는 방법은 비동기로 바꾸는 것입니다.
네트워크 카드에 새로운 데이터가 들어오면 외부 장치가 인터럽트 신호를 보내고, CPU는 실행 중인 현재 작업의 우선순위가 인터럽트 요청보다 높은지 판단합니다. 인터럽트가 더 높다면 현재 작업 실행을 일시 중지하고 인터럽트를 처리하며, 인터럽트 처리를 끝낸 후에 다시 현재 작업으로 돌아옵니다.