5

这是一个相当简单的应用程序,它通过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呼叫“修复”问题后添加延迟。

4

5 回答 5

5

您有一个竞争条件(即)您没有 stdio 的隐含线程安全性。

问题更加严重。您可以获得重复的“func”消息。

问题是 usingclone没有与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);
    }
}

更新:

您是否能够通过克隆重新创建 OP 问题?

绝对地。在我创建 pthreads 版本之前,除了测试 OP 的原始版本之外,我还创建了以下版本:

(1) 添加setlinebuf到开头main

(2)在and作为第一个语句fflush之前添加clone__fpurgefunc

(3)在前面加一个fflushinfuncreturn 0

版本(2)消除了重复的父消息,但重复的子消息仍然存在

如果您想亲自查看此内容,请从问题、我的版本和测试脚本中下载 OP 的版本。然后,在 OP 的版本上运行测试脚本。

我发布了足够的信息和文件,以便任何人都可以重现问题。

请注意,由于我的系统和 OP 之间的差异,我一开始仅尝试 3-4 次就无法重现该问题。所以,这就是我创建脚本的原因。

该脚本进行了 100,000 次测试运行,通常问题会在 5000-15000 内显现出来。

于 2016-07-20T23:01:44.920 回答
3

我无法重现 OP 的问题,但我认为 printf 的问题实际上不是问题。

glibc 文档

POSIX 标准要求默认情况下流操作是原子的。即,同时在两个线程中为同一流发出两个流操作将导致操作被执行,就好像它们是按顺序发出的一样。读取或写入时执行的缓冲区操作受到保护,不会被同一流用于其他用途。为此,每个流都有一个内部锁对象,在完成任何工作之前必须(隐式)获取该对象。

编辑:

正如 rici 指出的那样,尽管以上对于线程来说是正确的,但在sourceware上有一条评论:

基本上,除非孩子将自己限制为纯计算和直接系统调用(通过 sys/syscall.h),否则您无法安全地使用 CLONE_VM 做任何事情。如果您使用任何标准库,您可能会冒着父子关系破坏彼此内部状态的风险。您还有一些问题,例如 glibc 将 pid/tid 缓存在用户空间中,以及 glibc 期望始终有一个有效的线程指针,您对 clone 的调用无法正确初始化,因为它不知道(也不应该知道) 线程的内部实现。

显然,如果 CLONE_VM 设置但 CLONE_THREAD|CLONE_SIGHAND 未设置,glibc 不适合与克隆一起使用。

于 2016-07-20T22:16:29.583 回答
2

您的进程都使用相同的stdout(即 C 标准库FILE结构),其中包括一个意外共享的缓冲区。这无疑会造成问题。

于 2016-07-20T21:29:40.563 回答
2

屁股每个人都建议:这似乎真的是一个问题,我该怎么把它放在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;
}

第三次:这只是一个草图,没有时间写一个强大的版本,但这不应该妨碍你写一个。

于 2016-07-20T22:20:37.803 回答
2

正如evaitl指出的那样printf,glibc 的文档记录了它是线程安全的。但是,这通常假定您正在使用指定的 glibc 函数来创建线程(即pthread_create())。如果你不这样做,那么你就靠自己了。

所采用的锁printf()递归的(请参阅参考资料flockfile)。这意味着如果锁已被占用,则实现会根据锁柜检查锁的所有者。如果储物柜与所有者相同,则锁定尝试成功。

要区分不同的线程,您需要正确设置TLS,您不这样做,但pthread_create()确实如此。我猜发生的是,在您的情况下,标识线程的 TLS 变量对于两个线程都是相同的,因此您最终获得了锁。

TL;DR:请使用pthread_create()

于 2016-07-20T23:07:29.090 回答