<프로세스간 통신(IPC) - (1)>
[이번 글을 따라서 진행하기에 앞서]
여기까지 와서 글을 보고 공부하시는 분은 뭐... 얼마 없을거라고 생각합니다.
하지만 복습하는 차원에서 글을 읽는 분들에게 설명하는 것이 가장 좋은 학습 방법이라고 생각합니다.
그래서 글을 읽으시는 불특정 다수의 사람들에게 설명하듯이 글을 쓰게 되었습니다.
우선 이번 글을 공부하기에 앞서서, 책이 있으신 분들은 아마 문제가 안될 것이라고 생각합니다.
책이 없는 분, 혹은 강의만 들으신 분은 이 글에서 느닷없이 파일 입출력 함수가 나와서 당황하실 수도 있습니다.
그렇다고 책을 사라는 말은 아닙니다. 있으면 편하겠죠 애초에 이 글도 안보실거고요
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
위에 있는 세 함수(CreateFile, ReadFile, WriteFile)에 대해서 읽어보시고 오는 것을 추천드립니다.
어차피 한참 뒤에서 이 내용을 다시 다루긴 하겠지만, 지금 글에서는 위에 있는 세 함수를 반드시 사용하게 됩니다.
"영어라서 읽기 싫다!" 하시는 분들은 구글에서 위의 함수 이름 치면 정리해놓으신 분들이 많습니다.
그리고 구글 크롬 쓰시는 분들은 페이지 번역 돌려서 보셔도 됩니다.
생각보다 나쁘지 않게 번역 잘 돼서 나옵니다.
[프로세스간 통신(IPC)의 의미]
IPC란 (Inter-Process Communication)의 약자입니다.
말 그대로 "프로세스 사이의 통신"이라는 말입니다.
대체로 이런 상황은 아마 서버쪽 프로그래밍을 하다보면 접하게 될 일이 많습니다.
보편적으로 하나의 프로그램은 하나의 프로세스가 일반적이죠.
그런데 하나의 프로그램에는 여러 프로세스가 있을 수도 있습니다.
그리고 그 프로세스들끼리 통신을 해야되는 경우도 있습니다.
그런데 프로세스들이 서로 통신한다는 것은 도대체 뭘 말하는걸까요?
우리가 메일이나 메신저를 통해서 이야기를 주거니 받거니 하는 경우는 있는데.
프로세스끼리 통신을 한다? 얘네들끼리는 무슨 이야기를 주고받는 것도 아니고.
여기서 '통신'이라고 함은 기본적으로 데이터를 주고 받는 행위를 말합니다.
그래서 프로세스들이 서로 통신을 한다는 것은 둘 이상의 프로세스가 데이터를 주고 받는 행위라고 볼 수 있습니다.
그리고 프로세스간 통신의 핵심은 '메모리의 공유'에 있습니다.
[프로세스 사이에서 통신이 이뤄지기 위한 조건]
일단 프로세스 차원에서 생각해보기보다는 사람간의 관계를 놓고 이야기를 해봅시다.
우리가 누군가와 통신을 한다는 것은 대화를 한다거나 뭔가 주고 받을 것이 있는 상황입니다.
제일 쉬운 방법은 뭘까요?
직접 만나면 됩니다.
만나서 할 이야기도 하고 주고 받을 것을 주고 받으면 되는거죠.
그리고 직접 만나기 위해서는 '만날 장소'가 필요합니다.
그런데 한 사람은 미국에 살고 한 사람은 서울에 살고 있는 경우라면 직접 만나기가 여의치 않습니다.
만약 대화가 목적이라면 전화를 하거나, 메신저, 메일이나 편지등을 이용할 수도 있습니다.
물건을 보내야한다면 국제배송을 통해서 보내면 되는 것이고요.
이처럼 전화나 메신저, 국제배송을 이용하는 것을 IPC라고 할 수 있습니다.
책에서 저자는 농담으로 Inter-Person Communication이라고 했는데, 비슷한 개념이라 그런 것 같습니다.
실제로도 비슷합니다.
결론만 내리자면 다음과 같습니다.
1) 두 사람(프로세스)가 직접 만날 여건이 된다면 만나서 이야기(통신)을 하는 것은 문제가 없다.
2) 직접 만날 여건이 안된다면 다른 방법을 통해서 통신을 해야한다.
그래서 프로세스가 직접 만날 수 있다면 프로세스 간 통신은 아주 쉬운 일이 됩니다.
그런데 그게 아니라면? 다른 방법, 즉 보조적인 수단을 사용해야 합니다.
[프로세스들이 서로 만날 수 없는 이유]
앞에서 했던 이야기가 무색해지게 타이틀에서는 벌써 결론을 내리고 있습니다.
네, 못만납니다.
위의 그림처럼 프로세스는 각각 독립적으로 메모리 영역을 가지고 있습니다.
그래서 프로세스 A가 프로세스 B의 메모리 영역에 접근할 수가 없습니다.
역의 경우도 마찬가지입니다.
아니, 메모리의 공유가 프로세스간 통신의 핵심이라면서!
일단은 '직접적으로는 못한다'고 했습니다.
그래서 '보조적인 수단'을 이용해야 된다고 앞에서 이야기를 드렸습니다.
[프로세스들이 서로 만나지 못하게 설계한 이유]
오늘날의 운영체제들은 프로세스가 자신에게 할당된 메모리 공간 이외의 영역에 접근하는 것을 허용하지 않습니다.
예를 들어서 여러분이 음악을 들으면서 문서작업이 되었건, 게임을 하건 뭔가를 하고 있다고 칩시다.
못해도 2~3개의 프로세스가 올라와 있는 상황이죠.
그런데 갑자기 음악이 막 이상하게 나옵니다.
아니면 작업하고 있던 문서의 글자가 막 깨지기도 하고요.
게임을 하고 있는데 화면이 이상하게 나옵니다.
원인이 뭔가 했더니 각각의 프로세스가 서로 다른 프로세스의 공간에 접근하게 되어 생긴 문제였습니다.
물론 위와 같은 일을 겪으신 분들은 없으실겁니다.
이런 일은 일어날 리가 없습니다.
아까도 말했지만, 프로세스는 할당된 메모리 공간 이외에 다른 프로세스 영역에는 접근할 수가 없습니다.
만약에, 혹시라도 만약에 이게 가능하다고 치면 프로그램 개발하는 사람들은 죽을 맛일겁니다.
개발하기도 힘들어지고, 막상 개발을 해도 "이 프로그램 정말 안전한거 맞나?" 라며 의심을 하게 될거고요.
결국 프로세스는 자신에게 할당된 메모리 공간 이외에는 접근이 불가능하게 한 이유는 다음과 같습니다.
"프로그램의 안전성과 신뢰성을 높이기 위함이 목적입니다."
[메일슬롯 방식의 IPC]
프로세스끼리 메모리에 직접 접근도 못하니까 메모리 공유는 못하는데, 프로세스간 통신은 해야되겠고.
도대체 뭘 어떻게 하라는건지 싶을겁니다.
그래서 Windows 운영체제에서는 프로세스간 통신(이하 IPC)이 가능하도록 다양한 IPC기법을 제공합니다.
이게 바로 아까 말했던 '보조 수단'입니다.
여기서는 '메일슬롯(Mail Slot)' 기반의 IPC 기법을 알아보겠습니다.
참고로 메일슬롯은 Windows에만 있는 기법이 아니라 Linux와 같은 다른 운영체제에서에서도 지원하는 기법입니다.
[메일슬롯(Mail Slot) 원리]
일단 메일슬롯이라는 놈은 파이프(뒤에서 이야기할 다른 IPC 기법)와 더불어서 대표적인 IPC 기법입니다.
메일슬롯은 우체통의 입구, 즉 편지를 넣는 입구를 뜻합니다.
근데 그냥 편하게 우체통을 생각하시면 됩니다.
그냥 이름을 Mail Box(우체통)으로 하지, 뭐한다고 메일슬롯으로 했는지 모르겠습니다.
여하튼! 메일슬롯의 기본적인 원리는 다음과 같습니다.
"데이터를 주고 받기 위해서 프로세스가 운영체제에게 요청하여 우체통을 마련하는 것"
일단 과정을 좀 풀어서 설명해보겠습니다.
데이터를 전달하고자 하는 프로세스를 발송인(Sender), 데이터를 받는 프로세스는 수신인(Receiver)라고 합시다.
우선 수신인 측에서 우체통을 하나 깔아달라고 운영체제한테 요청을 합니다.
그리고 이 우체통이 '메일슬롯'입니다.
이 우체통이 통신을 위해 '만날 장소'가 되는 것이죠.
그래서 운영체제는 요청을 받아들이고 둘이 '만날 장소'인 '메일슬롯'을 깔아줍니다.
즉, '메모리를 공유할 수 있는 자리'를 마련해주는 것입니다.
발송인은 수신인의 주소를 가지고 메일슬롯에 데이터를 날립니다.
그러면 수신인은 발송인이 보낸 데이터를 읽어볼 수 있게 되는 것이죠.
쉽게 생각하면 편지를 보내는 것과 같습니다.
[메일슬롯(Mail Slot)구성을 위해 필요한 요소]
아까 위에서 말했던 과정을 요약하면 두 사람이 해야할 일로 정리해볼 수 있습니다.
Receiver와 Sender는 각각 무엇을 준비해야하는지 정리해보겠습니다.
1) Receiver(수신인)이 준비해야 할 것
- 메일슬롯을 운영체제에 요청하여 생성할 것
2) Sender(발송인)이 준비해야 할 것
- 메일슬롯의 이름 (Receiver의 우체통 주소)
- 보낼 데이터
생각보다 준비해야할 것은 많이 없습니다.
이제 예제 코드를 한 번 보겠습니다.
[메일슬롯의 예]
[MailReceiver.cpp]
/*
* Windows System Programming - 프로세스 간 통신(Inter-Process Communication, IPC)
* 파일명: MailReceiver.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 메일슬롯(MailSlot)의 사용 예제 - Receiver
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
// 사용할 메일슬롯의 주소
// \\computerName\mailslot\[path]name
// 아래의 주소는 \\.\mailslot\mailbox라고 쓴 것과 같다.
// 여기서 '.'이 의미하는 것은 로컬 컴퓨터, 즉 내 컴퓨터를 뜻한다.
// 루프백인 127.0.0.1과 같다고 보면 된다.
#define SLOT_NAME _T("\\\\.\\mailslot\\mailbox")
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hMailSlot; // mailslot 핸들
TCHAR messageBox[50];
DWORD bytesRead; // 몇 바이트나 읽었는가 확인
// mailslot 생성
hMailSlot = CreateMailslot(
SLOT_NAME, // 메일 슬롯의 이름, 즉 주소를 지정하는 것
0, // 메일 슬롯의 버퍼 크기 지정, 0을 전달하면 시스템이 허용하는 최대 크기로 지정
MAILSLOT_WAIT_FOREVER, // 메일슬롯을 통해 읽어들일 데이터가 채워질 때까지 기다리는 블로킹(Blocking) 타임아웃 설정
NULL // 보안 속성 지정시 사용, 정확히는 핸들의 상속과 관련이 있음.
);
if (hMailSlot == INVALID_HANDLE_VALUE)
{
_fputts(_T("Unable to create mailslot!\n\n"), stdout);
return 1;
}
// Message 수신
_fputts(_T("******** Message ********\n"), stdout);
while (1)
{
// ReadFile 함수를 통해 메일슬롯으로 전달된 데이터를 읽는다.
// 원래 파일로부터 데이터를 읽어들일 때 사용하는 함수인데, 여기서는 메일슬롯으로부터 데이터를 읽는다.
if(!ReadFile(hMailSlot, // 메일슬롯의 핸들은 인자로 전달. 파일이라면 파일의 핸들을 인자로 전달한다.
messageBox, // 읽어 들인 데이터를 저장할 버퍼를 지정
sizeof(TCHAR) * 50, // 읽어들일 데이터의 최대 크기를 지정
&bytesRead, // 실제로 읽어들인 데이터 크기를 바이트 단위로 저장하기 위한 변수의 주소를 지정
NULL)) // 여기는 현재 언급하기 어려우므로 NULL을 전달한다고 이해
{
_fputts(_T("Unable to read!\n"), stdout);
CloseHandle(hMailSlot);
return 1;
}
if (!_tcsncmp(messageBox, _T("exit"), 4))
{
_fputts(_T("Good Bye!\n"), stdout);
break;
}
messageBox[bytesRead / sizeof(TCHAR)] = 0; // 마지막에 NULL 문자 삽입
_fputts(messageBox, stdout);
}
CloseHandle(hMailSlot);
return 0;
}
[MailSender.cpp]
/*
* Windows System Programming - 프로세스 간 통신(Inter-Process Communication, IPC)
* 파일명: MailSender.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 메일슬롯(MailSlot)의 사용 예제 - Sender
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
// 사용할 메일슬롯의 주소
// \\computerName\mailslot\[path]name
// 아래의 주소는 \\.\mailslot\mailbox라고 쓴 것과 같다.
// 여기서 '.'이 의미하는 것은 로컬 컴퓨터, 즉 내 컴퓨터를 뜻한다.
// 루프백인 127.0.0.1과 같다고 보면 된다.
#define SLOT_NAME _T("\\\\.\\mailslot\\mailbox")
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hMailSlot; // mailslot 핸들
TCHAR message[50];
DWORD bytesWritten; // 몇 바이트나 읽었는가 확인
// mailslot에 메시지를 전달하기 위한 데이터 스트림 생성
hMailSlot = CreateFile(
SLOT_NAME, // 메일 슬롯의 주소, 즉 해당 주소의 메일슬롯을 개방한다.
GENERIC_WRITE, // 쓰기 모드
FILE_SHARE_READ, // 파일의 공유 방식, 여기서는 동시 읽기 접근 가능
NULL, // 보안 속성 지정, 핸들 상속과 관련있는 부분. 여기서는 디폴트 속성 지정을 위해 NULL 사용
OPEN_EXISTING, // 기존의 파일을 개방. 없다면 함수 호출 실패. Receiver에 의해 메일슬롯이 생성되어 있는 상황.
FILE_ATTRIBUTE_NORMAL, // 파일의 속성. 별다른 특성이 없는 보통 파일. 일반적으로 이걸 쓴다고 보면 됨.
NULL // 기존에 존재하는 파일과 동일한 특성을 가지는 새 파일을 만들때 사용되는 전달 인자. 일반적으로 NULL 사용.
); // 함수 호출이 성공하면 파일의 핸들이 반환됨.
if (hMailSlot == INVALID_HANDLE_VALUE)
{
_fputts(_T("Unable to create mailslot stream!\n"), stdout);
return 1;
}
// Message 송신
while (1)
{
// 문자열을 입력받는다.
_fputts(_T("MY CMD>"), stdout);
_fgetts(message, sizeof(message) / sizeof(TCHAR), stdin);
// WriteFile 함수를 통해 메일슬롯으로 데이터를 전송한다.
if (!WriteFile(hMailSlot, // 데이터를 저장할 파일을 지정한다. 여기서는 데이터를 메일슬롯에 저장한다(보낸다).
message, // 전송할 데이터가 저장되어 있는 버퍼를 지정한다.
_tcslen(message) * sizeof(TCHAR), // 전송할 데이터 크기를 지정
&bytesWritten, // 함수 호출 완료 후 전송된 실제 데이터의 크기를 바이트 단위로 얻기 위한 변수의 주소를 지정.
NULL)) // ReadFile과 마찬가지로 현재는 NULL을 넣는다고만 알아둔다.
{
_fputts(_T("Unable to write!\n"), stdout);
CloseHandle(hMailSlot);
return 1;
}
if (!_tcsncmp(message, _T("exit"), 4))
{
_fputts(_T("Good Bye!\n"), stdout);
break;
}
}
CloseHandle(hMailSlot);
return 0;
}
코드 자체는 어려운 코드가 아니라서 어려운 부분은 없습니다.
다만 하나 걸리는 부분이 있을겁니다.
"메일슬롯도 파일인가요?"
아닙니다.
근데 파일 입/출력 함수를 가지고 데이터를 주고 받게 해놨습니다.
Windows에서 메일슬롯은 Windows 파일 시스템을 기반으로 구현을 했습니다.
ReadMailslot이나 WriteMailslot같은 함수를 만든다고 칩시다.
그런데 ReadFile이랑 WriteFile하고 기능상 아무런 차이가 없고 이름만 다른 함수를 만드는게 됩니다.
그래서 MS에서는 이러한 형태의 함수를 디자인하지 않고 기존에 만든 파일 입/출력 함수를 쓰는 것으로 퉁친겁니다.
[메일슬롯에 대해서]
[메일슬롯에 대하여]
메일슬롯을 이용하면 간단한 채팅 프로그램을 만들 수 있겠다는 생각이 드실겁니다.
저도 결과창을 딱 보자마자 이거 되겠다 싶었거든요.
근데 뭔가 2% 부족한 부분이 있습니다. (사실 더 부족한데)
위에서 작성한 예제는 '단방향' 통신을 한다는 것입니다.
맞습니다. '단방향 통신만 가능하다'가 메일슬롯의 기본적인 특성입니다.
그래서 채팅 프로그램을 만들고 싶다면 양쪽에 메일슬롯을 만들어주면 가능할겁니다.
크게 어렵지도 않으니까 해보고 싶은 분들은 해보시면 바로 만드실겁니다.
+ 추가사항
메일슬롯은 생성과 동시에 Usage Count가 1이 됩니다.
참조하는 프로세스는 메일슬롯을 생성한 프로세스 하나 밖에 없기 때문에 그렇습니다.
부모 프로세스가 자식 프로세스를 생성하는 경우에는 부모 프로세스가 생성과 동시에 참조를 하기 때문에 2가 됩니다.
쓰레드도 마찬가지입니다. (뒤에서 다룰 내용입니다.)
그래서 프로세스와 쓰레드를 제외한 리소스의 커널 오브젝트는 생성과 동시에 Usage Count가 1이 됩니다.
이 사실 정도까지만 알아두면 좋을 것 같습니다.
[IPC에 대한 고찰]
우리가 보편적으로 생각했을 때 통신은 '양방향으로 주거니받거니 하는 것 아닌가?' 할 수 있습니다.
그런데 메일슬롯은 단방향으로만 통신이 가능하니 우리가 기대했던 통신은 아닙니다.
물론! 메일슬롯 외에도 다른 IPC 기법이 있는데 '파이프(Pipe)'라는 기법도 있습니다.
이건 추후에 다룰 예정입니다만, 이 기법 중 Named Pipe를 이용하면 우리가 생각하는 통신을 가능하게 합니다.
여하튼, 메일슬롯을 이용한 통신은 우리가 생각했던 통신은 아닙니다.
그렇다고 해서 쓸 일이 없을 것 같다 싶으면 그것도 절대 아니라는겁니다.
메일슬롯은 '브로드캐스팅(Broadcasting)' 방식의 통신을 지원합니다.
그러니까 Sender 하나가 여러 Receiver에게 동일한 메시지를 동시에 전송하는 것이 가능하다는 겁니다.
쉽게 생각하면 KBS, SBS, MBC같은 채널에서 동일한 시간에 동일한 프로그램을 볼 수 있잖아요.
여기서 B가 Broadcasting입니다.
그래서 보내는 대상이 여럿일 때 메일슬롯이 유용하게 사용될 수 있습니다.
[Signaled vs Non-Signaled]
이제 잠깐 이야기를 틀겠습니다.
IPC 기법 이야기는 아까 파이프라는 놈이 있다고 했는데 그건 다음에 마저 다루겠습니다.
여기서는 다시 커널 오브젝트에 대한 이야기를 좀 하려고 합니다.
실제 책에서도 커널 오브젝트의 상태와 핸들 테이블에 대한 이야기를 다루기 시작하거든요.
책의 저자분이 쓰레드와 쓰레드의 동기화(먼 훗날의 이야기) 부분에서 다룰까 하다가 IPC에서 다루기로 했습니다.
사실 이 내용도 그렇지만 커널 오브젝트때부터 뭔 이야기인가 싶으신 분들도 분명히 있었을겁니다.
어차피 알고 가야하는 내용입니다.
여기서는 커널 오브젝트의 상태(State)에 대해서 다루겠습니다.
[커널 오브젝트의 두 가지 상태(State)]
살다보면 우리 주변에는 상태(State) 정보를 갖고 있는 것들을 볼 수 있습니다.
제일 쉬운 예로 자동차를 생각해봅시다.
자동차는 주행 상태와 정지 상태로 나뉘게 됩니다. (물론 이분법적으로만 나누면 안되지만)
Windows 운영체제에 의해서 생성되는 커널 오브젝트도 두 가지 상태를 지니게 됩니다.
이 상태는 리소스에 특정 상황이 발생되었음을 알리기 위한 용도로 사용됩니다.
[상태에 대한 이해]
상태(State)라는 말을 사용하는 것은 변하는 것을 표현하기 위해서입니다.
아까 자동차를 예로 들었듯이, 주행 중이면 주행 상태가 되고 브레이크를 밟아서 멈추면 정지 상태가 됩니다.
이 상태라는 것은 특정 상황이 발생하면 변경이 됩니다.
그리고 이건 커널 오브젝트에도 해당이 되는 이야기입니다.
Signaled 상태라는 것은 무언가 신호를 받은 상태고, Non-Signaled는 신호를 받지 않은 상태라는 것이 되겠죠?
[커널 오브젝트의 상태에 대한 이해]
그래서 커널 오브젝트의 상태는 리소스에 특정한 상황이 발생했음을 알리기 위한 것입니다.
그런데 특정 상황이라는 것이 리소스별로 다 다릅니다.
그래서 커널 오브젝트의 상태가 변하는 시점도 커널 오브젝트의 종류에 따라 다 달라집니다.
일단 프로세스만 가지고 예를 들어보겠습니다.
프로세스 커널 오브젝트는 프로세스가 생성될 때 만들어집니다.
처음에 커널 오브젝트가 생성이 되면 커널 오브젝트의 상태는 Non-Signaled 상태에 놓이게 됩니다.
그리고 프로세스가 종료가 될 때, Signaled 상태로 변경됩니다.
요약하자면, 프로세스 커널 오브젝트는 프로세스 실행 중에는 Non-Signaled 상태입니다.
그러다가 프로세스가 종료되면 Windows 운영체제에 의해서 자동적으로 Signaled 상태가 됩니다.
그러면 Signaled 상태에서 Non-Signaled 상태가 되는 것은 언제일까요?
종료된 프로세스가 다시 실행을 재개하면 가능하겠죠?
근데 종료된 프로세스는 다시 실행을 재개하지 못한다는 겁니다.
쉽게 생각하면 이미 죽은 사람을 다시 살려내겠다는 것과 다를 바가 없는 것이거든요.
그래서 커널 오브젝트의 상태가 Signaled라면 되었다면 죽었다 깨어나도 Non-Signaled가 될 일은 없습니다.
[커널 오브젝트의 두 가지 상태를 확인하는 용도의 함수]
https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject
WaitForSingleObject라는 함수가 있습니다.
이 함수가 커널 오브젝트의 상태를 확인하는 데 사용되는 함수입니다.
이제 이 함수를 사용한 예를 한 번 보도록 하겠습니다.
[커널 오브젝트의 상태 확인이 필요한 상황의 연출]
일단 부모 프로세스가 자식 프로세스를 두 개를 만듭니다.
그리고 한 쪽은1~5까지, 다른 한 쪽은 6~10까지 더하는 과정을 수행하고 결과값을 받아오는 프로그램을 만든다 칩시다.
[PartAdder.cpp]
/*
* Windows System Programming - 커널 오브젝트의 상태(State)
* 파일명: PartAdder.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트의 상태(State)와 종료 코드(Exit Code)의 이해
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
if (argc != 3)
return 1;
DWORD start = _ttoi(argv[1]);
DWORD end = _ttoi(argv[2]);
DWORD total = 0;
for (DWORD i = start; i <= end; i++)
total += i;
return total;
}
자식 프로세스가 될 PartAdder 프로그램 코드입니다.
별 다른 특징은 없고 함수의 인자로 시작과 끝을 받아오는 정도입니다.
그리고 반환하는 값은 덧셈을 한 값을 반환하게 됩니다.
[NonStopAdderManager.cpp]
/*
* Windows System Programming - 커널 오브젝트의 상태(State)
* 파일명: NonstopAdderManager.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트의 상태(State)와 종료 코드(Exit Code)의 이해
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si1 = { 0, };
STARTUPINFO si2 = { 0, };
PROCESS_INFORMATION pi1 = { 0, };
PROCESS_INFORMATION pi2 = { 0, };
DWORD return_val1;
DWORD return_val2;
TCHAR command1[] = _T("PartAdder.exe 1 5");
TCHAR command2[] = _T("PartAdder.exe 6 10");
DWORD sum = 0;
si1.cb = sizeof(si1);
si2.cb = sizeof(si2);
CreateProcess(NULL, command1, NULL, NULL, TRUE,
0, NULL, NULL, &si1, &pi1);
CreateProcess(NULL, command2, NULL, NULL, TRUE,
0, NULL, NULL, &si2, &pi2);
CloseHandle(pi1.hThread);
CloseHandle(pi2.hThread);
GetExitCodeProcess(pi1.hProcess, &return_val1);
GetExitCodeProcess(pi2.hProcess, &return_val2);
if (return_val1 == -1 || return_val2 == -1)
return -1;
sum += return_val1;
sum += return_val2;
_tprintf(_T("Total: %d\n"), sum);
CloseHandle(pi1.hProcess);
CloseHandle(pi2.hProcess);
return 0;
}
그리고 더한 값을 부모 프로세스에서 받아와서 출력을 하는 프로그램 코드입니다.
얼핏 보기에는 문제가 없어보입니다.
그런데 실행 결과는 애석하게도 우리가 기대했던 55가 나오지 않습니다.
결과는 518이 나옵니다. 왜일까요?
우리는 아래와 같은 가정을 가지고 프로그램을 작성했기 때문입니다.
"종료코드를 받아오기 전에 두 자식 프로세스는 연산을 모두 마치고 종료코드를 반환하면서 종료할 것이다."
다시 말해서 자식 프로세스가 먼저 끝이 날 것이고, 그 결과를 부모 프로세스에서 받을 것이라고 생각한거죠.
근데 실제로 프로세스를 생성하는 과정은 시스템에 부담을 주는 작업입니다.
그래서 종료 코드를 받아오기 전에 자식 프로세스가 생성이 되고 연산 결과를 다 마치고 종료코드를 반환한다?
사실상 무리가 있는 부분입니다.
그래서 아까 앞에서 이야기했던 WaitForSingleObject 함수를 사용하면 해결할 수 있습니다.
프로세스가 정말로 종료되어서 Signaled 상태가 되었는지를 확인할 필요가 있는 것이죠.
부모 프로세스에서 이 함수를 호출하면, 자식 프로세스의 상태를 확인하게 됩니다.
그리고 자식 프로세스가 종료될 때까지 블로킹(Blocking) 상태가 됩니다.
부모 프로세스는 여기서 더 실행되지 않고 자식 프로세스의 종료를 기다리게 됩니다.
자식 프로세스가 종료되면 블로킹 상태에서 빠져나오게 되고 부모 프로세스는 나머지 부분을 실행하게 됩니다.
+추가사항
위에서 55가 아닌 518이 나왔는데 이 값이 의미하는 것이 뭘까요?
프로세스가 종료되지 않은 상태에서 종료코드를 받아오면 STILL_ACTIVE라는 값을 받아오게 됩니다.
그런데 이 값이 2개가 더해져서 나온 것이 518인 것이죠.
그래서 대충 짐작해보면 STILL_ACTIVE라는 상수값은 259가 된다고 유추해볼 수 있습니다.
[AdderManager.cpp]
/*
* Windows System Programming - 커널 오브젝트의 상태(State)
* 파일명: AdderManager.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트의 상태(State)를 확인하는 방식으로 NonstopAdderManager.cpp의 문제를 해결한 코드
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si1 = { 0, };
STARTUPINFO si2 = { 0, };
PROCESS_INFORMATION pi1 = { 0, };
PROCESS_INFORMATION pi2 = { 0, };
DWORD return_val1;
DWORD return_val2;
TCHAR command1[] = _T("PartAdder.exe 1 5");
TCHAR command2[] = _T("PartAdder.exe 6 10");
DWORD sum = 0;
si1.cb = sizeof(si1);
si2.cb = sizeof(si2);
CreateProcess(NULL, command1, NULL, NULL, TRUE,
0, NULL, NULL, &si1, &pi1);
CreateProcess(NULL, command2, NULL, NULL, TRUE,
0, NULL, NULL, &si2, &pi2);
CloseHandle(pi1.hThread);
CloseHandle(pi2.hThread);
// 자식 프로세스가 진짜로 종료될 때까지 블로킹 상태로 대기.
// 다시 말해서 non-signaled 상태에서 signaled 상태가 되었을 때의 커널 오브젝트 상태를 보겠다는 것.
WaitForSingleObject(pi1.hProcess, INFINITE);
WaitForSingleObject(pi2.hProcess, INFINITE);
// 연산 결과에 해당하는 종료코드를 반환하고 종료할 것이라는 가정을 가지고 진행
// 하지만 실제 결과는 STILL_ACTIVE였기 때문에 55가 아닌 518이라는 결과값이 나옴
GetExitCodeProcess(pi1.hProcess, &return_val1);
GetExitCodeProcess(pi2.hProcess, &return_val2);
if (return_val1 == -1 || return_val2 == -1)
return -1;
sum += return_val1;
sum += return_val2;
_tprintf(_T("Total: %d\n"), sum);
CloseHandle(pi1.hProcess);
CloseHandle(pi2.hProcess);
return 0;
}
추가된 부분은 종료코드를 받기 전에 WaitForSingleObject 함수를 이용하는 것입니다.
위의 함수를 이용해서 실제로 프로세스의 종료를 확인하고 종료코드를 받아오는 것으로 해결이 됩니다.
그리고 WaitForMultipleObject라는 함수도 있습니다.
이건 하나의 프로세스가 아닌 여러 프로세스의 종료를 한 번에 확인하고 싶을 때 쓰는 함수입니다.
이 함수를 이용하면 위 예제의 함수 호출 두 문장을 한 문장으로 바꾸는 것도 가능합니다.