프로세스란

 "프로그램"이란 일반적으로 실행코드를 뜻하고 "프로세스"란 프로그램을 구동하여 메모리상에서 실행되는 

 작업 단위를 말합니다.

NotePad 프로그램 및 프로세스

 예를 들어서 그림을 보면 "notepad.exe"라는 프로그램이 있고 notepad에 의한 프로세스가 2개가 띄워져 있는 것을  확인할 수 있습니다. 

프로세스의 구성 요소

  • 프로세스를 위한 커널 오브젝트
    • 프로세스의 커널 오브젝트에는 프로세스에 대한 각종 통계정보들을 저장해서 관리합니다.
  • 실행 모듈이나 DLL의 코드와 데이터를 저장하는 주소 공간 
    • 스레드 스택 및 힙 메모리 같은 동적 메모리 공간도 포함

프로세스의 코드 실행 방식

 프로세스는 스스로 실행 될 수 없으며 프로세스가 수행되기 위해서는 프로세스의 내부에 수행되는 스레드가

 있어야 합니다.

 프로세스가 생성되면 시스템을 자동으로 첫 스레드를 생성하는데 이 스레드를 주 스레드(primary thread)라고 합니다.

 프로세스는 다수의 스레드를 가질 수 있으며 프로세스의 주소 공간에서 동시에 수행됩니다.

 OS은 모든 스레드가 동시에 수행될 수 있도록 라운드 로빈 방식으로 CPU 시간을 단위 시간만큼 나누어 주는데

 때문에 사용자는 모든 스레드들이 동시에 실행되는 것처럼 느낍니다.

 스레드는 실행 주체이기 때문에 동작을 위해서 자신만의 고유한 CPU 레지스터 컨텍스트 저장 공간 및 스택

 가져합니다. 

윈도우 Application 종류 

 윈도우는 두 가지 형태의 Application을 지원합니다.

 GUI(그래픽 유저 인터페이스)와 CUI(콘솔 유저 인터페이스)입니다. 

 VS2019에서는 프로젝트 생성 시 GUI 또는 CUI 기본 생성 템플릿을 제공해서 만들 수 있도록 지원합니다. 

VS2019 C++ 프로젝트 생성

GUI & CUI Application

 GUI의 대표적인 예제 프로그램인 윈도우와 함께 배포되는 메모장이나 워드패드, 계산기를 들 수 있는데

 GUI의 장점은 사용자에게 보다 편리한 정보를 제공해 줄 수 있습니다. 

 CUI의 대표적인 예제 프로그램은 명령 프롬프트(cmd.exe)로 들 수 있는데 콘솔 창으로 인터페이스를 제공합니다.

 CUI의 장점은 그래픽 인터페이스를 제공하지 않는 대신 컴퓨터 자원을 더 적게 소모합니다.

 일반적으로 서버 프로그램은 CUI로 많이 만들어집니다.

 프로젝트 생성 후 GUI 및 CUI로 변경하려면 Visual Studio에서 속성 -> 링커 -> 시스템에서 하위 시스템을

 "/SUBSYSTEM:WINDOWS"로 설정합니다. (CUI는 "/SUBSYSTEM:CONSOLE" 설정)

VS에서 GUI 및 CUI 설정방법

프로세스 타입별 생성 시 동작 방식

 윈도우 c/c++ Application은 수행을 시작할 진입점(Entry) 함수를 가져야 하는데 C++ 개발자는 GUI 및 CUI에 선택에

 따라서 두 가지 형태의 진입점 함수를 사용할 수 있습니다.

타입 시작점 함수
GUI int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow);
CUI int _tmain( int argc, TCHAR * argv[], TCHAR *envp[]);

 유니코드 사용 여부에 따라서도 진입점 함수가 달라지는데 _tWinMain은 WinMain, wWinMain함수를

 _tmain은 main, wmain함수 호출을 디파인으로 정의되어 있습니다. 

진입점 함수

 위 그림은 진입점(main) 함수에서 중단점을 걸고 디버깅을 하게 되면 Call Stack으로 진입점 함수가 호출되기 전에

 kernel32.dll에서 wmainCRTStartup이라는 C++ 런타임 시작 함수를 호출하는 것을 볼 수 있습니다. 

 OS는 진입점 함수를 직접 호출하지 않고 프로세스 시작 시 C/C++ 런타임 시작 함수를 호출하며 런타임 시작 함수

 내부에서 C/C++ 런타임 라이브러리 초기화 작업, 전역 오브젝트나 static 오브젝트를 생성하는 작업을 수행 후에

 진입점 함수를 실행하게 됩니다.

어플리케이션 타입 진입점 함수 런타임 시작 함수
ANSI 문자열 GUI Application _tWinMain(WinMain) WinMainCRTStartup
유니코드 문자열 GUI Application _tWinMain(wWinMain) wWinMainCRTStartup
ANSI 문자열 CUI Application _tmain(main) mainCRTStartup
유니코드I 문자열 CUI Application _tmain(wmain) wmainCRTStartup

 링커는 링커 스위치가 "/SUBSYSTEM:WINDOWS"로 설정되어 있으면 WinMain이나 wWinMain을 찾습니다. 

 해당하는 시작점 함수가 없다면 "unresolved external symbol" 에러를 발생시킵니다. 시작점 함수를 정상적으로 찾으면

 WinMainCRTStartup이나 wWinMainCRTStartup 런타임 시작 함수를 실행합니다.

런타임 시작 함수 호출 시

런타임 시작함수

// Visual Studio 2019 C RumTime 함수
static __declspec(noinline) int __cdecl __scrt_common_main_seh()
{
    if (!__scrt_initialize_crt(__scrt_module_type::exe))
        __scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);

    bool has_cctor = false;
    __try
    {
    	// 다양한 작업 진행 
        .....

        //
        // Initialization is complete; invoke main...
        //

        int const main_result = invoke_main();

__scrt_common_main_seh에서 초기화 작업후 invoke_main함수 호출 

// Visual Studio 2019 C RumTime 함수
  static int __cdecl invoke_main()
  {
        return wmain(__argc, __wargv, _get_initial_wide_environment());
  }

 MSVC의 C++ 런타임 시작함수가 호출되면 아래와 같은 작업이 진행됩니다.

  •  프로세스의 매개변수 및 환경변수를 가르키는 포인터를 획득합니다.
  • C/C++ 런타임 라이브러리의 전역변수를 초기화 합니다. 
  • C/C++ 런타임 라이브러리의 메모리 할당 함수와 저수준 입출력이 사용하는 힙을 초기화합니다.
  • 모든 전역 오브젝트와 static C++ 클래스 오브젝트의 생성자를 호출합니다. 

 런타임 시작함수는 위의 내용을 전부 수행하고 진입점 함수를 호출 합니다.

 개발자는 진입점 함수부터 로직 구현이 가능합니다. 

진입점 함수 반환 시 

/// Visual Studio 2019 C RumTime 함수
        int const main_result = invoke_main();

        //
        // main has returned; exit somehow...
        //

        if (!__scrt_is_managed_app())
            exit(main_result);

        if (!has_cctor)
            _cexit();

        // Finally, we terminate the CRT:
        __scrt_uninitialize_crt(true, false);
        return main_result;

 프로세스가 모든 로직을 수행 후 진입점 함수가 반환되면 반환값(main_result)으로 런타임 라이브러리의 exit 함수를

 호출하고 ExitProcess 함수 호출 하여 프로세스를 종료합니다.

  • _onexit 함수를 이용해서 등록해 두었던 함수를 호출 합니다.
  • 모든 전역 클래스 및 Static 클래스 오브젝트의 파괴자를 호출합니다. 
  • DEBUG 빌드의 경우 _CRTDBG_LEAK_CHECK_DF 플래그가 설정되어 있으면 C/C++ 런타임 메모리의 메모리 누수를 상황을 _CrtDumpMemoryLeaks 함수를 호출하여 나열합니다. 
  • 진입점 함수 반환값을 인자로 ExitProcess 함수를 호출 합니다.  이 함수를 호출하면 OS는 프로세스를 종료하고 프로세스의 종료 코드를 설정합니다.

CreateProcess 함수

 CreateProcess 함수를 사용하면 새로운 프로세스를 생성 할 수 있습니다. 

 아래 예제는 메모장을 새로운 프로세스로 실행 시키는 예제입니다. 

	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	memset(&si, 0, sizeof(si));
	memset(&pi, 0, sizeof(pi));

	// 메모장 실행 예제
	TCHAR command[] = TEXT("notepad.exe");
    
    // 이렇게 전달하면 a.txt 파일 메모장으로 오픈합니다.
    //TCHAR command[] = TEXT("notepad.exe C:\\Users\\Kim\\Desktop\\전략내용\\a.txt");
    
	if (CreateProcess(NULL, command, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
	{
		// pi.hProcess 생성된 프로세스 핸들, pi.hThread 생성된 프로세스 스레드 핸들
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}

CreateProcess 함수 구조

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
    );

 

 첫번째 인자인 "pszApplicationName"를 직접 실행 파일 경로를 지정할 수 있습니다. 

 하지만 일반적으로 NULL을 전달해서 사용합니다. 이 값을 NULL을 전달하면 두번째 인자인 "pszCommandLine"의

 첫번째 토큰을 실행하고자하는 프로그램 파일명으로 간주하며 확장자가 전달되지 않으면 .exe로 추론합니다.

 (예제에서는 TEXT("notepad.exe")를 전달하여 메모장 프로그램을 호출합니다. )

  1.  생성할 프로세스 실행 파일명에 포함된 디렉토리
  2.  생성할 프로세스의 현재 디렉토리
  3.  윈도우 시스템 디렉토리( GetSystemDirectory가 반환하는 System32 서브 폴더)
  4.  윈도우 디렉토리
  5.  PATH 환경변수에 포함된 디렉토리들

 순서대로 검색해서 실행파일을 찾아서 실행합니다. 

"lpProcessAttributes"와 "lpThreadAttributes"는 보안 특성으로 새로운 프로세스가 생성되면 프로세스 커널 오브젝트와

 스레드 커널 오브젝트에 전달 될 정보입니다.

 "dwCreationFlags"는 생성되는 프로세스를 어떻게 생성할지 전달하는 프로세스입니다.

 몇 개의 플레그만 설명을 드립니다. 나머지는 링크의 MSDN을 참조해 주세요

플레그  
CREATE_SUSPENDED 새로운 프로세스가 생성된 후 프로세스의 주스레드를 정지 상태로 만듭니다.
DEBUG_PROCESS 생성되는 프로세스와 이후 생성되는 모든 프로세스에게 디버깅 한다는 것을 알림니다.
CREATE_NO_WINDOW 어플리케이션의 콘솔 윈도우를 생성하지 않습니다. 

https://docs.microsoft.com/ko-kr/windows/win32/procthread/process-creation-flags

 

Process Creation Flags (WinBase.h) - Win32 apps

Process Creation Flags In this article --> The following process creation flags are used by the CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, and CreateProcessWithTokenW functions. They can be specified in any combination, except as noted. C

docs.microsoft.com

 "lpStartupInfo"는 STARTUPINFO 구조체를 전달합니다.

 "lpProcessInformation"는 PROCESS_INFORMATION 구조체를 전달합니다. 

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

 CreateProcess함수는 프로세스 생성이 성공으로 반환 될 때 부모 프로세스가 자식 프로세스의 프로세스 및 스레드의

 커널 오브젝트의 접근 할 수 있도록 핸들 정보 및 ID 값을 설정 합니다.

 성공적으로 새로 생성된 프로세스의 커널 오브젝트는 사용 카운트가 2가 됩니다.

 (부모의 커널 오브젝트 테이블과 자기 자신이 커널 오브젝트 테이블에 등록되기 때문)

 그렇기 때문에 새로 생성된 프로세스의 커널 오브젝트에 접근하지 않게 된다면 CloseHandle을 통해서 반환해줘야

 합니다.

프로세스의 종료 

 프로세스는 다양한 방법에 의해서 종료 될 수 있습니다. 

  • 주 스레드의 진입점 함수가 반환 됩니다. 
  • 프로세스내에서 ExitProcess가 호출 되거나 TerminateProcess를 호출합니다.
  • 프로세스 내의 모든 스레드가 각자 종료 된다.

 하지만 프로세스의 종료는 주 스레드의 진입점 함수가 반환되는 형태로 종료되도록 해야합니다. 

 ExitProcess함수로 종료된다면 이 함수 뒤에 있는 코드는 절대로 수행되지 않습니다. 

 또한 C/C++ 어플리케이션이 이 함수를 수행하게 되면 C/C++ 런타임이 관리하는 리소스에 대한 정리 작업이

 수행되지 않기 때문에 오브젝트이 소멸자가 호출 되지 않고 프로세스가 종료됩니다.

 TerminateProcess는 자신의 프로세스 뿐만 아니라 다른 프로세스도 종료 시킬 수 있습니다. 


class SomeObj
{
public:
	SomeObj(const wchar_t * name) : m_strName(name)
	{
		std::wcout << m_strName << TEXT(" Construct SomeObj!") << std::endl;
	}

	~SomeObj()
	{
		std::wcout << m_strName << TEXT(" Destruct SomeObj!") << std::endl;
	}
private :
	std::wstring  m_strName;
};

SomeObj g_some(TEXT("g_some"));

// print result
// g_some Construct SomeObj!
// some1 Construct SomeObj!
// some1 Destruct SomeObj!
// g_some Destruct SomeObj!
int _tmain()
{
	SomeObj some1(TEXT("some1"));
	return 0;
}


// print result
// g_some Construct SomeObj!
// some1 Construct SomeObj!
// c++ runtime 종료처리 동작 안함, 소멸자 호출 안됨
int _tmain()
{
	SomeObj some1(TEXT("some1"));
	ExitProcess(0);
	return 0;
}

 예제의 코드를 보시면 ExitProcess가 호출되면 SomeObj의 소멸자가 호출되지 않고 프로세스가 종료되는 것을 확인 

 할 수 있습니다. 

프로세스가 종료되면 아래 나열된 작업들이 수행됩니다.

  1.  프로세스의 남아 있는 스레드가 종료됩니다.
  2.  프로세스에 의해서 할당된 모든 사용자 오브젝트 및 GDI 오브젝트가 삭제되며 모든 커널 오브젝트를 반환합니다.
  3.  프로세스의 종료코드는 STILL_ACTIVE에서 설정된 종료코드로 변경됩니다. 
  4. 프로세스 커널 오브젝트의 상태가 시그널 상태로 변경됩니다.
  5.  프로세스의 커널 오브젝트의 사용 카운트가 1만큼 감소 됩니다. 

+ Recent posts