Linux, Version 3.7, zufällige Segfaults in libpthread und libc Version 2.13
-
Nacht, Leute. Ich habe hier seit dem Wochenende ein Problem mit einem Mutlithreading-Programm unter Linux, an dem ich gerade arbeite. Ich habe jetzt bereits mehrere dutzende Tests gemacht, kann aber anscheinen keinen Fehler in meinem Code entdecken.
Meine Anwendung muss in der Lage ein, ein paar tausend/zehntausend Threads zu erstellen, diese abzuarbeiten und dann wieder aufzuräumen - es läuft dabei in einer Endlosschleife, d.h. wenn alle Threads abgearbeitet sind, fängt der Main-Thread wieder von vorne an, aber eventuell mit anderen Eingabedaten (siehe nächster Absatz). Auf Forks kann ich wegen absurden Speicher- und CPU-Anforderungen nicht zugreifen, ich muss LWPs verwenden.
Mein Programm liest eine Datei ein, geht diesen Zeilenweise durch und erstellt Threads hierfür, die mit diesen Zeilen/Einträgen arbeiten. Weil zudem Speicherreservierungen und -Freigebungen teuer sind, wollte ich eine eigene Speicherverwaltung einfügen - jeder Thread bekommt
THREAD_STACK_SIZE
Bytes reserviert, der beim Erstellen blockweise durch pthread_attr_setstack gesetzt wird:pthread_attr_setstack(&tattr,&(glob_thread_mem[cur_params->cid*THREAD_STACK_SIZE]),THREAD_STACK_SIZE);
Wenn ich diesen Code allerdings verwende, segfaultet mein Programm mit Adressen im Bereich der libpthread und libc - diese Einträge habe ich z.B. meiner syslog entnommen:
`
test.exe[4632]: segfault at 4100c0 ip 00007facee62d68f sp 00007fff1d24da20 error 6 in libpthread-2.13.so[7facee626000+17000]
test.exe[3379]: segfault at 7f8910000020 ip 00007f8910000020 sp 00007f89150cae60 error 15
test.exe[16462]: segfault at 7fffd4000000 ip 00007f4127655a79 sp 00007f4127d53ea0 error 4 in libc-2.13.so[7f41275da000+180000]
`
Die Fehlermeldungen tauchen immer zufällig und bei verschiedenen Zeitpunkten auf, meist aber nach einigen Sekunden, oft auch sofort, selten nach einer Minute. Ich habe bereits mit dem Eintrag
THREAD_STACK_SIZE
gespielt und diesen testweise auf 16 KB (niedrigster Wert) bis 8 MB (ulimit-Standard) gesetzt. Ich habe mein Programm coredumpen lassen und diesen durch den gdb gejagt, die Ausgabe ist aber auch hier unterschiedlich.Erster Versuch:
~$ gdb test.exe core GNU gdb (GDB) 7.4.1-debian <Lizenzkack> Reading symbols from ./test.exe...(no debugging symbols found)...done. [New LWP 5072] [New LWP 5146] [New LWP 23832] warning: Can't read pathname for load map: Input/output error. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Und dann war es das. Das Programm hängt. Auch nach mehrmaligen Coredumps bekomme ich nicht mehr vom Programm.
Zweiter Versuch:
~$ gdb test.exe # Diesmal ohne Core dump, diesmal wollte ich in Echtzeit sehen, was passiert. GNU gdb (GDB) 7.4.1-debian <Lizenzkack> Reading symbols from ./test.exe...(no debugging symbols found)...done. (gdb) start Temporary breakpoint 1 at 0x400ed7 Starting program: ./test.exe [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Temporary breakpoint 1, 0x0000000000400ed7 in main () (gdb) continue Continuing. [New Thread 0x7ffff5035700 (LWP 24778)] [Thread 0x7ffff5035700 (LWP 24778) exited] ...
Und das geht immer nur so weiter, kein Fehler taucht auf.
Dritter Versuch:
Ist eigentlich nicht fair, weil ich ihn nicht mit meinem Testcase-Programm reproduzieren konnte, sondern nur in meinem Hauptprogramm. Hier konnte ich den gdb überreden, einige Core Dumps einzulesen, ohne dass der Debugger hängenbleibt, und nach der Analyse habe ichbt
eingegeben, das kam raus:#0 0x0000000000000000 in ?? () #1 0x0000000000000000 in ?? ()
Und ich bin mir sicher, dass diese Meldung irgendwo im Universum Sinn ergibt, aber in meinem Gehirn derzeit noch nicht.
Dies ist mein Testcase-Programm-Code. Damit man das Programm sehr schnell gedebuggen kann, habe ich eine Präprozessordirektive eingetragen,
THREAD_USE_MY_OWN_STACK
, die das Problem ein- und ausschaltet. Bei(1)
taucht der Segfault auf, bei(0)
nicht.#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 (8*1024*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*/ char*threads_status; /*Array, welches an der cid-te Position den gegenwaertigen Status des cid-te Threads beinhaltet*/ }my_env; /*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"); 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; } /*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) { if(mem_lookups++==5) { if(cur_params) { my_env.threads_status[cur_params->cid]=THREAD_STATUS_FAILED; free(cur_params); } fprintf(stderr,"Thread ist mit ungueltigen Werten gestartet worden und sagt leise \"Fuck Off\"\n"); return 0; } sleep(1); } /*Link beschneiden, sonst kann dieser ein \n am Ende beinhalten*/ cur_params->link[strlen(cur_params->link)-1]=0; #ifdef THREAD_DEBUG fprintf(stderr,"[TID: %i|LINK: %s]\n",cur_params->cid,cur_params->link); #endif /*Status korrekt setzen und Ende*/ my_env.threads_status[cur_params->cid]=THREAD_STATUS_EXITED; free(cur_params->link);free(cur_params); return 0; } int main(void) { int i; /*Allgemeine Zaehlervariable*/ 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.*/ pthread_t 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 /*Threading-Attribute setzen*/ pthread_attr_init(&tattr); pthread_attr_setdetachstate(&tattr,PTHREAD_CREATE_DETACHED); /*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 fprintf(stderr,"Start ...\n"); #endif /*Programm soll in der Realitaet inner Endlossschleife laufen.*/ while(1) { 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) { sleep(10); continue; } /*Ein Array erzeugen fuer den gegenwaertigen Status unserer Threads*/ my_env.threads_status=malloc(my_env.threads_count*sizeof(char)); if(!my_env.threads_status) { perror("malloc"); exit(EXIT_FAILURE); } memset(my_env.threads_status,THREAD_STATUS_UNKNOWN,my_env.threads_count*sizeof(char)); #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) { glob_thread_mem=malloc(THREAD_STACK_SIZE*my_env.threads_count); if(!my_env.threads_status) { perror("malloc"); exit(EXIT_FAILURE); } } #endif /*Nochmal alle Zeilen durchgehen, aber diesmal wirklich Threads erzeugen*/ i=0; rewind(fp); while((cur_bytes_read=getline(&cur_line,&cur_line_size,fp))!=-1) { cur_params=malloc(sizeof(struct thread_param_t)); if(!cur_params) { perror("malloc"); exit(EXIT_FAILURE); } memset(cur_params,0,sizeof(cur_params)); cur_params->cid=i++; cur_params->link_size=cur_bytes_read; cur_params->link=malloc(cur_params->link_size+1); if(!cur_params->link) { perror("malloc"); exit(EXIT_FAILURE); } strncpy(cur_params->link,cur_line,cur_bytes_read+1); #if(THREAD_USE_MY_OWN_STACK) pthread_attr_setstack(&tattr,&(glob_thread_mem[cur_params->cid*THREAD_STACK_SIZE]),THREAD_STACK_SIZE); #endif my_env.threads_status[cur_params->cid]=THREAD_STATUS_CREATED; if(pthread_create(&cur_params->tid,&tattr,process_thread,cur_params)) { perror(NULL);exit(EXIT_FAILURE); i--; free(cur_params->link); free(cur_params); my_env.threads_status[cur_params->cid]=THREAD_STATUS_FAILED; } } /*Nachdem die Threads erstellt worden sind, immer wieder pruefen, ob alle Threads inzwischen fertig sind (im Realcode ist hier ein usleep vorhanden, damit die CPU-Last nicht so hoch geht).*/ while(1) { for(i=0;i<my_env.threads_count;i++) if(my_env.threads_status[i]!=THREAD_STATUS_EXITED && my_env.threads_status[i]!=THREAD_STATUS_FAILED) { #ifdef THREAD_DEBUG fprintf(stderr,"[TID: %i|RES: %i]\n",i,my_env.threads_status[i]); #endif goto NOT_FINISHED; } break; NOT_FINISHED: ; } /*OK, alle Threads sind abgearbeitet. Speicher, den wir nicht mehr brauchen, freigeben, und alle Sockets schliessen*/ #ifdef THREAD_DEBUG 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); }
Die my.file, welche von Programm eingelesen wird, hat diesen Inhalt:
1 2 3 4 5 6
Könnte sich mal einer den Code durchsehen und mich auf den Fehler aufmerksam machen? Ich sitze hier seit zwei Tagen an diesem Code und sehe vor lauter Zeilen keine Fehler mehr.
-
Ach was ein Dreck, das wichtigste vergessen:
Kompiliert mit:
gcc test.c -o test.exe -lpthread~$ gcc -V Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/4.7/lto-wrapper Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Debian 4.7.2-5' --with-bugurl=file:///usr/share/doc/gcc-4.7/README.Bugs --enable-languages=c,c++,go,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.7 --enable-shared --enable-linker-build-id --with-system-zlib --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.7 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --enable-objc-gc --with-arch-32=i586 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 4.7.2 (Debian 4.7.2-5)
-
Mal einige allgemeine Kommentare:
- Ich würde mit Debugging-Symbolen kompilieren, d.h noch ein "-g -ggdb3" dranhängen, dann spuckt dir gdb mehr aus (sollte die ?? mit Datei:Zeile ersetzen)
- Hast du mal valgrind probiert? Das liefert manchmal bessere Informationen.
- 10'000 Threads?!? Hört sich nach viel zu viel an. mit "cat /proc/sys/kernel/threads-max" kannst du dir die maximale (theoretische) maximale Anzahl Threads anzeigen lassen. Auf 32-Bit-Systemen ist das so um die 3000, in 64-Bit-Systemen wie du eines hast hängt das vom verfügbaren Speicher ab, aber es ist gut möglich, dass dir der Speicher ausgeht. Ist das wirklich nötig? Rein Performance-mässig wären 10-100 Threads wohl um einiges geschickter.
-
1. -g habe ich schon eingefügt, -ggdb3 noch nicht. Werde ich heute Abend ausprobieren.
2. Habe ich schon von gehört, wollte jetzt aber nicht mit einer neuen Sache anfangen und mich da einarbeiten, wenn eine Sache noch nicht beendet wurde - ich lerne am Besten in Ruhe und ohne Druck. Auch hier kann ich mir heute Abend die Suite mal anschauen.
3. Das Threading-Limit ist wohl kein Problem, bei mir sinds max. 126.289.Da passen die 10.000 locker rein, sind auch die Anzahl der möglichen Threads, also dem, wovon ich zu Spitzenzeiten ausgehen kann. Mein Speicher sollte kein Problem sein, auf meinem Desktop habe ich eine Memory-Anzeige, die mit dem Testprogramm nicht mal einen Springer im Verlauf verursacht. Beim richtigen Programm, wo ich die alte Speicherverwaltung verwende, können das mit Lasttests schon 1,5 GB werden, aber auf meiner Testmaschine habe ich 8 GB Real und 15 GB Swap. Ich habe schon mit der alten Speicherverwaltung Lasttests mit 32.000 Threads pro Run gemacht, die einen Load Average von 4334 verursachten, und die Maschine war immer noch ansprechbar und hatte nur 2,4 GB ingesamt (also auch mit VLC und Chatprogramm) belegt.
-
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.