assignment 0 C++ primter
目的是熟悉一下C++,这里的提交需要注意一下格式,比如statement必须在{}之间的要求等等,否则gradescope通过不了。我的第一次提交就有这样的问题,这个评测系统对于格式的要求很高。
GDB:
- Debugging Under Unix: gdb Tutorial
- GDB Tutorial: Advanced Debugging Tips For C/C++ Programmers
- Give me 15 minutes & I’ll change your view of GDB [VIDEO]
顺便复习了一下智能指针:
- 某一时刻只能有一个
unique_ptr<T>
指向对象,不支持拷贝赋值 shared_ptr<T>
可以拷贝赋值,内有自己的计数器
本地测试结果:
1 | [ OK ] StarterTest.ElementAccessTest (0 ms) |
gradescope提交结果
Assignment 1 Buffer Pool
ass1主要需要实现数据库的存储管理部分,分为三部分:
- LRU
- Buffer Pool
- Parallel Buffer Pool
首先第一个是实现一个LRU,这里主要使用hash list(一个unordered_map 和一个list)就可以:
1 | size_t cap_; // the capacity |
实现过程不复杂但是我这里看了好久,主要是任务说明刚开始没太看懂。它的意思是LRU模块是给上层缓存池用的,缓存池对于一个page进行pin,pin之后相当于DBMS要拿来用这个Page,所以这时候是不能淘汰的(即需要从LRU中删除)。当且仅当buffer unpin到pin_count=0时才丢给LRU进行管理,这时候可以淘汰。
注意写的时候要完全按照clang-format-tidy进行,否则语法检查不通过。
单元测试结果:
1 | ➜ build git:(ghess/p2-refinement) ✗ ./test/lru_replacer_test |
之后是缓冲池管理器的实现,BufferPoolManger从DiskManager中获取数据库页,存进内存,同样负责将脏页写回磁盘。
页表管理
在数据库原理的博客中我已经介绍了页表的管理,DBMS由于其特殊性(可知道缓存数据是否需要使用、查询临近)等原因可以自己设计缓冲池。
加锁
在数据库的背景下,locks和latches概念不同:
Locks:
- 保护索引的逻辑内容不被别的txn改
- 在整个txn过程中保持
- 需要回滚
Latches:
- 保护索引的内部关键数据结构不被别的线程改
- 在整个操作的过程中保持
- 不需要回滚(本身就没有事务的概念)
Page:
1 | char data_[PAGE_SIZE]{}; |
PAGE_SIZE 定为4096 Bytes,即4KB,page_id_
唯一标注了Page,当一个进程pin了页的时候pin_count_
就++,然后is_dirty_标注脏页,rwlathch_是封装的读写锁。
Buffer Pool Manager:
1 | Page *pages_; |
- pages_是缓存池的page数组,由page id进行索引,是缓存池的核心数据
- disk_manager 和log_manger 是指向磁盘管理器和日志管理器的指针
- page_table_是page id和真实frame_id的映射,两者都是int32_t
- replacer_ 是LRUReplacer对象,主要负责unpin之后的frame的管理
- free_list_是一个空闲页的list,里面是frame的id
- latch_是防止资源同时被多个进程读写
这里要搞明白几个概念,frame是缓存池中的缓存frame page,page_id是请求的页表编号,DBMS向缓存池请求的是page_id,磁盘中存储的也是。所以我们需要一个page_id到frame_id的映射来快速进行缓存选择,所以page_table_一定映射的是有效的页,故当淘汰页出缓存时需要将映射删除,这个我debug了好久。
下面是实现细节:
FetchPgImp:
本函数实现从缓存中拿page的操作,可以通过page_table_查询page_id对应的缓存frame,如果没查到就在free_list_里面找个空闲的frame_id,如果free_list_为空说明已经用完了缓存(buffer pool满了),这时候需要进行LRU,里面的replacer调用victim方法找到一个“牺牲”的frame_id,如果脏就读回disk,然后在page_table中删除(因为我们已经淘汰了这个页),并将新的page_id => frame_id映射加入page_table,最后更新frame就可以了(更新page_id,pin_count, is_dirty_)。
UnpinPageImpl
本函数主要处理unpin一个页的操作,如果pin_count > 0那么减一即可,之后如果==0
就加入lru_replacer中。如果进程拿到的pin_count就是<= 0,那么直接return false。这里注意要在pin_count = 0时从page_table_中erase掉:
1 | if (pages_[frame_id].GetPinCount() == 0) { |
FlushPageImpl
将page刷新到磁盘中,找到这个page然后WritePage就可以了,记得更新page信息。
FlushAllPgsImp
将所有有效地页面全部写回到disk中去,这里注意要在page_table_中找,因为其保存的都是有效的页。
NewPgImp
在buffer pool中创建一个新page,返回指向page的指针。这里麻烦的地方在于缓存池可能满,此时可能需要将页面写回磁盘,并删除映射和重置数据。
如果不能新建page(free_list_和LRU都满了),那么return nullptr。找到一个空闲的frame_id并更新data即可。新建页完成之后别忘了让replacer_ Pin一下frame_id(frame对应的页是新建的,不应该在LRU淘汰队列中)。
DeletePgImp
在buffer pool中删除某一个page。注意只能删pin_count>0的,之后更新frame即可。
这里主要的思路就是通过buffer pool 和lru_replacer进行frame的管理。buffer pool的page_table_是page_id到frame_id的映射,注意这里主要管理pinned page,而unpinned page则会交给lru_replacer进行管理。
1 | ➜ build git:(ghess/p2-refinement) ✗ ./test/buffer_pool_manager_instance_test |
Parallel Buffer Pool Manager
这里的意思是如果只用一个buffer pool的话,虽然我们有互斥锁来保证线程安全,但是当交互的时候可能会产生大量争用,故我们可以设置多个buffer pool来缓解。具体思想就是根据页表编号进行分流,具体是编号page_id % buffer_pool_instance 的缓冲池负责page_id的缓存。
具体实现在ParallelBufferPoolManager,这里我的数据结构设计如下:
1 | private: |
初始化时new出所有的缓冲池,并由buffer_pools_进行管理。具体的page操作都只需要找到对应的缓冲池进行操作即可,这里注意NewPgImp的时候要加锁,因为涉及到start_index变量的修改问题:
1 | std::scoped_lock scoped_db_latch(parallel_latch_); |
加完锁之后从缓冲池
1 | Running main() from gmock_main.cc |
gradescope 测试: