2025. 3. 15. 17:50ㆍ개발/Windows
이 글은 윤성우 님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.
프로세스의 구성요소는 무엇일까요. 프로그램이 메모리에 할당되니, 메모리는 구성요소에 포함될 것입니다. 그리고 책에서는 레지스터도 프로그램의 구성요소로 취급합니다. 왜냐하면 프로세스를 실행하기 위해서는 레지스터의 값을 저장하고 불러와야 할 수 있기 때문입니다. 우선 프로세스의 메모리 구조를 확인해 보겠습니다.
Process memory structure
Low Address |
... |
CODE |
DATA |
BSS |
HEAP |
Unused Memory |
STACK |
... |
High Address |
- CODE: 실행된 프로그램의 코드가 저장되는 공간, 함수와 메서드가 여기에 존재하며, 읽기 전용으로 컴파일 타임에 영역의 크기가 정해집니다.
- DATA: 초기화된 상수/전역/정적변수들이 저장되는 공간입니다. 상수는 컴파일러 따라 지역변수인 경우 STACK에 포함될 수도 있습니다. 그리고 컴파일 타임에 크기가 정해집니다.
- BSS: 초기화되지 않은 전역/정적변수들이 저장되는 공간입니다. 초기화되지 않았기 때문에 컴파일 타임에 크기가 정해지지 않고 런타임에 0으로 초기화되며 크기가 정해집니다.
- HEAP: 메모리의 동적할당을 통해 사용되는 영역, 런타임에 크기가 결정됩니다. 참고로 Heap 자료구조와 상관없으며 쌓여있는 더미란 뜻의 Heap으로 사용됩니다.
- STACK: 함수 혹은 메서드 호출 시 매개변수, 반환주소, 지역변수들을 저장하는 공간입니다. Stack 자료구조를 통해 함수가 종료되면 호출하였던 이전 상태로 돌아올 수 있습니다. STACK은 컴파일 타임에 스택 프레임 크기를 결정하지만 운영체제에 따라 실제 할당되는 STACK 크기는 런타임에 결정됩니다.
이후 메모리에 있는 프로세스의 데이터가 Fetch 되고 Decode, Excute로 CPU가 프로세스를 실행할 것입니다. 이후 프로세스가 종료될 때까지 명령주기는 계속될 것입니다. 하지만 중간에 프로세스는 IO작업을 하거나 우선순위에 밀려 잠시 중단될 수 있습니다. 이때 중단하기 전 CPU가 사용 중인 레지스터 상태를 저장하여 다음 실행에 다시 사용할 수 있게 처리해야 할 것입니다. 이때 사용되는 것이 PCB(Process Control Block)입니다.
PCB, Process Control Block
Process ID |
Process State |
Process Priority |
Accounting Information |
Program Counter |
CPU Registers |
CPU Scheduling Information |
Memory Management Information |
I/O Status Information |
... |
PCB는 운영체제가 프로세스를 제어하는데 필요한 모든 정보를 저장하는 데이터 구조입니다. 위 정보 이외도 프로세스의 현재 디렉터리, 프로세스의 열린 파일들 등 많은 정보들이 저장됩니다. 이 구조는 운영체제 별로 상이할 수 있으며, 중요한 것은 운영체제는 프로세스를 중단하고 실행시키는데 PCB를 사용하여 정보를 저장한다는 것입니다. 그리고 이 PCB들은 커널 공간에 저장됩니다.
프로세스는 왜 중단되고 실행되는 것일까요. 그것은 운영체제에서 멀티태스킹을 지원하기 때문입니다. 여러 개의 프로세스를 동시에 처리하기 위해 각각의 프로세스는 아주 짧은 시간씩 실행되고 시간이 모두 소모되면, 다시 중단되어 다음 프로세스를 실행합니다. 그럼 운영체제는 실행되는 프로세스를 어떻게 선정할까요.
Process Scheduling/Process Lifecycle
운영체제는 프로세스의 실행을 스케줄링을 통해 관리합니다. 스케줄링의 방법은 여러 가지가 있지만 책에서는 우선순위 방식을 설명합니다. 프로세스의 PCB에서는 Process Priority가 존재하며 이 값이 높을수록 실행에 대한 우선권을 가집니다. 낮은 Priority를 가진 프로세스는 실행 중이라도 높은 프로세스가 새로 실행되면, 낮은 프로세스가 중단되며 새로 실행된 높은 프로세스가 실행됩니다.
이런 우선순위 스케줄링은 중요 프로세스의 실행을 선점할 수 있다는 장점이 있지만, 높은 우선순위를 가진 프로세스가 계속 실행될 경우 낮은 우선순위의 프로세스는 실행이 되지 않는 기아현상(Starvation)을 발생시킬 수 있습니다. Windows에서는 이 문제를 회피하기 위해 Aging 기법을 사용하였습니다. 낮은 우선순위 프로세스라도 시간이 지나면 점진적으로 우선순위가 높아져 시간이 지날수록 높은 우선순위를 가지게 됩니다. 물론 실행된 후 다시 원래의 낮은 우선순위를 가지게 됩니다.
이제 스케줄러에 의한 프로세스의 상태변화를 알아보겠습니다.
- Start/New -> Ready (Admit): 프로세스가 생성되어 실행되기 전 준비상태가 됩니다. 이때부터 스케줄링에 의해 프로세스가 관리됩니다.
- Ready -> Running (Dispatch): 프로세스가 스케줄러에 의해 CPU를 할당받아 실행된 상태입니다.
- Running -> Termination (Release): 프로세스가 모두 실행되어 혹은 강제로 종료된 상태입니다.
- Running -> Ready (Pause): 정해진 시간 동안 실행되고 중단되어 다시 준비상태가 됩니다.
- Running -> Waiting (Event Wait, Blocked): 이벤트 발생을 기다리는 대기상태가 됩니다. 주로 I/O작업에 대한 대기상태입니다. 이때 Waiting은 스케줄링에 영향을 받지 않습니다.
- Waiting -> Ready (Event Completion): 이벤트가 발생하여 다시 준비상태가 됩니다.
프로세스들은 위 생명주기를 반복하며 실행과 중단 사이에 PCB를 사용하여 프로세스의 정보를 저장하고 다시 사용합니다. 이런 작업을 컨텍스트 스위칭(Context Switching)이라 부릅니다.
Context Switching
컨텍스트 스위칭은 프로세스가 사용 중인 CPU를 다른 프로세스가 사용하도록 이전 프로세스의 상태를 PCB 보관하고 새로운 프로세스의 상태를 PCB에서 불러오는 과정을 말합니다. 아래 그림은 컨텍스트 스위칭의 흐름을 간략히 표현하였습니다.
프로세스 간의 컨텍스트 스위칭은 커널 메모리에 상태를 불러오고 저장하기 때문에 오버헤드가 큰 작업입니다. 그래서 멀티 프로세스의 작업은 오히려 반대로 성능의 저하를 가져올 수 있습니다. 하지만 I/O 작업이 많은 경우 멀티 프로세스는 성능향상에 큰 도움을 줄 것입니다. 그렇기에 개발자는 다양한 분야를 공부하며 해당 작업에 대한 처리방법을 고민해야 합니다.
CreateProcess
Windows에서 새로운 프로세스를 생성하는 함수입니다. 특이한 점은 새로운 프로세스를 생성하며 실행파일을 지정해야 한다는 것입니다. 우선 CreateProcess의 선언을 알아보겠습니다.
WINBASEAPI
BOOL
WINAPI
CreateProcessA(
_In_opt_ LPCSTR lpApplicationName,
_Inout_opt_ LPSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOA lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
WINBASEAPI
BOOL
WINAPI
CreateProcessW(
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
#ifdef UNICODE
#define CreateProcess CreateProcessW
#else
#define CreateProcess CreateProcessA
#endif // !UNICODE
ASCII와 UNICODE를 모두 지원하는 조건부 컴파일이 되어 있는 것을 확인할 수 있습니다. 그리고 인자들에 대해 설명하겠습니다.
- lpApplicationName: 프로세스의 실행파일 경로입니다.
- lpCommandLine: 프로세스에 전달될 매개변수(argc, argv)입니다.
- lpProcessAttributes: 프로세스의 보안 속성, NULL을 넣으면 기본 보안 속성이 지정됩니다.
- lpThreadAttributes: 스레드의 보안 속성, 동일하게 NULL을 넣으면 기본 보안 속성이 지정됩니다.
- bInheritHandles: 핸들 상속 여부, TRUE인 경우 생성할 프로세스에게 핸들 일부를 상속합니다.
- dwCreationFlags: 프로세스 생성 옵션, 우선순위를 결정지을 때 사용됩니다. 0을 넣으면 옵션을 사용하지 않습니다.
- lpEnvironment: 환경변수 블록, NULL을 넣으면 현재 프로세스의 환경변수를 생성할 프로세스에 복사합니다.
- lpCurrentDirectory: 프로세스의 현재 디렉터리, NULL을 넣으면 현재 프로세스의 현재 디렉터리를 복사합니다.
- lpStartupInfo: 프로세스의 속성, STARTUPINFO 구조체를 사용하여 생성할 프로세스의 속성을 지정할 수 있습니다.
- lpProcessInformation: 생성한 프로세스 정보, PROCESS_INFORMATION 구조체의 주소를 인자로 전달하면, 전달된 구조체에 프로세스의 정보가 채워집니다.
그리고 인자로 사용되는 구조체 STARTUPINFO, PROCESS_INFORMATION를 알아보겠습니다. 먼저 STARTUPINFO입니다.
typedef struct _STARTUPINFOA {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
typedef struct _STARTUPINFOW {
DWORD cb;
LPWSTR lpReserved;
LPWSTR lpDesktop;
LPWSTR lpTitle;
DWORD dwX;
DWORD dwY;
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;
#ifdef UNICODE
typedef STARTUPINFOW STARTUPINFO;
typedef LPSTARTUPINFOW LPSTARTUPINFO;
#else
typedef STARTUPINFOA STARTUPINFO;
typedef LPSTARTUPINFOA LPSTARTUPINFO;
#endif // UNICODE
- cb: 구조체 크기입니다. sizeof(STARTUPINFO))
- lpReserved: 예약입니다. 반드시 NULL이어야 합니다.
- lpDesktop: 데스크톱 환경 지정, 바탕 화면의 이름 또는 이 프로세스에 대한 데스크톱 및 창의 이름입니다.
- lpTitle: 콘솔인 경우 창 제목, NULL인 경우 실행파일 이름이 됩니다.
- dwX: 프로세스 윈도우 창의 X 좌표입니다.
- dwY: 프로세스 윈도우 창의 Y 좌표입니다.
- dwXSize: 프로세스 윈도우 창의 너비입니다.
- dwYSize: 프로세스 윈도우 창의 높이입니다.
- dwXCountChars: dwFlags에 STARTF_USECOUNTCHARS를 지정한 경우 콘솔 창의 가로 문자 수입니다.
- dwYCountChars: dwFlags에 STARTF_USECOUNTCHARS를 지정한 경우 콘솔 창의 세로 문자 수입니다.
- dwFillAttribute: dwFlags STARTF_USEFILLATTRIBUTE 지정한 경우 콘솔 창의 텍스트 색상 및 속성입니다.
- dwFlags: 창 스타일 플래그, 프로세스가 창을 만들 때 특정 STARTUPINFO 멤버가 사용되는지 여부를 결정하는 비트 필드입니다.
- wShowWindow: dwFlags STARTF_USESHOWWINDOW 지정한 경우 창 표시 상태를 지정합니다.
- cbReserved2: C 런타임에서 사용하는 예약입니다. 반드시 0이어야 합니다.
- lpReserved2: C 런타임에서 사용하는 예약입니다. 반드시 NULL이어야 합니다.
- hStdInput: dwFlags STARTF_USESTDHANDLES 지정한 경우 표준 입력 핸들이 됩니다.
- hStdOutput: dwFlags STARTF_USESTDHANDLES 지정한 경우 표준 출력 핸들이 됩니다.
- hStdError: dwFlags STARTF_USESTDHANDLES 지정한 경우 표준 오류 핸들이 됩니다.
멤버가 많은데 dwFlags를 사용하지 않으면 주로 사용하는 멤버는 dwX, dwY, dwXSize, dwYSize입니다. 이제 다음으로 PROCESS_INFORMATION을 알아보겠습니다.
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
- hProcess: 생성한 프로세스의 핸들입니다.
- hThread: 생성한 프로세스의 기본 스레드 핸들입니다.
- dwProcessId: 새 프로세스의 ID입니다.
- dwThreadId: 새 프로세스의 기본 스레드 ID입니다.
CreateProcess가 정상적으로 프로세스를 생성하면 멤버들이 채워집니다. 이 멤버를 통해 프로세스와 스레드를 제어할 수 있습니다. 마지막으로 CreateProcess 예제를 작성해 보겠습니다.
#include <windows.h>
#include <iostream>
int main() {
WCHAR cmd[] = TEXT("notepad.exe");
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
if (CreateProcess(
NULL,
cmd,
NULL, NULL, FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si, &pi)) {
std::cout << "Success create process! PID: " << pi.dwProcessId << "\n";
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
else {
std::cerr << "Failed create process! error code: " << GetLastError() << "\n";
}
return 0;
}
메모장을 CreateProcess로 실행해 보는 예제입니다. WaitForSingleObject는 생성한 프로세스에서 종료신호가 올 때까지 기다리는 함수입니다. 그리고 CloseHandle은 해당 개체의 핸들을 닫는 함수입니다. 핸들을 닫아주는 이유는 커널 오브젝트 때문입니다. 자세한 내용은 다음 포스트에서 다루겠습니다.
'개발 > Windows' 카테고리의 다른 글
IPC (0) | 2025.03.26 |
---|---|
커널 오브젝트와 오브젝트 핸들 (0) | 2025.03.17 |
레지스터와 명령어 구조 (0) | 2025.03.14 |
Windows 32/64비트 시스템 (0) | 2025.02.26 |
Windows 유니코드 (0) | 2025.02.21 |