2016-07-01 40 views
9

Sto usando GO per verificare se un processo (non padre) ha terminato ben, in pratica qualcosa come il comando pwait in FreeBSD ma scritto in go.Come attendere correttamente che un evento/processo finisca di non essere il genitore?

Attualmente mi sto cercando un for loop con un kill -0, ma noto che l'utilizzo della CPU è molto elevato 99% con questo approccio, ecco il codice:

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) 
} 

Qualsiasi idea di come migliorare o implementare correttamente questo.

Grazie in anticipo.

UPDATE

Aggiunta di un sleep all'interno del ciclo, come suggerito, aiuta a ridurre il carico.

Dai collegamenti forniti, sembra essere possibile allegare al pid esistente, darò una prova PtraceAttach ma non so se questo può avere effetti collaterali, qualche idea?

Come suggerito che ero disponibile per l'uso 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") 
} 

funziona bene, ma chiedendosi come implementare/ottenere gli stessi su Linux dal penso kqueue non disponibile su di esso, tutte le idee?

+3

Alcune idee da questa domanda: [Come attendere l'uscita di processi non figli] (http://stackoverflow.com/q/1157700) –

+2

Metti un breve sonno nel ciclo for. Linux non fornisce un modo efficiente per farlo – JimB

+1

L'API 'kqueue' è utilizzabile da' go' (nel pacchetto 'syscall'), e se la portabilità non è richiesta, oltre a * BSD e Darwin, quindi il BSD' pwait 'l'utilità potrebbe essere tradotta in' go'. – kdhp

risposta

1

Una soluzione potrebbe essere utilizzare il connettore netlink proc, che è un socket utilizzato dal kernel per consentire a userspace di conoscere i diversi eventi del processo. Il official documentation è un po 'carente, anche se ci sono un paio di good examples in C che sono probabilmente meglio leggere.

L'avvertimento principale sull'utilizzo del connettore proc è che il processo deve essere eseguito come root. Se è necessario eseguire il programma come utente non root, è necessario considerare altre opzioni, ad esempio il polling periodico /proc per controllare le modifiche. Qualsiasi approccio che utilizza il polling, come altri hanno sottolineato, è suscettibile a una condizione di competizione se il processo è terminato e un altro viene avviato con lo stesso PID tra un sondaggio e l'altro.

In ogni caso, per utilizzare il connettore proc in Go, dovremo fare qualche traduzione dal C. In particolare, abbiamo bisogno di definire i proc_event e exit_proc_event struct da cn_proc.h, e le cn_msg e cb_id struct da connector.h.

// 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 
} 

Abbiamo anche bisogno di creare un socket netlink e chiamare 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 
} 

Avanti, dobbiamo inviare il messaggio PROC_CN_MCAST_LISTEN al kernel per fargli sapere che vogliamo ricevere gli eventi. Possiamo importarlo direttamente da C, dove è definito come enum, per salvare un po 'di digitazione, e metterlo in una funzione poiché dovremo chiamarlo di nuovo con PROC_CN_MCAST_IGNORE quando abbiamo finito di ricevere i dati dal kernel.

// #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) 
} 

Dopo lasciamo che il kernel sa che siamo pronti a ricevere gli eventi, siamo in grado di riceverli sul socket stiamo creato. Una volta ricevuti, dobbiamo analizzarli e verificare i dati rilevanti. Ci interessa solo i messaggi che soddisfano i seguenti criteri:

  • venire dal kernel
  • hanno un tipo di intestazione di NLMSG_DONE
  • hanno un valore proc_event_header.what di PROC_EVENT_EXIT
  • abbinare la nostra PID

Se soddisfano questi criteri, possiamo estrarre le informazioni di processo rilevanti in una struttura proc_event_exit, che contiene il PID del processo.

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) 
      } 
     } 
    } 
} 

Un esempio di codice completo è here.