Cron no Linux: história, uso e dispositivo

Cron no Linux: história, uso e dispositivo

O clássico escreveu que happy hours não assistem. Naqueles tempos difíceis não havia programadores nem Unix, mas hoje os programadores sabem com certeza: o cron controlará o tempo em vez deles.

Os utilitários de linha de comando são um ponto fraco e uma tarefa árdua para mim. sed, awk, wc, cut e outros programas antigos são executados por scripts em nossos servidores todos os dias. Muitos deles são projetados como tarefas para o cron, um agendador originário dos anos 70.

Por muito tempo usei o cron superficialmente, sem entrar em detalhes, mas um dia, ao encontrar um erro ao executar um script, resolvi investigá-lo a fundo. Foi assim que este artigo apareceu, enquanto o escrevia me familiarizei com o crontab POSIX, as principais opções de cron em distribuições populares do Linux e a estrutura de algumas delas.

Você está usando Linux e executando tarefas cron? Você está interessado na arquitetura de aplicativos de sistema em Unix? Então estamos a caminho!

Conteúdo

Origem das especies

A execução periódica de programas de usuário ou de sistema é uma necessidade óbvia em todos os sistemas operacionais. Portanto, os programadores perceberam há muito tempo a necessidade de serviços que lhes permitam planejar e executar tarefas centralmente.

Os sistemas operacionais do tipo Unix têm suas origens na versão 7 do Unix, desenvolvida na década de 70 do século passado no Bell Labs, inclusive pelo famoso Ken Thompson. A versão 7 do Unix também incluía o cron, um serviço para execução regular de tarefas de superusuário.

Um típico cron moderno é um programa simples, mas o algoritmo operacional da versão original era ainda mais simples: o serviço acordava uma vez por minuto, lia uma tabela com tarefas de um único arquivo (/etc/lib/crontab) e executava para o superusuário aquelas tarefas que deveriam ter sido executadas no momento atual.

Posteriormente, versões melhoradas do serviço simples e útil foram fornecidas com todos os sistemas operacionais do tipo Unix.

Descrições generalizadas do formato crontab e dos princípios básicos de operação do utilitário foram incluídas no principal padrão de sistemas operacionais do tipo Unix - POSIX - em 1992, e assim o cron de um padrão de fato tornou-se um padrão de jure.

Em 1987, Paul Vixie, tendo pesquisado usuários Unix sobre seus desejos em relação ao cron, lançou outra versão do daemon que corrigiu alguns dos problemas do cron tradicional e expandiu a sintaxe dos arquivos de tabela.

Na terceira versão do Vixie o cron passou a atender aos requisitos POSIX, além disso, o programa tinha licença liberal, ou melhor, não havia licença nenhuma, exceto pelos desejos no README: o autor não dá garantias, o nome do autor não pode ser excluído e o programa só pode ser vendido junto com o código-fonte. Esses requisitos acabaram sendo compatíveis com os princípios do software livre que estava ganhando popularidade naqueles anos, então algumas das principais distribuições Linux que apareceram no início dos anos 90 tomaram o Vixie cron como seu sistema e ainda o estão desenvolvendo hoje.

Em particular, Red Hat e SUSE desenvolvem um fork do Vixie cron - cronie, e Debian e Ubuntu usam a edição original do Vixie cron com muitos patches.

Vamos primeiro nos familiarizar com o utilitário de usuário crontab descrito no POSIX, depois veremos as extensões de sintaxe fornecidas no Vixie cron e o uso de variações do Vixie cron em distribuições populares do Linux. E, finalmente, a cereja do bolo é a análise do dispositivo cron daemon.

crontab POSIX

Se o cron original sempre funcionou para o superusuário, os agendadores modernos geralmente lidam com tarefas de usuários comuns, o que é mais seguro e conveniente.

Os crons são fornecidos como um conjunto de dois programas: o daemon cron em execução constante e o utilitário crontab disponível aos usuários. Este último permite editar tabelas de tarefas específicas para cada usuário do sistema, enquanto o daemon inicia tarefas a partir de tabelas de usuários e do sistema.

В Padrão POSIX o comportamento do daemon não é descrito de forma alguma e apenas o programa do usuário é formalizado crontab. A existência de mecanismos para iniciar tarefas do usuário está, obviamente, implícita, mas não descrita em detalhes.

Ao chamar o utilitário crontab, você pode fazer quatro coisas: editar a tabela de tarefas do usuário no editor, carregar a tabela de um arquivo, mostrar a tabela de tarefas atual e limpar a tabela de tarefas. Exemplos de como funciona o utilitário crontab:

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

Mediante chamada crontab -e o editor especificado na variável de ambiente padrão será usado EDITOR.

As próprias tarefas são descritas no seguinte formato:

# строки-комментарии игнорируются
#
# задача, выполняемая ежеминутно
* * * * * /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

Os primeiros cinco campos dos registros: minutos [1..60], horas [0..23], dias do mês [1..31], meses [1..12], dias da semana [0.. .6], onde 0 é domingo. O último e sexto campo é uma linha que será executada pelo interpretador de comandos padrão.

Nos primeiros cinco campos os valores podem ser listados separados por vírgulas:

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

Ou com um hífen:

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

O acesso do usuário ao agendamento de tarefas é regulado no POSIX pelos arquivos cron.allow e cron.deny, que listam usuários com acesso ao crontab e usuários sem acesso ao programa, respectivamente. A norma não regula de forma alguma a localização desses arquivos.

De acordo com o padrão, pelo menos quatro variáveis ​​de ambiente devem ser passadas para programas iniciados:

  1. HOME - diretório inicial do usuário.
  2. LOGNAME — login do usuário.
  3. PATH é o caminho onde você pode encontrar utilitários padrão do sistema.
  4. SHELL — caminho para o interpretador de comandos usado.

Notavelmente, o POSIX não diz nada sobre a origem dos valores dessas variáveis.

Mais vendido - Vixie cron 3.0pl1

O ancestral comum das variantes populares do cron é o Vixie cron 3.0pl1, introduzido na lista de discussão comp.sources.unix em 1992. Consideraremos os principais recursos desta versão com mais detalhes.

O Vixie cron vem em dois programas (cron e crontab). Como de costume, o daemon é responsável por ler e executar tarefas da tabela de tarefas do sistema e das tabelas de tarefas individuais do usuário, e o utilitário crontab é responsável por editar as tabelas do usuário.

Tabela de tarefas e arquivos de configuração

A tabela de tarefas do superusuário está localizada em /etc/crontab. A sintaxe da tabela do sistema corresponde à sintaxe do Vixie cron, com a exceção de que a sexta coluna indica o nome do usuário em cujo nome a tarefa é iniciada:

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

As tabelas regulares de tarefas do usuário estão localizadas em /var/cron/tabs/username e usam a mesma sintaxe. Quando você executa o utilitário crontab como usuário, esses são os arquivos que são editados.

As listas de usuários com acesso ao crontab são gerenciadas nos arquivos /var/cron/allow e /var/cron/deny, onde basta inserir o nome do usuário em uma linha separada.

Sintaxe estendida

Comparada ao crontab POSIX, a solução de Paul Vixey contém várias modificações muito úteis na sintaxe das tabelas de tarefas do utilitário.

Uma nova sintaxe de tabela foi disponibilizada: por exemplo, você pode especificar dias da semana ou meses por nome (segunda, terça e assim por diante):

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

Você pode especificar a etapa pela qual as tarefas são iniciadas:

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

Etapas e intervalos podem ser misturados:

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

São suportadas alternativas intuitivas à sintaxe usual (reinicialização, anualmente, anualmente, mensalmente, semanalmente, diariamente, à meia-noite, de hora em hora):

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

Ambiente de execução de tarefas

Vixie cron permite alterar o ambiente de execução de aplicativos.

As variáveis ​​de ambiente USER, LOGNAME e HOME não são simplesmente fornecidas pelo daemon, mas são retiradas de um arquivo passwd. A variável PATH é definida como "/usr/bin:/bin" e a variável SHELL é definida como "/bin/sh". Os valores de todas as variáveis, exceto LOGNAME, podem ser alterados nas tabelas do usuário.

Algumas variáveis ​​de ambiente (principalmente SHELL e HOME) são usadas pelo próprio cron para executar a tarefa. Esta é a aparência de usar bash em vez de sh padrão para executar tarefas personalizadas:

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

Por fim, todas as variáveis ​​de ambiente definidas na tabela (usadas pelo cron ou necessárias ao processo) serão passadas para a tarefa em execução.

Para editar arquivos, o crontab usa o editor especificado na variável de ambiente VISUAL ou EDITOR. Se o ambiente onde o crontab foi executado não tiver essas variáveis ​​definidas, então "/usr/ucb/vi" será usado (ucb é provavelmente a Universidade da Califórnia, Berkeley).

cron no Debian e Ubuntu

Os desenvolvedores do Debian e distribuições derivadas lançaram versão altamente modificada Vixie cron versão 3.0pl1. Não há diferenças na sintaxe dos arquivos de tabela, para os usuários é o mesmo Vixie cron. Maior novo recurso: suporte syslog, SELinux и PAM.

Mudanças menos perceptíveis, mas tangíveis, incluem a localização dos arquivos de configuração e tabelas de tarefas.

As tabelas de usuários no Debian estão localizadas no diretório /var/spool/cron/crontabs, a tabela do sistema ainda está lá - em /etc/crontab. As tabelas de tarefas específicas do pacote Debian são colocadas em /etc/cron.d, de onde o daemon cron as lê automaticamente. O controle de acesso do usuário é controlado pelos arquivos /etc/cron.allow e /etc/cron.deny.

O shell padrão ainda é /bin/sh, que no Debian é um pequeno shell compatível com POSIX traço, iniciado sem ler nenhuma configuração (em modo não interativo).

O próprio Cron nas versões mais recentes do Debian é iniciado via systemd, e a configuração de inicialização pode ser visualizada em /lib/systemd/system/cron.service. Não há nada de especial na configuração do serviço; qualquer gerenciamento de tarefas mais sutis pode ser feito através de variáveis ​​de ambiente declaradas diretamente no crontab de cada usuário.

amigo no RedHat, Fedora e CentOS

comparsa — fork do Vixie cron versão 4.1. Assim como no Debian, a sintaxe não mudou, mas foi adicionado suporte para PAM e SELinux, trabalhando em cluster, rastreando arquivos usando inotify e outros recursos.

A configuração padrão está nos locais habituais: a tabela do sistema está em /etc/crontab, os pacotes colocam suas tabelas em /etc/cron.d, as tabelas de usuários vão em /var/spool/cron/crontabs.

O daemon é executado sob controle do systemd, a configuração do serviço é /lib/systemd/system/crond.service.

Em distribuições do tipo Red Hat, /bin/sh é usado por padrão na inicialização, que é o bash padrão. Deve-se observar que ao executar tarefas cron via /bin/sh, o shell bash inicia em modo compatível com POSIX e não lê nenhuma configuração adicional, executando em modo não interativo.

amigo no SLES e openSUSE

A distribuição alemã SLES e seu derivado openSUSE usam o mesmo cronie. O daemon aqui também é iniciado no systemd, a configuração do serviço está localizada em /usr/lib/systemd/system/cron.service. Configuração: /etc/crontab, /etc/cron.d, /var/spool/cron/tabs. /bin/sh é o mesmo bash em execução no modo não interativo compatível com POSIX.

Dispositivo cron Vixie

Os descendentes modernos do cron não mudaram radicalmente em comparação com o Vixie cron, mas ainda adquiriram novos recursos que não são necessários para compreender os princípios do programa. Muitas dessas extensões são mal projetadas e confundem o código. O código-fonte cron original de Paul Vixey é um prazer de ler.

Portanto, decidi analisar o dispositivo cron usando o exemplo de um programa cron comum a ambos os ramos de desenvolvimento - Vixie cron 3.0pl1. Simplificarei os exemplos removendo ifdefs que complicam a leitura e omitindo pequenos detalhes.

O trabalho do demônio pode ser dividido em várias etapas:

  1. Inicialização do programa.
  2. Coletando e atualizando a lista de tarefas a serem executadas.
  3. Loop cron principal em execução.
  4. Inicie uma tarefa.

Vamos examiná-los em ordem.

Inicialização

Quando iniciado, após verificar os argumentos do processo, o cron instala os manipuladores de sinais SIGCHLD e SIGHUP. O primeiro faz uma entrada de log sobre o encerramento do processo filho, o segundo fecha o descritor do arquivo de log:

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

O daemon cron sempre roda sozinho no sistema, apenas como superusuário e a partir do diretório cron principal. As chamadas a seguir criam um arquivo de bloqueio com o PID do processo daemon, certifique-se de que o usuário está correto e altere o diretório atual para o principal:

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

Está definido o caminho padrão, que será usado ao iniciar processos:

setenv("PATH", _PATH_DEFPATH, 1);

Então o processo é “daemonizado”: ​​ele cria uma cópia filha do processo chamando fork e uma nova sessão no processo filho (chamando setsid). O processo pai não é mais necessário e sai:

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

O encerramento do processo pai libera o bloqueio no arquivo de bloqueio. Além disso, é necessário atualizar o PID do arquivo para o filho. Depois disso, o banco de dados de tarefas é preenchido:

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

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

Então o cron passa para o ciclo de trabalho principal. Mas antes disso, vale a pena dar uma olhada no carregamento da lista de tarefas.

Coletando e atualizando a lista de tarefas

A função load_database é responsável por carregar a lista de tarefas. Ele verifica o crontab principal do sistema e o diretório com arquivos do usuário. Se os arquivos e o diretório não tiverem sido alterados, a lista de tarefas não será relida. Caso contrário, uma nova lista de tarefas começa a se formar.

Carregando um arquivo de sistema com nomes especiais de arquivos e tabelas:

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

Carregando tabelas de usuários em um loop:

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);
}

Após o qual o banco de dados antigo é substituído por um novo.

Nos exemplos acima, a chamada de função process_crontab verifica se existe um usuário que corresponde ao nome do arquivo da tabela (a menos que seja um superusuário) e então chama load_user. Este último já lê o próprio arquivo linha por linha:

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;
    }
}

Aqui, a variável de ambiente é definida (linhas no formato VAR=value) usando as funções load_env / env_set ou a descrição da tarefa é lida (* * * * * /path/to/exec) usando a função load_entry.

A entidade de entrada que load_entry retorna é nossa tarefa, que é colocada na lista geral de tarefas. A própria função realiza uma análise detalhada do formato da hora, mas estamos mais interessados ​​​​na formação de variáveis ​​​​de ambiente e parâmetros de inicialização de tarefas:

/* пользователь и группа для запуска задачи берутся из 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);

O loop principal funciona com a lista atual de tarefas.

Loop principal

O cron original da Versão 7 Unix funcionava de forma bastante simples: ele lia a configuração em um loop, iniciava as tarefas do minuto atual como superusuário e dormia até o início do minuto seguinte. Esta abordagem simples em máquinas mais antigas exigia muitos recursos.

Uma versão alternativa foi proposta no SysV, na qual o daemon dormia até o minuto mais próximo para o qual a tarefa foi definida, ou por 30 minutos. Menos recursos foram consumidos para reler a configuração e verificar tarefas neste modo, mas atualizar rapidamente a lista de tarefas tornou-se inconveniente.

Vixie cron voltou a verificar listas de tarefas uma vez por minuto, felizmente no final dos anos 80 havia significativamente mais recursos em máquinas Unix padrão:

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

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

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

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

A função cron_sleep está diretamente envolvida na execução de tarefas, chamando as funções job_runqueue (enumerar e executar tarefas) e do_command (executar cada tarefa individual). Vale a pena examinar a última função com mais detalhes.

Executando uma tarefa

A função do_command é executada no bom estilo Unix, ou seja, faz um fork para executar a tarefa de forma assíncrona. O processo pai continua a lançar tarefas, o processo filho prepara o processo de tarefa:

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

Há muita lógica em child_process: ele pega saída padrão e fluxos de erro para si mesmo, para depois enviá-los para o correio (se a variável de ambiente MAILTO estiver especificada na tabela de tarefas) e, finalmente, aguarda o principal processo da tarefa a ser concluído.

O processo da tarefa é formado por outra bifurcação:

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;
}

Isso é basicamente tudo que o cron é. Omiti alguns detalhes interessantes, por exemplo, contabilização de usuários remotos, mas descrevi o principal.

Posfácio

Cron é um programa surpreendentemente simples e útil, feito nas melhores tradições do mundo Unix. Ela não faz nada extra, mas tem feito seu trabalho maravilhosamente há várias décadas. Não demorei mais de uma hora para conhecer o código da versão que acompanha o Ubuntu e me diverti muito! Espero ter conseguido compartilhar com você.

Não sei quanto a você, mas fico um pouco triste ao perceber que a programação moderna, com sua tendência a complicar e abstrair demais, há muito tempo não conduz a tal simplicidade.

Existem muitas alternativas modernas ao cron: os timers do systemd permitem organizar sistemas complexos com dependências, o fcron permite regular com mais flexibilidade o consumo de recursos por tarefas. Mas pessoalmente, o crontab mais simples sempre foi suficiente para mim.

Resumindo, ame Unix, use programas simples e não esqueça de ler o mana da sua plataforma!

Fonte: habr.com

Adicionar um comentário