Corrigindo "Text file busy"

Corrigindo "Text file busy"

O que significa "Text file busy"?

Conclusão: Você está tentando sobrescrever ou truncar o arquivo que sustenta um programa atualmente em execução (ou uma biblioteca compartilhada em uso). O kernel retorna ETXTBSY (errno 26). Pare o processo em execução, ou mude de "sobrescrever" para "substituir" (rename) e o erro desaparece.

Geralmente aparece ao copiar, compilar ou fazer deploy de um binário:

cp: cannot create regular file '/usr/local/bin/myapp': Text file busy

O "text" em Text file busy é o antigo termo Unix para o segmento de texto de um programa (seu código executável). Não tem nada a ver com o arquivo conter texto legível -- significa "o arquivo que contém código executável está em uso".

O kernel retorna esse erro principalmente para estas operações:

  • Abrir um binário em execução para escrita (sobrescrever com cp, redirecionar com > file)
  • Sobrescrever uma biblioteca compartilhada (.so) em uso (já mapeada na memória com mmap)
  • Tentar execve() em um arquivo que ainda está aberto para escrita (uma condição de corrida no build)

Text file busy não é o mesmo que Permission denied ou Read-only file system. As permissões e o sistema de arquivos podem estar perfeitamente corretos -- a escrita é rejeitada unicamente porque o arquivo está sendo executado. Interpretar o erro errado leva à correção errada.

Por que um binário em execução não pode ser sobrescrito?

Conclusão: O Linux protege o inode de um executável em execução (e qualquer biblioteca compartilhada carregada) negando escritas. Modificar código em execução no meio do caminho causaria crashes ou comportamento indefinido, então o kernel bloqueia antecipadamente com ETXTBSY.

Quando um programa inicia, o kernel mapeia o executável na memória e o executa. Páginas são carregadas sob demanda, então o arquivo no disco continua sendo referenciado durante toda a vida do processo.

Se alguém reescreve o arquivo agora, uma página ainda não carregada pode mudar para outra coisa e quebrar a consistência. Para prevenir isso, o kernel marca o inode como "sendo executado" e rejeita aberturas para escrita (O_WRONLY / O_RDWR) e truncamento com ETXTBSY. Bibliotecas compartilhadas contêm código executável também, então recebem a mesma proteção.

A proteção funciona em ambas as direções. Se você abrir um arquivo para escrita e depois tentar execve() nesse mesmo arquivo, você também recebe ETXTBSY. Um script de build que esquece de fechar seu arquivo de saída antes de executá-lo encontra exatamente esse caso.

Note que shell scripts geralmente não disparam esse erro. Um script é apenas lido como dados por um interpretador como o bash; ele nunca se torna uma imagem executável protegida. ETXTBSY afeta principalmente binários ELF e bibliotecas compartilhadas.

Como encontrar o processo que segura o arquivo?

Conclusão: Use lsof <arquivo> ou fuser <arquivo> para listar o processo executando aquele binário. O PID que ele exibe é o que você precisa parar. Se ele é gerenciado por um serviço, pare com systemctl em vez de matá-lo diretamente.

Inspecionar por arquivo com lsof

Passe o caminho que você quer sobrescrever para o lsof, e ele mostra qual processo tem o arquivo aberto ou em execução.

lsof /usr/local/bin/myapp
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
myapp    4821 deploy  txt    REG  259,1  6291456 131080 /usr/local/bin/myapp

O txt na coluna FD significa "em uso como texto executável (código)". Aquele myapp (PID 4821) é o culpado. Para uma biblioteca compartilhada ele aparece como mem (mmap'd).

Obter o PID rapidamente com fuser

Se você quer apenas o PID rapidamente, use fuser.

fuser /usr/local/bin/myapp
/usr/local/bin/myapp: 4821e

O e no final significa executável sendo executado. Adicione fuser -v para ver também USER / COMMAND.

Se quem segura o arquivo é um serviço ou daemon de longa duração, matá-lo diretamente com kill pode disparar um auto-restart ou deixá-lo em um estado de atualização parcial. Pare serviços com systemctl stop e só considere kill para processos iniciados manualmente.

Como substituir com segurança em vez de sobrescrever?

Conclusão: Se você não pode parar o processo em execução, pare de usar cp para sobrescrever e use mv (rename) ou install para trocar por um novo arquivo. Rename cria um novo inode e re-aponta a entrada do diretório, então o processo em execução mantém seu inode antigo e nunca colide com o novo binário.

Por que mv funciona mas cp falha

  • cp new /usr/local/bin/myapp -- abre o inode existente com O_TRUNC e reescreve no lugar -> ETXTBSY porque está em execução
  • mv new /usr/local/bin/myapp -- renomeia um novo inode para myapp e substitui o nome antigo -> nunca toca o inode em execução, então funciona

O ponto chave é manter dentro do mesmo sistema de arquivos. Executar mv de /tmp (um sistema de arquivos diferente) se transforma em cópia-mais-exclusão internamente e pode tomar o caminho de sobrescrita. O padrão confiável é colocar o novo arquivo no mesmo diretório, depois renomear.

# Baixar/compilar no mesmo diretorio, depois renomear
cp myapp.new /usr/local/bin/myapp.new   # preparar sob um nome temporario primeiro
mv /usr/local/bin/myapp.new /usr/local/bin/myapp   # troca atomica

O processo em execução continua executando o binário antigo (desapareceu do diretório, mas seu inode ainda está vivo) e pega o novo na próxima inicialização.

install é ainda mais limpo

install remove o arquivo existente e cria um novo (ele nunca sobrescreve o inode em execução no lugar), então evita ETXTBSY enquanto define permissões e propriedade de uma vez -- ideal para deploy.

sudo install -m 0755 -o root -g root myapp.new /usr/local/bin/myapp

Se você precisa escrever no mesmo inode, remova-o primeiro

Exclua a entrada do diretório com rm, depois escreva -- torna-se uma criação nova e evita ETXTBSY (unlink em si é permitido mesmo durante a execução).

sudo rm /usr/local/bin/myapp        # unlink funciona mesmo durante a execucao
sudo cp myapp.new /usr/local/bin/myapp

A opção mais confiável é "pare o processo, depois sobrescreva": para um serviço, systemctl stop myapp && cp ... && systemctl start myapp. Use a abordagem rename/install quando precisar trocar sem downtime -- essa divisão torna a decisão rápida.

Como evitar que se repita em deploys e builds?

Conclusão: Faça os deploys como "substituição atômica via rename", não "sobrescrever". Em builds, feche confiavelmente o file descriptor de saída e nunca escreva diretamente no binário que está em execução no momento.

Pontos de verificação para prevenir recorrência:

  • Elimine escritas diretas com cp -f em scripts de deploy -- prepare sob um nome temporário, depois troque com mv / install. Muitas ferramentas de deploy (substituindo a saída de um go build, etc.) já funcionam assim
  • Pare, atualize, reinicie serviços em execução por padrão -- se você precisa de atualizações sem downtime, use a abordagem rename, ou considere o ExecReload do systemd / socket activation
  • Feche o fd de saída antes de executar um novo build -- encadear make && ./out falha com ETXTBSY se o build deixou um handle de escrita aberto. Feche explicitamente ou divida em etapas separadas
  • Trate atualizações de bibliotecas compartilhadas da mesma forma -- nunca sobrescreva um .so em uso; substitua com rename. Gerenciadores de pacotes (apt/dnf) fazem isso internamente, então evite cp manual sobre um .so

Em alguns sistemas de arquivos de rede como NFS, o comportamento do ETXTBSY nem sempre é consistente para um binário executando em um host diferente. Ao atualizar um executável em armazenamento compartilhado, o caminho seguro é parar o processo em cada host antes de fazer a troca.

Resumo / Próximas leituras