Wann den Stack eines Threads löschen?



  • Hallo Leute,

    bevor ich mich mit Linux beschäftigt habe, war auch ich der dunklen Seite der Macht zugetan, sprich MS Windows! Zwar fehlt einem dort soetwas wie fork, aber dafür hat mir dort das Threading sehr gut gefallen (bzgl API). Was mir daran gut gefällt ist, dass es seeeehr einfach zu verwenden ist, pthreads sind nicht wirklich kompliziert, aber IMHO schon etwas umständlicher.

    Darum habe ich mich von 6 Monaten mal dran gemacht, eine Cross-Plattform-Bibliothek zur Systemprogrammierung zu entwickeln (Threads, Semaphoren, Netzwerk ...), mit dem obersten Ziel, dass sie sehr intuitiv und einfach zu bedienen sein soll.

    Inzwischen ist sie zum größten Teil fertig, aber bei den Threads möchte ich noch ein paar kosmetische Korrekturen vornehmen. Dabei habe ich folgendes Problem:
    Einen Thread erzeugt man ja bekannter massen mit clone(), wobei man auch einen Zeiger auf den Stack übergeben muss, der zuvor (z.B. mittels mmap) angelegt wurde (kurze Zwiscehnfrage: was ist der Vorteil von mmap gegenüber malloc?). Nun sollte der Stack auch wieder gelöscht werden, wenn der Thread beendet wurde. Bisher habe ich dies so gelöst, dass ich für das Signal "SIGCHLD" ein Signalhandler eingerichtet habe, der alle Threadinformationen, die u.a. den Stackpointer enthalten, in eine Queue einreiht und mittels Semaphoren dem Threadleader mitteilt, dass da etwas neues in der Queue ist. Der Threadleader löscht dann den Stack.

    Die Sache hat nun einen kleinen Schönheitsfehler: Damit ich überhaupt das Signal SIGCHLD ebfangen kann, müssen die alle Threads echte "Kinder" vom Threadleader. Als erstes erzeuge ich den Threadleader mit den folgenden Cloneflags:

    SIGCHLD | CLONE_FS | CLONE_FILES | CLONE_PTRACE | CLONE_VM | CLONE_SIGHAND
    

    Von dort aus werden dann die eigentlichen Threads erzeugt (ein Thread der die eigentliche "Main-Funktion" vom Programm startet und von dort aus können dann Threads erzeugt werden). Diese Threads werden zusätzlich noch mit dem Flag CLONE_PARENT erzeugt - somit sind alle Threads Kinder vom Threadleader und er enthält das SIGCHLD Signal.

    Dieses Vorgehen hat allerdings zur Folge, dass die Threads keine echte Threadid haben (siehe gettid), sondern über ihre Prozessid identifiziert werden. Ausserdem sieht es unschön aus, wenn mir in der Shell "ps" X Prozesse anzeigt, obwohl es nur ein Prozess mit X Threads ist.

    Aber es geht ja auch anders, indem man kein CLONE_THREAD verwendet: Threads haben alle die selbe ProzessID, aber nun eine ThreadID und "ps" zeigt mir (ohne spezielle Parameter) nur einen Prozess an. Leider ergibt sich dadurch ein Problem. Es wird natürlich kein SIGCHLD-Signal mehr gefeuert.

    Aber wann kann ich den Stack löschen, wenn ich nicht mitbekomme, wann der Thread "BEENDET IST". Ich habe schon alles probiert und Google konnte mir auch nicht helfen. Beim Beenden des Threads (atexit) dies dem Threadleader mitzuteilen bringt auch nichts, denn wenn der Scheduler genau dann zum Threadleader wechselt, löscht er den Stack und der Thread hat kurz vor seinem Ableben den Stack unter den Füssen entzogen bekommen. Ich habe leider auch keine Funktion wie "waittid" gefunden, die mir weiterhelfen würde (mit waitpid funktionert es nicht). Auch der PThread-Code hilft mir nicht weiter, weil ich den entsprechenden Teil nicht verstehe.

    Hat von euch jemand eine Idee? Ich weiss echt nicht mehr weiter, möchte aber gerne die vielen PID's vermeiden.

    Danke und Gruß,

    Sven



  • clone nimmt man eigentlich nicht. clone ist ein Linux spezifischer Aufruf. Schau dir lieber man: pthread_create und co an.



  • Wenn du schon eine neue Thread API baust, dann solltest du dir die NPTL Sourcen anschauen und verstehen, wie das bei PThread funktioniert.

    Ich verstehe aber nicht, warum du eine eigene Thread API bauen möchtest. PThread ist eigentlich das Beste, was man erhalten kann. Vor allem im Vergleich zu Pthread lese ich immer wieder, wie "brain-dead" das Windows Threading ist.

    Eine Diskussion mit vielen Verweisen auf andere Quellen ist zum Beispiel:

    http://softwareforums.intel.com/ids/board/message?board.id=42&message.id=96



  • kingruedi schrieb:

    clone nimmt man eigentlich nicht. clone ist ein Linux spezifischer Aufruf. Schau dir lieber man: pthread_create und co an.

    Klar ist Clone Linux-Spezifisch.Meine Biblothek soll aber PThread ersetzen und nicht darauf aufbauen. Aber auch PThread ist nur eine Bibliothek, die unter Linux selber Clone verwendet.

    Ponto schrieb:

    Wenn du schon eine neue Thread API baust, dann solltest du dir die NPTL Sourcen anschauen und verstehen, wie das bei PThread funktioniert.

    Naja, ich bin nun nicht gaaanz blöd 😉 und verstehe auch die Source zum größten Teil. Und schliesslich funktioniert meine Bibliothek auch. Es geht mir dabei nur um deren Perfektion. Bei meiner Rechersche habe ich mich auch mit den PThread Sourcen beschäftigt. Es gibt übrigens mehrere Implementierungen davon, einige lösen dies so, wie ich es gemacht habe über den Signalhandler und erzeugen jeweils eine eigene PID für jeden Thread. Andere (u.a. NPTL und NGPT) machen dies so, wie ich es auch haben will. Bei diesen Sourcen ist es aber nur eben dieser Teil des Stack löschens, den ich nicht verstehe. Die arbeiten dort mit "setjmp, longjmp, ..." usw in dem entsprechenden Teil - aber da kann ich nicht folgen (manpages habe ich natürchlich gelesen).

    Ich habe gehofft, dass jemand zu meiner Frage eine Lösung weiss?
    Oder vielleicht kann mir jemand den entsprechenden Code der PThread Bib's (die das Flag CLONE_THREAD verweden) erklären.

    Danke und Gruß,
    Sven



  • Es ist doch ganz einfach: Anstatt bei der Thread-Erzeugung die Adresse des User-Codes zu uebergeben, uebergibst Du die Adresse auf eine eigene Routine, an deren Ende Du Code fuer die Benachrichtigung des Thread-Leaders machst:

    void* thr_main( void* arg ) {
       mythrinfo* thrinf = (mythrinfo*) arg;
       void* retcode = thrinf->usercode( thrinf->userarg );
       thr_sigend( thrinf );
       return retcode;
    }
    


  • Hallo "Power Off",
    erst einmal schön, dass sich jemand meinem Problem annimmt. Ich weiss nicht, ob ich Deinen Code richtig verstanden habe. Ich versuche erst mal zu erläutern, wie ich ihn verstehe:

    void* thr_main( void* arg ) {
       mythrinfo* thrinf = (mythrinfo*) arg;
       void* retcode = thrinf->usercode( thrinf->userarg );
       thr_sigend( thrinf );
       return retcode;
    }
    

    Also, thr_main ist nun meine initiale Threadfunktion und von der aus wird dann erst die eigentliche Funktion aufgerufen (usercode). Gut, soetwas mache ich natürlich eh schon in meiner Bibliothek.
    Wenn die Funktion thrinf->usercode fertig ist, teilt thr_main via thr_sigend dem Threadleader mit, dass der Thread beendet wurde und der Stack gelöscht werden kann. Anschliessend gibt thr_main den Rückgabewert von "usercode" zurück.

    Richtig verstande? Ok, dann gibt es immer noch das Problem, dass der Threadleader nicht garantiert zur richtigen Zeit den Stack löscht, denn wenn thr_sigend aufgerufen wurde, ist zwar die eigentliche Funktion (usercode) fertig, aber der Thread noch nicht, weil thr_main ja noch den Stack benötigt. Auch wenn es nur noch ein return ist, so handelt es sich bei diesem Sachverhalt nicht um eine Kleinigkeit. Obwohl mir dies schon vorher klar war, habe ich aus Spass genau das schonmal probiert. Und das Programm stürtzt damit nicht ab und zu mal ab, wenn ein Thread fertig ist, sondern immer, d.h. der Threadscheduler haut immer genau zwischen "thr_sigend" und "return" zu (ist eigentlich auch klar, wenn man sich etwas mit gängigen Strategien für Prozess- oder Threadschedulern beschäftigt... führt aber zu weit).

    Ja, ist aber hoffentlich rüber gekommen, dass ich damit der Funktion thr_main (die ja schon auf dem neuen Stack operiert) den Stack unter den Füssen weg ziehe. Kein Stack -> keine Rücksprungadresse bei return!

    Immer noch dankbar für Anregungen!
    Cu,
    Sven



  • Ach so ja, das mit dem Stack war das Hauptthema Deines Threads, hatte ich uebersehen.
    Na ja, dann schalt halt vor der Benachrichtigung den Stack um, indem Du das Register ESP setzt.
    Einen Pointer auf diesen Cleanup-Stack kannst Du ja in "thrinfo" speichern.

    Hat der Compiler die uebliche "push ebp", "mov ebp,esp"-Sequenz am Anfang der Thread-Funktion
    erzeugt, dann steht bei "-8[ebp]" die Ruecksprungadresse, die Du auslesen und auf den temporaeren
    Stack legen musst. Von dort aus rufst Du die Benachrichtigungsfunktion auf.

    Also z.B.:

    void* thr_main( void* arg ) {
       mythrinfo* thrinf = (mythrinfo*) arg;
       void* retcode = thrinf->usercode( thrinf->userarg );
       void* stk = thrinf->tempstk;
       _asm {
          mov eax,retcode     ; Rueckgabewert nach EAX
          mov edx,thrinf      ; thrinf-Zeiger nach EDX
          mov esp,stk         ; Stackpointer aendern
          push -8[ebp]        ; Ruecksprungadresse auf den Stack legen
          push eax            ; Rueckgabewert auf den Stack legen
          push edx            ; thrinf als Parameter fuer thr_sigend() auf den Stack legen
          call _thr_sigend    ; thr_sigend() aufrufen
          add esp,4           ; Parameter vom Stack entfernen
          pop eax             ; Return-Wert wiederherstellen
          ret                 ; Rueckkehr von thr_main() (auf temporaerem Stack, Vorsicht!)
       }
    }
    

    Alternativ kannst Du auch in eine Beendigungsroutine springen, je nachdem wie Deine Library
    funktioniert.

    EDIT: Besorg Dir mal 'ne Liste von Linux-Systemaufrufen (syscalls). Vielleicht gibt's
    eine Art Gegenstueck zu clone() mit dem Du den Thread ausdruecklich beenden kannst.
    Dann kannst Du diesen Syscall als letzten Befehl ausfuehren (nach der Stackumschaltung
    und Benachrichtigung des Thread-Leaders).



  • Jo, diese PThread-Implementierungen machen das mit dem Stackwechsel soweit ich das verstanden habe über diese "setjmp" und "longjmp" Aufrufe. Dazu habe ich leider die Manpages nicht verstanden... 😡 Aber wäre gut die zu verstehen, denn bei Deinem Code bin ich neben Linux auch noch auf i386-Architektur fixiert. Aber irgend wie so werde ich es vielleicht machen.

    Aber Dein Tipp mit dem Thread töten gefällt mir. Es gibt für Thread nämlich "tkill". Ich werde am Ende den Listener einfach benachrichtigen und den Thread dabei schlafen legen. Der Listener tötet ihn und löscht den Stack! Gefällt mir! Mal sehen ob die Theorie sich auch umsetzen lässt.

    Vielen Danke, hat mir wahrscheinlich sehr geholfen.

    Gruß,
    Sven


Anmelden zum Antworten