مکانیزم ارتباط میان پردازه ای وسیله ای برای ارتباط میان پردازه ها هنگامی که در حال اجرا هستند فراهم می کند و بدین ترتیب امکان ارتباط با پردازه ای دیگر را خارج از رابطه ی والد-فرزندی ایجاد می کند. راه های متفاوتی برای ارتباط میان پردازه های والد-فرزند، پردازش های غیر مرتبط و حتی پردازش های روی ماشین های مختلف وجود دارد. در ابنجا ما انواع IPCها در سیستم عامل لینوکس را شرح داده و به بیان تفاوت های میان آن ها می پردازیم.
این سری مقالات از چهار بخش تشکیل شده است. در بخش اول مفاهیم IPC و مطالب تـئوریک مربوط به ارتباط میان پردازه ها بیان می شود. سپس در بخش دوم به بررسی امکانات فراهم شده در سیستم عامل لینوکس برای ارتباط میان پردازه ها و شرح توابع موجود می پردازیم. در بخش سوم به عنوان یک جمع بندی روش های IPC را با هم مقایسه کرده و تفاوت های آن ها را بیان می کنیم. در پایان هم چهار برنامه همکار با استفاده از یکی از روش های IPC می نویسیم.
شایان ذکر است که مطالب بخش اول عینا مانند مطالب موجود در فصل سوم کتاب Operating System Concepts نوشته Abraham Silberschatz است. همچنین برای تهیه مطالب بخش دوم، از فصل های سیزده و چهارده کتاب beginning Linux Programming نوشته Neil Matthew و Richard Stones استفاده شده است.
پردازه هایی که در سیستم عامل به طور همروند در حال اجرا هستند، ممکن است یا پردازه هایی مستقل و یا پردازه هایی همکار باشند. یک پردازه زمانی مستقل است که نتواند بر پردازه های دیگر که در سیستم عامل در حال اجرا هستند اثر بگذارد یا از آن ها تاثیر بپذیرد. هر پردازه ای که داده ای را با پردازه ای دیگر به اشتراک نمی گذارد، یک پردازه مستقل می باشد. یک پردازه در صورتی همکار است که بتواند بر پردازه های دیگر که درون سیستم عامل در حال اجرا هستند اثر بگذارد یا از آن ها تاثیر بپذیرد. به طور واضح، هر پردازه ای که داده ای را با پردازه های دیگر به اشتراک بگذارد یک پردازه همکار می باشد.
چندین دلیل برای فراهم کردن محیطی که اجازه همکاری پردازه ها را می دهد، وجود دارد:
پردازه های همکار به یک مکانیزم ارتباط میان پردازه ای نیاز دارند تا به آن ها اجازه تبادل داده و اطلاعات را بدهد. برای ارتباط میان پردازه ای دو مدل اساسی وجود دارد: حافظه اشتراکی و تبادل پیام . در مدل حافظه اشتراکی قسمتی از حافظه که قرار است توسط پردازه های همکار به اشتراک گذاشته شود، تعیین می شود. از این پس پردازه ها می توانند با خواندن و نوشتن داده در حافظه اشتراکی، اطلاعات مبادله کنند. در مدل تبادل پیام، برقراری ارتباط به وسیله پیام هایی است که بین پردازه های همکار جابجا می شود. این دو مدل ارتباطات در شکل زیر با هم مقایسه شده اند.
دو مدلی که هم اکنون ذکر کردیم در سیستم عامل ها رایج می باشند و بسیاری از سیستم ها هر دوی آن ها را پیاده سازی می کنند. تکنیک تبادل پیام برای جابجایی مقدار کمی از داده مفید می باشد، زیرا نیاز به پرهیز از هیچ برخوردی نیست. همچنین در یک سیستم توزیع شده، پیاده سازی تکنیک تبادل پیام راحت تر از تکنیک حافظه اشتراکی است. حافظه اشتراکی سریع تر از تبادل پیام است، زیرا تبادل پیام غالبا به وسیله فراخوانی های سیستم پیاده سازی می شود و به همین دلیل به زمان بیشتری نیاز دارند. (این زمان اضافی صرف عملیات درون کرنل می شود) در حافظه اشتراکی فراخوانی های سیستم تنها در زمان به ثبت رساندن حافظه اشتراکی مورد نیاز هستند. بعد از اینکه حافظه اشتراکی به ثبت رسید، با تمام دسترسی ها به صورت دسترسی های عادی به حافظه رفتار می شود و نیاز به کمکی از جانب کرنل نخواهیم داشت.
تحقیقات اخیر بر روی سیستم هایی با چندین هسته پردازشی نشان داده است که تبادل پیام کارایی بهتری را نسبت به حافظه اشتراکی بر روی چنین سیستم هایی فراهم می کند. حافظه اشتراکی از یک مساله رنج می برد و آن زمانی است که از حافظه نهان استفاده شود، زیرا داده های به اشتراک گذاری شده بین چندین کش جابجا می شوند و سازگار نگه داشتن آن ها مشکل می باشد. با افزایش تعداد هسته های پردازشی، تکنیک تبادل پیام برای ارتباط میان پردازه ای ترجیح داده می شود.
در این سیستم ها، پردازه هایی که با هم در ارتباط هستند باید یک ناحیه را به عنوان حافظه اشتراکی به ثبت برسانند. به طور معمول، ناحیه ی حافظه اشتراکی در فضای آدرس پردازه ای قرار دارد که قطعه حافظه اشتراکی را ایجاد کرده است. پردازه های دیگر که خواهان برقراری ارتباط با این قطعه حافظه اشتراکی هستند، باید آن را به فضای آدرس خود ضمیمه کنند. همان طور که می دانید سیستم عامل در حالت عادی، یک پردازه را از دسترسی به حافظه پردازه دیگر منع می کند.
حافظه اشتراکی نیازمند این است که دو یا چند پردازه برای برداشتن این منع با هم به توافق برسند. با این کار آن ها قادر به جابجا کردن اطلاعات به وسیله خواندن و نوشتن داده در ناحیه به اشتراک گذاری شده هستند. شکل داده ها و مکان قرارگیری آن ها به وسیله این پردازه ها تعیین می شود و تحت کنترل سیستم عامل نیست. پردازه ها همچنین مسئول اطمینان حاصل کردن از عدم نوشتن در یک مکان یکسان به طور همزمان هستند.
در بخش قبل نشان دادیم که چگونه پردازه های همکار در یک محیط حافظه اشتراکی می توانند با یکدیگر ارتباط برقرار کنند. این طرح نیازمند این است که پردازه ها یک ناحیه از حافظه را به اشتراک بگذارند و کد مورد نیاز برای دسترسی و تغییر حافظه اشتراکی باید به طور صریح توسط برنامه نویس نوشته شود. یک راه دیگر برای دستیابی به نتیجه ای مشابه، این است که سیستم عامل برای پردازه های همکار امکاناتی را فراهم کنند تا آن ها بتوانند از طریق امکان تبادل پیام با همدیگر ارتباط برقرار کنند.
تبادل پیام مکانیزمی را فراهم می کند که به پردازه ها اجازه برقراری ارتباط و همزمان کردن فعالیت هایشان را بدون به اشتراک گذاری فضای آدرسی مشابه می دهد. امکان تبادل پیام به طور خاص در محیط های توزیع شده که در آن پردازه های همکار بر روی کامپیوترهای گوناگون واقع شده اند، مفید می باشد.
مکانیزم تبادل پیام حداقل دو عملیات را فراهم می کند: (send (message و (recieve (message .
سایز پیامی که توسط یک پردازه فرستاده می شود می تواند ثابت یا متغیر باشد. پیاده سازی در سطح سیستم ، زمانی که تنها پیام های با سایز ثابت اجازه ارسال دارند، سر راست خواهد بود اما این محدودیت کار برنامه نویسی را سخت تر می کند. برعکس پیام های با سایز متغیر نیاز به پیاده سازی در سطح سیستم پیچده تری دارد اما کار برنامه نویسی ساده تر خواهد شد.
اگر پردازه های P و Q بخواهند با هم ارتباط برقرار کنند، باید پیام هایی را به یکدیگر بفرستند یا از یکدیگر دریافت کنند: [به همین دلیل] یک لینک ارتباطی باید بین آن ها وجود داشته باشد. در ادامه چندین روش برای پیاده سازی منطقی یک لینک و عملیات ()send()/receive آورده شده است:
در ادامه به مسائل مرتبط با هر یک از موارد بالا نگاه می اندازیم.
پردازه هایی که می خواهند با یکدیگر ارتباط برقرار کنند، باید راهی برای ارجاع به همدیگر داشته باشند. پردازه ها می توانند از ارتباط مستقیم یا غیر مستقیم استفاده کنند.تحت ارتباط مستقیم، هر پردازه ای که خواهان برقراری ارتباط است باید به طور صریح گیرنده یا فرستنده ارتباط را مشخص کند(نام گذاری کند). در این طرح، اعمال اولیه ()send و ()receive به صورت زیر تعریف می شوند:
یک لینک ارتباطی در این طرح دارای ویژگی های زیر است:
از اشکالات این طرح می توان به خاصیت پیمانه ای محدود آن اشاره کرد. با تغییر شناسه یک پردازه ناگزیر باید تمامی تعریفات پردازه های دیگر چک شود. تمامی ارجاعات به شناسه قدیم باید یافت شده و با شناسه جدید جایگزین شود. درحالت کلی چنین تکنیک هایی منجربه hard-coding می شود و [در نتیجه] باید شناسه ها به طور صریح تعیین شود. به همین جهت این تکنیک نسبت به تکنیک های غیر مستقیم، کمتر مورد پسند است.
در ارتباط غیر مسقیم ، ارسال و دریافت پیام از طریق صندوق های پستی یا پورت ها صورت می گیرد. یک صندوق پست را می توان در حالت انتزاعی به عنوان یک شی که پردازه ها می توانند در آن پیام بگذارند یا پیام ها را از آن حذف کنند، نمایش داد. هر صندوق پست یک شناسه یکتا دارد. یک پردازه می تواند با یک پردازه دیگر از طریق چندین صندوق پستی متفاوت ارتباط برقرار کند. به این نکته توجه داشته باشید که دو پردازه تنها زمانی قادر به برقراری ارتباط خواهند بود که یک صندوق پست به اشتراک گذاری شده داشته باشند. اعمال اولیه ()send و ()receive به صورت زیر تعریف می شوند:
در این طرح، یک لینک ارتباطی دارای ویژگی های زیر است:
حال فرض کنید که پردازه های P1 ، P2 و P3 همگی صندوق پستی A را به اشتراک گذاشته اند. پردازه ی P1 یک پیام را به A می فرستد، در حالی که P2 و P3 هر دو دستور receive() را به اجرا در می آورند. کدام پردازه پیامی که به وسیله A فرستاده شده است را دریافت می کند؟ جواب این سوال بستگی به این دارد که کدام یک از روش های زیر را اتخاذ کرده باشیم:
صندوق پست ممکن است در مالکیت پردازه یا سیستم عامل باشد. اگر صندوق پست در مالکیت پردازه باشد (در این حالت صندوق پست بخشی از فضای آدرس پردازه است.)، می توانیم بین مالک (که تنها قادر به دریافت پیام از طریق صندوق پستی است) و کابر (که تنها قادر به ارسال پیام به صندوق پستی است) تمایز قائل شویم. از آنجا که هر صندوق پست یک مالک یکتا دارد، دیگر استباهی در رابطه با این که کدام پردازه باید پیام ارسالی به صندوق پستی را دریافت کند، رخ نمی دهد. وقتی پردازه ای که مالک صندوق پست است خاتمه می یابد، صندوق پست نیز محو می شود. هر پردازه ای که متعاقبا پیامی به این صندوق پست ارسال می کند باید نسبت به عدم وجود صندوق پست مطلع شود.
در مقایسه، صندوق پستی که در مالکیت سیستم عامل است به خودی خود وجود خواهد شد. این صندوق پست مستقل است و به پردازه ی خاصی ضمیمه نمی شود. برای اینکار سیستم عامل باید مکانیزمی را فراهم آورد که به پردازه اجازه کارهای زیر را بدهد:
پردازه ای که صندوق پست را ایجاد می کند به طور پیش فرض مالک آن صندوق پست است. در ابتدا مالک تنها پردازه ای است که می تواند از طریق این صندوق پستی پیامهایی را دریافت کند. با این حال مالکیت و حق دریافت پیام از طریق فراخوانی های سیستمی مناسب قابل واگذاری به دیگر پرداز ها است. با این کار می توان برای هر صندوق پست چندین گیرنده تعیین کرد.
در مقاله قبل به بررسی سیستم های تبادل پیام و روش های پیاده سازی منطقی عملیات ()send و ()receive پرداختیم. سپس در قسمت 1.3.1 چگونگی ارجاع پردازه ها به همدیگر شرح داده شد. در همین راستا، در این مقاله ابتدا به بررسی مسائل دیگری که حول پیاده سازی عملیات ()send و ()receive وجود دارد می پردازیم (1.3.2 و 1.3.3) و سپس در قسمت های 1.4 و 1.5 دو مورد از دیگر روش های IPC شرح داده می شود.
همانطور که گفته شد ارتباط بین پردازه ها به وسیله اعمال اولیه ()send و ()receive انجام می شود. گزینه های طراحی متفاوتی برای پیاده سازی هر کدام از اعمال اولیه وجود دارد. تبادل پیام ممکن است بلوکه شونده (همزمان) یا غیر بلوکه شونده (غیر همزمان) باشند.
در برقراری ارتباط چه به صورت مستقیم و چه به صورت غیر مستقیم، پیام های مبادله شده به وسیله پردازه های شرکت کننده در ارتباط درون یک صف موقتی نگه داری می شوند. همچین صف هایی به سه طریق قابل پیاده سازی هستند:
گاهی اوقات به حالت ظرفیت صفر، سیستم پیام رسانی بدون بافر گفته می شود. دو حالت دیگر به عنوان سیستم هایی با بافرینگ اتوماتیک شناخته می شوند.
پایپ به عنوان یک مجرا عمل می کند و در نتیجه به دو پردازه اجازه رد و بدل اطلاعات را می دهد. پایپ ها یکی از اولین امکانات IPC سیستم های یونیکس اولیه بودند. پایپ ها با وجود محدودیت هایی که دارند یکی از ساده ترین راه ها را برای دو پردازه که می خواهند با هم ارتباط داشته باشند، فراهم می کنند. در پیاده سازی یک پایپ چهار مسئله باید در نظر گرفته شود:
در پایپ های اولیه یک پردازه داده ها را در یک طرف پایپ می نویسد و پردازه ی دیگر از طرف دیگر پایپ داده ها را می خواند. به عنوان نتیجه، پایپ های اولیه یک طرفه هستند. اگر نیاز به ارتباط دو طرفه باشد، باید از دو پایپ استفاده شود، به طوری که هر پایپ داده ها را در جهت های مختلف انتقال دهد. در سیستم های یونیکس پایپ های اولیه با استفاده از تابع ([]pipe(int fd ساخته می شوند. به این پایپ از طریق توصیفگرهای فایل [] int fd می توان دسترسی پیدا کرد. [fd[0 بخش خواندن و [fd[1 بخش نوشتن پایپ است. یونیکس با پایپ به عنوان یک فایل خاص برخورد می کند، در نتیجه می توانیم به پایپ ها از طریق فراخوانی های سیستمی () read و () write دسترسی پیدا کنیم.
به یک پایپ اولیه نمی توان خارج از پردازه ای که آن را ایجاد کرده است، دسترسی پیدا کرد. غالبا پردازه ی پدر یک پایپ را ایجاد کرده و از آن برای ارتباط با پردازه ی فرزندی که از طریق دستور () fork ایجاد می کند، استفاده می کند. همانطور که می دانید، پردازه فرزند فایل های باز را از پدر خود به ارث می برد. از آنجا که پایپ نوع خاصی از فایل است، پس فرزند پایپ را از پردازه پدر خود به ارث می برد. شکل زیر رابطه ی توصیف کننده فایل fd را با پردازه های پدر و فرزند نشان می دهد:
پایپ های اولیه با فراهم کردن یک مکانیزم ساده، به یک جفت پردازه اجازه برقراری ارتباط را می داد. اما پایپ های اولیه تنها زمانی وجود دارند که دو پردازه بخواهند با هم ارتباط برقرار کنند. در سیستم های یونیکس وقتی ارتباط به پایان رسید و پردازه ها خاتمه پیدا کردند، پایپ هم دیگر وجود نخواهد داشت.
named pipeها ابزار ارتباطی قدرتمندتری فراهم می کنند. در اینجا ارتباط می تواند دو جهته باشد و نیاز به وجود رابطه والد-فرزندی هم نیست. با برپا شدن یک named pipe چندین پردازه از آن می توانند برای ارتباط استفاده کنند. در حالت عادی یک named pipe چندین نویسنده دارد. به علاوه named pipeها حتی بعد از به پایان رسیدن پردازه های دخیل در ارتباط نیز وجود خواهند داشت.
در سیستم های یونیکس از named pipeها به عنوان FIFO یاد می شود. FIFOها در سیستم فایل به عنوان یک فایل عادی ظاهر می شوند. FIFO با استفاده از فراخوانی های سیستمی () mkfifo ساخته و بوسیله فراخوانی های سیستمی () open()، read و () write دستکاری می شوند. FIFO تا زمانی که به طور صریح از سیستم فایل حذف نشود، وجود خواهد داشت. با وجود اینکه FIFO اجازه ارتباط دو سویه را می دهد، اما تنها انتقال half-duplex مجاز می باشد. اگر داده ها نیاز به حرکت در هر دو جهت داشته باشند، معمولا از دو FIFO استفاده می شود. به علاوه پردازه های دخیل در ارتباط باید بر روی یک ماشین قرار داشته باشند.
حافظه اشتراکی یکی دیگر از امکانات IPC است که به دو پردازه ی نامرتبط این اجازه را می دهد که به یک حافظه منطقی مشابه دسترسی پیدا کنند. این تکنیک یک راه بسیار موثر برای انتقال داده بین دو پردازه در حال اجرا است.حافظه اشتراکی بازه ی خاصی از آدرس هاست که به وسیله IPC برای یک پردازه ایجاد و در فضای آدرس آن پردازه ظاهر می شود. پردازه های دیگر سپس می توانند این قطعه حافظه به اشتراک گذاشته شده را به فضای آدرس خودشان «ضمیمه» کنند. تمامی پردازه ها درست مثل اینکه حافظه به وسیله malloc تخصیص داده شده باشد، می-توانند به مکان های حافظه دسترسی داشته باشند. اگر یک پردازه درون حافظه اشتراکی بنویسد، تغییرات بالافاصله برای پردازه ی دیگری که دسترسی به حافظه اشتراکی مشابه دارد، قابل رویت خواهد بود.
همانطور که ذکر شد، حافظه اشتراکی یک روش کارآمد برای اشتراک و انتقال داده بین چندین پردازه فراهم می کند. حافظه اشتراکی خود امکانات همزمانی را برای ما فراهم نمی کند، به همین جهت شما معمولا نیاز به مکانیزم-های دیگری برای همزمان کردن دسترسی به حافظه اشتراکی دارید. برای نمونه، ممکن است شما از حافظه اشتراکی برای دسترسی بهینه به نواحی بزرگی از حافظه استفاده کنید و برای همزمان کردن دسترسی به حافظه، پیام های کوچکی را انتقال دهید. (نمونه ای از این انتقال پیام در ادامه آورده شده است.) هیچ امکان اتوماتیکی برای منع پردازه دوم از شروع عمل خواندن از حافظه قبل از اینکه پردازه اول عمل نوشتن را به پایان برساند وجود نخواهد داشت. این وظیفه برنامه نویس است که دسترسی را همزمان کند. در زیر لیست توابع موجود برای تکنیک حافظه اشتراکی آمده است:
#include <sys/shm.h> void *shmat(int shm_id, const void *shm_addr, int shmflg); int shmctl(int shm_id, int cmd, struct shmid_ds *buf); int shmdt(const void *shm_addr); int shmget(key_t key, size_t size, int shmflg);
شایان ذکر است که shm.h به طور اتوماتیک سرآیندهای systypes.h و sysipc.h را نیز شامل می شود.
حافظه اشتراکی به وسیله تابع shmget ایجاد می شود:
int shmget(key_t key, size_t size, int shmflg);
در زیر به شرح هر یک از پارامترهای این تابع می پردازیم:
در بحث حافظه اشتراکی، پرچم های جواز بسیار مفید هستند. زیرا به وسیله آنها می توان به پردازه هایی که توسط ایجاد کننده حافظه اشتراکی به وجود آمده اند اجازه نوشتن در حافظه را بدهیم، در حالی که دیگر پردازه ها (که به وسیله ایجاد کننده حافظه اشتراکی به وجود نیامده اند) تنها اجازه خواندن از حافظه را داشته باشند. با این کار دسترسی فقط-خواندنی کارآمدی به داده ها می توانیم ایجاد کنیم.
اگر حافظه اشتراکی با موفقیت ایجاد شود، یک عدد صحیح نامنفی که همان شناسه حافظه اشتراکی است، برگردانده می شود. در صورت شکست، مقدار 1- برگردانده می شود.
وقتی برای اولین بار یک قطعه حافظه اشتراکی را ایجاد می کنید، این قطعه حافظه قابل دسترس برای هیچ پردازه ای نیست. برای فعال کردن دسترسی به حافظه اشتراکی، باید آن را به فضای آدرس یک پردازه ضمیمه کنید. این کار به وسیله تابع shmat انجام می شود:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
در صورت فراخوانی موفقیت آمیز shmat، یک اشاره گر که به اولین بایت حافظه اشتراکی اشاره می کند، برگردانده می شود. در صورت شکست این تابع 1- را برمی گرداند.
تابع shmdt حافظه اشتراکی را از پردازه ی جاری جدا می کند. این تابع یک اشاره گر به حافظه (همان اشاره-گری که توسط shmat برگردانده شد) را به عنوان پارامتر دریافت می کند. در صورت موفقیت 0 و در صورت بروز خطا 1- برگردانده می شود. توجه کنید که جدا کردن حافظه اشتراکی باعث حذف آن نمی شود. این تابع تنها حافظه را از دسترس پردازه جاری خارج می کند.
با این تابع می توان بر روی حافظه اشتراکی کنترل داشت.
int shmctl(int shm_id, int command, struct shmid_ds *buf);
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; }
در صورت اجرای موفقیت آمیز این تابع، 0 و در صورت شکست 1- برگردانده می شود.
با استفاده از تابع msgget ، یک صف پیام را ایجاد و به آن دسترسی پیدا می کنید.
int msgget(key_t key, int msgflg);
برنامه باید یک مقدار را برای key تعیین کند. در واقع این مقدار برای یک صف پیام مشخص، یک نام تعیین می کند. پارامتر دوم یعنی msgflg، از نه پرچم جواز تشکیل شده است. برای ایجاد یک صف پیام جدید، باید یک بیت خاص که به وسیله IPC__CREAT مشخص شده است با مجوزها یای بیتی شوند. (برای نمونه به این صورت عمل می کنیم: 0666 |IPC__CREAT). شایان ذکر است که اگر کلید یک صف موجود را به این تابع فرستاده و IPC__CREAT را هم ست کنیم، خطایی رخ نمی دهد. مقدار IPC__CREAT در صورت وجود صف به طور اتوماتیک نادیده گرفته می شود.در صورت اجرای موفقیت آمیز این تابع، یک مقدار مثبت که شماره شناسه صف است، برگردانده می شود. در صورت شکست 1- برگرانده می شود.
این تابع به شما اجازه اضافه کردن یک پیام به صف پیام ها را می دهد.
int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);
شما باید بر روی ساختار پیام خود دو محدودیت را اعمال کنید. اول اینکه باید کوچک تر از حد سیستم باشد و دوم اینکه باید با long int شروع شود. به هنگام استفاده از پیام ها، بهتر است ساختار پبام های خود را به صورت زیر تعریف کنید:
struct my_message { long int message_type; // The data you wish to transfer }
چون گیرنده پیام از message__type استفاده می کند، شما نمی توانید از آن در ساختمان صرف نظر کنید و ساختمان داده تعریف شده به وسیله شما حتما باید آن را شامل شود. همچنین بهتر است آن را مقدار دهی اولیه کنید. به وسیله این عضو از ساختمان در حقیقت می توانیم پیام های خود را اولویت دهی کنیم.
در صورت موفقیت تابع مقدار 0 و در صورت شکست مقدار 1- را برمی گرداند. در صورت فراخوانی موفقیت آمیز، یک کپی از داده های پیام برداشته می شود و درون صف پیام گذاشته می شود.
تابع msgrcv پیام ها را از یک صف پیام بازیابی می کند.
int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
در صورت موفقیت، تابع msgrcv تعداد بایت های قرار گرفته درون بافر را بر می گرداند؛ همچنین پیام درون بافری که کاربر تخصیص داده است (همان فضایی که پارامتر msg__ptr به آن اشاره می کند) کپی شده و سپس از صف پیام حذف می شود. در صورت بروز خطا 1- برگردانده می شود.
عملکرد این تابع بسیار مشابه تابع shmctl در حافظه اشتراکی است. به همین جهت تنها به ذکر نام آن بسنده می کنیم.
int msgctl(int msqid, int command, struct msqid_ds *buf);
تابع pipe وسیله ای برای انتقال داده بین دو برنامه فراهم می کند. این تابع به صورت زیر می باشد:
#include <unistd.h> int pipe(int file_descriptor[2]);
به تابع pipe (یک اشاره گر به) یک آرایه از نوع صحیح فرستاده می شود. تابع این آرایه را با دو توصیف کننده فایل پر کرده و صفر را به عنوان مقدار برگشتی، برمی گرداند. در صورت شکست 1- برگردانده می شود.توصیف کننده های فایل به طریق خاصی به هم متصل شده اند. هر داده ای که در [file_descriptor[1 نوشته شده است، از طریق [file_descriptor[0 قابل خواندن می باشد. داده ها به صورت اولین ورودی-اولین خروجی پردازش می شوند.
به این معنا که اگر بایت های 1، 2 و 3 را در [file__descriptor[1 بنویسید، خواندن از [file__descriptor[0 خروجی 1، 2 و 3 را تولید خواهد کرد.توجه کنید که در اینجا ما با file descriptor ها (توصیفگرهای فایل) کار می کنیم نه file stream ها، در نتیجه برای دستیابی به داده ها به جای استفاده از fread و fwrite باید از فراخوانی های سیستمی سطح پایین read و write استفاده کنید.در زیر به طور خلاصه فراخوانی های read و write معرفی شده اند.
پایه ای ترین و متداول ترین مکانیزم مورد استفاده برای خواندن، فراخوانی سیستمی ()read می باشد که به صورت زیر تعریف شده است:
#include <unistd.h> ssize_t read (int fd, void *buf, size_t len);
هر فراخوانی این تابع len بایت از آفست کنونی فایلی که به وسیله fd به آن اشاره می شود را می خواند. سپس محتویات خوانده شده درون حافظه ای که بوسیله buf به آن اشاره می شود، قرار داده می شود. در صورت موفقیت تعداد بایت های نوشته شده درون buf برگردانده می شود. در صورت بروز خطا، 1- برگردانده می شود. مکان فایل(آفست فایل) هم به اندازه تعداد بایت های خوانده شده، جلو برده می شود.
پایه ای ترین و متداول ترین فراخوانی سیستمی مورد استفاده برای نوشتن، ()write می باشد. ()write مشابه ()read است و به صورت زیر تعریف شده است.
#include <unistd.h> ssize_t write (int fd, const void *buf, size_t count);
هر فراخوانی این تابع، count بایت از buf را درون فایلی که توصیف کننده فایل fd به آن اشاره می کند، می نویسد. در صورت موفقیت، تعداد بایت های نوشته شده برگردانده شده و همچنین مکان فایل (مکان فعلی فایل - آفست) آپدیت می شود. در صورت بروز خطا 1- برگردانده می شود.
در قسمت قبل تنها قادر به انتقال داده بین برنامه های مرتبط به هم بودیم، یعنی برنامه هایی که از یک جد مشترک نشات می گرفتند. این نوع از ارتباط غالبا مفید نخواهد بود، زیرا در بیشتر اوقات ما خواهان تبادل داده بین برنامه های نامرتبط هستیم.این کار به وسیله FIFOها که گاهی اوقات به آن ها named pipe هم گفته می شود، انجام می شود. named pipeها با یک نام درون سیستم فایل قرار می گیرند اما رفتاری مشابه پایپ های بدون نام در قسمت قبل دارند.درون برنامه ها، FIFO به صورت زیر ایجاد می شود
int mknod(const char *filename, mode_t mode);
برای باز کردن یک FIFO یک محدودیت وجود دارد و آن این است که برنامه نباید با تنظیم mode به صورت O__RDRW ، یک FIFO را هم برای خواندن و هم برای نوشتن باز کند. این محدودیت عاقلانه به نظر می رسد، زیرا شما معمولا از یک FIFO تنها برای انتقال داده در یک جهت استفاده می کنید، پس نیازی به تنظیم mode به صورت O__RDRW نیست.اگر می خواهید در دو جهت داده انتقال دهید، بهتر است از دو FIFO استفاده کنید، و یا به طور صریح جهت جریان داده را با بسته و دوباره باز کردن FIFO تغییر دهید. (البته این کار زیاد متداول نیست)
تفاوت دیگری که در باز کردن یک FIFO با یک فایل عادی وجود دارد در استفاده از open__flag (دومین پارامتر تابع open) با گزینه O__NONBLOCK است. استفاده از این حالت نه تنها باعث تغییر در چگونگی پردازش فراخوانی open می شود، بلکه در نحوه پردازش درخواست های read و write هم تغییر ایجاد می کند.چهار ترکیب مختلف برای پرچم های O__RDONLY ، O__WRONLY و O__NONBLOCK وجود دارد. حال به ترتیب به هر یک از آن ها می پردازیم.
open(const char *path, O_RDONLY);
در این حالت فراخوانی open بلوکه خواهد شد و تا زمانی که یک پردازه، FIFO مشابه را برای نوشتن باز نکرده است، برنخواهد گشت.
open(const char *path, O_RDONLY | O_NONBLOCK);
در این حالت فراخوانی FIFO موفقیت آمیز بوده و بلافاصله بر می گردد، حتی اگر FIFO توسط پردازه ی دیگری برای نوشتن باز نشده باشد.
open(const char *path, O_WRONLY);
در این حالت فراخوانی open تا زمانی که یک پردازه ی دیگر FIFO مشابه را برای نوشتن باز نکرده باشد، بلوکه خواهد شد.
open(const char *path, O_WRONLY | O_NONBLOCK);
این فراخوانی بلافاصله برمی گردد، اما اگر هیچ پردازه ای این FIFO را برای خواندن باز نداشته باشد، فراخوانی open با 1- برمی گردد و FIFO باز نخواهد شد. اگر پردازه ای FIFO را برای خواندن باز داشته باشد، از توصیفگر فایل برگشتی می توان برای نوشتن در FIFO استفاده کرد.
در حافظه اشتراکی پردازها یک فضای آدرس را به اشتراک می گذارند و اعمال خواندن و نوشتن را از طریق فضای آدرس به اشتراک گذاشته انجام می دهند. در این روش پردازه ها خود مسئول همزمان کردن دسترسی به قطعه حافظه اشتراکی هستند اما تبادل پیام مکانیزمی را فراهم می کند که به پردازه ها اجازه برقراری ارتباط و همزمان کردن فعالیت هایشان را بدون به اشتراک گذاری فضای آدرسی مشابه می دهد. همچنین در روش تبادل پیام امکان تبادل اطلاعات در محیط های توزیع شده وجود دارد. روش حافظه اشتراکی تا حدی سریع ترین روش برای ارتباط پردازه ها است، اما راحتی کار با این روش به اندازه بقیه روش ها نیست.
پایپ یک مکانزیم ساده برای انتقال اطلاعات به صورت همگام بین چندین پردازه فراهم می کند. پایپ ها خود به دو دسته تقسیم می شوند: anonymous pipe و named pipe . از anonymous pipe ها تنها می توان بین پردازه های مرتبط استفاده کرد. مثلا بین پردازه ها والد-فرزند یا دو زیر پردازه. named pipe ها یک مدخل درون درخت فایل های سیستم عامل لینوکس ایجاد می کنند، به همین جهت می توان از آن ها بین برنامه ها نامرتبط نیز استفاده کرد.
زمان انتقال اطلاعات در pipe صرف نظر از مقدار داده انتقال داده شده، بدون تغییر باقی می ماند. برای برپاسازی یک پایپ مقداری زمان صرف می شود اما بعد از برپاسازی، زمان مورد نیاز برای انتقال داده صرف نظر از مقدار داده، مشابه هم است. اما با افزایش بایت های انتقالی توسط حافظه اشتراکی، زمان انتقال اطلاعات نیز افزایش می یابد. پس وقتی نیاز به انتقال مقدار زیادی داده داریم، تکنیک خط لوله ترجیح داده می شود.
وقتی مقدار داده انتقال داده شده کم تر از 4600 بایت باشد، حافظه اشتراکی مزیت های واضحی را در انتقال اطلاعات با سرعت بالا دارد. اما وقتی مقدار داده ارسالی از 4600 بایت فراتر می رود، این برتری از بین می رود چون در اینگونه موارد برای همزمان کردن دسترسی به قطعه حافظه اشتراکی، باید از ابزار های همزمانی پردازش مانند سمافورها استفاده کنیم؛ در نتیجه کارایی ارتباط در روش حافظه اشتراکی نسبت به خط لوله ضعیف تر می شود.
برای تهیه قسمت های 1 الی 6 از این سری مقالات از مراجع زیر استفاده شده است.
References
1.Silberschatz, Abraham, Galvin, Peter Baer and Gagne, Greg. Operating System Concepts. s.l. : John Wiley & Sons, Inc., 2013. pp. 122-147. 978-1-118-06333-0.
2.Matthew, Neil and Stones, Richard. beginning Linux Programming. 4th. s.l. : Wiley Publishing, Inc., 2008. pp. 526-605. 978-0-470-14762-7.
3.Love, Robert. Linux System Programming. 2nd. s.l. : O’Reilly Media, Inc., 2013. pp. 32-37. 978-1-449-33953-1.
4. یک مثال
با استفاده از یکی از روش های IPC، چهار برنامه به گونه ای می نویسیم که:
برای پیاده سازی این مثال، از تکنیک حافظه اشتراکی استفاده می کنیم.
توجه: در هر چهار برنامه ما یک ساختمان به صورت زیر تعریف کرده و در بدنه برنامه یک شی از آن می سازیم. این شی به حافظ اشتراکی ایجاد شده اشاره خواهد کرد. (در حقیقت محتوای اعضای این ساختمان، همان محتوای حافظه اشتراکی می باشد). به وسیله همین ساختمان ما می توانیم نوعی از همزمانی را نیز پیاده سازی کنیم. در این ساختمان به وسیله turn ، بین پردازه ها همزمانی ایجاد می کنیم و someText هم نشانگر محتوایی است برنامه اول آن را از فایل می خواند و برنامه های بعدی بر روی آن تغیرات اعمال می کنند.
struct shared_space{ int turn; char someText[text_size]; };
توجه: هر جهار برنامه بعد از اتمام کار خود، حافظه اشتراکی را از فضای آدرس خود جدا می کنند. آخرین برنامه، یعنی برنامه چهارم علاوه بر جدا کردن حافظه اشتراکی آن را حذف کرده و فضای اختصاص داده شده به آن را به سیستم برمی گرداند.
در این برنامه ابتدا باید حافظه اشتراکی ایجاد شود. سایز مورد نیاز برای حافظه اشتراکی همان سایز ساختمان ذکر شده در بالا خواهد بود. این کار به صورت زیر انجام می شود:
shmId=shmget((key_t)6543,sizeof(struct shared_space),0666|IPC_CREAT);
در ادامه باید یک شی از ساختمان shared__space ساخته شود و سپس باید کاری کنیم که این شی به حافظه اشتراکی اشاره کند. این کار به صورت زیر انجام می شود:
struct shared_space *shMem; shMem=(struct shared_space*)shmAdr;
در پایان باید فایل ورودی را باز کرده، محتوای آن را خوانده و درون حافظه اشتراکی بریزیم. این مراحل به صورت زیر انجام می شوند.
char bf[text_size]; FILE *myFile=fopen("db.txt","r"); fgets(bf,text_size,myFile); strncpy(shMem->someText,bf,sizeof(shMem->someText));
در ادامه سورس کامل برنامه اول آورده شده است.
#include "unistd.h" #include "stdlib.h" #include "stdio.h" #include "string.h" #include "sys/shm.h" #define text_size 2048 struct shared_space{ int turn; char someText[text_size]; }; void main() { int shmId; struct shared_space *shMem; void* shmAdr=NULL; shmId=shmget((key_t)6543,sizeof(struct shared_space),0666|IPC_CREAT); if(shmId!=-1) { printf("prog 1 ::: shared memory id: %d\n",shmId); shmAdr=shmat(shmId,NULL,0); printf("prog 1 ::: shared memory attached at %X\n",(int)shmAdr); shMem=(struct shared_space*)shmAdr; shMem->turn=0; while(1) { if(shMem->turn==0) { char bf[text_size]; FILE *myFile=fopen("db.txt","r"); fgets(bf,text_size,myFile); printf("prog 1 ::: file content = %s\n",bf); strncpy(shMem->someText,bf,sizeof(shMem->someText)); shMem->turn=1; break; } } } else printf("prog 1 ::: failed to create shared memory\n"); if(shmdt(shmAdr)!=-1) printf("prog 1 ::: shared memory detached\n\n"); else printf("prog 1 ::: failed to delete shared memory\n"); }
بیشتر قسمت های این برنامه همانند برنامه اول است، با این تفاوت که در این برنامه باید به محتوای حافظه اشتراکی یا بهتر است بگوییم رشته قرار گرفته در عضو someText از ساختمان shared__space دسترسی پیدا کنیم و به کد اسکی هر کاراکتر 2 واحد اضافه کنیم. این برنامه زمانی اجازه تغییر در حافظه اشتراکی را دارد که مقدار عضو turn از ساختمان shared_space برابر با 1 باشد؛ در غیر این صورت برنامه برای مدت زمان مشخصی به خواب می رود.برای خواندن حافظه اشتراکی، اضافه کردن 2 واحد به کد اسکی هر کاراکتر و اعمال تغییرات به صورت زیر عمل می کنیم:
if(shMem->turn==1) { char temp[text_size]; strcpy(temp,shMem->someText); //خواندن حافظه اشتراکی int i=0; while(1) { if(i!=strlen(temp)) { temp[i]+=2; //اضافه کردن 2 واحد به کد اسکی هر کاراکتر i++; } else break; } strcpy(shMem->someText,temp); //اعمال تغییرات در حافظه اشتراکی }
در ادامه سورس کامل برنامه دوم آورده شده است (هدر فایل ها و نحوه تعریف ساختمان، مشابه برنامه اول است. به همین جهت در برنامه های آتی، از ذکر دوباره آن خودداری کرده ایم):
void main() { int shmId; struct shared_space *shMem; void* shmAdr=NULL; shmId=shmget((key_t)6543,sizeof(struct shared_space),0666|IPC_CREAT); if(shmId!=-1) { printf("prog 2 ::: shared memory id: %d\n",shmId); shmAdr=shmat(shmId,NULL,0); printf("prog 2 ::: shared memory attached at %X\n",(int)shmAdr); shMem=(struct shared_space*)shmAdr; while(1) { if(shMem->turn==1) { char temp[text_size]; strcpy(temp,shMem->someText); int i=0; while(1) { if(i!=strlen(temp)) { temp[i]+=2; i++; } else break; } printf("prog 2 ::: shared memory content: %s\n",shMem->someText); strcpy(shMem->someText,temp); printf("prog 2 ::: modified shared memory: %s\n",temp); shMem->turn=2; break; } else { srand(time(NULL)); printf("it's not prog 2's turn\n"); sleep(1+rand()%10); } } } else printf("prog 2 ::: failed to create shared memory\n"); if(shmdt(shmAdr)!=-1) printf("prog 2 ::: shared memory detached\n",getpid()); }
اسکلت این برنامه نیز مانند برنامه های قبل می باشد، به جز این که در اینجا حروف کوچک را به بزرگ و حروف بزرگ را به کوچک تبدیل می کنیم. اگر کد اسکی بین 64 و 91 باشد یعنی اینکه کاراکتر یک حررف بزرگ است و برای تبدیل کردن آن به حرف کوچک کافی است که 32 واحد به آن اضافه کنیم. اگر کد اسکی بین 96 و 123 باشد یعنی اینکه کاراکتر یک حرف کوچک است و برای تبدیل کردن آن به حرف بزرگ کافی است 32 واحد از آن کم کنیم. لازم به ذکر است که این برنامه زمانی اجازه تغییر در حافظه اشتراکی را دارد که مقدار turn از ساختمان shared__space برابر 2 باشد؛ در غیر اینصورت برنامه برای مدت زمان مشخصی به خواب می رود. در پایان تغییرات صورت گرفته را در حافظه اشتراکی اعمال می کنیم.در زیر کد مورد نیاز برای موارد ذکر شده در بالا آورده شده است:
if(shMem->turn==2) { char temp[text_size]; strcpy(temp,shMem->someText); //خواندن حافظه اشتراکی int i=0; while(1) { if(i!=strlen(temp)) { if(temp[i]>64&&temp[i]<91)temp[i]+=32; //تبدیل حروف بزرگ به کوچک else if(temp[i]>96&&temp[i]<123)temp[i]-=32; //تبدیل حروف کوچک به بزرگ i++; } else break; } strcpy(shMem->someText,temp); //اعمال تغییرات در حافظه اشتراکی }
در ادامه سورس برنامه سوم به صورت کامل آورده شده است.
void main() { int shmId; struct shared_space *shMem; void* shmAdr=NULL; shmId=shmget((key_t)6543,sizeof(struct shared_space),0666|IPC_CREAT); if(shmId!=-1) { printf("prog 3 ::: shared memory id: %d\n",shmId); shmAdr=shmat(shmId,NULL,0); printf("prog 3 ::: shared memory attached at %X\n",(int)shmAdr); shMem=(struct shared_space*)shmAdr; while(1) { if(shMem->turn==2) { char temp[text_size]; strcpy(temp,shMem->someText); int i=0; while(1) { if(i!=strlen(temp)) { if(temp[i]>64&&temp[i]<91)temp[i]+=32; else if(temp[i]>96&&temp[i]<123)temp[i]-=32; i++; } else break; } printf("prog 3 ::: shared memory content: %s\n",shMem->someText); printf("prog 3 ::: modified shared memory: %s\n",temp); strcpy(shMem->someText,temp); shMem->turn=3; break; } else { srand(time(NULL)); printf("it's not prog 3's turn\n"); sleep(1+rand()%10); } } } else printf("prog 3 ::: failed to create shared memory\n"); if(shmdt(shmAdr)!=-1) printf("prog 3 ::: shared memory detached\n",getpid()); }
وظیفه این برنامه تنها خواندن حافظه اشتراکی و نوشتن محتوای آن در یک فایل با پسوند "output." است. در اینجا منظور از محتوای حافظه اشتراکی همان رشته قرار گرفته در عضو someText از ساختمان shared__space می باشد. البته برنامه زمانی که مقدار turn برابر با 3 باشد اجازه نوشتن در فایل را پیدا می کند.این کار به صورت زیر انجام می شود.
if(shMem->turn==3) { char bf[text_size]; strncpy(bf,shMem->someText,sizeof(shMem->someText)); //خواندن محتوای حافظه اشتراکی FILE *myFile=fopen("db.output","w"); //باز کردن یک فایل (در صورت عدم وجود ایجاد یک فایل جدید) fputs(bf,myFile); //نوشتن محتوای حافظه اشتراکی در فایل }
در ادامه سورس کامل برنامه چهارم آورده شده است.
void main() { int shmId; struct shared_space *shMem; void* shmAdr=NULL; shmId=shmget((key_t)6543,sizeof(struct shared_space),0666|IPC_CREAT); if(shmId!=-1) { printf("prog 4 ::: shared memory id: %d\n",shmId); shmAdr=shmat(shmId,NULL,0); printf("prog 4 ::: shared memory attached at %X\n",(int)shmAdr); shMem=(struct shared_space*)shmAdr; while(1) { if(shMem->turn==3) { char bf[text_size]; strncpy(bf,shMem->someText,sizeof(shMem->someText)); printf("prog 4 ::: (write to a file) shared memory content = %s\n",bf); FILE *myFile=fopen("db.output","w"); fputs(bf,myFile); //shMem->turn=1; break; } } } else printf("prog 4 ::: failed to create shared memory\n"); if(shmdt(shmAdr)!=-1) printf("prog 4 ::: shared memory detached\n",getpid()); if(shmctl(shmId,IPC_RMID,0)!=-1) printf("prog 4 ::: shared memory deleted...\n\n"); else printf("prog 4 ::: failed to delete shared memory\n"); }
نمونه اجرای برنامه
زمان پاسخ گویی روز های شنبه الی چهارشنبه ساعت 9 الی 18
فقط به موضوعات مربوط به محصولات آموزشی و فروش پاسخ داده می شود