تاکنون بر برنامهنویسی امبدد (Embedded) تمرکز داشتیم. در این نوع برنامهنویسی، با حافظه و منابع محدودی سروکار داریم. اما زبان برنامهنویسی C برای کار روی ماشینهای بزرگتر دارای سیستمعامل (که نیازی به برنامهنویسی جداگانه برای آن نیست) طراحی شده که در این ماشینهای بزرگ کاربردی هستند و قابلیتهای زیادی دارد.
یکی از این قابلیتها، ناحیهای در حافظه به نام «هیپ(heap)» است. هیپ به ما کمک میکند در صورت نیاز، حافظه را برای ذخیرهسازی اجزاء پیچیده تخصیص و آزاد کنیم. مرورگرهای وب و آنالیزگرهای XML بهطور گسترده از هیپ استفاده میکنند.
ما قبلاً به این موضوع نپرداختهایم زیرا به سختی حافظه کافی برای stack داشتیم. تقسیم کردن حافظه به stack و هیپ، مانند تقسیم کردن یک قطره آب بین دو لیوان است. این کار ممکن است، اما بسیار دشوار و همچنان چندان مفید نیست.
علاوه بر این، سیستم ورودی/خروجی های C را نیز پوشش ندادهایم. در برنامهنویسی امبدد، مجبور بودیم خودمان I/O را مدیریت کنیم و مستقیماً به سختافزار دسترسی پیدا کنیم. اما در ماشینهای بزرگ دارای سیستمعامل، سیستم I/O زبان C و سیستمعامل تمام این جزئیات را از شما پنهان میکنند.
در ادامه، به بررسی تفاوتهای بین برنامهنویسی امبدد و غیرامبدد میپردازیم.
در برنامهنویسی امبدد، زمانی که یک دستگاه را برنامهنویسی میکنید، مستقیماً با آن ارتباط برقرار میکنید. بنابراین شما باید جزئیات دستگاهی که استفاده میکنید را بدانید. در مقابل، در برنامهنویسی غیر امبدد، زمانی که تابع «write» را برای ارسال داده به یک دستگاه فراخوانی میکنید، در واقع به سیستمعامل دستور میدهید این کار را انجام دهد. این شامل بافرکردن (buffering) برای کارآمدی بیشتر عملیات ورودی/خروجی و کنترل ارتباط با سخت افزار دستگاه میشود.
در برنامهنویسی امبدد، با محدودیت حافظه روبرو هستید. باید بدانید هر بایت از حافظه کجا قرار دارد و چگونه استفاده میشود. در برنامهنویسی غیرامبدد، سیستمعامل و سیستم مدیریت حافظه به شما امکان دسترسی به حافظهی بسیار بیشتری میدهند. اکثر برنامههای این سیستمها میتوانند مقداری از حافظه را هدر بدهند و اغلب این کار را هم انجام میدهند.
کد یک سیستم امبدد توسط یک لودر (یا پروگرامر) خارجی بر روی حافظه فلش پروگرام میشود. در مثال ما، این ابزار همان ST-LINK است که هرچند در پسزمینه محیط IDE پنهان شده، اما در عمل وجود دارد. این برنامه بهصورت دائم روی فلش میماند و در طول کارکرد عادی سیستم، هرگز پاک (Unload) یا جایگزین نمیشود. در نقطه مقابل، سیستمهای غیرامبدد دارای سیستمعاملی هستند که برنامهها را بسته به نیاز سیستم، بهصورت داینامیک Load و Unload میکند.
یک سیستم امبدد تنها یک برنامه را اجرا میکند. شما به زحمت اندازهی کافی حافظه برای همان یک برنامه را هم خواهید داشت. با این حال، سیستمهای غیر امبدد میتوانند چندین برنامه را به طور همزمان اجرا کنند. سیستمی که در حال حاضر با آن کار میکنید، در حال اجرای ۳۴۱ برنامه است و این یک سیستم کوچک محسوب میشود.
برنامههای امبدد هرگز متوقف نمیشوند، در حالی که برنامههای غیر امبدد میتوانند خاتمه یافته و کنترل را به سیستمعامل بازگردانند.
سیستمهای امبدد همهی دادههای خود را در حافظه ذخیره میکنند. در مقابل، سیستمهای غیر امبدد دارای یک سیستمفایل هستند و علاوه بر صفحهنمایش، شبکه و سایر تجهیزات جانبی، میتوانند اطلاعات را از فایلها بخوانند و یا در آنها بنویسند.
در نهایت، خطاهای سیستمهای امبدد باید توسط برنامهی شما مدیریت شوند. در سیستمهای غیر امبدد، یک سیستمعامل وجود دارد که خطاهای کنترلنشده توسط برنامه را شناسایی کرده و یک هشدار چاپ میکند یا برنامه را متوقف میسازد. سیستمعامل مانع از آسیبرساندن برنامههای معیوب به دیگر منابع سیستم میشود. برعکس، اگر یک برنامهی امبدد با مشکل مواجه شود، به سادگی میتواند کل سیستم را غیرقابلاستفاده کند (bricking).
++C برای سیستمهای بزرگتر بسیار بهتر است، زیرا در اغلب موارد، سربار اضافی آن تأثیر قابلتوجهی بر عملکرد سیستم ندارد. برای مثال، فرض کنید میخواهید برنامهای بنویسید که حجم زیادی داده را از پایگاه داده بخواند و یک گزارش تهیه کند. برای گزارشی که یک بار در روز اجرا میشود، چه اهمیتی دارد که برنامهی شما ۰.۵ ثانیه زمان پردازنده مصرف کند یا ۰.۲ ثانیه؟
با این حال، اگر در حوزهی محاسبات با کارایی بالا مانند بازی، انیمیشن یا ویرایش ویدئو فعالیت میکنید، به عملکرد و دقت زبان C نیاز دارید. با وجود اینکه C یک زبان قدیمیتر است، امروزه در رایانههای مرکزی همچنان کاربرد دارد.
در این بخش، نحوهی استفاده از هیپ (حافظهی پویا) را یاد خواهید گرفت که میتوان آن را به صورت دلخواه تخصیص یا آزاد کرد. همچنین با مدیریت I/O سیستمعامل آشنا خواهید شد – در واقع با دو سیستم I/O:
در نهایت، متوجه خواهید شد که چگونه از اعداد ممیز شناور استفاده کنید. اکثر پردازندههای امبدد ارزانقیمت، واحدی برای محاسبات ممیز شناور ندارند، بنابراین نمیتوانیم از اعداد ممیز شناور در برنامههای امبدد استفاده کنیم. همچنین، اگرچه رایانههای مرکزی سختافزار اختصاصی برای محاسبات ممیز شناور دارند، باید با دقت از این ویژگی استفاده کنید، در غیر این صورت ممکن است با نتایج غیرمنتظرهای روبرو شوید.
سیستمهای امبدد حافظهی رم (RAM) با دسترسی تصادفی بسیار محدودی دارند. تا به اینجا، حافظهی آزاد را به یک stack کوچک تقسیم کردهایم که دیگر فضایی برای چیز دیگری باقی نمیماند.
اما وقتی با سیستمهای بزرگتر سروکار داریم، گیگابایتها حافظه در اختیار داریم که تقسیم کردن حافظه به دو بخش را آسانتر میکند: stack و حافظه Heap.
در قسمت های قبلی درمورد stack صحبت کردیم. جایی است که برنامه در صورت نیاز برای هر رویهای، متغیرهای محلی و مقادیر موقت را اختصاص میدهد. «حافظهی پویا» کمی متفاوت است. شما تصمیم میگیرید چه زمانی و چه مقدار حافظه از «حافظهی پویا» اختصاص داده شود و همچنین چه زمانی به آن بازگردانده شود. با استفاده از «حافظهی پویا»، میتوانید ساختارهای دادهی بسیار پیچیده و بزرگی ایجاد کنید. برای مثال، مرورگرهای وب از «حافظهی پویا» برای ذخیرهسازی عناصر ساختاری تشکیلدهندهی یک صفحهی وب استفاده میکنند.
این فصل نحوه allocate و deallocate کردن حافظه را توضیح میدهد. همچنین بررسی میکنیم که چگونه یک linked list را پیادهسازی کنیم تا عملیات متداول dynamic memory را نشان دهیم و یاد بگیریم چگونه مشکلات رایج مربوط به حافظه را debug کنیم.
تابع malloc برای دریافت فضایی از «حافظهی پویا» می باشد و استفاده از آن به صورت زیر است:
|
1 |
pointer = malloc(number-of-bytes); |
این تابع، مقدار number-of-bytes را از هیپ دریافت کرده و یک اشارهگر pointer به آنها برمیگرداند. حافظهی اختصاصیافته، مقداردهی اولیه نشده است، بنابراین حاوی مقادیر تصادفی میباشد. اگر در برنامه به انتهای هیپ رسیده باشیم، این تابع اشارهگرتهی NULL را برمیگرداند.
برنامهی زیر، حافظه را برای یک ساختار روی هیپ اختصاص میدهد و سپس هیچ کاری با آن انجام نمیدهد.
simple.c
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdlib.h> #include <stdio.h> // Singly linked list with name as payload struct aList { struct aList* next; // Next node on the list char name[50]; // Name for this node }; int main() { struct aList* listPtr = malloc(sizeof(*listPtr)); if (listPtr == NULL) { printf("ERROR: Ran out of memory\n"); exit(8); } return (0); } |
برای اینکه برنامه قابلاعتمادتر شود، از sizeof(*listPtr) برای تعیین تعداد بایتهایی که باید اختصاص داده شود استفاده میکنیم که یک الگوی طراحی رایج است:
|
1 |
pointer = malloc(sizeof(*pointer)); |
یک اشتباه رایج، حذف ستاره (*) است، به این صورت:
|
1 |
struct aList* listPtr = malloc(sizeof(listPtr)); |
در برنامهنویسی، تمایز مهمی بین «داده ها» (data) و «اشارهگر به داده ها» (pointers) وجود دارد. در کد بالا، متغیر listPtr یک اشارهگر است. اشارهگرها آدرسهایی در حافظه هستند که به «داده های» واقعی اشاره میکنند. در مقابل، عبارت *listPtr خود «داده» است. این عبارت، مقدار واقعی موجود در حافظهی اختصاصیافته توسط اشارهگرlistPtr را نشان میدهد.
اندازهی یک اشارهگر معمولاً کوچک است، مثلاً ۸ بایت روی یک سیستم ۶۴ بیتی. اما اندازهی «داده» که اشارهگر به آن اشاره میکند، میتواند بسیار بزرگتر باشد. در مثال ما، ساختار alist اندازهای برابر با ۵۶ بایت دارد.
الگوی طراحیِ استفاده از sizeof(*listPtr) در تابع malloc اطمینان میدهد که تعداد درستی از بایتها را برای «داده» مورد نظر اختصاص میدهیم. این مهم است، زیرا خود متغیر listPtr که اشارهگراست، درون آرگومانِ تابع malloc قرار میگیرد و اندازهی آن با اندازهی «داده» که میخواهیم به آن اشاره کند، متفاوت است.
بسیاری از مواقع، به جای استفاده از یک اشارهگر به ساختار، خود ساختار را مستقیماً درون sizeof میگذاریم:
|
1 |
struct aList* listPtr = malloc(sizeof(struct aList)); |
این روش کار میکند، اما کمی خطرناک است. فرض کنید کسی نوع listPtr را تغییر دهد. برای مثال، کد زیر اشتباه است:
|
1 |
struct aListImproved* listPtr = malloc(sizeof(struct aList)); |
چه اتفاقی افتاد؟ قبلاً این قطعه کد را داشتیم که از لحاظ فنی درست بود، اما خطرناک به حساب میآمد:
|
1 |
struct aList* listPtr = malloc(sizeof(struct aList)); |
همه چیز کار میکرد، چون listPtr یک نشانگر به struct aList بود. تا زمانی که نوعها مطابقت داشتند، مشکلی پیش نمیآمد. حالا فرض کنید کسی تصمیم بگیرد کد را تغییر دهد و listPtr را به نسخهی جدید و بهبودیافتهی alist با نام alistImproved اشاره دهد، اما نوع را در تابع malloc تغییر ندهد. بدتر از آن، تصور کنید که کد دیگر آن ساده و واضح قبلی نباشد مانند زیر:
|
1 2 3 4 |
struct aListImproved* listPtr; // 3,000 lines of dense code // WRONG listPtr = malloc(sizeof(struct aList)); |
این کد فضای کافی برای فیلدهای جدید اختصاص نمیدهد، بنابراین هر بار که کسی از فیلدهای جدید استفاده میکند، حافظهی تصادفی بازنویسی میشود.
یک روش مناسب برای بررسی کمبود حافظه، چک کردن مقدار بازگشتی تابع malloc برای NULL بودن است:
|
1 2 3 4 |
if (listPtr == NULL) { printf("ERROR: Ran out of memory\n"); exit(8); } |
حتی اگر فکر میکنید عملکرد malloc صحیح میباشد، لازم است.
برنامهی ما دچار memory leak شده است، به این معنی که حافظهی استفاده شده را آزاد نمیکند. هر زمان برنامهای حافظه را آزاد میکند، آن حافظه به «حافظهی پویا» بازگردانده میشود تا بعداً توسط malloc دوباره مورد استفاده قرار گیرد. برای آزاد کردن حافظه از تابع free استفاده میکنیم:
|
1 |
free(listPtr);listPtr = NULL; |
تنظیمِ listPtr روی NULL یک الگوی طراحی است که تضمین میکند بعد از آزاد شدن حافظه، تلاشی برای استفاده از آن نکنیم. این کار از نظر زبان C الزامی نیست.
اگر بدون اینکه listPtr را روی NULL تنظیم کنیم، سعی در استفاده از حافظهی آزاد شده داشته باشیم، در ناحیهای از حافظه که نباید چیزی نوشته شود، مقداردهی انجام میدهیم. در اینجا یک مثال آورده شده است:
|
1 2 3 4 5 |
free(listPtr); listPtr->name[0] = '\0'; // Wrong, but will execute and // possibly create a strange error // much later in the program |
زمانی که روی حافظهی آزاد شده مقداردهی میکنیم، ممکن است در بخشهای بعدی برنامه اتفاق ناخوشایندی رخ دهد که دیباگ آن دشوار است، زیرا ارتباط بین باگ و اشتباه قبلی آشکار نیست.
این خوب است که اشتباهات خود را به این صورت نشان دهیم، :
|
1 2 3 4 |
free(listPtr); listPtr = NULL; listPtr->name[0] = '\0'; // Program crashes with a good // indication of where and why |
این نوعی از «برنامهنویسی محتاطانه» است. هدف این است که یک اشتباه ظریف و پنهان را به اشتباهی تبدیل کنیم که کل برنامه را کرش دهد و در نتیجه، پیدا کردن آن بسیار سادهتر خواهد بود.
ماکروهای کاربردی در امبدد C | نوشتن ماکرو...
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.