درس‌هایی از سیستم‌عامل برای زندگی روزمره: ارسال پیام

همانطور که از قسمت قبل سری «درس‌هایی از سیستم‌عامل» یادتان هست، در این سری کاربردهای مفاهیمی که در سیستم‌عامل‌ها وجود دارد را در زندگی روزمره بررسی می‌کنیم.

در قسمت قبل الگوریتم‌های زمان‌بندی را بررسی کردیم و یاد گرفتیم چه روش‌هایی برای مدیریت زمانمان داریم و چگونه می‌توانیم با الهام از سیستم‌عامل آن‌ها، از زمانمان بهتر استفاده کنیم، مثلا از روش‌های مرسوم زمان‌بندی و اولویت‌بندی استفاده کنیم یا از شیوه‌های پیچیده‌تر مثلا چند صف استفاده کنیم.

در این قسمت می‌خواهم منطق‌های پشت ارسال پیام و ارتباط را بررسی کنیم. (از ارسال پیام منظورم همان message passing است.)


در سیستم‌عامل چه اتفاقی می‌افتد

چرا در سیستم‌عامل نیاز به ارسال و دریافت پیام وجود دارد؟

اصلا مگر چند جزء متفاوت هستند که بخواهند با هم ارتباط برقرار کنند؟

بله در سیستم‌عامل چند جزء متفاوت وجود دارد. هر برنامه‌ای که در حال اجرا می‌بینیم خودش یک (یا بیشتر) پروسس است. یک‌سری پروسس هم مستقل از برنامه‌های در حال اجرا در پس‌زمینه مشغول سرویس‌دهی هستند.

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

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

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

(بر حرفه‌ای‌ترها مشخص است که اجزای مرورگر به همین ۲ قسمت محدود نمی‌شود و چندین بخش مختلف وجود دارد ولی این مثال صرفا برای تقریب به ذهن بود.)

انواع ارسال پیام

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

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

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

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

راه‌های ارسال پیام بین پروسس‌ها در سیستم‌عامل

اول بگذارید قضیه را روشن کنم، سیستم‌عامل پیام‌رسان ندارد که بگوییم خب اگر خواستی پیام بدهی به آی‌دی مقصد پیام بده! اما راه حلی که داریم چیزی در همین حدود است!


راه اول، حافظه مشترک (shared memory)

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



راه دوم، صف ارسال پیام (message queue)

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

این شیوه نه تنها در پست بلکه در پیام‌رسان‌ها و شبکه‌های اجتماعی انجام می‌شود و بسیار در زندگی واقعی پرکاربرد است، فقط محدودیتی که وجود دارد این است که اگر شما هنوز پیام قبلی دوستان را باز نکرده‌اید آیا اون باز هم می‌تواند پیام بفرستید؟ اگر باز هم باز نکردید چی؟ صندوق ورودی شما چند تا باید جا داشته باشد؟ اگر آن هم پر شود پیام ها دور ریخته شود یا دوستتان بابت زیاد پیام فرستادن دستگیر شود (مثلا دیگر نتواند به شما پیام بفرستد یا به دید کامپیوتر پروسس بلاک/ترمینیت شود)

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


راه سوم: جویبار داده

اگر کاربر لینوکس باشید حتما از pipe استفاده کرده‌اید، مثلا cat a.txt | grep salam

این خط یعنی محتوای a.txt را چاپ کن (دستور cat) و خروجی آن را به ورودی grep لوله‌کشی کن، در این حالت هرخطی که از stdout دستور cat خارج شود، وارد stdin دستور grep می‌شود.

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

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

در این حالت هم مشکل اندازه بافر وجود دارد چرا که ممکن است برنامه‌ی تولید کننده بسیار سریع تولید کند ولی برنامه‌ی مصرف کننده در مصرف سریع نباشد، در این بین اطلاعات باید بافر شوند. لینوکس اندازه‌ی این بافر را عددی مثل ۶۴ کیلوبایت می‌گیرد. در حالتی که هم بافر پر شود تولید کننده صبر می‌کند تا مصرف کننده مقداری مصرف کند.


ارسال از طریق شبکه

این راه، عمومی‌تر از ارتباط داخل یک سیستم‌عامل است و می‌توانیم با آن به سراسر جهان (اگر فیلتر و تحریم نباشد البته) مسیج ارسال کنیم، مثلا همین مطلب را از طریق شبکه از سایت ویرگول دریافت کرده‌اید!

اما داخل یک سیستم هم می‌توان با باز کردن سوکت به localhost یا 127.0.0.1 و مشخص کردن پورتی که برنامه‌ی دیگر روی آن گوش می‌کند یک کانکشن باز کرد، این روش البته سربار شبکه را دارد اما در اکثر سیستم‌عامل ها جواب می‌دهد و برای همین جذابیت خاص خودش را دارد.

در زندگی واقعی مثل این است که یک بسته پستی را با پست بین‌الملل برای یک نفر در کشور خود بفرستید، بله هزینه احتمالا بیشتر می‌شود ولی اگر کشور شما سامانه پست ندارد (سیستم‌عاملتان) این روش می‌تواند جوابگو باشد.


روش‌های دیگر

البته خلاقیت بشر و روش‌ها نامحدودند! در ویکیپدیا می‌توانید چند مورد آن را بخوانید، اگر دوست داشتید در کامنت‌ها به راه‌هایی که من اشاره نکرده‌ام اشاره کنید.


https://en.wikipedia.org/wiki/Inter-process_communication


منابع خواندنی دیگر:

https://www.cs.princeton.edu/courses/archive/fall16/cos318/lectures/11.MessagePassing.pdf


https://www.mtholyoke.edu/courses/dstrahma/cs322/ipc.htm