Por que um programa com fork () às vezes imprime sua saída várias vezes?

50

No Programa 1, Hello worldé impresso apenas uma vez, mas quando eu removo \ne o executo (Programa 2), a saída é impressa 8 vezes. Alguém pode me explicar o significado \ndaqui e como isso afeta o fork()?

Programa 1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

Saída 1:

hello world... 

Programa 2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

Saída 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
fonte
10
Tente executar o Programa 1 com saída para um arquivo ( ./prog1 > prog1.out) ou um pipe ( ./prog1 | cat). Se prepare para ficar extremamente surpreso. :-) ⁠
G-Man diz 'Restabelecer Monica'
Relevante Q + A cobrindo uma outra variante deste problema: sistema C ( “bash”) ignora stdin
Michael Homer
13
Isso reuniu alguns votos íntimos, portanto, um comentário sobre o assunto: perguntas sobre "API UNIX C e interfaces de sistema" são explicitamente permitidas . Problemas de buffer são um encontro comum também em scripts de shell e também fork()são específicos para unix, portanto, parece que isso é bastante tópico para o unix.SE.
Ilkkachu
@ilkkachu, na verdade, se você ler esse link e clicar na meta questão a que se refere, explicita claramente que isso está fora do tópico. Só porque algo é C, e o unix tem C, não entra no tópico.
Patrick
@ Patrick, na verdade, eu fiz. E ainda acho que se encaixa na cláusula "dentro da razão", mas é claro que sou apenas eu.
precisa saber é

Respostas:

93

Ao produzir para a saída padrão usando a printf()função da biblioteca C , a saída geralmente é armazenada em buffer. O buffer não é liberado até você emitir uma nova linha, chamar fflush(stdout)ou sair do programa ( _exit()embora não seja através da chamada ). O fluxo de saída padrão é, por padrão, armazenado em buffer de linha desta maneira quando conectado a um TTY.

Quando você bifurca o processo no "Programa 2", os processos filhos herdam todas as partes do processo pai, incluindo o buffer de saída não liberado. Isso efetivamente copia o buffer não liberado para cada processo filho.

Quando o processo termina, os buffers são liberados. Você inicia um total geral de oito processos (incluindo o processo original) e o buffer não liberado será liberado no final de cada processo individual.

São oito, porque em cada um fork()você obtém o dobro do número de processos que tinha antes fork()(já que são incondicionais), e você tem três deles (2 3 = 8).

Kusalananda
fonte
14
Relacionado: você pode terminar maincom _exit(0)apenas fazer uma chamada ao sistema de saída sem liberar buffers e, em seguida, ela será impressa zero vezes sem uma nova linha. ( A implementação em Syscall de exit () e Como é que _exit (0) (saindo por syscall) me impede de receber qualquer conteúdo stdout? ). Ou você pode canalizar o Program1 catou redirecioná-lo para um arquivo e vê-lo ser impresso 8 vezes. (o padrão stdout é totalmente armazenado em buffer quando não é um TTY). Ou adicionar um fflush(stdout)para o caso de não-nova linha antes da 2ª fork()...
Peter Cordes
17

Não afeta o garfo de forma alguma.

No primeiro caso, você acaba com 8 processos sem nada para gravar, porque o buffer de saída já estava esvaziado (devido ao \n).

No segundo caso, você ainda possui 8 processos, cada um com um buffer contendo "Hello world ..." e o buffer é gravado no final do processo.

edc65
fonte
12

@Kusalananda explicou por que a saída é repetido . Se você está curioso para saber por que a saída é repetida 8 vezes e não apenas 4 vezes (o programa base + 3 garfos):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}
Honza Zidek
fonte
2
isso é básico do fork
Prvt_Yadav
3
@Debian_yadav provavelmente óbvio apenas se você estiver familiarizado com suas implicações. Como liberar buffers stdio , por exemplo.
roaima
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - por que devemos fazer perguntas se todo mundo sabe de tudo?
Honra Zidek
8
@Debian_yadav Não consigo ler a mente do OP, então não sei. De qualquer forma, stackexchange é um lugar onde outros também buscam conhecimento e acho que minha resposta pode ser uma adição útil à boa resposta de Kulasandra. Minha resposta adiciona algo (básico, mas útil), comparado ao do edc65, que apenas repete o que Kulasandra disse duas horas antes dele.
Honza Zidek
2
Este é apenas um breve comentário para uma resposta, não uma resposta real. A pergunta sobre "várias vezes" não porque é exatamente 8.
tubo de
3

O pano de fundo importante aqui é que stdouté necessário que a linha seja armazenada em buffer pelo padrão como configuração padrão.

Isso faz com que \na descarga da saída.

Como o segundo exemplo não contém a nova linha, a saída não é liberada e, como fork()copia todo o processo, também copia o estado do stdoutbuffer.

Agora, essas fork()chamadas no seu exemplo criam 8 processos no total - todos com uma cópia do estado do stdoutbuffer.

Por definição, todos esses processos são chamados exit()ao retornar main()e exit()chamadas fflush()seguidos fclose()em todos os fluxos de stdio ativos . Isso inclui stdoute, como resultado, você vê o mesmo conteúdo oito vezes.

É uma boa prática chamar fflush()todos os fluxos com saída pendente antes de chamar fork()ou permitir que o filho bifurcado chame explicitamente _exit()que só sai do processo sem liberar os fluxos de stdio.

Observe que a chamada exec()não libera os stdio buffers, portanto, não há problema em se preocupar com os stdio buffers se você (após a chamada fork()) ligar exec()e (se isso falhar) ligar _exit().

BTW: Para entender que o buffer incorreto pode causar, eis um bug anterior no Linux que foi corrigido recentemente:

O padrão requer stderrque não seja stderrarmazenado o buffer por padrão, mas o Linux ignorou isso e tornou a linha armazenada em buffer e (ainda pior) totalmente armazenada em buffer, caso o stderr fosse redirecionado através de um canal. Assim, os programas escritos para UNIX produziram coisas sem nova linha tarde demais no Linux.

Veja o comentário abaixo, parece estar corrigido agora.

Isto é o que eu faço para solucionar esse problema do Linux:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

Esse código não faz mal a outras plataformas, já que chamar fflush()um fluxo que acabou de ser liberado é um noop.

esperto
fonte
2
Não, é necessário que o stdout seja totalmente armazenado em buffer, a menos que seja um dispositivo interativo e, nesse caso, não seja especificado, mas, na prática, é armazenado em buffer da linha. É necessário que o stderr não seja totalmente armazenado em buffer. Veja pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas
Minha página de manual para setbuf(), no Debian ( a página man7.org é semelhante ), declara que "o padrão stderr de fluxo de erro é sempre sem buffer por padrão." e um teste simples parece agir dessa maneira, independentemente de a saída ir para um arquivo, um tubo ou um terminal. Você tem alguma referência para qual versão da biblioteca C faria de outra maneira?
Ilkkachu
4
O Linux é um kernel, o buffer stdio é um recurso da terra do usuário, o kernel não está envolvido lá. Existem várias implementações da libc disponíveis para os kernels do Linux, a mais comum em sistemas do tipo servidor / estação de trabalho é a implementação da GNU, com a qual o stdout é com buffer completo (linha com buffer se tty) e o stderr é sem buffer.
Stéphane Chazelas
11
@ Schily, apenas o teste que eu executei: paste.dy.fi/xk4 . Também obtive o mesmo resultado com um sistema terrivelmente desatualizado.
precisa saber é o seguinte
11
@ Schily Isso não é verdade. Por exemplo, estou escrevendo este comentário usando o Alpine Linux, que usa musl.
NieDzejkob 5/06