Como a substituição de processo é implementada no bash?

12

Eu estava pesquisando a outra pergunta , quando percebi que não entendia o que estava acontecendo, quais são esses /dev/fd/*arquivos e como os processos filhos podem abri-los.

x-yuri
fonte
Essa pergunta não está respondida?
Php 07/07

Respostas:

21

Bem, há muitos aspectos nisso.

Descritores de arquivo

Para cada processo, o kernel mantém uma tabela de arquivos abertos (bem, pode ser implementada de forma diferente, mas como você não pode vê-lo de qualquer maneira, basta assumir que é uma tabela simples). Essa tabela contém informações sobre qual arquivo está / onde pode ser encontrado, em qual modo você o abriu, em qual posição está lendo / gravando e o que mais é necessário para realizar operações de E / S nesse arquivo. Agora, o processo nunca chega a ler (ou mesmo escrever) essa tabela. Quando o processo abre um arquivo, ele recebe de volta o chamado descritor de arquivo. O que é simplesmente um índice na tabela.

O diretório /dev/fde seu conteúdo

No Linux, dev/fdna verdade, é um link simbólico para /proc/self/fd. /procé um pseudo sistema de arquivos no qual o kernel mapeia várias estruturas de dados internas para serem acessadas com a API do arquivo (para que pareçam arquivos / diretórios / links simbólicos regulares para os programas). Especialmente, há informações sobre todos os processos (que deram o nome). O link simbólico /proc/selfsempre se refere ao diretório associado ao processo atualmente em execução (ou seja, o processo que o solicita; portanto, processos diferentes verão valores diferentes). No diretório do processo, há um subdiretóriofd que para cada arquivo aberto contém um link simbólico cujo nome é apenas a representação decimal do descritor de arquivo (o índice na tabela de arquivos do processo, consulte a seção anterior) e cujo destino é o arquivo ao qual ele corresponde.

Descritores de arquivo ao criar processos filhos

Um processo filho é criado por a fork. A forkfaz uma cópia dos descritores de arquivo, o que significa que o processo filho criado possui a mesma lista de arquivos abertos que o processo pai. Portanto, a menos que um dos arquivos abertos seja fechado pelo filho, o acesso a um descritor de arquivo herdado no filho acessará o mesmo arquivo que o descritor de arquivo original no processo pai.

Observe que, após uma bifurcação, você inicialmente possui duas cópias do mesmo processo, que diferem apenas no valor de retorno da chamada da bifurcação (o pai obtém o PID do filho e o filho recebe 0). Normalmente, um fork é seguido por um execpara substituir uma das cópias por outro executável. Os descritores de arquivo aberto sobrevivem a esse executivo. Observe também que, antes do exec, o processo pode fazer outras manipulações (como fechar arquivos que o novo processo não deve obter ou abrir outros arquivos).

Tubos sem nome

Um canal sem nome é apenas um par de descritores de arquivos criados a pedido do kernel, para que tudo o que foi escrito no primeiro descritor de arquivo seja passado para o segundo. O uso mais comum é para a construção foo | barde tubulação de bash, onde a saída padrão de fooé substituída pela parte de gravação do tubo e a entrada padrão é substituída pela parte de leitura. A entrada padrão e a saída padrão são apenas as duas primeiras entradas na tabela de arquivos (as entradas 0 e 1; 2 são erro padrão) e, portanto, substituí-las significa reescrever essa entrada da tabela com os dados correspondentes ao outro descritor de arquivo (novamente, o implementação real pode ser diferente). Como o processo não pode acessar a tabela diretamente, há uma função do kernel para fazer isso.

Substituição de processo

Agora, temos tudo junto para entender como a substituição do processo funciona:

  1. O processo do bash cria um canal sem nome para comunicação entre os dois processos criados posteriormente.
  2. Bash garfos para o echoprocesso. O processo filho (que é uma cópia exata do bashprocesso original ) fecha a extremidade de leitura do tubo e substitui sua própria saída padrão pela extremidade de gravação do tubo. Dado que echoé um shell embutido, bashpode poupar a execchamada, mas isso não importa de qualquer maneira (o shell embutido também pode ser desativado; nesse caso, é executado /bin/echo).
  3. Bash (o original, pai) substitui a expressão <(echo 1)pelo link do pseudo arquivo ao /dev/fdse referir ao final da leitura do canal não nomeado.
  4. Executivos Bash para o processo PHP (observe que, após o fork, ainda estamos dentro de [uma cópia do] bash). O novo processo fecha a extremidade de gravação herdada do canal não nomeado (e executa outras etapas preparatórias), mas deixa a extremidade de leitura aberta. Então ele executou o PHP.
  5. O programa PHP recebe o nome no /dev/fd/. Como o descritor de arquivo correspondente ainda está aberto, ele ainda corresponde à extremidade de leitura do canal. Portanto, se o programa PHP abre o arquivo fornecido para leitura, o que ele realmente faz é criar um seconddescritor de arquivo para o final da leitura do canal não nomeado. Mas isso não é problema, poderia ler também.
  6. Agora, o programa PHP pode ler a extremidade de leitura do canal através do novo descritor de arquivo e, assim, receber a saída padrão do echocomando que vai para o final de gravação do mesmo canal.
celtschk
fonte
Claro, agradeço seu esforço. Mas eu queria apontar várias questões. Primeiro, você está falando de phpcenário, mas phpnão lida bem com canos . Além disso, considerando o comando cat <(echo test), o estranho aqui é que bashgarfos uma vez cat, mas duas vezes echo test.
X-yuri
13

Tomando emprestado da celtschkresposta, /dev/fdé um link simbólico para /proc/self/fd. E /procé um pseudo sistema de arquivos, que apresenta informações sobre processos e outras informações do sistema em uma estrutura hierárquica semelhante a arquivo. Os arquivos /dev/fdcorrespondem aos arquivos, abertos por um processo e têm o descritor de arquivos como seus nomes e os próprios arquivos como seus destinos. Abrir o arquivo /dev/fd/Né equivalente a duplicar o descritor N(assumindo que o descritor Nesteja aberto).

E aqui estão os resultados da minha investigação de como funciona (a stracesaída é livre de detalhes desnecessários e modificada para expressar melhor o que está acontecendo):

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

Basicamente, bashcria um canal e passa suas extremidades para seus filhos como descritores de arquivo (leia final para 1.oute escreva fim 2.out). E passa read end como um parâmetro de linha de comando para 1.out( /dev/fd/63). Por aqui1.out é possível abrir /dev/fd/63.

x-yuri
fonte