Странное поведение клона

Это довольно простое приложение, которое создает легкий процесс (поток) с 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;
}

Вот результаты:

Прогон 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

Прогон 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

Прогон 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

Прогон 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

Что здесь происходит? Почему сообщение "Я создал ребенка" иногда печатается несколько раз?

Также я заметил, что добавление задержки после clone звоните "исправляет" проблему.

5 ответов

Решение

У вас есть состояние гонки (то есть) у вас нет подразумеваемой безопасности потока stdio.

Проблема еще серьезнее. Вы можете получить дубликаты "func" сообщений.

Проблема в том, что с помощью clone не имеет таких же гарантий, как pthread_create, (т.е.) Вы не получаете потокобезопасные варианты printf,

Я не знаю наверняка, но, IMO, словоблудие о потоках stdio и безопасности потоков, на практике, применимо только при использовании pthreads,

Итак, вам придется обрабатывать свою собственную блокировку между нитями.

Вот версия вашей программы, перекодированная для использования pthread_create, Кажется, работает без инцидентов:

#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;
}

Вот тестовый скрипт, который я использовал для проверки ошибок [он немного грубоват, но должен быть в порядке]. Запустите вашу версию, и она быстро прекратит работу. pthread_create версия вроде проходит просто отлично

#!/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);
    }
}

ОБНОВИТЬ:

Удалось ли вам воссоздать проблему ОП с клоном?

Абсолютно. Перед созданием версии pthreads, в дополнение к тестированию исходной версии OP, я также создал версии, которые:

(1) добавлено setlinebuf к началу main

(2) добавлено fflush как раз перед clone а также __fpurge как первое утверждение func

(3) добавил fflush в func перед return 0

Версия (2) устраняет дубликаты родительских сообщений, но дубликаты дочерних сообщений остаются

Если вы хотите сами убедиться в этом, загрузите версию OP из вопроса, мою версию и тестовый скрипт. Затем запустите тестовый скрипт для версии OP.

Я разместил достаточно информации и файлов, чтобы каждый мог воссоздать проблему.

Обратите внимание, что из-за различий между моей системой и операционными системами, я не мог сначала воспроизвести проблему всего за 3-4 попытки. Вот почему я создал сценарий.

Сценарий выполняет 100000 тестовых прогонов, и обычно проблема проявляется в течение 5000-15000.

Ваши процессы используют одинаково stdout (то есть стандартная библиотека C FILE struct), которая включает в себя случайно общий буфер. Это, несомненно, вызывает проблемы.

Я не могу воссоздать проблему OP, но я не думаю, что printf на самом деле проблема.

glibc docs:

Стандарт POSIX требует, чтобы по умолчанию потоковые операции были атомарными. То есть одновременная выдача двух потоковых операций для одного и того же потока в двух потоках приведет к выполнению операций, как если бы они были выполнены последовательно. Операции с буфером, выполняемые во время чтения или записи, защищены от другого использования того же потока. Для этого каждый поток имеет внутренний объект блокировки, который (неявно) должен быть получен до того, как любая работа может быть выполнена.

Редактировать:

Хотя вышеприведенное верно и для потоков, как указывает rici, на sourceware есть комментарий:

По сути, вы ничего не можете сделать с CLONE_VM, если только дочерний процесс не ограничивается чисто вычислениями и прямыми вызовами (через sys/syscall.h). Если вы используете какую-либо стандартную библиотеку, вы рискуете, что родительский и дочерний блокируют внутренние состояния друг друга. У вас также есть проблемы, такие как тот факт, что glibc кэширует pid / tid в пользовательском пространстве, и тот факт, что glibc ожидает всегда иметь действительный указатель потока, который ваш вызов клону не может правильно инициализировать, потому что он не знает (и не должен знать) внутренняя реализация потоков.

По-видимому, glibc не предназначен для работы с клоном, если установлен CLONE_VM, а CLONE_THREAD|CLONE_SIGHAND - нет.

Задница всем подсказывает: действительно, кажется, проблема с тем, как бы это выразить в случае clone()безопасность процесса? С черновым эскизом блокирующей версии printf (используя write(2)) результат соответствует ожиданиям.

#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;
}

В третий раз: это всего лишь эскиз, не время для надежной версии, но это не должно помешать вам написать его.

Как указывает evaitl printf документируется как поточно-ориентированный в документации glibc. НО, это обычно предполагает, что вы используете назначенную функцию glibc для создания потоков (то есть pthread_create()). Если вы этого не сделаете, то вы сами по себе.

Замок взят printf() является рекурсивным (см. flockfile). Это означает, что, если блокировка уже взята, реализация проверяет владельца блокировки по отношению к шкафчику. Если шкафчик совпадает с владельцем, попытка блокировки завершается успешно.

Чтобы различать разные потоки, вам нужно правильно настроить TLS, чего вы не делаете, но pthread_create() делает. Я предполагаю, что случается так, что в вашем случае переменная TLS, которая идентифицирует поток, одинакова для обоих потоков, поэтому вы в конечном итоге берете блокировку.

TL; DR: пожалуйста, используйте pthread_create()

Другие вопросы по тегам