Lowlevel
Lowlevel => OS-Design => Thema gestartet von: LittleFox am 30. June 2010, 13:39
-
Moin,
bei mir geht es auch langsam in Richtung IPC.
Ich hab mir überlegt, dass Nachrichten über einen Syscall gesendet werden.
Zum empfangen muss sich jedes Programm einen Handler beim Kernel registrieren, der bei Nachrichten immer aufgerufen wird.
Momentan hänge ich beim Syscall fest, mit dem der Handler registriert werden soll. Am Ende soll etwas ähnliches rauskommen, wie in der WinAPI die WindowProcedure ;).
Hier ist mein Code aus dem Kernel:
if(r->int_no == 49)
{
// RegisterEventHandler
// eax = Funktionsnummer (momentan immer 0)
// edx = Speicheradresse des Eventhandlers
if(r->eax == 0)
{
currentEventHandler = (void*)r->edx;
}
}
Die Variable CurrentEventHandler deklariere ich so:
void (*currentEventHandler)(int message, long params);
Und hier ist mein Testprogramm:
void eventHandler(int message, long params);
void _start(void)
{
asm("movl %0, %%edx;"::"r"(eventHandler));
asm("movl $0, %eax");
asm("int $49");
while(1);
}
void eventHandler(int message, long params)
{
if(message == 0x00000001)
{
char temp[2] = "";
temp[0] = params;
temp[1] = 0;
asm("mov $0x07, %edx");
asm("movl %0, %%esi;"::"r"(temp));
asm("movl $0, %eax");
asm("movl $10, %ebx");
asm("movl $5, %ecx");
asm("int $48");
}
}
Die Nachricht 0x00000001 ist übrigens für Tastatureingaben.
So bekomme ich immer einen Bluescreen (Invalid Opcode), nachdem das Programm gestartet wurde.
Danke schonmal für die Hilfe.
LittleFox
EDIT: upps, im falschen Forum gelandet, müsste eigentlich nach OS-Design. Kann das bitte mal ein Mod verschieben? Danke :-)
-
Scheinbar wird die variable currentEventHandler nicht mit dem neuen Wert belegt.
Es geht auch nicht, wenn ich das direkt im Kernel mache. Die Adresse von currentEventHander bleibt die ganze Zeit über NULL ...
Irgendwelche Ideen?
-
Also als allererstes solltest du jeweils einen einzigen asm-Block nehmen (und die Instruktionen per ; oder \n trennen), nicht mehrere. Was bei dir passiert ist folgendes:
void _start(void)
{
// Registerzustand ist vom C-Compiler definiert
asm("movl %0, %%edx;"::"r"(eventHandler)); // Kopiere eventHandler nach edx
// Hier ist wieder C. Der Compiler kann mit den Registern anstellen was er will.
asm("movl $0, %eax"); // Kopiere 0 nach edx, eax ist undefiniert
// Hier ist wieder C. Der Compiler kann mit den Registern anstellen was er will.
asm("int $49"); // Rufe int 49 auf. Der Registerinhalt sowohl von eax als auch edx ist undefiniert.
Wenn du besonders viel Glück hast, erkennt der Compiler sogar, dass dein Assembler-Block nichts sinnvolles beiträgt und "optimiert" ihn ganz raus. Am besten schaust du dir mal mit objdump an, was da tatsächlich produziert worden ist.
-
OK, ich hab das jetzt in einen asm block geschrieben.
Laut objdump wird nach edx $0x0 gelegt.
der neue asm block:
asm("movl %0, %%edx;"
"movl $0, %%eax;"
"int $49;"::"r"((void*)eventHandler));
Ausgabe von objdump:
main.o: file format elf32-i386
Disassembly of section .text:
00000000 <puts>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 89 c6 mov %eax,%esi
8: b8 02 00 00 00 mov $0x2,%eax
d: cd 30 int $0x30
f: 5d pop %ebp
10: c3 ret
00000011 <putc>:
11: 55 push %ebp
12: 89 e5 mov %esp,%ebp
14: 83 ec 14 sub $0x14,%esp
17: 8b 45 08 mov 0x8(%ebp),%eax
1a: 88 45 ec mov %al,-0x14(%ebp)
1d: 8a 45 ec mov -0x14(%ebp),%al
20: 88 45 fe mov %al,-0x2(%ebp)
23: c6 45 ff 00 movb $0x0,-0x1(%ebp)
27: 8d 45 fe lea -0x2(%ebp),%eax
2a: 50 push %eax
2b: e8 fc ff ff ff call 2c <putc+0x1b>
30: 83 c4 04 add $0x4,%esp
33: a1 00 00 00 00 mov 0x0,%eax
38: 40 inc %eax
39: a3 00 00 00 00 mov %eax,0x0
3e: c9 leave
3f: c3 ret
00000040 <putn>:
40: 55 push %ebp
41: 89 e5 mov %esp,%ebp
43: 83 ec 50 sub $0x50,%esp
46: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
4d: 83 7d 0c 24 cmpl $0x24,0xc(%ebp)
51: 7f 65 jg b8 <putn+0x78>
53: 8d 45 b7 lea -0x49(%ebp),%eax
56: 83 c0 40 add $0x40,%eax
59: 89 45 fc mov %eax,-0x4(%ebp)
5c: 8b 45 fc mov -0x4(%ebp),%eax
5f: c6 00 00 movb $0x0,(%eax)
62: ff 4d fc decl -0x4(%ebp)
65: 8b 55 0c mov 0xc(%ebp),%edx
68: 8b 45 08 mov 0x8(%ebp),%eax
6b: 89 d1 mov %edx,%ecx
6d: ba 00 00 00 00 mov $0x0,%edx
72: f7 f1 div %ecx
74: 89 d0 mov %edx,%eax
76: 03 45 f8 add -0x8(%ebp),%eax
79: 8a 10 mov (%eax),%dl
7b: 8b 45 fc mov -0x4(%ebp),%eax
7e: 88 10 mov %dl,(%eax)
80: 8b 55 0c mov 0xc(%ebp),%edx
83: 8b 45 08 mov 0x8(%ebp),%eax
86: 89 d1 mov %edx,%ecx
88: ba 00 00 00 00 mov $0x0,%edx
8d: f7 f1 div %ecx
8f: 89 45 08 mov %eax,0x8(%ebp)
92: 83 7d 08 00 cmpl $0x0,0x8(%ebp)
96: 75 ca jne 62 <putn+0x22>
98: 83 7d 0c 10 cmpl $0x10,0xc(%ebp)
9c: 75 0d jne ab <putn+0x6b>
9e: 68 25 00 00 00 push $0x25
a3: e8 fc ff ff ff call a4 <putn+0x64>
a8: 83 c4 04 add $0x4,%esp
ab: ff 75 fc pushl -0x4(%ebp)
ae: e8 fc ff ff ff call af <putn+0x6f>
b3: 83 c4 04 add $0x4,%esp
b6: eb 01 jmp b9 <putn+0x79>
b8: 90 nop
b9: c9 leave
ba: c3 ret
000000bb <printf>:
bb: 55 push %ebp
bc: 89 e5 mov %esp,%ebp
be: 83 ec 20 sub $0x20,%esp
c1: 8d 45 0c lea 0xc(%ebp),%eax
c4: 89 45 ec mov %eax,-0x14(%ebp)
c7: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
ce: e9 14 01 00 00 jmp 1e7 <printf+0x12c>
d3: 8b 45 08 mov 0x8(%ebp),%eax
d6: 8a 00 mov (%eax),%al
d8: 3c 25 cmp $0x25,%al
da: 0f 85 f3 00 00 00 jne 1d3 <printf+0x118>
e0: ff 45 08 incl 0x8(%ebp)
e3: 8b 45 08 mov 0x8(%ebp),%eax
e6: 8a 00 mov (%eax),%al
e8: 0f be c0 movsbl %al,%eax
eb: 83 f8 70 cmp $0x70,%eax
ee: 74 7d je 16d <printf+0xb2>
f0: 83 f8 70 cmp $0x70,%eax
f3: 7f 2a jg 11f <printf+0x64>
f5: 83 f8 63 cmp $0x63,%eax
f8: 0f 84 98 00 00 00 je 196 <printf+0xdb>
fe: 83 f8 63 cmp $0x63,%eax
101: 7f 12 jg 115 <printf+0x5a>
103: 85 c0 test %eax,%eax
105: 0f 84 eb 00 00 00 je 1f6 <printf+0x13b>
10b: 83 f8 25 cmp $0x25,%eax
10e: 74 7a je 18a <printf+0xcf>
110: e9 a1 00 00 00 jmp 1b6 <printf+0xfb>
115: 83 f8 64 cmp $0x64,%eax
118: 74 36 je 150 <printf+0x95>
11a: e9 97 00 00 00 jmp 1b6 <printf+0xfb>
11f: 83 f8 75 cmp $0x75,%eax
122: 74 2c je 150 <printf+0x95>
124: 83 f8 78 cmp $0x78,%eax
127: 74 44 je 16d <printf+0xb2>
129: 83 f8 73 cmp $0x73,%eax
12c: 0f 85 84 00 00 00 jne 1b6 <printf+0xfb>
132: 8b 45 ec mov -0x14(%ebp),%eax
135: 8d 50 04 lea 0x4(%eax),%edx
138: 89 55 ec mov %edx,-0x14(%ebp)
13b: 8b 00 mov (%eax),%eax
13d: 89 45 f0 mov %eax,-0x10(%ebp)
140: ff 75 f0 pushl -0x10(%ebp)
143: e8 fc ff ff ff call 144 <printf+0x89>
148: 83 c4 04 add $0x4,%esp
14b: e9 94 00 00 00 jmp 1e4 <printf+0x129>
150: 8b 45 ec mov -0x14(%ebp),%eax
153: 8d 50 04 lea 0x4(%eax),%edx
156: 89 55 ec mov %edx,-0x14(%ebp)
159: 8b 00 mov (%eax),%eax
15b: 89 45 f4 mov %eax,-0xc(%ebp)
15e: 6a 0a push $0xa
160: ff 75 f4 pushl -0xc(%ebp)
163: e8 fc ff ff ff call 164 <printf+0xa9>
168: 83 c4 08 add $0x8,%esp
16b: eb 77 jmp 1e4 <printf+0x129>
16d: 8b 45 ec mov -0x14(%ebp),%eax
170: 8d 50 04 lea 0x4(%eax),%edx
173: 89 55 ec mov %edx,-0x14(%ebp)
176: 8b 00 mov (%eax),%eax
178: 89 45 f4 mov %eax,-0xc(%ebp)
17b: 6a 10 push $0x10
17d: ff 75 f4 pushl -0xc(%ebp)
180: e8 fc ff ff ff call 181 <printf+0xc6>
185: 83 c4 08 add $0x8,%esp
188: eb 5a jmp 1e4 <printf+0x129>
18a: 6a 25 push $0x25
18c: e8 fc ff ff ff call 18d <printf+0xd2>
191: 83 c4 04 add $0x4,%esp
194: eb 4e jmp 1e4 <printf+0x129>
196: 8b 45 ec mov -0x14(%ebp),%eax
199: 8d 50 04 lea 0x4(%eax),%edx
19c: 89 55 ec mov %edx,-0x14(%ebp)
19f: 8b 00 mov (%eax),%eax
1a1: 89 45 fc mov %eax,-0x4(%ebp)
1a4: 8b 45 fc mov -0x4(%ebp),%eax
1a7: 0f be c0 movsbl %al,%eax
1aa: 50 push %eax
1ab: e8 fc ff ff ff call 1ac <printf+0xf1>
1b0: 83 c4 04 add $0x4,%esp
1b3: 90 nop
1b4: eb 2e jmp 1e4 <printf+0x129>
1b6: 6a 25 push $0x25
1b8: e8 fc ff ff ff call 1b9 <printf+0xfe>
1bd: 83 c4 04 add $0x4,%esp
1c0: 8b 45 08 mov 0x8(%ebp),%eax
1c3: 8a 00 mov (%eax),%al
1c5: 0f be c0 movsbl %al,%eax
1c8: 50 push %eax
1c9: e8 fc ff ff ff call 1ca <printf+0x10f>
1ce: 83 c4 04 add $0x4,%esp
1d1: eb 11 jmp 1e4 <printf+0x129>
1d3: 8b 45 08 mov 0x8(%ebp),%eax
1d6: 8a 00 mov (%eax),%al
1d8: 0f be c0 movsbl %al,%eax
1db: 50 push %eax
1dc: e8 fc ff ff ff call 1dd <printf+0x122>
1e1: 83 c4 04 add $0x4,%esp
1e4: ff 45 08 incl 0x8(%ebp)
1e7: 8b 45 08 mov 0x8(%ebp),%eax
1ea: 8a 00 mov (%eax),%al
1ec: 84 c0 test %al,%al
1ee: 0f 85 df fe ff ff jne d3 <printf+0x18>
1f4: eb 01 jmp 1f7 <printf+0x13c>
1f6: 90 nop
1f7: 8b 45 f8 mov -0x8(%ebp),%eax
1fa: c9 leave
1fb: c3 ret
000001fc <eventHandler>:
1fc: 55 push %ebp
1fd: 89 e5 mov %esp,%ebp
1ff: 83 ec 10 sub $0x10,%esp
202: 83 7d 08 01 cmpl $0x1,0x8(%ebp)
206: 75 2f jne 237 <eventHandler+0x3b>
208: 66 a1 28 00 00 00 mov 0x28,%ax
20e: 66 89 45 fe mov %ax,-0x2(%ebp)
212: 8b 45 0c mov 0xc(%ebp),%eax
215: 88 45 fe mov %al,-0x2(%ebp)
218: c6 45 ff 00 movb $0x0,-0x1(%ebp)
21c: ba 07 00 00 00 mov $0x7,%edx
221: 8d 45 fe lea -0x2(%ebp),%eax
224: 89 c6 mov %eax,%esi
226: b8 00 00 00 00 mov $0x0,%eax
22b: bb 0a 00 00 00 mov $0xa,%ebx
230: b9 05 00 00 00 mov $0x5,%ecx
235: cd 30 int $0x30
237: c9 leave
238: c3 ret
00000239 <_start>:
239: 55 push %ebp
23a: 89 e5 mov %esp,%ebp
23c: b8 00 00 00 00 mov $0x0,%eax
241: 89 c2 mov %eax,%edx
243: b8 00 00 00 00 mov $0x0,%eax
248: cd 31 int $0x31
24a: 68 00 00 00 00 push $0x0
24f: 68 2c 00 00 00 push $0x2c
254: e8 fc ff ff ff call 255 <_start+0x1c>
259: 83 c4 08 add $0x8,%esp
25c: b8 00 00 00 00 mov $0x0,%eax
261: c9 leave
262: c3 ret
Hab inzwischen noch eine printf funktion eingebaut, um die pointer zu vergleichen. Da hat komischerweise alles gestimmt ...
Danke für die Antwort
-
Ich hab jetzt in den Syscall, der für das Handler registrieren verantwortlich ist, noch 2 printf's eingebaut, um zu sehen,
ob die Speicheradresse aus dem Syscall richtig ankommt und ob die Variable currentEventHandler den neuen Wert bekommt.
Komischerweise stimmen alle 3 Werte überein :? :? :?
Ich versteh nicht, warum der Handler nicht aufgerufen wird ...
Hier der Code aus der keyboard.c, der bei Tastatureingaben den aktuellen Handler aufruft:
if(currentEventHandler != NULL)
currentEventHandler(MESSAGE_KEYINPUT, kbdus[scancode]);
else
echoKeyboard(kbdus[scancode]);
MESSAGE_KEYINPUT ist mit 0x000001 definiert.
-
Öhm, du willst den Handler aus dem Kernelcode heraus, also in Ring 0 aufrufen? Das scheint mir keine besonders schlaue Idee zu sein, schon allein aus Sicherheitsgründen. Und ist der Eventhandler überhaupt gemappt, wenn die Nachricht geschickt wird, während ein anderer Prozess aktiv ist?
-
bei mir läuft momentan alles noch im Ring 0, im Ring 3 ging das Multitasking irgendwie nicht ... da muss ich nochmal gucken.
Der Arbeitsspeicher ist bis jetzt auch an einem Stück, daran kann es also auch nicht liegen.
Mir ist jetzt noch eine andere Idee gekommen, nämlich in jedem Programm ein struct erstellen und die Adresse zu diesem Struct an den Kernel schicken.
Der Verändert dann die Werte. Das Programm läuft in einer Endlosschleife und überprüft immer, ob in dem struct was geändert wurde.
Wäre das vielleicht besser?
Wäre später auf jeden Fall kein Handler im Ring 0
-
Hallo,
Das Programm läuft in einer Endlosschleife und überprüft immer, ob in dem struct was geändert wurde.
Wäre das vielleicht besser?
Also Busy-Waiting, das würde ich als noch unsinniger als einen Event-Handler im Ring0 betrachten.
Das abschicken von Nachrichten ist für gewöhnlich kein Problem aber das Empfangen dagegen schon. Da gibt es im wesentlichen 2 Möglichkeiten: einmal aktives Abholen (z.B. getMyNextMessage() mit oder ohne Time-Out) und zum anderen das Empfänger-Programm unterbrechen (so wie klassische Unix-Signal-Handler). Beides hat einige Nachteile. Beim aktiven Abholen muss der Empfänger sich absichtlich für das Empfangen von Messages bereit machen, was natürlich nicht immer geht und daher Messages nicht gleich verarbeitet werden können. Dafür ist diese Lösung mMn jene die sich am leichtesten Implementieren lässt, wenn man die Daten 2 mal kopiert und im Kernel zwischenspeichert. Dazu benötigt man zwar im Kernel etwas (oder sehr viel) Speicher aber man bekommt ein relativ sicheres System. Das mit dem Unterbrechen scheint zwar auf den ersten Blick deutlich schneller zu arbeiten hat aber auch ne ganze Menge unangenehmer Nebenwirkungen (so eine Unterbrechung könnte z.B. in einem kritischen Moment passieren wo die libc gerade an der Heap-Verwaltung arbeitet), die tyndur-Leute können darüber viel trauriges erzählen.
Du kannst Dir also überlegen welches wohl das kleinere Übel ist oder versuchen einen besseren Weg zu finden. ;)
Eine andere Frage sollte auch noch bearbeitet werden: möchtest Du synchrone oder asynchrone Messages?
Bei den Synchronen wartet der Sender solange bis der Empfänger die passende Antwort zurückschickt und das OS kümmert sich darum das jede Antwort auch den richtigen Prozess erreicht, das macht das Handling in den Programmen um einiges einfacher aber der Sender blockiert eben auch so lange bis seine Anfrage bearbeitet ist. Bei den asynchronen Messages schickt einfach ein Programm einem anderen eine Nachricht und das wars, wenn da zwischen mehreren Nachrichten ein Zusammenhang hergestellt werden soll (als eine konkrete Antwort der zugehörigen Anfrage zugeordnet werden soll) ist dass das Problem der Programme, sowas lässt sich auch nur sehr schwer gegen absichtliche Manipulationen durch dritte Programme absichern (mMn für ein General-Purpose-OS ein absolutes KO-Kriterium). Die meiste Kommunikation in einem Micro-Kernel-OS ist klassisches RPC (http://de.wikipedia.org/wiki/Remote_Procedure_Call) (open/close/read/write und da will man immer einen zugehörigen Rückgabewert, also eine Antwort vom ausführenden Service, haben und wartet auch gerne darauf) und das geht am einfachsten/besten/sichersten mit synchronen Messages, sieh Dir dazu mal die Konzepte (und die dazugehörigen Gründe) an die im L4-Micro-Kernel umgesetzt wurden. Es gibt aber auch Situationen in denen asynchrone Messages kaum zu ersetzen sind, z.B. wenn der Kernel an einen Treiber-Prozess einen IRQ als asynchrone Message schickt, der Kernel wird ja wohl nicht warten wollen bis der Treiber mit seiner Hardware gequatscht hat.
Im Endeffekt wirst Du für ein ordentliches, stabiles und schnelles Micro-Kernel-OS beides benötigen, synchrone und asynchrone Messages.
Grüße
Erik
PS. falls Du kein Micro-Kernel-OS programmierst kannst Du meinen Beitrag ignorieren.
-
Hi,
ich hoffe ich habe jetzt nichts übersehen, hab die posts nur mal übelflogen.
1. bei mir würde dein asm-code so aussehen:
asm volatile ("int $49" :: "a" (0x00), "d" ((void*)eventHandler));
das kann gcc besser optimieren,
2. das im objdump edx mit 0 belegt wird liegt wohl daran dass du das noch nicht durch den linker hast laufen lassen, der die Null durch die Adresse von eventHandler ersetzt.
3. es könnte nötig sein dein currentEventHandler als volatile zu deklarieren weil er durch einen Interrupt verändert wird, also für gcc an völlig unvorhersehbaren stellen. (mit volatile zwingst du den Compiler die variable immer direkt aus dem Speicher zu lesen und in ihn zu schreiben)
-
erstmal danke für die antworten.
@erik eigentlich soll es ja ein monolithischer Kernel werden, wobei Treiber als Modul geladen werden, aber im Ring 0 laufen.
@MNemo
1. ich probier das jetzt mal aus
2. upps, daran hab ich gar nicht gedacht
ich melde mich nochmal ob es geklappt hat.
-
wie deklariert man einen Funktionsprototypen als volatile? :?
wenn ich void volatile (*currentEventHandler)(int message, long params) schreibe,
meckert er: warning: function return types not compatible due to 'volatile'
Edit: Ah, void (*volatile currentEventHandler)(blablabla), oder?
Wenn ja: geht trotzdem nicht
-
so müsste es lauten:
volatile void (*currentEventHandler)(foo)
nee, dass ist doch richtig:
void (* volatile currentEventHandler)(foo);
-
Hallo,
@erik eigentlich soll es ja ein monolithischer Kernel werden, wobei Treiber als Modul geladen werden, aber im Ring 0 laufen.
Warum machst Du Dir dann so einen Kopf wegen IPC (Inter-Process-Comunication)? Innerhalb eines Kernel kannst Du das Verbinden von Aufrufern mit ihrem Ziel dem (Runtime-)Linker überlassen.
Grüße
Erik
-
Der Kernel muss doch trotzdem mit normalen Programmen 'reden' können?
Oder hab ich da was falsch verstanden? Das soll gerade ein normales Programm werden, kein Treiber
-
Hallo,
Der Kernel muss doch trotzdem mit normalen Programmen 'reden' können?
Dazu gibt es die Syscalls. Man kann zwar über die Syscalls auch IPC zu Verfügung stellen aber ansonsten haben diese 2 Konzepte kaum was gemeinsam.
Und wenn der Kernel was von den Programmen will (was bei einem Monolithen eher selten ist) dann läuft das auf das selbe Problem wie mit dem Abholen von Messages raus (explizites Abholen vs. Unterbrechen vs. ganz-was-anderes).
Grüße
Erik
-
Momentan sollen die Nachrichten vom Kernel an's Programm über diesen Handler geschickt werden. Damit weiß das Programm bescheid, dass es Nachrichten hat und kann sie bearbeiten, wenn Zeit dafür ist.
Dadurch muss das Programm nicht dauernd fragen "hab ich Post?" und wird auch nicht unterbrochen.
Was das Programm mit den Nachrichten macht, kann es selber entscheiden. Es kann sagen, behandle ich jetzt sofort oder eben jetzt nicht, später.
Wenn ich irgendetwas komplett falsch verstanden habe, bitte bescheid sagen. :|
-
Naja, die Arbeit geht vom Programm aus, nie vom Kernel. Das heißt, dass das Programm immer eine Anfrage an den Kernel richtet (via Syscall) und ein Ergebnis zurückbekommt. Im einfachsten Falle ist dies blockierend, d.h. das Programm wartet, bis der Kernel die Antwort parat hat. Alternativ gibt es nicht-blockierende Syscalls, die dann später die Antworten zurückliefern.
Persönlich finde ich Callback-Funktionen ungünstig, da sie die Anwendungsentwicklung erschweren (asynchroner Programmfluss). Ein synchroner Ablauf sollte immer möglich sein.
Vergleiche mit den Syscalls einfach DOS: Anwendung setzt Register, ruft den Interrupt 21h auf, der tut irgendwas, setzt die Register und dann das Programm fort. Das Ergebnis des Syscalls kann direkt gelesen werden und die Aktion ist erfüllt - also eine blockierende Schnittstelle. Man kann es natürlich auch anders aufbauen, aber Interrupts sind eine Möglichkeit.
Wenn man nicht möchte, dass das Programm bis zum Abschluss des Syscalls steht, kann man einerseits die Syscalls auch nicht-blockierend bauen oder auf Multithreading setzen (ein Thread steht, die anderen laufen weiter). Beides ist aber aufwändiger.
Ein ereignisbasierendes System, wie du es gerade aufbaust, bringt an der Stelle nur wenig. In Unix-Systemen läuft die GUI etwa so ab, dass du ein Socket/Dateideskriptor nutzt und von diesem in einer Endlosschleife liest. Der Lesevorgang wartet, bis etwas geschieht (Maus bewegt, ...) und das wird dann behandelt. Die Verarbeitung läuft in einem anderen Thread ab; in Ereignisse konvertieren ist dann in der Anwendung möglich, wenn man das wünscht:
while(true)
{
read(events, &buf);
ping_worker_thread(&buf);
}
Gruß,
Svenska
-
also mach ich das jetzt ungefähr so:
int main()
{
// initialisierung des programs
bool is_running = true;
MESSAGE message;
while(is_running)
{
message = get_message();
do_something_with_the_message(message);
}
die funktionen sind in der SDK deklariert.
Ist es so in ordnung?
-
Hallo,
Momentan sollen die Nachrichten vom Kernel an's Programm über diesen Handler geschickt werden.
Also wird der aktuelle Programmfluss unterbrochen und der Handler im aktuellen Kontext wie eine Unterfunktion ausgeführt, im Prinzip das selbe wie bei einem IRQ. Das kann man machen, es vermittelt auch den subjektiven Eindruck einer hohen Geschwindigkeit und geringen Latenz weil die Message sofort bearbeitet wird und nicht erst dann wenn das Programm mal wieder aktiv nach neuen Messages fragt. Aber der Message-Handler hat auch die selben Probleme wie ein IRQ-Handler, alle Mechanismen die durch einen IRQ unterbrochen werden können kann der IRQ-Handler selber nicht benutzen, denn die könnten ja in einem kritischen/inkonsistenten Zustand sein wenn der IRQ kommt. Wenn Dein Message-Handler also malloc benutzen können soll dann muss innerhalb von malloc (und free usw.) das Handler-Aufrufen gesperrt werden (ähnlich dem sperren von IRQs). Das hat eine ganze Menge Nebenwirkungen. Eine davon ist das wenn der Kernel selber eine Message schicken können soll das dann der Kernel in der Lage sein muss seinen aktuellen Zustand zu sichern/unterbrechen und erstmal das Programm weiterlaufen zu lassen damit es eine solche verbotene Zone wieder verlassen kann (auf einem Multi-Core-System ginge es vielleicht das der Kernel einfach wartet aber toll ist das auch nicht). Wenn es im Kontext des Kernels nicht eigenständige unterbrechbare Threads gibt dürfte das nur mit sehr viel Aufwand umsetzbar sein. Für mein System hab ich mir daher ein Konzept ausgedacht wie es (fast) immer möglich ist einen solchen Handler aufzurufen ohne dass das Programm dadurch behindert wird.
Damit weiß das Programm bescheid, dass es Nachrichten hat und kann sie bearbeiten, wenn Zeit dafür ist.
Dieses "weiß bescheid" mus das Programm aber auch erst erfragen (z.B. auf ein Flag pollen) und damit ist das Konzept IMHO nicht viel besser als wenn das Programm gleich einen Syscall HabIchNeMessageDannGibMirDieGleichAnsonstenWarteNicht() aufruft.
und wird auch nicht unterbrochen.
Wenn der Kernel selbstständig im Kontext eines Programms einen Handler aufrufen soll dann wird das Programm dafür (kurz) unterbrochen.
Naja, die Arbeit geht vom Programm aus, nie vom Kernel.
Das trifft nicht immer zu, es gibt auch Dinge die immer vom Kernel ausgehen, könnten bei einem Monolithen eventuell sogar mehr sein als bei einem Micro-Kernel.
Alternativ gibt es nicht-blockierende Syscalls, die dann später die Antworten zurückliefern.
Das bedeutet dass das Programm mehrere Syscalls benötigt, einen um den Job beim Kernel zu starten und einen anderen um nach dem Ergebnis zu fragen. Der zweite Syscall könnte oft sogar mehrmals benötigt werden falls der Job eben noch nicht fertig ist.
Persönlich finde ich Callback-Funktionen ungünstig, da sie die Anwendungsentwicklung erschweren (asynchroner Programmfluss). Ein synchroner Ablauf sollte immer möglich sein.
Das stimmt zwar aber die meisten Ereignisse entstehen asynchron (Tastatur-Events, empfangene Netzwerk-Pakete usw.), wenn man maximale Performance (bei vertretbarer CPU-Last) möchte kommt man um eine asynchrone Ereignisbehandlung kaum herum.
Grüße
Erik
-
die idee mit dem handler hab ich wirklich von den IRQs übernommen ;)
man hätte man ja auch so machen können, dass eine variable wie bool has_message auf true gesetzt wird.
hab ich dich richtig verstanden, dass mein beispiel oben besser ist?
-
Hallo,
man hätte man ja auch so machen können, dass eine variable wie bool has_message auf true gesetzt wird.
Du meinst der Message-Handler setzt nur eine einzelne Variable und das Programm muss pollen? Wo ist da der Vorteil gegenüber einem Syscall "HabIchNeMessageDannGibMirDieGleichAnsonstenWarteNicht()"? Okay der Syscall benötigt etwas mehr CPU-Takte aber das Prinzip ist exakt das selbe.
hab ich dich richtig verstanden, dass mein beispiel oben besser ist?
Du meinst das von heute Früh 8:44? Das ist ungefähr die einfachste Lösung. Die kann man am schnellsten implementieren, bietet nur sehr wenig Fehlerpotential und ist für einen Monolithen erstmal recht brauchbar. Das Problem ist wie wartest Du auf mehrere verschiedene Events? Ein HTTP-Server mit sehr vielen gleichzeitig zu bearbeitenden Anfragen wird wohl kaum für jede TCP-Connection einen eigenen Thread starten (das geht zwar macht man aber nur bei Servern die nur wenig belastet sind). In der Win-API gibts dafür WaitForMultipleObject(), das stellt eine recht brauchbare Lösung für das Problem dar bis Du auf einer Multi-Core-Maschine auf die Idee kommst für jede CPU einen eigenen Thread starten zu wollen wo dann alle gleichzeitig WaitForMultipleObject() aufrufen wollen, auch das ist lösbar aber Du verstehst bestimmt was ich meine.
Für eine ordentliche Lösung muss man schon ziemlich tief in die Trickkiste greifen. Du solltest also erst mal überlegen was eigentlich Dein Ziel ist und dann prüfen welchen Aufwand Du bereit bist dafür zu investieren.
Grüße
Erik
-
momentan werd ich wohl für alles die einfachste Lösung nehmen.
Was ich dann später mache, weiß ich noch nicht.
mit dem Handler hätte es einen winzigsten Geschwindigkeitsvorteil gebracht, weil das Programm nur Nachrichten abruft, wenn welche da sind.
Ich werde jetzt int main()
{
// initialisierung des programs
bool is_running = true;
MESSAGE message;
while(is_running)
{
message = get_message();
do_something_with_the_message(message);
}
diese lösung nehmen.
Danke an alle für die Hilfe.
-
Hallo,
Naja, die Arbeit geht vom Programm aus, nie vom Kernel.
Das trifft nicht immer zu, es gibt auch Dinge die immer vom Kernel ausgehen, könnten bei einem Monolithen eventuell sogar mehr sein als bei einem Micro-Kernel.
Hier ging es aber um die Kommunikation zwischen Kernel und Userland. Sachen, die vom Kernel ausgehen, bleiben meist auch da drin. Bei einem Mikrokernel - wenn z.B. die Tastatur etwas raushaut und die Speicherverwaltung reagieren muss - ist das etwas anderes, aber dort bleibt es trotzdem "im" Kernel.
Ich sehe bei Callback-Funktionen immer das Problem des Programmabsturzes, wenn der Handler nicht mehr existiert aber noch registriert ist. Dann hast du nämlich ein Problem. (In einem Mikrokernel ist das relativ egal, wenn die Speicherverwaltung abstürzt, ist das System eh im Popöchen.)
Alternativ gibt es nicht-blockierende Syscalls, die dann später die Antworten zurückliefern.
Das bedeutet dass das Programm mehrere Syscalls benötigt, einen um den Job beim Kernel zu starten und einen anderen um nach dem Ergebnis zu fragen. Der zweite Syscall könnte oft sogar mehrmals benötigt werden falls der Job eben noch nicht fertig ist.
Ich sehe kein Problem darin.
read() muss ja nicht blocken und liefert dann -EAGAIN zurück. Blockierendes Verhalten wird immer dann schwierig, wenn man quasiparalleles, asynchrones Verhalten in nur einem Thread erzeugen möchte, aus welchen Gründen auch immer. Aber als Haupt-API würde ich nie ein Callback-Verhalten verwenden.
Persönlich finde ich Callback-Funktionen ungünstig, da sie die Anwendungsentwicklung erschweren (asynchroner Programmfluss). Ein synchroner Ablauf sollte immer möglich sein.
Das stimmt zwar aber die meisten Ereignisse entstehen asynchron (Tastatur-Events, empfangene Netzwerk-Pakete usw.), wenn man maximale Performance (bei vertretbarer CPU-Last) möchte kommt man um eine asynchrone Ereignisbehandlung kaum herum.
Und? Die kann man doch ganz einfach simulieren, man nehme einen Thread der nur poll() oder WaitForSomething() aufruft - ohne Ereignisse muss der ja nichtmal gestartet werden - und bei einem Ereignis eine Funktion in einem anderen Thread aufruft. Das ist die gleiche Last wie mit Callbackhandler, aber so wird die Asynchronität nicht erzwungen.
Grüßle
-
Hallo,
momentan werd ich wohl für alles die einfachste Lösung nehmen.
Was ich dann später mache, weiß ich noch nicht.
Falls Du Dir bis zu dem "später" nicht schon mit der existierenden Code-Basis andere Wege verbaut hast. Erst mal ein Provisorium zu entwickeln um dann später auf was besseres umzusteigen ist nicht nur doppelte Arbeit sondern oft auch nur sehr schwer umsetzbar. Du kennst http://www.scheissprojekt.de/hausbau.html (http://www.scheissprojekt.de/hausbau.html)? Ich hab schon mehrmals an Projekten in dieser Art mitmachen dürfen, da ist nie was gescheites bei raus gekommen (ich hab mich auch immer nach Kräften bemüht sowas schnellst möglich zu verlassen). Ich fange erst dann an mein OS zu coden wenn ich wenigstens einen guten Grobplan habe, klar laufe ich in die Gefahr das ich später bei den Details feststelle das mein Grobplan Mist ist und fange dann unter Umständen noch man ganz von vorne an, aber "No Risk - No Fun" und einen richtig tiefen Griff ins Klo hab ich persönlich noch nicht verursacht (bis jetzt zumindest).
Hier ging es aber um die Kommunikation zwischen Kernel und Userland.
Wenn der TCP-Socket (im monolithischen Kernel) einer wartenden Applikation die frisch eingetroffenen Daten überreichen will dann geht das IMHO schon vom Kernel ins Userland, der Tastatur-Treiber und viele andere mehr müssen einen ähnlichen Weg nutzen. Bei einem Micro-Kernel gehen nur die IRQs (und ein paar seltene Spezial-Sachen) vom Kernel ins Userland, alles andere ist User-2-User-Kommunikation.
Sachen, die vom Kernel ausgehen, bleiben meist auch da drin.
Dann würde der Computer "meist" nichts tun wenn irgend ein Ereignis anliegt. ;)
Ich sehe bei Callback-Funktionen immer das Problem des Programmabsturzes,
Das sehe ich ähnlich, da muss man schon recht gründlich vorgehen um da keine Stolperfallen zu bauen. Sowas meinte ich mit dem "deutlich" tieferen Griff in die Trickkiste.
.... Der zweite Syscall könnte oft sogar mehrmals benötigt werden falls der Job eben noch nicht fertig ist.
Ich sehe kein Problem darin.
Ich sehe da auch kein Problem, ich wollte diesen Umstand nur ansprechen, er kostet ein ganz klein wenig der kostbaren CPU-Zeit.
... wenn man quasiparalleles, asynchrones Verhalten in nur einem Thread erzeugen möchte
Das ist ja auch eine Kunst für sich, sowas sollte man immer vermeiden wenn es irgend möglich ist.
Aber als Haupt-API würde ich nie ein Callback-Verhalten verwenden.
Warum? Genau das habe ich in meinem OS vor.
.... wenn man maximale Performance (bei vertretbarer CPU-Last) möchte kommt man um eine asynchrone Ereignisbehandlung kaum herum.
Und? Die kann man doch ganz einfach simulieren ....
Mir ist klar das man mit asynchronen Dinge synchrone simulieren kann und auch umgedreht, aber damit kommt man nicht auf "maximale Performance (bei vertretbarer CPU-Last)". Wenn man ein annähernd optimales Ergebnis will dann muss man auch das passende Werkzeug benutzen, ansonsten kommt man zwar auch oft ins Ziel aber eben nicht mit dem Spitzenfeld sondern in der Nachzüglergruppe. Bitte nicht falsch verstehen, ich will hier keinen unnützen Effizienz-Wettbewerb starten. Aber man muss sich eben vorher überlegen was man eigentlich erwartet, ob man überhaupt ins Ziel will oder ob man auch einen der vorderen Plätze anstrebt.
und bei einem Ereignis eine Funktion in einem anderen Thread aufruft.
Wie ruft man denn Bitte eine Funktion in einem anderen Thread auf?
Grüße
Erik
-
:| :| :|
inzwischen habe ich mich entschieden, nochmal komplett von vorne anzufangen, da mein Sourcecode etwas (sehr ;)) unübersichtlich ist. Bis jetzt hab ich alles programmiert, wie dieses Haus gebaut wurde. :oops:
Es ist nur Schade, weil ich schon Grafiktreiber, Maustreiber, GDT, IDT, Tastaturtreiber usw. fertig hatte.
Was empfehlt ihr mir den jetzt für die Nachrichten? Bis jetzt kamen ja immer nur Nachteile von beiden ;)
Kann man auch beides gleichzeitig verwenden?
Naja, ich werd mich dann jetzt erstmal wieder an die GDT setzen, trotzdem Danke für die Hilfe.
EDIT: was mir gerade aufgefallen ist, wird bei euch die IP-Adresse 127.0.0.1 gespeichert?!?
-
Hallo,
inzwischen habe ich mich entschieden, nochmal komplett von vorne anzufangen
Ich hoffe das ist nicht meine Schuld, ich möchte wirklich nicht das andere Leute ihre wertvolle Arbeit wegwerfen nur weil ich mal wieder etwas zu überspitzt geschrieben habe.
Bis jetzt hab ich alles programmiert, wie dieses Haus gebaut wurde.
Dann versuche wenigstens aus deinen Fehlern zu lernen. Ich habe am Anfang meiner Programmiererei auch etliche Fehler gemacht (auch heute bin ich längst nicht perfekt und werde das mit Sicherheit auch nie sein), so wie sicher jeder hier. ;)
Was empfehlt ihr mir den jetzt für die Nachrichten? Bis jetzt kamen ja immer nur Nachteile von beiden ;)
Kann man auch beides gleichzeitig verwenden?
Du kannst natürlich beide Services parallel anbieten. Die Frage ist ob Du sowas bei einem Monolithen wirklich dringend benötigst. Fürs erste sollte es reichen wenn Deine Syscalls überwiegend blockierend arbeiten (so wie DOS), damit kommen die meisten Programme ganz gut zurecht. Wenn Du dann später was besseres dazu haben möchtest kannst Du dann (wenn es so weit ist) immer noch die konkreten Anforderungen analysieren.
Mit ein bisschen Weitsicht und Bedacht sollte es gut möglich sein auch später noch grundsätzlich neue Features zu integrieren. Leider braucht man dafür auch einiges an Erfahrung und die bekommt man nur in der Praxis also musst Du erst mal irgendwie anfangen um wenigstens vom Fleck zu kommen. Ich programmiere jetzt seit fast 20 Jahren, da fällt es leicht über "Weitsicht und Bedacht" zu lamentieren, aber Du darfst Dir sicher sein mein Anfang war auch ziemlich hart.
Das entscheidende bei den ersten Schritten ist nicht ob Du auf der Nase landest oder wie weit Du kommst, sondern ob Du was lernst!
was mir gerade aufgefallen ist, wird bei euch die IP-Adresse 127.0.0.1 gespeichert?!?
Was genau meinst Du damit?
Grüße
Erik
-
Wenn der TCP-Stack einer wartenden Applikation etwas mitteilen möchte, handelt es sich um eine Antwort auf eine von der Anwendung ausgehende Anfrage - sonst würde sie nicht warten.
Sachen, die vom Kernel ausgehen, bleiben da auch meistens drin - die Anwendung holt sich ihre Keystrokes etc. ja von dort ab. Im Extremfall (Callback) hat sie vorher halt Interesse angemeldet. Der Kernel schickt den Anwendungen niemals ungefragt etwas - davon stürzen die in der Regel ab.
Wenn ich eine Funktion in einem anderen Thread aufrufe, dann ist das quasi ein Callback. Zwischen Threads geht das mMn auch, weil die Anwendung ja zwangsweise darauf ausgelegt sein muss; als Haupt-API würde ich es wegen der komplzierten Verwendung nicht benutzen.
Oder, um es mit Fefe auszudrücken: Wenn eine API falsch zu bedienen leichter ist, als sie richtig zu bedienen, dann taugt sie nichts. Und Callback-Funktionen in jeder Lebenslage korrekt zu bedienen halte ich für schwierig.
Ein Thread, welcher in einer Endlosschleife Ereignisse abfragt und dann ein Callback aufruft ist nicht wesentlich langsamer, als ein Kernel, der diesen Callbackaufruf selbst tut. Zumal ein Threadwechsel keinen vollständigen Speicherkontext voraussetzt. Ein asynchrones Verhalten im Gegenzug synchronisieren halte ich für aufwändig, zumindest fällt mir kein schneller Weg dafür ein.
Gruß,
Svenska,
der jetzt wieder rausgeht und weitergrillt. :-P
-
Ich hoffe das ist nicht meine Schuld
Ne, ist es nicht, kannst dir meine main.h ja mal angucken: http://lf-os.googlecode.com/svn/trunk/main.h (http://lf-os.googlecode.com/svn/trunk/main.h)
Das ist wirklich das blanke Chaos (wer braucht schon mehrere Header Dateien :-P)
Also das Endergebnis ist, dass der Kernel nie etwas an die programme schicken sollte, außer in Ausnahmefällen (Runterfahren ...)??
was mir gerade aufgefallen ist, wird bei euch die IP-Adresse 127.0.0.1 gespeichert?!?
Was genau meinst Du damit?
Hier im Forum wird doch die IP Adresse gespeichert, wenn man einen Beitrag erstellt (unter dem Beitrag rechts, man sieht nur die eigenen).
Bei mir wird da 127.0.0.1 gespeichert, was ja localhost wäre :D
Achja, GDT und IDT hab ich schon gemeistert ;)
-
Bei mir wird auch nur 127.0.0.1 angezeigt - was gespeichert wird, mag vielleicht noch was anderes sein (bezweifel ich aber).
Ich würde das Endergebnis so umformulieren: Der Kernel schickt nie etwas an unvorbereitete(!) Programme. Also quasi ohne Anforderung von diesen. Signale sind da eine Ausnahme und werden zumindest unter Unix auch so behandelt. Mit der WinAPI kenne ich mich allerdings nicht aus, nur, dass sie stellenweise ähnlich (Sockets) und stellenweise völlig anders (ereignisbasierte Callback-GUI) funktioniert. :-)
Gruß,
Svenska
-
littlefox kannst du mir deinen jetztigen code bitte als zip bei rapidshare hochladen oder mir eine andere downloadquelle nennen. Ich würde mir den nämlich gerne mal ansehen. Vor allem, wie d das mit den treibern gemacht hast.
Programm Noob
-
reicht dir als Downloadquelle SVN?
http://lf-os.googlecode.com/svn/trunk (http://lf-os.googlecode.com/svn/trunk)
Die Treiber sind bei mir aber in den Kernel mit rein kompiliert.
EDIT: Bei mir wird auch nur 127.0.0.1 angezeigt - was gespeichert wird, mag vielleicht noch was anderes sein (bezweifel ich aber).
Vor dem Update gings ;)
Also sollen sich die Programme alles was Sie brauchen selber beim Kernel erfragen. Sehe ich das richtig?
Wie werden die Signale bei den UNIXoiden den umgesetzt?
-
Klar reicht SVN.
Meine Treiber sind momentan auch im Kernel. aber mir geht es drum wie du die aufrufst und wie du deinen VGA Treiber programmierst. Vielen Dank
Programm Noob
-
wobei ich zugeben muss, dass ich den VGA Treiber auch gefunden habe :oops:
die primitives.c ist aber selbst geschrieben ;)
gefunden hab ich den VGA treiber hier:
http://files.osdev.org/mirrors/geezer/osd/graphics/modes.c (http://files.osdev.org/mirrors/geezer/osd/graphics/modes.c)
-
Hallo,
Ich würde das Endergebnis so umformulieren: Der Kernel schickt nie etwas an unvorbereitete(!) Programme. Also quasi ohne Anforderung von diesen.
Dem kann ich ohne Einschränkungen zustimmen!
Das könnte man sogar auf ein Micro-Kernel-OS erweitern "Prozesse senden sich gegenseitig nur das was der Empfänger auch explizit erwartet". Ein Service ist für alle offen aber nur für bestimmte Anfragen und ein Client erwartet natürlich nur die zugehörigen Antworten auf seine Anfragen und ein Treiber bekommt vom Kernel auch nur die IRQ-Messages für die er sich registriert hat.
Signale sind da eine Ausnahme ...
Da der Prozess ja erst mal einen Signal-Handler registrieren muss bevor er was empfangen kann würde ich sagen das auch (UNIX-)Signale keine Ausnahme sind. Es könnte gut sein das die libc bei einem typischen *NIX sowas schon per Default immer tut und dann bloß das weitergibt was der User-Code tatsächlich will (über eine libc-API).
So wie ich das sehe gibt es genau 3 Möglichkeiten Daten vom monolithischen Kernel in eine User-Space-Applikation zu bekommen:
- Das Programm benutzt einen blockierenden Syscall (open/read/write/close/GetMyNextMessage/WaitForEvent/...) und wartet immer bis es etwas bekommt. Die allermeisten (unparallelisierten) Programme dürften damit gut zurecht kommen und es ist leicht im Kernel zu implementieren. Die Latenz ist erstmal recht gering (das Programm wartet ja bereits wenn was kommt), trotzdem wird man damit keine Performance-Wunder erleben. Wenn ein Programm mehrere Dinge parallel machen will dann muss es eine entsprechende Anzahl an Threads benutzen (z.B. ein HTTP-Server müsste für jede ankommende TCP-Connection einen neuen Thread starten), was natürlich nur für eine überschaubare Menge an Threads eine intelligente Lösung darstellt.
- Das Programm benutzt asynchrone nichtblockierende Syscalls, es startet also eine Aktion und kann dann erstmal weiterarbeiten und weitere Jobs starten. Später fragt es nach dem Status der Jobs und entscheidet dann was es als nächstes tut (ein HTTP-Server könnte z.B. den TCP-Socket als nächstes mit neuen Daten versorgen welcher am wenigsten Daten im Sende-Puffer hat). Die Latenz ist höher weil das Programm ja nicht ständig fragt was fertig ist aber dafür kann man (mit vertretbaren Aufwand) mit einem Thread ne ganze Menge Jobs kontinuierlich parallel am laufen halten so das man trotzdem eine ziemlich gute Gesamt-Performance erreicht. Solche Mechanismen bedeuten im Kernel und auch im Programm eine erhebliche Menge an (Programmier-)Mehraufwand und eine deutlich bessere Trickkiste.
- Das Programm könnte sich über erledigte Jobs per Call-Back benachrichtigen lassen, die Jobs werden wie zuvor über nichtblockierende Syscalls gestartet. Hierbei ist erstmal wieder die Latenz recht kurz da der Call-Back-Handler ja immer (fast) sofort losläuft. Die CPU-Last scheint auch kleiner als zuvor zu sein weil ja das Programm nicht wiederholt nachfragen muss ob schon irgendwas fertig ist (es fallen also weniger Syscalls an) und der Call-Back-Handler sich nur um die Jobs kümmert die tatsächlich Aufmerksamkeit benötigen (es müssen auch keine Listen mit anhängigen Jobs durchgearbeitet werden um einen zu finden für den was zu tun ist). Die benötigten Mechanismen sind nochmal deutlich komplexer und es gibt einige zusätzliche Stolperfallen (wenn z.B. ein Programm in dem Moment gekillt wird wo der Kernel gerade einen Call-Back starten will, bei Multi-Core ja problemlos möglich) die wiederum eine nochmals bessere Trickkiste und eine sehr saubere und gründliche Spezifikation (in der auch wirklich alle Sonderfälle geregelt sind) benötigen. Ein Nachteil ist das der Call-Back-Handler oft nicht alles machen kann (so wie ein IRQ-Handler auch). Zusätzlich benötigt der Call-Back-Handler einen Kontext im Programm in dem er läuft, dazu wird normalerweise einer der Programm-Threads unterbrochen wie bei den klassischen UNIX-Signalen.
Ein Ausweg wäre es wenn der Kernel für jeden Call-Back einen neuen (leeren) Thread im Programm erstellt, so ein Thread hätte dann alle Möglichkeiten im Programm und unterbricht auch nicht einen anderen Thread bei einer wichtigen Tätigkeit. So etwas wurde aber wohl noch nie gemacht, ich habe für dieses PopUp-Threads-Konzept zwar ein paar wenige theoretische Überlegungen aber keine praktischen Beispiele gefunden. Ich möchte diesen Weg in meinem Micro-Kernel-OS (für die Services) gehen, einer muss ja den Anfang machen, bei einem Micro-Kernel hat das auch eher Vorteile weil dort (fast) alle Services nicht direkt über Syscalls sondern über RPC/IPC abgewickelt werden.
Wenn ich eine Funktion in einem anderen Thread aufrufe, dann ist das quasi ein Callback.
Das habe ich immer noch nicht verstanden. Wie ruft man eine Funktion in einem anderen Thread auf? Der Kernel kann den Thread unterbrechen und dort einen Signal-Handler aufrufen (eben wie die CPU bei einem IRQ) so als würde der unterbrochene Thread einen CALL machen, aber zwischen zwei Threads in einem User-Mode-Prozess geht das IMHO nicht.
als Haupt-API würde ich es wegen der komplzierten Verwendung nicht benutzen.
Das kann man ja hinter der libc verbergen und für ein einfaches (open/read/write/close) wird man sicher keine Call-Backs benutzen, da sind blockierende Syscalls (beim Monolithen) oder synchrones RPC (beim Micro-Kernel) deutlich besser, Call-Backs sind nur dann interessant wenn man mit wirklich vielen parallel Jobs arbeitet oder für die Bereitstellung von Services in der Personality von einem Micro-Kernel-OS.
Grüße
Erik
-
Okay, mit den Signalen stimmt auch wieder.
Du hast irgendwo in deiner Auflistung das normale asynchrone I/O vergessen, wo also read()/write()/... zwar blockieren, aber es einen speziellen Syscall gibt, der vorher nachfragt, ob diese Syscalls blocken würden. Das heißt, der Programmfluss ist trotzdem synchron, aber es wird nicht gewartet. Das programmiert sich leichter. Beispiele dafür sind select(), poll(), epoll (Linux) und kqueue (*BSD). Als Grund-API hast du dann also nur blockierende Syscalls, kannst damit aber trotzdem recht einfach asynchrones I/O starten.
Dann brauchst du nicht für jede Anfrage einen Thread, was nicht skaliert, hast aber die Freiheit, beliebig viele Worker-Threads zu verwenden. Ein Thread, der im Prinzip nur auf Ereignisse wartet und dann beliebig verteilt.
Was die Threads angeht, da diese ja in einem gemeinsamen Speicherkontext laufen, müsste man da eigentlich auch einfach hinspringen können - ich dachte da eher an ein Syscall in den Kernel (bzw. die libc), welche den anderen Thread beeinflusst.
Gruß,
Svenska
-
select() usw. dürften Eriks Option 2 entsprechen.
Und asynchrones I/O ist das strenggenommen auch nicht: Du blockierst zwar nicht, wenn es nichts zu lesen gibt, aber wenn du dann mal das read() startest, läuft das eigentliche Lesen synchron ab. Dürfte bei Sockets natürlich in der Praxis nicht wirklich einen Unterschied machen, weil es wenig genug Daten sind, die außerdem nur im Speicher ein bisschen hin- und hergeschoben werden; und beim Zugriff auf normale Dateien ergibt select() nicht besonders viel Sinn.
-
Hallo,
... wo also read()/write()/... zwar blockieren, aber es einen speziellen Syscall gibt, der vorher nachfragt, ob diese Syscalls blocken würden.
Das ist asynchrones I/O, man initiiert einen Job und prüft dann ob schon Daten da sind damit man beim abholen eben nicht blockiert. Das ist ganz klar Option 2.
select() usw. dürften Eriks Option 2 entsprechen.
Sicher? Gibt es zu dem select() irgendwo eine verständliche Dokumentation? Ich hab leider nichts brauchbares gefunden.
Was die Threads angeht, da diese ja in einem gemeinsamen Speicherkontext laufen, müsste man da eigentlich auch einfach hinspringen können
Nur weil sich zwei Threads einen gemeinsamen Speicherkontext teilen ist der Gesamt-Kontext, dazu gehören die Register genauso wie der Stack, nicht gleich identisch. Man kann nicht mit einem CALL oder vergleichbares eine Funktion im Kontext eines anderen Threads aufrufen.
ich dachte da eher an ein Syscall in den Kernel (bzw. die libc), welche den anderen Thread beeinflusst.
So wie bei den (UNIX-)Signalen? Das würde ich meinen Worker-Threads nicht antun wollen, die sind mit ihrem letzten Job eventuell noch nicht fertig.
Grüße
Erik
-
select() ist genau diese Funktion, die vorher prüft, ob read blockieren würde. select funktioniert so, dass du drei verschiedene fd_sets (also Mengen von Filedeskriptoren) angibst: Einmal für lesen, einmal für schreiben und einmal für Fehler. select() wirft dann diejenigen Deskriptoren raus, die nicht bereit sind zum lesen/schreiben bzw. die keinen Fehler haben. Wenn keiner der Filedeskriptoren bereit ist, blockiert select. Für das Blockieren gibt es nochmal einen Parameter, das einen Timeout angibt.
Und nein, es ist nicht asynchrones I/O, weil die Übertragung der Daten (also das eigentliche read/write) nicht im Hintergrund abläuft.
-
Hallo,
select() ist genau diese Funktion, die vorher prüft, ...
Ahja, jetzt bin ich wieder etwas schlauer, Danke. Warum können andere Dokus nicht auch erst mal mit so einem kompakten Dreizeiler anfangen anstatt gleich mit Details um sich zu werfen so das man keine Chance hat das grobe Ganze zu erkennen? (darüber wie ich das in einer libc umsetzen kann will ich jetzt lieber noch nicht nachdenken)
Und nein, es ist nicht asynchrones I/O, weil die Übertragung der Daten (also das eigentliche read/write) nicht im Hintergrund abläuft.
Ich würde schon sagen das es sich dabei um asynchrones I/O handelt, auch wenn es nicht ganz der Definition entspricht. Für TCP betrachtet übertragen read und write doch nur zwischen einem User-Puffer und einem Kernel-Puffer (simples memcpy bei einem Monolithen) die eigentliche Datenübertragung von/nach draußen passiert dann doch echt asynchron. Bei Dateien muss man natürlich trotzdem warten bis die HDD die Daten gelesen/geschrieben hat (wenn wir mal SW-Caches außer acht lassen) aber da macht select() auch nur extrem wenig Sinn. Der einzigst verbleibende Unterschied ist das bei richtigem asynchronem I/O auch das memcpy asynchron im Hintergrund abläuft, das kann natürlich ein paar Auswirkungen für das Programm haben.
Vielleicht können wir uns auf "fast asynchrones I/O" einigen? ;)
Das select() ist auf jeden Fall unendlich viel dichter an Option 2 als an Option 1. (da fällt mir auf es gibt hier keine nummerierten Aufzählungen, dafür ne Menge unnützer Firlefanz)
Grüße
Erik
-
Wenn du Dateizugriffe multiplexen willst, kann es schon sinnvoll sein; beispielsweise liest du von einer modernen SSD mit ordentlich Datenrate und einer antiken Festplatte je ein GB. Für Schreiben gilt entsprechendes, für Sockets ebenfalls.
Während die alte Platte nicht hinterherkommt, blockt dein Programm - der Programmfluss ist synchron. Also lässt du besser immer nur ein paar Blöcke lesen (bzw. rufst ein readahead() auf, welches nicht blockiert) und wartest, bis die Daten gelesen sind, ehe du dann dein richtiges read() aufrufst. In dem Moment stört das nicht weiter, da dein readahead() garantiert, dass die Daten im Plattencache liegen und das read() nur zu einem memcpy() verkommt. Im Allgemeinen ist das nämlich nicht der Fall. Bei Sockets ist das noch schlimmer, weil die Geschwindigkeiten viel geringer sind.
Die Erweiterungen (select() und poll() sind umständlich zu handhaben und können allein von der API her nicht besser als linear skalieren) bezeichnet man als High-Performance AIO (asynchrones I/O), daher würde ich select()/poll() ebenfalls dem AIO zuordnen, obwohl der Programmfluss eines solchen Programms synchron ist. Übrigens skaliert unter Linux ein fork()-basierter Webserver besser als ein auf poll() basierender... vgl. Fefe :-)
Gruß,
Sebastian
-
Hm, mit readahead habe ich noch nicht gemacht, aber bist du dir sicher? Die Manpage sagt nämlich:
readahead() blocks until the specified data has been read.
-
Hallo,
Wenn du Dateizugriffe multiplexen willst, kann es schon sinnvoll sein; beispielsweise liest du von einer modernen SSD mit ordentlich Datenrate und einer antiken Festplatte je ein GB. Für Schreiben gilt entsprechendes, für Sockets ebenfalls.
Das ist dann aber klassisches asynchrones I/O, man initiierst mehrere Jobs und schaut wie weit die jeweils sind um immer an der wichtigsten Stelle (non-blocking) zu arbeiten. Aber gerade dieses select() würde im Szenario mit den Dateien von unterschiedlich schnellen HDDs nichts helfen, wenn ich das richtig verstanden habe prüft select() nur ob noch Bytes da sind (was bei Dateien wo der aktuelle File-Pointer noch nicht am Ende der Datei ist ja immer stimmt), bei Sockets kommen von außen immer wieder mal neue Bytes dazu im Gegensatz zu Dateien die nicht einfach so von alleine wachsen.
Übrigens skaliert unter Linux ein fork()-basierter Webserver besser als ein auf poll() basierender
Bis zu wie vielen gleichzeitigen Anfragen? Ich denke mal spätestens beim 100000-ten fork() hast Du auch auf nem dicken Server Probleme. Klar ist fork() relativ kostengünstig (wobei ich hoffe auf meiner segmentierten Plattform das CreateThread() deutlich billiger zu bekommen) aber man braucht trotzdem einiges an Speicher (für die vielen Stacks und Verwaltungsstrukturen).
Grüße
Erik
-
Hm, mit readahead habe ich noch nicht gemacht, aber bist du dir sicher? Die Manpage sagt nämlich:
readahead() blocks until the specified data has been read.
Akzeptiert, dann geht das in der Form nicht. Müsste man dann selbst implementieren. :-)
Aber gerade dieses select() würde im Szenario mit den Dateien von unterschiedlich schnellen HDDs nichts helfen, wenn ich das richtig verstanden habe prüft select() nur ob noch Bytes da sind (was bei Dateien wo der aktuelle File-Pointer noch nicht am Ende der Datei ist ja immer stimmt), bei Sockets kommen von außen immer wieder mal neue Bytes dazu im Gegensatz zu Dateien die nicht einfach so von alleine wachsen.
Korrekt, allerdings könnte man ja ein Flag dazu verwenden.
Grundgedanke ist halt, dafür einen Thread zu basteln, der im Hintergrund die benötigten Daten liefert.
Übrigens skaliert unter Linux ein fork()-basierter Webserver besser als ein auf poll() basierender
Bis zu wie vielen gleichzeitigen Anfragen? Ich denke mal spätestens beim 100000-ten fork() hast Du auch auf nem dicken Server Probleme. Klar ist fork() relativ kostengünstig (wobei ich hoffe auf meiner segmentierten Plattform das CreateThread() deutlich billiger zu bekommen) aber man braucht trotzdem einiges an Speicher (für die vielen Stacks und Verwaltungsstrukturen).
Ich beziehe mich auf diese PDF (http://bulk.fefe.de/scalable-networking.pdf), die ist allerdings von 2003. Der Kernel hat Threading noch nicht so lange, das wurde ewig in der glibc implementiert, von daher ist die Ansage nicht mehr ganz aktuell.
;-)
Gruß,
Svenska