10

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

目前我正在尝试 a for loopkill -0但我注意到这种方法的 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它不可用,有什么想法吗?

4

1 回答 1

3

一种解决方案是使用 netlink proc 连接器,这是内核用来让用户空间了解不同进程事件的套接字。官方文档有些欠缺,尽管有几个 C 语言的好例子可能更好读。

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

无论如何,要在 Go 中使用 proc 连接器,我们必须从 C 中进行一些翻译。具体来说,我们需要定义cn_proc.hproc_event中的andexit_proc_event结构体,以及connector.h中的and结构体。cn_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 套接字并调用 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消息发送到内核,让它知道我们想要接收事件。PROC_CN_MCAST_IGNORE我们可以直接从 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
  • 有一个proc_event_header.whatPROC_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)
            }
        }
    }
}

完整的代码示例在这里

于 2017-02-10T21:15:21.440 回答