Right now I try to understand the forking/rebinding of stdin/out/err of child processes and to manage the resources ( filehandles, sockets ) rightly without leaking any resources.
There are some questions left: After I create a socketpair and fork, I have in the parent 5 filedescriptors and in the child ( stdin/out/err/socket1/socket2 ). In the child process, I need to close the "parent" side of the socketpair. I close() stdin/out/err after the fork and dup() the "client end" of the socket three times. After the dup() , do I need to close the "source" of the dup? I guess yes ... but am I right?
When I create in this way (see below) a second child, is the resource handling right? I tried to rely heavily on RAII to not leak any fds, but is it right? Do I miss a big thing?
Bye and thanks in advance!
Georg
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include <cassert>
// this handle takes a fd, behaves like an int and makes sure the fd is closed again
class fdhandle {
public:
explicit fdhandle(int fd) {
mp_fd = std::shared_ptr<int>(new int, [=](int* pfd) {
close(*pfd);
delete pfd;
});
assert(mp_fd);
*mp_fd = fd;
}
operator int() {
assert(mp_fd);
return *mp_fd;
}
private:
std::shared_ptr<int> mp_fd;
};
void rebind_and_exec_child(fdhandle fd, std::string exe) {
// now close the std fds and connect them to the given fd
close(0); close(1); close(2);
// dup the fd three times and recreate stdin/stdout/stderr with fd as the target
if (dup(fd) != 0 || dup(fd) != 1 || dup(fd) != 2) {
perror("error duplicating socket for stdin/stdout/stderr");
exit(EXIT_FAILURE);
}
// now we can exec the new sub process and talk to it through
// stdin/stdout/stderr
char *arguments[4] = { exe.c_str(), exe.c_str(), "/usr/bin", NULL };
execv(exe.c_str(), arguments);
// this could should never be reached
perror("error: executing the binary");
exit(EXIT_FAILURE);
}
fdhandle fork_connected_child(std::string exe) {
// create the socketpair
int fd[2];
if (-1 == socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)) {
perror("error, could not create socket pair");
exit(EXIT_FAILURE);
}
fdhandle fdparent(fd[0]); fdhandle fdchild(fd[1]);
// now create the child
pid_t pid = fork();
switch (pid) {
case -1: // could not fork
perror("error forking the child");
exit(EXIT_FAILURE);
break;
case 0: // child
rebind_and_exec_child(fdchild);
break;
default: // parent
return fdparent;
break;
}
}
int main(int argc, const char** argv) {
// create 2 childs
fdhandle fdparent1 = fork_connected_child("/bin/ls");
fdhandle fdparent2 = fork_connected_child("/bin/ls");
}
I guess, I found the solution. For each created socket on the socketpair()
call, I set FD_CLOEXEC
. This way, I can be sure that the kernel closes all file descriptors. All other sockets which are handled by my code, will be closed by the fdhandle class call to close()
. The rebinding of the stdin/stdout/stderr, I replaced the dup()
for dup2()
because it does close and dup atomicly.
The hint was this page: http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html
File descriptors open in the calling process image shall remain open in the new process image, except for those whose close-on-exec flag
FD_CLOEXEC
is set. For those file descriptors that remain open, all attributes of the open file description remain unchanged. For any file descriptor that is closed for this reason, file locks are removed as a result of the close as described inclose()
. Locks that are not removed by closing of file descriptors remain unchanged.
This is now my adjusted code:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <memory>
#include <cassert>
#include <iostream>
// this handle takes a fd, behaves like an int and makes sure the fd is closed again
class fdhandle {
public:
fdhandle() {}
explicit fdhandle(int fd) {
mp_fd = std::shared_ptr<int>(new int, [=](int* pfd) {
close(*pfd);
delete pfd;
});
assert(mp_fd);
*mp_fd = fd;
// set FD_CLOEXEC on fd
int flags;
flags = fcntl(fd, F_GETFD);
if (-1 == flags) {
perror("error, could not get flags from filedescriptor");
exit(EXIT_FAILURE);
}
flags |= FD_CLOEXEC;
if (fcntl(fd, F_SETFD, flags) == -1) {
perror("error, could not set FD_CLOEXEC");
exit(EXIT_FAILURE);
}
}
operator int() {
assert(mp_fd);
return *mp_fd;
}
void show_fd_status() {
if (!mp_fd)
return;
int fd = *mp_fd;
using namespace std;
char buf[256];
int fd_flags = fcntl(fd, F_GETFD);
if (fd_flags == -1)
return;
int fl_flags = fcntl(fd, F_GETFL);
if (fl_flags == -1)
return;
char path[256];
sprintf(path, "/proc/self/fd/%d", fd);
memset(&buf[0], 0, 256);
ssize_t s = readlink(path, &buf[0], 256);
if (s == -1) {
cerr << " (" << path << "): " << "not available";
return;
}
cerr << fd << " (" << buf << "): ";
// file status
if (fd_flags & FD_CLOEXEC) cerr << "cloexec ";
if (fl_flags & O_APPEND) cerr << "append ";
if (fl_flags & O_NONBLOCK) cerr << "nonblock ";
// acc mode
if (fl_flags & O_RDONLY) cerr << "read-only ";
if (fl_flags & O_RDWR) cerr << "read-write ";
if (fl_flags & O_WRONLY) cerr << "write-only ";
if (fl_flags & O_DSYNC) cerr << "dsync ";
if (fl_flags & O_RSYNC) cerr << "rsync ";
if (fl_flags & O_SYNC) cerr << "sync ";
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_whence = 0;
fl.l_start = 0;
fl.l_len = 0;
fcntl(fd, F_GETLK, &fl);
if (fl.l_type != F_UNLCK)
{
if (fl.l_type == F_WRLCK)
cerr << "write-locked";
else
cerr << "read-locked";
cerr << "(pid:" << fl.l_pid << ") ";
}
}
private:
std::shared_ptr<int> mp_fd;
};
struct child
{
pid_t pid;
fdhandle fd;
};
void rebind_and_exec_child(fdhandle fd, std::string exe) {
// unset FD_CLOEXEC
int flags, oflags;
flags = oflags = fcntl(fd, F_GETFD);
if (-1 == flags) {
perror("error, could not get flags from filedescriptor");
exit(EXIT_FAILURE);
}
flags &= ~FD_CLOEXEC;
if (fcntl(fd, F_SETFD, flags) == -1) {
perror("error, could not unset FD_CLOEXEC");
exit(EXIT_FAILURE);
}
// close and rebind the stdin/stdout/stderr
// dup the fd three times and recreate stdin/stdout/stderr with fd as the target
if (dup2(fd, STDIN_FILENO) != 0 || dup2(fd, STDOUT_FILENO) != 1 || dup2(fd, STDERR_FILENO) != 2) {
perror("error duplicating socket for stdin/stdout/stderr");
exit(EXIT_FAILURE);
}
// restore the old flags
if (fcntl(fd, F_SETFD, oflags) == -1) {
perror("error, could not set FD_CLOEXEC");
exit(EXIT_FAILURE);
}
// now we can exec the new sub process and talk to it through
// stdin/stdout/stderr
char path[256];
char argv[256];
sprintf(path,"%s",exe.c_str());
sprintf(argv,"%d",30);
execlp(path, path, argv, 0);
// this should never be reached
perror("error: executing the binary");
exit(EXIT_FAILURE);
}
child fork_connected_child(std::string exe) {
// create the socketpair
int fd[2];
if (-1 == socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)) {
perror("error, could not create socket pair");
exit(EXIT_FAILURE);
}
fdhandle fdparent(fd[0]); fdhandle fdchild(fd[1]);
// now create the child
pid_t pid = fork();
switch (pid) {
case -1: // could not fork
perror("error forking the child");
exit(EXIT_FAILURE);
break;
case 0: // child
rebind_and_exec_child(fdchild, exe);
break;
default: // parent
std::cout << "forked " << exe << std::endl;
return child { pid, fdparent };
break;
}
}
int main(int argc, const char** argv) {
// setup the signal handler prior to forking
sleep(20);
// create 2 childs
{
child child1 = fork_connected_child("/usr/bin/sleep");
child child2 = fork_connected_child("/usr/bin/sleep");
int status;
waitpid(child1.pid, &status, 0);
waitpid(child2.pid, &status, 0);
}
sleep(20);
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.