C, Golang, Perl, and Unix
Over the past couple months I have had the somewhat uncomfortable realization that some of my assumptions about all programs are wrong. Read all about the journey involving Unix, C, Perl, and Go.
🔗 Unix Process Model Foundations: fork(2)
and execve(2)
Before going into any of this I need to make sure that you understand the Unix
Process Model. The main detail I want to communicate is how fork
and exec
can be used in conjunction.
If you’ve never used fork
before you will likely find it incredibly alien. To
anyone who uses high level languages it seems like it should take a code
reference or something. After you call fork
your process has branches into
the child and the parent. The child sees a 0
as the return code from fork
and the parent sees some other number, which is the process id of the child.
(This ignores the error case, which does not add to the discussion here.)
my $pid = fork();
if ($pid == 0) {
# this is a child process
} else {
# this is the parent
}
After fork
ing, almost everything is the same. In fact, the documentation
for fork(2)
(at least on Linux) primarily documents what is not the same.
All the files that were open in the parent are open in the child. All the
signal handlers are still in place. In fact even the memory is the same, but
it’s not shared. There are more inherited things I’ve not had to (gotten to?)
play with.
When you run a totally new program you use exec
after fork
ing. The exec
ed
process does not have the memory of the parent, but does have the file
descriptors and some of the signal handlers.
You can see some of the effect of this with snippets like this:
In the following code, cat
(or any child process) would ignore a SIGHUP
:
$SIG{HUP} = 'IGNORE';
system q(cat /proc/self/status | grep '^Sig');
Compare the output of the above to simply running
cat /proc/self/status | grep '^Sig'
in your shell.
In the following code we open /etc/passwd
and we can see that ls
also has it
open:
use Fcntl;
open my $fh, '<', '/etc/passwd'
or die "open: $!";
# ensure that the log doesn't get closed on exec
fcntl $fh, F_SETFD,
fcntl($fh, F_GETFD, 0) &~ FD_CLOEXEC;
system q(ls -l /proc/self/fd);
Super quick note about the file descriptors above, only because it will be a little relevant later: STDIN, STDOUT, and STDERR are simply file descriptors 0, 1, and 2 (respectively.) Supposedly those constants can change from system to system, so you might want to use whatever constant your compiler has. Here’s a way to see the fact that 0, 1, and 2 are just the normal file descriptors:
$ ls -l /proc/self/fd </etc/passwd 2>/dev/null
total 0
lr-x------ 1 frew frew 64 Apr 28 22:28 0 -> /etc/passwd
lrwx------ 1 frew frew 64 Apr 28 22:28 1 -> /dev/pts/7
l-wx------ 1 frew frew 64 Apr 28 22:28 2 -> /dev/null
lr-x------ 1 frew frew 64 Apr 28 22:28 3 -> /proc/135798/fd
You can see that 0
(STDIN) is reading from /etc/passwd
instead of the tty,
and 2
(STDERR) is writing to /dev/null
. We leave STDOUT alone so we can see
the output.
🔗 Neat Applications
There are a lot of cool ways you can take advantage of this. One obvious, very
basic way is to open a log file in append mode. All newly forked child
processes will inherit the file descriptor and be able to atomically append
(depending on size of your write(2)
s and capabilities provided by your OS.)
Another use of inheriting file descriptors is UCSPI, which I wrote about a couple of years ago. Really though, a huge amount of the modern djb inspired init systems leverage these details to allow simple tools (the Unix philosophy) to function within the Unix Process Model (or the Unix Environment.)
🔗 Bizarre Bugs
On the other hand, these behaviours can be surprising if you are not aware of them. A couple of years ago at work we had a really bizarre bug. The cause was that our web server (which does a bunch of stuff before forking off a few dozen workers) was connecting to memcache before the fork, and the children were inheriting the socket. Honestly, I don’t even know all of the implications of this. I know that (at least in Perl) database wrappers typically detect a fork and reconnect because things will not go well for you if you keep using the same file descriptor (which is a shared TCP socket.)
I suspect this can work for very simple, single write(2)
, line based protocols
where interleaving with the other children is not possible, but even then I’m
not sure how the children would get the response from the other end.
🔗 Pomotimer and My “hesitating at the angles of stairs” Moment
As discussed a couple times now I am working to port some of my personal kit to
Go. One of the tools I ported recently is called pomotimer
. I
originally used this tool when I was doing the pomodoro technique.
While I have both abandoned the pomodoro technique and completely
reimplemented the set of tools I used before, pomotimer
is still a
really convenient tool.
pomotimer
takes a duration (as in 3m20s
) and updates a “remaining time” view
every second. Every thirty seconds it updates the tmux title, so if you aren’t
looking right at it the window will be called something like PT5:30
. You can
press !
to abort the timer, p
to pause it, and r
to reset it entirely.
Originally the tool was written with IO::Async in Perl. Migrating to Go,
despite being written in a totally different style, was not hard… Except for
the tmux title feature. In Perl (and many other languages) you can set the
process title (see setproctitle(3)
) with a simple assignment to $0
:
$0 = "program name"
The benefit is that under ps(1)
, top(1)
, and in this case, tmux(1)
itself,
the program will show up with the name you choose. I have used this trick in
web servers so that the workers show which URL they are servicing when you look
at ps
. Pretty handy!
Go does not have a built in setproctitle(3)
mechanism, but people have built
libraries to do it as well as written simple snippets. When
I ported pomotimer
to Go I figured I would simply use one of the above and be
on my way. The hard part is the timer and keyboard interaction, right?
Wrong. I found that sometimes setting the title worked, and sometimes it
didn’t. When I looked closely, it appeared to be because Go was scheduling the
prctl(2)
code to run in one of the threads. This meant that while the thread
got named, the main process did not. Go specifically reserves the right to
reschedule your goroutine onto other OS threads, so bouncing from one thread to
another does happen. (The linked library uses a different, hackier, but more
reliable method.)
What I realized when all this was happening is that Go is not a simple “client Unix language,” exposing various system calls and abstractions to the programmer. Go is far stranger, in the world of Unix. What I didn’t realize was how much I had taken for granted the fact that Perl, my daily driver for decades, is nestled close to both Unix and C with respect to memory model and process model.
I am not sure if I would have noticed this if it weren’t for Mike Conrad’s post about Perl.
🔗 The Right Tool for the Job
(The following includes affiliate links.)
Languages are built with use-cases in mind. Whether you like Perl or not, it is
fairly clear that it is optimizied more for one-liner commandline scripts than
almost anything else, except sed(1)
and awk(1)
. I remember reading in
either
The Camel Book
or
The Llama Book
about twenty years ago: “if you
implement your favorite sort algorithm in Perl and then find that it is slow, do
not be surprised. That’s like using a violin as a hammer.”
Go is no exception here. Go is built to be fast, simple, and concurrent. Go is not built to match the Unix and C semantics.
🔗 Tsar
At work we have this tool called tsar
; it exists to capture STDOUT and STDERR,
unmerged, and log them to datestamped files. You could think of it as a
multilog on steroids. Unlike multilog
, tsar
handles running the
actual service in question, so that it can easily maintain separate pipes for
STDERR and STDOUT. Unfortunately we found that it is very difficult to follow
both STDOUT and STDERR, bubble up error codes, and forward signals, without race
conditions.
Instead of spending hours trying to fix the actual problems, we decided to
vastly simplify tsar
…
🔗 Iconoclastic fork
/exec
If you have code that runs another program, almost universally the pattern is as follows (likely abstracted:)
my $pid = fork();
if ($pid != 0) {
# parent waits for child to return
waitpid(...)
} else {
# child runs whatever program
exec('/bin/ls', '/home')
}
(As before, error handling left out for clarity.)
But you don’t have to exec
in the child! If you exec
in the parent, you can
create a child process of an unsuspecting parent. Imagine we want to capture
the output of some third party program. You can arrange to receive STDOUT and
STDERR on a couple of pipes, fork, exec the child (like tsar
) and in the
parent, exec
the third party program.
🔗 The C That Worked
Because Go is implemented as a runtime executing code across multiple threads,
calling fork(2)
will not work the way that we would need to do the above.
fork(2)
splits a single thread in two, not a VM in two. Furthermore, you
cannot call exec(2)
on any thread other than the main one. So ultimately
implementing this pattern directly in Go (such that the child process does not
have to be exec
’d, to be clear) ends up with a single threaded runtime as the
child. (Note: for kicks I did actually implement a forking thing in Go and
was not able to break the child process in ways I expected.)
Perl would work, because Perl is a relatively thin layer on C, when it comes to this kind of thing. We decided to write it in C to avoid forcing containers to have Perl installed. I wrote it in C and it compiled, ran, and worked without flaw the first try. I will forever be proud of this surprising twist of fate.
Here’s the meat of the C, slightly simplified:
int main (int argc, char **argv) {
const int READ = 0;
const int WRITE = 1;
int pid, oldpid;
int errpipefd[2];
int outpipefd[2];
char errenv[16];
char outenv[16];
oldpid = getpid();
do_pipe(outpipefd);
do_pipe(errpipefd);
pid = fork();
if (pid > 0) {
// service
do_close(errpipefd[READ]);
do_close(outpipefd[READ]);
do_dup2(errpipefd[WRITE], STDERR_FILENO);
do_close(errpipefd[WRITE]);
do_dup2(outpipefd[WRITE], STDOUT_FILENO);
do_close(outpipefd[WRITE]);
do_exec(service);
}
// tsar-logger
do_close(STDIN_FILENO);
do_close(errpipefd[WRITE]);
do_close(outpipefd[WRITE]);
sprintf(errenv, "%d", errpipefd[READ]);
sprintf(outenv, "%d", outpipefd[READ]);
setenv("TSARERRFD", errenv, 1);
setenv("TSAROUTFD", outenv, 1);
setenv("TSAR_RUNNING", "1", 1);
execv("tsar-logger");
fprintf(stderr, "tsar wrapper: exec %s failed: %s (%d)\n", argv[0], strerror(errno), errno);
kill(oldpid, SIGTERM);
exit(2);
}
static void
do_close(int fd) {
if (close(fd) == -1 ) {
fprintf(stderr, "tsar wrapper: close(%d) failed: %s (%d)\n", fd, strerror(errno), errno);
exit(2);
}
}
All of the do_
methods follow the general pattern above. Most of the error
handling is left out of the above. If the tsar-logger
fails to exec(2)
we
are in a particularly tough spot, where if the service doesn’t check the return
value when it writes to STDOUT
or STDERR
it will just keep running forever.
In an effort to maintain simplicity but also protect against this, we send a
TERM
signal to the parent. If it ignores TERM
or that fails… oh well,
there’s only so much you can do, right?
🔗 Basic Logger Examples
Here are a couple simple loggers I made to test the code above. I only handled
one pipe at a time, but with Go it should be trivial to follow both. With Perl
it could be done without a ton of effort with select(2)
. Here’s the Perl
version:
open my $stdout, '<&', $ENV{TSAROUTFD}
or die "couldn't open TSAROUTFD: $!\n";
while (<$stdout>) {
print localtime . " --> $_"
}
And here’s the Go version:
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"time"
)
func main() {
fd, err := strconv.Atoi(os.Getenv("TSAROUTFD"))
if err != nil {
fmt.Printf("Couldn't parse TSAROUTFD: %s\n", err)
os.Exit(3)
}
outPipe := os.NewFile(uintptr(fd), "outPipe")
scanner := bufio.NewScanner(outPipe)
for scanner.Scan() {
fmt.Println(time.Now().Truncate(time.Second), "-->", scanner.Text())
}
}
I really enjoyed this little adventure. I’ve only written C professionally a couple of times, and this was a rewarding and interesting little project. I also very much enjoy the mental shifts when I realize that something I have been taking for granted is actually false.
There is so much that you can read to learn more about all of this stuff, aside from your friendly local manpages. As I’ve mentioned before, to learn more about Go, I strongly recommend The Go Programming Language. It’s one of the best language oriented programming books I’ve ever read, and one of the best on it’s own. I suggest reading it even if you already know Go in and out.
For the Unix stuff discussed here, Advanced Programming in the UNIX Environment (aka Stevens). is the canonical resource. It can be a bit slow but will definitely help fill in the various gaps you may have in your mental model.
🔗 Forking Go
This worked for me; would be interested in hearing where it breaks for others:
package main
import (
"fmt"
"golang.org/x/sys/unix"
"os"
"os/exec"
"syscall"
"time"
)
func main() {
pid, _, err := unix.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
if int(pid) == -1 {
fmt.Println("Failed to fork:", err)
os.Exit(1)
}
if pid == 0 {
child()
} else {
parent(int(pid))
}
}
func parent(childPid int) {
fmt.Println("I'm the parent")
unix.Wait4(childPid, nil, 0, nil)
fmt.Println("baby returned")
}
func child() {
fmt.Println("I'm the baby, gotta love me!")
time.Sleep(1 * time.Second)
out, err := exec.Command("date").Output()
if err != nil {
fmt.Println("failed to run date:", err)
os.Exit(1)
}
fmt.Printf("The date is %s\n", out)
ch := make(chan bool)
go func() {
<-ch
fmt.Println("in a goroutine!")
}()
ch <- true
}
If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.