Proven C BookEnglish GitHub

3 프로그램과 프로세스 — 실행된다는 것

먼저 알아야 할 것

2장 기억의 구역들 · 기억이 코드·정적·스택·힙으로 나뉜다는 밑그림

돌아보기

2장에서 프로그램의 기억이 코드·정적·스택·힙으로 나뉜다고 했다. 그런데 그 구역들은 언제, 누가 마련해 주는가?

답. 프로그램이 실행되는 순간, 대개 운영체제가 마련한다. 디스크에 놓인 실행 파일은 아직 아무 기억도 갖고 있지 않다 — 그저 “코드는 이렇고, 정적 구역은 이만큼 필요하다”는 설계도다. 그 설계도를 읽어 실제 기억을 펼치고, 작업대를 세우고, 첫 명령으로 뛰어들게 하는 일 — 그것이 실행이고, 그렇게 살아난 한 벌을 프로세스(process)라 부른다.

이 장의 필요성과 맥락

2장이 「기억이 이렇게 나뉜다」고 했다면, 그 구역을 누가 언제 만들어 주는가가 남는다. 답은 운영체제이고, 그 답을 여기서 받아 두어야 15장의 첫 프로그램이 「실행된다」는 말이 빈말이 되지 않는다. 파일과 프로세스를 가르는 이 한 걸음이 1부의 마지막이다 — 여기까지가 C 를 배우기 전에 알아야 할 무대다.

이 장이 끝나면

파일로 있는 프로그램과, 실행되어 살아 있는 프로세스의 차이를 가른다. 운영체제가 프로세스에게 무엇을 주는지(자기만의 주소 공간, 열린 파일, 종료 상태(exit status)), 그리고 프로세스가 어떻게 태어나는지 — 유닉스 계열의 fork와 윈도우의 방식 — 를 짧게 본다. 여기 나오는 것은 C 표준이 아니다. 표준이 약속하는 것과 운영체제가 주는 것의 경계를 처음으로 긋는 장이기도 하다.

이 장에서 답할 질문

  1. “자기만의 기억”이라는 말이 이상하다 — 기계에 붙은 메모리는 하나 아닌가?
  2. 그러면 프로세스를 지금 알아 두어야 하는 이유는 무엇인가?

3.1 프로그램은 명사, 프로세스는 동사

구별은 단순하다.

같은 프로그램을 세 번 실행하면 프로세스가 셋 생긴다. 셋은 같은 코드를 공유하지만 기억은 각자 따로다 — 한쪽에서 값을 바꿔도 다른 쪽은 모른다.

운영체제가 프로세스에게 주는 것무엇인가이 책에서
주소 공간자기만의 기억 지도(2장의 네 구역)83장
실행 위치지금 어느 명령을 하는 중인지
열린 파일 목록표준 입출력을 포함한 통로10장·63장
명령줄 인자·환경실행할 때 건네받은 값들53장
종료 상태끝나며 남기는 한 숫자15장·53장

표 3.1

이 목록에서 두 가지가 이후 장들과 곧장 이어진다. 첫째, 10장에서 볼 표준 입력·출력·오류는 운영체제가 프로세스에게 미리 열어 준 통로다 — 프로그램이 직접 여는 것이 아니다. 둘째, 15장의 헬로 월드가 끝내며 돌려주는 0이 바로 종료 상태이고, 그 숫자를 받는 쪽은 이 프로세스를 실행한 누군가다.

문. “자기만의 기억”이라는 말이 이상하다 — 기계에 붙은 메모리는 하나 아닌가?

답. 물리적으로는 하나지만, 운영체제가 프로세스마다 저마다 다른 주소 공간을 보여 준다. 프로세스가 보는 주소는 물리 주소가 아니라 그 프로세스만의 번호이고, 하드웨어가 그것을 실제 자리로 옮겨 준다(가상 메모리). 그래서 두 프로세스가 똑같은 주소를 써도 서로 다른 자리를 만지고, 한쪽이 무너져도 다른 쪽은 멀쩡하다.

이 격리가 없는 세계도 있다 — 운영체제 없이 도는 작은 칩에서는 프로세스라는 개념 자체가 없고, 코드 하나가 기계 전체를 쓴다(83장). 그래서 이 장의 이야기는 “C가 도는 모든 곳”의 이야기가 아니라 운영체제 위에서 돌 때의 이야기다.

3.2 프로세스는 어떻게 태어나는가

여기서부터는 운영체제마다 다르다. 두 갈래를 얼굴만 익힌다.

플랫폼 노트. 유닉스 계열 — forkexec

유닉스 계열(리눅스·macOS·BSD)은 프로세스를 만드는 일을 두 걸음으로 나눈다. 이 설계가 셸과 파이프라인(10장)의 뿌리다.

  • fork() — 지금 프로세스를 복제한다. 복제된 쪽(자식)은 부모와 같은 코드, 같은 기억 내용, 같은 열린 파일을 갖고 시작한다. 기묘한 것은 이 함수가 두 번 돌아온다는 점이다 — 부모에게는 자식의 번호(PID)를, 자식에게는 0을 돌려준다. 그 반환값으로 자기가 누구인지 안다.
  • exec 계열 — 지금 프로세스의 내용을 다른 프로그램으로 갈아 끼운다. 기억이 통째로 새 프로그램의 것으로 바뀌고, 돌아오지 않는다(성공하면).
  • wait 계열 — 부모가 자식이 끝나기를 기다리고 종료 상태를 받는다.
pid_t pid = fork();
if (pid == 0) {               /* 자식 */
    execl("/bin/ls", "ls", (char *)0);
    _exit(127);               /* exec 이 실패했을 때만 여기 온다 */
} else if (pid > 0) {         /* 부모 */
    int status;
    waitpid(pid, &status, 0);
}

터미널에서 명령을 하나 치면 셸이 정확히 이 일을 한다 — 자기를 복제하고, 복제된 쪽을 그 명령으로 갈아 끼우고, 끝나기를 기다린다. 10장에서 본 리다이렉션과 파이프도 이 사이에서 일어난다: 복제한 뒤 갈아 끼우기 전에 자식의 입출력 통로를 바꿔 두는 것이다. 두 걸음으로 나눈 설계가 그 자리를 만들어 준 셈이다.

복제라고 해서 기억을 통째로 베끼지는 않는다. 오늘의 구현은 쓸 때까지 미룬다(copy-on-write) — 부모와 자식이 같은 자리를 공유하다가, 어느 한쪽이 쓰는 순간 그 부분만 복사한다.

플랫폼 노트. 윈도우 — CreateProcess

윈도우에는 fork가 없다. 대신 한 걸음이다 — CreateProcess가 “이 프로그램을 새 프로세스로 시작하라”고 한 번에 지시한다. 복제 단계가 없으므로 자식은 부모의 기억을 물려받지 않고, 부모가 물려주고 싶은 것(열린 통로, 환경, 작업 디렉터리)은 인자로 명시한다.

두 방식의 성격 차이가 이식에서 드러난다. 유닉스 코드의 “복제한 뒤 조금 고쳐서 갈아 끼운다”는 무늬는 윈도우에서 그대로 옮겨지지 않고, 필요한 것을 인자로 적어 넘기는 형태로 다시 써야 한다. POSIX 쪽에도 이 무늬를 한 걸음으로 만든 posix_spawn이 있다.

흔한 오해. “이런 것도 C 표준에 들어 있겠지”

들어 있지 않다. forkexecCreateProcess도 C 표준이 아니다. C 표준이 프로세스에 대해 말하는 것은 놀랄 만큼 적다 — 프로그램이 시작해서 main이 돌고, 끝나며 종료 상태를 남긴다는 정도다. 프로세스를 새로 만들거나, 다른 프로그램을 실행하거나, 자식을 기다리는 방법은 표준에 없다(그나마 system 하나가 “명령 문자열을 실행한다”고만 정해져 있고, 그것이 무엇을 뜻하는지는 구현에 맡긴다).

왜 이렇게 인색한가 — C가 운영체제가 없는 기계에서도 돌아야 하기 때문이다(62장의 자유 사용 구현). 프로세스가 없는 칩에도 C는 있어야 하므로, 프로세스는 표준의 밖, 운영체제의 영역에 남았다.

이 경계 감각이 이 책 전체에서 중요하다. 표준에 있는 것은 어디서나 통하고, 그 밖의 것은 그 운영체제에서만 통한다. 이 책이 운영체제에 묶인 이야기를 “플랫폼 노트” 상자에 따로 담는 이유이기도 하다.

문. 그러면 프로세스를 지금 알아 두어야 하는 이유는 무엇인가?

답. 세 가지가 곧 필요해지기 때문이다.

첫째, 10장의 스트림이 어디서 오는지 — 표준 입출력은 프로세스가 태어날 때 이미 열려 있는 통로다. 둘째, 15장의 헬로 월드가 남기는 종료 상태 0이 누구에게 가는지. 셋째, 2장에서 본 네 구역이 프로세스마다 따로라는 것 — 전역 변수가 “프로그램 전체에서 하나”라는 말은 한 프로세스 안에서 하나라는 뜻이다.

더 깊은 이야기 — 여러 갈래로 도는 프로그램(스레드), 프로세스 사이의 통신, 신호 — 는 이 책의 범위를 넘지만, 그중 C 표준이 다루는 조각들은 제자리에서 만난다(신호는 75장, 스레드와 원자적 연산(atomic operation)은 80장).

복습 정리

기억할 것요지
프로그램 / 프로세스파일 / 실행되어 살아 있는 한 벌
운영체제가 주는 것주소 공간, 열린 통로, 종료 상태
기억은 각자같은 프로그램을 여러 번 실행해도 서로 모른다
유닉스fork(복제) + exec(갈아 끼우기) 두 걸음
윈도우CreateProcess 한 걸음
표준 밖프로세스는 C 표준의 영역이 아니다 — OS 가 준다

표 3.2

제1부가 끝났다. C가 어떤 언어이고 왜 지금 배우는지(1장), 프로그램이 기억을 어떻게 나눠 쓰는지(2장), 그리고 실행된다는 것이 무엇인지(3장)까지 — 바닥을 깔았다.

다음 부는 그 바닥 아래로 한 층 더 내려간다. 컴퓨터를 세 부품으로 그리는 것에서 시작해, 비트에서 글자까지 표현의 사다리를 오르고, 기계가 왜 이렇게 복잡해졌는지를 본다.