linux - 如何正确等待事件/过程完成不是 parent ?

标签 linux go freebsd kqueue

我正在使用 GO检查进程(不是父进程)是否已终止,基本上类似于 pwait FreeBSD 中的命令而是用 go 写的。

目前我正在尝试使用 kill -0for 循环,但我注意到 CPU 使用率非常高 99%这种方法,代码如下:

package main

import (
    "fmt"
    "os"
    "strconv"
    "syscall"
    "time"
)

func main() {

    if len(os.Args) != 2 {
        fmt.Printf("usage: %s pid", os.Args[0])
        os.Exit(1)
    }

    pid, err := strconv.ParseInt(os.Args[1], 10, 64)
    if err != nil {
        panic(err)
    }

    process, err := os.FindProcess(int(pid))

    err = process.Signal(syscall.Signal(0))
    for err == nil {
        err = process.Signal(syscall.Signal(0))
        time.Sleep(500 * time.Millisecond) 
    }
    fmt.Println(err)
}

关于如何改进或正确实现这一点的任何想法。

提前致谢。

更新

像建议的那样在循环中添加 sleep 有助于减少负载。

从提供的链接来看,似乎可以附加到现有的pid,我会试试PtraceAttach但不知道这会不会有副作用,有什么想法吗?

按照建议,我可以使用 kqueue :

package main

import (
    "fmt"
    "log"
    "os"
    "strconv"
    "syscall"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Printf("usage: %s pid", os.Args[0])
        os.Exit(1)
    }

    pid, err := strconv.ParseInt(os.Args[1], 10, 64)
    if err != nil {
        panic(err)
    }

    process, _ := os.FindProcess(int(pid))

    kq, err := syscall.Kqueue()
    if err != nil {
        fmt.Println(err)
    }

    ev1 := syscall.Kevent_t{
        Ident:  uint64(process.Pid),
        Filter: syscall.EVFILT_PROC,
        Flags:  syscall.EV_ADD,
        Fflags: syscall.NOTE_EXIT,
        Data:   0,
        Udata:  nil,
    }

    for {
        events := make([]syscall.Kevent_t, 1)
        n, err := syscall.Kevent(kq, []syscall.Kevent_t{ev1}, events, nil)
        if err != nil {
            log.Println("Error creating kevent")
        }
        if n > 0 {
            break
        }
    }

    fmt.Println("fin")
}

工作正常,但想知道如何在 linux 上实现/实现相同的功能,因为我认为 kqueue 不可用,有什么想法吗?

最佳答案

一种解决方案是使用 netlink proc 连接器,它是内核用来让用户空间了解不同进程事件的套接字。 official documentation 有点欠缺,尽管 C 中有几个 good examples 可能更好读。

使用 proc 连接器的主要注意事项是进程必须以 root 身份运行。如果要求以非 root 用户身份运行程序,则应考虑其他选项,例如定期轮询 /proc 以观察更改。正如其他人指出的那样,任何使用轮询的方法都容易受到竞争条件的影响,如果进程终止并且在轮询之间使用相同的 PID 启动另一个进程。

无论如何,要在 Go 中使用 proc 连接器,我们必须从 C 中进行一些翻译。具体来说,我们需要从 cn_proc.h 定义 proc_eventexit_proc_event 结构,以及来自 connector.hcn_msgcb_id 结构体。

// CbID corresponds to cb_id in connector.h
type CbID struct {
    Idx uint32
    Val uint32
}

// CnMsg corresponds to cn_msg in connector.h
type CnMsg struct {
    ID CbID
    Seq uint32
    Ack uint32
    Len uint16
    Flags uint16
}

// ProcEventHeader corresponds to proc_event in cn_proc.h
type ProcEventHeader struct {
    What uint32
    CPU uint32
    Timestamp uint64
}

// ExitProcEvent corresponds to exit_proc_event in cn_proc.h
type ExitProcEvent struct {
    ProcessPid uint32
    ProcessTgid uint32
    ExitCode uint32
    ExitSignal uint32
}

我们还需要创建一个netlink socket并调用bind。

sock, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_DGRAM, unix.NETLINK_CONNECTOR)

if err != nil {
    fmt.Println("socket: %v", err)
    return
}

addr := &unix.SockaddrNetlink{Family: unix.AF_NETLINK, Groups: C.CN_IDX_PROC, Pid: uint32(os.Getpid())}
err = unix.Bind(sock, addr)

if err != nil {
    fmt.Printf("bind: %v\n", err)
    return
}

接下来,我们必须向内核发送 PROC_CN_MCAST_LISTEN 消息,让它知道我们想要接收事件。我们可以直接从 C 中导入它,它被定义为一个枚举,以节省一些输入,并将其放入一个函数中,因为当我们完成从内核。

// #include <linux/cn_proc.h>
// #include <linux/connector.h>
import "C"

func send(sock int, msg uint32) error {
    destAddr := &unix.SockaddrNetlink{Family: unix.AF_NETLINK, Groups: C.CN_IDX_PROC, Pid: 0} // the kernel
    cnMsg := CnMsg{}
    header := unix.NlMsghdr{
        Len: unix.NLMSG_HDRLEN + uint32(binary.Size(cnMsg) + binary.Size(msg)),
        Type: uint16(unix.NLMSG_DONE),
        Flags: 0,
        Seq: 1,
        Pid: uint32(unix.Getpid()),
    }
    msg.ID = CbID{Idx: C.CN_IDX_PROC, Val: C.CN_VAL_PROC}
    msg.Len = uint16(binary.Size(msg))
    msg.Ack = 0
    msg.Seq = 1
    buf := bytes.NewBuffer(make([]byte, 0, header.Len))
    binary.Write(buf, binary.LittleEndian, header)
    binary.Write(buf, binary.LittleEndian, cnMsg)
    binary.Write(buf, binary.LittleEndian, msg)

    return unix.Sendto(sock, buf.Bytes(), 0, destAddr)
}

在我们让内核知道我们已经准备好接收事件之后,我们可以在我们创建的套接字上接收它们。一旦我们收到它们,我们需要解析它们,并检查相关数据。我们只关心满足以下条件的消息:

  • 来自内核
  • 具有 NLMSG_DONE
  • 的 header 类型
  • 有一个 proc_event_header.what 值为 PROC_EVENT_EXIT
  • 匹配我们的 PID

如果它们满足这些条件,我们可以将相关的进程信息提取到一个 proc_event_exit 结构体中,其中包含进程的 PID。

for {
    p := make([]byte, 1024)
    nr, from, err := unix.Recvfrom(sock, p, 0)

    if sockaddrNl, ok := from.(*unix.SockaddrNetlink); !ok || sockaddrNl.Pid != 0 {
        continue
    }

    if err != nil {
        fmt.Printf("Recvfrom: %v\n", err)
        continue
    }

    if nr < unix.NLMSG_HDRLEN {
        continue
    }

    // the sys/unix package doesn't include the ParseNetlinkMessage function
    nlmessages, err := syscall.ParseNetlinkMessage(p[:nr])

    if err != nil {
        fmt.Printf("ParseNetlinkMessage: %v\n", err)
        continue
    }

    for _, m := range(nlmessages) {
        if m.Header.Type == unix.NLMSG_DONE {
            buf := bytes.NewBuffer(m.Data)
            msg := &CnMsg{}
            hdr := &ProcEventHeader{}
            binary.Read(buf, binary.LittleEndian, msg)
            binary.Read(buf, binary.LittleEndian, hdr)

            if hdr.What == C.PROC_EVENT_EXIT {
                event := &ExitProcEvent{}
                binary.Read(buf, binary.LittleEndian, event)
                pid := int(event.ProcessTgid)
                fmt.Printf("%d just exited.\n", pid)
            }
        }
    }
}

完整的代码示例是 here

关于linux - 如何正确等待事件/过程完成不是 parent ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38153350/

相关文章:

linux - 指向文件夹的符号链接(symbolic link)未按预期工作

go - JetBrains GoLand 中没有 go.exe。程序不会运行。不知道问题是什么

go - 为什么不能通过同时计算 slice 的不同部分来加速程序?

linux - 空闲超时 Bourne 风格的 shell

linux - 无法以非 root 用户身份聚集

freebsd - 强制核心从 FreeBSD 上事件的、正常运行的程序转储

c - 在不知道文件系统类型或源设备的情况下使用 mount() 重新挂载文件系统

linux - 有没有办法从 git lfs 指针下载实际文件?

python - 如何在循环中使用 os.pipe() (复制多个管道)?

go - 在本地使用带有 go mod 的子包