[System Programming] 5장 프로세스의 생성과 소멸

프로세스의 이해

프로세스란 무엇인가?

프로세스란 실행 중에 있는 프로그램을 의미한다.

예를 들어, 벽돌깨기 게임이 하드에 저장되어 있고, 이 실행 파일 이름을 block.exe라 한다면 이를 프로그램이라 한다.

이제 이를 클릭하면, 메모리 할당이 일어나고, 메모리 공간으로 해당 파일의 바이너리 코드가 올라가게 된다. 이제부턴 이를 프로세스라 한다.

프로세스를 구성하는 요소

Execution of "C" Program

alt text 프로그램이 실행될 때 구성되는 메모리 공간

실행 파일 내에 존재하는 명령어들이 메모리상에 올라가서 순차적으로 실행된다. 이렇게 실행 파일을 구성하는 명령어들이 올라가는 메모리 영역을 Code영역이라 한다.

위와 같은 메모리 공간의 구성을 통틀어 실행 중인 프로세스의 실체라 할 수 있다. 그리고 실행되고 있는 프로세스 개수 만큼 생성된다.

Resister Set

프로세스의 구성하는 요소로 더불어 생각해야할 것은 cpu내의 레지스터이다. 프로그램의 실행을 위해 레지스터는 필수불가결하다.

어떤 프로그램을 실행 중이라면 cpu를 구성하는 레지스터들은 해당 실행을 위한 필요한 데이터들로 채워지게 된다. 따라서 레지스터들의 상태까지도 프로세스의 일부로 포함시킬 수 있다.

  • 프로세스 = 구성되는 메모리 공간 + 프로그램의 데이터로 채워진 레지스터 셋

프로세스의 스케줄링과 상태 변화

프로세스의 스케줄링(Scheduling)

cpu는 하나인데 동시에 여러 프로세스가 실행된다.

이는 cpu는 아주 빠르기 때문에 하나의 cpu가 여러 프로세스를 고속으로 번갈아 가며 실행 시킬 경우, 동시에 여러 개의 프로그램을 실행시킨다고 느껴지게 된다.

이때, 여러 개의 프로세스들이 cpu 할당 시간을 나누게 된다.

스케줄링의 기본 원리

alt text 여러개의 프로세스가 동시에 실행되어야 한다고 가정해보자. 어떠한 기준으로 cpu를 할당해주어야 할까?

이렇게 프로세스의 cpu할당 순서 및 방법을 결정짓는 일을 가리켜 스케줄링이라 한다. 이때 사용되는 알고리즘을 가리켜 스케줄링 알고리즘이라고 한다. 또한 이를 적용시켜 실제로 프로세스를 관리하는 운영체제 요소(모듈)을 가리켜 스케줄러라고 한다.

스케줄러는 어떤 하드한 장치가 아니라 일종의 소프트웨어로써, 운영체제를 구성하는 중요한 요소 중에 하나이다.

멀티 프로세스는 cpu를 바쁘게 한다.

우리가 실행해야 할 프로세스 a,b,c가 있다고 가정해보자. 이 프로세스 들이 실행되는 형태를 두가지로 볼 수 있다.

첫째로 고전적인 방식으로 a를 실행, a가 끝나면 b를 실행하는 식이다.

둘째로 멀티 프로세스 운영체제의 방식으로 a,b,c를 모두 실행시킨 후 스케줄러에 의해 관리되도록 하는 것이다.

일반적으로 프로그램이 실행되는 과정에서 많은 시간이 할당되는 경우는 언제일까? 보통 I/O일 것이다.

비단 파일이나 키보드 등의 입출력 말고도 네트워크 상에서의 송수신 과정도 I/O에 해당된다.

이러한 I/O과정에서, 즉 홈페이지에 접속하여 로딩하는 동안 컴퓨터의 모든 프로세스가 동작을 멈춘다면 시스템 자원적으로 얼마나 ㅛ낭비가 클까?

이러한 측면에서 고전적인 방식보단 멀티 프로세스 기반으로 프로세스를 처리하는 것이 훨씬 효율적일 것이다.

프로세스의 상태 변화

alt text 멀티 프로세스 운영체제에서 프로세스 각각의 상태는 시간흐름에 따라 변화한다.

상황 1 : S(Start) 에서 Ready 상태로의 전이를 보여준다. S는 프로세스의 생성을 의미한다. 생성과 동시에 ready 상태로 돌입하며 언제든지 스케줄러에 의해 cpu로 실행되기를 기다리는 상태이다.

상황 2 : Ready 상태에서 Running 상태로의 전이를 보여준다. ready 상태의 프로세스들은 스케줄러에 의해 관리 되는 프로세스들이다. 스케줄링 알고리즘에 기반하여 running 상태로 끌어와 실행되게 한다.

상황 3 : Running 상태에서 Ready 상태로의 전이를 보여준다. alt text 프로세스는 우선순위의 개념이 존재한다. 즉 프로세스들은 생성 시 중요도에 따라 우선순위가 매겨진다. 예를 들어 A > B인 우선순위를 가진 프로세스가 있을때, B가 실행 중이다. 그런데 갑자기 A가 생성돼 ready 상태로 존재한다면 스케줄러는 A를 running으로 B는 ready로 다시 끌어 내릴 것이다. B는 다시 ready 상태에서 우선순위에 따라 선택되기를 기다리게 된다.

상황4 : Running 상태에서 Blocked 상태로의 전이를 보여준다. alt text 실행 중인 프로세스가 Blocked 상태로 들어가는 것이다. 일반적으로 I/O 작업시 발생하는데, 이러한 작업이 진행중인 프로세스 Blocked로 내리고 ready 상태의 프로세스를 running으로 올리는 것이 cpu를 효율적으로 이용하는 방법이다.

  • I/O작업 중 blocked되었다고 가정했을 때, blocked된 상태에서는 스케줄러의 선택을 받지 못하며, 해당 작업이 끝난 후 ready 상태로 복귀하게 될 것이다.

상황 5 : Blocked 상태에서 Ready 상태로의 전이를 보여준다. blocked 상태는 스케줄러에 의해 선택될 수 없는 상태를 의미한다. ready 상태와 똑같이 현재 cpu에 실행되지는 않지만 분명한 차이점이 있는 것이다. 그러하므로 작업이 끝난 blocked 상태의 프로세스는 ready로 한번 더 넘어가 스케줄러의 선택을 기다리게 될 것이다.

  • 스케줄러의 관심 대상 O -> Ready 프로세스
  • " X -> Blocked 프로세스

컨텍스트 스위칭(Context Switching)

앞에서 멀티프로세스 운영체제의 기본 원리에 대해 알아보았다. 여러개의 프로세스들이 스케줄러에 의해 관리됨을 이해하고 프로그램 실행시간의 대부분을 I/O에 소모하기 때문에 둘 이상의 프로세스 실행은 cpu 활용도를 높여 성능 향상을 도모한다는 결론도 내리게 되었다.

하지만 그와 함께 다른 부하를 가져올 수도 있는데,, "CPU 내에 존재하는 레지스터들은 현재 실행 중인 프로세스 관련 데이터들로 채워진다."

즉 이때, 우리는 한 가지 고민을 하지 않을 수 없다. 실행 중인 프로세스가 변경되면 CPU 내에 존재하는 레지스터들의 값이 변경되기 때문이다.

프로세스 A가 실행 중이다가 ready 상태로 들어간다. 이때 A는 아직 종료되지 않았으니 현재 상태의 레지스터 데이터들을 어디에다 백업? 저장해놔야한다. 이후 다시 running상태가 되었을때 다시 사용해야하니 말이다.

그 뿐이 아니다 지금 당장 B가 running 상태로 들어온다고 치면 어딘가에 백업되어있는 B의 레지스터 셋을 복원시켜 주어야한다. 그래야 멈췄었던 B의 작업이 오롯이 재진행될 수 있는 것이다.

alt text

위의 그림은 현재 A가 실행중인 상황이다. 그렇다보니 현재 레지스터에는 A의 데이터로 채워져있고 B는 ready 상태로 그의 레지스터 데이터는 메모리 어딘가에 저장(백업)되어 있다.

alt text

위 그림의 작업 과정을 가리켜 컨텍스트 스위칭(Context Switching)이라 한다. B의 작업이 이제 실행되기 위해 저장되어 있던 B의 관련 레지스터 데이터를 cpu의 레지스터에 복원시키고있다. 동시에 A의 레지스터 데이터는 저장되고 있다. (마치 스위칭하는 모습이다.)

여기서,

"실행 되는 프로세스의 변경과정에서 발생하는 컨텍스트 스위칭은 시스템에 많은 부담을 준다."

이는 레지스터가 많을수록 데이터 종류가 많을수록 더할 것이다. 이것이 멀티 프로세스 운영체제의 단점이라 말할 수 있다.

프로세스의 생성

기본적인 프로세스의 실행 방법이 무엇일까? 그렇다 실행파일을 더블 클릭해서 실행하는 것이다. 그러나 시스템 프로그래머는 다른 방법을 사용할 수 있다.

"프로그램 실행 중에 또 하나의 프로세스 생성"

CreateProcess 함수의 이해

win에서는 프로새스 생성을 위해 CreateProcess를 제공한다. 이때 해당 함수를 호출하는 프로세스를 가리켜 부모 프로세스라 하고, CreateProcess 함수 호출에 의해 생성된 프로세스를 자식 프로세스라 한다.

즉, 생성하는 프로세스와 생성되는 프로세스는 부모 자식의 관계를 갖는다.

alt text

다음은 CreateProcess의 선언 형태를 보여준다. 몇몇 주요 인자만 살펴보자.

BOOL CreateProcess (
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    ...
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
);

주요 전달인자

  • lpApplicationName: 실행할 파일의 이름. 전체 경로를 포함할 수 있으며, 경로를 지정하지 않으면 현재 작업 디렉터리에서 파일을 찾는다. 이 인자를 NULL로 설정하면, lpCommandLine에서 지정된 파일 이름으로 프로세스를 시작하려고 시도한다.
  • lpCommandLine: 실행할 명령줄. 명령줄은 프로그램의 실행 파일 이름과 인수를 포함한다. 보통 main함수에 인자를 전달해본 기억이 있을것이다. 이처럼 새로 생성하는 프로세스에 인자를 전달할 때 이 두번째 매개변수를 사용한다. lpApplicationName이 NULL일 때는 반드시 명령줄에 실행 파일 이름이 포함되어야 한다. 이 인자를 설정하면, 첫 번째 공백 전까지의 문자열이 실행 파일 이름으로 해석되고, 그 이후는 인수로 전달된다. 그리고 그러한 경우 실행파일의 이름은 __표준 검색경로__를 기준으로 찾게된다.
  • lpStartupInfo: 새 프로세스의 창 크기, 위치, 표준 입출력 핸들 등을 지정하는 STARTUPINFO 구조체의 포인터. 이 구조체는 프로세스가 시작될 때 창 모드나 표준 입출력 핸들 등의 정보를 설정하는 데 사용된다.
  • lpProcessInformation: 새로 생성된 프로세스와 주 스레드에 대한 핸들 및 식별자를 받는 PROCESS_INFORMATION 구조체의 포인터. 함수가 성공하면 이 구조체가 프로세스 정보로 채워진다.

예제를 통한 CreateProcess 함수의 이해

AdderProcess.cpp

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

int _tmain(int argc, TCHAR* argv[]) {
	DWORD val1, val2;
	val1 = _ttoi(argv[1]);
	val2 = _ttoi(argv[2]);

	_tprintf(_T("%d+%d=%d \n"), val1, val2, val1 + val2);
	_gettchar();
	return 0;
}

CreateProcess.cpp

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

#define DIR_LEN MAX_PATH+1

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

	si.cb = sizeof(si);
	si.dwFlags = STARTF_USEPOSITION | STARTF_USESIZE;
	si.dwX = 100;
	si.dwY = 200;
	si.dwXSize = 300;
	si.dwYSize = 300;
    WCHAR title[] = _T("I am a boy!");
	si.lpTitle = title //LPWSTR 형 이기때문에 직접 인자로 넘겨줄 수 없다. 포인터인 title로 넘겨주었다.
	TCHAR command[] = _T("AdderProcess.exe 10 20");
	TCHAR cDir[DIR_LEN];
	BOOL state;

	GetCurrentDirectory(DIR_LEN, cDir);
	_fputts(cDir, stdout);
	_fputts(_T("\n"), stdout);

	SetCurrentDirectory(_T("C:\\WinSystem"));

	GetCurrentDirectory(DIR_LEN, cDir);
	_fputts(cDir, stdout);
	_fputts(_T("\n"), stdout);

	state = CreateProcess(
		NULL,
		command,
		NULL, NULL, TRUE,
		CREATE_NEW_CONSOLE,
		NULL, NULL, &si, &pi
	);

	if (state != 0)
		_fputts(_T("Creation OK! \n"), stdout);
	else
		_fputts(_T("Creation Error! \n"), stdout);

	return 0;
}

아래의 단계에 따라 CreateProcess가 실행될 것이다.

프로세스 생성 1단계 : STARTUPINFO 구조체 변수의 생성 및 초기화 STARTUPINFO si = { 0, }; 으로 초기화후 si 인자에 접근해 인자값들을 초기화해준다. (0으로 꼭 초기화하여 사용한다.)

프로세스 생성 2단계 : 현재 디렉터리의 설정 GetCurrentDirectory(DIR_LEN, cDir); SetCurrentDirectory(_T("C:\WinSystem")); 이후 위의 메소드들이 등장하는데, 터미널 출력 결과를 보면 쉽게 이해가 된다.

GetCurrentDirectory(DIR_LEN, cDir); 을 총 두번 호출했는데, 첫번째는 실행파일의 위치, 두번째 호출할때는 SetCurrentDirectory(_T("C:\WinSystem")); 이 실행된 이후이니 WinSystem 디렉터리가 출력된다. 알맞게 설정된 것이다.

프로세스 생성 2단계 : CreateProcess 함수의 호출 앞서배웠던 인자값들에 따라 매개변수를 할당하여 호출한다. 특히 첫번째, 두번째, 마지막에서 첫번째, 두번째가 중요하다. 마지막 인자값으로 위의 코드에서 미리 선언하였던 &si, &pi를 적절하게 넣어주었다.

추가사항으로 6번째 인자인 CREATE_NEW_CONSOLE, 대신 0을 넣어서 전달해보면 어떻게 될까? 부모 자식 프로세스가 같은 콘솔을 공유해서 새 콘솔이 뜨지 않고 부모 콘솔에 함께 결과가 출력된다!

AdderProcess.exe의 위치는 어디에 있어야 할까?

CreateProcess 함수를 보자. 위의 주요인자에서 설명한것처럼 첫번째 인자로 NULL을 전달하였다. 그러면? 두번째 인자는 꼭 전달해주어야 하며, 이때 표준 검색경로 순서대로 실행파일을 찾게된다.

표준 검색경로 순서 두가지 정도만 보면,

  1. 실행 중인 프로세스의 실행파일이 존재하는 디렉터리
  2. 실행 중인 프로세스의 현재 디렉터리(Current Dir) ..

자 실행파일이 존재하는 디렉터리는? 어디서 실행되냐에 따라 다르겠지만 비쥬얼 스튜디오 기준 빌드파일이 Debug폴더에 생성되고 실행되니,, debug폴더에서 AdderProcess.exe를 찾을것이다.

혹은 그다음으로 C:\WinSystem 이 Current Dir 이기 때문에 해당 디렉터리에서 찾을 것이다!

alt text

출처

윤성우, ⌜뇌를 자극하는 윈도우즈 시스템 프로그래밍⌟, 한빛미디어, 2006
유튜브, "한빛미디어", "뇌를 자극하는 윈도우즈 시스템 프로그래밍", https://www.youtube.com/watch?v=GVX-m3RF-K0&list=PLVsNizTWUw7E2KrfnsyEjTqo-6uKiQoxc Github, "system_programming", https://github.com/connieya/system_programming