Duplicated File Descriptors and system()
For the last month or so I've been pretty heavily invested in building and programming an autonomous R/C car for Sparkfun's AVC. I decided to use a Raspberry Pi Model A+ as my embedded platform with an Arch Arm distro installed on it. All had been going quite well until I started working on the control software.
The Problem
Some time ago I stumbled across a fantastic driver written by richardghirst for the Raspberry Pi called servoblaster. Essentially, this driver enables the configuration and use of the Pi's GPIO header pins as PWM outputs. In other words, it allows you to easily drive R/C servos from the Pi with no additional hardware! This was exactly what I was looking for. There were two implementations of the driver, one kernel level, and another user level. richardghirst recommended the user level, so I decided to heed his suggestion.
At this time I began working on a simple UDP server that would run on the Pi. The server would allow me to control the car remotely over WiFi. This is what the beginning of that server looked like.
int main(int argc, char* argv[])
{
// open socket, setup sockaddr_in structure
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr = { };
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
printf("Setup using port %d\n", ntohs(addr.sin_port)); // say what port
assert(sock > 0); // sanity check
// bind the process to the desired port
int res = bind(sock, (const struct sockaddr*)&addr, sizeof(addr));
assert(res >= 0); // sanity check
// start up the control module, and servoblaster
assert(!conInit());
// ...
In the function call conInit() the driver daemon is started with the following calls.
// does the servo blaster device exist? (is the driver running?)
if(!stat("/dev/servoblaster", &buf)){
fprintf(stderr, "Servo driver already running\n");
}
// execute the servo blaster daemon
else if(system("servod --p1pins=37,38")){
fprintf(stderr, "Failed to start servo driver\n");
return -1;
}
Here, the program first looks to see if the driver has been started. It assumes if the device file exists, then the driver is live. Otherwise it attempts to start it with the call system("servod --p1pins=37,38").
This is where the problem was. If the driver isn't running and system("servod --p1pins=37,38") is called it forks a new child process. When that occurs all open file descriptors in the host process are automatically inherited by the child. So that means...
int sock = socket(AF_INET, SOCK_DGRAM, 0);
is thus inherited by the child process, which in this case is the servo driver. But because the driver is a daemon it will keep running after the UDP server has shut down. This means, the servo driver will keep sock open and bound to the port specified above. Which will result in an EADDRINUSE errno if a bind() to that port is attempted again.
The Solution
After some searching I found that luckily this was a very easy fix. It came down entirely to adding one function call shortly after opening the socket.
int sock = socket(AF_INET, SOCK_DGRAM, 0);
fcntl(sock, F_SETFD, fcntl(sock, F_GETFD) | FD_CLOEXEC);
The fcntl function is used to get and set properties of file descriptors. The current flags of sock are retrieved with the fcntl(sock, F_GETFD) call. Those flags are then or'ed together with the flag FD_CLOEXEC. Here's what the man pages say about that flag.
FD_CLOEXEC
Close-on-exec; the given file descriptor will be auto-
matically closed in the successor process image when
one of the execv(2) or posix_spawn(2) family of system
calls is invoked.
Perfect! That was exactly the behavior that I had needed. And sure enough the server worked as intended. So much so that I could drive my car around from my laptop :)