Denne artikkelen beskriver implementeringen av pipelines i Unix-kjernen. Jeg var litt skuffet over at en nylig artikkel med tittelen "
Hva snakker vi om?
Pipelines, "sannsynligvis den viktigste oppfinnelsen i Unix," er en definerende egenskap ved den underliggende Unix-filosofien om å koble sammen små programmer, samt et kjent tegn på kommandolinjen:
$ echo hello | wc -c
6
Denne funksjonaliteten avhenger av kjernen-levert systemkall pipe
, som er beskrevet på dokumentasjonssidene
Rørledninger gir en ensrettet kanal for kommunikasjon mellom prosesser. Rørledningen har en inngang (skriveende) og en utgang (lesende). Data skrevet til inngangen til rørledningen kan leses ved utgangen.
Rørledningen opprettes ved hjelp av samtalen
pipe(2)
, som returnerer to filbeskrivelser: en som refererer til inngangen til rørledningen, den andre til utdata.
Sporingsutgangen fra kommandoen ovenfor viser opprettelsen av rørledningen og strømmen av data gjennom den fra en prosess til en annen:
$ 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, "hellon", 6) = 6
[pid 2604796] dup2(3, 0) = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2) = 2
Foreldreprosessen kaller pipe()
for å få monterte filbeskrivelser. En underordnet prosess skriver til ett håndtak, og en annen prosess leser de samme dataene fra et annet håndtak. Skallet bruker dup2 for å "gi nytt navn" til beskrivelser 3 og 4 for å matche stdin og stdout.
Uten rør, ville skallet måtte skrive utdataene fra en prosess til en fil og sende den til en annen prosess for å lese dataene fra filen. Som et resultat ville vi kaste bort mer ressurser og diskplass. Imidlertid er rørledninger gode ikke bare fordi de lar deg unngå bruk av midlertidige filer:
Hvis en prosess prøver å lese fra en tom rørledning da
read(2)
vil blokkere til dataene blir tilgjengelige. Hvis en prosess prøver å skrive til en full pipeline, dawrite(2)
vil blokkere til nok data er lest fra rørledningen til å utføre skrivingen.
I likhet med POSIX-kravet er dette en viktig egenskap: å skrive til rørledningen frem til PIPE_BUF
byte (minst 512) må være atomiske slik at prosesser kan kommunisere med hverandre gjennom rørledningen på en måte som vanlige filer (som ikke gir slike garantier) ikke kan.
Når du bruker en vanlig fil, kan en prosess skrive all utdata til den og sende den videre til en annen prosess. Eller prosesser kan operere i en svært parallell modus, ved å bruke en ekstern signaleringsmekanisme (som en semafor) for å varsle hverandre når en skriving eller lesing er fullført. Transportbånd sparer oss fra alt dette bryet.
Hva leter vi etter?
Jeg vil forklare det på en enkel måte slik at det er lettere for deg å forestille deg hvordan en transportør kan fungere. Du må tildele en buffer og en tilstand i minnet. Du trenger funksjoner for å legge til og fjerne data fra bufferen. Du trenger noen midler for å kalle opp funksjoner under lese- og skriveoperasjoner på filbeskrivelser. Og du trenger låser for å implementere den spesielle oppførselen beskrevet ovenfor.
Nå er vi klare til å avhøre kjernekildekoden under sterkt lampelys for å bekrefte eller avkrefte vår vage mentale modell. Men vær alltid forberedt på det uventede.
Hvor ser vi?
Jeg vet ikke hvor mitt eksemplar av den berømte boken er "
Å vandre gjennom TUHS-arkivene er som å besøke et museum. Vi kan se på vår felles historie, og jeg har respekt for mange års innsats for å gjenvinne alt dette materialet bit for bit fra gamle bånd og trykk. Og jeg er svært klar over de fragmentene som fortsatt mangler.
Etter å ha tilfredsstilt vår nysgjerrighet om transportørenes eldgamle historie, kan vi se på moderne kjerner for sammenligning.
Forresten, pipe
er systemanrop nummer 42 i tabellen sysent[]
. Tilfeldigheter?
Tradisjonelle Unix-kjerner (1970–1974)
Jeg fant ingen spor pipe(2)
verken i
Det opplyser TUHS
Unix 1973rd Edition var den siste versjonen med en kjerne skrevet i assemblerspråk, men også den første versjonen med pipelines. I løpet av XNUMX ble det arbeidet med å forbedre den tredje utgaven, kjernen ble skrevet om i C, og dermed dukket den fjerde utgaven av Unix opp.
En leser fant en skanning av et dokument der Doug McIlroy foreslo ideen om å "koble programmer som en hageslange."
I Brian Kernighans bok
Da Unix kom ut, førte min fascinasjon for koroutiner til at jeg spurte OS-forfatteren, Ken Thompson, om å la data skrevet til en prosess gå ikke bare til enheten, men også til en annen prosess. Ken bestemte at det var mulig. Men som minimalist ønsket han at hver systemfunksjon skulle spille en betydelig rolle. Er det virkelig en stor fordel å skrive direkte mellom prosesser fremfor å skrive til en mellomfil? Det var først da jeg kom med et spesifikt forslag med det fengende navnet «pipeline» og en beskrivelse av syntaksen for interaksjon mellom prosesser at Ken til slutt utbrøt: «Jeg skal gjøre det!»
Og gjorde. En skjebnesvanger kveld endret Ken kjernen og skallet, fikset flere standardprogrammer for å standardisere hvordan de godtok input (som kunne komme fra en pipeline), og endret også filnavn. Dagen etter begynte rørledninger å bli brukt veldig mye i applikasjoner. Ved slutten av uken brukte sekretærer dem til å sende dokumenter fra tekstbehandlere til skriveren. Litt senere erstattet Ken den originale API-en og syntaksen for å pakke bruken av rørledninger med renere konvensjoner, som har blitt brukt siden.
Dessverre har kildekoden for den tredje utgaven av Unix-kjernen gått tapt. Og selv om vi har kjernekildekoden skrevet i C
Vi har tekstdokumentasjon for pipe(2)
fra begge utgivelsene, så du kan begynne med å søke i dokumentasjonen pipe(2)
er skrevet på assemblerspråk og returnerer bare én filbeskrivelse, men gir allerede den forventede grunnleggende funksjonaliteten:
Systemanrop rør oppretter en input/output-mekanisme kalt en pipeline. Den returnerte filbeskrivelsen kan brukes til lese- og skriveoperasjoner. Når noe skrives til rørledningen, bufres opptil 504 byte med data, hvoretter skriveprosessen stanses. Ved lesing fra rørledningen blir de bufrede dataene tatt bort.
Året etter hadde kjernen blitt skrevet om i C, og pipe(fildes)
'
Systemanrop rør oppretter en input/output-mekanisme kalt en pipeline. De returnerte filbeskrivelsene kan brukes i lese- og skriveoperasjoner. Når noe skrives til rørledningen, brukes håndtaket som returneres i r1 (resp. fildes[1]), bufret til 4096 byte med data, hvoretter skriveprosessen suspenderes. Ved lesing fra rørledningen tar håndtaket tilbake til r0 (resp. fildes[0]) dataene.
Det antas at når en rørledning er definert, to (eller flere) kommunikasjonsprosesser (skapt av påfølgende anrop til gaffel) vil overføre data fra rørledningen ved hjelp av samtaler lese и skrive.
Skallet har en syntaks for å definere en lineær rekke prosesser forbundet med en rørledning.
Anrop om å lese fra en tom pipeline (som ikke inneholder bufrede data) som bare har én ende (alle skrivefilbeskrivelser er lukket) returnerer "end of file". Oppfordringer til å skrive i en lignende situasjon ignoreres.
Tidligst
Sjette utgave av Unix (1975)
La oss begynne å lese Unix-kildekoden
I mange år boken Lions var det eneste dokumentet på Unix-kjernen tilgjengelig utenfor Bell Labs. Selv om lisensen for sjette utgave tillot lærere å bruke kildekoden, utelukket lisensen for syvende utgave denne muligheten, så boken ble distribuert i form av ulovlige maskinskrevne kopier.
I dag kan du kjøpe et opptrykk av boken, hvis omslag viser elevene ved en kopimaskin. Og takket være Warren Toomey (som startet TUHS-prosjektet) kan du laste ned
For mer enn 15 år siden skrev jeg en kopi av kildekoden som ble gitt inn Lions, fordi jeg ikke likte kvaliteten på kopien min fra et ukjent antall andre kopier. TUHS eksisterte ikke ennå, og jeg hadde ikke tilgang til de gamle kildene. Men i 1988 fant jeg et gammelt 9-spors bånd som inneholdt en sikkerhetskopi fra en PDP11-datamaskin. Det var vanskelig å si om det fungerte, men det var et intakt /usr/src/-tre der de fleste filene var merket med årstallet 1979, som selv da så gammelt ut. Det var den syvende utgaven eller dens avledede PWB, som jeg trodde.
Jeg tok funnet som grunnlag og redigerte kildene manuelt til sjette utgave. Noe av koden forble den samme, men noen måtte redigeres litt, og endret det moderne +=-tokenet til det utdaterte =+. Noen ting ble rett og slett slettet, og noen måtte skrives helt om, men ikke for mye.
Og i dag kan vi lese online på TUHS kildekoden til den sjette utgaven fra
Forresten, ved første øyekast, er hovedtrekket til C-koden før perioden til Kernighan og Ritchie dens korthet. Det er ikke ofte jeg klarer å sette inn kodebiter uten omfattende redigering for å passe til et relativt smalt visningsområde på nettstedet mitt.
Tidlig
/*
* 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
Bufferstørrelsen har ikke endret seg siden den fjerde utgaven. Men her ser vi, uten offentlig dokumentasjon, at rørledninger en gang brukte filer som backup-lagring!
Når det gjelder LARG filer, tilsvarer de
Her er den virkelige systemanropet 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;
}
Kommentaren beskriver tydelig hva som skjer her. Men å forstå koden er ikke så lett, delvis på grunn av måten "R0
и R1
Systemanropsparametere og returverdier sendes.
La oss prøve med
pipe()
må gjennom R0
и R1
returnere filbeskrivelsesnumre for lesing og skriving. falloc()
returnerer en peker til filstrukturen, men "returnerer" også via u.u_ar0[R0]
og en filbeskrivelse. Det vil si at koden lagres i r
filbeskrivelse for lesing og tildeler en filbeskrivelse for å skrive direkte fra u.u_ar0[R0]
etter den andre samtalen falloc()
.
flagg FPIPE
, som vi angir når vi lager rørledningen, kontrollerer funksjonen til funksjonen
/*
* 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);
}
/* … */
}
Deretter funksjonen readp()
в pipe.c
leser data fra rørledningen. Men det er bedre å spore implementeringen fra writep()
. Igjen har koden blitt mer kompleks på grunn av konvensjonene for å sende argumenter, men noen detaljer kan utelates.
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;
}
Vi ønsker å skrive bytes til pipeline-inngangen u.u_count
. Først må vi låse inoden (se nedenfor plock
/prele
).
Deretter sjekker vi inodereferansetelleren. Så lenge begge ender av rørledningen forblir åpne, skal telleren være lik 2. Vi holder en lenke (fra rp->f_inode
), så hvis telleren er mindre enn 2, må det bety at leseprosessen har lukket enden av rørledningen. Med andre ord, vi prøver å skrive til en lukket rørledning, og dette er en feil. Første gangs feilkode EPIPE
og signal SIGPIPE
dukket opp i den sjette utgaven av Unix.
Men selv om transportøren er åpen, kan den være full. I dette tilfellet slipper vi låsen og går i dvale i håp om at en annen prosess vil lese fra rørledningen og frigjøre nok plass i den. Etter å ha våknet går vi tilbake til begynnelsen, legger på låsen igjen og starter en ny opptakssyklus.
Hvis det er nok ledig plass i rørledningen, skriver vi data til den ved hjelp av i_size1
inode (hvis rørledningen er tom, kan den være lik 0) indikerer slutten av dataene som den allerede inneholder. Hvis det er nok opptaksplass, kan vi fylle rørledningen fra i_size1
til PIPESIZ
. Så slipper vi låsen og prøver å vekke enhver prosess som venter på å lese fra rørledningen. Vi går tilbake til begynnelsen for å se om vi var i stand til å skrive så mange byte som vi trengte. Hvis det mislykkes, starter vi en ny opptakssyklus.
Vanligvis parameteren i_mode
inode brukes til å lagre tillatelser r
, w
и x
. Men når det gjelder rørledninger, signaliserer vi at en eller annen prosess venter på å skrive eller lese ved hjelp av biter IREAD
и IWRITE
hhv. Prosessen setter flagget og kaller sleep()
, og det forventes at en annen prosess i fremtiden vil føre til wakeup()
.
Den virkelige magien skjer i sleep()
и wakeup()
. De er implementert i
/*
* 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) /* … */
Prosessen som forårsaker sleep()
for en bestemt kanal, kan senere vekkes av en annen prosess, som vil forårsake wakeup()
for samme kanal. writep()
и readp()
koordinere sine handlinger gjennom slike sammenkoblede samtaler. noter det pipe.c
alltid prioriterer PPIPE
når du ringer sleep()
, så det er det sleep()
kan bli avbrutt av et signal.
Nå har vi alt for å forstå funksjonen 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);
}
Du kan finne det lettere å lese denne funksjonen fra bunn til topp. "Les og returner"-grenen brukes vanligvis når det er noen data i rørledningen. I dette tilfellet bruker vi f_offset
lesing, og oppdater deretter verdien til den tilsvarende forskyvningen.
Ved påfølgende avlesninger vil rørledningen være tom hvis avlesningsforskyvningen har nådd i_size1
på inode. Vi tilbakestiller posisjonen til 0 og prøver å vekke enhver prosess som ønsker å skrive til rørledningen. Vi vet at når transportøren er full, writep()
vil sovne på ip+1
. Og nå som rørledningen er tom, kan vi vekke den for å gjenoppta skrivesyklusen.
Hvis du ikke har noe å lese, da readp()
kan sette et flagg IREAD
og sovne videre ip+2
. Vi vet hva som vil vekke ham writep()
, når den skriver noen data til rørledningen.
Kommentarer til u
"Vi kan behandle dem som vanlige I/O-funksjoner som tar en fil, en posisjon, en buffer i minnet og teller antall byte å lese eller skrive.
/*
* 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;
/* … */
Når det gjelder den "konservative" blokkeringen, altså readp()
и writep()
blokker inoden til de er ferdige med arbeidet eller mottar et resultat (det vil si ring wakeup
). plock()
и prele()
arbeid enkelt: ved å bruke et annet sett med samtaler sleep
и wakeup
la oss vekke enhver prosess som trenger låsen vi nettopp har sluppet:
/*
* 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);
}
}
Først kunne jeg ikke forstå hvorfor readp()
ikke forårsaker prele(ip)
før samtalen wakeup(ip+1)
. Det første er writep()
årsaker i sin syklus, dette plock(ip)
, som fører til vranglås hvis readp()
har ikke fjernet blokkeringen min ennå, så på en eller annen måte må koden fungere riktig. Hvis du ser på wakeup()
, så blir det klart at det bare markerer soveprosessen som klar til å utføres, slik at i fremtiden sched()
virkelig lanserte den. Så readp()
årsaker wakeup()
, fjerner låsen, setter IREAD
og samtaler sleep(ip+2)
- alt dette før writep()
gjenopptar syklusen.
Dette fullfører beskrivelsen av transportbånd i sjette utgave. Enkel kode, vidtrekkende konsekvenser.
Xv6, en enkel Unix-lignende kjerne
For å lage kjernen
Koden inneholder en tydelig og gjennomtenkt implementering 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()
angir tilstanden til resten av implementeringen, som inkluderer funksjonene piperead()
, pipewrite()
и pipeclose()
. Faktisk systemanrop sys_pipe
er en innpakning implementert i
Linux 0.01
Linux 0.01 kildekode kan bli funnet. Det vil være lærerikt å studere gjennomføringen av rørledninger i hans fs
/pipe.c
. Denne bruker en inode for å representere rørledningen, men selve rørledningen er skrevet i moderne C. Hvis du har jobbet deg gjennom XNUMX. utgave kode, vil du ikke ha noen problemer her. Slik ser funksjonen ut 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;
}
Uten engang å se på strukturdefinisjonene, kan du finne ut hvordan inodereferansetellingen brukes til å sjekke om en skriveoperasjon resulterer i SIGPIPE
. I tillegg til å jobbe byte-for-byte, er denne funksjonen enkel å sammenligne med ideene beskrevet ovenfor. Til og med logikk sleep_on
/wake_up
ser ikke så fremmed ut.
Moderne Linux-kjerner, FreeBSD, NetBSD, OpenBSD
Jeg kjørte raskt gjennom noen moderne kjerner. Ingen av dem har en diskimplementering lenger (ikke overraskende). Linux har sin egen implementering. Selv om de tre moderne BSD-kjernene inneholder implementeringer basert på kode som ble skrevet av John Dyson, har de med årene blitt for forskjellige fra hverandre.
Å lese fs
/pipe.c
(på Linux) eller sys
/kern
/sys_pipe.c
(på *BSD), det krever virkelig dedikasjon. Dagens kode handler om ytelse og støtte for funksjoner som vektor og asynkron I/O. Og detaljene for minneallokering, låser og kjernekonfigurasjon varierer veldig. Dette er ikke hva høyskoler trenger for et introduksjonskurs i operativsystemer.
Uansett, jeg var interessert i å grave opp noen gamle mønstre (som å generere SIGPIPE
og returnere EPIPE
når du skriver til en lukket pipeline) i alle disse forskjellige moderne kjernene. Jeg kommer nok aldri til å se en PDP-11 datamaskin i virkeligheten, men det er fortsatt mye å lære av kode som ble skrevet år før jeg ble født.
En artikkel skrevet av Divi Kapoor i 2011:
Kilde: www.habr.com