zoukankan      html  css  js  c++  java
  • fork()和fopen()

    最近在看IO相关内容,忽然想起几年前遇到的一个查了很久的bug。

    当时上线了一个地图路况瓦片服务器,根据请求中的瓦片ID号,从一个静态数据库文件读取对应记录并下发。

    服务器基于C++写的,在Linux系统上开启了4进程运行。

    上线一段时间后,有人报告系统日志中发现了崩溃调用栈。崩溃的复现概率极低,也没有找到稳定的复现方法。

    因为服务器框架实现了自动重启机制,客户端也有重试机制,这个bug影响倒是很轻微。

    直到几个月后经同事提醒,查阅服务器初始化代码时,才发现了一些端倪:

    各个子进程共享了同一个只读文件,而文件的fopen()是在用fork()创建子进程之前执行的。

    如果将fork()和fopen()互换,问题就解决了。

    两者的顺序为什么会有影响?

    做具体分析前,我们可以将问题抽象为以下代码片段:(为了简化,错误处理和reap暂不考虑)

     1 #include <assert.h>
     2 #include <stdio.h>
     3 #include <string.h>
     4 #include <unistd.h>
     5 
     6 static void _init() {
     7     FILE *f = fopen("a.txt", "w");
     8     fprintf(f, "helloworld
    ");
     9     fclose(f);
    10 }
    11 
    12 int main() {
    13     _init();
    14 
    15     FILE *f = fopen("a.txt", "r");
    16     int pid = fork();
    17     for (;;) {
    18         char s[6] = {};
    19         if (pid) {
    20             fseek(f, 0, SEEK_SET);
    21             fread(s, 1, 5, f);
    22             assert(strcmp(s, "hello") == 0);
    23         } else {
    24             fseek(f, 5, SEEK_SET);
    25             fread(s, 1, 5, f);
    26             assert(strcmp(s, "world") == 0);
    27         }
    28     }
    29     fclose(f);
    30     return 0;
    31 }

    程序很简单,磁盘文件a.txt包含"helloworld"10个字符,两个进程分别读取这个文件的[0..5)和[5..10)片段,对应"hello"和"world"两个字符串,验证读到的结果是否正确。

    运行后不出所料地触发了assert断言失败。

    • 文件在进程中的表示

    当我们用fopen()打开一个文件时,就新建了一个文件描述符(file descriptor),一个整数,对应descriptor table中的一个条目。

    一般情况下,每个descriptor指向系统open file table中的一个条目,里面记录着文件当前的偏移、读写模式、引用计数等状态。

    值得注意的是,同一个磁盘文件可以被多次fopen()打开,这样会创建多个descriptor,指向互相独立的状态。

    上述代码执行完15行fopen()之后是这样的:

    • fork()对文件的处理

    fork()是Unix系统创建进程的函数,执行之后父进程和子进程各返回一次,通过返回值pid进行区分。fork会对当前进程的堆、栈地址空间都原样复制一遍,但对于打开的文件怎么处理呢?其实看man fork文档里面写得相当清楚:

    o   The child process has its own copy of the parent's descriptors.  These descriptors reference
        the same underlying objects, so that, for instance, file pointers in file objects are shared
        between the child and the parent, so that an lseek(2) on a descriptor in the child process
        can affect a subsequent read or write by the parent.  This descriptor copying is also used by
        the shell to establish standard input and output for newly created processes as well as to
        set up pipes.

    所以当我们执行完fork()之后是这样的:

    两个进程指向同一个条目,共享同样的文件状态包括偏移等等,这就产生了竞态条件,引发bug了。

    要解决这个bug,只需要调换15、16行,先fork()再fopen()即可:

    因为两个进程的文件状态互相独立了,各读各的,就没有问题了。

    • 引用计数

    有的时候我们反而是希望两个进程共享一个文件状态轮流处理文件的,只要做好同步即可。

    那么fork()之后两个文件都fclose()是否会有问题?

    答案是不会。注意到open file table中有一项refcnt引用计数,表示该项被几个进程同时引用。刚开始引用计数为1,fork()之后就变成2了。最后某进程fclose()之后降为1,另一进程fclose()之后降为0,此时操作系统才真正关闭此文件。

  • 相关阅读:
    JavaScript的运动框架学习总结
    Canvas设置width与height 的问题!
    JavaScript 学习—— js获取行间样式和非行间样式
    Css 学习——left与offsetLeft的区别
    BaseServlet
    Java集合 Json集合之间的转换
    Java对象 json之间的转换(json-lib)
    mybatis hellworld
    XStream的例子
    c语言之“/”和“%”运算符
  • 原文地址:https://www.cnblogs.com/xrst/p/14675276.html
Copyright © 2011-2022 走看看