Questo articolo descrive l'implementazione delle pipeline nel kernel Unix. Sono rimasto un po' deluso dal fatto che un recente articolo intitolato "
Di cosa stai parlando?
Le pipeline sono "probabilmente l'invenzione più importante in Unix" - una caratteristica distintiva della filosofia alla base di Unix di mettere insieme piccoli programmi e il familiare slogan della riga di comando:
$ echo hello | wc -c
6
Questa funzionalità dipende dalla chiamata di sistema fornita dal kernel pipe
, descritto nelle pagine della documentazione
Le pipeline forniscono un canale unidirezionale per la comunicazione tra processi. La pipeline ha un input (fine scrittura) e un output (fine lettura). I dati scritti nell'input della pipeline possono essere letti nell'output.
La pipeline viene creata chiamando
pipe(2)
, che restituisce due descrittori di file: uno si riferisce all'input della pipeline, il secondo all'output.
L'output di traccia del comando precedente dimostra la creazione di una pipeline e il flusso di dati attraverso di essa da un processo all'altro:
$ 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
Il processo padre chiama pipe()
per ottenere descrittori di file allegati. Un processo figlio scrive su un descrittore e un altro processo legge gli stessi dati da un altro descrittore. La shell "rinomina" i descrittori 2 e 3 con dup4 in modo che corrispondano a stdin e stdout.
Senza pipeline, la shell dovrebbe scrivere l'output di un processo in un file e reindirizzarlo a un altro processo per leggere i dati dal file. Di conseguenza, sprecheremmo più risorse e spazio su disco. Tuttavia, le pipeline sono utili per qualcosa di più che evitare i file temporanei:
Se un processo tenta di leggere da una pipeline vuota, allora
read(2)
si bloccherà fino a quando i dati non saranno disponibili. Se un processo tenta di scrivere su una pipeline completa, allorawrite(2)
si bloccherà fino a quando non saranno stati letti dati sufficienti dalla pipeline per completare la scrittura.
Come il requisito POSIX, questa è una proprietà importante: scrivere nella pipeline fino a PIPE_BUF
i byte (almeno 512) devono essere atomici in modo che i processi possano comunicare tra loro attraverso la pipeline in un modo che i file normali (che non forniscono tali garanzie) non possono.
Con un file normale, un processo può scrivervi tutto il suo output e passarlo a un altro processo. Oppure i processi possono operare in modalità hard parallel, utilizzando un meccanismo di segnalazione esterno (come un semaforo) per informarsi a vicenda sul completamento di una scrittura o lettura. I nastri trasportatori ci salvano da tutte queste seccature.
Cosa stiamo cercando?
Spiegherò sulle mie dita per renderti più facile immaginare come può funzionare un nastro trasportatore. Dovrai allocare un buffer e uno stato in memoria. Avrai bisogno di funzioni per aggiungere e rimuovere dati dal buffer. Avrai bisogno di alcune funzionalità per chiamare le funzioni durante le operazioni di lettura e scrittura sui descrittori di file. E i blocchi sono necessari per implementare il comportamento speciale sopra descritto.
Siamo ora pronti per interrogare il codice sorgente del kernel alla luce intensa di una lampada per confermare o smentire il nostro vago modello mentale. Ma sii sempre preparato agli imprevisti.
Dove stiamo cercando?
Non so dove si trovi la mia copia del famoso libro.
Girovagare per gli archivi TUHS è come visitare un museo. Possiamo guardare alla nostra storia condivisa e ho rispetto per gli anni di sforzi per recuperare tutto questo materiale poco a poco da vecchie cassette e stampe. E sono acutamente consapevole di quei frammenti che mancano ancora.
Soddisfatta la nostra curiosità sulla storia antica delle condutture, possiamo guardare ai nuclei moderni per un confronto.
Tra l'altro, pipe
è la chiamata di sistema numero 42 nella tabella sysent[]
. Coincidenza?
Kernel Unix tradizionali (1970–1974)
Non ho trovato alcuna traccia pipe(2)
né dentro
TUHS afferma che
La terza edizione di Unix è stata l'ultima versione con un kernel scritto in assembler, ma anche la prima versione con pipeline. Nel corso del 1973 si lavorava per migliorare la terza edizione, il kernel fu riscritto in C, e così nacque la quarta edizione di Unix.
Un lettore ha trovato una scansione di un documento in cui Doug McIlroy proponeva l'idea di "collegare i programmi come un tubo da giardino".
Nel libro di Brian Kernighan
Quando è apparso Unix, la mia passione per le coroutine mi ha fatto chiedere all'autore del sistema operativo, Ken Thompson, di consentire ai dati scritti su un processo di andare non solo al dispositivo, ma anche all'uscita di un altro processo. Ken pensava che fosse possibile. Tuttavia, da minimalista, voleva che ogni caratteristica del sistema giocasse un ruolo significativo. La scrittura diretta tra processi è davvero un grande vantaggio rispetto alla scrittura su un file intermedio? E solo quando ho fatto una proposta specifica con il nome accattivante "pipeline" e una descrizione della sintassi dell'interazione dei processi, Ken ha finalmente esclamato: "Lo farò!".
E lo ha fatto. Una fatidica sera, Ken ha cambiato il kernel e la shell, ha corretto diversi programmi standard per standardizzare il modo in cui accettano l'input (che potrebbe provenire da una pipeline) e ha cambiato i nomi dei file. Il giorno successivo, le condutture sono state ampiamente utilizzate nelle applicazioni. Entro la fine della settimana, le segretarie li usavano per inviare i documenti dagli elaboratori di testi alla stampante. Poco dopo, Ken ha sostituito l'API e la sintassi originali per avvolgere l'uso delle pipeline con convenzioni più pulite che sono state utilizzate da allora.
Sfortunatamente, il codice sorgente del kernel Unix della terza edizione è andato perduto. E sebbene abbiamo il codice sorgente del kernel scritto in C
Abbiamo il testo della documentazione per pipe(2)
da entrambe le versioni, quindi puoi iniziare cercando la documentazione pipe(2)
è scritto in assembler e restituisce solo un descrittore di file, ma fornisce già le funzionalità di base previste:
Chiamata di sistema tubo crea un meccanismo di I/O chiamato pipeline. Il descrittore di file restituito può essere utilizzato per le operazioni di lettura e scrittura. Quando qualcosa viene scritto nella pipeline, memorizza nel buffer fino a 504 byte di dati, dopodiché il processo di scrittura viene sospeso. Durante la lettura dalla pipeline, vengono acquisiti i dati memorizzati nel buffer.
Entro l'anno successivo, il kernel era stato riscritto in C, e pipe(fildes)
"
Chiamata di sistema tubo crea un meccanismo di I/O chiamato pipeline. I descrittori di file restituiti possono essere utilizzati nelle operazioni di lettura e scrittura. Quando qualcosa viene scritto nella pipeline, viene utilizzato il descrittore restituito in r1 (risp. fildes[1]), bufferizzato fino a 4096 byte di dati, dopodiché il processo di scrittura viene sospeso. Durante la lettura dalla pipeline, il descrittore restituito a r0 (risp. fildes[0]) prende i dati.
Si presume che una volta definita una pipeline, due (o più) processi interagenti (creati da successive invocazioni forcella) passerà i dati dalla pipeline utilizzando le chiamate read и scrivere.
La shell ha una sintassi per definire un array lineare di processi connessi tramite una pipeline.
Le chiamate per leggere da una pipeline vuota (che non contiene dati bufferizzati) che ha solo un'estremità (tutti i descrittori di file di scrittura chiusi) restituiscono "fine del file". Le chiamate di scrittura in una situazione simile vengono ignorate.
Più presto
Unix sesta edizione (1975)
Iniziare a leggere il codice sorgente Unix
Per molti anni il libro Lions era l'unico documento sul kernel Unix disponibile al di fuori dei Bell Labs. Sebbene la licenza della sesta edizione consentisse agli insegnanti di utilizzare il suo codice sorgente, la licenza della settima edizione escludeva questa possibilità, quindi il libro è stato distribuito in copie dattiloscritte illegali.
Oggi è possibile acquistare una copia ristampata del libro, la cui copertina raffigura gli studenti alla fotocopiatrice. E grazie a Warren Toomey (che ha avviato il progetto TUHS), puoi scaricare
Oltre 15 anni fa, ho digitato una copia del codice sorgente fornito in Lionsperché non mi piaceva la qualità della mia copia rispetto a un numero imprecisato di altre copie. TUHS non esisteva ancora e non avevo accesso alle vecchie fonti. Ma nel 1988 ho trovato un vecchio nastro con 9 tracce che aveva un backup da un computer PDP11. Era difficile sapere se funzionava, ma c'era un albero /usr/src/ intatto in cui la maggior parte dei file era contrassegnata come 1979, che anche allora sembrava antiquata. Era la settima edizione, o un derivato PWB, pensavo.
Ho preso la scoperta come base e ho modificato manualmente le fonti allo stato della sesta edizione. Parte del codice è rimasta la stessa, parte ha dovuto essere leggermente modificata, cambiando il token moderno += nell'obsoleto =+. Qualcosa è stato semplicemente cancellato e qualcosa doveva essere completamente riscritto, ma non troppo.
E oggi possiamo leggere online al TUHS il codice sorgente della sesta edizione di
A proposito, a prima vista, la caratteristica principale del codice C prima del periodo di Kernighan e Ritchie è la sua brevità. Non capita spesso di poter inserire frammenti di codice senza modifiche approfondite per adattarli a un'area di visualizzazione relativamente ristretta sul mio sito.
Presto
/*
* 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
La dimensione del buffer non è cambiata dalla quarta edizione. Ma qui vediamo, senza alcuna documentazione pubblica, che una volta le pipeline utilizzavano i file come archiviazione di riserva!
Per quanto riguarda i file LARG, corrispondono a
Ecco la vera chiamata di 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;
}
Il commento descrive chiaramente cosa sta succedendo qui. Ma non è così facile capire il codice, anche per come "R0
и R1
vengono passati i parametri della chiamata di sistema e i valori restituiti.
Proviamo con
pipe()
dovuto attraverso R0
и R1
restituisce i numeri dei descrittori di file per la lettura e la scrittura. falloc()
restituisce un puntatore a una struttura di file, ma anche "restituisce" via u.u_ar0[R0]
e un descrittore di file. Cioè, il codice è memorizzato in r
descrittore di file per la lettura e assegna un descrittore per la scrittura direttamente da u.u_ar0[R0]
dopo la seconda convocazione falloc()
.
bandiera FPIPE
, che impostiamo durante la creazione della pipeline, controlla il comportamento della funzione
/*
* 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);
}
/* … */
}
Poi la funzione readp()
в pipe.c
legge i dati dalla pipeline. Ma è meglio tracciare l'implementazione partendo da writep()
. Ancora una volta, il codice è diventato più complicato a causa delle peculiarità della convenzione di passaggio degli argomenti, ma alcuni dettagli possono essere omessi.
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;
}
Vogliamo scrivere byte nell'input della pipeline u.u_count
. Per prima cosa dobbiamo bloccare l'inode (vedi sotto plock
/prele
).
Quindi controlliamo il conteggio dei riferimenti inode. Finché entrambe le estremità della pipeline rimangono aperte, il contatore dovrebbe essere 2. Manteniamo un collegamento (da rp->f_inode
), quindi se il contatore è inferiore a 2, ciò dovrebbe significare che il processo di lettura ha chiuso la fine della pipeline. In altre parole, stiamo cercando di scrivere a una pipeline chiusa, il che è un errore. Primo codice di errore EPIPE
e segnale SIGPIPE
apparso nella sesta edizione di Unix.
Ma anche se il trasportatore è aperto, potrebbe essere pieno. In questo caso, rilasciamo il blocco e andiamo a dormire nella speranza che un altro processo legga dalla pipeline e liberi abbastanza spazio al suo interno. Quando ci svegliamo, torniamo all'inizio, riattacchiamo il lucchetto e iniziamo un nuovo ciclo di scrittura.
Se c'è abbastanza spazio libero nella pipeline, scriviamo i dati usando i_size1
l'inode'a (con una pipeline vuota può essere uguale a 0) punta alla fine dei dati che già contiene. Se c'è abbastanza spazio per scrivere, possiamo riempire la pipeline da i_size1
a PIPESIZ
. Quindi rilasciamo il blocco e proviamo a riattivare qualsiasi processo in attesa di lettura dalla pipeline. Torniamo all'inizio per vedere se siamo riusciti a scrivere tutti i byte di cui avevamo bisogno. In caso contrario, iniziamo un nuovo ciclo di registrazione.
Di solito parametro i_mode
inode viene utilizzato per memorizzare le autorizzazioni r
, w
и x
. Ma nel caso delle pipeline, segnaliamo che qualche processo sta aspettando una scrittura o una lettura usando i bit IREAD
и IWRITE
rispettivamente. Il processo imposta il flag e chiama sleep()
, e si prevede che in futuro chiamerà un altro processo wakeup()
.
La vera magia avviene dentro sleep()
и wakeup()
. Sono implementati in
/*
* 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) /* … */
Il processo che chiama sleep()
per un particolare canale, può essere successivamente svegliato da un altro processo, che chiamerà wakeup()
per lo stesso canale. writep()
и readp()
coordinare le loro azioni attraverso tali chiamate accoppiate. notare che pipe.c
dare sempre la priorità PPIPE
quando chiamato sleep()
, quindi tutto sleep()
può essere interrotto da un segnale.
Ora abbiamo tutto per capire la funzione 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);
}
Potresti trovare più facile leggere questa funzione dal basso verso l'alto. Il ramo "read and return" viene solitamente utilizzato quando sono presenti alcuni dati nella pipeline. In questo caso, usiamo f_offset
leggere, quindi aggiornare il valore dell'offset corrispondente.
Nelle letture successive, la pipeline sarà vuota se è stato raggiunto l'offset di lettura i_size1
all'inode. Ripristiniamo la posizione su 0 e proviamo a riattivare qualsiasi processo che desideri scrivere sulla pipeline. Sappiamo che quando il trasportatore è pieno, writep()
addormentarsi ip+1
. E ora che la pipeline è vuota, possiamo riattivarla per riprendere il suo ciclo di scrittura.
Se non c'è niente da leggere, allora readp()
può impostare una bandiera IREAD
e addormentarsi ip+2
. Sappiamo cosa lo risveglierà writep()
quando scrive alcuni dati nella pipeline.
Commenti su u
» possiamo trattarle come normali funzioni di I/O che prendono un file, una posizione, un buffer in memoria e contano il numero di byte da leggere o scrivere.
/*
* 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;
/* … */
Per quanto riguarda il blocco "conservativo", quindi readp()
и writep()
bloccare gli inode fino al termine o ottenere un risultato (ad esempio call wakeup
). plock()
и prele()
funziona semplicemente: utilizzando un diverso insieme di chiamate sleep
и wakeup
consentirci di riattivare qualsiasi processo che richieda il blocco che abbiamo appena rilasciato:
/*
* 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);
}
}
All'inizio non riuscivo a capire perché readp()
non causa prele(ip)
prima della chiamata wakeup(ip+1)
. La prima cosa writep()
chiama nel suo ciclo, this plock(ip)
, che si traduce in un deadlock if readp()
non ha ancora rimosso il suo blocco, quindi il codice deve in qualche modo funzionare correttamente. Se guardi wakeup()
, diventa chiaro che contrassegna solo il processo dormiente come pronto per l'esecuzione, in modo che in futuro sched()
l'ha lanciato davvero. COSÌ readp()
cause wakeup()
, sblocca, imposta IREAD
e cause sleep(ip+2)
- tutto questo prima writep()
ricomincia il ciclo.
Questo completa la descrizione delle pipeline nella sesta edizione. Codice semplice, implicazioni di vasta portata.
Xv6, un semplice kernel simile a Unix
Per creare un nucleo
Il codice contiene un'implementazione chiara e ponderata 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()
imposta lo stato di tutto il resto dell'implementazione, che include le funzioni piperead()
, pipewrite()
и pipeclose()
. La vera chiamata di sistema sys_pipe
è un wrapper implementato in
Linux 0.01
È possibile trovare il codice sorgente per Linux 0.01. Sarà istruttivo studiare l'implementazione delle condutture nel suo fs
/pipe.c
. Qui viene utilizzato un inode per rappresentare la pipeline, ma la pipeline stessa è scritta in C moderno. Se ti sei fatto strada attraverso il codice della sesta edizione, non avrai problemi qui. Questo è l'aspetto della funzione 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;
}
Anche senza guardare le definizioni della struttura, puoi capire come viene utilizzato il conteggio dei riferimenti inode per verificare se un'operazione di scrittura risulta in SIGPIPE
. Oltre al lavoro byte per byte, questa funzione è facile da confrontare con le idee di cui sopra. Anche logica sleep_on
/wake_up
non sembra così alieno.
Kernel Linux moderni, FreeBSD, NetBSD, OpenBSD
Ho esaminato rapidamente alcuni kernel moderni. Nessuno di loro ha già un'implementazione basata su disco (non sorprende). Linux ha una sua implementazione. E sebbene i tre moderni kernel BSD contengano implementazioni basate sul codice scritto da John Dyson, nel corso degli anni sono diventati troppo diversi l'uno dall'altro.
Leggere fs
/pipe.c
(su Linux) o sys
/kern
/sys_pipe.c
(su *BSD), ci vuole vera dedizione. Le prestazioni e il supporto per funzionalità come l'I/O vettoriale e asincrono sono oggi importanti nel codice. E i dettagli dell'allocazione della memoria, dei blocchi e della configurazione del kernel variano notevolmente. Questo non è ciò di cui le università hanno bisogno per un corso introduttivo sui sistemi operativi.
In ogni caso, è stato interessante per me portare alla luce alcuni vecchi schemi (ad esempio, generare SIGPIPE
e ritorno EPIPE
quando si scrive su una pipeline chiusa) in tutti questi kernel moderni così diversi. Probabilmente non vedrò mai un computer PDP-11 dal vivo, ma c'è ancora molto da imparare dal codice che è stato scritto qualche anno prima che io nascessi.
Scritto da Divi Kapoor nel 2011, l'articolo "
Fonte: habr.com