Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien

C programma werkt niet in FreeBSD wel in Linux

Pagina: 1
Acties:

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Ik heb een C programma welke perfect werkt in allerlei Linux distributies, maar niet in FreeBSD.
Het probleem is dat de loops in de threads niet goed afsluiten. Daardoor lukt het niet om alle benodigde garbage collection functies te draaien en daarmee het programma goed af te sluiten. Iemand een idee wat ik fout doe?

Een voorbeeld code dat de situatie in mijn programma schets:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <pthread.h>
#include <setjmp.h>
#include <signal.h>

pthread_t pth;

static sigjmp_buf gc_cleanup;

int main_loop = 1;
int thread_loop = 1;
int thread_running = 0;
int gc_enable = 1;

void *thread(void *param) {

    thread_running++;
    while(thread_loop) {
        sleep(1);
        printf("thread timeout\n");
    }
    printf("thread stopped\n");
    thread_running--;

    return NULL;
}

void gc_handler(int sig) {
    if(gc_enable) {
        gc_enable = 0;
        siglongjmp(gc_cleanup, sig);
    }
}

int main_gc() {
    thread_loop = 0;

    printf("gc\n");

    while(thread_running > 0) {
        printf("waiting\n");
        sleep(1);
    }

    main_loop = 0;
    return 0;
}

/* Initialize the catch all gc */
void gc_catch(void) {
    struct sigaction act, old;

    memset(&act, 0, sizeof(act));
    act.sa_handler = gc_handler;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT,  &act, &old);
    sigaction(SIGQUIT, &act, &old);
    sigaction(SIGTERM, &act, &old);

    if(sigsetjmp(gc_cleanup, 0) == 0)
        return;

    /* Call all GC functions */
    main_gc();
}
    
int main() {
    gc_catch();
    
    pthread_create(&pth, NULL, &thread, (void *)NULL);

    while(main_loop) {
        sleep(1);
    }

    printf("program stopped\n");
    
    return (EXIT_SUCCESS);
}


Uitvoer in linux:
code:
1
2
3
4
5
6
./test
[CTRL-C]
gc
thread timeout
thread stopped
program stopped


Uitvoer van hetzelfde programma in FreeBSD:
code:
1
2
3
4
5
./test
[CTRL-C]
gc
waiting
Segmentation fault


Valgrind linux:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
==13575== Memcheck, a memory error detector
==13575== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al.
==13575== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info
==13575== Command: ./test
==13575==
^Cgc
thread timeout
thread stopped
program stopped
==13575==
==13575== HEAP SUMMARY:
==13575==     in use at exit: 0 bytes in 0 blocks
==13575==   total heap usage: 1 allocs, 1 frees, 136 bytes allocated
==13575==
==13575== All heap blocks were freed -- no leaks are possible
==13575==
==13575== For counts of detected and suppressed errors, rerun with: -v
==13575== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 13 from 6)


Valgrind FreeBSD:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
valgrind --dsymutil=yes --tool=memcheck --leak-check=full --track-origins=yes ./test
==923== Memcheck, a memory error detector
==923== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==923== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==923== Command: ./test
==923==
^C==923==
==923== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==923==  General Protection Fault
==923==    at 0x1219F2A: ??? (in /lib/libthr.so.3)
==923==    by 0x3804A267: ??? (in /usr/local/lib/valgrind/memcheck-amd64-freebsd)
==923==    by 0x121ED5B: ??? (in /lib/libthr.so.3)
==923==    by 0x400DC9: thread (in /data/Hobby/ICT/RPi/433.92/thread/test)
==923==    by 0x12154A3: ??? (in /lib/libthr.so.3)
==923==
==923== HEAP SUMMARY:
==923==     in use at exit: 6,444 bytes in 9 blocks
==923==   total heap usage: 9 allocs, 0 frees, 6,444 bytes allocated
==923==
==923== LEAK SUMMARY:
==923==    definitely lost: 0 bytes in 0 blocks
==923==    indirectly lost: 0 bytes in 0 blocks
==923==      possibly lost: 0 bytes in 0 blocks
==923==    still reachable: 6,444 bytes in 9 blocks
==923==         suppressed: 0 bytes in 0 blocks
==923== Reachable blocks (those to which a pointer was found) are not shown.
==923== To see them, rerun with: --leak-check=full --show-reachable=yes
==923==
==923== For counts of detected and suppressed errors, rerun with: -v
==923== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Killed


gdb linux:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/arm-linux-gnueabihf/libthread_db.so.1".
[New Thread 0xb6e81470 (LWP 13594)]
^C
Program received signal SIGINT, Interrupt.
0xb6f1ca20 in nanosleep () at ../sysdeps/unix/syscall-template.S:82
82      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) signal 2
Continuing with signal SIGINT.
gc
thread timeout
thread stopped
program stopped
[Thread 0xb6e81470 (LWP 13594) exited]
[Inferior 1 (process 13593) exited normally]
(gdb)


gdb FreeBSD:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "amd64-marcel-freebsd"...
(gdb) run
Starting program: /data/Hobby/ICT/RPi/433.92/thread/test
[New LWP 101232]
^C[New Thread 801406800 (LWP 101389/test)]

Program received signal SIGINT, Interrupt.
[Switching to Thread 801406800 (LWP 101389/test)]
0x000000080083089c in __error () from /lib/libthr.so.3
(gdb) signal 2
Continuing with signal SIGINT.
gc

Program received signal SIGSEGV, Segmentation fault.
0x0000000000000001 in ?? ()
(gdb) backtrace
#0  0x0000000000000001 in ?? ()
#1  0x0000000000000000 in ?? ()
(gdb)

[ Voor 6% gewijzigd door CurlyMo op 20-04-2014 23:53 . Reden: Stukje ingekort omdat het zelfs met een sleep al niet werkt. ]

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
Er gebeuren wel wat dubieuze dingen in je code (het lezen/schrijven van die globale variabelen heeft eigenlijk wat extra synchronisatie nodig) maar ik zie niet zo 1-2-3 hoe dat hier tot een segfault zou kunnen leiden.

Kun je je code eens compileren met -g zodat er wat meer informatie in de stack trace te zien is?

  • PrisonerOfPain
  • Registratie: Januari 2003
  • Laatst online: 26-05 17:08
Ik heb niet veel kennis van deze platformen, maar je code heeft wel een aantal data-races

C:
1
2
3
int main_loop = 1; 
int thread_loop = 1; 
int thread_running = 0; 


Zijn niet ge-guard voor toegang vanaf verschillende threads dus je hebt een berg aan data-races.

Mag je printf() uberhaupt wel aanroepen in een signal-handler? http://man7.org/linux/man-pages/man7/signal.7.html

[ Voor 17% gewijzigd door PrisonerOfPain op 20-04-2014 22:57 ]


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
Trouwens, is het probleem misschien dat de signal in de tweede thread wordt gedispatcht terwijl gc_cleanup alleen geldig is in de context van de eerste thread?

Verwijderd

Het geheugen van struct sigaction act is ook niet netjes gealloceerd. Dat kan problemen geven als main_gc of gc_catch eerder returnen dan thread. Als de sigsetjmp(gc_cleanup, 0) lukt, dan mag het geheugen waarnaar act verwijst niet meer worden gebruikt en gaat de software bij een afgevangen signal inderdaad onderuit.

  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
Volgens mij is dat geen probleem: die struct dient alleen om parameters door te geven aan sigaction(); de waarden erin worden door de C library gekopieerd. De pointers die je meegeeft aan sigaction() hoeven dus (voor zover ik weet) niet na de call geldig te blijven.

(Je kunt in plaats van &old gewoon NULL passen als de oude handler je niet interesseert, trouwens, maar dat is verder niet belangrijk.)

[ Voor 6% gewijzigd door Soultaker op 20-04-2014 23:14 ]


Verwijderd

Ik was er niet helemaal zeker van, maar het klinkt inderdaad niet onlogisch dat de thread library een kopie maakt. Ik durf echter even geen uitspraken te doen over hoe dat bij FreeBSD zit. Ik kan het zo vlug niet in de documentatie vinden.

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Soultaker schreef op zondag 20 april 2014 @ 22:54:
Er gebeuren wel wat dubieuze dingen in je code (het lezen/schrijven van die globale variabelen heeft eigenlijk wat extra synchronisatie nodig) maar ik zie niet zo 1-2-3 hoe dat hier tot een segfault zou kunnen leiden.
Wat gaat er buiten de synchronisatie nog meer fout? :) Ik ben leergierig.
Kun je je code eens compileren met -g zodat er wat meer informatie in de stack trace te zien is?
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[root@server /data/Hobby/ICT/RPi/433.92/thread]# gdb test
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "amd64-marcel-freebsd"...
(gdb) run
Starting program: /data/Hobby/ICT/RPi/433.92/thread/test
[New LWP 101213]
thread timeout
^C[New Thread 801406800 (LWP 101410/test)]

Program received signal SIGINT, Interrupt.
[Switching to Thread 801406800 (LWP 101410/test)]
0x0000000800b5619a in nanosleep () from /lib/libc.so.7
(gdb) signal 2
Continuing with signal SIGINT.
gc
waiting
waiting
[New Thread 801406400 (LWP 101213/test)]

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 801406400 (LWP 101213/test)]
0x000000080082f6a4 in pthread_testcancel () from /lib/libthr.so.3
(gdb) backtrace
#0  0x000000080082f6a4 in pthread_testcancel () from /lib/libthr.so.3
#1  0x0000000800827c43 in sleep () from /lib/libthr.so.3
#2  0x0000000800827c33 in sleep () from /lib/libthr.so.3
#3  0x0000000000400b66 in main () at test.c:24
(gdb)
Dat heb ik even gedaan om te laten zien wat er gebeurd.
Soultaker schreef op zondag 20 april 2014 @ 22:58:
Trouwens, is het probleem misschien dat de signal in de tweede thread wordt gedispatcht terwijl gc_cleanup alleen geldig is in de context van de eerste thread?
Kan je dit meer toelichten?

M.b.t. die globale variabelen. Die *_loop variabelen worden maar op één plek beschreven en dat is in de GC functie behorende tot de thread. Ze staan bij het starten van het programma dus op 1 en bij het afsluiten worden ze op 0 gezet.

De thread_running variabele wordt normaal gesproken helemaal aan het begin van een thread opgehoogd en aan het eind van de thread weer verlaagd om zeker te weten dat alle threads gestopt zijn. Hij staat nu in de loop. Dat is een foutje. Ik zal het even aanpassen.

[ Voor 28% gewijzigd door CurlyMo op 20-04-2014 23:49 ]

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
Ik zie trouwens een nog veel fundamentelere fout: wanneer je uit gc_catch() returnt invalidate je de context die je met setjmp() hebt aangemaakt.

Dat kun je fixen door de boel zo te herschrijven:

edit:
Nope, ik snap het niet. Vanuit een signal handler jumpen naar een andere context is gewoon raar. Het is nu laat -- ik zal er morgen nog eens naar kijken. Ik heb het idee dat jumpen vanuit een signal handler gewoon een slecht idee is en eigenlijk snap ik niet waarom het onder Linux überhaupt werkt.
CurlyMo schreef op zondag 20 april 2014 @ 23:36:
Wat gaat er buiten de synchronisatie nog meer fout? :) Ik ben leergierig.
Je lijkt er vanuit te gaan dat alle statements atomair uitgevoerd worden en in de volgorde waarin je ze opgeschreven hebt. Dat is niet per se zo. En dan nog heeft de C compiler vrij veel vrijheid om te bepalen welke data wanneer gelezen en geschreven wordt; het kan bijvoorbeeld best dat de read op regel 22 uitgevoerd wordt vóór de write op regel 21, en in dat geval klopt je logica niet (want er kan een interrupt tussendoor komen, en dan draait je GC functie tegelijk met je thread, wat je wilde voorkomen).

Om dit soort code correct te krijgen moet je een helder beeld hebben van hoe het memory model in C eruit ziet in een multithreaded omgeving en expliciet memory barriers introduceren om te garanderen dat wijzigingen uit de ene thread zichtbaar zijn in de andere. Maar meestal is het makkelijker om primitieven uit pthreads (of een andere threading library) te gebruiken. In plaats van integers als globale variabelen kun je bijvoorbeeld pthread condition variables gebruiken.

Maar goed, dat lijkt me allemaal niet relevant voor je huidige probleem, dus ik zal er nu niet al te diep op ingaan.
Kan je dit meer toelichten?
Met sigaction() geïnstalleerde signal handlers gelden voor het hele proces. Als er een signal binnenkomt wordt die uitgevoerd op de stack van een willekeurige thread. Dat hoeft dus niet de main thread te zijn; het kan ook de nieuwe thread zijn die thread() uitvoert. Je kan echter niet van de ene thread naar de andere jumpen (om dezelfde reden dat je niet uit de functie waarin je setjmp() callt mag returnern: je wordt namelijk geacht naar hoger op je huidige stack te jumpen). Als je dat wel doet zit je met twee threads op dezelfde stack te werken en dat kan natuurlijk niet.

Je moet er dus voor zorgen dat die interrupts afgehandeld worden in de main thread, waarin de in gc_cleanup opgeslagen context geldig is. Om dat te doen kun je in de nieuwe thread pthread_sigmask() callen en de betreffende signals masken. (Eigenlijk moet je er ook voor zorgen dat dit gebeurd is vóór je de signal handler installeert, anders is er nog steeds een window waarin beide threads signals accepteren.)

[ Voor 17% gewijzigd door Soultaker op 21-04-2014 02:21 ]


  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Soultaker schreef op maandag 21 april 2014 @ 02:09:
In plaats van integers als globale variabelen kun je bijvoorbeeld pthread condition variables gebruiken.

Maar goed, dat lijkt me allemaal niet relevant voor je huidige probleem, dus ik zal er nu niet al te diep op ingaan.
Die gebruik ik normaal ook in het echte programma, maar ik had het weggehaald om een voorbeeld te maken die zo kernachtig mogelijk het probleem zou schetsen.
Je moet er dus voor zorgen dat die interrupts afgehandeld worden in de main thread, waarin de in gc_cleanup opgeslagen context geldig is. Om dat te doen kun je in de nieuwe thread pthread_sigmask() callen en de betreffende signals masken. (Eigenlijk moet je er ook voor zorgen dat dit gebeurd is vóór je de signal handler installeert, anders is er nog steeds een window waarin beide threads signals accepteren.)
Hoe kan je dat doen vóórdat de signal handler geïnstalleerd wordt in het geval dat er dynamisch threads bij komen en gestopt worden?



Eureka _/-\o_

Het probleem is opgelost door dit toe te voegen aan het begin van de thread functie:
C:
1
2
3
4
5
    sigset_t set;
    sigfillset(&set);
    if(pthread_sigmask(SIG_SETMASK, &set, NULL) != 0) {
        printf("pthread_sigmask");
    }

Zover ik het begrijp kan normaal gesproken een signaal óók een thread stoppen. Door een sigmask toe te voegen zeg je tegen je thread, negeer het signaal waardoor de main_gc alle controle krijgt om zijn dingen te doen. Klopt dat?

Kunnen we nu aandacht besteden aan alle fouten die ik verder in het programma had zitten ;) Je had het over conditions. In mijn echte programma doe ik het zo:

In de thread staat i.p.v. sleep:
C:
1
2
3
4
5
6
7
8
9
        pthread_mutex_unlock(&mutex);

        gettimeofday(&tp, NULL);
        ts.tv_sec = tp.tv_sec;
        ts.tv_nsec = tp.tv_usec * 1000;     
        ts.tv_sec += 1;

        pthread_mutex_lock(&mutex);
        pthread_cond_timedwait(&cond, &mutex, &ts);

De mutex en de cond zijn hier geldig per thread. Zo kan ik (denk ik) veilig aangeven wanneer ik een thread wil stoppen.

De main_gc ziet er dan zo uit:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main_gc() {
    printf("gc\n");

    thread_loop = 0;

    pthread_mutex_unlock(&mutex);
    pthread_cond_signal(&cond);
    
    while(thread_running > 0) {
        printf("waiting\n");
        sleep(1);
    }

    pthread_join(pth, NULL);

    main_loop = 0;
    return 0;
}


Is er trouwens een manier om te tellen hoeveel locks een PTHREAD_MUTEX_RECURSIVE mutex heeft? Dan zou ik de thread_running vervangen door het aantal locks op een mutex op te vragen. Of is het beter om een aanvullende recursive mutex aan te maken en dan in de garbage collector te proberen of de mutex gelocked kan worden. Als dat zo is, dan zouden alle threads klaar moeten zijn en hun eigen lock weggehaald moeten hebben. Of is er nog een betere optie 3?

[ Voor 47% gewijzigd door CurlyMo op 21-04-2014 10:51 ]

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
CurlyMo schreef op maandag 21 april 2014 @ 10:30:
Het probleem is opgelost door dit toe te voegen aan het begin van de thread functie:
Een alternatief is om signals te masken in de main thread, dan de child thread te starten (die erft de signal mask), en dan in de main thread signals te unmasken. Op die manier voorkom je de race condition waarbij een signal binnenkomt net voordat je pthread_sigmask() callt in thread().
Zover ik het begrijp kan normaal gesproken een signaal óók een thread stoppen. Door een sigmask toe te voegen zeg je tegen je thread, negeer het signaal waardoor de main_gc alle controle krijgt om zijn dingen te doen. Klopt dat?
De signal handler wordt aangeroepen in de context van één van de threads (die 'm niet gemaskt heeft) — welke precies hangt van je besturingssysteem af.

Wat je nu doet met setjmp() en longjmp() klopt nog steeds niet. Je mag niet meer jumpen naar de opgeslagen context wanneer je uit de functie waarin die context gecreëerd is gereturnt hebt. En je kunt eigenlijk niet doorgaan met de code in main() omdat je feitelijk niet returnt vanuit je signal handler.

edit:
Dat het lijkt te werken komt waarschijnlijk omdat de stack frame in gc_catch() op hetzelfde adres stond als de stack frame van de signal handler, waardoor een tweede keer returnen uit gc_catch() (wat eigenlijk niet mag!) effectief neerkomt op returnen uit de signal handler, die daarna main() resumet.

edit 2:
Als je GCC gebruikt kun je compileren met -fstack-protector-all en dan crasht 'ie ook onder Linux.

[ Voor 16% gewijzigd door Soultaker op 21-04-2014 16:15 ]


  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Soultaker schreef op maandag 21 april 2014 @ 15:58:
[...]

Een alternatief is om signals te masken in de main thread, dan de child thread te starten (die erft de signal mask), en dan in de main thread signals te unmasken. Op die manier voorkom je de race condition waarbij een signal binnenkomt net voordat je pthread_sigmask() callt in thread().
Wat bedoel je met de child thread?
De signal handler wordt aangeroepen in de context van één van de threads (die 'm niet gemaskt heeft) — welke precies hangt van je besturingssysteem af.

Wat je nu doet met setjmp() en longjmp() klopt nog steeds niet. Je mag niet meer jumpen naar de opgeslagen context wanneer je uit de functie waarin die context gecreëerd is gereturnt hebt. En je kunt eigenlijk niet doorgaan met de code in main() omdat je feitelijk niet returnt vanuit je signal handler.
Dit begrijp ik niet helemaal. Ook wat je hieronder zegt niet... Zou je een voorbeeld kunnen geven?
edit:
Dat het lijkt te werken komt waarschijnlijk omdat de stack frame in gc_catch() op hetzelfde adres stond als de stack frame van de signal handler, waardoor een tweede keer returnen uit gc_catch() (wat eigenlijk niet mag!) effectief neerkomt op returnen uit de signal handler, die daarna main() resumet.

edit 2:
Als je GCC gebruikt kun je compileren met -fstack-protector-all en dan crasht 'ie ook onder Linux.

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
CurlyMo schreef op maandag 21 april 2014 @ 17:24:
Wat bedoel je met de child thread?
De ene nieuwe thread in dit voorbeeld, die je vanuit main() start.

(Officieel hebben threads geen parent/child-relatie dus misschien moet ik 'm niet child thread maar tweede thread noemen.)
Dit begrijp ik niet helemaal. Ook wat je hieronder zegt niet... Zou je een voorbeeld kunnen geven?
Je kunt jouw programma versimpelen tot zoiets:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <setjmp.h>

static jmp_buf ctx;
static int var;

void foo()
{
    if (setjmp(ctx) == 0)
        return;
    var = 1;
}

int main()
{
    foo();
    if (var == 0) longjmp(ctx, 1);
    return 0;
}

Maar dit mag niet, want de context die je setjmp() creëert wordt geïnvalideert zodra je returnt vanuit foo(), en die mag je dus niet meer gebruiken in longjmp(). Waarom mag dat niet? Omdat de manual dat zegt:

setjmp()  and  longjmp(3) are useful for dealing with errors and inter-
rupts encountered in a low-level subroutine  of  a  program.   setjmp()
saves the stack context/environment in env for later use by longjmp(3).
The stack context will be invalidated  if  the  function  which  called
setjmp() returns.


Ok, maar waarom mag het niet? Omdat de state van je programma bestaat uit de waarden op de call stack gecombineerd met state in processor registers. In het stack frame van elke functie worden de lokale variabelen van die functie opgeslagen (en nog een aantal andere dingen: temporaries, returnadressen, gesavede registers, et cetera). Wanneer je een functie callt wordt er een nieuwe stack frame bovenop de stack toegevoegd, en wanneer je returnt wordt dat stackframe weer afgebroken en kan de ruimte opnieuw gebruikt worden.

Wat setjmp() feitelijk doet is alle processor-registers opslaan in de context variabele; daaronder valt onder andere de huidige instruction pointers en de stack en base pointer die het huidige stack frame identificeren. Maar setjmp() maakt geen kopie van de stack (dat zou veel te veel tijd kosten), en je kunt dus alleen de context herstellen als de stack ongewijzigd is gebleven. Vandaar dat je met longjmp() alleen terug mag jumpen naar een stack frame dat nog de stack staat.

Dit is bijvoorbeeld wél een geldig programma:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <setjmp.h>

static jmp_buf ctx;
static int var;

void bar()
{
    if (var == 0) longjmp(ctx, 1);
}

void foo()
{
    if (setjmp(ctx) == 0)
    {
        bar();
        return;
    }
    var = 1;
}

int main()
{
    foo();
    return 0;
}

Merk op dat in dit geval longjmp() uitgevoerd wordt vanuit bar() die is aangeroepen vanuit foo(): de context in ctx is dus nog geldig.

Probeer eerst dit deel even te doorgronden, dan kunnen we daarna verder met signal handlers, threads en synchronisatie, wat eigenlijk allemaal aparte (en behoorlijk ingewikkelde) dingen zijn.

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Soultaker schreef op maandag 21 april 2014 @ 17:50:
Je kunt jouw programma versimpelen tot zoiets:
Eerste voorbeeld
Is het niet zo dat de setjmp(ctx) blokkeert totdat dat hij wordt aangeroepen door longjmp? Dat dacht ik namelijk. Als ik het goed begrijp gebeurt dat niet en wordt door de return de context van foo() vrijgegeven waardoor deze niet meer te gebruiken is. Anders gezegd:

main roept foo aan. Daarin wordt een jump geset, maar door de return wordt foo vrijgegeven. De var wordt door de return niet op 1 gezet en daardoor wordt er een invalide longjmp aangeroepen. Oftewel een roep naar een niet meer bestaande functie.

Tweede voorbeeld
Nu wordt de bar aangeroepen (met daarin de longjmp) voordat de return plaatsvind. Hierdoor blijft de context van de foo bestaan terwijl bar wordt aangeroepen.
Probeer eerst dit deel even te doorgronden, dan kunnen we daarna verder met signal handlers, threads en synchronisatie, wat eigenlijk allemaal aparte (en behoorlijk ingewikkelde) dingen zijn.
Kom maar op. Mits ik geslaagd ben :)

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
CurlyMo schreef op maandag 21 april 2014 @ 18:46:
Is het niet zo dat de setjmp(ctx) blokkeert totdat dat hij wordt aangeroepen door longjmp? Dat dacht ik namelijk.
Nee, als dat zo zou zijn zou je thread helemaal niet gestart worden.
main roept foo aan. Daarin wordt een jump geset, maar door de return wordt foo vrijgegeven. De var wordt door de return niet op 1 gezet en daardoor wordt er een invalide longjmp aangeroepen. Oftewel een roep naar een niet meer bestaande functie.
Klopt precies.

Goed, nu die signal handler. Als de kernel een signal naar een proces wil sturen dan onderbreekt 'ie een geschikte thread om 'm op te dispatchen, pusht 'ie de huidige state op de stack (die state bestaat voornamelijk uit dezelfde data als zo'n jmp_buf — kijk in sys/ucontext.h voor de details als je dat interessant vindt) en pusht tenslotte als return address het adres van een of andere cleanup functie alvorens naar de geïnstalleerde signal handler te jumpen.

Normaal gesproken return je vroeger of later uit de signal handler (beter vroeger dan later!), dan gaat control naar de cleanup functie, die de gepushte state van de stack haalt en herstelt, zodat de onderbroken thread verder kan gaan waar 'ie mee bezig was.

In jouw code return je echter niet uit gc_handler(), omdat je longjmp() aanroept, en die functie returnt niet. De thread waarop de signal handler uitgevoerd wordt, wordt dus doodgemaakt. En als de handler op een andere thread uitgevoerd wordt, dan zit je met twee threads op dezelfde stack te werken, wat blijkbaar ook een crash oplevert.

Als je je beperkt tot geldige C code zou je dus zoiets krijgen:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static jmp_buf ctx;
static int var;

void sig_handler(int signo)
{
    puts("Nu zijn we hier.");
    longjmp(ctx, 1);
    puts("Hier komen we nooit!");
}

int main()
{
    struct sigaction act = { .sa_handler = &sig_handler };
    sigaction(SIGINT, &act, NULL);
    if (setjmp(ctx) == 0)
    {
        while (!var)
        {
            puts("Sleeping...");
            sleep(1);
        }
        puts("Hier komen we ook nooit!");
        return;
    }
    var = 1;
    return 0;
}

... maar merk op dat je nu nooit uit de main-loop komt. Deze hele constructie werkt dus niet. (En dat is nog vóór je er een thread aan hebt toegevoegd!)

Je moet het dus eigenlijk veel simpeler houden:
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static jmp_buf ctx;
static int var;

void sig_handler(int signo)
{
    puts("INT caught");
    var = 1;
}

int main()
{
    struct sigaction act = { .sa_handler = &sig_handler };
    sigaction(SIGINT, &act, NULL);
    while (!var)
    {
        puts("Sleeping...");
        sleep(1);
    }
    puts("Einde.");
    return 0;
}

... en het stoppen van de andere threads kun je dan beter aan het einde van de main loop implementeren.

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Mijn conclusie was inderdaad al om de jump te verwijderen en gelijk in de gc_handler de main_gc aan te roepen. Goed uitgelegd dus :)

Waar ik nog wel benieuwd naar ben is hoe je (op een nette manier) zorgt dat een signal gemasked kan worden voordat een thread wordt gestart en daarna de signal weer wordt teruggezet.

[ Voor 38% gewijzigd door CurlyMo op 21-04-2014 22:44 ]

Sinds de 2 dagen regel reageer ik hier niet meer


  • Soultaker
  • Registratie: September 2000
  • Laatst online: 21-11 22:57
Dat was al langsgekomen: door de signals te masken wanneer je de thread aanmaakt:
C:
1
2
3
4
5
6
7
8
sigset_t new, old;
sigemptyset(&new);
sigaddset(&new, SIGINT);
sigaddset(&new, SIGQUIT);
sigaddset(&new, SIGTERM);
pthread_sigmask(SIG_BLOCK, &new, &old);
pthread_create(...);
pthread_sigmask(SIG_SETMASK, &old, NULL);

De nieuwe thread erft zijn signal mask immers van de thread die 'm maakt. (Dit staat ook ongeveer zo uitgelegd, met voorbeeld, in de pthread_sigmask man-page).

Merk overigens op dat het eigenlijk niet uitmaakt in welke thread signals uitgevoerd worden als je geen rare dingen doet zoals longjmp().

  • CurlyMo
  • Registratie: Februari 2011
  • Laatst online: 13:24
Ik dacht dat je die pthread_sigmask in de thread zelf moest zetten. Niet begrepen dat het zo ook kon.
Is het sowieso niet het best om de signals uit te zetten in elke thread behalve de main? Dan weet ik tenminste zeker waar de signal wordt afgehandeld.


Zojuist de hier besproken veranderingen doorgevoerd in mijn programma en het werkt weer als een tierelier d:)b

[ Voor 61% gewijzigd door CurlyMo op 21-04-2014 23:50 ]

Sinds de 2 dagen regel reageer ik hier niet meer

Pagina: 1