fork()与C++继承机制的冲突问题排查
我用单个类同时实现服务端与客户端功能,但核心问题不在此。服务端构造函数中,我会先检查端口是否被占用,若未被占用则通过fork()创建后台守护进程,但生成的服务端始终是基类实例——子类重写的消息处理逻辑完全不生效。我怀疑fork()执行时子类尚未完成初始化,子进程继承的是基类状态,求确认该问题原因及可行解决方案。
注:子进程的输出目前无法正常工作,尝试将spdlog替换为标准iostream但操作有误未成功。
相关代码
//=============== HEADER ====================== #include <cstdint> #include <vector> #include <netinet/in.h> #include <fstream> #include <iostream> #define DAEMON_BUFSIZE (1024) class daemon { private: int sockfd; struct sockaddr_in servaddr; void init_logger(const char *fn="log.txt") { std::ofstream out("out.txt"); std::streambuf *coutbuf = std::cout.rdbuf(); //save old buf std::cout.rdbuf(out.rdbuf()); //redirect std::cout to out.txt! } void server(); void client(); protected: enum server_cmd { server_reply, server_noreply, server_teardown }; virtual void init_server() {} virtual server_cmd handle_msg(std::vector<char> &msg) { std::cout << "base class called!" << std::endl; return server_noreply; }; public: class exception : public std::exception {}; explicit daemon(uint16_t port); daemon(const char *addr, uint16_t port); virtual ~daemon() = default; int send_msg(std::vector<char> &msg, bool needs_reply= false); }; //=============== IMPLEMENTATION ====================== #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #include <cstring> static int check_daemon(uint16_t port) { int sockfd; // Creating socket file descriptor if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { std::cout << "[CRITICAL] server socket creation failed (should never happen): " << strerror(errno) << std::endl; exit(EXIT_FAILURE); } struct sockaddr_in servaddr{AF_INET, htons(port), INADDR_ANY}; // Bind the socket with the server address if (bind(sockfd, (const struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { // port is busy, so daemon is running! std::cout << "daemon found" << std::endl; return -1; } return sockfd; } /** * Server constructor * @param port */ daemon::daemon(uint16_t port) { sockfd = check_daemon(port); if (sockfd <= 0) { throw exception(); } auto pid = fork(); if (pid < 0) { std::cout << "[CRITICAL] fork failed: " << strerror(errno) << std::endl; } else if (pid == 0) { init_logger(); // child; daemonize server(); exit(EXIT_SUCCESS); } // parent; wait for child sleep(1); // should be done better } void daemon::server() { std::cout << "[INFO] starting daemon" << std::endl; ::daemon(1, 1); // here we are in daemon std::cout << "[INFO] server_daemon: initializing" << std::endl; init_server(); struct sockaddr_in cliaddr{}; char buff[DAEMON_BUFSIZE]; bool running = true; std::cout << "[TRACE] server_daemon: entering loop" << std::endl; while (running) { socklen_t len = sizeof(cliaddr); auto n = recvfrom(sockfd, buff, DAEMON_BUFSIZE, MSG_WAITALL, (struct sockaddr*) &cliaddr, &len); if (n == -1) { std::cout << "[CRITICAL] recvfrom error (" << errno << "): " << strerror(errno) << std::endl; throw exception(); } std::vector<char> msg(buff, buff+n); std::cout << "[TRACE] server_daemon: message received (" << n << " bytes)" << std::endl; auto v = handle_msg(msg); switch (v) { case server_reply: sendto(sockfd, msg.data(), msg.size(), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, len); std::cout << "[TRACE] reply sent: " << msg.size() << " bytes" << std::endl; break; case server_noreply: std::cout << "[TRACE] no reply sent" << std::endl; break; case server_teardown: running = false; break; } } std::cout << "[INFO] server_daemon: at exit" << std::endl; } daemon::daemon(const char *addr, uint16_t port) { // Creating socket file descriptor if ( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) { std::cout << "[CRITICAL] socket creation failed (" << errno <<"): " << strerror(errno) << std::endl; throw exception(); } // Filling server information servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); servaddr.sin_addr.s_addr = inet_addr(addr); } int daemon::send_msg(std::vector<char> &msg, bool needs_reply) { auto ret = sendto(sockfd, msg.data(), msg.size(), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); if (ret < 0) { std::cout << "[ERROR] send failed: " << strerror(errno) << std::endl; return -1; } if (needs_reply) { char buff[DAEMON_BUFSIZE]; socklen_t len; auto n = recvfrom(sockfd, buff, DAEMON_BUFSIZE, MSG_WAITALL, (struct sockaddr *)&servaddr, &len); msg.assign(buff, buff+n); std::cout << "[TRACE] received answer (" << n << " bytes)" << std::endl; return n; } return 0; } void daemon::client() { } #define test_me #ifdef test_me #include <sstream> class server : public daemon { public: explicit server(uint16_t port) : daemon(port) { std::cout << "[INFO] server constructor" << std::endl; } protected: void init_server() override { std::cout << "[INFO] init_server" << std::endl; } server_cmd handle_msg(std::vector<char> &msg) override { std::string s(msg.begin(), msg.end()); std::cout << "[INFO] received '" << s << "'" << std::endl; return (s == "END")? server_teardown: server_noreply; }; public: }; class client : public daemon { public: explicit client(const char *addr, uint16_t port) : daemon(addr, port) {} protected: server_cmd handle_msg(std::vector<char> &msg) override { std::cout << "[CRITICAL] client handle_msg() called" << std::endl; return server_teardown; }; public: }; int main() { try { server server(12345); } catch (daemon::exception &e) { std::cout << "[INFO] daemon already running" << std::endl; } client client("127.0.0.1", 12345); for (auto i : {1, 2, 3}) { std::ostringstream msg; msg << "message No." << i << " sent"; auto str = msg.str(); std::vector<char> vec(str.begin(), str.end()); client.send_msg(vec); } std::vector<char> v({'E', 'N', 'D'}); client.send_msg(v); } #endif
问题确认与解决方案
问题原因
你的怀疑完全正确。C++子类对象的构造顺序是先执行基类构造函数,再执行子类构造函数。当你在基类daemon(uint16_t port)构造函数中调用fork()时,子类的构造逻辑还未开始执行,此时对象的虚函数表仍是基类版本。子进程会复制父进程的内存空间,因此子进程中的对象始终处于基类未完全构造的状态,后续调用init_server()、handle_msg()等虚函数时,只会执行基类的默认实现。
解决方案
方案1:将fork()逻辑移至子类构造完成后
把启动守护进程的代码从基类构造函数中移出,改为在子类构造完成后调用:
- 给基类添加启动方法:
class daemon { // ... 原有成员 ... public: // ... 原有构造函数 ... void start_daemon() { auto pid = fork(); if (pid < 0) { std::cout << "[CRITICAL] fork failed: " << strerror(errno) << std::endl; } else if (pid == 0) { init_logger(); server(); exit(EXIT_SUCCESS); } sleep(1); } };
- 修改子类构造函数,在末尾调用启动方法:
class server : public daemon { public: explicit server(uint16_t port) : daemon(port) { std::cout << "[INFO] server constructor" << std::endl; start_daemon(); // 子类完全构造后启动守护进程 } // ... 原有成员 ... };
注意:需确保父进程不会关闭socket描述符,子进程才能正常继承使用。
方案2:使用静态工厂方法创建子类
通过静态方法完成子类对象的创建和守护进程启动,确保子类完全构造后再执行fork():
class server : public daemon { public: static void create_and_start(uint16_t port) { server srv(port); // 子类完全构造 srv.start_daemon(); } // ... 原有成员 ... }; // main函数中调用 server::create_and_start(12345);
子进程输出修复
你的init_logger()中,std::ofstream out("out.txt")是局部变量,函数结束后会被销毁,导致std::cout指向无效缓冲区。需将输出流改为静态成员:
class daemon { private: static std::ofstream log_out; // 静态成员 // ... void init_logger(const char *fn="log.txt") { log_out.open(fn); std::cout.rdbuf(log_out.rdbuf()); } }; // 在实现文件中初始化静态成员 std::ofstream daemon::log_out;
另外,::daemon(1,1)会将标准输入输出重定向到/dev/null,需在调用该函数前完成输出重定向,或者自行实现守护进程化逻辑(如创建会话、切换工作目录等)。
内容的提问来源于stack exchange,提问作者ZioByte

