Linux, Version 3.7, zufällige Segfaults in libpthread und libc Version 2.13
-
Wozu Schlafstörungen alles gut sind ... -.-
Habe mir jetzt doch noch mal Valgrind genauer angeschaut und wollte mal dem memchecker über mein Programm laufen lassen - und bekomme direkt eine Fehlermeldung, dass die Symbole für die ld-linux-x86-64.so.2 nicht drinne sind (dass die Datei also gestrippt wurde). Und was soll ich sagen, das ist der Fall:
~# file /lib/x86_64-linux-gnu/ld-2.13.so /lib/x86_64-linux-gnu/ld-2.13.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0xDEEDBEEF, stripped
Ja klar könnte ich die glibc von Hand nachbauen, wäre mal eine neue Erfahrung, aber darauf habe ich grade so viel Lust wie auf Bauchschmerzen (meh).
Kennt hier vielleich einer ein anderes Memchecker-Programm, welches nicht so regiede wie die Valgrind-Suite ist, oder muss ich mir ernsthaft die Mühe machen und die glibc neu bauen? Drepper meinte ja bereits, dass man lieber die glibc von seinem Distributor verwenden sollte (Tolle Wurst).
-
Dachschaden schrieb:
Kennt hier vielleich einer ein anderes Memchecker-Programm, welches nicht so regiede wie die Valgrind-Suite ist, oder muss ich mir ernsthaft die Mühe machen und die glibc neu bauen? Drepper meinte ja bereits, dass man lieber die glibc von seinem Distributor verwenden sollte (Tolle Wurst).
Warum installierst du nicht einfach die libc-Debug-Pakete deiner Distro und verwendest die? Unter Debian war das IIRC libc-dbg.
-
Weil es sich zufälligerweise so begeben hat, dass ich dieses Paket bereits installiert habe und mein Distibutor (kein reines Debian - welches Debian verwendet auch schon Kernel 3.7) anscheinend nur die stripped-Variante anbietet.
~# apt-get install libc6-dbg Reading package lists... Done Building dependency tree Reading state information... Done libc6-dbg is already the newest version. 0 upgraded, 0 newly installed, 0 to remove and 12 not upgraded #OK, lass mal neuinstallieren ~# apt-get install --reinstall libc6-dbg Reading package lists... Done Building dependency tree Reading state information... Done 0 upgraded, 0 newly installed, 1 reinstalled, 0 to remove and 12 not upgraded. Need to get 0 B/2,587 kB of archives. After this operation, 0 B of additional disk space will be used. (Reading database ... 333170 files and directories currently installed.) Preparing to replace libc6-dbg:amd64 2.13-38 (using .../libc6-dbg_2.13-38_amd64.deb) ... Unpacking replacement libc6-dbg:amd64 ... Setting up libc6-dbg:amd64 (2.13-38) ... #Wurde neuinstalliert? Dann man ausprobieren. ~# file /lib/x86_64-linux-gnu/ld-2.13.so /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, stripped
Hat scheinbar nichts gebracht, die werden immer noch so ausgeliefert.
-
Direkt in /lib wirst du die eher nicht finden; typischerweise liegen die Debug-Infos eher irgendwo in /usr/lib/debug/lib oder so. Mach mal
dpkg -L libc6-dbg
. (An der ld-2.13.so selbst wird das natürlich nichts ändern; die Debug-Infos werden nur _zusätzlich_ installiert.)Aber an sich sollte gdb die normalerweise automatisch verwenden. valgrind IIRC auch.
-
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 mitmalloc
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.