Por que precisamos trabalhar para criar novos processos?

95

No Unix, sempre que queremos criar um novo processo, bifurcamos o processo atual, criando um novo processo filho que é exatamente igual ao processo pai; então fazemos uma chamada de sistema exec para substituir todos os dados do processo pai pelos do novo processo.

Por que criamos uma cópia do processo pai em primeiro lugar e não criamos um novo processo diretamente?

sarthak
fonte
2
Veja também unix.stackexchange.com/questions/31118/…
Ellen Spertus

Respostas:

61

A resposta curta é: forkestá no Unix porque era fácil se encaixar no sistema existente na época e porque um sistema predecessor em Berkeley havia usado o conceito de garfos.

Em A evolução do sistema de compartilhamento de tempo Unix (o texto relevante foi destacado ):

O controle de processo em sua forma moderna foi projetado e implementado em alguns dias. É surpreendente como ele se encaixou facilmente no sistema existente; ao mesmo tempo, é fácil ver como algumas das características levemente incomuns do design estão presentes precisamente porque representavam pequenas mudanças facilmente codificadas no que existia . Um bom exemplo é a separação das funções fork e exec. O modelo mais comum para a criação de novos processos envolve a especificação de um programa para a execução do processo; no Unix, um processo bifurcado continua executando o mesmo programa que seu pai até executar um exec explícito. A separação das funções certamente não é exclusiva do Unix e , de fato, estava presente no sistema de compartilhamento de tempo de Berkeley, que era bem conhecido por Thompson. Ainda assim, parece razoável supor que ele exista no Unix principalmente devido à facilidade com que o fork pode ser implementado sem alterar muito mais . O sistema já lidou com vários processos (isto é, dois); havia uma tabela de processos e os processos foram trocados entre a memória principal e o disco. A implementação inicial do fork exigia apenas

1) Expansão da tabela de processos

2) Adição de uma chamada de bifurcação que copiou o processo atual para a área de troca de disco, usando as primitivas de E / S de troca já existentes, e fez alguns ajustes na tabela de processos.

De fato, a chamada da bifurcação do PDP-7 exigiu precisamente 27 linhas de código de montagem. Obviamente, outras alterações no sistema operacional e nos programas do usuário foram necessárias, e algumas delas foram bastante interessantes e inesperadas. Mas um fork-exec combinado teria sido consideravelmente mais complicado , apenas porque o exec em si não existia; sua função já foi executada, usando E / S explícita, pelo shell.

Desde esse artigo, o Unix evoluiu. forkseguido por execnão é mais a única maneira de executar um programa.

  • O vfork foi criado para ser um fork mais eficiente para o caso em que o novo processo pretende executar um executivo logo após o fork. Depois de executar um vfork, os processos pai e filho compartilham o mesmo espaço de dados e o processo pai é suspenso até que o processo filho execute um programa ou saia.

  • posix_spawn cria um novo processo e executa um arquivo em uma única chamada do sistema. São necessários vários parâmetros que permitem compartilhar seletivamente os arquivos abertos do chamador e copiar sua disposição do sinal e outros atributos para o novo processo.

Mark Plotnick
fonte
5
Boa resposta, mas gostaria de acrescentar que o vfork não deve mais ser usado. A diferença de desempenho é marginal agora e seu uso pode ser perigoso. Veja este stackoverflow.com/questions/4856255/... SO questão, este site ewontfix.com/7 , e "Advanced Unix programação" página 299 sobre vfork
Raphael Ahrens
4
As maquinações (configuração da estrutura de dados) necessárias para posix_spawn()executar as mesmas tarefas de reabastecimento pós-garfo que podem ser feitas facilmente usando o fork()código embutido tornam um argumento convincente por fork()ser muito mais simples de usar.
Jonathan Leffler
34

[Vou repetir parte da minha resposta daqui .]

Por que não apenas ter um comando que cria um novo processo do zero? Não é absurdo e ineficiente copiar um que só será substituído imediatamente?

De fato, isso provavelmente não seria tão eficiente por alguns motivos:

  1. A "cópia", produzido por fork()um pouco de uma abstração, uma vez que o kernel usa um copy-on-write sistema ; tudo o que realmente precisa ser criado é um mapa de memória virtual. Se a cópia ligar imediatamente exec(), a maioria dos dados que teriam sido copiados se tivessem sido modificados pela atividade do processo nunca precisará realmente ser copiada / criada porque o processo não faz nada que exija seu uso.

  2. Vários aspectos significativos do processo filho (por exemplo, seu ambiente) não precisam ser duplicados ou configurados individualmente com base em uma análise complexa do contexto etc. Eles assumem que eles são os mesmos do processo de chamada e este é o sistema bastante intuitivo com o qual estamos familiarizados.

Para explicar # 1 um pouco mais, a memória que é "copiada", mas nunca acessada posteriormente nunca é realmente copiada, pelo menos na maioria dos casos. Uma exceção nesse contexto pode ser se você bifurcar um processo e depois encerrar o processo pai antes que o filho se substitua exec(). Eu digo poder porque muito do pai poderia ser armazenada em cache se não houver memória livre suficiente, e eu não tenho certeza até que ponto isso seria explorado (que dependem da implementação OS).

Obviamente, isso não torna a cópia mais eficiente do que usar uma folha em branco - exceto "a folha em branco" não é literalmente nada e deve envolver alocação. O sistema pode ter um modelo de processo em branco / novo genérico que copie da mesma maneira, 1 mas que, na verdade, não salvaria nada em comparação com o fork de copiar na gravação. Portanto, o nº 1 apenas demonstra que o uso de um "novo" processo vazio não seria mais eficiente.

O ponto 2 explica por que o uso do garfo é provavelmente mais eficiente. O ambiente de uma criança é herdado de seu pai, mesmo que seja um executável completamente diferente. Por exemplo, se o processo pai for um shell, e o filho, um navegador da web, $HOMEainda for o mesmo para os dois, mas como qualquer um deles poderá alterá-lo posteriormente, elas deverão ser duas cópias separadas. O da criança é produzido pelo original fork().

1. Uma estratégia que pode não fazer muito sentido literal, mas o que quero dizer é que criar um processo envolve mais do que copiar sua imagem na memória do disco.

Cachinhos Dourados
fonte
3
Embora os dois pontos sejam verdadeiros, nenhum dos dois suporta o motivo pelo qual o método de bifurcação foi escolhido em vez de executar um novo processo a partir de um determinado executável.
SkyDan
3
Eu acho que isso responde à pergunta. O fork é usado porque, nos casos em que a criação de um novo processo é o caminho mais eficiente, o custo do uso do fork é trivial (provavelmente menos de 1% do custo de criação do processo). Por outro lado, há muitos lugares em que o fork é muito mais eficiente ou muito mais simples de uma API (como lidar com identificadores de arquivo). A decisão que o Unix tomou foi oferecer suporte apenas a uma API, simplificando a especificação.
precisa
1
@SkyDan Você está certo, é mais uma resposta para o porquê do que para o porquê , que Mark Plotnick responde mais diretamente - o que eu interpretaria para significar não apenas que essa foi a escolha mais fácil, mas também que provavelmente foi a mais eficiente escolha (de acordo com a citação de Dennis Richie: "a chamada da bifurcação do PDP-7 exigia precisamente 27 linhas de montagem ... exec, como tal, não existia; sua função já estava executada"). Portanto, este "por que não" é realmente uma meditando cerca de duas estratégias em que um superficialmente parece mais simples e mais eficiente, quando talvez não seja (testemunhar o destino duvidoso de ...
goldilocks
1
Cachinhos Dourados está correto. Há situações em que bifurcar e modificar é mais barato do que criar um novo a partir do zero. O exemplo mais extremo, é claro, é quando você deseja um comportamento de bifurcação. fork()pode muito rapidamente (como GL mencionado, na ordem de 27 linhas de montagem). Olhando na outra direção, se você deseja "criar um processo do zero", fork()custa apenas um pouquinho mais do que começar com um processo criado em branco (27 linhas de montagem + custo de fechamento de identificadores de arquivo). Portanto, forklida com o garfo e cria bem, enquanto createsó pode lidar com criar bem.
Cort Ammon
2
Sua resposta se refere às melhorias de hardware: memória virtual, cópia na gravação. Antes disso, forkcopiava toda a memória do processo e era muito caro.
Barmar 19/06
6

Eu acho que o motivo pelo qual o Unix tinha apenas a forkfunção de criar novos processos é resultado da filosofia do Unix

Eles constroem uma função que faz uma coisa bem. Cria um processo filho.

O que se faz com o novo processo depende do programador. Ele pode usar uma das exec*funções e iniciar um programa diferente ou não pode usar exec e usar as duas instâncias do mesmo programa, o que pode ser útil.

Assim, você obtém um maior grau de liberdade, pois pode usar

  1. garfo sem exec *
  2. bifurque-se com exec * ou
  3. apenas exec * sem garfo

e, além disso você só tem que memorizar os forke as exec*chamadas de função, que na década de 1970 que tinha que fazer.

Raphael Ahrens
fonte
3
Entendo como os garfos funcionam e como usá-los. Mas por que eu gostaria de criar um novo processo, quando posso fazer a mesma coisa, mas com menos esforço? Por exemplo, meu professor me deu uma tarefa na qual eu tive que criar um processo para cada número passado para argv, para verificar se o número é primo. Mas isso não é apenas um desvio de fazer a mesma coisa? Eu poderia ter usado apenas uma matriz e uma função para cada número ... Então, por que criamos processos filhos, em vez de fazer todo o processamento no processo principal?
precisa saber é o seguinte
2
Atrevo-me a dizer que você entende como os garfos funcionam e como usá-los, porque uma vez você teve um professor que lhe deu uma tarefa na qual você teve que criar vários processos (com o número especificado em tempo de execução), controlá-los, coordená-los e comunicar-se entre eles. É claro que ninguém faria algo trivial assim na vida real. Mas, se você tiver um grande problema que é facilmente decomposto em partes que podem ser tratadas em paralelo (por exemplo, detecção de borda em uma imagem), o bifurcação permite usar vários núcleos da CPU simultaneamente.
Scott Scott
5

Existem duas filosofias de criação de processos: bifurcar-se com herança e criar com argumentos. Unix usa garfo, obviamente. (OSE, por exemplo, e VMS usam o método create.) O Unix possui MUITAS características herdáveis ​​e mais são adicionadas periodicamente. Por herança, essas novas características podem ser adicionadas SEM ALTERAR PROGRAMAS EXISTENTES! Usando um modelo de criação com argumentos, adicionar novas características significaria adicionar novos argumentos à chamada de criação. O modelo Unix é mais simples.

Ele também oferece o modelo fork-sem-exec altamente útil, onde um processo pode se dividir em várias partes. Isso era vital quando não havia forma de E / S assíncrona e é útil ao tirar vantagem de várias CPUs em um sistema. (Pré-threads.) Fiz isso muito ao longo dos anos, mesmo recentemente. Em essência, ele permite agrupar vários 'programas' em um único programa, para que não haja absolutamente espaço para corrupção ou incompatibilidade de versão, etc.

O modelo fork / exec também oferece a capacidade de um filho específico herdar um ambiente radicalmente estranho, configurado entre o fork e o exec. Coisas como descritores de arquivos herdados, especialmente. (Uma extensão do stdio fd's.) O modelo de criação não oferece a capacidade de herdar qualquer coisa que não foi prevista pelos criadores da chamada de criação.

Alguns sistemas também podem oferecer suporte à compilação dinâmica de código nativo, onde o processo está efetivamente gravando seu próprio programa de código nativo. Em outras palavras, ele quer um novo programa que esteja gravando em tempo real, SEM ter que passar pelo ciclo de código-fonte / compilador / vinculador e ocupando espaço em disco. (Acredito que exista um sistema de linguagem Verilog que faça isso.) O modelo de fork suporta isso, o modelo de criação normalmente não.

Jim Cathey
fonte
Os descritores de arquivo não são "uma extensão do stdio"; ponteiros de arquivo stdio são um invólucro em torno dos descritores de arquivo. Os descritores de arquivo foram os primeiros e são os identificadores fundamentais de E / S do Unix. Mas, caso contrário, este é um bom ponto.
Scott
2

A função fork () não serve apenas para copiar o processo pai, ele retorna um valor que se refere ao processo pai ou filho; a imagem abaixo explica como você pode usar o fork () como pai e Filho:

insira a descrição da imagem aqui

conforme mostrado quando o processo é o fork pai () retorna o ID do processo filho; caso PID contrário, ele retorna0

por exemplo, você pode utilizá-lo se tiver um processo (servidor da web) que recebe as solicitações e, a cada solicitação, cria um son processpara processar essa solicitação, aqui o pai e seus filhos têm trabalhos diferentes.

Portanto, não é possível executar uma cópia de um processo exatamente como fork ().

Networker
fonte
5
Embora seja verdade, isso não responde à pergunta. Por que o bifurcação é necessário para a criação do processo, se eu quero executar um executável diferente?
SkyDan
1
Eu concordo com o SkyDan - isso não responde à pergunta. posix_spawn é uma versão mais extravagante do que poderia ter sido imaginado 30 anos atrás (antes da existência do Posix) como uma função fork_execve ; aquele que cria um novo processo, inicializando sua imagem de um executável, sem nem mesmo sugerir a cópia da imagem do processo pai (exceto para a lista de argumentos, o ambiente e os atributos do processo (por exemplo, diretório de trabalho)) e retorna o PID do novo processo para o chamador (o processo pai) .
Scott
1
Existem outras maneiras de passar informações "principais" para uma criança. A técnica valor de retorno só acontece de ser a forma mais eficiente de fazê-lo a partir de um fork , se você assumir que você quer fork, em primeiro lugar
Cort Ammon
0

O redirecionamento de E / S é mais facilmente implementado após o fork e antes do exec. A criança, sabendo que é a criança, pode fechar os descritores de arquivos, abrir novos, dup () ou dup2 () para obter o número fd correto, etc., tudo sem afetar o pai. Depois de fazer isso, e talvez qualquer variável de ambiente desejada seja alterada (também não afetando o pai), ele pode executar o novo programa no ambiente personalizado.

Richard Hamilton
fonte
Tudo o que você está fazendo aqui é repetir o terceiro parágrafo da resposta de Jim Cathey com um pouquinho mais de detalhes.
Scott
-2

Acho que todo mundo aqui sabe como funciona o fork, mas a questão é por que precisamos criar uma duplicata exata do pai usando o fork? Resposta ==> Veja um exemplo de servidor (sem bifurcação), enquanto o cliente-1 está acessando o servidor, se ao mesmo tempo o segundo cliente-2 chegar e quiser acessar o servidor, mas o servidor não der permissão ao recém-chegado cliente-2 porque o servidor está ocupado para atender o cliente-1, portanto o cliente-2 precisa aguardar. após a conclusão de todos os serviços para o cliente-1, o cliente-2 agora pode acessar o servidor. Agora, considere se ao mesmo tempo o cliente-3 chega, portanto o cliente-3 precisa aguardar até que todos os serviços para o cliente-2 sejam concluídos.Tome o cenário em que milhares de clientes precisam acessar o servidor ao mesmo tempo ... então todos os clientes precisam aguarde (o servidor está ocupado !!).

Isso é evitado através da criação (usando bifurcação) de cópia duplicada exata (ou seja, filho) do servidor, onde cada filho (que é cópia duplicada exata de seu pai ou seja, servidor) é dedicado ao cliente recém-chegado, portanto, simultaneamente, todos os clientes acessam o mesmo servidor.

Harshil Mania
fonte
É por isso que os processos do servidor não devem ser de thread único, manipulando solicitações de clientes consecutivamente quando podem ser tratadas simultaneamente - por exemplo, em processos separados. Mas o modelo de servidor multithread pode ser facilmente implementado com um processo de ouvinte que aceita solicitações de clientes e cria um novo processo no qual executar o programa de serviço ao cliente. A única vantagem oferecida pela forkchamada que copia o processo pai é que você não precisa ter dois programas separados - mas ter programas separados (por exemplo, inetd) pode tornar o sistema mais modular.
Scott