Por que é possível mover um programa em execução no Ubuntu?

24

Acabei de perceber que sou capaz de mover um programa ativo em execução para um diretório diferente. Na minha experiência, isso não foi possível em MacOs ou Windows. Como isso funciona no Ubuntu?

Edit: Eu pensei que não era possível no Mac, mas aparentemente é possível como verificar comentários. Talvez apenas não seja possível no Windows. Obrigado por todas as respostas.

n0.ob
fonte
2
Um truque entre sites: stackoverflow.com/a/196910/1394393 .
precisa saber é
1
Você não pode rename(2)executar um executável no OS X? O que acontece, você recebe EBUSYou algo assim? Por que isso não funciona? A página de manual rename (2) não documenta ETXTBUSYessa chamada do sistema e apenas fala sobre EBUSYa possibilidade de renomear diretórios, então eu não sabia que um sistema POSIX poderia impedir a renomeação de executáveis.
22616 Peter Cordes
3
Os aplicativos macOS também podem ser movidos enquanto são executados, mas não na lixeira. Presumo que alguns aplicativos possam errar depois disso, por exemplo, se eles armazenarem URLs de arquivos em seus recursos binários ou agrupados em algum lugar como uma variável, em vez de gerar esse URL via NSBundle et al. Eu suspeito que seja a conformidade POSIX do macOS.
Constantino Tsarouhas
1
Na verdade, ele funciona como o Linux pretende, você deve saber o que está fazendo. : P
userDepth 21/08/16
2
Eu acho que outra maneira de pensar sobre isso é, por que não seria possível? Só porque o Windows não permite, não significa necessariamente que fundamentalmente não é possível por causa de como os processos funcionam ou algo assim.
Thomas

Respostas:

32

Deixe-me explicar.

Quando você executa um executável, uma sequência de chamadas do sistema é executada, principalmente fork()e execve():

  • fork()cria um processo filho do processo de chamada, que é (principalmente) uma cópia exata do pai, ambos ainda executando o mesmo executável (usando páginas de memória de cópia na gravação, para que seja eficiente). Ele retorna duas vezes: no pai, ele retorna o PID filho. No filho, ele retorna 0. Normalmente, o processo filho chama execve imediatamente:

  • execve()toma um caminho completo para o executável como argumento e substitui o processo de chamada pelo executável. Nesse ponto, o processo recém-criado obtém seu próprio espaço de endereço virtual, ou seja, memória virtual, e a execução começa no ponto de entrada (em um estado especificado pelas regras da plataforma ABI para novos processos).

Nesse momento, o carregador ELF do kernel mapeou os segmentos de texto e dados do executável na memória, como se tivesse usado a mmap()chamada do sistema (com mapeamentos compartilhados somente leitura e leitura / gravação privados, respectivamente). O BSS também é mapeado como se estivesse com MAP_ANONYMOUS. (BTW, estou ignorando o vínculo dinâmico aqui por simplicidade: o vinculador dinâmico open()es e mmap()todas as bibliotecas dinâmicas antes de pular para o ponto de entrada do executável principal.)

Somente algumas páginas são realmente carregadas na memória do disco antes que um recém-exec () ed comece a executar seu próprio código. As páginas adicionais serão paginadas conforme necessário, se / quando o processo tocar essas partes do seu espaço de endereço virtual. (Pré-carregar qualquer página de código ou dados antes de começar a executar o código do espaço do usuário é apenas uma otimização de desempenho.)


O arquivo executável é identificado pelo inode no nível inferior. Depois que o arquivo começa a ser executado, o kernel mantém o conteúdo do arquivo intacto pela referência do inode, não pelo nome do arquivo, como para descritores de arquivos abertos ou mapeamentos de memória suportados por arquivos. Assim, você pode mover o executável facilmente para outro local do sistema de arquivos ou mesmo para um sistema de arquivos diferente. Como uma observação lateral, para verificar as várias estatísticas do processo, você pode espiar no /proc/PIDdiretório (PID é o ID do processo do processo fornecido). Você pode até abrir o arquivo executável como /proc/PID/exe, mesmo que tenha sido desvinculado do disco.


Agora vamos cavar o movimento:

Quando você move um arquivo dentro de um mesmo sistema de arquivos, a chamada do sistema que é executada é rename(), que apenas renomeia o arquivo para outro nome, o inode do arquivo permanece o mesmo.

Enquanto entre dois sistemas de arquivos diferentes, duas coisas acontecem:

  • O conteúdo do arquivo foi copiado primeiro para o novo local, por read()ewrite()

  • Depois disso, o arquivo é desvinculado do diretório de origem unlink()e, obviamente, o arquivo receberá um novo inode no novo sistema de arquivos.

rmestá na verdade apenas unlink()inserindo o arquivo fornecido na árvore de diretórios, portanto, ter a permissão de gravação no diretório garante o direito de remover qualquer arquivo desse diretório.

Agora, por diversão, imagine o que acontece quando você move arquivos entre dois itens e não tem permissão para unlink()o arquivo da fonte?

Bem, o arquivo será copiado para o destino primeiro ( read(), write()) e depois unlink()falhará devido à permissão insuficiente. Portanto, o arquivo permanecerá nos dois sistemas de arquivos !!

heemail
fonte
5
Sua memória física e virtual é um tanto confusa. Sua descrição da maneira como o programa é carregado na memória física é imprecisa. A chamada do sistema exec não copia as várias seções de um executável na memória física, mas apenas carrega a que é necessária para iniciar o processo. Depois disso, as páginas necessárias são carregadas sob demanda, possivelmente muito tempo depois. Os bytes do arquivo executável fazem parte da memória virtual do processo e podem ser lidos e possivelmente lidos novamente durante toda a vida útil do processo.
Jlliagre
@ jlliagre Editado, espero que esteja esclarecido agora. Obrigado.
heemayl
6
A declaração "O processo não está mais usando o sistema de arquivos" ainda é questionável.
Jlliagre
2
O entendimento básico de que um determinado arquivo no sistema de arquivos não é identificado diretamente pelo nome do arquivo deve ser muito mais claro.
Thorbjørn Ravn Andersen
2
Ainda existem imprecisões na sua atualização. As chamadas mmape unmapsystem não são usadas para carregar e descarregar as páginas sob demanda; as páginas são carregadas pelo kernel ao acessá-las, geram uma falha de página; as páginas são descarregadas da memória quando o sistema operacional considera que a RAM seria melhor usada para outra coisa. Nenhuma chamada do sistema está envolvida nessas operações de carregamento / descarregamento.
Jlliagre
14

Bem, isso é bem direto. Vamos pegar um executável chamado / usr / local / bin / whoopdeedoo. Isso é apenas uma referência ao chamado inode (estrutura básica de arquivos no Unix Filesystems). É o inode que é marcado como "em uso".

Agora, quando você exclui ou move o arquivo / usr / local / whoopdeedoo, a única coisa que é movida (ou limpa) é a referência ao inode. O próprio inode permanece inalterado. É basicamente isso.

Devo verificar, mas acredito que você também pode fazer isso nos sistemas de arquivos do Mac OS X.

O Windows adota uma abordagem diferente. Por quê? Quem sabe...? Não estou familiarizado com os internos do NTFS. Teoricamente, todos os sistemas de arquivos que usam referências a estruturas intenais para nomes de arquivos devem poder fazer isso.

Eu admito, simplifiquei demais, mas leia a seção "Implicações" na Wikipedia, que faz um trabalho muito melhor do que eu.

jawtheshark
fonte
1
Bem, se você usa um atalho no Windows para iniciar o executável, também pode limpar o atalho, se quiser compará-lo dessa maneira, talvez? = 3
Ray
2
Não, isso seria como limpar um link simbólico. Em outros comentários, afirma-se que o comportamento se deve ao suporte herdado dos sistemas de arquivos FAT. Isso soa como uma provável razão.
precisa saber é o seguinte
1
Isso não tem nada a ver especificamente com inodes. O NTFS usa registros MFT para rastrear o estado do arquivo, e o FAT usa entradas de diretório para isso, mas o Linux ainda funciona da mesma maneira com esses sistemas de arquivos - do ponto de vista do usuário.
Ruslan
13

Uma coisa que parece estar faltando em todas as outras respostas é que: depois que um arquivo é aberto e um programa contém um descritor de arquivo aberto, o arquivo não será removido do sistema até que o descritor de arquivo seja fechado.

As tentativas de excluir o inode referenciado serão adiadas até que o arquivo seja fechado: renomear no mesmo sistema de arquivos ou em um sistema de arquivos diferente não pode afetar o arquivo aberto, independentemente do comportamento da renomeação, nem excluir ou substituir explicitamente o arquivo por um novo. A única maneira pela qual você pode atrapalhar um arquivo é abrindo explicitamente seu inode e mexendo com o conteúdo, não por operações no diretório, como renomear / excluir o arquivo.

Além disso, quando o kernel executa um arquivo, ele mantém uma referência ao arquivo executável e isso impede novamente qualquer modificação dele durante a execução.

Portanto, mesmo que pareça que você possa excluir / mover os arquivos que compõem um programa em execução, na verdade o conteúdo desses arquivos é mantido na memória até o término do programa.

Bakuriu
fonte
1
Isto não está certo. execve()não retorna nenhum FD, ele simplesmente executa o programa. Assim, por exemplo, se você executar tail -f /foo.logem seguida, a sua é uma FD ( /proc/PID/fd/<fd_num>) associado tailpara o foo.log, mas não para o próprio executável, taile não em seu pai também. Isso também é válido para os executáveis ​​únicos.
precisa saber é
@ heemayl eu não mencionei, execveentão não vejo como isso é relevante. Uma vez que o kernel comece a executar um arquivo, tentar substituí-lo não modificará o programa que o kernel carregará, tornando o ponto discutível. Se você deseja "atualizar" o executável enquanto está em execução, você pode chamar execveem algum momento para fazer o kernel reler o arquivo, mas não vejo como isso importa. O ponto é: excluir um "executável em execução" realmente não aciona nenhuma exclusão de dados até que o executável pare.
Bakuriu 19/08/19
Estou falando dessa parte se o programa consistir em um único arquivo executável, uma vez que você iniciar a execução, o programa será executado independentemente de qualquer alteração no diretório: renomear no mesmo sistema de arquivos ou em um sistema de arquivos diferente não pode afetar o manipulador aberto , você está necessariamente falando sobre execve()e um DF quando não houver DF envolvido neste caso.
heemayl
2
Você não precisa de um identificador de arquivo para ter uma referência ao arquivo - ter páginas mapeadas também é suficiente.
Simon Richter
1
O Unix não possui "identificadores de arquivo". open()retorna um descritor de arquivo , com o qual o heemayl está falando aqui execve(). Sim, um processo em execução tem uma referência ao seu executável, mas esse não é um descritor de arquivo. Provavelmente, mesmo que munmap()editasse todos os mapeamentos de seu executável, ele ainda teria uma referência (refletida em / proc / self / exe) que impedisse a liberação do inode. (Isso seria possível sem travar se fizesse isso a partir de uma função de biblioteca que nunca retornou.) Aliás, truncar ou modificar um executável em uso pode fornecer ETXTBUSY, mas pode funcionar.
Peter Cordes
7

Em um sistema de arquivos Linux, quando você move um arquivo, desde que ele não ultrapasse os limites do sistema de arquivos (leia-se: permanece no mesmo disco / partição), tudo o que você está alterando é o inode de ..(diretório pai) para o novo local . Os dados reais não foram movidos no disco, apenas o ponteiro para que o sistema de arquivos saiba onde encontrá-los.

É por isso que as operações de movimentação são tão rápidas e, provavelmente, não há problema em mover um programa em execução, pois você não está movendo o próprio programa.

I_GNU_it_all_along
fonte
Sua resposta parece implicar que a transferência de um executável binário para outro sistema de arquivos impactaria os processos em execução iniciados a partir desse binário.
precisa saber é o seguinte
6

É possível porque a movimentação de um programa não afeta os processos em execução iniciados por iniciá-lo.

Depois que um programa é iniciado, seus bits no disco ficam protegidos contra a substituição, mas não há necessidade de proteger o arquivo a ser renomeado, movido para um local diferente no mesmo sistema de arquivos, equivalente a renomear o arquivo ou movido para um sistema de arquivos diferente, que é equivalente a copiar o arquivo em outro lugar e depois removê-lo.

A remoção de um arquivo em uso, seja porque um processo possui um descritor de arquivo aberto ou porque um processo está sendo executado, não remove os dados do arquivo, que permanece referenciado pelo inode do arquivo, mas remove apenas a entrada do diretório, ou seja, um caminho a partir do qual o inode pode ser alcançado.

Observe que o lançamento de um programa não carrega tudo de uma vez na memória (física). Pelo contrário, apenas o mínimo estrito necessário para o início do processo é carregado. Em seguida, as páginas necessárias são carregadas sob demanda durante toda a vida útil do processo. isso é chamado de paginação por demanda. Se houver falta de RAM, o sistema operacional poderá liberar a RAM contendo essas páginas, portanto, é possível que um processo carregue várias vezes a mesma página a partir do inode executável.

A razão pela qual isso não era possível com o Windows é originalmente provável devido ao fato de o sistema de arquivos subjacente (FAT) não suportar o conceito de divisão de entradas de diretório vs inodes. Essa limitação não estava mais presente no NTFS, mas o design do sistema operacional foi mantido por um longo tempo, levando à restrição desagradável de ter que reiniciar ao instalar uma nova versão de um binário, o que não acontece mais nas versões recentes do Windows.

jlliagre
fonte
1
Acredito que versões mais recentes do Windows podem substituir os binários em uso sem reiniciar.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Gostaria de saber por que todas as atualizações ainda requerem reinicialização :(
Braiam
1
@Braiam Eles não. Dê uma olhada. Embora os binários possam ser atualizados, o kernel não pode (que eu saiba) e requer que uma reinicialização seja substituída por uma versão mais recente. Isso é válido para a maioria dos kernels do sistema operacional. As pessoas mais inteligentes do que eu tenho kpatch escrito para Linux que pode corrigir um kernel Linux durante a execução - ver en.wikipedia.org/wiki/Kpatch
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Eu quis dizer "todas as atualizações do Windows"
Braiam
@ Braiam sim - eu também. Por favor, dê uma olhada.
Thorbjørn Ravn Andersen
4

Basicamente, no Unix e no seu gênero, um nome de arquivo (incluindo o caminho do diretório que leva a ele) é usado para associar / encontrar um arquivo ao abri- lo (executar um arquivo é uma maneira de abri-lo de uma maneira). Após esse momento, a identidade do arquivo (via seu "inode") é estabelecida e não é mais questionada. Você pode remover o arquivo, renomeá-lo e alterar suas permissões. Desde que qualquer processo ou caminho de arquivo tenha um identificador nesse arquivo / inode, ele permanecerá, como um canal entre processos (na verdade, no UNIX histórico, um canal era um inodo sem nome com um tamanho que apenas cabia no arquivo referência de armazenamento em disco de "blocos diretos" no inode, algo como 10 blocos).

Se você tiver um visualizador de PDF aberto em um arquivo PDF, poderá excluí-lo e abrir um novo com o mesmo nome e, enquanto o visualizador antigo estiver aberto, continuará acessando o arquivo antigo (a menos que assista ativamente) o sistema de arquivos para perceber quando o arquivo desaparece com o nome original).

Os programas que precisam de arquivos temporários podem simplesmente abrir esse arquivo com algum nome e removê-lo imediatamente (ou melhor, sua entrada de diretório) enquanto ele ainda estiver aberto. Posteriormente, o arquivo não será mais acessível por nome, mas qualquer processo que possua um descritor de arquivo aberto ainda poderá acessá-lo e, se houver uma saída inesperada do programa posteriormente, o arquivo será removido e o armazenamento recuperado automaticamente.

Portanto, o caminho para um arquivo não é uma propriedade do próprio arquivo (na verdade, os links físicos podem fornecer vários caminhos diferentes) e é necessário apenas para abri-lo, não para o acesso contínuo por processos que já o abrem.

user584745
fonte