Corrigindo "Text file busy"

Corrigindo "Text file busy"

O que significa "Text file busy"?

Conclusao: Voce esta tentando sobrescrever ou truncar o arquivo que sustenta um programa atualmente em execucao (ou uma biblioteca compartilhada em uso). O kernel retorna ETXTBSY (errno 26). Pare o processo em execucao, ou mude de "sobrescrever" para "substituir" (rename) e o erro desaparece.

Geralmente aparece ao copiar, compilar ou fazer deploy de um binario:

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

O "text" em Text file busy e o antigo termo Unix para o segmento de texto de um programa (seu codigo executavel). Nao tem nada a ver com o arquivo conter texto legivel -- significa "o arquivo que contem codigo executavel esta em uso".

O kernel retorna esse erro principalmente para estas operacoes:

  • Abrir um binario em execucao para escrita (sobrescrever com cp, redirecionar com > file)
  • Sobrescrever uma biblioteca compartilhada (.so) em uso (ja mapeada na memoria com mmap)
  • Tentar execve() em um arquivo que ainda esta aberto para escrita (uma condicao de corrida no build)

Text file busy nao e o mesmo que Permission denied ou Read-only file system. As permissoes e o sistema de arquivos podem estar perfeitamente corretos -- a escrita e rejeitada unicamente porque o arquivo esta sendo executado. Interpretar o erro errado leva a correcao errada.

Por que um binario em execucao nao pode ser sobrescrito?

Conclusao: O Linux protege o inode de um executavel em execucao (e qualquer biblioteca compartilhada carregada) negando escritas. Modificar codigo em execucao no meio do caminho causaria crashes ou comportamento indefinido, entao o kernel bloqueia antecipadamente com ETXTBSY.

Quando um programa inicia, o kernel mapeia o executavel na memoria e o executa. Paginas sao carregadas sob demanda, entao o arquivo no disco continua sendo referenciado durante toda a vida do processo.

Se alguem reescreve o arquivo agora, uma pagina ainda nao carregada pode mudar para outra coisa e quebrar a consistencia. 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 contem codigo executavel tambem, entao recebem a mesma protecao.

A protecao funciona em ambas as direcoes. Se voce abrir um arquivo para escrita e depois tentar execve() nesse mesmo arquivo, voce tambem recebe ETXTBSY. Um script de build que esquece de fechar seu arquivo de saida antes de executa-lo encontra exatamente esse caso.

Note que shell scripts geralmente nao disparam esse erro. Um script e apenas lido como dados por um interpretador como o bash; ele nunca se torna uma imagem executavel protegida. ETXTBSY afeta principalmente binarios ELF e bibliotecas compartilhadas.

Como encontrar o processo que segura o arquivo?

Conclusao: Use lsof <arquivo> ou fuser <arquivo> para listar o processo executando aquele binario. O PID que ele exibe e o que voce precisa parar. Se ele e gerenciado por um servico, pare com systemctl em vez de mata-lo diretamente.

Inspecionar por arquivo com lsof

Passe o caminho que voce quer sobrescrever para o lsof, e ele mostra qual processo tem o arquivo aberto ou em execucao.

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 executavel (codigo)". Aquele myapp (PID 4821) e o culpado. Para uma biblioteca compartilhada ele aparece como mem (mmap'd).

Obter o PID rapidamente com fuser

Se voce quer apenas o PID rapidamente, use fuser.

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

O e no final significa executavel sendo executado. Adicione fuser -v para ver tambem USER / COMMAND.

Se quem segura o arquivo e um servico ou daemon de longa duracao, mata-lo diretamente com kill pode disparar um auto-restart ou deixa-lo em um estado de atualizacao parcial. Pare servicos com systemctl stop e so considere kill para processos iniciados manualmente.

Como substituir com seguranca em vez de sobrescrever?

Conclusao: Se voce nao pode parar o processo em execucao, 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 diretorio, entao o processo em execucao mantem seu inode antigo e nunca colide com o novo binario.

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 esta em execucao
  • mv new /usr/local/bin/myapp -- renomeia um novo inode para myapp e substitui o nome antigo -> nunca toca o inode em execucao, entao funciona

O ponto chave e manter dentro do mesmo sistema de arquivos. Executar mv de /tmp (um sistema de arquivos diferente) se transforma em copia-mais-exclusao internamente e pode tomar o caminho de sobrescrita. O padrao confiavel e colocar o novo arquivo no mesmo diretorio, 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 execucao continua executando o binario antigo (desapareceu do diretorio, mas seu inode ainda esta vivo) e pega o novo na proxima inicializacao.

install e ainda mais limpo

install remove o arquivo existente e cria um novo (ele nunca sobrescreve o inode em execucao no lugar), entao evita ETXTBSY enquanto define permissoes e propriedade de uma vez -- ideal para deploy.

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

Se voce precisa escrever no mesmo inode, remova-o primeiro

Exclua a entrada do diretorio com rm, depois escreva -- torna-se uma criacao nova e evita ETXTBSY (unlink em si e permitido mesmo durante a execucao).

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

A opcao mais confiavel e "pare o processo, depois sobrescreva": para um servico, systemctl stop myapp && cp ... && systemctl start myapp. Use a abordagem rename/install quando precisar trocar sem downtime -- essa divisao torna a decisao rapida.

Como evitar que se repita em deploys e builds?

Conclusao: Faca os deploys como "substituicao atomica via rename", nao "sobrescrever". Em builds, feche confiavelmente o file descriptor de saida e nunca escreva diretamente no binario que esta em execucao no momento.

Pontos de verificacao para prevenir recorrencia:

  • Elimine escritas diretas com cp -f em scripts de deploy -- prepare sob um nome temporario, depois troque com mv / install. Muitas ferramentas de deploy (substituindo a saida de um go build, etc.) ja funcionam assim
  • Pare, atualize, reinicie servicos em execucao por padrao -- se voce precisa de atualizacoes sem downtime, use a abordagem rename, ou considere o ExecReload do systemd / socket activation
  • Feche o fd de saida 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 atualizacoes de bibliotecas compartilhadas da mesma forma -- nunca sobrescreva um .so em uso; substitua com rename. Gerenciadores de pacotes (apt/dnf) fazem isso internamente, entao evite cp manual sobre um .so

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

Resumo / Proximas leituras