<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 7.2 組件 本節獨立地討論內存分配器中的幾個組件:`fixalloc`、`linearAlloc`、`mcache`。 ## fixalloc `fixalloc`是一個基于自由列表的固定大小的分配器。其核心原理是將若干未分配的內存塊連接起來, 將未分配的區域的第一個字為指向下一個未分配區域的指針使用。 Go 的主分配堆中 malloc(span、cache、treap、finalizer、profile、arena hint 等) 均 圍繞它為實體進行固定分配和回收。 fixalloc 作為抽象,非常簡潔,只包含三個基本操作:初始化、分配、回收 ### 結構 ``` // fixalloc 是一個簡單的固定大小對象的自由表內存分配器。 // Malloc 使用圍繞 sysAlloc 的 fixalloc 來管理其 MCache 和 MSpan 對象。 // // fixalloc.alloc 返回的內存默認為零,但調用者可以通過將 zero 標志設置為 false // 來自行負責將分配歸零。如果這部分內存永遠不包含堆指針,則這樣的操作是安全的。 // // 調用方負責鎖定 fixalloc 調用。調用方可以在對象中保持狀態, // 但當釋放和重新分配時第一個字會被破壞。 // // 考慮使 fixalloc 的類型變為 go:notinheap. type fixalloc struct { size uintptr first func(arg, p unsafe.Pointer) // 首次調用時返回 p arg unsafe.Pointer list *mlink chunk uintptr // 使用 uintptr 而非 unsafe.Pointer 來避免 write barrier nchunk uint32 inuse uintptr // 正在使用的字節 stat *uint64 zero bool // 歸零的分配 } ``` ### 初始化 Go 語言對于零值有自己的規定,自然也就體現在內存分配器上。而`fixalloc`作為內存分配器內部組件的來源于 操作系統的內存,自然需要自行初始化,因此,`fixalloc`的初始化也就不可避免的需要將自身的各個字段歸零: ``` // 初始化 f 來分配給定大小的對象。 // 使用分配器來按 chunk 獲取 func (f *fixalloc) init(size uintptr, first func(arg, p unsafe.Pointer), arg unsafe.Pointer, stat *uint64) { f.size = size f.first = first f.arg = arg f.list = nil f.chunk = 0 f.nchunk = 0 f.inuse = 0 f.stat = stat f.zero = true } ``` ### 分配 `fixalloc`基于自由表策略進行實現,分為兩種情況: 1. 存在被釋放、可復用的內存 2. 不存在可復用的內存 對于第一種情況,也就是在運行時內存被釋放,但這部分內存并不會被立即回收給操作系統, 我們直接從自由表中獲得即可,但需要注意按需將這部分內存進行清零操作。 對于第二種情況,我們直接向操作系統申請固定大小的內存,然后扣除分配的大小即可。 ``` const _FixAllocChunk = 16 &lt;&lt; 10 // FixAlloc 一個 Chunk 的大小 func (f *fixalloc) alloc() unsafe.Pointer { // fixalloc 的個字段必須先被 init if f.size == 0 { print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n") throw("runtime: internal error") } // 如果 f.list 不是 nil, 則說明還存在已經釋放、可復用的內存,直接將其分配 if f.list != nil { // 取出 f.list v := unsafe.Pointer(f.list) // 并將其指向下一段區域 f.list = f.list.next // 增加使用的(分配)大小 f.inuse += f.size // 如果需要對內存清零,則對取出的內存執行初始化 if f.zero { memclrNoHeapPointers(v, f.size) } // 返回分配的內存 return v } // f.list 中沒有可復用的內存 // 如果此時 nchunk 不足以分配一個 size if uintptr(f.nchunk) &lt; f.size { // 則向操作系統申請內存,大小為 16 &lt;&lt; 10 pow(2,14) f.chunk = uintptr(persistentalloc(_FixAllocChunk, 0, f.stat)) f.nchunk = _FixAllocChunk } // 指向申請好的內存 v := unsafe.Pointer(f.chunk) if f.first != nil { // first 只有在 fixalloc 作為 spanalloc 時候,才會被設置為 recordspan f.first(f.arg, v) // 用于為 heap.allspans 添加新的 span } // 扣除并保留 size 大小的空間 f.chunk = f.chunk + f.size f.nchunk -= uint32(f.size) f.inuse += f.size // 記錄已經使用的大小 return v } ``` 我們在稍后討論`memclrNoHeapPointers`和`persistentalloc`。 ### 回收 回收就更加簡單了,直接將回收的地址指針放回到自由表中即可: ``` func (f *fixalloc) free(p unsafe.Pointer) { // 減少使用的字節數 f.inuse -= f.size // 將要釋放的內存地址作為 mlink 指針插入到 f.list 內,完成回收 v := (*mlink)(p) v.next = f.list f.list = v } ``` ## linearAlloc `linearAlloc`是一個基于線性分配策略的分配器,但由于它只作為`mheap_.heapArenaAlloc`和`mheap_.arena`在 32 位系統上使用,這里不做詳細分析。 ``` // linearAlloc 是一個簡單的線性分配器,它預留一塊內存區域并按需將其映射到 Ready 狀態。 // 調用方有責任對齊進行加鎖。 type linearAlloc struct { next uintptr // 下一個可用的字節 mapped uintptr // 映射空間后的一個字節 end uintptr // 保留空間的末尾 } func (l *linearAlloc) init(base, size uintptr) { l.next, l.mapped = base, base l.end = base + size } func (l *linearAlloc) alloc(size, align uintptr, sysStat *uint64) unsafe.Pointer { p := round(l.next, align) if p+size &gt; l.end { return nil } l.next = p + size if pEnd := round(l.next-1, physPageSize); pEnd &gt; l.mapped { // We need to map more of the reserved space. sysMap(unsafe.Pointer(l.mapped), pEnd-l.mapped, sysStat) l.mapped = pEnd } return unsafe.Pointer(p) } ``` ## mcache `mcache`是一個 per-P 的緩存,因此每個線程都只訪問自身的`mcache`,因此也就不會出現 并發,也就省去了對其進行加鎖步驟。 ``` //go:notinheap type mcache struct { // 下面的成員在每次 malloc 時都會被訪問 // 因此將它們放到一起來利用緩存的局部性原理 next_sample uintptr // 分配這么多字節后觸發堆樣本 local_scan uintptr // 分配的可掃描堆的字節數 // 沒有指針的微小對象的分配器緩存。 // 請參考 malloc.go 中的 "小型分配器" 注釋。 // // tiny 指向當前 tiny 塊的起始位置,或當沒有 tiny 塊時候為 nil // tiny 是一個堆指針。由于 mcache 在非 GC 內存中,我們通過在 // mark termination 期間在 releaseAll 中清除它來處理它。 tiny uintptr tinyoffset uintptr local_tinyallocs uintptr // 不計入其他統計的極小分配的數量 // 下面的不在每個 malloc 時被訪問 alloc [numSpanClasses]*mspan // 用來分配的 spans,由 spanClass 索引 stackcache [_NumStackOrders]stackfreelist // 本地分配器統計,在 GC 期間被刷新 local_largefree uintptr // bytes freed for large objects (&gt;maxsmallsize) local_nlargefree uintptr // number of frees for large objects (&gt;maxsmallsize) local_nsmallfree [_NumSizeClasses]uintptr // number of frees for small objects (&lt;=maxsmallsize) // flushGen indicates the sweepgen during which this mcache // was last flushed. If flushGen != mheap_.sweepgen, the spans // in this mcache are stale and need to the flushed so they // can be swept. This is done in acquirep. flushGen uint32 } ``` ### 分配 運行時的`runtime.allocmcache`從`mheap`上分配一個`mcache`。 由于`mheap`是全局的,因此在分配期必須對其進行加鎖,而分配通過 fixAlloc 組件完成: ``` // 虛擬的MSpan,不包含任何對象。 var emptymspan mspan func allocmcache() *mcache { var c *mcache systemstack(func() { lock(&amp;mheap_.lock) c = (*mcache)(mheap_.cachealloc.alloc()) c.flushGen = mheap_.sweepgen unlock(&amp;mheap_.lock) } for i := range c.alloc { c.alloc[i] = &amp;emptymspan // 暫時指向虛擬的 mspan 中 } // 返回下一個采樣點,是服從泊松過程的隨機數 c.next_sample = nextSample() return c } ``` 由于運行時提供了采樣過程堆分析的支持, 由于我們的采樣的目標是平均每個`MemProfileRate`字節對分配進行采樣, 顯然,在整個時間線上的分配情況應該是完全隨機分布的,這是一個泊松過程。 因此最佳的采樣點應該是服從指數分布`exp(MemProfileRate)`的隨機數,其中`MemProfileRate`為均值。 ``` func nextSample() uintptr { if GOOS == "plan9" { // Plan 9 doesn't support floating point in note handler. if g := getg(); g == g.m.gsignal { return nextSampleNoFP() } } return uintptr(fastexprand(MemProfileRate)) } ``` `MemProfileRate`是一個公共變量,可以在用戶態代碼進行修改: 1 var MemProfileRate int = 512 * 1024 ### 釋放 由于`mcache`從非 GC 內存上進行分配,因此出現的任何堆指針都必須進行特殊處理。 所以在釋放前,需要調用`mcache.releaseAll`將堆指針進行處理: ``` func (c *mcache) releaseAll() { for i := range c.alloc { s := c.alloc[i] if s != &amp;emptymspan { // 將 span 歸還 mheap_.central[i].mcentral.uncacheSpan(s) c.alloc[i] = &amp;emptymspan } } // 清空 tinyalloc 池. c.tiny = 0 c.tinyoffset = 0 } ``` ``` func freemcache(c *mcache) { systemstack(func() { // 歸還 span c.releaseAll() // 釋放 stack stackcache_clear(c) lock(&amp;mheap_.lock) // 記錄局部統計 purgecachedstats(c) // 將 mcache 釋放 mheap_.cachealloc.free(unsafe.Pointer(c)) unlock(&amp;mheap_.lock) }) } ``` ### per-P? per-M? mcache 其實早在[調度器: 調度循環](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/exec)中與 mcache 打過照面了。 首先,mcache 是一個 per-P 的 mcache,我們很自然的疑問就是,這個 mcache 在 p/m 這兩個結構體上都有成員: ``` type p struct { (...) mcache *mcache (...) } type m struct { (...) mcache *mcache (...) } ``` 那么 mcache 是跟著誰跑的?結合調度器的知識不難發現,m 在執行時需要持有一個 p 才具備執行能力。 有利的證據是,當調用`runtime.procresize`時,初始化新的 P 時,mcache 是直接分配到 p 的; 回收 p 時,mcache 是直接從 p 上獲取: ``` func procresize(nprocs int32) *p { (...) // 初始化新的 P for i := int32(0); i &lt; nprocs; i++ { pp := allp[i] (...) // 為 P 分配 cache 對象 if pp.mcache == nil { if old == 0 &amp;&amp; i == 0 { if getg().m.mcache == nil { throw("missing mcache?") } pp.mcache = getg().m.mcache } else { // 創建 cache pp.mcache = allocmcache() } } (...) } // 釋放未使用的 P for i := nprocs; i &lt; old; i++ { p := allp[i] (...) // 釋放當前 P 綁定的 cache freemcache(p.mcache) p.mcache = nil (...) } (...) } ``` 因而我們可以明確: * mcache 會被 P 持有,當 M 和 P 綁定時,M 同樣會保留 mcache 的指針 * mcache 直接向操作系統申請內存,且常駐運行時 * P 通過 make 命令進行分配,會分配在 Go 堆上 ## 其他 ### memclrNoHeapPointers `memclrNoHeapPointers`用于清理不包含堆指針的內存區塊: ``` // memclrNoHeapPointers 清除從 ptr 開始的 n 個字節 // 通常情況下你應該使用 typedmemclr,而 memclrNoHeapPointers 應該僅在調用方知道 *ptr // 不包含堆指針的情況下使用,因為 *ptr 只能是下面兩種情況: // 1. *ptr 是初始化過的內存,且其類型不是指針。 // 2. *ptr 是未初始化的內存(例如剛被新分配時使用的內存),則指包含 "junk" 垃圾內存 // 見 memclr_*.s // //go:noescape func memclrNoHeapPointers(ptr unsafe.Pointer, n uintptr) ``` 清理過程是匯編實現的,就是一些內存的歸零工作,簡單瀏覽一下: ``` TEXT runtime·memclrNoHeapPointers(SB), NOSPLIT, $0-8 MOVL ptr+0(FP), DI MOVL n+4(FP), BX XORL AX, AX // MOVOU 好像總是比 REP STOSL 快 tail: (...) loop: MOVOU X0, 0(DI) MOVOU X0, 16(DI) MOVOU X0, 32(DI) MOVOU X0, 48(DI) MOVOU X0, 64(DI) MOVOU X0, 80(DI) MOVOU X0, 96(DI) (...) ``` ## 系統級內存管理調用 系統級的內存管理調用是平臺相關的,這里以 Linux 為例,運行時的`sysAlloc`、`sysUnused`、`sysUsed`、`sysFree`、`sysReserve`、`sysMap`和`sysFault`都是系統級的調用。 其中`sysAlloc`、`sysReserve`和`sysMap`都是向操作系統申請內存的操作,他們均涉及關于內存分配的系統調用就是`mmap`,區別在于: * `sysAlloc`是從操作系統上申請清零后的內存,調用參數是`_PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE`; * `sysReserve`是從操作系統中保留內存的地址空間,并未直接分配內存,調用參數是`_PROT_NONE, _MAP_ANON|_MAP_PRIVATE`,; * `sysMap`則是用于通知操作系統使用先前已經保留好的空間,參數是`_PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE`。 不過`sysAlloc`和`sysReserve`都是操作系統對齊的內存,但堆分配器可能使用更大的對齊方式,因此這部分獲得的內存都需要額外進行一些重排的工作。 ``` // runtime/mem_linux.go //go:nosplit func sysAlloc(n uintptr, sysStat *uint64) unsafe.Pointer { p, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0) if err != 0 { if err == _EACCES { print("runtime: mmap: access denied\n") exit(2) } if err == _EAGAIN { print("runtime: mmap: too much locked memory (check 'ulimit -l').\n") exit(2) } return nil } (...) return p } func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) if err != 0 { return nil } return p } func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) { (...) p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0) if err == _ENOMEM { throw("runtime: out of memory") } if p != v || err != 0 { throw("runtime: cannot map pages in arena address space") } } ``` Linux 下內存分配調用有多個: * brk: 可以讓進程的堆指針增長,從邏輯上消耗一塊虛擬地址空間 * mmap: 可以讓進程的虛擬地址空間切分出一塊指定大小的虛擬地址空間,mmap 映射返回的地址也是從邏輯上被消耗的,需要通過 unmap 進行回收。 熟悉 C 語言的讀者應該知道 malloc,它只是 C 語言的標準庫函數,本質上是通過上述兩個系統調用完成, 當分配內存較小時調用 brk,反之則會調用 mmap。不過 64 位系統上的 Go 運行時并沒有使用 brk,目的很明顯, 是為了能夠更加靈活的控制虛擬地址空間。 而對于 unmap 操作,它被封裝在了`sysFree`中: ``` //go:nosplit func sysFree(v unsafe.Pointer, n uintptr, sysStat *uint64) { (...) munmap(v, n) } ``` `sysUnused`、`sysUsed`是`madvice`的封裝,我們知道`madvice`用于向操作系統通知某段內存區域是否被應用所使用。`sysFault`用于將`sysAlloc`獲得的內存區域標記為故障,只用于運行時調試。 最后我們來理一下這些系統級調用的關系: 1. 當開始保留內存地址時,調用`sysReserve`; 2. 當需要使用或不適用保留的內存區域時通知操作系統,調用`sysUnused`、`sysUsed`; 3. 正式使用保留的地址,使用`sysMap`; 4. 釋放時使用`sysFree`以及調試時使用`sysFault`; 5. 非用戶態的調試、堆外內存則使用`sysAlloc`直接向操作系統獲得清零的內存
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看