Hallo,
CDI ist eine Bibliothek, insofern über ganz normale Funktionsaufrufe.
Ich denke mal das ist allen klar, ich vermute die Frage zielt eher darauf ab was dahinter steckt also wie diese Funktionsaufrufe bei einem
Mikro-Kernel-OS von einem User-Mode-Prozess zum nächsten User-Mode-Prozess kommen.
Ich persönlich vermute mal das da überall ein paar Stubs drin stecken die das dann auf das OS-spezifische IPC umsetzen und wieder entgegennehmen.
@taljeth: Falls diese Vermutung richtig ist und sie auch auf tyndur zutrifft wäre es schön wenn Du uns dazu mal etwas detailliertere Infos geben könntest.
Ich vermute mal die freigebenen IDs werden wieder recycled, ansonsten wären echte Langläufer wirklich kaum möglich.
Das ist halt nicht möglich, so wie ich den Standard verstanden habe.
Also wenn Du den Standard richtig verstanden hast dann ist das schon ziemlich mysteriös. Im Prinzip verbraucht ja jedes fork eine ID und wenn die endlich sein sollten dürften so manche Web-Server (solche die für jede ankommende TCP-Verbindung einen fork machen) nicht besonders lange laufen. Ich vermute mal die OSe halten sich in diesem Punkt nicht an den Standard, anders kann ich mir das nicht erklären.
Meine Aussage bleibt trotzdem richtig Denn die Festplatte kann nur eine Anfrage zur gleichen Zeit entgegen nehmen, denn du hast ja nicht 32x Ports um die Festplatte anzusprechen!
Nein! Man kann einer modernen SATA-HDD, wenn sie an einem Host-Controller hängt der im AHCI-Modus arbeitet (und nicht mehr im klassischen IDE-Modus), wirklich mehrere Jobs geben ohne warten zu müssen bis die vorangegangenen Jobs fertig sind. SATA arbeitet paket-orientiert und da ist es wirklich möglich mehrere Aufträge hintereinander an die Platte zu schicken, ließ Dir am besten mal die SATA-Spezifikation und die AHCI-Spezifikation durch.
Bei dem Fall würde es sich aber wirklich lohnen, wenn man 32 Threads hätte. Dann könnte jeder Thread warten bis neue Arbeit da ist und er könnte auch warten bis die Festplatte fertig ist (ich hoffe du verstehst wie ich das meine).
Eben genau das meinte ich, wenn Du nur dann Anfragen entgegennehmen kannst wenn ein Thread darauf wartet dann muss es möglich sein das mehrere Threads gleichzeitig an einem Deiner Message-Ports/Pipes/was-auch-immer warten.
Ich sehe es erstmal so, das die Initialisierung ruhig ein wenig komplexer/langsamer sein darf, wenn der Vorgang an sich dann schneller geht.
Was ist mit einfachem Code in der Art:
int randValue;
{
FILE* randFile = fopen("/dev/random",ro);
fread(randFile,&randValue,sizeof(int));
fclose(randFile);
}
oder etwas anderem wo nur von einer (oder vielen) Datei(en) die Größe ermittelt werden soll. Da würdest Du einen Haufen Arbeit machen nur um wenige Infos zu bekommen, das erscheint mir unnötig kompliziert (und auch nicht schneller). Nebst dessen das Du in Deinem Kernel eine Menge Zeugs implementieren musst und ja theoretisch jeder Teil davon Fehler enthalten kann, zumindest generierst Du so eine Menge Code für den Kernel und steigerst damit die Wahrscheinlichkeit für Fehler (ohne nennenswert mehr Funktionalität zu bekommen).
Dann holst du dir die Anfrage mit "pipeRead(&msg,sizeof(struct msg_t))" (lohnt sich vorallem bei größeren Msgs, wie z.B. wenn man eine Datei laden soll)
Woher weiß Dein Dateisystemtreiber wie groß dieser Auftrag ist wenn er die Länge des Dateinamens noch nicht kennt? Byte-orientierte Pipes sind für solche Dinge extrem ungeeignet weil der Empfänger niemals die Synchronisation auf die Message-Grenzen verlieren darf.
arbeitest die Msg ab und schreibst deine Daten dann per pipeWrite(&data,dateLen)". Abschließend musst du dann noch ein "pipeFlush()" aufrufen, damit der Thread der auf die Daten wartet sie lesen kann.
Das sieht nach ner Menge Syscalls aus. 1. der Client für das Abschicken der Anfrage. 2. der Service für das Abholen der Anfrage (hier eventuell noch ein weiterer weil er erst mal nur die Länge der eigentlichen Anfrage erfahren wollte). 3. der Service für das Schreiben der Antwort (das Flush zähle ich mal nicht extra, das ist ein Flag beim Schreiben). 4. der Client zum Abholen der Antwort. Macht 4 Syscalls mit insgesamt 8 CPU-Mode-Wechseln (+ zusätzliches zur Vorbereitung/Aufräumen). Bei mir sind es nur 2 Syscalls (zum einen macht der Client seine Anfrage und zum anderen meldet der PopUp-Thread das er fertig ist und gekillt werden kann) mit insgesamt 4 CPU-Mode-Wechseln (ohne das irgendwelche Shared-Memorys o.ä. extra vorbereitet/aufgeräumt werden müssen).
Der Vorteil ist, das diese Pipe´s fast komplett im UserSpace laufen, du also nicht erst die Daten in den Kernel kopierst (Server) um sie dann wieder in den UserSpace zu kopieren (Client).
In meinem Konzept wird auch nichts kopiert sondern der Client stellt alle Speicherbereiche zur Verfügung und diese werden in den Service eingeblendet. Falls der Service seinerseits auch wieder die Dienste eines weiteren Service benötigt werden die geerbten Speicherbereiche einfach weitergereicht so das auch der letzte Service direkt mit dem Speicher des ursprünglichen Clients arbeitet. Ein HDD-Treiber würde also direkt in den Speicher der Applikation schreiben (bzw. den HDD-Host-Controller schreiben lassen) obwohl da noch das VFS und ein Datei-System-Treiber dazwischen hängen, das heißt das bevor die Applikation die Daten verarbeitet (nach dem fread) keines der Nutzdatenbytes überhaupt von einer CPU angefasst wurde. Der HDD-Controller schreibt die Daten (als Bus-Master) in den Haupt-Speicher und erst die Applikation (nach dem ganzen IPC-Vorgang) greift tatsächlich auf diese Daten zu.
Das fread in meiner libc würde etwa so aussehen (ich beziehe mich auf die IPC-Syscalls von
http://forum.lowlevel.eu/index.php?topic=2433):
int fread(FILE* const handle,void* const data,const ulong length)
{
ulong fread_length; //hier kommt die Anzahl der tatsächlich gelesenen Bytes rein
{
File_Read_Request_t request;
//'request' passend befüllen mit Infos von 'handle'
File_Read_Responce_t responce;
ulong responce_length = ~0;
//ein blockierender Syscall (synchrones IPC) :
const long retval = syscall_msg_sync_send(vfs_msg_id,&request,sizeof(request),NULL,0,&responce,&responce_length,sizeof(responce),data,&fread_length,length);
//den Erfolg des Syscalls ansich auswerten :
if (retval != KERNEL_SYSCALL_OK_Code)
{ /* IPC ansich ist fehlgeschlagen */ return ???; }
//die Antwort vom aufgerufenen Service (RPC-Handler) auswerten :
if (responce_length != sizeof(responce))
{ /* die Antwort vom RPC-Handler hat nicht die passende Größe */ return ???; }
if (responce != ????)
{ /* Fehler beim eigentlichen fread im VFS oder tiefer */ return ???; }
assert(fread_length <= length); //nur zur Sicherheit
}
return fread_length; //Anzahl der tatsächlich gelesenen Bytes zurückmelden
}
Ich finde das recht einfach und übersichtlich und es ist völlig egal ob 5 Bytes oder 500Mbytes gelesen werden sollen. Als einzigste Vorbereitung muss 'vfs_msg_id' von der libc einmal beim Prozessstart ermittelt werden, damit man weiß wohin mit den IPC-Anfragen.
Folgendes Szenario, ein Client sendet eine Nachricht und der Server kann sie gleich entgegen nehmen. Also kehrt der Client gleich wieder zurück ohne zu blockiert, dann wartet er auf eine Antwort, blockiert also.
Das sind schon 2 Syscalls für den Client obwohl IMHO einer reicht.
Aber in der Zwischenzeit sendet, aus welchem Grund auch immer, ein anderer Thread eine Nachricht an den Clienten und dieser wird aufgeweckt und bekommt nun eine Nachricht die er gar nicht haben will (im Moment) bzw. nicht erwartet.
Wieso kann man eigentlich überhaupt einem reinen Client (z.B. ein simples Hello-World-Programm) eine Message schicken? Das simple Programm ist doch darauf gar nicht ausgelegt. Ich persönlich sehe es als enormes Risiko an wenn man Prozessen etwas geben kann womit sie nichts anfangen können bzw. was sie gar nicht explizit erlaubt haben. Einem PC im Internet kann man doch auch nur auf die TCP/UDP-Ports was schicken die dieser PCs explizit geöffnet hat (um eben was von außen empfangen zu können).
Was ist eigentlich wenn ein böser Prozess in genau diesem Moment dem Client eine fingierte Antwort schickt? Ich persönlich bin der Meinung das der OS-Kernel es garantieren muss das der Client genau seine Antwort erhält und nichts anderes.
Wenn ich das von der Seite betrachte, ist asynchrones IPC mit aufgesetztem synchronen (das Warten auf eine Nachricht eines bestimmten Threads) doch besser. Da du dir die Möglichkeit der Asynchronität offen hälst.
Ich bleibe lieber dabei das ich beides anbiete, der zusätzliche Code im Kernel dürfte sehr überschaubar bleiben und den Performance-Verlust um aus etwas Asynchronem etwas Synchrones zu machen will ich nicht (eben weil das Synchrone das signifikant Häufigere und deutlich Wichtigere ist).
Wenn der Festplattentreiber Daten liest und diese an den Storage-Server weitergibt, dann würde ich das natürlich über SharedMem machen, viel einfacher und schneller.
Wenn Du erst für jeden kurzen Datei-Lesevorgang dann ein paar Ebenen tiefer Shared-Memory vorbereiten musst stelle ich mir das nicht besonders performant vor.
Genau dort liegt jetzt wieder das Problem (und der redundante Code). Stell dir vor du musst mehrmals Speicher anfordern, dann wird jedes Mal eine "NotEnoughMemoryException" geschmissen, aber da das Anfordern an verschieden Stellen stattfindet, musst du unterschiedlich Aufräumen, aber desto weiter du in einer Funktion bist, desto mehr musst du "gleich" machen! ...
Das soll man ja auch anders machen:
try
{ //Ebene 1
mem = memAlloc4kb();
try
{ //Ebene 2
id = allocNewID();
try
{ //Ebene 3
stack = memAlloc4kb();
... //das Spiel kannst du jetzt weiter treiben, indem noch mehr Ressourcen benötigt werden
} //Ebene 3
catch (NotEnoughMemoryException e)
{
freeID(id);
throw(NoFreeIDException); //noch den Aufräum-Code von Ebene 2 ansteuern
}
} //Ebene 2
catch (NoFreeIDException e)
{
memFree4kb(mem);
throw(NotEnoughMemoryException); //noch den Aufräum-Code von Ebene 1 ansteuern
}
} //Ebene 1
catch (NotEnoughMemoryException e)
{
return 0;
}
Man räumt quasi von innen nach außen auf. Jeden Aufräum-Code hast Du nur ein einziges mal (keine Redundanzen), Du musst immer nur darauf achten das beim von innen nach außen gehen keine Ebene ausgelassen wird.
In diesem konkreten Fall (wo es nur um malloc u.ä. geht) würde ich das aber schon etwas anders (einfacher) machen:
try
{
mem = memAlloc4kb();
id = allocNewID();
stack = memAlloc4kb();
//arbeiten ....
memFree4kb(stack);
freeID(id);
memFree4kb(mem);
return OK_Code;
}
catch (KernelException e) //KernelExpection ist Parent von NotEnoughMemoryException und NoFreeIDException
{
if (stack != NULL) { memFree4kb(stack); }
if (id != NULL) { freeID(id); }
if (mem != NULL) { memFree4kb(mem); }
return Fehler_Code;
}
Grüße
Erik