🔖 1.1 정보는 비트와 컨텍스트로 이루어진다.
// Hello.c
#include <stdio.h>
int main(void)
{
printf("Hello, World!");
return 0;
}
위와 같은 내용을 가진 'Hello.c' 파일이 있다. 이때, 'Hello.c' 파일은 텍스트 파일이다. 텍스트 파일은 내부적으로 0과 1인 Binary로 이루어져 있다. 우리가 읽을 수 있는 문자의 형태로 나타나게 하기 위해서 인코딩(Encoding)을 한다. 인코딩 과정을 통해서 우리가 이해할 수 없는 Binary 데이터를 친숙한 문자의 형태로 변경할 수 있다.
그리고 이런 텍스트 파일의 개념과는 반대로 컴퓨터나 다른 Player 성격(비디오 플레이어와 같이 확장자를 읽어 들이는 프로그램)의 프로그램이 읽기 쉽게 변경하는 파일들은 바이너리 파일(Binary File)이라고 부른다.
바이너리 파일은 실행되면 컨텍스트(Context)를 가지게 되는데, 간략하게 말해서 컨텍스트는 프로그램이 실행되기 위해서 필요한 정보 집합을 의미한다. 이를 통해서 프로세스를 구별할 수 있다
▶️ 컨텍스트(Context) ?
이전에는 컨텍스트를 단순하게 프로세스 실행 중에 CPU 내부의 레지스터에 위치한 데이터라 생각했는데, 이번 책을 읽으면서 컨텍스트가 가지는 범위가 제법 크구나를 느꼈다. 근데 크게 다른 건 없는 것 같다. 내가 알고 있던 것도 컨텍스트가 맞고, 다만 일부를 전부라고 알았다. 컨텍스트는 해당 프로그램이 구동되기 위해서 필요한 값들(변수 데이터, 프로세스 정보, 등)을 의미한다.
[스터디] 시스템 프로그래밍 - Chapter 2. 아스키코드 vs 유니코드
뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우) 01. Windows에서의 유니코드(Unicode) - 문자셋Character Sets의 종류와 특성 가장 대표적인 문자셋에 해당하는 아스키코드ASCII CODE와 유니코드UN
taeyeokim.tistory.com
이 전에 시스템 프로그래밍 스터디를 하면서 인코딩에 대한 이야기를 조금 작성했던 게 있어서 첨부해 둔다
🔖 1.2 프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.
위의 Hello.c 텍스트 파일을 프로그램(바이너리 파일)으로 만들기 위해서 여러 절차(컴파일 단계)를 거쳐야 한다. 지금 텍스트 파일은 컴퓨터가 읽을 수 없다. 그렇기 때문에 컴파일 단계(전처리 -> 컴파일 -> 어셈블러 -> 링커)를 거쳐서 실행 가능한 목적 파일(exe, 등) 형태로 만들 수 있어야 한다.
이 단계들을 거치면서 Hello.c 텍스트 파일은 컴퓨터가 읽을 수 있는 바이너리 파일의 형태로, 내부적으로는 저급 기계어 인스트럭션들로 번역된다.
1️⃣ 전처리기
전처리기를 통해서 #include와 같은 전처리문들을 치환하는 작업을 진행한다. 예를 들어서 지금은 include 전처리문이 stdio 헤더를 입력했다. 그러면, 전처리기는 사용된 stdio 헤더의 경로와 함수 원형을 가져와 i 확장자의 Hello.i 파일을 생성한다.
2️⃣ 컴파일러
Hello.i 파일에 들어간 내용을 어셈블리어로 치환하는 작업을 진행하며, 진행이 끝났을 경우 s 확장자의 Hello.s 파일을 생성한다.
3️⃣ 어셈블러
전달받은 Hello.s 파일을 o 확장자의 재배치가 가능한 목적 파일로 Hello.o 파일을 반환한다. Hello.o 파일의 내부에는 인코딩을 위한, 17바이트를 포함하는 Binary가 존재한다. 정확히는 CPU가 가지는 ISA(Instruction Set Architectures)에 대응되는 Binary 데이터를 가지게 된다. 이때, NotePad 프로그램을 통해서 실행하면 쓰레기 값이 보인다. 해당 파일을 열 수 있는 프로그램으로 봐야 한다.
4️⃣ 링커
Hello.c 파일로 다시 되돌아가면, 우리는 printf 함수를 사용했다. printf 함수는 우리가 정의한 것이 아니라, stdio 헤더에 있는 함수를 사용한 것인데, 해당 단계를 통해서 printf 함수를 사용할 수 있도록 Hello.o 파일과 Printf.o 파일을 서로 연결해 준다. 연결되지 않은 상태라면, Hello.o 파일에서는 printf 함수를 사용할 수 없어진다. 해당 단계가 종료되고 exe 확장자 혹은 다른 실행 가능한 확장 파일로 변환된다.
🔖 1.3 컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.
해당 챕터의 내용은 왜? 프로그래머가 컴파일 시스템 작동을 이해하는 것이 중요한지에 대해서 예시가 적혀있다. 그렇기 때문에 별도의 언급 없이 넘어간다.
🔖 1.4 프로세서는 메모리에 저장된 인스트럭션을 읽고 해석한다.
해당 챕터는 일반적인 컴퓨터 시스템의 아키텍처를 소개한다. 열심히 그리면서 생각했지만, 미뿌감 님의 그림을 이길 수 없어서 양해를 구하고 참고 자료로 추가했다. 감사합니다! 화이트보드의 신
이번 내용도 이전에 올렸던 블로그 글과 겹치는 부분이 있어서 대부분 설명은 아래 링크로 대체한다.
[스터디] 시스템 프로그래밍 - Chapter 1. 컴퓨터 구조에 대한 첫 번째 이야기
뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우) 01. 시스템 프로그래밍의 이해와 접근 - 시스템 프로그래밍의 정의 시스템 프로그래밍을 알아보기 위해서 시스템 프로그램에 대해서
taeyeokim.tistory.com
1️⃣ 메인 메모리
메인 메모리는 물리적으로는 DRAM(Dynamic Random Access Memory)칩들로 구성이 되어 있으며, 논리적으로는 일련의 바이트 배열이다. 내부적으로 시작 주소 0부터 메모리 용량 N까지 고유의 주소를 가진다. ( 배열 인덱스를 가지는 것과 동일하게 생각해도 괜찮다. )
( 이후, SRAM이라는 용어가 나타나면 DRAM과 SRAM의 차이에 대해서 알아보자 )
2️⃣ 프로세서
인스트럭션을 수행하고 처리하는 중요한 역할을 담당한다. 프로세서는 CPU와 GPU를 나타내지만, 이 설명에서는 CPU에 한정되어 있다. 어쨌든 프로세서는 처리 상태일 때, 다음과 같은 행동을 반복한다.
1. PC, Program Counter 레지스터가 참조하는 Instruction을 수행한다.
2. PC 레지스터는 다음 레지스터를 참조한다.
CPU 내부의 산술 논리 장치 ALU를 통해서 모든 행동을 처리한다. 단, ALU는 산술 연산(더하기, 빼기), 논리 연산만 수행할 수 있기 때문에 인스트럭션을 가져오고(Load) 계산하고(Operate) 결과를 전달하는(Store) 행동은 Control Unit에서 처리하게 된다.
🔖 1.5 캐시가 중요하다
위 내용에서는 생략했는데, 이전 챕터에서 Hello.c 프로그램을 실행할 때, 어떤 Sequence로 실행되는지 설명한 예시가 있었다. 다소 내용이 복잡했는데(Disk -> Main Memory, CPU -> Main Memory....) 이렇게 생각해보자. 간단한 'Hello, World!'를 출력하는 프로그램이 이렇게 복잡하다면, 다른 기능이 추가된 프로그램들을 더 복잡하지 않을까?
우리 선배 개발자분들은 빠르게 Data에 접근할 수 있도록 캐시(Cache) 메모리를 고안했다. CPU 관점에서 캐시는 L0, L1, L2, L3, L4까지 존재한다. L1, L2 캐시는 SRAM(Static Random Access Memory) 기술을 바탕으로 구현한다. 이후의 수들은 Network, Local Disk, 등과 같은 저장 장치를 뜻한다.
L 뒤의 숫자가 클수록 용량이 크고 접근 속도가 느리다. 속도에 대한 차이가 나타나는 사유로는 물리적인 거리 차이로 인해서 전기 신호 접근이 L0에 비해서 느린 것과 용량이 적지만 단순한 L0 캐시에 비해서 L3 캐시는 용량이 크고 더 복잡하기 때문인 점도 있다.
단, 프로세서에서 L2 캐시를 액세스 할 때 L0 캐시와 L1 캐시에 비해서는 느리지만, 메인 메모리에 엑세스 할 때보다는 최소 5배, 최대 10배의 접근 속도 차이가 난다.
▶️ SRAM, Static Random Access Memory
( 참고 : https://blog.naver.com/ycpiglet/221984934010?viewType=pc )
SRAM은 Flip-Flop으로 작동하는 방식이다. Flip-Flop은 전류 신호가 오기 전에는 상태가 변하지 않는 소자다. 그렇기 때문에 전류가 오지 않으면 내용이 소멸 혹은 안정적인 상태를 이룰 수 있다. 이외로 속도가 굉장히 빠르기 때문에 Cache Memory에서 주로 사용된다. 그러면 Main Memory에서 SRAM을 사용하면 성능에 아주 유익하겠지만, DRAM에 비해 용량이 적고 가격면에서 SRAM이 더 비싸기 때문에 DRAM을 Main Memory로 사용한다.
▶️ DRAM, Dynamic Random Access Memory
( 참고 : https://blog.naver.com/ycpiglet/221984934010?viewType=pc )
DRAM은 축전기로 작동된다. 축전기의 특징은 시간이 지나면 스스로 방전된다. 그렇기 때문에 시간이 흐르면 메모리가 알아서 방전이 된다는 소리다. 이럴 경우, 데이터를 다시 보관할 수 있도록 새로고침(데이터를 다시 불러와야한다.)이 필요로 하다. 이로 인해서 DRAM에는 Refresh 회로가 추가로 붙어 있게 된다. SRAM보다 가격이 저렴하며 저장 용량이 크기 때문에 Main Memory로 사용된다.
🔖 1.6 저장장치들은 계층구조를 이룬다
어쩔 수 없다. 크래프톤 정글에서 하나하나 이미지를 만들어서 블로그에 작성하는 것보다는 화이트보드에 작성한 내용을 첨부하는 것이 더 빠르다. 그리고 이미지 하나가 설명하는 내용을 담고 있다면, 응당 재활용하는 것이 좋은 거다!!!!
본론으로 돌아가자, 좌측에 피라미드 형태의 그림을 보자. 해당 그림은 메모리 계층 구조를 나타냈다. L0 캐시부터 L4 캐시까지. 앞선 설명과 동일하게 L 뒤 숫자가 낮은, 맨 위의 캐시부터 하단으로 갈수록 접근 속도가 느려지지만, 담을 수 있는 용량이 증가한다.
위 이미지에서 L1 캐시와 L2 캐시를 보자. 해당 캐시들은 각자 자신의 하위 계층에 위치한 캐시 메모리의 캐시 역할을 수행한다. 어라? 그냥 단순하게 캐시는 계층 구조에서 자신의 하위 계층인 캐시의 캐시 역할을 담당한다. L1 캐시와 L2 캐시는 각자 L2 캐시와 L3 캐시의 캐시인 것이다.
🔖 1.7 운영체제는 하드웨어를 관리한다
여기는 조금 재미있는 부분이다. 알다시피 우리는 Hello.c 파일을 작성하면서 하드웨어를 제어하는 코드를 작성하지 않았다. 단순하게 print 함수를 통해서 하드웨어에 전달한 것이다. print 함수의 내부는 OS에서 제공하는 시스템 함수들을 사용해서 전달한다. 즉, 우리는 결론적으로 OS에서 제공하는 시스템 함수들을 통해서 하드웨어를 조작한 것이다.
이렇게 시스템 함수로 OS의 제공하는 기능으로 하드웨어를 움직이면, 다음과 같은 이점이 있다.
1. 응용 프로그램들이 하드웨어에 잘못된 접근을 하는 것을 최소화할 수 있다.
2. 단순하고 간단한 함수들을 통해서 복잡하고 저수준의 하드웨어 기능을 다룰 수 있다.
이제는 진짜 다루기 무섭고 어려운 것들에 대해서 알아보자.
1️⃣ 프로세스
프로세스는 실행되고 있는 프로그램을 의미한다고 생각하면 된다. 우리가 프로그램을 하나 실행하면, 프로세스 하나를 실행하는 것과 동일한 의미다. 단순히 OS 관점에서 봤을 때, 실행 중인 프로그램에 대한 추상적 개념인 것이다.
프로세스는 코어 당 하나의 프로세스를 실행할 수 있다.
싱글코어 프로세서는 한 번에 하나의 프로세스를 실행할 수 있는데, 그럼에도 불구하고 여러 프로세스를 동시에 실행하고 있는 것 같은 느낌을 받는다. 이는 컨텍스트 스위칭(Context Switching)을 통해서 그렇게 느낄 수 있게끔 유도한 것이다.
컨텍스트 스위칭에 대한 내용은 다음 아래 링크를 참고해 주길 바란다.
[스터디] 시스템 프로그래밍 - Chapter 5. 프로세스의 생성과 소멸
뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우) 01. 프로세스의 이해 오늘날의 운영체제를 가리켜서 '멀티 프로세스Multi-Process' 운영체제라고 한다. 이는 프로세스가 여러 개 존재하는
taeyeokim.tistory.com
2️⃣ 쓰레드
프로세스는 서로가 서로에게 독립되어 있다. 이 말은 다른 프로세스끼리 서로에게 간섭할 수 없다는 것이다. Excel 프로그램 실행해서 텍스트를 타이핑하는데 Word 프로그램에서 타이핑되면.. 이 얼마나 황당한가. 이러한 경우를 막기 위한 장치다. 단, 서로에게 참조할 수 없기 때문에 참조가 필요로 할 때 문제가 생긴다.
이런 경우 쓰레드를 보라. 쓰레드는 프로세스의 Stack 영역을 제외하고 나머지 부분을 공유한다. 지금 시점에서 쓰레드는 Stack 영역을 제외한 나머지 영역을 공유하는 프로세스 정도로 이해해 두자. 자세한 내용이 궁금하다면 아래 챕터부터 읽어보자.
[스터디] 시스템 프로그래밍 - Chapter 11. 쓰레드의 이해
뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우) 01. 쓰레드란 무엇인가? 두 개 이상의 일을 동시에 처리하기 위해서 추가적으로 프로세스를 생성하는 것은 막대한 컨텍스트 스위칭Conte
taeyeokim.tistory.com
3️⃣ 가상메모리
가상메모리는 물리 메모리의 추상화 개념이다. 모든 프로세스들은 자기 자신이 충분한 메모리 공간을 할당받았다고 생각하게끔 만드는 메모리 관리에서 중요한 개념 중 하나다. 이 내용이야말로 이전에 작성한 글을 올리고 싶지만, Notion에서 작성한 모든 게시글을 블로그에 옮긴 것이 아니라서 매우 안타깝다.. 지금 챕터에서 나온 설명만 가지고 이야기해 보자.
Linux의 메모리에서 Top-Level에는 모든 프로세스들이 공통적으로 사용하는 OS 코드와 데이터를 위한 공간이 있다. 이후에는 Code, Data, Heap, Stack, 공유 라이브러리, 커널 영역이 있다.
- Code, Data 영역
- 모든 프로세스들이 같은 고정 주소에서 시작한다.
- Hello와 같은 실행가능한 프로그램이 시작하면서 직접 초기화한다.
- Heap 영역
- 런타임에 동적으로 크기가 늘었다 줄었다 하는 영역이다. 즉, 동적할당 시행에 따라 크기가 달라진다.
- 공유 라이브러리 영역
- 공유되는 라이브러리를 위한 공간이다. C언어 자체의 공유 라이브러리나, 다른 공유 라이브러리를 위한 영역인데.. 지금 시점에서는 나도 자세히 이해가 가지는 않는다.
- Stack 영역
- 함수에서 코드에 의해 실행되는 Stack 영역을 의미한다. 프로그램 실행 중간에 동적으로 크기가 줄었다가 늘어난다.
- 커널 가상메모리 영역
- 커널을 위한 영역이다. 응용 프로그램이 해당 영역에 접근해서 어떠한 변경을 하는 것도 허락하지 않는다. 접근하는 방법은 커널을 사용해야 한다.
4️⃣ 파일
이 친구는 연속되는 바이트 모음. Unix I/O라는 시스템 콜을 통해서 입력을 관리한다.
🔖 생략된 내용
생략한 내용에서는 네트워크는 일종의 I/O 장치임과 암달(Amdahl)의 법칙에 대해서 설명한다. 암달의 법칙은 시스템의 성능을 개선할 때, 그 개선 성능을 측정할 수 있는 공식이다.
🔖 1.9 중요한 주제들
동시성(Concurrency)과 병렬성(Parallelism)이란 개념이 있다. 두 용어는 혼용해서 사용하기 쉬운 개념인데, 서로 다른 개념이기 때문에 주의해야 한다. 책에 나와있는 대로 학습하면 동시성은 프로세스 기반으로 설명하지만, 프로세스에 종속된 개념이 아니기 때문에 책의 내용을 포함해서 내용을 조금 달리 설명한다.
동시성은 1960년대 시간 공유(Time Sharing) 기법의 출현으로 동시 실행에 관련된 개념으로 처음 선보여졌다. 여러 작업을 번갈아가면서 작업들이 마치 동시에 실행되고 있는 것처럼 느끼게 한다. A, B, C가 있다면 특정한 규칙에 따라서 A -> B -> C -> A 순서대로 빠르게 Swap 해나가는 식이다. 이전, 싱글코어 프로세스의 시대에서 복수의 프로세스를 위와 같은 방식으로 처리했다.
병렬성은 단순하게 생각하면 싱글코어 프로세서에서 하나의 코어가 처리하던 일을 여러 개의 코어에서 일을 처리하게 하는 것이다. 그렇기 때문에 멀티코어 프로세서에서 실행이 되며, 싱글코어 프로세서에서는 실행할 수 없다. 기존에 혼자 할 일을 여럿이 함께 하기 되기 때문에 싱글코어 프로세서보다는 속도가 더 빨라지는 것을 기대할 수 있지만, 경쟁 상태(Race Condition)가 일어날 수 있다.
마지막으로 정리하면 동시성이 한 번에 많은 일을 처리한다면, 병렬성은 한 번에 많은 일을 한다고 정리할 수 있다.