الأدلة التقنية

سطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفر

كيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.

سطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفر

عندما تدخل إلى سيرفرك لأول مرة عبر SSH كما شرحنا في دليل تأمين السيرفر من أول دخول، لا تجد أمامك نوافذ ولا أيقونات، وإنما سطراً واحداً ينتهي بعلامة $ ومؤشراً ينتظر أن تكتب شيئاً، وكل ما سوف تفعله بعد ذلك، من تثبيت Docker إلى قراءة السجلات وإعادة تشغيل خدمة متوقفة، يمر عبر هذا السطر. لذلك فإن أول مرحلة في مسار أساسيات Linux هي أن تعرف أين أنت في نظام الملفات، وكيف تنشئ الملفات وتنسخها وتحذفها دون أن تخسر شيئاً، وكيف ترى البرامج التي تعمل الآن وتوقف ما علق منها، وكيف تعرف أن الأمر نجح أو فشل.

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

  • ما هي الطرفية Terminal والصدفة Shell، وكيف تقرأ أي أمر: الأمر والخيارات Options والمعاملات Arguments.
  • شجرة نظام الملفات Filesystem في Linux، والمسار المطلق والمسار النسبي، والتنقل بين المجلدات.
  • إنشاء الملفات والمجلدات ونسخها ونقلها وحذفها وقراءتها والبحث عنها.
  • الأنماط Globbing مثل * و?، ولماذا تفسدها المسافة في اسم الملف.
  • القنوات الثلاث stdin وstdout وstderr، والتوجيه Redirection، والأنابيب Pipes.
  • العمليات Processes: كيف تولد وتعيش وتنتهي، وكيف تراها بالأمرين ps وtop، وكيف ترسل لها الإشارات Signals بالأمر kill، والمهام Jobs في الخلفية.
  • رمز الخروج Exit Code، وكيف تبني عليه سلسلة أوامر لا تكمل بعد الفشل.

وهذا المقال هو الأساس الذي تبنى عليه بقية عناصر المرحلة، فتحرير الملفات بالمحررين nano وvim، ومعالجة النصوص والسجلات بالأوامر grep وawk وsed، والمستخدمين والمجموعات وsudo، والخدمات مع systemd، كلها عناصر مستقلة في مرحلة أساسيات Linux، ولن نكررها هنا. أما الصلاحيات Permissions التي تظهر في كل مخرج للأمر ls -l فقد خصصنا لها مقالاً مستقلاً يأتي بعد هذا المقال مباشرة.

من الذي يقرأ ما تكتبه؟ الطرفية Terminal والصدفة Shell

الطرفية Terminal هي النافذة التي تكتب فيها وتقرأ منها، سواءً كانت برنامجاً على جهازك أو جلسة SSH إلى سيرفر بعيد، أما الذي يقرأ السطر الذي تكتبه ويفهمه ويشغل البرامج المطلوبة فهو برنامج آخر اسمه الصدفة Shell، والصدفة الافتراضية على Ubuntu وDebian هي Bash. والتشبيه الأقرب أن الطرفية هي سماعة الهاتف، والصدفة هي الموظف على الطرف الآخر الذي يسمع طلبك وينفذه، ولذلك فإن أشياء كثيرة تظن أنها من الأمر نفسه، مثل توسيع النجمة * إلى أسماء الملفات والتوجيه إلى ملف بالعلامة >، هي في الحقيقة من عمل الصدفة قبل أن يبدأ الأمر، وسوف نرى أثر ذلك عدة مرات في هذا المقال.

والسطر الذي ينتظرك يسمى المحث Prompt، وشكله عادة sara@lab:~$، أي اسم المستخدم ثم اسم السيرفر ثم المجلد الحالي، والرمز ~ يعني مجلدك الرئيسي Home Directory. ولاحظ العلامة في آخره: $ تعني أنك مستخدم عادي، و# تعني أنك تعمل بحساب root الذي يملك كل شيء على السيرفر، فإذا رأيت # فتمهل قبل كل أمر حذف.

وكل أمر تكتبه يتكون من ثلاثة أجزاء يفصل بينها مسافة: اسم الأمر، ثم الخيارات Options التي تغير سلوكه وتبدأ عادة بشرطة واحدة مثل -l أو بشرطتين مثل --all، ثم المعاملات Arguments وهي غالباً أسماء الملفات أو المجلدات التي يعمل عليها. ففي الأمر ls -l /etc اسم الأمر ls والخيار -l والمعامل /etc. وإذا نسيت خيارات أي أمر فاكتب بعده --help، أو اقرأ صفحة الدليل Manual بالأمر man ls على سيرفرك.

تجهيز المختبر Lab

سوف ننفذ كل أمر في هذا المقال داخل Container، حتى ترى المخرجات حقيقية وتستطيع أن تخطئ دون أن تخسر شيئاً. واستخدمنا Docker Image موجودة عندنا مسبقاً هي postgres:18، ولا نحتاج منها قاعدة البيانات وإنما النظام الذي بنيت عليه، وهو Debian 13 مع Bash 5.2 وأدوات GNU coreutils 9.7 وحزمة procps-ng 4.0.4 التي فيها ps وtop، وهي الأدوات نفسها التي تجدها على سيرفر Ubuntu أو Debian. وإذا لم يكن Docker مثبتاً عندك فقد شرحنا تثبيته في دليل تثبيت Docker على Ubuntu، أو نفذ الأوامر مباشرة على سيرفر تجارب.

docker run -d --name lab-linux-cli --hostname lab --entrypoint sleep postgres:18 infinity
docker exec lab-linux-cli useradd -m -s /bin/bash sara
docker exec -it -u sara -w /home/sara lab-linux-cli bash

في الإعداد أعلاه لاحظ التالي:

  • الخيار --entrypoint sleep مع infinity يجعل ال Container يعمل دون أن يبدأ PostgreSQL، فالعملية الوحيدة فيه هي sleep infinity، وسوف نعود إليها عند الحديث عن العمليات.
  • أنشأنا مستخدماً عادياً اسمه sara ودخلنا به، والسبب أنك على السيرفر الحقيقي يجب أن تعمل بمستخدم عادي لا بحساب root، كما شرحنا في إنشاء مستخدم عادي بصلاحية sudo.
  • شغلنا كل مجموعة أوامر في المقال عبر سكربت صغير اسمه run.sh يطبع الأمر بعد العلامة $ ثم ينفذه في الصدفة نفسها، لذلك سوف ترى بعض رسائل Bash في المخرجات مسبوقة ب /usr/local/bin/run.sh: line 7:، وفي طرفيتك سوف تراها تبدأ ب bash:.
📌
إذا دخلت إلى Container مبني على Alpine Linux، مثل ghost:6-alpine أو caddy:2، فسوف تجد أن أوامر مثل ps وtop وfind وstat ليست برامج مستقلة وإنما روابط إلى برنامج واحد اسمه BusyBox، وقد تأكدنا من ذلك على هاتين ال Images عندنا. والأوامر الأساسية فيه هي نفسها، لكن بعض الخيارات المذكورة في هذا المقال غير موجودة فيه، مثل ps -o … --forest، لذلك إذا رفض أمر خياراً معيناً فتأكد أولاً من أي نسخة من الأداة تستخدم.

شجرة نظام الملفات Filesystem

في Windows لكل قرص حرف مثل C:، أما في Linux فكل شيء يبدأ من مجلد واحد اسمه /، ثم تتفرع منه بقية المجلدات، وأي قرص إضافي تضيفه يركب Mount في مجلد داخل هذه الشجرة نفسها. وأسماء المجلدات الرئيسية ليست عشوائية، وإنما يحددها معيار اسمه Filesystem Hierarchy Standard، واختصاراً FHS، وتشرحها صفحة الدليل hier(7)، والصورة التالية تبين أهمها:

شجرة تبدأ بمجلد / وتتفرع منه المجلدات etc وhome وroot وvar وtmp وusr وopt وsrv وproc وdev، وتحت كل منها مثال: ملف sshd_config في etc/ssh، ومجلد sara في home، ومجلدا log وlib/docker في var، والبرامج في usr/bin، ومجلد لكل عملية في proc، والقرص sda كملف في dev، مع تمييز proc وdev بلون مختلف لأن النواة تنشئهما في الذاكرة
كل شيء يبدأ من / ، والمجلدان /proc و/dev تنشئهما النواة في الذاكرة ولا يوجدان على القرص
  • /etc: ملفات الإعداد Configuration لكل البرامج، فإعداد SSH في /etc/ssh/sshd_config وإعداد Docker في /etc/docker/daemon.json، وإذا أردت أن تعرف لماذا يتصرف برنامج بطريقة معينة فابدأ من هنا.
  • /home: مجلدات المستخدمين، لكل مستخدم مجلد باسمه مثل /home/sara، وهو الذي يرمز له ب ~. أما مجلد حساب root فليس هنا وإنما في /root.
  • /var: البيانات التي تتغير أثناء عمل النظام، ومنها السجلات Logs في /var/log، وبيانات Docker كلها في /var/lib/docker، لذلك فهو أول مكان تبحث فيه عندما يمتلئ القرص، كما شرحنا في تحليل أداء السيرفر.
  • /tmp: ملفات مؤقتة يكتب فيها كل المستخدمين، ولا تضع فيه شيئاً تريد الاحتفاظ به.
  • /usr: البرامج والمكتبات التي ثبتها النظام، والأوامر نفسها في /usr/bin. وفي Debian وUbuntu الحديثة أصبح /bin مجرد رابط إلى /usr/bin، وقد تأكدنا من ذلك في Debian 13 وUbuntu 24.04.
  • /opt و/srv: الأول للبرامج الإضافية التي تثبتها خارج مدير الحزم، والثاني للبيانات التي يقدمها السيرفر، وكثير من المسؤولين يضعون فيهما مجلدات مشاريع Docker Compose.
  • /proc و/dev: مجلدان لا يوجدان على القرص، وإنما تنشئهما النواة Kernel في الذاكرة، ففي /proc مجلد لكل عملية تعمل الآن باسم رقمها، كما تشرح صفحة الدليل proc(5)، وفي /dev ملف لكل جهاز، فالقرص الأول مثلاً هو الملف /dev/sda. وهذا ما يقصد بالعبارة المشهورة «كل شيء في Linux ملف».

المسار المطلق Absolute Path والمسار النسبي Relative Path

المسار المطلق Absolute Path يبدأ دائماً ب / ويصف مكان الملف من أعلى الشجرة، مثل /etc/hostname، فيعمل كما هو أينما كنت. أما المسار النسبي Relative Path فلا يبدأ ب /، وإنما يحسب من المجلد الذي أنت فيه الآن، فإذا كنت في /etc فإن hostname تعني /etc/hostname. وهناك ثلاثة رموز سوف تستخدمها كثيراً: النقطة . وتعني المجلد الحالي، والنقطتان .. وتعنيان المجلد الأعلى، والرمز ~ ويعني مجلدك الرئيسي. وفي السكربتات وملفات الإعداد وأوامر cron استخدم المسار المطلق دائماً، والسبب أن السكربت لا يعمل بالضرورة من المجلد الذي تتوقعه.

التنقل بين المجلدات Directories

هناك ثلاثة أوامر للتنقل: pwd يطبع المجلد الحالي، وls يعرض محتوى مجلد، وcd ينقلك إلى مجلد آخر، والمثال التالي يبين ذلك:

pwd
ls /
cd /etc
pwd
ls -l hostname os-release
cd ..
pwd
cd ~
pwd
cd -
cd
ls -la

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

$ pwd
/home/sara
$ ls /
bin   dev                         etc   lib    media  opt   root  sbin  sys  usr
boot  docker-entrypoint-initdb.d  home  lib64  mnt    proc  run   srv   tmp  var
$ cd /etc
$ pwd
/etc
$ ls -l hostname os-release
-rw-r--r-- 1 root root  4 Oct  7 05:14 hostname
lrwxrwxrwx 1 root root 21 Jul 17 17:05 os-release -> ../usr/lib/os-release
$ cd ..
$ pwd
/
$ cd ~
$ pwd
/home/sara
$ cd -
/
$ cd
$ ls -la
total 20
drwx------ 2 sara sara 4096 Oct  7 05:15 .
drwxr-xr-x 1 root root 4096 Oct  7 05:15 ..
-rw-r--r-- 1 sara sara  220 Jul 12 16:58 .bash_logout
-rw-r--r-- 1 sara sara 3526 Jul 12 16:58 .bashrc
-rw-r--r-- 1 sara sara  807 Jul 12 16:58 .profile

في الإعداد أعلاه لاحظ التالي:

  • المجلد docker-entrypoint-initdb.d في أعلى الشجرة خاص ب Docker Image الخاصة ب PostgreSQL، ولن تجده على سيرفرك، وبقية المجلدات هي نفسها التي في الصورة أعلاه.
  • الخيار -l يعرض كل ملف في سطر مع صلاحياته ومالكه وحجمه وتاريخ تعديله، والحرف الأول من السطر يبين النوع: - لملف عادي وd لمجلد وl لرابط رمزي Symbolic Link، ولاحظ أن os-release رابط يشير إلى ملف في /usr/lib. وشرح بقية الحروف في مقال الصلاحيات.
  • الأمر cd - يعيدك إلى المجلد الذي كنت فيه قبل آخر انتقال ويطبع اسمه، وcd وحده يعيدك إلى مجلدك الرئيسي.
  • الخيار -a يعرض الملفات المخفية، وهي التي يبدأ اسمها بنقطة مثل .bashrc، فلا يوجد في Linux خاصية «مخفي» وإنما هي تسمية فقط. ويظهر معها المجلدان . و...

وقبل أن تكتب أي مسار طويل، تعود على مفتاح Tab، فإذا كتبت cd /va ثم ضغطت Tab أكملت الصدفة الاسم إلى /var/، وإذا كان هناك أكثر من احتمال فاضغطه مرتين لترى الاحتمالات. وهذا يوفر الوقت ويمنع أخطاء الكتابة في المسارات، لأن الصدفة لا تكمل إلا اسماً موجوداً فعلاً. ومثله السهم للأعلى الذي يعيد آخر أمر كتبته، والأمر history الذي يعرض الأوامر السابقة كلها.

العمل مع الملفات Files والمجلدات Directories

الآن سوف نقوم بإنشاء مجلد لمشروع صغير، ونضع فيه ملفات، ثم ننسخ ونقل ونحذف، ونقرأ ملفاً ونبحث عن ملفات، والمثال التالي يبين الأوامر بالترتيب:

mkdir -p projects/site/logs
touch projects/site/index.html projects/site/logs/app.log
echo "hello from sara" > notes.txt
cat notes.txt
cp notes.txt projects/notes-copy.txt
mv projects/notes-copy.txt projects/site/README.txt
ls -R projects
cp -r projects/site projects/site-backup
rm projects/site/README.txt
rmdir projects/site-backup
rm -r projects/site-backup
ls projects

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

$ mkdir -p projects/site/logs
$ touch projects/site/index.html projects/site/logs/app.log
$ echo "hello from sara" > notes.txt
$ cat notes.txt
hello from sara
$ cp notes.txt projects/notes-copy.txt
$ mv projects/notes-copy.txt projects/site/README.txt
$ ls -R projects
projects:
site

projects/site:
index.html  logs  README.txt

projects/site/logs:
app.log
$ cp -r projects/site projects/site-backup
$ rm projects/site/README.txt
$ rmdir projects/site-backup
rmdir: failed to remove 'projects/site-backup': Directory not empty
$ rm -r projects/site-backup
$ ls projects
site

في الإعداد أعلاه لاحظ التالي:

  • الخيار -p مع mkdir ينشئ المجلدات الوسيطة إذا لم تكن موجودة، ولا يعطي خطأ إذا كان المجلد موجوداً، لذلك تجده في كل سكربت تثبيت تقريباً.
  • الأمر touch ينشئ ملفاً فارغاً إذا لم يكن موجوداً، وإذا كان موجوداً حدث تاريخ تعديله فقط دون أن يغير محتواه.
  • الأمر mv يستخدم للنقل ولإعادة التسمية معاً، فلا يوجد في Linux أمر منفصل اسمه rename للاستخدام اليومي، وقد نقلنا الملف وغيرنا اسمه في خطوة واحدة.
  • الأمر cp لا ينسخ المجلدات إلا مع الخيار -r، والأمر rmdir يحذف المجلد الفارغ فقط، ولذلك رفض الحذف، أما rm -r فيحذف المجلد وكل ما فيه.
🛑
لا توجد سلة محذوفات Trash في سطر الأوامر، فالملف الذي يحذفه rm يختفي فوراً ولا يمكن استعادته إلا من النسخة الاحتياطية. لذلك اكتب المسار كاملاً قبل rm -r، ونفذ ls على المسار نفسه أولاً لترى ما الذي سيحذف، ولا تضف الخيار -f إلا إذا كنت تعرف لماذا تحتاجه، فهو يمنع rm من أن يسألك ويخفي بعض الأخطاء.

قراءة الملفات ومعرفة تفاصيلها Metadata

الأمر cat يطبع الملف كاملاً، وهو مناسب للملفات القصيرة فقط، أما الملف الطويل فاقرأه بالأمر less الذي يعرضه صفحة صفحة، وتتحرك فيه بالأسهم وتبحث فيه بالعلامة / وتخرج منه بالحرف q. والأمران head وtail يعرضان أول الملف وآخره، والأمر stat يعرض كل ما يعرفه النظام عن الملف، والمثال التالي يبين ذلك:

stat notes.txt
head -n 3 /etc/passwd
tail -n 2 /etc/passwd

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

$ stat notes.txt
  File: notes.txt
  Size: 16              Blocks: 8          IO Block: 4096   regular file
Device: 0,108   Inode: 36218       Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/    sara)   Gid: ( 1000/    sara)
Access: 2026-10-07 05:16:29.203003018 +0000
Modify: 2026-10-07 05:16:29.203003018 +0000
Change: 2026-10-07 05:16:29.203003018 +0000
 Birth: 2026-10-07 05:16:29.203003018 +0000
$ head -n 3 /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
$ tail -n 2 /etc/passwd
postgres:x:999:999::/var/lib/postgresql:/bin/bash
sara:x:1000:1000::/home/sara:/bin/bash

في الإعداد أعلاه لاحظ التالي:

  • حجم الملف 16 بايت، وهي الحروف الخمسة عشر في الجملة مع حرف نهاية السطر الذي أضافه echo.
  • السطر Access: (0644/-rw-r--r--) هو الصلاحيات بالأرقام وبالحروف، وUid وGid هما رقم المالك ورقم مجموعته، وهذه الأرقام هي ما يخزنه النظام فعلاً، أما الأسماء فتأتي من ملف /etc/passwd.
  • كل سطر في /etc/passwd مستخدم واحد، والحقول تفصلها النقطتان، وآخر حقل هو الصدفة التي تعمل عند دخوله، وnologin تعني أن هذا الحساب لا يستطيع الدخول، لأنه حساب لخدمة وليس لإنسان.

والخيار الأهم مع tail على السيرفر هو -f، فالأمر tail -f /var/log/nginx/access.log يبقى مفتوحاً ويطبع كل سطر جديد يضاف إلى السجل لحظة كتابته، وتوقفه بالضغط على Ctrl+C.

البحث عن الملفات ومعرفة المساحة Disk Usage

الأمر find يبحث في الشجرة ابتداءً من مجلد تحدده، بالاسم أو النوع أو الحجم أو التاريخ، كما تشرح صفحة الدليل find(1). والأمر du يحسب ما يشغله مجلد من القرص، والأمر df يعرض المساحة الكلية والمستخدمة في كل قرص مركب، والمثال التالي يبين ذلك:

du -sh /usr/share
df -h /
find /etc -maxdepth 1 -name "*.conf"
find / -xdev -type f -size +20M 2>/dev/null

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

$ du -sh /usr/share
80M     /usr/share
$ df -h /
Filesystem      Size  Used Avail Use% Mounted on
overlay        1007G   56G  901G   6% /
$ find /etc -maxdepth 1 -name "*.conf"
/etc/resolv.conf
/etc/debconf.conf
/etc/libaudit.conf
/etc/ld.so.conf
/etc/xattr.conf
/etc/host.conf
/etc/pam.conf
/etc/nsswitch.conf
/etc/gai.conf
/etc/ucf.conf
/etc/locale.conf
/etc/deluser.conf
/etc/adduser.conf
$ find / -xdev -type f -size +20M 2>/dev/null
/usr/lib/x86_64-linux-gnu/libz3.so.4
/usr/lib/x86_64-linux-gnu/libLLVM.so.19.1
/usr/lib/x86_64-linux-gnu/libicudata.so.76.1

في الإعداد أعلاه لاحظ التالي:

  • الخيار -h يعني أرقاماً يقرؤها الإنسان Human Readable مثل 80M بدل عدد الكتل، و-s مع du يعطي مجموع المجلد في سطر واحد بدل سطر لكل مجلد فرعي.
  • القرص الذي يظهره df داخل ال Container هو قرص النظام الذي يعمل عليه Docker، وعلى سيرفرك سوف ترى القرص الحقيقي مثل /dev/sda1.
  • كتبنا "*.conf" بين علامتي تنصيص، والسبب أننا نريد أن تصل النجمة إلى find كما هي ليبحث بها، ولا نريد أن توسعها الصدفة قبل ذلك، وهذا ما سيتضح في القسم التالي.
  • الخيار -xdev يمنع find من الدخول إلى أقراص أخرى مركبة مثل /proc، والجزء 2>/dev/null يخفي رسائل الخطأ، وسوف نشرحه في قسم التوجيه.

والأمر الأخير هو ما تكتبه عندما يمتلئ القرص وتريد أن تعرف أين ذهبت المساحة، وقد شرحنا الطريقة الكاملة لذلك في قسم القرص من تحليل الأداء.

الأنماط Globbing: النجمة وعلامة الاستفهام

عندما تريد أن تعمل على عدة ملفات دفعة واحدة، مثل كل ملفات السجل، فإنك تكتب نمطاً Pattern بدل الأسماء، والرموز الأساسية ثلاثة كما يشرحها دليل Bash وصفحة glob(7): النجمة * تطابق أي عدد من الحروف، وعلامة الاستفهام ? تطابق حرفاً واحداً، والأقواس المربعة مثل [jf] تطابق حرفاً واحداً من الحروف التي بداخلها. والنقطة المهمة هنا أن الصدفة هي التي توسع النمط إلى قائمة الأسماء المطابقة، ثم تعطي هذه القائمة للأمر، فالأمر ls لا يرى النجمة أبداً. والمثال التالي يبين ذلك:

mkdir reports && cd reports
touch jan.log feb.log mar.log report-2025.csv report-2026.csv "my report.txt"
ls
ls *.log
ls report-202?.csv
ls [jf]*.log
echo *.csv
echo "*.csv"
echo *.pdf
ls *.pdf

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

$ mkdir reports && cd reports
$ touch jan.log feb.log mar.log report-2025.csv report-2026.csv "my report.txt"
$ ls
 feb.log   jan.log   mar.log  'my report.txt'   report-2025.csv   report-2026.csv
$ ls *.log
feb.log  jan.log  mar.log
$ ls report-202?.csv
report-2025.csv  report-2026.csv
$ ls [jf]*.log
feb.log  jan.log
$ echo *.csv
report-2025.csv report-2026.csv
$ echo "*.csv"
*.csv
$ echo *.pdf
*.pdf
$ ls *.pdf
ls: cannot access '*.pdf': No such file or directory

في الإعداد أعلاه لاحظ التالي:

  • الأمر echo *.csv يثبت أن التوسيع من عمل الصدفة، فالأمر echo لا يعرف شيئاً عن الملفات، ومع ذلك طبع أسماءها، لأنه استلم الأسماء جاهزة. وعندما وضعنا النمط بين علامتي تنصيص توقف التوسيع ووصلت النجمة كما هي.
  • عندما لا يطابق النمط أي ملف، مثل *.pdf، فإن Bash يتركه كما هو، فيبحث ls عن ملف اسمه حرفياً *.pdf ولا يجده. وهذا مهم في السكربتات، لأن الحلقة على *.pdf في مجلد فارغ سوف تعمل مرة واحدة على النص *.pdf نفسه.
  • عرض ls الاسم 'my report.txt' بين علامتين ليبين لك أن فيه مسافة.

وهناك توسيع آخر يشبه الأنماط لكنه لا يبحث في الملفات أصلاً، وهو توسيع الأقواس Brace Expansion، فالصدفة تولد منه كل الاحتمالات سواءً كانت موجودة أم لا، كما يشرح دليل Bash، وهو مفيد لإنشاء عدة مجلدات في أمر واحد:

echo backup-{2025,2026}.tar.gz
mkdir -p archive/{2025,2026}/{jan,feb}
ls archive/*

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

$ echo backup-{2025,2026}.tar.gz
backup-2025.tar.gz backup-2026.tar.gz
$ mkdir -p archive/{2025,2026}/{jan,feb}
$ ls archive/*
archive/2025:
feb  jan

archive/2026:
feb  jan

المسافة Space في اسم الملف

الصدفة تفصل بين أجزاء الأمر بالمسافة، لذلك فإن اسم ملف فيه مسافة يصل إلى الأمر كاسمين منفصلين إذا لم تحطه بعلامتي تنصيص، ولنحاول الآن أن نحذف الملف my report.txt:

$ rm my report.txt
rm: cannot remove 'my': No such file or directory
rm: cannot remove 'report.txt': No such file or directory
$ rm "my report.txt"
$ ls
archive  feb.log  jan.log  mar.log  report-2025.csv  report-2026.csv

في المحاولة الأولى بحث rm عن ملفين اسمهما my وreport.txt، ولم يجدهما فلم يحذف شيئاً، لكن تخيل لو كان في المجلد ملف اسمه report.txt، فسوف يحذفه دون أن تقصد. والقاعدة التي تمنع هذا الخطأ أن تضع كل اسم ملف وكل متغير بين علامتي تنصيص مزدوجتين، مثل "$file"، في كل سكربت تكتبه.

القنوات الثلاث Streams والتوجيه Redirection والأنابيب Pipes

كل عملية تبدأ ومعها ثلاث قنوات مفتوحة لها أرقام ثابتة: القناة 0 هي المدخل القياسي stdin ويأتي عادة من لوحة المفاتيح، والقناة 1 هي المخرج القياسي stdout وتذهب عادة إلى الشاشة، والقناة 2 هي مخرج الأخطاء stderr وتذهب أيضاً إلى الشاشة. والفصل بين المخرج والأخطاء ليس تفصيلاً، فهو الذي يسمح لك بأن تحفظ النتيجة في ملف وترى الأخطاء على الشاشة، أو العكس. والصورة التالية تبين القنوات الثلاث، ثم الأنبوب الذي يوصل أمراً بالذي بعده:

في الأعلى أمر sort يستقبل مدخله من لوحة المفاتيح عبر القناة 0 stdin، ويخرج النتيجة عبر القناة 1 stdout إلى الشاشة أو إلى ملف بالعلامة أكبر من، والأخطاء عبر القناة 2 stderr إلى الشاشة أو إلى ملف بالعلامة 2 أكبر من. وفي الأسفل ثلاثة أوامر ls /etc ثم grep conf ثم wc -l يصل بينها أنبوب، فيصبح مخرج كل أمر مدخلاً للذي بعده، والنتيجة الرقم 14، بينما تذهب الأخطاء إلى الشاشة مباشرة
التوجيه يغير وجهة قناة واحدة، والأنبوب يوصل stdout أمر ب stdin الأمر الذي بعده

والتوجيه Redirection، كما يشرحه دليل Bash، يغير وجهة قناة من هذه القنوات: > يكتب stdout في ملف ويستبدل محتواه إن كان موجوداً، و>> يضيف إلى آخره، و2> يكتب stderr في ملف، و2>&1 يرسل stderr إلى حيث يذهب stdout، و< يقرأ stdin من ملف. والملف الخاص /dev/null يقبل أي شيء ويرميه. أما الأنبوب Pipe، أي العلامة |، فيوصل stdout الأمر الذي قبله ب stdin الأمر الذي بعده، كما تشرح صفحة Pipelines، فتبني من أوامر صغيرة أمراً يفعل شيئاً لا يفعله أي منها وحده. والمثال التالي يبين ذلك:

ls /etc | wc -l
ls /etc | grep conf | wc -l
cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
date > out.txt
date >> out.txt
cat out.txt
ls /etc/ssl/private > list.txt
cat list.txt
ls /etc/ssl/private > list.txt 2> errors.txt
cat errors.txt
find / -name "*.conf" 2>/dev/null | wc -l

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

$ ls /etc | wc -l
85
$ ls /etc | grep conf | wc -l
14
$ cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
     16 /usr/sbin/nologin
      3 /bin/bash
      1 /bin/sync
$ date > out.txt
$ date >> out.txt
$ cat out.txt
Wed Oct  7 05:17:18 AM UTC 2026
Wed Oct  7 05:17:18 AM UTC 2026
$ ls /etc/ssl/private > list.txt
ls: cannot open directory '/etc/ssl/private': Permission denied
$ cat list.txt
$ ls /etc/ssl/private > list.txt 2> errors.txt
$ cat errors.txt
ls: cannot open directory '/etc/ssl/private': Permission denied
$ find / -name "*.conf" 2>/dev/null | wc -l
34

في الإعداد أعلاه لاحظ التالي:

  • الأمر wc -l يعد الأسطر، فالسطر الأول يعني أن في /etc خمسة وثمانين اسماً، والسطر الثاني أن أربعة عشر منها فيها الكلمة conf.
  • السطر الثالث أنبوب من أربعة أوامر: cut يأخذ الحقل السابع من كل سطر في /etc/passwd وهو الصدفة، ثم sort يرتبها، ثم uniq -c يعد المتكرر منها، ثم sort -rn يرتب النتيجة تنازلياً بالأرقام، فعرفنا في سطر واحد أن ستة عشر حساباً لا تستطيع الدخول.
  • عندما وجهنا stdout فقط إلى list.txt، بقيت رسالة الخطأ على الشاشة وبقي الملف فارغاً، لأن الخطأ يمر في القناة 2. وعندما أضفنا 2> errors.txt اختفت الرسالة من الشاشة ووجدناها في الملف.
  • في السطر الأخير رمينا رسائل Permission denied إلى /dev/null حتى لا تختلط بالنتيجة، ومر stdout وحده في الأنبوب إلى wc.

وإذا أردت المخرج والأخطاء في ملف واحد، كما في سجل سكربت يعمل من cron، فاكتب > all.txt 2>&1 بهذا الترتيب، والسبب أن 2>&1 تعني «أرسل القناة 2 إلى حيث تشير القناة 1 الآن»، فإذا كتبتها قبل > all.txt فسوف تذهب الأخطاء إلى الشاشة لأن القناة 1 كانت تشير إليها في تلك اللحظة. والمخرج التالي من المختبر يبين أن الخطأ والنتيجة وصلا إلى الملف نفسه:

$ head -n 3 all.txt
ls: cannot access '/nothing': No such file or directory
/etc:
adduser.conf

وهذا مدخل فقط إلى الأنابيب، أما الأدوات التي تجعلها قوية فعلاً في قراءة السجلات، أي grep وawk وsed، فلها عنصر مستقل في مرحلة أساسيات Linux بعنوان «معالجة النصوص والسجلات: grep وawk وsed والأنابيب Pipes».

العمليات Processes: البرنامج وهو يعمل

البرنامج ملف على القرص، مثل /usr/bin/sleep، أما العملية Process فهي نسخة من هذا البرنامج تعمل الآن في الذاكرة، ولها رقم فريد اسمه معرف العملية Process ID، واختصاراً PID، ولها مالك هو المستخدم الذي شغلها، ولها عملية أم Parent ورقمها PPID. ويمكن أن يعمل البرنامج نفسه في عشر عمليات منفصلة في الوقت نفسه، ولكل منها رقمها. وقد شرحنا بالتفصيل ما تفعله النواة لتحميل البرنامج وتشغيله في ماذا يحدث عندما تكتب ./hello، أما هنا فيكفيك أن تعرف دورة حياة العملية كما في الصورة التالية:

الصدفة bash برقم 812 تنفذ fork فتنشئ نسخة منها برقم 913 وأمها 812، ثم exec فتصبح النسخة البرنامج sleep، ثم تتنقل العملية بين ثلاث حالات: Running وSleeping عندما تنتظر القرص أو الشبكة وStopped عند الضغط على Ctrl+Z ثم تعود ب fg أو bg، ثم تنتهي ب exit بنفسها أو بإشارة SIGTERM أو SIGKILL، فتصبح Zombie تحتفظ برمز خروجها فقط حتى تقرأه الأم ب wait فتختفي
كل عملية تولد من عملية أخرى، وتنتهي برمز خروج تقرؤه أمها
  1. عندما تكتب أمراً، تنشئ الصدفة نسخة من نفسها بالاستدعاء fork()، فتصبح النسخة عملية جديدة برقم جديد وأمها هي الصدفة.
  2. تستبدل النسخة نفسها بالبرنامج المطلوب عبر execve()، فيبقى رقمها كما هو ويتغير البرنامج الذي تشغله.
  3. أثناء حياتها تتنقل العملية بين التشغيل Running والانتظار Sleeping، فأغلب العمليات على السيرفر نائمة معظم الوقت تنتظر طلباً من الشبكة أو بيانات من القرص، ويمكن إيقافها مؤقتاً Stopped ثم استئنافها.
  4. عندما تنتهي، بنفسها أو بإشارة، تبقى في جدول العمليات كعملية ميتة Zombie لا تحمل إلا رمز خروجها، حتى تقرأه الأم بالاستدعاء wait()، وعندها تختفي تماماً.

رؤية العمليات بالأمر ps

لنشغل الآن عمليتين في الخلفية بإضافة & في آخر الأمر، حتى يعود إلينا المحث فوراً ولا ننتظر انتهاءهما، ثم نعرضهما بعدة طرق:

echo $$
sleep 300 &
sleep 600 &
jobs -l
ps
ps -o pid,ppid,user,stat,etime,cmd --forest

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

$ echo $$
133
$ sleep 300 &
$ sleep 600 &
$ jobs -l
[1]-   139 Running                 sleep 300 &
[2]+   140 Running                 sleep 600 &
$ ps
  PID TTY          TIME CMD
  133 pts/0    00:00:00 bash
  139 pts/0    00:00:00 sleep
  140 pts/0    00:00:00 sleep
  141 pts/0    00:00:00 ps
$ ps -o pid,ppid,user,stat,etime,cmd --forest
  PID  PPID USER     STAT     ELAPSED CMD
  133     0 sara     Ss         00:00 bash -c set -m; source /usr/local/bin/run.sh /tmp/proc1.tx
  139   133 sara     S          00:00  \_ sleep 300
  140   133 sara     S          00:00  \_ sleep 600
  142   133 sara     R+         00:00  \_ ps -o pid,ppid,user,stat,etime,cmd --forest

في الإعداد أعلاه لاحظ التالي:

  • المتغير الخاص $$ هو رقم الصدفة نفسها، وهو 133، وكل العمليات التي شغلناها أمها 133 كما يظهر في عمود PPID، والخيار --forest يرسم هذه العلاقة كشجرة.
  • الأمر jobs يعرض المهام Jobs التي شغلتها هذه الصدفة في الخلفية، ولكل مهمة رقم صغير بين قوسين مربعين مثل [1] تستخدمه في هذه الصدفة فقط، ورقم PID حقيقي يعرفه النظام كله.
  • عمود STAT يبين حالة العملية كما في الصورة: R تعمل، وS نائمة تنتظر، وT موقوفة، وZ ميتة تنتظر أمها، وD انتظار لا تقطعه الإشارات وغالباً يكون انتظاراً للقرص، والعلامة + تعني أنها في المقدمة، وs أنها قائدة الجلسة. والقائمة الكاملة في صفحة الدليل ps(1).
  • السطر الطويل للصدفة هو طريقة تشغيلنا للأوامر في المختبر، وعلى سيرفرك سوف ترى -bash فقط.

وعلى السيرفر الحقيقي سوف تستخدم غالباً ps aux لرؤية كل العمليات لكل المستخدمين، أو ps -ef --forest لرؤية الشجرة كاملة، وتبحث عن عملية بالاسم بالأمر pgrep -a nginx.

من يستهلك المعالج والذاكرة الآن؟ الأمر top

الأمر ps صورة ثابتة للحظة واحدة، أما top فيعرض العمليات ويحدثها كل بضع ثوان، مرتبة بحسب استهلاك المعالج، كما تشرح صفحة الدليل top(1). وفي المختبر شغلناه بالخيارين -b -n 1 حتى يطبع مرة واحدة ويخرج:

$ top -b -n 1 | head -n 12
top - 05:18:51 up 6 min,  0 users,  load average: 0.60, 1.36, 0.84
Tasks:   6 total,   1 running,   5 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.0 us,  0.0 sy,  0.0 ni, 96.9 id,  2.1 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  32036.0 total,  27176.5 free,   2414.5 used,   2921.4 buff/cache
MiB Swap:   8192.0 total,   8192.0 free,      0.0 used.  29621.5 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0    5572   1024    932 S   0.0   0.0   0:00.14 sleep
  155 sara      20   0    7076   3580   3288 S   0.0   0.0   0:00.09 bash
  171 sara      20   0    7076   3584   3324 S   0.0   0.0   0:00.00 worker.sh
  176 sara      20   0    5572   1096   1008 S   0.0   0.0   0:00.00 sleep
  178 sara      20   0   10404   5212   3160 R   0.0   0.0   0:00.00 top

الأسطر الخمسة الأولى ملخص السيرفر كله: متوسط الحمل Load Average، وعدد العمليات في كل حالة، وتوزيع وقت المعالج، والذاكرة وSwap، ثم جدول العمليات. وعند تشغيله في الطرفية تخرج منه بالحرف q، وترتبه بحسب الذاكرة بالحرف M. ولاحظ أن الذاكرة والمعالج هنا للسيرفر كله وليس لل Container، وقد شرحنا السبب وقراءة كل رقم من هذه الأرقام في قسم top من تحليل الأداء.

الإشارات Signals وإيقاف العمليات بالأمر kill

الإشارة Signal رسالة قصيرة ترسلها النواة أو عملية أخرى إلى عملية، ولكل إشارة رقم واسم، والقائمة الكاملة وسلوك كل إشارة في صفحة الدليل signal(7). والاسم kill مضلل قليلاً، فالأمر لا يقتل بالضرورة، وإنما يرسل إشارة، والإشارة الافتراضية هي SIGTERM. وهذه الإشارات التي سوف تستخدمها:

  • SIGTERM (15): طلب مهذب بالتوقف، يستطيع البرنامج أن يلتقطه فيحفظ عمله ويغلق الاتصالات ويحذف ملفاته المؤقتة ثم يخرج، وهي ما يرسله kill PID دون خيارات، وهي أيضاً ما يرسله docker stop أولاً إلى ال Container.
  • SIGKILL (9): النواة تنهي العملية فوراً، ولا يستطيع البرنامج أن يلتقطها أو يتجاهلها، فلا يحصل على أي فرصة للتنظيف، ويرسلها Docker إذا لم يتوقف ال Container بعد المهلة.
  • SIGINT (2): ما ترسله الطرفية عندما تضغط Ctrl+C.
  • SIGTSTP (20) وSIGCONT (18): الأولى ترسلها الطرفية عند Ctrl+Z فتوقف العملية مؤقتاً، والثانية تستأنفها، وهي ما يرسله fg وbg. وهناك SIGSTOP (19) التي توقف العملية ولا يمكن التقاطها.
  • SIGHUP (1): ترسل عندما تغلق الطرفية، وبعض البرامج التي تعمل كخدمات تستخدمها لإعادة قراءة ملف الإعداد دون توقف، ومنها Nginx.

وأرقام الإشارات يعرضها الأمر kill -l، وهذه أول ثلاثة أسطر منه:

$ kill -l | head -n 3
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM

الآن سوف نكمل على العمليتين اللتين شغلناهما في الخلفية، فنوقف الأولى مؤقتاً ثم نستأنفها، ثم ننهيها ب SIGTERM، وننهي الثانية ب SIGKILL. ويمكنك أن تكتب رقم المهمة %1 بدل رقم العملية في هذه الصدفة:

$ kill -STOP %1
$ ps -o pid,stat,cmd -p $(pgrep -d, sleep)
  PID STAT CMD
    1 Ss   sleep infinity
  139 T    sleep 300
  140 S    sleep 600
$ kill -CONT %1
$ ps -o pid,stat,cmd -p $(pgrep -d, sleep)
  PID STAT CMD
    1 Ss   sleep infinity
  139 S    sleep 300
  140 S    sleep 600
$ kill %1
$ sleep 0.2; jobs -l
[1]+   139 Terminated              sleep 300
[2]-   140 Running                 sleep 600 &
$ kill -9 %2
$ sleep 0.2; jobs -l
/usr/local/bin/run.sh: line 7:   140 Killed                  sleep 600

لاحظ أن حالة العملية 139 تغيرت إلى T بعد إيقافها ثم عادت إلى S، وأن الصدفة تكتب لك كيف انتهت كل مهمة: Terminated بعد SIGTERM وKilled بعد SIGKILL. ولاحظ أيضاً العملية رقم 1 وهي sleep infinity التي بدأنا بها ال Container، فكل Container له شجرة عمليات مستقلة تبدأ من رقم 1.

لماذا لا تبدأ ب kill -9؟

وقد يتساءل البعض: إذا كانت kill -9 تنهي أي عملية فوراً، فلماذا لا أستخدمها دائماً؟ والإجابة أن البرنامج لا يعرف أنه سوف ينتهي، فلا يحفظ ما في ذاكرته ولا يغلق ملفاته ولا يحذف ملفات القفل Lock Files التي أنشأها، وقد تبقى بعدها ملفات نصف مكتوبة. ولنرى ذلك بأنفسنا، كتبنا سكربتاً صغيراً اسمه worker.sh ينشئ ملف قفل عند بدايته، ويلتقط SIGTERM بالأمر trap ليحذفه قبل أن يخرج:

#!/bin/bash
# a small worker that cleans up when asked to stop
cleanup() {
    echo "worker: got SIGTERM, saving state and closing"
    rm -f /tmp/worker.lock
    exit 0
}
trap cleanup TERM
touch /tmp/worker.lock
echo "worker: started as PID $$"
while true; do sleep 1; done

ثم شغلناه مرتين، وأنهيناه في الأولى ب SIGTERM وفي الثانية ب SIGKILL، وطبعنا بعد كل مرة رمز خروجه وبحثنا عن ملف القفل. والمتغير $! هو رقم آخر عملية شغلناها في الخلفية، وwait ينتظر انتهاءها:

$ ./worker.sh & pid=$!
$ sleep 2; kill $pid; wait $pid; echo "exit code: $?"
worker: started as PID 208
worker: got SIGTERM, saving state and closing
exit code: 0
$ ls /tmp/worker.lock
ls: cannot access '/tmp/worker.lock': No such file or directory
$ ./worker.sh & pid=$!
$ sleep 2; kill -9 $pid; wait $pid; echo "exit code: $?"
worker: started as PID 216
/usr/local/bin/run.sh: line 7:   216 Killed                  ./worker.sh
exit code: 137
$ ls /tmp/worker.lock
/tmp/worker.lock

في المرة الأولى نظف السكربت وراءه وخرج برمز 0، وفي الثانية لم يطبع شيئاً وبقي ملف القفل، ولو كان هذا برنامجاً حقيقياً يرفض العمل إذا وجد ملف القفل، فسوف يرفض أن يبدأ من جديد حتى تحذفه بيدك. لذلك فالترتيب الصحيح هو أن ترسل kill PID أولاً، وتنتظر بضع ثوان، ثم تتأكد بالأمر ps -p PID، ولا تستخدم kill -9 إلا إذا بقيت العملية بعد ذلك. والأفضل من الاثنين على السيرفر أن توقف الخدمة بالطريقة التي صممت لها، أي systemctl stop أو docker stop، وسوف نشرح systemd في عنصر «الخدمات Services والسجلات Logs مع systemd وjournalctl» من المرحلة.

المهام Jobs: المقدمة والخلفية

عندما تشغل أمراً طويلاً ثم تتذكر أنك تحتاج الطرفية، اضغط Ctrl+Z فيتوقف مؤقتاً وتعود إلى المحث، ثم اكتب bg ليكمل في الخلفية، أو fg ليعود إلى المقدمة. والأمر jobs يعرض المهام، وfg %2 يعيد المهمة رقم 2 تحديداً، كما يشرح دليل Bash عن التحكم في المهام.

لكن انتبه إلى أن هذه المهام مرتبطة بجلسة الطرفية، فإذا انقطع اتصال SSH أرسلت الطرفية SIGHUP إلى الصدفة ومنها إلى مهامها فتنتهي غالباً. لذلك لا تشغل عملية نقل بيانات تستغرق ساعتين ب & ثم تغلق الحاسوب، وإنما شغلها ب nohup التي تجعلها تتجاهل SIGHUP وتكتب مخرجها في ملف، أو داخل جلسة tmux أو screen التي تبقى بعد انقطاع الاتصال، أما البرنامج الذي يجب أن يعمل دائماً فمكانه خدمة systemd أو Container وليس الطرفية.

العمليات الميتة Zombies والعملية رقم 1 PID 1 في ال Container

ذكرنا أن العملية بعد أن تنتهي تبقى Zombie حتى تقرأ أمها رمز خروجها. وإذا ماتت الأم قبل ذلك، تتبنى العملية رقم 1 أبناءها، والمتوقع من رقم 1 أن يقرأ رموز خروجهم فيختفوا. وعلى السيرفر رقم 1 هو systemd ويفعل ذلك، أما في ال Container فرقم 1 هو أي برنامج بدأت به ال Container، وفي مختبرنا هو sleep infinity الذي لا يعرف شيئاً عن أبنائه. ولنصنع عملية ميتة: الأمر التالي يشغل sleep 1 في الخلفية ثم يستبدل الصدفة نفسها ب sleep 30 عبر exec، فلا يبقى من ينتظر sleep 1 عندما ينتهي:

$ bash -c 'sleep 1 & exec sleep 30' & sleep 2
$ ps -o pid,ppid,stat,cmd --forest -u sara
  PID  PPID STAT CMD
  220     1 Z    [sleep] <defunct>
  194     1 Z    [sleep] <defunct>
  171     1 Z    [worker.sh] <defunct>
  224     0 Ss+  bash -c source /usr/local/bin/run.sh /tmp/exit.txt
  239   224 S+    \_ sleep 30
  241   239 Z+    |   \_ [sleep] <defunct>
  242   224 R+    \_ ps -o pid,ppid,stat,cmd --forest -u sara

العملية 241 ميتة بالحالة Z وكلمة <defunct> لأن أمها sleep 30 لا تنتظرها، ولاحظ الأسطر الأولى: هذه عمليات ميتة من التجارب السابقة في المقال نفسه، ماتت أمهاتها فتبنتها العملية 1، ولأن sleep infinity لا يقرأ رموز خروجها فقد بقيت في الجدول. والعملية الميتة لا تستهلك معالجاً ولا ذاكرة تذكر، لكنها تحجز رقماً من أرقام العمليات، والبرنامج الذي ينشئ آلاف العمليات بهذا الشكل قد يستنفد الحد الأقصى فلا يستطيع ال Container تشغيل أي عملية جديدة.

والحل الذي يقدمه Docker هو الخيار --init الذي يشغل برنامجاً صغيراً كرقم 1 «يمرر الإشارات ويقرأ رموز خروج العمليات»، كما تقول وثائق Docker. وقد أعدنا التجربة في Container ثان شغلناه بالأمر docker run -d --init --name lab-linux-cli-init --entrypoint sleep postgres:18 infinity، فكانت النتيجة كما يلي:

$ ps -o pid,ppid,cmd -p 1
  PID  PPID CMD
    1     0 /sbin/docker-init -- sleep infinity
$ ps -o pid,ppid,stat,cmd -u sara --forest
  PID  PPID STAT CMD
   18     0 Ss+  bash -c echo "\$ ps -o pid,ppid,cmd -p 1"; ps -o pid,ppid,cmd -
   25    18 S+    \_ sleep 3
   27    25 Z+    |   \_ [sleep] <defunct>
   28    18 R+    \_ ps -o pid,ppid,stat,cmd -u sara --forest
$ ps -o pid,ppid,stat,cmd -u sara --forest   # after sleep 3 ends
  PID  PPID STAT CMD
   18     0 Rs+  ps -o pid,ppid,stat,cmd -u sara --forest

رقم 1 أصبح docker-init، والعملية الميتة ظهرت ما دامت أمها حية، فلما انتهت أمها تبناها docker-init وقرأ رمز خروجها فاختفت. لذلك إذا كتبت Dockerfile يبدأ بسكربت Shell يشغل برامج أخرى، فاستخدم --init أو init: true في Docker Compose، أو اجعل السكربت ينتهي ب exec للبرنامج الرئيسي حتى يصبح هو رقم 1 ويستقبل SIGTERM من docker stop مباشرة.

رمز الخروج Exit Code: هل نجح الأمر؟

كل أمر ينتهي برقم بين 0 و255 اسمه رمز الخروج Exit Code أو Exit Status، و0 يعني النجاح وأي رقم آخر يعني الفشل، وتحفظه الصدفة في المتغير الخاص $? حتى تشغل الأمر التالي. وبعض الأرقام لها معنى ثابت يحدده دليل Bash: 127 عندما لا يوجد الأمر، و126 عندما يوجد الملف لكنه غير قابل للتشغيل، و128 مضافاً إليه رقم الإشارة عندما تنهي إشارة الأمر. والمثال التالي يبين ذلك:

$ ls /etc/hostname; echo "exit code: $?"
/etc/hostname
exit code: 0
$ ls /nothing; echo "exit code: $?"
ls: cannot access '/nothing': No such file or directory
exit code: 2
$ grep sara /etc/passwd; echo "exit code: $?"
sara:x:1000:1000::/home/sara:/bin/bash
exit code: 0
$ grep ahmad /etc/passwd; echo "exit code: $?"
exit code: 1
$ dockr ps; echo "exit code: $?"
bash: line 1: dockr: command not found
exit code: 127
$ /etc/hostname; echo "exit code: $?"
bash: line 1: /etc/hostname: Permission denied
exit code: 126

في الإعداد أعلاه لاحظ التالي:

  • الأمر grep يخرج ب 1 عندما لا يجد ما تبحث عنه، وهذا ليس خطأ في التشغيل وإنما نتيجة، وكثير من السكربتات تبني قرارها على هذا الرقم.
  • الرقم 127 مع خطأ الكتابة dockr، و126 عندما حاولنا تشغيل ملف نصي لا يملك صلاحية التشغيل، وسوف نعود إلى هذا الخطأ في مقال الصلاحيات.
  • في تجربة worker.sh رأينا الرقم 137، وهو 128 مضافاً إليه 9 أي SIGKILL، ورأينا 143 عندما أنهينا sleep ب SIGTERM، فهو 128 مضافاً إليه 15. وهذا ما تراه في docker ps -a بجانب ال Container الذي انتهى: Exited (137) تعني أنه أنهي ب SIGKILL، إما من docker stop بعد المهلة وإما من قاتل العمليات عند نفاد الذاكرة، كما شرحنا في قسم OOM Killer.
$ sleep 300 & pid=$!
$ sleep 1; kill $pid; wait $pid; echo "exit code: $?"
exit code: 143

وتستطيع أن تبني على رمز الخروج سلسلة أوامر: && تشغل الأمر التالي فقط إذا نجح الذي قبله، و|| تشغله فقط إذا فشل، و; تشغله في كل الأحوال:

$ mkdir -p backup && cp notes.txt backup/ && echo "copied"
copied
$ cp nothing.txt backup/ && echo "copied" || echo "copy failed, stopping here"
cp: cannot stat 'nothing.txt': No such file or directory
copy failed, stopping here

والقاعدة العملية أن تستخدم && بين الخطوات التي تعتمد كل منها على التي قبلها، مثل cd /srv/app && git pull && docker compose up -d، والسبب أن ; سوف تكمل حتى لو فشل cd، فتنفذ بقية الأوامر في مجلد آخر لم تقصده.

أخطر سطر في أي سكربت Script هو rm مع متغير Variable فارغ

نجمع الآن ثلاثة أشياء تعلمناها: توسيع النجمة من عمل الصدفة، والمتغير الذي قد يكون فارغاً، والأمر rm -r الذي لا يسأل. لنفرض أن سكربتاً يحذف محتوى مجلد البرنامج قبل إعادة تثبيته بالسطر rm -rf "$STEAMROOT/"*، فماذا يحدث إذا كان المتغير فارغاً لسبب ما؟ استخدمنا echo بدل rm حتى نرى الأمر الذي سوف يصل إلى rm دون أن ننفذه:

$ STEAMROOT=""
$ echo rm -rf "$STEAMROOT/"*
rm -rf /bin /boot /dev /docker-entrypoint-initdb.d /etc /home /lib /lib64 /media /mnt /opt /proc /root /run /sbin /srv /sys /tmp /usr /var
$ echo rm -rf "${STEAMROOT:?is empty}/"*
/usr/local/bin/run.sh: line 7: STEAMROOT: is empty

عندما فرغ المتغير أصبح المسار /*، فوسعته الصدفة إلى كل مجلد في أعلى الشجرة. ولاحظ أن حماية rm المدمجة --preserve-root لا تنفع هنا، لأنها تمنع حذف / نفسه فقط، وrm لم يستلم / وإنما استلم قائمة مجلدات. أما الصيغة ${STEAMROOT:?is empty} فتجعل الصدفة توقف السكربت بخطأ إذا كان المتغير فارغاً أو غير معرف، فلا يصل الأمر إلى rm أصلاً، ولذلك لم يطبع السطر الأخير أي أمر.

📌
في يناير 2015 فتح أحد مستخدمي Steam على Ubuntu 14.04 بلاغاً في مستودع Valve على GitHub قال فيه إنه نقل مجلد Steam إلى قرص آخر ثم شغل البرنامج، فحذف كل ملف يملكه مستخدمه ابتداءً من أعلى الشجرة، ومنها قرص النسخ الاحتياطي الخارجي الذي كان مركباً في /media. ووجد المشاركون في البلاغ السطر المسؤول في سكربت steam.sh وهو rm -rf "$STEAMROOT/"*، الذي يصبح rm -rf "/"* إذا كان المتغير فارغاً. والدرس أن كل متغير في مسار حذف يحتاج تحققاً صريحاً قبل التنفيذ، وأن الصلاحيات حدت من الضرر فقط بقدر ما كان المستخدم لا يملكه، ولو كان البرنامج يعمل بحساب root لشمل الحذف النظام كله.

أخطاء شائعة Common Mistakes

  • العمل بحساب root طوال الوقت: كل خطأ كتابة يصبح كارثة على السيرفر كله، لذلك اعمل بمستخدم عادي واستخدم sudo للأمر الذي يحتاجه فقط.
  • نسخ أمر من الإنترنت ولصقه كما هو: اقرأ كل أمر قبل تشغيله، خصوصاً ما يحتوي على rm أو | bash أو sudo، ولاحظ أن اللصق قد يحمل سطراً جديداً في آخره فيتنفذ الأمر قبل أن تراجعه.
  • نسيان علامات التنصيص حول المتغيرات وأسماء الملفات: كما رأينا مع my report.txt، والحل أن تكتب "$file" دائماً.
  • التوجيه بالعلامة > إلى ملف مهم بدل >>: العلامة الواحدة تمسح محتوى الملف قبل الكتابة فيه، وإذا كان هذا ملف إعداد فقد خسرته.
  • البدء ب kill -9: أرسل SIGTERM أولاً وانتظر، كما رأينا مع ملف القفل.
  • تشغيل مهمة طويلة ب & ثم إغلاق جلسة SSH: استخدم nohup أو tmux، أو اجعلها خدمة.
  • ربط الأوامر ب ; بدل &&: فيكمل السكربت بعد خطوة فاشلة.

تدريب عملي Practice

  1. أنشئ بأمر واحد المجلدات ~/app/{config,data,logs}، ثم أنشئ ثلاثة ملفات سجل في logs، واحذف ملفين منها فقط بنمط واحد، وتأكد بالأمر echo قبل rm مما سيحذف.
  2. شغل sleep 1000، ثم أوقفه ب Ctrl+Z، وانظر حالته في ps، ثم أكمله في الخلفية ب bg، ثم أنهه ب kill واطبع رمز خروجه ب wait و$?، وتأكد أن الرقم 143.
  3. اكتب أنبوباً يعد الحسابات في /etc/passwd التي صدفتها /bin/bash فقط.

حذف المختبر Cleanup

docker rm -f lab-linux-cli lab-linux-cli-init

الخلاصة

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

  • الطرفية نافذة، والصدفة Bash هي التي تقرأ ما تكتبه، وهي التي توسع النجمة وتنفذ التوجيه قبل أن يبدأ الأمر.
  • كل شيء يبدأ من /: الإعدادات في /etc، والبيانات المتغيرة والسجلات في /var، ومجلدك في /home، و/proc و/dev تنشئهما النواة.
  • لا توجد سلة محذوفات، فتأكد بالأمر ls أو echo قبل rm -r، وضع المتغيرات وأسماء الملفات بين علامتي تنصيص، واحم كل متغير في مسار حذف ب ${VAR:?}.
  • stdout وstderr قناتان منفصلتان، والتوجيه يغير وجهة إحداهما، والأنبوب يمرر stdout وحده إلى الأمر التالي.
  • العملية برنامج يعمل له رقم وأم، وتراها ب ps وtop، وتوقفها ب kill أولاً، ولا تستخدم kill -9 إلا إذا لم تستجب، وفي ال Container استخدم --init إذا كان رقم 1 سكربتاً يشغل برامج أخرى.
  • رمز الخروج 0 نجاح وغيره فشل، و126 و127 و137 و143 أرقام سوف تراها كثيراً، و&& تمنع الخطوة التالية بعد الفشل.

والمقال التالي في المرحلة يشرح ما تعنيه الحروف rwx في أول كل سطر من ls -l، ولماذا يظهر لك Permission denied، وأخطاء الصلاحيات التي تتكرر مع Docker والسكربتات.

دليلصلاحيات الملفات Permissions في Linux وأخطاؤها الشائعةماذا تعني الحروف rwx، ولماذا تظهر رسالة Permission denied، وكيف تصلحها دون أن تلجأ إلى chmod 777.دليلتأمين سيرفر VPS على Ubuntu من أول دخولقبل أن تثبت أي تطبيق على سيرفرك الجديد، هذه الخطوات تغلق أبرز الثغرات وأولها الدخول بمفتاح SSH بدل كلمة المرور.دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.دليلماذا يحدث عندما تشغل برنامج Hello World؟نتتبع برنامج C صغيراً من المحرر إلى الشاشة، فترى كل طبقة يمر بها في الجهاز حتى تظهر الجملة أمامك.دليلتثبيت Docker Engine وCompose على Ubuntuكيف تثبت Docker على Ubuntu من المستودع الرسمي، وتضبطه من البداية حتى لا يمتلئ القرص أو تنكشف منافذك للإنترنت.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، وتنفيذ الأوامر كلها في Container من Docker Image ‏postgres:18 (Debian 13، Bash 5.2.37، GNU coreutils 9.7، procps-ng 4.0.4) على Docker Engine 29.8.0.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.