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.
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.
Respostas:
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/fd
e seu conteúdo
No Linux, dev/fd
na 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/self
sempre 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 fork
faz 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 exec
para 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 | bar
de 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:
echo
processo. O processo filho (que é uma cópia exata do bash
processo 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, bash
pode poupar a exec
chamada, mas isso não importa de qualquer maneira (o shell embutido também pode ser desativado; nesse caso, é executado /bin/echo
).<(echo 1)
pelo link do pseudo arquivo ao /dev/fd
se referir ao final da leitura do canal não nomeado./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 second
descritor de arquivo para o final da leitura do canal não nomeado. Mas isso não é problema, poderia ler também.echo
comando que vai para o final de gravação do mesmo canal.php
cenário, mas php
não lida bem com canos . Além disso, considerando o comando cat <(echo test)
, o estranho aqui é que bash
garfos uma vez cat
, mas duas vezes echo test
.
Tomando emprestado da celtschk
resposta, /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/fd
correspondem 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 N
esteja aberto).
E aqui estão os resultados da minha investigação de como funciona (a strace
saí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, bash
cria um canal e passa suas extremidades para seus filhos como descritores de arquivo (leia final para 1.out
e 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
.