使用bash如何找到给定进程的顶级父PID?

21

假设我运行ps axf命令,我能够看到我的进程树如下:

  800 ?        Ss     0:00 /usr/sbin/sshd
10186 ?        Ss     0:00  \_ sshd: yukondude [priv]
10251 ?        S      0:00      \_ sshd: yukondude@pts/0
10252 pts/0    Ss     0:00          \_ -bash
10778 pts/0    S      0:00              \_ su -
10785 pts/0    S      0:00                  \_ -su
11945 pts/0    R+     0:00                      \_ ps axf

我知道可以使用$$查看当前shell的PID(10785),或者$PPID查看父进程的PID(10778)。

但是我只想要顶层父进程的PID,在这个例子中应该是800(SSH守护程序)。有没有简单的方法来做到这一点?

我从这个SO答案中了解到,我可以递归检查/proc/PID/stat文件中的第四个条目以找到每个进程的父进程PID:

# cut -f4 -d' ' /proc/10785/stat
10778
# cut -f4 -d' ' /proc/10778/stat
10252
# cut -f4 -d' ' /proc/10252/stat
10251
# cut -f4 -d' ' /proc/10251/stat
10186
# cut -f4 -d' ' /proc/10186/stat
800
# cut -f4 -d' ' /proc/800/stat
1

(顶层父PID将是在到达init的PID之前的最后一个PID,即1。)

在编写一个小循环之前(我甚至不确定在bash中是否可以使用递归),有没有我正在忽略的更为直接的方法?也许是/proc下的另一个文件参数?通过这些文件进行grep并没有发现任何明显的东西。

编辑:当然,所有Linux进程的顶级进程都是具有PID 1的/sbin/init。我想要的是在那之前的父进程的PID:倒数第二个父进程的PID。


1
你不需要使用递归,只需要一个简单的循环就可以了... - JanC
1
没错,但递归更有趣,更具计算机科学感。 - yukondude
1
不行。循环更基础 :P 不要忘记你最基本的工具。 - Matt Joiner
科学包括寻找更有效的方法来实现相同的结果。不是更复杂和昂贵的方式... - Felipe Alvarez
@MattJoiner 是的和不是。大多数情况下,循环和调用堆栈是实现递归的手段。但这并不一定是这样的-特别是在实现尾调用优化的语言中-而且由于最直观和明显的解决方案通常是递归的,因此不应轻率地将递归驳回。递归和迭代都是强大的工具-在抽象数学、计算机科学和软件工程中。然而,递归的bash实现涉及分叉,所以如果我是你,我会避免使用它。 - Parthian Shot
6个回答

13

Bash 绝对可以进行递归。

您可以通过类似以下方式获取 stat 文件的第四个字段,而无需使用外部的 cut 实用程序:

stat=($(</proc/$$/stat))    # create an array
ppid=${stat[3]}             # get the fourth field

如果命令的名称可能包含空格,您可以从数组末尾开始计数(假设字段数量稳定)。即使命令名称中没有空格,这也适用。
ppid=${stat[-49]}           # gets the same field but counts from the end

这里有另一种技巧,它可以避免这些问题(但如果命令名称包含换行符可能会失败):

mapfile -t stat < /proc/$$/status
ppid=${stat[5]##*$'\t'}

那个文件中的第五个字段看起来像:
PPid:    1234

brace expansion会删除制表符之前的所有内容,只留下数字部分。

谢谢你的建议。我在发布的小脚本中使用了它作为可能的解决方案。 - yukondude
1
虽然这是一个旧的线程,但我想提供一个“sh”友好版本的上述代码:read _ _ _ ppid _ < /proc/${pid}/stat - Dave
如果命令名称中至少有一个空格字符,则ppid将不正确。 - Wis
@Wis:添加了一种替代方案,避免了那个问题(可能会以不同的方式付出代价)。 - Dennis Williamson
@DennisWilliamson 我找不到 manpage 中说字段数是一个常量的地方。我的想法是从开头迭代,直到找到一个以 ) 结尾的字段,然后返回它之后的字段,通常是 S 的字段。 - Wis
@Wis:我添加了另一种技术。 - Dennis Williamson

13

如果没有更好的解决方案,这里提供了一个简单(递归)脚本来获取任何进程号的顶层父进程PID(或者当前shell如果您省略PID参数):

#!/bin/bash
# Look up the top-level parent Process ID (PID) of the given PID, or the current
# process if unspecified.

function top_level_parent_pid {
    # Look up the parent of the given PID.
    pid=${1:-$$}
    stat=($(</proc/${pid}/stat))
    ppid=${stat[3]}

    # /sbin/init always has a PID of 1, so if you reach that, the current PID is
    # the top-level parent. Otherwise, keep looking.
    if [[ ${ppid} -eq 1 ]] ; then
        echo ${pid}
    else
        top_level_parent_pid ${ppid}
    fi
}

只需引用此脚本,然后根据需要使用或不使用PID参数调用top_level_parent_pid

感谢 @Dennis Williamson 在如何紧凑高效地编写此脚本上提供的许多建议。


1
我建议在脚本中使用函数来进行递归,而不是整个脚本。这样看起来更整洁。另外,按照现在的方式,你的脚本必须在 $PATH 中,否则你需要将 $0 $ppid 作为倒数第二行(递归调用)。顺便说一下,你的第一个 if...fi 可以被替换为这个:pid=${1:-$$} - Dennis Williamson
非常好的技巧。每次遇到bash参数替换规则时,我总是不得不查阅,但它们可以节省空间。我也考虑过使用函数,但在这个小脚本完成任务后,我变得有些懒惰了。如果它最终成为一个更常用的实用程序,我将不得不对其进行改进。 - yukondude
1
+1 看起来不错。你也可以将默认值放在函数内部。然后,如果你将该文件作为源文件,使得该函数“常驻”,它就可以在没有任何参数的情况下被调用。pid=${1:-$$} - Dennis Williamson
这是一个非常好的脚本。干得好!另外,您不应该使用社区维基,这个答案仍然值得点赞。我最终也用Python写了类似的东西,很高兴看到其他人也是这么做的。 - Matt Joiner

6

另一个解决方案(来自这里):

ps -p $$ -o ppid=

这只查找进程的直接父进程的PID,而不是“顶级”父进程。虽然这是一种非常整洁的方法。 - yukondude
1
如果您想避免空格填充,请使用“-o ppid:1 =” - bukzor
太棒了!谢谢你的分享,我们只需要添加递归,就能得到比切割、排列和解析其他命令更简洁的解决方案了。现在我只需要改变我一生中编写的所有bash脚本,将切割、awk和grep替换为简单的ps -o命令...太棒了! - undefined

1

迭代版本:

# ppid -- Show parent PID
# $1 - The process whose parent you want to show, default to $$
function ppid() {
    local stat=($(</proc/${1:-$$}/stat))
    echo ${stat[3]}
}

# apid -- Show all ancestor PID
# $1 - The process whose ancestors you want to show, default to $$
# $2 - Stop when you reach this ancestor PID, default to 1
function apid() {
    local ppid=$(ppid ${1:$$})
    while [ 0 -lt $ppid -a ${2:-1} -ne $ppid ]; do
        echo $ppid
        ppid=$(ppid $ppid)
    done
}

作为两个不同的函数,因为有时你只想要父进程的PID,有时你想要整个进程树。

1

我改进了你 (@yukondude) 的递归解决方案,以避免命令名称包含内部字段分隔符 (IFS) 字符(如空格、制表符和换行符),这些字符是合法的 Unix 文件名字符时出现问题。

#!/bin/bash
# Look up the top-level parent Process ID (PID) of the given PID, or the current
# process if unspecified.

function top_level_parent_pid {
    # Look up the parent of the given PID.
    pid=${1:-$$}
    ppid="$(awk '/^PPid:/ { print $2 }' < /proc/"$pid"/status)"
    # /sbin/init always has a PID of 1, so if you reach that, the current PID is
    # the top-level parent. Otherwise, keep looking.
    if [[ ${ppid} -eq 1 ]] ; then
        echo "${pid}"
    else
        top_level_parent_pid "${ppid}"
    fi
}

0

OS X版本,改编自@albert和@yukondude的答案:

#!/usr/bin/env bash
# Look up the top-level parent Process ID (PID) of the given PID, or the current
# process if unspecified.

# From https://dev59.com/GHA65IYBdhLWcg3w4CwL
function top_level_parent_pid {
    # Look up the parent of the given PID.
    PID=${1:-$$}
    PARENT=$(ps -p $PID -o ppid=)

    # /sbin/init always has a PID of 1, so if you reach that, the current PID is
    # the top-level parent. Otherwise, keep looking.
    if [[ ${PARENT} -eq 1 ]] ; then
        echo ${PID}
    else
        top_level_parent_pid ${PARENT}
    fi
}

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