2016-07-20 1 views
5

Ceci est une application assez simple qui crée un processus léger (thread) avec appel clone().Comportement étrange de clone

#define _GNU_SOURCE 

#include <sched.h> 
#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <errno.h> 
#include <stdlib.h> 
#include <time.h> 

#define STACK_SIZE 1024*1024 

int func(void* param) { 
    printf("I am func, pid %d\n", getpid());  
    return 0; 
} 

int main(int argc, char const *argv[]) { 
    printf("I am main, pid %d\n", getpid()); 
    void* ptr = malloc(STACK_SIZE); 

    printf("I am calling clone\n");    
    int res = clone(func, ptr + STACK_SIZE, CLONE_VM, NULL); 
    // works fine with sleep() call 
    // sleep(1); 

    if (res == -1) { 
     printf("clone error: %d", errno);  
    } else { 
     printf("I created child with pid: %d\n", res);  
    } 

    printf("Main done, pid %d\n", getpid());   
    return 0; 
} 

Voici les résultats:

Run 1:

➜ LFD401 ./clone 
I am main, pid 10974 
I am calling clone 
I created child with pid: 10975 
Main done, pid 10974 
I am func, pid 10975 

Run 2:

➜ LFD401 ./clone 
I am main, pid 10995 
I am calling clone 
I created child with pid: 10996 
I created child with pid: 10996 
I am func, pid 10996 
Main done, pid 10995 

Run 3:

➜ LFD401 ./clone 
I am main, pid 11037 
I am calling clone 
I created child with pid: 11038 
I created child with pid: 11038 
I am func, pid 11038 
I created child with pid: 11038 
I am func, pid 11038 
Main done, pid 11037 

Run 4:

➜ LFD401 ./clone 
I am main, pid 11062 
I am calling clone 
I created child with pid: 11063 
Main done, pid 11062 
Main done, pid 11062 
I am func, pid 11063 

Qu'est-ce qui se passe ici? Pourquoi le message "J'ai créé l'enfant" est parfois imprimé plusieurs fois?

J'ai également remarqué que l'ajout d'un délai après clone appel "corrige" le problème.

+1

Notez que sur un système rare où 'int/unsigned' est de 16 bits, mais' size_t' est de 32 bits: '#define STACK_SIZE 1024 * 1024 * ptr vide = malloc (STACK_SIZE); 'est un problème. À chaque fois que les maths sont faites dans une "constante", faites attention au débordement. Suggérer '#define STACK_SIZE ((size_t) 1024 * 1024)' – chux

+0

Puisqu'il est apparemment difficile de reproduire ce problème, peut-être auriez-vous l'amabilité de nous en dire plus sur votre architecture. Par exemple. Système d'exploitation et version; version glibc; version gcc. (Aussi, tous les indicateurs de compilateur non-par défaut que vous utilisez) – rici

+0

Ubuntu 16.04, GLIBC 2.23-0ubuntu3, gcc 5.4.0. CPU i7-5600U. Aucun indicateur de compilation supplémentaire. – lstipakov

Répondre

5

Vous avez une condition de concurrence (c'est-à-dire) vous n'avez pas la sécurité de thread implicite de stdio.

Le problème est encore plus grave. Vous pouvez obtenir des messages "func" en double. Le problème est que l'utilisation clone n'a pas les mêmes garanties que pthread_create. (c'est-à-dire) Vous faites pas obtenir les variantes de thread sécurisé de printf.

Je ne sais pas avec certitude, mais, IMO le verbiage sur les flux stdio et la sécurité des threads, en pratique, ne s'applique que lorsque vous utilisez pthreads. Par conséquent, vous devrez gérer votre propre verrouillage inter-thread. Voici une version de votre programme recodée pour utiliser pthread_create. Il semble fonctionner sans incident:

#define _GNU_SOURCE 

#include <sched.h> 
#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <errno.h> 
#include <stdlib.h> 
#include <time.h> 
#include <pthread.h> 

#define STACK_SIZE 1024*1024 

void *func(void* param) { 
    printf("I am func, pid %d\n", getpid()); 
    return (void *) 0; 
} 

int main(int argc, char const *argv[]) { 
    printf("I am main, pid %d\n", getpid()); 
    void* ptr = malloc(STACK_SIZE); 

    printf("I am calling clone\n"); 

    pthread_t tid; 
    pthread_create(&tid,NULL,func,NULL); 
    //int res = clone(func, ptr + STACK_SIZE, CLONE_VM, NULL); 
    int res = 0; 

    // works fine with sleep() call 
    // sleep(1); 

    if (res == -1) { 
     printf("clone error: %d", errno); 
    } else { 
     printf("I created child with pid: %d\n", res); 
    } 

    pthread_join(tid,NULL); 
    printf("Main done, pid %d\n", getpid()); 
    return 0; 
} 

Voici un script de test, je l'ai utilisé pour vérifier les erreurs [il est un peu rude, mais devrait être correct]. Courir contre votre version et il va avorter rapidement. La version pthread_create semble passer très bien

#!/usr/bin/perl 
# clonetest -- clone test 
# 
# arguments: 
# "-p0" -- suppress check for duplicate parent messages 
# "-c0" -- suppress check for duplicate child messages 
# 1 -- base name for program to test (e.g. for xyz.c, use xyz) 
# 2 -- [optional] number of test iterations (DEFAULT: 100000) 

master(@ARGV); 
exit(0); 

# master -- master control 
sub master 
{ 
    my(@argv) = @_; 
    my($arg,$sym); 

    while (1) { 
     $arg = $argv[0]; 
     last unless (defined($arg)); 

     last unless ($arg =~ s/^-(.)//); 
     $sym = $1; 

     shift(@argv); 

     $arg = 1 
      if ($arg eq ""); 

     $arg += 0; 
     ${"opt_$sym"} = $arg; 
    } 

    $opt_p //= 1; 
    $opt_c //= 1; 
    printf("clonetest: p=%d c=%d\n",$opt_p,$opt_c); 

    $xfile = shift(@argv); 
    $xfile //= "clone1"; 
    printf("clonetest: xfile='%s'\n",$xfile); 

    $itermax = shift(@argv); 
    $itermax //= 100000; 
    $itermax += 0; 
    printf("clonetest: itermax=%d\n",$itermax); 

    system("cc -o $xfile -O2 $xfile.c -lpthread"); 
    $code = $? >> 8; 
    die("master: compile error\n") 
     if ($code); 

    $logf = "/tmp/log"; 

    for ($iter = 1; $iter <= $itermax; ++$iter) { 
     printf("iter: %d\n",$iter) 
      if ($opt_v); 
     dotest($iter); 
    } 
} 

# dotest -- perform single test 
sub dotest 
{ 
    my($iter) = @_; 
    my($parcnt,$cldcnt); 
    my($xfsrc,$bf); 

    system("./$xfile > $logf"); 

    open($xfsrc,"<$logf") or 
     die("dotest: unable to open '$logf' -- $!\n"); 

    while ($bf = <$xfsrc>) { 
     chomp($bf); 

     if ($opt_p) { 
      while ($bf =~ /created/g) { 
       ++$parcnt; 
      } 
     } 

     if ($opt_c) { 
      while ($bf =~ /func/g) { 
       ++$cldcnt; 
      } 
     } 
    } 

    close($xfsrc); 

    if (($parcnt > 1) or ($cldcnt > 1)) { 
     printf("dotest: fail on %d -- parcnt=%d cldcnt=%d\n", 
      $iter,$parcnt,$cldcnt); 
     system("cat $logf"); 
     exit(1); 
    } 
} 

MISE À JOUR:

Avez-vous pu recréer problème de clone avec OPs?

Absolument.Avant que j'ai créé la version pthreads, en plus de tester la version originale de l'OP, je versions aussi créé que:

(1) ajouté setlinebuf au début de main

(2) ajouté fflush juste avant le clone et __fpurge comme la première déclaration de func

(3) a ajouté un fflush dans func avant la return 0

version (2) a éliminé le double messages parent, mais les messages enfants en double sont restés

Si vous souhaitez voir cela par vous-même, téléchargez la version OP à partir de la question, de ma version et du script de test. Ensuite, exécutez le script de test sur la version OP. J'ai posté suffisamment d'informations et de fichiers pour que tout le monde puisse recréer le problème.

Notez qu'en raison des différences entre mon système et les OP, je n'ai pas pu reproduire le problème en seulement 3-4 essais. Donc, c'est pourquoi j'ai créé le script.

Le script effectue 100 000 tests et le problème se manifeste généralement entre 5 000 et 15 000.

+0

Étiez-vous capable de recréer un problème d'OP avec un clone? – evaitl

+0

@evaitl Oui, absolument. Voir ma réponse mise à jour pour plus de détails. –

+0

Très cool +1. Je ne prévoyais pas d'utiliser clone(), mais maintenant j'ai une raison. – evaitl

3

Vos processus utilisent tous deux la même stdout (c'est-à-dire la structure standard C FILE struct), qui inclut un tampon partagé accidentellement. Cela cause sans aucun doute des problèmes.

+0

glibc printf est censé être [thread safe] (http://stackoverflow.com/a/468105/5058676) – evaitl

+0

@evaitl Cependant, 'clone()' ne crée pas "officiellement" un thread. Donc, il peut y avoir une configuration supplémentaire requise qui ne se produit pas. – duskwuff

+0

Je suppose, mais je ne vois pas la différence. La glibc docs [confirmez] (https://www.gnu.org/software/libc/manual/html_node/Streams-and-Threads.html) qu'il n'y a pas de problème avec l'écriture de plusieurs threads en sortie: * "yes, the le même travail peut être fait avec un appel fprintf mais ce n'est parfois pas possible "*. Incidemment, je ne peux pas recréer le problème d'OP. – evaitl

3

Je ne peux pas recréer le problème de l'OP, mais je ne pense pas que le printf soit réellement un problème.

glibc docs:

La norme POSIX exige que par défaut, les opérations de flux sont atomique. Par exemple, l'émission simultanée de deux opérations de flux pour le même flux dans deux threads entraînera l'exécution des opérations si elles ont été émises de manière séquentielle. Les opérations de tampon effectuées pendant la lecture ou l'écriture sont protégées contre d'autres utilisations du même flux . Pour ce faire, chaque flux possède un objet de verrou interne qui doit être acquis (implicitement) avant que tout travail puisse être effectué.

Edit:

Même si ce qui précède est vrai pour les fils, en tant que points RICI out, il y a un commentaire sur sourceware:

Fondamentalement, il n'y a rien que vous pouvez faire en toute sécurité avec CLONE_VM sauf si l'enfant se limite au calcul pur et aux appels système directs (via sys/syscall.h). Si vous utilisez l'une des bibliothèques standard, vous risquez que le parent et l'enfant ne se coupent mutuellement les états internes.Vous aussi avez des problèmes comme le fait que glibc met en cache le pid/tid dans l'espace utilisateur, et le fait que la glibc s'attend à toujours avoir un pointeur de thread valide que votre appel à cloner est incapable d'initialiser correctement car ne sait pas (et ne devrait pas savoir) la mise en œuvre interne des threads .

Apparemment, glibc n'est pas conçu pour fonctionner avec clone si CLONE_VM est défini mais pas CLONE_THREAD | CLONE_SIGHAND.

2

Ass tout le monde suggère: il semble vraiment être un problème avec, comment dois-je le mettre en cas de clone(), la sécurité des processus? Avec une esquisse d'une version verrouillable de printf (en utilisant write(2)) la sortie est comme prévu.

#define _GNU_SOURCE 

#include <sched.h> 
#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <errno.h> 
#include <stdlib.h> 
#include <time.h> 

#define STACK_SIZE 1024*1024 

// VERY rough attempt at a thread-safe printf 
#include <stdarg.h> 
#define SYNC_REALLOC_GROW 64 
int sync_printf(const char *format, ...) 
{ 
    int n, all = 0; 
    int size = 256; 
    char *p, *np; 
    va_list args; 

    if ((p = malloc(size)) == NULL) 
    return -1; 

    for (;;) { 
    va_start(args, format); 
    n = vsnprintf(p, size, format, args); 
    va_end(args); 
    if (n < 0) 
     return -1; 
    all += n; 
    if (n < size) 
     break; 
    size = n + SYNC_REALLOC_GROW; 
    if ((np = realloc(p, size)) == NULL) { 
     free(p); 
     return -1; 
    } else { 
     p = np; 
    } 
    } 
    // write(2) shoudl be threadsafe, so just in case 
    flockfile(stdout); 
    n = (int) write(fileno(stdout), p, all); 
    fflush(stdout); 
    funlockfile(stdout); 
    va_end(args); 
    free(p); 
    return n; 
} 


int func(void *param) 
{ 
    sync_printf("I am func, pid %d\n", getpid()); 
    return 0; 
} 

int main() 
{ 

    sync_printf("I am main, pid %d\n", getpid()); 
    void *ptr = malloc(STACK_SIZE); 

    sync_printf("I am calling clone\n"); 
    int res = clone(func, ptr + STACK_SIZE, CLONE_VM, NULL); 
    // works fine with sleep() call 
    // sleep(1); 

    if (res == -1) { 
    sync_printf("clone error: %d", errno); 
    } else { 
    sync_printf("I created child with pid: %d\n", res); 
    } 
    sync_printf("Main done, pid %d\n\n", getpid()); 
    return 0; 
} 

Pour la troisième fois: il est seulement un croquis, pas de temps pour une version robuste, mais qui ne doit pas vous empêcher d'écrire un.

2

Comme evaitl indique printf est documenté pour être thread-safe par la documentation de la glibc. BUT, cela suppose généralement que vous utilisez la fonction de la glibc désignée pour créer des unités d'exécution (c'est-à-dire, pthread_create()). Si vous ne le faites pas, alors vous êtes seul. Le verrou pris par printf() est recursive (voir flockfile). Cela signifie que si le verrou est déjà pris, l'implémentation vérifie le propriétaire du verrou par rapport au casier. Si le casier est le même que le propriétaire, la tentative de verrouillage réussit.

Pour distinguer les différents threads, vous devez configurer correctement TLS, ce que vous ne faites pas, mais pthread_create(). Ce que je devine est que dans votre cas, la variable TLS qui identifie le thread est la même pour les deux threads, de sorte que vous finissez par prendre le verrou.

TL; DR: s'il vous plaît utiliser pthread_create()