Examen CA/CC 2014-2015

Urmăriți precizările din pagina de reguli.

Examen final

Puteți participa la o singură sesiune de examen final.

Foi de examen

Lucrări

  • Dacă nu puteți participa la seria fiecăruia, puteți veni la cealaltă serie. Pentru aceasta trimiteți un e-mail catre Răzvan cu subiectul [SO][Lucrare X] Transfer Prenume Nume, Grupa unde:
    • X este indexul lucrării (1, 2, 3 sau 4)
    • Prenume este prenumele.
    • Nume este numa.
    • Grupa este grupa.
  • Nu există sesiune de contestații pentru lucrările de curs. În cazul în care considerați că au fost lipsuri la corectarea lucrării, trimiteți un e-mail catre Răzvan.
    • Folosiți subiectul [SO][Lucrare X] Prenume Nume, Grupa; de exemplu [SO][Lucrare 1] Andreea Popescu, 332CA.
  • Pentru a fi punctat, răspunsul la o întrebare trebuie să fie justificat.

Lucrare 1

  • La începutul cursului 4:
    • marți, 17 martie 2015, 09:05-09:15, EC004, seria CA
    • miercuri, 18 martie 2015, 17:05-17:15, EC004, seria CC
3CA, varianta 1
  1. În ce situație practică este folosit apelul dup()?
    • Răspuns: Apelul dup() este folosit practic pentru redirectarea ieșirii, intrării sau erorii standard în fișier. Altă situație practică este pentru operatorul | (pipe) de comunicare între procese.
  2. Ce conține tabela de descriptori de fișier a unui proces?
    • Răspuns: Tabela de descriptori de fișier a unui proces conține pointeri; ca structură de date este un vector de pointeri. Acești pointeri referă structuri de fișier deschis de proces. Când un proces deschide un fișier, se alocă o structură de fișier deschis, iar adresa acestei structuri este stocată într-un loc liber (indicat de descriptorul de fișier) din tabela de descriptori de fișier.
  3. Apelul wait() este un apel blocant. Când are loc deblocarea procesului blocat în wait()?
    • Răspuns: Un proces este deblocat din apelul wait() atunci când unul dintre procesele sale copil își încheie execuția. În acel moment, apelul wait() se deblochează și întoarce informații despre modul în care și-a încheiat procesul copil execuția.
3CA, varianta 2
  1. De ce nucleul sistemului de operare rulează, în general, într-un spațiu dedicat, numit kernel space?
    • Răspuns: Pentru că în kernel space au loc operații privilegiate. Spațiul kernel este un spațiu privilegiat la care doar nucleul sistemului de operare are acces. În felul acesta se păstrează securitatea sistemului, orice operație privilegiată necesitând trecerea în spațiul kernel și acordul nucleului sistemului de operare pentru execuție.
  2. Care este un avantaj al apelurilor de tipul buffered I/O (precum fread, fwrite) și care este un avantaj al celor de tipul system I/O (precum read, write)?
    • Răspuns: Apelurile de tipul buffered I/O fac mai puține apeluri de sistem, deci overhead mai redus, întrucât informația este ținută în buffere până la nevoia de flush. Sunt, de asemenea, portabile. Apelurile de tipul system I/O au o latența mai redusă, informațiile ajung repede pe dispozitiv. De asemenea, apelurile de tipul system I/O nu alocă memorie suplimentară pentru buffering, sunt mai economice din acest punct de vedere.
  3. De ce spunem despre apelul fork() că este invocat o dată dar se întoarce de două ori?
    • Răspuns: Apelul fork() este invocat o dată de procesul părinte și se întoarce de două ori: o dată în procesul părinte pentru continuarea execuției acestuia și altă dată în procesul copil de unde va rula acesta.
3CC, varianta 1
  1. Un descriptor de fișier gestionează/referă, în general, un fișier obișnuit (regular file). Ce altceva mai poate referi?
    • Răspuns: Un descriptor de fișier mai poate referi un director, un link simbolic, un pipe, un socket, un dispozitiv bloc sau caracter. Toate aceste entități sunt gestionate de un proces prin intermediul unui descriptor de fișier.
  2. Dați exemplu de apel care modifică dimensiunea unui fișier.
    • Răspuns: Apeluri care pot modifica dimensiunea unui fișier sunt write (poate scrie dincolo de limita unui fișier), ftruncate (modifică chiar câmpul dimensiune) sau open cu argumentul O_TRUNC care reduce dimensiunea fișierului la 0.
  3. Ce este un proces zombie?
    • Răspuns: Un proces zombie este un proces care și-a încheiat execuția dar care nu a fost încă așteptat de procesul său părinte.
3CC, varianta 2
  1. Câte tabele de descriptori de fișier există la nivelul sistemului de operare?
    • Răspuns: Fiecare proces are o tabelă de descriptori de fișier, deci vor exista, la nivelul sistemului de operare, atâtea tabele de descriptori de fișier câte procese există în acel moment în sistem.
  2. Dați un exemplu de informație care se găsește în structura de fișier deschis și un exemplu de informație care se găsește în structura de fișier pe disc (inode).
    • Răspuns: În structura de fișier deschis se găsesc cursorul de fișier, permisiunile de deschidere a fișierului, pointer către structura de fișier pe disc. În structura de fișier pe disc se găsesc permisiuni de acces, informații despre utilizatorul deținător, grupul deținător, dimensiunea fișierului, timpi de acces, tipul fișierului, pointeri către blocurile de date.
  3. Dați exemplu de situație care duce la trecerea unui proces din starea RUNNING în starea READY.
    • Răspuns: Un proces trece din starea RUNNING în starea READY atunci când îi expiră cuanta de rulare sau când există un proces cu prioritate mai mare în coada READY (care să îi ia locul).

Lucrare 2

  • La începutul cursului 7:
    • marți, 7 aprilie 2015, 08:05-08:15, EC004, seria CA
    • miercuri, 8 aprilie 2015, 17:05-17:15, EC004, seria CC
3CA, varianta 1
  1. Numiți o sursă de overhead care apare atunci când sistemul de operare schimbă contextul de execuție între două procese.
    • Răspuns: Surse de overhead pentru schimbarea de context între procese sunt schimbarea tabelei de pagini, care conduce la flush la TLB, algoritmul de alegere a următorului proces și schimbarea efectivă de context, cu salvarea registrelor procesului curent și restaurarea procesului ales.
  2. De ce este uzual și avantajos ca spațiul virtual de adrese al proceselor să cuprindă o zonă dedicată pentru kernel?
    • Răspuns: Prezența zonei dedicate pentru kernel în spațiul de adresă al fiecărui proces înseamnă că la fiecare apel de sistem, adică la trecerea din user space în kernel space, tabela de pagini rămâne aceeași și nu se face flush la TLB. În cazul în care kernel-ul ar avea o zonă dedicată, atunci ar avea și o tabelă de pagini dedicată și ar trebui schimbată tabela de pagini la fiecare apel de sistem și la fiecare revenire din apel de sistem.
  3. Procesul P1 folosește 100MB de memorie fizică (RAM) rezidentă. P1 execută fork() și rezultă procesul P2. Câtă memorie fizică (RAM) rezidentă folosesc împreună P1 și P2 imediat dupa fork()? De ce?
    • Răspuns: Apelul fork() folosește copy-on-write ceea ce înseamnă că nu se alocă memorie rezidentă nouă pentru noul proces. Se alocă, într-adevăr, o nouă tabelă de pagini, dar spațiul rezident al procesului P1 este acum partajat cu procesul P2 până la prima operație de scriere, când pagina aferentă va fi duplicată.
3CA, varianta 2
  1. Descrieți o problemă posibilă care poate apărea dacă un sistem de operare implementează un algoritm de planificare de tipul Shortest Job First.
    • Răspuns: În cazul unei planificări Shortest Job First, dacă sunt adăugate în sistem, în mod constant, procese noi și de durată scurtă, procesele de durată mai lungă nu vor apuca să ruleze. Va rezulta într-un timp de așteptare foarte mare pentru procesele de lungă durată sau chiar în starvation (așteptare nedefinită pentru ca un proces să poată rula pe procesor).
  2. Care este utilitatea conceptului de demand paging?
    • Răspuns: Atunci când sistemul de operare folosește demand paging alocarea de memorie fizică este amânată până în momentul în care nevoie (adică la primul acces). Sistemul de operare doar rezervă memorie virtuală și nu alocă memorie fizică în spate, economisind memorie fizică. La primul acces se alocă și memorie fizică, la cerere (adică on demand) și se face maparea acesteia la spațiul virtual (paging).
  3. De ce zonele .text și .rodata din cadrul bibiliotecilor partajate (shared libraries) pot fi partajate între mai multe procese?
    • Răspuns: Zonele .text și .rodata sunt zone read only. Acest lucru înseamna că pot fi partajate în siguranță pentru că nici un proces care accesează zona nu o va putea modifica. Zonele conțin permanent aceleași informații indiferent de numărul de procese care le folosesc și pot fi, deci, partajate.
3CC, varianta 1
  1. La ce se referă noțiunea de timp de așteptare (waiting time) în contextul planificării proceselor (process scheduling)?
    • Răspuns: Noțiunea de waiting time se referă la timpul de așteptare al unui proces în coada READY a planificatorului. Pentru un sistem interactiv/responsiv este de dorit ca timpul de așteptare să fie cât mai scurt.
  2. De ce este utilă paginarea ierarhică?
    • Răspuns: Dacă nu am folosi paginare ierarhică tabelele de pagini ar ocupa foarte mult spațiu; ar fi neovie de o intrare pentru fiecare pagină virtuală a unui proces. Paginarea ierarhică conduce la reducerea spațiului ocupat de tabela de pagini, profitând de faptul că o bună parte din spațiul virtual de adrese al procesului nu este folosit.
  3. Care este o cauză sursă pentru evacuarea unei pagini din memoria fizică (RAM) pe disc (swap out)?
    • Răspuns: Cauze sursă pentru evacuarea unei pagini din RAM sunt:
      • operația de swap in, care necesită o pagină liberă în RAM, conducând la o operația de swap out
      • alocarea unei pagini fizice noi; nu există pagini libere, se execută swap out
      • demand paging, la fel ca mai sus
      • copy on write care necesită alocarea unei noi pagini fizice, posibil inexistente
3CC, varianta 2
  1. Care este un avantaj și un dezavantaj al folosirii unei cuante de timp scurte în planificarea proceselor (process scheduling)?
    • Răspuns: Folosirea unei cuante de timp scurte înseamnă un sistem interactiv și responsiv. Dar înseamnă și schimbări dese de context adică un randament mai scăzut al sistemului în a rula procese, deci o productivitate (throughput) redusă.
  2. Ce conține tabela de pagini a unui proces?
    • Răspuns: Tabela de pagini a unui proces conține pointeri de pagini fizice. Indexul în tabelă este pagina virtuală. În general tabela de pagini mai conține și informații legate de permisiuni, validatate, dacă pagina a fost sau nu modificată.
  3. Dați exemplu de situație care cauzează page fault fără a rezulta în trimiterea unei excepții de acces la memorie (de tipul SIGSEGV, Segmentation fault) către procesul care a generat page fault-ul.
    • Răspuns: Dacă un proces are rezervat un spațiu virtual în modul demand paging atunci accesarea unei pagini virtuale din acel spațiu va conduce la page fault. În urma page fault-ului, se va aloca și mapa o pagină fizică, iar procesul își va continua execuția. Nu va fi generată excepție de acces la memorie. La fel se întâmplă și în cazul copy-on-write.

Lucrare 3

  • La începutul cursului 10:
    • marți, 5 mai 2015, 09:05-09:15, EC004, seria CA
    • miercuri, 6 mai 2015, 17:05-17:15, EC004, seria CC
3CA, varianta 1
  1. De ce este relevant, în contextul securității memoriei, faptul că adresa de retur a unei funcții se reține pe stivă?
    • Răspuns: Adresa de retur stocată în memorie oferă unui atacator posibilitatea suprascrierii acesteia și alterarea fluxului normal de execuție al programului. Pentru aceasta este nevoie de o vulnerabilitate într-un buffer la nivelul stivei. În general folosirea de adrese pe stive oferă această posibilitate si e de evitat, dar nu putem face asta în privința adresei de retur; este nevoie de stocarea pe stivă pentru a putea reveni în stack frame-ul anterior.
  2. Care este un avantaj și un dezavantaj al folosirii unei implementări de thread-uri în user space (user-level threads)?
    • Răspuns:
      • Avantaje pot fi:
        • timp de creare mai mic decât thread-urile kernel level
        • schimbări de context mai rapide
        • control mai bun asupra aspectelor de planificare (totul se întâmplă în user space, sub controlul programatorului)
      • Dezavantaje pot fi:
        • blocarea unui thread duce la blocarea întregului proces
        • nu poate fi folosit suportul multiprocesor
  3. De ce este importantă o instrucțiune de tip TSL (test and set lock) la nivelul procesorului?
    • Răspuns: Implementările de mecanisme de sincronizare se bazează pe instrucțiuni hardware. Fără suportul procesorului pentru operații atomice (precum TSL sau cmpxchg) nu ar fi posibilă implementarea unor mecanisme precum spinlock-uri. Astfel de instrucțiuni vor fi disponibile pentru orice procesor pentru a permite implementarea mecanismelor de sincronizare.
3CA, varianta 2
  1. De ce folosirea DEP (Data Execution Prevention) nu previne atacurile de tipul return-to-libc?
    • Răspuns: DEP previne existența simultană a permisiunilor de scriere și execuție. Adică nu se poate scrie într-o zonă un shellcode (sau ceva similar) care apoi să se execute. Un atac de tipul return-to-libc presupune suprascrierea unei adrese (de retur, pointer de funcție) ca să pointeze către o funcție din biblioteca standard C. Întrucât un atac de tipul return-to-libc nu presupune scriere și execuție a aceleiași zone, nu poate fi prevenit de DEP.
  2. Care este un avantaj și un dezavantaj al folosirii unei implementări de thread-uri cu suport în kernel (kernel-level threads)?
    • Răspuns:
      • Avantaje pot fi:
        • dacă un thread se blochează celelalte thread-uri pot rula
        • se folosește suportul multiprocesor al sistemului
        • planificator robust asigurat de sistemul de operare, preemptiv
      • Dezavantaje pot fi:
        • timp de creare mai mare (necesită apel de sistem)
        • schimbare de context mai lentă (overhead datorat trecerii în kernel space pentru invocarea planificatorului)
  3. Precizați un dezavantaj al folosirii primitivelor de sincronizare.
    • Răspuns: Dezavantaje sunt:
      • lock contention: mai multe thread-uri așteaptă la un lock (un singur thread poate accesa regiunea critică protejată de lock) → ineficiență
      • lock overhead: apelul de lock/unlock produce overhead, de multe ori însemnând apel de sistem
      • serializarea codului: codul protejat de un lock este cod serial, accesibil unui singur thread; nu avem paralelism
      • deadlock: o folosire necorespunzătoare a primitivelor de sincronizare duce la deadlock sau livelock
3CC, varianta 1
  1. De ce trebuie avut grijă la construcțiile precum cea de mai jos în cadrul unei funcții?
            int (*fn_ptr)(int, int);  /* fn_ptr is a function pointer */
            char buffer[128];    /* buffer for storing strings */
    • Răspuns: În construcția din exercițiu dacă nu se ține cont de dimensiunea buffer-ului se poate obține un buffer overflow. În urma overflow-ului, se suprascrie pointer-ul de funcție fn_ptr. Probabil acest pointer va fi folosit la un moment dat rezultând în execuția arbitrară și alterând fluxul normal de execuție al programului.
  2. De ce dimensiunea spațiului virtual de adresă al unui proces crește în momentul creării unui thread (chiar dacă thread-ul nu ajuns încă să se execute)?
    • Răspuns: În momentul creării unui thread se alocă o stivă nouă acelui thread. În mod implicit, pe sistemele Linux, dimensiunea stivei este de 8 MB de memorie, observând o creștere semnificativă a spațiului virtual de adresă al procesului.
  3. Când este recomandat să folosim un spinlock în locul unui mutex?
    • Răspuns: Spinlock-ul folosește busy-waiting și are operații de lock() și unlock() ieftine prin comparație cu mutex-ul. Operațiilor de lock() și unlock() pe mutex sunt de obicei costisitoare întrucât pot ajunge să invoce planificatorul. Având operații rapide, spinlock-ul este potrivit pe secțiuni critice de mici dimensiuni în care nu se fac operații blocante; în aceste cazuri faptul că face busy-waiting nu contează așa de mult pentru că va intra rapid în regiunea critică. Dacă am folosi un mutex pentru o regiune critică mică, atunci overhead-ul cauzat de operațiile pe mutex ar fi relativ semnificativ față de timpul scurt petrecut în regiunea critică, rezultând în ineficiența folosirii timpului pe procesor.
3CC, varianta 2
  1. De ce, în general, un shellcode se încheie cu invocarea apelului de sistem execve?
    • Răspuns: Un shellcode încearcă, în general, rularea unui program nou, de exemplu a unui shell în forma echivalentă a unui apel execve(”/bin/sh”). Întrucât un apel de bibliotecă este mai dificil de realizat, se preferă o instrucțiune simplă de apel de sistem (precum int 0x80. Se face un apel de sistem execve cu un argument de forma unui șir /bin/sh într-un registru rezultând în crearea unui shell nou.
  2. De ce schimbarea de context între două thread-uri ale aceluiași proces este, în general, mai rapidă decât schimbarea de context între două procese?
    • Răspuns: Schimbarea între două thread-uri ale aceluiași proces este mai rapidă decât schimbarea de context între două procese pentru că nu este nevoie de schimbarea spațiului de adresă. Schimbarea spațiului de adresă este relativ costisitoare pentrucă presupune schimbarea tabelei de pagini și golirea multor intrări din TLB.
  3. Care sunt cele două tipuri de operații aferente mecanismelor de sincronizare prin secvențiere/ordonare?
    • Răspuns: Cele două operații aferente mecanismelor de sincronizare prin secvențiere/ordonare sunt:
      • wait() pentru așteptarea îndeplinirii unei condiții după care thread-ul curent va rula;
      • notify() sau signal() pentru a anunța thread-ul/thread-urile blocate în operația wait() de îndeplinirea condiției.

Lucrare 4

  • La începutul cursului 13:
    • marți, 26 mai 2015, 09:05-09:15, EC004, seria CA
    • miercuri, 27 mai 2015, 17:05-17:15, EC004, seria CC
3CA, varianta 1
  1. O aplicație execută un apel send() cu 1024 de octeți de date, iar apelul send întoarce 1024. Alegeți varianta corectă de mai jos și argumentați: În acest moment, aplicația sender poate fi sigură că datele au fost livrate cu succes către
    • nucleul SO de pe sistemul destinație
    • aplicația destinație
    • datele au fost salvate în send-buffer-ul de pe sistemul transmițătorului
    • Răspuns: Datele au fost salvate în buffer-ul de send al transmițătorului. În momentul în care datele au fost scrise acolo, apelul se întoarce. Este posibil ca datele să nu fi părăsit sistemul, dar apelul se va întoarce. Stiva TCP se va ocupa de transmiterea datelor din buffer-ul de send către destinație.
  2. Indicați două obiective ale algoritmilor de planificare a cererilor pentru hard disk, și dați un exemplu de algoritm care le îndeplinește.
    • Răspuns: Un obiectiv este performanță ridicată. Un alt obiectiv este fairness: asigurarea că toate procesele au acces echitabil la resurse și că un proces nu așteaptă mai mult ca altul accesul la disc. Un algoritm care colectează mai multe cereri și apoi le sortează și agregă, independent de procesul care le cauzează va atinge obiectivele. Algoritmi precum C-SCAN sau C-LOOK sau altele satisfac aceste obiective.
  3. Precizați două diferențe între un symbolic link și un hard link.
    • Răspuns: Un symbolic link are un inode al său, pe când un hard link este un dentry (un nume și un index de inode). Un symbolic link poate referi directoare în timp ce un hard link nu; un symbolic link poate fereri un fișier de pe altă partiție/alt sistem de fișiere, în timp ce un hard link nu.
3CA, varianta 2
  1. Explicați motivul pentru care este indicat să folosim pentru apelurile send() și recv() buffere de dimensiuni mari (de exemplu mai mari decât 100KB).
    • Răspuns: Ca să transmitem mai multe date o dată si să evităm apelurile de sistem generate de apelul send și recv. Un apel de sistem va însemna overhead de timp (intare în kernel mode și apoi revenire în user mode) și overhead de copiere (transfer de date din buffer-ul din user space în buffer-ul din kernel space sau invers).
  2. Ce limitează performanța hard disk-ului în cazul accesului aleator la date din diverse zone ale discului?
    • Răspuns: Mutarea capului de citire pe sectoarele/zonele necesare. Dacă există acces aleator, atunci datele vor fi plasate în diverse zone iar o operație va consta în două suboperații:
      1. plasarea capului de citire
      2. citirea sau scrierea datelor respective
      • Dacă datele sunt plasate aleator, operația de plasare va dura mult și va limita performanța; putem optimiza prin ordonarea cererilor și limitarea timpului de accesare. Operația de citire și scriere este standard, ține de mecanica discului, nu o putem optimiza.
  3. Un director conține N subdirectoare. Câte hard link-uri pointează la acest director?
    • Răspuns: Directorul va avea N+2 hard link-uri. N hard link-uri sunt date de intrarea .. (dot dot) a fiecărui subdirector (link către directorul părinte). Celelalte două hard link-uri sunt numele directorului și intrarea . (dot) care referă directorul însuși.
3CC, varianta 1
  1. De ce la plăcile de rețea de mare viteză are sens folosirea polling în locul întreruperilor pentru partea de intrare/ieșire?
    • Răspuns: Având viteze mari, vor veni pachete foarte des și vor fi generate întreruperi foarte des. În această situație, procesorul va fi ocupat foarte mult timp rulând rutine de tratare a întreruperilor. Prin trecere la polling, procesorul interoghează placa de rețea și, dacă are date, le citesțe repede, fără întreruperi. În restul timpului face și alte lucruri, fără a mai consuma timp în rutina de tratare a întreruperilor.
  2. În ce situație operația send() pe un socket se blochează?
    • Răspuns: Apelul send() pe socket se blochează în situația în care buffer-ul din kernel (send buffer) nu dispune de loc pentru copierea datelor din buffer-ul de user space. Sau, în anumite cazuri, precum în cazul sockeților non-blocanți, dacă nu există nici măcar 1 octet liber în buffer-ul de kernel (send buffer).
  3. Care este un avantaj și un dezavantaj al alocării indexate (cu i-node) pentru blocuri de date pentru fișiere?
    • Răspuns: Principalul dezavantaj al alocării indexate este limitarea dimensiunii fișierului la numărul de intrări din lista de indecși (pointeri către blocuri). Avantajele este accesul rapid la blocuri (se citește indexul) și absența fragmentării externe: blocurile se pot găsi oriunde și pot fi referite din lista de indecși. Dezavantajul este compensat prin folosirea indirectării (simple, duble, triple) ducând la o mai mare dimensiune a fișierului, dar introducând un alt dezavantaj: timp mai mare de acces pentru blocurile din partea finală a fișierului; întrucât se trece prin blocurile de indirectare. Un dezavantaj aici poate fi și ocuparea de blocuri doar cu indecși, în loc să conțină date efective.
3CC, varianta 2
  1. De ce operația lseek() nu are sens pe dispozitive de tip caracter, ci doar pe dispozitive de tip bloc?
    • Răspuns: Pentru că pe dispozitivele de tip caracter datele vin și sunt citite/scrise octet cu octet, ca într-o țeavă. Nu putem anticipa date și ne putem plasa mai sus sau mai jos pe banda de date. În cazul dispozitivelor de tip bloc însă, datele se găsesc pe un spațiu de stocare pe care ne putem plimba/glisa; putem "căuta" date prin plasarea pe un sector/bloc al dispozitivului de stocare și atunci operația lseek() are sens.
  2. În cazul unui apel recv() comandat pentru citirea a 789 de octeți, se citesc 123 de octeți. Cum se explică citirea unui număr mai mic de octeți decât cel comandat?
    • Răspuns: În momentul citirii datelor, doar 123 de octeți erau disponibili în buffer-ul din kernel aferent socket-ului (receive buffer). În această situație apelul recv() se întoarce cu numărul de octeți disponibili (123) deși exista spațiu mai mare (789) în buffer-ul din user space.
  3. Ce conțin blocurile de date aferente unui inode de tip director?
    • Răspuns: Conțin un vector de dentry-uri. Un dentry este o structură ce conține numele fișierului și indexul inode-ului aferent. Fiecare intrare din director (indiferent de tipul acesteia: fișier, director, link symbolic) are un dentry.

Examene anterioare

so/meta/examen/2014-2015.txt · Last modified: 2016/06/03 18:00 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0