
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 não descreve de forma alguma o comportamento do daemon e formaliza apenas o programa do usuário . 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 arquivoAo 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.logOs 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 -cOu por hífen:
# задача, выполняемая в каждую из первых десяти минут каждого часа
0-9 * * * * /path/to/exec -a -b -cO 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:
- HOME — diretório pessoal do usuário.
- LOGNAME — nome de login do usuário.
- PATH — caminho pelo qual é possível encontrar utilitários padrão do sistema.
- 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/execAs 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/execIntervalos e passos podem ser combinados:
# Запускается с шагом в две минуты в первых десять минут каждого часа
0-10/2 * * * * /path/to/execAlternativas intuitivas à sintaxe comum são suportadas (reboot, yearly, annually, monthly, weekly, daily, midnight, hourly):
# Запускается после перезагрузки системы
@reboot /exec/on/reboot
# Запускается раз в день
@daily /exec/daily
# Запускается раз в час
@hourly /exec/dailyAmbiente 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 . 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/execNo 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 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 , e .
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 , 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
— é 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:
- Inicialização do programa.
- Coleta e atualização da lista de tarefas a serem executadas.
- Execução do ciclo principal do cron.
- 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
