IPC چیست؟ معرفی ارتباط بین پردازه ای در لینوکس

مکانیزم ارتباط میان پردازه ای وسیله ای برای ارتباط میان پردازه ها هنگامی که در حال اجرا هستند فراهم می کند و بدین ترتیب امکان ارتباط با پردازه ای دیگر را خارج از رابطه ی والد-فرزندی ایجاد می کند. راه های متفاوتی برای ارتباط میان پردازه های والد-فرزند، پردازش های غیر مرتبط و حتی پردازش های روی ماشین های مختلف وجود دارد. در ابنجا ما انواع IPCها در سیستم عامل لینوکس را شرح داده و به بیان تفاوت های میان آن ها می پردازیم.

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

این سری مقالات از چهار بخش تشکیل شده است. در بخش اول مفاهیم IPC و مطالب تـئوریک مربوط به ارتباط میان پردازه ها بیان می شود. سپس در بخش دوم به بررسی امکانات فراهم شده در سیستم عامل لینوکس برای ارتباط میان پردازه ها و شرح توابع موجود می پردازیم. در بخش سوم به عنوان یک جمع بندی روش های IPC را با هم مقایسه کرده و تفاوت های آن ها را بیان می کنیم. در پایان هم چهار برنامه همکار با استفاده از یکی از روش های IPC می نویسیم.

شایان ذکر است که مطالب بخش اول عینا مانند مطالب موجود در فصل سوم کتاب Operating System Concepts نوشته Abraham Silberschatz است. همچنین برای تهیه مطالب بخش دوم، از فصل های سیزده و چهارده کتاب beginning Linux Programming نوشته Neil Matthew و Richard Stones استفاده شده است.

بررسی مفاهیم IPC

1.1. ارتباط میان پردازه ای

پردازه هایی که در سیستم عامل به طور همروند در حال اجرا هستند، ممکن است یا پردازه هایی مستقل و یا پردازه هایی همکار باشند. یک پردازه زمانی مستقل است که نتواند بر پردازه های دیگر که در سیستم عامل در حال اجرا هستند اثر بگذارد یا از آن ها تاثیر بپذیرد. هر پردازه ای که داده ای را با پردازه ای دیگر به اشتراک نمی گذارد، یک پردازه مستقل می باشد. یک پردازه در صورتی همکار است که بتواند بر پردازه های دیگر که درون سیستم عامل در حال اجرا هستند اثر بگذارد یا از آن ها تاثیر بپذیرد. به طور واضح، هر پردازه ای که داده ای را با پردازه های دیگر به اشتراک بگذارد یک پردازه همکار می باشد.

چندین دلیل برای فراهم کردن محیطی که اجازه همکاری پردازه ها را می دهد، وجود دارد:

  • اشتراک اطلاعات: چندین کاربر ممکن است به یک قطعه اطلاعاتی خاص علاقه داشته باشند (به عنوان نمونه، یک فایل به اشتراک گذاری شده)، به همین دلیل باید محیطی را فراهم آوریم که اجازه دسترسی همروند به چنین اطلاعاتی را بدهد.
  • تسریع محاسبات: اگر می خواهیم که یک وظیفه سریع تر اجرا شود، باید آن را به زیر وظیفه هایی بشکنیم؛ هر کدام از این زیر وظیفه ها به طور موزای با دیگران اجرا می شوند. توجه داشته باشید که چنین تسریعی تنها زمانی قابل دسترسی است که کامپیوتر چندین هسته پردازشی داشته باشد.
  • پیمانه ای بودن: ممکن است بخواهیم که سیستم را به سبک پیمانه ای بسازیم، یعنی عملکردهای سیستم را به چندین پردازه و نخ تقسیم کنیم.
  • راحتی: یک کاربر ممکن است در یک زمان بر روی چندین وظیفه کار کند. به عنوان نمونه کاربر ممکن است در حال ویرایش یک متن، گوش دادن به موزیک و کامپایل یک برنامه به طور موازی باشد.

پردازه های همکار به یک مکانیزم ارتباط میان پردازه ای نیاز دارند تا به آن ها اجازه تبادل داده و اطلاعات را بدهد. برای ارتباط میان پردازه ای دو مدل اساسی وجود دارد: حافظه اشتراکی و تبادل پیام . در مدل حافظه اشتراکی قسمتی از حافظه که قرار است توسط پردازه های همکار به اشتراک گذاشته شود، تعیین می شود. از این پس پردازه ها می توانند با خواندن و نوشتن داده در حافظه اشتراکی، اطلاعات مبادله کنند. در مدل تبادل پیام، برقراری ارتباط به وسیله پیام هایی است که بین پردازه های همکار جابجا می شود. این دو مدل ارتباطات در شکل زیر با هم مقایسه شده اند.

وب سایت توسینسو

دو مدلی که هم اکنون ذکر کردیم در سیستم عامل ها رایج می باشند و بسیاری از سیستم ها هر دوی آن ها را پیاده سازی می کنند. تکنیک تبادل پیام برای جابجایی مقدار کمی از داده مفید می باشد، زیرا نیاز به پرهیز از هیچ برخوردی نیست. همچنین در یک سیستم توزیع شده، پیاده سازی تکنیک تبادل پیام راحت تر از تکنیک حافظه اشتراکی است. حافظه اشتراکی سریع تر از تبادل پیام است، زیرا تبادل پیام غالبا به وسیله فراخوانی های سیستم پیاده سازی می شود و به همین دلیل به زمان بیشتری نیاز دارند. (این زمان اضافی صرف عملیات درون کرنل می شود) در حافظه اشتراکی فراخوانی های سیستم تنها در زمان به ثبت رساندن حافظه اشتراکی مورد نیاز هستند. بعد از اینکه حافظه اشتراکی به ثبت رسید، با تمام دسترسی ها به صورت دسترسی های عادی به حافظه رفتار می شود و نیاز به کمکی از جانب کرنل نخواهیم داشت.

تحقیقات اخیر بر روی سیستم هایی با چندین هسته پردازشی نشان داده است که تبادل پیام کارایی بهتری را نسبت به حافظه اشتراکی بر روی چنین سیستم هایی فراهم می کند. حافظه اشتراکی از یک مساله رنج می برد و آن زمانی است که از حافظه نهان استفاده شود، زیرا داده های به اشتراک گذاری شده بین چندین کش جابجا می شوند و سازگار نگه داشتن آن ها مشکل می باشد. با افزایش تعداد هسته های پردازشی، تکنیک تبادل پیام برای ارتباط میان پردازه ای ترجیح داده می شود.

1.2. سیستم های حافظه اشتراکی چیست؟

در این سیستم ها، پردازه هایی که با هم در ارتباط هستند باید یک ناحیه را به عنوان حافظه اشتراکی به ثبت برسانند. به طور معمول، ناحیه ی حافظه اشتراکی در فضای آدرس پردازه ای قرار دارد که قطعه حافظه اشتراکی را ایجاد کرده است. پردازه های دیگر که خواهان برقراری ارتباط با این قطعه حافظه اشتراکی هستند، باید آن را به فضای آدرس خود ضمیمه کنند. همان طور که می دانید سیستم عامل در حالت عادی، یک پردازه را از دسترسی به حافظه پردازه دیگر منع می کند.

حافظه اشتراکی نیازمند این است که دو یا چند پردازه برای برداشتن این منع با هم به توافق برسند. با این کار آن ها قادر به جابجا کردن اطلاعات به وسیله خواندن و نوشتن داده در ناحیه به اشتراک گذاری شده هستند. شکل داده ها و مکان قرارگیری آن ها به وسیله این پردازه ها تعیین می شود و تحت کنترل سیستم عامل نیست. پردازه ها همچنین مسئول اطمینان حاصل کردن از عدم نوشتن در یک مکان یکسان به طور همزمان هستند.

1.3. سیستم های تبادل پیام چه هستند؟

در بخش قبل نشان دادیم که چگونه پردازه های همکار در یک محیط حافظه اشتراکی می توانند با یکدیگر ارتباط برقرار کنند. این طرح نیازمند این است که پردازه ها یک ناحیه از حافظه را به اشتراک بگذارند و کد مورد نیاز برای دسترسی و تغییر حافظه اشتراکی باید به طور صریح توسط برنامه نویس نوشته شود. یک راه دیگر برای دستیابی به نتیجه ای مشابه، این است که سیستم عامل برای پردازه های همکار امکاناتی را فراهم کنند تا آن ها بتوانند از طریق امکان تبادل پیام با همدیگر ارتباط برقرار کنند.

تبادل پیام مکانیزمی را فراهم می کند که به پردازه ها اجازه برقراری ارتباط و همزمان کردن فعالیت هایشان را بدون به اشتراک گذاری فضای آدرسی مشابه می دهد. امکان تبادل پیام به طور خاص در محیط های توزیع شده که در آن پردازه های همکار بر روی کامپیوترهای گوناگون واقع شده اند، مفید می باشد.

مکانیزم تبادل پیام حداقل دو عملیات را فراهم می کند: (send (message و (recieve (message .

سایز پیامی که توسط یک پردازه فرستاده می شود می تواند ثابت یا متغیر باشد. پیاده سازی در سطح سیستم ، زمانی که تنها پیام های با سایز ثابت اجازه ارسال دارند، سر راست خواهد بود اما این محدودیت کار برنامه نویسی را سخت تر می کند. برعکس پیام های با سایز متغیر نیاز به پیاده سازی در سطح سیستم پیچده تری دارد اما کار برنامه نویسی ساده تر خواهد شد.

اگر پردازه های P و Q بخواهند با هم ارتباط برقرار کنند، باید پیام هایی را به یکدیگر بفرستند یا از یکدیگر دریافت کنند: [به همین دلیل] یک لینک ارتباطی باید بین آن ها وجود داشته باشد. در ادامه چندین روش برای پیاده سازی منطقی یک لینک و عملیات ()send()/receive آورده شده است:

  • ارتباط مستقیم و غیر مستقیم
  • ارتباط همزمان یا غیرهمزمان
  • بافرینگ اتوماتیک یا صریح

در ادامه به مسائل مرتبط با هر یک از موارد بالا نگاه می اندازیم.

1.3.1. نام دادن به پردازش ها

پردازه هایی که می خواهند با یکدیگر ارتباط برقرار کنند، باید راهی برای ارجاع به همدیگر داشته باشند. پردازه ها می توانند از ارتباط مستقیم یا غیر مستقیم استفاده کنند.تحت ارتباط مستقیم، هر پردازه ای که خواهان برقراری ارتباط است باید به طور صریح گیرنده یا فرستنده ارتباط را مشخص کند(نام گذاری کند). در این طرح، اعمال اولیه ()send و ()receive به صورت زیر تعریف می شوند:

  • (send(P, message : یک پیام را به پردازه P می فرستد.
  • (receive(Q, message : یک پیام را از پردازه Q دریافت می کند.

یک لینک ارتباطی در این طرح دارای ویژگی های زیر است:

  • یک لینک به طور اتوماتیک بین هر جفت پردازه ای که خوهان برقراری ارتباط هستند، برپا می شود. پردازه ها برای برقراری ارتباط تنها باید از شناسه های یکدیگر اطلاع داشته باشند.
  • هر لینک دقیقا در ارتباط با دو پردازه است.
  • بین هر جفت پردازه دقیقا یک لینک وجود خواهد داشت.

از اشکالات این طرح می توان به خاصیت پیمانه ای محدود آن اشاره کرد. با تغییر شناسه یک پردازه ناگزیر باید تمامی تعریفات پردازه های دیگر چک شود. تمامی ارجاعات به شناسه قدیم باید یافت شده و با شناسه جدید جایگزین شود. درحالت کلی چنین تکنیک هایی منجربه hard-coding می شود و [در نتیجه] باید شناسه ها به طور صریح تعیین شود. به همین جهت این تکنیک نسبت به تکنیک های غیر مستقیم، کمتر مورد پسند است.

در ارتباط غیر مسقیم ، ارسال و دریافت پیام از طریق صندوق های پستی یا پورت ها صورت می گیرد. یک صندوق پست را می توان در حالت انتزاعی به عنوان یک شی که پردازه ها می توانند در آن پیام بگذارند یا پیام ها را از آن حذف کنند، نمایش داد. هر صندوق پست یک شناسه یکتا دارد. یک پردازه می تواند با یک پردازه دیگر از طریق چندین صندوق پستی متفاوت ارتباط برقرار کند. به این نکته توجه داشته باشید که دو پردازه تنها زمانی قادر به برقراری ارتباط خواهند بود که یک صندوق پست به اشتراک گذاری شده داشته باشند. اعمال اولیه ()send و ()receive به صورت زیر تعریف می شوند:

  • (send(A, message : یک پیام را به صندوق پست A می فرستد.
  • (receive(A, message: یک پیام را از صندوق پست A دریافت می کند.

در این طرح، یک لینک ارتباطی دارای ویژگی های زیر است:

  • یک لینک تنها زمانی بین یک جفت پردازه برپا می شود که هر دوی آنها یک صندوق پستی اشتراکی داشته باشند.
  • یک لینک ممکن است در ارتباط با بیش از دو پردازه باشد.
  • بین هر جفت از پردازه هایی که با هم در ارتباط هستند ممکن است چندین لینک ارتباطی وجود داشته باشد، به طوری که هر لینک مربوط به یک صندوق پست است.

حال فرض کنید که پردازه های P1 ، P2 و P3 همگی صندوق پستی A را به اشتراک گذاشته اند. پردازه ی P1 یک پیام را به A می فرستد، در حالی که P2 و P3 هر دو دستور receive() را به اجرا در می آورند. کدام پردازه پیامی که به وسیله A فرستاده شده است را دریافت می کند؟ جواب این سوال بستگی به این دارد که کدام یک از روش های زیر را اتخاذ کرده باشیم:

  • به یک لینک اجازه بدهیم که حداکثر در ارتباط با دو پردازه باشد.
  • حداکثر به یک پردازه در یک زمان اجازه اجرای عملیات () receive را بدهیم.
  • به سیستم این اجازه را بدهیم که به طور دلخواه پردازه ی دریافت کننده پیام (یا P1 یا P2، اما نه هر دوی آنها) را انتخاب کند. سیستم ممکن است برای انتخاب پردازه ی دریافت کننده پیام یک الگوریتم را تعریف کند. (برای مثال round robin). سیستم ممکن است دریافت کننده را به ارسال کننده بشناساند.

صندوق پست ممکن است در مالکیت پردازه یا سیستم عامل باشد. اگر صندوق پست در مالکیت پردازه باشد (در این حالت صندوق پست بخشی از فضای آدرس پردازه است.)، می توانیم بین مالک (که تنها قادر به دریافت پیام از طریق صندوق پستی است) و کابر (که تنها قادر به ارسال پیام به صندوق پستی است) تمایز قائل شویم. از آنجا که هر صندوق پست یک مالک یکتا دارد، دیگر استباهی در رابطه با این که کدام پردازه باید پیام ارسالی به صندوق پستی را دریافت کند، رخ نمی دهد. وقتی پردازه ای که مالک صندوق پست است خاتمه می یابد، صندوق پست نیز محو می شود. هر پردازه ای که متعاقبا پیامی به این صندوق پست ارسال می کند باید نسبت به عدم وجود صندوق پست مطلع شود.

در مقایسه، صندوق پستی که در مالکیت سیستم عامل است به خودی خود وجود خواهد شد. این صندوق پست مستقل است و به پردازه ی خاصی ضمیمه نمی شود. برای اینکار سیستم عامل باید مکانیزمی را فراهم آورد که به پردازه اجازه کارهای زیر را بدهد:

  • یک صندوق پست جدید ایجاد کند.
  • از طریق صندوق پست پیام هایی را ارسال و دریافت کند.
  • یک صندوق پست را حذف کند.

پردازه ای که صندوق پست را ایجاد می کند به طور پیش فرض مالک آن صندوق پست است. در ابتدا مالک تنها پردازه ای است که می تواند از طریق این صندوق پستی پیامهایی را دریافت کند. با این حال مالکیت و حق دریافت پیام از طریق فراخوانی های سیستمی مناسب قابل واگذاری به دیگر پرداز ها است. با این کار می توان برای هر صندوق پست چندین گیرنده تعیین کرد.

در مقاله قبل به بررسی سیستم های تبادل پیام و روش های پیاده سازی منطقی عملیات ()send و ()receive پرداختیم. سپس در قسمت 1.3.1 چگونگی ارجاع پردازه ها به همدیگر شرح داده شد. در همین راستا، در این مقاله ابتدا به بررسی مسائل دیگری که حول پیاده سازی عملیات ()send و ()receive وجود دارد می پردازیم (1.3.2 و 1.3.3) و سپس در قسمت های 1.4 و 1.5 دو مورد از دیگر روش های IPC شرح داده می شود.

1.3.2.همزمانی پردازش ها

همانطور که گفته شد ارتباط بین پردازه ها به وسیله اعمال اولیه ()send و ()receive انجام می شود. گزینه های طراحی متفاوتی برای پیاده سازی هر کدام از اعمال اولیه وجود دارد. تبادل پیام ممکن است بلوکه شونده (همزمان) یا غیر بلوکه شونده (غیر همزمان) باشند.

  • • ارسال به صورت بلوکه شونده: پردازه ارسال کننده تا زمان دریافت پیام به وسیله پردازه دریافت کننده یا صندوق پست، بلوکه می شود.
  • • ارسال به صورت غیر بلوکه شونده: پردازه ارسال کننده پیام را فرستاده و به انجام عملیات خود ادامه می دهد.
  • • دریافت به صورت بلوکه شونده: دریافت کننده تا زمان مهیا بودن یک پیام بلوکه می شود.
  • • دریافت به صورت غیر بلوکه شونده: دریافت کننده یا یک پیام معتبر و یا یک پیام تهی را بازیابی می کند.

1.3.3. بافرینگ در پردازش ها

در برقراری ارتباط چه به صورت مستقیم و چه به صورت غیر مستقیم، پیام های مبادله شده به وسیله پردازه های شرکت کننده در ارتباط درون یک صف موقتی نگه داری می شوند. همچین صف هایی به سه طریق قابل پیاده سازی هستند:

  • ظرفیت صفر : صف حداکثر دارای طولی برابر صفر خواهد بود، در نتیجه لینک ارتباطی نمی تواند دارای پیام های منتظر درون صف باشد. در این حالت ارسال کننده باید تا زمان دریافت پیام بلوکه شود.
  • ظرفیت محدود (کران دار) : صف دارای طول متناهی n است؛ در نتیجه حداکثر n پیام می توانند درون آن ساکن شوند. به هنگام ارسال یک پیام اگر صف پر نباشد، پیام ارسالی درون صف گذاشته می شود و ارسال-کننده بدون اینکه نیازی به انتظار داشته باشد می تواند به اجرای خود ادامه دهد. ظرفیت لینک متناهی است. اگر صف پر باشد، ارسال کننده باید تا زمان فراهم شدن فضا درون صف، بلوکه شود.
  • ظرفیت نامحدود (بی کران) : در این حالت طول صف نامتناهی است در نتیجه هر تعداد پیام قابل نگه داری درون آن است و ارسال کننده هیچ وقت بلوکه نمی شود.

گاهی اوقات به حالت ظرفیت صفر، سیستم پیام رسانی بدون بافر گفته می شود. دو حالت دیگر به عنوان سیستم هایی با بافرینگ اتوماتیک شناخته می شوند.

 Pipes چیست؟

پایپ به عنوان یک مجرا عمل می کند و در نتیجه به دو پردازه اجازه رد و بدل اطلاعات را می دهد. پایپ ها یکی از اولین امکانات IPC سیستم های یونیکس اولیه بودند. پایپ ها با وجود محدودیت هایی که دارند یکی از ساده ترین راه ها را برای دو پردازه که می خواهند با هم ارتباط داشته باشند، فراهم می کنند. در پیاده سازی یک پایپ چهار مسئله باید در نظر گرفته شود:

  1. آیا پایپ اجازه ارتباط دو طرفه را می دهد یا تنها ارتباط یکرطرفه در آن مجاز است؟
  2. اگر ارتباط دو طرفه امکان پذیر باشد، آیا این ارتباط half duplex است (یعنی در یک زمان داده ها تنها در یک جهت اجازه حرکت دارند) یا full duplex (یعنی داده ها در یک زمان اجازه حرکت در دو جهت را دارند) ؟
  3. آیا باید بین دو پردازه ای که درگیر ارتباط هستند، یک رابطه (مانند رابطه والد-فرزندی) وجود داشته باشد؟
  4. آیا پایپ ها از طریق شبکه می توانند ارتباط برقرار کنند یا اینکه پردازه های برقرار کننده ارتباط تنها باید بر روی یک ماشین باشند؟

در پایپ های اولیه یک پردازه داده ها را در یک طرف پایپ می نویسد و پردازه ی دیگر از طرف دیگر پایپ داده ها را می خواند. به عنوان نتیجه، پایپ های اولیه یک طرفه هستند. اگر نیاز به ارتباط دو طرفه باشد، باید از دو پایپ استفاده شود، به طوری که هر پایپ داده ها را در جهت های مختلف انتقال دهد. در سیستم های یونیکس پایپ های اولیه با استفاده از تابع ([]pipe(int fd ساخته می شوند. به این پایپ از طریق توصیفگرهای فایل [] int fd می توان دسترسی پیدا کرد. [fd[0 بخش خواندن و [fd[1 بخش نوشتن پایپ است. یونیکس با پایپ به عنوان یک فایل خاص برخورد می کند، در نتیجه می توانیم به پایپ ها از طریق فراخوانی های سیستمی () read و () write دسترسی پیدا کنیم.

به یک پایپ اولیه نمی توان خارج از پردازه ای که آن را ایجاد کرده است، دسترسی پیدا کرد. غالبا پردازه ی پدر یک پایپ را ایجاد کرده و از آن برای ارتباط با پردازه ی فرزندی که از طریق دستور () fork ایجاد می کند، استفاده می کند. همانطور که می دانید، پردازه فرزند فایل های باز را از پدر خود به ارث می برد. از آنجا که پایپ نوع خاصی از فایل است، پس فرزند پایپ را از پردازه پدر خود به ارث می برد. شکل زیر رابطه ی توصیف کننده فایل fd را با پردازه های پدر و فرزند نشان می دهد:

وب سایت توسینسو

Named Pipes چیست؟

پایپ های اولیه با فراهم کردن یک مکانیزم ساده، به یک جفت پردازه اجازه برقراری ارتباط را می داد. اما پایپ های اولیه تنها زمانی وجود دارند که دو پردازه بخواهند با هم ارتباط برقرار کنند. در سیستم های یونیکس وقتی ارتباط به پایان رسید و پردازه ها خاتمه پیدا کردند، پایپ هم دیگر وجود نخواهد داشت.

named pipeها ابزار ارتباطی قدرتمندتری فراهم می کنند. در اینجا ارتباط می تواند دو جهته باشد و نیاز به وجود رابطه والد-فرزندی هم نیست. با برپا شدن یک named pipe چندین پردازه از آن می توانند برای ارتباط استفاده کنند. در حالت عادی یک named pipe چندین نویسنده دارد. به علاوه named pipeها حتی بعد از به پایان رسیدن پردازه های دخیل در ارتباط نیز وجود خواهند داشت.

در سیستم های یونیکس از named pipeها به عنوان FIFO یاد می شود. FIFOها در سیستم فایل به عنوان یک فایل عادی ظاهر می شوند. FIFO با استفاده از فراخوانی های سیستمی () mkfifo ساخته و بوسیله فراخوانی های سیستمی () open()، read و () write دستکاری می شوند. FIFO تا زمانی که به طور صریح از سیستم فایل حذف نشود، وجود خواهد داشت. با وجود اینکه FIFO اجازه ارتباط دو سویه را می دهد، اما تنها انتقال half-duplex مجاز می باشد. اگر داده ها نیاز به حرکت در هر دو جهت داشته باشند، معمولا از دو FIFO استفاده می شود. به علاوه پردازه های دخیل در ارتباط باید بر روی یک ماشین قرار داشته باشند.

بررسی امکانات لینوکس برای ارتباط میان پردازه ها

2.1.حافظه اشتراکی چیست؟

حافظه اشتراکی یکی دیگر از امکانات 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

حافظه اشتراکی به وسیله تابع shmget ایجاد می شود:

int shmget(key_t key, size_t size, int shmflg);

در زیر به شرح هر یک از پارامترهای این تابع می پردازیم:

  • key : این مقدار توسط برنامه ( یا بهتر است بگوییم برنامه نویس!) مشخص می شود. با این مقدار ما قطعه حافظه ی اشتراکی خود را نامگذاری می کنیم. تابع shmget یک شناسه برای ما برمی گرداند. ما از این مقدار برگشتی (شناسه حافظه اشتراکی) متعاقبا در دیگر توابع مربوطه استفاده می کنیم.
  • size : دومین پارامتر یعنی size ، میزان حافظه مورد نیاز به بایت را مشخص می کند.
  • shmflg : سومین پارامتر یعنی shmflg از نه پرچم تشکیل شده است. این پرچم ها مجوزها را مشخص می کنند و نحوه ی استفاده از آن ها مانند تعیین مجوز برای فایل است. یک بیت خاص که توسط IPC__CREAT تعیین می شود باید با مجوزها یای بیتی شود تا یک قطعه حافظه اشتراکی جدید به وجود آید. (مثلا به صورت 0666 | IPC__CREAT عمل می کنیم) اگر IPC__CREAT را ست کرده و قطعه حافظه از قبل موجود باشد، خطایی رخ نمی دهد. پرچم IPC__CREAT در مواقعی که نیازی به آن نیست، به طور اتوماتیک در نظر گرفته نمی شود.

در بحث حافظه اشتراکی، پرچم های جواز بسیار مفید هستند. زیرا به وسیله آنها می توان به پردازه هایی که توسط ایجاد کننده حافظه اشتراکی به وجود آمده اند اجازه نوشتن در حافظه را بدهیم، در حالی که دیگر پردازه ها (که به وسیله ایجاد کننده حافظه اشتراکی به وجود نیامده اند) تنها اجازه خواندن از حافظه را داشته باشند. با این کار دسترسی فقط-خواندنی کارآمدی به داده ها می توانیم ایجاد کنیم.

اگر حافظه اشتراکی با موفقیت ایجاد شود، یک عدد صحیح نامنفی که همان شناسه حافظه اشتراکی است، برگردانده می شود. در صورت شکست، مقدار 1- برگردانده می شود.

  • shmat

وقتی برای اولین بار یک قطعه حافظه اشتراکی را ایجاد می کنید، این قطعه حافظه قابل دسترس برای هیچ پردازه ای نیست. برای فعال کردن دسترسی به حافظه اشتراکی، باید آن را به فضای آدرس یک پردازه ضمیمه کنید. این کار به وسیله تابع shmat انجام می شود:

void *shmat(int shm_id, const void *shm_addr, int shmflg);

معرفی پارامترهای تابع

  • shm__id : این پارمتر شناسه حافظه اشتراکی است که توسط تابع shmget برگردانده می شود.
  • shm__addr : آدرسی است که حافظه ی اشتراکی باید خود را به آن ضمیمه کند. این مقدار همیشه باید یک اشاره گر تهی باشد تا سیستم خود آدرس جایی که حافظه ظاهر می شود را انتخاب کند.
  • shmflg : مجموعه ای از پرچم های بیتی است. دو مقدار ممکن SHM__RND و SHM__RDONLY هستند. SHM__RND به همراه shm__addr آدرس جایی که حافظه استراکی به آن ضمیمه شده است را کنترل می کند. SHM__RDONLY حافظه ضمیمه شده را فقط-خواندنی می کند. بسیار کم پیش می آید که شما نیاز به کنترل آدرسی داشته باشید که حافظه اشتراکی به آن ضمیمه شده است؛ در حالت عادی شما باید وظیفه انتخاب آدرس را به عهده سیستم بگذارید.

در صورت فراخوانی موفقیت آمیز shmat، یک اشاره گر که به اولین بایت حافظه اشتراکی اشاره می کند، برگردانده می شود. در صورت شکست این تابع 1- را برمی گرداند.

  • shmdt

تابع shmdt حافظه اشتراکی را از پردازه ی جاری جدا می کند. این تابع یک اشاره گر به حافظه (همان اشاره-گری که توسط shmat برگردانده شد) را به عنوان پارامتر دریافت می کند. در صورت موفقیت 0 و در صورت بروز خطا 1- برگردانده می شود. توجه کنید که جدا کردن حافظه اشتراکی باعث حذف آن نمی شود. این تابع تنها حافظه را از دسترس پردازه جاری خارج می کند.

  • shmctl

با این تابع می توان بر روی حافظه اشتراکی کنترل داشت.

int shmctl(int shm_id, int command, struct shmid_ds *buf);

معرفی پارامترهای تابع

  • shm__id: شناسه حافظه اشتراکی است که از مقدار برگشتی تابع shmget به دست می آید.
  • command : کاری است که باید انجام گیرد. این پارامتر سه مقدار می تواند به خود بگیرد. این مقادیر در زیر نشان داده شده اند.
      1. IPC__STAT : اطلاعات موجود در ساختمان داده کرنل که مرتبط با shm__id است را به درون یک ساختمان از نوع shmid__ds کپی می کند. (سومین پارامتر یعنی buf به چنین ساختمانی اشاره می کند). در این حالت فراخوانی کننده باید اجازه خواندن از حافظه اشتراکی را داشته باشد.
      2. IPC__SET : مقادیر اعضای ساختمان داده shmid__ds را درون آن بخش از ساختمان داده کرنل که مربوط به این قطعه حافظه اشتراکی است، می نویسد.
      3. IPC__RMID : قطعه حافظه اشتراکی را حذف می کند.
    • buf : بالاخره سومین پارامتر یعنی buf، یک اشاره گر به ساختمان داده ای است که حالات و مجوزهای حافظه اشتراکی را شامل می شود. این ساختمان داده shmid_ds نام داشته و به صورت زیر تعریف شده است.
struct shmid_ds {
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
}

در صورت اجرای موفقیت آمیز این تابع، 0 و در صورت شکست 1- برگردانده می شود.

2.2. صف های پیام

  • msgget

با استفاده از تابع msgget ، یک صف پیام را ایجاد و به آن دسترسی پیدا می کنید.

int msgget(key_t key, int msgflg);

برنامه باید یک مقدار را برای key تعیین کند. در واقع این مقدار برای یک صف پیام مشخص، یک نام تعیین می کند. پارامتر دوم یعنی msgflg، از نه پرچم جواز تشکیل شده است. برای ایجاد یک صف پیام جدید، باید یک بیت خاص که به وسیله IPC__CREAT مشخص شده است با مجوزها یای بیتی شوند. (برای نمونه به این صورت عمل می کنیم: 0666 |IPC__CREAT). شایان ذکر است که اگر کلید یک صف موجود را به این تابع فرستاده و IPC__CREAT را هم ست کنیم، خطایی رخ نمی دهد. مقدار IPC__CREAT در صورت وجود صف به طور اتوماتیک نادیده گرفته می شود.در صورت اجرای موفقیت آمیز این تابع، یک مقدار مثبت که شماره شناسه صف است، برگردانده می شود. در صورت شکست 1- برگرانده می شود.

  • msgsnd

این تابع به شما اجازه اضافه کردن یک پیام به صف پیام ها را می دهد.

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 استفاده می کند، شما نمی توانید از آن در ساختمان صرف نظر کنید و ساختمان داده تعریف شده به وسیله شما حتما باید آن را شامل شود. همچنین بهتر است آن را مقدار دهی اولیه کنید. به وسیله این عضو از ساختمان در حقیقت می توانیم پیام های خود را اولویت دهی کنیم.

معرفی پارامترهای تابع

  • msqid : شناسه صف پیام است که به وسیله تابع msgget برگردانده می شود.
  • msg__ptr : یک اشاره گر به پیامی است که باید برگردانده شود. طبق توضیحات قبل، پبام باید با نوع long int شروع شود.
  • msg__sz : سایز پیام مشخص شده به وسیله اشاره گر msg__prt را تعیین می کند. توجه کنید که در محاسبه سایز پیام، نباید سایز long int message__type را دخالت دهید.
  • msgflg : وقتی که صف پیام پر باشد یا محدودیت سیستم بر روی پیام های درون صف به حد نهایی خود رسیده باشد، این پارامتر مشخص می کند چه اتفاقی رخ بدهد. اگر این پارامتر پرچم IPC__NOWAIT را به صورت تنظیم شده داشته باشد، تابع بدون ارسال پیام برمی گردد و مقدار 1- را به عنوان مقدار بازگشتی برمی گرداند. اگر پارامتر msgflg پرچم IPC__NOWAIT را به صورت پاک داشته باشد، پردازه ارسال کننده پیام معلق خواهد شد و در انتظار فراهم شدن فضا درون صف می ماند.

در صورت موفقیت تابع مقدار 0 و در صورت شکست مقدار 1- را برمی گرداند. در صورت فراخوانی موفقیت آمیز، یک کپی از داده های پیام برداشته می شود و درون صف پیام گذاشته می شود.

  • msgrcv

تابع msgrcv پیام ها را از یک صف پیام بازیابی می کند.

int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

معرفی پارامترهای تابع

  • msqid : شناسه صف پیام است که به وسیله تابع msgget برگردانده می شود.
  • msg__ptr : یک اشاره گر به پیامی است که باید بازیابی شود. همانطور که در قبل اشاره شد، پیام باید با نوع long int شروع شود.
  • msg__sz : سایز پیام مشخص شده به وسیله اشاره گر msg__prt را تعیین می کند. توجه کنید که در محاسبه سایز پیام، نباید سایز long int message__type را دخالت دهید.
  • msgtype : این متغیر که از نوع long int است به ما این اجازه را می دهد که یک فرم ساده ای از اولویت دریافت را پیاده سازی کنیم. اگر msgtype مقدار 0 داشته باشد، اولین پیام موجود در صف بازیابی می شود. اگر مقداری بزرگتر از صفر داشته باشد، اولین پیام که نوعش (اولویتش) مشابه مقدار این پارامتر است، بازیابی می شود. اگر مقدار کوچک تر از صفر باشد، اولین پیام که نوعش (اولویتش) برابر یا کوچکتر از قدر مطلق مقدار msgtype است بازیابی می شود.
  • msgflg : وقتی هیچ پیامی با نوع (اولویت) مناسب در انتظار دریافت شدن نباشند، این پارامتر مشخص می کند که چه اتفاقی رخ بدهد. اگر پرچم IPC__NOWAIT در msgflg تنظیم شده باشد، تابع فراخوانی شده بلافاصله با مقدار برگشتی 1- برمی گردد. اگر پرچم IPC__NOWAIT پاک شده باشد، پردازه معلق شده و منتظر رسیدن یک پیام با اولویت مناسب می ماند.

در صورت موفقیت، تابع msgrcv تعداد بایت های قرار گرفته درون بافر را بر می گرداند؛ همچنین پیام درون بافری که کاربر تخصیص داده است (همان فضایی که پارامتر msg__ptr به آن اشاره می کند) کپی شده و سپس از صف پیام حذف می شود. در صورت بروز خطا 1- برگردانده می شود.

  • msgctl

عملکرد این تابع بسیار مشابه تابع shmctl در حافظه اشتراکی است. به همین جهت تنها به ذکر نام آن بسنده می کنیم.

int msgctl(int msqid, int command, struct msqid_ds *buf);

نحوه فراخوانی Pipe

تابع 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

پایه ای ترین و متداول ترین مکانیزم مورد استفاده برای خواندن، فراخوانی سیستمی ()read می باشد که به صورت زیر تعریف شده است:

#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);

هر فراخوانی این تابع len بایت از آفست کنونی فایلی که به وسیله fd به آن اشاره می شود را می خواند. سپس محتویات خوانده شده درون حافظه ای که بوسیله buf به آن اشاره می شود، قرار داده می شود. در صورت موفقیت تعداد بایت های نوشته شده درون buf برگردانده می شود. در صورت بروز خطا، 1- برگردانده می شود. مکان فایل(آفست فایل) هم به اندازه تعداد بایت های خوانده شده، جلو برده می شود.

  • ()write

پایه ای ترین و متداول ترین فراخوانی سیستمی مورد استفاده برای نوشتن، ()write می باشد. ()write مشابه ()read است و به صورت زیر تعریف شده است.

#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);

هر فراخوانی این تابع، count بایت از buf را درون فایلی که توصیف کننده فایل fd به آن اشاره می کند، می نویسد. در صورت موفقیت، تعداد بایت های نوشته شده برگردانده شده و همچنین مکان فایل (مکان فعلی فایل - آفست) آپدیت می شود. در صورت بروز خطا 1- برگردانده می شود.

2.4. Named Pipes: FIFOS چه هستند؟

در قسمت قبل تنها قادر به انتقال داده بین برنامه های مرتبط به هم بودیم، یعنی برنامه هایی که از یک جد مشترک نشات می گرفتند. این نوع از ارتباط غالبا مفید نخواهد بود، زیرا در بیشتر اوقات ما خواهان تبادل داده بین برنامه های نامرتبط هستیم.این کار به وسیله FIFOها که گاهی اوقات به آن ها named pipe هم گفته می شود، انجام می شود. named pipeها با یک نام درون سیستم فایل قرار می گیرند اما رفتاری مشابه پایپ های بدون نام در قسمت قبل دارند.درون برنامه ها، FIFO به صورت زیر ایجاد می شود

int mknod(const char *filename, mode_t mode);

آموزش باز کردن یک FIFO به وسیله دستور open

برای باز کردن یک 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 استفاده کرد.

3. مقایسه روش های IPC در پردازش ها

در حافظه اشتراکی پردازها یک فضای آدرس را به اشتراک می گذارند و اعمال خواندن و نوشتن را از طریق فضای آدرس به اشتراک گذاشته انجام می دهند. در این روش پردازه ها خود مسئول همزمان کردن دسترسی به قطعه حافظه اشتراکی هستند اما تبادل پیام مکانیزمی را فراهم می کند که به پردازه ها اجازه برقراری ارتباط و همزمان کردن فعالیت هایشان را بدون به اشتراک گذاری فضای آدرسی مشابه می دهد. همچنین در روش تبادل پیام امکان تبادل اطلاعات در محیط های توزیع شده وجود دارد. روش حافظه اشتراکی تا حدی سریع ترین روش برای ارتباط پردازه ها است، اما راحتی کار با این روش به اندازه بقیه روش ها نیست.

پایپ یک مکانزیم ساده برای انتقال اطلاعات به صورت همگام بین چندین پردازه فراهم می کند. پایپ ها خود به دو دسته تقسیم می شوند: 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، چهار برنامه به گونه ای می نویسیم که:

  1. برنامه اول یک فایل مشخص را باز کرده و محتوای آن را درون IPC بنویسید.
  2. برنامه دوم IPC را خوانده و محتوای آن را پیمایش کرده و به کد اسکی هر کاراکتر 2 واحد اضافه می کند و رشته حاصل را به IPC می فرستد.
  3. برنامه سوم IPC را خوانده و همه حروف کوچک را به حروف بزرگ و همه حروف بزرگ را به حروف کوچک تبدیل می کند.
  4. برنامه ی چهارم IPC را خوانده و در فایلی هم نام با فایل ورودی و با پسوند "output." ذخیره می کند.

برای پیاده سازی این مثال، از تکنیک حافظه اشتراکی استفاده می کنیم.

توجه: در هر چهار برنامه ما یک ساختمان به صورت زیر تعریف کرده و در بدنه برنامه یک شی از آن می سازیم. این شی به حافظ اشتراکی ایجاد شده اشاره خواهد کرد. (در حقیقت محتوای اعضای این ساختمان، همان محتوای حافظه اشتراکی می باشد). به وسیله همین ساختمان ما می توانیم نوعی از همزمانی را نیز پیاده سازی کنیم. در این ساختمان به وسیله 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");
}

نمونه اجرای برنامه

وب سایت توسینسو

نظرات