Denne artikel beskriver implementeringen af pipelines i Unix-kernen. Jeg var noget skuffet over, at en nylig artikel med titlen "
Hvad taler vi om?
Pipelines er "sandsynligvis den vigtigste opfindelse i Unix" - et definerende træk ved Unix's underliggende filosofi om at sammensætte små programmer og det velkendte kommandolinjeslogan:
$ echo hello | wc -c
6
Denne funktionalitet afhænger af det kerneleverede systemkald pipe
, som er beskrevet på dokumentationssiderne
Rørledninger giver en envejskanal til kommunikation mellem processer. Rørledningen har et input (skriveende) og et output (læseende). Data skrevet til input af pipelinen kan læses ved output.
Pipelinen oprettes ved opkald
pipe(2)
, som returnerer to filbeskrivelser: den ene refererer til input af pipelinen, den anden til outputtet.
Sporingsoutputtet fra ovenstående kommando viser oprettelsen af en pipeline og strømmen af data gennem den fra en proces til en anden:
$ 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
Forældreprocessen kalder pipe()
for at få vedhæftede filbeskrivelser. En underordnet proces skriver til en deskriptor, og en anden proces læser de samme data fra en anden deskriptor. Skallen "omdøber" beskrivelserne 2 og 3 med dup4 for at matche stdin og stdout.
Uden pipelines ville shellen skulle skrive output fra en proces til en fil og overføre den til en anden proces for at læse dataene fra filen. Som et resultat ville vi spilde flere ressourcer og diskplads. Imidlertid er pipelines gode til mere end blot at undgå midlertidige filer:
Hvis en proces forsøger at læse fra en tom pipeline, så
read(2)
vil blokere, indtil dataene er tilgængelige. Hvis en proces forsøger at skrive til en fuld pipeline, såwrite(2)
vil blokere, indtil der er læst nok data fra pipelinen til at fuldføre skrivningen.
Ligesom POSIX-kravet er dette en vigtig egenskab: at skrive til pipeline op til PIPE_BUF
bytes (mindst 512) skal være atomare, så processer kan kommunikere med hinanden gennem rørledningen på en måde, som normale filer (som ikke giver sådanne garantier) ikke kan.
Med en almindelig fil kan en proces skrive hele sit output til den og sende den videre til en anden proces. Eller processer kan fungere i en hård parallel tilstand ved at bruge en ekstern signaleringsmekanisme (som en semafor) til at informere hinanden om færdiggørelsen af en skrivning eller læsning. Transportører sparer os for alt dette besvær.
Hvad leder vi efter?
Jeg vil forklare på mine fingre for at gøre det lettere for dig at forestille dig, hvordan en transportør kan fungere. Du bliver nødt til at allokere en buffer og en tilstand i hukommelsen. Du skal bruge funktioner til at tilføje og fjerne data fra bufferen. Du skal bruge en vis facilitet til at kalde funktioner under læse- og skriveoperationer på filbeskrivelser. Og låse er nødvendige for at implementere den særlige adfærd beskrevet ovenfor.
Vi er nu klar til at afhøre kernens kildekode under skarpt lampelys for at bekræfte eller modbevise vores vage mentale model. Men vær altid forberedt på det uventede.
Hvor søger vi?
Jeg ved ikke, hvor mit eksemplar af den berømte bog ligger.
At vandre gennem TUHS-arkiverne er som at besøge et museum. Vi kan se på vores fælles historie, og jeg har respekt for de mange års indsats for at genvinde alt dette materiale lidt efter lidt fra gamle kassetter og udskrifter. Og jeg er meget opmærksom på de fragmenter, der stadig mangler.
Efter at have tilfredsstillet vores nysgerrighed om rørledningernes gamle historie, kan vi se på moderne kerner til sammenligning.
Af den måde, pipe
er systemkald nummer 42 i tabellen sysent[]
. Sammentræf?
Traditionelle Unix-kerner (1970-1974)
Jeg fandt intet spor pipe(2)
hverken i
Det hævder TUHS
Den tredje udgave af Unix var den sidste version med en kerne skrevet i assembler, men også den første version med pipelines. I løbet af 1973 arbejdede man på at forbedre tredje udgave, kernen blev omskrevet i C, og dermed var den fjerde udgave af Unix født.
En læser fandt en scanning af et dokument, hvor Doug McIlroy foreslog ideen om at "tilslutte programmer som en haveslange."
I Brian Kernighans bog
Da Unix dukkede op, fik min passion for coroutines mig til at bede OS-forfatteren, Ken Thompson, om at tillade, at data, der er skrevet til en proces, ikke kun går til enheden, men også til afslutningen til en anden proces. Ken troede, det var muligt. Men som minimalist ønskede han, at alle systemfunktioner skulle spille en væsentlig rolle. Er direkte skrivning mellem processer virkelig en stor fordel i forhold til at skrive til en mellemfil? Og først da jeg lavede et specifikt forslag med det iørefaldende navn "pipeline" og en beskrivelse af syntaksen for processers interaktion, udbrød Ken endelig: "Jeg vil gøre det!".
Og gjorde. En skæbnesvanger aften ændrede Ken kernen og skallen, rettede adskillige standardprogrammer for at standardisere, hvordan de accepterer input (som kan komme fra en pipeline), og ændrede filnavne. Den næste dag blev rørledninger meget udbredt i applikationer. I slutningen af ugen brugte sekretærerne dem til at sende dokumenter fra tekstbehandlere til trykkeriet. Noget senere erstattede Ken den originale API og syntaks for at indpakke brugen af pipelines med renere konventioner, der har været brugt lige siden.
Desværre er kildekoden til den tredje udgave af Unix-kernen gået tabt. Og selvom vi har kernekildekoden skrevet i C
Vi har dokumentationstekst til pipe(2)
fra begge udgivelser, så du kan starte med at søge i dokumentationen pipe(2)
er skrevet i assembler og returnerer kun én filbeskrivelse, men giver allerede den forventede kernefunktionalitet:
Systemkald rør skaber en I/O-mekanisme kaldet en pipeline. Den returnerede filbeskrivelse kan bruges til læse- og skriveoperationer. Når noget skrives til pipelinen, bufferer det op til 504 bytes data, hvorefter skriveprocessen afbrydes. Ved læsning fra pipelinen tages de bufferlagrede data.
Året efter var kernen blevet omskrevet i C, og pipe(fildes)
»:
Systemkald rør skaber en I/O-mekanisme kaldet en pipeline. De returnerede filbeskrivelser kan bruges i læse- og skriveoperationer. Når noget er skrevet til pipelinen, bruges deskriptoren, der returneres i r1 (resp. fildes[1]), bufferet op til 4096 bytes data, hvorefter skriveprocessen suspenderes. Når man læser fra pipelinen, tager deskriptoren tilbage til r0 (resp. fildes[0]) dataene.
Det antages, at når en pipeline er blevet defineret, to (eller flere) interagerende processer (skabt af efterfølgende påkaldelser gaffel) vil videregive data fra pipelinen ved hjælp af opkald læse и skriver.
Skallen har en syntaks til at definere en lineær række af processer forbundet via en pipeline.
Kald til at læse fra en tom pipeline (som ikke indeholder bufferdata), der kun har den ene ende (alle skrivefilbeskrivelser lukket) returnerer "end of file". Skriveopkald i en lignende situation ignoreres.
Tidligst
Unix sjette udgave (1975)
Begynder at læse Unix-kildekoden
I mange år bogen Lions var det eneste dokument på Unix-kernen, der var tilgængeligt uden for Bell Labs. Selvom den sjette udgave licens tillod lærere at bruge dens kildekode, udelukkede den syvende udgave licens denne mulighed, så bogen blev distribueret i ulovlige maskinskrevne kopier.
I dag kan du købe et genoptrykt eksemplar af bogen, hvis omslag forestiller elever ved kopimaskinen. Og takket være Warren Toomey (som startede TUHS-projektet), kan du downloade
For over 15 år siden indtastede jeg en kopi af kildekoden Lionsfordi jeg ikke kunne lide kvaliteten af mit eksemplar fra et ukendt antal andre kopier. TUHS eksisterede ikke endnu, og jeg havde ikke adgang til de gamle kilder. Men i 1988 fandt jeg et gammelt bånd med 9 spor, der havde en backup fra en PDP11 computer. Det var svært at vide, om det virkede, men der var et intakt /usr/src/-træ, hvori de fleste af filerne var markeret 1979, som allerede dengang så gammel ud. Det var den syvende udgave, eller en PWB-afledt, troede jeg.
Jeg tog fundet som grundlag og redigerede manuelt kilderne til tilstanden i den sjette udgave. En del af koden forblev den samme, en del skulle redigeres lidt, hvilket ændrede det moderne token += til det forældede =+. Noget blev simpelthen slettet, og noget skulle skrives helt om, men ikke for meget.
Og i dag kan vi online på TUHS læse kildekoden til sjette udgave af
Forresten, ved første øjekast er hovedtræk ved C-koden før perioden med Kernighan og Ritchie dens korthed. Det er ikke ofte, at jeg er i stand til at indsætte kodestykker uden omfattende redigering for at passe til et relativt snævert visningsområde på mit websted.
Early
/*
* 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 ændret sig siden den fjerde udgave. Men her ser vi, uden offentlig dokumentation, at pipelines engang brugte filer som reservelager!
Hvad angår LARG filer, svarer de til
Her er det rigtige systemkald 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 tydeligt, hvad der sker her. Men det er ikke så let at forstå koden, blandt andet på grund af hvordan "R0
и R1
systemopkaldsparametre og returværdier videregives.
Lad os prøve med
pipe()
forfalder igennem R0
и R1
returnere filbeskrivelsesnumre til læsning og skrivning. falloc()
returnerer en pointer til en filstruktur, men "returnerer" også via u.u_ar0[R0]
og en filbeskrivelse. Det vil sige, at koden er gemt i r
filbeskrivelse til læsning og tildeler en deskriptor til skrivning direkte fra u.u_ar0[R0]
efter andet opkald falloc()
.
flag FPIPE
, som vi indstiller ved oprettelse af pipeline, styrer funktionsmåden
/*
* 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);
}
/* … */
}
Derefter funktionen readp()
в pipe.c
læser data fra rørledningen. Men det er bedre at spore implementeringen fra writep()
. Igen er koden blevet mere kompliceret på grund af arten af argumentet, der passerer konventionen, men nogle detaljer kan udelades.
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 at skrive bytes til pipeline-inputtet u.u_count
. Først skal vi låse inoden (se nedenfor plock
/prele
).
Derefter tjekker vi inodereferencetallet. Så længe begge ender af rørledningen forbliver åbne, skal tælleren være 2. Vi holder fast i ét led (fra rp->f_inode
), så hvis tælleren er mindre end 2, skulle det betyde, at læseprocessen har lukket sin ende af pipelinen. Vi forsøger med andre ord at skrive til en lukket pipeline, hvilket er en fejl. Første fejlkode EPIPE
og signal SIGPIPE
dukkede op i den sjette udgave af Unix.
Men selvom transportøren er åben, kan den være fuld. I dette tilfælde frigiver vi låsen og går i dvale i håb om, at en anden proces vil læse fra rørledningen og frigøre nok plads i den. Når vi vågner, vender vi tilbage til begyndelsen, lukker låsen op igen og starter en ny skrivecyklus.
Hvis der er nok ledig plads i rørledningen, så skriver vi data til den ved hjælp af i_size1
inode'a (med en tom pipeline kan være lig med 0) peger mod slutningen af de data, som den allerede indeholder. Hvis der er plads nok til at skrive, kan vi fylde rørledningen fra i_size1
til PIPESIZ
. Så slipper vi låsen og forsøger at vække enhver proces, der venter på at læse fra rørledningen. Vi går tilbage til begyndelsen for at se, om vi formåede at skrive så mange bytes, som vi havde brug for. Hvis ikke, så starter vi en ny optagelsescyklus.
Normalt parameter i_mode
inode bruges til at gemme tilladelser r
, w
и x
. Men i tilfælde af pipelines signalerer vi, at en eller anden proces venter på en skrivning eller læsning ved hjælp af bits IREAD
и IWRITE
henholdsvis. Processen sætter flaget og kalder sleep()
, og det forventes, at en anden proces vil kalde i fremtiden wakeup()
.
Den virkelige magi sker i sleep()
и wakeup()
. De er implementeret 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) /* … */
Processen der kalder sleep()
for en bestemt kanal, kan senere vækkes af en anden proces, som vil kalde wakeup()
for samme kanal. writep()
и readp()
koordinere deres handlinger gennem sådanne parrede opkald. Noter det pipe.c
altid prioritere PPIPE
når der ringes op sleep()
, så alle sleep()
kan afbrydes af et signal.
Nu har vi alt for at forstå funktionen 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 vil måske finde det nemmere at læse denne funktion fra bund til top. "Læs og returner"-grenen bruges normalt, når der er nogle data i pipelinen. I dette tilfælde bruger vi f_offset
læs, og opdater derefter værdien af den tilsvarende offset.
Ved efterfølgende aflæsninger vil rørledningen være tom, hvis aflæsningsforskydningen er nået i_size1
ved inoden. Vi nulstiller positionen til 0 og forsøger at vække enhver proces, der ønsker at skrive til pipelinen. Vi ved, at når transportøren er fuld, writep()
falde i søvn på ip+1
. Og nu hvor pipelinen er tom, kan vi vække den for at genoptage skrivecyklussen.
Hvis der ikke er noget at læse, så readp()
kan sætte et flag IREAD
og falder i søvn ip+2
. Vi ved, hvad der vil vække ham writep()
når den skriver nogle data til pipelinen.
Kommentarer vedr u
»Vi kan behandle dem som almindelige I/O-funktioner, der tager en fil, en position, en buffer i hukommelsen og tæller antallet af bytes, der skal læses eller skrives.
/*
* 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;
/* … */
Med hensyn til "konservativ" blokering, altså readp()
и writep()
lås inoder, indtil de er færdige eller opnår et resultat (dvs. ring wakeup
). plock()
и prele()
arbejde enkelt: ved at bruge et andet sæt opkald sleep
и wakeup
tillad os at vække enhver proces, der har brug for den lås, vi lige har frigivet:
/*
* 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årsager prele(ip)
før opkaldet wakeup(ip+1)
. Den første ting writep()
kalder i sin løkke, dette plock(ip)
, hvilket resulterer i et dødvande, hvis readp()
har ikke fjernet sin blok endnu, så koden skal på en eller anden måde fungere korrekt. Hvis man ser på wakeup()
, bliver det klart, at det kun markerer soveprocessen som klar til udførelse, således at i fremtiden sched()
virkelig lanceret det. Så readp()
årsager wakeup()
, låser op, sætter IREAD
og opkald sleep(ip+2)
- alt dette før writep()
genstarter cyklussen.
Dette fuldender beskrivelsen af rørledninger i sjette udgave. Simpel kode, vidtrækkende implikationer.
Xv6, en simpel Unix-lignende kerne
At skabe en kerne
Koden indeholder en klar og gennemtænkt 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()
indstiller tilstanden for resten af implementeringen, som inkluderer funktioner piperead()
, pipewrite()
и pipeclose()
. Selve systemkaldet sys_pipe
er en indpakning implementeret i
Linux 0.01
Du kan finde kildekoden til Linux 0.01. Det vil være lærerigt at studere implementeringen af rørledninger i hans fs
/pipe.c
. Her bruges en inode til at repræsentere pipelinen, men selve pipelinen er skrevet i moderne C. Hvis du har hacket dig igennem den sjette udgave kode, får du ingen problemer her. Sådan ser funktionen ud 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;
}
Selv uden at se på strukturdefinitionerne, kan du finde ud af, hvordan inodereferencetællingen bruges til at kontrollere, om en skriveoperation resulterer i SIGPIPE
. Ud over byte-for-byte-arbejde er denne funktion let at sammenligne med ovenstående ideer. Selv logik sleep_on
/wake_up
ser ikke så fremmed ud.
Moderne Linux-kerner, FreeBSD, NetBSD, OpenBSD
Jeg gik hurtigt over nogle moderne kerner. Ingen af dem har allerede en disk-baseret implementering (ikke overraskende). Linux har sin egen implementering. Og selvom de tre moderne BSD-kerner indeholder implementeringer baseret på kode, der blev skrevet af John Dyson, er de i årenes løb blevet for forskellige fra hinanden.
At læse fs
/pipe.c
(på Linux) eller sys
/kern
/sys_pipe.c
(på *BSD), det kræver virkelig dedikation. Ydeevne og understøttelse af funktioner såsom vektor og asynkron I/O er vigtig i kode i dag. Og detaljerne om hukommelsesallokering, låse og kernekonfiguration varierer alle meget. Det er ikke, hvad universiteterne har brug for til et introduktionskursus i operativsystemer.
Under alle omstændigheder var det interessant for mig at afdække et par gamle mønstre (for eksempel at generere SIGPIPE
og vende tilbage EPIPE
når du skriver til en lukket pipeline) i alle disse, så forskellige, moderne kerner. Jeg kommer nok aldrig til at se en PDP-11 computer live, men der er stadig meget at lære af den kode, der blev skrevet et par år før jeg blev født.
Skrevet af Divi Kapoor i 2011, artiklen "
Kilde: www.habr.com