Ceph源码解析:读写流程
Posted 陈小跑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Ceph源码解析:读写流程相关的知识,希望对你有一定的参考价值。
转载注明出处,整理也是需要功夫的,http://www.cnblogs.com/chenxianpao/p/5572859.html
一、OSD模块简介
1.1 消息封装:在OSD上发送和接收信息。
cluster_messenger -与其它OSDs和monitors沟通
client_messenger -与客户端沟通
1.2 消息调度:
Dispatcher类,主要负责消息分类
1.3 工作队列:
1.3.1 OpWQ: 处理ops(从客户端)和sub ops(从其他的OSD)。运行在op_tp线程池。
1.3.2 PeeringWQ: 处理peering任务,运行在op_tp线程池。
1.3.3 CommandWQ:处理cmd命令,运行在command_tp。
1.3.4 RecoveryWQ: 数据修复,运行在recovery_tp。
1.3.5 SnapTrimWQ: 快照相关,运行在disk_tp。
1.3.6 ScrubWQ: scrub,运行在disk_tp。
1.3.7 ScrubFinalizeWQ: scrub,运行在disk_tp。
1.3.8 RepScrubWQ: scrub,运行在disk_tp。
1.3.9 RemoveWQ: 删除旧的pg目录。运行在disk_tp。
1.4 线程池:
有4种OSD线程池:
1.4.1 op_tp: 处理ops和sub ops
1.4.2 recovery_tp:处理修复任务
1.4.3 disk_tp: 处理磁盘密集型任务
1.4.4 command_tp: 处理命令
1.5 主要对象:
ObjectStore *store;
OSDSuperblock superblock; 主要是版本号等信息
OSDMapRef osdmap;
1.6 主要操作流程: 参考文章
1.6.1 客户端发起请求过程
1.6.2 op_tp线程处理数据读取
1.6.3 对象操作的处理过程
1.6.4 修改操作的处理
1.6.5 日志的写入
1.6.6 写操作处理
1.6.7 事务的sync过程
1.6.8 日志恢复
1.7 整体处理过程图
二、客户端写入数据大致流程及保存形式
2.1 读写框架
2.2 客户端写入流程
在客户端使用 rbd 时一般有两种方法:
- 第一种 是 Kernel rbd。就是创建了rbd设备后,把rbd设备map到内核中,形成一个虚拟的块设备,这时这个块设备同其他通用块设备一样,一般的设备文件为/dev/rbd0,后续直接使用这个块设备文件就可以了,可以把 /dev/rbd0 格式化后 mount 到某个目录,也可以直接作为裸设备使用。这时对rbd设备的操作都通过kernel rbd操作方法进行的。
- 第二种是 librbd 方式。就是创建了rbd设备后,这时可以使用librbd、librados库进行访问管理块设备。这种方式不会map到内核,直接调用librbd提供的接口,可以实现对rbd设备的访问和管理,但是不会在客户端产生块设备文件。
应用写入rbd块设备的过程:
- 应用调用 librbd 接口或者对linux 内核虚拟块设备写入二进制块。下面以 librbd 为例。
- librbd 对二进制块进行分块,默认块大小为 4M,每一块都有名字,成为一个对象
- librbd 调用 librados 将对象写入 Ceph 集群
- librados 向主 OSD 写入分好块的二进制数据块 (先建立TCP/IP连接,然后发送消息给 OSD,OSD 接收后写入其磁盘)
- 主 OSD 负责同时向一个或者多个次 OSD 写入副本。注意这里是写到日志(Journal)就返回,因此,使用SSD作为Journal的话,可以提高响应速度,做到服务器端对客户端的快速同步返回写结果(ack)。
- 当主次OSD都写入完成后,主 OSD 向客户端返回写入成功。
- 当一段时间(也许得几秒钟)后Journal 中的数据向磁盘写入成功后,Ceph通过事件通知客户端数据写入磁盘成功(commit),此时,客户端可以将写缓存中的数据彻底清除掉了。
- 默认地,Ceph 客户端会缓存写入的数据直到收到集群的commit通知。如果此阶段内(在写方法返回到收到commit通知之间)OSD 出故障导致数据写入文件系统失败,Ceph 将会允许客户端重做尚未提交的操作(replay)。因此,PG 有个状态叫 replay:“The placement group is waiting for clients to replay operations after an OSD crashed.”。
也就是,文件系统负责文件处理,librbd 负责块处理,librados 负责对象处理,OSD 负责将数据写入在Journal和磁盘中。
2.3 RBD保存形式
如下图所示,Ceph 系统中不同层次的组件/用户所看到的数据的形式是不一样的:
- Ceph 客户端所见的是一个完整的连续的二进制数据块,其大小为创建 RBD image 是设置的大小或者 resize 的大小,客户端可以从头或者从某个位置开始写入二进制数据。
- librados 负责在 RADOS 中创建对象(object),其大小为 pool 的 order 决定,默认情况下 order = 22 此时 object 大小为 4MB;以及负责将客户端传入的二进制块条带化为若干个条带(stripe)。
- librados 控制哪个条带由哪个 OSD 写入(条带 ---写入哪个----> object ----位于哪个 ----> OSD)
- OSD 负责创建在文件系统中创建文件,并将 librados 传入的数据写入数据。
Ceph client 向一个 RBD image 写入二进制数据(假设 pool 的拷贝份数为 3):
(1)Ceph client 调用 librados 创建一个 RBD image,这时候不会做存储空间分配,而是创建若干元数据对象来保存元数据信息。
(2)Ceph client 调用 librados 开始写数据。librados 计算条带、object 等,然后开始写第一个 stripe 到特定的目标 object。
(3)librados 根据 CRUSH 算法,计算出 object 所对应的主 OSD ID,并将二进制数据发给它。
(4)主 OSD 负责调用文件系统接口将二进制数据写入磁盘上的文件(每个 object 对应一个 file,file 的内容是一个或者多个 stripe)。
(5)主 ODS 完成数据写入后,它使用 CRUSH 算啊计算出第二个OSD(secondary OSD)和第三个OSD(tertiary OSD)的位置,然后向这两个 OSD 拷贝对象。都完成后,它向 ceph client 反馈该 object 保存完毕。
(6)然后写第二个条带,直到全部写入完成。全部完成后,librados 还应该会做元数据更新,比如写入新的 size 等。
完整的过程(来源):
该过程具有强一致性的特点:
- Ceph 的读写操作采用 Primary-Replica 模型,Client 只向 Object 所对应 OSD set 的 Primary 发起读写请求,这保证了数据的强一致性。
- 由于每个 Object 都只有一个 Primary OSD,因此对 Object 的更新都是顺序的,不存在同步问题。
- 当 Primary 收到 Object 的写请求时,它负责把数据发送给其他 Replicas,只要这个数据被保存在所有的OSD上时,Primary 才应答Object的写请求,这保证了副本的一致性。这也带来一些副作用。相比那些只实现了最终一致性的存储系统比如 Swift,Ceph 只有三份拷贝都写入完成后才算写入完成,这在出现磁盘损坏时会出现写延迟增加。
- 在 OSD 上,在收到数据存放指令后,它会产生2~3个磁盘seek操作:
- 把写操作记录到 OSD 的 Journal 文件上(Journal是为了保证写操作的原子性)。
- 把写操作更新到 Object 对应的文件上。
- 把写操作记录到 PG Log 文件上。
三、客户端请求流程(转的一只小江的博文,写的挺好的)
RADOS读对象流程
RADOS写对象操作流程
例子:
#!/usr/bin/env python
import sys,rados,rbd
def connectceph():
cluster = rados.Rados(conffile = \'/root/xuyanjiangtest/ceph-0.94.3/src/ceph.conf\')
cluster.connect()
ioctx = cluster.open_ioctx(\'mypool\')
rbd_inst = rbd.RBD()
size = 4*1024**3 #4 GiB
rbd_inst.create(ioctx,\'myimage\',size)
image = rbd.Image(ioctx,\'myimage\')
data = \'foo\'* 200
image.write(data,0)
image.close()
ioctx.close()
cluster.shutdown()
if __name__ == "__main__":
connectceph()
1. 首先cluster = rados.Rados(conffile = \'ceph.conf\'),用当前的这个ceph的配置文件去创建一个rados,这里主要是解析ceph.conf中中的集群配置参数。然后将这些参数的值保存在rados中。
2. cluster.connect() ,这里将会创建一个radosclient的结构,这里会把这个结构主要包含了几个功能模块:消息管理模块Messager,数据处理模块Objector,finisher线程模块。
3. ioctx = cluster.open_ioctx(\'mypool\'),为一个名字叫做mypool的存储池创建一个ioctx ,ioctx中会指明radosclient与Objector模块,同时也会记录mypool的信息,包括pool的参数等。
4. rbd_inst.create(ioctx,\'myimage\',size) ,创建一个名字为myimage的rbd设备,之后就是将数据写入这个设备。
5. image = rbd.Image(ioctx,\'myimage\'),创建image结构,这里该结构将myimage与ioctx 联系起来,后面可以通过image结构直接找到ioctx。这里会将ioctx复制两份,分为为data_ioctx和md_ctx。见明知意,一个用来处理rbd的存储数据,一个用来处理rbd的管理数据。
流程图:
1. image.write(data,0),通过image开始了一个写请求的生命的开始。这里指明了request的两个基本要素 buffer=data 和 offset=0。由这里开始进入了ceph的世界,也是c++的世界。
由image.write(data,0) 转化为librbd.cc 文件中的Image::write() 函数,来看看这个函数的主要实现
ssize_t Image::write(uint64_t ofs, size_t len, bufferlist& bl)
{ ImageCtx *ictx = (ImageCtx *)ctx; int r = librbd::write(ictx, ofs, len, bl.c_str(), 0); return r; }
2. 该函数中直接进行分发给了librbd::wrte的函数了。跟随下来看看librbd::write中的实现。该函数的具体实现在internal.cc文件中。
ssize_t write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf, int op_flags)
{ Context *ctx = new C_SafeCond(&mylock, &cond, &done, &ret); //---a AioCompletion *c = aio_create_completion_internal(ctx, rbd_ctx_cb);//---b r = aio_write(ictx, off, mylen, buf, c, op_flags); //---c while (!done) cond.Wait(mylock); // ---d
}
---a.这句要为这个操作申请一个回调操作,所谓的回调就是一些收尾的工作,信号唤醒处理。
---b。这句是要申请一个io完成时 要进行的操作,当io完成时,会调用rbd_ctx_cb函数,该函数会继续调用ctx->complete()。
---c.该函数aio_write会继续处理这个请求。
---d.当c句将这个io下发到osd的时候,osd还没请求处理完成,则等待在d上,直到底层处理完请求,回调b申请的 AioCompletion, 继续调用a中的ctx->complete(),唤醒这里的等待信号,然后程序继续向下执行。
3.再来看看aio_write 拿到了 请求的offset和buffer会做点什么呢?
int aio_write(ImageCtx *ictx, uint64_t off, size_t len, const char *buf, AioCompletion *c, int op_flags)
{ //将请求按着object进行拆分 vector<ObjectExtent> extents; if (len > 0) { Striper::file_to_extents(ictx->cct, ictx->format_string, &ictx->layout, off, clip_len, 0, extents); //---a } //处理每一个object上的请求数据 for (vector<ObjectExtent>::iterator p = extents.begin(); p != extents.end(); ++p) { C_AioWrite *req_comp = new C_AioWrite(cct, c); //---b AioWrite *req = new AioWrite(ictx, p->oid.name, p->objectno, p- >offset,bl,….., req_comp); //---c r = req->send(); //---d }
}
根据请求的大小需要将这个请求按着object进行划分,由函数file_to_extents进行处理,处理完成后按着object进行保存在extents中。file_to_extents()存在很多同名函数注意区分。这些函数的主要内容做了一件事儿,那就对原始请求的拆分。
一个rbd设备是有很多的object组成,也就是将rbd设备进行切块,每一个块叫做object,每个object的大小默认为4M,也可以自己指定。file_to_extents函数将这个大的请求分别映射到object上去,拆成了很多小的请求如下图。最后映射的结果保存在ObjectExtent中。
原本的offset是指在rbd内的偏移量(写入rbd的位置),经过file_to_extents后,转化成了一个或者多个object的内部的偏移量offset0。这样转化后处理一批这个object内的请求。
4. 再回到 aio_write函数中,需要将拆分后的每一个object请求进行处理。
---b.为写请求申请一个回调处理函数。
---c.根据object内部的请求,创建一个叫做AioWrite的结构。
---d.将这个AioWrite的req进行下发send().
5. 这里AioWrite 是继承自 AbstractWrite ,AbstractWrite 继承自AioRequest类,在AbstractWrite 类中定义了send的方法,看下send的具体内容.
int AbstractWrite::send() { if (send_pre()) //---a
}
#进入send_pre()函数中
bool AbstractWrite::send_pre()
{
m_state = LIBRBD_AIO_WRITE_PRE; // ----a FunctionContext *ctx = //----b new FunctionContext( boost::bind(&AioRequest::complete, this, _1)); m_ictx->object_map.aio_update(ctx); //-----c
}
---a.修改m_state 状态为LIBRBD_AIO_WRITE_PRE。
---b.申请一个回调函数,实际调用AioRequest::complete()
---c.开始下发object_map.aio_update的请求,这是一个状态更新的函数,不是很重要的环节,这里不再多说,当更新的请求完成时会自动回调到b申请的回调函数。
6. 进入到AioRequest::complete() 函数中。
void AioRequest::complete(int r)
{ if (should_complete(r)) //---a
}
---a.should_complete函数是一个纯虚函数,需要在继承类AbstractWrite中实现,来7. 看看AbstractWrite:: should_complete()
bool AbstractWrite::should_complete(int r)
{ switch (m_state) { case LIBRBD_AIO_WRITE_PRE: //----a { send_write(); //----b
----a.在send_pre中已经设置m_state的状态为LIBRBD_AIO_WRITE_PRE,所以会走这个分支。
----b. send_write()函数中,会继续进行处理,
7.1.下面来看这个send_write函数
void AbstractWrite::send_write()
{ m_state = LIBRBD_AIO_WRITE_FLAT; //----a add_write_ops(&m_write); // ----b int r = m_ictx->data_ctx.aio_operate(m_oid, rados_completion, &m_write);
}
---a.重新设置m_state的状态为 LIBRBD_AIO_WRITE_FLAT。
---b.填充m_write,将请求转化为m_write。
---c.下发m_write ,使用data_ctx.aio_operate 函数处理。继续调用io_ctx_impl->aio_operate()函数,继续调用objecter->mutate().
8. objecter->mutate()
ceph_tid_t mutate(……..) { Op *o = prepare_mutate_op(oid, oloc, op, snapc, mtime, flags, onack, oncommit, objver); //----d return op_submit(o);
}
---d.将请求转化为Op请求,继续使用op_submit下发这个请求。在op_submit中继续调用_op_submit_with_budget处理请求。继续调用_op_submit处理。
8.1 _op_submit 的处理过程。这里值得细看
ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a int r = _get_session(op->target.osd, &s, lc); //---b _session_op_assign(s, op); //----c _send_op(op, m); //----d
}
----a. _calc_target,通过计算当前object的保存的osd,然后将主osd保存在target中,rbd写数据都是先发送到主osd,主osd再将数据发送到其他的副本osd上。这里对于怎么来选取osd集合与主osd的关系就不再多说,在《ceph的数据存储之路(3)》中已经讲述这个过程的原理了,代码部分不难理解。
----b. _get_session,该函数是用来与主osd建立通信的,建立通信后,可以通过该通道发送给主osd。再来看看这个函数是怎么处理的
9. _get_session
int Objecter::_get_session(int osd, OSDSession **session, RWLock::Context& lc)
{ map<int,OSDSession*>::iterator p = osd_sessions.find(osd); //----a OSDSession *s = new OSDSession(cct, osd); //----b osd_sessions[osd] = s;//--c s->con = messenger->get_connection(osdmap->get_inst(osd));//-d
}
----a.首先在osd_sessions中查找是否已经存在一个连接可以直接使用,第一次通信是没有的。
----b.重新申请一个OSDSession,并且使用osd等信息进行初始化。
---c. 将新申请的OSDSession添加到osd_sessions中保存,以备下次使用。
----d.调用messager的get_connection方法。在该方法中继续想办法与目标osd建立连接。
10. messager 是由子类simpleMessager实现的,下面来看下SimpleMessager中get_connection的实现方法
ConnectionRef SimpleMessenger::get_connection(const entity_inst_t& dest)
{ Pipe *pipe = _lookup_pipe(dest.addr); //-----a if (pipe) { } else { pipe = connect_rank(dest.addr, dest.name.type(), NULL, NULL); //----b }
}
----a.首先要查找这个pipe,第一次通信,自然这个pipe是不存在的。
----b. connect_rank 会根据这个目标osd的addr进行创建。看下connect_rank做了什么。
11. SimpleMessenger::connect_rank
Pipe *SimpleMessenger::connect_rank(const entity_addr_t& addr, int type, PipeConnection *con, Message *first)
{
Pipe *pipe = new Pipe(this, Pipe::STATE_CONNECTING, static_cast<PipeConnection*>(con)); //----a pipe->set_peer_type(type); //----b pipe->set_peer_addr(addr); //----c pipe->policy = get_policy(type); //----d pipe->start_writer(); //----e return pipe; //----f
}
----a.首先需要创建这个pipe,并且pipe同pipecon进行关联。
----b,----c,-----d。都是进行一些参数的设置。
----e.开始启动pipe的写线程,这里pipe的写线程的处理函数pipe->writer(),该函数中会尝试连接osd。并且建立socket连接通道。
目前的资源统计一下,写请求可以根据目标主osd,去查找或者建立一个OSDSession,这个OSDSession中会有一个管理数据通道的Pipe结构,然后这个结构中存在一个发送消息的处理线程writer,这个线程会保持与目标osd的socket通信。
12. 建立并且获取到了这些资源,这时再回到_op_submit 函数中
ceph_tid_t Objecter::_op_submit(Op *op, RWLock::Context& lc)
{
check_for_latest_map = _calc_target(&op->target, &op->last_force_resend); //---a int r = _get_session(op->target.osd, &s, lc); //---b _session_op_assign(s, op); //----c MOSDOp *m = _prepare_osd_op(op); //-----d _send_op(op, m); //----e
}
---c,将当前的op请求与这个session进行绑定,在后面发送请求的时候能知道使用哪一个session进行发送。
--d,将op转化为MOSDop,后面会以MOSDOp为对象进行处理的。
---e,_send_op 会根据之前建立的通信通道,将这个MOSDOp发送出去。_send_op 中调用op->session->con->send_message(m),这个方法会调用SimpleMessager-> send_message(m), 再调用_send_message(),再调用submit_message().在submit_message会找到之前的pipe,然后调用pipe->send方法,最后通过pipe->writer的线程发送到目标osd。
自此,客户就等待osd处理完成返回结果了。
1.看左上角的rados结构,首先创建io环境,创建rados信息,将配置文件中的数据结构化到rados中。
2.根据rados创建一个radosclient的客户端结构,该结构包括了三个重要的模块,finiser 回调处理线程、Messager消息处理结构、Objector数据处理结构。最后的数据都是要封装成消息 通过Messager发送给目标的osd。
3.根据pool的信息与radosclient进行创建一个ioctx,这里面包好了pool相关的信息,然后获得这些信息后在数据处理时会用到。
4.紧接着会复制这个ioctx到imagectx中,变成data_ioctx与md_ioctx数据处理通道,最后将imagectx封装到image结构当中。之后所有的写操作都会通过这个image进行。顺着image的结构可以找到前面创建并且可以使用的数据结构。
5.通过最右上角的image进行读写操作,当读写操作的对象为image时,这个image会开始处理请求,然后这个请求经过处理拆分成object对象的请求。拆分后会交给objector进行处理查找目标osd,当然这里使用的就是crush算法,找到目标osd的集合与主osd。
6.将请求op封装成MOSDOp消息,然后交给SimpleMessager处理,SimpleMessager会尝试在已有的osd_session中查找,如果没有找到对应的session,则会重新创建一个OSDSession,并且为这个OSDSession创建一个数据通道pipe,把数据通道保存在SimpleMessager中,可以下次使用。
7.pipe 会与目标osd建立Socket通信通道,pipe会有专门的写线程writer来负责socket通信。在线程writer中会先连接目标ip,建立通信。消息从SimpleMessager收到后会保存到pipe的outq队列中,writer线程另外的一个用途就是监视这个outq队列,当队列中存在消息等待发送时,会就将消息写入socket,发送给目标OSD。
8. 等待OSD将数据消息处理完成之后,就是进行回调,反馈执行结果,然后一步步的将结果告知调用者。
四、Ceph读流程
OSD端读消息分发流程
OSD端读操作处理流程
总体流程图:
int read(inodeno_t ino,
file_layout_t *layout,
snapid_t snap,
uint64_t offset,
uint64_t len,
bufferlist *bl, // ptr to data
int flags,
Context *onfinish,
int op_flags = 0) --------------------------------Filer.h
Striper::file_to_extents(cct, ino, layout, offset, len, truncate_size, extents);//将要读取数据的长度和偏移转化为要访问的对象,extents沿用了brtfs文件系统的概念
objecter->sg_read_trunc(extents, snap, bl, flags, truncate_size, truncate_seq, onfinish, op_flags);//向osd发起请求
对于读操作而言:
1.客户端直接计算出存储数据所属于的主osd,直接给主osd上发送消息。
2.主osd收到消息后,可以调用Filestore直接读取处在底层文件系统中的主pg里面的内容然后返回给客户端。具体调用函数在ReplicatedPG::do_osd_ops中实现。
CEPH_OSD_OP_MAPEXT||CEPH_OSD_OP_SPARSE_READ
r = osd->store->fiemap(coll, soid, op.extent.offset, op.extent.length, bl);
CEPH_OSD_OP_READ
r = pgbackend->objects_read_sync(soid, miter->first, miter->second, &tmpbl);
五、Ceph写流程
OSD端写操作处理流程
而对于写操作而言,由于要保证数据写入的同步性就会复杂很多:
1.首先客户端会将数据发送给主osd,
2.主osd同样要先进行写操作预处理,完成后它要发送写消息给其他的从osd,让他们对副本pg进行更改,
3.从osd通过FileJournal完成写操作到Journal中后发送消息告诉主osd说完成,进入5
4.当主osd收到所有的从osd完成写操作的消息后,会通过FileJournal完成自身的写操作到Journal中。完成后会通知客户端,已经完成了写操作。
5.主osd,从osd的线程开始工作调用Filestore将Journal中的数据写入到底层文件系统中。
写的逻辑流程图如图:
从图中我们可以看到写操作分为以下几步:
1.OSD::op_tp线程从OSD::op_wq中拿出来操作如本文开始的图上描述,具体代码流是
ReplicatePG::apply_repop中创建回调类C_OSD_OpCommit和C_OSD_OpApplied
FileStore::queue_transactions中创建了回调类C_JournaledAhead
2.FileJournal::write_thread线程从FileJournal::writeq中拿出来操作,主要就是写数据到具体的journal中,具体代码流:
3.Journal::Finisher.finisher_thread线程从Journal::Finisher.finish_queue中拿出来操作,通过调用C_JournalAhead留下的回调函数FileStore:_journaled_ahead,该线程开始工作两件事:首先入底层FileStore::op_wq通知开始写,再入FileStore::ondisk_finisher.finisher_queue通知可以返回。具体代码流:
4.FileStore::ondisk_finisher.finisher_thread线程从FileStore::ondisk_finisher.finisher_queue中拿出来操作,通过调用C_OSD_OpCommit留下来的回调函数ReplicatePG::op_commit,通知客户端写操作成功
5.FileStore::op_tp线程池从FileStore::op_wq中拿出操作(此处的OP_WQ继承了父类ThreadPool::WorkQueue重写了_process和_process_finish等函数,所以不同于OSD::op_wq,它有自己的工作流程),首先调用FileStore::_do_op,完成后调用FileStore::_finish_op。
6. FileStore::op_finisher.finisher_thread线程从FileStore::op_finisher.finisher_queue中拿出来操作,通过调用C_OSD_OpApplied留下来的回调函数ReplicatePG::op_applied,通知数据可读。
具体OSD方面的源码逐句解析可以参考一只小江的博文
此文主要整理了参考资料里的ceph客户端读写流程,OSD端读写流程等,使用了参考资料的内容,如果侵犯到参照资料作者的权益,请联系我,我会及时删除相关内容。
参考资料:
http://blog.sina.com.cn/s/blog_c2e1a9c7010151xb.html
作者:ywy463726588 http://blog.csdn.net/ywy463726588/article/details/42676493
http://blog.csdn.net/ywy463726588/article/details/42679869
作者:刘世民(Sammy Liu)http://www.cnblogs.com/sammyliu/p/4836014.html
作者:一只小江 http://my.oschina.net/u/2460844/blog/532755
http://my.oschina.net/u/2460844/blog/534390?fromerr=PnkKCbYU
感谢以上作者无私的分享!
以上是关于Ceph源码解析:读写流程的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段