سلام ، در اولین سری از نوشته هایم در اینجا قصد دارم به شرح IPC (Inter Process Communication) در سیستم عامل Windows بپردازم ، در هر متن از سلسله متونی که قصد نگارش دارم، به بررسی یکی از شیوه های آن خواهم پرداخت ، رویکرد این متن ها یادگیری برنامه نویسی سیستمی (Windows System Programming) میباشد و مطالب آن عمدتا از داکیومنت های msdn ، کتاب Windows 10 System Programming و Windows Via Cpp تهیه و جمع آوری شده است.
معرفی : Inter-Process Communication در حقیقت به معنای مکانیزم هایی است که پردازه های گوناگون برای تبادل پیام با یکدیگر به کار میبرند، روش های مختلفی برای اینکار وجود دارد مانند : PIPE, MailSlot, Filemapping و ...
برخی از این شیوه ها برای پردازه های موجود در سیستم پایانی (End-System) مشترک و برخی به منظور مبادله پیام پردازه هایی در سیستم های پایانی غیر یکسان هستند ، که در ادامه به مرور با آن ها آشنا میشویم.
این روش به منظور ارسال پیام بین پردازه های با رابطه پدر-فرزندی (Child-Parent) که به طور واضح در یک سیستم پایانی مشترک هستند به کار میرود و نکته مهم در این شیوه این است که ارتباط برقرار شده یکطرفه می باشد و یک پردازه عملیات Write و پردازه دیگر عملیات Read را انجام میدهد ، تصویر زیر که در کتاب Windows 10 System Programming آمده است میتواند در درک این موضوع به ما کمک کند :
برای اینکه به یاد آورید در کجا این IPC را مشاهده کرده اید دستور زیر را مشاهده کنید :
C:\>dir | findstr "User" 03/02/2023 10:04 AM <DIR> Users
مثال بالا ، مثالی از Anonymous Pip است ، که مشاهده میکنید دستور dir ابتدا اسم فایل های موجود در یک مسیر را لیست میکند سپس آن ها را به | که یک Anonymous PIPEاست ، ارسال میکند ، تا خروجی dir به عنوان ورودی به findstr داده شود و به دنبال فایل هایی که در نام آن ها "User" هست بگردد.
ساخت PIPE با استفاده از تابع سیستمی CreatePipe انجام میشود که نگارش آن را میتواند مشاهده کنید :
c++ BOOL CreatePipe( [out] PHANDLE hReadPipe, [out] PHANDLE hWritePipe, [in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes, [in] DWORD nSize );
اگر به یاد داشته باشید ، ذکر کردیم PIPE یک ارتباط یکسویه را برای ما ایجاد میکند ، که یک پردازه عملیات Write و دیگری عملیات Read را انجام میدهد فلذا این تابع سیستمی دو HANDLE را به ما برمیگرداند که یکی از آن ها به منظور استفاده پردازه نویسنده و دیگری به منظور استفاده پردازه خواننده است .
نکته ای که باید توجه داشته باشید این است hReadPipe دسترسی readonly و hWritePipe دسترسی writeonly به Pipe دارند.
حال سوالی که مطرح میشود این است که عملیات های خواندن و نوشتن از PIPE توسط کدام توابع سیستمی انجام میشود ؟
پاسخ این سوال بسیار ساده است ، این عملیات ها با استفاده از توابع سیستمی ReadFile و WriteFile انجام میشود.
تصویر فوق یک Object مشترک بین دو پردازه را با Handle های یکسان 0xa8 نشان میدهد که نامی هم ندارد و این Object در حقیقت همان PIPE است که ما بین این دو پردازه ایجاد کرده ایم .
سوال بعدی که ایجاد میشود این است ، که پردازه فرزند ، چطور میتواند این object را در اختیار داشته باشد ، در حقیقت همانطور که در ادامه در کد مشاهده میکنید ، PIPE توسط پردازه پدر ساخته میشود و با پردازه فرزند به اشتراک گذاشته میشود اما چگونه این به اشتراک گذاری انجام میشود ؟
یکی از روش های به اشتراک گذاری Kernel Object ها ارث بری فرزند از پدر است ، در این سناریو ، یک یا چند kernel object در پردازه پدر موجود است ، که تصمیم میگیرد ، پردازه فرزند به تعداد مشخصی از آن ها دسترسی داشته باشد . برای این به اصطلاح ارث بری Parent Process باید گام هایی بردارد:
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // Make the returned handle inheritable. HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
در حقیقت در این حالت اگر پردازه A ، پردازه B را بسازد Inheritable handle های A با B به اشتراک گذاشته میشود و مشاهده میکنید که مقادریرشان یکسان میباشد ،جدول های زیر را با دقت مشاهده کنید ، این جدول ها از کتاب Windows Via Cpp اورده شده است :
در جدول فوق پردازه پدر دارای دو handle به kernel object های مختلفی است که شماره 1 به اشتراک با پردازه فرزند گذاشته نشده است اما در شماره 3 این اشتراک گذاری اتفاق افتاده است حال در child داریم :
اما نکته مهم این است که پردازه فرزند نمیداند کدام یک از Handle های جدول خود را به ارث برده است ! و ما این را با استفاده از Standard Output ها به آن میفهمانیم :
حال به کدهایی که برای پردازه های پدر و فرزند توسعه داده شده است دقت کنید :
پردازه پدر :
#include <windows.h> #include <iostream> using namespace std; int main() { HANDLE hReadPipe, hWritePipe; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; // create pipe if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) { cerr << "CreatePipe failed" << endl; return 1; } STARTUPINFO si = { 0 }; si.cb = sizeof(si); si.hStdOutput = hWritePipe; // redirect child process's stdout to the write end of the pipe si.dwFlags |= STARTF_USESTDHANDLES; PROCESS_INFORMATION pi = { 0 }; // create child process"D:\Windows_Via_CC\SelfCodes\IPC\IPCUsingUnnamedPipe\childProcess\x64\Debug\childProcess.exe" if (!CreateProcess(NULL, (LPSTR)"D:\\Windows_Via_CC\\SelfCodes\\IPC\\IPCUsingUnnamedPipe\\childProcess\\x64\\Debug\\childProcess.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { cerr << "CreateProcess failed" << endl; return 1; } // close the write end of the pipe in the parent process CloseHandle(hWritePipe); // read message from the read end of the pipe char buffer[1024]; DWORD bytesRead = 0; while (ReadFile(hReadPipe, buffer, 1024, &bytesRead, NULL) && bytesRead > 0) { std::cout.write(buffer, bytesRead); } // close the read end of the pipe in the parent process CloseHandle(hReadPipe); return 0; }
پردازه فرزند :
#include <windows.h> #include <iostream> using namespace std; int main() { // get the handle of the standard output HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); // write message to the standard output const char* message = "Hello from child process!" DWORD bytesWritten = 0; if (!WriteFile(hStdout, message, strlen(message), &bytesWritten, NULL)) { cerr << "WriteFile failed" << endl; return 1; } return 0; }
خروجی اجرای برنامه پدر :
Hello from child process!