如何等待非子进程退出

2024-10-24 08:50:00
admin
原创
62
摘要:问题描述:对于子进程,可以使用wait()和waitpid()函数暂停当前进程的执行,直到子进程退出。但该函数不能用于非子进程。是否有其他函数可以等待任何进程的退出?解决方案 1:没有什么等同于wait()。通常的做法是使用 进行轮询kill(pid, 0)并查找返回值 -1 和errno来ESRCH指示该进...

问题描述:

对于子进程,可以使用wait()waitpid()函数暂停当前进程的执行,直到子进程退出。但该函数不能用于非子进程。

是否有其他函数可以等待任何进程的退出?


解决方案 1:

没有什么等同于wait()。通常的做法是使用 进行轮询kill(pid, 0)并查找返回值 -1 和errnoESRCH指示该进程已消失。

更新:自 Linux 内核 5.3 以来,有一个pidfd_open系统调用,它为给定的 pid 创建一个 fd,当 pid 退出时可以轮询该 fd 以获取通知。

解决方案 2:

在 BSD 和 OS X 上,您可以使用 kqueue 和 EVFILT_PROC+NOTE_EXIT 来实现这一点。无需轮询。不幸的是,没有 Linux 等效版本。

解决方案 3:

到目前为止,我发现了三种在 Linux 上执行此操作的方法:

  • 轮询:你可以不时地检查进程是否存在,要么使用kill,要么通过测试是否存在/proc/$pid,就像在大多数其他答案中一样

  • 使用ptrace系统调用像调试器一样附加到进程,以便在进程退出时收到通知,如a3nm 的答案所示

  • 使用netlink接口来监听PROC_EVENT_EXIT消息 - 这样,每次进程退出时内核都会通知您的程序,您只需等待正确的进程 ID。我只在互联网上的一个地方看到过这种描述。

无耻的推销:我正在开发一个程序(当然是开源的;GPLv2),可以实现这三项功能中的任何一项。

解决方案 4:

您还可以创建一个套接字或 FIFO 并在其上读取。FIFO 特别简单:将子进程的标准输出与 FIFO 连接并读取。读取将阻塞,直到子进程退出(出于任何原因)或发出一些数据。因此,您需要一个小循环来丢弃不需要的文本数据。

如果您有权访问子进程的源代码,请在启动时打开 FIFO 进行写入,然后就忘掉它吧。当子进程终止时,操作系统将清理打开的文件描述符,然后您的等待“父”进程将被唤醒。

现在,这可能是一个您未启动或拥有的进程。在这种情况下,您可以用一个脚本替换二进制可执行文件,该脚本启动真正的二进制文件,但也会添加监控,如上所述。

解决方案 5:

这是一种无需轮询即可等待 Linux 中任何进程(不一定是子进程)退出(或被终止)的方法:

使用 inotify 等待 /proc'pid' 被删除将是完美的解决方案,但不幸的是 inotify 不适用于 /proc 之类的伪文件系统。但是我们可以将它用于进程的可执行文件。当进程仍然存在时,此文件将保持打开状态。因此,我们可以使用 inotify 和 IN_CLOSE_NOWRITE 来阻止,直到文件关闭。当然,它可以因其他原因而关闭(例如,如果另一个具有相同可执行文件的进程退出),因此我们必须通过其他方式过滤这些事件。

我们可以使用 kill(pid, 0),但这不能保证它是否仍然是同一个进程。如果我们真的对此很担心,我们可以做其他事情。

这里有一种方法可以 100% 安全地避免 pid 重用问题:我们打开伪目录 /proc/'pid',并保持打开状态直到我们完成。如果在此期间使用相同的 pid 创建了一个新进程,我们持有的目录文件描述符仍将引用原始文件描述符(如果旧进程停止存在,则变为无效),但永远不会引用使用重用 pid 的新进程。然后,我们可以通过检查文件“cmdline”是否存在于目录中(例如,使用 openat())。当进程退出或被终止时,这些伪文件也将停止存在,因此 openat() 将失败。

以下是示例代码:

// return -1 on error, or 0 if everything went well
int wait_for_pid(int pid)
{
    char path[32];
    int in_fd = inotify_init();
    sprintf(path, "/proc/%i/exe", pid);
    if (inotify_add_watch(in_fd, path, IN_CLOSE_NOWRITE) < 0) {
        close(in_fd);
        return -1;
    }
    sprintf(path, "/proc/%i", pid);
    int dir_fd = open(path, 0);
    if (dir_fd < 0) {
        close(in_fd);
        return -1;
    }

    int res = 0;
    while (1) {
        struct inotify_event event;
        if (read(in_fd, &event, sizeof(event)) < 0) {
            res = -1;
            break;
        }
        int f = openat(dir_fd, "fd", 0);
        if (f < 0) break;
        close(f);
    }

    close(dir_fd);
    close(in_fd);
    return res;
}

解决方案 6:

您可以使用 附加到进程ptrace(2)。从 shell 来看,strace -p PID >/dev/null 2>&1似乎可行。这可避免忙等待,尽管这会减慢跟踪的进程速度,并且不会对所有进程起作用(仅适用于您的进程,这比仅适用于子进程要好一些)。

解决方案 7:

从 Linux 内核 5.3 开始,有一个pidfd_open系统调用,它为给定的 pid 创建一个 fd,当 pid 退出时可以轮询该 fd 以获取通知。

解决方案 8:

我的解决方案(使用inotifywait

不可能的事是不可接受的。因此,这是我的解决方法

这是基于Linux的的/proc文件系统。

我的需求是,一旦所有容器备份完成,就启动第二次(整体)备份。容器备份由计划任务。

关注cron任务

read -r wpid < <(ps -C backup.sh ho pid)
ls -l /proc/$wpid/fd
total 0
lr-x------ 1 user user 64  1 aoû 09:13 0 -> pipe:[455151052]
lrwx------ 1 user user 64  1 aoû 09:13 1 -> /tmp/#41418 (deleted)
lrwx------ 1 user user 64  1 aoû 09:13 2 -> /tmp/#41418 (deleted)

其中已删除的条目是由创建的cron。请注意,即使已删除,您仍然可以直接监视文件描述符

inotifywait  /proc/$wpid/fd/1

...

/proc/511945/fd/1 CLOSE_WRITE,CLOSE 

或者

inotifywait  /proc/$wpid/fd/0
/proc/511945/fd/0 CLOSE_NOWRITE,CLOSE 

注意:我的整体备份是以root 用户身份运行的!如果没有,则可能需要sudo,因为命令是在cron会话下运行的(不是同一用户)!

警告/限制:

关于Gabor Csardi 的评论:

...其他进程也可能打开和关闭同一个文件以作为被监视进程的标准输入进行读取。

您必须仔细选择/proc/$wpid&/要监视的条目!每个案例都要进行测试/检查。

在我的示例中,我的脚本使用了如下整体重定向:

#!/bin/bash

exec 0< <(/bin/ls -d1 /storage/clients/cln*/.)

/proc/$wpid/0这就是为什么是链接到的原因 pipe:[455151052]

文件基本上无法被任何其他进程访问(除了直接寻址的另一个/proc/$wpid/0进程)。

如果您的脚本由类似的程序运行myScript | gzip >myScript.log.gz,那么您可能会看到一个1链接到的pipe[...]

但是有些工具会在正常运行时关闭所有 FD...在这种情况下,您可能必须找到其他东西来监视。

同一会话

为了测试/展示这一点,您可以在同一个用户桌面上打开两个不同的终端会话,然后:

在第一个窗口中,点击:

sleep 0.42m <<<'' >/dev/null 2>&1

在第二个窗口中:

read -r wpid < <(ps -C sleep wwho pid,cmd| sed 's/ sleep 0.42m$//p;d')
ls -l /proc/$wpid/fd
total 0
lr-x------ 1 user user 64  1 aoû 09:38 0 -> pipe:[455288137]
l-wx------ 1 user user 64  1 aoû 09:38 1 -> /dev/null
l-wx------ 1 user user 64  1 aoû 09:38 2 -> /dev/null

警告:不要试图监视12!因为它们指向!!如果你监视其中任何一个,那么系统中/dev/null任何访问的进程都会触发!!/dev/null`inotifywait`

inotifywait  /proc/$wpid/fd/0
/proc/531119/fd/0 CLOSE_NOWRITE,CLOSE

已用时间(秒)

尝试在短时间内在两个窗口上运行两个命令(sleep在第一个窗口和inotifywait第二个窗口:hit )。Return

第一个窗口:

sleep 0.42m <<<'' >/dev/null 2>&1

第二个窗口:

read -r wpid < <(ps -C sleep wwho pid,cmd| sed 's/ sleep 0.42m$//p;d')
startedAt=$(ps ho lstart $wpid | date -f - +%s)
inotifywait  /proc/$wpid/fd/0;echo $((EPOCHSECONDS-startedAt))
/proc/533967/fd/0 CLOSE_NOWRITE,CLOSE 
25

结论。

使用inotifywait似乎是一个很好的解决方案,主要是监视命令的标准输入(fd/0)。但这必须逐个测试。

解决方案 9:

我不知道。除了混乱的解决方案外,如果您可以更改要等待的程序,则可以使用信号量。

库函数包括sem_open(3)sem_init(3), sem_wait(3),...

sem_wait(3)执行等待,因此您不必像在混沌解决方案中那样忙于等待。当然,使用信号量会使您的程序更加复杂,可能不值得这么麻烦。

解决方案 10:

也许可以等待 /proc/[pid] 或 /proc/[pid]/[something] 消失?

有 poll() 和其他文件事件等待函数,也许可以有帮助?

解决方案 11:

PR_SET_PDEATHSIG 可用于等待父进程终止

解决方案 12:

只需轮询 /proc/[PID]/stat 中的值 22 和 2。值 2 包含可执行文件的名称,而 22 包含启动时间。如果它们发生变化,则表示其他某个进程已占用了相同的(已释放的)PID。因此该方法非常可靠。

解决方案 13:

您可以使用eBPF来实现这一点。

bcc工具包基于实现了许多优秀的监控功能eBPF。其中包括exitsnoop跟踪进程终止,显示命令名称和终止原因,退出或致命信号。

   It catches processes of all users, processes in containers,  as  well  as  processes  that
   become zombie.

   This  works by tracing the kernel sched_process_exit() function using dynamic tracing, and
   will need updating to match any changes to this function.

   Since this uses BPF, only the root user can use this tool.

相关实现可以参考此工具。

您可以从下面的链接获取有关此工具的更多信息:

  • Github repo:tools/ exitsnoop:跟踪进程终止(退出和致命信号)。示例。

  • Linux 扩展 BPF (eBPF) 跟踪工具

  • ubuntu 联机帮助页:exitsnoop-bpfcc

你可以先安装这个工具并使用它看看它是否满足你的需求,然后参考它的实现进行编码,或者使用它提供的一些库来实现你自己的功能。

exitsnoop例子:

   Trace all process termination
          # exitsnoop

   Trace all process termination, and include timestamps:
          # exitsnoop -t

   Exclude successful exits, only include non-zero exit codes and fatal signals:
          # exitsnoop -x

   Trace PID 181 only:
          # exitsnoop -p 181

   Label each output line with 'EXIT':
          # exitsnoop --label EXIT

另一种选择

使用 Linux 的 PROC_EVENTS 等待(非子)进程退出

参考项目:
https://github.com/stormc/waitforpid

项目中提及:

使用 Linux 的 PROC_EVENTS 等待(非子)进程退出。由于 waitforpid 二进制文件允许使用 CAP_NET_ADMIN POSIX 功能,因此无需将其设置为 suid root。您需要一个启用了 CONFIG_PROC_EVENTS 的 Linux 内核。

解决方案 14:

赞赏@Hongli 针对 macOS 和 kqueue 的回答。我用 swift 实现的

/// Wait any pids, including non-child pid. Block until all pids exit.
/// - Parameters:
///   - timeout: wait until interval, nil means no timeout
/// - Throws: WaitOtherPidError
/// - Returns: isTimeout
func waitOtherPids(_ pids: [Int32], timeout: TimeInterval? = nil) throws -> Bool {
    
    // create a kqueue
    let kq = kqueue()
    if kq == -1 {
        throw WaitOtherPidError.createKqueueFailed(String(cString: strerror(errno)!))
    }
    
    // input
    // multiple changes is OR relation, kevent will return if any is match
    var changes: [Darwin.kevent] = pids.map({ pid in
        Darwin.kevent.init(ident: UInt(pid), filter: Int16(EVFILT_PROC), flags: UInt16(EV_ADD | EV_ENABLE), fflags: NOTE_EXIT, data: 0, udata: nil)
    })
    
    let timeoutDeadline = timeout.map({ Date(timeIntervalSinceNow: $0)})
    let remainTimeout: () ->timespec? = {
        if let deadline = timeoutDeadline {
            let d = max(deadline.timeIntervalSinceNow, 0)
            let fractionalPart = d - TimeInterval(Int(d))
            return timespec(tv_sec: Int(d), tv_nsec: Int(fractionalPart * 1000 * 1000 * 1000))
        } else {
            return nil
        }
    }
    
    // output
    var events = changes.map{ _ in Darwin.kevent.init() }
    
    while !changes.isEmpty {
        
        // watch changes
        // sync method
        let numOfEvent: Int32
        if var timeout = remainTimeout() {
            numOfEvent = kevent(kq, changes, Int32(changes.count), &events, Int32(events.count), &timeout);
        } else {
            numOfEvent = kevent(kq, changes, Int32(changes.count), &events, Int32(events.count), nil);
        }
        
        if numOfEvent < 0 {
            throw WaitOtherPidError.keventFailed(String(cString: strerror(errno)!))
        }
        if numOfEvent == 0 {
            // timeout. Return directly.
            return true
        }
        
        // handle the result
        let realEvents = events[0..<Int(numOfEvent)]
        let handledPids = Set(realEvents.map({ $0.ident }))
        changes = changes.filter({ c in
            !handledPids.contains(c.ident)
        })

        for event in realEvents {
            if Int32(event.flags) & EV_ERROR > 0 { // @see 'man kevent'
                let errorCode = event.data
                if errorCode == ESRCH {
                    // "The specified process to attach to does not exist"
                    // ingored
                } else {
                    print("[Error] kevent result failed with code (errorCode), pid (event.ident)")
                }
            } else {
                // succeeded event, pid exit
            }
        }
    }
    return false
}
enum WaitOtherPidError: Error {
    case createKqueueFailed(String)
    case keventFailed(String)
}

解决方案 15:

这里的答案中已经有很多好主意了。我不得不为多个操作系统实现等待多个非子进程的进程。以下是摘要。

  1. 如果操作系统支持kqueue(2),那么就使用它,效果很好。这适用于 macOS 和似乎所有的 *BSD 系统。

  2. 在 Linux 5.3 或更高版本上使用pidfd_open(2)。您可以编译一个pidfd_open(2)在较新的 Linux 上使用的二进制文件,并在较旧的 Linux 上回退到下一个解决方案,特别是 RHEL 7 和 RHEL 8。

  3. 在较旧的 Linux 上使用。我通过观察上的事件inotify(7)获得了最佳结果,但有几个问题。其他进程可能正在关闭该文件上的 fd,而不是我们正在等待的 fd。此外,进程可能会调用,这也是一个事件,但进程仍在运行。如果发生这种情况,我们需要开始观察新的。close()`/proc/<pid>/exeexecve()close()`/proc/&lt;pid>/exe

  4. 在 Windows 上,我们可以使用WaitForMultipleObjects()。这里烦人的是,我们需要解决它只能监视最多 64 个进程的限制。但这并不难,先监视前 64 个,当它们完成后(并且我们没有达到超时)开始监视仍在运行的下 64 个进程,等等。

我写了一篇关于所有这些细节的文章,其中包含指向 C 实现的链接,该实现特定于 R,但可能对非 R 实现者仍然有帮助。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用