الغوص في الأنظمة › 13. نظام التشغيل 14 / 18 السابق التالي

13. نظام التشغيل

نظام التشغيل (operating system أو OS) طبقة برمجيات نظامية خاصة تقع بين عتاد الحاسوب وبرامج التطبيقات العاملة عليه (انظر الشكل 1). وتبقى برمجيات نظام التشغيل مقيمة على الحاسوب من لحظة التشغيل إلى لحظة الإطفاء. وغرضه الأساسي هو إدارة مكوّنات العتاد الكامنة لتشغيل أحمال البرامج بكفاءة وجعل الحاسوب سهل الاستخدام.

يقع نظام التشغيل بين المستخدم والعتاد الشكل 1. نظام التشغيل برنامج نظامي خاص بين المستخدم والعتاد. يدير عتاد الحاسوب وينفّذ تجريدات تجعل العتاد أسهل في الاستخدام.

من أوجه تسهيل نظام التشغيل لاستخدام عتاد الحاسوب دعمه لبدء تشغيل البرامج عليه. فكّر فيما يحدث عندما ينقر مستخدم نقرًا مزدوجًا على أيقونة أو يكتب اسم ملف تنفيذي لبرنامج عند مطالبة الصدفة (مثل ./a.out) لتشغيل برنامج على النظام الكامن. يتولى نظام التشغيل كل تفاصيل هذه العملية، مثل تحميل البرنامج من القرص إلى RAM وتهيئة المعالج لبدء تنفيذ تعليمات البرنامج؛ فيخفي نظام التشغيل عن المستخدمين هذه الأنواع من الإجراءات منخفضة المستوى اللازمة لتشغيل برنامج المستخدم على الحاسوب.

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

ومن أمثلة أنظمة التشغيل Windows من Microsoft وmacOS وiOS من Apple وSolaris من Oracle والمتغيرات مفتوحة المصدر لـ Unix مثل OpenBSD وLinux. ونستخدم أمثلة Linux في هذا الكتاب. لكن جميع أنظمة التشغيل العامة الأخرى هذه تنفّذ وظائف مشابهة، وإن كانت أحيانًا بطرق مختلفة.

النواة

غالبًا ما يُستخدم مصطلح نظام التشغيل للإشارة إلى مجموعة كبيرة من البرمجيات النظامية التي تؤدي نوعًا من إدارة الموارد وتنفّذ تجريدات «سهلة الاستخدام» للنظام الكامن. ونركّز في هذا الفصل على نواة (kernel) نظام التشغيل؛ ولذلك عندما نستخدم مصطلح نظام التشغيل وحده، فإننا نعني نواة نظام التشغيل.

تنفّذ نواة نظام التشغيل الوظائف الجوهرية لنظام التشغيل، أي الوظائف اللازمة لأي استخدام للنظام. وتشمل هذه الوظائف إدارة طبقة عتاد الحاسوب لتشغيل البرامج، وتنفيذ تجريدات نظام التشغيل المُصدَّرة لمستخدمي النظام وإدارتها (مثلًا، الملفات تجريد من نظام التشغيل فوق البيانات المخزَّنة)، وتنفيذ واجهات لطبقة تطبيقات المستخدم وطبقة أجهزة العتاد. وتنفّذ النواة آليات (mechanisms) تتيح للعتاد تشغيل البرامج وتنفيذ تجريداته مثل العمليات. والآليات هي الجزء المتعلق بـ «كيف» من وظائف نظام التشغيل. كما تنفّذ النواة سياسات (policies) لإدارة عتاد الحاسوب بكفاءة وحكم تجريداته. وتملي السياسات الجزء المتعلق بـ «ماذا» و«متى» و«لمن» من وظائف نظام التشغيل. فمثلًا، تنفّذ آلية تهيئة المعالج لتنفيذ تعليمات عملية معينة، وتقرر سياسة أي عملية ستأتي دورها في العمل على المعالج.

وتنفّذ النواة واجهة برمجية لمستخدمي النظام: واجهة نداءات النظام (system call interface). ويتفاعل المستخدمون والبرامج مع نظام التشغيل عبر واجهة نداءات النظام. فمثلًا، إذا أراد برنامج معرفة الوقت الحالي لليوم، فيمكنه الحصول على تلك المعلومة من نظام التشغيل باستدعاء نداء النظام gettimeofday.

وتوفّر النواة أيضًا واجهة للتفاعل مع أجهزة العتاد (واجهة الأجهزة، device interface). وعادةً تتفاعل أجهزة الإدخال/الإخراج مثل محركات الأقراص الصلبة (HDD) ولوحات المفاتيح ومحركات الأقراص ذات الحالة الصلبة (SSD) مع النواة عبر هذه الواجهة. وتأتي هذه الأجهزة ببرمجيات مشغّلات أجهزة خاصة تعمل في نظام التشغيل وتتولى نقل البيانات من جهاز معين أو إليه. وتتفاعل برمجيات مشغّل الجهاز مع نظام التشغيل عبر واجهة الأجهزة فيه؛ ويمكن إضافة جهاز جديد إلى نظام حاسوبي بتحميل شيفرة مشغّل جهازه، المكتوبة لتتوافق مع واجهة أجهزة نظام التشغيل، في نظام التشغيل. وتدير النواة مباشرةً أجهزة عتاد أخرى مثل المعالج وRAM. ويعرض الشكل 2 طبقة نواة نظام التشغيل بين تطبيقات المستخدم وعتاد الحاسوب، بما في ذلك واجهتها البرمجية للمستخدمين وواجهة أجهزة العتاد فيها.

ينفّذ نواة نظام التشغيل واجهة للتطبيقات وأجهزة العتاد الشكل 2. نواة نظام التشغيل: الوظائف الأساسية لنظام التشغيل الضرورية لاستخدام النظام وتسهيل التعاون بين أجهزة الإدخال/الإخراج ومستخدمي النظام

وفي بقية هذا الفصل، نفحص الدور الذي يؤديه نظام التشغيل في تشغيل البرامج وإدارة موارد النظام بكفاءة. وتركّز مناقشتنا أساسًا على آلية (أي «كيف») وظائف نظام التشغيل وتنفيذ تجريدين رئيسيين من تجريداته: العملية (برنامج عامل) والذاكرة الافتراضية (virtual memory، رؤية لفضاء ذاكرة العملية مجرَّدة عن تخزينها الفيزيائي الكامن في RAM أو التخزين الثانوي).

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

يشغّل نظام التشغيل البرامج على العتاد الشكل 1. الخطوات التي يتخذها نظام التشغيل لبدء تشغيل برنامج جديد على العتاد الأساسي

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

13.1.1. إقلاع نظام التشغيل

تُعرف عملية تحميل نظام التشغيل لنفسه على الحاسوب وتهيئته لها بـ الإقلاع (booting) — فالنظام «ينهض بنفسه من حذائه»، أو يقلع نفسه على الحاسوب. ويحتاج نظام التشغيل إلى قليل من المساعدة ليُحمَّل ابتدائيًا على الحاسوب ويبدأ تشغيل شيفرة الإقلاع لديه. ولبدء تشغيل شيفرة نظام التشغيل، تعمل شيفرة مخزَّنة في البرمجيات الثابتة للحاسوب (ذاكرة غير متطايرة في العتاد) عند تشغيل الحاسوب أول مرة؛ ومن أمثلة هذا النوع من البرمجيات الثابتة BIOS (نظام الإدخال/الإخراج الأساسي) وUEFI (واجهة البرمجيات الثابتة القابلة للتوسيع الموحَّدة). وعند التشغيل، تعمل BIOS أو UEFI وتجري من تهيئة العتاد ما يكفي لتحميل أول جزء من نظام التشغيل (كتلة الإقلاع) من القرص إلى RAM وبدء تنفيذ تعليمات كتلة الإقلاع على المعالج. وبمجرد بدء عمل نظام التشغيل، يحمّل بقية نفسه من القرص، ويكتشف موارد العتاد ويهيّئها، ويهيّئ بنى بياناته وتجريداته ليجعل النظام جاهزًا للمستخدمين.

13.1.2. جعل نظام التشغيل يفعل شيئًا: المقاطعات والمصائد

بعد أن ينهي نظام التشغيل الإقلاع وتهيئة النظام للاستخدام، ينتظر عندئذٍ ببساطة أن يكون له عمل. وتُنفَّذ معظم أنظمة التشغيل كـأنظمة مدفوعة بالمقاطعات (interrupt-driven systems)، أي أن نظام التشغيل لا يعمل حتى يحتاج كيان ما إليه لفعل شيء — فيُوقظ نظام التشغيل (يُقاطَع من نومه) لمعالجة طلب.

قد تحتاج الأجهزة في طبقة العتاد إلى أن يفعل نظام التشغيل شيئًا من أجلها. فمثلًا، بطاقة واجهة الشبكة (network interface card أو NIC) واجهة عتادية بين حاسوب وشبكة. وعندما تتلقى البطاقة بيانات عبر اتصالها الشبكي، تقاطع (أو توقظ) نظام التشغيل لمعالجة البيانات المتلقاة (انظر الشكل 2). فمثلًا، قد يحدد نظام التشغيل أن البيانات التي تلقتها البطاقة جزء من صفحة ويب طلبها متصفح ويب؛ ثم يسلّم البيانات من البطاقة إلى عملية متصفح الويب المنتظرة.

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

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

أما المقاطعات التي تأتي من طبقة العتاد، مثل تلقّي البطاقة بيانات من الشبكة، فيُشار إليها عادة باسم مقاطعات العتاد، أو المقاطعات (interrupts) فقط. وأما المقاطعات التي تأتي من طبقة البرمجيات نتيجة تنفيذ تعليمات، مثل إجراء تطبيق نداء نظام، فيُشار إليها عادة بـ المصائد (traps). أي أن نداء النظام «يصطاد داخل نظام التشغيل»، الذي يعالج الطلب نيابةً عن البرنامج على مستوى المستخدم. وقد تقاطع الاستثناءات من أي من الطبقتين نظام التشغيل أيضًا. فمثلًا، قد يقاطع محرك الأقراص الصلبة نظام التشغيل إذا فشلت قراءة بسبب كتلة قرص تالفة، وقد يثير برنامج تطبيقي مصيدة إلى نظام التشغيل إذا نفّذ تعليمة قسمة على صفر.

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

وإليك مثالًا على شكل نداء النظام write على نظام IA32 Linux:

/* C code */
ret = write(fd, buff, size);

# IA32 translation
write:

...            # set up state and parameters for OS to perform write
movl $4, %eax  # load 4 (unique ID for write) into register eax
int  $0x80     # trap instruction: interrupt the CPU and transition to the OS
addl $8, %ebx  # an example instruction after the trap instruction

تضع التعليمة الأولى (movl $4, %eax) رقم نداء النظام الخاص بـ write (وهو 4) في السجل eax. وتُفعّل التعليمة الثانية (int $0x80) المصيدة. وعندما تعمل شيفرة معالج المصيدة في نظام التشغيل، تستخدم القيمة في السجل eax (4) لتحديد أي نداء نظام يُستدعى، وتشغّل شيفرة معالج المصيدة المناسبة (وهي في هذه الحالة شيفرة معالج write). وبعد عمل معالج نظام التشغيل، يواصل النظام تنفيذ البرنامج عند التعليمة التالية مباشرةً لتعليمة المصيدة (addl في هذا المثال).

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

ناقل المقاطعات الشكل 3. يُرسل جهاز عتاد (قرص) إشارة إلى المعالج عبر ناقل المقاطعات لتشغيل نظام التشغيل نيابةً عنه.

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

ولأن نظام التشغيل برمجية، وشيفرته تُحمَّل في RAM وتعمل على المعالج كما تعمل شيفرة برنامج المستخدم، يجب أن يحمي نظام التشغيل شيفرته وحالته من العمليات العادية العاملة في النظام. ويساعد المعالج في ذلك بتعريف نمطي تنفيذ:

  1. في وضع المستخدم (user mode) ينفّذ المعالج تعليمات على مستوى المستخدم فقط ويصل إلى مواقع الذاكرة التي يتيحها له نظام التشغيل فقط. ويمنع نظام التشغيل عادةً معالجًا في وضع المستخدم من الوصول إلى تعليمات نظام التشغيل وبياناته. كما يقيّد وضع المستخدم مكوّنات العتاد التي يستطيع المعالج الوصول إليها مباشرةً.
  2. وفي وضع النواة (kernel mode)، ينفّذ المعالج أي تعليمات ويصل إلى أي موقع ذاكرة (بما في ذلك المواقع التي تخزّن تعليمات نظام التشغيل وبياناته). ويمكنه أيضًا الوصول مباشرةً إلى مكوّنات العتاد وتنفيذ تعليمات خاصة.

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

يشغّل نظام التشغيل شيفرة معالج المقاطعات الشكل 4. المعالج والمقاطعات. تُقاطَع شيفرة المستخدم الجارية على المعالج (عند الوقت X على الخط الزمني) وتعمل شيفرة معالج المقاطعات لنظام التشغيل. وبعد انتهاء نظام التشغيل من معالجة المقاطعة، يُستأنَف تنفيذ شيفرة المستخدم (عند الوقت Y على الخط الزمني).

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

يُخطَّط نظام التشغيل في فضاء عناوين كل عملية الشكل 5. فضاء عناوين العملية: تُخطَّط نواة نظام التشغيل في أعلى فضاء عناوين كل عملية.

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

13.1.3. المراجع

  1. Meltdown and Spectre. https://meltdownattack.com/

من التجريدات الرئيسية التي ينفّذها نظام التشغيل العملية. وتمثّل العملية نسخة من برنامج عامل في النظام، وتشمل الشيفرة التنفيذية الثنائية للبرنامج وبياناته وسياق (context) التنفيذ. ويتتبع السياق تنفيذ البرنامج بالحفاظ على قيم سجلاته وموقع مكدّسه والتعليمة التي ينفّذها حاليًا.

والعمليات تجريدات ضرورية في أنظمة تعدد البرامج التي تدعم وجود عمليات متعددة في النظام في الوقت نفسه. ويستخدم نظام التشغيل تجريد العملية لتتبّع النسخ الفردية للبرامج العاملة في النظام وإدارة استخدامها لموارد النظام.

ويوفّر نظام التشغيل لكل عملية تجريد «الرؤية المنفردة» للنظام. أي أن نظام التشغيل يعزل العمليات عن بعضها ويمنح كل عملية وهم أنها تتحكم بالآلة كلها. وفي الواقع، يدعم نظام التشغيل عمليات نشطة كثيرة ويدير تقاسم الموارد بينها. ويخفي نظام التشغيل تفاصيل تقاسم موارد النظام والوصول إليها عن المستخدم، ويحمي العمليات من أفعال العمليات الأخرى العاملة في النظام.

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

13.2.1. تعدد البرامج وتبديل السياق

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

وكثيرًا ما تنفّذ أنظمة التشغيل عامة الغرض المشاركة الزمنية (timesharing)، وهي تعدد برامج يجعل فيه نظام التشغيل كل عملية تحين دورها للتنفيذ على المعالج لفترات زمنية قصيرة (تُعرف بـ شريحة زمنية أو كم زمني، time slice أو quantum). وعندما تكمل عملية شريحتها الزمنية على المعالج، يزيلها نظام التشغيل من المعالج ويترك عملية أخرى تعمل. وتعرف معظم الأنظمة الشرائح الزمنية بأنها بضعة أجزاء من الألف من الثانية (10-3 ثانية)، وهو زمن طويل بمقاييس دورات المعالج لكنه غير ملحوظ للإنسان.

وتدعم أنظمة المشاركة الزمنية كذلك «الرؤية المنفردة» للنظام الحاسوبي للمستخدم؛ ولأن كل عملية تنفّذ على المعالج في دفعات قصيرة متكررة، فإن كونها كلها تتشارك المعالج غير محسوس عادةً للمستخدم. ولا يلاحظ المستخدم آثار العمليات الأخرى في النظام إلا عندما يكون النظام محمّلًا تحميلًا ثقيلًا جدًا. ويسرد أمر Unix ps -A جميع العمليات العاملة في النظام — وقد تُفاجأ بعددها. كما يفيد أمر top في رؤية حالة النظام أثناء عمله بعرض مجموعة العمليات التي تستهلك حاليًا أكبر قدر من موارد النظام (مثل زمن المعالج ومساحة الذاكرة).

في الأنظمة متعددة البرامج والمشاركة زمنيًا، تعمل العمليات بالتزامن (concurrently)، أي أن تنفيذها يتداخل في الزمن. فمثلًا، قد يبدأ نظام التشغيل تشغيل العملية A على المعالج، ثم ينتقل إلى تشغيل العملية B لفترة، ثم يعود لاحقًا إلى تشغيل العملية A أكثر. وفي هذا السيناريو، تعمل العمليتان A وB بالتزامن لأن تنفيذهما على المعالج يتداخل بسبب تبديل نظام التشغيل بينهما.

تبديل السياق

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

يجري نظام التشغيل تبديل السياق (context switching)، أي تبديل حالة العملية على المعالج، كآلية رئيسية وراء تعدد البرامج (والمشاركة الزمنية). وثمة خطوتان رئيسيتان لتنفيذ تبديل سياق المعالج:

  1. يحفظ نظام التشغيل سياق العملية الحالية العاملة على المعالج، بما في ذلك جميع قيم سجلاتها (PC ومؤشرات المكدّس والسجلات عامة الغرض وأكواد الشروط وغيرها)، وحالة ذاكرتها، وبعض الحالات الأخرى (مثل حالة موارد النظام التي تستخدمها، كالملفات المفتوحة).
  2. يستعيد نظام التشغيل السياق المحفوظ من عملية أخرى على المعالج ويبدأ تشغيل المعالج لهذه العملية الأخرى، فيواصل تنفيذها من التعليمة التي توقف عندها.

ومن أوجه تبديل السياق ما قد يبدو مستحيل التحقيق: وهو أن شيفرة نظام التشغيل التي تنفّذ تبديل السياق يجب أن تعمل على المعالج أثناء حفظ (استعادة) سياقات تنفيذ عملية من المعالج (إليه)؛ فتعليمات شيفرة تبديل السياق تحتاج إلى استخدام سجلات عتاد المعالج للتنفيذ، لكن قيم سجلات العملية التي يُبدَّل سياقها خارج المعالج يجب أن تُحفظ بشيفرة تبديل السياق. ويقدّم عتاد الحاسوب بعض المساعدة لجعل ذلك ممكنًا.

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

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

13.2.2. حالة العملية

في الأنظمة متعددة البرامج، يجب أن يتتبع نظام التشغيل العمليات المتعددة الموجودة في النظام في أي لحظة معينة ويديرها. ويحتفظ نظام التشغيل بمعلومات عن كل عملية، منها:

  • معرّف العملية (process id أو PID)، وهو معرّف فريد للعملية. ويسرد الأمر ps معلومات عن العمليات في النظام، بما في ذلك قيم PID الخاصة بها.
  • معلومات فضاء العنوان للعملية.
  • حالة تنفيذ العملية (مثل قيم سجلات المعالج وموقع المكدّس).
  • مجموعة الموارد المخصصة للعملية (مثل الملفات المفتوحة).
  • حالة العملية (process state) الحالية، وهي قيمة تحدد أهليتها للتنفيذ على المعالج.

وتمر العملية في أثناء عمرها بعدة حالات تقابل فئات مختلفة من أهلية تنفيذ العملية. ومن أوجه استخدام نظام التشغيل لحالة العملية تحديد مجموعة العمليات المرشحة للجدولة على المعالج.

ومجموعة حالات تنفيذ العملية هي:

  • جاهزة (Ready): يمكن للعملية أن تعمل على المعالج لكنها غير مجدولة حاليًا (فهي مرشحة لتبديل سياقها إلى المعالج). وبمجرد أن ينشئ نظام التشغيل عملية جديدة ويهيّئها، تدخل حالة الجاهزية (فهي جاهزة ليبدأ المعالج تنفيذ تعليمتها الأولى). وفي نظام مشاركة زمنية، إذا بُدِّل سياق عملية خارج المعالج لانتهاء شريحتها الزمنية، تُوضع أيضًا في حالة الجاهزية (فهي جاهزة لينفّذ المعالج تعليمتها التالية، لكنها استهلكت شريحتها الزمنية وعليها انتظار دورها لتُجدول مرة أخرى على المعالج).
  • عاملة (Running): العملية مجدولة على المعالج وتنفّذ التعليمات فعليًا.
  • محجوبة (Blocked): العملية تنتظر حدثًا ما قبل أن يمكن متابعة تنفيذها. فمثلًا، تنتظر العملية قراءة بعض البيانات من القرص. والعمليات المحجوبة ليست مرشحة للجدولة على المعالج. وبعد حدوث الحدث الذي تُحجب عنده العملية، تنتقل العملية إلى حالة الجاهزية (فهي جاهزة للعمل مرة أخرى).
  • منتهية (Exited): انتهت العملية لكن ما زال يلزم إزالتها بالكامل من النظام. وتنتهي العملية بإكمالها تنفيذ تعليمات برنامجها، أو بالخروج بخطأ (مثل محاولة القسمة على صفر)، أو بتلقّي طلب إنهاء من عملية أخرى. والعملية المنتهية لن تعمل مرة أخرى أبدًا، لكنها تبقى في النظام حتى يكتمل التنظيف النهائي المرتبط بحالة تنفيذها.

ويعرض الشكل 1 عمر العملية في النظام، موضحًا كيف تنتقل بين الحالات المختلفة. لاحظ الانتقالات (الأسهم) من حالة إلى أخرى. فمثلًا، يمكن أن تدخل العملية حالة الجاهزية بإحدى ثلاث طرق: أولًا، إذا أنشأها نظام التشغيل حديثًا؛ وثانيًا، إذا كانت محجوبة في انتظار حدث ما وحدث ذلك الحدث؛ وثالثًا، إذا كانت عاملة على المعالج وانتهت شريحتها الزمنية فبدّل نظام التشغيل سياقها لإعطاء عملية جاهزة أخرى دورها على المعالج.

حالة العملية الشكل 1. حالات العملية خلال فترة حياتها زمن تشغيل العملية

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

وثمة مقياسان مختلفان لزمن تشغيل العملية. الأول الزمن الحائطي (wall time) الإجمالي (أو زمن ساعة الحائط). والزمن الحائطي هو المدة بين بداية العملية واكتمالها؛ أي الزمن المنقضي من بداية العملية إلى انتهائها كما تقيسه ساعة معلقة على حائط. ويتضمن الزمن الحائطي الوقت الذي تقضيه العملية في الحالة العاملة منفّذةً على المعالج، والوقت الذي تقضيه في الحالة المحجوبة منتظرةً حدثًا مثل الإدخال/الإخراج، والوقت الذي تقضيه في الحالة الجاهزة منتظرةً دورها لتُجدول للعمل على المعالج. وفي الأنظمة متعددة البرامج والمشاركة زمنيًا، يمكن أن يتباطأ الزمن الحائطي لعملية بسبب عمليات أخرى تعمل بالتزامن في النظام وتتشارك موارد النظام.

والمقياس الثاني لزمن تشغيل العملية هو زمن المعالج (CPU time) الإجمالي (أو زمن العملية). ويقيس زمن المعالج مقدار الوقت الذي تقضيه العملية في الحالة العاملة فقط منفّذةً تعليماتها على المعالج. ولا يتضمن زمن المعالج الوقت الذي تقضيه العملية في الحالتين المحجوبة أو الجاهزة. ونتيجة لذلك، لا يتأثر إجمالي زمن المعالج لعملية بغيرها من العمليات العاملة بالتزامن في النظام.

13.2.3. إنشاء العمليات (وتدميرها)

ينشئ نظام التشغيل عملية جديدة عندما يجري كائن عملية قائمة نداء نظام يطلب منه ذلك. وفي Unix، ينشئ نداء النظام fork عملية جديدة. والعملية المستدعية لـ fork هي العملية الأب (parent) والعملية الجديدة التي ينشئها هي العملية الابن (child). فمثلًا، إذا شغّلت a.out في صدفة، تستدعي عملية الصدفة نداء النظام fork لتطلب من نظام التشغيل إنشاء عملية ابن جديدة تُستخدم لتشغيل برنامج a.out. ومثال آخر عملية متصفح ويب تستدعي fork لإنشاء عمليات ابن للتعامل مع أحداث تصفح مختلفة. فقد ينشئ متصفح الويب عملية ابن للتعامل مع الاتصال بخادم ويب عند تحميل المستخدم لصفحة ويب. وقد ينشئ عملية أخرى للتعامل مع مدخلات فأرة المستخدم، وعمليات أخرى للتعامل مع نوافذ أو تبويبات متصفح منفصلة. ويستطيع متصفح ويب متعدد العمليات كهذا مواصلة التعامل مع طلبات المستخدم عبر بعض عمليات ابن متصفحه، بينما قد تكون بعض عمليات ابنه الأخرى محجوبة في انتظار ردود خادم ويب بعيد أو نقرات فأرة المستخدم.

وتسلسل هرمي للعمليات من علاقات الأب-الابن موجود بين مجموعة العمليات النشطة في النظام. فمثلًا، إذا أجريت العملية A نداءين إلى fork، تُنشأ عمليتا ابن جديدتان، B وC_. وإذا استدعت العملية C بعد ذلك fork، تُنشأ عملية جديدة أخرى، D. والعملية C ابنة لـ A وأمّ لـ D. والعمليتان B وC شقيقتان (تتشاركان عملية أب واحدة، هي العملية A). والعملية A سلف لـ B وC وD. ويوضح الشكل 2 هذا المثال.

تسلسل العمليات الناتج من المثال. A هو السلف الأعلى وله ولدان، B وC تحته. وC لها ولد واحد، D، تحته. الشكل 2. مثال على تسلسل عمليات أنشأته عملية أم (A) باستدعاء fork مرتين لعمل عمليتَي ابن (B وC). واستدعاء C لـ fork يُنشئ عمليته الابنة D. لعرض تسلسل العمليات في أنظمة Linux، نفّذ pstree أو ps -Aef --forest.

ولأن العمليات القائمة هي التي تُثير إنشاء العمليات، يحتاج النظام إلى عملية واحدة على الأقل لإنشاء أي عمليات جديدة. وعند الإقلاع، ينشئ نظام التشغيل أول عملية على مستوى المستخدم في النظام. وهذه العملية الخاصة، المسماة init، تقع في أعلى التسلسل الهرمي للعمليات كسلف لجميع العمليات الأخرى في النظام.

fork

يُستخدم نداء النظام fork لإنشاء عملية. وفي وقت الـ fork، يرث الابن حالة تنفيذ أبيه. فينشئ نظام التشغيل نسخة من حالة تنفيذ العملية المستدعية (الأب) عند النقطة التي يستدعي فيها الأب fork. وتشمل حالة التنفيذ هذه محتويات فضاء عنوان الأب، وقيم سجلات المعالج، وأي موارد نظام خصصها (مثل الملفات المفتوحة). كما ينشئ نظام التشغيل بنية تحكم عملية (process control struct) جديدة، وهي بنية بيانات في نظام التشغيل لإدارة عملية الابن، ويعيّن لعملية الابن PID فريدًا. وبعد أن ينشئ نظام التشغيل العملية الجديدة ويهيّئها، يكون الابن والأب متزامنين — فيواصلان العمل معًا ويتداخل تنفيذهما عند تبديل نظام التشغيل بينهما على المعالج وخارجه.

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

ولتمييز عمليتي الابن والأب في البرنامج، يعيد نداء fork قيمتين مختلفتين للأب والابن. فدائمًا تتلقى عملية الابن قيمة إرجاع تساوي 0، بينما يتلقى الأب قيمة PID الخاصة بالابن (أو -1 إذا فشل fork).

فمثلًا، يعرض مقطع الشيفرة التالي نداءً إلى نداء النظام fork ينشئ عملية ابن جديدة للعملية المستدعية:

pid_t pid;

pid = fork();   /* create a new child process */

print("pid = %d\n", pid);  /* both parent and child execute this */

بعد أن ينشئ نداء fork عملية ابن جديدة، يواصل الأب والابن التنفيذ معًا، في سياقَي تنفيذ منفصلين، عند نقطة عودة نداء fork. وتُسند كلتا العمليتين قيمة إرجاع fork إلى متغيرهما pid، وكلتاهما تستدعيان printf. ويطبع نداء عملية الابن القيمة 0، ويطبع الأب قيمة PID الخاصة بالابن.

ويعرض الشكل 3 مثالًا على شكل التسلسل الهرمي للعمليات بعد تنفيذ هذه الشيفرة. فتحصل عملية الابن على نسخة مطابقة لسياق تنفيذ العملية الأب عند نقطة الـ fork، لكن القيمة المخزَّنة في متغيرها pid تختلف عن قيمة أبيها لأن fork يعيد قيمة PID الخاصة بالابن (14 في هذا المثال) إلى العملية الأب، ويعيد 0 إلى الابن.

تحصل عملية الابن المُنتَجة بـ fork على نسخة من حالة الأب، لكن fork تُرجع قيمة مختلفة للعملية الابنة والأب الشكل 3. تستدعي عملية (المعرّف 12) fork لإنشاء عملية ابن جديدة. تحصل عملية الابن الجديدة على نسخة دقيقة من عنوان أبيها وحالة تنفيذه، لكنها تحصل على معرّف عمليتها الخاص (المعرّف 14). تُرجع fork القيمة 0 لعملية الابن وقيمة معرّف الابن (14) للأب.

وغالبًا ما يريد المبرمج أن تؤدي عمليتا الابن والأب مهمتين مختلفتين بعد نداء fork. ويستطيع المبرمج استخدام قيم الإرجاع المختلفة من fork لدفع عمليتي الأب والابن إلى تنفيذ فرعي شيفرة مختلفين. فمثلًا، ينشئ مقطع الشيفرة التالي عملية ابن جديدة ويستخدم قيمة الإرجاع من fork لجعل عمليتي الابن والأب تنفذان فرعي شيفرة مختلفين بعد النداء:

pid_t pid;

pid = fork();   /* create a new child process */

if (pid == 0) {
    /* only the child process executes this code */
    ...
} else if (pid != -1)  {
    /* only the parent process executes this code */
    ...
}

من المهم تذكّر أن عمليتي الابن والأب، بمجرد إنشائهما، تعملان بالتزامن في سياقَي تنفيذهما الخاصين، وتعدّلان نسختيهما المنفصلتين من متغيرات البرنامج، وربما تنفّذان فرعين مختلفين في الشيفرة.

فكّر في البرنامج التالي الذي يحتوي نداءً إلى fork مع تفرع على قيمة pid لدفع عمليتي الأب والابن إلى تنفيذ شيفرة مختلفة (يعرض هذا المثال أيضًا نداءً إلى getpid يعيد PID العملية المستدعية):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {

    pid_t pid, mypid;

    printf("A\n");

    pid = fork();   /* create a new child process */

    if(pid == -1) {  /* check and handle error return value */
        printf("fork failed!\n");
        exit(pid);
    }

    if (pid == 0) { /* the child process */
        mypid = getpid();
        printf("Child: fork returned %d, my pid %d\n", pid, mypid);

    } else  {  /* the parent process */
        mypid = getpid();
        printf("Parent: fork returned %d, my pid %d\n", pid, mypid);
    }

    printf("B:%d\n", mypid);

    return 0;
}

عند تشغيل هذا البرنامج، قد تبدو مخرجاته كما يلي (افترض أن PID الأب هو 12 وPID الابن هو 14):

A
Parent: fork returned 14, my pid 12
B:12
Child: fork returned 0, my pid 14
B:14

وفي الواقع، قد تبدو مخرجات البرنامج أيًّا من الخيارات الممكنة المعروضة في الجدول 1 (وغالبًا ما سترى أكثر من ترتيب ممكن للمخرجات إذا شغّلت البرنامج عدة مرات). وفي الجدول 1، يطبع الأب B:12 ويطبع الابن B:14 في هذا المثال، لكن قيم PID الدقيقة ستتفاوت من تشغيل إلى آخر.

الخيار 1 الخيار 2 الخيار 3 الخيار 4 الخيار 5 الخيار 6
A A A A A A
Parent…​ Parent…​ Parent…​ Child…​ Child…​ Child…​
Child…​ Child…​ B:12 Parent…​ Parent…​ B:14
B:12 B:14 Child…​ B:12 B:14 Parent…​
B:14 B:12 B:14 B:14 B:12 B:12

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

بعد استدعاء الأب لـ fork، تُنفَّذ العمليتان بشكل متزامن الشكل 4. الخط الزمني لتنفيذ البرنامج. لا وجود لعملية الأب إلا قبل استدعاء fork. وبعد إرجاع fork، يعمل الاثنتان بشكل متزامن (كما هو موضَّح بالخطوط المتقطعة).

13.2.4. exec

عادةً ما تُنشأ عملية جديدة لتنفيذ برنامج يختلف عن برنامج عملية أبيها. وهذا يعني أن fork كثيرًا ما يُستدعى لإنشاء عملية بقصد تشغيل برنامج جديد من نقطة بدايته (أي بدء تنفيذه من تعليمته الأولى). فمثلًا، إذا كتب مستخدم ./a.out في صدفة، تشقّ عملية الصدفة عملية ابن جديدة لتشغيل a.out. وكعمليتين منفصلتين، تكون الصدفة وعملية a.out محميتين إحداهما من الأخرى؛ فلا تستطيع أي منهما التأثير في حالة تنفيذ الأخرى.

ومع أن fork ينشئ عملية الابن الجديدة، فهو لا يجعل الابن يشغّل a.out. ولتهيئة عملية الابن لتشغيل برنامج جديد، تستدعي عملية الابن أحد نداءات النظام exec. ويوفّر Unix عائلة من نداءات نظام exec تُفعّل نظام التشغيل ليكسو صورة العملية المستدعية بصورة جديدة من ملف تنفيذي ثنائي. وبعبارة أخرى، يطلب نداء نظام exec من نظام التشغيل أن يكتب فوق محتويات فضاء عنوان العملية المستدعية بالملف a.out المحدد وأن يعيد تهيئة حالة تنفيذها لتبدأ تنفيذ أول تعليمة في برنامج a.out.

ومن أمثلة نداءات نظام exec النداء execvp، ونموذج دالته كما يلي:

int execvp(char *filename, char *argv[]);

ويحدد المُعامِل filename اسم برنامج تنفيذي ثنائي لتهيئة صورة العملية، ويحتوي argv وسائط سطر الأوامر التي ستُمرَّر إلى الدالة main في البرنامج عند بدء تنفيذه.

وإليك مثالًا لمقطع شيفرة ينشئ، عند تنفيذه، عملية ابن جديدة لتشغيل a.out:

pid_t pid;
int  ret;
char *argv[2];

argv[0] = "a.out";  // initialize command line arguments for main
argv[1] = NULL;

pid = fork();
if (pid == 0) { /* child process */
    ret = execvp("a.out", argv);
    if (ret < 0) {
        printf("Error: execvp returned!!!\n");
        exit(ret);
    }
}

ويُهيَّأ المتغير argv بقيمة المُعامِل argv الذي يُمرَّر إلى الدالة main في a.out:

int main(int argc, char *argv) { ...

وسيحدد execvp القيمة التي سيمرّرها إلى argc بناءً على قيمة argv هذه (وهي 1 في هذه الحالة).

ويعرض الشكل 5 شكل التسلسل الهرمي للعمليات بعد تنفيذ هذه الشيفرة:

بعد fork يستدعي الابن exec الشكل 5. عندما تستدعي عملية الابن execvp (يسار)، يستبدل نظام التشغيل صورتها بالملف a.out (يمين) وتهيئ عملية الابن لبدء تشغيل برنامج a.out من بدايته.

ومما ينبغي ملاحظته في مثال الشيفرة المعروض أعلاه رسالة الخطأ التي تبدو غريبة بعد نداء execvp: فلماذا تكون العودة من نداء نظام exec خطأً؟ إذا نجح نداء نظام exec، فلن تُنفَّذ شيفرة اكتشاف الأخطاء ومعالجتها التي تليه فورًا أبدًا لأن العملية ستنفّذ الآن شيفرة في برنامج a.out بدلًا من هذه الشيفرة (فقد غيّر exec محتويات فضاء عنوان العملية). أي أنه عند نجاح نداء إلى دالة exec، لا تواصل العملية تنفيذها عند عودة نداء exec. ولهذا السلوك، فإن مقطع الشيفرة التالي يكافئ المقطع المعروض أعلاه (لكن المقطع أعلاه أسهل فهمًا عادةً):

pid_t pid;
int ret;

pid = fork();
if (pid == 0) { /* child process */
    ret = execvp("a.out", argv);
    printf("Error: execvp returned!!!\n");  /* only executed if execvp fails */
    exit(ret);
}

13.2.5. exit وwait

لتنتهي، تستدعي العملية نداء النظام exit الذي يُفعّل نظام التشغيل لتنظيف معظم حالة العملية. وبعد تشغيل شيفرة الخروج، تُخطر العملية عملية أبيها بأنها انتهت. والأب مسؤول عن تنظيف ما تبقى من حالة الابن المنتهي من النظام.

ويمكن دفع العمليات إلى الانتهاء بعدة طرق. أولًا، قد تكمل العملية كل شيفرة تطبيقها. فالعودة من الدالة main تؤدي إلى استدعاء العملية نداء النظام exit. وثانيًا، يمكن أن تجري العملية فعلًا غير صالح، مثل القسمة على صفر أو إلغاء الإشارة عن مؤشر فارغ، يؤدي إلى انتهائها. وأخيرًا، يمكن أن تتلقى العملية إشارة (signal) من نظام التشغيل أو من عملية أخرى تأمرها بالخروج (وفي الواقع، تؤدي القسمة على صفر وإلغاء الإشارة عن المؤشرات الفارغة إلى إرسال نظام التشغيل إشارتي SIGFPE وSIGSEGV إلى العملية تأمرها بالخروج).

الإشارات

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

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

ويحتوي قسم الإشارات مزيدًا من المعلومات عن الإشارات ومعالجتها.

وإذا أرادت عملية صدفة إنهاء عملية ابنها التي تشغّل a.out، فيمكنها إرسال إشارة SIGKILL إلى الابن. وعندما يتلقى الابن الإشارة، يشغّل شيفرة معالج الإشارة الخاصة بـ SIGKILL التي تستدعي exit، فينتهي الابن. وإذا كتب مستخدم CTRL-C في صدفة Unix تشغّل برنامجًا حاليًا، يتلقى الابن إشارة SIGINT. ويستدعي معالج الإشارة الافتراضي لـ SIGINT أيضًا exit، فينتهي الابن.

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

وتُحصِّد عملية الأب ابنها الزومبي (تنظّف بقية حالته من النظام) باستدعاء نداء النظام wait. وإذا استدعى الأب wait قبل انتهاء عملية ابنه، يحجب الأب حتى يتلقى إشارة SIGCHLD من الابن. ونداء النظام waitpid نسخة من wait تأخذ مُعامِل PID، ما يتيح للأب الحجب في انتظار انتهاء عملية ابن محددة.

ويعرض الشكل 6 تسلسل الأحداث التي تحدث عند انتهاء عملية.

تنتهي العملية الابنة الشكل 6. انتهاء العملية. يسار: تستدعي عملية الابن استدعاء النظام exit لتنظيف معظم حالة تنفيذها. وسط: بعد تنفيذ exit، تصبح عملية الابن زومبي (تدخل الحالة المنتهية ولا يمكنها العمل مرة أخرى)، ويُرسَل إلى العملية الأب إشارة SIGCHLD إعلاماً بأن ابنها قد انتهى. يمين: يستدعي العملية الأب waitpid لجني عملية الابن الزومبي (نظافة بقية حالة الابن من النظام).

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

ويستطيع المبرمج أيضًا تصميم شيفرة العملية الأب بحيث لا تحجب أبدًا في انتظار انتهاء عملية ابن. فإذا نفّذ الأب معالج إشارة SIGCHLD يحتوي نداء wait، فلا يستدعي الأب wait إلا عند وجود عملية ابن منتهية ليحصّدها، وبذلك لا يحجب في نداء wait. ويمكن ملاحظة هذا السلوك بتشغيل برنامج في الخلفية في صدفة (a.out &). فتستمر الصدفة في التنفيذ، وتطبع مطالبة، وتنفّذ أمرًا آخر بينما يشغّل ابنها a.out. وإليك مثالًا على كيفية ملاحظة الفرق بين أب يحجب في wait وآخر غير حاجب لا يستدعي wait إلا داخل معالج إشارة SIGCHLD (تأكد من تنفيذ برنامج يعمل مدة كافية لملاحظة الفرق):

$  a.out        # shell process forks child and calls wait

$  a.out &      # shell process forks child but does not call wait
$  ps           # (the shell can run ps and a.out concurrently)

وفيما يلي مثال لمقطع شيفرة يحتوي نداءات النظام fork وexec وexit وwait (مع إزالة معالجة الأخطاء للوضوح). صُمّم هذا المثال لاختبار فهمك لهذه النداءات وآثارها في تنفيذ العمليات. في هذا المثال، تنشئ العملية الأب عملية ابن وتنتظر انتهاءها. ثم يشقّ الابن ابنًا آخر لتشغيل برنامج a.out (فالابن الأول أب للابن الثاني). ثم ينتظر انتهاء ابنه.

pid_t pid1, pid2, ret;
int status;

printf("A\n");

pid1 = fork();
if (pid1 == 0 ) {       /* child 1 */
    printf("B\n");

    pid2 = fork();
    if (pid2 == 0 ){    /* child 2 */
        printf("C\n");
        execvp("a.out", NULL);
    } else {            /* child 1 (parent of child 2) */
        ret = wait(&status);
        printf("D\n");
        exit(0);
    }
} else {                /* original parent */
    printf("E\n");
    ret = wait(&status);
    printf("F\n");
}

ويوضح الشكل 7 الخط الزمني لتنفيذ أحداث إنشاء العمليات وتشغيلها وحجبها وانتهائها من تنفيذ المثال أعلاه. وتمثّل الخطوط المنقّطة أوقاتًا يتداخل فيها تنفيذ عملية مع ابنها أو أحفادها: فالعمليات متزامنة ويمكن جدولتها على المعالج بأي ترتيب. وتمثّل الخطوط الصلبة تبعيات على تنفيذ العمليات. فمثلًا، لا يستطيع الابن 1 استدعاء exit حتى يحصّد عملية ابنه المنتهية، الابن 2. وعندما تستدعي عملية wait، تحجب حتى ينتهي ابنها. وعندما تستدعي عملية exit، لا تعمل مرة أخرى أبدًا. وتُدوَّن مخرجات البرنامج على طول الخط الزمني لتنفيذ كل عملية عند نقاط في تنفيذها يمكن أن تحدث فيها عبارة printf المقابلة.

الخط الزمني للتنفيذ لمثال fork-wait الشكل 7. الخط الزمني لتنفيذ البرنامج المثال، ويوضّح تسلسلاً ممكناً لاستدعاءات fork وexec وwait وexit من العمليات الثلاث. تمثّل الخطوط المتصلة التبعيات في ترتيب التنفيذ بين العمليات، والخطوط المتقطعة نقاط التنفيذ المتزامن. العملية الأب هي العملية الأم للعملية الابنة 1، والعملية الابنة 1 هي العملية الأم للعملية الابنة 2.

وبعد إجراء نداءات fork في هذا البرنامج، تعمل العملية الأب وعملية الابن الأولى بالتزامن، ولذلك يمكن أن يتشابك نداء wait في الأب مع أي تعليمة من تعليمات ابنه. فمثلًا، قد يستدعي الأب wait ويحجب قبل أن يستدعي ابنه fork لإنشاء عملية ابنه. ويسرد الجدول 2 جميع المخرجات الممكنة لتشغيل البرنامج المثال.

الخيار 1 الخيار 2 الخيار 3 الخيار 4
A A A A
B B B E
C C E B
D E C C
E D D D
F F F F

ومخرجات البرنامج في الجدول 2 كلها ممكنة لأن الأب يعمل بالتزامن مع عمليات نسله حتى يستدعي wait. وبذلك يمكن أن يتشابك نداء الأب إلى printf("E\n") في أي نقطة بين بداية عمليات نسله وانتهائها.

ويوفّر تجريد العملية في نظام التشغيل لكل عملية فضاء ذاكرة افتراضية. والذاكرة الافتراضية تجريد يمنح كل عملية فضاء عنوان منطقيًا خاصًا بها تُخزَّن فيه تعليماتها وبياناتها. ويمكن التفكير في فضاء العنوان الافتراضي لكل عملية كمصفوفة من البايتات القابلة للعنونة، من العنوان 0 حتى عنوان أقصى ما. فمثلًا، في الأنظمة باثنين وثلاثين بتًا، العنوان الأقصى هو 232 -1. ولا تستطيع العمليات الوصول إلى محتويات فضاءات عنوان بعضها. وتأتي بعض أجزاء فضاء العنوان الافتراضي للعملية من الملف التنفيذي الثنائي الذي تشغّله (مثلًا، جزء النص يحتوي تعليمات البرنامج من ملف a.out). وتُنشأ أجزاء أخرى من فضاء العنوان الافتراضي للعملية في زمن التشغيل (مثل المكدّس).

وتنفّذ أنظمة التشغيل الذاكرة الافتراضية كجزء من تجريد الرؤية المنفردة للعمليات. أي أن كل عملية تتفاعل مع الذاكرة فقط من حيث فضاء عنوانها الافتراضي الخاص، لا من حيث واقع تشارك عمليات كثيرة في الوقت نفسه ذاكرة الحاسوب الفيزيائية (RAM). ويستخدم نظام التشغيل تنفيذه للذاكرة الافتراضية أيضًا لحماية العمليات من الوصول إلى فضاءات ذاكرة بعضها. وكمثال، فكّر في برنامج C البسيط التالي:

/* a simple program */
#include <stdio.h>

int main(int argc, char* argv[]) {
    int x, y;

    printf("enter a value: ");
    scanf("%d", &y);

    if (y > 10) {
        x = y;
    } else {
        x = 6;
    }
    printf("x is %d\n", x);

    return 0;
}

وإذا نفّذت عمليتان هذا البرنامج في الوقت نفسه، تحصل كل منهما على نسختها الخاصة من ذاكرة المكدّس كجزء من فضاءات عنوانها الافتراضية المنفصلة. ونتيجة لذلك، إذا نفّذت إحدى العمليتين x = 6، فلن يؤثر ذلك في قيمة x في العملية الأخرى — فلكل عملية نسختها الخاصة من x في فضاء عنوانها الافتراضي الخاص، كما يظهر في الشكل 1.

فضاء العناوين الافتراضية الشكل 1. ينتج تنفيذ a.out مرتين عن عمليتين، تُشغّل كل منهما نسخاً معزولة من برنامج a.out. ولكل عملية فضاء عناوين افتراضية خاص بها يحتوي نسخها من تعليمات البرنامج والمتغيّرات العامة ومساحات ذاكرة المكدّس والكومة. فمثلاً قد تحتوي كل منهما على متغيّر محلي x في جزء المكدّس من فضاء عناوينها الافتراضية.

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

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

عن ذاكرة الكومة والمكدّس

لا تتغير السعة الإجمالية الفعلية لمساحة ذاكرة الكومة والمكدّس عادةً عند كل نداء إلى malloc وfree، ولا عند كل نداء دالة وعودة منها. بل كثيرًا ما تُحدث هذه الإجراءات تغييرات في مقدار ما يُستخدم فعليًا من جزأي الكومة والمكدّس المخصصين حاليًا في فضاء الذاكرة الافتراضية. لكن هذه الإجراءات تؤدي أحيانًا إلى تغييرات في السعة الإجمالية لمساحة الكومة أو المكدّس.

ونظام التشغيل مسؤول عن إدارة فضاء العنوان الافتراضي للعملية، بما في ذلك تغيير السعة الإجمالية لمساحتي الكومة والمكدّس. ويمكن استخدام نداءات النظام brk أو sbrk أو mmap لطلب أن يغيّر نظام التشغيل السعة الإجمالية لذاكرة الكومة. ولا يستدعي مبرمجو C هذه النداءات عادةً مباشرةً. بل يستدعي مبرمجو C الدالة malloc من مكتبة C القياسية (وfree) لتخصيص (وتحرير) مساحة ذاكرة الكومة. وداخليًا، قد يستدعي مدير الكومة على مستوى المستخدم في مكتبة C القياسية أحد هذه النداءات ليطلب من نظام التشغيل تغيير حجم مساحة ذاكرة الكومة لتلبية طلب malloc.

13.3.1. عناوين الذاكرة

ولأن العمليات تعمل داخل فضاءات عنوانها الافتراضية الخاصة، يجب أن تميّز أنظمة التشغيل تمييزًا مهمًا بين نوعين من عناوين الذاكرة. فـ العناوين الافتراضية (virtual addresses) تشير إلى مواقع التخزين في فضاء العنوان الافتراضي للعملية، والعناوين الفيزيائية (physical addresses) تشير إلى مواقع التخزين في الذاكرة الفيزيائية (RAM).

الذاكرة الفيزيائية (RAM) وعناوينها

نعرف من فصل التخزين وهرم الذاكرة أن الذاكرة الفيزيائية (RAM) يمكن النظر إليها كمصفوفة من البايتات القابلة للعنونة تتراوح عناوينها من 0 إلى قيمة عنوان أقصى تعتمد على الحجم الإجمالي لـ RAM. فمثلًا، في نظام فيه 2 غيغابايت (GB) من RAM، تتراوح عناوين الذاكرة الفيزيائية من 0 إلى 231 -1 (والغيغابايت الواحد 230 بايت، فـ2 GB إذن 231 بايت).

ولتشغيل المعالج برنامجًا، يجب أن يحمّل نظام التشغيل تعليمات البرنامج وبياناته في RAM؛ فلا يستطيع المعالج الوصول مباشرةً إلى أجهزة تخزين أخرى (مثل الأقراص). ويدير نظام التشغيل RAM ويحدد أي المواقع فيها ينبغي أن تخزّن محتويات فضاء العنوان الافتراضي لعملية. فمثلًا، إذا عملت عمليتان، P1 وP2، بـالبرنامج المثال المذكور أعلاه، فلكل من P1 وP2 نسختان منفصلتان من المتغير x، مخزَّنة كل منهما في موقع مختلف في RAM. أي أن x في P1 وx في P2 لهما عنوانان فيزيائيان مختلفان. ولو منح نظام التشغيل P1 وP2 العنوان الفيزيائي نفسه لمتغيرَي x لديهما، لأدى ضبط P1 لـ x على 6 إلى تعديل قيمة x في P2 أيضًا، ما يخرق خصوصية فضاء العنوان الافتراضي لكل عملية.

وفي أي لحظة، يخزّن نظام التشغيل في RAM محتويات فضاءات عنوان عمليات كثيرة، بالإضافة إلى شيفرة نظام التشغيل التي قد يخطّطها في فضاء العنوان الافتراضي لكل عملية (تُحمَّل شيفرة نظام التشغيل عادةً بدءًا من العنوان 0x0 في RAM). ويعرض الشكل 2 مثالًا على نظام التشغيل وثلاث عمليات (P1 وP2 وP3) محمَّلة في RAM. وتحصل كل عملية على مواقع تخزين فيزيائية منفصلة خاصة بمحتويات فضاء عنوانها (فمثلًا، حتى لو شغّلت P1 وP2 البرنامج نفسه، تحصلان على مواقع تخزين فيزيائية منفصلة لمتغيرهما x).

العناوين الفيزيائية الشكل 2. مثال على محتويات RAM يوضّح نظام تشغيل محمّلاً على العنوان 0x0، وعمليات محمّلة على عناوين ذاكرة فيزيائية مختلفة في RAM. وإذا كانت P1 وP2 تشغّلان نفس a.out، يكون العنوان الفيزيائي لـ x في P1 مختلفاً عن العنوان الفيزيائي لـ x في P2.

الذاكرة الافتراضية والعناوين الافتراضية

الذاكرة الافتراضية هي رؤية كل عملية لفضاء ذاكرتها، والعناوين الافتراضية عناوين في رؤية العملية لذاكرتها. وإذا شغّلت عمليتان الملف التنفيذي الثنائي نفسه، فلهما العناوين الافتراضية نفسها بالضبط لشيفرة الدوال وللمتغيرات العامة في فضاءات عنوانهما (قد تختلف العناوين الافتراضية للمساحة المخصصة ديناميكيًا في ذاكرة الكومة وللمتغيرات المحلية على المكدّس اختلافًا طفيفًا بين العمليتين بسبب فروق زمن التشغيل في تنفيذيهما المنفصلين). وبعبارة أخرى، سيكون للعمليتين العنوانان الافتراضيان نفسهما لموقع دالة main لديهما، والعنوان الافتراضي نفسه لموقع متغير عام x في فضاءات عنوانهما، كما يظهر في الشكل 3.

عناوين افتراضية لعمليتين تشغّلان نفس الملف a.out الشكل 3. مثال على محتويات ذاكرة افتراضية لعمليتين تشغّلان نفس الملف a.out. تملك P1 وP2 العنوان الافتراضي نفسه للمتغيّر العام x.

13.3.2. ترجمة العنوان الافتراضي إلى عنوان فيزيائي

تشير تعليمات لغة التجميع والشيفرة الآلية للبرنامج إلى عناوين افتراضية. ونتيجة لذلك، إذا نفّذت عمليتان برنامج a.out نفسه، ينفّذ المعالج تعليمات بعناوين افتراضية متطابقة للوصول إلى الأجزاء المقابلة في فضاءات عنوانهما الافتراضية المنفصلة. فمثلًا، لنفترض أن x يقع عند العنوان الافتراضي 0x24100، فقد تبدو تعليمات التجميع لضبط x على 6 كما يلي:

movl $0x24100, %eax    # load 0x24100 into register eax
movl $6, (%eax)        # store 6 at memory address 0x24100

وفي زمن التشغيل، يحمّل نظام التشغيل متغير x لكل عملية عند عنوان ذاكرة فيزيائي مختلف (في موقع مختلف في RAM). وهذا يعني أنه كلما نفّذ المعالج تعليمة تحميل أو تخزين إلى الذاكرة تحدد عناوين افتراضية، يجب ترجمة العنوان الافتراضي من المعالج إلى عنوانه الفيزيائي المقابل في RAM قبل قراءة البايتات من RAM أو كتابتها.

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

ووحدة إدارة الذاكرة (memory management unit أو MMU) جزء عتاد الحاسوب الذي ينفّذ ترجمة العنوان. ويترجم عتاد MMU ونظام التشغيل معًا العناوين الافتراضية إلى فيزيائية عندما تصل التطبيقات إلى الذاكرة. وتعتمد قسمة العمل بين العتاد والبرمجيات على المزيج المحدد من العتاد ونظام التشغيل. وفي أكمل صوره، ينفّذ عتاد MMU الترجمة الكاملة: فيأخذ عنوانًا افتراضيًا من المعالج ويترجمه إلى عنوان فيزيائي يُستخدم لعنونة RAM (كما يظهر في الشكل 4). وأيًا كان مدى الدعم العتادي للذاكرة الافتراضية، ستبقى بعض ترجمات العنوان الافتراضي إلى الفيزيائي التي يجب أن يتولاها نظام التشغيل. وفي مناقشتنا للذاكرة الافتراضية، نفترض وجود MMU أكثر اكتمالًا يقلل مقدار مشاركة نظام التشغيل المطلوبة لترجمة العنوان.

تُخطَّط وحدة إدارة الذاكرة MMU العناوين الافتراضية إلى العناوين الفيزيائية الشكل 4. تُخطَّط وحدة إدارة الذاكرة (MMU) العناوين الافتراضية إلى العناوين الفيزيائية. وتُستخدم العناوين الافتراضية في التعليمات التي ينفّذها المعالج. وعندما يحتاج المعالج إلى جلب بيانات من الذاكرة الفيزيائية، تُترجم وحدة إدارة الذاكرة العنوان الافتراضي أولاً إلى عنوان فيزيائي يُستخدم لعنونة RAM.

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

13.3.3. الترقيم

ومع أنه اقترح كثير من أنظمة الذاكرة الافتراضية على مر السنين، فإن الترقيم هو التنفيذ الأكثر استخدامًا للذاكرة الافتراضية اليوم. وفي نظام الذاكرة الافتراضية المرقَّمة (paged virtual memory)، يقسم نظام التشغيل فضاء العنوان الافتراضي لكل عملية إلى أجزاء ثابتة الحجم تُسمى الصفحات (pages). ويعرّف نظام التشغيل حجم الصفحة للنظام. وتُستخدم أحجام صفحات من بضعة كيلوبايتات شائعةً في أنظمة التشغيل العامة اليوم — وحجم الصفحة الافتراضي 4 KB (4,096 بايت) في أنظمة كثيرة.

ويقسم نظام التشغيل الذاكرة الفيزيائية بالمثل إلى أجزاء بحجم الصفحة تُسمى الإطارات (frames). ولأن الصفحات والإطارات معرَّفة بالحجم نفسه، يمكن تخزين أي صفحة من الذاكرة الافتراضية لعملية في أي إطار من RAM الفيزيائية.

في نظام الترقيم:

  • الصفحات والإطارات بالحجم نفسه، لذا يمكن تحميل (تخزين) أي صفحة من الذاكرة الافتراضية في أي إطار فيزيائي من RAM.
  • لا حاجة إلى تخزين صفحات العملية في إطارات RAM متتابعة (عند تسلسل عناوين متجاورة كلها في RAM).
  • لا حاجة إلى تحميل كل صفحة من فضاء العنوان الافتراضي في RAM لكي تعمل العملية.

ويعرض الشكل 5 مثالًا على كيفية تخطيط صفحات من فضاء العنوان الافتراضي لعملية إلى إطارات في RAM الفيزيائية.

تُخطَّط صفحات الذاكرة الافتراضية إلى إطارات RAM الفيزيائية الشكل 5. الذاكرة الافتراضية المُصفَّحة. تُخزَّن الصفحات الفردية لفضاء عناوين العملية في إطارات RAM. ويمكن تحميل أي صفحة من فضاء العناوين الافتراضية في أي إطار من الذاكرة الفيزيائية (تخزَّن فيه). وفي هذا المثال، تُخزَّن الصفحة الافتراضية 1000 لـ P1 في الإطار الفيزيائي 100، وتقيم صفحتها 500 في الإطار 513. وتُخزَّن الصفحة الافتراضية 1000 لـ P2 في الإطار الفيزيائي 880، وتقيم صفحتها 230 في الإطار 102.

العناوين الافتراضية والفيزيائية في الأنظمة المرقَّمة

تقسم أنظمة الذاكرة الافتراضية المرقَّمة بتات العنوان الافتراضي إلى جزأين: البتات العليا تحدد رقم الصفحة (page number) التي يُخزَّن فيها العنوان الافتراضي، والبتات الدنيا تقابل إزاحة البايت (byte offset) داخل الصفحة (أي بايت من أعلى الصفحة يقابل العنوان).

وبالمثل، تقسم أنظمة الترقيم العناوين الفيزيائية إلى جزأين: البتات العليا تحدد رقم الإطار (frame number) في الذاكرة الفيزيائية، والبتات الدنيا تحدد إزاحة البايت داخل الإطار. ولأن الإطارات والصفحات بالحجم نفسه، فإن بتات إزاحة البايت في العنوان الافتراضي مطابقة لبتات إزاحة البايت في عنوانه الفيزيائي المترجَم. وتختلف العناوين الافتراضية عن عناوينها الفيزيائية المترجَمة في بتاتها العليا، التي تحدد رقم الصفحة الافتراضية ورقم الإطار الفيزيائي.

بتّات العناوين الافتراضية والفيزيائية الشكل 6. بتّات العناوين في العناوين الافتراضية والفيزيائية

فمثلًا، فكّر في نظام (صغير جدًا) بعناوين افتراضية بطول 16 بتًا، وعناوين فيزيائية بطول 14 بتًا، وصفحات بحجم 8 بايتات. ولأن حجم الصفحة ثمانية بايتات، تحدد البتات الثلاث الدنيا للعنوانين الفيزيائي والافتراضي إزاحة البايت داخل الصفحة أو الإطار — فثلاث بتات تستطيع ترميز ثماني قيم إزاحة بايت متميزة، من 0 إلى 7 (23 = 8). ويتبقى بذلك 13 بتًا عليا من العنوان الافتراضي لتحديد رقم الصفحة، و11 بتًا عليا من العنوان الفيزيائي لتحديد رقم الإطار، كما يظهر في المثال في الشكل 7.

تفسير بتّات العناوين في المثال الشكل 7. تقسيمات بتّات العناوين الافتراضية والفيزيائية في نظام مثال بعناوين افتراضية من 16 بتّة، وعناوين فيزيائية من 14 بتّة، وحجم صفحة 8 بايتات.

في المثال في الشكل 7، العنوان الافتراضي 43357 (بالنظام العشري) له إزاحة بايت تساوي 5 (0b101 بالنظام الثنائي)، وهي البتات الثلاث الدنيا من العنوان، ورقم صفحة يساوي 5419 (0b1010100101011)، وهي البتات الـ13 العليا من العنوان. وهذا يعني أن العنوان الافتراضي يقع عند البايت 5 من أعلى الصفحة 5419.

وإذا حُمِّلت هذه الصفحة من الذاكرة الافتراضية في الإطار 43 (0b00000101011) من الذاكرة الفيزيائية، فإن عنوانها الفيزيائي هو 349 (0b00000101011101)، حيث تحدد البتات الثلاث الدنيا (0b101) إزاحة البايت، وتحدد البتات الـ11 العليا (0b00000101011) رقم الإطار. وهذا يعني أن العنوان الفيزيائي يقع عند البايت 5 من أعلى الإطار 43 في RAM.

جداول الصفحات لتخطيط الصفحات الافتراضية إلى الفيزيائية

ولأن كل صفحة من فضاء الذاكرة الافتراضية لعملية يمكن أن تخطّط إلى إطار مختلف في RAM، يجب أن يحتفظ نظام التشغيل بتخطيطات لكل صفحة افتراضية في فضاء عنوان العملية. ويحتفظ نظام التشغيل بـجدول صفحات (page table) لكل عملية يستخدمه لتخزين تخطيطات أرقام الصفحات الافتراضية إلى أرقام الإطارات الفيزيائية للعملية. وجدول الصفحات بنية بيانات ينفّذها نظام التشغيل وتُخزَّن في RAM. ويعرض الشكل 8 مثالًا على كيفية تخزين نظام التشغيل لجدولَي صفحات عمليتين في RAM. ويخزّن جدول صفحات كل عملية تخطيطات صفحاتها الافتراضية إلى إطاراتها الفيزيائية في RAM بحيث يمكن تخزين أي صفحات من الذاكرة الافتراضية في أي إطار فيزيائي من RAM.

جدولا صفحات عمليتين مخزّنان في RAM الشكل 8. لكل عملية جدول صفحات يحتوي تخطيطات صفحاتها الافتراضية إلى الإطارات الفيزيائية. وتستخدم النظام جداول الصفحات المخزّنة في RAM لترجمة العناوين الافتراضية للعمليات إلى عناوين فيزيائية تُستخدم لعنونة مواقع RAM. ويوضّح هذا المثال جداول الصفحات المنفصلة المخزّنة في RAM للعمليتين P1 وP2، ولكل جدول صفحات تخطيطاته الخاصة من الصفحات الافتراضية إلى الإطارات الفيزيائية.

ولكل صفحة من الذاكرة الافتراضية، يخزّن جدول الصفحات مدخل جدول صفحات واحدًا (page table entry أو PTE) يحتوي رقم الإطار في الذاكرة الفيزيائية (RAM) الذي يخزّن الصفحة الافتراضية. وقد يحتوي مدخل جدول الصفحات أيضًا معلومات أخرى عن الصفحة الافتراضية، منها بت الصلاحية (valid bit) الذي يُستخدم للدلالة على ما إذا كان المدخل يخزّن تخطيطًا صالحًا. وإذا كان بت الصلاحية لصفحة صفرًا، فالصفحة من فضاء العنوان الافتراضي للعملية غير محمَّلة حاليًا في الذاكرة الفيزيائية.

مدخل جدول الصفحات الشكل 9. يخزّن مدخل جدول الصفحات (PTE) رقم الإطار (23) لإطار RAM الذي حُمِّلت فيه الصفحة الافتراضية. ونعرض رقم الإطار (23) بالعشري، رغم أنه مرمّز فعلياً بالثنائي في مدخل PTE ‏(0…​010111). وتشير بتّة صالحيّة قيمة 1 إلى أن هذا المدخل يخزّن تخطيطاً صالحاً.

استخدام جدول الصفحات لتخطيط العناوين الافتراضية إلى الفيزيائية

ثمة أربع خطوات لترجمة عنوان افتراضي إلى عنوان فيزيائي (معروضة في الشكل 10). ويحدد المزيج المعين من C13-OS/العتاد أيًّا من نظام التشغيل أو العتاد ينفّذ كل خطوة أو جزءًا منها. ونفترض MMU كامل الميزات ينفّذ أكبر قدر ممكن من ترجمة العنوان في العتاد عند وصف هذه الخطوات، لكن نظام التشغيل قد ينفّذ أجزاء من هذه الخطوات في بعض الأنظمة.

  1. أولًا، تقسم MMU بتات العنوان الافتراضي إلى جزأين: بالنسبة إلى حجم صفحة 2k بايت، ترمّز البتات k الدنيا (بتات VA من (k-1) إلى 0) إزاحة البايت (d) داخل الصفحة، وترمّز البتات n-k العليا (بتات VA من (n-1) إلى k) رقم الصفحة الافتراضية (p).
  2. بعد ذلك، تستخدم MMU قيمة رقم الصفحة (p) فهرسًا في جدول الصفحات للوصول إلى مدخل جدول الصفحات الخاص بالصفحة p. وتملك معظم المعماريات سجل قاعدة جدول الصفحات (page table base register أو PTBR) يخزّن عنوان RAM لجدول صفحات العملية العاملة. وتُدمج القيمة في PTBR مع قيمة رقم الصفحة (p) لحساب عنوان مدخل جدول الصفحات الخاص بالصفحة p.
  3. وإذا كان بت الصلاحية في المدخل مضبوطًا (يساوي 1)، فإن رقم الإطار في المدخل يمثل تخطيطًا صالحًا من عنوان افتراضي إلى عنوان فيزيائي. وإذا كان بت الصلاحية 0، يحدث إخفاق صفحة (page fault) يُفعّل نظام التشغيل للتعامل مع ترجمة العنوان هذه (سنناقش معالجة إخفاق الصفحة في نظام التشغيل لاحقًا).
  4. وتبني MMU العنوان الفيزيائي مستخدمةً بتات رقم الإطار (f) من مدخل جدول الصفحات كبتات عليا، وبتات إزاحة الصفحة (d) من العنوان الافتراضي كبتات دنيا للعنوان الفيزيائي.

خطوات ترجمة العناوين باستخدام مدخل جدول الصفحات الشكل 10. يُستخدم جدول صفحات العملية لتنفيذ ترجمات من العناوين الافتراضية إلى الفيزيائية. ويخزّن سجل PTBR العنوان الأساسي لجدول صفحات العملية الجاري تنفيذها.

مثال: تخطيط عنوان افتراضي إلى فيزيائي بجدول صفحات

فكّر في نظام ترقيم (صغير) مثالًا حيث:

  • حجم الصفحة 4 بايتات
  • العناوين الافتراضية 6 بتات (البتات الأربع العليا رقم الصفحة والبتان الدنيا إزاحة البايت)
  • العناوين الفيزيائية 7 بتات

لنفترض أن جدول صفحات العملية P1 في هذا النظام يبدو كما في الجدول 1 (القيم مسرودة بالنظامين العشري والثنائي).

المدخل صالح رقم الإطار
0 (0b0000) 1 23 (0b10111)
1 (0b0001) 0 17 (0b10001)
2 (0b0010) 1 11 (0b01011)
3 (0b0011) 1 16 (0b10000)
4 (0b0100) 0 08 (0b01000)
5 (0b0101) 1 14 (0b01110)
…​ …​ …​
15 (0b1111) 1 30 (0b11110)

ويوحي استخدام المعلومات المقدمة في هذا المثال بأمور مهمة عدة عن أحجام العناوين وأجزاء العناوين وترجمة العناوين، منها:

  • يتحدد حجم جدول الصفحات (عدد مدخلاته) بعدد بتات العنوان الافتراضي وحجم الصفحة في النظام. فالبتات الأربع العليا من كل عنوان افتراضي بطول 6 بتات تحدد رقم الصفحة، وعليه يوجد 16 (24) صفحة إجمالًا من الذاكرة الافتراضية. ولأن جدول الصفحات له مدخل واحد لكل صفحة افتراضية، يوجد إجمالًا 16 مدخل جدول صفحات في جدول صفحات كل عملية.
  • يتحدد حجم كل مدخل جدول صفحات بعدد بتات العنوان الفيزيائي وحجم الصفحة في النظام. ويخزّن كل مدخل بت صلاحية ورقم إطار فيزيائي. ويحتاج بت الصلاحية إلى بت واحد. ويحتاج رقم الإطار إلى 5 بتات لأن العناوين الفيزيائية 7 بتات وإزاحة الصفحة هي البتتان الدنيتان (لعنونة البايتات الأربعة في كل صفحة)، فيتبقى 5 بتات عليا لرقم الإطار. وبذلك يحتاج كل مدخل جدول صفحات إلى 6 بتات: واحدة لبت الصلاحية وخمس لرقم الإطار.
  • يتحدد الحجم الأقصى للذاكرة الافتراضية والفيزيائية بعدد بتات العناوين. ولأن العناوين الافتراضية 6 بتات، يمكن عنونة 26 بايت من الذاكرة، فيكون فضاء العنوان الافتراضي لكل عملية 26 (أي 64) بايت. وبالمثل، الحجم الأقصى للذاكرة الفيزيائية 27 (أي 128) بايت.
  • يحدد حجم الصفحة وعدد بتات العناوين الافتراضية والفيزيائية وجدول الصفحات تخطيط العناوين الافتراضية إلى الفيزيائية. فمثلًا، إذا نفّذت العملية P1 تعليمة لتحميل قيمة من عنوانها الافتراضي 0b001110، يُستخدم جدول صفحاتها لتحويل العنوان الافتراضي إلى العنوان الفيزيائي 0b1000010، الذي يُستخدم بعد ذلك للوصول إلى القيمة في RAM.

وخطوات ترجمة العنوان الافتراضي (VA) إلى العنوان الفيزيائي (PA) هي:

  1. افصل بتات العنوان الافتراضي إلى بتات رقم الصفحة (p) وإزاحة البايت (d): البتات الأربع العليا رقم الصفحة (0b0011 أو الصفحة 3) والبتان الأدنيان إزاحة البايت داخل الصفحة (0b10 أو البايت 2).
  2. استخدم رقم الصفحة (3) فهرسًا في جدول الصفحات لقراءة مدخل جدول الصفحات الخاص بالصفحة الافتراضية 3 (PT[3]: صالح:1 رقم الإطار:16).
  3. افحص بت الصلاحية بحثًا عن تخطيط صالح في المدخل. وفي هذه الحالة، بت الصلاحية 1، فالمدخل يحتوي تخطيطًا صالحًا، أي أن الصفحة الافتراضية 3 مخزَّنة في الإطار الفيزيائي 16.
  4. ابنِ العنوان الفيزيائي مستخدمًا رقم الإطار ذا الخمس بتات من المدخل كبتات العنوان العليا (0b10000)، وإزاحة البتتين الدنيا من العنوان الافتراضي (0b10) كبتين دنيا: العنوان الفيزيائي 0b1000010 (في إطار RAM رقم 16 عند إزاحة البايت 2).

تنفيذ الترقيم

توفّر معظم عتاد الحاسوب بعض الدعم للذاكرة الافتراضية المرقَّمة، وينفّذ نظام التشغيل والعتاد معًا الترقيم في نظام معين. وكحد أدنى، توفّر معظم المعماريات سجل قاعدة جدول الصفحات (PTBR) يخزّن العنوان الأساسي لجدول صفحات العملية العاملة حاليًا. ولتنفيذ ترجمات العنوان الافتراضي إلى الفيزيائي، يُدمج جزء رقم الصفحة الافتراضية من العنوان الافتراضي مع القيمة المخزَّنة في PTBR لإيجاد مدخل جدول الصفحات الخاص بالصفحة الافتراضية. وبعبارة أخرى، رقم الصفحة الافتراضية فهرس في جدول صفحات العملية، وقيمته المدمجة بقيمة PTBR تعطي عنوان RAM لمدخل جدول الصفحات الخاص بالصفحة p (فمثلًا، PTBR + p × (حجم المدخل) هو عنوان RAM لمدخل جدول الصفحات الخاص بالصفحة p). وقد تدعم بعض المعماريات البحث الكامل في جدول الصفحات بمعالجة بتات المدخل في العتاد. وإلا، فيجب مقاطعة نظام التشغيل للتعامل مع بعض أجزاء البحث في جدول الصفحات والوصول إلى بتات المدخل لترجمة عنوان افتراضي إلى عنوان فيزيائي.

وعند تبديل السياق، يحفظ نظام التشغيل قيم PTBR للعمليات ويستعيدها لضمان أن العملية عندما تعمل على المعالج تصل إلى تخطيطات عناوينها الافتراضية إلى الفيزيائية من جدول صفحاتها الخاص في RAM. وهذه إحدى الآليات التي يحمي بها نظام التشغيل فضاءات العنوان الافتراضية للعمليات من بعضها؛ فتغيير قيمة PTBR عند تبديل السياق يضمن ألا تستطيع عملية الوصول إلى تخطيطات VA-PA لعملية أخرى، وبالتالي لا تستطيع قراءة القيم عند عناوين فيزيائية تخزّن محتويات فضاء العنوان الافتراضي لأي عملية أخرى أو الكتابة فيها.

مثال: تخطيطات العناوين الافتراضية إلى الفيزيائية لعمليتين

وكمثال، فكّر في نظام مثال بصفحات بحجم ثمانية بايتات، وعناوين افتراضية بسبع بتات، وعناوين فيزيائية بست بتات.

جدول صفحات P1 جدول صفحات P2
المدخل صالح رقم الإطار المدخل صالح رقم الإطار
0 1 3 0 1 1
1 1 2 1 1 4
2 1 6 2 1 5
…​ …​
11 1 7 11 0 3
…​ …​

وبالنظر إلى الحالة الحالية التالية (المعروضة جزئيًا) لجدولَي صفحات عمليتين (P1 وP2) في الجدول 2، لنحسب العناوين الفيزيائية لتسلسل عناوين الذاكرة الافتراضية التالي المولَّد من المعالج (يُسبق كل عنوان بالعملية العاملة على المعالج):

P1: 0000100
P1: 0000000
P1: 0010000
              <---- context switch
P2: 0010000
P2: 0001010
P2: 1011001
              <---- context switch
P1: 1011001

أولًا، حدّد قسمة البتات في العناوين الافتراضية والفيزيائية. ولأن حجم الصفحة ثمانية بايتات، ترمّز البتات الثلاث الدنيا من كل عنوان إزاحة الصفحة (d). والعناوين الافتراضية سبع بتات. وبذلك، مع ثلاث بتات لإزاحة الصفحة، تتبقى أربع بتات عليا لتحديد رقم الصفحة (p). ولأن العناوين الفيزيائية بطول ست بتات والثلاث الدنيا لإزاحة الصفحة، تحدد البتات الثلاث العليا رقم الإطار.

بعد ذلك، ولكل عنوان افتراضي، استخدم بتات رقم صفحته (p) للبحث في جدول صفحات العملية عن مدخل جدول الصفحات الخاص بالصفحة p. وإذا كان بت الصلاحية في المدخل مضبوطًا، فاستخدم رقم الإطار (f) للبتات العليا من العنوان الفيزيائي. أما البتات الدنيا من العنوان الفيزيائي فتأتي من بتات إزاحة البايت (d) في العنوان الافتراضي.

وتظهر النتائج في الجدول 3 (لاحظ أي جدول صفحات يُستخدم لترجمة كل عنوان).

العملية العنوان الافتراضي p d مدخل جدول الصفحات f d العنوان الفيزيائي
P1 0000100 0000 100 PT[0]: 1(v),3(f) 011 100 011100
P1 0000000 0000 000 PT[0]: 1(v),3(f) 011 000 011000
P1 0010000 0010 000 PT[2]: 1(v),6(f) 110 000 110000
تبديل السياق من P1 إلى P2
P2 0010000 0010 000 PT[2]: 1(v),5(f) 101 000 101000
P2 0001010 0001 010 PT[1]: 1(v),4(f) 100 010 100010
P2 1011001 1011 001 PT[11]: 0(v),3(f) إخفاق صفحة (بت الصلاحية 0)
تبديل السياق من P2 إلى P1
P1 1011001 1011 001 PT[11]: 1(v),7(f) 111 001 111001

وكمثال واحد، فكّر في أول وصول لعناوين تجريه العملية P1. فعندما تصل P1 إلى عنوانها الافتراضي 8 (0b0000100)، يُقسَّم العنوان إلى رقم الصفحة 0 (0b0000) وإزاحة البايت 4 (0b100). ويُستخدم رقم الصفحة 0 للبحث عن مدخل جدول الصفحات رقم 0، الذي بت صلاحيته 1 دلالةً على مدخل تخطيط صفحة صالح، ورقم إطاره 3 (0b011). ويُبنى العنوان الفيزيائي (0b011100) باستخدام رقم الإطار (0b011) كبتات عليا وإزاحة الصفحة (0b100) كبتات دنيا.

وعندما يُبدَّل سياق العملية P2 إلى المعالج، تُستخدم تخطيطات جدول صفحاتها (لاحظ العناوين الفيزيائية المختلفة عندما تصل P1 وP2 إلى العنوان الافتراضي نفسه 0b0010000). وعندما تصل P2 إلى مدخل جدول صفحات ببت صلاحية 0، يثير ذلك إخفاق صفحة إلى نظام التشغيل ليتعامل معه.

13.3.4. كفاءة الذاكرة

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

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

تنفيذ الذاكرة الافتراضية باستخدام RAM والقرص واستبدال الصفحات

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

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

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

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

  1. يجد نظام التشغيل إطارًا حرًا (مثل الإطار j) في RAM ليحمّل فيه الصفحة المُخفقة.
  2. ثم يصدر قراءة إلى القرص لتحميل الصفحة من القرص إلى الإطار j في RAM.
  3. وعند اكتمال القراءة من القرص، يحدّث نظام التشغيل مدخل جدول الصفحات، فيضبط رقم الإطار على j وبت الصلاحية على 1 (فصار لهذا المدخل الخاص بالصفحة المُخفقة تخطيط صالح إلى الإطار j).
  4. وأخيرًا، يعيد نظام التشغيل تشغيل العملية من التعليمة التي سببت إخفاق الصفحة. والآن بعد أن صار جدول الصفحات يحمل تخطيطًا صالحًا للصفحة التي أخفقت، تستطيع العملية الوصول إلى عنوان الذاكرة الافتراضية الذي يخطّط إلى إزاحة في الإطار الفيزيائي j.

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

ركّزت مناقشتنا للذاكرة الافتراضية أساسًا على جزء الآلية في تنفيذ الذاكرة الافتراضية المرقَّمة. غير أن ثمة جزء سياسة مهمًا في الترقيم في تنفيذ نظام التشغيل. ويحتاج نظام التشغيل إلى تشغيل سياسة استبدال صفحات (page replacement policy) عند نفاد RAM الحرة في النظام. وتختار سياسة استبدال الصفحات إطارًا من RAM قيد الاستخدام وتستبدل محتوياته بالصفحة المُخفقة؛ وتُخلى الصفحة الحالية من RAM لتفريغ مساحة لتخزين الصفحة المُخفقة. ويحتاج نظام التشغيل إلى تنفيذ سياسة استبدال صفحات جيدة لاختيار أي إطار في RAM ستُكتب محتوياته عودةً إلى القرص لتفريغ مساحة للصفحة المُخفقة. فمثلًا، قد ينفّذ نظام التشغيل سياسة الأقل استخدامًا مؤخرًا (least recently used أو LRU) التي تستبدل الصفحة المخزَّنة في إطار RAM الأقل وصولًا مؤخرًا. وتعمل LRU جيدًا عند وجود درجة عالية من المحلية في عمليات الوصول إلى الذاكرة. وثمة سياسات أخرى كثيرة قد يختار نظام التشغيل تنفيذها. راجع كتابًا في أنظمة التشغيل لمزيد من المعلومات عن سياسات استبدال الصفحات.

جعل الوصول إلى الصفحات أسرع

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

ومن طرق تقليل الكلفة الإضافية للترقيم تخزين تخطيطات جدول الصفحات من أرقام الصفحات الافتراضية إلى أرقام الإطارات الفيزيائية في ذاكرة مؤقتة. وعند ترجمة عنوان افتراضي، تتحقق MMU أولًا من رقم الصفحة في الذاكرة المؤقتة. وإذا وجدته، فيمكن أخذ تخطيط رقم إطار الصفحة من مدخل الذاكرة المؤقتة، بتجنّب وصول واحد إلى RAM لقراءة مدخل جدول الصفحات.

ومخزن الترجمة الجانبي (translation look-aside buffer أو TLB) ذاكرة مؤقتة عتادية تخزّن تخطيطات (رقم الصفحة، رقم الإطار). وهي ذاكرة مؤقتة صغيرة ترابطية كاملة ومحسَّنة لعمليات بحث سريعة في العتاد. وعندما تجد MMU تخطيطًا في TLB (إصابة TLB)، لا حاجة إلى البحث في جدول الصفحات، ولا يلزم إلا وصول واحد إلى RAM لتنفيذ تحميل أو تخزين إلى عنوان ذاكرة افتراضية. وعندما لا يُوجد التخطيط في TLB (إخفاق TLB)، يلزم وصول إضافي إلى RAM لقراءة مدخل جدول الصفحات الخاص بالصفحة لبناء العنوان الفيزيائي للتحميل أو التخزين في RAM أولًا. ويُضاف التخطيط المرتبط بإخفاق TLB إلى TLB. ومع محلية جيدة لمراجع الذاكرة، يكون معدل الإصابة في TLB مرتفعًا جدًا، ما يؤدي إلى وصولات سريعة إلى الذاكرة في الذاكرة الافتراضية المرقَّمة — فمعظم وصولات الذاكرة الافتراضية لا تتطلب إلا وصولًا واحدًا إلى RAM. ويعرض الشكل 11 كيفية استخدام TLB في تخطيطات العنوان الافتراضي إلى الفيزيائي.

بحث TLB عن تخطيط الصفحة الافتراضية إلى الإطار الفيزيائي الشكل 11. مخزن الترجمة الجانبي (TLB) ذاكرة مؤقتة عتادية صغيرة لتخطيطات الصفحات الافتراضية إلى الإطارات الفيزيائية. ويُبحث أولاً في TLB عن مدخل للصفحة p. وإذا وُجد، فلا حاجة لبحث في جدول الصفحات لترجمة العنوان الافتراضي إلى عنوانه الفيزيائي.

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

وتنفّذ أنظمة التشغيل عادةً دعمًا لعدة أنواع من التواصل بين العمليات، أو طرق يمكن للعمليات أن تتواصل بها أو تتشارك حالة تنفيذها. والإشارات شكل مقيّد جدًا من التواصل بين العمليات يمكن لعملية أن ترسل به إشارة إلى عملية أخرى لإبلاغها بحدث ما. ويمكن للعمليات أن تتواصل أيضًا بـتمرير الرسائل (message passing)، حيث ينفّذ نظام التشغيل تجريدًا لقناة تواصل رسائل تستخدمها عملية لتبادل الرسائل مع عملية أخرى. وأخيرًا، قد يدعم نظام التشغيل التواصل بين العمليات عبر الذاكرة المشتركة (shared memory) التي تتيح لعملية أن تتشارك كل فضاء عنوانها الافتراضي أو جزءًا منه مع عمليات أخرى. وتستطيع العمليات ذات الذاكرة المشتركة القراءة من عناوين في المساحة المشتركة أو الكتابة فيها للتواصل مع بعضها.

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

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

وتستطيع عملية إرسال إشارة إلى عملية أخرى بتنفيذ نداء النظام kill الذي يطلب من نظام التشغيل إرسال إشارة إلى عملية أخرى. ويتولى نظام التشغيل إرسال الإشارة إلى العملية الهدف وضبط حالة تنفيذها لتشغيل شيفرة معالج الإشارة المرتبطة بالإشارة المرسلة بعينها.

ملاحظة

قد يكون اسم نداء النظام kill مضللًا فضلًا عن كونه عنيفًا للأسف. فمع أنه يمكن استخدامه (ويُستخدم كثيرًا) لإرسال إشارة إنهاء، فإنه يُستخدم أيضًا لإرسال أي نوع آخر من الإشارات إلى عملية.

ويستخدم نظام التشغيل نفسه الإشارات أيضًا لإبلاغ العمليات بأحداث معينة. فمثلًا، يرسل نظام التشغيل إشارة SIGCHLD إلى عملية عندما تنتهي إحدى عمليات أبنائها.

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

ويسرد الجدول 1 بعض الإشارات المعرَّفة. راجع صفحة الدليل (man 7 signal) لأمثلة إضافية.

اسم الإشارة الوصف
SIGSEGV خطأ تقسيم (مثل إلغاء الإشارة عن مؤشر فارغ)
SIGINT مقاطعة العملية (مثل Ctrl-C في نافذة الطرفية لإيقاف العملية)
SIGCHLD عملية الابن انتهت (مثل صيرورة الابن زومبي بعد تشغيل exit)
SIGALRM إبلاغ عملية بأن مؤقتًا انتهى (مثل alarm(2) كل ثانيتين)
SIGKILL إنهاء عملية (مثل pkill -9 a.out)
SIGBUS حدث خطأ ناقل (مثل عنوان ذاكرة غير محاذى للوصول إلى قيمة int)
SIGSTOP تعليق عملية ونقلها إلى الحالة المحجوبة (مثل Ctrl-Z)
SIGCONT متابعة عملية محجوبة (نقلها إلى الحالة الجاهزة؛ مثل bg أو fg)

وعندما تتلقى عملية إشارة، يمكن أن يحدث أحد عدة أفعال افتراضية:

  • يمكن أن تنتهي العملية
  • يمكن تجاهل الإشارة
  • يمكن حجب العملية
  • يمكن إلغاء حجب العملية

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

يدعم Linux نداءَي نظام مختلفين يمكن استخدامهما لتغيير السلوك الافتراضي لإشارة أو لتسجيل معالج إشارة لإشارة معينة: sigaction وsignal. ولأن sigaction متوافق مع POSIX وأغنى بالميزات، ينبغي استخدامه في البرمجيات الإنتاجية. لكننا نستخدم signal في مثال شيفرتنا لأنه أسهل فهمًا.

وفيما يلي برنامج مثال يسجّل معالجات إشارات للإشارات SIGALRM وSIGINT وSIGCONT باستخدام نداء النظام signal (أُزيلت معالجة الأخطاء للوضوح):

/*
 * Example of signal handlers for SIGALRM, SIGINT, and SIGCONT
 *
 * A signal handler function prototype must match:
 *   void handler_function_name(int signum);
 *
 * Compile and run this program, then send this process signals by executing:
 *  kill -INT  pid  (or Ctrl-C) will send a SIGINT
 *  kill -CONT pid  (or Ctrl-Z fg) will send a SIGCONT
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

/* signal handler for SIGALRM */
void sigalarm_handler(int sig) {
    printf("BEEP, signal number %d\n.", sig);
    fflush(stdout);
    alarm(5);  /* sends another SIGALRM in 5 seconds */
}

/* signal handler for SIGCONT */
void sigcont_handler(int sig) {
    printf("in sigcont handler function, signal number %d\n.", sig);
    fflush(stdout);
}

/* signal handler for SIGINT */
void sigint_handler(int sig) {
    printf("in sigint handler function, signal number %d...exiting\n.", sig);
    fflush(stdout);
    exit(0);
}

/* main: register signal handlers and repeatedly block until receive signal */
int main(void) {

    /* Register signal handlers. */
    if (signal(SIGCONT, sigcont_handler) == SIG_ERR) {
        printf("Error call to signal, SIGCONT\n");
        exit(1);
    }

    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
        printf("Error call to signal, SIGINT\n");
        exit(1);
    }

    if (signal(SIGALRM, sigalarm_handler) == SIG_ERR) {
        printf("Error call to signal, SIGALRM\n");
        exit(1);
    }

    printf("kill -CONT %d to send SIGCONT\n", getpid());

    alarm(5);  /* sends a SIGALRM in 5 seconds */

    while(1) {
        pause(); /* wait for a signal to happen */
    }
}

عند تشغيله، تتلقى العملية إشارة SIGALRM كل 5 ثوانٍ (بسبب نداء alarm في main وsigalarm_handler). ويمكن إثارة الإشارتين SIGINT وSIGCONT بتشغيل الأمرين kill أو pkill في صدفة أخرى. فمثلًا، إذا كان PID العملية 1234 وملفها التنفيذي a.out، فإن أمر الصدفة التالي يرسل إلى العملية إشارتي SIGINT وSIGCONT، فيُفعّل تشغيل دالتَي معالج إشارتهما:

pkill -INT a.out
kill  -INT 1234

pkill -CONT a.out
kill  -CONT 1234

كتابة معالج SIGCHLD

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

وفيما يلي مقطع شيفرة يعرض تنفيذ دالة معالج إشارة لإشارات SIGCHLD. ويعرض هذا المقطع أيضًا أجزاء من دالة main تسجّل دالة معالج الإشارة لإشارة SIGCHLD (لاحظ أن ذلك ينبغي أن يحدث قبل أي نداءات إلى fork):

/*
 * signal handler for SIGCHLD: reaps zombie children
 *  signum: the number of the signal (will be 20 for SIGCHLD)
 */
void sigchld_handler(int signum) {
    int status;
    pid_t pid;

    /*
     * reap any and all exited child processes
     * (loop because there could be more than one)
     */
    while( (pid = waitpid(-1, &status, WNOHANG)) > 0) {
        /* uncomment debug print stmt to see what is being handled
        printf("signal %d me:%d child: %d\n", signum, getpid(), pid);
         */
    }
}

int main(void) {

    /* register SIGCHLD handler: */
    if ( signal(SIGCHLD, sigchld_handler) == SIG_ERR) {
        printf("ERROR signal failed\n");
	exit(1);
    }

    ...

    /* create a child process */
    pid = fork();
    if(pid == 0) {
        /* child code...maybe call execvp */
        ...
    }
    /* the parent continues executing concurrently with child */
    ...

يمرّر المثال أعلاه -1 قيمةً لـ PID في waitpid، وهذا يعني «احصد أي عملية ابن زومبي». كما يمرّر العلم WNOHANG الذي يعني أن نداء waitpid لا يحجب إذا لم توجد عمليات ابن زومبي لحصادها. لاحظ أيضًا أن waitpid يُستدعى داخل حلقة while تستمر ما دامت تعيد قيمة PID صالحة (أي ما دامت تحصد عملية ابن زومبي). ومن المهم أن تستدعي دالة معالج الإشارة waitpid في حلقة لأنه أثناء عملها قد تتلقى العملية إشارات SIGCHLD إضافية من عملية ابن منتهية أخرى. ولا يتتبع نظام التشغيل عدد إشارات SIGCHLD التي تتلقاها العملية، بل يلاحظ فقط أن العملية تلقت إشارة SIGCHLD ويقاطع تنفيذها ليشغّل شيفرة المعالج. ونتيجة لذلك، من دون الحلقة، قد يفوّت معالج الإشارة حصاد بعض أبناء الزومبي.

يُنفَّذ معالج الإشارة كلما تلقّى الأب إشارة SIGCHLD، سواء كان الأب محجوبًا في نداء wait أو waitpid أم لا. وإذا كان الأب محجوبًا في نداء wait عند تلقّيه إشارة SIGCHLD، يستيقظ ويشغّل شيفرة معالج الإشارة لحصاد واحد أو أكثر من أبنائه الزومبي. ثم يواصل التنفيذ عند النقطة في البرنامج بعد نداء wait (فقد حصد للتو عملية ابن منتهية). أما إذا كان الأب محجوبًا في نداء waitpid لابن معين، فقد يواصل الحجب أو لا يفعله بعد عمل شيفرة معالجه لحصاد ابن منتهٍ. فيواصل الأب التنفيذ بعد نداء waitpid إذا كانت شيفرة معالج الإشارة قد حصدت الابن الذي كان ينتظره. وإلا، يواصل الأب الحجب في نداء waitpid لانتظار انتهاء الابن المحدد. ونداء waitpid بـ PID لعملية ابن غير موجودة (ربما عملية حُصدت سابقًا في حلقة معالج الإشارة) لا يحجب المستدعي.

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

ويتألف نموذج التواصل بين العمليات بتمرير الرسائل من ثلاثة أجزاء:

  1. تخصص العمليات نوعًا ما من قنوات الرسائل من نظام التشغيل. ومن أمثلة أنواع قنوات الرسائل الأنابيب (pipes) للتواصل أحادي الاتجاه، والمقابس (sockets) للتواصل ثنائي الاتجاه. وقد تكون هناك خطوات إعداد اتصال إضافية تحتاج العمليات إلى اتخاذها لتهيئة قناة الرسائل.
  2. تستخدم العمليات قناة الرسائل لإرسال الرسائل إلى بعضها وتلقّيها.
  3. تغلق العمليات طرفها من قناة الرسائل عند انتهائها من استخدامها.

والأنبوب (pipe) قناة تواصل أحادية الاتجاه لعملية تعملان على الآلة نفسها. وأحادي الاتجاه يعني أن أحد طرفي الأنبوب لإرسال الرسائل (أو الكتابة فيه) فقط، والطرف الآخر لتلقّي الرسائل (أو القراءة منه) فقط. وتُستخدم الأنابيب كثيرًا في أوامر الصدفة لإرسال مخرجات عملية إلى مدخلات عملية أخرى.

فمثلًا، فكّر في الأمر التالي المكتوب عند مطالبة صدفة bash وينشئ أنبوبًا بين عمليتين (فإجراء cat يُخرج محتويات الملف foo.c، والأنبوب (|) يعيد توجيه تلك المخرجات إلى مدخلات أمر grep الذي يبحث عن السلسلة "factorial" في مدخلاته):

$ cat foo.c | grep factorial

لتنفيذ هذا الأمر، تستدعي عملية صدفة bash نداء النظام pipe لتطلب من نظام التشغيل إنشاء تواصل بأنبوب. وسيستخدم الأنبوب عمليتا ابن الصدفة (cat وgrep). ويهيّئ برنامج الصدفة stdout لعملية cat للكتابة في طرف الكتابة من الأنبوب، وstdin لعملية grep للقراءة من طرف القراءة منه، بحيث أنه عند إنشاء عمليتي الابن وتشغيلهما، تُرسَل مخرجات cat كمدخلات إلى grep (انظر الشكل 1).

مثال على أنبوب يرسل مخرجات cat إلى مدخلات grep الشكل 1. الأنابيب قنوات اتصال أحادية الاتجاه للعمليات على النظام نفسه. وفي هذا المثال، تُرسل عملية cat معلومات لعملية grep بالكتابة في نهاية الكتابة من الأنبوب. وتستقبل عملية grep هذه المعلومات بالقراءة من نهاية القراءة من الأنبوب.

ومع أن الأنابيب تنقل البيانات من عملية إلى أخرى في اتجاه واحد فقط، تتيح تجريدات أخرى لتمرير الرسائل للعمليات التواصل في الاتجاهين. والمقبس (socket) قناة تواصل ثنائية الاتجاه، أي أن كل طرف من طرفَي المقبس يمكن استخدامه لإرسال الرسائل وتلقّيها معًا. ويمكن استخدام المقابس من عمليات متواصلة تعمل على الحاسوب نفسه أو على حواسيب مختلفة متصلة بشبكة (انظر الشكل 2). وقد تكون الحواسيب متصلة بـشبكة محلية (local area network أو LAN) تربط حواسيب في منطقة صغيرة، مثل شبكة في قسم علوم الحاسوب بجامعة. وقد تكون العمليات المتواصلة على شبكتين محليتين مختلفتين متصلتين بالإنترنت. وما دام يوجد مسار ما عبر اتصالات الشبكة بين الآلتين، تستطيع العمليات استخدام المقابس للتواصل.

تمكّن المقابس عمليتين على آلات مختلفة من التواصل عبر شبكة. الشكل 2. المقابس قنوات اتصال ثنائية الاتجاه يمكن للعمليات المتصِّلة على آلات مختلفة متصلة بشبكة استخدامها.

ولأن كل حاسوب فردي نظام خاص به (عتاد ونظام تشغيل)، ولأن نظام التشغيل في نظام لا يعرف موارد النظام الآخر ولا يديرها، فإن تمرير الرسائل هو السبيل الوحيد الذي يمكن لعمليات على حواسيب مختلفة أن تتواصل به. ولدعم هذا النوع من التواصل، تحتاج أنظمة التشغيل إلى تنفيذ بروتوكول تمرير رسائل مشترك لإرسال الرسائل عبر شبكة وتلقّيها. وTCP/IP مثال على بروتوكول رسائل يمكن استخدامه لإرسال الرسائل عبر الإنترنت. وعندما تريد عملية إرسال رسالة إلى أخرى، تجري نداء النظام send، فتمرّر إلى نظام التشغيل مقبسًا تريد الإرسال عليه، ومخزن الرسالة، وربما معلومات إضافية عن الرسالة أو مستلمها المقصود. ويتولى نظام التشغيل تغليف الرسالة في مخزن الرسالة وإرسالها عبر الشبكة إلى الآلة الأخرى. وعندما يتلقى نظام التشغيل رسالة من الشبكة، يفكّ تغليفها ويسلّمها إلى العملية في نظامه التي طلبت تلقّي الرسالة. وقد تكون هذه العملية في حالة محجوبة منتظرةً وصول الرسالة. وفي هذه الحالة، يجعل تلقّي الرسالة العملية جاهزة للعمل مرة أخرى.

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

ويفيد تمرير الرسائل باستخدام المقابس في التواصل ثنائي الاتجاه بين عمليات تعمل على الآلة نفسها وبين عمليات تعمل على آلات مختلفة. لكن عندما تعمل عمليتان على الآلة نفسها، يمكنهما الاستفادة من موارد النظام المشتركة للتواصل بكفاءة أعلى من استخدام تمرير الرسائل.

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

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

التواصل بين العمليات عبر صفحات ذاكرة مشتركة الشكل 1. يستطيع نظام التشغيل دعم مشاركة صفحات من فضاء العناوين الافتراضية بضبط مداخل جداول صفحات العمليات المشتركة على رقم الإطار الفيزيائي نفسه (مثل الإطار 100). ولاحظ أن العمليات لا حاجة لها إلى استخدام العنوان الافتراضي نفسه للإشارة إلى صفحة الذاكرة الفيزيائية المشتركة.

وإذا كان نظام التشغيل يدعم الذاكرة المشتركة الجزئية، فهو ينفّذ واجهة للمبرمج لإنشاء صفحات (أو مناطق/مقاطع) ذاكرة مشتركة والارتباط بها. وفي أنظمة Unix، ينشئ نداء النظام shmget مقطع ذاكرة مشتركة أو يرتبط به. ويقابل كل مقطع ذاكرة مشتركة مجموعة متتابعة من العناوين الافتراضية تُتشارك تخطيطاتها الفيزيائية مع عمليات أخرى ترتبط بمقطع الذاكرة المشتركة نفسه.

كما تدعم أنظمة التشغيل عادةً تشارك فضاء عنوان افتراضي واحد كامل. والخيط (thread) تجريد نظام التشغيل لتدفق تحكم تنفيذي. وللعملية خيط واحد من تدفق التحكم التنفيذي في فضاء عنوان افتراضي واحد. وللعملية متعددة الخيوط خيوط متزامنة متعددة من تدفق التحكم التنفيذي في فضاء عنوان افتراضي واحد مشترك — فجميع الخيوط تتشارك فضاء العنوان الافتراضي الكامل للعملية الحاوية لها.

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

وفي الأنظمة متعددة المعالجات (SMP أو متعددة الأنوية)، يمكن جدولة خيوط فردية لعملية متعددة الخيوط للعمل في الوقت نفسه، على التوازي، على الأنوية المتعددة. وفي فصل الذاكرة المشتركة، نناقش الخيوط والبرمجة المتوازية متعددة الخيوط بمزيد من التفصيل.

فحصنا في هذا الفصل ما هو نظام التشغيل، وكيف يعمل، والدور الذي يؤديه في تشغيل برامج التطبيقات على الحاسوب. وكنظام للبرمجيات النظامية بين عتاد الحاسوب وبرامج التطبيقات، يدير نظام التشغيل عتاد الحاسوب بكفاءة وينفّذ تجريدات تجعل الحاسوب أسهل استخدامًا. وتنفّذ أنظمة التشغيل تجريدين، هما العمليات والذاكرة الافتراضية، لدعم تعدد البرامج (السماح بأكثر من برنامج واحد يعمل على النظام الحاسوبي في الوقت نفسه). ويتتبع نظام التشغيل جميع العمليات في النظام وحالاتها، وينفّذ تبديل سياق العمليات العاملة على أنوية المعالج. كما يوفّر نظام التشغيل طريقة للعمليات لإنشاء عمليات جديدة، وللانتهاء، وللتواصل مع بعضها. ومن خلال الذاكرة الافتراضية، ينفّذ نظام التشغيل تجريد فضاء ذاكرة افتراضية خاص بكل عملية. ويحمي تجريد الذاكرة الافتراضية العمليات من رؤية آثار العمليات الأخرى التي تتشارك فضاء الذاكرة الفيزيائية للحاسوب. والترقيم أحد تطبيقات الذاكرة الافتراضية الذي يخطّط صفحات فردية من فضاء العنوان الافتراضي لكل عملية إلى إطارات في فضاء RAM الفيزيائية. والذاكرة الافتراضية أيضًا طريقة يستخدمها نظام التشغيل لاستخدام RAM بكفاءة أعلى؛ فبمعاملته RAM كذاكرة مؤقتة للقرص، يتيح تخزين صفحات من فضاء الذاكرة الافتراضية في RAM أو على القرص.

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

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

وأخيرًا، توفّر معظم أنظمة التشغيل درجة ما من قابلية التوسيع يمكن لمستخدم (غالبًا مسؤول نظام) أن يضبط بها نظام التشغيل. فمثلًا، تتيح معظم الأنظمة الشبيهة بـ Unix للمستخدمين (وهم يحتاجون عادةً إلى صلاحيات الجذر، أو المستخدم الفائق) تغيير أحجام مخازن نظام التشغيل المؤقتة وذاكراته المؤقتة وقواطع المبادلة، والاختيار من مجموعة سياسات جدولة مختلفة في أنظمة نظام التشغيل الفرعية وأجهزة العتاد. وبهذه التعديلات، يستطيع المستخدم ضبط النظام لنوع أحمال العمل التطبيقية التي يشغّلها. وكثيرًا ما تدعم هذه الأنواع من أنظمة التشغيل وحدات نواة قابلة للتحميل (loadable kernel modules)، وهي شيفرة تنفيذية يمكن تحميلها في النواة والعمل في وضع النواة. وتُستخدم وحدات النواة القابلة للتحميل غالبًا لإضافة تجريدات أو وظائف إضافية إلى النواة، فضلًا عن تحميل شيفرة مشغّلات الأجهزة في النواة للتعامل مع إدارة جهاز عتاد معين.

لمزيد من الشمول والعمق في تغطية أنظمة التشغيل، نوصي بقراءة كتاب في أنظمة التشغيل مثل "Operating Systems: Three Easy Pieces"1.

13.5.1. المراجع

  1. Operating Systems: Three Easy Pieces، لـ Remzi H. Arpaci-Dusseau وAndrea C. Arpaci-Dusseau، Arpaci-Dusseau Books، أغسطس 2018.

تمارين إضافية

  1. فكّر في مقطع الشيفرة أدناه الذي يحتوي نداءات إلى fork (مع إزالة شيفرة معالجة الأخطاء للوضوح فقط):
    pid_t ret;

    printf("A\n");

    ret = fork();
    if(ret == 0) {
        printf("B\n");

        ret = fork();
        if(ret == 0) {
            printf("C\n");
        }
        printf("D\n");
    } else {
        printf("E\n");

        ret = fork();
        printf("F\n");
    }

    printf("G\n");
  1. ارسم التسلسل الهرمي للعمليات الذي ينشئه تنفيذ هذه الشيفرة.
  2. لكل عملية في التسلسل الهرمي، بيّن تسلسل مخرجاتها (الترتيب الذي تنفّذ به عبارات printf).

الحلول

إذا كان متصفحك يدعم الحلول المخفية، انقر هنا لكشفها.

بالنسبة إلى مقطع الشيفرة:

   int ret;

   printf("A\n");
   ret = fork();    // fork 1
   if(ret == 0) {
     printf("B\n");
     ret = fork();  // fork 2
     if(ret == 0) {
       print("C\n");
     }
     printf("D\n");
   } else {
     printf("E\n");
     ret = fork();  // fork 3
     printf("F\n");
  }
  printf("G\n");

التسلسل الهرمي للعمليات الذي تنشئه هذه الشيفرة (نداء fork المحدد الذي ينشئ كل عملية مُدوَّن على كل حافة في التسلسل الهرمي):

تسلسل عمليات من 4 عمليات: تُنشئ العملية الأم العملية الابنة 1 باستدعاء fork الأول، وتُنشئ العملية الابنة ولدها العملية الابنة 2 باستدعاء fork الثاني، وتُنشئ العملية الأم ولدها الثاني العملية الابنة 3 باستدعاء fork الثالث

لكل عملية في التسلسل الهرمي، بيّن تسلسل مخرجاتها (أي عبارات printf تنفّذها وبأي ترتيب):

parent: A, E, F, G
child1: B, D, G
child2: F, G
child3: C, D, G