8 minute read

프로세스

프로세스란?

프로세스(Process)는 실행 중인 프로그램을 의미하며, 단순히 저장된 프로그램(파일)과 구별된다. 운영체제(OS)는 프로그램을 실행하기 위해 프로세스를 생성하고 관리하는데, 이는 CPU, 메모리, 입출력 장치 등 시스템 자원을 효율적으로 사용하기 위함이다.

프로그램이 실행될 때, 운영체제의 로더(Loader)가 프로그램을 메모리에 적재하여 실행을 시작한다. 이때 프로세스는 운영체제의 스케줄링에 따라 실행 상태가 바뀌고, 동시에 여러 개의 프로세스가 실행될 수 있다.

프로세스의 메모리 구조

프로세스가 메모리에 로드되면 일정한 메모리 구조를 가진다. 일반적으로 프로세스의 메모리 구조는 다음과 같이 나뉜다.

  • 스택 영역(Stack Segment)
    • 함수 호출 시 지역 변수, 매개변수, 리턴 주소가 저장되는 영역
    • 함수가 호출될 때마다 새로운 스택 프레임이 생성되며, 함수가 종료되면 해당 프레임이 제거됨
    • LIFO(Last In, First Out) 방식
  • 힙 영역(Heap Segment)
    • 동적 할당된 메모리(예: malloc() or new로 할당된 메모리)
    • 실행 도중 크기가 변할 수 있음
    • 사용이 끝나면 free() or delete로 해제해야함(안 하면 메모리 누수 발생)
  • 데이터 영역(Data Segment)
    • 프로그램의 전역 변수와 정적(static)변수가 저장되는 영역
    • 프로그램 실행 중 변경 가능
  • 텍스트(코드) 영역(Text Segment)
    • 실행할 프로그램의 명령어(코드)가 저장되는 영역
    • CPU가 이 영역의 명령어를 읽고 실행
    • 읽기 전용(Read-Only)로 설정되기도 함

Alt text

프로세스 컨텍스트

프로세스 컨텍스트(Process Context)는 프로세스 문맥이라고도 하며 운영체제가 관리하는 프로세스 정보라고 생각하면 된다. 프로세스 컨텍스트 종류는 다음과 같다.

  • CPU 상태 : CPU 레지스터, 현재 프로세스가 수행되고 있는 위치 등(ex. IP(Instruction Pointer) )
  • PCB(Process Control Block) : 커널이 관리하는 프로세스 정보 구조체
  • 가상주소공간 데이터 : 코드, 데이터, 스택, 힙

멀티태스킹 운영체제에서 실행되는 프로세스는 위와 같은 정보를 토대로 커널이 프로세스를 실행하고 있다는 것을 의미하며, 컨텍스트 스위칭(Context Switching)은 지금 실행하고 있는 프로세스의 실행을 멈추고 다른 프로세스의 컨텍스트를 가져와 실행함을 의미한다.

Alt text

컨텍스트 스위칭은 엄밀하게 말해서 스레드단에서 발생한다. 프로세스는 스레드를 담는 그릇에 불과하며 스레드가 실제 실행 단위에 해당한다. 스레드는 등가의 의미로 테스크(task)라고 불리기도 한다.

PCB(Process Control Block)

PCBProcess Control Block(프로세스 제어 블록)는 운영체제 커널의 자료구조로써 프로세스를 표현하기 위해 사용된다. 커널은 이 자료구조를 사용해서 프로세스를 관리한다. PCB는 프로세스가 생성될 때 같이 생성되며 프로세스 고유의 정보를 포함한다. WIN32 프로세스에서의 PCB 구조는 아래와 같다.

Alt text

PCB는 체인으로 다른 PCB와 연결되어 있다. PCB에 포함된 정보는 다음과 같다.

  • OS가 관리상 사용하는 정보 : 프로세스 상태, 프로세스 ID, 스케줄링 정보, 우선순위
  • CPU 수행 관련 하드웨어 값 : 프로그램 카운터(PC), 레지스터
  • 메모리 정보 : 코드, 데이터, 스택의 위치 정보
  • 파일 정보 : 열어둔 파일 정보(핸들)

PCB의 위치 : PCB가 프로세스의 중요한 정보를 포함하고 있기 때문에, 일반 사용자가 접근하지 못하도록 보호된 메모리 영역 안에 남는다. 일부 운영 체제에서 PCB는 커널 스택의 처음에 위치한다.(이 메모리 영역은 편리하면서도 보호를 받는 위치이기 때문이다.)

프로세스 상태

컨텍스트 스위칭에 의해서 프로세스는 실행 상태에 놓일 수도 있고 정지 상태에 놓일 수도 있다.

Alt text

  • 프로세스 상태
    • 생성(Create) : 프로세스가 생성되는 중
    • 실행(Running) : 프로세스가 CPU를 점유하여 명령어들이 실행되고 있는 상태
    • 준비(Ready) : 프로세스가 CPU를 사용하고 있지는 않지만 언제든지 사용할 수 있는 상태로, CPU가 할당되기를 기다리고 있는 상태. 일반적으로 준비 상태의 프로세스 중 우선순위가 높은 프로세스가 CPU를 할당 받는다.
    • 대기(Waiting) : 보류(block)라고 부르기도 한다. 프로세스가 입출력 완료, 시그널 수신 등 어떤 사건을 기다리고 있는 상태를 말한다.
    • 종료(Terminated) : 프로세스의 실행이 종료된 상태
  • 프로세스의 상태 전이
    • 하나의 프로그램이 실행되면 그 프로그램에 대응되는 프로세스가 생성되어 준비 리스트의 끝에 들어간다. 준비 리스트상의 다른 프로세스들이 CPU를 할당받아 준비 리스트를 떠나면, 그 프로세스는 점차 준비 리스트의 앞으로 나가게 되고 언젠가 CPU를 사용할 수 있게 된다.

    • 디스패치(dispatch) : 준비 리스트의 맨 앞에 있던 프로세스가 CPU를 점유하게 되는 것, 즉 준비 상태에서 실행 상태로 바뀌는 것을 의미한다.

     dispatch (processname) : ready -> running
    
    • 보류(block) : 실행 상태의 프로세스가 허가된 시간을 다 쓰기 전에 입출력 동작을 필요로 하는 경우 프로세스는 CPU를 스스로 반납하고 보류 상태로 넘어 간다.
     block (processname) : running -> blocked
    
    • 깨움(wake up) : 입출력 작업 종료 등 기다리던 사건이 일어났을 때 보류 상태에서 준비 상태로 넘어가는 과정을 깨움이라고 한다.
     wakeup (processname) : blocked -> ready
    
    • 시간제한(timeout) : 운영체제는 프로세스가 프로세서를 계속 독점해서 사용하지 못하게 하기 위해 clock interrupt를 두어서 프로세스가 일정 시간 동안만(시분할 시스템의 time slice) 프로세서를 점유할 수 있게 한다.
     timeout (processname) : running -> ready
    

컨텍스트 스위칭(Context Switching)

CPU가 한 프로세스에서 다른 프로세스의 PCB 정보로 스위칭 되는 과정을 의미한다.

Alt text

위 그림은 두 프로세스가 실행과 중지를 반복하며 컨텍스트 스위칭을 하는 과정을 보여준다. 프로세스A가 실행 중에 있다가 컨텍스트 스위칭이 발생할 경우 운영체제는 프로세스A의 PCB를 저장한 뒤 프로세스B의 PCB를 복원시킨다. 그 후 프로세스의 실행을 개시한다. 이 과정을 반복하면서 두 프로세스는 실행과 중지를 반복한다. 컨텍스트 스위칭은 시스템 콜이나 외부 인터럽트에 의해 발생한다.

스레드

스레드란?

프로세스가 프로그램의 주체라면 스레드는 프로세스의 실제 실행 단위다. 프로세스는 여러 개의 스레드를 담고 있으며 커널은 프로세스가 담고 있는 스레드를 관리해서 프로세스의 동작을 조정한다.

Alt text

위 그림과 같이 스레드는 프로세스의 자원을 공유한다. 코드 영역을 동시에 접근하는 것은 문제가 되지 않지만 쓰기 가능한 데이터 영역에 동시에 접근할 경우 데이터 무결성에 문제가 생길 수 있다. 스택은 스레드 고유의 자원이며 일반적인 경우에는 다른 스레드로부터 간섭을 전혀받지 않기 때문에 동기화 걱정 없이 마음대로 사용할 수 있다.

스레드 경합(Race Condition)

Race Condition(경생 상태)는 여러 개의 스레드 또는 프로세스가 공유 자원에 대해 동시 접근하면서 실행 결과가 실행 순서에 따라 달라지는 문제를 의미한다. 주로 멀티스레드 환경에서 발생하며, 공유 데이터에 대한 동기화가 적절히 이루어지지 않을 때 문제가 발생한다.

 int counter = 0;
 //스레드의 메인 엔트리 함수
 void compute(){
    counter++;
    printf("Counter value: %d\n", counter);
 }

 int main(){
    pthread_t thread1, thread2;
    //스레드를 두 개 생성
    pthread_create(&thread1, NULL, compute, NULL);
    pthread_create(&thread2, NULL, compute, NULL);

    //스레드가 종료할 때까지 대기
    pthread_join(thread1, NULL);
    pthread_join(thread1, NULL);
    return 0;
 }

위 프로그램의 실행 결과를 정확히 예측하는 것은 불가능하다. counter++; 라인이 단위 연산이 아니기 때문이다. 따라서 위 프로그램의 실행 결과는 다음과 같을 수 있다.

 Count value : 1
 Count value : 1
 Count value : 1
 Count value : 2

이런 문제를 해결하기 위해 동기화(Synchronization)를 적용해야 한다. 동기화란 여러 스레드가 공유 자원에 동시에 접근할 때, 예상치 못한 동작을 방지하기 위해 실행 순서를 제어하는 기술이다. 동기화를 적용하기 위해서는 동기화 객체를 사용하면 된다. 동기화 객체의 종류는 다음과 같다.

  • 크리티컬 섹션(Critical Section) : 특정 코드 블록을 하나의 스레드만 실행하도록 보호하는 개념
    • 하나의 프로세스 내에서만 사용 가능
    • 실행 시간이 짧고 간단한 경우 적합
    • Windows에서는 CRITICAL_SECTION, Linux에서는 pthread_mutex를 사용
  • 뮤텍스(Mutex) : 여러 개의 스레드가 공유 자원에 접근할 때, 한 번에 하나의 스레드만 접근하도록 하는 동기화 객체
    • 프로세스 간 공유 가능(크리티컬 섹션과 차이점)
    • 한 스레드가 lock()을 하면 다른 스레드는 대기해야 함
    • std::mutex (C++), pthread_mutex_t (POSIX) 등을 사용
  • 세마포어(Semaphore) : 일정 개수의 스레드만 공유 자원에 접근할 수 있도록 제한하는 동기화 기법
    • 카운팅 세마포어 : 최대 N개의 스레드가 접근 가능
    • 이진 세마포어(=MUTEX) : N=1인 경우, Mutex처럼 동작
  • 스핀 락(Spin lock) : 스레드가 Lock을 획득할 때까지 반복적으로 확인(Busy-wating)하는 기법
    • 일반적으로 Mutex는 Lock이 풀릴 때까지 대기 상태로 전환되지만, Spin Lock은 CPU를 계속 사용하며 Lock을 획득하려고 시도
    • 빠르게 Lock이 풀릴 경우 효과적이지만, 장기간 대기하면 CPU 낭비가 심함
    • std::atomic_flag를 사용하여 구현 가능

TCB

프로세스에 PCB가 존재하듯이 스레드 정보를 관리하기 위해 TCBThread Control Block(스레드 제어 블록)가 존재한다. TCB는 커널에서 스레드를 관리하기 위해 필요로 하는 정보를 담고 있는 구조체이다.

Alt text

KTHREAD가 TCB를 나타낸다.

TCB는 다음 정보를 유지한다.

  • 스레드 식별자 : 고유 아이디는 스레드마다 새롭게 할당된다.
  • 스택 포인터 : 스레드의 스택을 가리킨다.
  • 프로그램 카운터 : 스레드가 현재 실행 중인 명령어의 주소
  • 스레드 상태 : 실행, 준비, 대기, 시작, 완료
  • 레지스터 값들
  • 스레드를 담고 있는 프로세스의 PCB의 포인터

스택

스택stackFILO 자료구조First In Last Out로, 마지막에 입력된 자료가 먼저 나오는 형태다.

콜스택

특정 함수가 호출될 때에는 지역 변수나 함수 파라미터가 특정 공간에 저장되는데 이 공간을 콜스택Call Stack이라고 부른다. x86 아키텍쳐에서는 스택에 변수나 파라미터가 저장될 때 주소 공간이 줄어드는 방향으로 데이터가 저장된다. 다음과 같은 함수가 존재한다고 가정하자.

 void CreateGenesisBlock(DWORD genesisTime, DWORD nounce){
    Timestamp timestamp;
    .......
 }

위와 같은 함수를 호출할 때 콜스택의 상황은 아래와 같다.

Alt text

CreateGenesisBlock 함수의 수행이 끝나면 이 함수를 호출한 실행 코드의 위치로 돌아갈 필요가 있다. 이때 필요한 값은 해당 함수의 복귀 주소와 EBP값이다. 이런 값들은 함수 호출 시마다 자동으로 생성되는데 이러한 값들의 모음을 스택 프레임Stack Frame이라 부른다.

  • ESP 레지스터 : 스택의 밑바닥을 가리키는 포인터다. 최초 함수가 호출될 때 EBPExtended Base PointerESPExtended Stack Pointer의 값은 같으며 로컬 변수가 선언되면 ESP는 낮은 값으로 증가한다.(x86 아키텍쳐의 경우). ESP는 다음 데이터를 Push할 위치가 아니라 Pop을 할 때 뽑아낼 데이터의 위치를 가리킨다.

  • EBP 레지스터 : 스택 프레임의 시작 주소를 가리킨다. 새로운 함수가 호출되면 파라미터와 복귀 주소 값이 스택에 채워지는데 EBP 레지스터값은 바로 그 다음 주소를 가리킨다. 즉 호출된 함수가 로컬 변수를 선언하기 직전의 시작점이 되며 EBP 값은 함수 실행 동안 변하지 않으므로(다른 함수를 호출하지 않는 한) 파라미터나 로컬 변수를 참조할 수 있는 기준점이 된다. EBP 레지스터는 현재 실행 중인 함수가 종료돼 리턴되면 이 함수를 호출한 함수의 EBP값으로 변경된다(스택프레임에 저장된 EBP 값).

호출 규약

호출자Caller가 스택을 정리할 것인지 아니면 피호출자(Callee)가 스택을 정리할 것인지에 따라 호출 규약이 정해진다. 즉 스택을 정리하는 방법과 파라미터를 입력하는 방식을 통츨어 호출 규약Calling Convention이라 부른다. 호출 규약의 종류는 아래와 같다.

Alt text

일반적인 함수 선언 시 호출 규약은 cdecl이나 stdcall을 따른다. thiscall은 클래스 객체의 메서드를 호출할 때 객체 자신을 가리키는 포인터를 파라미터로 전달하기 위해 쓰인다. 함수 호출 규약 방식을 변경하기 위해서는 컴파일 옵션을 변경하면 된다.

  • 컴파일 옵션 : /Gz(stdcall), /Gr(fastcall), /Gd(cdecl)

stdcall은 호출되는 함수가 스택을 정리하기 때문에 얼마나 많은 인자가 들어오는지를 알 수 없다. 반면 cdecl은 호출자가 스택을 정리하기 때문에 가변 매개 변수를 취급할 수 있다. 하지만 호출자가 스택을 정리하므로 함수 호출 시마다 스택을 정리하는 코드가 삽입되므로 코드가 길어진다.

pascall은 이제 거의 쓰이지 않으며 인자를 스택에 넣을 때 왼쪽에서 오른쪽 순으로 인자를 집어넣는다. 모든 호출 규약이 가장 중요하다고 할 수 있지만 반드시 기억해야 할 호출 규약 키워드는 naked다. 이 키워드를 활용해서 함수를 호출할 경우에는 스택 프레임을 형성하지 않으므로 함수 종료 시 우리가 스택을 정리하는 작업을 수행해야 한다. 운영체제 제작을 위해서는 이러한 형태의 함수 호출이 반드시 필요한만큼 익숙해질 필요가 있다.

64비트에서는 더 이상 32비트의 호출 규약 방식을 따르지 않는다. 함수 파라미터의 경우 변수가 몇 개 되지 않으면 스택에 값을 저장하지 않고 레지스터에 값을 유지시킨다. 또한 인라인 어셈블리 언어를 직접 사용할 수 없기 때문에 naked 구문을 사용할 수 없다.

네임 맹글링

네임 맹글링name mangling소스코드가 컴파일되고 오브젝트 파일을 생성한 후 링킹 과정에서 함수나 전역 변수의 이름이 일정한 규칙을 가진채 변경되는 과정을 의미한다. 네임 데커레이션Name Decoration이라 부르기도 한다. 이런 작업이 일어나는 이유는 링커가 다른 범위에 있는 같은 이름의 함수와 변수들을 구별하기 위해서다.

네임 맹글링 시 고려되는 사항은 다음과 같다.

  • 함수 이름
  • 파라미터 타입
  • 호출 규약

C++ 컴파일러는 제조사마다 네임 맹글링 방식이 다를 수 있으며 C 언어로 작성된 dll 파일 등을 C++로 작성된 프로그램에 링크할 때 링크되지 않는 문제가 발생할 수 있다. 이 경우 extern “C” 키워드를 활용하면 네임 맹글링 문제를 해결해서 링커가 함수를 찾지 못하는 문제를 해결할 수 있다.

Reference

  • Computer Systems A Programmer’s Perspecive - Randal E.Bryan
  • 위키백과, 프로세스
  • 위키백과, 프로세스 제어블록
  • 위키백과, 네임 맹글링
  • C++로 나만의 운영체제 만들기 - 박주항

Tags: ,

Categories:

Updated: