Verständnisfragen zum Linux v0.01 Source



  • Hallo,

    ich beschäftige mich in letzter Zeit viel mit Assembler und um OS-Development. Ziel des ganzen ist der Lernerfolg. Aktuell bin ich beim Thema Scheduling und habe mir dazu auch den Linux v0.01 Source angesehen, dabei sind mir ein paar Fragen aufgekommen:

    #define switch_to(n) {\
    struct {long a,b;} __tmp; \
    __asm__("cmpl %%ecx,_current\n\t" \
    	"je 1f\n\t" \
    	"xchgl %%ecx,_current\n\t" \
    	"movw %%dx,%1\n\t" \
    	"ljmp %0\n\t" \
    	"cmpl %%ecx,%2\n\t" \
    	"jne 1f\n\t" \
    	"clts\n" \
    	"1:" \
    	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    	"m" (last_task_used_math),"d" _TSS(n),"c" ((long) task[n])); \
    }
    

    Hier sieht man Inline-Assembler, wobei unten 5 Input-Operanden angegeben werden, im ASM aber nur 3 genutzt werden. Übersehe ich hier etwas, oder werden die letzten beiden wirklich ignoriert?
    Dann wird gleich ein Struct erstellt und die beiden long-Variablen übergeben, der ljump geht z.b. auf %0. Ist %0 nicht *&__tmp.a?
    Außerdem verstehe ich auch das &__tmp.a nicht. Ist das nicht der Inhalt () der Adresse (&) von __tmp.a, also im Endeffekt das gleiche wie "__tmp.a"?

    Für die letzte Frage noch ein bisschen mehr Code:

    void schedule(void)
    {
    	int i,next,c;
    	struct task_struct ** p;
    
    /* check alarm, wake up any interruptible tasks that have got a signal */
    
    	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
    		if (*p) {
    			if ((*p)->alarm && (*p)->alarm < jiffies) {
    					(*p)->signal |= (1<<(SIGALRM-1));
    					(*p)->alarm = 0;
    				}
    			if ((*p)->signal && (*p)->state==TASK_INTERRUPTIBLE)
    				(*p)->state=TASK_RUNNING;
    		}
    
    /* this is the scheduler proper: */
    
    	while (1) {
    		c = -1;
    		next = 0;
    		i = NR_TASKS;
    		p = &task[NR_TASKS];
    		while (--i) {
    			if (!*--p)
    				continue;
    			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
    				c = (*p)->counter, next = i;
    		}
    		if (c) break;
    		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
    			if (*p)
    				(*p)->counter = ((*p)->counter >> 1) +
    						(*p)->priority;
    	}
    	switch_to(next);
    }
    
    void do_timer(long cpl)
    {
    	if (cpl)
    		current->utime++;
    	else
    		current->stime++;
    	if ((--current->counter)>0) return;
    	current->counter=0;
    	if (!cpl) return;
    	schedule();
    }
    
    _timer_interrupt:
    	push %ds		; save ds,es and put kernel data space
    	push %es		; into them. %fs is used by _system_call
    	push %fs
    	pushl %edx		; we save %eax,%ecx,%edx as gcc doesn't
    	pushl %ecx		; save those across function calls. %ebx
    	pushl %ebx		; is saved as we use that in ret_sys_call
    	pushl %eax
    	movl $0x10,%eax
    	mov %ax,%ds
    	mov %ax,%es
    	movl $0x17,%eax
    	mov %ax,%fs
    	incl _jiffies
    	movb $0x20,%al		; EOI to interrupt controller #1
    	outb %al,$0x20
    	movl CS(%esp),%eax
    	andl $3,%eax		; %eax is CPL (0 or 3, 0=supervisor)
    	pushl %eax
    	call _do_timer		; 'do_timer(long CPL)' does everything from
    	addl $4,%esp		; task switching to accounting ...
    	jmp ret_from_sys_call
    

    Der _timer_interrupt ist Logischerweise der IRQ0-Handler. So wie ich das sehe läuft es so ab:
    - Timer-Interrupt trifft ein
    - _timer_interrupt pusht ein wenig Zeugs, macht ein paar dinge und callt dann _do_timer
    - do_timer testet dann wieder ein wenig rum und wenn alles glatt läuft callt er schedule()
    - schedule findet dann einen neuen task und führt das (eingebundene) switch_to aus
    - dort wird dann wieder ein wenig Überprüft und dann ein long-jump zu %0 gemacht

    Ich nehme mal an, dass in %0 dann auf irgendeine Weise CS:IP des anderen Tasks stehen und dann eben dorthin gesprungen wird, um diesen weiter auszuführen. Allerdings würde es dann nie wieder zurückkommen, es würde nie der iret kommen, beim nächsten Timer-Interrupt dann das gleiche Spiel bis irgendwann der Stack übergelaufen ist. Vermutlich habe ich nur ein Verständnisproblem was in dem kurzen InlineASM wirklich passiert, aber das uninitialisierte und ungenutzte __tmp verwirrt mich etwas...



  • tkausl schrieb:

    #define switch_to(n) {\
    struct {long a,b;} __tmp; \
    __asm__("cmpl %%ecx,_current\n\t" \
    	"je 1f\n\t" \
    	"xchgl %%ecx,_current\n\t" \
    	"movw %%dx,%1\n\t" \
    	"ljmp %0\n\t" \
    	"cmpl %%ecx,%2\n\t" \
    	"jne 1f\n\t" \
    	"clts\n" \
    	"1:" \
    	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    	"m" (last_task_used_math),"d" _TSS(n),"c" ((long) task[n])); \
    }
    

    Hier sieht man Inline-Assembler, wobei unten 5 Input-Operanden angegeben werden, im ASM aber nur 3 genutzt werden. Übersehe ich hier etwas, oder werden die letzten beiden wirklich ignoriert?

    "d" = d-Register, "c" = c-Register, wie hier unter I386 zu lesen:
    https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html#Machine-Constraints
    %ecx ist also task[n] , %dx ist _TSS(n) .



  • hab mal ein Mini OS geschrieben, ist aber schon lange her. Multitasking im x86 Real Mode hab ich wie folgt gelöst:

    Mein Timer Interrupt Handler wird jede ms aufgerufen.
    Beim Eintritt in meinen Handler sieht der Stack wie folgt aus:

    Stack beim Eintritt in Interrupt Handler
        IP: SP+0
        CS: SP+2
        FLAGS: SP+4
        SP: SP+6
        SS: SP+8
    

    Der interessante Teil ist IP und CS: dort ist codiert, wo nach Beendigung des Handlers (also durch iret) die Programmausführung weitergeht.

    Ich habe alle 100ms einen Threadwechsel durchgeführt. Dazu habe ich den gespeicherten IP am Stack getauscht.
    Nach dem iret kommt nun der "alte" Thread wieder dran. Den IP vom nun schlafenden Thread speichere ich, sodass ich diesen später wieder reinwechseln kann.

    Im Protected Mode gibts da schönere Möglichkeiten, ich glaub Task State Register oder so ähnlich nennt sich das. Das zeigt auf eine Datenstruktur wo alles nötige für den Threadwechsel drinnensteht.
    Nach dem iret geht die Programmausführung eben dort weiter.
    Der Stack geht auch nicht über, da ja nach dem iret der Stack wieder abgebaut wird. Außerdem hat jeder Thread seinen eigenen Stack - dieser muss natürlich auch bei jedem Wechsel angepasst werden - so wie viele andere Dinge auch (z.B. die Register).

    Genauer kann ichs jetzt leider auch nicht erklären - ist schon zu lange her.
    Habe damals das Buch "Understanding the Linux Kernel" durchgearbeitet, das hat sehr ausführlich den Kernel 2.6 erklärt - auch mit vielen Codebeispielen.


Anmelden zum Antworten