본문 바로가기

운영체제/공룡책

프로세스

프로세스

 프로세스란 실행 중인 프로그램을 뜻한다. 현대의 컴퓨팅 시스템에서 작업의 단위이다.

운영체제는 커널 안에서 병렬 처리로 여러 프로세스를 동시에 처리한다. 이로 인해 CPU들은 프로세스 중에서 다중화(multiplex)되어진다.

 

프로세스 개념

프로세스의 현재 상태는 프로그램 카운터(PC)와 레지스터의 내용으로 나타낸다.

프로세스 메모리 배치는 다음과 같다.

 

- 텍스트 섹션 : 실행 코드

- 데이터 섹션 : 전역 변수

- 힙 섹션 : 프로그램 실행 중에 동적으로 할당되는 메모리

- 스텍 섹션: 함수가 호출될 때 임시 저장 데이터 저장장소 (함수 매개변수, 복귀 주소 및 로컬 변수)

 

데이터 섹션의 크기는 고정적으로, 프로그램 실행 시간동안 크기가 변하지 않는다.

스택 , 힙 섹션은 프로그램 동적 실행 중에 메모리에 할당되는 크기만큼 유동적으로 변한다.

함수가 호출되어질 때마다, 활성화 레코드(activation record)가 스택에 푸시된다.

 

프로그램과 프로세서의 차이

프로그램 : 명령어 레지스터로 이루어진 집합으로, 흔히 exe(실행파일)이라고 불리며, 수동적인 (passive entity) 존재이다.

프로세서 : 프로그램 카운터와 같이 다음 실행할 명령어들을 지정하는 능동적인(active entity)이다.

 

 

프로세스 상태

가장 잘 알려진, 현재의 프로세스 상태를 정의할 수 있는 다이어그램이다.

 

 

 

new :  프로세스가 생성 중인 상태

running :  명령어들이 실행 중인 상태

waiting : 프로세스가 이벤트가 일어나기를 기다리고 있는 상태

ready : 프로세스가 처리기에 할당되기를 기다리고 있는 상태

teriminated : 프로세스의 실행이 종료됨을 알리는 상태

 

 

 

프로세스 제어 블록 (Process Control Block)

- 프로그램 카운터 : 다음에 실행할 명령어의 주소를 가리킨다

- CPU 레지스터 : accumulator, condition code( 상태 코드), 레지스터들, 스택 레지스터, 인덱스 레지스터 등

 프로그램 카운터와 같이 상태 정보는, 프로세스가 다시 실행되었을 때를 기다렸다 인터럽트 발생 시 저장된다.

- CPU- 스케줄링 : 프로세스 우선순위, 스케줄 큐에 대한 포인터 등 스케줄링 정보를 저장한다.

- 메모리 관리 정보 : 운영체제에 따른 메모리 블록의 한계(limit) 과 운영체제가 사용하는 메모리 시스템에 따라 페이지 테이블 또는 세그먼트 테이블 등과 같은 정보를 포함한다.

- Accounting 정보 : CPU의 실시간 정보, 시간, 프로세스 번호 등을 포함한다.

- I/O : 입출력 정보를 포함한다.

 

 

스레드

멀티 코어 스레드 환경에서는 프로세스의 단일 프로그램을 수행하는 것이 아닌, 프로세스의 개념이 확장된, 프로세스가 다수의 실행 스레드를 가질 수 있도록 허용한다. 스레드를 지원하는 시스템에서 PCB는 각 스레드에 대한 정보를 포함하도록 확장된다.

 

 

프로세스 스케줄링

다중 프로그래밍의 목적은 CPU 자원을 최대한 효율적으로 사용하는 것이다. 따라서 프로세스는 항상 어떤 프로그램을 실행하는 것을 전제로 하여, 각 프로그램이 실행되는 동안 사용자가 상호 작용할 수 있도록 프로세스 사이에서 CPU 코어를 빈번하게 교체하는 것이다. 이 목적을 달성하기 위해서 프로세스 스케줄러는 코어에서 실행 가능한 여러 프로세스 중에서 하나의 프로세스를 선택한다

 

 

스케줄링 큐(Scheduling Queue)

프로세스가 실행에 옮겨지기 위해 준비 큐에 들어간 이후, 실행되기를 기다린다.

 큐는 일반적으로 연결 리스트에 저장되며, 다음 명령어에 대한 PCB를 가리키는 포인터 필드가 같이 포함된다.

Linux의 프로세스

long state; // 프로세스 상태
struct sched_entity se; // 스케줄링 정보
struct task_struc *parent // 프로세스 부모
struct list_head children // 프로세스 자식 정보
struct files_struct *files // 오픈 파일
struct mm_struct *mm // 프로세스의 주소 공간

 

 

CPU 스케줄링

 CPU 스케줄링의 역할은 준비 큐에 있는 프로세스 중 선택된 하나의 프로세스에 CPU 코어를 할당하는 것이다.

앞서 프로세스 스케줄링을 위해, 자주 CPU 스케줄링하는 것이 중요하다. 일반적으로, 스케줄러는 CPU의 코어에서 강제로 제거하기 때문에, 100밀리초마다 한번씩 실행된다. 

  일부 운영체제는 스와핑으로 알려진 중간 형태의 스케줄링을 갖는다. 스와핑은 메모리 상태에서 강제로 CPU 할당을 해제하고 나중에 프로세스에 메모리를 다시 적재할 때, 중단된 위치에서 다시 프로그램이 실행되게 할 수 있다. 프로세스 메모리로 강제로 "스왑 아웃" 하고 이후 디스크에서 다시 메모리에 적재하는 "스왑인" 을 기법을 스와핑이라고 한다.

 

 

Context Switching

 CPU의 스케줄링은 빈번하게 일어나기 때문에, 인터럽트의 발생 처리 후 원래 상태로 복구할 수 있도록, 실행 중이었던 프로세스의 상태를 저장할 필요가 있다. Context Switching은 프로세스의 PCB에 저장된다. Context는 레지스터의 값, 프로세스 상태, 메모리 관리 정보 등을 포함한다. State Save -> State restore 작업을 수행하며, 보관된 정보를 복구하는 Context Switching이 발생한다. 

 Context Switching이 발생하는 도중에는 레지스터의 값, 메모리 정보 등을 복사해야하기 때문에 순수한 오버헤드가 발생한다. 특히, 하드웨어의 성능에 따라 교환 시간은 천차만별이다. 

 

 

 

프로세스에 대한 연산

 

프로세스 생성

UNIX, Linxux 등 운영체제에서 프로세스를 생성하게 될 때, 일반적으로 정수 값을 담은 pid를 생성한다. 

이는 커널에서 프로세스를 구분하기 위해 생성되는 고유 값인 index이다. systemd 프로세스는 1인 index로 생성되며, 순차적으로 프로세스가 생성됨에 따라 부모 트리로부터 프로세스가 트리를 형성한다.

Linux 시스템 보편적인 트리

 

 프로세스는 일반적으로 자식 프로세스를 생성할 때, 새로운 자원을 할당하는 것이 아닌, 자신의 일부 자원을 (CPU 시간, I/ O ,  메모리, 파일) 등을 공유하게 된다. 따라서, 자식 프로세스는 부모 프로세스의 일부분만을 할당받아 사용하기 때문에, 자식 프로세스가 무수히 생성되더라도, 시스템을 과부하 상태로 만드는 프로세스를 방지할 수 있다.

 

 

 프로세스가 새로운 프로세스를 실행할 때, 두가지 방법이 존재한다.

1. 부모가 자식과 병행하게 실행을 계속한다.

2. 부모는 일부 또는 모든 자식이 실행을 종료할 때까지 기다린다.

혹은

1. 자식 프로세스는 부모 프로세스의 복사본이다.

2. 자식 프로세스가 자신에게 적재될 새로운 프로그램을 가지고 있다. 

 

새로운 프로세스를 실행되어졌을 때, 부모 프로세스와는 다른 복귀 코드를 가지게된다. 자식 프로세스 (복귀가 0이 아닌) 식별자가 부모로 복귀되는 반면에, 새로운 프로세스는 '0'으로 복귀된다.

 

다음 코드의 예제를 뜯어보면서, 조금 더 깊게 이해해보도록 하자. 

#include <sys/types.h>
#include <stdio.h>
#inlcude <unistd.h>

int main()
{
pid_t pid;

// 새 프로세스를 생성한다(fork)
pid = fork()l

if(pid <0)
{
	fprintf(stderr, "Fork Failed");
	return 1;
	}

	else if(pid == 0) // 자식 프로세스
	{
	execle("/bin/ls", "ls", NULL);
	}
	else
	{
	// 부모 프로세스가 자식이 완료될 때까지 기다림
	wait(NULL);
	printf("Child_Complete");
	}
	return 0;
}

 

 동일한 프로그램의 복사본을 실행하는 두 개의 서로 다른 프로세스를 갖는다. 

차이점은 자식 프로세스에 보이는 값은 pid = 0, 부모 프로세스 pid > 0 인 것이다.

자식 프로세스는 부모 프로세스로부터 스케줄링 속성을 상속받으며 execlp() 시스템 콜에 의하여, 자신의 주소 공간을 UNIX 명령 /bin/ls 로 덮어쓴다. 이후, 자식 프로세스가 끝나기를 기다리며  자식 프로세스가 종료하는 시점에 exit() 호출하며 시스템 콜을 끝난다.

 

 깊이 있는 공부를 하기 위해서는 Windows API의 프로세스 생성 방식을 따르면, 구조체는 다음과 같다. 

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

void _tmain( int argc, TCHAR *argv[] )
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );

    if( argc != 2 )
    {
        printf("Usage: %s [cmdline]\n", argv[0]);
        return;
    }

    // Start the child process. 
    if( !CreateProcess( NULL,   // No module name (use command line)
        argv[1],        // Command line
        NULL,           // Process handle not inheritable
        NULL,           // Thread handle not inheritable
        FALSE,          // Set handle inheritance to FALSE
        0,              // No creation flags
        NULL,           // Use parent's environment block
        NULL,           // Use parent's starting directory 
        &si,            // Pointer to STARTUPINFO structure
        &pi )           // Pointer to PROCESS_INFORMATION structure
    ) 
    {
        printf( "CreateProcess failed (%d).\n", GetLastError() );
        return;
    }

    // Wait until child process exits.
    WaitForSingleObject( pi.hProcess, INFINITE );

    // Close process and thread handles. 
    CloseHandle( pi.hProcess );
    CloseHandle( pi.hThread );
}

 

 이처럼, CreateProcess가 성공하면 새 프로세스 및 기본 스레드에 대한 핸들 및 식별자가 포함된 PROCESS_INFORMATION 구조를 반환합니다. 스레드 및 프로세스 핸들은 전체 액세스 권한으로 만들어지지만 보안 설명자를 지정하면 액세스를 제한할 수 있습니다. 이러한 핸들이 더 이상 필요하지 않으면 CloseHandle 함수를 사용하여 핸들을 닫습니다.

 

 

 

 프로세스 종료

프로세스의 exit() 시스템 콜이 호출된 이후, 운영체제로부터 프로세스는 자신이 할당받았던 부모 프로세스에 버퍼 값, 물리 메모리와 가상 메모리, 등 모든 자원을 할당 해제하고 운영체제에 반납하게 된다.

 

 일반적으로, 자식 프로세스는 부모 프로세스가 종료되었을 때, 존재할 수 없다. 이는 Cascading termingation 이라고 부르며, 정상적인 종료 상황에서 exit() 같이 직접 호출되거나 간접적으로 호출되어질 수 있다. 하지만, 때로는 모든 자식 프로세스가 exit() 시스템 콜을 호출하였지만, 부모 프로세스는 여전히 wait() 상태에 존재하는 Zombie 프로세스가 되기도 한다.

반면, 갑작스럽게 부모 프로세스가 종료되었을 때, 고아(orphan) 프로세스가 존재하기도 한다. UNIX는 이러한 고아 프로세스를 새로운 부모 프로세스로 init(초기화)함으로서 문제를 해결한다. 이때 , 새롭게 init 프로세스는 고아 프로세스의 종료 상태를 수집하고 프로세스 식별자와 프로세스 데이터 테이블 항목을 반환한다. 

'운영체제 > 공룡책' 카테고리의 다른 글

운영체제 서비스  (0) 2024.09.14
운영체제 구조  (0) 2024.09.09