الشروحات

العمليات وعناصرها في نظام تشغيل الحاسوب

جميعنا على دراية بنظام التشغيل الحديث الذي يدير العديد من المهام في وقت واحد أو ما يسمى بتعدد المهام Multitasking، حيث تُعَدّ العملية حزمةً من العناصر التي تحتفظ بها النواة لتعقّب جميع المهام التي تكون قيد التشغيل.

عناصر العملية

 

معرف العملية

يضبط نظام التشغيل معرّف العملية Process ID -أو PID اختصارًا- ويكون فريدًا لكل عملية مُشغَّلة.

الذاكرة

سنتعلم كيف تحصل عملية ما على ذاكرتها لاحقًا، وتُعَدّ الذاكرة أحد الأجزاء الأساسية لكيفية عمل نظام التشغيل، ولكن سنكتفي حاليًا بمعرفة أنّ كل عملية لها قسمها الخاص من الذاكرة.

تُخزَّن شيفرة البرنامج في هذه الذاكرة مع المتغيرات وأيّ عمليات تخزين أخرى مخصَّصة، ويمكن مشاركة أجزاء من الذاكرة بين العمليات، إذ تسمَّى بالذاكرة المشتركة Shared Memory، كما يمكن أن تراها بالاسم System Five Shared Memory -أو SysV SHM اختصارًا- بعد التطبيق الأصلي في نظام تشغيل أقدم.

مفهوم مهم آخر يمكن أن تستخدِمه العملية هو مفهوم ربط ملف موجود في القرص الصلب مع الذاكرة أو ما يُسمى mmaping، إذ يبدو الملف كما لو كان أيّ نوع آخر من الذاكرة RAM بدلًا من الاضطرار إلى فتح الملف واستخدام أوامر مثل read()‎ و write()‎، كما تمتلك مناطق mmaped أذونات يجب تعقّبها مثل القراءة والكتابة والتنفيذ، فمهمّة نظام التشغيل هي الحفاظ على الأمن والاستقرار، لذلك يجب التحقق مما إذا كانت العملية تحاول الكتابة في منطقة للقراءة فقط وإعادة خطأ بذلك.

الشيفرة والبيانات

يمكن تقسيم العملية بصورة أكبر إلى قسمين هما الشيفرة Code والبيانات Data، إذ يجب الاحتفاظ بشيفرة البرنامج وبياناته بصورة منفصلة لأنها تتطلب أذونات مختلفة من نظام التشغيل، ويسهّل هذا المقال بينهما مشاركة الشيفرة كما سنرى لاحقًا، كما يجب أن يعطي نظام التشغيل إذنًا لشيفرة البرنامج للتمكّن من قراءتها وتنفيذها دون الكتابة فيها، في حين تتطلب البيانات (المتغيرات) أذونات القراءة والكتابة ولا ينبغي أن تكون قابلةً للتنفيذ، ولكن لا تدعم جميع المعماريات ذلك، مما أدى إلى مجموعة واسعة من مشاكل الأمان في العديد منها.

المكدس Stack

يُعَدّ المكدس جزءًا مهمًا آخر من العملية وهو منطقة من الذاكرة وجزء من قسم البيانات في العملية، ويشارك في تنفيذ أيّ برنامج، كما يُعَدّ بنية بيانات عامة تعمل مثل مجموعة الأطباق، إذ يمكنك دفع push عنصر أو وضع طبق أعلى كومة من الأطباق بحيث يصبح العنصر العلوي، أو يمكنك سحب pop عنصر أو سحب طبق وظهور الطبق السابق.

المكدسات أساسية لاستدعاءات الدوال، إذ تحصل على إطار مكدس stack frame جديد في كل مرة تُستدعَى فيها الدالة، وهي منطقة من الذاكرة تحتوي على الأقل على العنوان الذي يجب الرجوع إليه عند الانتهاء ووسائط دخل الدالة وفضاء المتغيرات المحلية.

تنمو المكدسات عادةً إلى الأسفل، إذ يبدأ المكدس عند عنوان مرتفع في الذاكرة وينخفض تدريجيًا، في حين تحتوي بعض المعماريات مثل PA-RISC من HP على مكدسات تنمو للأعلى، وتوجد في بعض المعماريات الأخرى مثل IA64 مناطق تخزين أخرى (مخزن داعم للمسجّل) تنمو من الأسفل باتجاه المكدس.

يعطي وجود مكدس العديد من الميزات للدوال ومنها ما يلي:

أولًا، لكل دالة نسختها الخاصة من وسائط الدخل، إذ يُخصَّص إطار مكدس جديد لكل دالة مع وسائطها في منطقة جديدة من الذاكرة، + وبالتالي لا يمكن أن ترى الدوال الأخرى المتغير المُعرَّف في دالة ما، في حين تُخزَّن المتغيرات العامة التي يمكن أن تراها أيّ دالة في منطقة منفصلة من ذاكرة البيانات، مما يسهّل الاستدعاءات العودية Recursive Calls، وهذا يعني أنّ الدالة حرة في استدعاء نفسها مرةً أخرى، إذ سيُنشَأ إطار مكدس جديد لجميع متغيراتها المحلية.

ثانيًا، يحتوي كل إطار على عنوان للعودة إليه، وتسمح لغة C فقط بإعادة قيمة واحدة من الدالة، لذلك تُعاد هذه القيمة إلى دالة الاستدعاء في مسجّل محدد بدلًا من المكدس.

ثالثًا، يحتوي كل إطار على مرجع للإطار الذي يسبقه، لذا يمكن لمنقّح الأخطاء العودة للخلف متتبّعًا المؤشرات ليصل إلى أعلى المكدس، ويمكن أن ينتج متعقّب مكدسات Stack Trace الذي يُظهِر لك جميع الدوال التي جرى استدعاؤها حتى الوصول إلى هذه الدالة، ويُعَدّ ذلك مفيدًا لتنقيح الأخطاء، كما يمكنك رؤية كيف تتناسب الطريقة التي تعمل بها الدوال مع طبيعة المكدس، إذ يمكن لأيّ دالة استدعاءُ أيّ دالة أخرى، وبالتالي تصبح الدالة الأعلى بحيث تُوضَع في أعلى المكدس، وستعيد هذه الدالة في النهاية النتيجة إلى الدالة التي استدعتها، أي تخرج من المكدس.

رابعًا، تجعل المكدسات استدعاء الدوال أبطأ، لأنه يجب نقل القيم خارج المسجلات إلى الذاكرة، في حين تسمح بعض المعماريات الحاسوبية بتمرير الوسائط في المسجلات مباشرةً، ولكن يجب تدوير المسجلات للحفاظ على دلالات حصول كل دالة على نسخة فريدة من كل وسيط.

خامسًا، لا بد أنك سمعت بمصطلح طفحان المكدس Stack Overflow الذي يُعَدّ طريقةً شائعةً لاختراق النظام من خلال تمرير قيم وهمية، فإذا كنت مبرمجًا تقبل بالإدخال العشوائي في متغير المكدس مثل القراءة من لوحة المفاتيح أو عبر الشبكة، فيجب عليك تحديد حجم هذه البيانات صراحةً، إذ يؤدي السماح بأيّ كمية من البيانات دون تحديد إلى الكتابة فوق الذاكرة، مما يؤدي إلى حدوث عطل، ولكن أدرك بعض الأشخاص أنهم إذا كتبوا في ذاكرة كافية فقط لوضع قيمة محددة في جزء العنوان المُعاد من إطار المكدس، فيمكنهم إعادتها في البيانات التي أرسلوها للتو عند اكتمال الدالة بدلًا من الإعادة إلى المكان الصحيح الذي استدعاها، فإذا احتوت هذه البيانات على شيفرة ثنائية قابلة للتنفيذ وتخترق النظام مثل تشغيل طرفية للمستخدِم مع صلاحيات الجذر، فهذا يعني تعرّض حاسوبك للاختراق. يحدث ذلك بسبب نمو المكدس للأسفل، ولكن تُقرَأ البيانات للأعلى أي من العنوان الأدنى إلى العناوين الأعلى.

هناك عدة طرق للتغلب على هذه المشكلة، إذ يجب عليك التأكد -بصفتك مبرمجًا- من أنك تتحقق دائمًا من كمية البيانات التي تتلقاها في متغير، ويمكن أن يساعد نظام التشغيل في تجنّب ذلك نيابةً عن المبرمج من خلال التأكد من تمييز المكدس على أنه غير قابل للتنفيذ، وبالتالي لن يشغّل المعالج أيّ شيفرة برمجية، حتى إذا حاول مستخدِم سيئ تمرير شيفرة برمجية إلى برنامجك، وتدعم المعماريات وأنظمة التشغيل الحديثة هذه الوظيفة.

سادسًا، يدير المصرّف Compiler المكدسات، فهو المسؤول عن إنشاء شيفرة البرنامج، ويبدو المكدس بالنسبة لنظام التشغيل مثل أيّ منطقة أخرى من الذاكرة الخاصة بالعملية.

يعرِّف العتاد المسجِّل بوصفه مؤشر المكدس Stack Pointer بهدف تعقّب نمو المكدس الحالي، إذ يستخدِم المصرّف -أو المبرمج عند الكتابة باستخدام لغة التجميع- هذا المسجِّل لتعقّب الجزء العلوي الحالي من المكدس، وإليك مثال عن مؤشر المكدس:

$ cat sp.c
void function(void)
{
  int i = 100;
  int j = 200;
  int k = 300;
}

$ gcc -fomit-frame-pointer -S sp.c

$ cat sp.s
  .file   "sp.c"
  .text
.globl function
  .type   function, @function
function:
  subl    $16, %esp
  movl    $100, 4(%esp)
  movl    $200, 8(%esp)
  movl    $300, 12(%esp)
  addl    $16, %esp
  ret
  .size   function, .-function
  .ident  "GCC: (GNU) 4.0.2 20050806 (prerelease) (Debian 4.0.1-4)"
  .section        .note.GNU-stack,"",@progbits

عرضنا في المثال السابق دالةً بسيطةً تخصّص ثلاثة متغيرات على المكدس، إذ توضّح شيفرة فك التجميع السابقة استخدام مؤشر المكدس في معمارية x86.

أولًا، نخصّص مساحةً على المكدس لمتغيراتنا المحلية، كما تنمو المكدسات للأسفل، لذلك يجب أن نطرح من القيمة الموجودة في مؤشر المكدس.

تُعَدّ القيمة 16 قيمةً كبيرةً بما يكفي للاحتفاظ بمتغيراتنا المحلية، ولكن يمكن ألّا تكون بالحجم المطلوب للحفاظ على محاذاة المكدس في الذاكرة ضمن الحدود التي يتطلبها المصرّف، إذ نحتاج مثلًا 12 بايت فقط وليس 16 مع 3 قيم من النوع int المكوَّن من 4 بايتات، وننقل بعد ذلك القيم إلى ذاكرة المكدس التي تستخدِمها الدالة الحقيقية.

أخيرًا، نسحب القيم من المكدس قبل العودة إلى الدالة الأصلية من خلال تحريك مؤشر المكدس إلى حيث كان قبل أن نبدأ.

ملاحظة: لاحظ أننا استخدمنا رايةً خاصةً في مصرّف gcc، وهذه الراية هي ‎-fomit-frame-pointer التي تحدِّد أنه لا ينبغي استخدام مسجل إضافي للاحتفاظ بمؤشر إلى بداية إطار المكدس، إذ يساعد وجود هذا المؤشر منقّحات الأخطاء للانتقال للأعلى عبر إطارات المكدس، ولكنه يجعل المسجِّل متاحًا بصورة أقل للتطبيقات الأخرى.

الكومة Heap

الكومة Heap هي مساحة من الذاكرة تديرها العملية لتخصيص الذاكرة السريع، وتُستخدَم مع المتغيرات التي لا تكون متطلبات ذاكرتها معروفةً في وقت التصريف، ويُعرَف الجزء السفلي من الكومة بالاسم brk وهو استدعاء النظام الذي يعدّل الكومة، كما يمكن للعملية أن تطلب من النواة تخصيص مزيد من الذاكرة لاستخدامها باستخدام الاستدعاء brk لتوسيع المنطقة إلى الأسفل.

يدير استدعاء مكتبة malloc الكومةَ، وهذا يجعل إدارة الكومة أمرًا سهلًا للمبرمج من خلال السماح له بتخصيص وتحرير كومة ذاكرة باستخدام الاستدعاء free، كما يمكن أن يستخدِم الاستدعاء malloc أنظمةً مثل مخصّص الأصدقاء Buddy Allocator لإدارة كومة ذاكرة المستخدِم، ويمكن أن يكون الاستدعاء malloc ذكيًا فيما يتعلق بعملية التخصيص ويمكنه استخدام عمليات ربط مجهولة Anonymous mmaps لذاكرة عملية إضافية، وهو المكان الذي يربط منطقة من ذاكرة RAM الخاصة بالنظام مباشرةً بدلًا من ربط ملف مع ذاكرة العملية، إذ يمكن أن يكون ذلك أكثر كفاءةً، كما أنه ليس مألوفًا أن يكون لأيّ برنامج حديث سبب لاستدعاء brk مباشرة نظرًا لتعقيد إدارة الذاكرة بصورة صحيحة.

تخطيط الذاكرة

تمتلك العملية مناطق أصغر من الذاكرة المخصَّصة لها ويكون لكل منها غرض محدد.

ذكرنا في الشكل السابق كيفية وضع العملية في الذاكرة باستخدام النواة، إذ تحتفظ النواة لنفسها ببعض الذاكرة في الجزء العلوي من العملية، ويمكن مشاركة هذه الذاكرة فعليًا بين جميع العمليات باستخدام الذاكرة الوهمية، كما يوجد أسفل ذلك مساحة للملفات والمكتبات المربوطة mmaped، ثم يوجد المكدس وتحته الكومة، في حين توجد في الجزء السفلي صورة البرنامج كما جرى تحميلها من الملف القابل للتنفيذ على القرص الصلب، وسنلقي نظرةً على عملية تحميل هذه البيانات في مقالات لاحقة.

واصفات الملف File Descriptors

تعرّفنا سابقًا على الملفات الافتراضية المعطاة لكل عملية وهي stdin و stdout و stderr، إذ يكون لهذه الملفات دائمًا رقم واصف الملف نفسه (0 و 1 و 2 على التوالي)، وبالتالي تحتفظ النواة بواصفات الملفات بصورة فردية لكل عملية.

تمتلك واصفات الملفات أذونات أيضًا، إذ يمكن أن تتمكّن من القراءة من ملف ولكن لا يمكنك الكتابة فيه مثلًا، إذ يحتفظ نظام التشغيل عند فتح الملف بسجل أذونات العمليات لهذا الملف في واصف الملف ولا يسمح للعملية بفعل أيّ شيء لا ينبغي فعله.

المسجلات Registers

يطبّق المعالج عمليات بسيطة على القيم الموجودة في المسجِّلات، وتُقرَأ هذه القيم وتُكتَب في الذاكرة، فلكل عملية منطقة مخصَّصة لها في الذاكرة التي تتعقّبها النواة، لذا يجب تعقّب المسجلات، فإذا حان الوقت لتتخلّى العملية المُشغَّلة عن المعالج لتشغيل عملية أخرى، فيجب حفظ حالتها الحالية، كما يجب أن نكون قادرِين على استعادة هذه الحالة عند منح العملية مزيدًا من الوقت للتشغيل على وحدة المعالجة المركزية، لذا يجب على نظام التشغيل تخزين نسخة من مسجِّلات وحدة المعالجة المركزية في الذاكرة.

ينسخ نظام التشغيل قيم المسجّل مرةً أخرى من الذاكرة إلى مسجلات وحدة المعالجة المركزية عندما يحين وقت تشغيل العملية مرةً أخرى وستعود العملية للعمل من حيث توقفت.

حالة النواة

يجب أن تتعقّب النواة عددًا من العناصر لكل عملية والتي سنوضّحها فيما يلي.

حالة العملية

يجب على نظام التشغيل تعقّب حالة العملية، فإذا كانت العملية قيد التشغيل حاليًا، فيجب أن تكون بحالة تشغيل Running، لكن إذا طلبت العملية قراءة ملف من القرص الصلب، فسنعلَم من تسلسل الذواكر الهرمي أنّ ذلك يمكن أن يستغرق وقتًا طويلًا، إذ يجب على العملية التخلي عن تنفيذها الحالي للسماح بتشغيل عملية أخرى، ولكن لا يجب أن تسمح النواة بتشغيل العملية مرةً أخرى حتى تصبح البيانات من القرص الصلب متاحةً في الذاكرة، وبالتالي يمكن تحديد العملية على أنها في حالة انتظار القرص الصلب Disk Wait حتى تصبح البيانات جاهزةً.

الأولوية Priority

تُعَدّ بعض العمليات أكثر أهميةً من غيرها وتحظى بأولوية أعلى.

الإحصائيات

يمكن للنواة الاحتفاظ بإحصائيات حول سلوك كل عملية والتي يمكن أن تساعدها في اتخاذ قرارات حول كيفية تصرف العملية مثل معرفة ما إذا كانت العملية تقرأ من القرص الصلب في أغلب الأحيان أم أنها تنفّذ عمليات مكثفةً في وحدة المعالجة المركزية بمعدّل أعلى.

مقالات ذات صلة

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى