Programming Languages

ما يعلمك إياه بناء صدفة عن يونكس

كل مطور يستخدم الصدفة (shell) يومياً. لكن قلة منهم يفهمون ما تفعله حقاً. تبدو الصدفة كتطبيق عادي — تكتب أوامر فتنفذها — لكنها في الواقع طبقة رقيقة فوق مجموعة من بدائيات يونكس الأساسية التي تشكل جوهر عمل أنظمة التشغيل. بناء صدفة من الصفر هو أحد أفضل الطرق لفهم العمليات (processes)، وواصفات الملفات (file descriptors)، والأنابيب (pipes)، والإشارات (signals) — مفاهيم تقوم عليها كل شيء من خوادم الويب إلى حاويات Docker.

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

حلقة القراءة-التنفيذ-الطباعة

في جوهرها، الصدفة هي حلقة REPL. اقرأ المدخلات، قيّمها (نفّذها)، اطبع النتائج، كرّر. جزء 'الطباعة' تتولاه الأوامر نفسها — الصدفة فقط توفر البيئة لتشغيلها.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
// The simplest possible shell
int main(void) {
char line[1024];
while (1) {
printf("$ ");
if (!fgets(line, sizeof(line), stdin))
break;  // EOF (Ctrl+D)
// Remove trailing newline
line[strcspn(line, "\n")] = '\0';
// Fork a child process
pid_t pid = fork();
if (pid == 0) {
// Child: execute the command
execlp(line, line, NULL);  // This only handles single-word commands
perror("exec");
exit(1);
}
// Parent: wait for child to finish
waitpid(pid, NULL, 0);
}
return 0;
}

هذه الصدفة المكونة من 25 سطراً تعمل فعلاً — يمكنها تشغيل أوامر مثل ls وpwd وdate. لا تتعامل مع المعاملات أو الأنابيب أو إعادة التوجيه أو أي ميزة أخرى تتوقعها. لكنها توضح النمط الأساسي: fork، exec، wait.

Fork وExec: نموذج العمليات في يونكس

الفصل بين fork وexec هو أكثر قرارات تصميم يونكس تميزاً، وبناء صدفة يجعلك تفهم لماذا هو موجود.

fork() تنشئ نسخة طبق الأصل من العملية الحالية. العملية الابن لها نفس الذاكرة، ونفس الملفات المفتوحة، ونفس متغيرات البيئة. exec() تستبدل برنامج العملية الابن ببرنامج جديد. هاتان عمليتان منفصلتان لأن الفجوة بينهما — بعد fork وقبل exec — هي المكان الذي تُعدّ فيه الصدفة بيئة العملية الابن.

هذه هي الفكرة الجوهرية. عندما تكتب ls > output.txt، تقوم الصدفة بعمل fork، ثم في العملية الابن (قبل exec)، تفتح output.txt وتعيد توجيه stdout إليه، ثم تنفذ exec لـls. برنامج ls لا يعرف شيئاً عن إعادة التوجيه — يكتب إلى stdout كالمعتاد، والتلاعب بواصفات الملفات الذي حدث بين fork وexec يوجّه ذلك الخرج إلى ملف.

// How 'ls > output.txt' works
pid_t pid = fork();
if (pid == 0) {
// Child process — between fork and exec
// Open the output file
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
// Redirect stdout (fd 1) to the file
dup2(fd, STDOUT_FILENO);  // Now fd 1 points to output.txt
close(fd);                 // Close the original fd (no longer needed)
// exec ls — it writes to stdout, which now goes to output.txt
execlp("ls", "ls", NULL);
perror("exec");
exit(1);
}
waitpid(pid, NULL, 0);

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

الأنابيب: ربط العمليات ببعضها

الأنابيب هي أقوى آلية تركيب في يونكس، وتنفيذها يكشف مدى بساطة الآلية الكامنة وراءها.

استدعاء النظام pipe() ينشئ زوجاً من واصفات الملفات: واحد للقراءة وواحد للكتابة. البيانات المكتوبة في طرف الكتابة تظهر في طرف القراءة. لتنفيذ ls | grep foo، تنشئ الصدفة أنبوباً، تقوم بعمل fork مرتين، وتربط stdout الخاص بـls بطرف الكتابة وstdin الخاص بـgrep بطرف القراءة.

// How 'ls | grep foo' works
int pipefd[2];
pipe(pipefd);  // pipefd[0] = read end, pipefd[1] = write end
pid_t pid1 = fork();
if (pid1 == 0) {
// First child: ls
close(pipefd[0]);              // Don't need read end
dup2(pipefd[1], STDOUT_FILENO); // stdout → pipe write end
close(pipefd[1]);
execlp("ls", "ls", NULL);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Second child: grep
close(pipefd[1]);              // Don't need write end
dup2(pipefd[0], STDIN_FILENO);  // stdin → pipe read end
close(pipefd[0]);
execlp("grep", "grep", "foo", NULL);
exit(1);
}
// Parent: close both pipe ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

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

الأوامر المدمجة

بعض الأوامر لا يمكن أن تكون برامج خارجية. cd هو المثال الكلاسيكي. إذا أنشأت الصدفة عملية ابن واستدعت العملية الابن chdir()، فإن مجلد العمل الخاص بالعملية الابن فقط هو الذي يتغير — مجلد العملية الأب لا يتأثر، وبعد انتهاء العملية الابن، تبقى الصدفة في نفس المجلد. لكي يعمل cd، يجب أن تنفذه الصدفة في عمليتها الخاصة دون عمل fork.

من الأوامر المدمجة الأخرى export (تعديل بيئة الصدفة)، وexit (إنهاء عملية الصدفة)، وsource (تنفيذ سكريبت في سياق الصدفة الحالية). جميعها تعدّل حالة الصدفة نفسها، وهذا لا يمكن أن يحدث إلا في عملية الصدفة ذاتها.

فهم أي الأوامر مدمجة ولماذا يعلّمك شيئاً جوهرياً عن عزل العمليات. العملية الابن لا تستطيع تعديل العملية الأب. هذه ميزة أمنية، وميزة موثوقية، وأحياناً مصدر إزعاج — لكنها جوهر طريقة عمل عمليات يونكس.

الإشارات والتحكم بالمهام

اضغط Ctrl+C في الطرفية وسيتوقف الأمر الجاري. يبدو هذا بسيطاً لكنه يتضمن تفاعلاً معقداً بشكل مفاجئ بين الطرفية والصدفة والإشارات ومجموعات العمليات.

Ctrl+C يرسل SIGINT إلى مجموعة العمليات الأمامية. مشغّل الطرفية هو الذي يتعامل مع هذا — وليس الصدفة. مهمة الصدفة هي وضع كل أمر في مجموعة عمليات خاصة به حتى يذهب SIGINT إلى الأمر وليس إلى الصدفة. إذا لم تُعدّ الصدفة مجموعات العمليات بشكل صحيح، فإن Ctrl+C يقتل الصدفة بدلاً من الأمر الجاري.

التحكم بالمهام — العمليات الخلفية (&)، وfg، وbg، وCtrl+Z — يضيف طبقة أخرى. تحتاج الصدفة لتتبع أي العمليات تنتمي لأي مهمة، وإدارة المجموعات الأمامية مقابل الخلفية، ومعالجة إشارات مثل SIGTSTP (إيقاف مؤقت عبر Ctrl+Z) وSIGCHLD (انتهاء عملية ابن). تنفيذ هذا بشكل صحيح هو الجزء الأصعب في بناء صدفة.

ما الذي ستتعلمه

بناء صدفة يعلّمك مفاهيم تظهر باستمرار في تطوير البرمجيات، حتى لو لم تكتب كود أنظمة مرة أخرى.

  • واصفات الملفات هي الواجهة الشاملة. الملفات، الأنابيب، المقابس (sockets)، الطرفيات — كلها واصفات ملفات. إعادة التوجيه والأنابيب والاتصال الشبكي كلها تستخدم نفس الآلية الأساسية. فهم هذا يجعل كل شيء من شبكات Docker إلى Unix domain sockets إلى واجهات نظام لينكس البرمجية أوضح.
  • عزل العمليات أمر جوهري. العملية الابن لا تستطيع تعديل العملية الأب. متغيرات البيئة ومجلد العمل والملفات المفتوحة هي نسخ موروثة وليست مراجع مشتركة. لهذا السبب لا يؤثر cd في صدفة فرعية على العملية الأب، ولهذا ترث حاويات Docker بيئة المضيف لكنها لا تتشاركها.
  • التركيب يتفوق على الميزات. لا يوجد في يونكس أمر واحد لـ'إيجاد الملفات المطابقة لنمط وعدّها'. بل يوجد find وgrep وwc، متصلة بالأنابيب. آلية الأنابيب في الصدفة تجعل الأدوات البسيطة قابلة للتركيب في سير عمل معقدة. فلسفة التصميم هذه — أدوات صغيرة متصلة بواجهات معيارية — هي الجد الفكري للخدمات المصغّرة ومقابس يونكس وتصميم الـAPI.
  • معالجة الأخطاء تدور في معظمها حول واصفات الملفات. عندما ينكسر أنبوب، أو تنهار عملية ابن، أو ينفد stdin — الآلية الأساسية دائماً هي إغلاق واصفات الملفات، أو تسليم الإشارات، أو خروج العمليات برموز حالة. بمجرد فهمك لنموذج واصفات الملفات، تصبح أنماط معالجة الأخطاء عبر أنظمة يونكس بديهية.

من أين تبدأ

إذا أردت بناء صدفة، ابدأ بالنسخة المكونة من 25 سطراً أعلاه وأضف الميزات تدريجياً. أولاً: تحليل المعاملات (قسّم المدخلات على المسافات). ثم: إعادة توجيه الإدخال/الإخراج (> و<). ثم: الأنابيب. ثم: الأوامر المدمجة (cd، exit). ثم: متغيرات البيئة. كل ميزة تعلّمك مفهوماً جديداً في يونكس، وكل واحدة تمرين مستقل بذاته.

استخدم لغة C للحصول على أقرب تطابق مع استدعاءات النظام. يمكنك بناء صدفة بلغة Python أو Rust، لكن التنفيذ بلغة C يجعل استدعاءات النظام الأساسية صريحة — ترى بالضبط ما تفعله fork() وexec() وdup2() وpipe() لأنك تستدعيها مباشرة.

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