Redis源码学习——BIO

 Redis源码学习之BIO

BIO顾名思义,background IO,是redis中运行的后台IO。 网上千篇一律的说法是redis是单线程单进程。 实际上redis运行过程中并不是严格单进程单线程应用。
Redis中的多进程:
在写入备份(RDB,AOF)的时候,会fork出子进程进行备份文件的写入。
Redis中的多线程:

  1. AOF的备份模式中,如果我们设置的是AOF_FSYNC_EVERYSEC(每秒备份一次,这个设置可理解为弱同步备份),redis会create一个backgroud线程,在这个线程中执行aof备份文件的写入。
  2. 新生成的AOF文件,在覆盖旧AOF文件时。 如果在此之前AOF备份已经开启,在执行该fd的close前,我们的Redis进程与旧的AOF文件存在引用, 旧的AOF文件不会真正被删除。 所以当我们执行close(oldfd)时,旧AOF文件的被打开该文件的进程数为0,即没有进程打开过这个文件,这时这个文件在执行close时会被真正删除。 而删除旧AOF文件可能会阻塞服务,所以我们将它放到另一个线程调用。
  3. 执行DEL操作,假如碰巧这个key对应有非常多对象,那么这个删除操作会阻塞服务器几秒钟时间, 所以将删除操作放到另一个线程执行。 具体可看这篇文章: Lazy Redis is better Redis

BIO

Redis将所有多线程操作封装到BIO中,在bio.c,bio.h中可以看到。 本文我们关注的不是具体的操作,而是Redis封装的BIO行为, 这个代码简洁,维护性好。 值得学习一下。
BIO提供以下几个api:

void bioInit(void); //初始化BIO
void bioCreateBackgroundJob(int type, void arg1, void arg2, void *arg3);   //新建一个BIO任务
unsigned long long bioPendingJobsOfType(int type);  //获取当前BIO任务类型,队列中待执行的任务个数
unsigned long long bioWaitStepOfType(int type); //阻塞等待某个类型的BIO任务的执行,返回等待任务个数
void bioKillThreads(void); //中断所有BIO进程

BIO操作的类型:

/ Background job opcodes /
#define BIO_CLOSE_FILE    0 / 关闭文件/
#define BIO_AOF_FSYNC     1 / AOF写入  /
#define BIO_LAZY_FREE     2 / 释放对象 /
#define BIO_NUM_OPS       3 /BIO数/

BIO对象:

static pthread_t bio_threads[BIO_NUM_OPS];  //BIO线程
static pthread_mutex_t bio_mutex[BIO_NUM_OPS]; //BIO每个线程的mutex锁变量
static pthread_cond_t bio_newjob_cond[BIO_NUM_OPS]; //BIO线程锁的条件变量, 监听这个条件变量唤起当前线程
static pthread_cond_t bio_step_cond[BIO_NUM_OPS]; //BIO线程阻塞锁,bioWaitStepOfType监听这个条件变量被通知该操作的执行。
static list *bio_jobs[BIO_NUM_OPS];
static unsigned long long bio_pending[BIO_NUM_OPS]; // BIO未执行的

我们先看初始化的时候执行的部分:

bioInit() {
    for (j = 0; j < BIO_NUM_OPS; j++) {
        void arg = (void)(unsigned long) j;
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) { // 初始化线程
            serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}

主要功能分为两个部分:

  1. bioCreateBackgroundJob: 创建BIO任务,插入bio_jobs,并调用pthread_cond_signal,通知进程解锁。
  2. bioProcessBackgroundJobs: 执行BIO任务线程。 线程中通过pthread管理进程锁,当bioCreateBackgroundJob执行pthread_cond_signal通知到该任务对应的线程时,从bio_jobs读出上一个任务,并执行。

bioCreateBackgroundJob

void bioCreateBackgroundJob(int type, void arg1, void arg2, void *arg3) {
    struct bio_job job = zmalloc(sizeof(job));
    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;
    pthread_mutex_lock(&bio_mutex[type]); // 加锁 保护bio_jobs和bio_pending的一致性
    listAddNodeTail(bio_jobs[type],job);  //插入到任务队列中
    bio_pending[type]++;
    pthread_cond_signal(&bio_newjob_cond[type]);  //通知preocess线程,执行任务
    pthread_mutex_unlock(&bio_mutex[type]);  //解锁
}

bioProcessBackgroundJobs

void bioProcessBackgroundJobs(void arg) {
    // 使进程可以被手动kill
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    // 加锁 确保不会有两个进程使用pthread_cond_wait监听同一个锁
    pthread_mutex_lock(&bio_mutex[type]);
    while(1) {
        listNode *ln;
        / The loop always starts with the lock hold. /
        if (listLength(bio_jobs[type]) == 0) {
            pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]); // 等待bioCreateBackgroundJob通知解锁
            continue;
        }
        //取队列中第一个任务
        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        /* It is now possible to unlock the background system as we know have
          a stand alone job structure to process./
        pthread_mutex_unlock(&bio_mutex[type]); //解锁
        // 根据type执行任务
        // do somethings...
        pthread_cond_broadcast(&bio_step_cond[type]); // 广播解锁,用于解bioWaitStepOfType中的锁, 接触阻塞。
        pthread_mutex_lock(&bio_mutex[type]); // 为下面的操作加锁,且用于下一个循环的pthread_cond_wait阻塞。
        listDelNode(bio_jobs[type],ln);  // 操作bio_jobs 和 bio_pending  标志这个任务已完成。
        bio_pending[type]--;
    }
}

pthread

整个BIO就是通过锁进行的阻塞后台IO。 如果我们梳理一下这个锁过程:

  1. bioInit,新建线程,执行bioProcessBackgroundJobs。
  2. bioProcessBackgroundJobs 中,pthread_mutex_lock(&bio_mutex[type]),给该任务的锁变量加锁。
  3. 进入while循环, 调用pthread_cond_wait, 等待解锁。 由于mutex锁是“sleep-lock”,线程会sleep,等待唤醒。
  4. 主线程调用创建BIO任务, 调用bioCreateBackgroundJob。
  5. bioCreateBackgroundJob中 pthread_mutex_lock(&bio_mutex[type]); 又对bio_mutex[type]加锁
  6. bioCreateBackgroundJob中pthread_cond_signal(&bio_newjob_cond[type]) //发送信号,通知BIO线程继续执行。
  7. bioCreateBackgroundJob中pthread_mutex_unlock(&bio_mutex[type]); //解锁
  8. bioProcessBackgroundJobs 中被唤醒继续进行。
  9. 执行任务完毕后,pthread_mutex_unlock解锁, pthread_cond_broadcast广播解锁。
  10. 再pthread_mutex_lock加锁 。 用于下一次while循环。

在梳理的时候,我发现一个奇怪的地方,我们第2步在BIO线程中加锁,第5步调用bioCreateBackgroundJob在主线程中又对mutex进行了一次加锁。 而在他们之间并没有pthread_mutex_unlock执行。 为什么bioCreateBackgroundJob没有被mutex的锁阻塞?
一切的关键都在pthread_cond_wait这个函数中。 按照我原来的理解,pthread_cond_wait应该只是进行了一次信号等待, 等到某个信号后,将mutex[type]解锁。 为什么在信号发送前,pthread_mutex_lock没有将主线程的bioCreateBackgroundJob阻塞住。 所以我猜测, pthread_cond_wait不不仅仅是一次wait signal,而是unlock+wait。
为了验证这个猜想,我们进去看pthread_cond_wait的实现:
glibc中的pthread_cond_wait

// line 93
int __pthread_cond_wait (cond, mutex)
// line 110
  err = __pthread_mutex_unlock_usercnt (mutex, 0);  //解锁mutex
do {
// line 155
lll_futex_wait (&cond->__data.__futex, futex_val, pshared);  // wait signal
} while (val == seq || cond->__data.__woken_seq == val);
// line 193
return __pthread_mutex_cond_lock (mutex);

可以看到, pthread_cond_wait 实际上就是一次 Unlock -> Wait -> Lock。

  1. Redis源码
  2. Redis源码注释
时间: 2024-10-30 11:10:49

Redis源码学习——BIO的相关文章

Redis源码学习——基础数据结构之SDS

Redis数据结构-SDS Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理. 首先介绍下Redis的基础数据结构 -- SDSRedis没有使用传统C语言的字符串(字符数组)表示.而是自己构建了一种名为sds(Simple Dymamic String)的抽象类型,作为redis的默认字符类型. SDS用于保存数据库中的字符串值,用户客户端的输入的缓冲区,AOF模块中的缓冲区都是由SDS实现的. SDS相比于C字符串的优点: 常数复杂度获取字符

STL源码学习——Lists(链表)

STL源码学习--Lists(链表) 今天突然想起来看看开源项目,找了找最后决定好好看看经典的STL喵~ 和STL里的代码比起来我突然觉得以前写的代码也太不规范了喵,估计很多ACMer都一样吧喵. 先从简单的看.先挑了一发list的源码来看.总结如下: 欢迎大家一起讨论喵~ 1 :list是用双向循环链表实现的,就是说 list.end()+1 == list.begin() 2 :list中有一个关键结点,这个结点是 list.end() 3 :在看了list中的erase函数后,发现这个函数

DotText源码学习——ASP.NET的工作机制

--本文是<项目驱动学习--DotText源码学习>系列的第一篇文章,在这之后会持续发表相关的文章. 概论 在阅读DotText源码之前,让我们首先了解一下ASP.NET的工作机制,可以使我们更好的理解.ASP.NET是Web服务器(IIS)的 ISAPI(Internet Server API)扩展.当IIS接收到客户端浏览器发来的请求后,它根据请求的文件类型确定由哪个ISAPI扩展来处理该请求,并将请求转发给ASP.NET(如 果是ASP.NET处理的相应文件类型的话,如*.aspx.*.

Hadoop源码学习:RPC

Hadoop源码学习:RPC Hadoop RPC使用java NIO编写,达到高性能,轻量级,可控性. 主要分为四层:序列化层,函数调用层,网络传输层,服务器端处理框架 序列化层:实现Writable接口 函数调用层:java反射机制和动态代理实现函数调用 网络传输层:使用Socket机制 服务器端处理框架:基于Reactor设计模式的事件驱动I/O模型 如何使用Hadoop RPC: RPC Server: 1.定义一个协议,实现VersionedProtocol接口, public int

Java集合源码学习(二)ArrayList分析

Java集合源码学习笔记(二)ArrayList分析 1.关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口, 为什么叫"ArrayList",因为ArrayList内部是用一个数组存储元素值,相当于一个可变大小的数组,也就是动态数组. (1)继承和实现 继承了AbstractList,实现了List:ArrayList是一个数组队列,提供了相关的添加.删除.修改.遍历

有谁写过asp.netsql2000进销存的,我购源码学习?

问题描述 有谁写过asp.netsql2000进销存的,我购源码学习?我最近想学ASP.NET看了很多书,都不行.都差不多.所以想购源码来学习,价格在3000左右,要求商业,但不要求很完善的QQ:421520476 解决方案 解决方案二:但不要求很完善的????商业项目哪个不是比较完善后才出"厂"的,你去51aspx找一下吧,应该有供学习的--

Android源码学习之组合模式定义及应用_Android

组合模式定义: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. 将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性. 如上图所示(截取自<Head First De

Android源码学习之组合模式定义及应用

组合模式定义: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly. 将对象组合成树形结构以表示"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性. 如上图所示(截取自<Head First De

日志分析系统——Hangout源码学习

这两天看了下hangout的代码,虽然没有运行体验过,但是也算是学习了一点皮毛. 架构浅谈 Hangout可以说是java版的Logstash,我是没有测试过性能,不过据说是kafka这边性能要高出Logstash5倍.不知道真的假的,不过看代码,确实要比Logstash高效一点. 关于input,filter,output的关系 在Logstash里面,Input,filter,output是三个独立的部分,每个部分通过Buffer存储数据. 但是Hangout没有采用这种思想,每个Input