뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우)
01. 프로세스의 이해
오늘날의 운영체제를 가리켜서 '멀티 프로세스Multi-Process' 운영체제라고 한다. 이는 프로세스가 여러 개 존재하는 운영체제라는 뜻이다. 프로세스란 무엇일까.
프로세스는 실행 중에 있는 프로그램을 의미한다. 정확히는 메모리 공간으로 바이너리 코드가 올라간 순간부터를 프로세스라고 명한다.
01. A. 프로세스의 구성 요소
메모리 구조는 Data 영역, Stack 영역, Heap 영역, Code 영역으로 나뉘는데, 각자의 역할은 다음과 같다.
영역 명 | 설명 |
Data | 전역 변수나 static 변수의 할당을 담당하는 영역이다. |
Stack | 지역변수 할당과 함수 호출 시 전달되는 인자값들을 저장하는 영역이다. |
Heap | 동적 할당(malloc, calloc 함수에 의한 할당)을 위해 존재한다. |
Code | 실행 파일을 구성하는 명령어들이 올라가는 메모리 영역을 가르킨다. |
참고로 block.exe를 실행 중에 있다면, CPU를 구성하는 레지스터들은 block.exe의 실행을 위해 필요한 데이터들로 채워지게 된다. 이는 실행 중인 프로그램을 기준으로 CPU에 실행 중인 프로그램들의 데이터로 채워진다는 뜻이다. 그렇기에 레지스터들까지 상태도 프로세스의 일부로 포함시켜 말할 수 있다.
02. 프로세스의 스케줄링과 상태 변화
CPU는 하나인데, 어떻게 여러 개의 프로그램이 동시에 실행 가능한가?
기본적으로 CPU는 한 순간에 하나의 프로그램만 실행이 가능하다. 동시에 둘 이상의 프로그램을 실행시킬 수 없다는 뜻이다. 하지만 우리가 보았을 때에는 CPU가 동시에 여러 프로그램을 실행하는 것처럼 보인다. 왜 이럴까?
02. A. 프로세스의 스케줄링
우리가 사용하는 멀티 프로세스 운영체제에서 여러 개의 프로세스가 실행되는 것처럼 보이는 이유는 여러 개의 프로세스들이 CPU 할당 시간을 나누기 때문이다.
프로세스들에게 CPU의 동작 시간을 할당하는 기준은 무엇일까? 가장 쉬운 것은 공평하게 시간을 나눠서 적립하는 것이다. 이렇게 프로세스의 할당 순서와 방법을 결정짓는 일을 가리켜서 스케쥴링이라 말한다. 그리고 이때 사용되는 알고리즘을 스케줄링 알고리즘이라 한다.
또, 스케줄링 알고리즘을 적용해서 실제로 프로세스를 관리하는 운영체제 모듈을 가르켜서 스케줄러Scheduler라 한다. 이는 물리적 장치가 아니라 소프트웨어적으로 구현되어 있는 요소다.
- 멀티 프로세스는 CPU를 바쁘게 한다.
실행해야 하는 프로세스 A, B, C가 있다고 가정하고 프로세스들의 실행 형태를 다음과 같이 나눠보자.
처음은 고전적인 방식으로 A 프로세스를 실행시키고 완전히 종료되면 B 프로세스를 실행시킨다. 그리고 B 프로세스도 완전히 종료되면 C 프로세스를 실행한다. 즉, 실행해야 하는 일을 순차적으로 실행하는 것이다.
두 번째는 동시에 실행되는 형태다. A, B, C 프로세스를 모두 실행시키고 멀티 프로세스 운영 체제의 스케쥴러에 의해서 프로세스들이 관리되도록 한다. 정해진 순서에 의해 CPU의 실행 시간을 나눠서 할당받아 실행하는 형태가 되는 것이다.
▶ 프로그램의 특성에 따라 두 방법은 차이가 날 수 있다.
: 프로그램이 실행되는 과정에서 많은 시간을 I/O(입력 및 출력)에 할당한다. 웹페이지에 접속하기 위해서 주소를 입력한 상황에서 2~3초가 소요된다고 생각하자. CPU는 2~3초의 시간 동안에 아무 것 도 할 수 없는 것이다.
첫 번째 방법으로 진행하게 된다면 CPU는 웹페이지가 열리기 전 까지 다른 일을 하지 못하고 쉬게 된다. 하지만 멀티 프로세스로 진행하게 된다면 다른 작업도 동시에 할 수 있게 된다.
- 프로세스의 상태 변화
멀티 프로세스 운영체제에서는 하나의 프로세스만 실행되는 것이 아니라 여러 개의 프로세스들이 돌아가며 실행되기 때문에 프로세스의 상태들은 시간 흐름에 따라 변화한다.
상황 1 : Start에서 Ready 상태로 전이
프로세스는 생성과 동시에 Ready 상태로 들어간다. Ready 상태의 프로세스는 이미 실행되고 있는 프로세스가 있을 수 있기 때문에 CPU의 실행을 기다리고 있는 상태가 된다. 이후, 스케줄러의 조정에 따라서 실행된다.
상황 2 : Ready 상태에서 Running 상태로 전이
스케줄링 알고리즘에 거쳐서 선별된 Ready 상태의 프로세스를 실행하게 된다면 해당 프로세스는 Running 상태로 변하여 실행된다.
상황 3 : Running 상태에서 Ready 상태로 전이
프로세스의 우선순위에 따라 Running 상태로 진행중인 프로세스를 Ready 상태로 전이할 수 있다.
상황 4 : Running 상태에서 Blocked 상태로 전이
실행 중에 있는 프로세스가 실행을 멈추는 상태로 들어가는 것이다. 데이터 I/O에서 많이 발생한다.
상황 5 : Blocked 상태에서 Ready 상태로 전이
Blocked 상태는 스케줄러에 의해서 선택될 수 없는 상태를 의미한다. Blocked 상태로 전이하게 된 사유가 끝나게 된다면 Ready 상태로 돌아가 스케줄러의 선택을 기다리고 있어야 한다.
▶ Ready 상태와 Blocked 상태의 차이점
: Ready 상태는 스케줄러의 선택을 받을 수 있지만, Blocked 상태는 스케줄러의 선택을 받을 수 없어서 실행이 불가능한 상태를 말한다.
03. 컨텍스트 스위칭Context Swithing
앞서 이야기에서는 멀티 프로세스의 단점을 짚고 넘어간 적이 없었는데, 멀티 프로세스는 장점뿐만 아니라, 단점도 존재한다. 실행 중인 프로세스의 변경은 시스템에 많은 부하를 주는 것인데, 어떤 과정을 통해서 프로세스가 변경되는지 살펴보자.
CPU에 존재하는 레지스터들은 현재 실행 중에 있는 프로세스 관련 데이터로 채워진다.
이 말은 실행 중인 프로세스를 변경하게 된다면, 그에 따라서 레지스터들에 들어 있는 데이터도 변경된다는 말이다. 그렇다고 이전 데이터를 지울 수 없다. 실행 중에 준비 상태로 돌아가게 되는 것이지, 완료된 것이 아니기 때문이다. 이후에 다시 데이터를 꺼내올 일이 생기므로, 데이터를 한 곳에 저장해두어야 한다.
또, 만약에 프로세스를 변경했을 때, 해당 프로세스가 새롭게 생성된 것이 아니라 실행 중에 있던 프로세스라면 어딘가에 프로세스의 데이터들이 저장되어 있을 것이니 그것을 가져와야 한다.
위 사진과 같이 CPU 레지스터 데이터와 메모리에 있는 데이터를 변경하는 작업을 컨텍스트 스위칭이라고 한다. 실행되는 프로세스의 변경과정에서 발생하는 컨텍스트 스위칭은 시스템에 많은 부담을 준다. 레지스터 개수가 많고, 프로세스별로 관리되어야 할 데이터 종류가 많을수록 더하다.
그래서 프로그램 구현에 있어서 컨텍스트 스위칭 부담을 최소화하기 위해 많은 노력을 기울여야 한다. 간과할 경우, 컨텍스트 스위칭이 프로그램 성능 저하를 가져올 수 있기 때문이다.
무엇이 답인지는 구현하는 프로그램의 종류에 따라 달라지기 때문에 다양한 프로그램을 구현하고, 공부하는 것이 좋다.
04. 프로세스의 생성
가장 기본적인 프로세스 생성 방법은 실행 파일을 실행시키는 것이다. 간단한 사칙연산 프로그램을 만들어서 실행해보자.
04. A. CreateProcess 함수의 이해
Window는 프로세스 생성을 돕기 위해서 CreateProcess 함수를 제공한다. CreateProcess 함수를 호출하는 프로세스를 가리켜 부모 프로세스Parent Process라고 하고 함수에 의해 생성된 프로세스를 자식 프로세스Child Process라 한다. 이는 프로세스 간 부모 <-> 자식 관계가 성립된다는 것이다.
BOOL CreateProcess(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
위에는 CreateProcess 함수의 선언 형태다. 상단부터 하단까지 매개변수들에 대해서 알아보자.
매개변수 명 | 설명 |
lpApplicationName | 생성할 프로세스의 실행파일 이름을 인자로 전달한다. 경로명을 추가 지정할 수 있고, 지정하지 않을 경우 현재 디렉터리에서 실행 파일을 찾게 된다. |
lpCommandLine | 새롭게 생성하는 프로세스에 인자를 전달할 때 사용된다. 첫 번째 인자에는 NULL을 전달하고 이 두 번째 인자(lpCommandLine)에는 실행 파일의 이름을 전달할 수 있다. 이때, 실행파일의 이름은 표준 검색경로를 기준으로 찾게된다. |
lpProcessAttributes | 프로세스의 보안 속성을 지정할 때 사용된다. 보통은 NULL을 전달하여 Default 보안 속성을 지정한다. |
lpThreadAttributes | 쓰레드의 보안 속성을 지정할 때 사용된다. NULL을 전달할 경우, Default 보안 속성이 지정된다. |
bInheritHandles | 전달인자가 True인 경우, 생성되는 자식 프로세스는 부모 프로세스가 소유하는 핸들 중 상속 가능한 일부 핸들을 상속한다. |
dwCreationFlags | 생성하는 프로세스의 특성을 결정지을 때 사용된다. 특별한 설정이 필요 없다면 0을 전달한다. |
lpEnvironment | 프로세스마다 환경 블록environment Block이라는 메모리 블록을 관리한다. 이 블록을 통해서 프로세스가 실행에 필요로 하는 문자열을 저장할 수 있다. 이 전달 인자를 통해 환경 블록을 지정한다. NULL이 전달되면, 자식 프로세스는 부모 프로세스의 환경 블록에 저장되어 있는 문자열을 복사한다. |
lpCurrentDirectory | 생성하는 프로세스의 현재 디렉터리를 설정한다. 전달 인자는 디렉터리 정보를 포함하는 완전 경로 형태로 구성되어야 한다. NULL이 전달되면, 부모 프로세스의 현재 디렉터리를 가져온다. 일반적으로 NULL이 전달된다. |
lpStartupInfo | STARTUPINFO 구조체 변수를 초기화 하고 변수의 포인터를 인자로 전달한다. STARTUPINFO 구조체 변수는 생성하는 프로세스의 속성을 지정할 때 사용된다. |
lpProcessInformation | 생성하는 프로세스 정보를 얻기 위해 사용되는 인자다. PROCESS_INFORMAITON 구조체 변수의 주소값을 인자로 전달한다. 전달된 주소값이 가리키는 변수에 프로세스 정보가 채워진다. |
04. B. 예제를 통한 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 = 200;
si.lpTitle = _T("I am a boy!");
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 = CreateProcessA(
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 구조체 변수의 생성 및 초기화
typedef struct _STARTUPINFOW {
DWORD cb; // 구조체 변수의 크기
LPWSTR lpReserved;
LPWSTR lpDesktop;
LPWSTR lpTitle; // 콘솔 윈도우의 타이틀 바 제목
DWORD dwX; // 프로세스 윈도우의 x 좌표
DWORD dwY; // 프로세스 윈도우의 y 좌표
DWORD dwXSize; // 프로세스 윈도우의 가로 길이
DWORD dwYSize; // 프로세스 윈도우의 세로 길이
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags; // 설정된 멤버의 정보
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOW, * LPSTARTUPINFOW;
위 내용은 CreateProcess.cpp에 있는 STARTUPINFO 구조체의 선언이다. 초기화된 멤버에 대해서 어떤 역할을 하는지 주석에 적혀있다.
CreateProcess.cpp에서는 STARTUPINFO 구조체를 통해서 윈도우의 속성을 설정하고 있다. 이후, CreateProcess 메서드를 통해서 설정한 STARTUPINFO 구조체를 전달했다.
- 프로세스 생성 2 단계 : 현재 디렉터리 설정
코드 분석을 뒤로하고 Window에서 제공하고 있는 현재 디렉터리라는 개념을 보자. 이는 특정 파일을 찾을 경우에 기본이 되는 디렉터리를 말한다. 일반적으로 프로세스 생성 시, 현재 디렉터리는 프로세스의 실행 파일이 존재하는 디렉터리로 설정된다. 현재 디렉터리 위치는 다음 함수를 통해 확인이 가능하다.
DWORD GetCurrentDirectory (
DWORD nBufferLength,
LPTSTR lpBuffer
);
위 함수의 두 번째 전달인자 lpBuffer는 현재 디렉터리 정보가 저장된 메모리 버퍼의 포인터고 첫 번째 전달인자는 현재 디렉터리 정보가 저장될 메모리 버퍼의 크기로 바이트 단위 길이 정보가 아니라, 저장 가능한 문자열 길이 정보가 전달되어야 한다.
프로세스의 현재 디렉터리는 아래 함수를 통해서 변경이 가능하다. 전달인자를 통해 변경하고자 하는 현재 디렉터리 경로명을 전달하면 된다.
BOOL SetCurrentDirectory (
LPCTSTR lpPathName
);
- 프로세스 생성 3 단계 : CreateProcess 함수의 호출
CreateProcess 함수 호출 시, 주의해야 하는 것은 두 번째 인자에 Const 형식을 추가할 수 없다는 것이다. 내부적으로 전달된 문자열에 변경을 가하고 호출이 종료될 때, 문자열을 다시 원래 상태로 돌리기 때문에 인식이 어렵다.
하지만 이는 CreateProcess 함수의 구조로 인한 점이니 주의해야 한다. 컴파일 타임에서는 오류가 발생하지 않지만 런타임에서는 메모리 참조 오류가 발생한다. 반드시, 변수 형태로 문자열을 선언해서 CreateProcess 함수의 두 번째 인자로 전달해야 한다.
참고로 첫 인자로 실행파일 이름이 전달되면 현재 디렉터리를 기준으로 실행파일을 찾지만, 두번째 인자로 전달 시 아래의 표준 검색 경로 순서대로 실행파일을 찾게 된다.
1. 표준 검색 경로 : 실행 중인 프로세스의 실행파일이 존재하는 디렉터리
2. 표준 검색 경로 : 실행 중인 프로세스의 현재 디렉터리(Current Directory)
3. 표준 검색 경로 : Windows의 시스템 디렉터리(System Directory)
4. 표준 검색 경로 : Windows 디렉터리(Windows Directory)
5. 표준 검색 경로 : 환경변수 PATH에 의해 지정되어 있는 디렉터리
- 실습 환경 구성 및 실행
실행 방법 1 : 2번 표준 검색 경로 활용
CreateProcess.cpp의 33번째 줄을 보면 현재 디렉터리 위치를 C:\WinSystem으로 변경하고 있다. C 드라이브 아래에 WinSystem이라는 이름의 디렉터리를 만들고 실행파일 AdderProcess.exe를 그 디렉터리 안으로 이동시킨다.
실행 방법 2 : 1번 표준 검색 경로 활용
실행 중인 프로세스의 실행파일이 존재하는 디렉터리다. 따라서 AddProcess.exe와 CreateProcess.exe를 하나의 디렉터리에 같이 넣어두면 어디서든지 실행이 가능하다.
'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
[스터디] 시스템 프로그래밍 - Chapter 7. 프로세스간 통신(IPC) 1 (2) | 2023.10.04 |
---|---|
[스터디] 시스템 프로그래밍 - Chapter 6. 커널 오브젝트와 오브젝트 핸들 (2) | 2023.10.03 |
[스터디] 시스템 프로그래밍 - Chapter 4. 컴퓨터 구조에 대한 두 번째 이야기 (1) | 2023.09.19 |
[스터디] 시스템 프로그래밍 - Chapter 3. 64비트 기반 프로그래밍 (1) | 2023.09.13 |
[스터디] 시스템 프로그래밍 - Chapter 2. 아스키코드 vs 유니코드 (1) | 2023.09.11 |