W tym artykule opisano implementację potoków w jądrze systemu Unix. Byłem nieco rozczarowany, że niedawny artykuł zatytułowany „
O czym gadamy?
Potoki są „prawdopodobnie najważniejszym wynalazkiem w Uniksie” - cechą charakterystyczną leżącej u podstaw filozofii Uniksa polegającej na łączeniu małych programów oraz znanego sloganu wiersza poleceń:
$ echo hello | wc -c
6
Ta funkcjonalność zależy od wywołania systemowego dostarczonego przez jądro pipe
, który jest opisany na stronach dokumentacji
Potoki zapewniają jednokierunkowy kanał komunikacji między procesami. Potok ma wejście (koniec zapisu) i wyjście (koniec odczytu). Dane zapisane na wejściu potoku można odczytać na wyjściu.
Potok jest tworzony przez wywołanie
pipe(2)
, która zwraca dwa deskryptory plików: jeden odnosi się do wejścia potoku, drugi do wyjścia.
Dane wyjściowe śledzenia z powyższego polecenia pokazują tworzenie potoku i przepływ danych przez niego z jednego procesu do drugiego:
$ 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
Wywołania procesu nadrzędnego pipe()
aby uzyskać dołączone deskryptory plików. Jeden proces potomny zapisuje do jednego deskryptora, a inny proces odczytuje te same dane z innego deskryptora. Powłoka „zmienia nazwy” deskryptorów 2 i 3 za pomocą dup4, aby pasowały do stdin i stdout.
Bez potoków powłoka musiałaby zapisywać dane wyjściowe jednego procesu do pliku i przekazywać je do innego procesu w celu odczytania danych z pliku. W rezultacie zmarnowalibyśmy więcej zasobów i miejsca na dysku. Jednak potoki są dobre nie tylko do unikania plików tymczasowych:
Jeśli proces próbuje odczytać z pustego potoku, to
read(2)
będzie blokować, dopóki dane nie będą dostępne. Jeśli proces próbuje pisać do pełnego potoku, towrite(2)
będzie blokować, dopóki z potoku nie zostanie odczytana wystarczająca ilość danych, aby zakończyć zapis.
Podobnie jak wymagania POSIX, jest to ważna właściwość: zapis do potoku do PIPE_BUF
bajty (co najmniej 512) muszą być atomowe, aby procesy mogły komunikować się ze sobą za pośrednictwem potoku w sposób, w jaki normalne pliki (które nie zapewniają takich gwarancji) nie mogą.
W zwykłym pliku proces może zapisać w nim wszystkie swoje dane wyjściowe i przekazać je innemu procesowi. Lub procesy mogą działać w twardym trybie równoległym, wykorzystując zewnętrzny mechanizm sygnalizacyjny (taki jak semafor), aby informować się nawzajem o zakończeniu zapisu lub odczytu. Przenośniki ratują nas przed tymi wszystkimi kłopotami.
Czego szukamy?
Wyjaśnię na palcach, aby łatwiej było Ci wyobrazić sobie, jak może działać przenośnik. Będziesz musiał przydzielić bufor i trochę stanu w pamięci. Będziesz potrzebował funkcji do dodawania i usuwania danych z bufora. Będziesz potrzebował pewnych ułatwień do wywoływania funkcji podczas operacji odczytu i zapisu na deskryptorach plików. Do zaimplementowania opisanego powyżej specjalnego zachowania potrzebne są blokady.
Jesteśmy teraz gotowi do zbadania kodu źródłowego jądra w jasnym świetle lampy, aby potwierdzić lub obalić nasz niejasny model mentalny. Ale zawsze bądź przygotowany na nieoczekiwane.
Gdzie szukamy?
Nie wiem, gdzie leży mój egzemplarz słynnej książki.
Wędrówka po archiwach TUHS jest jak wizyta w muzeum. Możemy spojrzeć na naszą wspólną historię i mam szacunek dla lat starań, aby kawałek po kawałku odzyskać cały ten materiał ze starych kaset i wydruków. I doskonale zdaję sobie sprawę z tych fragmentów, których wciąż brakuje.
Zaspokoiwszy naszą ciekawość starożytną historią rurociągów, możemy dla porównania spojrzeć na współczesne rdzenie.
By the way, pipe
jest numerem wywołania systemowego 42 w tabeli sysent[]
. Zbieg okoliczności?
Tradycyjne jądra Uniksa (1970–1974)
Nie znalazłem żadnego śladu pipe(2)
ani w
TUHS twierdzi, że
Trzecia edycja Uniksa była ostatnią wersją z jądrem napisanym w asemblerze, ale także pierwszą wersją z potokami. W 1973 roku trwały prace nad ulepszeniem trzeciej edycji, jądro zostało przepisane w C i tak narodziła się czwarta edycja Uniksa.
Jeden z czytelników znalazł skan dokumentu, w którym Doug McIlroy zaproponował pomysł „łączenia programów jak wąż ogrodowy”.
W książce Briana Kernighana
Kiedy pojawił się Unix, moja pasja do współprogramów sprawiła, że poprosiłem autora systemu operacyjnego, Kena Thompsona, aby pozwolił danym zapisanym do jakiegoś procesu trafiać nie tylko do urządzenia, ale także do wyjścia do innego procesu. Ken pomyślał, że to możliwe. Jednak jako minimalista chciał, aby każda funkcja systemu odgrywała znaczącą rolę. Czy bezpośrednie pisanie między procesami ma naprawdę dużą przewagę nad pisaniem do pliku pośredniego? I dopiero gdy złożyłem konkretną propozycję o chwytliwej nazwie „pipeline” i opisie składni interakcji procesów, Ken w końcu wykrzyknął: „Zrobię to!”.
I zrobił. Pewnego pamiętnego wieczoru Ken zmienił jądro i powłokę, poprawił kilka standardowych programów w celu ujednolicenia sposobu przyjmowania danych wejściowych (które mogą pochodzić z potoku) i zmienił nazwy plików. Następnego dnia potoki były bardzo szeroko stosowane w aplikacjach. Do końca tygodnia sekretarki wykorzystywały je do przesyłania dokumentów z edytorów tekstu do drukarki. Nieco później Ken zastąpił oryginalny interfejs API i składnię opakowującą użycie potoków czystszymi konwencjami, które były używane do tej pory.
Niestety, kod źródłowy trzeciej edycji jądra Uniksa zaginął. I chociaż mamy kod źródłowy jądra napisany w C
Mamy tekst dokumentacji dla pipe(2)
z obu wydań, więc możesz zacząć od przeszukania dokumentacji pipe(2)
jest napisany w asemblerze i zwraca tylko jeden deskryptor pliku, ale już zapewnia oczekiwaną podstawową funkcjonalność:
Wywołanie systemowe rura tworzy mechanizm we/wy zwany potokiem. Zwróconego deskryptora pliku można użyć do operacji odczytu i zapisu. Kiedy coś jest zapisywane do potoku, buforuje do 504 bajtów danych, po czym proces zapisu jest zawieszany. Podczas odczytu z potoku pobierane są buforowane dane.
W następnym roku jądro zostało przepisane w C i pipe(fildes)
»:
Wywołanie systemowe rura tworzy mechanizm we/wy zwany potokiem. Zwróconych deskryptorów plików można używać w operacjach odczytu i zapisu. Kiedy coś jest zapisywane do potoku, używany jest deskryptor zwrócony w r1 (odp. fildes[1]), buforowany do 4096 bajtów danych, po czym proces zapisu jest zawieszany. Podczas odczytu z potoku deskryptor zwrócony do r0 (odp. fildes[0]) pobiera dane.
Zakłada się, że po zdefiniowaniu potoku dwa (lub więcej) współdziałające procesy (utworzone przez kolejne wywołania widelec) przekaże dane z potoku za pomocą wywołań czytać и napisać.
Powłoka ma składnię do definiowania liniowej tablicy procesów połączonych potokiem.
Wywołania odczytu z pustego potoku (niezawierającego buforowanych danych), który ma tylko jeden koniec (wszystkie deskryptory plików zapisu są zamknięte) zwracają „koniec pliku”. Wywołania zapisu w podobnej sytuacji są ignorowane.
najwcześniej
Unix wydanie szóste (1975)
Rozpoczęcie czytania kodu źródłowego systemu Unix
Od wielu lat książka Lwy był jedynym dokumentem dotyczącym jądra Uniksa dostępnym poza Bell Labs. Chociaż licencja na szóstą edycję zezwalała nauczycielom na korzystanie z jej kodu źródłowego, licencja na siódmą edycję wykluczała tę możliwość, więc książka była rozpowszechniana w nielegalnych kopiach maszynopisu.
Dziś można kupić przedruk książki, której okładka przedstawia studentów przy kopiarce. A dzięki Warrenowi Toomeyowi (który zapoczątkował projekt TUHS) możesz pobrać
Ponad 15 lat temu wpisałem kopię dostarczonego kodu źródłowego Lwyponieważ nie podobała mi się jakość mojego egzemplarza z nieznanej liczby innych egzemplarzy. TUHS jeszcze nie istniało, a ja nie miałem dostępu do starych źródeł. Ale w 1988 roku znalazłem starą kasetę z 9 utworami, która miała kopię zapasową z komputera PDP11. Trudno było stwierdzić, czy to zadziałało, ale istniało nienaruszone drzewo /usr/src/, w którym większość plików była oznaczona jako 1979, który nawet wtedy wyglądał na stary. To było wydanie siódme albo pochodna PWB, pomyślałem.
Wziąłem to znalezisko za podstawę i ręcznie zredagowałem źródła do stanu wydania szóstego. Część kodu pozostała ta sama, część trzeba było nieco zmodyfikować, zmieniając nowoczesny token += na przestarzały =+. Coś zostało po prostu usunięte, a coś musiało zostać całkowicie przepisane, ale nie za dużo.
A dziś możemy przeczytać online na TUHS kod źródłowy szóstej edycji
Nawiasem mówiąc, na pierwszy rzut oka główną cechą kodu C sprzed okresu Kernighana i Ritchiego jest jego zwięzłość. Nieczęsto udaje mi się wstawić fragmenty kodu bez szczegółowej edycji, aby zmieściły się na stosunkowo wąskim obszarze wyświetlania w mojej witrynie.
Wcześnie
/*
* 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
Rozmiar bufora nie zmienił się od czwartej edycji. Ale tutaj widzimy, bez żadnej publicznej dokumentacji, że potoki używały kiedyś plików jako zapasowej pamięci masowej!
Jeśli chodzi o pliki LARG, odpowiadają one
Oto prawdziwe wywołanie systemowe 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;
}
Komentarz jasno opisuje, co się tutaj dzieje. Ale zrozumienie kodu nie jest takie łatwe, częściowo z powodu tego, jak „R0
и R1
przekazywane są parametry wywołania systemowego i zwracane wartości.
Spróbujmy z
pipe()
należny przez R0
и R1
zwracają numery deskryptorów plików do odczytu i zapisu. falloc()
zwraca wskaźnik do struktury plików, ale także „zwraca” via u.u_ar0[R0]
i deskryptor pliku. Oznacza to, że kod jest przechowywany w r
deskryptor pliku do odczytu i przypisuje deskryptor do bezpośredniego zapisu u.u_ar0[R0]
po drugim wezwaniu falloc()
.
Flaga FPIPE
, który ustawiamy podczas tworzenia potoku, steruje zachowaniem funkcji
/*
* 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);
}
/* … */
}
Następnie funkcja readp()
в pipe.c
odczytuje dane z potoku. Ale lepiej jest prześledzić implementację zaczynając od writep()
. Ponownie kod stał się bardziej skomplikowany ze względu na naturę konwencji przekazywania argumentów, ale niektóre szczegóły można pominąć.
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;
}
Chcemy zapisać bajty na wejściu potoku u.u_count
. Najpierw musimy zablokować i-węzeł (patrz poniżej plock
/prele
).
Następnie sprawdzamy liczbę odwołań do i-węzłów. Dopóki oba końce rurociągu pozostają otwarte, licznik powinien wynosić 2. Trzymamy się jednego łącza (z rp->f_inode
), więc jeśli licznik jest mniejszy niż 2, powinno to oznaczać, że proces odczytu zamknął swój koniec potoku. Innymi słowy, próbujemy pisać do zamkniętego potoku, co jest błędem. Pierwszy kod błędu EPIPE
i sygnał SIGPIPE
pojawił się w szóstej edycji systemu Unix.
Ale nawet jeśli przenośnik jest otwarty, może być pełny. W takim przypadku zwalniamy blokadę i idziemy spać w nadziei, że inny proces odczyta z potoku i zwolni w nim wystarczająco dużo miejsca. Po przebudzeniu wracamy do początku, ponownie odkładamy blokadę i rozpoczynamy nowy cykl zapisu.
Jeśli w potoku jest wystarczająco dużo wolnego miejsca, zapisujemy do niego dane za pomocą i_size1
i-węzeł (przy pustym potoku może być równy 0) wskazuje na koniec danych, które już zawiera. Jeśli jest wystarczająco dużo miejsca do pisania, możemy wypełnić potok i_size1
do PIPESIZ
. Następnie zwalniamy blokadę i próbujemy obudzić każdy proces, który czeka na odczyt z potoku. Wracamy do początku, aby sprawdzić, czy udało nam się zapisać tyle bajtów, ile potrzebowaliśmy. Jeśli to się nie powiedzie, rozpoczynamy nowy cykl nagrywania.
Zazwyczaj parametr i_mode
i-węzeł służy do przechowywania uprawnień r
, w
и x
. Ale w przypadku potoków sygnalizujemy za pomocą bitów, że jakiś proces oczekuje na zapis lub odczyt IREAD
и IWRITE
odpowiednio. Proces ustawia flagę i wywołuje sleep()
, i oczekuje się, że w przyszłości wywołany zostanie jakiś inny proces wakeup()
.
Prawdziwa magia dzieje się w sleep()
и wakeup()
. Realizowane są w
/*
* 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) /* … */
Proces, który wzywa sleep()
dla określonego kanału, może później zostać obudzony przez inny proces, który wywoła wakeup()
dla tego samego kanału. writep()
и readp()
koordynować swoje działania poprzez takie sparowane połączenia. zauważ to pipe.c
zawsze ustalaj priorytety PPIPE
kiedy wezwany sleep()
, więc wszystko sleep()
można przerwać sygnałem.
Teraz mamy wszystko, aby zrozumieć tę funkcję 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);
}
Być może łatwiej będzie ci przeczytać tę funkcję od dołu do góry. Gałąź „odczyt i powrót” jest zwykle używana, gdy w potoku znajdują się jakieś dane. W tym przypadku używamy f_offset
odczytać, a następnie zaktualizować wartość odpowiedniego przesunięcia.
Przy kolejnych odczytach potok będzie pusty, jeśli osiągnięto przesunięcie odczytu i_size1
w i-węźle. Resetujemy pozycję do 0 i próbujemy obudzić każdy proces, który chce pisać do potoku. Wiemy, że gdy przenośnik jest pełny, writep()
zasnąć na ip+1
. A teraz, gdy potok jest pusty, możemy go obudzić, aby wznowić cykl zapisu.
Jeśli nie ma co czytać, to tak readp()
może ustawić flagę IREAD
i zasypiać dalej ip+2
. Wiemy, co go obudzi writep()
kiedy zapisuje niektóre dane do potoku.
Komentarze do u
» możemy je traktować jak zwykłe funkcje I/O, które pobierają plik, pozycję, bufor w pamięci i zliczają liczbę bajtów do odczytania lub zapisania.
/*
* 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;
/* … */
Jeśli chodzi o blokowanie „konserwatywne”, to tak readp()
и writep()
zablokuj i-węzły, dopóki nie zakończą lub nie uzyskają wyniku (tj wakeup
). plock()
и prele()
działają po prostu: używając innego zestawu wywołań sleep
и wakeup
pozwalają nam obudzić każdy proces, który wymaga blokady, którą właśnie zwolniliśmy:
/*
* 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);
}
}
Na początku nie mogłem zrozumieć dlaczego readp()
nie wywołuje prele(ip)
przed wezwaniem wakeup(ip+1)
. Pierwsza rzecz writep()
wywołuje w swojej pętli this plock(ip)
, co skutkuje zakleszczeniem if readp()
nie usunął jeszcze swojej blokady, więc kod musi jakoś działać poprawnie. jeśli spojrzysz wakeup()
, staje się jasne, że oznacza to tylko proces uśpienia jako gotowy do wykonania, aby w przyszłości sched()
naprawdę go uruchomił. Więc readp()
powoduje wakeup()
, odblokowuje, ustawia IREAD
i dzwoni sleep(ip+2)
- to wszystko przed writep()
ponownie uruchamia cykl.
Na tym kończy się opis rurociągów w wydaniu szóstym. Prosty kod, dalekosiężne implikacje.
Xv6, proste jądro podobne do systemu Unix
Aby utworzyć jądro
Kod zawiera przejrzystą i przemyślaną implementację 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()
ustawia stan całej reszty implementacji, w tym funkcji piperead()
, pipewrite()
и pipeclose()
. Rzeczywiste wywołanie systemowe sys_pipe
jest opakowaniem zaimplementowanym w
Linux 0.01
Możesz znaleźć kod źródłowy dla Linuksa 0.01. Pouczające będzie przestudiowanie implementacji potoków w jego fs
/pipe.c
. Tutaj i-węzeł jest używany do reprezentowania potoku, ale sam potok jest napisany we współczesnym C. Jeśli przełamałeś się przez kod szóstej edycji, tutaj nie będziesz miał żadnych problemów. Tak wygląda ta funkcja 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;
}
Nawet bez patrzenia na definicje struktury można dowiedzieć się, w jaki sposób liczba odwołań do i-węzłów jest używana do sprawdzania, czy operacja zapisu skutkuje SIGPIPE
. Oprócz pracy bajt po bajcie, tę funkcję można łatwo porównać z powyższymi pomysłami. Nawet logika sleep_on
/wake_up
nie wygląda tak obco.
Nowoczesne jądra Linuksa, FreeBSD, NetBSD, OpenBSD
Szybko przejrzałem kilka nowoczesnych jąder. Żaden z nich nie ma jeszcze implementacji dyskowej (nic dziwnego). Linux ma własną implementację. I chociaż trzy nowoczesne jądra BSD zawierają implementacje oparte na kodzie, który napisał John Dyson, z biegiem lat za bardzo się od siebie różnią.
Czytać fs
/pipe.c
(w Linuksie) lub sys
/kern
/sys_pipe.c
(na *BSD), to wymaga prawdziwego poświęcenia. Wydajność i obsługa funkcji, takich jak wektorowe i asynchroniczne operacje we/wy, są dziś ważne w kodzie. A szczegóły alokacji pamięci, blokad i konfiguracji jądra są bardzo różne. To nie jest to, czego uniwersytety potrzebują na kurs wprowadzający do systemów operacyjnych.
W każdym razie interesujące było dla mnie odkrycie kilku starych wzorców (na przykład generowanie SIGPIPE
i powrót EPIPE
podczas pisania do zamkniętego potoku) we wszystkich tych, tak różnych, nowoczesnych jądrach. Prawdopodobnie nigdy nie zobaczę na żywo komputera PDP-11, ale wciąż jest wiele do nauczenia się z kodu, który został napisany kilka lat przed moimi narodzinami.
Napisany przez Divi Kapoor w 2011 roku artykuł „
Źródło: www.habr.com