【问题标题】:traversing directories using fork()使用 fork() 遍历目录
【发布时间】:2016-03-15 19:38:48
【问题描述】:

我尝试使用fork() 进入文件夹并读取文件。我使用file tree walk 函数递归地进入文件夹。基本思想是在一个目录中会有多个子文件和目录。孩子们分别和同时阅读每个文件。但是,如果有目录,则子级将成为读取文件的子级的父级。

static int soner_each_time(const char *filepath, const struct stat *info,
                    int typeflag, struct FTW *ftwinfo)
{

    pid_t   pid = 0;
    char    buf[BUFSIZE];
    int     status;
    int     i = 0;

    /* The variables are related to functions of reading file  */

    int     totalLines;
    char    arr[TOTALNUMBEROFLINES][BUFSIZE];
    int     retval;


    const char *const filename = filepath + ftwinfo->base;

    if (( pid = fork()) < 0) {
        const int cause = errno;
        fprintf(stderr, "Fork error: %s\n", strerror(cause));
        errno = cause;
        return -1;
    }
    else if( pid > 0 ) // parent
    {
        if (typeflag == FTW_DP || typeflag == FTW_D)
        {
            sprintf(buf, "%*s%s\n\n", ftwinfo->level * 4, "", filepath);
            write(1, buf, strlen(buf));
            pid = wait(&status);
            if (pid == -1)
                perror("Failed to wait for child");
            else if (WIFEXITED(status) && !WEXITSTATUS(status))
                printf("parent [%d] reaped child [%d]\n", getpid(), pid);
            else if (WIFEXITED(status))
                printf("Child %ld terminated with return status %d\n",
                       (long)pid, WEXITSTATUS(status));
            else if (WIFSIGNALED(status))
                printf("Child %ld terminated due to uncaught signal %d\n",
                       (long)pid, WTERMSIG(status));
            else if (WIFSTOPPED(status))
                printf("Child %ld stopped due to signal %d\n",
                       (long)pid, WSTOPSIG(status));
        }
    }


    if (pid == 0) // child
    {
        if (typeflag == FTW_F)
        {
            sprintf(buf, "||| Child [%d] of parent [%d]: %s |||\n", getpid(), getppid(), filename);
            write(1, buf, strlen(buf));

            /* Both of them are about reading function */
            totalLines = storeLinesInArray(filename, arr);

            retval = for_each_file(filename, totalLines, key, arr);

            sprintf(buf, "||| Child [%d] of parent [%d] is about to exit |||\n", getpid(), getppid());
            write(1, buf, strlen(buf));
        }

        else if (typeflag == FTW_DP || typeflag == FTW_D)
        {
            sprintf(buf, "%*s%s\n\n", ftwinfo->level * 4, "", filepath);
            write(1, buf, strlen(buf));
        }

    }
        return 0;
}

FTW_DPFTW_D 表示文件夹FTW_F 表示文件。基本上,我每次都尝试使用代码 fork()。如果它是父级,它等待其子级读取文件。因为该函数是递归的,所以它将分叉每个调用。但是,有一些关于它的东西,我不能让它分叉超过一个文件。例如,1a.txt 应该有一个孩子,但对于这个方案,它是 8。分叉主题真的很困难。我做日常练习并试图理解它。您的解释和帮助将提高我在该分支的技能。

@edit: mcve 代码

#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <unistd.h>
#include <dirent.h>
#include <stdlib.h>
#include <locale.h>
#include <string.h>
#include <ftw.h>
#include <stdio.h>

#define TOTALNUMBEROFLINES 1000
#define BUFSIZE 1000


void err_sys(const char *const str)
{
    perror(str);
    fflush(stdout);
    exit(1);
}

int storeLinesInArray(const char *file, char lines[][BUFSIZE])
{
    return 0;
}

static int for_each_file(const char *filepath, int totalLines, const char *key, const char arr[][BUFSIZE])
{

    fprintf(stdout, "File name is = %s\n", filepath);
    fflush(stdout);


    return 0;
}

static int soner_each_time(const char *filepath, const struct stat *info,
                           int typeflag, struct FTW *ftwinfo)
{

    pid_t   pid = 0;
    char    buf[BUFSIZE];
    int     status;

    /* The variables are related to functions of reading file  */

    int     totalLines;
    char    arr[TOTALNUMBEROFLINES][BUFSIZE];
    int     retval;


    const char *const filename = filepath + ftwinfo->base;

    if (( pid = fork()) < 0) {
        perror("failed fork");
        exit(-1);
    }
    else if( pid > 0 ) // parent
    {
        if (typeflag == FTW_DP || typeflag == FTW_D)
        {
            sprintf(buf, "%*s%s\n\n", ftwinfo->level * 4, "", filepath);
            write(1, buf, strlen(buf));
            pid = wait(&status);
            if (pid == -1)
                perror("Failed to wait for child");
            else if (WIFEXITED(status) && !WEXITSTATUS(status))
                printf("parent [%d] reaped child [%d]\n", getpid(), pid);
            else if (WIFEXITED(status))
                printf("Child %ld terminated with return status %d\n",
                       (long)pid, WEXITSTATUS(status));
            else if (WIFSIGNALED(status))
                printf("Child %ld terminated due to uncaught signal %d\n",
                       (long)pid, WTERMSIG(status));
            else if (WIFSTOPPED(status))
                printf("Child %ld stopped due to signal %d\n",
                       (long)pid, WSTOPSIG(status));
        }
    }


    if (pid == 0) // child
    {
        if (typeflag == FTW_F)
        {
            sprintf(buf, "||| Child [%d] of parent [%d]: %s |||\n", getpid(), getppid(), filename);
            write(1, buf, strlen(buf));

            /* Both of them are about reading function */
            totalLines = storeLinesInArray(filename, arr);

            retval = for_each_file(filename, totalLines, "not needed now", arr);

            sprintf(buf, "||| Child [%d] of parent [%d] is about to exit |||\n", getpid(), getppid());
            write(1, buf, strlen(buf));
        }

        else if (typeflag == FTW_DP || typeflag == FTW_D)
        {
            sprintf(buf, "%*s%s\n\n", ftwinfo->level * 4, "", filepath);
            write(1, buf, strlen(buf));
        }

    }
    return 0;
}



int main(int argc, char *argv[])
{

    if (nftw("here is directory path", soner_each_time, 15, FTW_CHDIR)) {
        fprintf(stderr, "Failed directory.\n");
        exit(-1);
    }

    return 0;
}

【问题讨论】:

  • “有一些关于它的东西,我不能让它分叉超过一个文件”。这到底是什么意思呢?也许会有助于给出预期行为和实际行为的具体示例。
  • 我已经编辑了问题@kaylum
  • 甚至不知道从哪里开始...您有一个 status 变量,您可以对其进行测试但从未设置过;您似乎正在检查孩子状态而没有等待孩子完成。
  • 你需要发布一个完整的、可编译的示例——MCVE
  • @snr 因为这是 * 通常的工作方式(阅读 MVCE 链接)。如果我能运行你的代码,我可能会很快告诉你它有什么问题;如果我不能运行你的代码,我可能不会费心去想办法。 (请注意,您的示例也应该是minimal。正如链接所说,减少它通常会帮助您自己解决问题。

标签: c unix fork posix


【解决方案1】:

你有一些错误。更正后的代码如下。

孩子没有调用exit,所以它会继续使用它自己的nftw,因此有很多文件被冗余处理。我添加了exit(0)

forks 完成得如此之快,以至于系统将耗尽免费的pids。

我添加了三件事来解决这个问题:

  1. 一个“reap”例程,在 waitpid(0,&amp;status,WNOHANG) 上循环以捕获完成的孩子
  2. fork 周围添加了一个循环,以解决“插槽不足”问题
  3. 添加了一个限制机制,将活动子节点的数量限制在合理/有用的值

我已经对来源进行了注释以指出错误所在的位置。

虽然不是硬错误,但为 每个 文件执行 fork 会增加大量开销。磁盘带宽将因大约四个活动子线程而饱和,因此使用更多只会减慢速度。为目录分叉一个子目录并没有多大作用,因为“大量”处理将针对文件。

无论如何,这是更正后的代码[请原谅无偿的风格清理]:

#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <unistd.h>
#include <dirent.h>
#include <stdlib.h>
#include <locale.h>
#include <string.h>
#include <errno.h>
#include <ftw.h>
#include <stdio.h>
#include <sys/wait.h>

#define TOTALNUMBEROFLINES 1000
#define BUFSIZE 1000

// output verbose/debug messages
int opt_v;

// limit of number of children that can be used at one time (if non-zero)
int opt_T;

int pendcnt;                            // number of active children

void
err_sys(const char *const str)
{
    perror(str);
    fflush(stdout);
    exit(1);
}

int
storeLinesInArray(const char *file, char lines[][BUFSIZE])
{
    return 0;
}

static int
for_each_file(const char *filepath, int totalLines, const char *key, const char arr[][BUFSIZE])
{

    fprintf(stdout, "File name is = %s\n", filepath);
    fflush(stdout);

    return 0;
}

// reap_some -- reap a few processes
int
reap_some(int final)
{
    pid_t pid;
    int status;
    int reapcnt;

    reapcnt = 0;

    // reap all completed children
    while (1) {
        pid = waitpid(0,&status,WNOHANG);
        if (pid == 0)
            break;

        if (pid == -1) {
            if (errno != ECHILD)
                perror("Failed to wait for child");
            break;
        }

        if (WIFSIGNALED(status)) {
            printf("Child %ld terminated due to uncaught signal %d\n",
                (long) pid, WTERMSIG(status));
            ++reapcnt;
            continue;
        }

        if (WIFSTOPPED(status)) {
            printf("Child %ld stopped due to signal %d\n",
                (long) pid, WSTOPSIG(status));
            continue;
        }

        if (WIFEXITED(status)) {
            ++reapcnt;
            if (WEXITSTATUS(status) == 0) {
                if (opt_v)
                    printf("parent [%d] reaped child [%d]\n", getpid(), pid);
            }
            else
                printf("Child %ld terminated with return status %d\n",
                    (long) pid, WEXITSTATUS(status));
            continue;
        }
    }

    // bump down the number of children that are "in-flight"
    pendcnt -= reapcnt;

    return reapcnt;
}

static int
soner_each_time(const char *filepath, const struct stat *info, int typeflag, struct FTW *ftwinfo)
{
    pid_t pid = 0;
    char *bp;
    int lvl;
    char buf[BUFSIZE];

    /* The variables are related to functions of reading file */

    int totalLines;
    char arr[TOTALNUMBEROFLINES][BUFSIZE];
    int retval;

    const char *const filename = filepath + ftwinfo->base;

    switch (typeflag) {
    case FTW_DP:
    case FTW_D:
        bp = buf;
        for (lvl = 0;  lvl < ftwinfo->level;  ++lvl)
            bp += sprintf(bp,"    ");
        bp += sprintf(bp, "%s\n\n",filepath);
        write(1, buf, strlen(buf));
        //reap_some(0);
        break;

    case FTW_F:
        // BUGFIX:
        // limit the number of in-flight children
        // too many children serves no purpose -- they saturate the system
        // resources and performance actually goes _down_ because the system
        // spends more time doing context switches between them than the actual
        // work. more than a few children to process files produces little
        // benefit after the disk I/O is running at maximum
        if (opt_T) {
            while (pendcnt > opt_T)
                reap_some(0);
        }

        // BUGFIX:
        // without a throttle, we spawn children so fast we're going to get
        // [many] failures here (i.e. we use up _all_ available pids)
        while (1) {
            pid = fork();
            if (pid >= 0)
                break;
            reap_some(0);
        }

        // parent
        // keep track of the child count
        if (pid > 0) {
            ++pendcnt;
            break;
        }

        // child
        sprintf(buf, "||| Child [%d] of parent [%d]: %s |||\n",
            getpid(), getppid(), filename);
        if (opt_v)
            write(1, buf, strlen(buf));

        /* Both of them are about reading function */
        totalLines = storeLinesInArray(filename, arr);

        retval = for_each_file(filename, totalLines, "not needed now", arr);

        sprintf(buf, "||| Child [%d] of parent [%d] is about to exit (RETVAL: %d) |||\n", getpid(), getppid(), retval);
        if (opt_v)
            write(1, buf, strlen(buf));

        // BUGFIX:
        // child won't exit without this -- causing multiple children to redo
        // the same files (i.e. they would continue the nftw -- only parent
        // should do that)
        exit(0);
        break;
    }

    return 0;
}

int
main(int argc, char **argv)
{
    char *cp;

    --argc;
    ++argv;

    opt_T = 10;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 'T':  // throttle
            cp += 2;
            opt_T = (*cp != 0) ? atoi(cp) : 0;
            break;
        case 'v':  // verbose messages
            opt_v = 1;
            break;
        }
    }

    cp = *argv;
    printf("opt_T=%d opt_v=%d -- %s\n",opt_T,opt_v,cp);
    sleep(3);
    printf("away we go ...\n");

    if (nftw(cp, soner_each_time, 15, FTW_CHDIR)) {
        fprintf(stderr, "Failed directory.\n");
        exit(1);
    }

    // wait for all children to complete
    while (pendcnt > 0)
        reap_some(1);

    return 0;
}

更新:

将代码更改为仅在父级中进行目录处理(即子级仅用于文件)。修复了一个错误。因此,现在,-T 油门参数的值要低得多,可以相当于“工人数量”。更改程序以使用默认油门值。


更新 #2:

我说父母是因为只有一位父母。我不知道我是否会追踪错误。

不,你是对的。只有 一个 父母。这是设计使然。

我想像第一个方案中解释的那样为每个目录创建父目录。

实际上,您不会/不会正确理解真正涉及的内容。 Obi Wan Kenobi:“这些不是你要找的机器人”

在每个目录上执行递归分叉会带来许多技术、性能和系统饱和问题。我编写的示例以设计和性能的最佳折衷方案避免了所有这些。它还允许主节点“跑在”子节点之前并让子节点尽可能忙碌,而不管给定目录中有多少文件/子目录。

旁注:我有 40 多年的经验,并且编写了许多 nftw 等效程序。因此,以下内容来自所有这些。

期望的最终结果是什么?

您只有骨架代码,但您实际所做的 [打算做的] 会影响架构。您的最终计划可能是:

  1. CPU 受限 [不断等待 CPU 操作,如乘法等]
  2. 内存受限 [不断等待对 DRAM 的读取或写入完成]
  3. I/O 绑定 [不断等待 I/O 操作完成]

另外,您想要预购还是后购 [如 FTW_DEPTH] 遍历?我想是预购

您不能再使用nftw

您将需要使用 opendir/readdir/closedir [这是 nftw 所做的]。

您需要的是一个在层次结构中执行单个级别的流程。让nftw 中止并启动一个新的来实现这一点是一种折磨。

下面是一些伪代码。

但是 ... 实现变得更加复杂,不会提供更好的性能,实际上可能会降低性能。也可能导致不相关的程序崩溃,比如火狐、vlc、窗口管理器等。

您现在需要进程间通信和共享内存

在我上面的例子中,只有一个控制过程。为了保持节流,只需要简单地增加/减少pendcnt

当您为目录添加 recursive 分支时,现在为目录分支的 任何 子进程必须增加/减少 @987654336 的 全局 副本@在共享内存中。它必须使用进程间信号量来控制对该变量的访问。或者,也许是一些原子递增/递减原语 [ala C11 atomics]。

现在,对该信号量的争用成为 [延迟] 因素。

性能:

拥有多个活动进程实际上会降低性能。换句话说,分叉目录实际上会比单个进程运行

除了一些对文件执行操作的“工作”进程之外,磁盘 I/O 带宽将被用完。通过添加更多进程,您将不会获得更多好处。

对于许多进程,它们实际上可能会相互干扰。考虑进程 A 请求磁盘读取。但是,进程 B 也是如此。 A 的读取在内核内部完成,但是在它可以返回给 A 之前,必须重新调整用于 A 读取的内核缓冲区以完成 B 的读取。必须重复 A 的读取。

这就是所谓的 [虚拟内存] 页面“抖动”。

系统锁定和崩溃

随着越来越多的磁盘 I/O 完成,越来越多的内核缓冲区必须用于包含数据。内核可能不得不逐出页面缓冲区以腾出空间。其中一些可能是针对上述不相关的程序。

换句话说,您的程序的许多进程可能会独占 CPU、磁盘和内存的使用。某些程序(如 Firefox)会超时 [并崩溃],因为它们会看到在其他情况下不会看到的长时间延迟,并假设它们内部的某些东西导致了延迟。

我运行过这样的nftw 程序,看到 Firefox 说:“Killing locked up javascript script”。

更糟糕的是,我已经让 vlc 在时间上落后并开始跳帧。这导致窗口管理器感到困惑,因为它认为这是由于某些逻辑错误而不是非常缓慢响应的系统。最终结果是窗口管理器中止,不得不手动重新启动。

这也会减慢更多关键程序和内核守护进程的速度。

在某些情况下,这只能通过系统重启来清除。

此外,在您与他人共享的系统上运行多个进程会使您成为“坏公民”,因此请注意不要消耗过多资源。


不管怎样,这是伪代码:

// pseudo -- loose pseudo-code for non-nftw method
//
// NOTES:
// (1) pendcnt must now be a _shared_ memory variable (e.g. shmget, etc)
// (2) access must be locked by a shared memory semaphore
// (3) we must now have a list of our outstanding children
// (4) we can no longer do a blind waitpid(0,&status,WNOHANG) as we need to
//     keep track of when our direct children complete

struct entfile {
    struct dirent ent;
    struct stat st;
};

// dodir -- enter/exit directory and perform all actions
void
dodir(const char *subdir)
{
    // NOTE: you can create a wrapper struct for this that also has stat
    struct entfile dirlist[1000];

    // add subdir to directory stack ...
    dirstack_push(subdir);

    // enter directory
    chdir(subdir);

    // do whatever you'd like ...
    process_directory(subdir);

    // open directory
    dirctl = opendir(".");

    // pre-save all entries [skipping "." and ".."]
    // this prevents too many open directory descriptors
    // NOTE: we should stat(2) the file if d_type not supported
    while (1) {
        dirent = readdir(dirctl);
        stat(dirent->d_name,&st);
        add_to_dirent_list(dirlist,dirent,&st);
    }

    // close directory _before_ we process any entries
    closedir(dirctl);

    // process all file entries -- pre-order
    for (ALL_IN_DIRLIST(ent,dirlist)) {
        if (ent->ent.d_type == ISFILE)
            doentry(ent);
    }
    wait_for_all_on_pendlist();

    // process all directory entries -- pre-order
    for (ALL_IN_DIRLIST(dirent,dirlist)) {
        if (ent->ent.d_type == ISDIR)
            doentry(ent);
    }
    wait_for_all_on_pendlist();

    // remove directory from stack
    dirstack_pop();

    // exit directory
    chdir("..")
}

// doentry -- process a directory entry
void
doentry(struct entfile *ent)
{
    char *tail;

    tail = ent->ent.d_name;

    do {
        // does throttle, etc.
        pid = forkme();

        // parent
        // see notes above
        if (pid) {
            // NOTE: these semaphore waits can be costly
            sem_wait();
            ++pendcnt;
            sem_post();

            add_pid_to_pendlist(pid,tail,...);
            break;
        }

        // child
        switch (ent->st.st.st_mode & ...) {
        case ISFILE:
            process_file(tail);
            break;
        case ISDIR:
            dodir(tail);
            break;
        }

        exit(0);
    } while (0);
}

// wait for immediate children
void
wait_for_all_on_pendlist(void)
{

    while (MORE_IN_PENDLIST) {
        for (FORALL_IN_PENDLIST(tsk)) {
            pid = waitpid(tsk->pid,&tsk->status,WNOHANG);

            // check status like reap_some
            if (pid > 0)
                remove_pid_from_pendlist(tsk);
        }
    }
}

【讨论】:

  • 能否请您发布您最新的完整代码。为了不在这里搞砸问题,请将其放在 pastebin 之类的东西上,并将链接发布在此处的评论中。我去看看。