异步清理子进程

4

这是来自于<《高级Linux编程》>的一个例子,第3.4.4章节。程序使用fork()和exec()创建了一个子进程。父进程不会阻塞等待子进程终止,而是希望能够异步清理掉子进程(否则,子进程将变成僵尸进程)。可以使用信号SIGCHLD来实现此功能。通过设置signal_handler,当子进程结束时可以进行清理工作。代码如下:

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

int spawn(char *program, char **arg_list){
    pid_t child_pid;

     child_pid = fork();
     if(child_pid == 0){    // it is the child process
        execvp(program, arg_list);
        fprintf(stderr, "A error occured in execvp\n");
        return 0;
     }
     else{
        return child_pid;
     }
}

int child_exit_status;

void clean_up_child_process (int signal_number){
    int status;
    wait(&status);
    child_exit_status = status;     // restore the exit status in a global variable
    printf("Cleaning child process is taken care of by SIGCHLD.\n");
};

int main()
{
    /* Handle SIGCHLD by calling clean_up_process; */
    struct sigaction sigchld_action;
    memset(&sigchld_action, 0, sizeof(sigchld_action));
    sigchld_action.sa_handler = &clean_up_child_process;
    sigaction(SIGCHLD, &sigchld_action, NULL);

    int child_status;
    char *arg_list[] = {    //deprecated conversion from string constant to char*
        "ls", 
        "-la",
        ".",
        NULL
    };

    spawn("ls", arg_list);

    return 0;
}

然而,在终端中运行程序时,父进程从未结束。似乎它没有执行clean_up_child_process函数(因为它不会打印出“Cleaning child process is taken care of by SIGCHLD.”)。这段代码有什么问题吗?
3个回答

2
在子进程的pid从fork()返回后,父进程会立即从main()返回,它没有等待子进程终止的机会。请注意保留HTML标签。

是的!这实际上解决了我的问题。我还注意到fork()和exec()函数比我想象的要花费更多的时间,因为我必须在“spawn()”后面放置冗长的内容,以使主程序在其子进程后终止。 - Nothing More

0

针对GNU/Linux用户

我已经阅读了这本书。虽然这本书提到了这个机制:

引用自该书第59页的3.4.4节:

一种更优雅的解决方案是在子进程终止时通知父进程。

但它只是说你可以使用sigaction来处理这种情况。


这里是如何处理进程的完整示例。

首先,为什么我们要使用这种机制?因为我们不想将所有进程同步。

真实例子
想象一下,你有10个.mp4文件,你想将它们转换成.mp3文件。那么,我这个初级用户会这样做:

ffmpeg -i 01.mp4 01.mp3 

并且重复执行此命令10次。稍微高级的用户会这样做:

ls *.mp4 | xargs -I xxx ffmpeg -i xxx xxx.mp3

这个命令将每行逐一管道传输10个mp4文件到xargs,然后一个接一个地转换为mp3

但是我作为一个资深用户会这样做:

ls *.mp4 | xargs -I xxx -P 0 ffmpeg -i xxx xxx.mp3

这意味着如果我有10个文件,就会创建10个进程并同时运行它们。这是有很大的不同的。在前两个命令中,我们只有1个进程;它被创建然后终止,然后继续到另一个进程。但是通过使用-P 0选项,我们可以同时创建10个进程,实际上有10个ffmpeg命令正在运行。


现在异步清理子进程的目的更加清晰。实际上,我们想要运行一些新进程,但是这些进程的顺序和可能的退出状态对我们来说并不重要。这样,我们可以尽可能快地运行它们并缩短时间。


首先,您可以查看man sigaction以获取更多详细信息。

其次,通过以下方式查看此信号编号:

T ❱ kill -l | grep SIGCHLD
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP

示例代码

目标:使用SIGCHLD清理子进程

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <wait.h>
#include <unistd.h>

sig_atomic_t signal_counter;

void signal_handler( int signal_number )
{
    ++signal_counter;
    int wait_status;
    pid_t return_pid = wait( &wait_status );
    if( return_pid == -1 )
    {
        perror( "wait()" );
    }
    if( WIFEXITED( wait_status ) )
    {
        printf ( "job [ %d ] | pid: %d | exit status: %d\n",signal_counter, return_pid, WEXITSTATUS( wait_status ) );
    }
    else
    {
        printf( "exit abnormally\n" );
    }

    fprintf( stderr, "the signal %d was received\n", signal_number );
}

int main()
{
    // now instead of signal function we want to use sigaction
    struct sigaction siac;

    // zero it
    memset( &siac, 0, sizeof( struct sigaction ) );

    siac.sa_handler = signal_handler;
    sigaction( SIGCHLD, &siac, NULL );

    pid_t child_pid;

    ssize_t read_bytes = 0;
    size_t  length = 0;
    char*   line = NULL;

    char* sleep_argument[ 5 ] = { "3", "4", "5", "7", "9" };

    int counter = 0;

    while( counter <= 5 )
    {
        if( counter == 5 )
        {
            while( counter-- )
            {
                pause();
            }

            break;
        }

        child_pid = fork();

        // on failure fork() returns -1
        if( child_pid == -1 )
        {
            perror( "fork()" );
            exit( 1 );
        }

        // for child process fork() returns 0
        if( child_pid == 0 ){
            execlp( "sleep", "sleep", sleep_argument[ counter ], NULL );
        }

        ++counter;
    }

    fprintf( stderr, "signal counter %d\n", signal_counter );

    // the main return value
    return 0;
}

这是示例代码的功能:

  1. 创建5个子进程
  2. 然后进入内部循环并暂停等待接收信号。参见man pause
  3. 当一个子进程终止时,父进程会唤醒并调用signal_handler函数
  4. 一直执行到最后一个:sleep 9

输出:(17代表SIGCHLD

ALP  ./a.out 
job [ 1 ] | pid: 14864 | exit status: 0
the signal 17 was received
job [ 2 ] | pid: 14865 | exit status: 0
the signal 17 was received
job [ 3 ] | pid: 14866 | exit status: 0
the signal 17 was received
job [ 4 ] | pid: 14867 | exit status: 0
the signal 17 was received
job [ 5 ] | pid: 14868 | exit status: 0
the signal 17 was received
signal counter 5

当您运行此示例代码时,在另一个终端中尝试执行以下操作:
ALP ❱ ps -o time,pid,ppid,cmd --forest -g $(pgrep -x bash)
    TIME   PID  PPID CMD
00:00:00  5204  2738 /bin/bash
00:00:00  2742  2738 /bin/bash
00:00:00  4696  2742  \_ redshift
00:00:00 14863  2742  \_ ./a.out
00:00:00 14864 14863      \_ sleep 3
00:00:00 14865 14863      \_ sleep 4
00:00:00 14866 14863      \_ sleep 5
00:00:00 14867 14863      \_ sleep 7
00:00:00 14868 14863      \_ sleep 9

正如您所看到的,a.out 进程有 5 个子进程。它们同时运行。每当它们中的任何一个终止时,内核 就会向它们的父进程即 a.out 发送信号 SIGCHLD

注意

如果我们不使用 pause 或任何机制,以便父进程可以 wait 其子进程,则我们将放弃创建的进程,而 upstart(在 Ubuntuinit 上)将成为它们的父进程。如果您删除 pause(),则可以尝试它。


0

我使用的是Mac,所以我的答案可能不太相关,但还是要说一下。我没有使用任何选项进行编译,因此可执行文件的名称是 a.out

我在控制台上也有同样的经验(进程似乎无法终止),但我注意到这只是终端显示问题,因为你实际上可以按Enter键,命令行就会恢复正常,而且从其他终端窗口执行的ps命令也不会显示a.out或者它启动的ls

另外,如果我运行./a.out >/dev/null,它会立即完成。

所以上述的重点是,其实所有东西都已经终止了,只是终端由于某种原因冻结了。

接下来,为什么它永远不打印Cleaning child process is taken care of by SIGCHLD.。简单来说,是因为父进程在子进程之前终止了。SIGCHLD信号无法传递给已经终止的进程,所以处理程序永远不会被调用。

在书中,提到父进程继续做一些其他的事情,如果它真的这样做,那么一切都正常,例如在spawn()之后添加sleep(1)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接