vendredi 13 avril 2018

Fork a Go process

Today I got a problem with the project I'm working on. Basically file descriptor were messing with each other. And rather than having a proper management of all the file descriptors, I decided to just "contain" the different things I was doing in different process. Each process has its own file descriptor table: problem solved.

However, Go developers seems to really want us NOT to do this. So, forking a Go process is not as simple as a C/C++ one. Here I'm going to explain how I did it anyway :P Also setting up some communication between the two processes: Go usual communication do not encompass this use case.

Edit: Well, after more usage, it appears this is not usable. The reason is we are probably conflicting with how Go manages thread and how that does not work very well with fork. Don't think there a good way to "properly" do that without diving into how the Go runtime is working. And at this point, I'm just better off just compiling another binary and calling it.

TL;DR: Fork using cgo and communication using pipes. Note that you can use any usual inter-process communication. Just not the usual Go ones.
Complete code: https://github.com/Jiliac/go-clone/blob/master/fork.go 

I - Forking

First make a wrapper around libc fork call. This could be done without libc/cgo, but I went for the simple way :-)

//#include 
//
//int cFork() {
//    pid_t pid;
//    pid = fork();
//    return ((int)pid);
//}
import "C"

You can test it works correctly with:

func forkTest() 
        pid := int(C.cFork())
        if pid == 0 {
                fmt.Println("Child")
        } else {
                fmt.Println("Parent")
        }
}

So not very complicated in the end.

II - Communication

Any IPC can be used here. Chose to use pipes because that's I'm comfortable with. Can use networking anything as well...

Problem using pipes is that the ones from os.Pipe are set with FD_CLOEXEC flag. (see https://golang.org/src/os/pipe_linux.go?s=319:360#L1).

So we have to open these pipes by hand; and then 'convert' them into os.File (names are not important) so we can conveniently use them:

func pipe() (r, w *os.File, err error) {
        var p [2]int
        err = syscall.Pipe(p[0:])
        if err != nil {
                return r, w, err
        }

        r = os.NewFile(uintptr(p[0]), "|0")
        w = os.NewFile(uintptr(p[1]), "|1")
        return r, w, err
}
I have a pipeTest function on github to check if these are working correctly. Putting it all together we have:

func forkComTest() {
        r, w, err := pipe()
        if err != nil {
                log.Printf("Failed to create pipes: %v.\n", err)
                return
        }

        pid := int(C.cFork())
        if pid == 0 { // Child
                fmt.Printf("(from child)\tHello.\n")
                err = r.Close()
                if err != nil {
                        log.Printf("Child could not close reading pipe: %v.\n", err)
                        return
                }

                _, err := w.Write([]byte(msg))
                if err != nil {
                        log.Printf("Could not write in pipe: %v.\n", err)
                }

        } else { // Parent
                fmt.Printf("(from parent)\tChild pid = %d.\n", pid)
                err = w.Close()
                if err != nil {
                        log.Printf("Parent could not close writing pipe: %v\n.", err)
                        return
                }

                buf := make([]byte, bufSize)

                _, err := r.Read(buf)
                if err != nil {
                        log.Printf("Could not reat from pipe: %v.\n", err)
                }

                msg := string(buf)
                fmt.Printf("(from parent)\tMessage: '%s'\n", msg)
        }
}
Which should output something like:

(from parent)   Child pid = 47158.
(from child)    Hello.
(from parent)   Message: 'Hello dad, it's your son :-).'
And here we go.

Aucun commentaire:

Publier un commentaire