Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

[C/Unix] sem_wait()

13 réponses
Avatar
JKB
Bonjour à tous,

Je suis en train de chercher un bug aléatoire dans un code
multithreadé. Je suis convaincu qu'il s'agit d'un problème de timing.
En débugant, je viens de tomber sur un truc un peu bizarre dans le code
suivant. J'avoue ne pas trop comprendre.

Variables :

- semaphore est un entier;
- semaphore_liste_threads est un semaphore (variable globale);
- semaphore_fork_processus_courant est une clef renvoyant un semaphore
local au thread;
- BUG est une macro dont le but est d'envoyer au processus un SIGBUS
si le premier argument est non nul (au passage, avant le SIGBUS, elle
exécute le second argument).

if (semaphore == 1)
{
while(sem_wait(&semaphore_liste_threads) == -1)
{
if (errno != EINTR)
{
pthread_sigmask(SIG_SETMASK, &oldset, NULL);

while(sem_wait((sem_t *) pthread_getspecific(
semaphore_fork_processus_courant)) == -1)
{
if (errno != EINTR)
{
BUG(1, printf("Lock error !\n")); // (*)
return;
}
}

BUG(1, printf("Lock error !\n"));
return;
}
}
}

Mon programme vient de planter sur un SIGBUS à la ligne (*).
Admettons. Ce qui me pose problème, c'est la valeur de errno :
4063235, qui ne correspond à aucune erreur renvoyée normalement par
sem_wait(). Si je suis l'exécution du code, pour planter là où il
plante, il a déjà fallu que la valeur de errno ne soit pas EINTR après
le premier sem_wait(). Or ce sémaphore est valide au moment où j'appelle
sem_wait().

Je n'ai _jamais_ vu le problème avec l'option -O2 de gcc. Cela
apparaît avec -O3, mais je ne sais pas si le fait de ne pas le voir
signifie qu'il s'agit d'un bug de gcc. Personnellement, je préférerais
pouvoir garder l'option de compilation -O3 car le programme en question
effectue 2E6 cycles par seconde avec -O3 contre 1.7E6 avec -O2.

J'essaye actuellement de reproduire le même fonctionnement sur une
sparc (Linux, même compilo) et jusqu'à présent, je n'y suis pas arrivé.
J'ai essayé sous Solaris 10/Sparc sans voir ce problème.

Le gcc en question est

cauchy:[~] > gcc -v
Using built-in specs.
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 4.3.3-14'
--with-bugurl=file:///usr/share/doc/gcc-4.3/README.Bugs
--enable-languages=c,c++,fortran,objc,obj-c++ --prefix=/usr
--enable-shared --enable-multiarch --enable-linker-build-id
--with-system-zlib --libexecdir=/usr/lib --without-included-gettext
--enable-threads=posix --enable-nls
--with-gxx-include-dir=/usr/include/c++/4.3 --program-suffix=-4.3
--enable-clocale=gnu --enable-libstdcxx-debug --enable-objc-gc
--enable-mpfr --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.3.3 (Debian 4.3.3-14)
cauchy:[~] >

Une idée ?

Cordialement,

JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2% de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.

3 réponses

1 2
Avatar
michko
On 7 août, 21:06, JKB wrote:
Le 07-08-2009, ? propos de
Re: [C/Unix] sem_wait(),
 Mickaël Wolff ?crivait dans fr.comp.lang.c :

> JKB wrote:

>>        Raté ;-) Les printf() sont dans le code originel prot égés par des
>> sémaphores. Pour faire simple, le programme crée un sémaphore no mmé
>> utilisé par tous les processus et threads de mon programme de calcul . Ça
>> me permet de gérer de façon transparente les problèmes d'écrit ure et de
>> lecture sur les terminaux.

>    Essayes quand meme d'utiliser les semaphores fournis par la
> bibliothèque pour vérifier que ce ne sont pas les tiens qui décon nent.

>    Au fait, est-ce que tu link avec pthread ?

        Naturellement.

        JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2 % de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.



Tu ne peux pas envoyer ton code source complet ?
Avatar
JKB
Le 08-08-2009, ? propos de
Re: sem_wait(),
michko ?crivait dans fr.comp.lang.c :
On 7 août, 21:06, JKB wrote:
Le 07-08-2009, ? propos de
Re: [C/Unix] sem_wait(),
 Mickaël Wolff ?crivait dans fr.comp.lang.c :

> JKB wrote:

>>        Raté ;-) Les printf() sont dans le code originel protégés par des
>> sémaphores. Pour faire simple, le programme crée un sémaphore nommé
>> utilisé par tous les processus et threads de mon programme de calcul. Ça
>> me permet de gérer de façon transparente les problèmes d'écriture et de
>> lecture sur les terminaux.

>    Essayes quand meme d'utiliser les semaphores fournis par la
> bibliothèque pour vérifier que ce ne sont pas les tiens qui déconnent.

>    Au fait, est-ce que tu link avec pthread ?

        Naturellement.

        JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2% de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.



Tu ne peux pas envoyer ton code source complet ?



Le source complet fait quelques 17 Mo... Par contre, je peux coller
ici la fonction qui merdoie. J'ai l'affreuse impression qu'elle ne pose
problème qu'en cas d'arrivée de signal asynchrone.

typedef struct thread
{
pid_t pid;
pthread_t tid;

logical1 thread_principal;

struct_processus *s_etat_processus;
} struct_thread;

typedef struct liste_chainee_volatile
{
volatile struct liste_chainee_volatile *suivant;
volatile void *donnee;
} struct_liste_chainee_volatile;


static volatile struct_liste_chainee_volatile *liste_threads
= NULL;
static volatile struct_liste_chainee_volatile *liste_threads_surveillance
= NULL;

void
retrait_thread(struct_processus *s_etat_processus)
{
volatile struct_liste_chainee_volatile *l_element_precedent;
volatile struct_liste_chainee_volatile *l_element_courant;

while(sem_wait(&semaphore_liste_threads) == -1)
{
if (errno != EINTR)
{
(*s_etat_processus).erreur_systeme = d_es_processus;
return;
}
}

l_element_precedent = NULL;
l_element_courant = liste_threads;

while(l_element_courant != NULL)
{
if (((*((struct_thread *) (*l_element_courant).donnee)).pid
== getpid()) && (pthread_equal((*((struct_thread *)
(*l_element_courant).donnee)).tid, pthread_self()) != 0))
{
break;
}

l_element_precedent = l_element_courant;
l_element_courant = (*l_element_courant).suivant;
}

if (l_element_courant == NULL)
{
sem_post(&semaphore_liste_threads);
(*s_etat_processus).erreur_systeme = d_es_processus;
return;
}

if (l_element_precedent == NULL)
{
liste_threads = (*l_element_courant).suivant;
}
else
{
(*l_element_precedent).suivant = (*l_element_courant).suivant;
}


free((void *) (*l_element_courant).donnee); // <- deadlock
free((struct_liste_chainee_volatile *) l_element_courant);

free(s_etat_processus);

if (sem_post(&semaphore_liste_threads) != 0)
{
//(*s_etat_processus).erreur_systeme = d_es_processus;
return;
}

return;
}

Elle ne pose problème que très rarement, mais lorsque ça coince, ça
coince toujours dans la ligne marquée 'deadlock' ou la suivante. Au
debugger, l_element_courant->suivant pointe sur autre chose qu'un
élément de la liste chaînée (?) comme si cette valeur était changée dans
le dos de ma fonction, ce qui ne risque pas d'arriver, toute
modification de la liste chaînée étant protégée par un sémaphore.
Je suis actuellement en train de faire tourner ledit programme avec une
macro redéfinissant sem_wait() et générant un SIGBUS si un
sem_wait(&semaphore_liste_threads) ne met pas le sémaphore à 0. Pour
l'instant, je n'ai levé aucun loup. J'ai fait tourner le programme sous
valgrind pour voir si j'avais des problèmes de corruption mémoire, cela
n'a strictement rien donné. Ça pourrait être une réutilisation d'un
pointeur après sa libération, mais je ne vois vraiment pas où (et
surtout, je ne vois pas pourquoi cela arriverait toujours au même
endroit).

Je dois préciser que cette fonction n'est jamais appelée depuis un
gestionnaire de signal. Par contre, les gestionnaires de signaux
utilisent la fameuse liste chaînée (en positionnant le sémaphore à la
valeur qui va bien) pour récupérer des données. Aucun gestionnaire de
signal ne modifie cette #@{^#@{_ de liste chaînée et le seul endroit où
un de ses maillons peut être enlevé est la fonction retrait_thread().
La structure s_processus est une structure assez énorme contenant en
particulier des variables permettant de remonter de façon propres toutes
les erreurs rencontrées. Tout cela est multithreadé, donc il faut
imaginer qu'un gestionnaire de signal peut être appelé en même temps
qu'une modification de la liste par retrait_thread().

Je teste sous Linux/amd64 (debian). Les fonctions sem_post() et
sem_wait() sont surchargées sous Solaris pour éviter le gag connu du
EINTR. Sous Linux, ces fonctions ne renvoient visiblement pas EINTR dans
le cas d'appel à un gestionnaire de signal, en tout cas, ce n'est pas
indiqué dans les pages man, mais un bout de man 7 signal me fait
douter). Est-ce vrai (je commence à douter fortement) ?

Ce qui m'échappe, c'est que selon les docs que je lis, j'ai au choix
sem_post() qui peut être appelé depuis un gestionnaire de signal (mais
pas sem_wait()), ou sem_post() et sem_wait() async safe tous les deux.
Qu'en est-il ? Si je ne peux utiliser sem_wait() dans un gestionnaire de
signal, que puis-je utiliser pour verrouiller mon accès à cette liste
chaînée ?

Bref, deux questions avant d'aller plus loin.
1/ EINTR renvoyé par un gestionnaire de signal. sem_post() ne peut pas
être interrompu et ne peut renoyer EINTR. Par contre sem_wait() le peut.
Tout ce qui est fprintf() semble ne pas renvoyer EINTR sous Linux même
en cas de traitement d'un signal. Vrai ou faux ?
2/ quid de sem_wait() dans un gestionnaire de signal ?

Cordialement,

JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2% de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.
Avatar
JKB
Le 08-08-2009, ? propos de
Re: sem_wait(),
JKB ?crivait dans fr.comp.lang.c :
Le 08-08-2009, ? propos de
Re: sem_wait(),
michko ?crivait dans fr.comp.lang.c :
On 7 août, 21:06, JKB wrote:
Le 07-08-2009, ? propos de
Re: [C/Unix] sem_wait(),
 Mickaël Wolff ?crivait dans fr.comp.lang.c :

> JKB wrote:

>>        Raté ;-) Les printf() sont dans le code originel protégés par des
>> sémaphores. Pour faire simple, le programme crée un sémaphore nommé
>> utilisé par tous les processus et threads de mon programme de calcul. Ça
>> me permet de gérer de façon transparente les problèmes d'écriture et de
>> lecture sur les terminaux.

>    Essayes quand meme d'utiliser les semaphores fournis par la
> bibliothèque pour vérifier que ce ne sont pas les tiens qui déconnent.

>    Au fait, est-ce que tu link avec pthread ?

        Naturellement.

        JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2% de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.



Tu ne peux pas envoyer ton code source complet ?



Le source complet fait quelques 17 Mo... Par contre, je peux coller
ici la fonction qui merdoie. J'ai l'affreuse impression qu'elle ne pose
problème qu'en cas d'arrivée de signal asynchrone.



Bon, j'ai trouvé, et ça fait encore partie des trucs lus en biais par
Drepper... Et ça n'a pas été facile à débugguer !...

Comme dit plus haut, mon programme lance un tas de threads qui se
terminent par un pthread_exit(NULL). Ces threads recoivent des signaux
asynchrones dont les portions critiques sont protégées par des
sémaphores, lesquels sont accessibles au travers de pthread_getspecific().
Pour que les sem_wait() ne bloquent pas, ces signaux sont bloqués dans
le programme susceptible d'être interrompu au mauvais moment.
Je pensais naïvement que le pthread_exit() faisait les choses dans
l'ordre, à savoir interdisait l'arrivée de signaux asynchrones au thread
mourant puis libérait les clefs. En fait, il libère les clefs mais
n'empêche nullement l'arrivée de signaux. J'avais donc de temps en temps
un signal asynchrone appelé depuis pthread_exit(NULL) qui utilisait une
clef libérée par ce même pthread_exit(), d'où une corruption du tas et
un free() qui bloque un peu plus loin dans le code sur un mutex interneà
la glibc... Pour information, ma glibc est la 2.9-23 (debian).

Visiblement, Solaris fait les choses dans l'ordre, parce que je n'ai
jamais observé le même dysfonctionnement (même en écrivant un programme
minimal qui force les choses).

Voilà, voilà...

JKB

--
Le cerveau, c'est un véritable scandale écologique. Il représente 2% de notre
masse corporelle, mais disperse à lui seul 25% de l'énergie que nous
consommons tous les jours.
1 2