> ## 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.

# نظام التشغيل Operating System: ماذا يفعل بين برامجك والجهاز؟
- URL: https://arabroot.io/articles/ما-هو-نظام-التشغيل-operating-system/
- Published: 2026-10-30T17:19:44.000Z
- Updated: 2026-10-30T18:19:17.000Z
- Description: خريطة للمبتدئين تريك بأوامر حقيقية كيف يدير نظام التشغيل العمليات والذاكرة والملفات، ولماذا لا يسقط جهازك كله عندما يسقط برنامج واحد.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, لينكس والسيرفرات

افتح مدير المهام في جهازك الآن، سواءً كان Task Manager في Windows أو Activity Monitor في macOS أو الأمر `top` في Linux، وسوف تجد مئات البرامج تعمل في نفس اللحظة، بينما معالجك فيه أربع أو ثماني أنوية فقط. وفي يوم ما توقف تبويب في المتصفح برسالة خطأ، ومع ذلك استمرت الموسيقى في العمل وبقي المحرر مفتوحاً كما هو، وفي يوم آخر حذفت ملفاً كبيراً من القرص ولم تعد المساحة إلا بعد أن أغلقت البرنامج الذي كان يستخدمه. وكل هذه المشاهد التي تمر عليك دون أن تتوقف عندها لها سبب واحد: برنامج كبير يعمل تحت كل برامجك اسمه نظام التشغيل Operating System، وهو الذي يقرر من يعمل على المعالج الآن، وأين يضع كل برنامج في الذاكرة، وكيف تحفظ ملفاتك على القرص، ومن يحق له أن يلمس ماذا.

والكثير من المبرمجين ومديري السيرفرات يعملون سنوات دون أن يعرفوا ما الذي يفعله نظام التشغيل بالضبط، ثم يصطدمون به عند أول مشكلة جدية: خدمة تستهلك الذاكرة ولا تعرف هل الرقم الذي تراه حقيقي أم لا، وسيرفر بطيء والمعالج فارغ، و Container يتوقف دون أي رسالة. وهذا المقال هو الخطوة التي تجعل هذه المشاكل مفهومة، فهو لا يغوص في التفاصيل الداخلية وإنما يرسم لك الخريطة الكاملة: ما هي الأجزاء الأساسية لنظام التشغيل، ولماذا وجدت، وكيف تراها بعينك على أي جهاز Linux بأوامر بسيطة، ثم يرسلك في كل جزء إلى المقال الذي يشرحه بعمق. وقد رأينا في مقال [ماذا يحدث عندما تشغل برنامج 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/) برنامجاً واحداً يمر عبر هذه الطبقات، وفي مقالي [المعالج من الداخل](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/) و[الذاكرة 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/) رأينا العتاد نفسه، وهنا سوف نقف في المنتصف بينهما.

وسوف نناقش في هذا المقال ما يلي:

- لماذا نحتاج نظام تشغيل أصلاً، وما المشكلة التي حلها منذ الستينيات.
- النواة 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](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-docker-%D8%B9%D9%84%D9%89-ubuntu/)، ويمكنك أيضاً تشغيل نفس الأوامر مباشرة على أي جهاز Linux. ولاحظ أننا اخترنا Container عن قصد، والسبب أن الـ Container يبدأ فارغاً من العمليات تقريباً، فتظهر لك الصورة نظيفة دون مئات العمليات التي تعمل على جهازك:

```bash
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 سوف نعرضهما في أقسامهما، وتشغيلها يكون بأمر واحد:

```bash
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](https://www.nokia.com/bell-labs/about/dennis-m-ritchie/hist.html?ref=arabroot.io) كيف بدأ على جهاز صغير مهمل ثم انتشر، وأغلب ما سوف تراه في هذا المقال من أفكار Unix التي بقيت إلى اليوم. وفي عام 1991 كتب طالب فنلندي اسمه Linus Torvalds [رسالته الشهيرة](https://groups.google.com/g/comp.os.minix/c/dlNtH7RRrGA/m/SwRavCzVE7gJ?ref=arabroot.io) يعلن فيها أنه يكتب نظام تشغيل مجانياً كهواية، وكان ذلك بداية Linux الذي يشغل اليوم أغلب السيرفرات في العالم.

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

## النواة Kernel وبقية النظام

عندما يقول الناس Linux أو Windows فهم يقصدون غالباً شيئين معاً: النواة Kernel، وهي البرنامج الذي يعمل في قلب النظام ويملك العتاد فعلاً، وكل ما حولها من برامج وأدوات ومكتبات وواجهات. وLinux بالمعنى الدقيق هو النواة فقط، أما ما تثبته على السيرفر باسم Ubuntu أو Debian فهو توزيعة Distribution تجمع نواة Linux مع آلاف البرامج الأخرى: الطرفية `bash`، والمكتبة `glibc`، ومدير الحزم، ومدير الخدمات `systemd`، وغيرها. وهذا يفسر لماذا تجد عدة توزيعات مختلفة في الشكل والأوامر، وكلها تعمل على نفس النواة، ولماذا يمكن أن تكتب في [موقع النواة الرسمي](https://www.kernel.org/?ref=arabroot.io) رقم إصدار واحد يخص كل هذه التوزيعات.

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

![طبقات نظام التشغيل من الأعلى: برامج المستخدم مثل المتصفح والطرفية وخدمة على السيرفر، ثم المكتبات مثل glibc، ثم حد استدعاءات النظام بطلبات مثل read وwrite وfork وmmap، ثم النواة بأجزائها الجدولة وإدارة الذاكرة وأنظمة الملفات والشبكة والتعريفات، ثم العتاد: المعالج والذاكرة والقرص وبطاقة الشبكة](https://arabroot.io/content/images/2026/10/os-basics-01-layers.webp)

البرامج تطلب، والنواة وحدها تلمس العتاد

ولاحظ في الصورة أن النواة نفسها ليست شيئاً واحداً، وإنما عدة أجزاء كبيرة لكل منها وظيفة: الجدولة 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)](https://man7.org/linux/man-pages/man2/syscalls.2.html?ref=arabroot.io)، وأشهرها `read` و`write` و`open` و`fork` و`mmap`. ولكي ترى هذه الطلبات بعينك نستخدم الأداة [strace](https://man7.org/linux/man-pages/man1/strace.1.html?ref=arabroot.io) التي تطبع كل طلب يرسله البرنامج إلى النواة. والقسم الأخير من السكربت يشغل الأمر `cat` على ملف صغير تحت `strace`:

```bash
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
```

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

```text
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 يحمل مكتبات ويبحث عن ملفات إعداده في عدة أماكن قبل أن يبدأ، وكل محاولة بحث لا تجد ملفاً تحسب طلباً فاشلاً.

والدرس من هذه الأرقام أن كل شيء يفعله برنامجك مع العالم الخارجي يمر عبر النواة، حتى طباعة كلمة واحدة، وأن هذا العبور له كلفة. وقد خصصنا لهذا الموضوع مقالاً كاملاً هو [فضاء النواة وفضاء المستخدم: ماذا يكلف استدعاء النظام؟](https://arabroot.io/articles/%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D8%A9-%D9%88%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%A7%D8%B3%D8%AA%D8%AF%D8%B9%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D8%B8%D8%A7%D9%85/) قسنا فيه كلفة كل عبور، ورأينا كيف يحول خطأ بسيط في طريقة القراءة نسخ ملف من أقل من ملي ثانية إلى عشرات الثواني.

💡

في 19 يوليو 2024 توقفت ملايين أجهزة Windows حول العالم عن الإقلاع بشاشة زرقاء بعد تحديث لبرنامج الحماية Falcon من شركة CrowdStrike. ويشرح [تقرير الشركة عن السبب الجذري](https://www.crowdstrike.com/wp-content/uploads/2024/08/Channel-File-291-Incident-Root-Cause-Analysis-08.06.2024.pdf?ref=arabroot.io) أن ملف إعداد جديد جعل التعريف `csagent.sys` يقرأ العنصر رقم 21 من مصفوفة فيها 20 عنصراً فقط، فقرأ ذاكرة خارج حدودها. ولو حدث نفس الخطأ في برنامج عادي في فضاء المستخدم لتوقف ذلك البرنامج وحده، ولكن التعريف كان يعمل داخل النواة، وخطأ الذاكرة في النواة لا يوجد من يحمي النظام منه، فيوقف النظام كله نفسه حتى لا يفسد البيانات. وهذه أوضح صورة للفرق بين الفضاءين.

## العملية Process: البرنامج وهو يعمل

البرنامج Program ملف على القرص، فيه تعليمات وبيانات أولية، ولا يستهلك معالجاً ولا ذاكرة وهو هناك. وعندما تشغله تنشئ النواة عملية Process، وهي البرنامج وهو يعمل مع كل ما يحتاجه: رقم فريد اسمه PID، ورقم العملية الأم التي أنشأته PPID، ومساحة ذاكرة خاصة به، وقائمة بالملفات التي فتحها، وحالة تقول هل يعمل الآن أم ينتظر شيئاً، والمستخدم الذي يعمل باسمه والصلاحيات التي يملكها. ولذلك يمكن أن يعمل نفس البرنامج في عشر عمليات في نفس الوقت، فكل نافذة طرفية تفتحها عملية `bash` مستقلة عن غيرها، وكل منها لها ذاكرتها وملفاتها:

![ملف البرنامج على القرص يتحول عند التشغيل بالطلبين fork وexec إلى عملية في الذاكرة لها رقم PID ورقم أب PPID ومساحة عناوين فيها Code وHeap وStack وملفات مفتوحة وحالة وخيوط ومستخدم، وإلى اليسار شجرة العمليات من التجربة تبدأ بالعملية رقم 1](https://arabroot.io/content/images/2026/10/os-basics-02-process.webp)

العملية تحمل كل ما يحتاجه البرنامج ليعمل، وكل عملية لها أب

وفي Linux تولد كل عملية جديدة من عملية موجودة، حيث تنسخ العملية الأم نفسها بالطلب [fork](https://man7.org/linux/man-pages/man2/fork.2.html?ref=arabroot.io)، ثم تستبدل النسخة الجديدة برنامجها ببرنامج آخر بالطلب `exec`، وهكذا تشغل الطرفية أي أمر تكتبه. وبهذا الشكل تصبح العمليات شجرة، وجذرها العملية رقم 1 التي تبدأها النواة عند الإقلاع، وهي في أغلب السيرفرات اليوم `systemd`. والقسم الأول من السكربت يشغل عمليتين من الأمر `sleep` في الخلفية ثم يعرض كل العمليات في الـ Container:

```bash
sleep 300 & sleep 300 &
ps -eo pid,ppid,user,stat,comm
```

والمخرج:

```text
  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](https://arabroot.io/articles/%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-threads-%D9%88%D8%A7%D9%84%D8%A3%D9%82%D9%81%D8%A7%D9%84-%D9%88-async-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)، وماذا يعني ذلك لعدد العمال على سيرفرك في مقال [التزامن Concurrency والخيوط](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/).

### كيف يشغل معالج واحد عشرات البرامج؟ مشاركة الوقت Time-Sharing

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

ولكي ترى ذلك بالأرقام يشغل القسم الخامس من السكربت برنامجين كل منهما حلقة لا تنتهي، ويجبرهما بالأمر `taskset -c 0` على العمل على النواة رقم 0 فقط، ثم ينتظر 10 ثوان ويسأل `ps` عنهما:

```bash
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
```

والمخرج:

```text
  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 أخذا جزءاً من الوقت، ولأن الأرقام تقريبية بطبيعتها.

والصورة التالية تبين ما حدث خلال هذه الثواني العشر:

![خط زمني لنواة معالج واحدة يتناوب عليها البرنامج A والبرنامج B في شرائح قصيرة، مع خطوط متقطعة تشير إلى تبديل السياق بين كل شريحة والتي تليها، وتحته نتيجة الأمر ps بعد 10 ثوان: كل برنامج 47.6 بالمئة من المعالج و4 ثوان من وقت التشغيل](https://arabroot.io/content/images/2026/10/os-basics-03-timeshare.webp)

كل برنامج يظن أنه يعمل وحده، ولكنه يعمل بنصف السرعة

والسؤال الذي يأتي بعد ذلك: كيف يقرر المجدول من يأخذ الشريحة التالية، ولماذا يأخذ أحد البرامج أكثر من غيره، وكيف تجعل تقريراً ليلياً لا يبطئ واجهتك البرمجية؟ هذه أسئلة مقال [العمليات وجدولة المعالج](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A7%D8%AA-%D9%88%D8%AC%D8%AF%D9%88%D9%84%D8%A9-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%81%D9%8A-linux/)، الذي يشرح المجدول العادل CFS وحالات العملية بالتفصيل ويقيس أثر الأولويات بأرقام حقيقية. أما هنا فيكفي أن تعرف أن الرقم %CPU لعملية لا يقول كم تحتاج، وإنما كم حصلت عليه وسط المنافسة، وهذه ملاحظة سوف تحتاجها كثيراً عند قراءة `top`.

## الذاكرة الافتراضية Virtual Memory: كل برنامج يظن أن الذاكرة له وحده

تخيل الآن أن كل برنامج يستخدم عناوين الذاكرة الحقيقية مباشرة، وهو الحل البسيط مرة أخرى. فإذا كتب برنامج في العنوان 1000 وكتب برنامج آخر في نفس العنوان أفسد أحدهما بيانات الآخر، وإذا كان في البرنامج خطأ في مؤشر استطاع أن يكتب فوق أي شيء في الجهاز، حتى النواة نفسها. والحل الذي تستخدمه كل أنظمة التشغيل الحديثة هو الذاكرة الافتراضية Virtual Memory: كل عملية ترى مساحة عناوين خاصة بها كأنها وحدها في الجهاز، والنواة مع وحدة في المعالج اسمها وحدة إدارة الذاكرة MMU تترجم كل عنوان تستخدمه العملية إلى مكان حقيقي في RAM، عبر جدول خاص بكل عملية اسمه جدول الصفحات Page Table. والترجمة تتم على قطع ثابتة الحجم اسمها الصفحات Pages، وحجمها في أغلب الأجهزة 4 KB، كما يشرح توثيق [مفاهيم إدارة الذاكرة في النواة](https://docs.kernel.org/admin-guide/mm/concepts.html?ref=arabroot.io).

ولكي ترى هذا بعينك كتبنا برنامجاً صغيراً ينشئ مساحة فيها كلمة `original`، ثم ينسخ نفسه بالطلب `fork`، فتغير العملية الابن الكلمة، وتطبع كل عملية عنوان المساحة وما فيها:

```python
# 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)
```

والمخرج:

```text
child  pid=11 addr=0x7fcc43858bd0 value=changed-by-child
parent pid=10 addr=0x7fcc43858bd0 value=original
```

وهذه نتيجة تبدو مستحيلة لمن يظن أن العنوان مكان حقيقي: نفس العنوان `0x7fcc43858bd0` بالضبط في العمليتين، والابن كتب فيه ثم انتهى، والأب قرأ نفس العنوان بعد ذلك فوجد الكلمة القديمة كما هي. والتفسير أن العنوان افتراضي، ولا معنى له إلا داخل عمليته، وكل عملية لها جدول صفحات يربط هذا العنوان بإطار Frame مختلف في الذاكرة الحقيقية:

![العملية الأب رقم 10 والعملية الابن رقم 11 تستخدمان نفس العنوان الافتراضي 0x7fcc43858bd0، وجدول صفحات كل عملية يربطه بإطار مختلف في الذاكرة الحقيقية، فيحمل إطار الأب كلمة original ويحمل إطار الابن كلمة changed-by-child](https://arabroot.io/content/images/2026/10/os-basics-04-virtual-memory.webp)

العنوان واحد، والمكان الحقيقي مختلف لأن لكل عملية جدولها

وقد يتساءل البعض: هل نسخت النواة كل ذاكرة العملية عند `fork`؟ والإجابة لا، فالنسخ الكامل مكلف، والنواة تجعل العمليتين تشتركان في نفس الإطارات بعد `fork`، ولا تنسخ الصفحة إلا في اللحظة التي تكتب فيها إحداهما، وهذه فكرة النسخ عند الكتابة Copy-on-Write التي قسنا أثرها بأرقام الإطارات الحقيقية في مقال [الذاكرة الافتراضية وجداول الصفحات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A9-virtual-memory-%D9%88%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B5%D9%81%D8%AD%D8%A7%D8%AA/).

### الذاكرة تعطى عند أول استخدام: التحميل عند الطلب Demand Paging

والفائدة الثانية للذاكرة الافتراضية أن النواة تستطيع أن تعد البرنامج بالذاكرة دون أن تعطيه إياها فعلاً، وتنتظر حتى يلمسها. والبرنامج التالي يحجز 1 GiB ثم يلمسه صفحة بعد صفحة، ويطبع بعد كل خطوة رقمين من الملف `/proc/self/status`: الرقم VmSize وهو حجم مساحة العناوين التي حجزتها العملية، والرقم VmRSS وهو ما تشغله العملية فعلاً من الذاكرة الحقيقية:

```python
# 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")
```

والمخرج:

```text
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 ثانية](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/).

ويوجد مكان ثالث للذاكرة غير RAM والحجز، وهو مساحة التبديل Swap على القرص، حيث تنقل النواة إليها الصفحات التي لم تستخدم منذ مدة طويلة لتفرغ مكانها في RAM لمن يحتاجه الآن. وهذا يجعل الجهاز يستمر في العمل عندما تقترب الذاكرة من الامتلاء، ولكن بثمن، لأن القرص أبطأ من الذاكرة بآلاف المرات، فإذا احتاج برنامج صفحة نقلت إلى Swap انتظر حتى تقرأ من القرص. والأمر `free -m` في نهاية القسم الرابع من السكربت يعرض هذه الأرقام كلها في سطرين:

```text
               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` اللتين أنشأناهما في البداية:

```bash
python3 -c "import ctypes; ctypes.string_at(0)"
echo "exit code: $?"
ps -eo pid,stat,comm | grep -E "PID|sleep"
```

والمخرج:

```text
exit code: 139
Segmentation fault (core dumped)
  PID STAT COMMAND
    7 S    sleep
    8 S    sleep
```

ورمز الخروج 139 له معنى محدد ستراه كثيراً في السجلات وفي Docker: عندما تنتهي عملية بسبب إشارة يكون رمز خروجها 128 مضافاً إليه رقم الإشارة، ورقم `SIGSEGV` هو 11 كما في صفحة [signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html?ref=arabroot.io)، فالمجموع 139\. وبنفس الطريقة يعني الرمز 137 أن العملية انتهت بالإشارة 9 وهي `SIGKILL`، وهذا ما تراه عندما يقتل قاتل العمليات Container تجاوز حد ذاكرته. وعمليتا `sleep` بقيتا على حالهما في الحالة S كأن شيئاً لم يحدث، وهذا هو العزل الذي يعطيه نظام التشغيل لكل عملية.

ولاحظ أن هذا العزل يحمي العمليات من بعضها، ولكنه لا يحمي النظام من النواة نفسها، كما رأينا في حادثة CrowdStrike، ولذلك يعامل كل كود يدخل النواة، مثل التعريفات وبرامج الحماية، بحذر أكبر بكثير من البرامج العادية.

## نظام الملفات File System: كيف تبقى ملفاتك بعد إطفاء الجهاز؟

القرص في نظر العتاد سلسلة طويلة من الكتل المرقمة، كل كتلة بضعة كيلوبايتات، ولا شيء فيه اسمه ملف أو مجلد. ونظام الملفات File System هو الطبقة التي تحول هذه الكتل إلى أسماء ومجلدات وصلاحيات وتواريخ، وفي Linux أشهرها ext4 وXFS وBtrfs. والفكرة المركزية فيها جميعاً أن الملف ليس اسمه، فكل ملف له سجل في نظام الملفات اسمه Inode يحمل كل شيء عنه: المالك والصلاحيات والحجم والتواريخ وأماكن كتل بياناته على القرص، إلا شيئاً واحداً هو الاسم. أما الاسم فيعيش في المجلد، والمجلد نفسه ليس إلا جدولاً صغيراً كل سطر فيه اسم ورقم Inode، كما تشرح صفحة [inode(7)](https://man7.org/linux/man-pages/man7/inode.7.html?ref=arabroot.io).

والقسم السادس من السكربت ينشئ ملفاً اسمه `a.txt`، ثم يعطيه اسماً ثانياً بالأمر `ln`، ثم يحذف الاسم الأول ويحاول قراءة الملف من الاسم الثاني:

```bash
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
```

والمخرج:

```text
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 على القرص مهما كان صغيراً.

![المجلد tmp فيه اسمان: a.txt المحذوف وb.txt، وكلاهما يشير إلى Inode رقم 220982 الذي يحمل المالك والصلاحيات والحجم 13 بايت وعدد الروابط وأوقات التعديل وأماكن الكتل، والـ Inode يشير إلى كتلة على القرص فيها النص invoice 1001](https://arabroot.io/content/images/2026/10/os-basics-05-inode.webp)

الاسم رابط، والـ Inode هو الملف، والمساحة تتحرر عند آخر رابط وآخر برنامج

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

وقد يتساءل البعض: إذا كتب البرنامج في الملف ثم انقطعت الكهرباء، هل وصلت البيانات إلى القرص؟ والإجابة ليس بالضرورة، فالنواة تجمع الكتابات في الذاكرة وترسلها إلى القرص لاحقاً لأن ذلك أسرع بكثير، ولذلك تحتاج قواعد البيانات إلى طلب خاص اسمه `fsync` يجبر النواة على الإرسال الآن. وهذه القصة كلها، مع تجربة نقطع فيها الكهرباء عن نظام ملفات يعمل، موجودة في مقال [نظام الملفات من الداخل: Inodes والسجل وfsync](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/).

### كل شيء ملف Everything is a File

من أفكار Unix التي بقيت حتى اليوم أن النواة تقدم أشياء كثيرة ليست ملفات على شكل ملفات، فتقرأها وتكتبها بنفس الأوامر. فالقرص نفسه ملف في `/dev`، والطرفية ملف، والمكان الذي يبتلع كل ما تكتبه فيه ملف اسمه `/dev/null`، ومعلومات كل عملية تجدها كملفات في المجلد `/proc`، وهو مجلد لا يوجد على أي قرص وإنما تولده النواة لحظة قراءته، كما تشرح صفحة [proc(5)](https://man7.org/linux/man-pages/man5/proc.5.html?ref=arabroot.io). والقسم السابع من السكربت يقرأ حالة العملية الحالية وقائمة ملفاتها المفتوحة:

```bash
head -5 /proc/self/status
ls -l /proc/self/fd
```

والمخرج:

```text
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)](https://man7.org/linux/man-pages/man7/namespaces.7.html?ref=arabroot.io)، وتحد ما تستهلكه من المعالج والذاكرة بميزة ثانية اسمها مجموعات التحكم cgroups:

![إلى اليمين جهازان افتراضيان لكل منهما تطبيق ومكتبات ونواة خاصة فوق مشرف Hypervisor يقسم العتاد، وإلى اليسار ثلاث Containers لكل منها تطبيق ومكتبات من توزيعة مختلفة هي debian وalpine وubuntu، وكلها فوق نواة واحدة مشتركة للسيرفر تعزلها بالـ Namespaces وتحدها بالـ cgroups](https://arabroot.io/content/images/2026/10/os-basics-06-container-kernel.webp)

الـ Container لا يحمل نواة، وإنما مكتبات وبرامج توزيعة فوق نواة السيرفر

وهذا يفسر ثلاثة أشياء ستلاحظها عند العمل مع 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](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/).

## أين ترى كل مفهوم على جهازك؟

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

| المفهوم                      | الأمر                                  | ما تبحث عنه                                 | المقال الأعمق                     |
| ---------------------------- | -------------------------------------- | ------------------------------------------- | --------------------------------- |
| النواة وإصدارها              | 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 التي بنيناها، واحذفها إذا لم تكن ستكمل مع بقية المقالات:

```bash
docker rmi sbl-os:1
```

## الخلاصة

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

- نظام التشغيل يملك العتاد ويقسمه بين البرامج، ويعزلها عن بعضها، ويخفي عنها تفاصيل الأجهزة، وقلبه النواة Kernel، وLinux بالمعنى الدقيق هو النواة فقط.
- البرامج تعمل في فضاء المستخدم ولا تلمس العتاد، وكل ما تطلبه يمر عبر استدعاءات النظام، وحتى طباعة كلمة من Python احتاجت 343 طلباً.
- العملية برنامج يعمل له رقم وذاكرة وملفات وحالة، والخيوط مسارات تنفيذ داخل العملية تشترك في ذاكرتها.
- المجدول يقسم وقت المعالج إلى شرائح، وبرنامجان على نواة واحدة حصل كل منهما على 47.6% فقط.
- كل عملية ترى عناوين افتراضية خاصة بها، فنفس العنوان حمل قيمتين في عمليتين، والذاكرة تعطى عند أول لمس لا عند الحجز.
- العزل يجعل خطأ الذاكرة يوقف برنامجه وحده برمز 139، أما خطأ في النواة فيوقف النظام كله.
- الملف هو الـ Inode والاسم رابط إليه، والمساحة تعود عند آخر رابط وآخر برنامج.
- الـ Container عمليات على نواة السيرفر، والجهاز الافتراضي نواة كاملة خاصة به.

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

[![](https://arabroot.io/content/images/2026/10/os-processes-scheduling-cover-cover.webp)دليلالعمليات Processes وجدولة المعالج CPU Scheduling في Linuxنرى من داخل النواة كيف يختار المجدول من يعمل الآن، ولماذا يبطئ سكربت ليلي واجهتك البرمجية، وكيف يعيدها سطر واحد إلى سرعتها.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A%D8%A7%D8%AA-%D9%88%D8%AC%D8%AF%D9%88%D9%84%D8%A9-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-%D9%81%D9%8A-linux/)[![](https://arabroot.io/content/images/2026/10/os-virtual-memory-cover-cover.webp)دليلالذاكرة الافتراضية Virtual Memory: جداول الصفحات وخطأ الصفحةنتابع العنوان من برنامجك إلى شريحة الذاكرة، ونفهم بأرقام حقيقية لماذا تخدعك أعمدة VIRT وRES، ومتى تسرع الصفحات الكبيرة برنامجك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D8%A7%D9%84%D8%A7%D9%81%D8%AA%D8%B1%D8%A7%D8%B6%D9%8A%D8%A9-virtual-memory-%D9%88%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B5%D9%81%D8%AD%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/kernel-user-syscalls-cover-cover.webp)دليلفضاء النواة والمستخدم: كم يكلف استدعاء النظام System Call؟نعبر الحد بين برنامجك والنواة بأيدينا دون أي مكتبة، ونقيس كلفة كل عبور، ونرى لماذا يأخذ نسخ ملف ببايت واحد في كل طلب عشرات الثواني بدلاً من أقل من ملي ثانية.](https://arabroot.io/articles/%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%86%D9%88%D8%A7%D8%A9-%D9%88%D9%81%D8%B6%D8%A7%D8%A1-%D8%A7%D9%84%D9%85%D8%B3%D8%AA%D8%AE%D8%AF%D9%85-%D9%88%D8%A7%D8%B3%D8%AA%D8%AF%D8%B9%D8%A7%D8%A1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D8%B8%D8%A7%D9%85/)[![](https://arabroot.io/content/images/2026/10/os-filesystems-io-cover-cover.webp)دليلنظام الملفات File System: Inodes والسجل Journal وfsyncنتابع الكتابة من برنامجك حتى القرص داخل ext4، ونقطع الكهرباء عن نظام ملفات يعمل لنرى ماذا يبقى فعلاً، ومتى تحتاج fsync وكم يكلف.](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/)[![](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: كتابة المقال، وتنفيذ التجارب داخل 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.