5 분 소요

프로세스

PCB와 문맥 교환

먼저 ‘프로세스’라는 것이 무엇인지부터 살펴보자.

용어 정의
프로세스
(process)
실행 중인 프로그램
포그라운드 프로세스
(foreground process)
사용자가 볼 수 있는 공간에서 실행되는 프로세스
백그라운드 프로세스
(background process)
사용자가 보지 못하는 공간에서 실행되는 프로세스
서비스(service)
/ 데몬(daemon)
사용자와 상호작용하지 않는 백그라운드 프로세스. 각각 Unix/Window에서 쓰는 명칭

우리가 프로그램을 실행하면, 즉 “프로세스를 생성하면” 해당 프로세스에 CPU의 한정된 자원을 할당하기 위해 일종의 번호표가 부여된다. 이후 프로세스의 차례가 돌아오면 정해진 시간, 자원만큼 CPU를 사용하고 시간이 끝나면(=타이머/타임아웃 인터럽트) 다음 차례를 기다린다.

이 번호표가 프로세스 제어 블록(PCB; Process Control Block)인데, 프로세스와 관련된 정보들이 저장된다. 일종의 태그와 비슷하다.

PCB는 프로세스 생성 시에 만들어져 프로세스가 종료되면 폐기된다. 이 PCB는 메모리의 커널 영역에 저장된다.

PCB는 일종의 자료 구조인만큼, 입력되는 데이터들이 있는데 다음과 같다.

PCB 정보 설명
프로세스 ID
(PID; Process ID)
특정 프로세스를 식별하기 위해 부여된 고유번호. 같은 일을 수행하는 프로그램도 두 번 실행되면, 서로 다른 프로세스이므로 PID도 달라짐
레지스터 값 자신의 실행 차례가 왔을 때, 이전에 사용했던 값들을 복원해야함. 해당 프로세스가 사용했던 PC를 비롯한 레지스터 값들이 담김
프로세스 상태 생성(new), 준비(ready), 실행(running), 대기(waiting/blocked), 종료(terminated) 등. 자세한 건 후술
CPU 스케줄링 정보 프로세스가 언제, 어떤 순서로 CPU를 할당받을 지에 대한 정보
메모리 관리 정보 프로세스가 저장된 주소에 대한 정보(base register, limit register 값 등) 및 페이지 테이블 정보
사용한 파일과 입출력장치 목록 프로세스 실행 과정에서 사용된 입출력장치나 파일에 대한 정보

이 PCB에 담긴 정보를 문맥(context)이라고 하는데, 해당 프로세스를 재개하기 위해선 문맥이 PCB에 백업될 필요가 있다.

이처럼 기존 프로세스의 문맥을 PCB에 백업한 후, 새로운 프로세르르 실행하기 위한 문맥을 PCB로부터 복구하여 새로운 프로세스를 실행하는 과정을 문맥 교환(context switching)이라고 한다.

문맥 교환이 잦을수록 프로세스는 그만큼 빨리 번갈아 수행되기에 좋아보이지만, 지나치게 잦으면 오버헤드(추가적인 간접 실행 시간)가 발생할 수 있어 꼭 그런 것은 아니다.

프로세스의 메모리 영역

커널 영역에 PCB가 생성된다면, 그 아래의 사용자 영역엔 프로세스가 어떻게 배치될까?

사용자 영역은 코드 영역(code/text segment. 기계어로 이루어진 명령어가 저장되는 읽기 전용 공간), 데이터 영역(data segment. 전역 변수 저장), 힙 영역(heap segment. 동적 할당 공간), 스택 영역(stack segment. 지역 변수 저장)으로 구성된다. 코드 영역과 데이터 영역은 크기가 고정된 정적 할당 영역인데 반해, 힙 영역과 스택 영역은 크기가 가변적인 동적 할당 영역이다.

스택 영역은 내부 자료구조로 그 스택(stack)을 활용하지만, 힙 영역은 내부 자료구조로 BST의 일종인 그 힙(heap)을 사용하지는 않는다! 힙 영역엔 메모리를 직접 할당하기에, 이를 반환하지 않으면 메모리 누수(memory leak)라는 낭비가 발생한다. 또 힙 영역은 낮은 주소에서 높은 주소로, 스택 영역은 높은 주소에서 낮은 주소로 할당해 두 영역이 섞이는 걸 방지한다.

프로세스 상태(Process State)와 계층 구조(Hierarchical Structure)

앞서 PCB에는 프로세스 상태가 저장된다고 하였는데, 생성(new), 준비(ready), 실행(running), 대기(waiting/blocked), 종료(terminated) 등이 있다. 구체적으로는 다음의 프로세스 상태 다이어그램(process state diagram) 같다.

또 프로세스는 실행 도중 시스템 호출을 통해 새 프로세스를 생성할 수 있는데, 이 경우 각각을 부모 프로세스(parent process)와 자식 프로세스(child process)라 한다. 계층 구조를 기록하기 위해 자식 프로세스의 PCB에는 부모 프로세스의 PID인 PPID(Parent PID)가 저장된다.

우리가 쓰는 모든 프로세스는 최초의 프로세스로부터 시작되는데, 유닉스에선 init, 리눅스에선 systemd, macOS에선 launchd라고 부른다.

부모 프로세스가 자식 프로세스를 만들 때는 fork와 exec라는 시스템 호출을 통해 실행된다. fork는 자기 자신의 복사본을 만드는 프로세스로, 부모 프로세스의 자원들(메모리 내용 등)을 상속하되, PID나 메모리 위치는 부모와 다르다. exec는 새로 만든 프로세스의 메모리 공간을 새로운 프로그램의 내용으로 전환하는 시스템 호출이다. 이해가 된다면 다음 프로세스와 관련한 코드를 이해해보자.

C++로 보는 프로세스

#include <stdio.h>
#include <unistd.h>

int main()
{
   printf("hello, os\n");
   printf("my pid is %d", getpid());
   return 0;
}

/* 실행 결과:
hello, os
my pid is 306*/

다음과 같은 코드에선 getpid() 함수에서 하나의 프로세스에 대한 PID값을 읽어와 출력한다. 이 내용은 유닉스 환경에만 해당되므로 unistd 헤더파일이 필요하다.

#include <stdio.h>
#include <unistd.h>

int main()
{
   printf("parent pid is %d\n", getpid());
   if (fork() == 0) {
       printf("child pid is %d\n", getpid());
   }
   return 0;
}

/* 실행 결과:
parent pid is 2154
parent pid is 2154
child pid is 2155*/

fork() 함수는 자식 프로세스를 포크하는 함수로, 오류가 발생하면 음수를, 자식 프로세스의 경우 0을, 부모 프로세스의 경우 양수를 반환한다. 기본적으로 부모 프로세스가 자식 프로세스보다 먼저 실행되므로, PPID가 찍힌 후 자식 프로세스의 PID가 찍히는 걸 볼 수 있다.

#include <stdio.h>
#include <unistd.h>

int main()
{
   printf("parent pid is %d\n", getpid());
   if (fork() == 0) {
       printf("child pid is %d\n", getpid());
   }
   printf("executed!\n");
   return 0;
}

/* 실행 결과:
parent pid is 3141
executed!
parent pid is 3141
child pid is 3142
executed!*/

이 역시 부모 프로세스를 먼저 거치면서 executed!가 먼저 출력된 후, 자식 프로세스를 거치며 조건문까지 들어갔다가 나오며 다음과 같은 결과가 나온 것이다.

다음은 이해를 도와줬던 영상 링크

스레드

스레드의 이해

스레드(thread)는 프로세스를 구성하는 하위 실행의 단위이다. 즉 한 프로세스에서 여러 개의 스레드를 동시에 실행할 수 있는데, 이 경우를 멀티스레드(multithread), 반대의 경우를 단일 스레드라도 부른다.

스레드라는 개념이 도입되며 하나의 프로세스에서 여러 명령어를 동시에 실행하게 될 수 있게 되었다. (여담으로 여러 프로세스를 동시에 실행하는 것을 멀티프로세스(multiprocess)라 한다.)

여기서 핵심은 프로세스의 스레드들은 실행에 필요한 최소한의 정보(스레드 ID, PC를 포함한 레지스터 값, 스택)을 유지한 채 나머지 코드/데이터/힙 영역 등을 공유한다. 따라서 멀티 프로세스로 병행 실행하는 것보다, 하나의 프로세스에서 멀티스레드로 병행 실행하는 것이 자원과 메모리를 절약할 수 있다. (여담으로 각 프로세스끼리도 메모리 영역을 공유할 수 있는데, 이를 공유 메모리(shared memory)라 하며, 이들 사이에선 프로세스 간 통신(IPC; Inter-Process Communication)이 이뤄진다.)

멀티스레드의 경우 하나의 프로세스에 속하므로 PCB의 PID는 공유하지만, 각 스레드별로 스레드 ID는 다르다. 또한 멀티스레드 환경에서 스레드끼리는 자원을 공유하기에, 하나의 스레드에 문제가 생기면 해당 프로세스 전체가 문제가 생길 수 있다.

C++로 보는 스레드

이번에도 교재에서 추가 학습자료로 준 스레드에 관한 코드를 살펴보자.

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void * foo() {
  printf("process id is %d\n", getpid());
  return NULL;
}

int main()
{
    pthread_t thread1;
    pthread_create(&thread1, NULL, foo, NULL);
    pthread_join(thread1, NULL);
    return 0;
}

/* 실행 결과:
process id is 1370*/

스레드를 실행하기 위해선 pthread 헤더파일도 필요하다. 위의 경우 foo() 함수에서 단일 스레드를 실행하여 PID값을 출력한 경우이다.

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void * foo() {
  long thread_id = (long int) pthread_self();
  printf("process id is %d\n", getpid());
  printf("this is thread %ld\n", thread_id);
  return NULL;
}

int main()
{
    pthread_t thread1;
    pthread_t thread2;
    pthread_t thread3;
    pthread_create(&thread1, NULL, foo, NULL);
    pthread_create(&thread2, NULL, foo, NULL);
    pthread_create(&thread3, NULL, foo, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    return 0;
}

/* 실행 결과:
process id is 726
this is thread 139630074762816
process id is 726
this is thread 139630057977408
process id is 726
this is thread 139630066370112*/

다음은 멀티스레드의 예인데, PID는 동일하지만 thread_id는 다름을 알 수 있다.

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void * foo() {
  printf("foo executed\n");
  return NULL;
}
void * bar() {
  printf("bar executed\n");
  return NULL;
}
void * baz() {
  printf("baz executed\n");
  return NULL;
}

int main()
{
    pthread_t thread1;
    pthread_t thread2;
    pthread_t thread3;
    pthread_create(&thread1, NULL, foo, NULL);
    pthread_create(&thread2, NULL, bar, NULL);
    pthread_create(&thread3, NULL, baz, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    return 0;
}

/* 실행 결과:
bar executed
baz executed
foo executed*/

다음은 각기 다른 프로세스에서 스레드를 하나씩 실행한 것이다. 당연히 이 경우 각 스레드의 PID도, 스레드 ID도 다르다.

아래는 멀티스레딩에 대한 더 자세한 영상인데, 등장하는 문제를 처음 풀어본다면 무조건 틀릴 것이다! ㅎ

출처: [혼자 공부하는 컴퓨터구조+운영체제 Chapter 10]