프로그래밍/General

[운영체제] 프로그램, 프로세스, 스레드 파헤치기

Lou Park 2021. 12. 17. 23:40

프로그램

디스크에 저장되어있는 실행 코드. 

프로그램 목록

 

프로세스

프로그램이 실행되어 메모리에 적재된 상태로, 실행중인 프로그램을 프로세스라고 한다. 프로그램과는 다르게 생명주기를 가진다. Windows에서는 tasklist 명령어를 이용하면 실행중인 프로세스의 PID, 이름, 세션 메모리 사용량을 볼 수 있다. 리눅스에서는 ps 명령어를 이용하면 된다.

 

Windows에서 프로세스 목록 조회

프로세스는 독립된 메모리 영역을 할당 받는다. 고정된 크기의 CODE, DATA, BSS 영역과, 동적인 크기의 HEAP, STACK으로 구성되어있다. 프로세스는 독립된 메모리 영역을 할당받았기 때문에 프로세스 간의 변수나 데이터 공유는 불가능하다. 프로세스간 통신, IPC(Inter-process communication)을 위해서는 별도의 매커니즘이 필요하게 된다.

 


프로세스의 메모리 영역 

https://www.hackerearth.com/practice/notes/memory-layout-of-c-program/

그림에서 위에서부터 STACK, HEAP, DATA(BSS + GVAR), CODE다.

 

STACK

Stack (http://alanclements.org/TEST_StackFrames2.pdf)

HEAP 영역 반대에 위치하며 높은 주소부터 아래로 데이터가 저장된다. 그리고 이름에서 알 수 있듯, 후입 선출(LIFO) 구조로 이루어져있다.  함수를 호출할때마다 STACK 공간에는 지역 변수(local variable)와 매개 변수(argument), 함수가 종료되었을때 반환 주소값이 저장된다.  이렇게 스택 영역에 차례로 저장되는 함수 호출정보를 스택 프레임(Stack frame)이라고 부른다. 스택 프레임은 함수의 호출과 함께 STACK에 PUSH되고, STACK 포인터가 증가하며, 종료하면 POP되며 STACK 포인터가 감소한다. STACK 포인터가 HEAP 영역을 침범하게되면 StackOverflow 오류가 발생하게 되는데, 계속해서 함수를 호출하게되면 이를 손쉽게 발생시킬 수 있다.  

def make_stack_overflow():
    make_stack_overflow()

 

프로그램 내에 동적으로 생성된 객체들은 모두 HEAP에 존재하기 때문에 지역변수로서 할당된 객체는 HEAP에 위치하고, 해당 참조값만 STACK에 저장한다. 반면에 원시타입인 경우는 값 전체가 저장된다. 

public class Main {
	public static void main(String[] args) {
		int age = 19 // stack에 저장
		String name = "Lou" // heap에 저장, stack에는 포인터만 저장
	}
}

위 예제 java 코드에서 Object를 상속하여 만들어진 객체인 String은 HEAP에 저장되고, STACK에는 String name을 가리키는 포인터만 저장된다. 

 

 

HEAP

HEAP은 프로그래머에 의한 동적 메모리 할당이 수행되는 공간이다. C에서 malloc, calloc, realloc, free를 통해 관리되며 Java에서는 new다. STACK처럼 CPU에 의해 타이트하게 관리되고 차곡차곡 쌓이는 반면, HEAP은 매우 넓은 공간에 자유롭게 떠있는(free-floating) 영역에 가깝다. 프로그래머가 할당한 객체들을 제대로 해제 해주지 않으면 메모리 누수(memory leak)를 발생시킬 수 있다. HEAP은 STACK에 비하면 읽고 쓰는데 느린편인데 그 이유는 HEAP의 메모리에 액세스하기 위해 포인터를 사용해야하기 때문이다. HEAP에 있는 데이터는 프로그램의 어디에서든 상관없이 전역적으로(global) 접근할 수 있다. HEAP이 STACK 공간을 침범하는 것을 Heap Overflow라고 한다.

 

public class Main {		
	public static void main(String[] args) {
		// HEAP에 fruits 리스트 할당
		List<String> fruits = new ArrayList<>();

		// HEAP에 String 객체 Apple이 생성됨
		// fruits[0]은 Apple의 주소를 참조 
		fruits.add("Apple");
		// HEAP에 String 객체 Banana가 생성됨
		fruits.add("Banana");

		// Apple의 참조값 대신 Coconut을 참조. 
		// Apple은 더 이상 어느곳에서도 참조하고 있지 않으므로 이후 Garbage Collector에의해 제거됨
		fruits.get(0) = "Coconut"
	}
}

 

 

DATA

DATA 영역은 BSS와 GVAR 영역을 합쳐 일컫는 말이다. BSS 영역은 초기값이 없는 전역변수/static/배열/구조체 등, GVAR은 초기값이 있는 전역변수/static/배열/구조체 등이다. 프로그램이 실행될때 생성되고, 프로그램이 종료되면 반환된다. GVAR은 초기값이 있으므로 ROM에, BSS는 RAM에 저장된다.   

 

CODE

프로그램을 시작할때 컴파일한 기계어 코드가 저장되어있다. 읽기 전용이다.

 


PCB

CPU는 한 번에 하나의 프로세스밖에 실행하지 못한다. 하지만 이렇게 많은 프로세스를 동시에 처리할 수 있는 것처럼 보이는 이유는 CPU가 아주 짧은 시간안에 실행중인 프로세스를 교체하기 때문이다. 이를 문맥교환 (Context switching)이라고 하는데, A 프로세스가 실행중이다가 잠깐 B 프로세스가 앞으로 오고, 다시 A로 돌아왔을때 A는 이전의 작업 상태를 유지해야 CPU가 멀티 태스킹 가능한 것 처럼 보일 것이다. 문맥교환 후에도 이전의 상태를 잃지 않기 위해서 PCB가 사용된다. PCB는 프로세스 제어 블록(Process Control Block)이며 각 프로세스 내에 위치한다. 프로세스 상태 정보를 저장하는 역할을 하고, 프로세스와 생명주기를 함께한다.

 

PCB가 저장하는 정보들

 

PCB는 운영체제(OS)에 의해 관리되는 자료구조로, 일반적으로 다음과 같은 정보들을 담고 있다.

 

Process ID (PID) 

프로세스가 생성되면 0이상의 정수(Integer) 형태의 고유한 프로세스 ID를 할당 받는다. 앞서 본 tasklist 명령어를 실행했을때도 PID를 볼 수 있다.

 

Process State

프로세스의 생명주기에 따른 각 상태들을 저장한다. 생성(New), 대기(Waiting), 실행(Running), 준비(Ready), 종료(Terminated)가 있다.

 

Process priority

프로세스 우선순위이며 정수형태로, 숫자가 낮으면 더 높은 우선순위를 가진다. 이 우선순위는 유저에 의해서나 OS 자체에 의해 결정된다. 우선순위 역시 프로세스 생성시에 함께 할당받게 된다. 

 

Process Accounting Information

프로세스에 의해 사용되는 자원/계정 정보를 담고있다. CPU 사용시간, 연결시간 등이있다.

 

Program Counter

이 프로세스에서 다음에 실행할 명령어의 주소를 가리킨다.

 

CPU Registers

프로세스간의 문맥교체에 의한 인터럽트가 발생할때, 임시 값이나 정보들이 레지스터에 저장된다. 이 덕분에 다음에 이 프로세스가 마저 실행되면 끊어진 시점부터 다시 재개될 수 있다. 여기서는 프로세스에 의해 사용되는 레지스터들이 무엇인지 담고있다. 누산기(ACC), 인덱스 레지스터, 스택 포인터, 범용 목적 레지스터(GPR) 등을 예로들 수 있다.

 

PCB Pointer

준비(Ready)상태에 있는 다음 프로세스의 PCB에 대한 주소를 담고있다.

 

리눅스에서 C로 작성된 PCB 구조체 코드를 보면 다음과 같다. 이외에 정말 많은 정보들을 담고있지만 앞에서 짚어본 녀석들만 가지고와서 보면, PCB는 Doubly Linked List 형태(더 정확히는 Circular doubly linked list)를 띄고 있고 대략 저런 타입을 사용하는구나~정도를 알 수 있었다.

struct task_struct
{
	...
	pid_t pid;
	volatile long state; 
	long counter;
	long priority;
	unsigned long flags;
	struct task_struct *parent;
	struct task_struct *next_task, *prev_task;
	...
}

 


문맥교환의 오버헤드

CPU가 이전 프로세스 상태를 PCB에 저장하고, 다음 프로세스 PCB를 읽고 레지스터에 적재하는 과정이 문맥교환인데,

이러한 문맥교환을 하는 동안에는 CPU가 다른 작업을 할 수 없다. 따라서 이에 소요되는 시간은 오버헤드로 여겨진다.

 

스레드

스레드는 프로세스 내에서 실행되는 흐름의 단위를 말하며 한 프로세스는 최소 하나의 스레드(Thread)를 가진다. 스레드를 여러개 동시에 실행할 수도 있는데, 이를 멀티 스레드(Multithread)라고 한다. 스레드는 프로세스 내에서 CODE, DATA, HEAP 영역을 공유하여 사용하고, STACK 영역만 따로 할당받는다. 따라서 문맥교환시에도 스레드는 STACK 영역만 처리하면 되기 때문에 프로세스간 문맥교환보다 훨씬 오버헤드가 적다.  

 

https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html

 


 

참고자료

https://binaryterms.com/process-control-block-pcb.html

http://www.cs.fsu.edu/~zwang/files/cop4610/Fall2016/chapter3.pdf

https://ko.wikipedia.org/wiki/%EC%8A%A4%EB%A0%88%EB%93%9C_(%EC%BB%B4%ED%93%A8%ED%8C%85) 

https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html

https://blog.naver.com/PostView.nhn?blogId=cjsksk3113&logNo=222270185816 

https://gribblelab.org/teaching/CBootCamp/7_Memory_Stack_vs_Heap.html

https://kyu9341.github.io/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C/2020/10/04/OS_Process_Structure/