> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# مكونات الحاسب Computer Components: كيف تعمل معاً؟
- URL: https://arabroot.io/articles/مكونات-الحاسب-وكيف-تعمل-معا/
- Published: 2026-10-30T22:21:53.000Z
- Updated: 2026-10-30T23:22:03.000Z
- Description: نفتح جهازاً حقيقياً ونتابع المعالج والذاكرة والأقراص واللوحة الأم وهي تعمل معاً، ثم نقيس كم ينتظر برنامجك ليحصل على البيانات من كل جزء.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة

عندما تشتري جهازاً محمولاً أو تستأجر سيرفراً تقرأ سطراً مثل «Core i7، ذاكرة 16 GB، قرص NVMe SSD بسعة 512 GB، منفذ USB-C ومنفذ شبكة 1 Gb»، وتقارن الأرقام وتختار، ثم يأتي يوم يبطئ فيه تطبيقك ويقول لك زميل «المشكلة في ال I/O» أو «القرص هو عنق الزجاجة» أو «السيرفر لا يرى إلا نصف الذاكرة»، فتهز رأسك وأنت لا تعرف بالضبط ماذا يقصد. وكل هذه العبارات تصبح واضحة إذا عرفت ما هي الأجزاء الموجودة داخل الصندوق، وما وظيفة كل جزء، وكيف تتصل ببعضها، وكم ينتظر برنامجك عندما يطلب شيئاً من كل جزء منها.

وهذا المقال هو الخريطة العامة لمرحلة [كيف يعمل الحاسب من الداخل](https://arabroot.io/start/#stage-foundations)، فهو لا يفتح المعالج أو الذاكرة أو الشاشة من الداخل، فلكل منها مقال خاص، وإنما يضع الأجزاء كلها على طاولة واحدة ويريك كيف تعمل معاً. وسوف نتابع فيه جهازاً حقيقياً واحداً من أوله لآخره: جهاز مكتبي بمعالج Intel Core i7-7700K ولوحة أم ASUS Z170-K وأربع وحدات ذاكرة DDR4 وثلاثة أقراص من ثلاثة أنواع، نقرأ عتاده بالأوامر، ثم نقيس كم يكلف الحصول على نفس الكمية من البيانات من كل مكان فيه. وسوف نناقش في هذا المقال ما يلي:

- ما هو الحاسب Computer، وكيف تبدو أجزاؤه في الصور الحقيقية.
- نموذج فون نيومان Von Neumann Architecture الذي ما زالت كل الأجهزة تتبعه منذ 1945، ولماذا كانت فكرة «البرنامج داخل الذاكرة» تغييراً كبيراً.
- المعالج CPU والذاكرة RAM والتخزين Storage: وظيفة كل منها، ولماذا نحتاج الثلاثة معاً بدل جزء واحد.
- اللوحة الأم Motherboard والنواقل Buses وخطوط PCIe، وكيف تحسب سرعة كل وصلة.
- الإدخال والإخراج I/O: المتحكمات Controllers والمقاطعات Interrupts والنقل المباشر للذاكرة DMA.
- ماذا يحدث خطوة بخطوة عندما يقرأ برنامجك ملفاً، وماذا يحدث من لحظة الضغط على زر التشغيل حتى يعمل نظام التشغيل.
- تجربة عملية تقرأ فيها عتاد جهاز حقيقي من داخل Container ومن Windows، وتقيس زمن الوصول إلى الذاكرة وثلاثة أقراص والشبكة، ثم ماذا يحدث إذا حددت موارد ال Container.

## ما هو الحاسب Computer؟

الحاسب في أبسط تعريف هو آلة تأخذ بيانات من الخارج، وتعالجها بحسب برنامج مخزن فيها، وتحفظ ما تحتاج حفظه، وتخرج النتيجة. وهذه الوظائف الأربع، أي الإدخال Input والمعالجة Processing والتخزين Storage والإخراج Output، تجدها في كل جهاز يحسب، من الساعة الذكية في يدك إلى السيرفر في مركز البيانات، والذي يتغير بينها هو الحجم والسرعة وعدد الأجزاء فقط. وقبل أن نتحدث عن أي جزء، الصورة التالية تبين حاسباً مكتبياً حقيقياً فتح غطاؤه الجانبي، وبجانبه قرصان من نوعين مختلفين:

![ثلاث صور حقيقية: على اليسار حاسب مكتبي مفتوح من الجانب يظهر فيه مبرد المعالج المعدني ووحدات ذاكرة حمراء بجانبه واللوحة الأم السوداء وبطاقتا رسومات في الأسفل ومزود الطاقة في القاع، وعلى اليمين في الأعلى قرص NVMe أخضر طويل عليه شرائح سوداء، وتحته قرص صلب مفتوح يظهر فيه القرص المعدني اللامع وذراع القراءة، مع أرقام من 1 إلى 12 تشير إلى كل جزء](https://arabroot.io/content/images/2026/10/computer-components-01-photos.webp)

أجزاء الحاسب في صور حقيقية. صورة الحاسب: Dllu، بترخيص CC BY-SA 4.0، وصورة قرص NVMe: User5515، بترخيص CC0، وصورة القرص الصلب: Evan-Amos، بترخيص CC BY-SA 3.0، من Wikimedia Commons ([الحاسب](https://commons.wikimedia.org/wiki/File:Dllu%5Fskylake%5Fcomputer.jpg?ref=arabroot.io) و[قرص NVMe](https://commons.wikimedia.org/wiki/File:1TB%5F2280%5FNVME%5FSSD.jpg?ref=arabroot.io) و[القرص الصلب](https://commons.wikimedia.org/wiki/File:Laptop-hard-drive-exposed.jpg?ref=arabroot.io))، ووضعنا عليها الأرقام

في الصورة أعلاه لاحظ التالي:

1. **المعالج CPU:** لا تراه في الصورة لأنه تحت المبرد المعدني الكبير، وهذا وحده يخبرك بشيء: المعالج يحول الكهرباء إلى حرارة بكمية تحتاج مبرداً بهذا الحجم، والمعالج في هذا الحاسب من نفس جيل معالج جهاز التجربة.
2. **الذاكرة RAM:** الوحدات الحمراء الطويلة بجانب المعالج، وقربها منه ليس صدفة، فكل سنتيمتر في المسافة يعني إشارات أطول وأبطأ.
3. **اللوحة الأم Motherboard:** اللوحة السوداء التي يركب عليها كل شيء آخر، وهي في الحقيقة شبكة من الأسلاك النحاسية الدقيقة داخل طبقات من الألياف.
4. **بطاقات الرسومات GPU:** مركبة في منافذ PCIe الطويلة، وهي حاسب صغير مستقل له معالجه وذاكرته.
5. **مزود الطاقة Power Supply:** يحول كهرباء المنزل إلى الجهود المنخفضة التي تحتاجها الأجزاء.
6. **منافذ الخلف Rear I/O:** منافذ USB والشبكة والصوت، وكلها مثبتة على اللوحة الأم نفسها.
7. **موصل M.2:** الأطراف الذهبية في طرف قرص NVMe، ومنها تمر أربعة خطوط PCIe، وسوف نعرف معنى هذا الرقم لاحقاً.
8. **المتحكم Controller:** الشريحة الكبيرة القريبة من الموصل، وهي معالج صغير داخل القرص يدير كل شيء فيه.
9. **شرائح NAND:** هنا تحفظ البيانات فعلاً، ولا يوجد فيها أي جزء يتحرك.
10. **القرص المغناطيسي Platter:** في القرص الصلب HDD تحفظ البيانات على قرص معدني يدور بسرعة ثابتة.
11. **ذراع القراءة Head Arm:** يتحرك فوق القرص الدوار ليصل إلى المسار المطلوب، وحركته هي سبب بطء القرص الصلب كما سوف نقيس.
12. **موصل SATA:** واجهة أقدم وأبطأ من NVMe، وما زالت شائعة في الأقراص الصلبة وأقراص SSD الرخيصة.

## نموذج فون نيومان Von Neumann Architecture: البرنامج بيانات في الذاكرة

قد يتساءل البعض: لماذا تتكون كل الأجهزة تقريباً من نفس الأجزاء، معالج وذاكرة وأجهزة إدخال وإخراج؟ والإجابة على ذلك تبدأ من أول حاسب إلكتروني كبير، وهو ENIAC الذي بني بين 1943 و1945، فقد كان يحسب بسرعة كبيرة لعصره، ولكن البرنامج لم يكن شيئاً مخزناً فيه، وإنما كان طريقة توصيل الأسلاك وضبط المفاتيح على لوحاته، وبحسب [متحف تاريخ الحاسب](https://www.computerhistory.org/revolution/birth-of-the-computer/4/78?ref=arabroot.io) فقد كان تجهيز الآلة لكل مسألة جديدة يستغرق أياماً من التوصيل والضبط، أي أن تغيير البرنامج كان عملاً يدوياً على الآلة نفسها:

![صورة أبيض وأسود لقاعة كبيرة على جدرانها لوحات ENIAC المليئة بالأسلاك والمقابس، ورجل يقف في وسط القاعة يقرأ ورقة، وامرأة في المقدمة تضبط مفاتيح على لوحة متحركة](https://arabroot.io/content/images/2026/10/computer-components-photo-eniac.webp)

البرمجة على ENIAC: الأسلاك والمفاتيح هي البرنامج. صورة الجيش الأمريكي U.S. Army، ملكية عامة، من [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Glen%5FBeck%5Fand%5FBetty%5FSnyder%5Fprogram%5Fthe%5FENIAC%5Fin%5Fbuilding%5F328%5Fat%5Fthe%5FBallistic%5FResearch%5FLaboratory.jpg?ref=arabroot.io)

وفي 1945 كتب John von Neumann تقريراً بعنوان [First Draft of a Report on the EDVAC](https://web.archive.org/web/20130314123032/http://qss.stanford.edu/~godfrey/vonNeumann/vnedvac.pdf) وصف فيه آلة يكون البرنامج فيها مخزناً في نفس الذاكرة التي تحفظ البيانات، بحسب [سيرته في جمعية IEEE Computer Society](https://history.computer.org/pioneers/von-neumann.html?ref=arabroot.io) التي تذكر أن هذا التقرير هو الذي نشر فكرة البرنامج المخزن Stored Program في الصناعة. والفكرة تبدو بسيطة جداً اليوم، ولكن أثرها كبير: إذا كان البرنامج مجرد أرقام في الذاكرة فتغييره لا يحتاج أسلاكاً وإنما كتابة أرقام جديدة، ويمكن لبرنامج أن يقرأ برنامجاً آخر ويكتبه، وهذا بالضبط ما يفعله المترجم Compiler ونظام التشغيل عندما يحمل برنامجك من القرص إلى الذاكرة ثم يشغله. والصورة التالية تبين الأجزاء التي وصفها هذا النموذج وكيف تتصل:

![رسم فيه صندوق المعالج على اليسار بداخله وحدة التحكم ووحدة الحساب والمسجلات، وصندوق الذاكرة على اليمين بداخله خلايا مرقمة من 100 إلى 103 فيها تعليمات باللون الأزرق وخلايا 200 و201 فيها أرقام باللون الأخضر، وبينهما الناقل مع سهم يجلب التعليمة وسهم يعيدها، وفي الأسفل صندوق الإدخال والإخراج فيه لوحة المفاتيح والتخزين والشبكة والشاشة](https://arabroot.io/content/images/2026/10/computer-components-02-von-neumann.webp)

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

في الصورة أعلاه لاحظ التالي:

- **المعالج** فيه وحدة التحكم Control Unit التي تجلب التعليمة وتفكها، ووحدة الحساب ALU التي تجمع وتقارن، والمسجلات Registers وهي خانات صغيرة داخله، ومنها عداد البرنامج Program Counter الذي يحمل عنوان التعليمة التالية، وقد شرحنا هذه الدورة بالتفصيل في مقال [كيف يعمل المعالج CPU من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-cpu-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/).
- **الذاكرة** صف طويل من الخلايا لكل منها عنوان، والخلية لا تعرف هل ما فيها تعليمة أم رقم، فالخلية 101 فيها تعليمة جمع، والخلية 201 فيها الرقم 12، والفرق الوحيد هو كيف يستخدمها المعالج.
- **الناقل Bus** هو الطريق بينهما، وفيه ثلاثة أنواع من الإشارات: العنوان Address الذي يقول أي خلية، والبيانات Data التي تحمل المحتوى، والتحكم Control الذي يقول هل هذه قراءة أم كتابة.
- **الأجهزة** مثل لوحة المفاتيح والقرص والشبكة والشاشة تتصل بنفس النظام، والمعالج يتعامل معها بقراءة وكتابة عناوين خاصة بها، كما سوف نرى في قسم الإدخال والإخراج.

ولكن هذا التصميم له ثمن معروف: كل تعليمة وكل رقم يجب أن يمر في نفس الطريق بين المعالج والذاكرة، والمعالج أصبح أسرع من الذاكرة بكثير، فيقضي جزءاً كبيراً من وقته في الانتظار، وهذا ما يسمى عنق زجاجة فون نيومان Von Neumann Bottleneck. والحلول التي تراها في كل معالج حديث هي محاولات للتخفيف منه وليس لإلغائه: ذاكرة مؤقتة Cache صغيرة وسريعة داخل المعالج، ثم فصل ال Cache الأول إلى جزء للتعليمات وجزء للبيانات، وهي فكرة من نموذج آخر يسمى Harvard Architecture يفصل التعليمات عن البيانات تماماً. وسوف ترى هذا الفصل بعينك في مخرج التجربة، حيث يظهر L1i للتعليمات وL1d للبيانات، بينما تبقى الذاكرة الرئيسية واحدة للاثنين كما في نموذج فون نيومان.

## المعالج CPU: من ينفذ التعليمات

المعالج هو الجزء الوحيد في الجهاز الذي ينفذ برنامجك فعلاً، وكل الأجزاء الأخرى إما تحفظ بيانات له أو تنقل بيانات إليه أو منه. ووظيفته في أبسط صورة حلقة لا تتوقف: يجلب تعليمة من الذاكرة، ويفكها، وينفذها، ثم ينتقل للتي بعدها، والمعالج في جهاز التجربة فيه أربع أنوية Cores، كل نواة منها تنفذ هذه الحلقة وحدها، وكل نواة تظهر لنظام التشغيل كخيطين Threads بسبب تقنية Hyper-Threading، فيرى النظام ثمانية معالجات منطقية. ولن نعيد هنا ما شرحناه عن خط التنفيذ Pipeline وتوقع القفزات وال Cache، فقد خصصنا لها مقال [المعالج من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-cpu-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)، وشرحنا الأنوية والخيوط وتبديل السياق في مقال [المعالج والخيوط والتزامن](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%88%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

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

## الذاكرة الرئيسية RAM: مساحة العمل التي تنسى

الذاكرة RAM، اختصاراً لـ Random Access Memory، هي المكان الذي يضع فيه نظام التشغيل البرامج التي تعمل الآن وبياناتها، وكلمة Random تعني أن الوصول إلى أي بايت فيها يأخذ نفس الوقت تقريباً مهما كان مكانه، بعكس الشريط المغناطيسي القديم الذي كان عليك أن تلفه حتى تصل إلى المكان المطلوب. ولكل بايت في الذاكرة عنوان، والمعالج يطلب البيانات بهذه العناوين، وهذا هو العنوان نفسه الذي رأيته إذا تعاملت مع المؤشرات في مقال [الذاكرة للمبرمجين](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/)، مع فرق أن البرامج ترى عناوين افتراضية يترجمها نظام التشغيل والمعالج إلى عناوين حقيقية.

وأهم صفة في RAM أنها متطايرة Volatile، أي أنها تفقد كل ما فيها عندما ينقطع التيار، والسبب أن كل بت فيها شحنة صغيرة في مكثف تتسرب بسرعة، وقد شرحنا ذلك والتحديث Refresh الذي يحافظ عليها في مقال [كيف تعمل الذاكرة RAM من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-ram-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/). وقد يتساءل البعض: إذا كانت الذاكرة تنسى، وكان القرص يحفظ، فلماذا لا يعمل المعالج على القرص مباشرة ونستغني عن RAM؟ والإجابة على ذلك في الأرقام التي سوف نقيسها في التجربة: نسخ 4 KiB من الذاكرة داخل برنامج Python أخذ نحو 1.4 ميكروثانية، وقراءة نفس الكمية من أسرع قرص في الجهاز أخذت نحو 112 ميكروثانية، أي أكثر بثمانين مرة تقريباً، والمعالج يحتاج إلى البيانات بسرعة أكبر بكثير من ذلك. ولذلك يقسم الحاسب العمل بين الاثنين: القرص يحفظ كل شيء بشكل دائم، والذاكرة تحمل نسخة مما يعمل عليه المعالج الآن، ونظام التشغيل يحتفظ في الذاكرة الفارغة بنسخ من الملفات التي قرأتها مؤخراً في ما يسمى Page Cache، فإذا طلبتها مرة أخرى لم يذهب إلى القرص أصلاً.

ومن هنا جاء الخطأ الذي يقع فيه الكثير عند قراءة استهلاك الذاكرة في Linux: يرى أن الذاكرة «الفارغة» قليلة فيظن أن السيرفر يحتاج ذاكرة أكبر، بينما معظم المستخدم منها هو Page Cache يتركه النظام لأن الذاكرة الفارغة لا فائدة منها، ويعيده للبرامج فوراً عندما تحتاجه، ولذلك انظر إلى قيمة `MemAvailable` وليس `MemFree`، وسوف تراها في مخرج التجربة.

## التخزين Storage: ما يبقى بعد إطفاء الجهاز

التخزين هو كل جهاز يحفظ البيانات بعد انقطاع الكهرباء، ونظام التشغيل يراه كجهاز كتل Block Device، أي صف طويل من الكتل الثابتة الحجم، كل كتلة 512 بايت أو 4096 بايت، يقرأها ويكتبها كاملة، ولا يستطيع أن يقرأ بايتاً واحداً منها وحده. وفوق هذه الكتل يبني نظام الملفات File System الأسماء والمجلدات، وقد شرحنا ذلك في مقال [نظام الملفات من الداخل](https://arabroot.io/articles/%D9%86%D8%B8%D8%A7%D9%85-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84-inodes-%D9%88%D8%A7%D9%84%D8%B3%D8%AC%D9%84-%D9%88-fsync/). أما هنا فنهتم بالجهاز نفسه، وفي جهاز التجربة ثلاثة أنواع منه، ولكل نوع سبب مختلف للسرعة أو البطء.

### القرص الصلب HDD: لماذا تكلف القراءة العشوائية عدة ملي ثوان؟

القرص الصلب Hard Disk Drive فيه قرص معدني أو أكثر مغطى بطبقة مغناطيسية، يدور بسرعة ثابتة، وفوقه ذراع في طرفه رأس قراءة وكتابة يطير على مسافة صغيرة جداً من السطح. ولكي يقرأ القرص كتلة معينة يجب أن يحدث أمران: أن يتحرك الذراع إلى المسار الصحيح، وهذا اسمه زمن البحث Seek Time، ثم أن ينتظر حتى يدور القرص ويصل القطاع المطلوب تحت الرأس، وهذا اسمه تأخير الدوران Rotational Latency. والحساب الثاني بسيط: القرص الذي يدور بسرعة 5400 دورة في الدقيقة يكمل الدورة الواحدة في 60,000 ÷ 5400 ≈ 11.1 ms، وفي المتوسط ينتظر نصف دورة، أي نحو 5.6 ms، والقرص الذي يدور بسرعة 7200 دورة ينتظر نحو 4.2 ms، ثم يضاف إلى ذلك زمن حركة الذراع. وهذه أزمنة ميكانيكية، أي أنها لا تتحسن بتحسن الإلكترونيات، ولذلك سوف ترى في التجربة أن القراءة العشوائية من القرص الصلب أخذت نحو 12 ms، أي أبطأ من قرص NVMe بنحو مئة مرة.

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

### قرص SSD: لا توجد أجزاء متحركة، ولكن يوجد متحكم Controller

قرص الحالة الصلبة Solid State Drive يحفظ البيانات في شرائح ذاكرة فلاش من نوع NAND، وكل خلية فيها تحبس شحنة كهربائية تبقى بعد انقطاع التيار، وبما أنه لا يوجد ذراع ولا قرص يدور فلا يوجد زمن بحث ولا انتظار دوران، والقراءة العشوائية قريبة جداً من القراءة المتتالية. ولكن الفلاش له قيود تجعل القرص يحتاج إلى معالج خاص به: لا يمكن الكتابة فوق صفحة مكتوبة إلا بعد مسح الكتلة الكبيرة التي تحتويها، وكل خلية تتحمل عدداً محدوداً من مرات المسح. ولذلك يحمل كل SSD متحكماً Controller يشغل برنامجاً يسمى Flash Translation Layer، يكتب البيانات الجديدة في صفحات فارغة ويحدث جدولاً يربط العنوان الذي يطلبه نظام التشغيل بالمكان الحقيقي في الفلاش، ويوزع الكتابة على الخلايا حتى لا تستهلك خلية قبل غيرها، وهذا اسمه Wear Leveling، وينظف الكتل القديمة في الخلفية.

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

### الواجهة Interface: لماذا NVMe أسرع من SATA؟

القرص يحتاج إلى طريقة يتحدث بها مع بقية الجهاز، وهذه هي الواجهة Interface، وفي جهاز التجربة نوعان منها. الأول SATA، وقد صمم في الأصل للأقراص الصلبة، وسرعته القصوى 6 Gb/s، ولأنه يرسل كل 8 بتات من البيانات في 10 بتات على السلك، وهذا ترميز يسمى 8b/10b، فالسرعة الفعلية للبيانات 6 × 8 ÷ 10 = 4.8 Gb/s، أي نحو 600 MB/s، وهذا سقف لا يتجاوزه أي قرص SATA مهما كانت سرعة الفلاش داخله. ويتحدث نظام التشغيل مع أقراص SATA عادة بطريقة تسمى AHCI صممت لزمن كانت فيه الأقراص بطيئة، فلها طابور أوامر واحد قصير.

والثاني NVMe، وهو ليس نوعاً من الفلاش وإنما طريقة تحدث صممت من البداية لأقراص الفلاش، تضع القرص مباشرة على خطوط PCIe، وتسمح بعدد كبير من طوابير الأوامر، طابور لكل نواة في المعالج تقريباً، فتستطيع كل نواة أن ترسل أوامرها دون أن تنتظر غيرها، بحسب [مواصفات NVMe](https://nvmexpress.org/specifications/?ref=arabroot.io). وقرص NVMe في جهاز التجربة يعمل على أربعة خطوط PCIe من الجيل الثالث كما سوف نقرأ من Windows، أي نحو 3.9 GB/s، وهذا أكثر من ستة أضعاف سقف SATA. ولاحظ أن الفرق في الزمن الذي قسناه لقراءة كتلة واحدة أصغر بكثير من الفرق في السرعة القصوى، فقرص SATA SSD أخذ نحو 155 ميكروثانية وقرص NVMe نحو 112، والسبب أن الكتلة الواحدة لا تملأ عرض الوصلة، وأغلب الزمن يذهب في قراءة الفلاش وطبقات نظام التشغيل، والفرق الكبير يظهر عندما تقرأ ملفات كبيرة أو ترسل أوامر كثيرة في نفس الوقت.

## اللوحة الأم Motherboard: أين يتصل كل شيء؟

اللوحة الأم هي التي تجمع الأجزاء وتوصلها ببعضها، وعليها مقبس المعالج Socket ومنافذ الذاكرة DIMM ومنافذ PCIe ومنافذ M.2 وSATA، وشريحة البرنامج الثابت Firmware، ومنظمات الجهد VRM التي تحول 12 V من مزود الطاقة إلى الجهد المنخفض الذي يحتاجه المعالج. ولكن أهم ما في اللوحة لا تراه بعينك، وهو من يتصل بمن، فالأجزاء لا تتصل كلها بالمعالج مباشرة. والصورة التالية تبين خريطة جهاز التجربة، والأرقام التي عليها من مخرج التجربة نفسها:

![خريطة فيها المعالج Core i7-7700K في الوسط، وعلى يساره الذاكرة بأربع وحدات في قناتين A وB بسرعة 38.4 GB/s، وعلى يمينه منفذ PCIe x16 فارغ بسرعة 15.8 GB/s، وتحته وصلة DMI إلى الشريحة المساعدة Z170، ومنها ستة أسهم إلى قرص NVMe بأربعة خطوط من الجيل الثالث، وقرص SATA SSD، وقرص صلب، ومتحكم USB 3.1 بخطين من الجيل الثاني، ومتحكم الشبكة بخط واحد من الجيل الأول، وشريحة UEFI](https://arabroot.io/content/images/2026/10/computer-components-03-motherboard-map.webp)

خريطة جهاز التجربة: الذاكرة وبطاقة الرسومات على المعالج مباشرة، والبقية خلف الشريحة المساعدة

في الخريطة أعلاه لاحظ التالي:

- **الذاكرة متصلة بالمعالج مباشرة** عبر قناتين Channels، في كل قناة وحدتان، ولكل قناة طريق مستقل بعرض 64 بت، وقد شرحنا سبب تركيب الوحدات في أزواج في مقال [الذاكرة RAM من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-ram-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/).
- **منفذ PCIe x16** متصل بالمعالج مباشرة أيضاً، وهو مخصص لبطاقة الرسومات لأنها تحتاج أكبر عرض نطاق، وهو فارغ في جهاز التجربة لأن الشاشة تعمل من الرسومات المدمجة في المعالج.
- **الشريحة المساعدة Chipset**، وتسمى في أجهزة Intel الحديثة PCH، تحمل بقية الأجهزة: قرص NVMe والأقراص SATA ومتحكم USB الإضافي ومتحكم الشبكة، وتتصل بالمعالج بوصلة واحدة اسمها DMI.
- **كل الأجهزة خلف الشريحة المساعدة تشترك في وصلة DMI نفسها**، فإذا نسخت ملفاً كبيراً من قرص NVMe إلى قرص SATA بينما تنزل ملفاً من الشبكة فكل هذه البيانات تمر في نفس الطريق إلى الذاكرة، وهذا شيء لا تخبرك به صفحة المواصفات.

### النواقل Buses: من الطريق المشترك إلى خطوط PCIe

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

وهذا هو PCI Express، أو PCIe، والخط الواحد Lane فيه زوج من الأسلاك للإرسال وزوج للاستقبال، والجهاز يأخذ 1 أو 2 أو 4 أو 8 أو 16 خطاً، وهذا هو الرقم الذي تراه في كتابة مثل x4 وx16\. وكل جيل من PCIe يضاعف سرعة الخط تقريباً، والجيل يحدد أيضاً الترميز: الجيلان الأول والثاني يستخدمان 8b/10b مثل SATA، والجيل الثالث يستخدم 128b/130b الذي يضيع أقل بكثير. وهذه نفس الفكرة التي تراها في USB وSATA وحتى في الشبكة، فكلها اليوم وصلات تسلسلية سريعة وليست أسلاكاً متوازية.

### عرض النطاق Bandwidth لكل وصلة: حساب بسيط

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

| الوصلة                  | السرعة على السلك                  | الحساب          | النتيجة لكل اتجاه                        |
| ----------------------- | --------------------------------- | --------------- | ---------------------------------------- |
| قناة ذاكرة DDR4-2400    | 2400 مليون نقلة في الثانية، 64 بت | 2400 × 8 bytes  | 19.2 GB/s، والقناتان 38.4 GB/s           |
| خط PCIe من الجيل الثالث | 8 GT/s                            | 8 × 128/130 ÷ 8 | نحو 0.985 GB/s، وأربعة خطوط نحو 3.9 GB/s |
| خط PCIe من الجيل الثاني | 5 GT/s                            | 5 × 8/10 ÷ 8    | 0.5 GB/s، وخطان 1 GB/s                   |
| خط PCIe من الجيل الأول  | 2.5 GT/s                          | 2.5 × 8/10 ÷ 8  | 0.25 GB/s                                |
| SATA III                | 6 Gb/s                            | 6 × 8/10 ÷ 8    | 0.6 GB/s                                 |
| شبكة Gigabit Ethernet   | 1 Gb/s                            | 1 ÷ 8           | 0.125 GB/s                               |

وهذا الحساب يكشف لك أشياء مفيدة من مخرج التجربة نفسه: متحكم الشبكة يعمل على خط واحد من الجيل الأول بسعة 250 MB/s، وهذا يكفي شبكة 1 Gb التي تحتاج 125 MB/s، فلا توجد مشكلة. أما متحكم USB 3.1 فيعمل على خطين من الجيل الثاني، أي 1 GB/s، فإذا كان منفذه من نوع USB 3.1 Gen 2 بسرعة 10 Gb/s، وهي أكثر من 1 GB/s، ثم وصلت قرصاً خارجياً سريعاً بهذا المنفذ فالوصلة بين المتحكم واللوحة هي التي تحدد السرعة وليس القرص ولا الكابل. ولاحظ أن كل هذه الأرقام نظرية، والسرعة الفعلية أقل منها بسبب الأوامر والردود التي تمر مع البيانات.

## الإدخال والإخراج I/O: كيف يتحدث المعالج مع الأجهزة؟

المعالج لا يعرف كيف يقرأ قطاعاً من فلاش NAND، ولا كيف يرسل إطاراً على كابل الشبكة، ولا كيف يقرأ ضغطة مفتاح، وكل جهاز من هذه الأجهزة فيه متحكم Device Controller يعرف هذه التفاصيل، ويعرض للمعالج مجموعة صغيرة من السجلات Registers يكتب فيها الأوامر ويقرأ منها الحالة. وفي نظام التشغيل جزء اسمه التعريف Driver يعرف لغة هذه السجلات لنوع معين من الأجهزة، فيحول طلب برنامجك «اقرأ هذا الملف» إلى الأوامر التي يفهمها المتحكم، وقد ذكرنا دور التعريفات في مقال [نظام التشغيل Operating System](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D9%86%D8%B8%D8%A7%D9%85-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-operating-system/). وفي الأجهزة الحديثة تظهر سجلات المتحكم كعناوين في الذاكرة، فيكتب المعالج فيها كما يكتب في RAM، ولكن الكتابة تذهب إلى الجهاز، وهذا اسمه Memory-Mapped I/O أو MMIO. ويبقى بعد ذلك سؤالان: كيف يعرف المعالج أن الجهاز انتهى؟ ومن ينقل البيانات نفسها؟

### الاستطلاع Polling أم المقاطعات Interrupts؟

الحل البسيط أن يسأل المعالج الجهاز باستمرار: «هل انتهيت؟»، ويقرأ سجل الحالة في حلقة حتى تتغير قيمته، وهذا اسمه الاستطلاع Polling. ولكن هذا الحل غير مناسب لأغلب الأجهزة، والسبب أن القرص الصلب يحتاج عدة ملي ثوان، وهذا وقت ينفذ فيه المعالج ملايين التعليمات، ولوحة المفاتيح قد لا تضغط عليها إلا بعد دقائق، فيضيع المعالج كل هذا الوقت في السؤال.

والحل الأفضل هو المقاطعة Interrupt: يرسل المعالج الأمر ثم ينشغل بشيء آخر، وعندما ينتهي الجهاز يرسل إشارة تقطع المعالج عما يفعله، فيحفظ المعالج مكانه وينفذ دالة صغيرة في النواة اسمها معالج المقاطعة Interrupt Handler، ثم يعود إلى ما كان يفعله. وفي الأجهزة الحديثة لا تكون المقاطعة سلكاً منفصلاً، وإنما رسالة يكتبها الجهاز في عنوان خاص عبر PCIe، وهذا اسمه MSI أو MSI-X، وسوف ترى هذه الكلمة نفسها في مخرج التجربة. وفي Linux يمكنك أن ترى عدد المقاطعات لكل جهاز على كل نواة في الملف `/proc/interrupts`.

ولكن المقاطعات أيضاً لها ثمن، فكارت شبكة يستقبل مئات الآلاف من الحزم في الثانية سوف يقاطع المعالج مئات الآلاف من المرات، وكل مقاطعة تكلف حفظ الحالة والعودة إليها، فيقضي المعالج وقته في المقاطعات ولا ينجز عملاً. ولذلك تجمع الأنظمة الحديثة بين الطريقتين، ففي Linux تعمل الشبكة بطريقة اسمها [NAPI](https://docs.kernel.org/networking/napi.html?ref=arabroot.io): تأتي أول مقاطعة، ثم تطفئ النواة المقاطعات وتستطلع الجهاز وتعالج كل الحزم الموجودة دفعة واحدة، ثم تعيد المقاطعات عندما يفرغ الطابور. وهذا مثال جيد على أن «الحل الصحيح» يعتمد على كمية العمل: المقاطعة أفضل عندما تكون الأحداث قليلة، والاستطلاع أفضل عندما تكون كثيرة جداً.

### النقل المباشر للذاكرة DMA

والسؤال الثاني هو من ينقل البيانات، والحل البسيط هنا أيضاً أن ينقلها المعالج بنفسه: يقرأ كلمة من سجل البيانات في الجهاز، ويكتبها في الذاكرة، ثم التي بعدها، وهذا اسمه Programmed I/O. وهذا مقبول للوحة المفاتيح التي ترسل بايتات قليلة، ولكنه غير مناسب إطلاقاً لقرص يرسل مئات الميجابايتات في الثانية، لأن المعالج يصبح ناقلاً للبيانات بدل أن ينفذ البرامج.

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

## كيف تعمل الأجزاء معاً: ماذا يحدث عندما تقرأ ملفاً؟

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

![رسم فيه لوحة المفاتيح على اليسار ومنها سهم مقاطعة إلى المعالج والنواة في الأعلى، ومن المعالج سهم إلى الذاكرة RAM التي فيها Page Cache وطابور الأوامر والمكان الذي تصل إليه البيانات، وسهم منحن من المعالج إلى قرص NVMe على اليمين الذي فيه المتحكم وشرائح NAND، وسهم برتقالي عريض من القرص إلى الذاكرة مكتوب عليه DMA، وسهم منحن من القرص إلى المعالج للمقاطعة، وسهم من المعالج إلى الشاشة، مع سبع خطوات مرقمة وشرح كل منها في الأسفل](https://arabroot.io/content/images/2026/10/computer-components-04-open-file.webp)

قراءة ملف ليس في الذاكرة: سبع خطوات بين لوحة المفاتيح والمعالج والذاكرة والقرص والشاشة

1. **ضغطت Enter:** متحكم USB يستقبل الضغطة ويرسل مقاطعة، والنواة تقرأ المفتاح وتعطيه للطرفية، فيبدأ البرنامج وينفذه المعالج.
2. **البرنامج يطلب `read()`:** هذا استدعاء نظام System Call ينقل التنفيذ إلى النواة، والنواة تبحث أولاً في Page Cache، فإذا كانت الصفحات هناك نسختها إلى برنامجك وانتهى الأمر في ميكروثوان، وهذا ما يحدث في المرة الثانية التي تقرأ فيها نفس الملف.
3. **الصفحات غير موجودة:** تعريف NVMe يكتب أمر قراءة في طابور الإرسال Submission Queue، وهو مساحة في RAM نفسها، ثم يكتب رقماً في سجل على القرص اسمه Doorbell عبر MMIO ليخبره أن في الطابور أمراً جديداً، وبرنامجك ينام، والمعالج يذهب ليعمل على برنامج آخر.
4. **القرص يعمل وحده:** المتحكم يقرأ الأمر من الطابور بال DMA، ويحول العنوان إلى مكان حقيقي في الفلاش، ويقرأ الصفحات من شرائح NAND، وهذه أبطأ خطوة في كل ما سبق.
5. **البيانات تدخل الذاكرة:** القرص يكتب البيانات بال DMA مباشرة في الصفحات التي جهزتها النواة في RAM، دون أن يمر شيء منها على المعالج.
6. **الانتهاء:** القرص يكتب نتيجة الأمر في طابور الإكمال Completion Queue ويرسل مقاطعة MSI-X، والنواة تضيف الصفحات إلى Page Cache وتنسخها إلى برنامجك وتوقظه.
7. **النتيجة:** المعالج يعالج البيانات، ومنها ما يرسله البرنامج إلى الطرفية، فتصل الحروف إلى الشاشة عبر بطاقة الرسومات كما شرحنا في مقال [كيف تعمل الشاشة من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/).

لاحظ أن كل جزء من أجزاء المقال ظهر هنا: المعالج نفذ البرنامج والنواة، والذاكرة حملت البرنامج والطابور والبيانات وال Page Cache، والقرص حفظ الملف، والناقل PCIe حمل الأوامر والبيانات، والمقاطعات وال DMA جعلت المعالج حراً أثناء الانتظار. وقد تابعنا قبل ذلك برنامجاً واحداً من الكتابة إلى الشاشة في مقال [Hello World من الداخل](https://arabroot.io/articles/%D8%B1%D8%AD%D9%84%D8%A9-hello-world-%D9%85%D9%86-%D9%84%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9/)، وهذه الصورة هي نفس القصة من جهة العتاد.

## من زر التشغيل إلى نظام التشغيل: الإقلاع Boot

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

![ست بطاقات متتالية من اليسار إلى اليمين: زر التشغيل، ثم البرنامج الثابت، ثم الفحص POST، ثم اختيار القرص، ثم محمل الإقلاع، ثم النواة، وتحت كل بطاقة شريط من خمسة مربعات CPU وRAM وFL وPCIe وDSK الملون منها هو الجزء الذي يعمل في تلك الخطوة](https://arabroot.io/content/images/2026/10/computer-components-05-boot.webp)

الإقلاع: كل خطوة تجهز ما تحتاجه الخطوة التي بعدها

1. **زر التشغيل Power On:** مزود الطاقة يثبت الجهود المختلفة، ثم يرسل إشارة اسمها Power Good، فيخرج المعالج من حالة الإعادة Reset.
2. **البرنامج الثابت Firmware:** المعالج ينفذ التعليمات الأولى من شريحة الفلاش مباشرة، لأن الذاكرة RAM لم تجهز بعد.
3. **الفحص POST:** البرنامج الثابت يجهز الذاكرة، فوحدات DDR4 تحتاج ضبطاً لتوقيتاتها قبل أن تعمل، ثم يبحث عن أجهزة PCIe ويعطي كل جهاز عناوين في الذاكرة لسجلاته، وهذه هي عناوين MMIO التي ذكرناها.
4. **اختيار القرص Boot Entry:** بحسب ترتيب الإقلاع Boot Order المحفوظ في اللوحة، يقرأ UEFI قسماً صغيراً على القرص اسمه EFI System Partition، وفيه ملف محمل الإقلاع.
5. **محمل الإقلاع Boot Loader:** مثل GRUB في Linux أو Windows Boot Manager، يحمل النواة إلى الذاكرة ويعطيها التحكم.
6. **النواة Kernel:** تحمل التعريفات، وتركب نظام الملفات الرئيسي، وتشغل أول عملية، وهي في أغلب توزيعات Linux الحديثة systemd، ومنها تبدأ كل البرامج الأخرى.

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

## التجربة العملية Lab: نقرأ عتاد جهاز حقيقي ونقيس زمن كل جزء

الآن نطبق كل ما سبق على جهاز التجربة، وسوف نقرأ العتاد مرتين: مرة من داخل Linux في Container من `debian:trixie-slim`، وهو ما سوف تفعله غالباً على سيرفر، ومرة من Windows نفسه، ثم نقيس كم يكلف الحصول على 4 KiB من كل مكان. وكل الملفات موجودة في مجلد `lab` مع المقال، ولا تحتاج إلا Docker وPython 3.13 دون أي مكتبة إضافية.

### من داخل Linux: ماذا يرى lscpu و lsblk و dmidecode؟

السكربت التالي يثبت أداتين صغيرتين، `dmidecode` لقراءة جداول البرنامج الثابت و`pciutils` لقراءة أجهزة PCIe، ثم يطبع من كل مصدر الأسطر المهمة فقط:

```bash
#!/bin/sh
# Reads the hardware this Linux sees: CPU, memory, disks, firmware tables, PCI devices, interrupts.
# Run: docker run --rm --privileged -v "$PWD":/lab debian:trixie-slim sh /lab/probe.sh
apt-get update -qq >/dev/null && apt-get install -y -qq dmidecode pciutils >/dev/null 2>&1
echo "== lscpu";                lscpu | grep -E '^(Architecture|CPU\(s\)|Model name|Thread|Core|Socket|L1d|L1i|L2|L3|Hypervisor|Virtualization type|NUMA node\(s\))'
echo "== /proc/meminfo";        grep -E '^(MemTotal|MemAvailable|Cached|SwapTotal)' /proc/meminfo
echo "== lsblk";                lsblk -d -o NAME,SIZE,ROTA,TYPE,TRAN,MODEL | grep -v -E '^(loop|ram)'
echo "== dmidecode system";     dmidecode -t system 2>&1 | tail -2
echo "== lspci";                lspci 2>&1 | head -10
echo "== /proc/interrupts (number, total on all CPUs, device)"
awk 'NR > 1 && /virtio/ {t = 0; for (i = 2; i <= 9; i++) t += $i; print $1, t, $NF}' /proc/interrupts | head -8
```

نشغله داخل Container بالخيار `--privileged`، والسبب أن `dmidecode` يحتاج إلى قراءة الذاكرة الخاصة بالبرنامج الثابت، وهذا غير مسموح لل Container العادي:

```bash
docker run --rm --privileged -v "$PWD":/lab debian:trixie-slim sh /lab/probe.sh
```

والمخرج سوف يكون كما يلي:

```text
== lscpu
Architecture:                         x86_64
CPU(s):                               8
Model name:                           Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
Thread(s) per core:                   2
Core(s) per socket:                   4
Socket(s):                            1
Hypervisor vendor:                    Microsoft
Virtualization type:                  full
L1d cache:                            128 KiB (4 instances)
L1i cache:                            128 KiB (4 instances)
L2 cache:                             1 MiB (4 instances)
L3 cache:                             8 MiB (1 instance)
== /proc/meminfo
MemTotal:       32804872 kB
MemAvailable:   30168264 kB
Cached:          7069880 kB
SwapTotal:       8388608 kB
== lsblk
NAME    SIZE ROTA TYPE TRAN MODEL
sda   388.4M    1 disk      Virtual Disk    
sdb       8G    1 disk      Virtual Disk    
sdc       1T    1 disk      Virtual Disk    
sdd   141.6M    1 disk      Virtual Disk    
== dmidecode system
Scanning /dev/mem for entry point.
# No SMBIOS nor DMI entry point found, sorry.
== lspci
3a24:00:00.0 3D controller: Microsoft Corporation Basic Render Driver
5582:00:00.0 SCSI storage controller: Red Hat, Inc. Virtio 1.0 console (rev 01)
bd61:00:00.0 System peripheral: Red Hat, Inc. Virtio 1.0 file system (rev 01)
== /proc/interrupts (number, total on all CPUs, device)
24: 1 virtio0-config
25: 24194 virtio0-virtqueues
26: 1 virtio1-config
27: 1 virtio1-hiprio
28: 22 virtio1-requests.0
```

في المخرج أعلاه لاحظ التالي:

- **المعالج:** `lscpu` يقرأ اسم المعالج من تعليمة خاصة في المعالج نفسه ومن `/proc/cpuinfo`، فيظهر الاسم الحقيقي i7-7700K، وأربع أنوية في مقبس واحد، وخيطان لكل نواة، أي ثمانية معالجات منطقية.
- **ال Cache:** لكل نواة 32 KiB للتعليمات L1i و32 KiB للبيانات L1d، وهذا هو الفصل الذي ذكرناه في قسم فون نيومان، و256 KiB من L2، ثم 8 MiB من L3 تشترك فيها الأنوية الأربع، و`lscpu` يجمع الأرقام لكل الأنوية فيكتب 128 KiB مع «4 instances».
- **السطر `Hypervisor vendor: Microsoft`:** هذا أول دليل على أن Linux هنا لا يعمل على العتاد مباشرة، وإنما داخل جهاز افتراضي، وهو جهاز WSL 2 الذي يشغل فيه Docker Desktop كل ال Containers على Windows.
- **الذاكرة:** `MemTotal` نحو 32 GB، بينما الجهاز فيه 64 GB، و`MemAvailable` نحو 30 GB، أي أن أغلبها متاح للبرامج، والسطر `Cached` هو Page Cache الذي تكلمنا عنه.
- **الأقراص:** أربعة أقراص اسمها `Virtual Disk`، ومنها قرص حجمه 1T بينما أكبر قرص حقيقي في الجهاز 932 GB، لأن القرص الافتراضي ملف ينمو عند الحاجة، وحجمه المعلن ليس مساحة محجوزة. والقرص `sdb` حجمه 8G ويطابق `SwapTotal`، فهو غالباً قرص ال Swap.
- **العمود `ROTA`:** قيمته 1 لكل الأقراص، أي أن Linux يظنها أقراصاً تدور، رغم أن ملفاتها في الحقيقة على أقراص SSD، لأن القرص الافتراضي لا يخبر النظام بنوع القرص الذي تحته.
- **`dmidecode`:** لم يجد جدول SMBIOS أصلاً، وهو الجدول الذي يكتب فيه البرنامج الثابت اسم اللوحة الأم ووحدات الذاكرة بحسب [مواصفة SMBIOS](https://www.dmtf.org/standards/smbios?ref=arabroot.io)، فالجهاز الافتراضي هنا لا يعرض هذا الجدول.
- **`lspci`:** لا توجد اللوحة الأم ولا قرص NVMe ولا متحكم الشبكة، وإنما أجهزة افتراضية من نوع Virtio، وهي أجهزة صممت للأجهزة الافتراضية لتكون سريعة وبسيطة.
- **`/proc/interrupts`:** لكل جهاز Virtio رقم مقاطعة وعدد المقاطعات التي وصلت منه على كل الأنوية، والسطر `virtio0-virtqueues` وصل إلى أكثر من 24 ألف مقاطعة، وهذا الرقم يزيد في كل مرة تشغل فيها السكربت.

### المشكلة التي ظهرت: الأوامر صحيحة ولكنها لا تصف العتاد Hardware

عندما شغلنا السكربت أول مرة كان المتوقع أن نرى اسم اللوحة الأم ووحدات الذاكرة الأربع من `dmidecode`، ونوع الأقراص من `lsblk`، ولكن `dmidecode` لم يطبع شيئاً، و`lsblk` قال إن كل الأقراص تدور. والسبب ليس خطأ في الأوامر، وإنما أن كل أمر يصف ما يراه من مكانه، وال Container يرى الجهاز الافتراضي وليس الجهاز الحقيقي. وهذا نفس ما يحدث لك على أي سيرفر سحابي: `lscpu` يعطيك اسم المعالج الحقيقي لأن المعالج الافتراضي يمرر تعليمة التعرف عليه، ولكن الأقراص والشبكة واللوحة الأم كلها أجهزة يصنعها برنامج الأجهزة الافتراضية Hypervisor. ولذلك لا تقم بالحكم على نوع القرص في سيرفر سحابي من عمود `ROTA`، وإنما من وصف الخدمة عند مزود السحابة، أو من قياس الزمن نفسه كما سوف نفعل بعد قليل.

والحل لكي نرى العتاد الحقيقي هو أن نسأل النظام الذي يعمل على العتاد مباشرة، وهو Windows في هذه الحالة، بالسكربت التالي في PowerShell:

```powershell
# The same machine seen from Windows itself (no virtual machine in between). Windows PowerShell 5.1:
#   powershell -ExecutionPolicy Bypass -File host.ps1
Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors | Format-List
Get-CimInstance Win32_BaseBoard | Select-Object Manufacturer, Product | Format-List
Get-CimInstance Win32_PhysicalMemory | Select-Object DeviceLocator, @{n = 'GiB'; e = { $_.Capacity / 1GB } }, ConfiguredClockSpeed | Format-Table -AutoSize
Get-PhysicalDisk | Select-Object FriendlyName, MediaType, BusType, @{n = 'SizeGB'; e = { [math]::Round($_.Size / 1GB) } } | Format-Table -AutoSize
# every PCI Express device with its link: generation (1, 2, 3...) and number of lanes (x1, x2, x4...)
Get-PnpDevice -PresentOnly | Where-Object InstanceId -like 'PCI\*' | ForEach-Object {
    $p = Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName DEVPKEY_PciDevice_CurrentLinkSpeed, DEVPKEY_PciDevice_CurrentLinkWidth -ErrorAction SilentlyContinue
    if ($p[1].Data) { "{0,-48} Gen{1} x{2}" -f $_.FriendlyName.Substring(0, [Math]::Min(48, $_.FriendlyName.Length)), $p[0].Data, $p[1].Data }
}
```

والمخرج سوف يكون كما يلي:

```text
Name                      : Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
NumberOfCores             : 4
NumberOfLogicalProcessors : 8

Manufacturer : ASUSTeK COMPUTER INC.
Product      : Z170-K

DeviceLocator GiB ConfiguredClockSpeed
------------- --- --------------------
DIMM_A1        16                 2400
DIMM_A2        16                 2400
DIMM_B1        16                 2400
DIMM_B2        16                 2400

FriendlyName         MediaType BusType SizeGB
------------         --------- ------- ------
Corsair Force LE SSD SSD       SATA       447
KINGSTON SNVS1000G   SSD       NVMe       932
WDC WD20EZRZ-00Z5HB0 HDD       SATA      1863

Standard NVM Express Controller                  Gen3 x4
ASMedia USB 3.1 eXtensible Host Controller - 1.1 Gen2 x2
Realtek PCIe GbE Family Controller               Gen1 x1
```

في المخرج أعلاه لاحظ التالي:

- **اللوحة الأم** ASUS Z170-K، وهي من الجيل الذي يحمل الشريحة المساعدة Z170 التي رسمناها في الخريطة.
- **الذاكرة** أربع وحدات في المنافذ `DIMM_A1` و`DIMM_A2` و`DIMM_B1` و`DIMM_B2`، أي قناتين في كل منهما وحدتان، وكلها تعمل بسرعة 2400، ومجموعها 64 GiB، وال 32 GB التي رأيناها في Linux هي نصف هذا الرقم، لأن WSL يعطي جهازه الافتراضي بحسب [توثيقه](https://learn.microsoft.com/en-us/windows/wsl/wsl-config?ref=arabroot.io) نصف ذاكرة Windows ما لم تغير ذلك.
- **الأقراص** ثلاثة حقيقية: Kingston بواجهة NVMe، وCorsair SSD بواجهة SATA، وقرص WD صلب HDD بواجهة SATA، والعمود `MediaType` يعرف هنا الفرق بين SSD وHDD لأنه يقرأ من القرص نفسه.
- **وصلات PCIe:** متحكم NVMe على الجيل الثالث بأربعة خطوط، ومتحكم USB 3.1 من ASMedia على الجيل الثاني بخطين، ومتحكم الشبكة Realtek على الجيل الأول بخط واحد، وهذه هي الأرقام التي حسبنا منها عرض النطاق في الجدول.

![ثلاث طبقات فوق بعضها: في الأسفل Windows والعتاد الحقيقي بأربع بطاقات تبين المعالج والذاكرة والأقراص ووصلة NVMe، وفي الوسط الجهاز الافتراضي WSL 2 ببطاقات lscpu وmeminfo وlsblk وdmidecode، اثنتان منها بإطار أحمر، وفي الأعلى ال Container المحدود بمعالجين و1 GiB ببطاقات تبين أن nproc وmeminfo لا يريان الحد بينما ملفات cgroup تعرضه](https://arabroot.io/content/images/2026/10/computer-components-06-layers.webp)

نفس الجهاز من ثلاث طبقات: كل أمر يقول الحقيقة من مكانه فقط

### كم يكلف الوصول إلى كل جزء؟ قياس زمن الاستجابة Latency

القراءة من المواصفات لا تكفي لنعرف الفرق الحقيقي بين الأجزاء، ولذلك كتبنا سكربتاً واحداً بمكتبة Python القياسية يقيس الزمن الذي ينتظره برنامج ليحصل على 4 KiB من كل مكان: من ذاكرة البرنامج نفسه، ومن Page Cache، ومن كل قرص من الأقراص الثلاثة، ومن برنامج آخر على نفس الجهاز عبر TCP، ومن Container، ومن سيرفرين على الإنترنت. والسكربت كاملاً كما يلي:

```python
# How long does it take to get 4 KiB from each place in the machine?
# Python 3.13 standard library only. Windows host: unbuffered disk reads go through CreateFileW with
# FILE_FLAG_NO_BUFFERING (ctypes), so the OS cache is skipped and the read really reaches the device.
#   python echo_server.py 19441   (in another window, for the same-machine round trip)
#   python latency.py C:\lab-tmp D:\lab-tmp F:\lab-tmp
# Each path is a folder on one drive; a 512 MiB test file is written there and deleted at the end.
import ctypes, ctypes.wintypes as wt, os, random, socket, statistics, sys, time

BLOCK = 4096
FILE_SIZE = 512 * 1024 * 1024
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.CreateFileW.restype = wt.HANDLE
k32.VirtualAlloc.restype = ctypes.c_void_p
k32.VirtualAlloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t, wt.DWORD, wt.DWORD]
k32.SetFilePointerEx.argtypes = [wt.HANDLE, ctypes.c_longlong, ctypes.c_void_p, wt.DWORD]
k32.ReadFile.argtypes = [wt.HANDLE, ctypes.c_void_p, wt.DWORD, ctypes.POINTER(wt.DWORD), ctypes.c_void_p]
k32.CloseHandle.argtypes = [wt.HANDLE]
GENERIC_READ, OPEN_EXISTING, NO_BUFFERING = 0x80000000, 3, 0x20000000

def report(name, samples_ns):
    s = sorted(samples_ns)
    med, p99 = statistics.median(s), s[int(len(s) * 0.99) - 1]
    print(f"{name:<34} median {med / 1000:>10.1f} us   p99 {p99 / 1000:>10.1f} us   n={len(s)}")

def make_file(path):
    with open(path, "wb") as f:
        for _ in range(FILE_SIZE // (8 << 20)):
            f.write(os.urandom(8 << 20))
        f.flush(); os.fsync(f.fileno())

def unbuffered_reads(path, n, sequential=False):
    h = k32.CreateFileW(path, GENERIC_READ, 1, None, OPEN_EXISTING, NO_BUFFERING, None)
    buf = k32.VirtualAlloc(None, BLOCK, 0x3000, 0x04)        # page-aligned buffer, as NO_BUFFERING requires
    got, out = wt.DWORD(0), []
    for i in range(n):
        off = (i * BLOCK) if sequential else random.randrange(FILE_SIZE // BLOCK) * BLOCK
        t = time.perf_counter_ns()
        k32.SetFilePointerEx(h, off, None, 0)
        k32.ReadFile(h, buf, BLOCK, ctypes.byref(got), None)
        out.append(time.perf_counter_ns() - t)
        assert got.value == BLOCK
    k32.CloseHandle(h)
    return out

def cached_reads(path, n):
    out = []
    with open(path, "rb", buffering=0) as f:
        for _ in range(20000):                               # warm: the first 64 MiB into the OS cache
            f.seek(random.randrange(16384) * BLOCK); f.read(BLOCK)
        for _ in range(n):
            off = random.randrange(16384) * BLOCK
            t = time.perf_counter_ns(); f.seek(off); f.read(BLOCK); out.append(time.perf_counter_ns() - t)
    return out

def memory_copies(n):
    data = bytearray(os.urandom(256 * 1024 * 1024)); mv, out = memoryview(data), []
    for _ in range(n):
        off = random.randrange(len(data) // BLOCK) * BLOCK
        t = time.perf_counter_ns(); bytes(mv[off:off + BLOCK]); out.append(time.perf_counter_ns() - t)
    return out

def tcp_round_trips(host, port, n):
    s = socket.create_connection((host, port)); s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    msg, out = os.urandom(BLOCK), []
    for _ in range(n):
        t = time.perf_counter_ns(); s.sendall(msg); got = 0
        while got < BLOCK:
            got += len(s.recv(BLOCK - got))
        out.append(time.perf_counter_ns() - t)
    s.close(); return out

def tcp_connects(host, port, n):
    host, out = socket.gethostbyname(host), []          # resolve once, so DNS time is not counted
    for _ in range(n):
        t = time.perf_counter_ns(); c = socket.create_connection((host, port), timeout=5); out.append(time.perf_counter_ns() - t); c.close()
        time.sleep(0.2)
    return out

if __name__ == "__main__":
    random.seed(7)
    report("RAM: copy 4 KiB inside Python", memory_copies(20000))
    files = []
    for folder in sys.argv[1:]:
        os.makedirs(folder, exist_ok=True); p = os.path.join(folder, "lat.bin"); make_file(p); files.append(p)
    try:
        report("OS cache (RAM): read 4 KiB", cached_reads(files[0], 20000))
        for p in files:
            report(f"disk {p[:2]} unbuffered 4 KiB", unbuffered_reads(p, 2000 if not p.upper().startswith("F") else 300))
        report(f"disk {files[-1][:2]} sequential 4 KiB", unbuffered_reads(files[-1], 2000, sequential=True))
    finally:
        for p in files:
            os.remove(p)
    report("TCP 4 KiB round trip, same machine", tcp_round_trips("127.0.0.1", 19441, 2000))
    report("TCP 4 KiB round trip, container", tcp_round_trips("127.0.0.1", 19440, 2000))
    report("TCP connect to www.google.com", tcp_connects("www.google.com", 443, 20))
    report("TCP connect to github.com", tcp_connects("github.com", 443, 20))
```

في السكربت أعلاه لاحظ التالي:

- **القراءة من القرص تتجاوز Page Cache:** لو قرأنا الملف بالطريقة العادية لأعطانا Windows نسخة من الذاكرة بعد أول قراءة، ولقسنا الذاكرة بدل القرص، ولذلك نفتح الملف بالخيار `FILE_FLAG_NO_BUFFERING` عبر `ctypes`، وهذا الخيار يشترط بحسب [توثيق Microsoft](https://learn.microsoft.com/en-us/windows/win32/fileio/file-buffering?ref=arabroot.io) أن يكون حجم القراءة ومكانها وعنوان الذاكرة التي تصل إليها البيانات من مضاعفات حجم القطاع، ولذلك نحجز المكان بالدالة `VirtualAlloc` التي تعطي عنواناً على حدود صفحة كاملة، ونقرأ دائماً 4096 بايت من مكان يقبل القسمة على 4096.
- **ملف بحجم 512 MiB من بيانات عشوائية:** نكتبه على كل قرص ونقرأ منه أماكن عشوائية، والحجم كبير حتى لا تكون القراءات من ذاكرة مؤقتة صغيرة داخل القرص نفسه.
- **Page Cache:** نقرأ أول 64 MiB من الملف بالطريقة العادية عشرين ألف مرة حتى تصبح في الذاكرة، ثم نقيس.
- **الشبكة:** نرسل 4 KiB إلى خادم صدى Echo Server يعيدها كما هي، مرة على نفس الجهاز ومرة داخل Container، ونقيس الذهاب والعودة، ثم نقيس زمن فتح اتصال TCP بسيرفرين على الإنترنت، وهو زمن ذهاب وعودة واحد تقريباً.
- **الوسيط Median وp99:** لا نعرض المتوسط، لأن قراءة واحدة بطيئة جداً تغيره كثيراً، وإنما الوسيط، أي القيمة التي نصف القراءات أسرع منها، وp99 أي القيمة التي 99% من القراءات أسرع منها، وهي التي تخبرك بأسوأ ما يراه المستخدم عادة.

وخادم الصدى ملف صغير نشغله مرتين، مرة في Container على المنفذ 19440 ومرة على الجهاز نفسه على المنفذ 19441:

```python
# A TCP echo server: sends back every byte it receives. Runs inside a container:
#   docker run -d --name scp3-echo -p 19440:19440 -v "$PWD":/lab python:3.13.17-slim python /lab/echo_server.py
# or on the host itself: python echo_server.py 19441
import socket, sys, threading

def serve(c):
    c.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    while data := c.recv(65536):
        c.sendall(data)
    c.close()

s = socket.create_server(("0.0.0.0", int(sys.argv[1]) if len(sys.argv) > 1 else 19440))
while True:
    conn, _ = s.accept()
    threading.Thread(target=serve, args=(conn,), daemon=True).start()
```

ثم نشغل القياس على ثلاثة مجلدات، واحد على كل قرص:

```powershell
docker run -d --name lab-echo -p 19440:19440 -v "${PWD}:/lab" python:3.13.17-slim python /lab/echo_server.py
Start-Process python -ArgumentList 'echo_server.py','19441' -WindowStyle Hidden
python latency.py C:\lab-tmp D:\lab-tmp F:\lab-tmp
```

والمخرج سوف يكون كما يلي:

```text
RAM: copy 4 KiB inside Python      median        1.4 us   p99        9.9 us   n=20000
OS cache (RAM): read 4 KiB         median       11.0 us   p99      136.4 us   n=20000
disk C: unbuffered 4 KiB           median      112.4 us   p99     3323.7 us   n=2000
disk D: unbuffered 4 KiB           median      155.2 us   p99     2145.6 us   n=2000
disk F: unbuffered 4 KiB           median    11935.6 us   p99    25929.7 us   n=300
disk F: sequential 4 KiB           median       95.0 us   p99     4823.9 us   n=2000
TCP 4 KiB round trip, same machine median       83.1 us   p99      290.8 us   n=2000
TCP 4 KiB round trip, container    median     1741.5 us   p99    49639.7 us   n=2000
TCP connect to www.google.com      median   281498.2 us   p99   757053.7 us   n=20
TCP connect to github.com          median   227157.4 us   p99   383593.3 us   n=20
```

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

![رسم أعمدة أفقية على محور لوغاريتمي من 1 ميكروثانية إلى 1 ثانية: النسخ من الذاكرة 1.4 ميكروثانية، ثم Page Cache بنحو 11، ثم TCP على نفس الجهاز 83، ثم القرص الصلب بالترتيب 95، ثم NVMe بنحو 112، ثم SATA SSD بنحو 155 ميكروثانية، ثم TCP إلى Container بنحو 1.7 ملي ثانية، ثم القرص الصلب عشوائياً بنحو 11.9 ملي ثانية، ثم الاتصال بـ github.com بنحو 227 ملي ثانية](https://arabroot.io/content/images/2026/10/computer-components-07-latency.webp)

زمن الحصول على 4 KiB من كل مكان في جهاز التجربة: العمود هو الوسيط والعلامة الرفيعة هي p99

في المخرج أعلاه لاحظ التالي:

- **الذاكرة 1.4 ميكروثانية:** هذا الرقم أغلبه زمن Python نفسه في إنشاء كائن `bytes` جديد، فنسخ 4 KiB في الذاكرة يأخذ أقل من ذلك بكثير، وقد قسنا الذاكرة بالنانوثانية بلغة C# في مقال [الذاكرة RAM من الداخل](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-ram-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/). والمهم هنا أنه أصغر رقم في الجدول.
- **Page Cache بنحو 11 ميكروثانية:** البيانات في الذاكرة أيضاً، ولكن الطلب يمر على النواة عبر استدعاء نظام، فيكلف عدة أضعاف النسخ داخل البرنامج، ومع ذلك هو أسرع من أي قرص بعشر مرات تقريباً، وهذا هو سبب وجود Page Cache.
- **قرص NVMe بنحو 112 ميكروثانية وSATA SSD بنحو 155:** كلاهما بدون أجزاء متحركة، والفرق بينهما أقل من الفرق في سرعة الواجهة كما شرحنا، ولكن p99 لكل منهما وصل إلى عدة ملي ثوان، أي أن قراءة واحدة من كل مئة انتظرت أكثر من الوسيط بما بين أربع عشرة وثلاثين مرة، وهذا ما يجعل بعض الطلبات بطيئة في تطبيق سريع في المتوسط.
- **القرص الصلب عشوائياً بنحو 11.9 ملي ثانية:** هذا زمن البحث وانتظار الدوران الذي حسبناه، وهو أبطأ من NVMe بأكثر من مئة مرة، بينما قراءته بالترتيب أخذت 95 ميكروثانية فقط، أسرع من NVMe العشوائي نفسه، لأن الرأس لا يتحرك.
- **TCP على نفس الجهاز بنحو 83 ميكروثانية:** البيانات لم تخرج من الجهاز أصلاً، ولكنها مرت على طبقات الشبكة في النواة مرتين، فكلفت مثل قراءة من قرص سريع تقريباً.
- **TCP إلى Container بنحو 1.7 ملي ثانية:** أبطأ من نفس الجهاز بعشرين مرة، لأن Docker Desktop على Windows يمرر المنفذ من Windows إلى الجهاز الافتراضي ثم إلى ال Container، فالطلب يعبر حدوداً بين نظامين، وهذا شيء تلاحظه عند تطوير تطبيق على Docker Desktop ثم نشره على سيرفر Linux حيث لا توجد هذه الطبقة.
- **الإنترنت بمئات الملي ثوان:** فتح اتصال واحد إلى github.com أخذ نحو 227 ms، وإلى www.google.com نحو 281 ms، أي زمن ذهاب وعودة عبر الشبكة إلى سيرفر بعيد، وهو أبطأ من قراءة NVMe بألفي مرة تقريباً.

وأرقام الشبكة بالذات تتغير كثيراً بين تشغيل وآخر، ففي تشغيل سابق على نفس الجهاز كانت الأرقام كما يلي:

```text
TCP 4 KiB round trip, same machine median       87.1 us   p99      441.0 us   n=2000
TCP 4 KiB round trip, container    median     8337.7 us   p99   125450.8 us   n=2000
TCP connect to www.google.com      median   143062.4 us   p99   328447.0 us   n=20
TCP connect to github.com          median    69218.5 us   p99   118907.9 us   n=20
```

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

وقد ظهرت في أول تشغيل مشكلة أخرى: كان السكربت يقيس الاتصال بالعنوان `1.1.1.1` على المنفذ 443، فتوقف بخطأ `TimeoutError: timed out` بعد أن قاس كل شيء آخر. والخطأ هنا ليس في الكود، وإنما أن الشبكة التي جربنا منها لا توصل إلى هذا العنوان، وتأكدنا من ذلك بالأمر `Test-NetConnection 1.1.1.1 -Port 443` الذي أعاد `False` بينما نجح مع github.com، فإذا ظهر لك نفس الخطأ فغير العنوان إلى سيرفر يصل من شبكتك، ولاحظ أن السكربت يحول الاسم إلى عنوان IP مرة واحدة قبل القياس حتى لا يدخل زمن DNS في الأرقام.

### ماذا يحدث إذا وضعنا حدود الموارد Resource Limits على ال Container؟

الآن نجرب شيئاً يحدث كل يوم في السيرفرات: نشغل Container ونحدد له معالجين و1 GiB من الذاكرة، وهي حدود يطبقها Docker عبر ميزة في النواة اسمها cgroups بحسب [توثيق Docker](https://docs.docker.com/engine/containers/resource%5Fconstraints/?ref=arabroot.io)، ثم نسأله عن عتاده بنفس الأوامر:

```bash
#!/bin/sh
# What does a limited container see? Run:
#   docker run --rm --cpus 2 --memory 1g -v "$PWD":/lab debian:trixie-slim sh /lab/limits.sh
echo "== lscpu CPU(s)";   lscpu | grep -E '^CPU\(s\)'
echo "== nproc";          nproc
echo "== MemTotal";       grep MemTotal /proc/meminfo
echo "== the real limits (cgroup v2 files, or v1 on older kernels)"
if [ -f /sys/fs/cgroup/cpu.max ]; then
  echo "cpu.max: $(cat /sys/fs/cgroup/cpu.max)"; echo "memory.max: $(cat /sys/fs/cgroup/memory.max)"
else
  echo "cpu.cfs_quota_us: $(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)  cpu.cfs_period_us: $(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)"
  echo "memory.limit_in_bytes: $(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)"
fi
```

```bash
docker run --rm --cpus 2 --memory 1g -v "$PWD":/lab debian:trixie-slim sh /lab/limits.sh
```

والمخرج سوف يكون كما يلي:

```text
== lscpu CPU(s)
CPU(s):                               8
== nproc
8
== MemTotal
MemTotal:       32804872 kB
== the real limits (cgroup v2 files, or v1 on older kernels)
cpu.cfs_quota_us: 200000  cpu.cfs_period_us: 100000
memory.limit_in_bytes: 1073741824
```

في المخرج أعلاه لاحظ التالي:

- **`lscpu` و`nproc` يقولان 8**، و`/proc/meminfo` يقول 32 GB، أي أن ال Container يرى الجهاز الافتراضي كاملاً ولا يرى الحد الذي وضعناه.
- **الحد الحقيقي في ملفات cgroup:** `cpu.cfs_quota_us` قيمته 200000 من فترة `cpu.cfs_period_us` قيمتها 100000، أي أن ال Container يحق له 200 ملي ثانية من وقت المعالج في كل 100 ملي ثانية، وهذا يساوي معالجين، و`memory.limit_in_bytes` قيمته 1073741824 بايت، أي 1 GiB بالضبط.
- **هذه الملفات من cgroups v1**، لأن نواة WSL في هذا الجهاز تعمل بهذا الإصدار، وعلى أغلب توزيعات Linux الحديثة تجد بدلها الملفين `cpu.max` و`memory.max` من cgroups v2، ولذلك يفحص السكربت الاثنين.

وهذا ليس فرقاً نظرياً، فأي برنامج يقرر حجم ذاكرته أو عدد خيوطه من عدد الأنوية والذاكرة التي يراها سوف يخطئ داخل Container. وأشهر مثال موثق على ذلك هو Java: فقد كانت آلة Java الافتراضية قبل JDK 10 تحسب حجم ال Heap الافتراضي وعدد خيوط جامع المهملات من ذاكرة الجهاز وأنويته كاملة وليس من حدود ال Container، فيكبر ال Heap حتى يتجاوز حد الذاكرة وتقتل النواة البرنامج دون رسالة واضحة، إلى أن أضافت OpenJDK التعرف على حدود ال Container في التغيير [JDK-8146115](https://bugs.openjdk.org/browse/JDK-8146115?ref=arabroot.io). والدرس أعم من Java: إذا كتبت برنامجاً يقرر عدد الخيوط من عدد الأنوية، فاقرأ حدود cgroup، أو اجعل الرقم إعداداً يضبطه من يشغل البرنامج.

وفي نهاية التجربة نحذف ال Container الذي يعمل في الخلفية، والسكربت نفسه حذف ملفات القياس من الأقراص الثلاثة:

```bash
docker rm -f lab-echo
```

## أخطاء شائعة Common Mistakes في فهم مكونات الحاسب

1. **«ذاكرة أكبر تعني جهازاً أسرع دائماً»:** الذاكرة الإضافية تسرع الجهاز فقط إذا كانت البرامج تحتاجها أو كانت تمتلئ فيلجأ النظام إلى ال Swap، وإلا بقيت Page Cache لملفات لا تقرؤها.
2. **قراءة `MemFree` على أنها الذاكرة المتاحة:** النظام يملأ الذاكرة الفارغة ب Page Cache عن قصد، والرقم الصحيح هو `MemAvailable`.
3. **الحكم على قرص من سرعته القصوى فقط:** رأينا أن قرص NVMe أسرع من SATA بأكثر من ستة أضعاف في السرعة القصوى، ولكن الفرق في زمن قراءة كتلة واحدة كان نحو 40% فقط، فاسأل أولاً: هل تطبيقي يقرأ ملفات كبيرة أم كتلاً صغيرة متفرقة؟
4. **وضع قاعدة بيانات على قرص صلب HDD:** قاعدة البيانات تقرأ صفحات متفرقة، والقراءة العشوائية من HDD كلفت نحو 12 ms مقابل أقل من 0.2 ms من SSD.
5. **الثقة بما يقوله الجهاز الافتراضي عن عتاده:** `ROTA = 1` ظهر لأقراص على SSD، و`dmidecode` لم يجد شيئاً، فاعرف في أي طبقة تقف قبل أن تقرأ الأرقام.
6. **الاعتماد على `nproc` و`/proc/meminfo` داخل Container:** كلاهما يعرض الجهاز كاملاً، والحد الحقيقي في ملفات cgroup.
7. **تجاهل الوصلة المشتركة خلف الشريحة المساعدة:** عدة أقراص سريعة ومتحكم شبكة كلها خلف وصلة DMI واحدة قد تصل إلى سقفها معاً.
8. **الحكم على الشبكة من قياس واحد:** نفس الاتصال أخذ 69 ms مرة و227 ms مرة أخرى.

## قائمة تحقق Checklist: كيف تقرأ مواصفات جهاز أو سيرفر؟

عندما تقرأ مواصفات جهاز أو سيرفر، أو عندما يبطئ تطبيق ولا تعرف أين المشكلة، استخدم الجدول التالي، ففيه لكل جزء السؤال الذي يهمك والأمر الذي يجيب عنه:

| الجزء                | السؤال الذي يهمك                              | في Linux                                       | في Windows                           |
| -------------------- | --------------------------------------------- | ---------------------------------------------- | ------------------------------------ |
| المعالج CPU          | كم نواة وخيطاً؟ هل هو جهاز افتراضي؟           | lscpu                                          | Win32\_Processor                     |
| الذاكرة RAM          | كم المتاح فعلاً؟ كم قناة تعمل؟                | /proc/meminfo، dmidecode -t memory             | Win32\_PhysicalMemory                |
| التخزين Storage      | SSD أم HDD؟ NVMe أم SATA؟ كم الزمن الفعلي؟    | lsblk -d -o NAME,ROTA,TRAN,MODEL ثم قياس الزمن | Get-PhysicalDisk                     |
| الوصلات Buses        | كم خطاً ومن أي جيل؟ هل الوصلة أبطأ من الجهاز؟ | lspci -vv (السطر LnkSta)                       | DEVPKEY\_PciDevice\_CurrentLinkSpeed |
| الإدخال والإخراج I/O | أي جهاز يقاطع المعالج أكثر؟                   | /proc/interrupts                               | مراقب الأداء Performance Monitor     |
| ال Container         | ما الحد الحقيقي للمعالج والذاكرة؟             | cpu.max وmemory.max أو ملفات cgroup v1         | لا ينطبق                             |

## تدريب عملي

- شغل `probe.sh` على سيرفر Linux حقيقي أو سحابي، وقارن المخرج بما رأيته هنا: هل يظهر سطر `Hypervisor vendor`؟ وهل يجد `dmidecode` جدول SMBIOS؟ وما قيمة `ROTA` للأقراص؟
- غير `FILE_SIZE` في `latency.py` إلى 64 MiB وأعد القياس على القرص الصلب فقط، وتوقع قبل التشغيل هل يتغير زمن القراءة العشوائية، والسبب أن الملف الأصغر يقع على مسارات أقرب لبعضها فيتحرك الذراع مسافة أقصر.
- شغل `limits.sh` مع `--cpus 0.5` ثم مع `--cpuset-cpus 0,1`، ولاحظ أي الخيارين يغير مخرج `nproc` وأيهما لا يغيره.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- كل حاسب يدخل بيانات ويعالجها ويخزنها ويخرجها، وأغلب الأجهزة تتبع نموذج فون نيومان: معالج وذاكرة واحدة للبرنامج والبيانات وأجهزة على نواقل، والطريق بين المعالج والذاكرة هو عنق الزجاجة الذي تحاول ال Cache تخفيفه.
- المعالج ينفذ، والذاكرة RAM سريعة ولكنها تنسى، والتخزين يحفظ ولكنه أبطأ بمئات أو آلاف المرات، ولذلك يحتفظ النظام بنسخ الملفات في Page Cache.
- القرص الصلب بطيء في القراءة العشوائية لأسباب ميكانيكية وسريع في القراءة المتتالية، وSSD بلا أجزاء متحركة ولكنه يحتاج متحكماً يدير الفلاش، وNVMe واجهة صممت للفلاش على خطوط PCIe.
- اللوحة الأم توصل الذاكرة وبطاقة الرسومات بالمعالج مباشرة، وبقية الأجهزة بالشريحة المساعدة عبر وصلة مشتركة، وسرعة كل وصلة تحسبها من النقلات وعرض الوصلة والترميز.
- المتحكمات والتعريفات والمقاطعات وال DMA هي التي تجعل المعالج حراً بينما الأجهزة تعمل، والإقلاع سلسلة يجهز فيها كل جزء ما يحتاجه الذي بعده.
- في التجربة: 1.4 ميكروثانية من الذاكرة، ونحو 11 من Page Cache، ونحو 112 من NVMe، ونحو 12 ملي ثانية من القرص الصلب عشوائياً، ومئات الملي ثوان من الإنترنت، وما يقوله أي أمر عن العتاد يعتمد على الطبقة التي يعمل فيها.

وفي نفس المرحلة نكمل بإذن الله إلى البوابات المنطقية التي يبنى منها المعالج، ثم إلى المعالج والذاكرة والشاشة كل منها من الداخل:

[![](https://arabroot.io/content/images/2026/10/cpu-internals-cover-cover.webp)دليلكيف يعمل المعالج CPU من الداخل ولماذا يهمك ذلكنتابع تعليمة واحدة داخل نواة المعالج، ونرى لماذا يصبح نفس الكود أبطأ بعشر مرات عندما تغير ترتيب المرور على البيانات.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-cpu-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/ram-internals-cover-cover.webp)دليلكيف تعمل الذاكرة RAM من الداخل، ولماذا تحتاج التحديث Refreshنفتح وحدة الذاكرة من الشريحة إلى الخلية، لتفهم ما تعنيه الأرقام المكتوبة عليها عندما تشتري جهازاً أو تختار سيرفراً.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-ram-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/display-internals-cover-cover.webp)دليلكيف تعمل الشاشة من الداخل: من بطاقة الرسومات إلى عينككيف يصل الإطار من بطاقة الرسومات إلى البكسل، وما الفرق بين LCD وOLED، ولماذا قد تعمل شاشتك بنصف ترددها بسبب الكابل.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/os-basics-cover-cover.webp)دليلنظام التشغيل Operating System: ماذا يفعل بين برامجك والجهاز؟خريطة للمبتدئين تريك بأوامر حقيقية كيف يدير نظام التشغيل العمليات والذاكرة والملفات، ولماذا لا يسقط جهازك كله عندما يسقط برنامج واحد.](https://arabroot.io/articles/%D9%85%D8%A7-%D9%87%D9%88-%D9%86%D8%B8%D8%A7%D9%85-%D8%A7%D9%84%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-operating-system/)[![](https://arabroot.io/content/images/2026/10/hello-world-cover-cover.webp)دليلماذا يحدث عندما تشغل برنامج Hello World؟نتتبع برنامج C صغيراً من المحرر إلى الشاشة، فترى كل طبقة يمر بها في الجهاز حتى تظهر الجملة أمامك.](https://arabroot.io/articles/%D8%B1%D8%AD%D9%84%D8%A9-hello-world-%D9%85%D9%86-%D9%84%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%B4%D8%A7%D8%B4%D8%A9/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على جهاز بمعالج Intel Core i7-7700K ولوحة ASUS Z170-K و64 GiB من DDR4 وثلاثة أقراص (NVMe وSATA SSD وHDD) يعمل بنظام Windows 11: قراءة العتاد بالأوامر lscpu وlsblk وdmidecode وlspci داخل Container من debian:trixie-slim (Debian 13.7) على Docker Desktop وWSL 2، وبأوامر PowerShell على Windows، وقياس الزمن بسكربت Python 3.13 بالمكتبة القياسية فقط، مع صور حقيقية من Wikimedia Commons ورسوم من عرب رووت.