C中的两个进程从同一个文件读取问题

时间:2018-12-27 01:59:58

标签: c linux process

我尝试使用父进程和子进程从文件中读取数字。

我有一个包含1至100之间的质数的文件。我尝试使用父进程读取前10个数字(效果很好)。之后,我使用fork创建一个子进程,并读取接下来的10个数字(效果也不错)。我使用wait()来使父亲进程等待孩子完成。然后,我使用kill()杀死子进程。然后,我尝试继续使用父进程从文件中读取剩余的数字,但结果与预期的不同:

这是我的代码:

pid_t create_process()
{
    pid_t pid;

do {

    pid = fork();       

} while (pid == -1 && errno == EAGAIN); 

return pid;
}

int main()
{
FILE *fichier = fopen("entiers.txt","r");
int i=0;
int n=0;

if (fichier != 0)
{

    printf("I am the father process of the pid %d\n",getpid());
    for(i=0; i<10; i++)
    {
        fscanf(fichier, "%d\n", &n);
        printf("%d\n",n);
    }

    pid_t pid = create_process();

    if(pid)
    {
        wait(NULL);

        printf("I am the father process of the pid %d\n",getpid());
        do
        {   
            n = fgetc(fichier);
            printf("%d\n",n);               
        } while (n != EOF);

        fclose(fichier);

        kill(pid,SIGKILL);
    }

    if (!pid)
    {
        printf("I am the child process of the pid %d\n",getpid());

        for(i=0; i<10; i++)
        {
            fscanf(fichier, "%d\n", &n);
            printf("%d\n",n);
        }


    }

}

return EXIT_SUCCESS;
}

这是我的文件

1
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

这是我的输出:

I am the father process of the pid: 8213

1
2
3
5
7
11
13
17
19
23

I am the child process of the pid: 8214

29
31
37
41
43
47
53
59
61
67

I am the father process of the pid: 8213

50
57
10
51
49
10
51
55
10
52
49
10
52
51
10
52
55
10
53
51
10
53
57
10
54
49
10
54
55
10
55
49
10
55
51
10
55
57
10
56
51
10
56
57
10
57
55
10
55
49
10
55
51
10
55
57
10
56
51
10
56
57
10
57
55
10
-1

有帮助吗?

3 个答案:

答案 0 :(得分:2)

基本答案

鉴于数据文件的长度为73个字节(给定或占用-您可能在空白处留有余地,我没想到),因此对fscanf()的第一次调用会将整个文件读入内存。然后,父进程从内存中读取10行,将读取指针移动到标准I / O缓冲区中。 fscanf()格式字符串中的尾随换行符并不是真正需要的; %d会跳过包含换行符的空格,并且如果输入不是来自文件,则尾随的空白行将是非常糟糕的用户体验-用户必须输入下一个数字(的开头)完成当前输入。 (请参见scanf() leaves the newline in the bufferWhat is the effect of trailing white space in a scanf() format string?。)

然后流程分叉。子代是父代的精确副本,因此它将继续读取父代停止的地方,并按预期打印10个数字,然后退出。

然后,父进程继续。它没有做任何改变指针在内存中的位置的操作,因此它从中断处继续执行。但是,读取代码现在可以读取单个字符并打印其十进制值,因此它变为50, 57, 10-'2''9''\n'的字符代码。因此,对于输入中所有其他质数,输出都会继续。

您确实需要修正输入内容才能使用fscanf()而不是fgetc()来恢复。

除了从缓冲的I / O更改为无缓冲的I / O之外,父母没有其他明智的方法来知道孩子所做的事情。如果您切换到无缓冲I / O,则在打开文件之后但对文件流执行任何其他操作之前,通过调用setbuf(fichier, NULL);setvbuf(fichier, NULL, _IONBF, 0);,那么您会看到父进程从中断处继续执行

一个旁注:我不相信create_process()中的循环-如果没有足够的资源,至少要稍等一下,让系统有时间找到一些资源,但这是更常见的将“资源不足”视为致命错误。

另一个说明:向已经死掉的进程发送信号(因为您等待它死掉)不会起作用。

这是一些修改后的代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

static pid_t create_process(void)
{
    pid_t pid = fork();
    if (pid < 0)
    {
        fprintf(stderr, "Failed to fork\n");
        exit(1);
    }
    return pid;
}

int main(void)
{
    const char filename[] = "entiers.txt";
    FILE *fichier = fopen(filename, "r");
    int i = 0;
    int n = 0;

    // setbuf(fichier, NULL);
    // setvbuf(fichier, NULL, _IONBF, 0);

    if (fichier == 0)
    {
        fprintf(stderr, "Failed to open file '%s' for reading\n", filename);
        exit(1);
    }

    printf("I am the parent process with the pid %d\n", getpid());
    for (i = 0; i < 10; i++)
    {
        if (fscanf(fichier, "%d", &n) != 1)
            break;
        printf("%d\n", n);
    }

    pid_t pid = create_process();

    if (pid == 0)
    {
        printf("I am the child process with the pid %d\n", getpid());
        for (i = 0; i < 10; i++)
        {
            if (fscanf(fichier, "%d", &n) != 1)
                break;
            printf("%d\n", n);
        }
    }
    else
    {
        wait(NULL);
        printf("I am the parent process with the pid %d\n", getpid());
        while (fscanf(fichier, "%d", &n) == 1)
            printf("%d\n", n);
    }

    fclose(fichier);

    return EXIT_SUCCESS;
}

示例输出:

I am the parent process with the pid 15704
2
3
5
7
11
13
17
19
23
29
I am the child process with the pid 15705
31
37
41
43
47
53
59
61
67
71
I am the parent process with the pid 15704
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

通常,诸如此类的问题涉及文件描述符I / O,并且讨论必须涵盖开放文件描述符和开放文件描述之间的差异,并解释进程之间共享的内容和不共享的内容。由于输入文件非常小,因此此代码不是问题。如果素数表上升到例如999983(最大的素数小于一百万),并且子进程读取了更多的数据,那么您将完全看到不同的效果。

scanf()格式的字符串中未缓冲的输入和尾随换行符

经验观察表明,当上面显示的代码的原始版本在父级的第一个读取循环和子级的读取循环中都具有scanf("%d\n", &n)时,程序被配置为使用无缓冲输入,则输出看起来像:

…
67
71
I am the parent process with the pid 27071
33
79
…

乍看之下不会出现33的地方。但是,有什么错误的解释。

流中至少有一个字节的推回(即使没有缓冲),因此在父派生的那一点,父级和子级在推回中都具有3中的31位置(因为换行符被读取为空格字符,并且第一个非空白,也就是包含3的行的31被读取并推回输入缓冲区)。

子级几乎是父级的完全相同的副本,读取后退字符并继续1并获取换行符,然后获取3的{​​{1}},并输出{如您所料{1}}。这一直持续到它在37的开头读取31并将其推回到其自己的输入缓冲区中为止,但这当然对父级的输入缓冲区没有影响(它们是独立的进程)。孩子退出。

父母继续。它的推回位置有一个7,然后从73获取3(因为父级和子级共享相同的打开文件描述,并且读取位置与该描述相关联,而不是描述符,因此子级移动了读取位置),然后它获取换行符并终止其扫描(最后一个循环仍然缺少3格式字符串中的尾随空白),并打印73正确。然后,它将继续干净地读取其余的输入,跳过空白(换行符),然后再读取每个数字。

始终更改代码以使用scanf()意味着子进程将在其回推缓冲区中以33之前的换行符停止,并且读取位置指向{{1 }},这正是父母需要的地方。

如果第一个父循环以fscanf(fichier, "%d", &n)格式省略了换行符,则子代仍然可以工作,但是父代将在恢复时将73报告为第一个数字,而不是7

答案 1 :(得分:1)

在父母中,您正在第二个循环中进行fgetc,但我认为您需要像在孩子中那样做fscanf

此外,由于前面有kill(即孩子已经[干净地]终止了),因此wait不是必需的。

请注意,父级将重做子级已处理的一些数字。这[可能]是因为父级的流已预先缓冲了这么小的文件。

要解决此问题,请在setbuf(fichier,NULL);之后立即添加fopen

还要从\n中删除所有fscanf。我已经在较早的版本中做到了,但是错过了一个(如戴维斯在下面指出的那样)。从我以前的一些编辑中,您可以看到它在最终的父输出中添加了多余的271而不是[正确] 71


这是您代码的修复程序(请原谅免费的样式清理和一些额外的调试代码):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>

char buf[30];

char *
tellme(FILE *fi)
{

    sprintf(buf,"pos=%llu",ftell(fi));

    return buf;
}

pid_t
create_process(void)
{
    pid_t pid;

    do {
        pid = fork();
    } while (pid == -1 && errno == EAGAIN);

    return pid;
}

int
main(void)
{
    FILE *fichier = fopen("entiers.txt", "r");
    int i = 0;
    int n = 0;

    if (fichier == NULL)
        return EXIT_FAILURE;

#if 1
    setbuf(fichier,NULL);
#endif

    printf("I am the father process of the pid %d\n", getpid());
    for (i = 0; i < 10; i++) {
        fscanf(fichier, "%d", &n);
        printf(" %d", n);
    }
    printf("\n");

    printf("I am the father process of the pid %d -- %s\n",
        getpid(),tellme(fichier));

    fflush(stdout);

    pid_t pid = create_process();

    if (pid) {
        wait(NULL);

#if 0
        fflush(fichier);
#endif

        printf("I am the father process of the pid %d -- %s\n",
            getpid(),tellme(fichier));
#if 0
        do {
            n = fgetc(fichier);
            printf("%d\n", n);
        } while (n != EOF);
#else
        while (1) {
            if (fscanf(fichier, "%d", &n) != 1)
                break;
            printf(" %d", n);
        }
        printf("\n");
#endif

        fclose(fichier);

// NOTE/BUG: process has already terminated
#if 0
        kill(pid, SIGKILL);
#endif
    }

    if (!pid) {
        printf("I am the child process of the pid %d -- %s\n",
            getpid(),tellme(fichier));

        for (i = 0; i < 10; i++) {
            fscanf(fichier, "%d", &n);
            printf(" %d", n);
        }
        printf("\n");

        printf("I am the child process of the pid %d -- %s\n",
            getpid(),tellme(fichier));

        fflush(stdout);
    }

    return EXIT_SUCCESS;
}

以下是输出(没有 setbuf):

I am the father process of the pid 395735
 1 2 3 5 7 11 13 17 19 23
I am the child process of the pid 395736
 29 31 37 41 43 47 53 59 61 67
I am the father process of the pid 395735
 1 2 3 5 7 11 13 17 19 23
I am the father process of the pid 395735
 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 71 73 79 83 89 97

以下是输出(从[various] setbuf中删除换行符的 with with ):

fscanf

答案 2 :(得分:1)

对不起,但你说:

  

我有一个包含1至100之间的质数的文件。我尝试使用父进程读取前10个数字(效果很好)。之后,我使用fork创建一个子进程,并读取接下来的10个数字(效果也不错)。 我使用wait()使父亲进程等待孩子完成。然后,我使用kill()杀死子进程。然后,我尝试继续使用父进程从文件中读取剩余的数字,但结果与预期的不同:

等待孩子死亡的原因是什么,如果孩子死亡,您又要杀死它。您是否有孩子死后生活的情况?

在任何情况下,当您使用任何<stdio.h>包例程时,输入都会被缓冲,因此,当您执行单个fgetc()fread()fscanf()等调用时, ,则输入以块为单位进行收集,使其余字节准备好进行后续读取,但已从系统中提取。

这是什么意思,从标准输入中,当您读取一个终端时,仅读取一行并且输入逐行(是的,即使您只要求输入一个字符),当您读取文本文件时,您的输入是逐块输入的(请参见#define BLOCSZ<stdio.h>的值)这意味着父级获得的不是不是第10个质数的文本,而是直到下一个10的末尾的文本。数据块。这将破坏下一个阅读器,就好像您共享了文件描述符(在fork()之前打开了它)在两个进程之间共享指向文件的指针一样……而且孩子得到的可能不正确文件中的偏移量。

第二点是,您不能控制进程的调度顺序,并且子进程是否在父进程执行文件之前或之后对文件进行读取。我已经看到您在分叉之前先读取了前10个数字。这保证了父进程会获得文件中的前10个数字...但是缓冲区可以存储更多以供读取(更糟的是,缓冲区可以以半个数字结尾,因此当您在子级中读取时,它可以位于中间的数量,只读取下半部分),但是如果您正确同步进程并正确考虑缓冲会发生什么,则可以按想要的顺序结束读取。