Como o shell/init cria os streams stdio?

Como o shell/init cria os streams stdio?

Estou lendo a fonte do MITSO xv6. Este trecho 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 sepelo menos3 descritores de arquivo estão abertos (presumivelmente para stdin, stdout e stderr) verificando se o descritor de arquivo recém-alocado está acima (ou igual a) 3.

1) Como é possível usar openo mesmo dispositivo várias vezes no mesmo processo e esperar descritores de arquivo diferentes?

2) Para entender isso, executei um trecho semelhante em minha máquina host (x86_64 Linux 4.6.0.1). O programa de teste openeditou repetidamente 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-ing um arquivo real e um dispositivo (como /dev/console) de alguma forma difere porque o trecho do 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á o resultado da execução:

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

EDITARCom base em uma das respostas, executei straceo executável e descobri queopen de fatoretorna vários descritores de arquivo, mas todos não são impressos, por algum motivo. Por que isso aconteceria?

3) Um pouco não relacionado, mas a convenção de usar fluxos stdio no fds 0-2 não é apenas isso - uma convenção? Por exemplo, se a sequência de inicialização alocasse os descritores de arquivo de entrada/saída para outra coisa - isso afetaria de alguma forma o modo como seus filhos executam sua E/S?

Responder1

Na verdade, são 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 fdfor maior que zero (o que, como os descritores de arquivo 0, 1 e 2 estão abertos, provavelmente é uma boa suposição).

Para #1: oopenchamada (quando bem-sucedida) é definida para retornar descritores de arquivo distintos. Se o dispositivo não puder ser reaberto, openretornaria -1.

Para o número 3: claro, isso é umconvenção, mas também noPOSIXpadrão. Outros sistemas usaram outras convenções, incluindo ter um quarto fluxo aberto para cada programa.

Leitura adicional:Usando seu ambiente Aegis (julho de 1988)
Consulte a página 6-9, que diz que o Apollo Domain/OS apresentou erroentradae **saída*

Responder2

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

Cada descritor de arquivo é simplesmente um ponteiro para algum local em algum arquivo. Portanto não há problema em 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 convenção forte sobre fd 0 a 2. Se algum código quiser imprimir em stdout, na verdade ele imprime em fd 1. Não há como "mapear" stdout para outra coisa.

Responder3

1) Se o sistema suporta a abertura do mesmo arquivo 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ê poderá acabar com o mesmo arquivo duas vezes no mesmo processo, ou seja, se for herdado uma vez e aberto uma vez pelo próprio processo. Verificar quais arquivos o processo abriu e retornar uma referência ao anterior seria um trabalho extra.

Além disso, há a questão de saber se diferentes partes do processo usam o mesmo arquivo simultaneamente. Digamos que uma biblioteca use algum arquivo de configuração ao mesmo tempo 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 (no mesmo fd), o que deve acontecer quando ele for fechado? Poderia haver outra camada de contagem de referência para decidir quandorealmentefeche o arquivo, mas isso não ajudaria com os outros problemas.


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

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

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

Executá-lo stracemostra que o arquivo foi 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 emPOSIX e o padrão ISO C:

No início do programa, três fluxos devem ser predefinidos e não precisam ser abertos explicitamente: entrada padrão (para leitura de entrada convencional), saída padrão (para escrita de saída convencional) e erro padrão (para escrita de saída de diagnóstico).

Mas, de qualquer forma, é uma convenção que pode ser possível executar um programa sem eles, sem que o kernel se importe. Ou pode não ser: lendo a especificação das execchamadas, parece serpermitiu que uma implementação abrisse algo para você:

Se o descritor de arquivo 0, 1 ou 2 for fechado após uma chamada bem-sucedida para uma das funções da família exec, as implementações poderão abrir um arquivo não especificado para o descritor de arquivo na nova imagem do processo.

(É claro que você pode fechá-los no próprio programa.)

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

Se um utilitário padrão ou uma aplicação conforme for executado com o descritor de arquivo 0 não aberto para leitura ou com o descritor de arquivo 1 ou 2 não aberto para escrita, o ambiente no qual o utilitário ou aplicação é executado será considerado não conforme e, conseqüentemente, o utilitário ou aplicativo pode não se comportar conforme descrito nesta norma.

OAs páginas de manual do Linux colocam isso de forma prática:

Como princípio geral, nenhum programa portátil, privilegiado ou não, pode assumir que esses três descritores de arquivo permanecerão fechados durante um execve().

informação relacionada