Классик писал, что счастливые часов не наблюдают. В те дикие времена ещё не было ни программистов, ни Unix, но в наши дни программисты знают твёрдо: вместо них за временем проследит cron.


Утилиты командной строки для меня одновременно слабость и рутина. sed, awk, wc, cut и другие старые программы запускаются скриптами на наших серверах ежедневно. Многие из них оформлены в виде задач для cron, планировщика родом из 70-х.


Я долго пользовался cron поверхностно, не вникая в детали, но однажды, столкнувшись с ошибкой при запуске скрипта, решил разобраться основательно. Так появилась эта статья, при написании которой я ознакомился с POSIX crontab, основными вариантами cron в популярных дистрибутивах Linux и устройством некоторых из них.


Используете Linux и запускаете задачи в cron? Вам интересна архитектура системных приложений в Unix? Тогда нам по пути!


Содержание



Происхождение видов


Периодическое выполнение пользовательских или системных программ — очевидная необходимость во всех операционных системах. Поэтому потребность в сервисах, позволяющих централизованно планировать и выполнять задачи, программисты осознали очень давно.


Unix-подобные операционные системы ведут свою родословную от Version 7 Unix, разработанной в 70-х годах прошлого века в Bell Labs в том числе и знаменитым Кеном Томпсоном (англ. Ken Thompson). Вместе c Version 7 Unix поставлялся и cron, сервис для регулярного выполнения задач суперпользователя.


Типичный современный cron — несложная программа, но алгоритм работы оригинального варианта был ещё проще: сервис просыпался раз в минуту, читал табличку с задачами из единственного файл (/etc/lib/crontab) и выполнял для суперпользователя те задачи, которые следовало выполнить в текущую минуту.


Впоследствии усовершенствованные варианты простого и полезного сервиса поставлялись со всеми Unix-подобными операционными системами.


Обобщённые описания формата crontab и базовых принципов работы утилиты в 1992 году были включены в главный стандарт Unix-подобных операционных систем — POSIX — и таким образом cron из стандарта де-факто стал стандартом де-юре.


В 1987 году Пол Викси (англ. Paul Vixie), опросив пользователей Unix на предмет пожеланий к cron, выпустил ещё одну версию демона, исправляющую некоторые проблемы традиционных cron и расширяющую синтаксис файлов-таблиц.


К третьей версии Vixie cron стал отвечать требованиям POSIX, к тому же у программы была либеральная лицензия, вернее не было вообще никакой лицензии, если не считать пожеланий в README: гарантий автор не даёт, имя автора удалять нельзя, а продавать программу можно только вместе с исходным кодом. Эти требования оказались совместимы с принципами набиравшего в те годы популярность свободного ПО, поэтому некоторые ключевые из появившихся в начале 90-х дистрибутивов Linux взяли Vixie cron в качестве системного и развивают его до сих пор.


В частности, Red Hat и SUSE развивают форк Vixie cron — cronie, а Debian и Ubuntu используют оригинальное издание Vixie cron со множеством патчей.


Давайте для начала познакомимся с описанной в POSIX пользовательской утилитой crontab, после чего разберём расширения синтаксиса, представленные в Vixie cron, и использование вариаций Vixie cron в популярных дистрибутивах Linux. И, наконец, вишенка на торте — разбор устройства демона cron.


POSIX crontab


Если оригинальный cron всегда работал для суперпользователя, то современные планировщики чаще имеют дело с задачами обычных пользователей, что более безопасно и удобно.


Сron-ы поставляются комплектом из двух программ: постоянно работающего демона cron и доступной пользователям утилиты crontab. Последняя позволяет редактировать таблицы задач, специфичные для каждого пользователя в системе, демон же запускает задачи из пользовательских и системной таблиц.


В стандарте POSIX никак не описывается поведение демона и формализована только пользовательская программа crontab. Существование механизмов запуска пользовательских задач, конечно, подразумевается, но не описано подробно.


Вызовом утилиты crontab можно сделать четыре вещи: отредактировать пользовательскую таблицу задач в редакторе, загрузить таблицу из файла, показать текущую таблицу задач и очистить таблицу задач. Примеры работы утилиты crontab:


crontab -e # редактировать таблицу задач
crontab -l # показать таблицу задач
crontab -r # удалить таблицу задач
crontab path/to/file.crontab # загрузить таблицу задач из файла

При вызове crontab -e будет использоваться редактор, указанный в стандартной переменной окружения EDITOR.


Сами задачи описаны в следующем формате:


# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /path/to/exec -a -b -c
# задача, выполняемая на 10-й минуте каждого часа
10 * * * * /path/to/exec -a -b -c
# задача, выполняемая на 10-й минуте второго часа каждого дня и использующая перенаправление стандартного потока вывода
10 2 * * * /path/to/exec -a -b -c > /tmp/cron-job-output.log

Первые пять полей записей: минуты [1..60], часы [0..23], дни месяца [1..31], месяцы [1..12], дни недели [0..6], где 0 — воскресенье. Последнее, шестое, поле — строка, которая будет выполнена стандартным интерпретатором команд.


В первых пяти полях значения можно перечислять через запятую:


# задача, выполняемая в первую и десятую минуты каждого часа
1,10 * * * * /path/to/exec -a -b -c

Или через дефис:


# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -c

Доступ пользователей к планированию задач регулируется в POSIX файлам cron.allow и cron.deny в которых перечисляются, соответственно, пользователи с доступом к crontab и пользователи без доступа к программе. Расположение этих файлов стандарт никак не регламентирует.


Запускаемым программам, согласно стандарту, должны передаваться по меньшей мере четыре переменные окружения:


  1. HOME — домашняя директория пользователя.
  2. LOGNAME — логин пользователя.
  3. PATH — путь, по которому можно найти стандартные утилиты системы.
  4. SHELL — путь к использованному командному интерпретатору.

Примечательно, что POSIX ничего не говорит о том, откуда берутся значения для этих переменных.


Хит продаж — Vixie cron 3.0pl1


Общий предок популярных вариантов cron — Vixie cron 3.0pl1, представленный в рассылке comp.sources.unix в 1992 году. Основные возможности этой версии мы и рассмотрим подробнее.


Vixie cron поставляется в двух программах (cron и crontab). Как обычно, демон отвечает за чтение и запуск задач из системной таблицы задач и таблиц задач отдельных пользователей, а утилита crontab — за редактирование пользовательских таблиц.


Таблица задач и файлы конфигурации


Таблица задач суперпользователя расположена в /etc/crontab. Синтаксис системной таблицы соответствует синтаксису Vixie cron с поправкой на то, что в ней шестой колонкой указывается имя пользователя, от лица которого запускается задача:


# Запускается ежеминутно от пользователя vlad
* * * * * vlad /path/to/exec

Таблицы задач обычных пользователей располагаются в /var/cron/tabs/username и используют общий синтаксис. При запуске утилиты crontab от имени пользователя редактируются именно эти файлы.


Управление списками пользователей, имеющих доступ к crontab, происходит в файлах /var/cron/allow и /var/cron/deny, куда достаточно внести имя пользователя отдельной строкой.


Расширенный синтаксис


По сравнению с POSIX crontab решение Пола Викси содержит несколько очень полезных модификаций в синтаксисе таблиц задач утилиты.


Стал доступен новый синтаксис таблиц: например, можно указывать дни недели или месяцы поимённо (Mon, Tue и так далее):


# Запускается ежеминутно по понедельникам и вторникам в январе
* * * Jan Mon,Tue /path/to/exec

Можно указывать шаг, через который запускаются задачи:


# Запускается с шагом в две минуты
*/2 * * * Mon,Tue /path/to/exec

Шаги и интервалы можно смешивать:


# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/exec

Поддерживаются интуитивные альтернативы обычному синтаксису (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):


# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/daily

Среда выполнения задач


Vixie cron позволяет менять окружение запускаемых приложений.


Переменные окружения USER, LOGNAME и HOME не просто предоставляются демоном, а берутся из файла passwd. Переменная PATH получает значение "/usr/bin:/bin", а SHELL — "/bin/sh". Значения всех переменных, кроме LOGNAME, можно изменить в таблицах пользователей.


Некоторые переменные окружения (прежде всего SHELL и HOME) используются самим cron для запуска задачи. Вот как может выглядеть использование bash вместо стандартного sh для запуска пользовательских задач:


SHELL=/bin/bash
HOME=/tmp/
# exec будет запущен bash-ем в /tmp/
* * * * * /path/to/exec

В конечном итоге все определённые в таблице переменные окружения (используемые cron или необходимые процессу) будут переданы запущенной задаче.


Для редактирования файлов утилитой crontab используется редактор, указанный в переменной окружения VISUAL или EDITOR. Если в среде, где был запущен crontab, эти переменные не определены, то используется "/usr/ucb/vi" (ucb — это, вероятно, University of California, Berkeley).


cron в Debian и Ubuntu


Разработчики Debian и производных дистрибутивов выпустили сильно модифицированную версию версию Vixie cron 3.0pl1. Отличий в синтаксисе файлов-таблиц нет, для пользователей это тот же самый Vixie cron. Крупнейшие новые возможности: поддержка syslog, SELinux и PAM.


Из менее заметных, но осязаемых изменений — расположение конфигурационных файлов и таблиц задач.


Пользовательские таблицы в Debian располагаются в директории /var/spool/cron/crontabs, системная таблица всё там же — в /etc/crontab. Специфичные для пакетов Debian таблицы задач помещаются в /etc/cron.d, откуда демон cron их автоматически считывает. Управление доступом пользователей регулируется файлами /etc/cron.allow и /etc/cron.deny.


В качестве командной оболочки по умолчанию по-прежнему используется /bin/sh, в роли которого в Debian выступает небольшой POSIX-совместимый шелл dash, запущенный без чтения какой-либо конфигурации (в неинтерактивном режиме).


Сам cron в последних версиях Debian запускается через systemd, а конфигурацию запуска можно посмотреть в /lib/systemd/system/cron.service. Ничего особенного в конфигурации сервиса нет, любое более тонкое управление задачами возможно осуществить через переменные окружения, объявленные прямо в crontab каждого из пользователей.


cronie в RedHat, Fedora и CentOS


cronie — форк Vixie cron версии 4.1. Как и в Debian, синтаксис не менялся, но добавлена поддержка PAM и SELinux, работы в кластере, слежения за файлами при помощи inotify и других возможностей.


Конфигурация по умолчанию находится в обычных местах: системная таблица — в /etc/crontab, пакеты помещают свои таблицы в /etc/cron.d, пользовательские таблицы попадают в /var/spool/cron/crontabs.


Демон запускается под управлением systemd, конфигурация сервиса — /lib/systemd/system/crond.service.


В Red Hat-подобных дистрибутивах при запуске по умолчанию используется /bin/sh, в роли которого выступает стандартный bash. Надо заметить, что при запуске задач cron через /bin/sh командная оболочка bash запускается в POSIX-совместимом режиме и не читает никакой дополнительной конфигурации, работая в неинтерактивном режиме.


cronie в SLES и openSUSE


Немецкий дистрибутив SLES и его дериватив openSUSE используют всё тот же cronie. Демон здесь тоже запускается под systemd, конфигурация сервиса лежит в /usr/lib/systemd/system/cron.service. Конфигурация: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. В качестве /bin/sh выступает тот же самый bash, запущенный в POSIX-совместимом неинтерактивном режиме.


Устройство Vixie cron


Современные потомки cron по сравнению с Vixie cron не изменились радикально, но всё же обзавелись новыми возможностями, не требующимися для понимания принципов работы программы. Многие из этих расширений оформлены неаккуратно и путают код. Оригинальный же исходный код cron в исполнении Пола Викси читать одно удовольствие.


Поэтому разбор устройства cron я решил провести на примере общей для обеих ветвей развития cron программы — Vixie cron 3.0pl1. Примеры я упрощу, убрав усложняющие чтение ifdef-ы и опустив второстепенные детали.


Работу демона можно разделить на несколько этапов:


  1. Инициализация программы.
  2. Сбор и обновление списка задач для запуска.
  3. Работа главного цикла cron.
  4. Запуск задачи.

Разберём их по порядку.


Инициализация


При запуске после проверки аргументов процесса cron устанавливает обработчики сигналов SIGCHLD и SIGHUP. Первый вносит в лог запись о завершении работы дочернего процесса, второй — закрывает файловый дескриптор файла-лога:


signal(SIGCHLD, sigchld_handler);
signal(SIGHUP, sighup_handler);

Демон cron в системе всегда работает один, только в роли суперпользователя и из главной директории cron. Следующие вызовы создают файл-лок с PID-ом процесса-демона, убеждаются, что пользователь правильный и меняют текущую директорию на главную:


acquire_daemonlock(0);
set_cron_uid();
set_cron_cwd();

Выставляется путь по умолчанию, который будет использоваться при запуске процессов:


setenv("PATH", _PATH_DEFPATH, 1);

Дальше процесс «демонизируется»: создаёт дочернюю копию процесса вызовом fork и новую сессию в дочернем процессе (вызов setsid). В родительском процессе больше надобности нет — и он завершает работу:


switch (fork()) {
case -1:
    /* критическая ошибка и завершение работы */
    exit(0);
break;
case 0:
    /* дочерний процесс */
    (void) setsid();
break;
default:
    /* родительский процесс завершает работу */
    _exit(0);
}

Завершение родительского процесса высвобождает лок на файле-локе. Кроме того, требуется обновить PID в файле на дочерний. После этого заполняется база задач:


/* повторный захват лока */
acquire_daemonlock(0);

/* Заполнение БД  */
database.head = NULL;
database.tail = NULL;
database.mtime = (time_t) 0;
load_database(&database);

Дальше cron переходит к главному циклу работы. Но перед этим стоит взглянуть на загрузку списка задач.


Сбор и обновление списка задач


За загрузку списка задач отвечает функция load_database. Она проверяет главный системный crontab и директорию с пользовательскими файлами. Если файлы и директория не менялись, то список задач не перечитывается. В противном случае начинает формироваться новый список задач.


Загрузка системного файла со специальными именами файла и таблицы:


/* если файл системной таблицы изменился, перечитываем */
if (syscron_stat.st_mtime) {
    process_crontab("root", "*system*",
    SYSCRONTAB, &syscron_stat,
    &new_db, old_db);
}

Загрузка пользовательских таблиц в цикле:


while (NULL != (dp = readdir(dir))) {
    char    fname[MAXNAMLEN+1],
            tabname[MAXNAMLEN+1];
    /* читать файлы с точкой не надо*/
    if (dp->d_name[0] == '.')
            continue;
    (void) strcpy(fname, dp->d_name);
    sprintf(tabname, CRON_TAB(fname));
    process_crontab(fname, fname, tabname,
                    &statbuf, &new_db, old_db);
}

После чего старая база данных подменяется новой.


В примерах выше вызов функции process_crontab убеждается в существовании пользователя, соответствующего имени файла таблицы (если только это не суперпользователь), после чего вызывает load_user. Последняя уже читает сам файл построчно:


while ((status = load_env(envstr, file)) >= OK) {
    switch (status) {
    case ERR:
        free_user(u);
        u = NULL;
        goto done;
    case FALSE:
        e = load_entry(file, NULL, pw, envp);
        if (e) {
            e->next = u->crontab;
            u->crontab = e;
        }
        break;
    case TRUE:
        envp = env_set(envp, envstr);
        break;
    }
}

Здесь либо выставляется переменная окружения (строки вида VAR=value) функциями load_env / env_set, либо читается описание задачи (* * * * * /path/to/exec) функцией load_entry.


Сущность entry, которую возвращает load_entry, — это и есть наша задача, помещаемая в общий список задач. В самой функции проводится многословный разбор формата времени, нас же больше интересует формирование переменных окружения и параметров запуска задачи:


/* пользователь и группа для запуска задачи берутся из passwd*/
e->uid = pw->pw_uid;
e->gid = pw->pw_gid;

/* шелл по умолчанию (/bin/sh), если пользователь не указал другое */
e->envp = env_copy(envp);
if (!env_get("SHELL", e->envp)) {
    sprintf(envstr, "SHELL=%s", _PATH_BSHELL);
    e->envp = env_set(e->envp, envstr);
}
/* домашняя директория */
if (!env_get("HOME", e->envp)) {
    sprintf(envstr, "HOME=%s", pw->pw_dir);
    e->envp = env_set(e->envp, envstr);
}
/* путь для поиска программ */
if (!env_get("PATH", e->envp)) {
    sprintf(envstr, "PATH=%s", _PATH_DEFPATH);
    e->envp = env_set(e->envp, envstr);
}
/* имя пользовтеля всегда из passwd */
sprintf(envstr, "%s=%s", "LOGNAME", pw->pw_name);
e->envp = env_set(e->envp, envstr);

С актуальным списком задач и работает главный цикл.


Главный цикл


Оригинальный cron из Version 7 Unix работал совсем просто: в цикле перечитывал конфигурацию, запускал суперпользователем задачи текущей минуты и спал до начала следующей минуты. Этот простой подход на старых машинах требовал слишком много ресурсов.


В SysV была предложена альтернативная версия, в которой демон засыпал либо до ближайшей минуты, для которой определена задача, либо на 30 минут. Ресурсов на перечитывание конфигурации и проверку задач в таком режиме потреблялось меньше, но быстро обновлять список задач стало неудобно.


Vixie cron вернулся к проверке списков задач раз в минуту, благо к концу 80-х ресурсов на стандартных Unix-машинах стало значительно больше:


/* первичная загрузка задач */
load_database(&database);
/* запустить задачи, поставленные к выполнению после перезагрузки системы */
run_reboot_jobs(&database);
/* сделать TargetTime началом ближайшей минуты */
cron_sync();
while (TRUE) {
    /* выполнить задачи, после чего спать до TargetTime с поправкой на время, потраченное на задачи */
    cron_sleep();

    /* перечитать конфигурацию */
    load_database(&database);

    /* собрать задачи для данной минуты */
    cron_tick(&database);

    /* перевести TargetTime на начало следующей минуты */
    TargetTime += 60;
}

Непосредственно выполнением задач занимается функция cron_sleep, вызывающая функции job_runqueue (перебор и запуск задач) и do_command (запуск каждой отдельной задачи). Последнюю функцию стоит разобрать подробнее.


Запуск задачи


Функция do_command исполнена в хорошем Unix-стиле, то есть для асинхронного выполнения задачи она делает fork. Родительский процесс продолжает запуск задач, дочерний — занимается подготовкой процесса задачи:


switch (fork()) {
case -1:
    /*не смогли выполнить fork */
    break;
case 0:
    /* дочерний процесс: на всякий случай еще раз пробуем захватить главный лок */
    acquire_daemonlock(1);
    /* переходим к формированию процесса задачи */
    child_process(e, u);
    /* по завершению дочерний процесс заканчивает работу */
    _exit(OK_EXIT);
    break;
default:
    /* родительский процесс продолжает работу */
    break;
}

В child_process довольно много логики: она принимает стандартные потоки вывода и ошибок на себя, чтобы потом переслать на почту (если в таблице задач указана переменная окружения MAILTO), и, наконец, ждёт завершения работы основного процесса задачи.


Процесс задачи формируется еще одним fork:


switch (vfork()) {
case -1:
    /* при ошибки сразу завершается работа */
    exit(ERROR_EXIT);
case 0:
    /* процесс-внук формирует новую сессию, терминал и т.д.
     */
    (void) setsid();

    /*
     * дальше многословная настройка вывода процесса, опустим для краткости
     */

    /* смена директории, пользователя и группы пользователя,
     * то есть процесс больше не суперпользовательский
     */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* запуск самой команды
     */
    {
        /* переменная окружения SHELL указывает на интерпретатор для запуска */
        char    *shell = env_get("SHELL", e->envp);

        /* процесс запускается без передачи окружения родительского процесса,
         * то есть именно так, как описано в таблице задач пользователя  */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* ошибка — и процесс на запустился? завершение работы */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* сам процесс продолжает работу: ждет завершения работы и вывода */
    break;
}

Вот, в общем-то, и весь cron. Какие-то интересные детали, например учёт удалённых пользователей, я опустил, но главное изложил.


Послесловие


Сron — на удивление простая и полезная программа, выполненная в лучших традициях мира Unix. Она не делает ничего лишнего, но свою работу выполняет замечательно на протяжении уже нескольких десятилетий. Ознакомление с кодом той версии, что поставляется с Ubuntu, заняло не больше часа, а удовольствия я получил массу! Надеюсь, я смог поделиться им с вами.


Не знаю, как вам, но мне немного грустно осознавать, что современное программирование с его склонностью к переусложнению и переабстрагированию уже давно не располагает к подобной простоте.


Существует множество современных альтернатив cron: systemd-timers позволяют организовать сложные системы с зависимостями, в fcron можно гибче регулировать потребление ресурсов задачами. Но лично мне всегда хватало простейших crontab.


Словом, любите Unix, используйте простые программы и не забывайте читать маны для вашей платформы!

Комментарии (60)


  1. evgenyk
    19.09.2019 16:03

    ИМХО Было бы полезно так же упомянуть как unset некоторые переменные окружения для выполняемой программы. Ту же MAILTO, например.


    1. VlK Автор
      19.09.2019 17:20

      Насколько видел из кода, переменные нельзя удалить в самом кроне, только перезаписать типа MAILTO=""


  1. am-habr
    19.09.2019 18:23

    cron был для меня настоящим открытием. Штука, которая просто работает.


    1. VlK Автор
      19.09.2019 18:26

      Ну, открытием он для меня был очень давно, но инструмент, безусловно полезный и простой до банальности. Я там уже писал, что очень люблю программы в этой традиции.


    1. evgenyk
      19.09.2019 18:43

      Многое из того, что сейчас делают на всяких крутых штуках вроде докера, кубернета и так далее командами по 10 человек, раньше за то же самое время делал один на голом линуксе плюс баш плюч крон.


    1. OnYourLips
      20.09.2019 00:08

      Давайте просто пример приведу, почему это не так.
      У вас на сервере, работающем в UTC, несколько проектов, два нормальных, а один работает в какой-то локальной таймзоне. И все требуют крона.


      И надо определённую задачу запускать для этого проекта по локальному для него времени.
      Можно пересчитать часы запуска команд автоматически при записи кронтаба, используя отстаток от 24.
      Усложним: запуск задачи в 2 утра в определенной таймзоне в пятницу и вторник. При пересчете придется менять день недели для некоторых таймзон. И это не автоматизируется в адекватное количество трудозатрат, пересчитывать придется вручную.


      1. scruff
        20.09.2019 07:16

        Проще проекты раскидать по виртуалкам, а на них уже настроить нужный часовой пояс.


      1. Zada
        20.09.2019 07:52

        Для разработчиков нет никакой другой таймзоны, окромя UTC.


        1. OnYourLips
          20.09.2019 11:17

          Вы никогда не работали в проектах, у истоков которых не стояли?
          Разве не было такого, что приходите в компанию, а там во всех проектах Moscow/CET/PDT?


          При этом все опускают головы и говорят: "Так исторически сложилось" или "Когда я пришел, это дерьмо уже было".
          И потом приходится строить костыли на костылях, чтобы это поддерживать, потому что отрефакторить такой объем единовременно или по частям хоть с какой-то стабильностью крайне опасно и на подобное просто не дают добро.


      1. VlK Автор
        21.09.2019 10:30

        Простите, что поздно отвечаю, как-то пропустил.


        а переменная CRON_TZ тут не помогает?


        1. OnYourLips
          21.09.2019 10:39

          Проверял — не помогает, точно не помню, что она меняет, но не подошло.
          К тому же она в Ubuntu, насколько помню, только в 18 появилась и в 16 её не было.


  1. JTG
    19.09.2019 19:48
    +4

    Здесь будет ветка переписи тех, кто случайно выполнял «crontab -r», промахнувшись мимо клавиши «e» :)


    1. am-habr
      19.09.2019 20:29

      Из статьи узнал про crontab -r, до этого всегда открывал crontab -e и удалял текст внутри. Судя по ветке, и хорошо, что не пользовал.


    1. McKinseyBA
      20.09.2019 14:17

      бэкап — наше все) Он и спас при промахе.


    1. simpleadmin
      20.09.2019 14:40

      Лет 15 назад я искал удаленную подработку. До этого я не имел опыта Linux и работал только на FreeBSD.
      И вот в первый-же день (скорее даже час) работы я «промахнулся» и узнал, что в Centos у crontab нет «глупых» вопросов а-ля

      # crontab -r
      remove crontab for root? 
      

      Всю ночь я его восстанавливал по логам. Странно, что не уволили…


  1. keydon2
    19.09.2019 20:28

    Ограничение, что крон может запускать задачи не чаще чем раз в секунду заставляло писать свои костыли.
    Крайне не хватало возможности указать частоту опроса, пускай и с перезапуском самого крона.


    1. am-habr
      19.09.2019 20:53
      +1

      Да, интересный вопрос. Ограничение по-умолчанию — минута, является большим для машины и относительно маленьким для человека. Предлагают скриптиком чаще запускать.
      Больше похоже на компромис, который сложился исторически, когда машины были ещё не так быстры. Частота в секунду для современных компьютеров была бы приемлемой, наверно.


      1. VlK Автор
        19.09.2019 22:43

        Смею предположить, что, во-первых, ежесекундные задачи — дело редкое, а во-вторых, в свое время ежесекундный запуск чего-либо было непозволительной роскошью.


        1. Iv38
          20.09.2019 11:10

          Ежесекундные задачи может и редкость, но вот разнести запуск ежеминутных в пределах минуты или запускать задачи два, три, четыре раза в минуту бывает нужно. То есть посекундная точность не помешала бы.


          1. VlK Автор
            20.09.2019 12:09

            это решаемая проблема. Какой-нибудь файл-лок можно, например, хватать на запуске задач.


            Хотя я понимаю, о чем вы. Но мы вот делали именно flock -x в похожей ситуации.


          1. NickyX3
            20.09.2019 15:41

            Это просто, bash скрипт запускающий нужное количество команд через слип прекрасно работал у нас лет 10


      1. istepan
        20.09.2019 08:09

        Если есть задача запускать что-то раз в секунду, то проще и правильнее демон написать.


        1. Smelekov
          20.09.2019 09:49

          всё равно не хватает иногда такой штуки */20 для секунд. в остальном крон это прелесть.


  1. pansa
    19.09.2019 21:21
    -1

    man systemd.timers


  1. rw6hrm
    19.09.2019 21:53

    … ну и упомянуть Cron для Windows тоже стоило бы ;), на порядок удобнее встроенного планировщика.


    1. pcdesign
      20.09.2019 13:42

      А можно ссылку?


      1. rw6hrm
        20.09.2019 14:31
        +1

        nnCron на сайте разработчика

        Заходим к разработчику
        1. pcdesign
          20.09.2019 14:35

          Понял. Спасибо.


    1. NickyX3
      20.09.2019 15:42
      +1

      О, да. nnCron прекрасен


  1. scruff
    20.09.2019 08:53

    Гуру cron-a! Подскажите по следующей ситуации — потребовалось мне как-то хранить логи почтовых треэйсов N-месяцев с граничного Антиспам-сервера. Задача проста до нельзя — раз в сутки-двое копировать файл /var/log/mail.log, аппендить к нему дату и ложить в какую-то папочку. Код crontab-a был приблизительно такой: 0 2 * * * cp /var/log/mail/ /home/user/mail_`%d%m%Y. Так вот в этом случае крон не отрабатывал, логи честно не помню, т.к. скорее всего не ыбло ничего вразумительного в них. Поступил проще — создал скрипт /home/user/copy_log.sh, вставил в него cp /var/log/mail/ /home/user/mail_`%d%m%Y, добавил «o+х» (каюсь, можно было и u+x, то бишь запуск только root-у) на скрипт, а сам скрипт закинул в crontab: 0 2 * * * /home/user/copy_log.sh. Вот тогда всё заработало без косяков. Вопрос — почему крон не работал напрямую с командой cp и требует скриптовую прослойку?


    1. fraks
      20.09.2019 09:14

      Скорее всего проблема в переменных среды, которые при запуске от cron совсем не те что при запуске из консоли. Например не то содержание переменной path и программы/команды просто не находит. Можно попробовать прописать везде абсолютные пути, можно вывести переменные в файлы и посмотреть какие там значения. Не претендую на правильность советов :)


    1. VlK Автор
      20.09.2019 10:01

      Надо бы указать версию вашего дистрибутива и крона. Но вообще использование скриптов-оберток — стандартный подход.


      Логов, кстати, у кронов может и не быть, стандартно бывает только отправка на почту, указанную в переменной MAILTO.


      Если хочется разобраться подробней, то можно попровать вызывать прямиком шелл с вашей командой, так, как его вызывает сам cron: /bin/sh -c "cp /from/file /to/file". Часто путаница бывает связана с тем, что шелл по умолчанию берется POSIX-совместимый и запущенный в неинтерактивном режиме.


      1. am-habr
        20.09.2019 10:35

        Лог можно настроить, но есть ли смысл в нём? Обычно им выполняют скрипты, которые логируют в своём окружении, елси это пёрл, питон или пхп.
        Однажды ковырялся в консоли и тут появляется сообщение: у вас есть новый емейл, по такому пути можете найти. «Ничего себе, консоль мне пишет про емейлы» — подумал я и, действительно, нашёл сообщение от крона в емейлах. Очень удобно оказалось.


        1. VlK Автор
          20.09.2019 10:38

          да, в старые добрые MAILTO хорошо работало. Но сейчас несколько потеряло актуальность.


      1. scruff
        20.09.2019 10:48

        Скорее всего в этом и дело. Спасибо, что подсказали. Хорошо что хоть из POSIX-режима можно скриптом вызывать шельные команды. Без этого ценность крона была бы околонулевой. Кстати в виндовом шедулере я встречал аналогичный трабл — длинные команды с кучей аргументов, напрямую прописанные в задании не работают. Как только закидываешь всё задание в батник — вуаля! Всё работает. Имхуется мне, что POSIX и там пробежал.


    1. am-habr
      20.09.2019 10:07
      +1

      Не требует. Пишут, что нужно \% или через переменную среды делать. Ссылка
      Скрипт удобен тем, что его можно как вручную для теста запустить, так и кроном выполнить.
      И в случае crontab -r скрипт останется.


      1. VlK Автор
        20.09.2019 10:15

        точно, надо ж экранировать проценты, т.к. проценты в кроне означают перенос строки, для передачи в команды нескольких строк :-)


        Из стандарта:


        A <percent-sign> character in this field shall be translated to a <newline>. Any character preceded by a
        <backslash> (including the '%' ) shall cause that character to be treated literally. Only the first line (up to a
        '%' or end-of-line) of the command field shall be executed by the command interpreter. The other lines shall > be made available to the command as standard input.


    1. VlK Автор
      20.09.2019 10:16

      вам ниже ответили, что проценты надо экранировать :-) Там же я привел выжимку из стандарта


    1. TaHKucT
      20.09.2019 11:43

      У logrotate'а есть параметр olddir и описание задачи выглядит так, как будто стоило использовать его для копирования, а сам logrotate запускать cron'ом


      1. Kanlas
        20.09.2019 15:18
        +1

        Если каждые сутки, то можно вообще ограничиться средствами logrotate с параметром daily


  1. fraks
    20.09.2019 09:04
    +1

    Для винды есть nnCron и nnCron Lite (последний — бесплатен).
    Формат crontab такой же + дополнения.
    Я использую версию 1.17.119.0 от 2005 года, которая, как заявлено, работает на Win95 — WinXP, но у меня работает и на Win7 и кажется даже на Win10.

    Неоднократно пробовал пользоваться родным виндовым планировщиком, но потом опять уходил на nnCron Lite.


    1. Iv38
      20.09.2019 14:04

      Ну как есть… Скорее был. В 2005 я его тоже использовал, но он не поддерживается с 2008. И помнится, под Win 7 он уже требовал каких-то телодвижений для нормальной работы.
      Так-то крутая была софтина, но она написана на форте и скрипты под нее надо было писать на диалекте форта, а это чудовищно. Обратная польская запись хороша для машин, а не для людей.


  1. red_led
    20.09.2019 09:11

    Для тех, кто ещё не столкнулся с этой замечательной фичей, процитирую википедию:


    Все условия (времени запуска) проверяются по «логическому И», кроме условий «день недели» и «день месяца» — указанные совместно, они обрабатываются по «логическому ИЛИ», то есть «по любому из дней», что отражено в документации (Ubuntu, Debian, FreeBSD). Однако такая логика неочевидна и не позволяет создать условие типа «первый понедельник каждого месяца» или «каждую пятницу в 13 число».

    Приходится писать костыли в баше.


  1. Xabik
    20.09.2019 09:25

    Т.е. Cron ежеминутно проверяет таблицы и выполняет команды при совпадении с системным временем/датой. Конечно возможно это что-то необычное, но если необходимо запустить задачу в определенную секунду, миллисекунду?


    1. VlK Автор
      20.09.2019 10:05

      Секунды (тем более — миллисекунды) традиционно не делали, т.к. для всех пользователей каждую секунду проверять задачи — дело неблагодарное. Тут Крон не поможет.


      Еще более гранулярно, на миллисекундах, фактически не имеет смысла делать, т.к. вся эта возня с шеллами, форками и экзеками уже занимает сотни миллисекунд, точности не выйдет.


      В таких случаях вам понадобится что-то очень специальное.


    1. evgenyk
      20.09.2019 11:02

      Дуп


    1. evgenyk
      20.09.2019 11:02

      Главная фишка систем, созданных на базе крона, это высокая надежность, достигнутая малой кровью. Т.е. например вы можете сделать скрипт, который падает каждый второй раз, запускать его из крона и все будет работать, лишь бы скрипт не портил данные.
      Тогда для себя я решил, насчет секунд, что раз cron имеет точность в минуту, значит так и надо и этот гвоздь торчит из стула не просто так. Поэтому ИМХО, если нужна точность лучше, чем минуты, нужно делать это в своей программе, а из крона только проверять, не упала ли эта программа и перезапускать в случае падения.


      1. Akdmeh
        20.09.2019 11:47

        А еще проще использовать тот же supervisord или systemctl, которые:
        1) включаются вместе с ОС и имеют возможность запуска скрипта после запуска зависимостей (т.е., когда включилась база данных или обработчик очередей)
        2) функция контроля (перезагружать ли после ошибки или штатного завершения работы, сколько попыток сделать, с каким диапазоном сделать перезапуск)
        3) все это логгируется и вытаскивается через тот же journalctl
        А сам скрипт исполняет работу и спит от 5 до 30 секунд. Чтобы не текла память, можно также завершать скрипт после получаса-часа работы, тогда systemctl автоматически включит задачу повторно.
        Поэтому cron в этом смысле выглядит довольно странным и не универсальным решением.


        1. evgenyk
          20.09.2019 12:04

          > Поэтому cron в этом смысле выглядит довольно странным и не универсальным решением.
          «Каждая программа в своем развитии доходит до необходимости отправлять электронную почту» © Не помню кто.
          В смысле каждая программа тащит в себя все фичи, до которых только может дотянуться.
          Крон не странная программа, это очень простая штука, для 95% задач его хватает и в этом его сила. В простоте.
          Универсальные решения это не UNIX way.


          1. Akdmeh
            20.09.2019 12:07

            Для многих задач — да, я его активно использую для автозапуска многих задач.
            Но для наблюдателя над ходом исполнения программы — возможно, это не самое эффективное решение, хотя и довольно простое. Конечно, все зависит от программы, которая запускается кроном, но supervisord или systemctl в этом плане более заточенные.


            1. evgenyk
              20.09.2019 12:27

              ИМХО у них разное назначение.
              cron — запуск скриптов, которые выполняют задачу и выходят.
              supervisord, systemctl — запуск демонов.


    1. evgenyk
      20.09.2019 12:08

      > но если необходимо запустить задачу в определенную секунду, миллисекунду?
      Если нужно запустить программу в нужную миллисекунду, то тут уже нехило пахнет специальной операционной системой реального времени да еще и с очерь хорошей синхронизацией времени.


  1. simpleadmin
    20.09.2019 14:58

    Шаги и интервалы можно смешивать:

    # Запускается каждую вторую минуту первых десяти минут каждого часа
    0-10/2 * * * * /path/to/exec

    Я бы не использовал в описании интервально-шаговых событий слово «каждую». Отнюдь, не придираюсь, просто зачастую формулировку «каждую вторую» воспринимают как «каждую чётную».
    С учётом данного примера с 0-й левой границей интервала это справедливо, но может вызывать непонимание конструкций вида
    1-11/2 * * * * /path/to/exec

    Разумеется и здесь можно говорить «каждую вторую начиная с 1», но сам стараюсь использовать формулировки «через две минуты», «с шагом в две минуты».


    1. VlK Автор
      20.09.2019 15:31

      согласен, сам раньше путал. Поправлю.


  1. Groosha
    20.09.2019 17:47

    Что раздражает у crontab, так это отсутствие (?) подтверждения при удалении через crontab -r. Особенно с учётом того, что клавиши "e" и "r" расположены рядом на клавиатуре, поэтому набирая "crontab -e" нужно быть КРАЙНЕ внимательным.


  1. 0xd34df00d
    20.09.2019 21:14

    Не знаю, как вам, но мне немного грустно осознавать, что современное программирование с его склонностью к переусложнению и переабстрагированию уже давно не располагает к подобной простоте.

    Тем временем в Gentoo как раз выпиливают vixie-cron как неподдерживаемый. В качестве одной из альтернатив как раз предлагают cronie.


    Эх, а я помню, как в 2004-м первый раз игрался с гентой, и emerge vixie-cron был одним из шагов в хендбуке. Уходят эпохи.


    1. VlK Автор
      20.09.2019 21:34

      Да судя по коду cronie не очень радикально изменился :-) цо… викси жив!


  1. sub31
    21.09.2019 09:02

    Коллега. А где же anacron? Где команды at?


    1. VlK Автор
      21.09.2019 10:19

      Это отдельные программы с отдельной историей :-) Хотя в redhat вроде как anacron вместе с cron поставляется...


      1. sub31
        21.09.2019 13:56

        Это все программы планирования заданий/выполнение заданий по времени.