FreeRTOS
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)FreeRTOS
معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) FreeRTOS
كريستوفر سفيك
إذا استمتعت بهذه الكتب، فقد تستمتع أيضًا بـ التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.
FreeRTOS (تُنطق "free-arr-toss") هو نظام تشغيل وقت حقيقي (real-time operating system, RTOS) مفتوح المصدر مخصّص للأنظمة المدمجة (embedded systems). ويدعم FreeRTOS بُنى معماريات وسلاسل أدوات مترجم كثيرة مختلفة، وقد صُمّم ليكون «صغيرًا وبسيطًا وسهل الاستخدام».
يخضع FreeRTOS للتطوير النشِط، وقد كان كذلك منذ أن بدأ ريتشارد باري العمل عليه عام 2002. أما أنا فلسـت مطوِّرًا لـFreeRTOS ولا مساهمًا فيه، إني مجرّد مستخدم ومُعجب به. وعليه، فإن هذا الفصل سيركّز على «ماذا» و«كيف» تتشكّل بنية FreeRTOS، أكثر من تركيزه على «لماذا» مقارنةً بالفصول الأخرى في هذا الكتاب.
مثل جميع أنظمة التشغيل، تتمثّل المهمة الأساسية لـFreeRTOS في تشغيل المهام (tasks). ويتضمّن معظم شيفرة FreeRTOS تحديد أولويات المهام (prioritizing) وجدولتها (scheduling) وتشغيل مهام معرَّفة من المستخدم. وبخلاف جميع أنظمة التشغيل، فإن FreeRTOS نظام تشغيل وقت حقيقي يعمل على الأنظمة المدمجة.
بنهاية هذا الفصل آمل أن تكون قد فهمت البنية الأساسية لـFreeRTOS. فمعظم FreeRTOS مخصّص لتشغيل المهام، لذا ستحصل على نظرة واضحة جدًّا على كيفية إنجاز ذلك بالضبط.
وإذا كانت هذه أول نظرة لك تحت غطاء نظام تشغيل، فآمل أيضًا أن تتعلّم أساسيات كيفية عمل أي نظام تشغيل. إنّ FreeRTOS بسيط نسبيًّا، خاصةً مقارنةً بـWindows أو Linux أو OS X، لكن جميع أنظمة التشغيل تشترك في المفاهيم والأهداف الأساسية ذاتها، لذا فإن إلقاء النظر في أي نظام تشغيل يمكن أن يكون مفيدًا ومشوّقًا.
3.1. ما المقصود بـ«المدمج» و«الوقت الحقيقي»؟
قد يعبّر «المدمج» و«الوقت الحقيقي» عن أشياء مختلفة لمختلف الناس، فلنعرّفهما بالطريقة التي يستخدم بها FreeRTOS.
النظام المدمج (embedded system) هو نظام حاسوبي مصمَّم لإنجاز أمور قليلة فقط، مثل النظام الموجود في وحدة التحكّم عن بُعد بالتلفاز، أو جهاز GPS في السيارة، أو الساعة الرقمية، أو منظّم ضربات القلب. والأنظمة المدمجة أصغر وأبطأ عادةً من أنظمة الحوسبة العامة الغرض، وهي أيضًا أقل كلفةً عادةً. فقد يحتوي نظام مدمج منخفض الطرف على معالج 8 بتات يعمل بتردد 25MHz، وعدّة كيلوبايتات من الذاكرة العشوائية RAM، وربما 32 كيلوبايت من ذاكرة الوميض flash. أما النظام المدمج عالي الطرف فقد يحتوي على معالج 32 بتًا يعمل بتردد 750MHz، وجيغابايت من الذاكرة العشوائية، وعدّة جيغابايتات من ذاكرة الوميض.
أنظمة الوقت الحقيقي مصمَّمة لإنجاز شيء ما ضمن مدة زمنية محدَّدة؛ فهي تضمن أن الأمور تحدث في وقتها المفترض.
منظّم ضربات القلب مثال ممتاز على نظام مدمج عامل في الوقت الحقيقي. إذ يجب أن ينقبض عضلة القلب في اللحظة الصحيحة لإبقائك حيًّا؛ ولا يمكن أن يكون مشغولًا إلى الحدّ الذي يمنعه من الاستجابة في وقتها. وتُصمَّم منظّمات ضربات القلب والأنظمة المدمجة الأخرى العاملة في الوقت الحقيقي بعناية كي تُنجز مهامها في وقتها في كل مرة.
3.2. لمحة عامة عن البنية
FreeRTOS تطبيق صغير نسبيًّا. لا تتجاوز نواة FreeRTOS الدنيا ثلاث ملفات مصدر (.c) وحفنة من ملفات الترويسات، بمجموع أقل قليلًا من 9000 سطر شيفرة، بما فيها التعليقات والأسطر الفارغة. وتكون صورة الشيفرة الثنائية النمطية أصغر من 10 كيلوبايت.
ينقسم شيفرة FreeRTOS إلى ثلاثة مجالات رئيسية: المهام، والتواصل، والتفاعل مع العتاد.
- المهام: يتعامل قرابة نصف شيفرة FreeRTOS الأساسية مع أهمّ ما يشغل كثير من أنظمة التشغيل: المهام. فالمهمة هي دالة C معرَّفة من المستخدم لها أولوية معيّنة. ويتولّى
tasks.cوtask.hكل الأعمال الثقيلة الخاصة بإنشاء المهام وجدولتها وصيانتها. - التواصل: المهام جيّدة، لكن المهام القادرة على التواصل فيما بينها أفضل!وهذا يوّلينا إلى المهمة الثانية لـFreeRTOS: التواصل. ويتعامل نحو 40% من شيفرة FreeRTOS الأساسية مع التواصل. ويتولّى
queue.cوqueue.hتواصل FreeRTOS. وتستخدم المهام والمقاطعات (interrupts) الطوابير (queues) لإرسال البيانات إلى بعضها بعضًا، ولإشارات استخدام الموارد الحرجة باستخدام السيمافورات (semaphores) والأقفال المتبادلة (mutexes). - الهمس مع العتاد: نحو 9000 سطر من الشيفرة التي تشكّل قاعدة FreeRTOS هي شيفرة مستقلة عن العتاد؛ فسواء كان FreeRTOS يعمل على شريحة 8051 المتواضعة أم على أحدث نوى ARM وأحدثها، تعمل الشيفرة ذاتها. ويكاد نحو 6% من شيفرة FreeRTOS الأساسية يكون طبقة وصل (shim) بين نواة FreeRTOS المستقلة عن العتاد وبين الشيفرة المعتمدة على العتاد. وسنناقش الشيفرة المعتمدة على العتاد في القسم التالي.
اعتبارات العتاد
تقع طبقة FreeRTOS المستقلة عن العتاد فوق طبقة معتمدة على العتاد. وهذه الطبقة المعتمدة على العتاد تعرف كيف تتحدّث مع أي بنية شرائح تختارها. ويعرض الشكل 3.1 طبقات FreeRTOS.
الشكل 3.1: طبقات برنامج FreeRTOS
يأتي FreeRTOS مع كامل الشيفرة المستقلة عن العتاد والشيفرة المعتمدة عليه التي ستحتاجها لتشغيل نظام ما. وهو يدعم مترجمين كثيرين (CodeWarrior وGCC وIAR وغيرها) وكذلك بُنى معالجات كثيرة (ARM7 وARM Cortex-M3 ومتنوّع من شرائح PIC وSilicon Labs 8051 وx86 وغيرها). راجع موقع FreeRTOS للاطلاع على قائمة البنى والمترجمين المدعومة.
صُمّم FreeRTOS ليكون قابلًا للتهيئة بدرجة عالية. إذ يمكن بناؤه كنظام تشغيل وقت حقيقي مجرّد (bare-bones RTOS) على معالج واحد يدعم مهمام قليلة فقط، أو يمكن بناؤه كوحش متعدّد النوى عالي الوظائف مع TCP/IP ونظام ملفات وUSB.
تُحدَّد خيارات التهيئة في الملف FreeRTOSConfig.h عبر ضبط تعليمات #define المختلفة. فسرعة الساعة وحجم الكومة والأقفال المتبادلة ومجموعات فرعية من واجهة البرمجة كلها قابلة للتهيئة في هذا الملف، إلى جانب خيارات كثيرة أخرى. وفيما يلي أمثلة قليلة تضبط الحدّ الأقصى من مستويات أولوية المهام، وتردد المعالج، وتردد نبض النظام، وأصغر حجم للمكدس، وحجم الكومة الكلّي:
#define configMAX_PRIORITIES ( ( unsigned portBASE_TYPE ) 5 )
#define configCPU_CLOCK_HZ ( 12000000UL )
#define configTICK_RATE_HZ ( ( portTickType ) 1000 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 100 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 4 * 1024 ) )
تقيم الشيفرة المعتمدة على العتاد في ملفات منفصلة لكل سلسلة أدوات مترجم وبنية معالج. فمثلًا، إذا كنت تعمل مع مترجم IAR على شريحة ARM Cortex-M3، فإن الشيفرة المعتمدة على العتاد تقع في المجلد FreeRTOS/Source/portable/IAR/ARM_CM3/. ويصرّح portmacro.h عن جميع الدوال الخاصة بالعتاد، بينما يحتوي port.c وportasm.s على كامل الشيفرة المعتمدة على العتاد. ويقوم ملف الترويسات المستقل عن العتاد portable.h بـ#include ملف portmacro.h الصحيح وقت التصريف. وينادي FreeRTOS الدوال الخاصة بالعتاد باستخدام دوال معرَّفة بـ#define والمصرَّح عنها في portmacro.h.
لننظر إلى مثال على كيفية نداء FreeRTOS لدالة معتمدة على العتاد. فكثيرًا ما يحتاج الملف المستقل عن العتاد tasks.c إلى الدخول في مقطع شيفرة حرج (critical section) لمنع استباق المهام (preemption). ويحدث الدخول في مقطع حرج بطرق مختلفة باختلاف البنى، ولا يرغب tasks.c المستقل عن العتاد في أن يضطر إلى فهم التفاصيل المعتمدة على العتاد. ولهذا ينادي tasks.c الماكرو العام portENTER_CRITICAL()، راضيًا بجهله بكيفية عمله الفعلية. وبافتراض أننا نستخدم مترجم IAR على شريحة ARM Cortex-M3، فإن FreeRTOS مبنيٌّ مع الملف FreeRTOS/Source/portable/IAR/ARM_CM3/portmacro.h الذي يعرّف portENTER_CRITICAL() هكذا:
#define portENTER_CRITICAL() vPortEnterCritical()
يُعرَّف vPortEnterCritical() فعليًا في FreeRTOS/Source/portable/IAR/ARM_CM3/port.c. والملف port.c معتمد على العتاد، ويحتوي شيفرة تفهم مترجم IAR وشريحة Cortex-M3. يدخل vPortEnterCritical() المقطع الحرج مستخدمًا هذه المعرفة الخاصة بالعتاد، ثم يعود إلى tasks.c المستقل عن العتاد.
يعرّف الملف portmacro.h أيضًا أنواع البيانات الأساسية لبنية معمارية. فأنواع البيانات الخاصة بالمتغيّرات العددية الأساسية والمؤشرات ونوع بيانات نبض مؤقّت النظام تُعرَّف هكذا لمترجم IAR على شرائح ARM Cortex-M3:
#define portBASE_TYPE long // Basic integer variable type
#define portSTACK_TYPE unsigned long // Pointers to memory locations
typedef unsigned portLONG portTickType; // The system timer tick type
قد تبدو هذه الطريقة في استخدام أنواع البيانات والدوال عبر طبقات رقيقة من تعليمات #define معقّدةً بعض الشيء، لكنها تتيح لـFreeRTOS أن يُعاد تصريفه لبنية نظام مختلفة تمامًا بتغيير الملفات المعتمدة على العتاد وحدها. وإذا أردت تشغيل FreeRTOS على بنية لا يدعمها حاليًا، فكل ما عليك هو تنفيذ الوظائف المعتمدة على العتاد، وهي أصغر بكثير من الجزء المستقل عن العتاد في FreeRTOS.
وكما رأينا، ينفّذ FreeRTOS الوظائف المعتمدة على العتاد باستخدام ماكروهات #define الخاصة بالمعالج المُسبق للغة C. ويستخدم FreeRTOS أيضًا تعليمات #define في شيفرة مستقلة عن العتاد كثيرة. وللطبيقات غير المدمجة، فإن هذا الاستخدام المتكرّر لـ#define يُعدّ خطيئة كبرى، لكن في كثير من الأنظمة المدمجة الأصغر لا تستحق كلفة نداء الدالة المزايا التي تقدّمها الدوال «الحقيقية».
3.3. جدولة المهام: لمحة سريعة
أولويات المهام وقائمة الجاهزية
لكل مهمة أولوية يعيّنها المستخدم تقع بين 0 (أدنى أولوية) وقيمة configMAX_PRIORITIES-1 وقت التصريف (أعلى أولوية). فمثلًا، إذا ضُبطت configMAX_PRIORITIES على 5، فإن FreeRTOS سيستخدم 5 مستويات للأولوية: 0 (أدنى أولوية) و1 و2 و3 و4 (أعلى أولوية).
يستخدم FreeRTOS «قائمة الجاهزية» (ready list) لمتابعة جميع المهام التي تكون حاليًا جاهزة للتشغيل. وينفّذ قائمة الجاهزية كمصفوفة من قوائم المهام هكذا:
static xList pxReadyTasksLists[ configMAX_PRIORITIES ]; /* Prioritised ready tasks. */
pxReadyTasksLists[0] قائمة بجميع المهام الجاهزة ذات الأولوية 0، وpxReadyTasksLists[1] قائمة بجميع المهام الجاهزة ذات الأولوية 1، وهكذا حتى pxReadyTasksLists[configMAX_PRIORITIES-1].
نبض النظام
تُسمّى نبضة قلب نظام FreeRTOS بنبض النظام (system tick). ويهيّئ FreeRTOS النظام ليولّد مقاطعة نبض دورية. ويمكن للمستخدم ضبط تردد مقاطعة النبض، وهو عادةً في حدود الميلي ثانية. ومع كل مرة تنطلق فيها مقاطعة النبض، تُنادى الدالة vTaskSwitchContext(). وتختار vTaskSwitchContext() المهمة الجاهزة ذات الأولوية الأعلى وتضعها في المتغيّر pxCurrentTCB هكذا:
/* Find the highest-priority queue that contains ready tasks. */
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )
{
configASSERT( uxTopReadyPriority );
--uxTopReadyPriority;
}
/* listGET_OWNER_OF_NEXT_ENTRY walks through the list, so the tasks of the same
priority get an equal share of the processor time. */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );
قبل بدء حلقة while، تكون uxTopReadyPriority مضمونًا أن تكون أكبر من أولوية المهمة الجاهزة الأعلى أولويةً أو مساوية لها. وتبدأ حلقة while() عند مستوى الأولوية uxTopReadyPriority وتهبط عبر مصفوفة pxReadyTasksLists[] باحثة عن مستوى الأولوية الأعلى الذي توجد فيه مهام جاهزة. ثم تلتقط listGET_OWNER_OF_NEXT_ENTRY() المهمة الجاهزة التالية من قائمة الجاهزية الخاصة بمستوى تلك الأولوية.
أصبح pxCurrentTCB الآن يشير إلى المهمة الأعلى أولويةً، وعندما تعود vTaskSwitchContext() تبدأ الشيفرة المعتمدة على العتاد في تشغيل تلك المهمة.
تلك التسعة أسطر من الشيفرة هي قلب FreeRTOS المطلق. أما الأسطر الـ8900 الأخرى فأكثر من موجودة لضمان أن تكون تلك التسعة أسطر كل ما يلزم لإبقاء المهمة الأعلى أولويةً قيد التشغيل.
الشكل 3.2 صورة من مستوى عالٍ لما تبدو عليه قائمة الجاهزية. وفي هذا المثال ثلاثة مستويات للأولوية، فيها مهمة واحدة ذات أولوية 0، ولا مهام ذات أولوية 1، وثلاث مهام ذات أولوية 2. وهذه الصورة دقيقة لكنها غير كاملة؛ إذ ينقصها بعض التفاصيل التي سنستكملها لاحقًا.
الشكل 3.2: رؤية أساسية لقائمة جاهزية FreeRTOS
والآن وقد فرغنا من النظرة العامة على مستوى عالٍ، فلنعُق إلى التفاصيل. سننظر إلى بُنى بيانات FreeRTOS الثلاث الرئيسية: المهام، والقوائم، والطوابير.
3.4. المهام
المهمة الأساسية لجميع أنظمة التشغيل هي تشغيل مهام المستخدم وتنسيقها. ومثل كثير من أنظمة التشغيل، فإن وحدة العمل الأساسية في FreeRTOS هي المهمة. ويستخدم FreeRTOS كتلة تحكّم المهمة (Task Control Block, TCB) لتمثيل كل مهمة.
كتلة تحكّم المهمة (TCB)
يُعرَّف TCB في tasks.c هكذا:
typedef struct tskTaskControlBlock
{
volatile portSTACK_TYPE *pxTopOfStack; /* Points to the location of
the last item placed on
the tasks stack. THIS
MUST BE THE FIRST MEMBER
OF THE STRUCT. */
xListItem xGenericListItem; /* List item used to place
the TCB in ready and
blocked queues. */
xListItem xEventListItem; /* List item used to place
the TCB in event lists.*/
unsigned portBASE_TYPE uxPriority; /* The priority of the task
where 0 is the lowest
priority. */
portSTACK_TYPE *pxStack; /* Points to the start of
the stack. */
signed char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* Descriptive name given
to the task when created.
Facilitates debugging
only. */
#if ( portSTACK_GROWTH > 0 )
portSTACK_TYPE *pxEndOfStack; /* Used for stack overflow
checking on architectures
where the stack grows up
from low memory. */
#endif
#if ( configUSE_MUTEXES == 1 )
unsigned portBASE_TYPE uxBasePriority; /* The priority last
assigned to the task -
used by the priority
inheritance mechanism. */
#endif
} tskTCB;
يخزّن TCB عنوان بداية المكدس في pxStack وقمة المكدس الحالية في pxTopOfStack. كما يخزّن مؤشرًا إلى نهاية المكدس في pxEndOfStack للتحقّق من فيض المكدس (stack overflow) إذا كان المكدس ينمو «إلى أعلى» نحو عناوين أعلى. وإذا كان المكدس ينمو «إلى أسفل» نحو عناوين أدنى، فيُفحص فيض المكدس بمقارنة قمة المكدس الحالية مع بداية ذاكرة المكدس في pxStack.
يخزّن TCB أولوية المهمة الابتدائية في uxPriority وuxBasePriority. وتُعطى المهمة أولوية عند إنشائها، ويمكن تغيير أولوية المهمة. وإذا نفّذ FreeRTOS الوراثة الأولوية (priority inheritance) فإنه يستخدم uxBasePriority ليتذكّر الأولوية الأصلية أثناء رفع المهمة مؤقتًا إلى الأولوية «الموروثة». (راجع مناقشة الأقفال المتبادلة أدناه لمزيد من التفاصيل عن الوراثة الأولوية.)
لكل مهمة عنصرا قائمة (list items) لاستخدامهما في قوائم جدولة FreeRTOS المختلفة. وعندما تُدرَج مهمة في قائمة، لا يُدرِج FreeRTOS مؤشرًا مباشرًا إلى TCB، بل يُدرِج مؤشرًا إلى إمّا xGenericListItem أو xEventListItem داخل TCB. وتتيح متغيّرات xListItem هذه أن تكون قوائم FreeRTOS أذكى مما لو كانت تحتفظ بمؤشر إلى TCB فحسب. وسنرى مثالًا على ذلك عند مناقشة القوائم لاحقًا.
يمكن أن تكون المهمة في إحدى أربع حالات: قيد التشغيل، أو جاهزة للتشغيل، أو معلّقة (suspended)، أو محظورة (blocked). وقد تتوقّع أن يكون لكل مهمة متغيّر يخبر FreeRTOS بحالتها، لكنه لا يتوفّر. وبدلًا من ذلك، يتابع FreeRTOS حالة المهمة ضمنيًا عبر وضع المهام في القائمة المناسبة: قائمة الجاهزية، أو قائمة المهام المعلّقة، وغيرها. فوجود المهمة في قائمة معيّنة يدل على حالتها. وعندما تنتقل المهمة من حالة إلى أخرى، لا يفعل FreeRTOS سوى نقلها من قائمة إلى أخرى.
إنشاء المهمة
لقد لمسنا من قبل كيفية اختيار المهمة وجدولتها عبر مصفوفة pxReadyTasksLists؛ والآن لننظر إلى كيفية إنشاء المهمة في بدايتها. تُنشأ المهمة عند نداء الدالة xTaskCreate(). ويستخدم FreeRTOS كائن TCB جديد المُخصَّص لتخزين اسم المهمة وأولويتها وتفاصيلها الأخرى، ثم يخصّص حجم المكدس الذي يطلبه المستخدم (بافتراض توفّر ذاكرة كافية) ويتذكّر بداية ذاكرة المكدس في العضو pxStack في TCB.
يُهيَّأ المكدس بحيث يبدو وكأن المهمة الجديدة قيد التشغيل بالفعل وقد قاطعها تبديل سياق (context switch). بهذه الطريقة يستطيع المجدول (scheduler) أن يعامل المهام المُنشأة حديثًا تمامًا كالمهام التي تعمل منذ فترة؛ إذ لا يحتاج المجدول إلى أي شيفرة حالة خاصة للتعامل مع المهام الجديدة.
تعتمد طريقة جعل مكدس المهمة يبدو وكأنه قُطع بتبديل سياق على البنية التي يعمل عليها FreeRTOS، لكن تنفيذية معالج ARM Cortex-M3 هذه مثال جيّد:
unsigned int *pxPortInitialiseStack( unsigned int *pxTopOfStack,
pdTASK_CODE pxCode,
void *pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on
entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( portSTACK_TYPE ) pxCode; /* PC */
pxTopOfStack--;
*pxTopOfStack = 0; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( portSTACK_TYPE ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
يدفع معالج ARM Cortex-M3 السجلات إلى المكدس عندما تُقاطَع مهمة. ويعدّل pxPortInitialiseStack() المكدس بحيث يبدو وكأن السجلات قد دُفعت، رغم أن المهمة لم تبدأ تشغيلها فعليًا بعد. وتُخزَّن قيم معروفة في المكدس لسجلات ARM وهي xPSR, PC, LR, وR0. أما السجلات المتبقية R1 -- R12 فتُخصَّص لها مساحة في المكدس عن طريق إنقاص مؤشر قمة المكدس، دون تخزين أي بيانات محدَّدة لها في المكدس. ويقول معمارية ARM إن هذه السجلات غير معرَّفة عند إعادة الضبط، لذا لن يعتمد برنامج (خلوّ من الأخطاء) على قيمة معروفة لها.
بعد تجهيز المكدس، تكون المهمة جاهزة تقريبًا للتشغيل. لكن أولًا، يعطّل FreeRTOS المقاطعات: نحن على وشك أن نبدأ العبث بقوائم الجاهزية وببُنى المجدول الأخرى، ولا نريد أن يغيّرها أحد آخر من تحت أيدينا.
إذا كانت هذه أول مهمة تُنشأ على الإطلاق، فإن FreeRTOS يهيّئ قوائم مهام المجدول. ولدى مجدول FreeRTOS مصفوفة من قوائم الجاهزية، وهي pxReadyTasksLists[]، تحوي قائمة جاهزية واحدة لكل مستوى أولوية ممكن. ولدى FreeRTOS أيضًا بعض القوائم الأخرى لمتابعة المهام المعلّقة والمقتولة والمؤجَّلة. وكلها تُهيَّأ الآن أيضًا.
بعد إتمام أي تهيئة لأول مرة، تُضاف المهمة الجديدة إلى قائمة الجاهزية عند مستوى أولويتها المحدَّد. ثم تُعاد المقاطعات إلى التشغيل ويكتمل إنشاء المهمة الجديدة.
3.5. القوائم
بعد المهام، تأتي القائمة (list) بوصفها بنية بيانات FreeRTOS الأكثر استخدامًا. ويستخدم FreeRTOS بنية القائمة لمتابعة المهام لأغراض الجدولة، وكذلك لتنفيذ الطوابير.
الشكل 3.3: رؤية كاملة لقائمة جاهزية FreeRTOS
قائمة FreeRTOS هي قائمة مرتبطة مزدوجة دورية (circular doubly linked list) قياسية مع إضافتين مثيرتين للاهتمام. وإليك عنصرًا في القائمة:
struct xLIST_ITEM
{
portTickType xItemValue; /* The value being listed. In most cases
this is used to sort the list in
descending order. */
volatile struct xLIST_ITEM * pxNext; /* Pointer to the next xListItem in the
list. */
volatile struct xLIST_ITEM * pxPrevious; /* Pointer to the previous xListItem in
the list. */
void * pvOwner; /* Pointer to the object (normally a TCB)
that contains the list item. There is
therefore a two-way link between the
object containing the list item and
the list item itself. */
void * pvContainer; /* Pointer to the list in which this list
item is placed (if any). */
};
يحمل كل عنصر في القائمة رقمًا هو xItemValue، وهو عادةً أولوية المهمة التي يجري متابعتها أو قيمة مؤقّت لأغراض جدولة الأحداث. وتُحفظ القوائم بترتيب تنازلي الأولوية، أي أن xItemValue الأعلى أولويةً (أكبر رقم) يقع في مقدمة القائمة، وxItemValue الأدنى أولويةً (أصغر رقم) يقع في نهايتها.
المؤشران pxNext وpxPrevious هما مؤشرا قائمة مرتبطة قياسيان. وpvOwner مؤشر إلى مالك عنصر القائمة، وهو عادةً مؤشر إلى كائن TCB الخاص بمهمة. ويُستخدم pvOwner لجعل تبديل المهام سريعًا في vTaskSwitchContext(): فبمجرد العثور على عنصر القائمة الخاص بالمهمة الأعلى أولويةً داخل pxReadyTasksLists[]، يقودنا مؤشر pvOwner الخاص بعنصر القائمة ذاك مباشرةً إلى TCB المطلوب لجدولة المهمة.
يشير pvContainer إلى القائمة التي يقع فيها هذا العنصر. ويُستخدم لتحديد سريع ما إذا كان عنصر قائمة معيّن في قائمة بعينها. ويمكن وضع كل عنصر قائمة داخل قائمة، وهي معرَّفة هكذا:
typedef struct xLIST
{
volatile unsigned portBASE_TYPE uxNumberOfItems;
volatile xListItem * pxIndex; /* Used to walk through the list. Points to
the last item returned by a call to
pvListGetOwnerOfNextEntry (). */
volatile xMiniListItem xListEnd; /* List item that contains the maximum
possible item value, meaning it is always
at the end of the list and is therefore
used as a marker. */
} xList;
يُحفظ حجم القائمة في أي لحظة في uxNumberOfItems، من أجل عمليات حجم القائمة السريعة. وتُهيَّأ كل القوائم الجديدة لاحتواء عنصر واحد فقط: عنصر xListEnd. وxListEnd.xItemValue قيمة حارسة (sentinel) مساوية لأكبر قيمة يمكن أن يأخذها المتغيّر xItemValue: وهي 0xffff عندما تكون portTickType قيمة بـ16 بت، و0xffffffff عندما تكون portTickType قيمة بـ32 بت. وقد تحمل عناصر القائمة الأخرى القيمة نفسها أيضًا؛ إذ يضمن خوارزمية الإدراج أن يبقى xListEnd العنصر الأخير في القائمة دائمًا.
ولأن القوائم مرتّبة تنازليًا بالأولوية، يُستخدم عنصر xListEnd كعلامة لبداية القائمة. ولأن القائمة دورية، فإن عنصر xListEnd هذا يخدم أيضًا كعلامة لنهاية القائمة.
معظم عمليات الوصول إلى القوائم «التقليدية» التي استخدمتها على الأرجح تنجز كل عملها داخل حلقة for() واحدة أو نداء دالة واحدة هكذا:
for (listPtr = listStart; listPtr != NULL; listPtr = listPtr->next) {
// Do something with listPtr here...
}
غالبًا ما يحتاج FreeRTOS إلى الوصول إلى قائمة عبر عدة حلقات for() وwhile() وكذلك عبر نداءات الدوال، ولهذا يستخدم دوال قوائم تُشغِّل المؤشر pxIndex ليمشي على القائمة. وتفعل الدالة listGET_OWNER_OF_NEXT_ENTRY() ما هو pxIndex = pxIndex->pxNext; وتعيد pxIndex. (|:- بالطبع أنها تُجري كشف الالتفاف الصحيح حول نهاية القائمة أيضًا.) بهذه الطريقة تتحمّل القائمة نفسها مسؤولية تتبّع «أين أنت» أثناء المرور عليها باستخدام pxIndex، مما يتيح لبقية FreeRTOS ألّا تقلق بشأن ذلك.
الشكل 3.4: رؤية كاملة لقائمة جاهزية FreeRTOS بعد نبضة مؤقّت النظام
التشغيل الذي يجري على القائمة pxReadyTasksLists[] في vTaskSwitchContext() مثال جيّد على كيفية استخدام pxIndex. لنفترض لدينا مستوى أولوية واحد فقط هو الأولوية 0، وهناك ثلاث مهام عند ذلك المستوى. وهذا يشبه صورة قائمة الجاهزية الأساسية التي نظرنا إليها سابقًا، لكن هذه المرة سنشمل جميع بُنى البيانات والحقول.
وكما ترى في الشكل 3.3، فإن pxCurrentTCB يدلّ على أننا نُشغّل حاليًا المهمة B. وفي المرة التالية التي تعمل فيها vTaskSwitchContext() فإنها تنادي listGET_OWNER_OF_NEXT_ENTRY() للحصول على المهمة التالية المراد تشغيلها. وتستخدم هذه الدالة pxIndex->pxNext لتحدّد أن المهمة التالية هي المهمة C، وأصبح الآن pxIndex يشير إلى عنصر القائمة الخاص بالمهمة C، ويشير pxCurrentTCB إلى TCB الخاص بالمهمة C، كما هو مبيّن في الشكل 3.4.
لاحظ أن كل كائن من نوع struct xListItem هو في الحقيقة كائن xGenericListItem الخاص بـTCB المرتبط به.
3.6. الطوابير
يتيح FreeRTOS للمهام أن تتواصل وتتزامن فيما بينها باستخدام الطوابير (queues). كما تستخدم روتينات خدمة المقاطعة (ISRs) الطوابير للتواصل والمزامنة.
بنية بيانات الطابور الأساسية هي:
typedef struct QueueDefinition
{
signed char *pcHead; /* Points to the beginning of the queue
storage area. */
signed char *pcTail; /* Points to the byte at the end of the
queue storage area. One more byte is
allocated than necessary to store the
queue items; this is used as a marker. */
signed char *pcWriteTo; /* Points to the free next place in the
storage area. */
signed char *pcReadFrom; /* Points to the last place that a queued
item was read from. */
xList xTasksWaitingToSend; /* List of tasks that are blocked waiting
to post onto this queue. Stored in
priority order. */
xList xTasksWaitingToReceive; /* List of tasks that are blocked waiting
to read from this queue. Stored in
priority order. */
volatile unsigned portBASE_TYPE uxMessagesWaiting; /* The number of items currently
in the queue. */
unsigned portBASE_TYPE uxLength; /* The length of the queue
defined as the number of
items it will hold, not the
number of bytes. */
unsigned portBASE_TYPE uxItemSize; /* The size of each items that
the queue will hold. */
} xQUEUE;
هذا طابور قياسي إلى حدٍّ كبير بمؤشري رأس وذيل، فضلًا عن مؤشرات لتتبّع الموضع الذي قرأنا منه للتو والموضع الذي كتبنا إليه للتو.
عند إنشاء طابور، يحدّد المستخدم طول الطابور وحجم كل عنصر يتتبّعه الطابور. ويُستخدم pcHead وpcTail لمتابعة التخزين الداخلي للطابور. وإضافة عنصر إلى طابور تؤدّي إلى نسخة عميقة (deep copy) من العنصر في التخزين الداخلي للطابور.
يُجري FreeRTOS نسخة عميقة بدلًا من تخزين مؤشر إلى العنصر، لأن عمر العنصر المُدرَج قد يكون أقصر بكثير من عمر الطابور. فمثلًا، لنفترض طابورًا من أعداد صحيحة بسيطة تُدرَج وتُزال باستخدام متغيّرات محلّية عبر عدة نداءات دوال. لو خزّن الطابور مؤشرات إلى تلك المتغيّرات المحلّية، لكانت المؤشرات غير صالحة بمجرد خروج المتغيّرات المحلّية عن نطاقها واستخدام ذاكرتها من أجل قيمة جديدة.
المستخدم هو من يقرّر ما الذي يُوضع في الطابور. ويمكن للمستخدم أن يضع في الطابور نسخًا من العناصر إذا كانت صغيرة، كما في مثال الأعداد الصحيحة البسيطة في الفقرة السابقة، أو أن يضع مؤشرات إلى العناصر إذا كانت كبيرة. ولاحظ أن FreeRTOS يجري نسخة عميقة في الحالتين: إذا اختار المستخدم وضع نسخ من العناصر، فإن الطابور يخزّن نسخة عميقة من كل عنصر؛ وإذا اختار وضع المؤشرات، فإن الطابور يخزّن نسخة عميقة من المؤشر. وبالطبع، إذا خزّن المستخدم مؤشرات في الطابور، فإن المستخدم هو المسؤول عن إدارة الذاكرة المرتبطة بتلك المؤشرات. فطابور لا يهمّه أي بيانات تضعها فيه، ولا يحتاج سوى أن يعرف حجم البيانات.
يدعم FreeRTOS عمليات الإدراج والإزالة في الطوابير الحاجبة (blocking) وغير الحاجبة (non-blocking). وتعيد العمليات غير الحاجبة الرجوع فورًا بحالة «هل نجح الإدراج في الطابور؟» أو «هل نجحت الإزالة من الطابور؟». أما العمليات الحاجبة فتُحدَّد بمهلة (timeout). ويمكن للمهمة أن تحجب نفسها إلى أجل غير مسمى أو لمدّة محدودة.
تبقى المهمة المحظورة—ولنسَمِّها المهمة A—محظورة ما دامت عملية الإدراج أو الإزالة الخاصة بها لا يمكن إتمامها ولم تنتهِ مهلةها (إن وُجدت). وإذا عدّل مقاطعة أو مهمة أخرى الطابور بحيث يمكن إتمام عملية المهمة A، فإن المهمة A ستُحجب عن الحظر. وإذا كانت عملية الطابور الخاصة بالمهمة A ما زالت ممكنة عند الوقت الذي تعمل فيه فعليًا، فإن المهمة A ستُتمّ عملية الطابور وتُعيد «نجاح». غير أنه، بحلول الوقت الذي تعمل فيه المهمة A فعليًا، من المحتمل أن تكون مهمة أعلى أولويةً أو مقاطعة قد نفّذت عملية أخرى على الطابور تمنع المهمة A من تنفيذ عمليتها. وفي هذه الحالة ستتحقّق المهمة A من مهلتها، إمّا أن تستأنف الحظر إن لم تنتهِ المهلة، أو تُعيد حالة «فشل» لعملية الطابور.
من المهمّ ملاحظة أن بقية النظام تستمر في العمل بينما مهمة ما تحجب نفسها على طابور؛ فالمهام والمقاطعات الأخرى تستمر في التشغيل. وبهذه الطريقة لا تُهدِر المهمة المحظورة دورات معالجة المعالج (CPU cycles) كان يمكن استغلالها بفاعلية من المهام والمقاطعات الأخرى.
يستخدم FreeRTOS القائمة xTasksWaitingToSend لمتابعة المهام التي تحجب نفسها في أثناء الإدراج في طابور. ومع كل مرة يُزال فيها عنصر من طابور، تُفحص القائمة xTasksWaitingToSend. وإذا كانت هناك مهمة تنتظر في تلك القائمة فإنها تُحجب عن الحظر.
وبالمثل، تحتفظ xTasksWaitingToReceive بمتابعة المهام التي تحجب نفسها في أثناء الإزالة من طابور. ومع كل مرة يُدرَج فيها عنصر جديد في طابور، تُفحص القائمة xTasksWaitingToReceive. وإذا كانت هناك مهمة تنتظر في تلك القائمة فإنها تُحجب عن الحظر.
السيمافورات والأقفال المتبادلة
يستخدم FreeRTOS طوابيره للتواصل بين المهام وداخلها. ويستخدم FreeRTOS طوابيره أيضًا لتنفيذ السيمافورات (semaphores) والأقفال المتبادلة (mutexes).
ما الفرق بينهما؟
قد تبدو السيمافورات والأقفال المتبادلةشيئًا واحدًا، لكنها ليست كذلك. ينفّذ FreeRTOSهما بطريقة متشابهة، لكن المقصود بهما أن يُستخدما بطرق مختلفة. فكيف ينبغي استخدام كلٍّ منهما بصورة مختلفة؟ خبير الأنظمة المدمجة Michael Barr يقول ذلك بأفضل صياغة في مقاله، «شرح مُبسّط للأقفال المتبادلة والسيمافورات»:
الاستخدام الصحيح للسيمافور هو الإشارات من مهمة إلى أخرى. أما القفل المتبادل فيُقصد أن يُؤخذ ويُحرَّر، بهذا الترتيب دائمًا، من كل مهمة تستخدم المورد المشترك الذي يحميه. وبالمقابل، فإن المهام التي تستخدم السيمافورات إمّا تُرسل إشارة [«أرسل» بمصطلح FreeRTOS] أو تنتظر [«استلم» بمصطلح FreeRTOS]—لا الأمرين معًا.
يُستخدم القفل المتبادل لحماية مورد مشترك. فالمهمة تكتسب القفل المتبادل، وتستخدم المورد المشترك، ثم تحرّر القفل المتبادل. ولا يمكن لأي مهمة أن تكتسب قفلًا متبادلًا أثناء امتلاك مهمة أخرى له. وهذا يضمن السماح لمهمة واحدة فقط باستخدام مورد مشترك في الوقت نفسه.
تُستخدم السيمافورات بواسطة مهمة واحدة لإشارة مهمة أخرى. ولنسوف مقالة Barr:
فمثلًا، قد تحتوي المهمة 1 على شيفرة تُرسل (أي تُشير أو تزيد) سيمافورًا معيّنًا عند الضغط على زر «الطاقة»، بينما تنتظر المهمة 2، التي تُوقظ الشاشة، على ذلك السيمافور نفسه. وفي هذا السيناريو، تكون إحدى المهام هي مُنتِج إشارة الحدث، والأخرى هي مُستهلكها.
وإن=torchورتك أي شكوك حول السيمافورات والأقفال المتبادلة، فرجاءً راجع مقالة Michael.
التنفيذ
ينفّذ FreeRTOS سيمافورًا من N عنصرًا كطابور قادر على احتواء N عنصر. وهو لا يخزّن أي بيانات فعلية لعناصر الطابور؛ فالسيمافور لا يهتمّ إلا بعدد مدخلات الطابور المشغولة حاليًا، وهو عدد يُتتبَّع في حقل uxMessagesWaiting الخاص بالطابور. فهو إذن يقوم بـ«مزامنة صرفة»، كما يسمّيها ملف ترويسات FreeRTOS semphr.h. ومن ثمّ فإن حجم عنصر الطابور يساوي صفر بايت (uxItemSize == 0). وكل وصول إلى السيمافور يزيد حقل uxMessagesWaiting أو ينقصه؛ ولا حاجة إلى نسخ أي عنصر أو بيانات.
مثل السيمافور، يُنفَّذ القفل المتبادل أيضًا كطابور، لكن عدة حقول من البنية xQUEUE تُحمَّل فوق معاني أخرى باستخدام تعليمات #define:
/* Effectively make a union out of the xQUEUE structure. */
#define uxQueueType pcHead
#define pxMutexHolder pcTail
لأن القفل المتبادل لا يخزّن أي بيانات في الطابور، فإنه لا يحتاج إلى أي تخزين داخلي، ولهذا لا حاجة إلى حقلي pcHead وpcTail. ويضبط FreeRTOS حقل uxQueueType (وهو في الحقيقة حقل pcHead) على 0 للإشارة إلى أن هذا الطابور مستخدَم لقفل متبادل. ويستخدم FreeRTOS حقلي pcTail المحمَّلين فوق معاني أخرى لتطبيق الوراثة الأولوية على الأقفال المتبادلة.
وإن كنت غير ملمّ بالوراثة الأولوية، فسأستشهد مجدّدًا بـMichael Barr لتعريفها، هذه المرة من مقاله، «مدخل إلى انقلاب الأولوية»:
[الوراثة الأولوية] تقتضي أن ترث المهمة الأدنى أولويةً أولويةَ أي مهمة أعلى أولويةً في انتظار مورد تشتركان فيه. وينبغي أن يحدث هذا التغيير في الأولوية بمجرد أن تبدأ المهمة الأعلى أولويةً في الانتظار؛ وينبغي أن ينتهي عند تحرير المورد.
ينفّذ FreeRTOS الوراثة الأولوية باستخدام حقل pxMutexHolder (وهو في الحقيقة مجرّد حقل pcTail المحمَّل بـ#define). ويسجّل FreeRTOS المهمة التي تمسك قفلًا متبادلًا في حقل pxMutexHolder. وعندما يُعثر على مهمة أعلى أولويةً وهي تنتظر قفلًا متبادلًا محجوزًا حاليًا لدى مهمة أدنى أولويةً، فإن FreeRTOS «يرفع» المهمة الأدنى أولويةً إلى أولوية المهمة الأعلى أولويةً حتى يصبح القفل المتبادل متاحًا من جديد.
3.7. الخلاصة
لقد فرغنا من استعراض بنية FreeRTOS. ونأمل أن تكون قد أتقنت الآن كيف تعمل مهام FreeRTOS وتتواصل. وإذا كنت لم تسبق لك النظر من قبل في البنية الداخلية لأي نظام تشغيل، فنأمل أن تكون قد اكتشفت صورةً أولية عن كيفية عملها.
من الواضح أن هذا الفصل لم يغطِّ بنية FreeRTOS كلها. ولم أذكر تحديدًا تخصيص الذاكرة (memory allocation)، ولا روتينات خدمة المقاطعة (ISRs)، ولا التنقيح (debugging)، ولا دعم وحدة حماية الذاكرة (MPU). كما أن هذا الفصل لم يناقش كيفية إعداد FreeRTOS أو استخدامه. وقد كتب ريتشارد باري كتابًا ممتازًا هو استخدام نواة FreeRTOS الزمنية الحقيقية: دليل عملي، يناقش هذا بالضبط؛ وأوصي به بشدّة إذا كنت تنوي استخدام FreeRTOS.
3.8. شكر وتقدير
أودّ أن أشكر ريتشارد باري على إنشاء FreeRTOS وصيانته، وعلى اختياره جعله مفتوح المصدر. وكان ريتشارد متعاونًا جدًا في كتابة هذا الفصل، إذ قدّم بعض التاريخ عن FreeRTOS إضافةً إلى مراجعة تقنية بالغة القيمة.
كما أشكر Amy Brown وGreg Wilson على جمع كل هذا العمل «AOSA» معًا.
وأخيرًا والأهم (وهذا عكس «وليس في المرتبة الأخيرة»)، أودّ أن أشكر زوجتي Sarah على مشاركتها معي البحث وكتابة هذا الفصل. ولحسن الحظ كانت تعرف أنني مهووس بالتقنية حين تزوّجتني!