如何等待非子进程退出
- 2024-10-24 08:50:00
- admin 原创
- 64
问题描述:
对于子进程,可以使用wait()
和waitpid()
函数暂停当前进程的执行,直到子进程退出。但该函数不能用于非子进程。
是否有其他函数可以等待任何进程的退出?
解决方案 1:
没有什么等同于wait()
。通常的做法是使用 进行轮询kill(pid, 0)
并查找返回值 -1 和errno
来ESRCH
指示该进程已消失。
更新:自 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
警告:不要试图监视1
或2
!因为它们指向!!如果你监视其中任何一个,那么系统中/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:
这里的答案中已经有很多好主意了。我不得不为多个操作系统实现等待多个非子进程的进程。以下是摘要。
如果操作系统支持
kqueue(2)
,那么就使用它,效果很好。这适用于 macOS 和似乎所有的 *BSD 系统。在 Linux 5.3 或更高版本上使用
pidfd_open(2)
。您可以编译一个pidfd_open(2)
在较新的 Linux 上使用的二进制文件,并在较旧的 Linux 上回退到下一个解决方案,特别是 RHEL 7 和 RHEL 8。在较旧的 Linux 上使用。我通过观察上的事件
inotify(7)
获得了最佳结果,但有几个问题。其他进程可能正在关闭该文件上的 fd,而不是我们正在等待的 fd。此外,进程可能会调用,这也是一个事件,但进程仍在运行。如果发生这种情况,我们需要开始观察新的。close()
`/proc/<pid>/exeexecve()
close()`/proc/<pid>/exe
在 Windows 上,我们可以使用
WaitForMultipleObjects()
。这里烦人的是,我们需要解决它只能监视最多 64 个进程的限制。但这并不难,先监视前 64 个,当它们完成后(并且我们没有达到超时)开始监视仍在运行的下 64 个进程,等等。
我写了一篇关于所有这些细节的文章,其中包含指向 C 实现的链接,该实现特定于 R,但可能对非 R 实现者仍然有帮助。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件