프로세스
프로세스는 돌아가는 프로그램 하나에 자기 몫의 메모리와 자원을 떼어 주고 남과 갈라 놓습니다. 운영체제는 이 단위로 누가 언제 계산할 차례를 받을지 정합니다. 누가 무엇을 쥐고 있는지도 이 단위로 셉니다. 그래서 편집기 하나가 잘못 돌아도 옆에 켜 둔 브라우저는 멀쩡히 돕니다.
쉽고 빠른 이해
프로세스는 돌아가는 프로그램 하나에게 운영체제가 자기 몫의 메모리를 떼어 주고 남과 갈라 놓는 단위입니다. 편집기와 브라우저를 같이 켜 두면 둘은 서로 다른 프로세스입니다.
갈라 놓지 않으면 한 프로그램이 잘못 쓴 값이 옆 프로그램의 메모리를 덮어씁니다. 누가 망가뜨렸는지도 가릴 수 없습니다.
어떻게 도는가:
- 이미 도는 프로세스가 자기를 복제해 새 프로세스를 하나 만듭니다
- 그 복제본이 실행할 프로그램을 읽어 들여 안에서 도는 프로그램을 바꿉니다
- 끝날 때 종료 상태를 남깁니다. 만든 쪽이 그 값을 거둬 가면 자취가 사라집니다
대가는 갈라 놓은 만큼 서로 말을 주고받기가 번거로워진다는 것입니다.
상세
공유 사무실에서 팀마다 잠기는 방을 하나씩 받습니다. 옆 방 서랍은 열 수 없고, 옆 방이 어질러져도 우리 방 물건은 그대로 남습니다.
프로세스는 주소 공간 하나와 그 안에서 도는 실행 흐름 하나 이상, 그리고 그 실행 흐름들에게 필요한 시스템 자원을 묶은 것입니다. 주소 공간은 그 프로세스가 값을 읽고 쓰는 메모리 번지의 범위입니다. 실행 흐름 하나를 스레드라고 합니다. 스레드는 프로세스 안에서 도는 하나의 제어 흐름입니다. 자기만의 스레드 식별자와 스케줄링 우선순위를 갖습니다. 스케줄링은 누구에게 다음 차례를 줄지 정하는 일입니다.
flowchart TD
subgraph P["프로세스 하나"]
AS["주소 공간<br/>값을 읽고 쓰는 번지의 범위"]
subgraph T1["스레드 1"]
T1A["스레드 식별자 · 스택"]
end
subgraph T2["스레드 2"]
T2A["스레드 식별자 · 스택"]
end
end
P --> R["프로세스 단위로 딸린 것<br/>프로세스 식별자 · 열어 둔 파일 · 현재 작업 디렉터리"]
주소 공간은 프로세스마다 하나이고 스레드는 그 안에 여럿 들어갑니다. 프로세스 식별자처럼 프로세스 단위로 하나뿐인 것은 그 스레드들이 함께 씁니다.
POSIX(Portable Operating System Interface)는 유닉스 계열 운영체제가 맞춰야 할 인터페이스를 적어 둔 표준입니다. 위 정의가 거기 적힌 문장입니다.
이 낱말은 두 가지를 함께 가리킵니다. 하나는 살아서 도는 프로세스입니다. 다른 하나는 끝났지만 아직 만든 쪽이 결과를 거둬 가지 않은 좀비 프로세스입니다. 한 프로세스의 수명은 만들어질 때 시작해서 그 프로세스 식별자가 시스템에 되돌려질 때 끝납니다.
운영체제가 이 단위를 세우면 두 가지를 할 수 있습니다. 하나는 갈라 놓기입니다. 남의 메모리를 못 읽게 막으면 한 프로그램의 잘못이 그 프로그램 안에서 끝납니다. 다른 하나는 나눠 주기입니다. 명령을 실행하는 장치인 프로세서(CPU, Central Processing Unit)는 한 번에 하나만 돌립니다. 누구에게 얼마나 줄지 정하려면 받아 갈 쪽에 먼저 이름이 있어야 합니다.
프로그램과 프로세스의 차이
프로그램은 디스크에 놓인 파일입니다. 명령과 초기값이 적혀 있을 뿐 아무 일도 하지 않습니다. 프로세스는 그 파일을 읽어 들여 돌리기 시작한 뒤의 것입니다. 지금 몇 번째 명령을 하는지, 계산하다 만 값이 얼마인지 같은 것이 여기 붙습니다.
그래서 프로그램 하나에서 프로세스 여럿이 나올 수 있습니다. 파일은 한 벌입니다. 그것을 읽어 들인 덩이가 여럿입니다. 편집기를 두 번 띄우면 화면에 뜬 창이 둘이고 프로세스도 둘입니다.
반대 방향도 있습니다. 돌고 있던 프로세스가 다른 프로그램 파일을 읽어 들여 자기 안의 내용을 새로 채우는 일입니다. 프로세스 자신은 그대로 남고 그 안에서 도는 프로그램만 바뀝니다.
프로세스가 쥐는 몫
한 프로세스 안의 스레드들이 무엇을 함께 쓰고 무엇을 따로 갖는지를 봅니다. POSIX 가 그 선을 갈라 적어 두었습니다.
함께 쓰는 것부터입니다. 프로세스 식별자와 부모 프로세스 식별자가 그렇습니다. 지금 어느 디렉터리에서 일하고 있는지를 가리키는 현재 작업 디렉터리도 하나입니다. 열어 둔 파일을 가리키는 번호인 파일 디스크립터도 프로세스 단위입니다. 한 스레드가 연 파일을 옆 스레드가 그대로 읽고 쓸 수 있다는 뜻입니다.
스레드마다 따로 갖는 것도 있습니다. 스레드 식별자, 자기 스택, 그리고 마지막에 난 오류
번호를 담는 errno 값이 그렇습니다. 나머지 목록은 아래 경계 절의 표가 한 번에 보여 줍니다.
메모리는 프로세스마다 갈립니다. 새 프로세스를 복제로 만들면 그 순간 두 메모리 공간의 내용은 같습니다. 그래도 둘은 서로 다른 메모리 공간에서 돕니다. 한쪽이 자기 번지에 값을 써도 다른 쪽에는 아무 일도 일어나지 않습니다. 이렇게 번지를 프로세스마다 따로 잡아 주는 방식을 가상 메모리라고 합니다.
flowchart TD
subgraph G1["편집기 프로세스"]
A["번지 1000번"]
end
subgraph G2["브라우저 프로세스"]
B["번지 1000번"]
end
A --> MA["실제 메모리 가"]
B --> MB["실제 메모리 나"]
두 프로세스가 같은 번지를 불러도 커널이 각각 다른 실제 메모리로 이어 줍니다. 덕분에 옆 프로세스가 무엇을 하든 내 번지의 값은 남아 있습니다.
갈라 놓았으므로 값을 넘기는 데는 따로 마련한 통로가 필요합니다. 같은 프로세스 안이라면 변수 하나로 끝날 일입니다. 프로세스끼리는 그렇게 못 합니다. 이 통로를 통틀어 프로세스 간 통신이라 하고, 파이프와 소켓이 그런 통로입니다.
배경
기계 한 대에 프로그램 여럿을 올리기 시작하자 두 가지가 곤란해졌습니다. 첫째, 프로세서는 한 번에 하나만 돌리는데 올라온 프로그램은 여럿입니다. 누구를 얼마나 돌릴지 정해야 합니다. 둘째, 메모리는 한 덩이입니다. 그런데 프로그램마다 거기에 값을 씁니다. 한 프로그램이 번지를 잘못 잡으면 옆 프로그램이 쓰던 값을 덮어씁니다.
두 곤란은 같은 것을 요구합니다. 돌아가는 프로그램 하나하나에 이름을 붙이고, 그 이름에 딸린 몫을 정해 두어야 합니다. 이름이 있어야 누구에게 프로세서를 줄지 말할 수 있습니다. 몫이 정해져 있어야 남의 몫에 손대는 것을 막을 수 있습니다.
그 이름과 몫을 한 덩이로 묶어 부르는 것이 프로세스입니다. 운영체제는 프로그램 파일이 아니라 이 단위를 셉니다. 누가 돌고 있는지, 누가 무엇을 열어 두었는지, 누가 언제 끝났는지가 전부 이 단위에 매달립니다.
예시
리눅스의 /proc
리눅스는 도는 프로세스마다 /proc 아래에 디렉터리를 하나 내줍니다. 디렉터리 이름이 그
프로세스의 식별자입니다. 그 안의 status 파일이 사람이 읽기 쉬운 꼴로 상태를 보여 줍니다.
man page 에 실린 예시 출력에서 아래에 설명할 줄만 옮기면 이렇습니다.
Name: bash
State: S (sleeping)
Tgid: 17248
Pid: 17248
PPid: 17200
Threads: 1
voluntary_ctxt_switches: 150
nonvoluntary_ctxt_switches: 545
Name 은 이 프로세스가 돌리고 있는 명령입니다. 끝을 알리는 널 바이트까지 세어 열여섯 글자를
넘으면 잘립니다. 위 출력의 bash 는 명령을 받아 주는 프로그램인 셸입니다. State 는 지금
상태입니다.
Tgid 와 Pid 는 이름이 헷갈리는 두 줄입니다. Tgid 는 스레드 그룹 식별자이고 이것이 곧
프로세스 식별자입니다. Pid 는 그 줄 이름과 달리 스레드 식별자입니다. 스레드가 하나뿐인
프로세스에서는 둘이 같은 값이라 위 출력처럼 17248 이 두 번 나옵니다. 스레드 그룹이 무엇인지는
아래 경계 절이 풉니다. PPid 는 이 프로세스를 만든 쪽의 프로세스 식별자입니다. Threads 는
이 프로세스 안에서 도는 스레드 수입니다.
맨 아래 두 줄은 이 프로세스가 프로세서를 내려놓고 다른 프로세스에게 차례를 넘긴 횟수입니다. 스스로 내려놓은 것과 빼앗긴 것을 따로 셉니다. 이렇게 돌던 것을 멈추고 다음 것을 꺼내 끼우는 일을 문맥 교환이라고 합니다.
/proc/self 는 조금 다릅니다. 어떤 프로세스가 이 링크를 따라가면 그 프로세스 자신의
디렉터리로 이어집니다. 그래서 자기 식별자를 몰라도 자기 정보를 읽을 수 있습니다.
윈도우의 프로세스와 스레드
윈도우도 같은 단위를 씁니다. 프로세스는 프로그램을 실행하는 데 필요한 자원을 내줍니다. 가상 주소 공간, 실행 코드, 고유한 프로세스 식별자, 환경 변수, 우선순위 등급, 그리고 최소 하나의 실행 스레드가 그것입니다. 운영체제가 내준 물건을 잡는 손잡이 값인 핸들도 여기 딸립니다.
프로세스 하나는 스레드 하나로 시작합니다. 그 첫 스레드를 주 스레드라고 합니다. 스레드는 프로세스 안에서 스케줄링을 받을 수 있는 단위입니다. 한 프로세스의 스레드들은 그 프로세스의 가상 주소 공간과 시스템 자원을 함께 씁니다.
새 프로세스를 만들면 호출한 쪽은 PROCESS_INFORMATION 구조체로 그 결과를 받습니다.
hProcess 새로 만들어진 프로세스를 가리키는 핸들
hThread 그 프로세스의 주 스레드를 가리키는 핸들
dwProcessId 그 프로세스를 가리키는 데 쓰는 값
dwThreadId 그 스레드를 가리키는 데 쓰는 값
dwProcessId 는 프로세스가 만들어진 때부터 그 프로세스를 가리키는 핸들이 전부 닫히고 프로세스
객체가 풀릴 때까지 쓸 수 있습니다. 그 뒤로는 같은 값이 다시 쓰일 수 있습니다.
Chromium 브라우저
Chromium 은 응용 프로그램 하나를 여러 프로세스로 갈라 놓은 실물입니다. 까닭은 이렇습니다. 웹 페이지를 그리는 엔진을 한 번도 안 죽게 만드는 것도, 빈틈없이 안전하게 만드는 것도 거의 불가능합니다. 렌더링 엔진이나 플러그인의 버그 하나로 브라우저 전체와 열어 둔 탭 전부가 같이 내려갔습니다.
요즘 운영체제가 튼튼한 까닭도 프로세스에 있습니다. 응용 프로그램을 서로 벽이 쳐진 별도 프로세스에 넣기 때문입니다. 한 응용 프로그램이 죽어도 대개 다른 응용 프로그램이나 운영체제 까지 망가지지는 않습니다.
Chromium 은 같은 방식을 브라우저 안으로 들여왔습니다. 화면과 나머지 프로세스를 관리하는 주 프로세스를 브라우저 프로세스라고 합니다. 웹 내용을 다루는 프로세스는 렌더러 프로세스라고 합니다. 렌더러가 다른 프로세스와 시스템의 나머지에 손대는 것도 제한합니다.
동작
프로세스 하나가 생겨서 없어질 때까지의 순서는 아래와 같습니다. 유닉스 계열에서는 시스템 콜 네 개가 이 순서를 나눠 맡습니다.
flowchart TD
A["부모 프로세스"] -->|자기를 복제한다| B["자식 프로세스"]
B -->|실행할 프로그램을 읽어 들인다| C["프로그램이 바뀐 자식"]
C -->|끝내며 종료 상태를 남긴다| D["거둬지기를 기다리는 자식"]
D -->|부모가 거둬 간다| A
첫째, 만들기입니다. fork(2) 는 자기를 부른 프로세스를 복제해 새 프로세스를 만듭니다. 만든
쪽을 부모, 만들어진 쪽을 자식이라고 합니다. 자식은 자기만의 고유한 프로세스 식별자를 받고,
자식의 부모 식별자는 부모의 식별자와 같아집니다. 자원 사용량과 프로세서 시간 카운터는 자식
쪽에서 0으로 되돌아갑니다.
fork(2) 는 한 번 불렀는데 두 쪽에서 돌아옵니다. 부른 쪽은 이 반환값으로 자기가 부모인지
자식인지 가립니다.
flowchart TD
C["fork(2) 를 부른다"] --> Q{"자식이 만들어졌나"}
Q -->|성공 · 부모 쪽| P["자식의 프로세스 식별자가 돌아온다"]
Q -->|성공 · 자식 쪽| K["0 이 돌아온다"]
Q -->|실패| F["부모에게 -1 이 돌아오고 자식은 만들어지지 않는다"]
둘째, 갈아 끼우기입니다. execve(2) 는 지금 도는 프로그램을 다른 프로그램으로 바꿉니다.
스택과 힙, 데이터 영역을 새로 초기화합니다. 프로세스 자신은 그대로 남습니다. 이 호출은
성공하면 돌아오지 않습니다. 돌아왔다면 실패한 것입니다.
셋째, 오가기입니다. 프로세스가 늘 돌고 있는 것은 아닙니다. 상태를 바꾸는 계기 가운데 하나가
시그널입니다. 시그널은 운영체제가 프로세스에게 보내는 짧은 알림이고, 받은 쪽은 하던 일을
멈추거나 끝나거나 다시 풀립니다. status 파일의 State 값이 그 갈림을 보여 줍니다. 도는 중,
자는 중, 디스크를 기다리는 중, 멈춰 세워진 중, 좀비, 죽음이 그 값들입니다. 멈춰 세워진 중은
시그널로 멈춘 것과 디버거가 들여다보느라 멈춘 것 둘로 다시 갈립니다.
stateDiagram-v2
state "도는 중" as R
state "자는 중" as S
state "디스크를 기다리는 중" as D
state "멈춰 세워진 중" as T
state "좀비" as Z
[*] --> R: 만들어짐
R --> S: 결과를 기다림
S --> R: 기다리던 것이 옴
R --> D: 디스크에서 읽어 오기를 기다림
D --> R: 읽어 온 것이 옴
R --> T: 시그널로 멈춰 세워짐
T --> R: 다시 풀림
R --> Z: 끝남
Z --> [*]: 부모가 거둠
자는 중은 밖에서 올 것을 기다리는 때입니다. 네트워크로 답이 오기를 기다리는 동안이 그렇습니다. 이때 프로세서를 쥐고 있어 봐야 할 일이 없습니다. 그래서 운영체제가 거둬서 다른 프로세스에게 줍니다. 할 일이 남았는데도 정해진 몫을 다 써서 내려오는 때는 다릅니다. 이렇게 중간에 끊고 다음 차례에게 넘기는 방식을 선점이라고 합니다.
넷째, 끝내기입니다. _exit(2) 는 자기를 부른 프로세스를 즉시 끝냅니다. 그 프로세스가 열어
두었던 파일 디스크립터는 전부 닫힙니다. 남은 자식들은 init(1) 이나 가장 가까운 대리 부모
프로세스가 물려받습니다. 이렇게 부모를 잃고 다른 쪽에 넘겨진 자식을 고아 프로세스라고
합니다. 부모에게는 자식이 끝났다고 알리는 SIGCHLD 시그널이 갑니다. 종료 상태는 부모에게
넘어가 wait(2) 계열 호출로 거둬집니다.
다섯째, 거두기입니다. wait(2) 계열 호출은 자식의 상태 변화를 기다립니다. 자식이 끝났거나,
시그널로 멈춰 세워졌거나, 시그널로 다시 풀린 것이 상태 변화입니다. 끝난 자식을 거둬야 시스템이
그 자식에게 딸린 자원을 놓습니다.
거두지 않으면 끝난 자식이 좀비로 남습니다. 커널은 좀비에 대해 최소한의 정보만 들고 있습니다.
식별자와 종료 상태, 자원 사용량입니다. 부모가 나중에 거두러 올 수 있게 남겨 두는 것입니다.
좀비는 거둬지기 전까지 커널 프로세스 표의 칸 하나를 차지합니다. 그 표가 차면 새 프로세스를 더
만들 수 없습니다. 부모가 먼저 끝나면 남은 좀비 자식은 init(1) 이 입양해서 자동으로 거둬
갑니다.
경계
리눅스에서 스레드도 프로세스인가. 아닙니다. 다만 커널이 둘을 같은 방식으로 만든다는 것이 헷갈리는 대목입니다.
리눅스는 clone(2) 로 새 자식 프로세스를 만듭니다. fork(2) 와 비슷하되 다른 점이 하나
있습니다. 부른 쪽과 자식이 실행 맥락의 어느 조각을 함께 쓸지를 더 촘촘히 고를 수 있습니다.
메모리 공간을 함께 쓸지, 파일 디스크립터 표를 함께 쓸지, 시그널 처리 표를 함께 쓸지를 호출한
쪽이 정합니다.
| 플래그 | 켜면 | 끄면 |
|---|---|---|
CLONE_VM |
부른 쪽과 자식이 같은 메모리 공간에서 돈다. 한쪽이 쓴 값이 다른 쪽에 보인다 | 자식이 그 순간의 메모리 공간을 복사해서 따로 쓴다 |
CLONE_FILES |
파일 디스크립터 표를 함께 쓴다. 한쪽이 연 파일이 다른 쪽에서도 쓸 수 있다 | 자식이 파일 디스크립터의 복사본을 물려받는다 |
CLONE_THREAD |
자식이 부른 쪽과 같은 스레드 그룹에 들어간다 | 자식이 자기 스레드 그룹을 갖는다 |
스레드 그룹은 하나의 식별자를 함께 쓰는 실행 흐름의 묶음입니다. 그 식별자를 스레드 그룹
식별자(TGID, Thread Group ID)라고 합니다. 리눅스에서 getpid(2) 가 돌려주는 값이 이
TGID 입니다. 그래서 같은 프로그램의 스레드들은 프로세스 식별자를 같은 값으로 봅니다. 그 안에서
서로를 가리는 것은 스레드마다 다른 스레드 식별자입니다. 앞의 /proc 출력에서 Tgid 와 Pid
가 나뉘어 있던 까닭이 이것입니다.
판정의 근거는 무엇을 함께 쓰느냐입니다. POSIX 스레드 규약이 그 선을 적어 둡니다. 한 프로세스는 스레드 여럿을 품을 수 있고, 그 스레드들은 같은 프로그램을 돌립니다. 스레드들은 전역 메모리를 함께 쓰고, 스택은 저마다 따로 갖습니다.
| 프로세스 단위로 하나 | 스레드마다 따로 |
|---|---|
| 프로세스 식별자 · 부모 프로세스 식별자 | 스레드 식별자 |
| 프로세스 그룹 식별자 · 세션 식별자 | 시그널을 막아 두는 표 |
| 명령을 받는 터미널 | 오류 번호 errno |
| 사용자 식별자 · 그룹 식별자 | 스택 |
| 열어 둔 파일 디스크립터 · 파일 잠금 | |
| 시그널 처리 방식 · 파일 만들 때의 권한 마스크 | |
| 현재 디렉터리 · 루트 디렉터리 | |
| 우선순위 값 · 자원 한도 |
왼쪽 칸이 곧 프로세스가 쥐는 몫입니다. 스레드는 그 몫을 새로 받지 않고 들어와서 함께 씁니다. 그러니 스레드는 프로세스와 같은 호출로 만들어져도 프로세스가 아닙니다. 프로세스는 주소 공간 하나와 그 안의 스레드들을 묶은 단위입니다. 스레드는 그 안에서 도는 제어 흐름입니다.
이 선은 무엇을 새로 만들지도 갈라 줍니다. 스레드끼리는 전역 메모리를 함께 쓰므로 값을 넘기는 데 따로 통로가 필요 없습니다. 프로세스를 하나 더 만들면 그 통로부터 마련해야 합니다.
관련 항목
프로세스를 만들고 끝내는 시스템 콜
fork · exec · wait · exit · clone · kill · 시스템 콜
프로세스가 품는 실행 단위와 자원
스레드 · 가상 메모리 · 주소 공간 · 파일 디스크립터 · 스택 · 힙 · 프로세스 제어 블록 · 페이지 테이블
프로세스에게 프로세서를 나눠 주는 장치
커널 · 스케줄링 · 문맥 교환 · 선점 · 타임 슬라이스 · 인터럽트 · 우선순위
프로세스끼리 말을 주고받는 통로
프로세스 간 통신 · 파이프 · 소켓 · 공유 메모리 · 메시지 큐 · 시그널 · 세마포어
프로세스를 가리키고 묶는 식별자
프로세스 식별자 · 프로세스 그룹 · 세션 · 사용자 식별자 · 네임스페이스 · 제어 터미널
프로세스가 살아가는 실행 환경
운영체제 · 유닉스 · 리눅스 · 셸 · 데몬 · 컨테이너 · 가상 머신
프로세스 주위에서 자주 나는 장애
좀비 프로세스 · 고아 프로세스 · 데드락 · 경쟁 상태 · 메모리 누수 · 포크 폭탄 · 메모리 부족
프로세스를 대신하거나 쪼개는 실행 단위
코루틴 · 그린 스레드 · 경량 프로세스 · 이벤트 루프 · 액터 모델
프로세스의 비용을 재는 지표
다른 이름: process