Cron no Linux: história, uso e funcionamento

Cron no Linux: história, uso e funcionamento

Clássicos diziam que pessoas felizes não observam as horas. Naqueles tempos selvagens, ainda não havia programadores nem Unix, mas hoje os programadores sabem com certeza: é o cron que cuidará do tempo por eles.

Para mim, utilitários de linha de comando são uma fraqueza e uma rotina ao mesmo tempo. sed, awk, wc, cut e outros programas antigos são executados diariamente em nossos servidores por scripts. Muitos deles são configurados como tarefas para o cron, um agendador que vem dos anos 70.

Por muito tempo, eu usei o cron de forma superficial, sem me aprofundar nos detalhes, mas um dia, ao me deparar com um erro ao executar um script, decidi investigar a fundo. Assim nasceu este artigo, que escrevi após me familiarizar com o POSIX crontab, as principais variantes do cron nas distribuições populares do Linux e sua estrutura.

Você usa Linux e executa tarefas no cron? Está interessado na arquitetura de aplicativos do sistema em Unix? Então estamos no mesmo caminho!

Conteúdo

A Origem das Espécies

A execução periódica de programas de usuário ou do sistema — é uma necessidade óbvia em todos os sistemas operacionais. Por isso, a necessidade de serviços que permitem planificar e executar tarefas de forma centralizada foi percebida pelos programadores há muito tempo.

Os sistemas operacionais similares ao Unix têm sua linhagem do Version 7 Unix, desenvolvido nos anos 70 no Bell Labs, incluindo o famoso Ken Thompson. Juntamente com o Version 7 Unix, também foi fornecido o cron, um serviço para a execução regular de tarefas do superusuário.

Um cron moderno típico — é um programa simples, mas o algoritmo de funcionamento da versão original era ainda mais simples: o serviço acordava uma vez por minuto, lia a tabela de tarefas de um único arquivo (/etc/lib/crontab) e executava as tarefas que deveriam ser realizadas naquele minuto atual para o superusuário.

Subsequentemente, versões aprimoradas deste serviço simples e útil foram incluídas em todos os sistemas operacionais semelhantes ao Unix.

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

Em 1987, Paul Vixie, consultando usuários do Unix sobre suas sugestões para o cron, lançou mais uma versão do daemon, corrigindo alguns problemas do cron tradicional e expandindo a sintaxe dos arquivos de tabela.

Com a terceira versão, o Vixie cron atendeu aos requisitos do POSIX e, além disso, o programa tinha uma licença liberal, ou melhor, não tinha licença nenhuma, exceto pelos desejos expressos no README: o autor não oferece garantias, o nome do autor não pode ser removido, e o programa só pode ser vendido junto com o código-fonte. Esses requisitos se mostraram compatíveis com os princípios do software livre que começava a ganhar popularidade na época, portanto, algumas das principais distribuições do Linux que surgiram no início dos anos 90 adotaram o Vixie cron como sistema e o estão desenvolvendo até hoje.

Em particular, a Red Hat e a SUSE desenvolveram um fork do Vixie cron — o cronie, enquanto o Debian e o Ubuntu utilizam a edição original do Vixie cron com várias correções.

Vamos primeiro nos familiarizar com a utilidade crontab descrita no POSIX, depois discutiremos as extensões de sintaxe apresentadas no Vixie cron e o uso de variações do Vixie cron nas populares distribuições do Linux. E, finalmente, a cereja do bolo — a análise do funcionamento do daemon cron.

POSIX crontab

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

Os crons são fornecidos como um conjunto de dois programas: o daemon cron, que está sempre em funcionamento, e a utilidade crontab, acessível aos usuários. Este último permite editar tabelas de tarefas específicas para cada usuário no sistema, enquanto o daemon executa as tarefas das tabelas de usuários e da tabela do sistema.

Em padrão POSIX não descreve de forma alguma o comportamento do daemon e formaliza apenas o programa do usuário crontab. A existência de mecanismos para executar tarefas de usuários, certamente, é pressuposta, mas não é descrita em detalhes.

Com o comando da utilidade crontab, podem-se realizar quatro ações: editar a tabela de tarefas do usuário no editor, carregar a tabela a partir de um arquivo, mostrar a tabela de tarefas atual e limpar a tabela de tarefas. Exemplos de uso da utilidade crontab:

crontab -e # editar a tabela de tarefas
crontab -l # mostrar a tabela de tarefas
crontab -r # deletar a tabela de tarefas
crontab path/to/file.crontab # carregar a tabela de tarefas a partir de um arquivo

Ao chamar crontab -e será utilizado o editor especificado na variável de ambiente padrão EDITOR.

As 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 das entradas: minutos [1..60], horas [0..23], dias do mês [1..31], meses [1..12], dias da semana [0..6], onde 0 representa domingo. O último campo, o sexto, é uma string que será executada pelo interpretador de comandos padrão.

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

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

Ou por hífen:

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

O acesso dos usuários ao agendamento de tarefas é regulado nos arquivos POSIX cron.allow e cron.deny, que listam, respectivamente, os usuários com acesso ao crontab e os usuários sem acesso ao programa. A localização desses arquivos não é regulamentada por padrão.

Os programas executáveis, de acordo com o padrão, devem receber pelo menos quatro variáveis de ambiente:

  1. HOME — diretório pessoal do usuário.
  2. LOGNAME — nome de login do usuário.
  3. PATH — caminho pelo qual é possível encontrar utilitários padrão do sistema.
  4. SHELL — caminho para o interpretador de comandos utilizado.

É notável que o POSIX não menciona de onde vêm os valores dessas variáveis.

O Best-seller — Vixie cron 3.0pl1

O ancestral comum das versões populares do cron é o Vixie cron 3.0pl1, apresentado no mailing list comp.sources.unix em 1992. As principais características dessa versão serão discutidas em mais detalhes.

O Vixie cron é fornecido 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 dos usuários individuais, enquanto a utilitária crontab é responsável pela edição das tabelas de tarefas dos usuários.

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 ressalva de que, na sexta coluna, o nome do usuário sob o qual a tarefa será executada é especificado:

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

As tabelas de tarefas dos usuários comuns estão localizadas em /var/cron/tabs/username e utilizam a sintaxe comum. Ao executar a utilitária crontab em nome do usuário, esses arquivos são exatamente os que são editados.

O gerenciamento das listas de usuários que têm acesso ao crontab é feito nos arquivos /var/cron/allow e /var/cron/deny, onde é suficiente inserir o nome do usuário em uma linha separada.

Sintaxe avançada

Em comparação com o POSIX crontab, a solução de Paul Vixie contém várias modificações muito úteis na sintaxe das tabelas de tarefas da utilitária.

Foi disponibilizada uma nova sintaxe de tabelas: por exemplo, agora é possível especificar os dias da semana ou os meses por extenso (Seg, Ter e assim por diante):

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

É possível especificar o intervalo no qual as tarefas são executadas:

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

Intervalos e passos podem ser combinados:

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

Alternativas intuitivas à sintaxe comum são suportadas (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):

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

Ambiente de execução de tarefas

O Vixie cron permite alterar o ambiente das aplicações em execução.

As variáveis de ambiente USER, LOGNAME e HOME não são apenas fornecidas pelo daemon, mas são retiradas do arquivo passwd. A variável PATH recebe o valor ‘/usr/bin:/bin’ e SHELL é ‘/bin/sh’. Os valores de todas as variáveis, exceto LOGNAME, podem ser alterados nas tabelas de usuários.

Algumas variáveis de ambiente (principalmente SHELL e HOME) são utilizadas pelo próprio cron para executar a tarefa. Veja como pode parecer o uso do bash em vez do sh padrão para executar tarefas do usuário:

SHELL=/bin/bash
HOME=/tmp/
# exec será iniciado usando o bash em /tmp/
* * * * * /path/to/exec

No final, todas as variáveis de ambiente definidas na tabela (utilizadas pelo cron ou necessárias para o processo) serão passadas para a tarefa que é executada.

Para editar arquivos com a ferramenta crontab, é utilizado o editor indicado na variável de ambiente VISUAL ou EDITOR. Se no ambiente onde o crontab foi iniciado essas variáveis não estiverem definidas, é usado ‘/usr/ucb/vi’ (ucb é provavelmente a sigla para Universidade da Califórnia, Berkeley).

cron no Debian e Ubuntu

Os desenvolvedores do Debian e distribuições derivadas lançaram uma versão bastante modificada da versão Vixie cron 3.0pl1. Não há diferenças na sintaxe dos arquivos de tabela; para os usuários, este é o mesmo Vixie cron. As principais novas funcionalidades incluem suporte a syslog, SELinux e PAM.

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

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

Como interpretador de comandos padrão, ainda é utilizado /bin/sh, cujo papel em Debian é desempenhado por um pequeno shell compatível com POSIX dash, iniciado sem ler nenhuma configuração (em modo não interativo).

O cron nas versões mais recentes do Debian é iniciado através do systemd, e a configuração de inicialização pode ser verificada em /lib/systemd/system/cron.service. Não há nada de especial na configuração do serviço; qualquer gerenciamento mais avançado das tarefas pode ser realizado através das variáveis de ambiente, declaradas diretamente no crontab de cada um dos usuários.

cronie no RedHat, Fedora e CentOS

cronie — é um fork do Vixie cron na versão 4.1. Assim como no Debian, a sintaxe não foi alterada, mas foi adicionada a compatibilidade com PAM e SELinux, suporte a operações em cluster, monitoramento de arquivos através do inotify e outras funcionalidades.

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

O daemon é iniciado sob a gestão do systemd, e a configuração do serviço é /lib/systemd/system/crond.service.

Em distribuições baseadas no Red Hat, a inicialização padrão usa /bin/sh, onde o bash padrão é utilizado. É importante notar que ao iniciar tarefas cron através do /bin/sh, o shell bash é executado em modo compatível com POSIX e não lê nenhuma configuração adicional, operando em modo não interativo.

cronie no SLES e openSUSE

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

Estrutura do Vixie cron

Os descendentes modernos do cron, em comparação com o Vixie cron, não mudaram radicalmente, mas adquiriram novas funcionalidades que não são necessárias para compreender os princípios de funcionamento do programa. Muitas dessas extensões são mal estruturadas e confundem o código. No entanto, ler o código fonte original do cron, na versão de Paul Vixie, é um prazer.

Portanto, decidi realizar uma análise do funcionamento do cron usando como exemplo a versão comum a ambos os ramos de desenvolvimento do programa cron — Vixie cron 3.0pl1. Simplificarei os exemplos, eliminando os ifdef que complicam a leitura e omitindo detalhes secundários.

O funcionamento do daemon pode ser dividido em várias etapas:

  1. Inicialização do programa.
  2. Coleta e atualização da lista de tarefas a serem executadas.
  3. Execução do ciclo principal do cron.
  4. Início da tarefa.

Vamos analisá-los na ordem.

Inicialização

Ao iniciar, após a verificação dos argumentos, o cron configura os tratadores de sinal SIGCHLD e SIGHUP. O primeiro registra no log a saída do processo filho, e o segundo fecha o descritor de arquivo do arquivo de log:

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

O daemon cron no sistema sempre roda como um único processo, apenas na função de superusuário e a partir do diretório principal do cron. As seguintes chamadas criam um arquivo de lock com o PID do processo daemon, certificam-se de que o usuário está correto e mudam o diretório atual para o principal:

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

Um caminho padrão é definido, que será utilizado ao iniciar os processos:

setenv("PATH", _PATH_DEFPATH, 1);

Em seguida, o processo é 'daemonizado': cria uma cópia filha do processo por meio da chamada fork e uma nova sessão no processo filho (chamada setsid). Não há mais necessidade do processo pai — e ele termina:

switch (fork()) {
case -1:
    \/\* erro crítico e término *\/ 
    exit(0);
break;
case 0:
    \/\* processo filho *\/ 
    (void) setsid();
break;
default:
    \/\* processo pai termina *\/ 
    _exit(0);
}

O término do processo pai libera o lock no arquivo de lock. Além disso, é necessário atualizar o PID no arquivo para o do filho. Após isso, a base de tarefas é preenchida:

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

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

A seguir, o cron entra no loop principal de operação. Mas antes, vamos dar uma olhada na carga da lista de tarefas.

Coleta e atualização da lista de tarefas

A função load_database é responsável pela carga da lista de tarefas. Ela verifica o crontab do sistema principal e o diretório com os arquivos do usuário. Se os arquivos e diretório não mudaram, a lista de tarefas não é relida. Caso contrário, uma nova lista de tarefas começa a ser formada.

Carregando o arquivo do sistema com nomes especiais de arquivos e tabela:

/* если файл системной таблицы изменился, перечитываем */
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];
    \/\* não é necessário ler arquivos que começam com ponto *\/ 
    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 isso, o antigo banco de dados é substituído pelo novo.

Nos exemplos acima, a chamada da função process_crontab verifica a existência do usuário correspondente ao nome do arquivo da tabela (a menos que seja o superusuário), e então chama load_user. Esta última 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, ou a variável de ambiente (strings do tipo VAR=value) é definida pelas funções load_env / env_set, ou a descrição da tarefa é lida (* * * * * /path/to/exec) pela função load_entry.

A entidade entry retornada por load_entry é nossa tarefa, que é inserida na lista comum de tarefas. A função realiza uma análise detalhada do formato de tempo, mas estamos mais interessados em formar as variáveis de ambiente e os parâmetros de inicialização da tarefa:

/* пользователь и группа для запуска задачи берутся из 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 ciclo principal trabalha com a lista atual de tarefas.

Ciclo Principal

O cron original do Version 7 Unix funcionava de maneira bem simples: ele relia a configuração em um loop, executava tarefas do usuário root para o minuto atual e dormia até o início do próximo minuto. Essa abordagem simples consumia muitos recursos em máquinas antigas.

No SysV, foi proposta uma versão alternativa, onde o daemon dormia até o minuto mais próximo em que uma tarefa estava definida, ou por 30 minutos. Isso consumia menos recursos para reler a configuração e verificar tarefas, mas tornava difícil atualizar rapidamente a lista de tarefas.

O Vixie cron voltou a verificar a lista de tarefas a cada minuto, já que no final dos anos 80 os recursos nas máquinas Unix padrão haviam aumentado significativamente:

/* первичная загрузка задач */
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 execução das tarefas é realizada pela função cron_sleep, que chama as funções job_runqueue (iteração e execução de tarefas) e do_command (execução de cada tarefa individual). Esta última função merece uma análise mais detalhada.

Execução da Tarefa

A função do_command é escrita de acordo com o bom estilo Unix, ou seja, para a execução assíncrona da tarefa, ela faz um fork. O processo pai continua a iniciar tarefas, enquanto o filho se dedica à preparação do processo da tarefa:

switch (fork()) {
case -1:
    /* não foi possível executar o fork */
    break;
case 0:
    /* processo filho: mais uma vez tentamos capturar o principal lock */
    acquire_daemonlock(1);
    /* seguimos para a formação do processo da tarefa */
    child_process(e, u);
    /* ao terminar, o processo filho encerra sua execução */
    _exit(OK_EXIT);
    break;
default:
    /* processo pai continua a execução */
    break;
}

No child_process tem bastante lógica: ele aceita os fluxos de saída padrão e de erro, para depois enviá-los por e-mail (se a variável de ambiente MAILTO estiver especificada na tabela de tarefas), e, por fim, espera a conclusão do processo principal da tarefa.

O processo da tarefa é formado por mais um fork:

switch (vfork()) {
case -1:
    /* em caso de erro, a execução termina imediatamente */
    exit(ERROR_EXIT);
case 0:
    /* o processo-filho forma uma nova sessão, terminal, etc. */
    (void) setsid();

    /*
     * a partir daqui, configuração detalhada da saída do processo, vamos pular para brevidade
     *
     */

    /* mudança de diretório, usuário e grupo do usuário,
     * ou seja, o processo já não é mais superusuário
     * */
    setgid(e->gid);
    setuid(e->uid);
    chdir(env_get("HOME", e->envp));

    /* execução do próprio comando */
    {
        /* a variável de ambiente SHELL indica o interpretador para execução */
        char *shell = env_get("SHELL", e->envp);

        /* o processo é iniciado sem passar o ambiente do processo pai,
         * ou seja, exatamente como descrito na tabela de tarefas do usuário */
        execle(shell, shell, "-c", e->cmd, (char *)0, e->envp);

        /* erro — e o processo não foi iniciado? finalizando a execução */
        perror("execl");
        _exit(ERROR_EXIT);
    }
    break;
default:
    /* o próprio processo continua a execução: aguarda a conclusão e saída */
    break;
}

E, basicamente, esse é o cron. Algumas informações interessantes, como a consideração de usuários remotos, eu omiti, mas o principal eu expus.

Epílogo

O cron é uma programa surpreendentemente simples e útil, realizada nas melhores tradições do mundo Unix. Ele não faz nada além do necessário, mas desempenha sua função de maneira notável há várias décadas. A familiarização com o código da versão que vem com o Ubuntu levou não mais de uma hora, e eu consegui um imenso prazer! Espero que eu tenha conseguido compartilhar isso com você.

Não sei quanto a você, mas me entristece um pouco perceber que a programação moderna, com sua tendência ao excesso de complexidade e abstração, já há algum tempo não permite tal simplicidade.

Existem muitas alternativas modernas ao cron: os timers systemd permitem organizar sistemas complexos com dependências, enquanto o fcron pode regular de forma mais flexível o consumo de recursos das tarefas. Mas, pessoalmente, eu sempre estive satisfeito com os simples crontabs.

Em resumo, ame o Unix, utilize programas simples e não se esqueça de ler as manuais para sua plataforma!

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster