کرون جاب (cron job) دستوری است که سرویس cron لینوکس در زمان‌های مشخص و تکرارشونده خودکار اجرا می‌کند؛ مثل پشتیبان‌گیری هر شب ساعت ۲، پاک‌کردن فایل‌های موقت هر یکشنبه یا اجرای زمان‌بند لاراول هر دقیقه. برای ساختنش دستور crontab -e را می‌زنید و یک خط با این شکل اضافه می‌کنید: پنج فیلد زمان (دقیقه، ساعت، روز ماه، ماه، روز هفته) و بعد دستور. مثلاً 30 2 * * * /usr/local/bin/backup.sh یعنی هر روز ساعت ۲:۳۰ بامداد.

همین یک خط برای شروع کافی است، ولی بیشتر دردسرهای کرون جای دیگری است: اسکریپتی که در ترمینال درست کار می‌کند ولی در cron نه، خروجی‌ای که معلوم نیست کجا رفت، یا دو اجرای هم‌زمان که روی هم می‌افتند. در ادامه قالب زمان را با جدول مثال می‌بینیم، بعد سراغ همین دام‌ها می‌رویم و در آخر systemd timer را معرفی می‌کنیم که برای بعضی کارها جایگزین بهتری است. مثال‌ها برای Ubuntu 24.04 نوشته شده‌اند.

قالب زمان crontab: پنج فیلد

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

علامت‌هایی که در هر فیلد می‌شود استفاده کرد:

  • * یعنی «هر مقداری».
  • , فهرست: 1,15 یعنی روز ۱ و ۱۵.
  • - بازه: 1-5 یعنی دوشنبه تا جمعه.
  • / گام: */10 در فیلد دقیقه یعنی هر ۱۰ دقیقه.

جدول مثال‌ها

عبارت معنی
* * * * * هر دقیقه
*/5 * * * * هر ۵ دقیقه
0 * * * * سر هر ساعت
30 2 * * * هر روز ساعت ۲:۳۰
0 9-17 * * 1-5 سر هر ساعت از ۹ تا ۱۷، دوشنبه تا جمعه
0 0 * * 0 نیمه‌شب هر یکشنبه
0 3 1 * * ساعت ۳ بامداد روز اول هر ماه
15 4 * * 6 ساعت ۴:۱۵ هر شنبه
0 */6 * * * هر ۶ ساعت (۰، ۶، ۱۲، ۱۸)
@reboot یک بار بعد از هر بوت
@daily معادل 0 0 * * *

دو نکته که غافلگیرکننده‌اند:

  • روز هفته به سبک آمریکایی است. 0 یکشنبه است و 6 شنبه. اگر کاری را برای «اول هفته‌ی ایرانی» می‌خواهید، شنبه یعنی 6.
  • روز ماه و روز هفته با «یا» ترکیب می‌شوند، نه «و». 0 0 13 * 5 یعنی روز سیزدهم هر ماه و همچنین هر جمعه، نه فقط جمعه‌هایی که سیزدهم‌اند.

ساعت کرون همان ساعت سیستم است. اگر سرور روی UTC است (که برای سرور انتخاب خوبی است)، 30 2 یعنی ۲:۳۰ به وقت UTC. با timedatectl منطقه‌ی زمانی سرور را ببینید.

crontab -e و crontab -l

هر کاربر crontab شخصی خودش را دارد و جاب‌هایش با دسترسی همان کاربر اجرا می‌شوند:

bash
crontab -e      # ویرایش crontab کاربر فعلی
crontab -l      # نمایش crontab کاربر فعلی
sudo crontab -u deploy -l   # نمایش crontab کاربر دیگر

بار اول که crontab -e می‌زنید، می‌پرسد با کدام ویرایشگر باز کند. بعد از ذخیره و خروج، cron خودش تغییرات را می‌خواند و لازم نیست سرویسی را ری‌استارت کنید.

مراقب crontab -r باشید: کل crontab را بدون تأیید پاک می‌کند، و r روی صفحه‌کلید درست کنار e است. یک عادت خوب این است که هر از گاهی خروجی crontab -l را در فایلی نگه دارید، یا بهتر، جاب‌های مهم را در /etc/cron.d بگذارید که فایل است و می‌شود آن را در git نگه داشت.

/etc/cron.d: جاب‌های سیستمی با فیلد کاربر

فایل‌های داخل /etc/cron.d همان قالب crontab را دارند، با یک تفاوت مهم: بعد از پنج فیلد زمان، یک فیلد اسم کاربر هم دارند.

/etc/cron.d/myapp
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# زمان‌بند لاراول، هر دقیقه
* * * * * deploy cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

# پشتیبان شبانه
30 2 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

چند قاعده که اگر رعایت نشوند فایل بی‌صدا نادیده گرفته می‌شود:

  • مالک فایل باید root باشد و فقط root حق نوشتن داشته باشد (chmod 644).
  • اسم فایل فقط حروف، رقم، - و _ داشته باشد. فایلی با نقطه در اسم (مثل myapp.cron یا backup.bak) در اوبونتو اجرا نمی‌شود.
  • خط آخر فایل باید با newline تمام شود.

مزیت /etc/cron.d این است که هر برنامه فایل خودش را دارد، با ابزارهای استقرار قابل نصب است و برای دیدن همه‌ی جاب‌ها کافی است یک پوشه را نگاه کنید. پوشه‌های /etc/cron.daily، /etc/cron.weekly و مشابه هم هستند که اسکریپت‌های اجرایی داخلشان با زمان‌بندی ثابت اجرا می‌شوند؛ برای کارهایی که زمان دقیقشان مهم نیست مناسب‌اند.

چرا در ترمینال کار می‌کند و در cron نه؟

تقریباً همیشه جواب یکی از این سه است.

۱. PATH کوتاه است

cron فایل‌هایی مثل ~/.bashrc و ~/.profile را نمی‌خواند و PATH پیش‌فرضش فقط /usr/bin:/bin است. دستوری که در /usr/local/bin یا مسیری مثل ~/.local/bin است پیدا نمی‌شود. دو راه دارید: یا همه جا مسیر کامل بنویسید (/usr/local/bin/composer)، یا مثل مثال بالا، PATH را در بالای crontab تعریف کنید. متغیرهای دیگری که برنامه لازم دارد هم باید همین‌جا یا داخل خود اسکریپت تعریف شوند.

۲. شل sh است، نه bash

شل پیش‌فرض cron، /bin/sh است که در اوبونتو dash است. چیزهایی مثل [[ ... ]]، source یا آرایه‌ها در آن کار نمی‌کنند. یا SHELL=/bin/bash را بالای فایل بگذارید، یا بهتر، منطق را در اسکریپتی با #!/bin/bash بنویسید و در cron فقط آن اسکریپت را صدا بزنید.

۳. علامت % معنای خاص دارد

در خط crontab، % یعنی «خط جدید» و هر چه بعد از اولین % بیاید به‌عنوان ورودی به دستور داده می‌شود. این خط کار نمی‌کند:

text
0 3 * * * tar czf /backup/site-$(date +%F).tar.gz /var/www

باید % را با \ escape کنید:

text
0 3 * * * tar czf /backup/site-$(date +\%F).tar.gz /var/www

باز هم راه تمیزتر این است که این دستورها را در یک اسکریپت بگذارید؛ داخل اسکریپت % هیچ معنای خاصی ندارد.

خروجی و لاگ کرون جاب‌ها

cron خروجی هر جاب را برای کاربر صاحب آن ایمیل می‌کند، به شرطی که روی سرور برنامه‌ی ارسال ایمیل نصب باشد. روی بیشتر سرورها نیست، پس خروجی و خطاها بی‌صدا دور ریخته می‌شوند. همیشه خروجی را صریح به جایی بفرستید:

text
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

>> به انتهای فایل اضافه می‌کند و 2>&1 خطاها را هم به همان‌جا می‌فرستد. اگر این لاگ‌ها بزرگ می‌شوند، برایشان یک فایل در /etc/logrotate.d بسازید.

برای این‌که ببینید cron اصلاً جاب را اجرا کرده یا نه، لاگ خود سرویس را نگاه کنید:

bash
journalctl -u cron --since today

اگر خطی با CMD (...) برای جابتان می‌بینید، cron کارش را کرده و مشکل داخل خود دستور است. اگر هیچ خطی نیست، مشکل از قالب زمان، اسم فایل یا فیلد کاربر است.

یک ترفند ساده برای اشکال‌زدایی، اجرای اسکریپت در محیطی شبیه cron است:

bash
env -i SHELL=/bin/sh PATH=/usr/bin:/bin HOME="$HOME" /bin/sh -c '/usr/local/bin/backup.sh'

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

cron نمی‌داند اجرای قبلی تمام شده یا نه. اگر جابی هر ۵ دقیقه اجرا شود و یک بار ۷ دقیقه طول بکشد، دو نسخه هم‌زمان اجرا می‌شوند؛ برای پشتیبان‌گیری یا همگام‌سازی، این یعنی فایل‌های خراب یا کار تکراری. ابزار flock (که در اوبونتو نصب است) این را حل می‌کند:

text
*/5 * * * * deploy /usr/bin/flock -n /run/lock/myapp-sync.lock /usr/local/bin/sync.sh >> /var/log/myapp-sync.log 2>&1

flock پیش از اجرای دستور روی فایل قفل می‌گیرد. -n یعنی اگر قفل دست کس دیگری بود منتظر نمان و بی‌سروصدا خارج شو. قفل با تمام‌شدن دستور، حتی اگر با خطا تمام شود، خودبه‌خود آزاد می‌شود و فایل قفل جامانده دردسری درست نمی‌کند.

جایگزین: systemd timer

اوبونتو از systemd استفاده می‌کند و systemd زمان‌بند خودش را دارد. برای هر کار دو فایل می‌سازید: یک .service که می‌گوید چه اجرا شود و یک .timer که می‌گوید کی.

/etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=5min

[Install]
WantedBy=timers.target

فعال‌سازی و بررسی:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers

systemctl list-timers برای هر timer نشان می‌دهد آخرین بار کی اجرا شده و نوبت بعدی کی است. برای تست، سرویس را بدون صبر برای زمانش دستی اجرا کنید و لاگش را ببینید:

bash
sudo systemctl start backup.service
journalctl -u backup.service -n 50

قالب OnCalendar با cron فرق دارد، ولی خواناتر است: Mon..Fri *-*-* 09:00:00 یعنی روزهای کاری ساعت ۹، و hourly، daily و weekly هم پذیرفته می‌شوند. اگر مطمئن نیستید عبارتی چه معنایی دارد، systemd-analyze زمان‌های بعدی‌اش را حساب می‌کند:

bash
systemd-analyze calendar "Sat *-*-* 04:15:00" --iterations=3

cron یا systemd timer؟

cron systemd timer
راه‌اندازی یک خط دو فایل
لاگ باید دستی هدایت شود خودکار در journal
اجرای هم‌زمان با flock جلوگیری می‌شود تا اجرای قبلی تمام نشده، دوباره اجرا نمی‌شود
اجرای جامانده (سرور خاموش بوده) ندارد با Persistent=true بعد از روشن‌شدن اجرا می‌شود
محدودیت منابع، وابستگی به سرویس‌های دیگر ندارد همه‌ی امکانات systemd

برای کارهای ساده و سریع، cron هنوز کوتاه‌ترین راه است. برای کارهای مهم مثل پشتیبان‌گیری که لاگ، جلوگیری از هم‌پوشانی و اجرای جامانده برایشان مهم است، systemd timer انتخاب بهتری است.

جمع‌بندی

  • پنج فیلد: دقیقه، ساعت، روز ماه، ماه، روز هفته. یکشنبه 0 است و شنبه 6.
  • crontab -e برای جاب‌های شخصی؛ /etc/cron.d برای جاب‌های سیستمی، با فیلد کاربر و اسم فایل بدون نقطه.
  • cron پروفایل شل را نمی‌خواند: PATH را تعریف کنید، مسیر کامل بنویسید و % را escape کنید. بهتر از همه، کار را در یک اسکریپت بگذارید.
  • خروجی را همیشه به فایل لاگ بفرستید و برای جاب‌های تکراری از flock استفاده کنید.
  • برای کارهای حساس، systemd timer لاگ، جلوگیری از هم‌پوشانی و اجرای جامانده را بدون زحمت اضافه به شما می‌دهد.

اگر می‌خواهید از همین حالا یک جاب واقعی بسازید، پشتیبان‌گیری شبانه بهترین شروع است؛ در پشتیبان‌گیری با قاعده‌ی ۳-۲-۱ گفته‌ایم چه چیزی را کجا نگه دارید. برای کارهای پس‌زمینه‌ی داخل خود برنامه هم صف‌ها در لاراول را ببینید.