프로세스와 스레드
프로세스(Process)는 실행 중인 프로그램을 의미한다. 메모리에는 컴퓨터가 실행되는 순간부터, 여러 프로그램들이 프로세스들로 적재되어 실행된다. 즉, 정적인 프로그램 코드에 실행 상태가 더해져 운영체제에 의해 관리되는 일종의 단위인 것이다.
프로세스는 두 가지 유형으로 크게 나눌 수 있는데, 사용자가 볼 수 있는 공간에서 실행되는지 혹은 그 뒤에서 실행되는지에 따라 나뉜다.
유형 | |
---|---|
포그라운드 프로세스 | 사용자가 보는 공간에서 사용자와 상호 작용 |
백그라운드 프로세스 | 사용자가 보지 못하는 공간에서 실행됨 |
특히 백그라운드 프로세스 에서 사용자와 상호 작용 없이, 그저 주어진 작업만 수행하는 프로세스가 있는데 이를 데몬 (Deamon) 이라고 한다. 윈도우에서는 Service라고 한다.
프로세스 유형에 따라 메모리에 구성되는 정보는 크게 다르지 않다. 대신 커널 영역과 사용자 영역에는 저장되는 차이가 있다.
사용자 영역
프로세스가 사용자 영역에 저장될 경우, 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 나뉜다고 언급했다. 이들을 또 두 가지로 나누면, 정적 할당 영역과 동적 할당 영역으로 나눌 수 있다.
정적 할당 영역
코드 영역과 데이터 영역은 프로그램 실행 도중 크기가 변하지 않는 특징을 갖는다.
-
코드 영역 (텍스트 영역)
-
데이터 영역
- 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간
- 정적 / 전역 변수
데이터 영역과 비슷한 BSS 영역
BSS영역은 초기값이 없는 전역 변수나 Static 변수를 저장하는 공간이다.
일반적으로 데이터 영역에 있는 변수는 이미 값이 있는 상태로 저장되지만 초기에 값이 정해지지 않는 변수는 BSS에 저장된다. 초기에 값이 없는 변수는 프로그램이 시작 될 때 초기화 된다. 쉽게 말해, 데이터 영역은 “미리 써있는 종이”, BSS 영역은 “빈 종이”.
동적 할당 영역
힙 영역, 스택 영역은 프로그램 실행 도중 크기가 지속적으로 변형된다.
-
힙 영역
- 프로그램을 만드는 사용자가 직접 할당 가능한 저장 공간
- 자유롭게 할당하여 사용 가능한 메모리 공간이다.
- 다만 차후에 반환해야 한다. 반환하지 않으면 메모리 누수 문제를 초래한다.
- 가비지 컬렉션을 활용해 메모리 누수 문제를 해결한다
-
스택 영역
- 일시적으로 사용할 값들이 저장되는 공간
- 함수 실행이 끝나면 사라지는 매개변수, 지역변수, 함수 복귀 주소가 여기 포함된다
Stacktrace
스택 영역에는 스택 트레이스 형태의 함수 호출 정보가 저장된다.
특정 시점에 스택 영역에 저장된 함수 호출 정보를 의미하기 때문에, 디버깅에 많이 사용된다.python
의traceback
이 이에 해당한다.
주소 높이 | 메모리 영역 |
---|---|
높음 | Stack |
Heap | |
BSS | |
Data | |
낮음 | Text |
PCB와 문맥 교환하기
운영 체제가 메모리에 적재된 다수의 프로세스를 관리하려면, 식별할 수 있는 정보가 필요하다. 이 정보가 저장된 것이 PCB, Process Control Block, 프로세스 제어 블록이다. PCB는 프로세스와 관련한 다양한 정보를 내포하는 구조체의 일종이다. 새로운 프로세스가 메모리에 적재되었을 때, 커널 영역에 만들어지고, 프로세스의 실행이 끝나면 폐기되는 구조다.
PCB에 담기는 정보는 운영체제 마다 차이가 있다. 프로세스 ID (PID), 프로세스가 실행 과정에서 사용한 레지스터 값, 프로세스의 현재 상태, 어떤 순서로 CPU를 할당 받을지 나타내는 CPU 스케쥴링 (우선순위) 정보, 메모리 상 적재 위치 메모리 관련 정보, 프로세스가 사용한 파일 및 입출력 장치 관련 정보가 명시된다.
여러 PCB들은 커널 내 프로세스 테이블의 형태로 관리되는 경우가 많다. 프로세스 테이블은 실행중인 PCB의 모음을 의미한다. 새롭게 실행되는 프로세스가 있다면, 해당 프로세스의 PCB를 프로세스 테이블에 추가하고, 필요한 자원을 할당하게 된다. 반대로 종료하는 프로세스가 있다면, 사용 중 이던 자원을 해제하고 PCB도 프로세스 테이블에서 삭제된다. 간혹 프로세스가 비정상적으로 종료될 경우, 프로세스 테이블에 종료된 프로세스 PCB가 남아 있는 경우가 있다. 이러한 비정상 종료 상태를 좀비 프로세스라고 한다.
일반적으로 메모리에 적재된 프로세스는 한정된 시간 내에서 번갈아 실행된다. CPU가 구성하는 명령어와 데이터를 인출하여 실행하고, 운영체제가 CPU 자원을 할당하기 때문이다. 즉, 프로세스가 실행된다는 뜻은, 운영체제가 CPU의 자원을 번갈아 할당 받아서 이용한다는 뜻이다.
그렇다면 프로세스가 CPU 자원을 사용할 수 있는 시간은 무제한인가? 그렇지 않다. CPU 사용 시간은 타이머 인터럽트에 의해 제한된다. 타이머 인터럽트란 시간이 끝났음을 알리는 인터럽트로, 타임아웃 인터럽트라고 부르기도 한다. 프로세스는 자신의 차례가 되면 정해진 시간 만큼 CPU를 이용하고, 타이머 인터럽트가 발생하면 자신의 차례를 양보하고 다음 차례를 기다린다.
프로세스 A가 운영체제로부터 CPU를 할당 받아 실행하다가 타이머 인터럽트로 인해 프로세스 B로 CPU 사용을 양보했다고 가정해보자. 프로세스 A는 프로그램 카운터를 비롯한 각종 레지스터 값과 메모리 정보, 파일, 입출력 장치 등 지금까지 모든 정보를 백업해야 한다. 그래야만 차후 프로세스 A의 차례가 왔을 때 재개가 가능하기 때문이다. 이때 저장되는 중간 정보, 백업 정보를 문맥, Context라고 한다. 프로세스의 context는 PCB에 명시된다. 프로세스가 CPU를 사용할 시간이 다 되거나, 인터럽트가 발생 시, 운영체제는 프로세스의 PCB에 문맥을 백업하고 실행할 프로세스의 문맥을 복원한다. 이러한 과정으로 프로세스가 자연스레 Context Switching, 문맥 교환이 이루어지는 것이다.
이론으로만 보면, 프로세스 간 문맥 교환이 빈번히 이루어지더라도 크게 문제될 것이 없어 보인다. 하지만 문맥 교환이 많이 일어난다면, 캐시 메모리에서 캐시 미스가 발생할 가능성이 높아 오버 헤드 문제를 발생할 수 있다.
프로세스의 상태
프로세스는 여러 상태를 거치며 실행된다. 프로세스가 실행되면서 여러 상태가 보여지는데, 대표적으로는 생성, 준비, 실행, 대기, 종료로 상태를 보인다
- 생성 상태
- 프로세스를 생성 중인 상태로, 메모리에 적재되어 PCB를 할당받은 상태.
- 준비 상태
- 당장이라도 CPU를 할당받아 실행이 가능하지만, 차례를 기다리는 상태. 준비 상태인 프로세스가 CPU를 할당받으면 실행 상태로 변경되며 이를 dispatch, 디스패치라고 한다.
- 실행 상태
- CPU를 할당 받아 실행 중인 상태다. 타이머 인터럽트가 발생하여 프로세스가 할당된 시간을 모두 사용 시 준비 상태가 되고, 실행 도중 입출력장치를 사용해 입출력 장치의 작업이 끝날 때 까지 기다려야 한다면 대기 상태가 된다.
- 대기 상태
- 프로세스가 입출력 작업을 요청하거나 바로 확보할 수 없는 자원을 요청 하는 등, 곧장 실행이 불가능한 조건이라면 대기 상태로 된다.
- 종료 상태
- 프로세스가 종료된 상태. pcb와 프로세스가 사용한 메모리를 os가 정리한다.
일반적으로는 프로세스가 실행 도중 입출력 작업을 수행하게 되면, 프로세스는 대기 상태로 들어가고, 입출력 작업이 완료되면 다시 재개한다. 이러한 유형의 입출력 방식은 블로킹 입출력이라고 한다. 하지만 입출력 작업을 수행할 모든 프로세스가 반드시 대기 상태에 접어들 필요가 없다. 때로는 입출력 장치에게 입출력 작업을 맡기고 바로 명령어를 실행하게 할 수도 있다. 이러한 유형을 논 블로킹 입출력이라고 한다.
프로세스와 스레드는 동시에 작업이 가능하도록 각각을 나누어서 처리하는 경우가 있다. 여러가지 프로세스를 열거해서 처리하는 멀티 프로세스와 한 프로세스 안에 여러 코드를 동시에 작업하게 하는 멀티 스레드가 있다.
프로세스 간 통신
프로세스는 기본적으로 자원을 공유하지 않는다. 하지만 프로세스 간에도 자원을 공유하고 데이터를 주는 방법이 있다. 이를 프로세스 간 통신, IPC라고 부른다. 프로세스 간 통신이 이루어지는 방식에는 크게 2가지 유형이 있다. 각각 공유 메모리와 메시지 전달이 있다. 공유 메모리란, 데이터가 주고 받는 프로세스가 공통적으로 사용할 메모리 영역을 두는 방식이고, 메시지 전달은 프로세스 간에 주고 받을 데이터를 메시지의 형태로 주고받는 방식을 의미한다.
공유 메모린란, 프로세스 간에 공유하는 메모리 영역을 토대로 데이터를 주고 받는 통신 방식이다. 프로세스는 기본적으로 자원을 공유하지 않아 다른 프로세스의 메모리 공간을 임의로 수정할 수 없다. 하지만 특별한 메모리 공간을 할당하여 프로세스가 해당 메모리 공간을 공유하고 쓸 수 있게 된다.
이처럼 공유 메모리 기반 IPC는 프로세스가 공유하는 메모리 영역을 확보하는 시스템 콜을 기반으로 수행될 수 있고, 간단하게 프로세스가 공유하는 변수나 파일을 활용할 수 있다. 공유 메모리 기반 IPC의 가장 특별한 특징은, 통신을 주고 받는 각 프로세스가 마치 자신의 메모리 영역을 읽고 쓴다는 것 처럼 통신한다는 것이다. 커널의 개입도 없다. 다시 말해, 프로세스가 주고받는 데이터는 커널 영역을 거치지 않는 경우가 많다.
메시지 전달
메시지 전달은 프로세스 간에 주고 받을 데이터가 커널을 거쳐 송수신 되는 통신 방식이다. 각각의 프로세스가 자신의 메모리 영역 (사용자 영역)을 읽고 쓰는 것으로 통신이 가능한 공유 메모리 기반 IPC와 달리 메시지 전달 기반 IPC는 메시지를 보내는 수단과 받는 수단이 명확히 구분되어 있다. 데이터를 주고 받는 프로세스의 입장에서 메시지 전달 기반 IPC는 공유 메모리 기반 IPC보다 커널의 도움을 적극적으로 받을 수 있으므로, 레이스 컨디션, 동기화 등의 문제를 고려하는 일이 상대적을 적다. 다만, 주고 받는 데이터가 커널을 통해 송수신되므로 공유 메모리 기반 IPC보다 통신 속도는 느리다.
메시지 전달 기반 IPC를 위한 대표적인 방법은 파이프, 시그널, 소켓, 원격 프로시저 호출(RPC) 등이 있다. 파이프는 단방향 프로세스 간 통신 도구다. 먼저 파이프에 삽입된 데이터가 먼저 읽히는 것이다. 필요에 의해 양방향 통신을 하게 되면, 단뱡향 파이프를 2개 사용하는 것이 일반적이다.
파이프의 종류는 두 개가 있는데, 위에서 설명하는 전통적 파이프는 익명 파이프라고 한다. 이 것은 부모 프로세스와 자식 프로세스 만 통신이 가능하다. 반면에 지명 파이프는 양방향 통신을 지원하고, 임의의 프로세스 간에서도 사용이 가능하다.
시그널은 프로세스에게 특정 이벤트가 발생했음을 알리는 비동기적 신호다. 시그널 자체는 IPC만을 위해 존재하는 개념이 아니고, 시그널을 적절이 활용해 IPC를 수행할 수 있다는 것이다.
시그널의 종류는 다양하게 있다. 시그널이 발생시키는 이벤트가 다양하기 때문이다. 대부분 인터럽트와 관련된 이벤트지만, 사용자 정의 시그널 또한 존재한다.
프로세스는 시그널이 발생하면, 여느 인터럽트 처리 과정과 유사하게 하던 일을 잠시 멈추고, 시그널 처리를 위한 시그널 핸들러를 실행한 뒤 실행을 재개한다. 이 때 프로세스는 직접 특정 시그널을 발생시킬 수 있고, 직접 일부 시그널 핸들러를 정의할 수도 있다.
시그널은 IPC 기법들과 다르게 직접적으로 메시지를 주고 받지는 않는다. 하지만 비동기적으로 원하는 동작을 수행할 수 있는 수단이기에 많이 사용한다.
시그널 마다 기본 동작이 정해져 있다. 대부분 프로세스를 종료하거나, 코어 덤프를 생성한다. 코어 덤프는 주로 비정상적으로 종료할 때 생기는 파일로 특정 시점에 작업했던 메모리 상태가 기록되어 있다.
원격 프로시저 호출 (RPC)이나 네트워크 소켓을 통해 IPC를 수행할 수도 있다. RPC는 원격으로 IPC를 실행하는 기술이다. 한 프로세스 내 특정 코드 실행이 로컬 프로시저 호출이라면, 다른 프로세스의 원격 코드 실행이 원격 프로시저 호출인 것이다.
참고자료
※ 이 글은 『이것이 컴퓨터 과학이다』 책을 기반으로, 다양한 자료를 참고해 작성했습니다.