c - 克隆人的奇怪行为

标签 c linux multithreading clone lightweight-processes

这是一个相当简单的应用程序,它通过 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

这是怎么回事?为什么“I created child”消息有时会打印多次?

我还注意到在 clone 调用后添加延迟“修复”了这个问题。

最佳答案

您有竞争条件(即)您没有 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);
    }
}

更新:

Were you able to recreate OPs problem with clone?

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

(1) 添加了 setlinebufmain 的开始

(2) 在 clone__fpurge 之前添加 fflush 作为 func 的第一条语句

(3) 在return 0

之前在func中添加了一个fflush

版本(2)消除了重复的父消息,但保留了重复的子消息

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

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

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

脚本执行 100,000 次测试运行,通常问题会在 5000-15000 次内显现。

关于c - 克隆人的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38490401/

相关文章:

java - 迭代同步包装器是否安全?

c++ - SDL 在程序可执行文件中嵌入图像

html - 下载 HTML 页面 + 所有组件的最佳工具/库

linux - 在 bash cron 中处理不同的目录

linux - 将多个标准输出重定向到单个文件

java - Java线程什么时候空闲?

c++ - Boost线程开销

c - 需要帮助识别以空格分隔的最长字符串

c++ - 从 char 中删除转义字符

c - pthread_cancel() 本身导致内存泄漏