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

والتوجيه 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، أما هنا فيكفيك أن تعرف دورة حياة العملية كما في الصورة التالية:

- عندما تكتب أمراً، تنشئ الصدفة نسخة من نفسها بالاستدعاء fork()، فتصبح النسخة عملية جديدة برقم جديد وأمها هي الصدفة.
- تستبدل النسخة نفسها بالبرنامج المطلوب عبر execve()، فيبقى رقمها كما هو ويتغير البرنامج الذي تشغله.
- أثناء حياتها تتنقل العملية بين التشغيل Running والانتظار Sleeping، فأغلب العمليات على السيرفر نائمة معظم الوقت تنتظر طلباً من الشبكة أو بيانات من القرص، ويمكن إيقافها مؤقتاً Stopped ثم استئنافها.
- عندما تنتهي، بنفسها أو بإشارة، تبقى في جدول العمليات كعملية ميتة 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 أصلاً، ولذلك لم يطبع السطر الأخير أي أمر.
/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
- أنشئ بأمر واحد المجلدات
~/app/{config,data,logs}، ثم أنشئ ثلاثة ملفات سجل فيlogs، واحذف ملفين منها فقط بنمط واحد، وتأكد بالأمرechoقبلrmمما سيحذف. - شغل
sleep 1000، ثم أوقفه ب Ctrl+Z، وانظر حالته فيps، ثم أكمله في الخلفية بbg، ثم أنهه بkillواطبع رمز خروجه بwaitو$?، وتأكد أن الرقم 143. - اكتب أنبوباً يعد الحسابات في
/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.