Como o shell / init cria os streams do stdio?

3

Estou lendo a fonte do xv6 OS do MIT. Este snippet vem no início de sh.c :

// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
    if(fd >= 3){
      close(fd);
      break;
    }
}

Eu entendo que isso verifica se pelo menos 3 descritores de arquivos estão abertos (presumivelmente para stdin, stdout e stderr) verificando se o descritor de arquivo recém-alocado está acima (ou mesmo como) 3.

1) Como é possível open o mesmo dispositivo várias vezes do mesmo processo e esperar diferentes descritores de arquivo?

2) Para entender isso, executei um trecho similar em minha máquina host (x86_64 Linux 4.6.0.1). O programa de teste repetidamente open ed um arquivo de texto em um loop para ver se podemos esperar um fd diferente, mas sempre produziu o mesmo descritor de arquivo. A partir disso, concluí que open -em um arquivo real e um dispositivo (como /dev/console ) de alguma forma difere porque o snippet de xv6 obviamente funciona (testado no Qemu). Qual é exatamente a diferença?

#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>

int main(void)
{
    int fd;
    int cnt = 0;

    while ((fd = open("sample.txt", O_RDWR) > 0)) {
        if (cnt != 10) {
            cnt++;
            printf("File descriptor opened: %d\n", fd);
        } else {
            break;
        }
    }

    return 0;
}

Aqui está a saída ao executá-lo:

$ ./a.out
File descriptor opened: 1
File descriptor opened: 1
[snip]
File descriptor opened: 1
File descriptor opened: 1

EDITAR Com base em uma das respostas, executei strace no executável e descobri que open de fato retorna vários descritores de arquivos, mas todos eles não são impressos, por algum motivo. Por que isso seria?

3) Algo não relacionado, mas a convenção de usar stdio streams em fds 0-2 não é exatamente isso - uma convenção? Por exemplo, se a sequência de inicialização alocou os descritores de arquivo de entrada / saída a algo diferente - isso afetaria de alguma forma como seus filhos fazem sua E / S?

    
por Shrikant Giridhar 30.09.2016 / 06:51

3 respostas

1

Isso é na verdade 3 perguntas. Descarte o nº 2 imediatamente porque o programa está incorreto:

    while ((fd = open("sample.txt", O_RDWR) > 0)) {

você provavelmente quis dizer

    while ((fd = open("sample.txt", O_RDWR)) > 0) {

com os parênteses colocados incorretamente, você está testando apenas se fd for maior que zero (o que, como os descritores de arquivo 0, 1 e 2 estão abertos, provavelmente é uma boa suposição).

Para o nº 1: a open chamada ( quando bem sucedido) é definido para retornar descritores de arquivos distintos. Se o dispositivo não puder ser reaberto, open retornará -1 .

Para o nº 3: claro, isso é uma convenção , mas também no POSIX standard . Outros sistemas usaram outras convenções, incluindo uma quarta linha aberta para cada programa.

Outras leituras: Usando seu ambiente de Aegis (julho de 1988)
Veja a página 6-9, que diz que o Apollo Domain / OS teve erro input e ** output *

    
por 30.09.2016 / 18:46
1

Não, o código não verifica os descritores, na verdade, os abre. Não dando descritores ainda. Cada abertura dará um novo descritor de arquivo, ou seja, 0,1,2,3. O código quebra ao atingir fd 3 deixando 0 a 2 aberto.

Cada descritor de arquivo é simplesmente um ponteiro para algum local em algum arquivo. Portanto, não é problema ter mais de um descritor para o mesmo arquivo.

Se o seu programa de teste fornecer o mesmo fd para diferentes chamadas abertas, há um bug nele. Por favor, mostre o código.

Sim, existe uma strong convenção em fd 0 a 2. Se algum código quiser imprimir em stdout, ele realmente imprime em fd 1. Não há como "mapear" o stdout para outra coisa.

    
por 30.09.2016 / 09:52
1

1) Se o sistema suportar a abertura do mesmo arquivo a partir de vários processos ao mesmo tempo, por que não permitir que um processo seja aberto várias vezes também? Como os descritores de arquivo são herdados, você pode acabar com o mesmo arquivo duas vezes no mesmo processo, ou seja, se ele for herdado uma vez e depois de aberto pelo próprio processo. Verificar quais arquivos o processo abriu e retornar uma referência para o anterior seria um trabalho extra.

Além disso, há a questão se diferentes partes do processo usam o mesmo arquivo simultaneamente. Digamos que uma biblioteca use algum arquivo de configuração ao mesmo tempo em que o programa principal o utiliza. O descritor de arquivo está vinculado ao modo de acesso e à posição do ponteiro do arquivo. Se houvesse apenas uma cópia deles, coisas estranhas aconteceriam. Além disso, se o arquivo foi aberto duas vezes (para o mesmo fd), o que deve acontecer quando ele é fechado? Poderia haver outra camada de contagem de referência para decidir quando realmente fechar o arquivo, mas isso não ajudaria nos outros problemas.

2) Depende do seu código. Uma linguagem inteligente (ou seja, não C), pode refazer a variável que mantém o arquivo aberto e fechá-lo antes de você reabrir o arquivo. Difícil dizer sem ver o código.

Mas um pequeno teste, abrindo o mesmo arquivo duas vezes para a mesma variável em Perl, resulta no mesmo número de FD:

perl -e 'open F, "test.txt"; printf "%d ", fileno(F); open F, "test.txt"; printf "%d\n", fileno(F)'
3 3

A execução em strace mostra que o arquivo é fechado imediatamente antes da reabertura.

Com duas variáveis diferentes, obtemos dois números FD:

perl -e 'open F, "test.txt"; printf "%d ", fileno(F); open G, "test.txt"; printf "%d\n", fileno(G)'
3 4

3) Tecnicamente, você poderia dizer que os números de arquivo padrão são uma convenção. Uma convenção codificada em POSIX e no padrão ISO C :

At program start-up, three streams shall be predefined and need not be opened explicitly: standard input (for reading conventional input), standard output (for writing conventional output), and standard error (for writing diagnostic output).

Mas uma convenção de qualquer maneira em que pode ser possível rodar um programa sem eles, sem a atenção do kernel. Ou talvez não: ler a especificação para as chamadas exec , parece ser permitido para uma implementação abre algo para você :

If file descriptor 0, 1, or 2 would otherwise be closed after a successful call to one of the exec family of functions, implementations may open an unspecified file for the file descriptor in the new process image.

(Você pode, claro, fechá-los no próprio programa.)

Se você optar por não tê-los na inicialização, a compatibilidade será descartada:

If a standard utility or a conforming application is executed with file descriptor 0 not open for reading or with file descriptor 1 or 2 not open for writing, the environment in which the utility or application is executed shall be deemed non-conforming, and consequently the utility or application might not behave as described in this standard.

As páginas man do Linux explicam de alguma forma :

As a general principle, no portable program, whether privileged or not, can assume that these three file descriptors will remain closed across an execve().

    
por 30.09.2016 / 16:10