در این گزارش به بررسی مفهوم محیط اجرا در برنامه های می پردازیم که یکی از موارد مهم در زمان اجرای برنامه هاست. برنامه ها به صورت کلی در زمان اجرا از سه جزء بزرگ تحت عنوان کد برنامه، رویه ها و متغیرها تشکیل می شوند که این موارد می تواند جزئی از برنامه و یا مواردی باشد که در زمان اجرا نیاز می شود و می بایست توسط سیستم عامل تامین شود. به صورت کلی مفهوم محیط اجرا در برنامه ها ارتباط زیادی با بحث مدیریت حافظه و پردازه ها در سیستم عامل ها دارد که در این گزارش به بررسی آن می پردازیم.
به صورت کلی در بحث کامپایلر ها با زبان هایی رو به رو هستیم که برای راحتی برنامه نویس ها نزدیک به زبان انگلیسی طراحی شده اند و به آن ها زبان های سطح بالا(High-Level Language) گفته می شود، برنامه های نوشته شده به این زبان ها می بایست به زبان میانی ترجمه شده و سپس به زبان ماشین مخصوصی که قرار است بر روی آن اجرا شوند ترجمه شوند که به این لایه از زبان ها اصطلاحا زبان های سطح میانی(Middle-Level Language) گفته می شود که بین زبان های سطح بالا و زبان ماشین یعنی صفر و یک قرار دارند.
همه موارد گفته شده در مراحل کامپایل کاملا منطقی و دقیق است اما نکته ای حائز اهمیت می شود آن هم این است در که در بسیاری از موارد ممکن است نیاز باشد تا از کتابخانه های مخصوص ماشین های متفاوت استفاده شود تا بر روی آن ماشین قابلیت اجرای برنامه فراهم شود و یا از متغیر هایی که مخصوص سیستم عامل ها هستند استفاده شود تا مواردی مثل امکان استفاده از چند هسته و یا حافظه و از این قبیل موارد مشخص شود، چرا که در زمان برنامه نویسی نمی توان این موارد را مد نظر قرار داد و دلیل آن نیز کاملا شفاف است، چون که ممکن است این برنامه در ماشین های متعدد و متفاوتی از نظر ساختار سخت افزاری اجرا شود و به صورت کلی برنامه تا جای امکان نباید نیاز به تغییر ساختاری داشته باشد. مشکلات دیگر نیز از این قبیل وجود دارد به عنوان مثال اگر برنامه از آدرس های دقیق حافظه فیزیکی در کد استفاده کرده باشد، قابلیت اجرا در ماشین های متفاوت را نداشته و حتی قابلیت اجرای مجدد بر روی همان ماشین را نیز ممکن است نداشته باشد چرا که سیستم عامل وظیفه تخصیص حافظه را به عهده دارد و بر اساس برنامه هایی که در حال اجرا هستند و یا منتظر اجرا هستند این حافظه را مدیریت می کند، بنابراین امکان این که برنامه ما به صورت کاملا دقیق در همان خانه های از پیش تعیین شده قرار بگیرد تقریبا صفر درصد خواهد بود. به عنوان مثال ساده ترین روشی که برای حل این مسئله به کار برده می شود که زیرمجموعه بحث ما خواهد بود، این است که یک عدد ثابت توسط سیستم عامل با خانه های نوشته شده در کد برنامه جمع یا تفریق می شود که لازمه این قضیه این است که برنامه نوشته شده از قبل این قابلیت را در خود قرار داده باشد که این قابلیت توسط کامپایلر ها در کد قرار داده می شود.
مهم ترین موردی که در زمان اجرا می بایست مدیریت شود، حافظه اصلی می باشد. هر برنامه شامل سه بخش کلی می باشد که عبارت است از کد برنامه، رویه ها(Procedures)، متغیرها. هر یک از این موارد می بایست به شکل خاصی در حافظه اصلی نگه داری شوند که برنامه ای که در حال اجراست بتواند وظایف خود را به صورت صحیحی انجام دهد.
روش های تخصیص حافظه به صورت کلی به دو دسته تقسیم می شوند: تخصیص حافظه ایستا(static)، تخصیص حافظه پویا(Dynamic) که تخصیص پویا به منظور استفاده در ذخیره سازی پشته(Stack) و هرم(Heap) استفاده می شود.
نکته ای که باید به آن توجه شود این است که ذخیره سازی پویا به این معنی نیست که حافظه مصرفی برنامه بیشتر از مقدار مشخص شده توسط سیستم عامل خواهد بود، بلکه به این معنی است که در ابتدا تعداد خانه های اندکی از حافظه تخصیص داده می شوند و در زمان اجرای برنامه خانه های بیشتر و کم تر می شوند.
ذخیره سازی ایستا به این معنی است که در زمانی که سیستم عامل حافظه مخصوص برنامه را به آن تخصیص می دهد مقداری از این حافظه به صورت ثابت به برخی از موارد تخصیص داده می شود و تا زمان اتمام اجرای برنامه هیچ تغییری در محل ذخیره سازی این موارد صورت نگرفته و حتی کم و زیاد نیز نمی شود. این نوع ذخیره سازی برای متن کد برنامه و همین طور داده های ایستای دیگری که ممکن است در برنامه وجود داشته باشند قابل استفاده است.
ذخیره سازی پشته ها در فضای پویای حافظه تخصیص داده شده توسط سیستم عامل قرار می گیرد و دلیل این قضیه پویا بودن ماهیت پشته می باشد که دائما عملیات ذخیره سازی و خواند از پشته صورت می گیرد و از آن جایی که به صورت آخر-ورود-اول-خروج (LIFO: Last-In-First-Out) می باشد، می بایست قابلیت اضافه شدن خانه های آن موجود باشد چرا که همانند آرایه ها ثابت نبوده و قابلیت کم و زیاد شدن دارند.
شاید این سوال مطرح شود که چرا در ابتدا فضای زیادی برای پشته به آن ها تخصیص ندهیم؟ جواب این است که به دلیل این که حافظه تخصیص داده شده توسط سیستم عامل محدود می باشد و طبیعتا در ماشین مورد استفاده برنامه های دیگری نیز درحال اجرا هستند نمی توان هر مقدار حافظه ای را هرچند بدون استفاده به موارد مختلف تخصیص داد، چرا که پشته ها ممکن است در زمان اجرا صرفا یک خانه را اشغال کنند و در زمان های خاصی فضای زیادی اشغال کنند.
ذخیره سازی هرم ها نیز از آن جایی که ماهیت پویا ای دارند می بایست به صورت پویا باشد، روش مناسبی که برای این ذخیره سازی استفاده می شود از آن جایی که فضای ذخیره سازی برای برنامه محدود است، از انتهای فضایی که تخصیص داده شده است شروع به پر کردن داده ها می کند، به این شکل با توجه به این پشته نیز از ابتدای فضای حافظه شروع به پر کردن کرده است، به شکل بهینه ای می توان از حافظه تخصیص داده شده استفاده نمود.
دلیل اصلی ای که علاوه بر پشته ها نیاز به ذخیره سازی هرم ها نیز وجود دارد، بحث پیاده سازی اولویت می باشد، چرا که به وسیله پشته پیاده سازی اولویت تقریبا نشدنی است و حتی اگر روشی نیز پیشنهاد شود احتمال خیلی زیاد پیچیدگی زیادی دارد و نیاز به مصرف حافظه و پردازش زیاد در پردازنده مرکزی خواهد بود که هزینه بیشتری نسبت به حافظه مورد استفاده برای هرم خواهد داشت.
جمع کننده زباله(Garbage Collector) که یکی از موارد مهم در مدیریت حافظه می باشد که وظیفه اصلی آن به عهده سیستم عامل می باشد. به این نحو عمل می کند که حافظه هایی که به برنامه تخصیص داده شده اند در زمانی که دیگر مورد نیاز نباشد به حافظه های خالی حافظه اصلی باز می گرداند. این پردازش بسیار پر هزینه می باشد. به صورت کلی نیز نحوه عملکرد بسیار پیچیده ای دارد و شاید ساده ترین عملیاتی که انجام می دهد این است که زمانی که اسکوپ(Scope) استفاده متغییری به اتمام می رسد حافظه آن را پس گرفته و به حافظه های خالی بر می گرداند.
در زبان هایی همانند C# و Java که برای برنامه نویسی نرم افزار های بزرگ استفاده می شود این قابلیت به صورت خودکار وجود دارد تا دغدغه مدیریت حافظه را از برنامه نویس گرفته و به صورت خودکار آن را مدیریت کند. اما در زبان هایی مانند زبان C این قابلیت وجود نداشته و مدیریت حافظه را به عهده برنامه نویس گذاشته است که این قضیه به عنوان یک نقطه ضعف به حساب نمی آید بلکه یکی از ویژگی های این زبان می باشد چرا که رسالت خود را ایجاد دسترسی حداکثری برای برنامه نویس می داند. دلیل اصلی این قضیه نیز استفاده از این زبان برای توسعه نرم افزار های سیستمی می باشد چرا که حتی سیستم عامل ها نیز با این زبان توسعه داده می شوند و در صورتی که مدیریت حافظه را به صورت خودکار انجام دهد، سیستم عامل بهینه بودن خود را از دست داده و بسیار کند خواهد شد و می بایست مدیریت حافظه به صورت کامل توسط برنامه نویس سیستم عامل توسعه داده شود.
رویه ها(Procedures) مفهومی در زمان اجرای برنامه هاست که ارتباط زیادی با بحث توابع دارد. به صورت کلی اگر رویه ها را بخواهیم تعریف کنیم می توانیم بگوییم، رویه ها مجموعه ای از اطلاعات هستند که برای صدا زدن توابع و اجرای آن ها و بازگشت به محل صدا زدن توابع مورد نیاز است.
البته نکته مهمی که وجود دارد این است که در زمان اجرا، کد برنامه به صورت زبان ماشین درآمده است و این پرش ها بسیار اندک خواهد بود و برای مواردی که به عنوان مثال استفاده از کتابخانه های ماشین مورد نظر و یا مواردی که قسمتی که به آن پرش می شود قابلیت گسترش در محیط اجرا را دارد و یا قابلیت تغییر در اجرا های متوالی را خواهد داشت استفاده می شود، چرا که این عملیات بسیار هزینه بر می باشد زیرا متغییر های بسیاری را برای هر پرش می بایست ذخیره کند و همین طور لازم است چندین بار بین حافظه اصلی و پردازنده اطلاعات رد و بدل شود تا این عملیات انجام شود.
به عنوان مثال در شکل 8 نمونه ای از رویه ای ساده بیان شده است، فرضا تابع show_data یک رویه است و با صدا زدن آن به محل ذخیره سازی آن پرش می شود و خط به خط آن اجرا شده و سپس به محل اولیه باز می گردد. به همین شکل در داخل تابع show_data نیز تابع دیگری صدا زده شده است که این عمل در بین عمل قبل اتفاق می افتد.
همانگونه که در جدول 1 مشاهده می کنید اطلاعاتی که برای هر رویه ذخیره سازی می شود بسیار زیاد است و این یکی از علل پرهزینه بودن اجرای رویه ها می باشد.
با توجه به اینکه هر برنامهای در کامپیوتر توالی و ترتیبی از دستورالعملها است میتوان تعریف دیگری نیز برای رویه در نظر گرفت و آن عبارت است از «دستورالعملهای کنار یکدیگری که کار مستقل و مشخصی انجام میدهند» که به آن درختهای فعال سازی میگویند.
با این تعریف هر رویه یک بدنه دارد که در هنگام اجرای برنامه شروع به اجرا میکند. در مباحث مربوط به کامپایلر، به اجرای یک رویه «فعالسازی» گفته میشود. اما برای اجرای هر رویه، همانطور که در مطالب قبلی اشاره شد، نیاز به یکسری پارامتر و متغیر داریم که در جدول 1 آورده شده است و آنها را تحت عنوان «رکورد فعالسازی» میشناسیم و به طور خلاصه اطلاعات لازم جهت اجرای یک رویه است.
حال با داشتن چندین رویه در یک برنامه و برای اجرای آنها نیاز به یکسری از رکوردهای فعالسازی نیز داریم که اطلاعات اجرای رویهها را ذخیره نمایند. برای پیادهسازی این ساختار و ذخیره این رکوردها از پشتهای به نام «پشته کنترل» استفاده میشود. در این صورت وقتی یک رویه، رویهی دیگری را فراخوانی کند، اجرای فراخوانی کننده تا زمانی که رویهی فراخوانی شده به اجرای خود پایان بخشد، به تعویق میافتد. در این زمان رکورد فعالسازیِ روال فراخوانی شده روی پشته ذخیره میشود.
با این فرض که گردش کنترل برنامه به روش ترتیبی است و زمانی که یک رویه فراخوانی میشود، کنترل آن به رویهی فراخوانی شده انتقال مییابد، وقتی یک روال فراخوانی شده اجرا شد، کنترل برنامه دوباره به فراخوانی کننده باز میگردد. این نوع گردش کنترل باعث میشود که نمایش رکوردهای فعالسازی به شکل درخت آسانتر شود. این سریها به نام «درخت فعالسازی» نامیده میشوند. در شکل 9 نمونه ای از این درخت فعال سازی معادل یک قطعه کد آورده شده است.
همانطور که گفته شد یک برنامه از رویههای گوناگون و مستقلی تشکیل شده است. اما جهت تشکیل یک برنامه این رویهها نیاز است تا با یکدیگر ارتباط برقرار کنند. به این ترتیب مفهومی به نام پارامتر شکل میگیرد که واسطه و وسیلهی ارتباطی میان رویهها است.
اما قبل از آن که با نحوهی برقراری ارتباط میان رویهها توسط پارامترها بپردازیم، لازم است تقسیمبندیهای مختلفی که از پارامترها انجام شده است را مرور کنیم.
7-1. پارامترهای R-Value و L-Value
- پارامترهای R-Value: مقدار یک عبارت که قابل ریختن در یک متغیر است و همیشه در سمت راست عملگر انتساب قرار میگیرد را پارامتر R-Valueمینامند.
- پارامترهای L-Value: همچنین موقعیت حافظه و یا به عبارتی دیگر متغیری که در آن یک مقدار ریخته میشود و همیشه در سمت چپ عمیگر انتساب قرار دارد را نیز پارامتر L-Value مینامیم.
7-2. پارامترهای رسمیو واقعی
· پارامترهای رسمی (Formal Parameter): متغیرهایی که اطلاعات ارسال شده از سوی رویهی فراخوانی کننده را دریافت میکنند به نام پارامترهای رسمینامیده میشوند. این متغیرها در تعریف تابع فراخوانی شده نوشته میشوند.
· پارامترهای واقعی(Actual Parameter): متغیرهایی که مقدار یا آدرسهایشان به رویهی فراخوانی شده ارسال میشوند، به نام پارامترهای واقعی نامیده میشوند. این متغیرها در تابع فراخوانی به صورت آرگومان نامیده میشوند.
پس از اطلاع از موارد بالا به روشهای گوناگونی که جهت ارسال پارامترها استفاده میشود، میپردازیم:
7-3. ارسال با مقدار | Pass by Value
در مکانیسم ارسال با مقدار، رویهی فراخوانی کننده، پارامترهای واقعی و R-Value را ارسال میکند و کامپایلر آن را در رکورد فعالسازی رویهی فراخوانی شده قرار میدهد. پارامترهای رسمیمقادیر ارسالی نیز از سوی رویهی فراخوانی کننده را نگهداری میشوند و اگر مقادیر نگهداری شده از سوی پارامترهای رسمیتغییر یابند، تأثیری بر روی پارامترهای واقعی نخواهد داشت.
7-4. ارسال با ارجاع | Pass by Reference
در مکانیسم ارسال با ارجاع، پارامتر واقعی و L-Value در رکورد فعالسازی رویهی فراخوانی شده کپی میشود. بدین ترتیب رویهی فراخوانی شده در واقع آدرس (موقعیت حافظه) پارامتر واقعی را میگیرد و پارامتر رسمینیز به همان آدرس حافظه اشاره میکند. بنابراین اگر مقدار مورد اشاره از سوی پارامتر رسمیتغییر یابد، بر روی پارامتر واقعی نیز تأثیر میگذارد، چون هر دو به یک موقعیت حافظه اشاره دارند.
7-5. ارسال با کپی-بازیابی | Pass by Copy-Restore
این مکانیسم ارسال با کپی-بازیابی ارسال پارامتر مشابه روش ارسال با ارجاع است؛ با این تفاوت که تغییرات روی پارامترهای واقعی زمانی انجام مییابد که رویهی فراخوانی شده، پایان گیرد. به محض فراخوانی تابع، مقادیر پارامترهای واقعی در رکورد فعالسازیِ رویهی فراخوانی شده کپی میشوند. به این صورت تغییر پارامترهای رسمی به طور همزمان روی پارامترهای واقعی تأثیر نمیگذارد؛ اما وقتی که رویهی فراخوانی شده پایان یافت، L-Valueهای پارامترهای رسمی به L-Valueهای پارامترهای واقعی کپی میشوند. برای مثال کد شکل10 یک ارسال با کپی-بازیابی را نشان میدهد.
7-6. ارسال با نام | Pass by Name
زبانهایی مانند Algol روش جدیدی برای ارسال پارامتر دارند که مانند پیش پردازندهها در زبان C عمل میکنند. در روش ارسال با نام[1]، نام رویهی فراخوانی شده با بدنه واقعی آن جایگزین میشود. به این ترتیب متن عبارتهای آرگومان را در فراخوانی رویه با پارامترهای متناظر در بدنهی رویه تعویض میکند، بنابراین نمیتواند بر روی پارامترهای واقعی عمل کند و شبیه مکانیسم ارسال با ارجاع است.
1. Jeffrey D. Ullman, Ravi Sethi, Monica S. Lam, Alfred V. Aho; “Compilers principles, techniques, and tools”, Pearson, 2006
2. Tutorials point . “Compiler Design - Run-Time Environment” – 2020, https://www.tutorialspoint.com/compiler_design/compiler_design_runtime_environment.htm#:~:text=By%20runtime%2C%20we%20mean%20a,processes%20running%20in%20the%20system.
3. Geeks for Geeks . “Runtime Environments in Compiler Design” – 2020, https://www.geeksforgeeks.org/runtime-environments-in-compiler-design