بنناجح
المرحلة 2: الـ Processes والـ Scheduling · الدرس 2 من 5 · 30 دقيقة

fork و exec و wait

إزاي Process بتعمل Process تانية؟ في UNIX الإجابة غريبة شوية: انسخ نفسك بـ fork، وبعدين النسخة تبدّل نفسها ببرنامج تاني بـ exec، والأصل يستنى بـ wait. الدرس ده بيشرح التلاتة بأمثلة تشغّلها، وليه التصميم ده عبقري، وإزاي الـ Shell بيشغّل أي أمر بيهم، وإيه هي الـ Zombies والـ Signals.

في الدرس ده هتتعلم
  • تفهم fork وليه بترجع مرتين
  • تفهم exec وإنها بتبدّل البرنامج من غير ما تعمل Process جديدة
  • تستخدم wait وتعرف الـ Zombie و الـ Orphan
  • تفهم إزاي الـ Shell بيشغّل الأوامر والـ Redirection بالتلاتة دول
  • تبعت Signals وتفهم الفرق بين SIGTERM و SIGKILL

1. fork: انسخ نفسك

الحاجة الغريبة إن fork() بترجع مرتين: مرة في الـ Parent ومرة في الـ Child، بقيمتين مختلفتين، وده اللي بيخلّي كل واحد يعرف هو مين:

spawn.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    int jobs = 3;
    printf("before fork: pid=%d\n", getpid());

    pid_t pid = fork();                  // من هنا فيه Processين

    if (pid < 0) {
        perror("fork");                  // فشلت (مثلاً وصلت لحد عدد الـ Processes)
        return 1;
    } else if (pid == 0) {
        jobs += 10;                      // ده بيغيّر نسخة الـ Child بس
        printf("child:  pid=%d parent=%d jobs=%d\n", getpid(), getppid(), jobs);
    } else {
        waitpid(pid, NULL, 0);           // استنى الـ Child يخلص
        printf("parent: pid=%d child=%d jobs=%d\n", getpid(), pid, jobs);
    }
    return 0;
}
Terminal
$ gcc spawn.c -o spawn && ./spawn
before fork: pid=5001
child:  pid=5002 parent=5001 jobs=13
parent: pid=5001 child=5002 jobs=3
الـ Parent الـ Child
fork() بترجّع الـ PID بتاع الـ Child (رقم أكبر من صفر) 0
الذاكرة نسختها نسخة منفصلة: تعديل jobs في الـ Child ماأثرش على الـ Parent
الملفات المفتوحة بتاعتها نسخة من نفس الـ File Descriptors (بيشاوروا على نفس الملفات)

ومن غير waitpid، مين يطبع الأول؟ مش معروف. الاتنين Processes مستقلين، والـ Scheduler هو اللي يقرر. ده Non-determinism، وهو نفس أصل مشاكل الـ Concurrency.

fork بتعمل نسخة من الـ Process، وبترجع 0 في النسخة والـ PID في الأصل. والاتنين بيكمّلوا من نفس السطر، كل واحد بذاكرة منفصلة.

2. exec: بدّل البرنامج

الـ fork بتعمل نسخة من نفس البرنامج. طب لو عايز تشغّل برنامج تاني خالص؟

run_ls.c
pid_t pid = fork();
if (pid == 0) {
    char *argv[] = { "ls", "-la", "/var/www", NULL };
    execvp("ls", argv);                  // الـ Child بقى ls
    perror("execvp");                    // السطر ده بيتنفذ بس لو exec فشلت
    _exit(127);
}
int status;
waitpid(pid, &status, 0);
printf("ls exited with %d\n", WEXITSTATUS(status));
الـ Shell بيشغّل أمرجرّب بنفسك
في الداتابيزPID 3100bashPID 3101—
bash (Parent) · PID 3100⏱Child · PID 3101
  1. read line: "ls -la"فهم الأمر1
  2. pid = fork()رجعت 31012
  3. 3fork() رجعت 0أنا الـ Child
  4. 4execvp("ls", ["ls", "-la"])بقيت ls
  5. waitpid(3101, &status, 0)⏳ مستني5
  6. 6يطبع الملفات… exit(0)خلص
  7. waitpid رجعت · status = 0$ 7
▶

دوس ▶ شغّل، أو امشي خطوة خطوة بالأسهم.

3. ليه مش System Call واحد “شغّل البرنامج ده”؟

Windows عنده CreateProcess بيعمل كل حاجة مرة واحدة. ليه UNIX قسّمها لاتنين؟ لأن الفترة بين fork و exec هي فرصة الـ Child يجهّز البيئة للبرنامج الجديد، والبرنامج الجديد مايعرفش أي حاجة عن ده. ومن هنا جت الـ Redirection والـ Pipes:

ls > files.txt (اللي الـ Shell بيعمله)
pid_t pid = fork();
if (pid == 0) {
    close(STDOUT_FILENO);                                     // اقفل fd 1 (الشاشة)
    open("files.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);    // أصغر fd فاضي = 1، فالملف بقى هو الـ stdout
    execlp("ls", "ls", NULL);                                 // ls بيكتب على fd 1 زي العادة… وده بقى الملف
}
waitpid(pid, NULL, 0);

ls نفسه مايعرفش إنه بيكتب في ملف. هو بيكتب على fd 1 زي ما بيعمل دايماً. والـ Pipe (ls | grep php) نفس الفكرة: الـ Shell بيعمل pipe()، وبيعمل fork مرتين، ويوصّل الـ stdout بتاع الأول بالـ stdin بتاع التاني قبل الـ exec.

الفصل بين fork و exec بيدّي فرصة تغيّر بيئة البرنامج الجديد (ملفاته، وفولدره، وصلاحياته، ومتغيراته) من غير ما البرنامج نفسه يعرف. ده أساس الـ Redirection والـ Pipes.

4. wait والـ Zombies والـ Orphans

إيه اللي حصل النتيجة
Zombie الـ Child خلص، والـ Parent لسه ماعملش wait سجل صغير فاضل في جدول الـ Processes (حالة Z في ps). واحد مش مشكلة، بس آلاف بيملوا جدول الـ PIDs
Orphan الـ Parent خلص قبل الـ Child الـ Child بيتبنّاه init (PID 1، غالباً systemd)، وهو بيعمل wait عليه لما يخلص

لو شفت Zombies بتتراكم، المشكلة في الـ Parent: بيعمل Children ومش بيعمل wait. وده سبب مشهور في Docker: لو البرنامج بتاعك هو PID 1 جوه الـ Container ومش بيعمل wait على الـ Children، الـ Zombies بتتراكم. والحل docker run --init (بيحط init صغير زي tini كـ PID 1).

5. الـ Signals: تكلّم Process من بره

الـ Signal بيتبعت إمتى الافتراضي
SIGINT (2) Ctrl+C الـ Process تخرج
SIGTERM (15) kill <pid>، و docker stop، و systemd تخرج، والبرنامج يقدر يمسكه ويقفل بأدب
SIGKILL (9) kill -9، و الـ OOM Killer تموت فوراً، من غير ما تقدر تعمل أي حاجة
SIGHUP (1) الـ Terminal اتقفل، أو طلب إعادة تحميل الإعدادات كتير من السيرفرات بتعيد تحميل الإعدادات
SIGCHLD (17) Child خلص بيتجاهل افتراضياً، بس هو اللي بيصحّي الـ Parent يعمل wait

6. fork في الحياة الحقيقية

  • php-fpm: Master Process بتعمل fork لـ Workers، وكل Request بيتخدم في Worker، والـ Master بيراقبهم ويعمل fork جديد لو واحد مات.
  • Nginx و PostgreSQL: نفس الفكرة: Master + Workers (PostgreSQL بيعمل Process لكل اتصال).
  • Laravel Horizon: بيشغّل Worker Processes ويراقبها ويزوّد أو يقلّل عددها حسب الضغط.
  • pcntl_fork() في PHP: نفس الـ fork بالظبط، متاح في CLI بس.

7. أسئلة

فكّر في إجابتك الأول، وبعدين افتح السؤال.

01fork() بترجّع إيه؟ وليه بترجع مرتين؟

بتعمل نسخة من الـ Process، فبعدها فيه Processين بيكمّلوا من نفس السطر. في الـ Parent بترجّع الـ PID بتاع الـ Child، وفي الـ Child بترجّع 0، وفي حالة الفشل بترجّع -1. وكده كل واحد بيعرف هو مين.

02بعد fork، لو الـ Child غيّر متغير، الـ Parent بيشوف التغيير؟

لأ. كل واحد ليه نسخة منفصلة من الذاكرة. Linux بيستخدم Copy-on-Write: الصفحات بتبقى مشتركة لحد ما حد يكتب فيها، وساعتها بتتنسخ. أما الملفات المفتوحة فبيشتركوا في نفس الـ File Descriptions.

03exec بتعمل Process جديدة؟

لأ. بتبدّل البرنامج اللي شغال في نفس الـ Process ببرنامج تاني: نفس الـ PID والملفات المفتوحة، بس كود وذاكرة جداد. ولو نجحت مابترجعش أبداً.

04إزاي الـ Shell بيعمل ls > out.txt؟

بيعمل fork، وفي الـ Child قبل الـ exec بيقفل fd 1 ويفتح out.txt (فياخد رقم 1)، وبعدين exec لـ ls. ls بيكتب على fd 1 زي العادة، وده بقى الملف. والـ Parent بيعمل wait.

05يعني إيه Zombie Process؟ وإزاي تتخلص منها؟

Process خلصت والـ Parent لسه ماعملش wait يقرا كود خروجها، ففاضل منها سجل في جدول الـ Processes. الحل إن الـ Parent يعمل wait، أو يتقتل فالـ Zombies يتبناهم init ويعمل wait عليهم. وفي Docker: docker run --init.

06الفرق بين SIGTERM و SIGKILL؟

SIGTERM طلب إن الـ Process تخرج، والبرنامج يقدر يمسكه ويخلّص شغله ويقفل بأدب (Graceful Shutdown). SIGKILL بيقتل الـ Process فوراً ومحدش يقدر يمسكه. الصح تبعت SIGTERM الأول، ولو مخرجتش بعد مهلة، SIGKILL.

علّم عليه لما تقدر تجاوب أسئلة الإنترفيو من غير ما تبص.