Linux, Version 3.7, zufällige Segfaults in libpthread und libc Version 2.13



  • OK, da habe ich sie auch gefunden:

    ~# file /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.13.so
    /usr/lib/debug/lib/x86_64-linux-gnu/ld-2.13.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0xDEADBEEF, not stripped
    

    Aber funktionieren will Valgrind immer noch nicht. Generell scheint es recht viele Probleme mit Valgrind zu geben, weil dieses geinlinete Funktionen, die bei hohen Optimierungsstufen erscheinen (und die ich nicht einmal verwende 🤡, nicht korrekt erkennen will.

    Ich habe jetzt in den Code eine eigene strlen-Routine, mstrlen , reingepackt, neukompiliert, und ich erhalte immer noch die gleiche Fehlermeldung. (Ja, die Routine ist korrekt, wenn auch unsauber programmiert):

    uintptr_t mstrlen(const char*s)
    {
            const char*d=s;
            while(*s)s++;
            return (uintptr_t)((s-1)-d);
    }
    

    Und Valgrind möchte immer noch für strlen die Symbole laden ... ich weiß ja nicht, wie es euch geht, aber das verstärkt das Vertrauen in den Valgrind-Memchecker nicht gerade. 😞



  • BTW: Den Schalter -fno-builtin-strlen habe ich auch schon gefunden, führt allerdings nur wieder zum oben erwähnten Fehler.



  • Das Problem ist eine typische race condition. Die Threads laufen und du stellst nicht sicher, dass sie vollständig beendet sind, bevor du neue Threads startest, die den selben Stack-Speicher verwenden sollen.
    (Sieht man gut, wenn man sich anschaut, welcher Thread die SEGV wirft und wann der laut deinem Programm als beendet gilt.)

    Am Einfachsten lässt du dieses PTHREAD_CREATE_DETACHED. Statt dessen halt alle TIDs sammeln und joinen. Dann hast du auch das hässlichen Pollen am Ende weg.

    (Nach der Umstellung läuft das Programm ohne SEGV ...)

    Normal sollte man solche Probleme aber eher lösen, in dem man einen Pool von 10 (sollte man halt an die Hardware anpassen und konfigurierbar halten) Threads erzeugt, die dann die Arbeit nach und nach zugeteilt bekommen. Deutlich mehr Threads als Kerne machen keinen Sinn und bremsen eher die Verarbeitung aus.



  • Du hast recht - ich Dämlack bin davon ausgegangen, dass der Thread sofort beendet, wenn das Return kommt. Anscheinend war dieses "sofort" aber nicht "sofort ǵenug". Vielen Dank für das Hinweisen in die richtige Richtung. Jetzt ist mir auch klar, warum der GDB das nicht angezeigt hat, weil es sich hier praktisch um eine virtuelle Maschine handelt, die langsamer als nativ läuft.

    Wie hast du das Problem jetzt bemerkt? Erfahrung, oder hast du ein spezielles Tool verwendet?



  • Es kann Sinn ergeben, mehr Threads als Kerne anzugeben - wenn der Hauptteil der Laufzeit keine CPU-, sondern I/O-Zeit beinhaltet, beispielsweise Kommunikation über Sockets. Zumindest habe ich das bisher gedacht, deswegen mache ich das auch. Für Aufgaben, die tatsächlich CPU-intensiv sind, würde ich +1 oder +2 Threads wie Kerne verwenden, damit das Ding zwischen den Neustarts der Threads immer schön ausgelastet ist. 😋



  • War erstmal nur eine Vermutung... ließ sich aber schnell bestätigen.

    Klar, es kommt immer drauf an, was die begrenzende Resource ist. Wenn es allerdings nicht die Cpu ist, gibt es häufig auch Möglichkeiten ganz ohne Threads. Und bei 10k+ Threads, würde ich das Design als höchst zweifelhaft bezeichnen 😉
    Aber ohne das Problem zu kennen lässt sich da immer nur spekulieren.



  • Darüber lässt sich jetzt streiten, ob bei mir 10k Threads zu viel sind. 🙂

    Fürs Archiv: Die neue Speicherverwaltung ist nicht wirklich schneller. Bei den Lasttests war meine Variante immer 3 Sekunden langsamer als die Standardsstacks (egal welche Stackgröße und Threads) und verbrachte mehr Zeit mit Systemaufrufen als mit meinem Code. Eventuell hast du hierfür noch eine Erklärung?
    Nachdem ich mir mit malloc den Speicher geholt habe, initialisiere ich ihn einmalig über memset, damit sichergestellt ist, dass der virtuelle Speicher mit physikalischem verknüpft wird. Da mit meiner Speicherverwaltung nur zu Anfang Speicher reserviert wird, vermute ich, dass meine Variante so langsam ist, weil der Kernel mit dem Verwalten des Speichers an sich beschäftig ist. Dabei dachte ich, dass das System eher mit O(1) skaliert - war scheinbar ein Irrtum, es sei denn, du hättest auch hierzu noch eine Vermutung? 🙂



  • lagalopex schrieb:

    Aber ohne das Problem zu kennen lässt sich da immer nur spekulieren.

    Dachschaden schrieb:

    Bei den Lasttests war meine Variante immer 3 Sekunden langsamer als die Standardsstacks (egal welche Stackgröße und Threads) und verbrachte mehr Zeit mit Systemaufrufen als mit meinem Code.

    Wie oben schon gesagt, ohne das Problem zu kennen... Dein Programm vom Anfang braucht weder mit "deiner Speicherverwaltung" noch normal 3s. (Nicht einmal bei 100k Zeilen und 64KiB Stack.)

    Ändert sich bei dir die Anzahl an Zeilen? Häufig? Dann wäre dieses memset sehr teuer. (10k * 8MiB ~ 78 GiB?) Das spart man sich bei der vom System verwalteten Variante.
    Es könnte auch an der Speicherausrichtung liegen.



  • Örks - da habe ich gestern mal Benchmarks gemacht:

    Alte Speicherverwaltung:
    
    2 Minuten, 128 KB pro Stack, 8000 Threads pro Ausführung.
    
    Ausführungen: 113
    Threads:      1792000
    
    Real: 2m01.043s
    User: 0m26.620s
    Sys:  0m46.772s
    
    ----------------------------
    
    2 Minuten, 32 KB pro Stack, 8000 Threads pro Ausführung.
    
    Ausführungen: 112
    Threads:      1776000
    
    Real: 2m00.851s
    User: 0m26.736s
    Sys:  0m42.240s
    
    ----------------------------
    
    5 Minuten, 128 KB pro Stack, 8000 Threads pro Ausführung.
    
    Ausführungen: 274
    Threads:      4368000
    
    Real:   5m90.578s
    User:   1m05.688s
    Sys:    1m56.076s
    
    ===================================================================
    Neue Speicherverwaltung:
    
    2 Minuten, 128 KB pro Stack, 8000 Threads pro Ausführung.
    
    Ausführungen: 110
    Threads:      1744000
    
    Real: 2m01.093s
    User: 0m22.164s
    Sys:  0m52.872s
    
    2 Minuten, 32 KB pro Stack, 8000 Threads pro Ausführung.
    
    ----------------------------
    
    Ausführungen: 110
    Threads:      1744000
    
    Real: 2m00.797s
    User: 0m20.560s
    Sys:  0m58.440s
    
    ----------------------------
    
    5 Minuten, 128 KB pro Stack, 8000 Threads pro Ausführung.
    
    Ausführungen: 270
    Threads:      4304000
    
    Real:   4m59.966s
    User:   0m43.580s
    Sys:    2m51.000s
    

    Ich muss aber dazu sagen, dass in meiner Version das memset fehlt. In Zeile 191 (ausgehend vom Code des ET) muss ein memset(glob_thread_mem,0,THREAD_STACK_SIZE*my_env.threads_count); rein. Malloc führt nämlich nicht immer unbedingt zu einem Kernel-Call, wodurch die virtuellen Speicherbereiche nicht automatisch mit physikalischem Speicher assoziitert werden. Damit dies nicht zur Laufzeit der Threads geschehen muss, mache ich das memset einmalig, damit für jeden Thread dieses Binding bereits besteht.
    Ich glaube, wenn du da das memset reinpackst, kommst du zum gleichen Ergebnis.



  • Wenn ich das memset einfüge dauert es nicht wirklich länger. Der Aufruf ist nur einmal und dauert auch unter einer Sekunde. (Ist aber auch nur 8k * 128KiB = 1 GiB)

    Wobei das Programm bei mir mit dem Standard-Stack knapp über 500 Durchläufe schafft. Mit "deinem" Stack sind es etwas über 1500.

    Aber vielleicht postest du dein Benchmark?

    Nachtrag:
    Ändert sich die Anzahl bei jedem Durchlauf zwischen 7999 und 8000, dreht sich das Bild.
    Standard bleibt unverändert bei etwas über 500.
    "deins" mit memset schafft aber nurnoch 125, ohne memset noch 1150.



  • OK, danke, dass du mich noch mal darauf aufmerksam gemacht hast.
    Mein Problem war, dass ich im Anfang der Threading-Routine die Schleife durchgegangen bin, um zu prüfen, ob ich bereits alle Parameter habe (was essentiell ist, sonst kann der Thread nicht vernünftig arbeiten). Für den Fall, dass die Parameter noch nicht ordentlich auf dem Stack liegen, wartet das Programm immer eine Sekunde und prüft dann nochmal.

    Wenn der Thread erstellt wird, hat der Kernel bei normaler Konfiguration offenbar genug Zeit, die Parameter auf den Stack zu schieben, wahrscheinlich dadurch, weil nicht so viele Threads auf einmal gestartet werden können und dem System dadurch genug Zeit bleibt, noch den Main-Thread auszuführen, wodurch der Thread nicht in die "Keine Parameter angegeben"-Bedingung läuft (ist allerdings nur eine Vermutung und würde auch nur dann funktionieren, wenn der Main-Thread noch für das Legen auf den Thread-Stack verantwortlich wäre - aber eine andere Erklärung habe ich gerade nicht). Wenn ich den Speicher allerdings selbst zur Verfügung stelle, muss der Kernel keinen teuren Speicher reservieren, weil das bereits passiert ist, und es können sehr viel mehr Threads zur gleichen Zeit erstellt werden - denen dann allerdings noch Parameter fehlen, welche zum einsekündigen Sleep führen.

    Wie bin ich das Problem umgangen? Ein usleep von 125000 genügt, und plötzlich komme ich auch auf die von dir beschriebenen Werte.

    Den Code stelle ich hier einfach mal zur Verfügung, falls jemand in Zukunft ein ähnliches Problem mit Lasttests hat.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdint.h>
    
    #include <errno.h>
    #include <fcntl.h>
    
    #include <pthread.h>
    
    /*Egal ob eigene Speicherverwaltung oder ob der Stack vom OS gestellt wird, dies ist die Standardstackgroesse bei mir*/
    #define THREAD_STACK_SIZE (128*1024)
    
    /*Threads-Stadien*/
    #define THREAD_STATUS_UNKNOWN (0)
    #define THREAD_STATUS_CREATED (1)
    #define THREAD_STATUS_EXITED  (2)
    #define THREAD_STATUS_FAILED  (3)
    
    /*Zu 1 aendern, wenn Fehler produziert werden soll*/
    #define THREAD_USE_MY_OWN_STACK (1)
    
    /*Zu THERAD_DEBUG aendern (zweiten Unterstrich entfernen), wenn Debuggingausgaben gewuenscht sind*/
    #define THREAD__DEBUG
    
    /*Parameterobjekt fuer Threads*/
    struct thread_param_t
    {
            pthread_t tid;     /*POSIX Thread-ID*/
            uintptr_t cid;     /*Meine Thread-ID*/
            char*   link;      /*Der Eintrag in der Datei*/
            size_t  link_size; /*Laenge des Eintrags*/
    };
    
    /*Einige globale Werte, auf die jeder Thread zugreifen muss*/
    struct
    {
            uintptr_t threads_count; /*Anzahl der Threads, die wir starten*/
            pthread_t*threads_status;/*Array, welches an der cid-te Position den gegenwaertigen Status des cid-te Threads beinhaltet*/
    }my_env;
    
    void print_time(void)
    {
            char tm_str[strlen("YYYY:MM:DD HH:MM:DD]: ")+1];
            time_t t;
            struct tm bp;
    
            time(&t);
            localtime_r(&t,&bp);
            strftime(tm_str,23,"[%F %T]:",&bp);
            fprintf(stderr,"%s ",tm_str);
    }
    
    /*Kurze Routine, um eine Datei einzuladen*/
    char*load_file(const char*file,uintptr_t*size)
    {
            char*res;
            int fd;
            struct stat stat_buf;
    
            *size=0;
            if(stat(file,&stat_buf))
                    return NULL;
    
            fd=open(file,O_RDONLY);
            if(fd==-1)
                    return NULL;
    
            res=malloc(stat_buf.st_size+1);
            if(!res)
            {
                    perror("malloc load_file");
                    return 0;
            }
            errno=0;
    
            while(*size!=stat_buf.st_size && !errno)
                    *size+=read(fd,&res[*size],stat_buf.st_size);
            close(fd);
    
            if(errno)
            {
                    free(res);
                    *size=0;
                    return NULL;
            }
    
            return res;
    }
    
    uintptr_t mstrlen(const char*s)
    {
            const char*d=s;
            while(*s)s++;
            return (uintptr_t)((s-1)-d);
    }
    
    /*Thread-Funktion*/
    void*process_thread(void*param)
    {
            struct thread_param_t*cur_params=(struct thread_param_t*)param;
    
            /*Sicherstellen, dass wir keinen NULL-Pointer oder dergleichen bekommen haben. Ist mir bereits passiert.*/
            uintptr_t mem_lookups=0;
    
            while(!cur_params || !cur_params->link || !cur_params->tid)
            {
                    if(mem_lookups++==5)
                    {
                            if(cur_params)
                            {
                                    if(cur_params->link)
                                            free(cur_params->link);
                                    free(cur_params);
                            }
                            print_time();fprintf(stderr,"Thread ist mit ungueltigen Werten gestartet worden und sagt leise \"Fuck Off\"\n");
                            return 0;
                    }
                    usleep(125000);
            }
    
            /*Link beschneiden, sonst kann dieser ein \n am Ende beinhalten*/
            if(cur_params->link[strlen(cur_params->link)-1]==0x0a)
                    cur_params->link[strlen(cur_params->link)-1]=0;
    
    #ifdef THREAD_DEBUG 
            print_time();fprintf(stderr,"[TID: %i|LINK: \"%s\"]\n",cur_params->cid,cur_params->link);
    #endif
    
            /*Status korrekt setzen und Ende*/
            free(cur_params->link);free(cur_params);
            return 0;
    }
    
    int main(void)
    {
            uintptr_t conf_length;                             /*Groesse der geladenen Konfigurationsdatei (my.file, siehe unten)*/
    
            FILE*fp;                                           /*Fuer fmemopen, damit wir immer getline auf das Handle callen koennen.*/
            size_t cur_bytes_read,cur_line_size=10;
            char*conf_content,*cur_line=malloc(cur_line_size); /*So eine Eigenheit von getline, die brauchen einen Zeiger auf den Heap, und wenn es nur ein Byte ist. Die vergroessern dann automatisch*/
    
            struct thread_param_t* cur_params;                  /*Zeiger auf das gegenwaertige Parameterobjekt, welches auf dem Heap angelegt wird.*/
            uintptr_t              cur_cid;                     /*Gegenwaertige eigene Thread-ID*/
            pthread_t              cur_tid;                     /*Gegenwaertige POSIX-Thread-ID*/
            pthread_attr_t         tattr;                       /*Attribute fuer alle Threads*/
    
    #if(THREAD_USE_MY_OWN_STACK)
            char*     glob_thread_mem;                          /*Zeiger auf globalen Thread-Stack-Speicher. Jeder Stack liegt damit neben einem anderen Stack*/
            uintptr_t prev_thread_count=0;                      /*Damit wir bei eigener Speicherverwaltung nicht immer neuen Speicher reservieren muessen, wird hier die Anzahl der Threads beim letzten Durchlauf gesichert, und wenn diese sich geaendert hat, dann wird der Speicher neu reserviert*/
    #endif
    
            uintptr_t runs=0,threads=0;
    
            /*Threading-Attribute setzen*/
            pthread_attr_init(&tattr);
    
            /*XXX: Wenn KEIN eigener Stack verwendet werden soll, Standardgroesse setzen lassen*/
    #if(!THREAD_USE_MY_OWN_STACK)
            pthread_attr_setstacksize(&tattr,THREAD_STACK_SIZE);
    #endif
    
    #ifdef THREAD_DEBUG 
            print_time();fprintf(stderr,"Start ...\n");
    #endif
    
            /*Programm soll in der Realitaet inner Endlossschleife laufen.*/
            while(1)
            {
    #ifdef THREAD_DEBUG
                    print_time();fprintf(stderr,"Das ist der %u Durchlauf mit %u erstellten Threads ...\n",runs+1,threads);
    #endif
                    runs++;
                    if(!(conf_content=load_file("my.file",&conf_length)))
                    {
                            perror("load_file");
                            exit(EXIT_FAILURE);
                    }
    
                    /*Rausfinden, wie viele Threads wir starten muessen*/
                    fp=fmemopen(conf_content,conf_length,"r");
                    my_env.threads_count=0;
                    while((cur_bytes_read=getline(&cur_line,&cur_line_size,fp))!=-1)
                    {
                            my_env.threads_count++;
                    }
    
                    if(!my_env.threads_count)
                    {
                            free(conf_content);
                            fclose(fp);
                            sleep(10);
                            continue;
                    }
    
                    /*Ein Array erzeugen fuer den gegenwaertigen Status unserer Threads*/
                    cur_bytes_read=my_env.threads_count*sizeof(pthread_t);
                    if(!(my_env.threads_status=malloc(cur_bytes_read)))
                    {
                            perror("malloc threads_status");
                            exit(EXIT_FAILURE);
                    }
                    memset(my_env.threads_status,0,cur_bytes_read);
    
    #if(THREAD_USE_MY_OWN_STACK)
                    /*Muss der Stack-Speicher neu geholt werden, weil mehr Zeilen in der Datei eingekommen sind?*/
                    if(prev_thread_count && prev_thread_count!=my_env.threads_count)
                            free(glob_thread_mem);
    
                    if(!prev_thread_count || prev_thread_count!=my_env.threads_count)
                    {
                            cur_bytes_read=THREAD_STACK_SIZE*my_env.threads_count;
                            if(!(glob_thread_mem=malloc(THREAD_STACK_SIZE*my_env.threads_count)))
                            {
                                    perror("malloc glob_thread_mem");
                                    exit(EXIT_FAILURE);
                            }
                            memset(glob_thread_mem,0,cur_bytes_read);
                    }
    #endif
    
                    /*Nochmal alle Zeilen durchgehen, aber diesmal wirklich Threads erzeugen*/
                    cur_cid=0;
                    rewind(fp);
                    while((cur_bytes_read=getline(&cur_line,&cur_line_size,fp))!=-1)
                    {
                            if(!(cur_params=malloc(sizeof(struct thread_param_t))))
                            {
                                    perror("malloc");
                                    exit(EXIT_FAILURE);
                            }
                            memset(cur_params,0,sizeof(struct thread_param_t));
    
                            if(!(cur_params->link=strndup(cur_line,cur_bytes_read)))
                            {
                                    perror("malloc");
                                    exit(EXIT_FAILURE);
                            }
    
                            cur_params->link_size=cur_bytes_read;
                            cur_params->cid=cur_cid++;
    
    #if(THREAD_USE_MY_OWN_STACK)
                            pthread_attr_setstack(&tattr,&(glob_thread_mem[cur_params->cid*THREAD_STACK_SIZE]),THREAD_STACK_SIZE);
    #endif
                            if(pthread_create(&cur_tid,&tattr,process_thread,cur_params))
                            {
                                    cur_cid--;
                                    free(cur_params->link);
                                    free(cur_params);
                                    continue;
                            }
                            my_env.threads_status[cur_cid-1]=cur_tid;
                            cur_params->tid=cur_tid;
                            threads++;
                    }
    
                    for(cur_cid=0;cur_cid<my_env.threads_count;cur_cid++)
                    {
                            if(!my_env.threads_status[cur_cid])
                                    continue;
    
                            if(!pthread_join(my_env.threads_status[cur_cid],0))
                                    my_env.threads_status[cur_cid]=0;
                            else
                                    cur_cid--;
                    }
    
                    /*OK, alle Threads sind abgearbeitet. Speicher, den wir nicht mehr brauchen, freigeben, und alle Sockets schliessen*/
    
    #ifdef THREAD_DEBUG
                    print_time();fprintf(stderr,"Ende ...\n");
    #endif
    
                    fclose(fp);
                    free(my_env.threads_status);
                    free(conf_content);
    #if(THREAD_USE_MY_OWN_STACK)
                    prev_thread_count=my_env.threads_count;
    #endif
            }
    
    #if(THREAD_USE_MY_OWN_STACK)
            if(prev_thread_count)
                    free(glob_thread_mem);
    #endif
    
            free(cur_line);
            exit(EXIT_SUCCESS);
    }
    

    Vielen Dank für deine Geduld!



  • Wenn ich diese komische Schleife in Zeilen 106-120 durch eine einfache if Abfrage ersetze, stellt sich heraus, dass der "Fehler" nie auftritt. Und genau das würde ich auch erwarten. Immerhin wird der Speicher im Main-Thread angelegt und gefüllt und erst danach der Thread gestartet. Das heißt, bei wurde dort nie ein sleep aufgerufen.

    Du legst das cur_params an, übergibst es an den Thread, der es freigibt. Parallel dazu schreibst du noch im Mainthread hinein. Zu dem Zeitpunkt kann dieser Speicher schon wieder freigegeben sein. (Wenn du das pthread_t im Thread selbst brauchst, hol es dir einfach mit pthread_self.)

    cur_bytes_read für alles Mögliche zu verwenden ist nicht gerade guter Stil.

    Was für ein System (Hard- und Software) benutzt du eigentlich dafür?
    Und was macht der Thread? Einfach etwas herunterladen? Schonmal select oder epoll angeschaut?

    Wie schon mehrfach gesagt, ein Pool mit ein paar Threads erspart dir viel Overhead. Du musst nur einmal den Speicher für die Threads anfordern und diese werden auch nur einmal erstellt, egal ob sich die Anzahl Zeilen in der Datei jetzt ändert oder nicht. Zudem wird so nicht unnötig viel Speicher belegt. Was machst du, wenn es mal 100k Zeilen in der Datei sind?

    Aber ohne das Problem zu kennen ist das ganze auch kein sinnvoller Benchmark. Ein simples sequenzielles Programm wäre um Größenordnungen schneller im nichtstun.



  • Eine Architektur mit 10K Threads ist immer falsch. Du kannst mit select, poll oder epoll oder was auch immer auf I/O warten ohne Threads zu verbraten. Das hat der Vorposter ja auch schon erwähnt.

    Und wenn ich lese, dass Du hoffst , ob der Stack bereits gefüllt ist, dann hast Du die Synchronisation nicht korrekt implementiert.


Anmelden zum Antworten