포스트

[운영체제] 프로세스

프로세스

프로세스 정의

프로세스: 실행 단계인 프로그램

프로세스의 구성요소

프로세스는 3가지의 구성 요소를 가지고 있습니다.

  1. executable program: 실행 가능한 프로그램으로 코드 등이 있습니다.
  2. data: 실행 프로그램이 처리해야 할 대상 정보입니다.
  3. execution context: 컴퓨팅 자원 집합

프로세스와 하드웨어

간략하게 설명하자면, 메인 메모리 커널 안에 dispatcher라는게 존재 해, 이게 실행 능한 프로세스에게 cpu를 배정하고, 프로세스 실행 준비 역할을 담당합니다.

프로그램은 instruction의 sequence로 구성되어져, 프로세스의 상태를 정의하고 이를 적절하게 메모리에 로드해야 합니다.

이때, 버스를 통해서 메인메모리 <-> process끼리 program counter를 이용해 연속적으로 instruction을 메인 메모리로부터 읽어올 수 있습니다.

프로세스의 생성 원리

프로세스가 만들어지는 경우는 총 3개라고 볼 수 있습니다.

  1. 운영체제가 부팅하면서 서비스 프로세스를 생성하고, 커널 안에 다 생성하는게 아닌 일종의 프로그램 형태로 위임하는 식(백그라운드 동작)으로 이를 daemon process라고 합니다.
  2. 이미 있는 프로세스가 자기 자식 프로세스를 만듦. 운영체제에게 요청하여 만들어짐
  3. user command, process 생성 (사람이 만듬)

프로세스의 종료 원인

프로세스의 종료 원인은 총 4개가 있습니다.

  1. 정상 종료 exit()
  2. 부모 프로세스 요청 kill()
  3. 메모리 공간 침범 등 에러 (Error Termination - Fault)
  4. 비정상적 에러.. 입출력 실패.. 정의되지 않은 instruction 등 예외상황에 대한 에러 (Abnormal Termination - Exception)

가 있습니다.

프로세스 상태도

Five-State Process Model

프로세스의 상태는 5가지로 볼 수 있습니다.

  1. New
  2. Ready
  3. Running
  4. Blocked
  5. Terminated

이를 시스템 콜과 연관지어 짚고 넘어가려 합니다.

new state인 프로세스가 있으면, 이를 process list, job queue에 들어가면서 ready state로 바꿉니다.

이런 ready state가 스케줄링에 의해 dispatch당하면, running state로 변환됩니다.

running state에서 주어진 시간을 다 사용했으면, timeout이 발생하여 ready state로 돌아갑니다.

block state는 cpu를 줘도 동작 안하는 상태라고 봐도 무방합니다. (wait이거나, lock이 되어있는 경우)

wait(), sleep() syscall로 진입할 수 있습니다.

Running 상태에서 프로세스가 종료될 수 있는데, 이를 release라고 합니다.

위에서 봤던 것처럼 exit() kill() syscall로 terminated 상태로 진입할 수 있습니다.

terminated(zombie)는 컴퓨팅 자원을 가질 필요는 없지만, 자원 사용량 통계 정보(PCB), 종료된 이유 등의 정보는 필요해서, 프로세스를 남겨 놓습니다.

만약 wait()으로 정보를 수집하면, 완전히 프로세스에서 아웃.

Five-State Process Model 구현

가장 간단한 방법은 레디 큐와 블록 큐 2개 사용하는 것입니다.

블록은 여러 가지의 Event로 인해 wait된 상태입니다.

근데 Event는 유한하므로, Event에 대해서도 큐를 더 쪼갤 수 있습니다.

이를 Multiple Blocked Queue라고 합니다. 공간을 더 쓰지만, 처리 속도가 더 빠릅니다. (시간 복잡도, 공간 복잡도 tradeoff)

Suspended Processes

프로세스는 메인 메모리에 있어야 실행가능합니다.

cpu utilization을 높이기 위해 여러 개의 프로세스를 들여와야 하는데, 계속 프로세스들을 불러오다 보면, 메인 메모리의 공간보다 프로세스들을 더 불러오게 되어 공간에 대한 문제가 생깁니다.

이러한 해결책으로, suspended processes라는 것을 사용합니다.

이는 임시로 메인 메모리에서 보조기억장치로 옮겼다가 다시 로드해 메인 메모리 공간을 절약하는 방법입니다.

스와핑에는 디스크 I/O 오버헤드가 발생할 수 있으나, 보통 Block 상태로 전환될 때 I/O로 인해 wait되는 경우가 많습니다.

cpu가 I/O보다 매우 빠르게 동작하기 때문에, 이렇게 메인 메모리에서 보조기억장치로 옮기는 오버헤드가 크게 느껴지진 않습니다.

이렇게 메인 메모리에서 보조기억장치로 옮기는 행위를 swap이라고 하고,

보조기억장치로 나가는 것을 swap out, 메인 메모리로 들여오는 것을 swap in이라고 합니다.

그렇게 밀려나간 ready를 suspended ready, sleep을 suspended sleep라고 합니다.

suspended sleep은 suspended ready로 전환될 수 있습니다.

process suspension을 하는 이유

process suspension을 하는 이유에 있어서는 여러 가지 원인이 있지만.

공통적으로 메인 메모리 공간을 확보하기 위해서입니다.

  1. swapping
  2. timing (가끔 실행 하는 프로그램)
  3. interactive user request (유저 요청)
  4. parent process request (부모 프로세스 요청)
  5. other OS reason (문제가 suspect 되면)

등의 원인이 있습니다.

Process Control Block(PCB)

OS가 프로그램 실행을 위해 내부 정보에 대해 관리가 필요합니다.

이는 프로세스도 마찬가지기에, 모든 프로세스에 대해 정보를 관리해야합니다.

이때 PCB라는 것을 이용합니다.

PCB의 구성 요소

  1. Identifier (정수이고 불변입니다. 0부터 시작합니다.)
  2. State, Priority
  3. Program Counter
  4. Memory pointers (메모리 포인터)
  5. Address of memory context (메인 메모리 프로세스의 위치 정보)
  6. I/O status information (입출력 장치 정보)
  7. Accounting information (자원 통계 정보)

linux는 PCB가 어떻게 구현되어있는지 간단히 짚고 넘어갑시다.

linux은 process descriptor라는 것을 이용해 계층적으로 PCB가 설계되어 있습니다.

이를 이용해 다른 프로세스의 task product와 공유하기가 쉬워지는 기능이 있습니다.

identifier

자기 자신(process Identifier), 날 만든 프로세스(parent process), User identifier 3개를 저장합니다.

추가로, 프로세스 그룹의 식별자도 저장합니다.

Process State information

time sharing 방식에서 time slice이 발생하면, 다시 프로세스를 재개 시 이에 대한 상태가 저장되어야 하며, 이 말은 register(PC)가 필요하다는 것입니다.

따라서, 상태 값 저장 필드, 레지스터, 중단 시 프로세스 진행상황(상태정보) 등을 PCB에 저장합니다.

Process Control information

PCB는 스케쥴링에 대한 정보도 저장합니다.

대표적으로 process state, priority 등등을 포함합니다.

그 외에는 inter-process communication 프로세스 간 통신에 필요한 정보 저장 (signal, message queue 등), 프로세스 권한(process privilege), 메모리 저장 위치, 컴퓨터 자원 정보 등을 포함합니다.

각 PCB의 관계

**pcb는 부모 - 자식 관계가 가능합니다.

1대N 구조여서, sibiling(형제) 관계도 존재합니다.**

이는 이중 원형 연결 리스트로 구현됩니다.

추가로, 조금 헷갈릴만한 개념인데, Process List와는 다른 개념입니다.

Process List 또한 PCB들의 원형 이중 연결 리스트인데, 이는 생성 순서나 스케줄러 등록 순서 등을 기준으로 연결됩니다.

프로세스 컨텍스트

프로세스 컨텍스트의 정의

프로세스가 실행되는 데 필요한 컴퓨터 내의 정보 및 자원, 구성요소들의 모든 집합

이중 모드 운영

프로세스 컨텍스트를 설계에 따라 다음과 같이 2개로 나눌 수 있다.

  1. User Context, (code, data(전역변수), user stack(로컬 변수, 함수 파라미터, 이 프로세스 전 프로세스의 return address, register values)을 포함한다.)
  2. System Context (User Context와 마찬가지로 code, data, kernel stack이 존재한다.)

용어 정리: kernel stack: 커널 코드 내의 함수를 호출할 때, 필요한 인자 값 레지스터 값 지역변수 값을 저장한다.

kernel stack도 context saving을 진행할 수 있다. (User Context와 유사)

virtual address space

Virtual address space는 code, data(값이 주어짐), bss(값이 주어지지않음), heap, stack으로 구성된다.

다른 부분은 위에서 설명했고, heap에 대해 알아보자.

heap space는 shared memory. library, heap 공간 등이 이용한다.

VAS에서 스택은 위쪽에서 아래쪽 방향으로 성장하고, 힙은 아래쪽에서 위쪽 방향으로 성장한다.

이는 스택과 힙과 반대 방향으로 성장시키면, overflow 감지가 쉬워서이다.

virtual address space의 정보를 PCB에 저장할 수 있다.

이 말을 정확히 정의하면, Page Table을 가리키는 포인터를 PCB에 보관(memory info table)한다는 뜻이다.

ex)

1
2
3
4
5
PCB (task_struct)
  └─ mm_struct *mm         “메모리 정보 테이블”(memory info table)
         └─ pgd_t *pgd     최상위 페이지 디렉터리 (page table root)
               └─        4단계 페이지 테이블(p4dpudpmdpte) 이어짐

VAS에는 일종의 가짜(Reversed) 커널이 들어가있는데, VAS 안에 커널이 사용할 영역을 미리 예약해 둔 부분을 의미한다.

왜 이럴까? Context switching을 효율화 하기 위해서이다. 별도의 페이지 테이블을 전환하지 않고, 같은 주소 공간 내에서 바로 커널 코드를 실행하기 위해서이다.

왜냐면 페이지 테이블을 변환하려면, Page Fault가 발생하는데, 이에 대해서 시간이 많이 소요되기 때문이다.

추가로, 모든 프로세스가 같은 가상의 주소로 커널 함수를 호출하므로, 디버깅, 심볼 해석이 쉬워진다.

이게 TLB caching과 관련된 개념이라.. TLB 엔트리를 재사용할 수 있어 page table을 탐색하지 않고 물리 주소를 얻어 빠르게 실행되는 것이라고 할 수 있다.

간단하게 정리하자면, Context switching을 효율화 하기 위해서라고만 정리하겠다. (추후 자세히 정리하겠다)

VAS와 process state

프로세스가 맨 처음에 보조기억장치에 만들어지는데 바로 이 상태가 new state이다. PCB가 준비되고, VAS도 할당된다.

이 상태를 이제 메인 메모리에 들여오고, Page Table을 설정하고, ready_queue에 들여야 비로소 ready state가 된다.

추가로 ready_queue는 내부적으로 원형 이중 연결 리스트로 구현이 되어있는데,

큐에 집어 넣는 것 같지만 포인터로 연결하면서 큐처럼 동작하는 것이다.

프로세스 control

보안, 안정성을 위해 프로세스 제어 시에 모드를 user mode, kernel mode 2개로 분리한다.

여러 프로세스들이 user mode와 kernel mode를 왔다갔다하며 동작한다. 이를 mode change라고 한다.

대표적으로, fork() 라던지, time slicing 때 dispatcher, context switchingmode change가 발생한다.

이는 CPU가 여러 개인 멀티코어에서도 동일한데, 멀티코어에서도 mode change가 발생한다.

kernel mode 따로, user mode 따로 실행하는게 아니다. 따라서 항상 커널 모드인 processor는 없다.

프로세스 생성

프로세스 생성 과정

프로세서 생성 과정은 다음과 같은 과정을 가진다.

  1. 새로운 프로세스를 위해 PCB 자료구조를 생성한다.
  2. PCB에 고유 pid를 부여한다.
  3. PCB의 값을 채운다.
  4. 프로세스(PCB)를 부모, 형제 이중 연결 리스트에 연결한다.
  5. 다른 자료 구조들을 생성하고 확장한다.
  6. 유저 컨텍스트 생성. 부모 프로세스의 user context를 복사한다.
  7. 생성된 프로세스를 레디 큐에 집어넣는다.
  8. 부모 프로세스에게 자식 프로세스의 PID값을 리턴하고, 자식 프로세스에게 0 값을 리턴한다.

프로세스 생성 정리

대부분 fork()를 이용해 프로세스를 새로 생성한다.

이는 부모 프로세스가 자식 프로세스를 만들어 달라고 OS에게 요청하는 것이다.

사실, fork()말고 clone()을 통해 프로세스를 만들 수도 있고, fork()도 내부적으로는 clone()으로 구현되어있다.

2025-04-21 교수님한테 물어보니깐 clone()으로 구현되어있진 않고, 대신 내부적으로 구조가 비슷하다고는 하셨음.

따라서, 사람은 fork()를 통해 자신이 프로세스를 만드는 것 같지만, 내부적으로는 프로세스가 프로세스를 만드는 것이다.

fork()

fork()를 간단히 설명하면, 부모 프로세스가 자식 프로세스를 OS에게 만들어 달라고 요청하는 것이다, 자식 프로세스의 정보는 부모 프로세스의 PCB를 복사해서 온다.

fork(): 운영체제에게 자식 프로세스를 만들어 달라고 하는 system call이다.

fork()를 통해 부모 프로세스는 자식 프로세스의 PID 값을 가진다.

자식 프로세스는, fork()가 실행되지 않은 것으로 간주하기에 0의 값을 가진다.

fork()는 부모의 PCB 값 뿐만 아니라, text, data+BSS, user stack 등을 전부 복사받는다.

code는 불변이기에 부모 프로세스와 자식 프로세스가 공유할 수 있다.

그런데 이 복사를 최적화할 수 있는 방법이 있을까? 그것은 바로 실제 복사 작업을 수행하는 것을 뒤로 미루는 것이다.

Copy On Write(COW)

COW(copy on write)기법은 프로세스 생성 시간을 줄이고, 메모리 사용 효율 높이기 위해 lazy하게 data, stack 복사를 최대한 늦추는 방법이다.

data와 user stack의 값이 변하지 않는다면, 주소만 공유하는 식, 즉 ptr에 대해 표시만 한다.

만약 값이 변한다면, 복사를 수행하는 것이다.

이러한 작업을 쓸 곳이 마땅치 않아 보이는데,

fork() + exec()를 같이 사용할 때 진가를 발휘한다.

먼저 상황을 가정해보면, 부모와 자식이, 완전히 다른 프로세스 실행한다고 해보자.

fork()로 프로세스를 만들고, exec()로 실제 실행하고 싶은 프로그램을 지정함.

정확한 표현으로, 페이지 테이블 엔트리만 새 프로그램의 이미지로 바꿔주면 되니, exec() 덕분에 페이지 복사 없이 주소 공간 재설정이 가능해져 효율이 극대화되어 연산이 매우 수월해집니다. (장점!)

이전에 fork()가 내부적으로 clone()으로 구성되어 있다고 했는데, 정리하면 다음과 같다.

fork() = clone() + PCB 복제 + COW 메모리 설정으로 이루어진다.

컴퓨터 부팅

PID 에서 0, 1, 2번 PID를 가지는 프로세스들이 뭔지 알기 위해 부팅 개념을 조금은 알아야 한다.

파워를 키면, ROM BIOS(읽기만 할 수 있는 메모리)가 실행된다.

보조 기억 장치 부트 블록, 즉 부트 로더 코드를 읽어서 메인 메모리에 로드한다.

CPU가 그걸 읽고, execute하면, 진짜 os의 코드를 읽어, 이를 메인 메모리에 로드한다.

이제 들어온 OS를 로드하면, 먼저 swapper process(pid 0)을 만든다, 이러한 프로세스는 OS가 수동으로 만든다. 왜냐면 첫 프로세스이기 때문이다.

이제 이것으로 fork() + exec()를 이용하여, init process(pid 1), daemon process(pid 2) 등을 만듭니다!

레디 상태에 있는 프로세스가 없으면, 스케쥴러는 idle task(pid 0)을 프로세스에 배정하고, 이는 CPU 사용량을 최소화 하며 대기합니다.

프로세스 스위치(context switch)

프로세스간 전환이 일어나는 것을 의미한다.

발생하는 시점

프로세스 스위치가 발생하는 시점은 4가지가 있다.

  1. 프로세스의 종료(Termination)
  2. Blocking system call(블록 상태로 가서 기다려야 하는 시스템 콜) (supervisor call)
  3. Time sharing system에서 time slice로 인한 Expiration
  4. I/O interrupt

4번에 대해서, 우선순위가 높은 프로세스가 깨어날 수가 있습니다.

그래서 뺏길 가능성이 존재한다는 것이고, 말 그대로 가능성이므로, context switch가 일어날 수도, 안 일어날 수도 있다.

프로세스 스위치의 과정

프로세스 스위치의 과정입니다.

  1. 중단될 프로세스의 상태를 저장한다. (PC, register, state accounting info 등을 PCB에 저장) (context save)
  2. PCB를 update한다.
  3. PCB를 이후 상태에 따라 적절한 큐(run_queue, event_queue 등)로 옮긴다. 말이 큐로 옮기는 거지 포인터를 지정하는 것이다.
  4. 실행할 다른 프로세스를 선택한다. (Process Scheduling)
  5. 그 프로세스의 PCB를 업데이트한다.
  6. 메모리 관리 데이터 구조를 업데이트한다.
  7. 선택된 프로세스의 상태를 업데이트 한다. (PC, register 등)

프로세스 종료(Termination)

컴퓨터 자원들을 release하는 것을 의미합니다.

사용이 끝났다고 바로 프로세스에서 사라지는게 아닌, Terminated 상태로 남습니다.

kill() 혹은 exit() system call 이 발생했다고 가정해봅시다.

OS는 컴퓨팅 자원을 회수합니다. code, data, stack 등을 전부 회수하는데

단, PCB를 제외하고 회수합니다. 아직은 프로세스이기 때문에..

PCB가 존재하는 위치에 대해서도 알아야 하는데, 메인 메모리에서 커널이 쓰는 메모리에 존재합니다.

위의 말을 정리하면, user에 있는 자료구조에 대해서 전부 날려버린다는 뜻이기도 합니다.

위의 표현을 정리해봅시다.

프로세스가 종료되는 상황

4가지 상황이 존재합니다.

  1. exit() system call
  2. signal을 받음
  3. unrecoverable CPU exception
  4. parent process kill() the child

종료 진행

  • 프로세스 자원은 OS에게 release됩니다(모든 리소스를 회수당합니다).
  • death-of-child signal(SIGCHLD)을 부모 프로세스에게 보낸다.
  • OS는 그 프로세스를 terminated 상태로 배정한다.
  • 실행할 다른 프로세스를 선정한다.
  • 부모 프로세스가 wait()를 부를 때까지 기다린다. wait를 하는 이유는 자식의 output data를 수집하기 위해서이다.
  • 이제 OS는 PCB를 지워 프로세스를 끝낸다.

이런 과정이 있는 경우는 PCB 트리 구조에서 중간 노드가 사라질 경우 중간 노드의 자식 노드들을 처리하기 위해서입니다.

이런 중간 노드의 자식 노드들을 init이라는 특수한 프로세스의 자식 프로세스로 만들어 버립니다.

IPC(Inter-Process Communication)

서비스를 제공하기위해 여러 프로세스가 협업하도록 구현해야 합니다.

이 과정에서 정보를 공유해야 하며, 이 말은 정보를 주고 받을 수 있어야 합니다.

그러나 운영체제의 기능에 대해 알아야 하는데, 운영체제는 프로세스가 다른 프로세스 영역을 침범하는 것을 보호해주기 때문에, 이에 대한 해결책이 필요합니다.

memory passing

메세지 큐를 이용해서, 정보 교환 메세지를 커널에 보내면서 정보를 교환하는 방식이다.

send()receive() system call을 이용하여, 메세지 큐에서 메세지를 하나 송신하고, 수신한다.

장점 : 유저 프로그램이 매우 간단해지고, 구현이 매우 쉽다.

단점 : 커널을 거치므로 아무래도 시간이 소요된다.

커널 모드에 들어가면, 할 일만 하고 나오는 것이 아니라 해야하는 일을 하고 나오느라 오버헤드가 존재해서이다.

오버헤드에 대해서, 잠금(lock)·레디·블록 관리에 따른 추가 지연 (큐 락 경쟁 등)이 있다고 한다.

shared memory

이 또한 system call으로 shared memory 공간을 확보한다.

kernel을 거치진 않는다. mode change가 발생하지 않는.

장점 : 빠름

단점 : 구현 복잡도, 충돌 문제(동기화), 이런 문제를 해결하기 위한 교통 정리 필요.. race condition 고려

IPC의 특징, 이점

정보 공유, 처리속도 상승, 신뢰성, availability(가용성, 프로듀서, 컨슈머 분리, 한 쪽이 느려도 동작한다.) 등이 좋아진다.

하지만 동기화 매커니즘이 필요해, 이를 해결하기 위해 signal, semaphore, lock 등을 이용한다.

이는 나중에 race condition과 연계됩니다.

동기화 용어 정리

signal: 프로세스 2 프로세스에게 event가 발생했음을 알린다. OS를 거친다.

interrupt: 하드웨어 장치들이 event를 os에게 알려서 처리해달라고 요청하는 것이다.

system call: 프로세스가 OS에게 도움을 청하는 것이다.

시그널을 받으면 동작 4개중 하나를 취하도록 되어있다.

  1. 프로세스가 곧 terminated
  2. 프로세스가 곧 stopped
  3. 시그널을 ignored 함. (수신만 하고, 처리 X)
  4. user level catch function - 유저
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.