高并發內存池(三):PageCache(頁緩存)的實現

前言:

????????在前兩期內容中,我們深入探討了內存管理機制中在?ThreadCache?和?CentralCache兩個層級進行內存申請的具體實現。這兩層緩存作為高效的內存分配策略,能夠快速響應線程的內存需求,減少鎖競爭,提升程序性能。

????????本期文章將繼續沿著這一脈絡,聚焦于PageCache層級的內存申請邏輯。作為內存分配體系的更高層級,PageCache承擔著從操作系統獲取大塊內存,并對其進行管理和再分配的重要職責。通過對PageCache內存申請流程的剖析,我們將完整地勾勒出整個內存分配體系的工作流程。這不僅有助于我們理解內存分配的底層機制,也為后續深入探討內存釋放策略奠定了基礎。

????????接下來,讓我們一同走進PageCache的內存世界。

目錄

一、PageCache的概述

二、PageCache結構

三、內存申請的核心流程

四、PageCache類的設計

五、代碼實現

1.GetOneSpan的實現

2.NewSpan的實現

3.加鎖保護

六、源碼


一、PageCache的概述

? ? ? ? ?緩存是在central cache緩存上?的?層緩存,存儲的內存是以?為單位存儲及分配的。當central cache沒有內存對象時,從page cache分配出?定數量的page,并切割成定???的?塊內存,分配給central cache。當?個span的?個跨度?的對象都回收以后,page cache會回收central cache滿?條件的span對象,并且合并相鄰的?,組成更?的?,緩解內存碎?的問題。

二、PageCache結構

????????PageCache的結構是一個哈希表,如上圖所示,也就是一個儲存span雙鏈表的數組,這一點和CentralCache的結構完全類似,區別在于這里的Span是大塊的頁(1頁~128頁)沒有進行分割,而CentralCache中的Span是分割好的小塊內存(自由鏈表),這樣做的好處是方便頁與頁之間的分割合并有效的緩解內存外碎片,在下文會詳細講解。

????????PageCache結構的哈希映射方法是直接定址法,下標為n的位置儲存的Span雙鏈表中每個節點有n個頁。

? ? ? ? 這個哈希表也是所有線程共用一個,屬于臨界資源需要加鎖,但不是桶鎖,而是一整個大鎖,為什么呢?同樣在下文細講。

注:通常以4KB或8KB為一頁,這里我們以8KB為一頁。

三、內存申請的核心流程

  • ThreadCache \rightarrow?CentralCache?\rightarrow?PageCache?\rightarrow?系統

????????當ThreadCache的自由鏈表內沒有內存時,會向CentralCache中的一個Span申請,如果沒有可用的Span,就向PageCache申請一個Span大塊頁,如果沒有Span大塊頁了,最后才會向系統申請。

那么CentralCache具體是如何向PageCache申請內存的呢?

? ? ? ? 同樣的PageCache一次只給一個Span,然后把這個Span切割成小塊連接到CentralCache的哈希桶中,要給幾頁的Span呢?,這需要根據要將它切割成多大為單位的小塊內存來進行具體分配,如果要切較小的內存塊,頁數就給小一些,如果要切大內存塊,頁數就給多一些。這里我們就先記為n頁的Span。

? ? ? ? 因為PageCache中的哈希映射是直接定址法,所以直接找到n下標位置的Span鏈,如果有Span則取出并切成小塊連接到CentralCache的哈希桶中。

????????如果沒有n頁的Span并不是直接去系統申請,而是到更大的頁單位去找Span。比如找到有(n+k)頁的Span,k>0,n+k<=128,那么把(n+k)拆開為n頁與k頁,并連接到相應位置。這樣就有了n頁的Span,把它切割和連接就行。

? ? ? ? 如果直到找到128頁也沒有找到可用的Span那么就向系統申請128頁的內存,即128*8*1024字節,然后再執行上面邏輯。這就是整個在PageCache申請內存的邏輯。

處理臨界資源互斥問題

如上,PageCache的哈希桶中各個桶是互相聯動的,主要體現在這三方面:

  1. 當前桶沒Span要往后找。
  2. 后面的桶切割Span后要連接到前面的桶。
  3. 在內存回收時又需要前面桶的Span合并成大頁的Span連接到后面的桶。

????????所以這里不像CentralCache結構的桶相互獨立,如果用桶鎖會造成頻繁的鎖申請和釋放,效率反而變得很低。用一個大鎖來管理整個哈希桶更為合理。

四、PageCache類的設計

????????把這個類的設計放在頭文件PageCache.h里,它核心就兩個成員變量:哈希桶,然后再聲明一個用來申請Span的成員函數,如下:

class PageCache
{
public:Span* NewSpan(size_t k);std::mutex _pageMtx;
private:SpanList _spanLists[NPAGES];
};
  • Span* NewSpan(size_t k):申請一個k頁的Span
  • mutex _pageMtx:鎖,因為它要在外部使用,所以設為public。
  • SpanList _spanLists[NPAGES]:哈希桶,其中NPAGES是一個靜態全局變量為129,在Common.h中定義。設為129是因為哈希表中存1頁~128頁的Span,要通過頁數直接定址的方式找到Span鏈表,就需要開辟129大小的數組。

????????PageCache類和CentralCache類一樣所有線程共用一份,所以把它創建為單例模式,它分為以下幾步:

  • 把構造函數設為私有。
  • 把拷貝構造禁用。
  • 聲明靜態的PageCache對象,并在PageCache.cpp中定義。
  • 提供一個獲取PageCache對象的靜態成員函數,并設為public

代碼示例:

class PageCache
{
public:static PageCache* GetInstance(){return &_sInst;}Span* NewSpan(size_t k);std::mutex _pageMtx;
private:PageCache() {}PageCache(const PageCache&) = delete;SpanList _spanLists[NPAGES];static PageCache _sInst;
};

五、代碼實現

1.GetOneSpan的實現

????????回顧上一期我們只是假設通過GetOneSpan從CentralCache中取到了Span,然后繼續做后面的處理。接下來我們一起來實現GetOneSpan函數

????????因為要在Span鏈表中取一個有用的Span節點,所以需要遍歷Span鏈表,那么可以模擬一個迭代器。我們在SpanList類中封裝這兩個函數:

Span* Begin()
{return _head->_next;
}
Span* End()
{return _head;
}

注:SpanList是一個帶頭雙向環形鏈表。?

接下來遍歷鏈表找到可用的Span并返回,如果沒有則到PageCache中申請。

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{Span* it = list.Begin();while (it != list.End()){if (it->_freeList != nullptr){return it;}it = it->_next;}//走到這里說明沒有可用的Span了,向PageCache中申請。//......
}
  • SpanList& list:指定的一個哈希桶,即一個Span鏈表的頭結點。
  • size_t size:進行內存對齊后實際需要申請的字節大小。

????????注意在PageCache中申請的是大塊的Span頁,還需要把它切割,然后連接到CentralCache的桶中并返回。

????????在PageCache類中我們準備實現的函數NewSpan就是用來實現這個功能,現在需要考慮的是要傳入的參數是多少,即要申請多少頁的Span。

這里我們是以8KB為一頁,即 1頁=8*1024字節(2^13字節)為方便后面做位運算,我們在Common.h文件定義一個這樣一個變量:

????????????????static int const PAGE_SHIFT = 13;

當然了這里也可以做成宏定義。

????????接下來在SizeClass類里封裝一個函數NumMovePage用來計算一個size需要申請幾頁的Span,如下:

//用來計算ThreadCache向CentralCache申請幾個小內存塊
static inline size_t NumMoveSize(size_t size)
{int ret = MAX_BYTES / size;if (ret < 2) ret = 2;if (ret > 512) ret = 512;return ret;
}
//用來計算CentralCache向PageCache申請幾個頁的Span
static inline size_t NumMovePage(size_t size)
{int num = NumMoveSize(size);int npage = num * size;npage >>= PAGE_SHIFT; //除以8KBif (npage < 1) npage = 1;return npage;
}

????????在此,大家無需過度糾結于該設計背后的原理。這或許是相關領域的資深專家在歷經大量測試與實踐后,所總結提煉出的有效策略,我們繼續走下面的邏輯。

//申請Span
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//切割Span
//......

????????NewSpan函數我們待會再設計,現在假設已經申請到一個Span的大塊頁,接下來就是進行切割。?

????????首先我們需要得到的是這個頁的起始地址終止地址,在這個范圍內以size的跨度切割

? ? ? ? 用char*的指針變量start,end分別來儲存起始地址和終止地址,用char*類型是因為char*類型指針變量加多少,就移動多少字節,方便后面做切割運算。

起始頁地址的獲取:

取到Span的頁號,用頁號乘以8KB(2^13字節)就能得到頁的起始地址,即:

????????????????char* start = (char*)(span->_pageId << PAGE_SHIFT);

終止頁地址的獲取:

取到Span的頁數,用頁數乘以8KB再加上起始頁地址就能得到終止頁地址,即:

????????????????int bytes = span->_n << PAGE_SHIFT;
? ? ? ? ? ? ? ? char* end = start + bytes;

注:頁號是通過申請到的內存的虛擬地址除以8KB得到的。

????????接下來把start連接到Span的自由鏈表中,然后使用循環把[start,end]這塊內存以size為跨度進行切割,如下:

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{Span* it = list.Begin();while (it != list.End()){if (it->_freeList != nullptr){return it;}it = it->_next;}//申請SpanSpan* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//切割Spanchar* start = (char*)(span->_pageId << PAGE_SHIFT);int bytes = span->_n << PAGE_SHIFT;char* end = start + bytes;span->_freeList = start;while (start < end){Nextobj(start) = start + size;start = (char*)Nextobj(start);}Nextobj(end) = nullptr;//連接到CentralCache中list.PushFront(span);return span;
}

????????這里直接以尾插的方式切,好處在于用戶在使用內存時是連在一塊的,相關的數據會被一并載入寄存器中,可以增加高速緩存命中率,提高效率。

最后實現一個類方法PushFront,用來把Span連接到哈希桶里。

void PushFront(Span* node)
{//在Begin()前插入node,即頭插Insert(Begin(), node);
}

2.NewSpan的實現

NewSpan我們放在源文件PageCache.cpp中實現。

查找當前桶

????????首先可以用assert斷言頁數的有效性,然后直接定址找到Span鏈,如果不為空返回一個Span節點,如果為空到后面的大頁去找,如下:

Span* PageCache::NewSpan(size_t k)
{//判斷頁數的有效性assert(k > 0 && k < NPAGES);if (!_spanLists[k].Empty()){return _spanLists[k].PopFront();}//到后面更大塊的Span頁切割//......//向系統申請內存//......
}

到大頁切割

????????當k號桶沒有Span后,從k+1號桶開始往后找,比如找到i號桶不為空,則把Span節點取出來,記為nSpan,然后切割為k頁和i-k頁,切割的本質就是改變頁數_n和頁號_pageId

????????new一個Span空間,用kSpan變量指向,然后把nSpan頭k個頁切出來儲存到一個kSpan中,即:

  • kSpan->_pageId = nSpan->_pageId:更新kSpan的頁號。
  • kSpan->_n = k:更新kSpan的頁數。
  • nSpan->_pageId += k:原nSpan的頁起始頁號后移k位。
  • nSpan->_n -= k:原nSpan的頁數減少k。

????????這樣就相當于把原來大塊的nSpan切割成了兩塊,最后把割剩下的nSpan插入到對應的哈希桶中,把kSpan返回。

	for (int i = k + 1; i < NPAGES; i++){if (!_spanLists[i].Empty()){Span* nSpan = _spanLists[i].PopFront();Span* kSpan = new Span;kSpan->_pageId = nSpan->_pageId;kSpan->_n = k;nSpan->_pageId += k;nSpan->_n -= k;_spanLists[nSpan->_n].PushFront(nSpan);return kSpan;}}

向系統申請?

????????當k號桶往后的桶都是空的,那么我們就需要向系統申請內存了,直接申請一個128的頁,放在128號桶,再執行上面的邏輯。?

????????對于內存申請,在windows下我們使用VirtualAlloc函數,與malloc相比VirtualAlloc直接與操作系統交互,無額外開銷,效率高,通常用來申請超大塊內存。

VirtualAlloc的聲明

LPVOID VirtualAlloc(LPVOID  lpAddress,        SIZE_T  dwSize,           DWORD   flAllocationType, DWORD   flProtect         
);
  • LPVOID ?lpAddress:期望的起始地址(通常設為0由系統決定)
  • SIZE_T ?dwSize:分配的內存大小(字節)
  • DWORD ? flAllocationType:?分配的類型(如保留或提交)
  • DWORD ? flProtect:內存保護選項(如讀寫權限)

返回值:LPVOID ,本質是void*,需強制類型轉換后使用。

  • flAllocationType

    • MEM_COMMIT:提交內存,使其可用。

    • MEM_RESERVE:保留地址空間,暫不分配物理內存。

    • 二者可組合使用(MEM_RESERVE | MEM_COMMIT),同時保留并提交。

  • flProtect
    控制內存訪問權限,常用選項:

    • PAGE_READWRITE:可讀/寫。

    • PAGE_NOACCESS:禁止訪問(觸發訪問違規)。

    • PAGE_EXECUTE_READ:可執行/讀。

????????因為還要考慮其他系統的需求,我們在Common.h內封裝一個向系統申請內存的函數,并使用條件編譯來選擇不同的函數調用,如下:

#ifdef _WIN32#include <windows.h>
#else//Linux...
#endifinline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();//拋異常return ptr;
}

????????接下來繼續NewSpan的執行邏輯,向系統申請內存后,new一個Span儲存相關的頁信息,即計算頁號(地址/8KB),填寫頁數,然后把Span連接到128號哈希桶,最后需要做的就是把它切割為k頁的Span和(128-k)頁的Span和前面的邏輯一模一樣,為提高代碼復用率以遞歸的方式返回。如下:

Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES);if (!_spanLists[k].Empty()){return _spanLists[k].PopFront();}for (int i = k + 1; i < NPAGES; i++){if (!_spanLists[i].Empty()){Span* nSpan = _spanLists[i].PopFront();Span* kSpan = new Span;kSpan->_pageId = nSpan->_pageId;kSpan->_n = k;nSpan->_pageId += k;nSpan->_n -= k;_spanLists[nSpan->_n].PushFront(nSpan);return kSpan;}}//向系統申請內存Span* span = new Span;void* ptr = SystemAlloc(NPAGES - 1);span->_pageId = (PANGE_ID)ptr>>PAGE_SHIFT;span->_n = NPAGES - 1;_spanLists[span->_n].PushFront(span);return NewSpan(k);
}

3.加鎖保護

????????PageCache哈希桶是臨界資源,需要對它進行加鎖保護。上文已經講過只用加一個大鎖,而不是用桶鎖。

? ? ? ? 因為NewSpan函數涉及遞歸,需要使用遞歸鎖,這樣比較高效。但這里也可以把鎖加到NewSpan外面,也就是GetOneSpan函數里,如下:

? ? ? ? 其次有一個優化的點:線程進入PageCache這一層之前是先進入了CentralCache的,并在這一層加了桶鎖,那么此時CentralCache的哈希桶它暫時不用,但可能其他線程要用,可以先把鎖釋放掉,等從PageCache申請完內存后再去申請鎖。

? ? ? ? 雖然說線程走到PageCache這一層說明CentralCache的哈希桶已經沒有內存了,其他線程來了也申請不到內存,但別忘了還有內存的釋放呢。把桶鎖釋放了,其他線程釋放內存對象回來,就不會阻塞。如下:

六、源碼

代碼量比較大,就不放在這里了,需要的小伙伴到我的gitee上取:

PageCache/PageCache · 敲上癮/ConcurrentMemoryPool - 碼云 - 開源中國

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/75271.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/75271.shtml
英文地址,請注明出處:http://en.pswp.cn/web/75271.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

機器學習 | 強化學習方法分類匯總 | 概念向

文章目錄 ??Model-Free RL vs Model-Based RL??核心定義??核心區別??Policy-Based RL vs Value-Based RL??核心定義?? 核心區別??Monte-Carlo update vs Temporal-Difference update??核心定義??核心區別??On-Policy vs Off-Policy??核心定義??核心區別…

GSO-YOLO:基于全局穩定性優化的建筑工地目標檢測算法解析

論文地址:https://arxiv.org/pdf/2407.00906 1. 論文概述 《GSO-YOLO: Global Stability Optimization YOLO for Construction Site Detection》提出了一種針對建筑工地復雜場景優化的目標檢測模型。通過融合全局優化模塊(GOM)?、穩定捕捉模塊(SCM)?和創新的AIoU損失函…

Java學習手冊:JVM、JRE和JDK的關系

在Java生態系統中&#xff0c;JVM&#xff08;Java虛擬機&#xff09;、JRE&#xff08;Java運行時環境&#xff09;和JDK&#xff08;Java開發工具包&#xff09;是三個核心概念。它們共同構成了Java語言運行和開發的基礎。理解它們之間的關系對于Java開發者來說至關重要。本文…

lanqiaoOJ 2489 進制

//x的初始值一定要設置為0,否則測試的答案是對的,但是通不過去 #include<bits/stdc.h> using namespace std; const int N50; int a[N]; using lllong long; int main(){ ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); string s"2021ABCD"; for(int i…

Python基礎知識點(類和對象)

""" 編程思維---解決問題的方式方法 面向過程---C語言 面向對象---C java python python中封裝類的語法 class 類名&#xff08;父類&#xff09; 類體 注意&#xff1a; 1.類名--約定 大駝峰法 首字母要大寫 2.父類如果有的話就寫&#xff0c;沒有的話…

記錄一下學習docker的命令(不斷補充中)

#2025-04-10,22:12############### 在wsl2中安裝了ubuntu24.04.1后有部署了docker&#xff0c; 如果沒有啟動docker可以通過下列命令啟動docker&#xff1a; sudo systemctl start docker 執行下列命令可以看到docker狀態&#xff0c;并不占用控制臺的命令&#xff1a; su…

【01BFS】# P4667 [BalticOI 2011] Switch the Lamp On 電路維修 (Day1)|普及+

本文涉及知識點 CBFS算法 題目描述 Casper is designing an electronic circuit on a N M N \times M NM rectangular grid plate. There are N M N \times M NM square tiles that are aligned to the grid on the plate. Two (out of four) opposite corners of each …

參考平面跨分割情況下的信號回流

前言&#xff1a;弄清楚信號的回流路徑&#xff0c;是學習EMC和高速的第一步&#xff01; 如果我們不管信號的回流路徑&#xff0c;會造成什么后果&#xff1f;1、信號完整性問題&#xff0c;信號的回流路徑不連續會導致信號反射、衰減和失真。2、信號衰減和噪聲干擾&#xff…

almalinux 8 9 升級到指定版本

almalinux 8 update 指定版本 almalinux歷史版 所有版本almalinux最新版 所有版本vault歷史版 almalinux最新版 (https://repo.almalinux.org )地址后面增加不同名稱 echo "delete repos" rm -rf /etc/yum.repos.d/*echo "new almalinux repo" cat <&…

阿里云CDN應對DDoS攻擊策略

阿里云CDN遭遇DDoS攻擊時&#xff0c;可通過以下綜合措施進行應對&#xff0c;保障服務的穩定性和可用性&#xff1a; 1. 啟用阿里云DDoS防護服務 阿里云提供專業的DDoS防護服務&#xff0c;通過流量清洗中心過濾惡意流量&#xff0c;確保合法請求正常傳輸。該服務支持按需選…

CentOS Stream release 9安裝 MySQL(一)

在 CentOS Stream 上安裝 MySQL 的方法與傳統的 CentOS 類似&#xff0c;但由于 CentOS Stream 的軟件包更新策略不同&#xff0c;可能會遇到一些依賴問題。以下是詳細安裝步驟&#xff1a; 1. 添加 MySQL 官方 Yum 倉庫 sudo rpm -Uvh https://dev.mysql.com/get/mysql80-co…

數據結構 | 證明鏈表環結構是否存在

?個人主頁&#xff1a; 鏈表環結構 0.前言1.環形鏈表&#xff08;基礎&#xff09;2.環形鏈表Ⅱ&#xff08;中等&#xff09;3.證明相遇條件及結論3.1 問題1特殊情況證明3.2 問題1普適性證明 0.前言 在這篇博客中&#xff0c;我們將深入探討鏈表環結構的檢測方法&#xff1a;…

數字世界的免疫系統:惡意流量檢測如何守護網絡安全

在2023年全球網絡安全威脅報告中,某跨國電商平臺每秒攔截的惡意請求峰值達到217萬次,這個數字背后是無數黑客精心設計的自動化攻擊腳本。惡意流量如同數字世界的埃博拉病毒,正在以指數級速度進化,傳統安全防線頻頻失守。這場沒有硝煙的戰爭中,惡意流量檢測技術已成為守護網…

【JavaScript】十八、頁面加載事件和頁面滾動事件

文章目錄 1、頁面加載事件1.1 load1.2 DOMContentLoaded 2、頁面滾動事件2.1 語法2.2 獲取滾動位置 3、案例&#xff1a;頁面滾動顯示隱藏側邊欄 1、頁面加載事件 script標簽在html中的位置一般在</body>標簽上方&#xff0c;這是因為代碼從上往下執行&#xff0c;在htm…

Linux : 內核中的信號捕捉

目錄 一 前言 二 信號捕捉的方法 1.sigaction()?編輯 2. sigaction() 使用 三 可重入函數 四 volatile 關鍵字 一 前言 如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。在Linux: 進程信號初識-CSDN博客 這一篇中已經學習到了一種信號…

分布式id生成算法(雪花算法 VS 步長id生成)

分布式ID生成方案詳解:雪花算法 vs 步長ID 一、核心需求 全局唯一性:集群中絕不重復有序性:有利于數據庫索引性能高可用:每秒至少生成數萬ID低延遲:生成耗時<1ms二、雪花算法(Snowflake) 1. 數據結構(64位) 0 | 0000000000 0000000000 0000000000 0000000000 0 |…

函數式編程在 Java:Function、BiFunction、UnaryOperator 你真的會用?

大家好&#xff0c;我是你們的Java技術博主&#xff01;今天我們要深入探討Java函數式編程中的幾個核心接口&#xff1a;Function、BiFunction和UnaryOperator。很多同學雖然知道它們的存在&#xff0c;但真正用起來卻總是不得要領。這篇文章將帶你徹底掌握它們&#xff01;&am…

x265 編碼器中運動搜索 ME 方法對比實驗

介紹 x265 的運動搜索方法一共有 6 種方法,分別是 DIA、HEX、UMH、STAR、SEA、FULL。typedef enum {X265_DIA_SEARCH,X265_HEX_SEARCH,X265_UMH_SEARCH,X265_STAR_SEARCH,X265_SEA,X265_FULL_SEARCH } X265_ME_METHODS;GitHub

2025.4.8 dmy NOI模擬賽總結(轉化貢獻方式 dp, 交互(分段函數找斷點),SAM上計數)

文章目錄 時間安排題解T1.搬箱子(dp&#xff0c;轉化貢獻方式)T2.很多線。(分段函數找斷點)T3.很多串。(SAM&#xff0c; 計數) 時間安排 先寫了 T 3 T3 T3 60 p t s 60pts 60pts&#xff0c;然后剩下 2.5 h 2.5h 2.5h 沒有戰勝 T 1 T1 T1 40 p t s 40pts 40pts。 總得分…

ZYNQ筆記(四):AXI GPIO

版本&#xff1a;Vivado2020.2&#xff08;Vitis&#xff09; 任務&#xff1a;使用 AXI GPIO IP 核實現按鍵 KEY 控制 LED 亮滅&#xff08;兩個都在PL端&#xff09; 一、介紹 AXI GPIO (Advanced eXtensible Interface General Purpose Input/Output) 是 Xilinx 提供的一個可…