시스템 프로그래밍 0. Map
건국대학교 시스템 프로그래밍 진현욱 교수님의 수업을 정리한 내용입니다.
프로그램 실행 과정
프로그램을 실행시키면, 내부에서 어떤 동작이 일어나는가?
보통 프로그램을 실행하는 방법은 여러가지인데, 사용자가 실행파일을 눌러서 실행하는 상황이라고 가정하자.
- Shell이 명령을 받는다.
- 커널에게 요청(System Call)한다.
- Kernel에게 실행 파일 실행을 요청(System Call)한다.
- 디스크에 있는 프로그램 바이너리 코드를 읽어야 하는데, 하드웨어 자원 접근은 Kernel에게 있으므로 요청할 수 밖에 없다.
- 커널은 디스크에 있는 실행 파일을 Virtual Address Space을 만들어 그곳에 Load한다.
- 커널은 Virtual Address Space의 메모리 주소와 실제 물리공간의 메모리 주소를 Mapping하는 테이블을 관리한다.
- 프로세스를 생성한다.
(Process Control Block (PCB) 생성)
- CPU는 Program Counter (PC)가 가리키는 명령어를 읽고 실행하고, 다음 줄을 읽고 실행하고를 반복한다.
- CPU는 PC를 통해 현재 실행중인 명령어의 메모리 주소에 접근할 수 있다. 이 과정이 실행되는 동안, 스케줄러(Scheduler)가 적절한 Context Switch를 통해 여러 프로세스가 실행될 수 있도록 한다.
- 프로그램을 종료한다.
- 프로그램이 종료되면, 커널은 프로세스의 PCB를 삭제하고, 프로세스가 사용하던 Virtual Address Space를 해제한다.
운영체제와 커널과 Shell의 차이
OS (운영체제)
전체 컴퓨터 시스템을 관리하는 소프트웨어를 OS라고 한다.
OS는 Kernel과 Shell을 포함한다.
Kernel
하드웨어와 소프트웨어 사이의 인터페이스 역할을 하는 소프트웨어이다.
컴퓨터 자원을 관리해서 소프트웨어에게 적당한 자원을 할당해준다. 소프트웨어가 변수같은거 할당하면 커널에게 요청하고, 커널이 실제 메모리와 링크해주는 거임.
하드웨어 자원이 필요한 작업은 죄다 커널에게 요청(System Call)해서 수행된다.
[!NOTE] 관리하는 자원{title}
- CPU
- Memory
- Files
- 입출력 장치
- Process
- User 권한
이를 적절히 관리하는 작업을 스케쥴링이라고 하며, 커널 내 Scheduler가 담당한다.
[!NOTE] 커널의 특징{title}
커널은 작업을 직접 시작하는 경우가 없다. 다른 소프트웨어가 ‘이 변수 메모리 공간 주세요’, ‘이 주소의 파일 실행할게요’와 같은 요청이나 이벤트가 있을때만 동작한다.
커널은 한번 호출되면 쌓아둔 작업을 한번에 처리한다 커널은 누가 호출해줬을 떄만 실행되기 때문에, 커널이 해야하는 작업이 있으면 일단 대기한다. 누가 커널을 깨워주면, 실행된 김에 대기해뒀던 작업을 가능한 많이 수행한다.
Scheduler
많은 프로세스가 CPU를 적당하게 사용할 수 있도록 사용 시간을 분배, 우선순위를 결정해주는 역할을 한다.
커널이 포함하고 있는 객체다.
스케줄러는 Context Switch를 담당한다. 프로세스의 상태(Context)를 관리하여 CPU가 중단된 지점부터 프로세스를 재개할 수 있도록 한다.
Shell
사용자와 운영체제 사이의 인터페이스 역할을 하는 소프트웨어이다.
명령 줄 인터페이스(CLI)와 그래픽 사용자 인터페이스(GUI)로 구분할 수 있다.
쉘에서는 사용자가 입력한 명령어를 처리하다가, Kernel이 필요한 경우 System Call을 호출한다.
프로그램과 프로세스
프로그램과 프로세스의 차이는 무엇인가? 먼저 프로그램이란, 저장 장치에 저장된 실행 가능한 코드나 명령어의 집합이다.
프로세스란, 실행중인 프로그램이다. 운영체제가 프로그램을 메모리에 올리고, CPU에서 실행하면 프로세스가 된다. 프로세스의 상태는 메모리, 레지스터 값, 프로그램 카운터(Program Counter, PC)) 등이 포함된다.
클래스와 인스턴스의 개념으로 비유하면 프로그램이 클래스고 인스턴스가 프로세스다.
Process Group
한 프로세스에서 복제된 프로세스는 하나의 프로세스 그룹으로 묶인다.
Shell과 모든 프로세스는 그룹으로 묶인다.
만약 pgid = 10으로 Signal을 보내면, 모든 프로세스에게 시그널이 전달된다.
Program Counter
현재 실행중인 명령어의 주소 상태이다. 이는 CPU Registers에 저장되는 값이다.
프로그램이 실행되면, 메모리에 프로그램을 실행하기 위한 코드가 전부 메모리에 올라오며 프로세스가 된다. 프로세스를 구동하기 위해, CPU가 현재 어느 부분의 Code Line을 처리해야 하는지 알아야 한다. 이를 Program Counter라는 변수를 하나 만들어서 관리한다.
코드 한줄을 읽으면, Program Counter를 1 증가시키고, 다음 CPU는 그저 Program Counter에 위치한 코드의 줄을 읽는다. 만약 함수를 실행한다면, 그 함수가 저장되어 있는 메모리 주소로 Jump해서 이동하고, 함수를 탈출하면 다시 제자리로 Counter가 돌아온다.
PID
각 프로세스마다 고유의 PID (Process ID) 를 갖는다.
PGID
프로세스 그룹의 아이디. PID와 다르다.
CPU 코어와 프로세스의 관계
6코어 CPU면 동시에 실행할 수 있는 최대 프로세스 개수가 6개라는 의미이고, 8코어 CPU면 동시에 실행할 수 있는 최대 프로세스 개수가 8개라는 의미이다.
Process Pause
보통 shell에서 Ctrl + Z
를 누르면 프로세스에 SIGTSTP Signal이 전달되며, 현재 Forground로 실행중인 프로세스가 일시정지된다. 이후, 커맨드 라인을 실행할 수 있는 상태가 된다.
다시 실행시키려면 fg
명령어를 입력하면 된다. 해당 프로세스에게 SIGCONT Signal이 전달되며 다시 프로세스가 실행된다.
Process Stop
보통 shell에서 Ctrl + C
를 누르면 프로세스에게 SIGTERM Signal이 전달되며 프로세스가 종료된다.
Context Switch
CPU는 최대 코어 개수만큼밖에 프로세스를 실행할 수 없지만, 실제로는 그것보다 훨씬 더 많은 프로세스를 동시에 돌리는 것 처럼 보인다.
코어 개수보다 더 많은 개수의 프로세스를 실행하면 8개가 아닌 다른놈은 잠시 대기를 해야 할 수 밖에 없다.
운영체제는 이걸 프로세스를 잠깐 실행했다가 대기했다가 잠깐 실행했다가 대기했다가.. 이 과정을 아주 빠르게 해서 8개보다 훨씬 많은 프로세스가 동시에 실행되는 것처럼 보이게 한다.
이 과정을 Context Switch라고 한다.
프로세스가 갖는 상태 정보인 Context를 잠시 저장해뒀다가, 다시 실행할 때 Context 정보를 그대로 가지고 프로세스를 재실행하면, 전의 창 크기, 위치, 정보 등을 그대로 이어서 사용할 수 있다.
Switch를 너무 자주하면 연산 시간이 너무 많이걸리고, 또 너무 작게 하면 버벅임을 사용자가 느낄 것이다. 따라서 적절한 횟수의 Context switch가 이루어져야 한다.
이를 Kernal 내 Scheduler가 적절히 관리한다.
Context
Process 또는 Thread의 실행 상태 정보이다.
Context Switch를 위해 프로세스가 잠시 멈춰도 이전상태 그대로 실행될 수 있게 충분한 정보를 담고 있어야 한다.
- Program Counter (PC) : 프로그램 코드가 어디까지 실행되었는가?
- CPU Registers에서 계산중인 데이터
- 메모리 상태 : Virtual Address Space와 물리 메모리 간의 매핑 정보(Page Table).
위 상태들을 담는 자료구조를 Process Control Block (PCB) 라고 부른다.
Virtual Address Space
한 프로세스가 사용할 수 있는 가상의 메모리 공간.
[!tip]- 32bit vs 64bit 차이점{title} 흔히 컴퓨터가 32비트다, 64비트다 하는게 이 Virtual Address Space 메모리 주소 길이를 의미한다.
32비트 컴퓨터는 한 프로세스당 가상의 메모리 주소인
0x00000000 ~ 0xffffffff
를 사용할 수 있다.16진수 하나당 \(2^4\), 4비트의 정보를 표현할 수 있다.
8개 있으니까 \(4 \times 8 = 32\) 라서 32비트라고 부른다.
\(2^{32} byte = 2^2 \times 2^{10} \times 2^{10} \times 2^{10} byte\) G M K 총 4기가 바이트의 주소를 표현할 수 있음.
64비트 컴퓨터는 한 프로세스당 \(2^{64}\)개의 가상의 메모리 주소를 사용할 수 있다는 뜻이다.
어떻게 프로그램 하나당 4기가씩 메모리를 제공하는가?
실제로 메모리를 4기가를 주는게 아니라, 가상적으로 4기가가 있는 거처럼 준다. 만약 진짜 4기가를 주면 16기가 메모리를 꼽은 컴퓨터는 프로그램을 4개밖에 못돌린다...
이게 어떻게 가능한 것인가?
메모리에는 정말 필요한 것만 올리고, 당장에 사용하지 않는 정보는 스토리지에 올려두고, 필요해질 때 다시 메모리에서 사용하는 방식을 사용하기 때문에 실제 메모리보다 더 많이 돌리는 것처럼 보이게 해준다. 이 과정을 Swap라고 한다.
[!question]- What is Swap space?{title} Swap을 위해 사용되는 하드디스크, SSD의 공간이다.
메모리 공간이 많으면 프로세스 관련된 정보를 그냥 통째로 메모리에 올려서 사용할 수 있으므로 빠르다.
메모리 공간이 적으면, Swap이 자주 일어나게 된다. Swap은 디스크를 사용하는 작업이기 때문에 느릴 수 밖에 없다.
이래서 RAM은 다다익선이라고 하는구나.. Swap이 자주 일어나서 느려지면 RAM을 더 꽂으면 된다.
[!tip]- printf 해서 보여주는 메모리 주소의 진실{title} C언어에서 메모리 주소를 찍어서 보여주는 건 가상의 주소다.
두 프로세스에서 서로 다른 변수이지만 우연히 같은 메모리 주소를 사용하고 있더라도, 실제 위치하고 있는 메모리 주소는 서로 다르다.
운영체제는 이 Virtual Address를 실제 메모리 주소로 Mapping하는 작업이 필요하다.
Mappling을 위해 실시간으로 업데이트 되는 Table을 하나 만들어 사용한다.
주소가 할당되면, Table에
프로세스 & 가상의 주소 <-> 실제 메모리 주소
정보를 업데이트한다.만약 가상의 주소를 통해 실제 메모리 주소를 알고 싶으면, Table에다가 집어넣으면 된다.
메모리 주소 범위마다 용도가 다르다.
위 사진은 32비트 리눅스 기준 Virtual Address Space이다.
0xffffffff ~ 0xc000000
: Kernel 영역 거의 메모리의 1/4 영역을 차지한다. 운영체제도 일종의 프로그램이고, 프로그램이 실행되려면 메모리 위에 코드가 올라와 있어야 한다. 운영체제와 관련된 코드는 이 Kernel 영역에 올라가 실행된다.User Stack 함수 Stack, 지역 변수과 같이 runtime에 생성되는 것은 이 영역의 메모리 주소를 사용한다.
Memory mapped region for shared libraies printf()와 같은 표준 라이브러리 함수 코드가 이 영역의 메모리 주소를 사용한다.
Run-time heap 동적 메모리 할당시 Heap 영역에 생성된다.
Read/write data static 변수가 이 영역의 메모리 주소를 사용한다.
Read-only code and data 프로그램의 코드와 상수값이 이 영역의 메모리 주소를 사용한다.