رفتن به محتوا
LoopX

زمان‌بندی کارها با cron

توی این درس یاد می‌گیری سرور باید کارهای تکراری (بکاپ، پاک‌سازی، گزارش) را خودش و سر وقت انجام بدهد. ابزار کلاسیک این کار cron است: هر کاربر یک جدول زمان‌بندی (crontab) دارد که هر خطش یک زمان پنج‌قسمتی (دقیقه، ساعت، روز ماه، ماه، روز هفته) و یک دستور است. با crontab -e ویرایش و با crontab -l مرورش می‌کنی؛ می‌فهمی کارهای cron در یک محیط محدود اجرا می‌شوند (و چرا بسیاری از «در ترمینال کار می‌کند ولی در cron نه»‌ها از همین‌جاست)، خروجی و لاگشان را با grep CRON /var/log/syslog پیدا می‌کنی، و با systemd timer آشنا می‌شوی که جایگزین امروزی آن است. تمرین: هر شب ساعت ۲ یک بکاپ خودکار بگیر.

مسئله: «هر شب ساعت ۲ این کار را بکن»

Section titled “مسئله: «هر شب ساعت ۲ این کار را بکن»”

اگر باید هر شب بکاپ گرفته شود، هر هفته لاگ‌های قدیمی پاک شود و هر ساعت یک گزارش ساخته شود، نمی‌شود آدم ساعت ۲ شب بیدار بماند. به یک زمان‌بند نیاز داری: برنامه‌ای که همیشه روشن است، ساعت را زیر نظر دارد و وقتش که رسید، دستور را اجرا می‌کند.

تشبیه: زنگ ساعت دیواری با دفترچه‌ی برنامه

Section titled “تشبیه: زنگ ساعت دیواری با دفترچه‌ی برنامه”

cron مثل یک زنگ ساعت دیواری است که هر دقیقه یک نگاه به دفترچه‌ی برنامه می‌اندازد: «ساعت ۲:۰۰ هر شب: بکاپ؛ هر ۵ دقیقه: بررسی سلامت». هر دقیقه خطوطی را که با زمان الآن می‌خوانند اجرا می‌کند. این زنگ حافظه‌ی گذشته ندارد: اگر سرور ساعت ۲:۰۰ خاموش بوده باشد، آن اجرا از دست رفته و بعداً جبران نمی‌شود (برای همین بعضی کارها به anacron یا systemd timer با Persistent=true نیاز دارند).

cron هر دقیقه بیدار می‌شود، جدول‌های زمان‌بندی (crontab کاربرها و فایل‌های سیستمی) را می‌خواند و دستورهایی را که زمانشان رسیده با یک shell اجرا می‌کند؛ رویداد را در syslog می‌نویسد.

این درس روی ماشین systemd‌دار آزمایشی (که cron و rsyslog واقعی دارد) اجرا شده است. cron کمترین واحد زمانش یک دقیقه است، پس در مثال‌ها مجبورم بین ساختن یک کار و دیدن اثرش تا یکی دو دقیقه صبر کنم (در خروجی‌ها فقط نتیجه را می‌بینی). برای کاربر alice یک crontab شخصی می‌سازیم.

مثال ۱: ساختار پنج‌قسمتی

Section titled “مثال ۱: ساختار پنج‌قسمتی”

هر خط crontab این شکل را دارد: پنج فیلد زمان و بعد دستور:

ساختار یک خط cron
┌──────── دقیقه (0-59)
│ ┌────── ساعت (0-23)
│ │ ┌──── روز ماه (1-31)
│ │ │ ┌── ماه (1-12)
│ │ │ │ ┌ روز هفته (0-7؛ 0 و 7 = یکشنبه، 1 = دوشنبه)
│ │ │ │ │
* * * * * دستور

هر فیلد می‌تواند یکی از این شکل‌ها باشد:

شکل معنی نمونه
* هر مقدار * * * * *: هر دقیقه
عدد همان مقدار 30 8 * * *: هر روز ۸:۳۰
a,b فهرست 0 9,17 * * *: ساعت ۹ و ۱۷
a-b بازه 0 9 * * 1-5: ۹ صبح دوشنبه تا جمعه
*/n هر n واحد */5 * * * *: هر ۵ دقیقه
a-b/n بازه با قدم 0 8-18/2 * * *: هر ۲ ساعت از ۸ تا ۱۸

و چند میان‌بر:

میان‌بر معادل
@reboot یک بار بعد از هر بوت
@hourly 0 * * * *
@daily 0 0 * * *
@weekly 0 0 * * 0
@monthly 0 0 1 * *

یک اشتباه رایج: * 2 * * * هر شب ساعت ۲ نیست! دقیقه * است، یعنی «هر دقیقه‌ی ساعت ۲»: ۶۰ بار در شب. برای یک بار باید دقیقه را بنویسی: 0 2 * * *. (در بخش systemd timer با ابزاری که زمان‌ها را حساب می‌کند ثابتش می‌کنم.)

crontab -e جدول خودت را با یک ویرایشگر باز می‌کند و crontab -l آن را فهرست می‌کند. (crontab -r همه را پاک می‌کند؛ مثال اشتباهات رایج.) اول کاربر را می‌سازم و دو تابع ترمینال مجازی را:

Terminal window
useradd -m -s /bin/bash alice
run_in_tmux() { tmux -u kill-server 2>/dev/null; tmux -u new-session -d -s t -x 100 -y "${TH:-26}" "$@"; sleep 1; }
screen() { tmux capture-pane -t t -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
echo "--- هنوز جدولی ندارد:"
su - alice -c 'crontab -l' 2>&1
echo "--- ساعت و منطقه‌ی زمانی این سرور (cron با همین کار می‌کند):"
date
خروجی
--- هنوز جدولی ندارد:
no crontab for alice
--- ساعت و منطقه‌ی زمانی این سرور (cron با همین کار می‌کند):
Sun Oct 4 07:55:59 UTC 2026

حالا crontab -e، در ویرایشگر nano (با EDITOR=nano، وگرنه اولین بار انتخاب ویرایشگر می‌پرسد) با ترمینال مجازی:

Terminal window
TH=28
run_in_tmux "su - alice"
tmux send-keys -t t "export EDITOR=nano; crontab -e" Enter; sleep 2
screen
خروجی
GNU nano 7.2 /tmp/crontab.AhpOh2/crontab
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
[ Read 23 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location M-U Undo
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^/ Go To Line M-E Redo

ویرایشگر یک فایل موقت با چند خط توضیح (که با # شروع می‌شوند و نادیده گرفته می‌شوند) نشان می‌دهد. می‌روم انتهای فایل (Alt+/ در nano) و یک خط می‌نویسم: هر دقیقه، تاریخ و ساعت را به یک فایل اضافه کن. دقت کن % را با \ escape کرده‌ام (دلیلش در اشتباهات رایج):

Terminal window
tmux send-keys -t t M-/; sleep 1
tmux send-keys -t t -l "* * * * * date '+\%F \%T' >> /tmp/cron-hello.log 2>&1"; tmux send-keys -t t Enter; sleep 1
tmux send-keys -t t C-o; sleep 1; tmux send-keys -t t Enter; sleep 1; tmux send-keys -t t C-x; sleep 2
tmux send-keys -t t "crontab -l | grep -v '^#'" Enter; sleep 1
screen | tail -5
tmux -u kill-server
خروجی
crontab: installing new crontab
alice@server:~$ crontab -l | grep -v '^#'
* * * * * date '+\%F \%T' >> /tmp/cron-hello.log 2>&1
alice@server:~$

بعد از ذخیره، crontab پیام installing new crontab را داد و crontab -l خط را نشان می‌دهد (بدون خط‌های توضیح). حالا فقط باید صبر کنیم تا cron دقیقه‌ی بعد بیدار شود. من دو دقیقه صبر می‌کنم و فایل را می‌خوانم:

Terminal window
sleep 125
echo "--- فایل خروجی کار (هر دقیقه یک خط):"
cat /tmp/cron-hello.log
echo "--- لاگ خود cron در syslog:"
grep CRON /var/log/syslog | grep alice | grep -v 'No MTA' | tail -2 | sed -E 's/^[^ ]+ server //'
خروجی
--- فایل خروجی کار (هر دقیقه یک خط):
2026-10-04 07:57:01
2026-10-04 07:58:01
--- لاگ خود cron در syslog:
CRON[16938]: (alice) CMD (date '+%F %T' >> /tmp/cron-hello.log 2>&1)
CRON[16941]: (alice) CMD (date '+%F %T' >> /tmp/cron-hello.log 2>&1)

دو (یا بیشتر) خط با فاصله‌ی دقیق یک دقیقه: کار در ابتدای هر دقیقه (ثانیه‌ی ۰۰ تا ۰۱) اجرا شد. و هر اجرا در syslog با کاربر و دستور ثبت شد: CRON[pid]: (alice) CMD (...)، اولین جایی که برای دیدن «cron این کار را اجرا کرد یا نه» می‌گردی.

⚡ بررسی سریع

خط «0 2 * * *» چه زمانی اجرا می‌شود؟

مثال ۳: محیط محدود کارهای cron

Section titled “مثال ۳: محیط محدود کارهای cron”

کار cron در ترمینال تو اجرا نمی‌شود. یک shell بی‌ورودی و بدون ترمینال است که فایل‌های ~/.bashrc و ~/.profile را نمی‌خواند، متغیرهای shell تو را ندارد و پوشه‌ی جاری‌اش پوشه‌ی خانه است. ببین چه چیزهایی می‌بیند. این بار crontab را از یک فایل می‌سازم (crontab فایل، که کل جدول را عوض می‌کند) با چند کار آزمایشی:

Terminal window
cat > /tmp/cron-a.tab <<'EOF'
* * * * * env | sort > /tmp/cron-env.txt
* * * * * echo "SHELL=$SHELL RANDOM=[$RANDOM] BASH_VERSION=[$BASH_VERSION] cwd=$(pwd)" > /tmp/cron-shell.txt
* * * * * ls /no/such/dir >> /tmp/cron-err.log 2>&1
* * * * * ls /no/such/dir
EOF
su - alice -c 'crontab /tmp/cron-a.tab && crontab -l'
echo "(صبر برای اجرای دقیقه‌ی بعد...)"
sleep 65
echo "--- متغیرهای محیطی کار cron:"
cat /tmp/cron-env.txt
echo "--- کدام shell، و پوشه‌ی جاری؟"
cat /tmp/cron-shell.txt
خروجی
* * * * * env | sort > /tmp/cron-env.txt
* * * * * echo "SHELL=$SHELL RANDOM=[$RANDOM] BASH_VERSION=[$BASH_VERSION] cwd=$(pwd)" > /tmp/cron-shell.txt
* * * * * ls /no/such/dir >> /tmp/cron-err.log 2>&1
* * * * * ls /no/such/dir
(صبر برای اجرای دقیقه‌ی بعد...)
--- متغیرهای محیطی کار cron:
HOME=/home/alice
LANG=C.UTF-8
LOGNAME=alice
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PWD=/home/alice
SHELL=/bin/sh
--- کدام shell، و پوشه‌ی جاری؟
SHELL=/bin/sh RANDOM=[] BASH_VERSION=[] cwd=/home/alice

نتیجه:

  • محیط خیلی کوچک است: فقط چند متغیر (HOME، LOGNAME، PATH، SHELL، LANG، PWD)؛ نه EDITOR، نه متغیرهای ساخته‌ی ~/.bashrc، نه alias ها. (PATH این‌جا از فایل سیستمی خوانده می‌شود؛ روی بعضی سیستم‌ها فقط /usr/bin:/bin است. هیچ‌وقت فرض نکن ابزارت پیدا می‌شود: مسیر کامل بنویس.)
  • SHELL=/bin/sh، نه bash. sh در Ubuntu همان dash است: ساده و سریع، ولی ویژگی‌های bash (مثل $RANDOM، [[ ]]، آرایه) را ندارد. RANDOM و BASH_VERSION خالی ماندند.
  • پوشه‌ی جاری خانه‌ی کاربر است، پس مسیرهای نسبی به خانه اشاره می‌کنند، نه جایی که فکر می‌کنی.

اگر کارت به bash نیاز دارد، در اول crontab یک خط SHELL=/bin/bash بنویس (و PATH=... اگر لازم است). همین را آزمایش می‌کنم؛ در همین دقیقه‌ی انتظار، یک فایل سیستمی هم می‌سازم (مثال ۶):

Terminal window
cat > /tmp/cron-b.tab <<'EOF'
SHELL=/bin/bash
* * * * * echo "SHELL=$SHELL RANDOM=[$RANDOM] BASH_VERSION=[$BASH_VERSION]" > /tmp/cron-shell2.txt
EOF
su - alice -c 'crontab /tmp/cron-b.tab'
cat > /etc/cron.d/lx-demo <<'EOF'
* * * * * root echo "از /etc/cron.d در $(date +\%T)" >> /tmp/cron-d.log
EOF
echo "(صبر برای اجرای دقیقه‌ی بعد...)"
sleep 65
echo "--- با SHELL=/bin/bash:"
cat /tmp/cron-shell2.txt
خروجی
(صبر برای اجرای دقیقه‌ی بعد...)
--- با SHELL=/bin/bash:
SHELL=/bin/bash RANDOM=[29899] BASH_VERSION=[5.2.21(1)-release]

حالا RANDOM عدد دارد و BASH_VERSION مقدار. (فایل /etc/cron.d/lx-demo را در مثال ۶ می‌بینیم.)

مثال ۴: خروجی و لاگ کارها

Section titled “مثال ۴: خروجی و لاگ کارها”

کار cron بدون ترمینال اجرا می‌شود. پس خروجی‌اش (stdout و stderr) کجا می‌رود؟ به ایمیل کاربر (اگر یک MTA نصب باشد)، و گرنه دور ریخته می‌شود. دو کار آزمایشی در مثال ۳: یکی خروجی را به فایل ریدایرکت کرده بود و یکی نه:

Terminal window
echo "--- کاری که >> فایل 2>&1 داشت: خطا در فایل لاگ ثبت شد:"
cat /tmp/cron-err.log | tail -2
echo "--- و کاری که ریدایرکت نداشت؛ cron در syslog چه می‌گوید؟"
grep CRON /var/log/syslog | grep -E 'CMD \(ls /no/such/dir\)|No MTA' | tail -3 | sed -E 's/^[^ ]+ server //'
خروجی
--- کاری که >> فایل 2>&1 داشت: خطا در فایل لاگ ثبت شد:
ls: cannot access '/no/such/dir': No such file or directory
--- و کاری که ریدایرکت نداشت؛ cron در syslog چه می‌گوید؟
CRON[16193]: (CRON) info (No MTA installed, discarding output)
CRON[16960]: (alice) CMD (ls /no/such/dir)
CRON[16955]: (CRON) info (No MTA installed, discarding output)

No MTA installed, discarding output: چون روی این ماشین برنامه‌ی ایمیل (MTA) نیست، خروجی کاری که ریدایرکت نداشت بی‌صدا دور ریخته شد. پس برای هر کار cron که اهمیت دارد، خروجی را صریحاً ذخیره کن: دستور >> /var/log/job.log 2>&1 (>> اضافه می‌کند؛ 2>&1 خطا را هم به همان فایل می‌برد؛ درس ریدایرکت). و برای کارهایی که نمی‌خواهی ایمیل بیاید، بالای crontab MAILTO="" بگذار. دیدن اجراها:

چه می‌خواهی دستور
آیا cron کار را اجرا کرد؟ grep CRON /var/log/syslog (روی Debian/Ubuntu) یا journalctl -u cron
فقط کارهای یک کاربر grep 'CRON.*(alice)' /var/log/syslog
دستور کار چه خروجی داد؟ فایل لاگی که خودت ریدایرکت کرده‌ای
روی Red Hat /var/log/cron

مثال ۵: خطای بزرگ، علامت %

Section titled “مثال ۵: خطای بزرگ، علامت %”

در crontab علامت % معنی ویژه دارد: به «خط جدید» تبدیل می‌شود و هر چه بعد از اولین % بیاید به‌عنوان ورودی استاندارد (stdin) به دستور داده می‌شود. پس date +%F بدون escape خراب است. ببین در لاگ چه می‌شود (یک کار با escape و یک کار بدون آن):

Terminal window
cat > /tmp/cron-c.tab <<'EOF'
* * * * * echo "escaped: $(date +\%F)" >> /tmp/cron-pct.log 2>&1
* * * * * echo "unescaped: $(date +%F)" >> /tmp/cron-pct.log 2>&1
EOF
su - alice -c 'crontab /tmp/cron-c.tab'
echo "(صبر برای اجرای دقیقه‌ی بعد...)"
sleep 65
echo "--- خروجی (فقط کار با escape نوشت: escaped، نه unescaped):"
cat /tmp/cron-pct.log
echo "--- cron دستور دوم را چطور دید؟ (بعد از % بریده شد):"
grep CRON /var/log/syslog | grep 'CMD (echo "unescaped' | tail -1 | sed -E 's/^[^ ]+ server //'
خروجی
(صبر برای اجرای دقیقه‌ی بعد...)
--- خروجی (فقط کار با escape نوشت: escaped، نه unescaped):
escaped: 2026-10-04
--- cron دستور دوم را چطور دید؟ (بعد از % بریده شد):
CRON[17008]: (alice) CMD (echo "unescaped: $(date +)

کار بدون escape (unescaped) فقط تا قبل از % (echo "unescaped: $(date +) به shell رسید، پس یک دستور ناقص اجرا شد (گیومه‌ی باز و پرانتز باز) و خطای سینتکس داد، آن هم بدون هیچ لاگی. راه‌حل: % را همیشه به‌صورت \% بنویس، یا بهتر: دستورهای پیچیده را در یک اسکریپت بگذار (که در آن % معمولی است) و فقط اسکریپت را در crontab صدا بزن.

مثال ۶: cron سیستمی، /etc/crontab و /etc/cron.d

Section titled “مثال ۶: cron سیستمی، /etc/crontab و /etc/cron.d”

علاوه بر crontab هر کاربر، خود سیستم هم کارهایی دارد. تفاوت: در فایل‌های سیستمی بین زمان و دستور یک فیلد نام کاربر هست:

Terminal window
echo "--- /etc/crontab (بدون توضیح):"
grep -v '^#' /etc/crontab | grep -v '^$'
echo "--- /etc/cron.d:"
ls /etc/cron.d
echo "--- /etc/cron.daily:"
ls /etc/cron.daily
echo "--- لیست اسکریپت‌هایی که run-parts برای cron.daily اجرا می‌کند (--test):"
run-parts --test /etc/cron.daily
echo "--- فایلی که خودم در /etc/cron.d ساختم دقیقه‌ی قبل اجرا شد؟"
cat /tmp/cron-d.log | tail -2
خروجی
--- /etc/crontab (بدون توضیح):
SHELL=/bin/sh
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
--- /etc/cron.d:
e2scrub_all
lx-demo
--- /etc/cron.daily:
apt-compat
dpkg
logrotate
man-db
--- لیست اسکریپت‌هایی که run-parts برای cron.daily اجرا می‌کند (--test):
/etc/cron.daily/apt-compat
/etc/cron.daily/dpkg
/etc/cron.daily/logrotate
/etc/cron.daily/man-db
--- فایلی که خودم در /etc/cron.d ساختم دقیقه‌ی قبل اجرا شد؟
از /etc/cron.d در 08:00:01
از /etc/cron.d در 08:01:01
  • /etc/crontab: جدول اصلی سیستم؛ خط‌هایش دقیقه ساعت ... کاربر دستور هستند و با run-parts همه‌ی اسکریپت‌های پوشه‌های cron.hourly، cron.daily، cron.weekly و cron.monthly را اجرا می‌کنند (روی این ماشین cron.daily ساعت ۶:۲۵ هر روز).
  • /etc/cron.d/: هر برنامه می‌تواند فایل جدا بگذارد (با فیلد کاربر)؛ اضافه‌کردن فایل یک راه تمیز برای کار سیستمی است. فایل نمونه‌ام هم اجرا شد.
  • /etc/cron.daily/ و …: فقط یک اسکریپت اجرایی بگذار (بدون فیلد زمان)؛ زمانش را سیستم تعیین می‌کند. اسکریپت‌هایی مثل logrotate (چرخش لاگ‌ها) و man-db همین‌جا هستند.

مثال ۷: بکاپ شبانه (تمرین اصلی)

Section titled “مثال ۷: بکاپ شبانه (تمرین اصلی)”

حالا کار اصلی: هر شب ساعت ۲ بکاپ. دستور بکاپ را در یک اسکریپت می‌گذارم (به‌جای یک خط طولانی با %): tar از درس آرشیو، نام با تاریخ، و پاک‌کردن همه‌ی بکاپ‌ها به‌جز ۳ تای آخر. یک «سایت» نمونه هم می‌سازم:

/usr/local/bin/lx-backup.sh
#!/bin/bash
# بکاپ شبانه‌ی سایت: فشرده با تاریخ در نام؛ فقط ۳ نسخه‌ی آخر نگه داشته می‌شود
src=/srv/site
dest=/var/backups/site
mkdir -p "$dest"
f="$dest/site-$(date +%F_%H%M).tar.gz"
tar -czf "$f" -C "$(dirname "$src")" "$(basename "$src")" && echo "$(date '+%F %T') بکاپ ساخته شد: $f"
ls -1t "$dest"/site-*.tar.gz | tail -n +4 | xargs -r rm -v
Terminal window
chmod +x /usr/local/bin/lx-backup.sh
mkdir -p /srv/site && echo "<h1>سایت من</h1>" > /srv/site/index.html
echo "--- اول با دست اجرا می‌کنم تا مطمئن شوم خود اسکریپت درست است:"
/usr/local/bin/lx-backup.sh
ls /var/backups/site
خروجی
--- اول با دست اجرا می‌کنم تا مطمئن شوم خود اسکریپت درست است:
2026-10-04 08:01:30 بکاپ ساخته شد: /var/backups/site/site-2026-10-04_0801.tar.gz
site-2026-10-04_0801.tar.gz

قاعده‌ی طلایی: قبل از دادن کار به cron، آن را با دست اجرا کن (بهتر: با همان محیط محدود، مثلاً env -i HOME=/root /bin/sh -c '/usr/local/bin/lx-backup.sh'). حالا خط crontab. زمان واقعی‌اش 0 2 * * * است؛ ولی نمی‌خواهم تا ساعت ۲ شب صبر کنم، پس همان خط را می‌نویسم و فقط به‌جای 0 2 دقیقه و ساعتِ «دو دقیقه‌ی دیگر» را می‌گذارم (با date -d '+2 minutes') تا دقیقاً همین مکانیزم را ببینی:

Terminal window
when=$(date -d '+2 minutes' '+%-M %-H')
echo "زمان‌بندی دو دقیقه‌ی بعد: «$when * * *» (در واقعیت: «0 2 * * *»)"
printf '%s * * * /usr/local/bin/lx-backup.sh >> /var/log/lx-backup.log 2>&1\n' "$when" > /tmp/cron-bk.tab
crontab -u root /tmp/cron-bk.tab
crontab -l
echo "(صبر تا اجرای کار...)"
sleep 170
echo "--- لاگ بکاپ (کار cron):"
cat /var/log/lx-backup.log
ls -l /var/backups/site | tail -n +2 | awk '{print $5, $9}'
خروجی
زمان‌بندی دو دقیقه‌ی بعد: «3 8 * * *» (در واقعیت: «0 2 * * *»)
3 8 * * * /usr/local/bin/lx-backup.sh >> /var/log/lx-backup.log 2>&1
(صبر تا اجرای کار...)
--- لاگ بکاپ (کار cron):
2026-10-04 08:03:01 بکاپ ساخته شد: /var/backups/site/site-2026-10-04_0803.tar.gz
185 site-2026-10-04_0801.tar.gz
185 site-2026-10-04_0803.tar.gz

بکاپ در لحظه‌ی مقرر خودکار ساخته شد، خروجی‌اش در /var/log/lx-backup.log ثبت شد و نسخه‌های اضافه (اگر بیشتر از ۳ می‌شد) پاک می‌شدند. برای ساعت ۲ شب فقط همان 0 2 را می‌نویسی. توجه: چون cron با ساعت سیستم کار می‌کند، به منطقه‌ی زمانی (date) سرور دقت کن؛ ساعت ۲ شب به وقت UTC یا تهران؟

مثال ۸: جلوگیری از اجرای هم‌زمان با flock

Section titled “مثال ۸: جلوگیری از اجرای هم‌زمان با flock”

اگر کاری که هر ۵ دقیقه اجرا می‌شود گاهی ۷ دقیقه طول بکشد، اجراها روی هم می‌افتند. با flock یک «قفل فایل» می‌گیری تا نسخه‌ی دوم اگر اولی هنوز کار می‌کند، فوراً بیرون برود:

Terminal window
echo "--- یک کار طولانی (۵ ثانیه) در پس‌زمینه با قفل:"
flock -n /tmp/cron-job.lock sh -c 'echo "نسخه‌ی اول شروع شد"; sleep 5; echo "نسخه‌ی اول تمام شد"' &
sleep 1
echo "--- نسخه‌ی دوم در همان زمان (flock -n یعنی اگر قفل است منتظر نمان):"
flock -n /tmp/cron-job.lock sh -c 'echo "نسخه‌ی دوم اجرا شد"' || echo "قفل دست دیگری است؛ نسخه‌ی دوم رد شد"
wait
خروجی
--- یک کار طولانی (۵ ثانیه) در پس‌زمینه با قفل:
نسخه‌ی اول شروع شد
--- نسخه‌ی دوم در همان زمان (flock -n یعنی اگر قفل است منتظر نمان):
قفل دست دیگری است؛ نسخه‌ی دوم رد شد
نسخه‌ی اول تمام شد

در crontab می‌نویسی: */5 * * * * flock -n /tmp/job.lock /usr/local/bin/job.sh. نسخه‌ی دوم تا پایان اولی اجرا نمی‌شود.

مثال ۹: systemd timer، جایگزین امروزی

Section titled “مثال ۹: systemd timer، جایگزین امروزی”

systemd timer همان کار را با دو فایل unit انجام می‌دهد: یک .service (چه کاری) و یک .timer (چه زمانی). مزایا نسبت به cron: لاگ در journal با journalctl -u، وابستگی به سایر سرویس‌ها، گزینه‌ی Persistent=true (اگر ماشین موقع زمان مقرر خاموش بود، بعد از روشن‌شدن اجرا می‌کند)، و تأخیر تصادفی. زمان‌ها در systemd شکل دیگری دارند (OnCalendar=)؛ ابزار systemd-analyze calendar آن‌ها را بررسی می‌کند. ببین «هر شب ساعت ۲» و همان اشتباه * 2 * * * را:

Terminal window
echo "--- هر شب ساعت 2 (معادل 0 2 * * *):"
systemd-analyze calendar '*-*-* 02:00:00' --iterations=3 | grep -E 'Normalized|Next elapse|Iteration'
echo "--- و «هر دقیقه‌ی ساعت 2» (معادل * 2 * * *)، ۳ اجرای اول:"
systemd-analyze calendar '*-*-* 02:*:00' --iterations=3 | grep -E 'Next elapse|Iteration'
خروجی
--- هر شب ساعت 2 (معادل 0 2 * * *):
Normalized form: *-*-* 02:00:00
Next elapse: Mon 2026-10-05 02:00:00 UTC
Iteration #2: Tue 2026-10-06 02:00:00 UTC
Iteration #3: Wed 2026-10-07 02:00:00 UTC
--- و «هر دقیقه‌ی ساعت 2» (معادل * 2 * * *)، ۳ اجرای اول:
Next elapse: Mon 2026-10-05 02:00:00 UTC
Iteration #2: Mon 2026-10-05 02:01:00 UTC
Iteration #3: Mon 2026-10-05 02:02:00 UTC

سه اجرای اول برای ساعت 02:00:00 فقط هر شب یک بار است (سه شب پشت‌سرهم)، ولی برای 02:*:00 سه اجرای اول 02:00، 02:01، 02:02‌اند: همان اشتباه * 2 * * *. یک timer ساده‌ی واقعی: هر ۲۰ ثانیه یک بار («tick») بعد از ۵ ثانیه‌ی اول:

Terminal window
cat > /etc/systemd/system/lx-tick.service <<'EOF'
[Unit]
Description=Demo job run by a timer
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo tick at $(date +%%T)'
EOF
cat > /etc/systemd/system/lx-tick.timer <<'EOF'
[Unit]
Description=Run lx-tick every 20 seconds
[Timer]
OnActiveSec=5
OnUnitActiveSec=20
AccuracySec=1s
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now lx-tick.timer
sleep 50
echo "--- زمان‌بندها (systemctl list-timers):"
systemctl list-timers lx-tick.timer --no-pager | cut -c1-100
echo "--- لاگ اجراها (journalctl -u):"
journalctl -u lx-tick.service --since '2 minutes ago' --no-pager -o cat | grep '^tick at'
systemctl disable --now lx-tick.timer
خروجی
Created symlink /etc/systemd/system/timers.target.wants/lx-tick.timer → /etc/systemd/system/lx-tick.timer.
--- زمان‌بندها (systemctl list-timers):
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-10-04 08:05:32 UTC 17s Sun 2026-10-04 08:05:12 UTC 2s ago lx-tick.timer lx-tick.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
--- لاگ اجراها (journalctl -u):
tick at 08:04:30
tick at 08:04:51
tick at 08:05:12
Removed "/etc/systemd/system/timers.target.wants/lx-tick.timer".

%%T در unit file همان %T است (در فایل‌های systemd هم % ویژه است و باید دو بار نوشته شود). list-timers ستون‌های NEXT (اجرای بعدی)، LEFT (چقدر مانده)، LAST و PASSED (آخرین اجرا) دارد؛ و لاگ هر اجرا در journal است. برای بکاپ شبانه می‌نویسی OnCalendar=*-*-* 02:00:00 و Persistent=true. این درس cron را اصلی گرفت چون همه‌جا هست و ساده؛ systemd timer برای کارهای جدی‌تر یا سرویس‌محور بهتر است.

پشت پرده: cron چطور کار می‌کند؟

Section titled “پشت پرده: cron چطور کار می‌کند؟”

cron یک daemon است (سرویس systemd): هر دقیقه (در لحظه‌ی :۰۰) بیدار می‌شود، فهرست crontab ها را (اگر تغییری کرده باشند، دوباره) می‌خواند و برای هر خطی که با ساعت و تاریخ الآن می‌خورد، یک پروسه‌ی فرزند می‌سازد که دستور را با SHELL (پیش‌فرض /bin/sh -c) اجرا می‌کند. ببین:

Terminal window
systemctl is-active cron
ps -o pid,comm,args -C cron | cut -c1-80
echo "--- crontab های کاربرها این‌جا نگهداری می‌شوند (فقط root می‌خواند):"
ls -l /var/spool/cron/crontabs/
خروجی
active
PID COMMAND COMMAND
81 cron /usr/sbin/cron -f -P
--- crontab های کاربرها این‌جا نگهداری می‌شوند (فقط root می‌خواند):
total 8
-rw------- 1 alice crontab 320 Oct 4 08:00 alice
-rw------- 1 root crontab 259 Oct 4 08:01 root

crontab هر کاربر فایلی در /var/spool/cron/crontabs/ است (برای همین crontab -e از فایل موقت و بررسی نحو استفاده می‌کند و مستقیم ویرایشش نمی‌کنی). cron هر دقیقه mtime این فایل‌ها را نگاه می‌کند. دو پیامد مهم: cron کارهای از‌دست‌رفته را جبران نمی‌کند (اگر سرور ساعت ۲ خاموش بود، آن اجرا نیست)، و کمترین واحد زمانی یک دقیقه است (برای ثانیه‌ای باید از systemd timer یا حلقه‌ی sleep استفاده کنی).

دستور کار
crontab -e ویرایش crontab خودت
crontab -l فهرست crontab
crontab -r حذف همه‌ی crontab (بدون تأیید!)
crontab فایل جایگزین کردن کل جدول با یک فایل
crontab -u کاربر -l crontab کاربر دیگر (root)
grep CRON /var/log/syslog تاریخچه‌ی اجراها (Debian/Ubuntu)
journalctl -u cron همان از journal
flock -n /tmp/x.lock دستور جلوگیری از اجرای هم‌زمان
systemd-analyze calendar 'عبارت' آزمایش زمان‌بندی systemd
systemctl list-timers فهرست timer ها
نکته‌ی نوشتن crontab چرا
% را به \% بنویس % در cron یعنی خط جدید
مسیر کامل دستورها PATH محدود است
SHELL=/bin/bash در بالای فایل اگر bashism داری
>> /var/log/job.log 2>&1 وگرنه خروجی دور ریخته/ایمیل می‌شود
دستور پیچیده را در یک اسکریپت بگذار آزمایش و دیباگ ساده‌تر
خط آخر باید Enter داشته باشد بعضی نسخه‌ها آخرین خط بی‌خط‌جدید را نادیده می‌گیرند

مثال ۵. راه‌حل: \% یا اسکریپت.

مثال ۱ و ۹: ۶۰ بار در ساعت ۲ اجرا می‌شود. راه‌حل: دقیقه را بنویس.

۳) فرض اینکه «در ترمینال کار می‌کند، پس در cron هم»

Section titled “۳) فرض اینکه «در ترمینال کار می‌کند، پس در cron هم»”

مثال ۳: محیط کوچک، /bin/sh، پوشه‌ی جاری خانه. راه‌حل: مسیرهای کامل، SHELL=/PATH= در crontab، و آزمایش با env -i:

Terminal window
echo "--- آزمایش یک اسکریپت با محیط خالی (شبیه cron):"
env -i HOME=/root /bin/sh -c '/usr/local/bin/lx-backup.sh | head -1'
ls /var/backups/site | wc -l
خروجی
--- آزمایش یک اسکریپت با محیط خالی (شبیه cron):
2026-10-04 08:05:15 بکاپ ساخته شد: /var/backups/site/site-2026-10-04_0805.tar.gz
3

مثال ۴: بدون ریدایرکت خروجی (و خطا) دور ریخته می‌شود. راه‌حل: >> لاگ 2>&1 برای هر کار مهم، و grep CRON /var/log/syslog.

e و r کنار هم‌اند و -r بی‌پرسش همه‌ی جدول را پاک می‌کند:

Terminal window
useradd -m -s /bin/bash bob
su - bob -c 'echo "0 3 * * * /bin/true" | crontab -; crontab -l; echo "--- crontab -r (یک اشتباه تایپی):"; crontab -r; crontab -l'
userdel -r bob 2>/dev/null
خروجی
0 3 * * * /bin/true
--- crontab -r (یک اشتباه تایپی):
no crontab for bob

هیچ تأییدی نخواست و جدول رفت. راه‌حل: از crontab مهم یک کپی نگه دار (crontab -l > ~/crontab.bak)، و اگر می‌خواهی -i را (crontab -ri) برای پرسیدن تأیید به‌کار ببر.

۶) اجرای هم‌پوشان و کارهای از‌دست‌رفته

Section titled “۶) اجرای هم‌پوشان و کارهای از‌دست‌رفته”

مثال ۸ (flock) و توضیح «پشت پرده»: cron جبران نمی‌کند. برای کارهایی که از دست رفتنشان مهم است از systemd timer با Persistent=true یا anacron استفاده کن.

✎ تمرینآسان

فایل /etc/crontab را بخوان و بگو اسکریپت‌های cron.daily چه ساعتی اجرا می‌شوند.

دیدن جواب
Terminal window
grep 'cron.daily' /etc/crontab
خروجی
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }

فیلدهای اول 25 6 * * * هستند: دقیقه ۲۵، ساعت ۶، هر روز؛ یعنی هر روز ساعت ۶:۲۵ صبح (به وقت سرور).

✎ تمرینمتوسط

تمرین اصلی: هر شب ساعت ۲ یک بکاپ خودکار بگیر. خط واقعی crontab برای اسکریپت بکاپ مثال ۷ چیست؟ آن را بنویس، با crontab -l ببین، و صحتش را با یک فایل آزمایشی بررسی کن. (برای ساعت ۲ شب نمی‌شود صبر کرد؛ مثال ۷ همین را با زمان دو دقیقه بعد آزمایش کرد.)

دیدن جواب
Terminal window
echo '0 2 * * * /usr/local/bin/lx-backup.sh >> /var/log/lx-backup.log 2>&1' > /tmp/cron-real.tab
crontab -u root /tmp/cron-real.tab
crontab -l
echo "--- اگر این خط روی یک ماشین اجرا شود، اجراهای بعدی ساعت 02:00 هستند (با systemd-analyze همان زمان‌بندی را بررسی می‌کنم):"
systemd-analyze calendar '*-*-* 02:00:00' --iterations=2 | grep -E 'Next elapse|Iteration'
crontab -r
خروجی
0 2 * * * /usr/local/bin/lx-backup.sh >> /var/log/lx-backup.log 2>&1
--- اگر این خط روی یک ماشین اجرا شود، اجراهای بعدی ساعت 02:00 هستند (با systemd-analyze همان زمان‌بندی را بررسی می‌کنم):
Next elapse: Mon 2026-10-05 02:00:00 UTC
Iteration #2: Tue 2026-10-06 02:00:00 UTC
✎ تمرینسخت

همان بکاپ را با systemd timer بنویس: lx-backup.service (که اسکریپت را اجرا می‌کند) و lx-backup.timer با OnCalendar=*-*-* 02:00:00 و Persistent=true. تایمر را فعال کن، با systemctl list-timers ببین، و سرویس را یک بار دستی (systemctl start) اجرا کن تا مطمئن شوی کار می‌کند.

دیدن جواب
Terminal window
cat > /etc/systemd/system/lx-backup.service <<'EOF'
[Unit]
Description=Nightly site backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/lx-backup.sh
EOF
cat > /etc/systemd/system/lx-backup.timer <<'EOF'
[Unit]
Description=Run the site backup every night at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now lx-backup.timer
systemctl list-timers lx-backup.timer --no-pager | cut -c1-110
echo "--- آزمایش دستی سرویس:"
systemctl start lx-backup.service
journalctl -u lx-backup.service --since '2 minutes ago' --no-pager -o cat | grep -E 'بکاپ ساخته شد|Finished'
systemctl disable --now lx-backup.timer
خروجی
Created symlink /etc/systemd/system/timers.target.wants/lx-backup.timer → /etc/systemd/system/lx-backup.timer.
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-10-05 02:00:00 UTC 17h - - lx-backup.timer lx-backup.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
--- آزمایش دستی سرویس:
2026-10-04 08:05:15 بکاپ ساخته شد: /var/backups/site/site-2026-10-04_0805.tar.gz
Finished lx-backup.service - Nightly site backup.
Removed "/etc/systemd/system/timers.target.wants/lx-backup.timer".
؟ آزمونک
  1. پنج فیلد زمان cron به ترتیب چه هستند؟

  2. تفاوت «0 2 * * *» و «* 2 * * *»؟

  3. چرا date +%F در یک خط crontab مشکل ایجاد می‌کند و چطور درست می‌شود؟

  4. کار cron در ترمینال تو کار می‌کند ولی در cron نه. محتمل‌ترین علت؟

  5. کجا می‌بینی cron یک کار را اجرا کرد؟

  6. مزیت اصلی systemd timer با Persistent=true نسبت به cron؟

  • cron هر دقیقه crontab ها را می‌خواند و کارها را با /bin/sh اجرا می‌کند. هر خط: دقیقه ساعت روز-ماه ماه روز-هفته دستور؛ * هر مقدار، a,b فهرست، a-b بازه، */n قدم. @reboot، @daily و… میان‌بر.
  • crontab -e ویرایش، crontab -l فهرست؛ crontab -r همه را پاک می‌کند (بی‌پرسش!). جدول سیستمی: /etc/crontab و /etc/cron.d (با فیلد کاربر)، و اسکریپت‌های /etc/cron.daily و…
  • محیط کارها کوچک است (SHELL=/bin/sh، پوشه‌ی جاری خانه، بدون .bashrc)؛ مسیر کامل بنویس و در صورت نیاز SHELL= و PATH= بالای crontab بگذار.
  • % را \% بنویس؛ دستورهای پیچیده را در اسکریپت بگذار. * 2 * * * ≠ 0 2 * * *.
  • خروجی را صریحاً ذخیره کن (>> لاگ 2>&1)؛ اجراها را با grep CRON /var/log/syslog ببین؛ همپوشانی را با flock -n بگیر.
  • cron کارهای از‌دست‌رفته را جبران نمی‌کند؛ systemd timer (OnCalendar=، Persistent=true، لاگ در journal) جایگزین امروزی است.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
crontab -e / crontab -lویرایش / فهرست crontab خودت
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1هر شب ساعت ۲ با لاگ
*/5 * * * * دستورهر ۵ دقیقه
0 9 * * 1-5 دستور۹ صبح، دوشنبه تا جمعه
@reboot دستوربعد از هر بوت
date +\%F (داخل crontab)escape علامت %
SHELL=/bin/bash (خط اول crontab)استفاده از bash
grep CRON /var/log/syslogتاریخچه‌ی اجراها
*/5 * * * * flock -n /tmp/job.lock job.shبدون اجرای هم‌پوشان
systemd-analyze calendar "*-*-* 02:00:00"آزمایش زمان‌بندی systemd
systemctl list-timerstimer های فعال