افتح مدير المهام في جهازك الآن، سواءً كان Task Manager في Windows أو Activity Monitor في macOS أو الأمر top في Linux، وسوف تجد مئات البرامج تعمل في نفس اللحظة، بينما معالجك فيه أربع أو ثماني أنوية فقط. وفي يوم ما توقف تبويب في المتصفح برسالة خطأ، ومع ذلك استمرت الموسيقى في العمل وبقي المحرر مفتوحاً كما هو، وفي يوم آخر حذفت ملفاً كبيراً من القرص ولم تعد المساحة إلا بعد أن أغلقت البرنامج الذي كان يستخدمه. وكل هذه المشاهد التي تمر عليك دون أن تتوقف عندها لها سبب واحد: برنامج كبير يعمل تحت كل برامجك اسمه نظام التشغيل Operating System، وهو الذي يقرر من يعمل على المعالج الآن، وأين يضع كل برنامج في الذاكرة، وكيف تحفظ ملفاتك على القرص، ومن يحق له أن يلمس ماذا.
والكثير من المبرمجين ومديري السيرفرات يعملون سنوات دون أن يعرفوا ما الذي يفعله نظام التشغيل بالضبط، ثم يصطدمون به عند أول مشكلة جدية: خدمة تستهلك الذاكرة ولا تعرف هل الرقم الذي تراه حقيقي أم لا، وسيرفر بطيء والمعالج فارغ، و Container يتوقف دون أي رسالة. وهذا المقال هو الخطوة التي تجعل هذه المشاكل مفهومة، فهو لا يغوص في التفاصيل الداخلية وإنما يرسم لك الخريطة الكاملة: ما هي الأجزاء الأساسية لنظام التشغيل، ولماذا وجدت، وكيف تراها بعينك على أي جهاز Linux بأوامر بسيطة، ثم يرسلك في كل جزء إلى المقال الذي يشرحه بعمق. وقد رأينا في مقال ماذا يحدث عندما تشغل برنامج Hello World برنامجاً واحداً يمر عبر هذه الطبقات، وفي مقالي المعالج من الداخل والذاكرة RAM من الداخل رأينا العتاد نفسه، وهنا سوف نقف في المنتصف بينهما.
وسوف نناقش في هذا المقال ما يلي:
- لماذا نحتاج نظام تشغيل أصلاً، وما المشكلة التي حلها منذ الستينيات.
- النواة Kernel وبقية النظام، والفرق بين فضاء النواة وفضاء المستخدم User Space، واستدعاءات النظام System Calls.
- العملية Process والخيط Thread، وكيف يشغل معالج واحد عشرات البرامج بمشاركة الوقت Time-Sharing.
- الذاكرة الافتراضية Virtual Memory والعزل Isolation، ولماذا لا يسقط الجهاز كله عندما يسقط برنامج.
- نظام الملفات File System والـ Inode، وفكرة أن كل شيء ملف في Linux.
- الأجهزة والتعريفات Drivers، ثم نظام التشغيل داخل الـ Container والجهاز الافتراضي VM.
- تجربة عملية ترى فيها كل مفهوم بعينك، ثم الأخطاء الشائعة وجدول يربط كل مفهوم بالأمر الذي يكشفه.
تجهيز المختبر Lab
كل ما في هذا المقال يمكن أن تراه على جهازك بأوامر قليلة، ولكي تكون النتائج متطابقة مع ما ستقرأه نستخدم Container صغيراً من Debian 13 (trixie) فيه Python والأداة ps والأداة strace. وإذا لم يكن Docker مثبتاً لديك فقد شرحنا ذلك في دليل تثبيت Docker على Ubuntu، ويمكنك أيضاً تشغيل نفس الأوامر مباشرة على أي جهاز Linux. ولاحظ أننا اخترنا Container عن قصد، والسبب أن الـ Container يبدأ فارغاً من العمليات تقريباً، فتظهر لك الصورة نظيفة دون مئات العمليات التي تعمل على جهازك:
mkdir os-lab && cd os-lab
cat > Dockerfile <<'EOF'
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends python3 procps strace psmisc && rm -rf /var/lib/apt/lists/*
WORKDIR /lab
EOF
docker build -t sbl-os:1 .والتجارب كلها في سكربت واحد اسمه run.sh مع ملفين صغيرين بلغة Python سوف نعرضهما في أقسامهما، وتشغيلها يكون بأمر واحد:
docker run --rm -v "$PWD":/lab sbl-os:1 sh /lab/run.shوالجهاز الذي ظهرت عليه الأرقام في هذا المقال معالج فيه 8 أنوية منطقية وذاكرة 32 GB، ونواة Linux 5.15 داخل WSL2 تحت Docker Desktop، ولذلك سوف تختلف بعض الأرقام عندك مثل العناوين وأرقام العمليات، ولكن الشكل والمعنى يبقيان كما هما.
لماذا نحتاج نظام تشغيل Operating System أصلاً؟
لنبدأ من الحل البسيط: جهاز بلا نظام تشغيل، وبرنامج واحد يتحكم في كل شيء بنفسه. وهذا ليس خيالاً، فالحواسيب الأولى في الأربعينيات والخمسينيات كانت تعمل بهذا الشكل، حيث يحجز المبرمج الجهاز لساعة كاملة، ويضع برنامجه على بطاقات مثقبة، ويكتب في برنامجه كل التفاصيل: كيف يقرأ من القارئ، وكيف يطبع على الطابعة، وفي أي عنوان من الذاكرة يضع بياناته. وما زالت أجهزة صغيرة كثيرة تعمل اليوم بهذه الطريقة، مثل المتحكم الذي يشغل الغسالة أو جهاز التحكم في المكيف، والسبب أن عليها برنامجاً واحداً فقط لا يتغير.
ولكن هذا الحل يسقط بمجرد أن تريد شيئين في نفس الوقت. فأولاً كل برنامج يجب أن يعرف تفاصيل كل جهاز يستخدمه، فإذا تغير نوع القرص يجب أن يتغير كل برنامج يقرأ منه. وثانياً المعالج غالي الثمن ويبقى معطلاً أغلب الوقت، لأن البرنامج ينتظر القرص أو الطابعة أو المستخدم، والانتظار عند المعالج طويل جداً، فقراءة واحدة من قرص ميكانيكي تأخذ ملايين الدورات من المعالج. وثالثاً إذا عمل برنامجان معاً فلا شيء يمنع الأول من الكتابة فوق ذاكرة الثاني، أو من قراءة ملفات غيره.
والحل الثاني الذي ظهر في الخمسينيات كان برنامجاً صغيراً يبقى في الذاكرة ويشغل البرامج واحداً بعد الآخر من طابور، ويعرف وحده كيف يتعامل مع الأجهزة، وهذه أنظمة المعالجة على دفعات Batch Processing. وهذا حل مشكلة التفاصيل، ولكن المعالج بقي ينتظر، والمستخدم ينتظر ساعات ليرى نتيجة برنامجه. ثم جاءت فكرة مشاركة الوقت Time-Sharing في الستينيات: عدة برامج في الذاكرة معاً، والمعالج يتنقل بينها بسرعة، فإذا انتظر أحدها القرص عمل غيره، ويشعر كل مستخدم أن الجهاز له وحده. ومن هذه الفكرة خرج نظام Unix في مختبرات Bell عام 1969، ويحكي Dennis Ritchie في مقاله عن تاريخ Unix كيف بدأ على جهاز صغير مهمل ثم انتشر، وأغلب ما سوف تراه في هذا المقال من أفكار Unix التي بقيت إلى اليوم. وفي عام 1991 كتب طالب فنلندي اسمه Linus Torvalds رسالته الشهيرة يعلن فيها أنه يكتب نظام تشغيل مجانياً كهواية، وكان ذلك بداية Linux الذي يشغل اليوم أغلب السيرفرات في العالم.
وبهذا نصل إلى تعريف مباشر: نظام التشغيل هو البرنامج الذي يملك العتاد Hardware ويقسمه بين البرامج، فيعطي كل برنامج حصة من المعالج والذاكرة والقرص والشبكة، ويمنعه من الاعتداء على حصة غيره، ويخفي عنه تفاصيل الأجهزة خلف واجهة موحدة. ويمكن أن تفكر فيه مثل إدارة مبنى مكاتب مشترك: كل شركة في المبنى لها مكتبها ومفتاحها، ولا تحتاج أن تعرف كيف تعمل المصاعد والكهرباء، وإذا أرادت شيئاً من خدمات المبنى طلبته من الإدارة ولم تدخل غرفة الكهرباء بنفسها.
النواة Kernel وبقية النظام
عندما يقول الناس Linux أو Windows فهم يقصدون غالباً شيئين معاً: النواة Kernel، وهي البرنامج الذي يعمل في قلب النظام ويملك العتاد فعلاً، وكل ما حولها من برامج وأدوات ومكتبات وواجهات. وLinux بالمعنى الدقيق هو النواة فقط، أما ما تثبته على السيرفر باسم Ubuntu أو Debian فهو توزيعة Distribution تجمع نواة Linux مع آلاف البرامج الأخرى: الطرفية bash، والمكتبة glibc، ومدير الحزم، ومدير الخدمات systemd، وغيرها. وهذا يفسر لماذا تجد عدة توزيعات مختلفة في الشكل والأوامر، وكلها تعمل على نفس النواة، ولماذا يمكن أن تكتب في موقع النواة الرسمي رقم إصدار واحد يخص كل هذه التوزيعات.
والصورة التالية تبين الطبقات من الأعلى إلى الأسفل: برامجك في الأعلى، وتحتها المكتبات التي تستخدمها هذه البرامج، ثم حد فاصل اسمه حد استدعاءات النظام، ثم النواة بأجزائها الخمسة الكبرى، ثم العتاد الذي لا يصل إليه أحد إلا النواة:

ولاحظ في الصورة أن النواة نفسها ليست شيئاً واحداً، وإنما عدة أجزاء كبيرة لكل منها وظيفة: الجدولة Scheduler التي تقرر من يعمل على المعالج، وإدارة الذاكرة Memory Management التي تقرر أين يعيش كل برنامج في RAM، وأنظمة الملفات File Systems التي تحول القرص من كتل أرقام إلى ملفات ومجلدات، والشبكة Network Stack التي تحول الحزم إلى اتصالات TCP، والتعريفات Drivers التي تعرف كيف تتكلم مع كل جهاز بعينه. وكل قسم بعد هذا القسم يأخذ جزءاً من هذه الأجزاء ويريك إياه.
فضاء المستخدم User Space وفضاء النواة Kernel Space
كيف تمنع النواة البرامج من لمس العتاد مباشرة؟ الإجابة على ذلك في المعالج نفسه، فالمعالجات الحديثة تعمل في وضعين على الأقل: وضع مميز Privileged Mode تعمل فيه النواة وتستطيع فيه تنفيذ أي تعليمة والوصول إلى أي جهاز، ووضع مقيد تعمل فيه البرامج العادية، فإذا حاول برنامج في الوضع المقيد تنفيذ تعليمة ممنوعة أو الوصول إلى ذاكرة ليست له أوقفه المعالج فوراً وسلم الأمر إلى النواة. ولذلك نسمي المنطقة التي تعمل فيها البرامج فضاء المستخدم User Space، والمنطقة التي تعمل فيها النواة فضاء النواة Kernel Space.
ولكن البرنامج يحتاج أن يقرأ ملفاً ويرسل حزمة ويطبع على الشاشة، فكيف يفعل ذلك وهو لا يلمس العتاد؟ عن طريق استدعاء النظام System Call، وهو طلب رسمي من البرنامج إلى النواة: «اقرأ لي 100 بايت من هذا الملف»، فينتقل المعالج إلى الوضع المميز، وتتحقق النواة من الطلب ومن صلاحية البرنامج، وتنفذه، ثم ترجع النتيجة ويرجع المعالج إلى الوضع المقيد. ويوجد في Linux بضع مئات من هذه الطلبات، وتجد قائمتها في صفحة syscalls(2)، وأشهرها read وwrite وopen وfork وmmap. ولكي ترى هذه الطلبات بعينك نستخدم الأداة strace التي تطبع كل طلب يرسله البرنامج إلى النواة. والقسم الأخير من السكربت يشغل الأمر cat على ملف صغير تحت strace:
strace -f -e trace=openat,read,write,close cat /etc/hostname 2>&1 | grep -v ENOENT | tail -8
strace -c -f cat /etc/hostname 2>&1 >/dev/null | tail -5
strace -c -f python3 -c "print('hi')" 2>&1 >/dev/null | tail -3والمخرج سوف يكون كما يلي:
read(3, "85699df6de04\n", 262144) = 13
write(1, "85699df6de04\n", 1385699df6de04
) = 13
read(3, "", 262144) = 0
close(3) = 0
close(1) = 0
close(2) = 0
+++ exited with 0 +++
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 1 set_robust_list
0.00 0.000000 0 1 rseq
------ ----------- ----------- --------- --------- ----------------
100.00 0.000563 12 44 1 total
0.00 0.000000 0 1 rseq
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 0 343 41 totalفي المخرج أعلاه لاحظ التالي:
- السطر الأول طلب
readمن الملف رقم 3، وهو الملف/etc/hostnameالذي فتحهcatقبل ذلك، وطلب حتى 262144 بايت فرجعت النواة 13 بايت فقط لأن هذا حجم الملف. - السطر الثاني طلب
writeإلى الملف رقم 1، وهو المخرج القياسي، ولاحظ أن النص المطبوع نفسه ظهر في وسط السطر لأنcatوstraceيكتبان على نفس الشاشة في نفس اللحظة، وهذا شكل طبيعي لا يعني خطأ. - الطلب
readالثاني رجع 0، وهذه الطريقة التي تقول بها النواة للبرنامج إن الملف انتهى، ثم يغلق البرنامج ملفاته ويخرج. - السطر
totalالأول يقول إن الأمرcatكله احتاج 44 طلباً إلى النواة، أغلبها في التحميل والتجهيز قبل أن يقرأ الملف أصلاً، وواحد منها فشل وهو بحث عن ملف غير موجود. - السطر الأخير يقول إن طباعة كلمة واحدة من Python احتاجت 343 طلباً، منها 41 فشلت، والسبب أن Python يحمل مكتبات ويبحث عن ملفات إعداده في عدة أماكن قبل أن يبدأ، وكل محاولة بحث لا تجد ملفاً تحسب طلباً فاشلاً.
والدرس من هذه الأرقام أن كل شيء يفعله برنامجك مع العالم الخارجي يمر عبر النواة، حتى طباعة كلمة واحدة، وأن هذا العبور له كلفة. وقد خصصنا لهذا الموضوع مقالاً كاملاً هو فضاء النواة وفضاء المستخدم: ماذا يكلف استدعاء النظام؟ قسنا فيه كلفة كل عبور، ورأينا كيف يحول خطأ بسيط في طريقة القراءة نسخ ملف من أقل من ملي ثانية إلى عشرات الثواني.
csagent.sys يقرأ العنصر رقم 21 من مصفوفة فيها 20 عنصراً فقط، فقرأ ذاكرة خارج حدودها. ولو حدث نفس الخطأ في برنامج عادي في فضاء المستخدم لتوقف ذلك البرنامج وحده، ولكن التعريف كان يعمل داخل النواة، وخطأ الذاكرة في النواة لا يوجد من يحمي النظام منه، فيوقف النظام كله نفسه حتى لا يفسد البيانات. وهذه أوضح صورة للفرق بين الفضاءين.العملية Process: البرنامج وهو يعمل
البرنامج Program ملف على القرص، فيه تعليمات وبيانات أولية، ولا يستهلك معالجاً ولا ذاكرة وهو هناك. وعندما تشغله تنشئ النواة عملية Process، وهي البرنامج وهو يعمل مع كل ما يحتاجه: رقم فريد اسمه PID، ورقم العملية الأم التي أنشأته PPID، ومساحة ذاكرة خاصة به، وقائمة بالملفات التي فتحها، وحالة تقول هل يعمل الآن أم ينتظر شيئاً، والمستخدم الذي يعمل باسمه والصلاحيات التي يملكها. ولذلك يمكن أن يعمل نفس البرنامج في عشر عمليات في نفس الوقت، فكل نافذة طرفية تفتحها عملية bash مستقلة عن غيرها، وكل منها لها ذاكرتها وملفاتها:

وفي Linux تولد كل عملية جديدة من عملية موجودة، حيث تنسخ العملية الأم نفسها بالطلب fork، ثم تستبدل النسخة الجديدة برنامجها ببرنامج آخر بالطلب exec، وهكذا تشغل الطرفية أي أمر تكتبه. وبهذا الشكل تصبح العمليات شجرة، وجذرها العملية رقم 1 التي تبدأها النواة عند الإقلاع، وهي في أغلب السيرفرات اليوم systemd. والقسم الأول من السكربت يشغل عمليتين من الأمر sleep في الخلفية ثم يعرض كل العمليات في الـ Container:
sleep 300 & sleep 300 &
ps -eo pid,ppid,user,stat,commوالمخرج:
PID PPID USER STAT COMMAND
1 0 root Ss sh
7 1 root S sleep
8 1 root S sleep
9 1 root R psفي المخرج أعلاه لاحظ التالي:
- العملية رقم 1 هنا هي
shالتي تشغل السكربت، وليستsystemd، لأن الـ Container يبدأ بالأمر الذي تعطيه له مباشرة، وهذا أول فرق بين الـ Container والجهاز الكامل وسوف نرجع إليه. - العمود PPID لكل العمليات الأخرى هو 1، أي أن
shهي التي أنشأتها كلها، وهذه هي الشجرة في أبسط أشكالها. - العمود STAT يحمل حالة العملية: الحرف S يعني أنها نائمة تنتظر شيئاً، وعملية
sleepتنتظر مرور الوقت فلا تستهلك من المعالج شيئاً، والحرف R يعني أنها تعمل الآن أو جاهزة للعمل، والعمليةpsكانت تعمل لحظة التقاط الصورة لأنها هي التي تلتقطها. - الحرف s الصغير بعد S في العملية الأولى يعني أنها قائدة الجلسة Session Leader، وهو تفصيل لا تحتاجه الآن.
وفي المقابل شغلنا نفس الأمر على الجهاز الافتراضي الذي يشغل Docker نفسه، أي خارج الـ Container، فوجدنا 30 عملية فيها 275 خيطاً على 8 أنوية منطقية. وعلى جهاز مكتبي عادي فيه متصفح ومحرر وبرامج مراسلة ستجد مئات العمليات وآلاف الخيوط، وهذا يقودنا إلى سؤالين: ما هو الخيط، وكيف تعمل كل هذه العمليات على عدد قليل من الأنوية؟
الخيط Thread: أكثر من مسار تنفيذ في نفس العملية
الخيط Thread مسار تنفيذ داخل العملية، والعملية تبدأ بخيط واحد ويمكن أن تنشئ خيوطاً أخرى، وكل الخيوط في نفس العملية تشترك في نفس الذاكرة ونفس الملفات المفتوحة، ولكن لكل خيط مكانه الحالي في الكود وذاكرة Stack خاصة به. ولهذا يكون إنشاء خيط أرخص من إنشاء عملية، ويكون تبادل البيانات بين الخيوط أسهل بكثير، لأنها ترى نفس المتغيرات مباشرة، وفي نفس الوقت أخطر، لأن خيطين يكتبان على نفس المتغير في نفس اللحظة يفسدان قيمته. أما العمليات فكل منها في عالم منفصل، فإذا أرادت عمليتان أن تتبادلا البيانات احتاجتا إلى طريقة رسمية عبر النواة مثل الأنابيب Pipes أو الشبكة أو ملف مشترك.
والمتصفحات الحديثة مثال جيد على هذا الاختيار، فهي تشغل كل مجموعة من التبويبات في عملية منفصلة، وداخل كل عملية عدة خيوط، والسبب أن الخيوط تعطي سرعة داخل التبويب، والعمليات المنفصلة تعطي عزلاً بين التبويبات، فإذا انهار تبويب لم يأخذ معه بقية المتصفح. وتفاصيل الخيوط والأقفال وحالة السباق Race Condition شرحناها في مقال Threads وasync/await، وماذا يعني ذلك لعدد العمال على سيرفرك في مقال التزامن Concurrency والخيوط.
كيف يشغل معالج واحد عشرات البرامج؟ مشاركة الوقت Time-Sharing
نواة المعالج الواحدة تنفذ تعليمات خيط واحد في كل لحظة، فكيف يعمل 275 خيطاً على 8 أنوية؟ الحقيقة أن أغلبها لا يعمل أصلاً، فهي نائمة تنتظر رسالة من الشبكة أو ضغطة من لوحة المفاتيح أو مرور وقت، مثل عمليتي sleep في المثال السابق. أما الخيوط الجاهزة للعمل فيقسم بينها جزء من النواة اسمه المجدول Scheduler وقت المعالج إلى شرائح قصيرة، فيعطي هذا الخيط بضعة ملي ثوان ثم يوقفه ويحفظ حالته ويعطي غيره، وهذا الانتقال اسمه تبديل السياق Context Switch. ولأن الشرائح قصيرة جداً بالنسبة لإحساس الإنسان يبدو لك أن كل البرامج تعمل معاً.
ولكي ترى ذلك بالأرقام يشغل القسم الخامس من السكربت برنامجين كل منهما حلقة لا تنتهي، ويجبرهما بالأمر taskset -c 0 على العمل على النواة رقم 0 فقط، ثم ينتظر 10 ثوان ويسأل ps عنهما:
taskset -c 0 python3 -c "while True: pass" &
A=$!
taskset -c 0 python3 -c "while True: pass" &
B=$!
sleep 10
ps -o pid,psr,pcpu,etime,time,comm -p $A,$B
kill $A $Bوالمخرج:
PID PSR %CPU ELAPSED TIME COMMAND
17 0 47.6 00:10 00:00:04 python3
18 0 47.6 00:10 00:00:04 python3في المخرج أعلاه لاحظ التالي:
- العمود PSR يقول إن البرنامجين على المعالج رقم 0، كما طلبنا.
- العمود ELAPSED يقول إن كل برنامج عاش 10 ثوان، والعمود TIME يقول إن كلاً منهما حصل على 4 ثوان فقط من وقت المعالج، فكل برنامج يريد المعالج طوال الوقت ولكنه حصل على أقل من النصف.
- العمود %CPU يحمل 47.6% لكل منهما، والمجموع أقل قليلاً من 100% لأن السكربت نفسه وبدء تشغيل Python أخذا جزءاً من الوقت، ولأن الأرقام تقريبية بطبيعتها.
والصورة التالية تبين ما حدث خلال هذه الثواني العشر:

والسؤال الذي يأتي بعد ذلك: كيف يقرر المجدول من يأخذ الشريحة التالية، ولماذا يأخذ أحد البرامج أكثر من غيره، وكيف تجعل تقريراً ليلياً لا يبطئ واجهتك البرمجية؟ هذه أسئلة مقال العمليات وجدولة المعالج، الذي يشرح المجدول العادل CFS وحالات العملية بالتفصيل ويقيس أثر الأولويات بأرقام حقيقية. أما هنا فيكفي أن تعرف أن الرقم %CPU لعملية لا يقول كم تحتاج، وإنما كم حصلت عليه وسط المنافسة، وهذه ملاحظة سوف تحتاجها كثيراً عند قراءة top.
الذاكرة الافتراضية Virtual Memory: كل برنامج يظن أن الذاكرة له وحده
تخيل الآن أن كل برنامج يستخدم عناوين الذاكرة الحقيقية مباشرة، وهو الحل البسيط مرة أخرى. فإذا كتب برنامج في العنوان 1000 وكتب برنامج آخر في نفس العنوان أفسد أحدهما بيانات الآخر، وإذا كان في البرنامج خطأ في مؤشر استطاع أن يكتب فوق أي شيء في الجهاز، حتى النواة نفسها. والحل الذي تستخدمه كل أنظمة التشغيل الحديثة هو الذاكرة الافتراضية Virtual Memory: كل عملية ترى مساحة عناوين خاصة بها كأنها وحدها في الجهاز، والنواة مع وحدة في المعالج اسمها وحدة إدارة الذاكرة MMU تترجم كل عنوان تستخدمه العملية إلى مكان حقيقي في RAM، عبر جدول خاص بكل عملية اسمه جدول الصفحات Page Table. والترجمة تتم على قطع ثابتة الحجم اسمها الصفحات Pages، وحجمها في أغلب الأجهزة 4 KB، كما يشرح توثيق مفاهيم إدارة الذاكرة في النواة.
ولكي ترى هذا بعينك كتبنا برنامجاً صغيراً ينشئ مساحة فيها كلمة original، ثم ينسخ نفسه بالطلب fork، فتغير العملية الابن الكلمة، وتطبع كل عملية عنوان المساحة وما فيها:
# Two processes print the same virtual address but see different values
import os, ctypes
buf = ctypes.create_string_buffer(b"original", 32)
addr = ctypes.addressof(buf)
pid = os.fork()
if pid == 0:
buf.value = b"changed-by-child"
print(f"child pid={os.getpid()} addr={hex(addr)} value={buf.value.decode()}", flush=True)
os._exit(0)
os.waitpid(pid, 0)
print(f"parent pid={os.getpid()} addr={hex(addr)} value={buf.value.decode()}", flush=True)والمخرج:
child pid=11 addr=0x7fcc43858bd0 value=changed-by-child
parent pid=10 addr=0x7fcc43858bd0 value=originalوهذه نتيجة تبدو مستحيلة لمن يظن أن العنوان مكان حقيقي: نفس العنوان 0x7fcc43858bd0 بالضبط في العمليتين، والابن كتب فيه ثم انتهى، والأب قرأ نفس العنوان بعد ذلك فوجد الكلمة القديمة كما هي. والتفسير أن العنوان افتراضي، ولا معنى له إلا داخل عمليته، وكل عملية لها جدول صفحات يربط هذا العنوان بإطار Frame مختلف في الذاكرة الحقيقية:

وقد يتساءل البعض: هل نسخت النواة كل ذاكرة العملية عند fork؟ والإجابة لا، فالنسخ الكامل مكلف، والنواة تجعل العمليتين تشتركان في نفس الإطارات بعد fork، ولا تنسخ الصفحة إلا في اللحظة التي تكتب فيها إحداهما، وهذه فكرة النسخ عند الكتابة Copy-on-Write التي قسنا أثرها بأرقام الإطارات الحقيقية في مقال الذاكرة الافتراضية وجداول الصفحات.
الذاكرة تعطى عند أول استخدام: التحميل عند الطلب Demand Paging
والفائدة الثانية للذاكرة الافتراضية أن النواة تستطيع أن تعد البرنامج بالذاكرة دون أن تعطيه إياها فعلاً، وتنتظر حتى يلمسها. والبرنامج التالي يحجز 1 GiB ثم يلمسه صفحة بعد صفحة، ويطبع بعد كل خطوة رقمين من الملف /proc/self/status: الرقم VmSize وهو حجم مساحة العناوين التي حجزتها العملية، والرقم VmRSS وهو ما تشغله العملية فعلاً من الذاكرة الحقيقية:
# Reserve 1 GiB of virtual memory, then touch it page by page and watch VmSize and VmRSS
import mmap, time
def mem(label):
s = dict(l.split(":", 1) for l in open("/proc/self/status") if l.startswith(("VmSize", "VmRSS")))
print(f"{label:<28} VmSize={s['VmSize'].strip():>12} VmRSS={s['VmRSS'].strip():>12}", flush=True)
mem("start")
m = mmap.mmap(-1, 1 << 30) # 1 GiB, anonymous
mem("after reserving 1 GiB")
for off in range(0, 256 << 20, 4096): # touch the first 256 MiB, one byte per 4 KiB page
m[off] = 1
mem("after touching 256 MiB")
for off in range(256 << 20, 1 << 30, 4096):
m[off] = 1
mem("after touching all 1 GiB")
m.close()
mem("after releasing it")والمخرج:
start VmSize= 14612 kB VmRSS= 8488 kB
after reserving 1 GiB VmSize= 1063188 kB VmRSS= 8488 kB
after touching 256 MiB VmSize= 1063188 kB VmRSS= 271036 kB
after touching all 1 GiB VmSize= 1063188 kB VmRSS= 1057360 kB
after releasing it VmSize= 14612 kB VmRSS= 8912 kBفي المخرج أعلاه لاحظ التالي:
- في البداية كانت مساحة Python كلها حوالي 14 MB، يشغل منها 8 MB في الذاكرة الحقيقية.
- بعد حجز 1 GiB قفز VmSize بمقدار مليون كيلوبايت تقريباً، ولكن VmRSS لم يتغير ولا بكيلوبايت واحد، فالنواة سجلت الحجز في جدولها ولم تعط البرنامج أي صفحة حقيقية بعد.
- بعد لمس أول 256 MiB زاد VmRSS بحوالي 262 ألف كيلوبايت، أي 256 MiB بالضبط تقريباً، فكل صفحة لمسناها أخذت إطاراً حقيقياً في تلك اللحظة.
- بعد لمس الكل وصل VmRSS إلى حوالي 1 GiB، وعند الإغلاق رجع كل شيء إلى ما كان عليه تقريباً، لأن النواة استعادت الإطارات فوراً.
وهذه النتيجة تشرح لك شيئاً تراه كل يوم في top: العمود VIRT يمكن أن يكون ضخماً لبرنامج صغير، لأنه يحسب ما حجزه البرنامج لا ما يستخدمه، والعمود RES هو الأقرب إلى الاستهلاك الحقيقي. وتسمح هذه الطريقة للنواة بأن تعد البرامج بأكثر من الذاكرة الموجودة، وهو ما يسمى الحجز الزائد Overcommit، فإذا لمست البرامج كل ما وعدت به في نفس الوقت ولم تجد النواة ذاكرة كافية، اختارت عملية وأنهتها، وهذا هو قاتل العمليات OOM Killer. وتفاصيل هذا السلوك وكيف تقرأ أرقام الذاكرة دون أن تخدعك موجودة في مقال الذاكرة الافتراضية، وماذا يعني ذلك على السيرفر في قسم الذاكرة من مقال تحليل أداء سيرفر Linux في 60 ثانية.
ويوجد مكان ثالث للذاكرة غير RAM والحجز، وهو مساحة التبديل Swap على القرص، حيث تنقل النواة إليها الصفحات التي لم تستخدم منذ مدة طويلة لتفرغ مكانها في RAM لمن يحتاجه الآن. وهذا يجعل الجهاز يستمر في العمل عندما تقترب الذاكرة من الامتلاء، ولكن بثمن، لأن القرص أبطأ من الذاكرة بآلاف المرات، فإذا احتاج برنامج صفحة نقلت إلى Swap انتظر حتى تقرأ من القرص. والأمر free -m في نهاية القسم الرابع من السكربت يعرض هذه الأرقام كلها في سطرين:
total used free shared buff/cache available
Mem: 32036 2085 27160 41 3281 29950
Swap: 8192 0 8192ولاحظ هنا العمود buff/cache، وهو ذاكرة تستخدمها النواة لحفظ نسخ من الملفات التي قرئت مؤخراً حتى لا تقرأها من القرص مرة أخرى، وهي ذاكرة تستعيدها النواة فوراً عند الحاجة، ولذلك فالعمود الذي يهمك عند السؤال «هل بقيت ذاكرة؟» هو available وليس free.
العزل Isolation: لماذا لا يسقط الجهاز كله عندما يسقط برنامج؟
نرجع الآن إلى التبويب الذي توقف بينما بقيت الموسيقى تعمل. والسبب أن كل عملية لا ترى إلا جدول صفحاتها، فإذا حاولت الوصول إلى عنوان ليس في جدولها، أو إلى عنوان صفر الذي لا يربطه أي جدول عن قصد، رفض المعالج الوصول وأبلغ النواة، فترسل النواة إلى العملية إشارة Signal اسمها SIGSEGV، وهي اختصار Segmentation Violation، فتنتهي العملية وحدها ولا يتأثر أحد غيرها. والقسم الثالث من السكربت يجعل Python يقرأ من العنوان صفر عمداً، ثم يسأل عن عمليتي sleep اللتين أنشأناهما في البداية:
python3 -c "import ctypes; ctypes.string_at(0)"
echo "exit code: $?"
ps -eo pid,stat,comm | grep -E "PID|sleep"والمخرج:
exit code: 139
Segmentation fault (core dumped)
PID STAT COMMAND
7 S sleep
8 S sleepورمز الخروج 139 له معنى محدد ستراه كثيراً في السجلات وفي Docker: عندما تنتهي عملية بسبب إشارة يكون رمز خروجها 128 مضافاً إليه رقم الإشارة، ورقم SIGSEGV هو 11 كما في صفحة signal(7)، فالمجموع 139. وبنفس الطريقة يعني الرمز 137 أن العملية انتهت بالإشارة 9 وهي SIGKILL، وهذا ما تراه عندما يقتل قاتل العمليات Container تجاوز حد ذاكرته. وعمليتا sleep بقيتا على حالهما في الحالة S كأن شيئاً لم يحدث، وهذا هو العزل الذي يعطيه نظام التشغيل لكل عملية.
ولاحظ أن هذا العزل يحمي العمليات من بعضها، ولكنه لا يحمي النظام من النواة نفسها، كما رأينا في حادثة CrowdStrike، ولذلك يعامل كل كود يدخل النواة، مثل التعريفات وبرامج الحماية، بحذر أكبر بكثير من البرامج العادية.
نظام الملفات File System: كيف تبقى ملفاتك بعد إطفاء الجهاز؟
القرص في نظر العتاد سلسلة طويلة من الكتل المرقمة، كل كتلة بضعة كيلوبايتات، ولا شيء فيه اسمه ملف أو مجلد. ونظام الملفات File System هو الطبقة التي تحول هذه الكتل إلى أسماء ومجلدات وصلاحيات وتواريخ، وفي Linux أشهرها ext4 وXFS وBtrfs. والفكرة المركزية فيها جميعاً أن الملف ليس اسمه، فكل ملف له سجل في نظام الملفات اسمه Inode يحمل كل شيء عنه: المالك والصلاحيات والحجم والتواريخ وأماكن كتل بياناته على القرص، إلا شيئاً واحداً هو الاسم. أما الاسم فيعيش في المجلد، والمجلد نفسه ليس إلا جدولاً صغيراً كل سطر فيه اسم ورقم Inode، كما تشرح صفحة inode(7).
والقسم السادس من السكربت ينشئ ملفاً اسمه a.txt، ثم يعطيه اسماً ثانياً بالأمر ln، ثم يحذف الاسم الأول ويحاول قراءة الملف من الاسم الثاني:
cd /tmp && echo "invoice 1001" > a.txt && ln a.txt b.txt
ls -li a.txt b.txt
rm a.txt
cat b.txt
stat -c "name=%n inode=%i links=%h size=%s blocks=%b" b.txtوالمخرج:
220982 -rw-r--r-- 2 root root 13 Oct 30 18:00 a.txt
220982 -rw-r--r-- 2 root root 13 Oct 30 18:00 b.txt
invoice 1001
name=b.txt inode=220982 links=1 size=13 blocks=8في المخرج أعلاه لاحظ التالي:
- الخيار
-iفيlsيطبع رقم الـ Inode في أول السطر، والاسمان يحملان نفس الرقم 220982، أي أنهما ملف واحد باسمين وليسا نسختين. - الرقم 2 بعد الصلاحيات هو عدد الروابط Links، أي عدد الأسماء التي تشير إلى هذا الـ Inode.
- بعد حذف
a.txtبقي المحتوى كما هو، ونزل عدد الروابط إلى 1، فالأمرrmلا يحذف ملفاً وإنما يحذف اسماً. - الحجم 13 بايت ولكن عدد الكتل 8، والسبب أن
statيعد الكتل بوحدات من 512 بايت، فالملف يشغل كتلة واحدة من 4 KB على القرص مهما كان صغيراً.

وهذا يفسر المشهد الذي بدأنا به المقال: حذفت ملفاً كبيراً ولم تعد المساحة. فالنواة لا تحرر كتل الملف إلا عندما يجتمع شرطان: لا يوجد أي اسم يشير إليه، ولا يوجد أي برنامج يفتحه. فإذا كان برنامج ما زال يكتب في ملف سجل حذفته، بقي الملف موجوداً بلا اسم حتى يغلقه البرنامج، وهذه من أشهر مشاكل امتلاء القرص على السيرفرات، وقد شرحنا كيف تكتشفها في قسم الملفات المحذوفة من مقال تحليل الأداء.
وقد يتساءل البعض: إذا كتب البرنامج في الملف ثم انقطعت الكهرباء، هل وصلت البيانات إلى القرص؟ والإجابة ليس بالضرورة، فالنواة تجمع الكتابات في الذاكرة وترسلها إلى القرص لاحقاً لأن ذلك أسرع بكثير، ولذلك تحتاج قواعد البيانات إلى طلب خاص اسمه fsync يجبر النواة على الإرسال الآن. وهذه القصة كلها، مع تجربة نقطع فيها الكهرباء عن نظام ملفات يعمل، موجودة في مقال نظام الملفات من الداخل: Inodes والسجل وfsync.
كل شيء ملف Everything is a File
من أفكار Unix التي بقيت حتى اليوم أن النواة تقدم أشياء كثيرة ليست ملفات على شكل ملفات، فتقرأها وتكتبها بنفس الأوامر. فالقرص نفسه ملف في /dev، والطرفية ملف، والمكان الذي يبتلع كل ما تكتبه فيه ملف اسمه /dev/null، ومعلومات كل عملية تجدها كملفات في المجلد /proc، وهو مجلد لا يوجد على أي قرص وإنما تولده النواة لحظة قراءته، كما تشرح صفحة proc(5). والقسم السابع من السكربت يقرأ حالة العملية الحالية وقائمة ملفاتها المفتوحة:
head -5 /proc/self/status
ls -l /proc/self/fdوالمخرج:
Name: head
Umask: 0022
State: R (running)
Tgid: 26
Ngid: 0
total 0
lrwx------ 1 root root 64 Oct 30 18:00 0 -> /dev/null
l-wx------ 1 root root 64 Oct 30 18:00 1 -> pipe:[6233386]
l-wx------ 1 root root 64 Oct 30 18:00 2 -> pipe:[6233387]
lr-x------ 1 root root 64 Oct 30 18:00 3 -> /proc/27/fdوالاسم self يشير دائماً إلى العملية التي تقرأ، ولذلك ظهر في السطر الأول اسم head لأنه هو الذي قرأ الملف، وفي القائمة الثانية ظهرت ملفات ls نفسه. ولاحظ الأرقام 0 و1 و2، فكل عملية تبدأ بثلاثة ملفات مفتوحة: الإدخال القياسي stdin والمخرج القياسي stdout ومخرج الأخطاء stderr، والرقم الذي يعطيه النظام لكل ملف مفتوح اسمه واصف الملف File Descriptor. وهنا يشير الإدخال إلى /dev/null لأن Docker لم يعطنا لوحة مفاتيح، والمخرجان يشيران إلى أنابيب Pipes لأن Docker يجمع ما يطبعه البرنامج ليعرضه، وهذه نفس الأنابيب التي تستخدمها عندما تكتب | بين أمرين في الطرفية. والرقم 3 هو المجلد الذي فتحه ls ليقرأه. وكل الأوامر التي تعرض العمليات مثل ps وtop لا تفعل أكثر من قراءة هذه الملفات وتنسيقها لك.
الأجهزة والتعريفات Drivers والمقاطعات Interrupts
النواة تتكلم مع آلاف الأنواع من الأجهزة، ولكل نوع طريقته الخاصة في الأوامر والإعدادات، ولذلك لا تعرف النواة تفاصيل كل جهاز بنفسها، وإنما يضيف كل جهاز تعريفاً Driver، وهو جزء من الكود يعرف كيف يتكلم مع هذا الجهاز بعينه، ويقدمه لبقية النواة بواجهة موحدة. فنظام الملفات مثلاً يطلب «اقرأ الكتلة رقم 5000» دون أن يعرف هل القرص SSD من نوع NVMe أو قرص ميكانيكي قديم أو قرص افتراضي في السحابة، والتعريف هو الذي يترجم الطلب إلى لغة الجهاز. ولهذا يعمل نفس برنامجك على أجهزة مختلفة تماماً دون أن تغير فيه سطراً، وهذا من أهم ما يعطيه نظام التشغيل للمبرمج. وفي Linux أغلب التعريفات تعمل داخل النواة، ولهذا يكون خطأ في تعريف أخطر بكثير من خطأ في برنامج عادي.
وكيف تعرف النواة أن ضغطة وصلت من لوحة المفاتيح أو أن حزمة وصلت إلى بطاقة الشبكة؟ الطريقة البسيطة أن تسأل كل جهاز باستمرار «هل عندك شيء؟»، وهذا يضيع وقت المعالج في أسئلة جوابها غالباً لا. والطريقة التي تستخدمها الأجهزة أن الجهاز نفسه يرسل إشارة كهربائية إلى المعالج اسمها المقاطعة Interrupt، فيترك المعالج ما يفعله لحظة، وينفذ جزءاً صغيراً من التعريف يأخذ البيانات، ثم يرجع إلى عمله. ولهذا تبقى عملية تنتظر الشبكة في الحالة S دون أن تستهلك شيئاً، فالنواة توقظها عندما تصل المقاطعة ومعها البيانات. وعلى السيرفرات التي تستقبل عدداً هائلاً من الحزم في الثانية تتحول المقاطعات نفسها إلى حمل، فتجمع بطاقات الشبكة الحديثة عدة حزم في مقاطعة واحدة، وهذه تفاصيل لن تحتاجها في البداية ولكن من المفيد أن تعرف أنها موجودة عندما ترى العمود si في top.
نظام التشغيل داخل الـ Container والجهاز الافتراضي VM
رأينا في بداية التجربة أن العملية رقم 1 داخل الـ Container هي sh، وأن الـ Container يرى أربع عمليات فقط، بينما الجهاز الذي يعمل عليه فيه عشرات. فهل الـ Container نظام تشغيل مستقل؟ الإجابة لا، وهذا من أكثر الأفكار التي يخطئ فيها المبتدئون. فالجهاز الافتراضي Virtual Machine يشغل نواة كاملة خاصة به فوق برنامج اسمه المشرف Hypervisor يقسم العتاد بين الأجهزة الافتراضية، أما الـ Container فهو مجموعة عمليات عادية تعمل على نواة السيرفر نفسها، والنواة تعطيها نظرة محدودة عن النظام عبر ميزة اسمها مساحات الأسماء Namespaces، فترى أرقام عملياتها وملفاتها وشبكتها فقط، كما تشرح صفحة namespaces(7)، وتحد ما تستهلكه من المعالج والذاكرة بميزة ثانية اسمها مجموعات التحكم cgroups:

وهذا يفسر ثلاثة أشياء ستلاحظها عند العمل مع Docker. فأولاً عندما تكتب uname -r داخل Container من Debian ستجد إصدار نواة السيرفر وليس نواة Debian، وفي تجربتنا ظهر الإصدار 5.15.167.4-microsoft-standard-WSL2 داخل Container من Debian 13. وثانياً كلمة Debian في الـ Container تعني المكتبات والأدوات ومدير الحزم فقط، ولهذا يكون حجم الـ Image بضع عشرات من الميغابايت لا بضعة غيغابايت. وثالثاً أي خطأ في النواة أو ثغرة فيها يمس كل الـ Containers على السيرفر معاً، ولذلك يكون العزل في الجهاز الافتراضي أقوى، والـ Container أخف وأسرع في الإقلاع. ولاحظ أيضاً أن أدوات مثل free وtop داخل الـ Container قد تعرض أرقام السيرفر كله لا حدود الـ Container، والسبب أنها تقرأ ملفات /proc التي لا تعرف كلها بحدود الـ cgroups، وهذه نقطة شرحناها مع أرقامها في مقال تحليل الأداء.
أخطاء شائعة في فهم نظام التشغيل
- اعتبار Linux توزيعة: Linux هو النواة، وUbuntu وDebian وRocky توزيعات تبنى فوقها، ولهذا يختلف مدير الحزم بينها بينما تبقى استدعاءات النظام واحدة.
- قراءة العمود VIRT أو VmSize على أنه استهلاك: رأينا برنامجاً حجز 1 GiB وذاكرته الحقيقية لم تزد كيلوبايتاً واحداً حتى لمسها.
- الحكم بأن الذاكرة ممتلئة لأن العمود
freeصغير: النواة تستخدم الذاكرة الفارغة لنسخ الملفات وتعيدها عند الحاجة، والرقم الصحيح هوavailable. - تفسير %CPU لعملية على أنه ما تحتاجه: البرنامجان في التجربة كانا يحتاجان المعالج كاملاً، وحصل كل منهما على 47.6% فقط بسبب المنافسة.
- الظن أن
rmيحرر المساحة فوراً: يحذف الاسم فقط، والمساحة تعود عندما لا يبقى اسم ولا برنامج يفتح الملف. - تجاهل رمز الخروج: الرقم 139 يعني خطأ ذاكرة في البرنامج نفسه، والرقم 137 يعني أن شيئاً من الخارج قتله بالإشارة 9، والفرق بينهما يوجهك إلى مكان المشكلة مباشرة.
- معاملة الـ Container كجهاز افتراضي: لا توجد نواة داخله، وكل ما يحدث في نواة السيرفر يمسه.
- تشغيل كل شيء بالمستخدم root لأنه أسهل: العزل بين العمليات يعتمد على المستخدم الذي تعمل به كل عملية، وقد شرحنا ذلك في مقال صلاحيات الملفات Permissions في Linux.
أين ترى كل مفهوم على جهازك؟
الجدول التالي يجمع كل ما مر في المقال في سطر لكل مفهوم: ماذا تكتب، وماذا تبحث عنه في المخرج، وأين تقرأ عنه بعمق.
| المفهوم | الأمر | ما تبحث عنه | المقال الأعمق |
|---|---|---|---|
| النواة وإصدارها | uname -r | إصدار النواة، وهو نفسه داخل الـ Container | هذا المقال |
| العمليات وشجرتها | ps -eo pid,ppid,stat,comm أو pstree -p | من أنشأ من، والحالة R أو S أو D أو Z | العمليات وجدولة المعالج |
| الخيوط | ps -eLf | عدد الخيوط لكل عملية في العمود NLWP | التزامن Concurrency والخيوط |
| مشاركة المعالج | top ثم الضغط على 1 | %CPU لكل عملية ولكل نواة | العمليات وجدولة المعالج |
| استدعاءات النظام | strace -c | عدد الطلبات ونوعها وما فشل منها | فضاء النواة واستدعاءات النظام |
| الذاكرة الافتراضية والحقيقية | grep Vm /proc/PID/status | الفرق بين VmSize وVmRSS | الذاكرة الافتراضية وجداول الصفحات |
| الذاكرة المتاحة وSwap | free -m | العمود available وسطر Swap | تحليل أداء سيرفر Linux |
| الـ Inode والروابط | ls -li وstat | رقم الـ Inode وعدد الروابط | نظام الملفات من الداخل |
| الملفات المفتوحة | ls -l /proc/PID/fd | ما تفتحه العملية من ملفات وأنابيب واتصالات | هذا المقال |
| سبب نهاية عملية | echo $? بعد الأمر | رقم أكبر من 128 يعني إشارة، والفرق هو رقمها | هذا المقال |
حذف المختبر Cleanup
كل التجارب عملت داخل Container بالخيار --rm، فلا يبقى بعدها إلا الـ Image التي بنيناها، واحذفها إذا لم تكن ستكمل مع بقية المقالات:
docker rmi sbl-os:1الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- نظام التشغيل يملك العتاد ويقسمه بين البرامج، ويعزلها عن بعضها، ويخفي عنها تفاصيل الأجهزة، وقلبه النواة Kernel، وLinux بالمعنى الدقيق هو النواة فقط.
- البرامج تعمل في فضاء المستخدم ولا تلمس العتاد، وكل ما تطلبه يمر عبر استدعاءات النظام، وحتى طباعة كلمة من Python احتاجت 343 طلباً.
- العملية برنامج يعمل له رقم وذاكرة وملفات وحالة، والخيوط مسارات تنفيذ داخل العملية تشترك في ذاكرتها.
- المجدول يقسم وقت المعالج إلى شرائح، وبرنامجان على نواة واحدة حصل كل منهما على 47.6% فقط.
- كل عملية ترى عناوين افتراضية خاصة بها، فنفس العنوان حمل قيمتين في عمليتين، والذاكرة تعطى عند أول لمس لا عند الحجز.
- العزل يجعل خطأ الذاكرة يوقف برنامجه وحده برمز 139، أما خطأ في النواة فيوقف النظام كله.
- الملف هو الـ Inode والاسم رابط إليه، والمساحة تعود عند آخر رابط وآخر برنامج.
- الـ Container عمليات على نواة السيرفر، والجهاز الافتراضي نواة كاملة خاصة به.
وفي مقالات قادمة بإذن الله سوف ننتقل من الخريطة إلى السيرفر نفسه: كيف تدير الخدمات والسجلات مع systemd، وكيف تضيف الأقراص وتركبها، وكيف تشخص سيرفراً معطلاً. أما إذا أردت النزول إلى داخل كل جزء فهذه مقالات هذه المرحلة:
دليلالعمليات Processes وجدولة المعالج CPU Scheduling في Linuxنرى من داخل النواة كيف يختار المجدول من يعمل الآن، ولماذا يبطئ سكربت ليلي واجهتك البرمجية، وكيف يعيدها سطر واحد إلى سرعتها.
دليلالذاكرة الافتراضية Virtual Memory: جداول الصفحات وخطأ الصفحةنتابع العنوان من برنامجك إلى شريحة الذاكرة، ونفهم بأرقام حقيقية لماذا تخدعك أعمدة VIRT وRES، ومتى تسرع الصفحات الكبيرة برنامجك.
دليلفضاء النواة والمستخدم: كم يكلف استدعاء النظام System Call؟نعبر الحد بين برنامجك والنواة بأيدينا دون أي مكتبة، ونقيس كلفة كل عبور، ونرى لماذا يأخذ نسخ ملف ببايت واحد في كل طلب عشرات الثواني بدلاً من أقل من ملي ثانية.
دليلنظام الملفات File System: Inodes والسجل Journal وfsyncنتابع الكتابة من برنامجك حتى القرص داخل ext4، ونقطع الكهرباء عن نظام ملفات يعمل لنرى ماذا يبقى فعلاً، ومتى تحتاج fsync وكم يكلف.
دليلماذا يحدث عندما تشغل برنامج Hello World؟نتتبع برنامج C صغيراً من المحرر إلى الشاشة، فترى كل طبقة يمر بها في الجهاز حتى تظهر الجملة أمامك.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب داخل Container من Debian 13 (trixie) مع Python 3.13.5 وprocps-ng 4.0.4 وstrace 6.13، على نواة Linux 5.15 (WSL2) وDocker Engine 29.8.0.