프로세스들과 바람피는 운영체제

Eddy·2022년 8월 30일
27
post-thumbnail

운영체제를 이해하기 위해 필수적인 개념들을 하나씩 살펴보고 있다. 이전 글에서 '파일' 얘기를 했다. 오늘은 '프로세스' 얘기를 해보자.

프로세스는 많이 들어봤을 것이다. 그치만 여러번 들어도 아리송한 개념이다. 워낙 추상적이기 때문이다.

프로세스 및 관련 키워드를 최대한 쉽게 설명하려고 노력해보았다. 이 글을 읽고 나면 프로세스에 대한 감을 잡고, OS를 이해하는 탄탄한 기초를 만들 수 있을 것이다.

프로세스는 ‘호텔에 체크인한 고객’

초기 컴퓨터는 한번에 하나의 프로그램만 실행했다. 한 프로그램이 전체 컴퓨터 시스템을 사용했다. 비유하자면 한 사람이 집 전체를 빌려쓰는 전셋방이었던 셈이다. 세입자 한명이 짐을 빼기 전까지는 다른 세입자가 들어올 수 없었다.

그 후 컴퓨터의 활용도를 높이기 위한 발전이 이뤄졌다. 1970년대 이후부터 거의 모든 컴퓨터 시스템은 한 번에 여러 개 프로그램을 같이 실행할 수 있었다. '멀티 프로그래밍'이 가능해졌다.

멀티 프로그래밍 시스템에서는, 컴퓨터의 메모리 안에 여러개의 프로그램이 같이 저장된다. CPU는 각 프로그램의 명령어를 번갈아가면서 실행한다.

컴퓨터가 '전셋방'에서 '호텔'로 진화한 것이다!

전세방일 때는 그냥 자기맘대로 집을 쓰면 됐다.

하지만 호텔이 되자 새로운 문제가 발생했다. 수많은 사람이 동시에 쓰는 시설이 되었기 때문이다.

예를 들어, 여러 프로그램이 서로 충돌하지 않도록 보호해야했다. 각 프로그램이 필요할 때 효율적으로 하드웨어 자원을 배분해야 했다. 어떤 프로그램이 시설을 사용할 수 있는지 없는지 권한을 설정해야 했다.

다시 말해, '호텔 관리자'가 필요해졌다. 호텔 관리자는 많은 일을 한다. 호텔 시설/서비스를 효율적으로 여러 고객에게 전달하기 위해서다.

컴퓨터의 '호텔 관리자'가 바로 운영체제(OS)다.

'호텔 관리자'는 고객이 들어오면 '체크인'을 한다. 방을 배정해준다. 고객 정보를 시스템에 등록한다. 해당 고객에게 ID를 부여한다. ('202호 고객' 같은 식으로) 그래야 관리가 편하기 때문이다.

마찬가지로 프로그램도 실행하면 '체크인' 과정을 거친다. 구체적으로 말하면, OS가 메모리를 배정한다. 디스크에 있던 프로그램이 메모리로 불러온다. 시스템 서비스를 사용할 수 있도록 준비시킨다.

'체크인'을 마친 프로그램에 OS는 번호(process id)를 붙인다. 그래야 관리가 편하기 때문이다.

이렇게 OS가 관리 중인 프로그램, 실행 중인 프로그램을 '프로세스'라고 부른다.

'운영체제라는 관리자'가 운영하는,
'컴퓨터라는 호텔'에,
'체크인한 고객'이 바로 프로세스다.

프로그램과 프로세스는 뭐가 다르지?

'프로그램'과 '프로세스'는 헷갈리기 쉽다. 한번 더 짚고 넘어가자.

소스 코드 ➡️ 프로그램

다음과 같은 C 코드가 있다.

#include <stdio.h>

int main()
{
	printf("hello, world\n");
	return 0;
}

C 언어를 몰라도 상관없다. "Hello, world"를 출력하고 종료하는 단순한 코드니까.

이 코드는 '프로그램'일까?

엄밀히 말해 이 코드는 '아직' 프로그램이 아니다. 그저 텍스트가 담긴 'hello.c' 파일일 뿐이다. 'hello.c'는 프로그래머가 작성한 소스 코드다.

이 코드가 프로그램이 되려면, '번역'이 필요하다. 컴퓨터가 알아들을 수 있는 언어로. 즉, C 언어 코드를 컴파일해서 기계어 명령으로 바꿔야 한다.

C 컴파일러(gcc)를 사용해서 hello.c를 컴파일한다.

소스 코드는 전처리, 컴파일, 어셈블, 링킹을 거친다. 마지막엔 운영체제가 실행 가능한 파일 형식이 된다.

[그림]

이게 '프로그램' 또는 '바이너리'다. 컴퓨터가 이해할 수 있도록 0과 1로 번역된 명령어 덩어리다.

소스 코드가 '프로그램'이 되는 과정이었다. 이제 '프로그램'이 '프로세스'가 되는 과정을 보자.

프로그램 ➡️ 프로세스

사용자가 디스크에 있는 'hello.o' (실행 파일)을 실행시킨다.

운영체제는 '가상화된 메모리'를 할당한다. (가상화는 잠시 뒤에 살펴보자.) 운영체제는 디스크에 저장된 프로그램을 가상화된 메모리에 불러온다.


메모리 공간은 여러 구역으로 나뉜다.

프로그램 (=명령어)가 저장되는 공간은 Text라고 한다.
프로그램이 담고 있는 전역 변수, 정적 변수도 따로 저장한다. Data 영역이라고 한다.

Text나 Data는 실행을 해보지 않아도 어느 정도의 공간이 필요할지 미리 정해져있다. 늘어나거나 줄어들지 않는다. 왜냐하면 프로그램이 실행되는 내내 필요하기 때문이다. 따라서 처음부터 고정된 크기의 구역을 배정한다.

도서관으로 치면, 사용자가 배정된 '고정석'이라고 할 수 있다.

반면 나머지 구역은 '자유석'이다. 항상 필요한 데이터가 아니라서, 그때 그때 사용하고 비워주는 공간이다. Heap 구역과 Stack 구역이 여기 해당된다.

Heap과 Stack은 프로그램을 실행하면서 공간이 필요할 때마다 그때그때 메모리를 할당하고, 사용이 끝나면 해제해서 줄어든다. 계속해서 영역의 크기가 변한다.

Heap과 Stack의 차이를 아는 것은 매우 중요하다. 하지만 오늘 주제에서는 벗어나니까 일단 넘어가자.


자, 이제 프로그램을 불러왔다. 필요한 메모리 공간도 할당했다. 프로세서(CPU)가 프로그램을 실행할 준비가 되었다.

프로그램이 '프로세스'가 된 것이다.

프로세스는 운영체제 입장에서의 '고객/사용자'이라고 했다. 그래서 운영체제는 실행 중인 프로그램을 관리하기 위해 관련 각종 데이터를 따로 저장해둔다. (호텔 관리자가 고객 명부를 관리하듯이)

이 데이터를 '프로세스 제어 블록(Process Control Block, PCB)'라고 부른다. 기억해두자.

가상화는 '양다리'다

프로세스 얘기를 하다보면, '가상화된 프로세서' '가상화된 메모리' 같은 용어가 등장한다.

교과서를 보면 이렇게 말한다.

'가상화를 통해 프로세스는 전체 자원을 혼자 쓰는 듯한 착각을 지니게 되며, 이는 프로세스라는 개념의 핵심이다.'

나는 이 말을 꽤 오랫동안 이해하지 못했다. 하지만 원리를 알고보니, 그렇게 어려운 건 아니었다.

쉬운 이해를 위해, 호텔과는 다른 비유를 또 하나 들어보자.

운영체제가 '양다리를 걸치는 바람둥이'라고 생각해보는 거다.

김운영 씨 이야기

김운영 씨는 엄청난 바람둥이다.

운영 씨는 너무나 매력적이어서 주변에 여자가 끊이지 않는다. 운영 씨는 이걸 잘 알고 있기 때문에 한 명만 사귀고 싶어하지 않는다. (나쁜 놈이다.)

그럼 어떻게 하느냐. 금요일은 A와, 화요일은 B와, 수요일은 C와... 이런 식으로 시간을 나눠서 데이트를 한다.

데이트 장소도 마찬가지다. 겹치지 않도록 각 애인별로 철저하게 공간을 나눈다. A는 이태원에서만 만나고, B는 강남에서만, C는 종로에서만 만나는 식이다.

그리고 시공간을 분할하는 것보다 더 중요한 게 있다. 무엇일까?

바로 A, B, C는 김운영이 다른 사람과도 사귀고 있다는 걸 몰라야 한다는 점이다.

김운영 씨는 상당히 철저해서 모든 애인에게 가명을 쓴다. 심지어 사는 주소도 다 다르게 말한다.

그 결과 김운영 씨는 완전 범죄를 유지하며 여러명의 애인과 양다리를 걸칠 수 있었다. 그 인원 수는 무려 수백명에 달한다. 각각의 애인들은 서로의 존재를 까맣게 모르고 행복한 연애를 했다.

실제였다면 '그알'에 나올만한 이런 엄청난 사기가, 가능한 이유는 무엇일까?

김운영씨가 애인과 24시간 붙어있는 것은 아니기 때문이다.

데이트를 하는 것은 1-2주에 한번 정도였다. 그래서 몸은 하나지만, 스케줄링과 가명을 철저히 해서 양다리를 걸치는 게 가능했던 것이다.

운영체제 이야기

실제 '운영체제'가 프로세스에게 하드웨어 자원을 분배하는 방식도 이와 같다.

컴퓨터 하나, OS 하나에 프로세스는 여러 개다. 프로세스를 실행하려면 메모리 공간과 CPU가 필요하다. 한 컴퓨터에 있는 한정된 CPU와 한정된 메모리를 가지고, 어떻게 수백개의 프로세스를 돌릴 수 있을까?

당연히 자원을 나눠야 한다. OS는 CPU와 메모리를 쪼개서 각 프로세스에게 나눈다.

CPU는 시간적으로 분할한다. 10초는 A 프로세스가 썼다가, 다음 10초는 B 프로세스가 쓰는 식이다. 실행을 교차시킨다.

메모리는 공간적으로 분할한다. 1000번부터 2000번 메모리까지는 A 프로세스가 쓰는 공간. 2000번부터 3000번 메모리까지는 B 프로세스가 쓰는 공간. 이런 식이다.

(물론 가상 메모리는 단순히 공간적으로만 나누는 기법은 아니지만 여기서는 일단 단순하게 설명한다.)

OS의 역할은 여기서 끝나지 않는다. OS는 각 프로세스에게 자원을 공유하고 배분하고 있다는 사실을 '숨긴다'. 바람둥이가 양다리를 걸치는 것처럼 말이다.

왜? 사용하는 쪽에서 편리하기 때문이다. 운영체제는 각 프로세스가 멀티 프로그래밍 상태를 신경쓸 필요가 없도록 단순한 인터페이스를 제공한다.

숨긴다는 게 무슨 말일까? 메모리의 예를 들어보자.

사실 컴퓨터가 가진 메모리는 하나다. 0부터 1만까지 주소가 있는 메모리라고 치자.

0부터 999까지는 A 프로세스 공간이다.
1000부터 1999까지는 B 프로세스 공간이다.

하지만 B 프로세스에게 주소 공간을 1000 ~ 1999라고 알려준다면, B 프로세스는 '0 ~ 999는 이미 다른 프로세스가 쓰고 있다'는 사실을 알아야 한다. 다른 프로세스의 공간과 자신의 메모리 범위를 항상 신경쓰면서 메모리 조작 요청을 해야할 것이다.

OS는 이런 정보를 숨긴다. B 프로세스가 받은 공간이 1000-1999이든, 4342-5341이든 상관없다. 그냥 0-1000으로 바꿔서 알려준다. (김운영 씨가 가명을 쓰는 것이 떠오르지 않는가?)

그러면 B 프로세스는 다른 프로세스의 메모리 주소에 신경을 쓰지 않아도 된다. "그냥 200번에 '1'을 저장해줘"라고 하면 된다.

운영체제는 '알았어' 하고 대답한다.
'B 프로세스한테 200번이 실제로 몇 번이었지...?' 하고 찾는다.
'아 1200이었구나. 1200에 10을 저장하자'

이 부분은 프로세스가 전혀 모르게 이뤄진다.

운영체제는 수백개의 프로세스에게 하드웨어 자원과 서비스를 제공하기 위해서, 복잡한 공유, 스케줄링, 보호 메커니즘을 가지고 있다. 하지만 각 프로세스는 그걸 몰라도 되도록 정보를 최대한 숨긴다.

이게 바로 OS가 제공하는 '가상화'다. OS는 프로세스에게 '가상화된 프로세서'와 '가상화된 메모리'를 준다.

'가상화된 자원'이 가능한 이유는, 프로세스가 자원을 항상 사용하는 것은 아니기 때문이다. 필요할 때가 정해져 있다. 그래서 이런 시스템이 가능하고 효율적이다.

프로세스의 상태

프로세스는 처음부터 메모리를 할당받는다. 하지만 프로세서는 항상 가질 수 없다.

프로세스는 보통 수백개다. 반면 프로세서(코어)는 전체 컴퓨터에 몇 개 없는 희귀한 몸이다. 각 프로세서는 한번에 하나의 프로세스만 실행할 수 있다.

그래서 특정 시점만 잘라서 확인하면, 수많은 프로세스 중에서 몇 개 정도만 실행 상태다. 나머지는 모두 멈춰서 대기한다.

'어떤 프로세스를 대기시키고 어떤 프로세스를 실행시킬 것이냐'. 이걸 결정하는 게 운영체제의 핵심 임무 중 하나다.

운영체제는 스케줄링을 잘 하기 위해 프로세스에 '상태'를 부여해 구분한다.

준비(Ready), 실행(Running), 대기(Waiting)

먼저, 프로세스가 새로 생겨나면 준비(Ready) 상태가 된다.

커널은 준비 상태인 프로세스 중 하나를 골라서 프로세서를 준다.

이 때 프로세서는 '실행(Running)' 상태다.

프로세서가 프로세스 안에 있는 프로그램 명령어를 하나씩 실행한다. 그런데 디스크에서 abc.txt를 읽어오라는 명령이 있었다. 메모리가 아닌 보조 저장장치에서 정보를 읽어오는 것은 프로세서가 할 수 없다.

따라서 커널에 파일 입출력을 요청한다. 커널이 파일 입출력 작업을 완료해서 데이터를 넘겨줄 때까지는 다음 명령을 실행할 수가 없다. 입출력 작업은 프로세서보다 훨씬 더 느리다. 따라서 이 동안 프로세서는 다른 프로세스를 실행하는 게 효율적이다.

프로세스는 프로세서를 뺏기고 대기(Waiting)하는 상태가 된다. 자고 있는(Sleep) 상태, 멈춘(Block) 상태라고 하기도 한다.

대기 상태의 프로세스가 파일 입출력이 끝났다는 메시지를 받았다. 하지만 이미 프로세서는 다른 프로세스를 열심히 실행 중이다. 당장 실행을 할 수는 없다.

이 프로세스는 이제 '준비(Ready)' 상태로 간다. 실행할 준비가 되었다는 뜻이다.

그러다가 커널이 다시 프로세서를 할당해주면, 실행 상태로 되돌아간다. 아까 abc.txt를 읽어오라는 명령 다음부터 다시 실행하기 시작한다.

프로세서는 매우매우 빠르고, 입출력은 수십배에서 최대 수천만배까지 느리다. 따라서 대부분의 프로세스는 외부 작업을 기다리는 대기 상태에서 시간을 보낸다.

그렇다면 현재 준비 상태인 프로세스 중, 어떤 프로세스에게 프로세서를 줄 것인가?

이것을 결정하는 알고리즘이 CPU 스케줄링 알고리즘이다. 여기에 대해서는 나중 글에서 더 자세히 알아보겠다.

컨텍스트 스위칭

자, 가상화, 프로세스 상태, 스케줄링까지 왔다.

정말 가볍게 설명했지만, 그래도 처음 듣는다면 머리가 아플 수 있다. 마지막 하나 용어만 더 알고 가자.

아까 전 프로세스의 상태가 바뀌는 과정을 살펴봤다. 실행 중이던 프로세스는 대기, 혹은 준비 상태로 이동한다. 그리고 나중에 다시 실행 상태가 된다.

실행을 중간에 멈췄다가 다시 실행하려면, 실행을 멈췄던 지점까지의 정보를 알아야 한다. 즉, 맥락(Context)을 불러와야 한다.

이 맥락을 저장해두는 것은 OS의 역할이다. OS는 고객 명부처럼, 프로세스를 관리하는 프로세스 제어 블록(PCB)를 갖고 있다. 여기에 프로세스의 현재 상태를 저장한다.

따라서 실행 중인 프로세스가 바뀌면, 먼저 실행을 멈추는 프로세스의 현재 정보를 저장해둔다. 새롭게 실행할 프로세스의 정보를 불러온다.

마치 게임에서 저장해뒀던 플레이를 불러오는 것처럼.

이걸 '컨텍스트 스위칭(Context Switching)'이라고 한다. 컨텍스트 스위칭은 그 자체로 상당한 시간이 필요하기 때문에, 가급적 적게 일어나는 게 성능에 좋다.

사람도 너무 많은 걸 짧은 시간에 하려다보면, 일이 잘 안되는 경우가 있다. 컨텍스트 스위칭이 너무 자주 일어나기 때문이다. 코딩을 한 줄 하려다 메일이 오고, 메일 답장 한줄 쓰려다 카톡이 오는 상황을 상상해보자. 이렇게 하는 일을 빠르게 바꾸다보면 일 효율이 매우 떨어진다.

운영체제 입장에서도 컨텍스트 스위칭은 최소화하는 게 좋고, 그러기 위해서 또 다양한 기법들이 나오게 된다.

프로세스 ID

지금 컴퓨터로 이 글을 보고 있다면, 터미널을 켜고 top를 입력해보자.

그러면 이런 화면이 뜬다. 우리가 방금 배웠던 것을 확인해볼 수 있다.

내 맥북 에어에는 무려 589개의 프로세스, 3358개의 스레드가 돌아가고 있다.

그런데 589개 중 2개만 실행 상태다. 내 맥북은 코어가 여러개이기 때문에 2개 프로세스를 병렬 실행할 수 있다. 하지만 길게 보면 빠르게 컨텍스트 스위칭을 하면서 '동시적으로' 수백개의 프로세스를 실행하고 있다는 걸 알 수 있다.

또 여기서, pid라는 숫자를 볼 수 있다.

프로세스 ID (pid)는 OS에서 프로세스를 구분하기 위한 id다. 1부터 시작한다.

pid가 1인 프로세스는 항상 정해져있다. init 프로세스라고 한다. 컴퓨터가 부팅될 때, 커널은 가장 먼저 init 프로세스를 찾아 실행한다. init 프로세스는 pid 1이 된다.

init 프로세스는 부팅 과정에서 중요한 역할을 한다. 주로 설정을 초기화하고, 필요한 기본 프로그램들을 실행시킨다. 시스템 초기화, 웹 서버, 프린트 서버, ssh 서버 같은 서비스 실행, 로그인 프로그램 실행 등이다.

MacOS의 init 프로세스는 lanchd라는 프로그램이다. 리눅스에서는 systemd가 많이 쓰인다.

프로세스 계층

유닉스 계열의 새로운 프로세스는 fork() 라는 시스템 콜로 만들어진다.

#include <unistd.h>

pid_t fork(void);

create가 아니고 fork다. 프로세스를 만들 때 그냥 새로운 걸 만드는 게 아니다. 생성을 요청한 프로세스를 복사해서 만든다. 커널은 fork()를 호출한 프로세스를 복사해서, 다른 프로세스를 만든다.

이 때 원본 프로세스는 '부모'가 된다. 복사된 프로세스는 '자식'이 된다.

모든 자식 프로세스는 부모 프로세스가 있다. 따라서 프로세스는 ‘트리 계층 구조'를 만들게 된다. (부모 프로세스 아이디는 ppid라고 한다.)

linux 시스템에서는 pstree 를 입력하면 이렇게 생긴 프로세스의 트리를 볼 수 있다.

가장 먼저 실행되는 프로세스는 pid 1인 init 프로세스다. init 프로세스는 root 프로세스다. 모든 프로세스의 부모를 따라가보면 init과 만나게 된다.

사용자, 그룹

프로세스에는 pid 뿐 아니라 사용자 id (user id), 그룹 id (group id)라고 하는 고유한 숫자가 부여된다.

이 사용자/그룹 아이디는 매우 중요하다. 프로세스가 시스템 자원에 접근이 가능한지, 권한을 판단하기 때문이다.

특정 프로세스가 password.txt라는 파일을 열 수 있는지 없는지를 결정할 때, 파일에 어떤 사용자와 그룹이 허가되어있는지를 보고, 해당 프로세스의 uid와 gid와 맞춰본다.

uid 0번은 root 사용자다. 루트 사용자는 시스템 내에서 모든 것을 할 수 있는 강력한 권한을 가지고 있다.

(쉘에서 sudo 커맨드를 써본 적이 있겠지? sudo를 사용하면 root 사용자 권한으로 명령을 할 수 있다.)

호텔 관리자의 예를 들자면, 어떤 고객님은 일반 방과 시설에만 접근이 가능하고, 돈을 많이낸 VIP 고객은 별도의 라운지나 룸서비스를 요청할 수 있다. 호텔은 사용자 id라는 별도의 아이디를 부여하고, 고객이 서비스를 요청할 때 먼저 권한이 있는지 확인한다.

그러면 root는 호텔 오너쯤 되겠다. 고객인데 모든 일을 할 수 있으니까.

요약 정리

  • '운영체제라는 관리자'가 운영하는,'컴퓨터라는 호텔'에, '체크인한 고객'이 바로 프로세스다.
  • 소스 코드를 컴파일하면 프로그램이 되고, 프로그램을 커널이 실행하면 프로세스가 된다.
  • 운영체제는 마치 양다리 걸치는 바람둥이처럼, 여러 프로세스에게 자원을 배분하고 공유하지만 그 사실을 프로세스에게는 숨긴다.
  • 운영체제는 프로세서를 스케줄링하기 위해 준비, 실행, 대기 상태로 나눠 프로세스를 관리한다.
  • 프로세서가 실행 중인 프로세스가 바뀌면, 정보를 저장해두고 새로운 정보를 불러오는 '컨텍스트 스위칭'이 일어난다.
  • 프로세스는 ID를 가지고 있으며, fork()를 통해 복사 방식으로 만들어져서 트리 구조를 이룬다.
  • 프로세스는 사용자, 그룹 ID를 가지고 있으며 권한을 결정한다.
profile
개발 지식을 쉽고 재미있게 설명해보자. ▶️ www.youtube.com/@simple-eddy

3개의 댓글

comment-user-thumbnail
2022년 8월 31일

본문과 중요한 사항은 아니지만 한가지 짚고 넘어가자면 이하의 코드에서

#include <stdio.h>

int main()
{
	printf("hello, world\n");
	return 0;
}

 argument list 를 ommit 하는 행위는 그리 권장되지 않습니다. 따라서 이하의 형태가 더 바람직 합니다:

int main(void) { return 0; }
int main(int argc, char *argv[]) { return 0; }

C: A Reference Manual

Standard C permits main to be defined with either zero or two parameters:

int main(void) { ... }
int main() { ... } /* also OK, but not recommended */
int main(int argc, char *argv[]) { ... }

C Programming: A Modern Approach

main()
{
    ...
}

Omitting the word void in main's parameter list remains legal, but-as a matter of style-it's best to be explicit about the fact that main has no parameters.

출처

[Book] C: A Reference Manual 5/e, Samuel P. Harbison III, Guy L. Steele Jr., Prentice Hall, 303pg.
[Book] C Programming: A Modern Approach 2/e, K.N.King, W. W. Norton, 203pg.

1개의 답글
comment-user-thumbnail
2023년 4월 22일

설명이 너무너무 재미있어서 큰 도움이 됐어요!

답글 달기