阅读redis持久化RDB源码的时候一些c知识

Posted 5ycode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阅读redis持久化RDB源码的时候一些c知识相关的知识,希望对你有一定的参考价值。

在rdb的时候使用了fork()函数来创建子进程,详细了解下相关的知识点。

smaps

凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中,为了让程序在物理机器上运行,必须有得让虚拟内存空间映射到物理内存空间。操作系统中页映射表(page table)就是干的这事。内核为每个一个进程维护一份相互独立的页映射表。

通过top命令我们已经能看出进程的虚拟空间大小(VIRT)、占用的物理内存(RES)以及和其他进程共享的内存(SHR)。

Linux 通过proc文件系统为每个进程都提供一个smaps文件。

smapsb包含以下字段:

  • Size: 表示该映射区域在虚拟内存空间中的大小。

  • Rss: 表示该映射区域当前在物理内存中占用了多少空间

  • Shared_Clean: 和其他进程共享的未被改写的page的大小

  • Shared_Dirty: 和其他进程共享的被改写的page的大小

  • Private_Clean: 未被改写的私有页面的大小。

  • Private_Dirty: 已被改写的私有页面的大小。

  • Swap: 表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小。

  • Pss: 该虚拟内存区域平摊计算后使用的物理内存大小(有些内存会和其他进程共享,例如mmap进来的)。比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB,那么该进程分摊其中一半的内存,即Pss=500KB。

了解了smaps后,咱们接着了解下fork()

fork()

作用:fork 为子进程创建了虚拟地址空间,仍与父进程共享同样的物理空间,当父子进程某一方发生写操作时,系统才会为其分配物理空间,并复制一份副本以供其修改。

我们先了解下fork()函数。

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值;

所以fork()成功,以后会执行两次

  • == 0 的时候,是子进程执行
  • == 1 的时候,是父进程执行

在rdb执行的时候,有一段代码。

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi)     pid_t childpid;    if ((childpid = fork()) == 0)         //关闭自己不使用的父进程的资源(验证了父子进程共用一套物理存储)        closeClildUnusedResourceAfterFork();        //执行备份        retval = rdbSave(filename,rsi);        //获取子进程修改的部分大小,相当于rdb耗费的内存        size_t private_dirty = zmalloc_get_private_dirty(-1);    else         //主进程执行自己的逻辑    size_t zmalloc_get_private_dirty(long pid)     //获取smap中的Private_Dirty,这里就是在smaps中讲解的概念    return zmalloc_get_smap_bytes_by_field("Private_Dirty:",pid);

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。fork 把当前进程的页映射表(page table)拷贝了一份,两个进程使用的变量虽然名字一样,但是内存地址指向的不同(最终物理地址一样)

Fork()成功后,可以通过下面的函数进行操作

  • getpid() -获取当前进程的pid

  • getppid() -获取当前进程的父进程的pid

  • 进程终止后会向父进程发送SIGCHLD信号,可以通过wait系列接收

  • wait() waitpid() waittid() 如果子进程都在在运行,则阻塞,有一个终止就返回一个,没有子进程报错返回

  • wait3 wait4 额外能获取子进程使用的资源汇总

  • WIFEXITED 子进程正常结束,则为非0值

  • WEXITSTATUS 获取子进程返回的结束代码,一般先用WIFEXITED判断是否正常结束才用这个

  • WIFSIGNALED 如果子进程是否因为信号而结束,是返回true

  • WTERMSIG 取得子进程是否因信号而中止的信号代码

  • WIFSTOPPED 获取子进程是否处于暂停执行状态

  • WSTOPSIG 获取引发子进程暂停的信号代码

pid_t wait (int * status);
/**
 * wait3 等待所有子进程,wait4指定等待子进程
 * statloc: 保存着子进程退出时的一些状态,它是一个指向int类型的指针,设置为null,直接kill掉子进程
 * options:选项
 *    WNOHANG  如果没有结束的子进程,马上返回,不等待
 *    WUNTRACED 如果子进程进入暂停执行状态,则马上返回,不理会结束状态
 *    也可以WNOHANG | WUNTRACED 没有任何已结束了的子进程或子进程进入暂停执行的状态,则马上返回不等待
 */
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

在serverCron中

   if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||ldbPendingChildren())
        /**
         * 获取终止的进程id
         * statloc: 保存着子进程退出时的一些状态,它是一个指向int类型的指针,设置为null,直接kill掉子进程
         * options:选项
         *    WNOHANG  如果没有结束的子进程,马上返回,不等待
         *    WUNTRACED 如果子进程进入暂停执行状态,则马上返回,不理会结束状态
         *    也可以WNOHANG | WUNTRACED 没有任何已结束了的子进程或子进程进入暂停执行的状态,则马上返回不等待
         */

        if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) 
            //获取子进程的结束代码
            int exitcode = WEXITSTATUS(statloc);
            int bysignal = 0;
            //如果子进程因为信号而结束,获取信号代码
            if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

         
    

snprintf()

作用:将一个格式化的字符串输出到一个目的字符串中

语法:

int sprintf(char *str, char * format [, argument, ...]);
  • str 要写入的字符串
  • format为格式化字符串,与printf()函数相同
  • argument为变量。

在redis持久化的时候使用案例

snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());

pipe()

作用:创建一个管道,必须在fork中调用pipe,否则子进程不会继承文件描述符,两个进程不共享祖先进程,就不能使用pipe。

int  pipe(int fd[2]);

返回值:成功,返回0,否则返回-1,参数是pipe使用的两个文件描述符。

fd[0] 对应的参数是读管道,fd[1]对应的是写管道

什么是管道? 管道是一种把两个进程之间的标准输入和标准输出连接起来的机制,从而提供一种让多个进程间通信的方法。

在rdb中

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) 
   //创建一个pip管道,用于父子进程进行通信
    openChildInfoPipe();    


//在server.c中
    
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) 
    //获取子进程的结束代码
    int exitcode = WEXITSTATUS(statloc);
    int bysignal = 0;
    //如果子进程因为信号而结束,获取信号代码
    if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);

    if (pid == -1) 
     else if (pid == server.rdb_child_pid) 
        //是rdb子进程,说明rdb执行完了,执行后续的事件
        backgroundSaveDoneHandler(exitcode,bysignal);
        //不是因为信号中断的,接收子进程信息
        if (!bysignal && exitcode == 0) receiveChildInfo();
     else if (pid == server.aof_child_pid) 
        backgroundRewriteDoneHandler(exitcode,bysignal);
        if (!bysignal && exitcode == 0) receiveChildInfo();
     else 
        //无法识别的子进程,,直接移除
        if (!ldbRemoveChild(pid)) 
            serverLog(LL_WARNING,
                      "Warning, detected child with unmatched pid: %ld",
                      (long)pid);
        
    
    updateDictResizePolicy();
    //关闭子进程管道
    closeChildInfoPipe();
   

参考:

https://www.cnblogs.com/bravery/archive/2012/06/27/2560611.html

https://www.cnblogs.com/jeakon/archive/2012/05/26/2816828.html

https://www.cnblogs.com/tongye/p/9558320.html

以上是关于阅读redis持久化RDB源码的时候一些c知识的主要内容,如果未能解决你的问题,请参考以下文章

《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读

redis源码阅读-持久化之RDB

redis源码阅读-持久化之RDB

redis rdb持久化源码分析

阅读redis源码的时候一些c知识

《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读