Explique a função exec () e sua família

98

Qual é a exec()função e sua família? Por que essa função é usada e como funciona?

Por favor, alguém explique essas funções.

user507401
fonte
4
Tente ler Stevens novamente e esclareça o que você não entende.
vlabrecque

Respostas:

244

De forma simplista, no UNIX, você tem o conceito de processos e programas. Um processo é um ambiente no qual um programa é executado.

A ideia simples por trás do "modelo de execução" do UNIX é que existem duas operações que você pode fazer.

O primeiro é para fork(), que cria um novo processo contendo uma duplicata (principalmente) do programa atual, incluindo seu estado. Existem algumas diferenças entre os dois processos que lhes permitem descobrir quem é o pai e quem é o filho.

A segunda é para exec(), que substitui o programa no processo atual por um programa totalmente novo.

A partir dessas duas operações simples, todo o modelo de execução do UNIX pode ser construído.


Para adicionar mais alguns detalhes ao acima:

O uso fork()e exec()exemplifica o espírito do UNIX, pois fornece uma maneira muito simples de iniciar novos processos.

A fork()chamada torna quase uma duplicação do processo atual, idêntico em quase todos os sentidos (nem tudo é copiado, por exemplo, limites de recursos em algumas implementações, mas a ideia é criar uma cópia o mais próxima possível). Apenas um processo chama, fork() mas dois processos retornam dessa chamada - parece bizarro, mas é realmente muito elegante

O novo processo (chamado de filho) obtém um ID de processo (PID) diferente e tem o PID do processo antigo (o pai) como seu PID pai (PPID).

Como os dois processos agora estão executando exatamente o mesmo código, eles precisam ser capazes de dizer qual é qual - o código de retorno fork()fornece essa informação - o filho obtém 0, o pai obtém o PID do filho (se o fork()falhar, não filho é criado e o pai recebe um código de erro).

Dessa forma, o pai conhece o PID do filho e pode se comunicar com ele, matá-lo, esperá-lo e assim por diante (a criança sempre pode encontrar seu processo pai com uma chamada para getppid()).

A exec()chamada substitui todo o conteúdo atual do processo por um novo programa. Ele carrega o programa no espaço do processo atual e o executa a partir do ponto de entrada.

Portanto, fork()e exec()são frequentemente usados ​​em sequência para fazer um novo programa rodar como filho de um processo atual. Os shells normalmente fazem isso sempre que você tenta executar um programa como find- o shell bifurca, então o filho carrega o findprograma na memória, configurando todos os argumentos da linha de comando, E / S padrão e assim por diante.

Mas eles não precisam ser usados ​​juntos. É perfeitamente aceitável que um programa chame fork()sem seguimento exec()se, por exemplo, o programa contiver código pai e filho (você precisa ter cuidado com o que faz, cada implementação pode ter restrições).

Isso era muito usado (e ainda é) para daemons que simplesmente ouvem em uma porta TCP e bifurcam uma cópia de si mesmos para processar uma solicitação específica enquanto o pai volta a ouvir. Para essa situação, o programa contém o código pai e o filho.

Da mesma forma, os programas que sabem que foram concluídos e apenas desejam executar outro programa não precisam fazê-lo fork(), exec()e wait()/waitpid()para a criança. Eles podem simplesmente carregar o filho diretamente em seu espaço de processo atual com exec().

Algumas implementações do UNIX têm um otimizado fork()que usa o que eles chamam de cópia na gravação. Este é um truque para atrasar a cópia do espaço do processo fork()até que o programa tente alterar algo nesse espaço. Isso é útil para aqueles programas que usam apenas fork()e não exec()porque não precisam copiar um espaço de processo inteiro. No Linux, fork()apenas faz uma cópia das tabelas de páginas e uma nova estrutura de tarefas, exec()fará o trabalho árduo de "separar" a memória dos dois processos.

Se o exec for chamado em seguida fork(e isso é o que acontece principalmente), isso causa uma gravação no espaço do processo e é então copiado para o processo filho, antes que as modificações sejam permitidas.

O Linux também possui um vfork(), ainda mais otimizado, que compartilha quase tudo entre os dois processos. Por causa disso, existem certas restrições quanto ao que a criança pode fazer, e os pais param até que a criança chame exec()ou _exit().

O pai deve ser interrompido (e o filho não tem permissão para retornar da função atual) uma vez que os dois processos compartilham a mesma pilha. Isso é um pouco mais eficiente para o caso de uso clássico de fork()seguido imediatamente por exec().

Note-se que há uma família inteira de execchamadas ( execl, execle, execvee assim por diante), mas execno contexto aqui significa qualquer um deles.

O diagrama a seguir ilustra a fork/execoperação típica em que o bashshell é usado para listar um diretório com o lscomando:

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
    |
    | calls fork
    V
+--------+             +--------+
| pid=7  |    forks    | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash   |             | bash   |
+--------+             +--------+
    |                      |
    | waits for pid 22     | calls exec to run ls
    |                      V
    |                  +--------+
    |                  | pid=22 |
    |                  | ppid=7 |
    |                  | ls     |
    V                  +--------+
+--------+                 |
| pid=7  |                 | exits
| ppid=4 | <---------------+
| bash   |
+--------+
    |
    | continues
    V
paxdiablo
fonte
12
Obrigado por essa explicação elaborada :)
Faizan
2
Obrigado pela referência do shell com o programa find. Exatamente o que eu precisava entender.
Usuário
Por que execo utilitário é usado para redirecionar o IO do processo atual? Como o caso "nulo", executando exec sem um argumento, foi usado para essa convenção?
Ray,
@Ray, sempre pensei nisso como uma extensão natural. Se você pensar execno meio de substituir o programa atual (o shell) neste processo por outro, não especificar esse outro programa para substituí-lo pode simplesmente significar que você não deseja substituí-lo.
paxdiablo
Eu vejo o que você quer dizer se por "extensão natural" você quer dizer algo na linha de "crescimento orgânico". Parece que o redirecionamento teria sido adicionado para oferecer suporte à função de substituição de programa, e posso ver esse comportamento permanecendo no caso degenerado de execser chamado sem um programa. Mas é um pouco estranho neste cenário, já que a utilidade original de redirecionar para um novo programa - um programa que seria realmente execexecutado - desaparece e você tem um artefato útil, redirecionando o programa atual - que não está sendo execusado ou iniciado de qualquer forma - em vez disso.
Ray
36

As funções na família exec () têm comportamentos diferentes:

  • l: os argumentos são passados ​​como uma lista de strings para o main ()
  • v: os argumentos são passados ​​como uma matriz de strings para o main ()
  • p: caminho / s para pesquisar o novo programa em execução
  • e: o ambiente pode ser especificado pelo chamador

Você pode misturá-los, portanto, você tem:

  • int execl (const char * caminho, const char * arg, ...);
  • int execlp (const char * arquivo, const char * arg, ...);
  • int execle (const char * caminho, const char * arg, ..., char * const envp []);
  • int execv (const char * caminho, char * const argv []);
  • int execvp (const char * arquivo, char * const argv []);
  • int execvpe (const char * arquivo, char * const argv [], char * const envp []);

Para todos eles, o argumento inicial é o nome de um arquivo a ser executado.

Para obter mais informações, leia a página de manual exec (3) :

man 3 exec  # if you are running a UNIX system
T04435
fonte
1
Curiosamente, você perdeu execve()em sua lista, que é definida por POSIX, e adicionou execvpe(), que não é definida por POSIX (principalmente por razões de precedente histórico; ele completa o conjunto de funções). Caso contrário, uma explicação útil da convenção de nomenclatura para a família - um complemento útil para paxdiablo 'uma resposta que explica mais sobre o funcionamento das funções.
Jonathan Leffler
E, em sua defesa, vejo que a página de manual do Linux para execvpe()(et al) não lista execve(); ele tem sua própria página de manual separada (pelo menos no Ubuntu 16.04 LTS) - a diferença é que as outras exec()funções da família estão listadas na seção 3 (funções), enquanto que execve()estão listadas na seção 2 (chamadas de sistema). Basicamente, todas as outras funções da família são implementadas em termos de uma chamada para execve().
Jonathan Leffler
18

A execfamília de funções faz com que seu processo execute um programa diferente, substituindo o antigo programa que estava rodando. Ou seja, se você ligar

execl("/bin/ls", "ls", NULL);

então o lsprograma é executado com o id do processo, diretório de trabalho atual e usuário / grupo (direitos de acesso) do processo que chamou execl. Depois disso, o programa original não está mais funcionando.

Para iniciar um novo processo, a forkchamada do sistema é usada. Para executar um programa sem substituir o original, você precisa fork, então exec.

Fred Foo
fonte
Obrigado, isso foi muito útil. Atualmente estou fazendo um projeto que exige que usemos exec () e sua descrição solidificou meu entendimento.
TwilightSparkleTheGeek
7

qual é a função exec e sua família.

A execfamília função é todas as funções usadas para executar um arquivo, como execl, execlp, execle, execv, e execvp.Eles são todos os frontends para execvee fornecer diferentes métodos de chamá-lo.

porque esta função é usada

As funções Exec são usadas quando você deseja executar (lançar) um arquivo (programa).

e como isso funciona.

Eles funcionam substituindo a imagem do processo atual por aquela que você iniciou. Eles substituem (encerrando) o processo atualmente em execução (aquele que chamou o comando exec) pelo novo processo que foi iniciado.

Para mais detalhes: veja este link .

Reese Moore
fonte
7

execé frequentemente usado em conjunto com fork, pelo qual vi que você também perguntou, então discutirei isso com isso em mente.

exectransforma o processo atual em outro programa. Se você já assistiu Doctor Who, então é como quando ele se regenera - seu antigo corpo é substituído por um novo corpo.

A maneira como isso acontece com o seu programa execé que muitos dos recursos que o kernel do sistema operacional verifica para ver se o arquivo que você está passando execcomo o argumento do programa (primeiro argumento) é executável pelo usuário atual (id do usuário do processo fazer a execchamada) e, em caso afirmativo, ele substitui o mapeamento de memória virtual do processo atual por uma memória virtual do novo processo e copia os dados argve envpque foram passados ​​na execchamada para uma área desse novo mapa de memória virtual. Várias outras coisas também podem acontecer aqui, mas os arquivos que foram abertos para o programa que chamou execainda estarão abertos para o novo programa e eles compartilharão o mesmo ID de processo, mas o programa que chamou execirá parar (a menos que exec falhou).

O motivo pelo qual isso é feito dessa maneira é que, separando a execução de um novo programa em duas etapas como essa, você pode fazer algumas coisas entre as duas etapas. A coisa mais comum a fazer é certificar-se de que o novo programa possui determinados arquivos abertos como determinados descritores de arquivo. (lembre-se de que os descritores de arquivo não são iguais FILE *, mas são intvalores que o kernel conhece). Fazendo isso, você pode:

int X = open("./output_file.txt", O_WRONLY);

pid_t fk = fork();
if (!fk) { /* in child */
    dup2(X, 1); /* fd 1 is standard output,
                   so this makes standard out refer to the same file as X  */
    close(X);

    /* I'm using execl here rather than exec because
       it's easier to type the arguments. */
    execl("/bin/echo", "/bin/echo", "hello world");
    _exit(127); /* should not get here */
} else if (fk == -1) {
    /* An error happened and you should do something about it. */
    perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */

Isso realiza a execução de:

/bin/echo "hello world" > ./output_file.txt

a partir do shell de comando.

nategoose
fonte
5

Quando um processo usa fork (), ele cria uma cópia duplicada de si mesmo e essa duplicata se torna filha do processo. O fork () é implementado usando uma chamada de sistema clone () no linux que retorna duas vezes do kernel.

  • Um valor diferente de zero (ID de processo do filho) é retornado ao pai.
  • Um valor zero é retornado ao filho.
  • Caso o filho não seja criado com sucesso devido a algum problema como memória insuficiente, -1 é retornado ao fork ().

Vamos entender isso com um exemplo:

pid = fork(); 
// Both child and parent will now start execution from here.
if(pid < 0) {
    //child was not created successfully
    return 1;
}
else if(pid == 0) {
    // This is the child process
    // Child process code goes here
}
else {
    // Parent process code goes here
}
printf("This is code common to parent and child");

No exemplo, assumimos que exec () não é usado dentro do processo filho.

Mas um pai e um filho diferem em alguns dos atributos do PCB (bloco de controle do processo). Esses são:

  1. PID - filho e pai têm um ID de processo diferente.
  2. Sinais pendentes - a criança não herda os sinais pendentes dos pais. Ele estará vazio para o processo filho quando criado.
  3. Bloqueios de memória - a criança não herda os bloqueios de memória de seus pais. Os bloqueios de memória são bloqueios que podem ser usados ​​para bloquear uma área de memória e, em seguida, essa área de memória não pode ser trocada para o disco.
  4. Bloqueios de registro - o filho não herda os bloqueios de registro do pai. Os bloqueios de registro são associados a um bloco de arquivo ou a um arquivo inteiro.
  5. A utilização de recursos do processo e o tempo de CPU consumido são definidos como zero para o filho.
  6. O filho também não herda temporizadores do pai.

Mas e quanto à memória infantil? Um novo espaço de endereço foi criado para uma criança?

As respostas em não. Após o fork (), pai e filho compartilham o espaço de endereço de memória do pai. No Linux, esse espaço de endereço é dividido em várias páginas. Somente quando a criança escreve em uma das páginas de memória dos pais, uma duplicata dessa página é criada para a criança. Isso também é conhecido como cópia na gravação (Copiar páginas pai apenas quando o filho gravar nelas).

Vamos entender o copy on write com um exemplo.

int x = 2;
pid = fork();
if(pid == 0) {
    x = 10;
    // child is changing the value of x or writing to a page
    // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
}
else {
    x = 4;
}

Mas por que a cópia na gravação é necessária?

A criação de um processo típico ocorre por meio da combinação fork () - exec (). Vamos primeiro entender o que exec () faz.

O grupo de funções Exec () substitui o espaço de endereço da criança por um novo programa. Depois que exec () é chamado dentro de um filho, um espaço de endereço separado será criado para o filho, que é totalmente diferente do pai.

Se não houvesse cópia no mecanismo de gravação associado a fork (), páginas duplicadas teriam sido criadas para o filho e todos os dados teriam sido copiados para as páginas do filho. Alocar nova memória e copiar dados é um processo muito caro (leva tempo do processador e outros recursos do sistema). Também sabemos que, na maioria dos casos, a criança chamará exec () e isso substituirá a memória da criança por um novo programa. Portanto, a primeira cópia que fizemos teria sido um desperdício se a cópia com escrita não estivesse lá.

pid = fork();
if(pid == 0) {
    execlp("/bin/ls","ls",NULL);
    printf("will this line be printed"); // Think about it
    // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
    wait(NULL);
    // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.

Por que o pai espera por um processo filho?

  1. O pai pode atribuir uma tarefa a seu filho e esperar até que ele conclua sua tarefa. Então, ele pode realizar algum outro trabalho.
  2. Depois que o filho termina, todos os recursos associados ao filho são liberados, exceto o bloco de controle do processo. Agora, a criança está em estado de zumbi. Usando wait (), o pai pode perguntar sobre o status da criança e então pedir ao kernel para liberar o PCB. Caso o pai não use a espera, a criança permanecerá no estado zumbi.

Por que a chamada de sistema exec () é necessária?

Não é necessário usar exec () com fork (). Se o código que o filho irá executar estiver dentro do programa associado ao pai, exec () não é necessário.

Mas pense nos casos em que a criança precisa executar vários programas. Vejamos o exemplo do programa shell. Ele suporta vários comandos como find, mv, cp, date etc. Será correto incluir o código do programa associado a esses comandos em um programa ou fazer com que o filho carregue esses programas na memória quando necessário?

Tudo depende do seu caso de uso. Você tem um servidor web que fornece uma entrada x que retorna 2 ^ x aos clientes. Para cada solicitação, o servidor da web cria um novo filho e pede para ele computar. Você escreverá um programa separado para calcular isso e usar exec ()? Ou você apenas escreverá código de computação dentro do programa pai?

Normalmente, a criação de um processo envolve uma combinação de chamadas fork (), exec (), wait () e exit ().

Shivam Mitra
fonte
4

As exec(3,3p)funções substituem o processo atual por outro. Ou seja, o processo atual para e outro é executado, assumindo alguns dos recursos que o programa original tinha.

Ignacio Vazquez-Abrams
fonte
6
Não exatamente. Ele substitui a imagem do processo atual por uma nova imagem do processo. O processo é o mesmo processo com o mesmo pid, o mesmo ambiente e a mesma tabela de descritor de arquivo. O que mudou foi toda a memória virtual e o estado da CPU.
JeremyP
@JeremyP "O mesmo descritor de arquivo" foi importante aqui, ele explica como o redirecionamento funciona em shells! Fiquei intrigado sobre como o redirecionamento pode funcionar se o exec sobrescrever tudo! Obrigado
FUD