Berkeley DB 1.8.6源代码学习(四)

bt_conv.c 文件:用于字节顺序的转换,本机的字节顺序与硬盘上的机器独立格式的字节顺序;

__bt_pgin函数:从硬盘文件的字节顺序转化到本机的字节顺序;

参数列表:

t B 树指针;

pg :待转换的页面编号;

h :待转换的页面;

返回值:无;

伪代码

如果 t 被设置为无须转换标志,则表示本机与硬盘上数据库文件使用的字节顺序相同,返回;

如果 pg 是元数据编号,则使用 mswap 函数为 pp 做转换,然后返回;

对于 pp 是一般树结点页面的情况,首先转化页面头部的页面基本信息,使用 M_32_SWAP宏和 M_16_SWAP宏完成对 PAGE结构中对应成员变量的转换;

接着转换页面的数据区域;

如果页面是 B树内节点页面:

遍历页面内记录,对于每条记录,首先使用 M_16_SWAP宏转换其索引,然后根据索引获取数据,根据 BINTERNAL结构,使用 P_32_SWAP宏转换前两项成员变量;如果数据存储的是大数据,则由于数据值实际存储的是页面编号和编号占用的内存大小,所以需要使用 P_32_SWAP宏转换;

如果页面时 B 树的叶子节点页面:

遍历所有记录,对于每条记录,首先使用 使用 M_16_SWAP宏转换其索引,然后根据索引获取数据,根据 BLEAF结构,使用 P_32_SWAP宏转换前两项成员变量;如果数据存储的是大数据,则需要使用 P_32_SWAP宏转换数据区中的信息;

 

__bt_pgout函数:从本机转换到磁盘格式;

参数列表:

t B 树指针;

pg :待转换的页面编号;

h :待转换的页面;

返回值:无;

伪代码

如果 t 被设置为无须转换标志,则表示本机与硬盘上数据库文件使用的字节顺序相同,返回;

如果 pg 是元数据编号,则使用 mswap 函数为 pp 做转换,然后返回;

对于 pp 是一般树结点页面的情况:

如果是 B 树内部节点页面,则首先将页面中每条记录的数据部分转换,然后在转换索引,这点与 __bt_pgin函数相反,从而保证索引在定位数据时的有效性;

如果是 B树叶子节点页面,同样是首先将页面中每条记录的数据部分转换,然后转换其索引;

页面数据区转换完成后,转换页面头部的页面基本信息;

 

mswap函数:转换 B树元数据专用函数;

参数列表:

pg:页面指针;

返回值:无;

伪代码

根据 BTMETA的结构,依次对 magic version psize free nrecs flags成员做转换;

 

 

bt_get.c 文件:从 B 树中获取一条记录

__bt_get函数:从 B树中获取一条记录;

参数列表:

dbp:数据库指针;

key:待查找的键值;

data:返回查找结果的数据值;

flags:备用;

返回值:成功、失败、不确定(键值未找到);

伪代码

dbp中获取 B树指针 t

去除 t PIN页面标记;

如果使用了备用的 flags标志,则返回错误;

使用 __bt_search函数查找键值为 key的记录 e,如果没有找到,则返回不确定;否则使用 __bt_ret函数获取 e的数据值存放到 data t bt_rdata中;

如果 t正被并行访问,则去除 e所在页面的 PIN标记,否则将 e所在页面赋给 t bt_pinned成员变量;

 

bt_open.c 文件:打开 B 树的文件;

__bt_open 函数:打开一个 B 树,主要是根据参数信息创建一个 DB 结构,同时调用相关函数打开一个 B 树;

参数列表:

fname :文件名,如果为空则为内存树;

flags :文件打开的标志;

mode :文件的打开模式;

dflags :全局标志,支持并发访问等;

openinfo B 树的基本信息( BTREEINFO );

返回值:数据库指针;

伪代码

调用 byteorder 函数获取机器的字节顺序 machine_lorder

如果 openinfo 不为 NULL ,则将其赋值给 b ,检测其传入参数的合理性如下:

首先检测 BTREEINFO 结构中的 flags 成员是否设置了 R_DUP 之外的位,如果设置了,则返回失败;

检查 psize 的有效性,如果 psize 0 ,则无效,如果 psize 大于 0 ,但是小于 MINPSIZE 或大于 MAX_PAGE_OFFSET + 1 ,或不为最小数据单元的整数倍(即 b.psize & sizeof(indx_t) - 1 ,其中 sizeof(indx_t) 是数据存储的基本单元,当前是 2 个字节,减 1 则将其二进制表示时低位置 1 ,与 b.psize 的&操作,检测其相应的低位是否为 0 ,从而判断是否被 sizeof(indx_t) 整除),则无效;返回失败;

检测 minkeypage 的有效性,如果 minkeypage<2 即每个页面可以存储小于 2 个键,无效,返回失败,如果 minkeypage 调用方没有设置,即传入了 0 值,表示采用缺省值 DEFMINKEYPAGE

检测 compare ,如果调用方没有设置,则使用缺省的比较函数 __bt_defcmp ,同时如果前缀比较函数也没有设置,则使用缺省的前缀比较函数 __bt_defpfx

检测 lorder ,如果 lorder 传入为 0 ,则使用机器的字节顺序 machine_lorder

以上检测了传入 openinfo 的有效性;

如果 openinfo NULL ,则使用缺省值创建一个 BTREEINFO 结构 b

检测 b 的字节顺序是否合法,如果既不是大端有不是小端,则为非法,返回失败;

为树结构 t 和数据库结构 dbp 分配内存并初始化,需要注意的是如果树的字节顺序与机器的不同,则需要为 t 设置 B_NEEDSWAP (需要转换)标志;

如果 fname 不为空,则根据 flags mode 打开树文件,同时将文件描述符赋给树的 bt_fd 成员变量保存,如果 flags 设置了 O_RDONLY ,还需将 t 设置为只读树;

如果 fname 为空,则为内存树,如果 flags 设置了可读写,则为非法标志,返回失败;使用 tmp ()函数获取一个临时文件描述符赋给树的 bt_fd 成员变量,同时设置 t 为内存树;

根据 t->bt_fd 获取文件的状态信息,根据其中文件大小的信息来判断是新建树,还是打开已有的树:

如果文件大小不为 0 ,则读取文件前 sizeof(BTMETA) 个数据,保存到 m 中,由树文件的布局可知,这是读取了树的元数据;需要根据元数据信息重新更正树的一些基本信息,如下:

如果 m magic 变量等于 BTREEMAGIC ,则清除 t B_NEEDSWAP ,这是因为 m 是直接从文件中使用字节的方式读出,如果上述比较相等,则表明文件中存储的字节顺序和机器是相同的,所以即使传入的参数可能与文件的实际情况冲突,但是仍然应该以文件实际的存储情况为准;如果不相等,则需要为 t 设置 B_NEEDSWAP ,同时表明当前 m 的字节顺序不能得到有效的值,需要使用 M_32_SWAP 等宏将其成员变量转换成与机器相同的字节顺序。

经过上述处理后,如果 m magic 仍然与 BTREEMAGIC 不相等,或 m version 不等于当前程序的版本号 BTREEVERSION ,则返回失败;

检测 m psize 的有效性;

检测 m flags 成员变量的有效性;

根据 m psize 调整 b psize

t 设置 m flags 标志;

m 中保存的树信息传递到 t 的相关成员变量,主要是空闲页面链表和记录数量;

以上是在文件不为空文件时检查树的元数据有效性,并据此调整树的有关基本信息;

如果文件大小为 0 ,表示是新建树,此时需要设定一些信息,如下:

如果 b psize 即当前树的页面大小为 0 ,则采用文件的 st_blksize 作为页面大小,这样可以获得较高的 I/O 效率,如果 st_blksize 超出了 MINPSIZE MAX_PAGE_OFFSET + 1 设定的边界值,则 psize 取二者中与 st_blksize 较接近的那一个;

如果 b flags 不支持复键,则设置 t 不支持复键;

设置 t 的空闲页面链表为空,记录数量为 0

设置 t 的元数据需要更新标记;

这样处理完成 t 与文件之间的关系;

由于上述操作中可能会调整 t 的页面大小,将 b psize 内容更新到 t bt_psize 成员变量;

计算缓冲池的大小如下:

缓冲池的尺寸必须为树页面的整数倍(因为是按页面缓冲),对于 b cachesize 成员变量,如果已经设置且不是 cachesize 的整数倍,则补齐到页面的整数倍,即 b.cachesize += (~b.cachesize & b.psize - 1) + 1 表达式;

如果 b cachesize 成员变量小于 b.psize * MINCACHE (缓冲区最小尺寸),则 b.cachesize 赋值为缓冲区的最小值;

计算缓冲区所占的页面数 ncache

计算大数据的标准:

由于每个页面最少必须具备指定数量的记录, berkely DB 将这一限制转换成页面中平均每条记录(键 / 数据对)能够使用的尺寸(字节数),超出这一字数将作为大数据处理,将存放到溢出页面上;在计算时需要考虑到页面头部的页面基本信息尺寸、叶子节点的索引和节点本身的尺寸;即 t->bt_ovflsize = (t->bt_psize - BTDATAOFF) / b.minkeypage - (sizeof(indx_t) + NBLEAFDBT(0, 0)); 其中 (t->bt_psize - BTDATAOFF) 得到数据区的大小,除以 b.minkeypage 得到每条记录最大能够占用的空间,记录存储占用的空间包括索引和记录本身的信息,其中记录使用 BLEAF 结构存储,实际的数据还需要减去 BLEAF 结构中的辅助信息,其大小为 NBLEAFDBT(0, 0) ,索引的大小为 sizeof(indx_t) ,从而得到大数据的标准;

完成上述计算后,需要注意,为了防止由于用户设定的 minkeypage 过大,从而导致即使数据存放到溢出页面上,剩余的空间仍然不够存放索引也出页面的编号的现象出现,需要验证计算的大数据标准是否过小,即大数据的标准不能小于 NBLEAFDBT(NOVFLSIZE, NOVFLSIZE) + sizeof(indx_t) ,这一表达式是对大数据存储时使用的最小记录尺寸,其中 sizeof(indx_t) 是索引的尺寸, NOVFLSIZE 是页面标号及编号所占内存大小的和,由于记录由键值和数据组成,故 NBLEAFDBT(NOVFLSIZE, NOVFLSIZE) 计算出了存储一条大数据记录时占用的最小空间;

根据 bt_fd bt_psize ncache ,使用 mpool_open 函数初始化缓冲池;

如果是一新创建的树,使用 nroot 函数创建一个根结点页面;

根据 dflags 设置 t 的全局标志,如支持锁等;

返回 dbp

 

nroot 函数:为一个新树创建根页面;

参数列表:

t :树指针;

返回值:成功、失败;

伪代码

使用 mpool_get 函数获取 t 的第一页,即根结点页面 root

如果 root 存在,检测其有效性;返回;

如果 root 不存在,表示这是一个新建的树,其元数据页和根结点页都需要创建和初始化:

首先获取一个新的页面 meta ,再获取一个新的页面 root ,编号设定的方式都是递增;

如果获取 root 时得到的页面编号不为 P_ROOT ,则表示出现错误,返回失败;

设置 root 的页面编号,左右兄弟结点为空(此时树只有一个根结点,故为空),初始化空闲数据区指针,设置页面为叶子页面;

meta 页面初始化为 0 ,将 meta 页面和 root 页面置为 MPOOL_DIRTY

 

tmp 函数:创建临时文件夹;

参数列表:无

返回值:文件描述符;

伪代码

获取临时文件夹路径 envtmp

如果 envtmp 存在,则临时文件路径为 /$envtmp/bt.XXXXXX ;否则,为 /tmp/bt.XXXXXX

创建临时文件;

 

byteorder 函数:获取机器的字节顺序;

参数列表:无

返回值: BIG_ENDIAN (大端), LITTLE_ENDIAN (小端);

伪代码

x 初始化为 0x01020304

如果 x 的第一个字节为 1 ,则为 BIG_ENDIAN ;为 4 则为 LITTLE_ENDIAN ;否则操作失败;

 

__bt_fd 函数:获取数据库文件的描述符;

参数列表:

dbp :数据库指针;

返回值:文件描述符;

伪代码

dbp 中获取树指针 t

去除 t PIN 页面标记;

如果是内存树,返回- 1

否则返回 t bt_fd 成员变量;

 

 

bt_split.c 文件:用于树的分裂操作;

__bt_split 函数:完成向树中插入记录,并分裂相关结点的操作;

参数列表:

t :树指针;

sp :待分裂的结点页面;

key :待插入记录的键值;

data :待插入记录的数据值;

flags :记录是否是大数据;

ilen :插入记录的尺寸;

argskip :插入的位置;

返回值:成功、失败;

伪代码

由于在插入之前需要将 sp 分裂成左右两个结点页面,如果插入点位于右子结点,其插入的位置需要调整,将 argskip 赋给局部变量 skip ,便于后续的操作,实际插入的位置将有 skip 传递;

如果 sp 是根结点,使用 bt_root 函数分裂 sp ,否则使用 bt_page 函数分裂 sp ;分裂的结果是得到两个页面结点 l r ,返回实际插入的页面 h l r )及修订实际插入的位置 skip

key data 插入到 h skip 处的操作:

调整 h upper 成员变量为记录分配 ilen 大小的内存,并将首地址相对值传递给记录索引的 skip 处;

根据首地址相对计算出实际地址 dest

使用对应的宏完成将 key data 写入到 dest 的操作( B 树可以为 B 树算法和 RECNO 算法公用,二者实际用于写入的宏有所不同);

完成插入后,需要注意的是:结点的分裂,对于根结点实际上创建两个新的结点页面来存储根结点中的数据,然后再将这两个结点页面的编号存放到根结点中;对于普通的结点,则是创建一个新的页面来存储部分记录,然后将这个结点页面插入到 B 树中;

如果 sp 是根结点,则还需要使用 bt_rroot 函数( RECNO 算法)或 bt_broot 函数来重新调整根结点;

对于 sp 不是根结点的情况,沿着祖先结点栈向上回溯,将新生成的结点插入的父结点中,如果父结点也需要分裂才能接纳新的结点,则递归完成这一插入过程,知道到达根结点或不再分裂为止; 内结点记录中的关键字实际上是对应子结点的第一条记录的键值;

1 、弹出父结点记录 parent EPGNO );

2 、将 l 赋给 lchild r 赋给 rchild ,并使用 mpool_get 获取 parent 的页面 h

3 、将插入的位置 skip 设为 parent->index + 1 ,即原结点 sp 所在位置的下一位,因为分裂后左结点 l 继承了分裂前结点的位置,新插入的结点右结点 r 是其右兄弟;

4 、计算新结点在父结点中需要占用的空间;

如果插入结点是 B 树内结点,则使用 GETBINTERNAL 宏获取新结点的第一条记录的数据结构 bi ,再使用 NBINTERNAL 宏计算存储 bi 的键值需要的内存大小 nbytes

如果插入结点是 B 树叶子结点,则使用 GETBLEAF 宏获取新结点的第一条记录的数据结构 bl ,再使用 NBINTERNAL 宏计算存储 bl 的键值需要的内存大小 nbytes ;另外需要考虑前缀树的情况(不知道是干啥用的?),就是在 t bt_pfx 函数不为空的情况下, bl 存储的不是大数据,且 h 的坐兄弟结点不为空或插入位置大于 1 ,使用 bt_pfx 函数获取 lchild 的最后一条记录的键值和 bl 第一条记录键值的相同前缀的尺寸 nksize ,使用 NBINTERNAL 宏计算当键值长度为 nksize 时,存储需要的内存 n ,如果 n<nbytes, nbytes n ,即将会截断实际存储的键值,否则 nksize 0 ;推测这样可能会加快查询的速度,因为如此操作缩短了键值的长度,从而加快了比较的速度;

如果是 RECNO 算法结点,则使用 NRINTERNAL 宏计算 nbytes

5 、检测是否需要分裂 parent 所在的页面并调整插入点以用于插入新结点;

如果需要分裂父结点,则使用 bt_root bt_page 函数分裂父结点(同本函数开始时的情况),得到待插入结点的新的父结点页面 h ;将 parentsplit 1

如果不需要分裂,则需要将 h 中插入点以后的记录索引向后移动一位,为插入点的索引腾出空间,同时调整 h lower 指针; parensplit 0

6 、将新结点插入到父结点中:

如果 rchild B 树内结点,首先为其分配空间,即调整 h upper 和对应的索引值;

然后将 bi 的键值部分复制的新分配的位置,最后保存 rchild 的页面编号;

如果 rchild B 树叶子结点,同样是分配空间,然后使用 WR_BINTERNAL 宏将记录的结构部分填充,接着将 bl 的键值复制到对应内存,最后判断如果 bl 存储的是大数据,则需要使用 bt_preserve 函数将这一大数据标记为不可删除的,以免后续对记录的删除同时删除了索引用的键值,从而造成脏数据;

如果 rchild RECNO 算法的结点,暂无;

7 、如果 parentsplit 0 ,即无须继续插入操作,则将 h 置为需要写回 MPOOL_DIRTY ,结束分裂操作;

8 、对于 parentsplit 1 的情形,需要将分裂的父结点插入的 B 树中,这样需要判断当前分裂的父结点是否是根结点,若是,则需要使用 bt_rroot 函数或 bt_broot 函数来调整新的根结点;

9 、将 lchild rchild 结点页面置为需要写回 MPOOL_DIRTY ,同时去除 PIN 标记,转 1

结束分裂操作,将 l r 结点页面置为需要写回 MPOOL_DIRTY ,同时去除 PIN 标记,返回成功;

 

bt_page 函数:分裂一个非根结点;

参数列表:

t :树指针;

h :待分裂的页面;

lp :返回分裂产生的左页面;

rp :返回分裂产生的右页面;

skip :传入和返回插入点位置;

ilen :待插入数据的长度;

返回值:分裂后待插入的页面;

伪代码

使用 __bt_new 函数获取一个页面 r ,用于存储分裂的数据;

初始化 r ,其中 t 的右兄弟为 h 的右兄弟,左兄弟为 h ,页面类型与 h 相同;

如果插入的是 B 树当前级的最右边结点,则只用插入一个空的页面即可,这样即使下次需要插入并分裂时,也是可以的在作的;即 h 的右兄弟结点为空,且 *skip == NEXTINDEX(h) 时,将 h 的右兄弟设为 r r lower 指针下移一位,为插入的结点分配空间,同时插入点 skip 调整成 0 lp 指向 h rp 指向 r ,返回 r

为新的左页面分配存(其实是临时使用的内存) l

l 初始化,并将其左兄弟设为 h 的左兄弟,右兄弟设为 r ,类型与 h 相同;

由于 r 将会插入到 B 树中 h 的后面,所以需要调整 h 的右兄弟的左兄弟,使其指向 r

使用 bt_psplit 函数完成实际的分裂工作,即将 h 中的数据复制到 l r 中,同时为新结点分配索引的空间;同时返回实际需要插入的页面 tp 和位置 skip

l 的内容复制到 h 中,如果 tp == l 则, tp 改为 h ;释放 l 的所占的内存空间;

lp 设定为 h rp 设定为 r ,返回 tp

 

bt_root 函数:分裂 B 树的根结点;

参数列表:

t :树指针;

h :根结点页面;

lp :指向分裂的左页面;

rp :指向分裂的右页面;

skip :插入点;

ilen :插入记录的长度;

返回值:返回指向待插入记录的页面;
伪代码

使用 __bt_new 函数分别为左页面和右页面获取新的页面 l r

初始化 l r 页面;其中 l 的右兄弟为 r r 的左兄弟为 l l 的左兄弟和 r 的右兄弟为空, l r 的页面类型与 h 相同;

使用 bt_psplit 函数完成实际的分裂工作,即将 h 中的数据复制到 l r 中,同时为新结点分配索引的空间;同时返回实际需要插入的页面 tp 和位置 skip

lp 设定为 h rp 设定为 r ,返回 tp

 

bt_rroot 函数:

 

bt_broot 函数:在根结点分裂后,调整 B 树的根结点;

t :树指针;

h :根结点页面;

l :左子结点页面;

r :右子结点页面;

返回值:成功、失败;

伪代码

将左结点页面 l h 中注册,由于 l 是根结点最左边的记录,其关键字在树的搜索中永远不会用到,所以不用填写,这样,存放 l 结点相关信息就只剩下存储 l 的页面编号了,即使用 NBINTERNAL(0) 宏获取需要的内存大小 nbytes ,然后在 h 中为其分配内存,最后使用 WR_BINTERNAL 宏完成写入;

如果 h 是叶子结点,则经过分裂后,会变成内部结点;

存储 r 结点的信息:

如果 h 是叶子结点,使用 GETBLEAF 宏获取 r 第一条记录的信息 bl BLEAF );使用宏 NBINTERNAL 计算以 bl 的键值为键值的内结点需要的内存 nbytes ;在 h 中为存储 r 的信息分配内存,使用 WR_BINTERNAL 宏写入指向 r 的记录,并将 bl 的键值复制到对应的位置;同样,如果 bl 存储的是大数据,需要使用 bt_preserve 函数将其标记成永久保存的标志;

如果 h 是内部结点,则同样获取 r 的第一条记录 bi BINTERNAL ),计算以 bi 键值为键值的内结点记录需要的内存大小 nbytes ,分配内存,复制键值信息和填写 r 的页面编号信息;

由于根结点中已经有两条记录,所以调整 h lower 变量;

清除 h 的页面类型;

h 设置为 B 树内结点页面类型;

h 设置为需要写回标记 MPOOL_DIRTY

 

bt_psplit 函数:为页面分裂作实际的数据转移操作;

参数列表:

t :树指针;

h :待分裂的页面;

l :分裂后的左页面;

r :分裂后的右页面;

pskip :传入和返回记录插入的位置;

ilen :插入记录的尺寸;

返回值:返回待插入的页面;

伪代码

1 、首先完成对 l 页面的数据转储,转储的规则是尽量不要上大数据成为页面的键值,因为这会导致内结点处理效率的低下(需要先寻址,再比较),而且当对应的叶子结点删除时,大数据不能删除,从而降低了空间利用率;具体措施如下:

遍历 h 页面,直到符合适当的条件就停止遍历,在这之前的内结点记录全部转储到 l 页面;对于不是插入点位置的记录,根据记录的类型使用对应的函数获取记录的首地址 src 和尺寸 nbytes ,并使用 isbigkey 来标记是否是大数据;

如果遍历的插入点位置,则将 nbytes 赋值为 ilen

使用 used 保存已经转储的字节数;

如果如果已经到达或超过插入点,且计入当前记录后已经转储的字节数( used + nbytes )将超出页面存储数据的最大能力,或者已经遍历到 h 最后一条记录,这时,停止遍历,不再想 l 转储数据,保存已经转储的最后一条记录的编号 off

如果可以上述条件不满足,则表示当前记录可以转储到 l 结点中,如果当前位置不是插入点,则将当前记录转储到 l 中;并将 used 增加 nbytes

此时,如果已经转储的字节数 used 超过了页面存储能力的一半,则需要作一些判度,以决定是否继续转储:如果当前记录存储的不是大数据,或已经转储了 3 条存储大数据的记录,则停止转储,否则将已经转储的大数据记录个数加 1 ,继续转储;

完成对左结点页面的转储后,调整左结点的 lower 指针(在转储是为了效率,只是调整了 upper 的指针, lower 指针可以通过计算一次性调整);

由于插入点可能在 h 中,插入数据和分裂结点后需要调整游标的位置;如果游标已经初始化并且指向 h 中的记录,则调整如下:

如果游标指向的位置在 skip 之后,即大于 skip ,则游标的位置需要后移动一位,以便继续指向以前的记录;

如果经过上述调整后游标的位置小于 l 结点内记录的数量,表示游标指向的记录分配在 l 结点中,将游标的页面指向调整到 l 页面;否则,则需要将游标的页面指向 r ,并且游标指向的位置需要减去 l 结点存储结点的数量;

如果插入点在 l 中,则插入点的位置不需调整,返回的页面为 l ;否则,返回的页面为 r ,插入点的位置需要调整,即减去已经转储到 l 的记录数;

2 、转储记录到右结点 r

同转储到 l 一样;如果是插入点,则为其预留位置;如果不是插入点,根据 h 的类型,获取相关键值和存储需要的内存大小 nbytes ;由于剩下的记录将全部转储到 r 中,所以无须作过多的判断,直接将数据转储到 r 中的对应位置即可;

最后调整 r 结点的 lower 指针;

需要注意的是,如果插入点其实是在 h 的最后一条记录之后,则上述的遍历将不能找到插入点,这时需要将插入点追加到 r 的后面,即将 r lower 指针加 1 ,为插入点预留索引的存储空间;

返回待插入的页面;

 

bt_preserve 函数:对于作为内结点记录键值的大数据,需要将其设置成永久性保存;

参数列表:

t :树指针;

pg :待设置的页面编号;

返回值:成功、失败;

伪代码

获取页面编号为 pg 的页面 h

h 设置成 P_PRESERVE

h 标记成 MPOOL_DIRTY

 

rec_total 函数:

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值