امیررضا ریاحی
امیررضا ریاحی
خواندن ۶ دقیقه·۳ سال پیش

پروسه‌ها چگونه در کنار هم پیش می‌روند؟

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

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

چرا اجرا چند پروسه به صورت همزمان دشوار است؟

در اینجا منظور ما از همزمان، هر دو مفهوم parallel و concurrent است که در پست‌های قبل معرفی شدند. البته اطلاعات بیشتر درمورد تفاوت این دو مفهوم را در این لینک می‌توانید مطالعه کنید.

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

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

مسئله تولیدکننده و مصرف‌کننده

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

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

فرض می‌کنیم برنامه‌ای که برای پروسه مولد می‌نویسیم قطعه کد زیر باشد:

while (true) { /* produce an item in next produced */ if (count == BUFFER SIZE) continue; /* do nothing */ buffer[in] = next produced; in = (in + 1) % BUFFERـSIZE; count++; }

قطعه کد زیر هم برای پروسه مصرف‌کننده باشد:

while (true) { if (count == 0) continue; /* do nothing */ next consumed = buffer[out]; out = (out + 1) % BUFFERـSIZE; count--; /* consume the item in next consumed */ }

هر دو برنامه، به تنهایی درست کار می‌کنند. اما اگر هردو قرار باشد همزمان کار کنند. احتمال وقوع مشکل هست.

فرض کنید متغیر count عدد ۷ باشد. بدین معنا که ۷ خانه از بافر پر شده است. اگر پروسه مولد بخواهد داده‌ای اضافه کند، به خانه ۸ام اضافه می‌کند و متغیر count را یکی بالا می‌برد. اگر پروسه مصرف‌کننده بخواهد داده‌ای را بخواند، از خانه ۷ام، داده را می‌خواند و متغیر count را یکی کم می‌کند. یعنی الان ۶ خانه از بافر پر است.

حالا فرض کنید در شرایطی که متغیر count == 7 است، هر دو پروسه در حال اجرا و تغییر متغیر count باشند. در پردازنده وقتی قرار است متغیر count زیاد شود، ابتدا متغیر داخل یکی از رجیستر‌های پردازنده قرار می‌گیرد. سپس یکی به آن اضافه می‌شود. یعنی متغیر داخل رجیستر از ۷ به ۸ تغییر می‌کند. حالا مقدار داخل رجیستر، به متغیر count داده می‌شود. عینا مشابه همین فرایند برای کاهش مقدار count هم رخ می‌دهد.

فرض کنید وقتی پروسه مولد، متغیر count را داخل رجیستر پردازنده بارگذاری می‌کند، مقدار داخل رجیستر ۷ است. در همین زمان پروسه مصرف‌کننده (که به طور همزمان در حال اجراست) متغیر count را داخل رجیستر دیگری از پردازنده قرار می‌دهد که دوباره همان ۷ است. حالا هر دو پردازش روی رجیسترها توسط پروسه‌ها جداگانه انجام می‌شود. یعنی پروسه مولد عدد رجیستر را یکی افزایش می‌دهد و می‌شود ۸، و پروسه مصرف‌کننده نیز آن را به ۶ تغییر می‌دهد. حالا هر دو پروسه می‌خواهند مقدار داخل رجیستر خود را به متغیر count نسبت دهند. نتیجه چه خواهد شد؟ واضح است که بلاخره یکی از این دو عملیات assignment باید بعد از دیگری انجام شود. و این یعنی متغیر count در آخر کار، یا مقدار ۶ خواهد داشت و یا مقدار ۸، در صورتی که جواب نهایی و درست همان ۷ است.

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

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


محدوده بحرانی

محدوده بحرانی، به هر بخشی از برنامه می‌گویند، که پروسه دسترسی خواندن یا تغییر در داده‌ای دارد که پروسه دیگری نیز همین دسترسی را به همان داده دارد. اگر بتوانیم پروسه‌ها را طوری پیش ببریم، که هیچ دو پروسه‌ای، به طور همزمان وارد محدوده بحرانی یکدیگر نشوند، می‌توانیم مطمئن باشیم که race condition رخ نخواهد داد.

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

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

شیوه‌های مختلف برای ایجاد mutual exclusion

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

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

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


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