Cách dùng cơ bản

Mỗi người dùng có một crontab, các lệnh bên trong được thực thi với tư cách người dùng đó. Các thao tác thường dùng chỉ có bốn:

crontab -e

Chỉnh sửa crontab của người dùng hiện tại; sau khi thoát khỏi trình soạn thảo, các thay đổi được cài đặt tự động.

crontab -l

Liệt kê crontab của người dùng hiện tại.

crontab -r

Xóa crontab của người dùng hiện tại. Sau khi xóa, chạy lại crontab -l sẽ báo no crontab for root (ví dụ với root). Trước khi xóa hãy sao lưu: crontab -l > crontab.bak.

crontab -n ten_tep

Chạy thử: chỉ kiểm tra cú pháp của tệp này và in thông báo thành công, không ghi vào bất kỳ crontab nào.

Tệp crontab thực tế nằm ở /var/spool/cron/crontabs, không nên chỉnh sửa trực tiếp các tệp trong thư mục này, mà phải đi qua lệnh crontab.

Khi thao tác trong môi trường đã su sang người dùng khác, hướng dẫn sử dụng khuyên luôn dùng thêm -u user để chỉ định rõ crontab của người dùng nào, vì su có thể khiến crontab nhầm lẫn người dùng hiện tại.

Về quyền: nếu /etc/cron.allow tồn tại, chỉ những người dùng được liệt kê trong đó mới dùng được crontab; nếu nó không tồn tại mà /etc/cron.deny tồn tại, những người dùng được liệt kê trong deny không dùng được; nếu cả hai cùng tồn tại thì allow được ưu tiên. root luôn được phép.

Các lệnh và kết quả trong bài đến từ trang hướng dẫn của cron 3.0pl1-200ubuntu1 trên Ubuntu 26.04.1, cùng với việc tôi kiểm chứng thực tế (với người dùng root) trong container của hệ thống này. Các bản phân phối khác có thể khác; khi không chắc thì đối chiếu bằng man 5 crontab.

Năm trường thời gian

Trong crontab, chỉ những dòng hợp lệ sau được tính: thiết lập biến môi trường (name = value, khoảng trắng hai bên dấu bằng có hay không đều được) hoặc lệnh cron. Dòng lệnh có năm trường thời gian-giờ, các trường ngăn cách nhau bằng khoảng trắng hoặc tab, trường lệnh dài tối đa 998 ký tự.

Năm trường từ trái sang phải:

Phút  Giờ  Ngày  Tháng  Thứ
0-59  0-23  0-31  0-12  0-7

Trường thứ, giá trị 0 và 7 đều chỉ Chủ nhật. cron kiểm tra mỗi phút một lần; khi phút, giờ, tháng khớp với thời điểm hiện tại, và ít nhất một trong hai trường "ngày" và "thứ" khớp, lệnh được thực thi. Chi tiết "ít nhất một" này sẽ nói riêng ở phần sau.

Quy tắc cách viết:

  • * nghĩa là "từ đầu tiên đến cuối cùng", tức là bất kỳ giá trị nào.
  • Phạm vi dùng gạch nối, bao gồm cả hai đầu: 8-11 nghĩa là các giờ 8, 9, 10, 11.
  • Danh sách dùng dấu phẩy: 1,2,5,9, 0-4,8-12.
  • Bước nhảy dùng /, có thể theo sau phạm vi hoặc dấu sao: 0-23/2 nghĩa là mỗi hai giờ một lần, */2 cho hiệu quả tương tự.
  • Tháng và thứ có thể dùng ba chữ cái đầu của tên tiếng Anh (không phân biệt hoa thường), nhưng tên chỉ được dùng đơn lẻ, không được xuất hiện trong phạm vi hay danh sách.

Vài ví dụ lấy từ trang hướng dẫn:

5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1

Chạy lúc 0:05 mỗi ngày.

15 14 1 * * $HOME/bin/monthly

Ngày 1 hàng tháng lúc 14:15.

0 22 * * 1-5

10 giờ tối các ngày trong tuần (thứ Hai đến thứ Sáu).

23 0-23/2 * * *

Mỗi ngày 0:23, 2:23, 4:23, cứ thế tiếp tục.

Cách viết đặc biệt

Tám chuỗi đặc biệt sau có thể thay cho năm trường đầu tiên:

  • @reboot: chạy một lần lúc khởi động.
  • @yearly, @annually: tương đương 0 0 1 1 *.
  • @monthly: tương đương 0 0 1 * *.
  • @weekly: tương đương 0 0 * * 0.
  • @daily, @midnight: tương đương 0 0 * * *.
  • @hourly: tương đương 0 * * * *.

@reboot có một vấn đề về thời điểm cần lưu ý: "khởi động" ở đây là thời điểm daemon cron khởi động, có thể sớm hơn so với một số daemon hệ thống hay tiện ích khác. Nếu dịch vụ mà script của bạn phụ thuộc chưa lên thì lúc @reboot kích hoạt sẽ thất bại. Trong trường hợp này có thể thêm cơ chế thử lại hoặc trễ trong lệnh.

Biến môi trường và PATH

cron tự động thiết lập vài biến: SHELL đặt thành /usr/bin/sh (trang hướng dẫn ghi nguyên văn như vậy; trong môi trường kiểm chứng thực tế là /bin/sh), LOGNAME và HOME lấy từ dòng của chủ sở hữu crontab trong /etc/passwd. HOME và SHELL có thể ghi đè trong crontab, LOGNAME thì không.

Dễ gây rối nhất là PATH. Trong container môi trường kiểm chứng nêu trên, tôi cài một tác vụ * * * * * env > /tmp/cronenv; môi trường khi cron thực tế chạy là: HOME=/root, LOGNAME=root, PATH=/usr/bin:/bin, PWD=/root, SHELL=/bin/sh.

PATH của tác vụ cron chỉ có /usr/bin:/bin (kết quả trong môi trường kiểm chứng thực tế này), /usr/local/bin, /sbin, /usr/sbin đều không có trong đó. Vì vậy các công cụ bạn tự cài dưới /usr/local/bin, các lệnh hệ thống nằm ở /sbin, viết tên trống trơn trong cron sẽ không tìm thấy. Hai cách xử lý: viết đường dẫn tuyệt đối, hoặc thêm một dòng thiết lập PATH ở đầu crontab. Muốn biết PATH trong môi trường của bạn cụ thể là gì, có thể cài một tác vụ * * * * * env > /tmp/cronenv để tự xem.

Còn một bẫy khó nhận ra: giá trị biến môi trường trong crontab không được thay thế biến môi trường, cũng không được mở rộng dấu ngã. PATH=$HOME/bin:$PATH và PATH=~/bin:/usr/bin đều không hoạt động theo cách bạn mong đợi; sau khi A=1, B=2 mà viết C=$A $B cũng không được thay thế. Muốn thêm thư mục vào PATH thì viết đầy đủ đường dẫn.

MAILTO quyết định gửi kết quả cho ai: nếu được định nghĩa và khác rỗng thì gửi cho người dùng chỉ định; nếu định nghĩa rỗng (MAILTO="") thì không gửi thư; nếu không định nghĩa thì gửi cho chủ sở hữu crontab. Muốn tác vụ im lặng, chuyển hướng sang tệp hoặc /dev/null thì trực tiếp hơn.

Bẫy dấu phần trăm

Phần lệnh được thực thi bởi /usr/bin/sh hoặc shell bạn chỉ định qua SHELL trong crontab. Dấu phần trăm % trong lệnh, trừ khi được thoát bằng dấu gạch chéo ngược, sẽ bị thay bằng ký tự xuống dòng, và toàn bộ nội dung sau % đầu tiên được đưa vào làm đầu vào chuẩn của lệnh.

Kiểm chứng thực tế nói rõ nhất. Tác vụ sau trong container hoàn toàn không sinh ra tệp nhật ký:

* * * * * date +%s >> /tmp/cronout

Vì % bị coi là ký tự xuống dòng, lệnh bị cắt cụt ở chỗ date +. Sau khi thoát thì mới chạy bình thường:

* * * * * date +\%s >> /tmp/cronout 2>&1

Trong tệp thu được một dòng dấu thời gian Unix. Vì vậy date +%F, tar kết hợp chuỗi định dạng, bất kỳ lệnh nào có %, khi viết trong crontab đều phải viết thành \%.

Chú thích cùng dòng không phải là chú thích

Chú thích viết cùng dòng với lệnh cron không được coi là chú thích theo nghĩa của cron, mà là một phần của lệnh, được đưa cho shell xử lý; dòng thiết lập biến môi trường cũng vậy. Muốn viết chú thích thì để riêng một dòng, bắt đầu bằng #.

Giới hạn đồng thời ngày và thứ là quan hệ "hoặc"

Đây là điều phi trực giác nhất trong ngữ nghĩa crontab: khi cả hai trường "ngày" và "thứ" đều bị giới hạn (đều không phải *), điều kiện khớp là "hoặc" của hai trường, chứ không phải "và".

Ví dụ trong trang hướng dẫn: 30 4 1,15 * 5 sẽ chạy lúc 4:30 ngày 1 và ngày 15 hàng tháng, ngoài ra cũng chạy lúc 4:30 mọi thứ Sáu — chứ không phải "những thứ Sáu rơi vào ngày 1 và 15 hàng tháng".

Muốn hiệu quả "và", trang hướng dẫn đưa ra cách là thêm phép kiểm tra trong lệnh, ví dụ thứ Bảy thứ hai của tháng:

0 4 8-14 * * test $(date +\%u) -eq 6 && echo "2nd Saturday"

Chú ý ở đây %u phải viết thành \%u, chính là bẫy ở mục trước.

crontab cấp hệ thống và /etc/cron.d

/etc/crontab và các tệp dưới /etc/cron.d là cấu hình cấp hệ thống, có định dạng giống crontab của người dùng, chỉ thêm một trường tên người dùng nằm sau năm trường thời gian và trước lệnh. Sửa các tệp này không cần chạy lệnh crontab để cài, nhưng để chỉnh sửa chúng cần quyền root (ví dụ mở trình soạn thảo bằng sudo).

Trong môi trường kiểm chứng, dưới /etc/cron.d có thể thấy các mục có sẵn của bản phân phối (chẳng hạn e2scrub_all), dưới /etc/cron.daily có apt-compat, dpkg. Trong /etc/crontab, SHELL là /bin/sh, và có chú thích nói có thể ghi đè PATH.

Danh sách kiểm tra khi trục trặc

Khi tác vụ không chạy hoặc kết quả sai, kiểm tra theo thứ tự sau:

  1. crontab -l để xác nhận mục đã thực sự được cài; nếu có tệp gốc trong tay, dùng crontab -n tên_tệp để kiểm tra cú pháp, lệnh này chỉ kiểm tra, không cài.
  2. Các % trong lệnh đã viết thành \% hết chưa.
  3. Lệnh có phụ thuộc vào tệp thực thi không nằm trong PATH của cron không (môi trường kiểm chứng trong bài là /usr/bin:/bin). Thử đổi sang đường dẫn tuyệt đối.
  4. Trong script có cách viết phụ thuộc vào sự mở rộng $HOME, dấu ngã và tương tự không.
  5. Ngày và thứ có bị giới hạn đồng thời — có thể bạn dính phải ngữ nghĩa "hoặc".
  6. Chú thích có bị viết cùng dòng với lệnh không.
  7. Khi tác vụ @reboot thất bại, cân nhắc khả năng cron khởi động sớm hơn các dịch vụ mà nó phụ thuộc.

Thay thế bằng systemd timer

Timer unit của systemd cũng làm được việc định thời, so với crontab có thêm hai thứ: biểu thức lịch có thể xem trước thời điểm kích hoạt kế tiếp ngay trên dòng lệnh; tùy chọn Persistent= cho phép chạy bù những lần kích hoạt bị bỏ lỡ.

Nói về xem trước trước đã. systemd-analyze calendar có thể tính offline thời điểm kích hoạt kế tiếp của biểu thức lịch, không tạo ra bất cứ thứ gì, dùng được cả trong container không chạy systemd:

systemd-analyze calendar 'Mon..Fri *-*-* 09:00'

Kết quả gồm hai dòng Normalized form: (dạng chuẩn hóa) và Next elapse: (lần kích hoạt kế tiếp). Vài kết quả kiểm chứng thực tế: dạng chuẩn hóa của Mon..Fri *-*-* 09:00 là Mon..Fri *-*-* 09:00:00; lần kích hoạt kế tiếp của *-*-* 03:20:00 là Mon 2026-10-19 03:20:00 UTC; weekly được chuẩn hóa thành Mon *-*-* 00:00:00; *:0/15 nghĩa là mỗi 15 phút. Cú pháp biểu thức xem chi tiết ở systemd.time(7).

Timer cần đi kèm một service. Một ví dụ thực tế đang triển khai trên trang này (một đoạn [Timer]):

[Timer]
OnCalendar=*-*-* 03:20:00 Asia/Shanghai
Persistent=false
AccuracySec=1min
Unit=mot-tac-vu.service

Sau biểu thức có thể viết kèm múi giờ trực tiếp. Service tương ứng là Type=oneshot, tức là chạy một lần.

Persistent= mặc định là false. Khi đặt true, thời điểm kích hoạt lần cuối của service sẽ được lưu trên đĩa; khi timer được kích hoạt, nếu trong thời gian bị dừng lẽ ra có ít nhất một lần kích hoạt, service sẽ được chạy ngay. Điều này xử lý đúng cái việc "chạy bù tác vụ bỏ lỡ trong lúc tắt máy", chẳng hạn sao lưu, dọn nhật ký. Nó chỉ có hiệu lực với timer cấu hình OnCalendar=. Trang hướng dẫn còn nhắc: trước khi gỡ timer nên dùng systemctl clean --what=state tên-timer-unit.timer để xóa dấu thời gian nó lưu trên đĩa, trong đó tên timer unit thay bằng tên thực tế của bạn, nếu không khi cài lại timer trùng tên có thể đọc được trạng thái cũ.

Một điểm nữa cần biết: timer không nhất thiết kích hoạt đúng thời điểm chính xác như cấu hình OnCalendar=, do có yếu tố cửa sổ độ chính xác như AccuracySec=.

Chọn cách nào

Dưới đây là đề xuất của tôi, không phải kết luận chính thức. Với tác vụ đơn giản, bỏ lỡ một lần cũng không sao, máy luôn bật, thì crontab viết nhanh nhất, xong trong năm phút. Rơi vào một trong các trường hợp dưới đây thì cân nhắc systemd timer: tác vụ bỏ lỡ bắt buộc phải chạy bù (dùng Persistent=true); quy tắc thời gian phức tạp, muốn kiểm chứng biểu thức trước trên dòng lệnh; bạn đã dùng systemd quản lý các service unit cùng loại, muốn tác vụ định thời được quản lý theo cùng cách. Hai cách có thể song song tồn tại, quyết định theo từng tác vụ là được.

Đọc thêm

man 5 crontab, man 1 crontab và man systemd.timer là nguồn của mọi kết luận trong bài. }