Este tipo de inicialização sincronizada resulta em um tipo de serialização de partes significativas do processo de boot. Não seria ótimo se pudéssemos nos livrar dos custos computacionais da serialização e das dependências?
Bem, eu digo que podemos. O que precisamos é entender o quê um daemon requer de outro e quais as causas do atraso de sua inicialização. Para daemons tradicionais do Unix há uma única resposta: eles precisam esperar até que um soquete seja oferecido pelo outro daemon para que possam se servir dele.
Usualmente esse é um soquete do tipo AF_UNIX disponível no sistema de arquivo; mas pode ser um soquete do tipo AF_INET[6], também. Por exemplo, clientes de D-Bus aguardam por /var/run/dbus/system_bus_socket para serem conectados. Enquanto clientes de syslog aguardam por /dev/log; os clientes de CUPS aguardam por um soquete como /var/run/cups/cups.sock e uma montagem NFS pode aguardar por um soquete como /var/run/rpcbind.sock. Isso é tudo que eles precisam para funcionar.
Agora, se isso é tudo que eles precisam, então, se criarmos um jeito de tornar esses soquetes disponíveis de modo cedo, e então aguardarmos (de algum modo) que o daemon seja carregado depois podemos aumentar a velocidade do processo de boot iniciando processos paralelos. Mas como podemos fazer isso?
Atualmente isso pode ser considerado simples em sistemas do tipo Unix: podemos criar uma lista de soquetes do tipo "antes" (before) e quando realmente iniciarmos o daemon responsável passamos esses soquetes para ele durante a chamada exec(). Assim, podemos criar TODOS os soquetes para TODOS os daemons em um único passo durante o processo Init.
Num segundo passo podemos executar TODOS os daemons de uma vez. Se um serviço precisava de outro, e este não está disponível, tudo bem, sua conexão entra em uma fila e esse cliente bloqueia uma única requisição. Além disto, esse método faz com que configurar dependências entre serviços não seja mais necessário. Uma vez iniciados TODOS os soquetes, TODOS os serviços podem considerar suas dependências satisfeitas.
Essa é a ideia central, vou dizer novamente com outras palavras e um exemplo: Se você inicia syslog e vários clientes dele ao mesmo tempo, o que acontece é que, no esquema proposto acima, TODAS as mensagens são enviadas para o soquete em /dev/log; os clientes NÃO tem que aguardar por syslog. Tão logo syslog esteja disponível ele assume essa fila processa uma mensagem por vez.
Basicamente, um buffer de soquete do kernel nos ajuda a maximizar a paralelização além de ordenar e sincronizar tudo, sem nenhum tipo de gerenciamento feito a partir do espaço usuário! E, se TODOS os soquetes estivessem disponíveis antes da inicialização dos daemons, o gerenciamento de dependências se tornaria dispensável ou, pelo menos, secundário.
Um daemon não precisa mais aguardar por outro (a menos que haja uma requisição síncrona, neste caso, o daemon deverá ser autoinicializável). Da perspectiva do daemon não há diferença e como consequência o gerenciamento de dependências se torna ainda mais desnecessário. No topo disto, e o que torna tudo mais robusto, é que os soquetes estão disponíveis independentemente dos daemons. No futuro, um daemon poderia ser escrito para rodar e sair, em seguida rodar novamente um novo ciclo de execução e novamente sair. O cliente não perceberia que o daemon é executado em ciclos.
Inicialmente, vamos deixar claro algumas coisas: o que proponho é um novo tipo de lógica? Não, certamente não é. O mais proeminente sistema da atualidade funciona deste modo. Ele é o sistema launchd da Apple. No MacOS a escuta por soquete de todos os daemons é feita por launchd.
Os serviços por si mesmos podem ser inicializados em paralelo e não há configuração de dependências. Essa é a razão pela qual MacOS possui tempos de inicialização fantásticos. Claro que a ideia central é mais antiga do que launchd, mas apenas o pessoal da Apple aplicou o conceito com eficiência.
No Unix, inetd tentou fazer isso. Todavia, o foco em inetd não eram os serviços locais, mas os serviços de rede. O suporte para soquetes do tipo AF_UNIX vieram posteriormente em reimplementações. Essas funcionalidades não faziam de inetd uma ferramenta de paralelização ou tornavam as dependências desnecessárias.
Os serviços de inetd foram utilizados primeiramente para soquetes TCP, de modo que cada conexão entrante iniciava uma nova instância de um daemon. Esse comportamento não é a receita para servidores de alto desempenho. Entretanto, inetd também suportava outro modo, onde uma instância simples era lançada e aceitava conexões posteriores (com a opção de wait/nowait em inet.conf).
Essa era uma opção mal documentada, infelizmente. Daemons baseados em modo per-connection deram a Inetd uma má reputação de ser lento, o que de certa forma é injusto.
Paralelizando" Serviços de Barramento (BUS)
Daemons modernos no
Linux tendem a prover serviços via D-Bus em vez de fazê-lo via soquetes AF_UNIX. Agora, a questão é: para esses serviços podemos aplicar a mesma lógica de paralelização em tempo de boot que propomos para os serviços baseados em soquetes? A resposta é: sim, nós podemos.
D-Bus já nos dá todos os ganchos (hooks) para isso. Usando a ativação por barramento um serviço pode ser inicializado na primeira vez que é acessado. A ativação por barramento nos dá o mínimo de sincronização por requisição, necessário para iniciar os provedores e os consumidores de serviços D-bus ao mesmo tempo. Por exemplo, se precisamos de Avahi como dependência de CUPS, podemos executar os dois ao mesmo tempo. E, se CUPS for iniciado primeiro D-Bus controla isso em uma fila até que Avahi esteja disponível.
Então, em resumo: a ativação de serviços baseada em soquete e baseado em barramento funcionam juntas para ativar TODOS os daemons em paralelo, sem qualquer necessidade de sincronização. Essa configuração permite atrasar a inicialização de serviços quando esse é raramente utilizado. E, se isso não é uma coisa grande, então eu não sei o que seria!
"Paralelizando" Atividades do Sistema de Arquivos
Se olhar para os processos de disco em tempo de boot, verá que a serialização e a sincronização gerada pelo start-up de vários daemons é grande. A maior parte desses gargalos vem de atividades como montagem, checagem, ajuste de quotas, etc. Durante o boot, muito tempo é perdido até que todos os dispositivos listados no arquivo /etc/fstab sejam ajustados.
Os serviços somente podem ser inicializados depois que essas atividades de disco terminaram. Podemos melhorar isso? Acontece que podemos. O programador Harald Hoyer [1] veio com uma ideia chamada autofs para melhorar isso. Do mesmo modo que uma chamada connect() mostra o serviço que está interessado em outro, uma chamada open() ou outra similar, mostra que serviço está interessado em determinado arquivo ou sistema de arquivos.
Então, a fim de melhorar podemos paralelizar e fazer esses aplicativos aguardar somente se o sistema de arquivos não estiver disponível. Fazemos isso configurando esses pontos de montagem em modo automático com autofs. Enquanto esse sistema de arquivos não está disponível, todo acesso será colocado em uma fila no kernel e os processos serão bloqueados para acesso somente a um único daemon. Desta forma, podemos inicializar daemons mesmo que ainda faltem sistemas de arquivos para serem montados.
Paralelizar atividades do sistema de arquivos e de serviços não faz sentido para o diretório raiz, já que todos os binários são armazenados nesta estrutura. Todavia, para partes da árvore como o /home, que usualmente contém muitos arquivos e é muito grande ou até mesmo criptografado, isso pode aumentar o tempo de boot consideravelmente. Ainda podemos mencionar que sistemas de arquivo virtuais como procfs ou sysfs não podem ser montados via autofs.
Eu não ficaria surpreso se alguns leitores acharem que a integração de algo como autofs com um sistema init poderia fragilizar ou fazer as coisas quebrarem mais facilmente. Posso afirmar que esse contorno funciona muito bem. Utilizar autofs onde as coisas simplesmente significam criar um ponto de montagem e sem ter que fornecer algo mais, funcionam bem.
Se a aplicação tenta acessar um sistema de arquivos em autofs e leva muito tempo para substituir a fila pelo sistema real, ele pode suportar uma interrupção, isso significa que você pode cancelar a requisição com segurança, via C-c.
Se o ponto de montagem realmente falhar (por falha real de disco) então dizemos para autofs retornar um código de erro e deixar tudo com o estado de limpo. Na prática autofs se mostrou uma excelente ideia quando implementado para a coisa certa e do modo certo.