父母退出后如何使子进程死亡?


209

假设我有一个进程,它恰好产生了一个子进程。现在,当父进程出于某种原因(正常或异常,通过kill,^ C,断言失败或其他任何原因)退出时,我希望子进程死亡。如何正确地做到这一点?


一些关于stackoverflow的类似问题:


有关Windows的 stackoverflow的一些类似问题:

Answers:


189

子进程可以SIGHUP通过PR_SET_PDEATHSIGprctl()syscall中指定选项来要求父进程去世时内核发出(或其他信号),如下所示:

prctl(PR_SET_PDEATHSIG, SIGHUP);

有关man 2 prctl详细信息,请参见。

编辑:这是仅Linux


5
这是一个糟糕的解决方案,因为父母可能已经死亡。比赛条件。正确的解决方案:stackoverflow.com/a/17589555/412080
Maxim Egorushkin 2015年

16
将答案称为差不是很好-即使它不能解决比赛条件。请参阅我的答案,了解如何以prctl()无竞争条件的方式使用。顺便说一句,Maxim链接的答案不正确。
maxschlepzig '16

4
这只是一个错误的答案。它将在调用fork的线程死亡时而不是在父进程死亡时将信号发送给子进程。
Lothar

2
@Lothar很高兴看到某种证明。man prctl说:将调用进程的父进程终止信号设置为arg2(信号值在1..maxsig范围内,或在0之前清除)。这是调用进程在其父进程死亡时将获得的信号。执行set-user-ID或set-group-ID二进制文件时,将为fork(2)和(自Linux 2.4.36 / 2.6.23起)的子代清除此值。
qrdl

1
@maxschlepzig感谢您的新链接。好像上一个链接无效。顺便说一句,数年之后,仍然没有用于在父端设置选项的api。太遗憾了。
ROX

68

我正在尝试解决相同的问题,并且由于我的程序必须在OS X上运行,因此仅Linux解决方案对我不起作用。

我和本页上的其他人得出的结论是一样的-没有一种与POSIX兼容的方式可以在父母去世时通知孩子。因此,我想出了第二好的方法-对孩子进行投票。

当父进程终止(出于任何原因)时,子进程的父进程将变为进程1。如果子进程只是定期轮询,则可以检查其父进程是否为1。如果是,则子进程应退出。

这不是很好,但是它可以工作,并且比本页其他地方建议的TCP套接字/锁文件轮询解决方案更容易。


6
优秀的解决方案。继续调用getppid()直到返回1,然后退出。这很好,我现在也使用它。一个非政治的解决方案虽然不错。谢谢Schof。
neoneye 2010年

10
只是为了提供信息,在Solaris上,如果您位于区域中,gettpid()则不会变为1,而是获得pid区域调度程序(进程zsched)的。
PatrickSchlüter'10

4
如果有人想知道的话,当父母去世时,在Android系统中,pid似乎是0(进程System pid)而不是1。
Rui Marques 2012年

2
要拥有更健壮且与平台无关的方式,请在fork()之前,简单地使用getpid(),如果child的getppid()不同,则退出。
塞巴斯蒂安

2
如果您不控制子进程,则此方法不起作用。例如,我正在处理包装find(1)的命令,并且我想确保如果包装由于某种原因死亡,则查找被杀死。
Lucretiel

34

过去,我是通过在“子”中运行“原始”代码,在“父”中运行“生成的”代码来实现这一点的(也就是说,您可以颠倒后的测试常规fork())。然后将SIGCHLD捕获在“生成的”代码中...

在您的情况下可能无法实现,但在可行时很可爱。


非常好的解决方案,谢谢!当前接受的是更通用的,但是您的是更可移植的。
帕维尔Hajdan

1
在父级中进行工作的最大问题是您正在更改父级过程。如果服务器必须“永远”运行,则不是一种选择。
亚历克西斯·威尔克

29

如果您无法修改子进程,则可以尝试以下操作:

int pipes[2];
pipe(pipes)
if (fork() == 0) {
    close(pipes[1]); /* Close the writer end in the child*/
    dup2(0, pipes[0]); /* Use reader end as stdin */
    exec("sh -c 'set -o monitor; child_process & read dummy; kill %1'")
}

close(pipes[0]); /* Close the reader end in the parent */

这将从启用了作业控制的Shell进程中运行子进程。子进程在后台生成。Shell等待换行符(或EOF),然后杀死该子进程。

当父级死亡时,无论是什么原因,它将关闭管道的末端。子外壳将从读取的内容中获取EOF,然后继续杀死后台的子进程。


2
很好,但是有五个系统调用,以及十行代码产生的嘘声,让我对这段代码的性能有些怀疑。
Oleiade 2014年

+1。您可以dup2通过使用read -u标志从特定文件描述符中读取来避免和接管stdin 。我还添加了一个setpgid(0, 0)子项,以防止在终端中按^ C时退出子项。
格雷格·休吉尔

dup2()调用的参数顺序错误。如果要pipes[0]用作stdin,则必须编写dup2(pipes[0], 0)而不是dup2(0, pipes[0])dup2(oldfd, newfd)呼叫将在此关闭先前打开的newfd。
maxschlepzig

@Oleiade,我同意,尤其是因为生成的sh只是执行另一个叉来执行真正的子进程
而已

16

在Linux下,您可以在子级中安装父级死亡信号,例如:

#include <sys/prctl.h> // prctl(), PR_SET_PDEATHSIG
#include <signal.h> // signals
#include <unistd.h> // fork()
#include <stdio.h>  // perror()

// ...

pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() != ppid_before_fork)
        exit(1);
    // continue child execution ...

请注意,将父进程ID存储在fork之前,然后在子进程中对其进行测试,这prctl()消除了prctl()调用子进程的进程之间的竞争条件和退出。

还要注意,新创建的自己的孩子清除了孩子的父母死亡信号。它不受的影响execve()

如果我们确定负责采用所有孤儿的系统进程具有PID 1 ,则可以简化该测试:

pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() == 1)
        exit(1);
    // continue child execution ...

但是,依靠该系统进程init并拥有PID 1并不是可移植的。POSIX.1-2008指定

调用进程的所有现有子进程和僵尸进程的父进程ID必须设置为实现定义的系统进程的进程ID。即,这些过程应由特殊的系统过程继承。

传统上,采用所有孤儿的系统进程是PID 1,即init-它是所有进程的祖先。

在像LinuxFreeBSD这样的现代系统上,另一个进程可能扮演着这个角色。例如,在Linux上,一个进程可以调用prctl(PR_SET_CHILD_SUBREAPER, 1)以将其自身建立为继承其任何后代的所有孤儿的系统进程(请参见Fedora 25上的示例)。


我不理解“如果可以确定祖父母始终是初始化过程,则可以简化该测试”。当父进程死亡时,该进程将成为init进程的子进程(pid 1),而不是祖父母的子进程,对吗?因此测试似乎总是正确的。
约翰内斯·绍布


有趣,谢谢。但是,我看不到与祖父母有什么关系。
约翰内斯·绍布

1
@ JohannesSchaub-litb,您不能始终假设流程的祖父母将是init(8)流程...。您唯一可以假设的是,当父流程死亡时,其父ID将会改变。这实际上在流程的生命周期中发生一次。。。。只有一个主要例外,它是针对init(8)儿童的,但您却init(8)从未受到这种保护exit(2)(在这种情况下,内核恐慌)
路易斯·科罗拉多

1
不幸的是,如果子进程从线程派生,然后线程退出,则子进程将获得SIGTERM。
ROX

14

为了完整起见。在macOS上,您可以使用kqueue:

void noteProcDeath(
    CFFileDescriptorRef fdref, 
    CFOptionFlags callBackTypes, 
    void* info) 
{
    // LOG_DEBUG(@"noteProcDeath... ");

    struct kevent kev;
    int fd = CFFileDescriptorGetNativeDescriptor(fdref);
    kevent(fd, NULL, 0, &kev, 1, NULL);
    // take action on death of process here
    unsigned int dead_pid = (unsigned int)kev.ident;

    CFFileDescriptorInvalidate(fdref);
    CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example

    int our_pid = getpid();
    // when our parent dies we die as well.. 
    LOG_INFO(@"exit! parent process (pid %u) died. no need for us (pid %i) to stick around", dead_pid, our_pid);
    exit(EXIT_SUCCESS);
}


void suicide_if_we_become_a_zombie(int parent_pid) {
    // int parent_pid = getppid();
    // int our_pid = getpid();
    // LOG_ERROR(@"suicide_if_we_become_a_zombie(). parent process (pid %u) that we monitor. our pid %i", parent_pid, our_pid);

    int fd = kqueue();
    struct kevent kev;
    EV_SET(&kev, parent_pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
    kevent(fd, &kev, 1, NULL, 0, NULL);
    CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
    CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
    CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
    CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

您可以使用稍微更好一点的API来执行此操作,并使用带有DISPATCH_SOURCE_PROC和PROC_EXIT的调度源。
russbishop

无论出于何种原因,这都导致Mac出现紧急情况。使用此代码运行进程有50%的机会会冻结,从而导致风扇以我从未听说过的速度旋转(超快),然后Mac会关闭。请务必遵守本守则
Qix-蒙尼卡(Monica)

在我的macOS上,子进程似乎在父进程退出后自动退出。我不知道为什么
伊林·刘

我使用@YiLinLiu iirc NSTask或posix生成。startTask在这里查看我代码中的函数:github.com/neoneye/newton-commander-browse/blob/master/Classes/…–
neoneye

11

子进程是否具有往返于父进程的管道?如果是这样,则在写入时会收到SIGPIPE,在读取时会得到EOF-可以检测到这些情况。


1
我发现这并不可靠地发生,至少在OS X
Schof

我来这里是为了添加这个答案。这绝对是我在这种情况下使用的解决方案。
Dolda2000

注意点:systemd默认在其管理的服务中禁用SIGPIPE,但是您仍然可以检查管道是否闭合。请参阅IgnoreSIGPIPE下的freedesktop.org/software/systemd/man/systemd.exec.html
jdizzle

11

受到另一个答案的启发,我提出了以下全POSIX解决方案。总体思路是在父母与孩子之间创建一个中间过程,该过程具有一个目的:注意父母死亡的时间,并明确杀死孩子。

当子代码无法修改时,这种类型的解决方案很有用。

int p[2];
pipe(p);
pid_t child = fork();
if (child == 0) {
    close(p[1]); // close write end of pipe
    setpgid(0, 0); // prevent ^C in parent from stopping this process
    child = fork();
    if (child == 0) {
        close(p[0]); // close read end of pipe (don't need it here)
        exec(...child process here...);
        exit(1);
    }
    read(p[0], 1); // returns when parent exits for any reason
    kill(child, 9);
    exit(1);
}

此方法有两个小警告:

  • 如果您故意杀死中间过程,那么当父母去世时,孩子不会被杀死。
  • 如果子进程在父进程之前退出,则中间进程将尝试杀死原始的子进程pid,该进程现在可以引用其他进程。(这可以在中间过程中用更多代码解决。)

顺便说一句,我正在使用的实际代码是在Python中。这里是为了完整性:

def run(*args):
    (r, w) = os.pipe()
    child = os.fork()
    if child == 0:
        os.close(w)
        os.setpgid(0, 0)
        child = os.fork()
        if child == 0:
            os.close(r)
            os.execl(args[0], *args)
            os._exit(1)
        os.read(r, 1)
        os.kill(child, 9)
        os._exit(1)
    os.close(r)

请注意,前一阵子,在IRIX下,我使用了一个父子方案,我在两者之间都有一个管道,如果其中一个死亡,则从管道中读取会生成SIGHUP。那就是我过去杀死我fork()的孩子的方式,不需要中间过程。
亚历克西斯·威尔克

我认为您的第二个警告是错误的。子进程的pid是属于其父进程的资源,在父进程(中间进程)等待它(或终止并让init等待它)之前,它不能被释放/重用。
R .. GitHub停止帮助ICE

7

我认为无法保证仅使用标准POSIX调用。就像现实生活一样,一旦产生一个孩子,它就会拥有自己的生活。

可能的父进程捕捉最有可能终止事件,并试图在该点杀子过程,但总有一些不能被捕获。

例如,没有进程可以捕获 SIGKILL。当内核处理该信号时,它将杀死指定的进程,而不会通知该进程。

扩大类推-唯一的其他标准方式是让孩子在发现自己不再有父母的情况下自杀。

有一种仅Linux的方式来执行此操作prctl(2)-请参见其他答案。


6

正如其他人指出的那样,在父级退出时依靠父级pid变为1是不可移植的。无需等待特定的父进程ID,只需等待ID更改即可:

pit_t pid = getpid();
switch (fork())
{
    case -1:
    {
        abort(); /* or whatever... */
    }
    default:
    {
        /* parent */
        exit(0);
    }
    case 0:
    {
        /* child */
        /* ... */
    }
}

/* Wait for parent to exit */
while (getppid() != pid)
    ;

如果您不想全速轮询,请根据需要添加微睡眠。

对于我来说,此选项似乎比使用管道或依赖信号更简单。


不幸的是,该解决方案并不可靠。如果父进程在获得初始值之前死亡,该怎么办?孩子永远不会离开。
dgatwood

@dgatwood,你是什么意思?!?首先getpid()在调用之前在父级中完成fork()。如果父母在此之前去世,则该子女不存在。可能发生的情况是孩子在父母身边住了一段时间。
亚历克西斯·威尔克

在这个人为设计的示例中,它可以工作,但是在现实世界的代码中,fork总是紧随其后的是exec,新过程必须通过请求其PPID重新开始。在两次检查之间的时间里,如果父母离开了,孩子将一无所知。另外,您不太可能同时控制父代码和子代码(否则,您可以仅将PPID作为参数传递)。因此,作为一般解决方案,该方法效果不佳。实际上,如果在没有init为1的情况下出现类似UNIX的操作系统,那么很多东西都会崩溃,我无法想象有人这样做。
dgatwood

为子级执行exec时,传递parent pid是命令行参数。
Nish

2
全速轮询是疯狂的。
maxschlepzig

4

安装一个陷阱处理程序以捕获SIGINT,如果它仍然存在,它会杀死您的子进程,尽管其他张贴者也很正确,认为它不会捕获SIGKILL。

打开具有互斥访问权限的.lockfile,并对其进行子轮询以尝试打开它-如果打开成功,则子进程应退出


或者,孩子可以以阻塞模式在单独的线程中打开锁文件,在这种情况下,这可能是一个非常不错的解决方案。虽然可能它有一些可移植性限制。
吉恩(Jean)

4

此解决方案为我工作:

  • 将stdin管道传递给child-您不必将任何数据写入流中。
  • 从stdin到EOF为止,Child会无限期地阅读。一个EOF信号表明父母已经走了。
  • 这是万无一失的便携式方法,可以检测父母何时离开。即使父进程崩溃,操作系统也会关闭管道。

这是针对工人型过程的,只有当父母还活着时,这种存在才有意义。


@SebastianJylanki我不记得我是否尝试过,但是它可能起作用,因为原语(POSIX流)在操作系统之间是相当标准的。
joonas.fi

3

我认为一种快速而肮脏的方法是在孩子和父母之间建立一条管道。父母退出后,孩子将获得SIGPIPE。


SIGPIPE不会在管道关闭时发送,仅在子级尝试对其进行写入时才发送。
Alcaro's

3

一些海报已经提到了烟斗和烟斗kqueue。实际上,您还可以通过调用创建一对连接的Unix域套接字socketpair()。插座类型应为SOCK_STREAM

让我们假设您有两个套接字文件描述符fd1,fd2。现在fork()创建子进程,它将继承fds。在父级中,您关闭fd2,在子级中,您关闭fd1。现在,每个进程都可以poll()POLLIN事件本身剩余的剩余时间内打开fd 。只要双方close()在正常使用期限内未明确显示其fd,就可以确定某个POLLHUP标志应指示对方的终止(无论是否干净)。接到此事件的通知后,孩子可以决定要做什么(例如死)。

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    int sv[2];        /* sv[0] for parent, sv[1] for child */
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if ( pid > 0 ) {  /* parent */
        close(sv[1]);
        fprintf(stderr, "parent: pid = %d\n", getpid());
        sleep(100);
        exit(0);

    } else {          /* child */
        close(sv[0]);
        fprintf(stderr, "child: pid = %d\n", getpid());

        struct pollfd mon;
        mon.fd = sv[1];
        mon.events = POLLIN;

        poll(&mon, 1, -1);
        if ( mon.revents & POLLHUP )
            fprintf(stderr, "child: parent hung up\n");
        exit(0);
    }
}

您可以尝试编译上述概念验证代码,然后在这类终端中运行它./a.out &。您大约有100秒的时间来尝试通过各种信号杀死父PID,否则它就会退出。无论哪种情况,您都应该看到消息“孩子:父母挂了电话”。

与使用SIGPIPE处理程序的方法相比,此方法不需要尝试write()调用。

此方法也是对称的,即进程可以使用同一通道来监视彼此的存在。

此解决方案仅调用POSIX函数。我在Linux和FreeBSD上尝试过。我认为它应该可以在其他Unix上运行,但是我还没有真正测试过。

也可以看看:

  • unix(7)Linux的手册页,unix(4)为FreeBSD, ,,poll(2) 在Linux上。socketpair(2)socket(7)

很酷,我真的很想知道这是否存在任何可靠性问题。您是否已在生产中测试过此产品?使用不同的应用程序?
2013年

@Aktau,我一直在Linux程序中使用等效于该技巧的Python。我之所以需要它,是因为孩子的工作逻辑是“在父母退出后尽力而为,然后也退出”。但是,我真的不确定其他平台。C代码片段可以在Linux和FreeBSD上运行,但这就是我所知道的...此外,在某些情况下,您应该要小心,例如父级再次分叉,或者父级在真正退出之前放弃了fd(因此为比赛条件)。
聪马

@Aktau-这将是完全可靠的。
全方位1998年

1

POSIXexit()_exit()_Exit()函数定义为:

  • 如果该过程是控制过程,则将SIGHUP信号发送到属于呼叫过程的控制终端的前台过程组中的每个过程。

因此,如果您将父流程安排为其流程组的控制流程,则在父流程退出时,子流程应该收到SIGHUP信号。我不确定当父母崩溃时会发生这种情况,但我认为确实如此。当然,对于非崩溃情况,它应该可以正常工作。

请注意,您可能需要阅读了不少精美的印刷品-包括基本定义(定义)部分,以及系统服务信息exit(),并setsid()setpgrp()-来获得完整的信息。(我也会!)


3
嗯 该文档对此含糊不清并且相互矛盾,但是看来父流程必须是会话的主导流程,而不仅仅是流程组。会话的主要流程始终是登录,而现在让我的流程取代新会话的主要流程已经超出了我的能力范围。
朔夫

2
仅当退出进程是登录shell时,SIGHUP才有效地发送到子进程。 “ opengroup.org/onlinepubs/009695399/functions/exit.html ”进程的终止不会直接终止其子进程。如下所述,发送SIGHUP信号将在某些情况下间接终止子进程。
罗伯K 2010年

1
@Rob:正确-这也是我给的报价:只有在某些情况下,子进程才能获得SIGHUP。严格地说,这只是发送SIGHUP的登录shell,尽管这是最常见的情况。如果将具有多个子进程的进程设置为其自身及其子进程的控制进程,则在主进程死后,SIGHUP将(方便地)发送给其子进程。OTOH,流程很少会遇到这么大的麻烦-因此,我挑剔的工作比提出一个真正重要的小问题要多。
乔纳森·莱夫勒

2
我无所事事了几个小时,却无法正常工作。如果我有一个带有一些子进程的守护进程,当父进程退出时,所有子进程都需要死亡,这本来可以很好地处理的。
罗伯K

1

如果您将信号发送到pid 0,例如

kill(0, 2); /* SIGINT */

该信号被发送到整个过程组,从而有效地杀死了孩子。

您可以使用以下类似方法轻松对其进行测试:

(cat && kill 0) | python

如果再按^ D键,您会看到该文本"Terminated"表明Python解释器确实已被杀死,而不是因为关闭了stdin而退出了。


1
(echo -e "print(2+2)\n" & kill 0) | sh -c "python -" 快乐地打印4而不是终止
Kamil Szot 2014年

1

如果与其他任何人都相关,当我从C ++派生分叉的子进程中的JVM实例时,使父实例完成后才能正确终止JVM实例的唯一方法是执行以下操作。如果这不是最好的方法,希望有人可以在评论中提供反馈。

1)prctl(PR_SET_PDEATHSIG, SIGHUP)根据建议调用分叉的子进程,然后通过execv和启动Java应用程序。

2)向Java应用程序添加一个关闭挂钩,该轮询将轮询直到其父PID等于1,然后执行hard Runtime.getRuntime().halt(0)。通过启动运行ps命令的单独外壳程序来完成轮询(请参阅:如何在Java或Linux上的JRuby中找到我的PID?)。

编辑130118:

看来这不是一个可靠的解决方案。我仍在努力了解正在发生的细微差别,但在屏幕/ SSH会话中运行这些应用程序时,有时仍会遇到孤立的JVM进程。

我没有在Java应用程序中轮询PPID,而是简单地让shutdown钩子执行了清除操作,然后如上所述进行了硬停止。然后,waitpid当需要终止所有操作时,我确保在生成的子进程上的C ++父应用程序中调用。这似乎是一个更可靠的解决方案,因为子进程确保其终止,而父进程使用现有引用来确保其子进程终止。将此与以前的解决方案进行比较,前一个解决方案让父进程在需要时终止,并让子进程尝试在终止之前弄清是否是孤立的。


1
PID equals 1等待无效。新的父代可以是其他一些PID。您应该检查它是否从原始父对象(在fork()之前的getpid())更改为新父对象(在fork()之前被调用的子代中的getppid()不等于getpid())。
亚历克西斯·威尔克

1

特定于Linux的另一种方法是在新的PID名称空间中创建父对象。然后,它将在该名称空间中成为PID 1,并且退出时,其所有子级将立即被杀死SIGKILL

不幸的是,要创建一个新的PID名称空间,您必须具有CAP_SYS_ADMIN。但是,此方法非常有效,除了最初启动父级外,不需要对父级或子级进行任何实际更改。

请参见clone(2)pid_namespaces(7)unshare(2)


我需要以其他方式进行编辑。可以使用prctl使其成为所有子孙辈,曾孙辈等的初始化过程……
Omnifarious

0

如果父母去世,则孤儿的PPID更改为1-您只需要检查自己的PPID。从某种意义上说,这就是轮询。这是为此的壳件:

check_parent () {
      parent=`ps -f|awk '$2=='$PID'{print $3 }'`
      echo "parent:$parent"
      let parent=$parent+0
      if [[ $parent -eq 1 ]]; then
        echo "parent is dead, exiting"
        exit;
      fi
}


PID=$$
cnt=0
while [[ 1 = 1 ]]; do
  check_parent
  ... something
done

0

我发现2个解决方案,都不是完美的。

1.收到SIGTERM信号后,用kill(-pid)杀死所有孩子。
显然,此解决方案无法处理“ kill -9”,但它在大多数情况下都可以正常工作,并且非常简单,因为它不需要记住所有子进程。


    var childProc = require('child_process').spawn('tail', ['-f', '/dev/null'], {stdio:'ignore'});

    var counter=0;
    setInterval(function(){
      console.log('c  '+(++counter));
    },1000);

    if (process.platform.slice(0,3) != 'win') {
      function killMeAndChildren() {
        /*
        * On Linux/Unix(Include Mac OS X), kill (-pid) will kill process group, usually
        * the process itself and children.
        * On Windows, an JOB object has been applied to current process and children,
        * so all children will be terminated if current process dies by anyway.
        */
        console.log('kill process group');
        process.kill(-process.pid, 'SIGKILL');
      }

      /*
      * When you use "kill pid_of_this_process", this callback will be called
      */
      process.on('SIGTERM', function(err){
        console.log('SIGTERM');
        killMeAndChildren();
      });
    }

同样,如果在某个地方调用process.exit,则可以像上面那样安装“退出”处理程序。注意:操作系统已自动处理Ctrl + C和突然崩溃,以杀死进程组,因此此处不再赘述。

2.使用chjj / pty.js生成带有连接的控制终端的进程。
当您以任何方式杀死当前进程甚至杀死-9时,所有子进程也将被自动杀死(通过OS?)。我猜是因为当前进程位于终端的另一端,所以如果当前进程死亡,则子进程将获得SIGPIPE从而死亡。


    var pty = require('pty.js');

    //var term =
    pty.spawn('any_child_process', [/*any arguments*/], {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: process.cwd(),
      env: process.env
    });
    /*optionally you can install data handler
    term.on('data', function(data) {
      process.stdout.write(data);
    });
    term.write(.....);
    */

0

通过滥用终端控制和会话,我设法通过3个进程来制作一个可移植的非轮询解决方案。这是精神上的手淫,但有效。

诀窍是:

  • 进程A开始
  • 进程A创建一个管道P(从不读取它)
  • 流程A进入流程B
  • 流程B创建一个新会话
  • 进程B为该新会话分配虚拟终端
  • 进程B安装SIGCHLD处理程序以在子项退出时死亡
  • 进程B设置SIGPIPE处理程序
  • 流程B进入流程C
  • 进程C进行所需的任何操作(例如exec()修改后的二进制文件或运行任何逻辑)
  • 进程B写入管道P(并以此方式阻塞)
  • 进程A在进程B上等待并在其死时退出

那样:

  • 如果进程A死亡:进程B获得SIGPIPE并死亡
  • 如果进程B死亡:进程A的wait()返回并死亡,则进程C得到一个SIGHUP(因为当连接了终端的会话的会话负责人死亡时,前台进程组中的所有进程都得到了SIGHUP)
  • 如果进程C死亡:进程B获得SIGCHLD并且死亡,那么进程A死亡

缺点:

  • 进程C无法处理SIGHUP
  • 进程C将在不同的会话中运行
  • 进程C无法使用会话/进程组API,因为它会破坏脆弱的设置
  • 为每个这样的操作创建一个终端绝不是最好的主意

0

尽管已经过去了7年,但是我在运行SpringBoot应用程序时遇到了这个问题,该应用程序需要在开发过程中启动webpack-dev-server,并且需要在后端进程停止时将其杀死。

我尝试使用 Runtime.getRuntime().addShutdownHook但它在Windows 10上有效,但在Windows 7上却无效。

我已对其进行更改,以使用专用线程来等待进程退出,或者InterruptedException该线程似乎在两个Windows版本上均正常工作。

private void startWebpackDevServer() {
    String cmd = isWindows() ? "cmd /c gradlew webPackStart" : "gradlew webPackStart";
    logger.info("webpack dev-server " + cmd);

    Thread thread = new Thread(() -> {

        ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
        pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
        pb.redirectError(ProcessBuilder.Redirect.INHERIT);
        pb.directory(new File("."));

        Process process = null;
        try {
            // Start the node process
            process = pb.start();

            // Wait for the node process to quit (blocking)
            process.waitFor();

            // Ensure the node process is killed
            process.destroyForcibly();
            System.setProperty(WEBPACK_SERVER_PROPERTY, "true");
        } catch (InterruptedException | IOException e) {
            // Ensure the node process is killed.
            // InterruptedException is thrown when the main process exit.
            logger.info("killing webpack dev-server", e);
            if (process != null) {
                process.destroyForcibly();
            }
        }

    });

    thread.start();
}

0

从历史上看,从UNIX v7开始,流程系统通过检查流程的父ID来检测流程的孤立性。正如我所说,从历史上看,init(8)系统进程是一个特殊的进程,仅出于一个原因:它不会消亡。它不能消亡,因为内核算法处理分配新的父进程ID取决于这一事实。当一个进程执行其进程时,该进程在系统调用之前就成为孤立的。exit(2)调用时(通过进程系统调用或通过外部任务发送信号等),内核将该进程的所有子进程重新分配初始化进程的ID作为其父进程ID。这将导致最简单的测试和最可移植的方式来了解进程是否已成为孤儿。只需检查getppid(2)系统调用的结果,以及它是否是init(2)

这种方法产生了两个问题,这些问题可能会导致问题:

  • 首先,我们可以将init流程更改为任何用户流程,那么我们如何确保init流程始终是所有孤立流程的父级?好吧,在exit系统调用代码中,有一个显式检查,以查看执行调用的进程是否为init进程(pid等于1的进程),如果是这种情况,则内核恐慌(应该不再能够维护)流程层次结构),因此init流程不允许进行exit(2)调用。
  • 其次,上面暴露的基本测试中存在竞赛条件。过去一直假设Init进程的ID为1,但这并不是POSIX方法所保证的,它声明(如在其他响应中所公开的)仅为此目的保留了系统的进程ID。几乎没有posix实现可以做到这一点,并且您可以假设在原始的unix派生系统中,具有1作为getppid(2)系统调用响应的足以假设该过程是孤立的。另一种检查方法是getppid(2)在分叉之后进行一次,并将该值与新调用的结果进行比较。这根本不能在所有情况下都起作用,因为两个调用都不是原子的,并且父进程可以fork(2)在第一个getppid(2)系统调用之后和之前死亡。进程parent id only changes once, when its parent does an出口(2)call, so this should be enough to check if thegetppid(2)result changed between calls to see that parent process has exit. This test is not valid for the actual children of the init process, because they are always children ofinit(8)`,但您可以安全地假设这些进程也没有父进程(除非在系统中替换了init进程)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.