MooseFS源码分析----master服务器初始化过程

Moose File System 是一个具备容错功能的网络分布式文件系统,它将数据分布在网络中的不同服务器上,MooseFS 通过FUSE 使之看起来就是一个 Unix 的文件系统。MooseFS主要由主服务器、备份服务器、存储服务器和客户端组成。这篇文章主要介绍主服务器,也就是master服务器的初始化过程,不会涉及具体的与其他服务器的通信,这些留在后面介绍。
  MooseFS是群里一个哥们给推荐的,网上google了一下感觉挺好的,所以就以MooseFS为切入点来学习分布式系统。相对来说,MooseFS的安装配置都非常方便,如果想要尝试自己安装的话,可以参考官网上的文档(有中文的)。
  下面就从源码来分析master服务器的启动过程。

  在修改完配置后,通过命令mfsmaster start来启动服务器。master服务器的main函数入口在mfscommon/main.c中定义,它首先初始化系统的错误表及CRC表,初始化局部变量,这些局部变量会影响后面的初始化过程,有些在整个系统中都会存在,如下所示:

int main(int argc,char **argv) {
    ......
    strerr_init();
    mycrc32_init();

    cfgfile=strdup(ETC_PATH "/" STR(APPNAME) ".cfg");
    passert(cfgfile);
    locktimeout = 1800;
    rundaemon = 1;
    runmode = RM_RESTART;
    logundefined = 0;
    lockmemory = 0;
    appname = argv[0];
    ......
}
错误表由strerr_init()函数来初始化。这个错误表个人感觉实现的不好。首先它计算静态定义的错误表errtab(静态定义)的元素个数时是通过一个循环,通过sizeof就可以做到;其次,它会将errtab中的每个错误(根据错误码)进行哈希处理,存储在哈希表中errhash中。第二个操作完全没有必要,错误不会经常发生,所以不会频繁地访问错误表,而且错误表的个数很少,弄的这么复杂完全看不出来有性能的提升。个人认为完全可以通过宏的形式生成switch-case这样的方式来实现即可。(如果理解的有误,看到的帮忙指正)
  cfgfile存储的是配置文件的名称,是一个绝对路径,对应的文件默认是mfsmaster.cfg。ETC_PATH是在对源码执行configure操作后生成的config.h文件中定义的。如果你在configure的时候指定的prefix为/usr/local/mfs-1.6.26,则ETC_PATH的值为/usr/local/mfs-1.6.26/etc。APPNAME宏是在编译的时候通过-DAPPNAME定义的,mfsmaster可执行程序对应的值就是mfsmaster,而STR(APPNAME)相当于#APPNAME,所以STR(APPNAME)生成了字符串"mfsmaster"。
  接下来是获取启动时指定的选项,如-d、-u等,具体可以有的选项可以通过mfsmaster -h查看。在获取选项后,还要得到服务器的启动方式。master服务器的启动方式有:RM_RESTART、RM_START、RM_STOP、RM_RELOAD、RM_TEST、RM_KILL。我们这里只关心RM_START模式。
  在RM_START模式下,是否在后台运行服务器有局部变量rundaemon决定,这个变量在初始化时设置为1。假设不想在后台运行,在启动master服务器时加上-d选项即可。通常情况下,都会将master服务器运行在后台,所以这里不考虑rundaemon为0的情况。将服务器运行在后台的操作由makedaemon()函数完成。makedaemon()函数中我们只关心下面的部分:

void makedaemon() {
    ......

    int piped[2];

    ......
    if (pipe(piped)<0) {
        fprintf(stderr,"pipe error\n");
        exit(1);
    }
    f = fork();

    .......

    if (f>0) {
        wait(&f);
        ......        
        close(piped[1]);
//        printf("Starting daemon ...\n");
        while ((r=read(piped[0],pipebuff,1000))) {
            if (r>0) {
                ......
                happy = fwrite(pipebuff,1,r,stderr);
            } else {
                fprintf(stderr,"Error reading pipe: %s\n",strerr(errno));
                exit(1);
            }
        }
        exit(0);
    }
    ......
    f = fork();
    ......
    if (f>0) {
        exit(0);
    }
    ......
    close(STDERR_FILENO);
    sassert(dup(piped[1])==STDERR_FILENO);
    close(piped[1]);
    ......
}
我们将第一次fork()前的进程称为祖父进程,创建的进程称为父进程,第二次fork()创建的新进程称为子进程。我们知道通常在做daemon操作的时候,会将子进程的标准输入、标准输出和标准错误关闭,但是如果子进程在初始化过程中发生了错误,则不能将错误输出到终端。所以mfs在这里使用管道通过祖父进程来输出错误信息或调试信息,参见makedaemon()结尾处对STDERR_FILENO的处理和第一个if(f>0)代码块中的while循环。这里还有一个细节值得注意,就是第一个if(f>0)的代码块中调用了wait(),这个系统调用会一直阻塞直到它的任意一个子进程退出。在初始化过程中可以输出错误信息,也就是说wait()系统调用返回了,那祖父进程的子进程在什么时候退出呢?在第二个fork()之后,即第二个if(f>0)代码块中。之所以要在这里调用wait(),就是防止父进程成为僵尸进程。
  在执行daemon操作后,调用cfg_load()加载配置信息。配置信息存储在mfsmaster.cfg中,每个配置变量以“variable=value”形式配置,以#开头的为注释。如果要查看每个配置变量对应的含义,可以通过man mfsmaster.cfg命令来查看。但是前提是将mfs的man手册添加到系统的man路径中。mfs的man手册在安装目录的share/man下,假设你的安装目录为/usr/local/mfs-1.6.26,只需在/etc/man.config中添加下面一行:
MANPATH /usr/local/mfs-1.6.26/share/man
  接下来,mfs会调用openlog()来为当前程序与系统日志记录器的连接,设置在系统日志中的输出格式。在以后,mfs会将系统运行的一些日志信息记录到系统日志中。我是不赞成这种做法的。作为应用程序还是要建立自己的日志文件,写到系统日志中,不仅会影响查看系统运行时的日志信息,而且系统日志的容量是有限的,会回滚,不可控,某些关键的日志信息可能会丢失。系统日志和程序运行的日志混杂在一起,看起来非常不方便。
  mfs会调用changeugid()来更改运行时的用户和组,通常都会创建mfs组和mfs用户来作为master服务器所属的用户组和用户。如果要修改服务器运行时的用户组和用户,可以修改mfsmaster.cfg中的WORKING_USER和WORKING_GROUP配置变量。
  接着mfs将程序的执行路径切换到存储元数据的路径,这个路径由DATA_PATH配置变量指定。
  为了防止运行多个master服务器进程,mfs会调用wdlock()在元数据所在的路径下创建了一个隐藏文件.mfsmaster.lock文件,并且调用fcntl()对这个文件上锁。如果是RM_START模式,发现已存在.mfsmaster.lock文件并且被其他进程锁定,则会输出错误信息,然后返回。
  在上述准备工作完成后,调用initialize()函数初始化各个子模块。每个子模块的初始化函数都在RunTab数组中注册,initialize()只是调用每个数组中每个项的函数指针。本来想把这些函数也一一分析,但是这些初始化操作都是为后面的业务服务的,只有了解了具体的业务才能理解这些初始化操作的含义。所以在后面的分析中,用到时再回过头来看这些初始化操作。
  在子模块初始化后,可以调用close_msg_channel()关闭前面makedaemon()中遗留的标准错误描述符,如下所示:
void close_msg_channel() {
    fflush(stderr);
    close(STDERR_FILENO);
    sassert(open("/dev/null", O_RDWR, 0)==STDERR_FILENO);
}
  在关闭后,祖父进程也可以退出,最终只留下一个后台子进程,即mfsmaster进程。
  至此,初始化流程完成了,后面开始处理mfs的业务,这些操作在mainloop()中处理。下篇文章通过这个函数来了解mfs如何处理读写文件等操作。

你可能感兴趣的:(MooseFS源码分析----master服务器初始化过程)