|
14 | 14 | - [central\_cache的核心逻辑](#central_cache的核心逻辑) |
15 | 15 | - [central\_cache里面fetch\_range\_obj的逻辑](#central_cache里面fetch_range_obj的逻辑) |
16 | 16 | - [page\_cache整体框架](#page_cache整体框架) |
| 17 | + - [获取span详解](#获取span详解) |
| 18 | + - [关于 new\_span 如何加锁的文字(重要/容易出bug)](#关于-new_span-如何加锁的文字重要容易出bug) |
| 19 | + - [内存申请流程联调](#内存申请流程联调) |
17 | 20 |
|
18 | 21 | *** |
19 | 22 |
|
@@ -593,4 +596,302 @@ size_t central_cache::fetch_range_obj(void*& start, void*& end, size_t batch_num |
593 | 596 |
|
594 | 597 | 当然cc到这里还没有完全写完的,但是我们要继续先写pc,才能来完善这里的部分。 |
595 | 598 |
|
596 | | -## page_cache整体框架 |
| 599 | +## page_cache整体框架 |
| 600 | +
|
| 601 | + |
| 602 | +
|
| 603 | +**申请内存:** |
| 604 | +1. 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有 则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没 有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page span分裂为一个4页page span和一个6页page span。 |
| 605 | +2. 如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式 申请128页page span挂在自由链表中,再重复1中的过程。 |
| 606 | +3. 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质 区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist 中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的 spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。 |
| 607 | +
|
| 608 | +
|
| 609 | +**释放内存:** |
| 610 | +1. 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。 |
| 611 | +
|
| 612 | +**这里的映射和之前的不一样,这里一共是128个桶,第一个是一页,第二个是两页!** |
| 613 | +
|
| 614 | +**pc只关注cc要多少页!注意,单位是页!** |
| 615 | +
|
| 616 | +page_cache.hpp |
| 617 | +```cpp |
| 618 | +class page_cache { |
| 619 | +private: |
| 620 | + span_list __span_lists[PAGES_NUM]; |
| 621 | + std::mutex __page_mtx; |
| 622 | + static page_cache __s_inst; |
| 623 | + page_cache() = default; |
| 624 | + page_cache(const page_cache&) = delete; |
| 625 | +
|
| 626 | +public: |
| 627 | + static page_cache* get_instance() { return &__s_inst; } |
| 628 | +public: |
| 629 | + // 获取一个K页的span |
| 630 | +}; |
| 631 | +``` |
| 632 | + |
| 633 | +也是要设计成单例模式。 |
| 634 | + |
| 635 | +然后怎么初始化呢? |
| 636 | + |
| 637 | +一开始全部设置为空,然后向OS(heap)要128页的span,然后后面要(假设要两页),那就把这个128页的的切分成126页的和2页的。然后2页的给cc,126页的就挂到126的桶上。 |
| 638 | + |
| 639 | +然后当cc有内存不要的时候,就还到对应的span里面去。然后pc通过页号,查看前后相邻页是否空闲,是的话就合并,和病除更大的页,解决内存碎片问题。 |
| 640 | + |
| 641 | +## 获取span详解 |
| 642 | + |
| 643 | +我们要遍历cc中的span,我们可以在common.hpp里面写一些遍历链表的组建。 |
| 644 | + |
| 645 | +common.hpp |
| 646 | +```cpp |
| 647 | +public: |
| 648 | + // 遍历相关 |
| 649 | + span* begin() { return __head->__next; } |
| 650 | + span* end() { return __head; } |
| 651 | +``` |
| 652 | +
|
| 653 | +central_cache.cc |
| 654 | +```cpp |
| 655 | +span* central_cache::get_non_empty_span(span_list& list, size_t size) { |
| 656 | + // 先查看当前的spanlist中是否还有非空的span |
| 657 | + span* it = list.begin(); |
| 658 | + while (it != list.end()) { |
| 659 | + if (it->__free_list != nullptr) // 找到非空的了 |
| 660 | + return it; |
| 661 | + it = it->__next; |
| 662 | + } |
| 663 | + //如果走到这里,说明没有空闲的span了,就要找pc了 |
| 664 | + page_cache::get_instance()->new_span(); |
| 665 | + return nullptr; |
| 666 | +} |
| 667 | +``` |
| 668 | + |
| 669 | +问题是,要多少页呢?也是有一个计算方法的,放到size_class里面去! |
| 670 | + |
| 671 | +common.hpp |
| 672 | +```cpp |
| 673 | + static inline size_t num_move_page(size_t size) { |
| 674 | + size_t num = num_move_size(size); |
| 675 | + size_t npage = num * size; |
| 676 | + npage >>= PAGE_SHIFT; // 相当于 /= 8kb |
| 677 | + if (npage == 0) |
| 678 | + npage = 1; |
| 679 | + return npage; |
| 680 | + } |
| 681 | +``` |
| 682 | +
|
| 683 | +所以。 |
| 684 | +central_cache.cc |
| 685 | +```cpp |
| 686 | +span* central_cache::get_non_empty_span(span_list& list, size_t size) { |
| 687 | + // 先查看当前的spanlist中是否还有非空的span |
| 688 | + span* it = list.begin(); |
| 689 | + while (it != list.end()) { |
| 690 | + if (it->__free_list != nullptr) // 找到非空的了 |
| 691 | + return it; |
| 692 | + it = it->__next; |
| 693 | + } |
| 694 | + //如果走到这里,说明没有空闲的span了,就要找pc了 |
| 695 | + span* cur_span = page_cache::get_instance()->new_span(size_class::num_move_page(size)); |
| 696 | + // 切分的逻辑 |
| 697 | + return nullptr; |
| 698 | +} |
| 699 | +``` |
| 700 | + |
| 701 | +下面就是切分的逻辑了。 |
| 702 | + |
| 703 | +怎么找到这个内存的地址呢? |
| 704 | + |
| 705 | +如果页号是100。那么页的起始地址就是 `100 << PAGE_SHIFT`。 |
| 706 | + |
| 707 | +central_cache.cc |
| 708 | +```cpp |
| 709 | +span* central_cache::get_non_empty_span(span_list& list, size_t size) { |
| 710 | + // 先查看当前的spanlist中是否还有非空的span |
| 711 | + span* it = list.begin(); |
| 712 | + while (it != list.end()) { |
| 713 | + if (it->__free_list != nullptr) // 找到非空的了 |
| 714 | + return it; |
| 715 | + it = it->__next; |
| 716 | + } |
| 717 | + // 如果走到这里,说明没有空闲的span了,就要找pc了 |
| 718 | + span* cur_span = page_cache::get_instance()->new_span(size_class::num_move_page(size)); |
| 719 | + // 切分的逻辑 |
| 720 | + // 1. 计算span的大块内存的起始地址和大块内存的大小(字节数) |
| 721 | + char* addr_start = (char*)(cur_span->__page_id << PAGE_SHIFT); |
| 722 | + size_t bytes = cur_span->__n << PAGE_SHIFT; // << PAGE_SHIFT 就是乘8kb的意思 |
| 723 | + char* addr_end = addr_start + bytes; |
| 724 | + // 2. 把大块内存切成自由链表链接起来 |
| 725 | + cur_span->__free_list = addr_start; // 先切一块下来做头 |
| 726 | + addr_start += size; |
| 727 | + void* tail = cur_span->__free_list; |
| 728 | + while(addr_start < addr_end) { |
| 729 | + free_list::__next_obj(tail) = addr_start; |
| 730 | + tail = free_list::__next_obj(tail); |
| 731 | + addr_start += size; |
| 732 | + } |
| 733 | + list.push_front(cur_span); |
| 734 | + return cur_span; |
| 735 | +} |
| 736 | +``` |
| 737 | +
|
| 738 | +注意,这里的切分是指把大块内存切成自由链表。不是pc里面的把大页切成小页。 |
| 739 | +
|
| 740 | +然后写完上面那个,我们既要去写 `span* page_cache::new_span(size_t k)` 了。这里就要把大页切成小页了。 |
| 741 | +
|
| 742 | +page_cache.cc |
| 743 | +```cpp |
| 744 | +// cc向pc获取k页的span |
| 745 | +span* page_cache::new_span(size_t k) { |
| 746 | + assert(k > 0 && k < PAGES_NUM); |
| 747 | + // 先检查第k个桶是否有span |
| 748 | + if (!__span_lists[k].empty()) |
| 749 | + return __span_lists->pop_front(); |
| 750 | + // 第k个桶是空的->去检查后面的桶里面有无span,如果有,可以把它进行切分 |
| 751 | + for (size_t i = k + 1; i < PAGES_NUM; i++) { |
| 752 | + if (!__span_lists[i].empty()) { |
| 753 | + // 可以开始切了 |
| 754 | + // 假设这个页是n页的,需要的是k页的 |
| 755 | + // 1. 从__span_lists中拿下来 2. 切开 3. 一个返回给cc 4. 另一个挂到 n-k 号桶里面去 |
| 756 | + span* n_span = __span_lists[i].pop_front(); |
| 757 | + span* k_span = new span; |
| 758 | + // 在n_span头部切除k页下来 |
| 759 | + k_span->__page_id = n_span->__page_id; // <1> |
| 760 | + k_span->__n = k; // <2> |
| 761 | + n_span->__page_id += k; // <3> |
| 762 | + n_span->__n -= k; // <4> |
| 763 | + /** |
| 764 | + * 这里要好好理解一下 100 ------ 101 ------- 102 ------ |
| 765 | + * 假设n_span从100开始,大小是3 |
| 766 | + * 切出来之后k_span就是从100开始了,所以<1> |
| 767 | + * 切出来之后k_span就有k页了,所以 <2> |
| 768 | + * 切出来之后n_span就是从102开始了,所以 <3> |
| 769 | + * 切出来之后n_span就变成__n-k页了,所以 <4> |
| 770 | + */ |
| 771 | + // 剩下的挂到相应位置 |
| 772 | + __span_lists[n_span->__n].push_front(n_span); |
| 773 | + return k_span; |
| 774 | + } |
| 775 | + } |
| 776 | + // 走到这里,说明找不到span了:找os要 |
| 777 | + span* big_span = new span; |
| 778 | + big_span = |
| 779 | +} |
| 780 | +``` |
| 781 | + |
| 782 | +这里切分的逻辑,代码注释里面写的很清楚了! |
| 783 | + |
| 784 | +然后如果找到128页的都没找到,直接向系统申请! |
| 785 | + |
| 786 | +这里要区分windows和linux。 |
| 787 | + |
| 788 | +common.hpp |
| 789 | +```cpp |
| 790 | +inline static void* system_alloc(size_t kpage) { |
| 791 | + void* ptr = nullptr; |
| 792 | +#if defined(_WIN32) || defined(_WIN64) |
| 793 | +#include <windows.h> |
| 794 | + *ptr = VirtualAlloc(0, kpage * (1 << 12), MEM_COMMIT | MEM_RESERVE, |
| 795 | + PAGE_READWRITE); |
| 796 | +#elif defined(__aarch64__) // ... |
| 797 | +#include <sys/mman.h> |
| 798 | + void* ptr = mmap(NULL, kpage << 13, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); |
| 799 | +#else |
| 800 | +#include <iostream> |
| 801 | + std::cerr << "unknown system" << std::endl; |
| 802 | + throw std::bad_alloc(); |
| 803 | +#endif |
| 804 | + if (ptr == nullptr) |
| 805 | + throw std::bad_alloc(); |
| 806 | + return ptr; |
| 807 | +} |
| 808 | +``` |
| 809 | +
|
| 810 | +然后new_span最后: |
| 811 | +
|
| 812 | +```cpp |
| 813 | + // 走到这里,说明找不到span了:找os要 |
| 814 | + span* big_span = new span; |
| 815 | + void* ptr = system_alloc(PAGES_NUM - 1); |
| 816 | + big_span->__page_id = (PAGE_ID)ptr >> PAGE_SHIFT; |
| 817 | + big_span->__n = PAGES_NUM - 1; |
| 818 | + // 挂到上面去 |
| 819 | + __span_lists[PAGES_NUM - 1].push_front(big_span); |
| 820 | + return new_span(k); |
| 821 | +``` |
| 822 | + |
| 823 | +插入之后,不要重复写切分的逻辑了,递归调用一次自己就行了! |
| 824 | + |
| 825 | +### 关于 new_span 如何加锁的文字(重要/容易出bug) |
| 826 | + |
| 827 | +然后这里还有最关键的一步。这里整个方法是要加锁的! |
| 828 | + |
| 829 | +这里有个关键问题需要思考。 |
| 830 | + |
| 831 | +get_non_empty_span是被fetch_range_obj调用的(在cc.cc)里面。 |
| 832 | + |
| 833 | +但是get_non_empty_span会去调用pc的new_span。 |
| 834 | +现在有个关键问题了,此时,如果我们不做处理,在pc的new_span里面,其实是有cc的桶锁的。 |
| 835 | +这个是很不好的。因为这个桶可能有内存需要释放啊!你锁住了,别人就进不去了。 |
| 836 | +(其实这里我也是一知半解,要再去理解一下) |
| 837 | + |
| 838 | +所以,在`span* central_cache::get_non_empty_span(span_list& list, size_t size) {`里面这个`span* cur_span = page_cache::get_instance()->new_span(size_class::num_move_page(size));`这句话前面。我们先把桶锁解掉。 |
| 839 | + |
| 840 | +然后pc的new_span的全局锁,我们在cc.cc里面的`span* central_cache::get_non_empty_span(span_list& list, size_t size) {` 这里加。 |
| 841 | + |
| 842 | +cc.cc |
| 843 | +```cpp |
| 844 | + // 如果走到这里,说明没有空闲的span了,就要找pc了 |
| 845 | + page_cache::get_instance()->__page_mtx.lock(); |
| 846 | + span* cur_span = page_cache::get_instance()->new_span(size_class::num_move_page(size)); |
| 847 | + page_cache::get_instance()->__page_mtx.unlock(); |
| 848 | +``` |
| 849 | +
|
| 850 | +**现在问题来了,我们cc拿到这个新的span,后面还要切分的。刚刚在拿new_span之前解锁了,现在需要加上吗?** |
| 851 | +
|
| 852 | +不需要! |
| 853 | +
|
| 854 | +因为这个span是从pc拿来的,新的,也还没挂到cc上面去,所以别的线程拿不到这个span!所以不用加锁! |
| 855 | +
|
| 856 | +但是最后一步 `list.push_front(span)` 要访问cc对象了!就要加锁,我们把锁恢复一下。 |
| 857 | +
|
| 858 | +central_cache.cc |
| 859 | +```cpp |
| 860 | +span* central_cache::get_non_empty_span(span_list& list, size_t size) { |
| 861 | + // 先查看当前的spanlist中是否还有非空的span |
| 862 | + span* it = list.begin(); |
| 863 | + while (it != list.end()) { |
| 864 | + if (it->__free_list != nullptr) // 找到非空的了 |
| 865 | + return it; |
| 866 | + it = it->__next; |
| 867 | + } |
| 868 | + // 这里先解开桶锁 |
| 869 | + list.__bucket_mtx.unlock(); |
| 870 | +
|
| 871 | + // 如果走到这里,说明没有空闲的span了,就要找pc了 |
| 872 | + page_cache::get_instance()->__page_mtx.lock(); |
| 873 | + span* cur_span = page_cache::get_instance()->new_span(size_class::num_move_page(size)); |
| 874 | + page_cache::get_instance()->__page_mtx.unlock(); |
| 875 | +
|
| 876 | + // 切分的逻辑 |
| 877 | + // 1. 计算span的大块内存的起始地址和大块内存的大小(字节数) |
| 878 | + char* addr_start = (char*)(cur_span->__page_id << PAGE_SHIFT); |
| 879 | + size_t bytes = cur_span->__n << PAGE_SHIFT; // << PAGE_SHIFT 就是乘8kb的意思 |
| 880 | + char* addr_end = addr_start + bytes; |
| 881 | + // 2. 把大块内存切成自由链表链接起来 |
| 882 | + cur_span->__free_list = addr_start; // 先切一块下来做头 |
| 883 | + addr_start += size; |
| 884 | + void* tail = cur_span->__free_list; |
| 885 | + while(addr_start < addr_end) { |
| 886 | + free_list::__next_obj(tail) = addr_start; |
| 887 | + tail = free_list::__next_obj(tail); |
| 888 | + addr_start += size; |
| 889 | + } |
| 890 | + // 恢复锁 |
| 891 | + list.__bucket_mtx.lock(); |
| 892 | + list.push_front(cur_span); |
| 893 | + return cur_span; |
| 894 | +} |
| 895 | +``` |
| 896 | + |
| 897 | +## 内存申请流程联调 |
0 commit comments