日志灵异事件:全局类变量需谨慎使用描述符

最近的程序测试时发现一个堪称灵异事件的bug,守护进程A往进程B通过一个本地udp socket发送数据,但B居然收到了A里面的一句本应只是打印到日志进程C的log。
先检查代码,日志输出方式相当简单,构造好数据结构后,对一个对已打开的udp socket里面执行sendto。这个日志输出类的实现大概如下:

<!-- lang: cpp -->
class logger
{
    logger();
    ~logger();
    void print(const char* fmt, ...);
private:
    SOCKET m_socket;
}
logger::logger()
{
   // ...
   m_socket = socket(AF_INET, SOCK_DGRAM, 0);
   // ...
}
void logger::print(const char* fmt, ...)
{
    // ... // 构造数据结构
    sendto(m_socket, ...); // 发送出去
}
// 全局打印实例
static logger s_logger;
logger& get_logger()
{
    return s_logger;
}
// 打印日志
get_logger().print(“hello world!”);

问题来了,本来应该是发给C的日志为什么会跑到B上面去了?最初我们用strace跟踪系统调用,发现logger::print里面的sendto所使用的描述符是3;但从/proc接口看到进程A的文件描述符3居然是指向进程B的,而不是指向日志进程C。很明显问题出在logger::m_socket,不知道什么时候这个socket变成了指向B的;但审查了N遍代码,logger::m_socket打开的时候就已经指定了打开到日志进程C的端口,而且从来不会修改。同时,我们注意到另外一个异象,当用前台模式运行A时,这个问题就消失了。也就是说,这个问题只在A用守护模式运行时存在。

再来看看A的守护模式是怎么实现的,按开发者说,A进入守护模式函数是教科书式的,基本照抄APUE 13.3节的实例代码。仔细阅读了APUE的说明,进入守护进程的教科书步骤是:
1、umask(0);
2、fork;父进程立即exit,子进程继续;
3、setsid();成为新会话首进程;成为组长进程;没有控制终端
4、chdir(“/”);更改工作目录到根目录
5、关闭不需要的文件描述,不再持有父进程的文件描述符

 for(int i = 0; i < 1024; i++)
     close(i);

6、打开/dev/null,使得只进程具有0、1、2描述符

问题就出在第5点,原因就是fork后,子进程是继承了父进程的所有文件描述的,如果在守护进程化的过程中,子进程把继承的文件描述符关闭了,那么A进程的那个logger::m_socket就是已经被close掉了;而且没有置为无效值,当这个描述符数字再次被使用时,就出现了把日志写到新打开的描述符里面去了。

综述一下这个问题发生的过程:
1、前台以守护进程方式执行进程A
2、由于s_logger是全局变量,而且在其构造函数产生了一个udp socket,因此当进入A的main函数时s_logger.m_socket就已经存在了。
3、进入main函数后,执行APUE的标准守护化函数,父进程退出,子进程在上面第5步把s_logger.m_socket关闭了
4、A进程(这时是子进程)继续执行,打开一个新的描述符,由于已释放的描述符是可以重新使用的,这个新的描述符数值和原来的s_logger.m_socket是一样
5、logger::print和新的逻辑同时向同一个描述符写数据,问题出现。

问题在全局变量里面就把socket建立出来,这个初始化太早了,如此看来,进程的所有初始化逻辑还是统一在一处比较好,减少使用全局变量。(不要埋怨APUE,子进程本来就不该持有父进程的描述符)

你可能感兴趣的:(全局变量,守护进程,文件描述符)