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، بقيمتين مختلفتين، وده اللي بيخلّي كل واحد يعرف هو مين:
#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;
}$ 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 بتعمل نسخة من نفس البرنامج. طب لو عايز تشغّل برنامج تاني خالص؟
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));read line: "ls -la"فهم الأمر1pid = fork()رجعت 31012- 3
fork() رجعت 0أنا الـ Child - 4
execvp("ls", ["ls", "-la"])بقيت ls waitpid(3101, &status, 0)⏳ مستني5- 6
يطبع الملفات… exit(0)خلص waitpid رجعت · status = 0$ 7
دوس ▶ شغّل، أو امشي خطوة خطوة بالأسهم.
3. ليه مش System Call واحد “شغّل البرنامج ده”؟
Windows عنده CreateProcess بيعمل كل حاجة مرة واحدة. ليه UNIX قسّمها لاتنين؟ لأن الفترة بين fork و exec هي فرصة الـ Child يجهّز البيئة للبرنامج الجديد، والبرنامج الجديد مايعرفش أي حاجة عن ده. ومن هنا جت الـ Redirection والـ Pipes:
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.