Modificando o binário durante a execução

10

Muitas vezes me deparo com a situação no desenvolvimento, onde estou executando um arquivo binário, digamos a.outem segundo plano, pois ele faz algum trabalho demorado. Enquanto isso, faço alterações no código C, que é produzido a.oute compilado a.outnovamente. Até agora, não tive problemas com isso. O processo em execução a.outcontinua normalmente, nunca falha e sempre executa o código antigo a partir do qual foi iniciado originalmente.

No entanto, digamos que a.outera um arquivo enorme, talvez comparável ao tamanho da RAM. O que aconteceria neste caso? E diga que está vinculado a um arquivo de objeto compartilhado, libblas.soe se eu modificasse libblas.sodurante o tempo de execução? O que aconteceria?

Minha principal pergunta é: o sistema operacional garante que, quando eu executo a.out, o código original sempre será executado normalmente, conforme o binário original , independentemente do tamanho do binário ou dos .soarquivos aos quais ele se vincula, mesmo quando esses arquivos .oe .soarquivos são modificados durante tempo de execução?

Sei que existem perguntas que abordam problemas semelhantes: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -at-once O que acontece se você editar um script durante a execução? Como é possível fazer uma atualização ao vivo enquanto um programa está em execução?

O que me ajudou a entender um pouco mais sobre isso, mas não acho que eles estejam perguntando exatamente o que eu quero, que é uma regra geral para as consequências de modificar um binário durante a execução

texasflood
fonte
Para mim, as perguntas que você vinculou (principalmente a Stack Overflow) já fornecem ajuda significativa para entender essas consequências (ou a ausência delas). Como o kernel carrega seu programa nas regiões / segmentos de texto na memória , ele não deve ser afetado pelas alterações feitas no subsistema de arquivos.
John WH Smith
@JohnWHSmith No Stackoverflow, a resposta principal diz if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, então eu tive a impressão de que se o seu binário é enorme, se parte do seu binário fica sem memória RAM, mas é necessário novamente, ele é "recarregado da fonte" - portanto, quaisquer alterações no o .(s)oarquivo será refletido durante a execução. Mas é claro que podem ter entendido mal - que é por isso que estou fazendo esta pergunta mais específica
texasflood
@JohnWHSmith Também a segunda resposta diz: No, it only loads the necessary pages into memory. This is demand paging.Então, eu fiquei com a impressão de que o que pedi não pode ser garantido.
precisa saber é o seguinte

Respostas:

11

Embora a pergunta sobre estouro de pilha parecesse ser suficiente no começo, entendo, pelos seus comentários, por que você ainda tem alguma dúvida sobre isso. Para mim, esse é exatamente o tipo de situação crítica envolvida quando os dois subsistemas UNIX (processos e arquivos) se comunicam.

Como você deve saber, os sistemas UNIX geralmente são divididos em dois subsistemas: o subsistema de arquivos e o subsistema de processo. Agora, a menos que seja instruído de outra forma por meio de uma chamada do sistema, o kernel não deve ter esses dois subsistemas interagindo entre si. No entanto, há uma exceção: o carregamento de um arquivo executável nas regiões de texto de um processo . Claro, pode-se argumentar que esta operação também é desencadeada por uma chamada de sistema ( execve), mas isso geralmente é conhecido por ser o único caso em que o subsistema de processo faz uma solicitação implícita para o subsistema de arquivo.

Como o subsistema de processo naturalmente não tem como manipular arquivos (caso contrário, não faria sentido dividir tudo em dois), ele precisava usar o que o subsistema de arquivos fornecer para acessar arquivos. Isso também significa que o subsistema de processo é enviado para qualquer medida que o subsistema de arquivos tome em relação à edição / exclusão de arquivos. Nesse ponto, eu recomendaria ler a resposta de Gilles para essa pergunta de perguntas e respostas . O resto da minha resposta é baseada nessa mais geral de Gilles.

A primeira coisa a ser observada é que internamente, os arquivos são acessíveis apenas através de inodes . Se o caminho for dado ao kernel, seu primeiro passo será convertê-lo em um inode para ser usado em todas as outras operações. Quando um processo carrega um executável na memória, ele o faz através do seu inode, que foi fornecido pelo subsistema de arquivos após a conversão de um caminho. Os inodes podem estar associados a vários caminhos (links) e os programas podem excluir apenas links. Para excluir um arquivo e seu inode, a terra do usuário deve remover todos os links existentes para esse inode e garantir que ele não seja completamente utilizado. Quando essas condições forem atendidas, o kernel excluirá automaticamente o arquivo do disco.

Se você der uma olhada na parte executável substituta da resposta de Gilles, verá que, dependendo de como editar / excluir o arquivo, o kernel reagirá / se adaptará de maneira diferente, sempre através de um mecanismo implementado no subsistema de arquivos.

  • Se você tentar a estratégia um ( abrir / truncar para zero / gravar ou abrir / gravar / truncar para novo tamanho ), verá que o kernel não se incomodará em lidar com sua solicitação. Você receberá um erro 26: Arquivo de texto ocupado ( ETXTBSY). Sem consequências.
  • Se você tentar a estratégia dois, o primeiro passo é excluir o seu executável. No entanto, como está sendo usado por um processo, o subsistema de arquivos entra em ação e impede que o arquivo (e seu inode) seja realmente excluído do disco. A partir desse ponto, a única maneira de acessar o conteúdo do arquivo antigo é fazê-lo por meio do inode, que é o que o subsistema de processo faz sempre que precisa carregar novos dados nas seções de texto (internamente, não há sentido em usar caminhos, exceto ao traduzi-los em inodes). Mesmo que você tenha desvinculadoo arquivo (removido todos os caminhos), o processo ainda pode usá-lo como se você não tivesse feito nada. Criar um novo arquivo com o caminho antigo não muda nada: o novo arquivo receberá um inode completamente novo, do qual o processo em execução não tem conhecimento.

As estratégias 2 e 3 também são seguras para executáveis: embora a execução de executáveis ​​(e bibliotecas carregadas dinamicamente) não sejam arquivos abertos no sentido de ter um descritor de arquivo, eles se comportam de maneira muito semelhante. Enquanto algum programa estiver executando o código, o arquivo permanecerá em disco, mesmo sem uma entrada de diretório.

  • A estratégia três é bastante semelhante, pois a mvoperação é atômica. Provavelmente, isso exigirá o uso da renamechamada do sistema e, como os processos não podem ser interrompidos enquanto no modo kernel, nada pode interferir nessa operação até que ela seja concluída (com ou sem êxito). Novamente, não há alteração no inode do arquivo antigo: um novo é criado e os processos em execução não terão conhecimento dele, mesmo que tenham sido associados a um dos links do inode antigo.

Com a estratégia 3, a etapa de mover o novo arquivo para o nome existente remove a entrada de diretório que leva ao conteúdo antigo e cria uma entrada de diretório que leva ao novo conteúdo. Isso é feito em uma operação atômica, portanto, essa estratégia tem uma grande vantagem: se um processo abrir o arquivo a qualquer momento, ele verá o conteúdo antigo ou o novo conteúdo - não há risco de obter conteúdo misto ou o arquivo não existir.

Recompilando um arquivo : ao usar gcc(e o comportamento provavelmente é semelhante para muitos outros compiladores), você está usando a estratégia 2. Você pode ver isso executando um stracedos processos do seu compilador:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • O compilador detecta que o arquivo já existe através das chamadas state do lstatsistema.
  • O arquivo está desvinculado . Aqui, embora não seja mais acessível por meio do nome a.out, seu inode e seu conteúdo permanecem no disco, enquanto estiverem sendo usados ​​por processos já em execução.
  • Um novo arquivo é criado e tornado executável sob o nome a.out. Este é um novo inode e um conteúdo totalmente novo, com os quais os processos em execução não se importam.

Agora, quando se trata de bibliotecas compartilhadas, o mesmo comportamento será aplicado. Enquanto um objeto de biblioteca for usado por um processo, ele não será excluído do disco, não importa como você altere seus links. Sempre que algo tiver que ser carregado na memória, o kernel fará isso através do inode do arquivo e, portanto, ignorará as alterações feitas nos seus links (como associá-los a novos arquivos).

John WH Smith
fonte
Resposta fantástica e detalhada. Isso explica minha confusão. Então, estou correto ao supor que, como o inode ainda está disponível, os dados do arquivo binário original ainda estão no disco e, portanto, usando dfpara calcular o número de bytes livres no disco está errado, pois não leva inodes que todos os links do sistema de arquivos removidos foram levados em consideração? Então eu devo usar df -i? (Esta é apenas uma curiosidade técnica, eu realmente não precisa saber o uso do disco exato!)
texasflood
1
Apenas para esclarecer para futuros leitores - minha confusão foi que pensei em execução, todo o binário seria carregado na RAM; portanto, se a RAM era pequena, parte do binário deixaria a RAM e teria que ser recarregada do disco - o que causar problemas se você alterou o arquivo. Mas a resposta deixou claro que o binário nunca é realmente removido do disco, mesmo que você rmou mvo inode para o arquivo original não seja removido até que todos os processos removam o link para esse inode.
texasflood
@texasflood Exatamente. Depois que todos os caminhos foram removidos, nenhum novo processo ( dfincluído) pode obter informações sobre o inode. Quaisquer novas informações que você encontrar estão relacionadas ao novo arquivo e ao novo inode. O ponto principal aqui é que o subsistema de processo não tem interesse nesse problema, portanto as noções de gerenciamento de memória (paginação por demanda, troca de processos, falhas de página, ...) são completamente irrelevantes. Este é um problema do subsistema de arquivos e é resolvido pelo subsistema de arquivos. O subsistema de processo não se preocupa com isso, não é para isso que serve aqui.
John WH Smith
@texasflood Uma observação sobre df -i: essa ferramenta provavelmente recupera informações do superbloco do fs ou seu cache, o que significa que ela pode incluir o inode do binário antigo (para o qual todos os links foram excluídos). Isso não significa que novos processos sejam livres para usar esses dados antigos.
John WH Smith
2

Meu entendimento é que, devido ao mapeamento de memória de um processo em execução, o kernel não permitiria atualizar uma parte reservada do arquivo mapeado. Eu acho que, no caso de um processo estar em execução, todo o seu arquivo é reservado e, portanto, atualizá-lo porque você compilou uma nova versão do seu código-fonte na verdade resulta na criação de um novo conjunto de inodes. Em resumo, as versões mais antigas do seu executável permanecem acessíveis no disco através de eventos de falha de página. Portanto, mesmo se você atualizar um arquivo enorme, ele deverá permanecer acessível e o kernel ainda deverá ver a versão intocada enquanto o processo estiver em execução. Os inodes do arquivo original não devem ser reutilizados enquanto o processo estiver em execução.

Claro que isso tem que ser confirmado.


fonte
2

Esse nem sempre é o caso ao substituir um arquivo .jar. Os recursos jar e alguns carregadores de classe de reflexão em tempo de execução não são lidos do disco até que o programa solicite explicitamente as informações.

Isso é apenas um problema, porque um jar é simplesmente um arquivo morto, e não um único executável que é mapeado na memória. Isso é um pouco parado, mas ainda é um desdobramento da sua pergunta e algo com que eu me atirei no pé.

Então, para executáveis: sim. Para arquivos jar: talvez (dependendo da implementação).

Zhro
fonte