Como os pipelines Unix são implementados


Este artigo descreve a implementação de pipelines no kernel Unix. Fiquei um pouco decepcionado com um artigo recente intitulado " Como os pipelines funcionam no Unix?" " Não era sobre o dispositivo interno. Fiquei interessado e me enterrei nas fontes antigas para encontrar a resposta.

Sobre o que estamos conversando?


Pipelines - "provavelmente a invenção mais importante no Unix" - é a característica definidora da filosofia subjacente do Unix de combinar pequenos programas juntos, bem como a linha de comando familiar:

$ echo hello | wc -c
6

Essa funcionalidade depende da chamada do sistema fornecida pelo kernel pipe, descrita nas páginas de documentação do pipe (7) e pipe (2) :

Os transportadores fornecem um canal de comunicação entre processos unidirecional. O pipeline possui uma entrada (final de gravação) e uma saída (final de leitura). Os dados gravados na entrada do pipeline podem ser lidos.

O pipeline é criado usando uma chamada pipe(2)que retorna dois descritores de arquivo: um refere-se à entrada do pipeline e o segundo à saída.

Os resultados do rastreamento do comando acima demonstram a criação de um pipeline e o fluxo de dados através dele de um processo para outro:

$ strace -qf -e execve,pipe,dup2,read,write \
    sh -c 'echo hello | wc -c'

execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4])                            = 0
[pid 2604795] dup2(4, 1)                = 1
[pid 2604795] write(1, "hello\n", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hello\n", 16384) = 6
[pid 2604796] write(1, "6\n", 2)        = 2

O processo pai chama pipe()para obter os descritores de arquivo anexados. Um processo filho grava em um descritor e outro processo lê os mesmos dados de outro descritor. O wrapper usando dup2 “renomeia” os descritores 3 e 4 para corresponder a stdin e stdout.

Sem pipelines, o shell precisaria gravar o resultado de um processo em um arquivo e transferi-lo para outro processo, para que ele lesse os dados do arquivo. Como resultado, gastaríamos mais recursos e espaço em disco. No entanto, os pipelines são bons não apenas porque evitam o uso de arquivos temporários:

, read(2) , . , write(2) , .

Como o requisito POSIX, esta é uma propriedade importante: a gravação no pipeline de até PIPE_BUFbytes (pelo menos 512) deve ser atômica para que os processos possam interagir entre si através do pipeline da mesma maneira que os arquivos regulares (que não fornecem tais garantias).

Ao usar um arquivo regular, um processo pode gravar todos os seus dados de saída e transferi-los para outro processo. Ou os processos podem operar no modo de paralelização rígida, usando um mecanismo de sinalização externo (como um semáforo) para informar um ao outro sobre a conclusão da escrita ou leitura. Os transportadores nos poupam todo esse problema.

O que você está procurando?


Vou explicar nos meus dedos para facilitar a ideia de como o transportador pode funcionar. Você precisará alocar um buffer e algum estado na memória. Você precisará de funções para adicionar e remover dados do buffer. Serão necessários alguns meios para chamar funções durante operações de leitura e gravação em descritores de arquivo. E são necessários bloqueios para implementar o comportamento especial descrito acima.

Agora, estamos prontos para interrogar à luz brilhante das lâmpadas o código fonte do núcleo para confirmar ou refutar nosso modelo mental vago. Mas esteja sempre preparado para o inesperado.

Para onde estamos olhando?


Não sei onde está localizada minha cópia do famoso livro " Lions book " com o código-fonte do Unix 6, mas, graças à The Unix Heritage Society, você pode procurar online versões ainda mais antigas do Unix no código-fonte .

Vagando pelos arquivos do TUHS é como visitar um museu. Podemos dar uma olhada em nossa história comum e respeito os muitos anos de esforços para recuperar todos esses materiais, pouco a pouco, de cassetes e impressões antigas. E tenho plena consciência dos fragmentos que ainda estão faltando.

Tendo satisfeito nossa curiosidade em relação à história antiga dos transportadores, podemos observar os núcleos modernos para comparação.

A propósito, pipeé um número de chamada do sistema 42 na tabela sysent[]. Coincidência?

Núcleos tradicionais do Unix (1970-1974)


Não encontrei traços pipe(2)no PDP-7 Unix (janeiro de 1970), nem na primeira edição do Unix (novembro de 1971), nem no código fonte incompleto da segunda edição (junho de 1972).

A TUHS alega que a terceira edição do Unix (fevereiro de 1973) foi a primeira versão com pipelines:

A terceira edição do Unix era a versão mais recente com um kernel escrito em linguagem assembly, mas a primeira versão com pipelines. Durante 1973, estavam em andamento trabalhos para melhorar a terceira edição, o núcleo foi reescrito em C e, portanto, a quarta edição do Unix apareceu.

Um dos leitores encontrou uma digitalização de um documento no qual Doug McIlroy propôs a idéia de "conectar programas pelo princípio de uma mangueira de jardim".


No livro de Brian Kernighan, “ Unix: Uma História e um Livro de Memórias ”, na história da aparência de transportadores, este documento também é mencionado: “... ficou pendurado na parede do meu escritório no Bell Labs por 30 anos”. Aqui está uma entrevista com McIlroy e outra história do trabalho de McIlroy, escrita em 2014 :

Unix, , , , - , , . , . , , , . ? «» , , -, : « !».

. , , ( ), . . . API , .

Infelizmente, o código fonte do kernel para a terceira edição do Unix está perdido. E embora tenhamos o código fonte do kernel da quarta edição escrito em C , lançado em novembro de 1973, ele foi lançado alguns meses antes do lançamento oficial e não contém implementações de pipeline. É uma pena que o código fonte da lendária função Unix seja perdido, possivelmente para sempre.

Temos o texto da documentação pipe(2)dos dois releases, para que você possa começar pesquisando a terceira edição da documentação (para certas palavras, sublinhado “manualmente”, uma sequência de literais ^ H, seguida de sublinhados!). Este proto é pipe(2)escrito no assembler e retorna apenas um descritor de arquivo, mas já fornece a funcionalidade básica esperada:

A chamada do sistema de tubulação cria um mecanismo de entrada de saída chamado de pipeline. O descritor de arquivo retornado pode ser usado para operações de leitura e gravação. Quando algo é gravado no pipeline, até 504 bytes de dados são armazenados em buffer, após o qual o processo de gravação é pausado. Ao ler a partir de um pipeline, dados em buffer são obtidos.

No ano seguinte, o kernel foi reescrito em C, e o pipe (2) na quarta edição encontrou seu visual moderno com o protótipo " pipe(fildes)":

pipe , . . - , , r1 (. fildes[1]), 4096 , . , r0 (. fildes[0]), .

, ( ) ( fork) read write.

O shell tem sintaxe para definir uma matriz linear de processos conectados por meio de um pipeline.

As chamadas de leitura de um pipeline vazio (sem dados em buffer) que possui apenas uma extremidade (todos os descritores de arquivo de gravação estão fechados) retornam o "final do arquivo". A gravação de chamadas em uma situação semelhante é ignorada.

A implementação de pipeline sobrevivente mais antiga remonta à quinta edição do Unix (junho de 1974), mas é quase idêntica à que apareceu no próximo lançamento. Apenas comentários foram adicionados, para que a quinta edição possa ser pulada.

Sexta Edição do Unix (1975)


Começamos a ler o código fonte da sexta edição do Unix (maio de 1975). Em grande parte graças ao Lions, descobrir que é muito mais fácil do que o código fonte das versões anteriores:

Por muitos anos, o Lions tem sido o único documento básico do Unix disponível fora dos muros do Bell Labs. Embora a licença da sexta edição permitisse que os professores usassem seu código-fonte, a licença da sétima edição excluía essa possibilidade; portanto, o livro foi distribuído na forma de cópias ilegais datilografadas.

Hoje você pode comprar uma cópia reimpressa do livro, na capa da qual os alunos são mostrados na fotocopiadora. E, graças a Warren Tumi (que lançou o projeto TUHS), você pode baixar o arquivo PDF com o código-fonte da sexta edição . Quero dar uma idéia de quanto esforço foi necessário para criar o arquivo:

15 , Lions, . TUHS , . 1988- 9 , PDP11. , , /usr/src/, 1979- , . PWB, .

. , , += =+. - , - , .

E hoje podemos ler on-line no TUHS o código fonte da sexta edição do arquivo, ao qual Dennis Richie estava presente .

A propósito, à primeira vista, a principal característica do código C antes do período Kernigan e Richie é sua brevidade . Não muito frequentemente, consigo incorporar trechos de código sem edição extensa para caber em uma área de exibição relativamente estreita no meu site.

No início de /usr/sys/ken/pipe.c, há um comentário explicativo (e sim, também existe / usr / sys / dmr ):

/*
 * Max allowable buffering per pipe.
 * This is also the max size of the
 * file created to implement the pipe.
 * If this size is bigger than 4096,
 * pipes will be implemented in LARG
 * files, which is probably not good.
 */
#define    PIPSIZ    4096

O tamanho do buffer não mudou desde a quarta edição. Mas aqui, sem nenhuma documentação pública, vemos que uma vez que os pipelines usavam arquivos como armazenamento de backup!

Quanto aos arquivos LARG, eles correspondem ao sinalizador de inode LARG , usado pelo "algoritmo de alto endereçamento" para processar blocos indiretos para suportar sistemas de arquivos maiores. Como Ken disse que é melhor não usá-los, terei prazer em aceitar sua palavra.

Aqui está a chamada real do sistema pipe:

/*
 * The sys-pipe entry.
 * Allocate an inode on the root device.
 * Allocate 2 file structures.
 * Put it all together with flags.
 */
pipe()
{
    register *ip, *rf, *wf;
    int r;

    ip = ialloc(rootdev);
    if(ip == NULL)
        return;
    rf = falloc();
    if(rf == NULL) {
        iput(ip);
        return;
    }
    r = u.u_ar0[R0];
    wf = falloc();
    if(wf == NULL) {
        rf->f_count = 0;
        u.u_ofile[r] = NULL;
        iput(ip);
        return;
    }
    u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
    u.u_ar0[R0] = r;           /* rf's fd */
    wf->f_flag = FWRITE|FPIPE;
    wf->f_inode = ip;
    rf->f_flag = FREAD|FPIPE;
    rf->f_inode = ip;
    ip->i_count = 2;
    ip->i_flag = IACC|IUPD;
    ip->i_mode = IALLOC;
}

O comentário descreve claramente o que está acontecendo aqui. Mas entender o código não é fácil, em parte por causa da maneira com a ajuda de um usuário de estrutura u e registra R0e R1transfere parâmetros de chamadas do sistema e valores de retorno.

Vamos tentar usar ialloc () para colocar inode (inode ) no disco e usar falloc () para colocar dois arquivos na memória . Se tudo der certo, definiremos sinalizadores para definir esses arquivos como as duas extremidades do pipeline, apontá-los para o mesmo inode (cuja contagem de referência será 2) e marcar o inode como alterado e usado. Preste atenção às chamadas para iput ()em caminhos de erro para diminuir a contagem de referência no novo inode.

pipe()deve passar R0e R1retornar os números do descritor de arquivo para leitura e gravação. falloc()retorna um ponteiro para a estrutura do arquivo, mas também “retorna” através do u.u_ar0[R0]descritor de arquivo. Ou seja, o código salva em um rdescritor de arquivo para leitura e atribui um descritor para gravação diretamente u.u_ar0[R0]após a segunda chamada falloc().

O sinalizador FPIPEque definimos ao criar o pipeline controla o comportamento da função rdwr () em sys2.c , que chama rotinas de E / S de E / S específicas:

/*
 * common code for read and write calls:
 * check permissions, set base, count, and offset,
 * and switch out to readi, writei, or pipe code.
 */
rdwr(mode)
{
    register *fp, m;

    m = mode;
    fp = getf(u.u_ar0[R0]);
        /* … */

    if(fp->f_flag&FPIPE) {
        if(m==FREAD)
            readp(fp); else
            writep(fp);
    }
        /* … */
}

Em seguida, a função readp()de pipe.clê os dados do pipeline. Mas rastrear a implementação é melhor começar writep(). Mais uma vez, o código ficou mais complicado devido às especificidades do contrato de transferência de argumentos, mas alguns detalhes podem ser omitidos.

writep(fp)
{
    register *rp, *ip, c;

    rp = fp;
    ip = rp->f_inode;
    c = u.u_count;

loop:
    /* If all done, return. */

    plock(ip);
    if(c == 0) {
        prele(ip);
        u.u_count = 0;
        return;
    }

    /*
     * If there are not both read and write sides of the
     * pipe active, return error and signal too.
     */

    if(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        return;
    }

    /*
     * If the pipe is full, wait for reads to deplete
     * and truncate it.
     */

    if(ip->i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        goto loop;
    }

    /* Write what is possible and loop back. */

    u.u_offset[0] = 0;
    u.u_offset[1] = ip->i_size1;
    u.u_count = min(c, PIPSIZ-u.u_offset[1]);
    c =- u.u_count;
    writei(ip);
    prele(ip);
    if(ip->i_mode&IREAD) {
        ip->i_mode =& ~IREAD;
        wakeup(ip+2);
    }
    goto loop;
}

Queremos escrever bytes na entrada do pipeline u.u_count. Primeiro, precisamos bloquear o inode (veja abaixo plock/ prele).

Em seguida, verifique a contagem de referência do inode. Enquanto as duas extremidades do pipeline permanecem abertas, o contador deve ser 2. Mantemos um link (fora rp->f_inode), portanto, se o contador for menor que 2, isso significa que o processo de leitura fechou o final do pipeline. Em outras palavras, estamos tentando escrever em um pipeline fechado, e isso é um erro. O código EPIPEe o sinal do erro SIGPIPEapareceram pela primeira vez na sexta edição do Unix.

Mas, mesmo que o transportador esteja aberto, ele pode estar cheio. Nesse caso, removemos a trava e dormimos na esperança de que outro processo seja lido no pipeline e liberemos espaço suficiente nele. Depois de acordar, voltamos ao início, novamente bloqueamos a fechadura e iniciamos um novo ciclo de gravação.

Se houver espaço livre suficiente no pipeline, gravamos dados usando writei () . O parâmetro i_size1inode (com um pipeline vazio pode ser 0) indica o final dos dados que ele já contém. Se houver espaço de gravação suficiente, podemos encher o transportador de i_size1atéPIPESIZ. Em seguida, removemos a trava e tentamos despertar qualquer processo que esteja aguardando a oportunidade de ler a partir do pipeline. Voltamos ao início para ver se conseguimos escrever quantos bytes precisávamos. Se falhar, iniciamos um novo ciclo de gravação.

Tipicamente i_mode, um inodo parâmetro é utilizado para armazenar permissões r, we x. Mas no caso de pipelines, sinalizamos que algum processo está aguardando para escrever ou ler usando bits IREADe, IWRITErespectivamente. Um processo define um sinalizador e chama sleep(), e espera-se que outro processo chame no futuro wakeup().

Magia real acontece em sleep()e wakeup(). Eles são implementados no slp.c, a fonte do famoso comentário: "Você não deve entender isso". Felizmente, não somos obrigados a entender o código, basta ver alguns comentários:

/*
 * Give up the processor till a wakeup occurs
 * on chan, at which time the process
 * enters the scheduling queue at priority pri.
 * The most important effect of pri is that when
 * pri<0 a signal cannot disturb the sleep;
 * if pri>=0 signals will be processed.
 * Callers of this routine must be prepared for
 * premature return, and check that the reason for
 * sleeping has gone away.
 */
sleep(chan, pri) /* … */

/*
 * Wake up all processes sleeping on chan.
 */
wakeup(chan) /* … */

Um processo que chama sleep()para um canal específico pode ser acordado posteriormente por outro processo que chamará wakeup()para o mesmo canal. writep()e readp()coordenar suas ações por meio de chamadas emparelhadas. Observe que pipe.csempre dá prioridade PPIPEao ligar sleep(), para que todos sleep()possam interromper o sinal.

Agora temos tudo para entender a função readp():

readp(fp)
int *fp;
{
    register *rp, *ip;

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Very conservative locking. */

    plock(ip);

    /*
     * If the head (read) has caught up with
     * the tail (write), reset both to 0.
     */

    if(rp->f_offset[1] == ip->i_size1) {
        if(rp->f_offset[1] != 0) {
            rp->f_offset[1] = 0;
            ip->i_size1 = 0;
            if(ip->i_mode&IWRITE) {
                ip->i_mode =& ~IWRITE;
                wakeup(ip+1);
            }
        }

        /*
         * If there are not both reader and
         * writer active, return without
         * satisfying read.
         */

        prele(ip);
        if(ip->i_count < 2)
            return;
        ip->i_mode =| IREAD;
        sleep(ip+2, PPIPE);
        goto loop;
    }

    /* Read and return */

    u.u_offset[0] = 0;
    u.u_offset[1] = rp->f_offset[1];
    readi(ip);
    rp->f_offset[1] = u.u_offset[1];
    prele(ip);
}

Você pode achar mais fácil ler esta função de baixo para cima. A ramificação “ler e retornar” geralmente é usada quando há alguns dados no pipeline. Nesse caso, usamos readi () para ler o máximo de dados disponíveis a partir da f_offsetleitura atual e atualizamos o valor do deslocamento correspondente.

Nas leituras subsequentes, o pipeline ficará vazio se o deslocamento da leitura tiver atingido o valor do i_size1inode. Redefinimos a posição para 0 e tentamos despertar qualquer processo que ele queira gravar no pipeline. Sabemos que quando o transportador estiver cheio, ele writep()adormecerá ip+1. E agora que o pipeline está vazio, podemos ativá-lo para que ele retome seu ciclo de gravação.

Se não houver nada para ler, ele readp()poderá definir uma bandeira IREADe adormecer.ip+2. Sabemos o que o despertará writep()quando ele gravar alguns dados no pipeline.

Os comentários sobre readi () e writei () ajudarão a entender que, em vez de passar parâmetros " u", podemos tratá-los como funções usuais de E / S que pegam um arquivo, posição, buffer na memória e contam o número de bytes para ler ou escrever .

/*
 * Read the file corresponding to
 * the inode pointed at by the argument.
 * The actual read arguments are found
 * in the variables:
 *    u_base        core address for destination
 *    u_offset    byte offset in file
 *    u_count        number of bytes to read
 *    u_segflg    read to kernel/user
 */
readi(aip)
struct inode *aip;
/* … */

/*
 * Write the file corresponding to
 * the inode pointed at by the argument.
 * The actual write arguments are found
 * in the variables:
 *    u_base        core address for source
 *    u_offset    byte offset in file
 *    u_count        number of bytes to write
 *    u_segflg    write to kernel/user
 */
writei(aip)
struct inode *aip;
/* … */

Quanto ao bloqueio de "conservador", em seguida, readp()e writep()bloco de inode para contanto que terminar um trabalho ou não obter o resultado (ou seja a causa wakeup). plock()e eles prele()funcionam de maneira simples: usando um conjunto diferente de chamadas sleepe wakeupnos permite ativar qualquer processo que precise de um bloqueio que acabamos de remover:

/*
 * Lock a pipe.
 * If its already locked, set the WANT bit and sleep.
 */
plock(ip)
int *ip;
{
    register *rp;

    rp = ip;
    while(rp->i_flag&ILOCK) {
        rp->i_flag =| IWANT;
        sleep(rp, PPIPE);
    }
    rp->i_flag =| ILOCK;
}

/*
 * Unlock a pipe.
 * If WANT bit is on, wakeup.
 * This routine is also used to unlock inodes in general.
 */
prele(ip)
int *ip;
{
    register *rp;

    rp = ip;
    rp->i_flag =& ~ILOCK;
    if(rp->i_flag&IWANT) {
        rp->i_flag =& ~IWANT;
        wakeup(rp);
    }
}

No começo, eu não conseguia entender por que readp()ele não ligou prele(ip)antes da ligação wakeup(ip+1). A primeira coisa que writep()causa em seu loop é que ele plock(ip)leva a um impasse se readp()ainda não removeu seu bloco; portanto, o código deve funcionar de alguma maneira corretamente. Se você observar wakeup(), fica claro que ele só marca o processo de suspensão como pronto para execução, para que no futuro ele sched()realmente o inicie. Por isso , readp()causa wakeup(), desbloqueia, define IREADe chama sleep(ip+2)- tudo isso antes de writep()retomar o ciclo.

Isso completa a descrição dos transportadores na sexta edição. Código simples, consequências de longo alcance.

Sétima Edição do Unix(Janeiro de 1979) foi a nova versão principal (quatro anos depois), na qual muitos novos aplicativos e propriedades do kernel apareceram. Além disso, houve mudanças significativas em relação ao uso de conversão de tipo, union'ov e ponteiros digitados para estruturas. No entanto, o código do pipeline não mudou muito. Podemos pular esta edição.

Xv6, um kernel simples em forma de Unix


A sexta edição do Unix influenciou a criação do núcleo Xv6 , mas foi escrita em C moderno para rodar em processadores x86. O código é fácil de ler, é claro. Além disso, ao contrário das fontes Unix com o TUHS, você pode compilá-lo, modificá-lo e executá-lo em outra coisa além do PDP 11/70. Portanto, esse núcleo é amplamente utilizado nas universidades como material educacional em sistemas operacionais. As fontes estão no Github .

O código contém uma implementação clara e bem pensada do pipe.c , apoiada por um buffer na memória em vez de inode no disco. Aqui, dou apenas a definição de "pipeline estrutural" e função pipealloc():

#define PIPESIZE 512

struct pipe {
  struct spinlock lock;
  char data[PIPESIZE];
  uint nread;     // number of bytes read
  uint nwrite;    // number of bytes written
  int readopen;   // read fd is still open
  int writeopen;  // write fd is still open
};

int
pipealloc(struct file **f0, struct file **f1)
{
  struct pipe *p;

  p = 0;
  *f0 = *f1 = 0;
  if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
    goto bad;
  if((p = (struct pipe*)kalloc()) == 0)
    goto bad;
  p->readopen = 1;
  p->writeopen = 1;
  p->nwrite = 0;
  p->nread = 0;
  initlock(&p->lock, "pipe");
  (*f0)->type = FD_PIPE;
  (*f0)->readable = 1;
  (*f0)->writable = 0;
  (*f0)->pipe = p;
  (*f1)->type = FD_PIPE;
  (*f1)->readable = 0;
  (*f1)->writable = 1;
  (*f1)->pipe = p;
  return 0;

 bad:
  if(p)
    kfree((char*)p);
  if(*f0)
    fileclose(*f0);
  if(*f1)
    fileclose(*f1);
  return -1;
}

pipealloc()define o estado do restante da implementação, que inclui funções piperead(), pipewrite()e pipeclose(). A chamada do sistema real sys_pipeé um wrapper implementado no sysfile.c . Eu recomendo a leitura de todo o seu código. A complexidade está no nível de origem da sexta edição, mas é muito mais fácil e mais agradável de ler.

Linux 0.01


Você pode encontrar o código fonte do Linux 0.01. Será instrutivo estudar a implementação de gasodutos em seu fs/ pipe.c. Aqui, o inode é usado para representar o pipeline, mas o próprio pipeline é escrito no C. moderno. Se você passou pelo código da sexta edição, aqui não terá dificuldades. É assim que a função se parece write_pipe():

int write_pipe(struct m_inode * inode, char * buf, int count)
{
    char * b=buf;

    wake_up(&inode->i_wait);
    if (inode->i_count != 2) { /* no readers */
        current->signal |= (1<<(SIGPIPE-1));
        return -1;
    }
    while (count-->0) {
        while (PIPE_FULL(*inode)) {
            wake_up(&inode->i_wait);
            if (inode->i_count != 2) {
                current->signal |= (1<<(SIGPIPE-1));
                return b-buf;
            }
            sleep_on(&inode->i_wait);
        }
        ((char *)inode->i_size)[PIPE_HEAD(*inode)] =
            get_fs_byte(b++);
        INC_PIPE( PIPE_HEAD(*inode) );
        wake_up(&inode->i_wait);
    }
    wake_up(&inode->i_wait);
    return b-buf;
}

Mesmo sem examinar as definições de estruturas, você pode descobrir como o contador de referência do inode é usado para verificar se a operação de gravação leva a isso SIGPIPE. Além do trabalho em bytes, esta função é facilmente correlacionada com as idéias acima. Até a lógica sleep_on/ wake_upnão parece tão estranha.

Kernels modernos do Linux, FreeBSD, NetBSD, OpenBSD


Eu rapidamente examinei alguns kernels modernos. Nenhum deles já possui uma implementação de disco (não surpreendentemente). O Linux tem sua própria implementação. Embora os três kernels modernos do BSD contenham implementações baseadas no código que foi escrito por John Dyson, ao longo dos anos eles se tornaram muito diferentes um do outro.

Para ler fs/ pipe.c(no Linux) ou sys/ kern/ sys_pipe.c(* BSD), é necessária uma verdadeira dedicação. O desempenho e o suporte a recursos como vetor e E / S assíncrona são importantes no código atual. E os detalhes da alocação de memória, bloqueios e configuração do kernel - tudo isso é muito diferente. Não é isso que as universidades precisam para um curso introdutório sobre sistemas operacionais.

De qualquer forma, foi interessante para mim descobrir vários padrões antigos (por exemplo, gerar SIGPIPEe retornar EPIPEao gravar em um pipeline fechado) em todos esses núcleos modernos tão diferentes. Provavelmente nunca verei um computador PDP-11 ativo, mas ainda há muito a aprender com o código que foi escrito alguns anos antes do meu nascimento.

Escrito por Divi Kapoor em 2011, o artigo “ A Implementação do Linux Kernel de Pipes e FIFOs ” fornece uma visão geral de como (até agora) os pipelines funcionam no Linux. E o recente commit do Linux ilustra um modelo de interação em pipeline cujos recursos excedem os de arquivos temporários; e também mostra até que ponto os pipelines foram de "bloqueio muito conservador" no kernel Unix da sexta edição.

All Articles