Hongmengカーネルソースコード分析(メモリマッピング)|メモリの最も重要な実装基盤は何ですか?|中国の注釈HarmonyOSソースコード| v12.02
MMUの本質
MMUはMemoryManagement Unitの略で、中国語の名前はメモリ管理ユニットであり、ページメモリ管理ユニットと呼ばれることもあります(英語:ページメモリ管理ユニット、PMMUと略されます)。これは、中央処理装置(CPU)からのメモリアクセス要求の処理を担当する一種のコンピュータハードウェアです。その機能には、仮想アドレスから物理アドレスへの変換(つまり、仮想メモリ管理)、メモリ保護、および中央処理装置のキャッシュの制御が含まれます。比較的単純なコンピューターアーキテクチャでは、バスの調停とバンクの切り替え(特に、 8ビットシステム)。
仮想アドレス(VA):線形アドレスです。HongmengのメモリはすべてVAであり、プログラムを見つけるときにコンパイラとリンカによって割り当てられます。各アプリケーションプログラムは同じ仮想メモリアドレス空間を使用し、これらの仮想メモリはアドレスです。スペースは、実際には異なる実際の物理メモリスペースにマップされます。CPUは仮想アドレスのみを認識し、仮想アドレスからのデータを要求しますが、プロテクトモードでは、アドレス信号が路上でMMUによって傍受されるのは非常に悲しいことです。MMUは仮想アドレスを物理アドレスに置き換えます。実際のデータを取得します。
物理アドレス(PA):プログラム命令と定数データ、グローバル変数データ、および実行時に動的アプリケーションメモリによって割り当てられた実際の物理メモリストレージの場所。
MMUは、ページテーブル(pagetable)を使用して、仮想アドレスと実アドレスの変換を実現します。ページテーブルエントリは、仮想ページから物理ページへの直接変換を説明するだけでなく、ページアクセス許可(読み取り、書き込み、実行可能)およびストレージ属性も提供します。MMUの本質は、仮想アドレスの上位(20ビット)について大騒ぎすることであり、下位12ビットは、ページ内のオフセットアドレスが変更されないことを意味します。つまり、仮想アドレスと物理アドレスの下位12ビットは同じです。この記事では、MMUがどのように呼び出すかについて詳しく説明します。
MMUは、L1とL2の2レベルのページテーブル構造を介してマッピング機能を実装します。もちろん、Hongmengコアは2レベルのページテーブルの変換も実装します。この記事は、メモリ部分に関するシリーズの中で最も満足のいく記事であり、最も理解しにくい記事です。ソースコードと併せて読むことを強くお勧めします。Hongmengカーネルソースコードコメント中国語版[Giteeウェアハウス| CSDNウェアハウス| Githubウェアハウス|コーディングウェアハウス]メモリ部分コメントは基本的に完了しています。
レベル1ページテーブルL1
L1ページテーブルは、4Gアドレス空間全体を4096の1Mセクションに分割します。ページテーブルの各アイテム(ページテーブルエントリ)は32ビットであり、その内容はL2ページテーブルのベースアドレスまたは1M物理のベースアドレスです。メモリ。仮想アドレスの上位12ビットは、ページテーブルエントリ、つまり4096ページエントリのインデックスを見つけるために使用されます。L1ページテーブルのベースアドレスは、変換テーブルのベースアドレスとも呼ばれ、C2(TTB)に格納されます。 )CP15のレジスタ、Hongmengコアソースコード分析(メモリアセンブリの記事)に詳細な説明がありますので、ご自身でお読みください。
L1ページエントリの記述形式は3つあり、Hongmengのソースコードは次のとおりです。
/*L1descriptortype*/
#defineMMU_DESCRIPTOR_L1_TYPE_INVALID(0x0<< 0)
#define MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE (0x1 << 0)
#define MMU_DESCRIPTOR_L1_TYPE_SECTION (0x2 << 0)
#define MMU_DESCRIPTOR_L1_TYPE_MASK (0x3 << 0)
最初のタイプ:障害(INVALID)ページテーブルエントリ。対応する仮想アドレスがマップされておらず、アクセスによってデータアボート例外が生成されることを示します。
2番目のタイプ:PAGE_TABLEページテーブルエントリ。L2ページテーブルのページテーブルエントリを指します。これは、1Mがより多くのページ(256 * 4K)に分割されることを意味します。
3番目のタイプ:1Mセクション
[1:0]のページテーブルエントリの下位2ビットを指すSECTIONページテーブルエントリ。ページテーブルエントリのタイプを定義するために使用され、セクションページテーブルエントリは1Mに対応します。テーブルエントリの上位12ビットは、仮想アドレスの上位12ビットを置き換えて、物理アドレスを取得します。Hongmengのソースコードを直接見ると、各行に詳細なコメントが付いていることがわかります。
arch \ arm \ arm \ src los_arch_mmu.c
LOS_ArchMmuQueryは、仮想アドレスを介して物理アドレスとフラグを照会します
//通过虚拟地址查询物理地址/******************************************************************************
本函数是内核高频函数,通过MMU查询虚拟地址是否映射过,
带走映射的物理地址和权限
******************************************************************************/
STATUS_T LOS_ArchMmuQuery(const LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T *paddr, UINT32 *flags)
{
//archMmu->virtTtb:转换表基地址
PTE_T l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);//获取PTE vaddr右移20位 得到L1描述子地址
PTE_T l2Entry;
PTE_T* l2Base = NULL;
if (OsIsPte1Invalid(l1Entry)) {
//判断L1描述子地址是否有效
return LOS_ERRNO_VM_NOT_FOUND;//无效返回虚拟地址未查询到
} else if (OsIsPte1Section(l1Entry)) {
// section页表项: l1Entry低二位是否为 10
if (paddr != NULL) {
//物理地址 = 节基地址(section页表项的高12位) + 虚拟地址低20位
*paddr = MMU_DESCRIPTOR_L1_SECTION_ADDR(l1Entry) + (vaddr & (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1));
}
if (flags != NULL) {
OsCvtSecAttsToFlags(l1Entry, flags);//获取虚拟内存的flag信息
}
} else if (OsIsPte1PageTable(l1Entry)) {
//PAGE_TABLE页表项: l1Entry低二位是否为 01
l2Base = OsGetPte2BasePtr(l1Entry);//获取L2转换表基地址
if (l2Base == NULL) {
return LOS_ERRNO_VM_NOT_FOUND;
}
l2Entry = OsGetPte2(l2Base, vaddr);//获取L2描述子地址
if (OsIsPte2SmallPage(l2Entry) || OsIsPte2SmallPageXN(l2Entry)) {
if (paddr != NULL) {
//物理地址 = 小页基地址(L2页表项的高20位) + 虚拟地址低12位
*paddr = MMU_DESCRIPTOR_L2_SMALL_PAGE_ADDR(l2Entry) + (vaddr & (MMU_DESCRIPTOR_L2_SMALL_SIZE - 1));
}
if (flags != NULL) {
OsCvtPte2AttsToFlags(l1Entry, l2Entry, flags);//获取虚拟内存的flag信息
}
} else if (OsIsPte2LargePage(l2Entry)) {
//鸿蒙目前暂不支持64K大页,未来手机版应该会支持。
LOS_Panic("%s %d, large page unimplemented\n", __FUNCTION__, __LINE__);
} else {
return LOS_ERRNO_VM_NOT_FOUND;
}
}
return LOS_OK;
}
これは、Hongmengカーネルで最も頻繁に使用される関数であり、物理アドレスとフラグ情報は仮想アドレスを介して取得され、どこで呼び出されるかを確認します。
セカンダリページテーブルL2
L1ページテーブルエントリは1Mのアドレス範囲を表し、L2は1Mをより小さなページに分割し、Hongmengコアページは4Kで計算されるため、256の小さなページに分割されます。
L2ページテーブルには256ページテーブルエントリが含まれ、各エントリは32ビット(4バイト)です。L2ページテーブルには256 * 4 = 1Kのスペースが必要で、1Kで整列する必要があります。各L2ページテーブルエントリには4Kの仮想メモリがあります。アドレスは物理アドレスに変換され、各L2ページエントリは4Kページのベースアドレスを提供します。
L2ページテーブルエントリには、次の3つの形式があります。
/*L2descriptortype*/
#defineMMU_DESCRIPTOR_L2_TYPE_INVALID(0x0<< 0)
#define MMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE (0x1 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE (0x2 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN (0x3 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_MASK (0x3 << 0)
最初のタイプ:障害(INVALID)ページテーブルエントリ。対応する仮想アドレスがマップされておらず、アクセスによってデータアボート例外が生成されることを示します。
2番目のタイプ:64Kページへのポインターを含む大きなページテーブルエントリですが、Hongmengカーネルは大きなページテーブルのサポートを実装しておらず、実装されていないヒントを提供します
if(OsIsPte2LargePage(l2Entry)){
LOS_Panic("%s%d,largepageunimplemented\n",__FUNCTION__,__LINE__);}
マッピングの初期化のプロセス
まず、呼び出すことと呼び出されることの関係を見てください
//启动映射初始化
VOIDOsInitMappingStartUp(VOID){
OsArmInvalidateTlbBarrier();
//使TLB失效
OsSwitchTmpTTB();
//切换到临时
TTBOsSetKSectionAttr();
//设置内核段(text,rodata,bss)映射
OsArchMmuInitPerCPU();
//初始化CPU与mmu相关信息
}
簡単かつきちんと、4つの関数が呼び出され、そのうち3つは拡張せずにHongmengカーネルソースコード(メモリアセンブリ)の分析に関与します。これがOsSetKSectionAttrです。
カーネル空間のさまざまな領域のマッピングを実現します。カーネル自体もプログラムです。Hongmengはカーネル空間を物理メモリから分離します。つまり、カーネル空間によってのみ使用される物理メモリのセクションがあります。カーネルとAPPスペースは分離されており、カーネルの重要なデータ(コード、定数、グローバル変数を含む)が内部に配置されています。コードは非常に長く、関数全体が投稿され、コメントが追加されます。
OsSetKSectionAttrカーネルスペースの設定とマッピング
typedef struct ArchMmuInitMapping {
PADDR_T phys;//物理地址
VADDR_T virt;//虚拟地址
size_t size;//大小
unsigned int flags;//标识 读/写/.. VM_MAP_REGION_FLAG_PERM_*
const char *name;//名称
} LosArchMmuInitMapping;
VADDR_T *OsGFirstTableGet()
{
return (VADDR_T *)g_firstPageTable;//UINT8 g_firstPageTable[MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS]
}
//设置内核空间段属性,可看出内核空间是固定映射到物理地址
STATIC VOID OsSetKSectionAttr(VOID)
{
/* every section should be page aligned */
UINTPTR textStart = (UINTPTR)&__text_start;//代码段开始位置
UINTPTR textEnd = (UINTPTR)&__text_end;//代码段结束位置
UINTPTR rodataStart = (UINTPTR)&__rodata_start;//常量只读段开始位置
UINTPTR rodataEnd = (UINTPTR)&__rodata_end;//常量只读段结束位置
UINTPTR ramDataStart = (UINTPTR)&__ram_data_start;//全局变量段开始位置
UINTPTR bssEnd = (UINTPTR)&__bss_end;//bss结束位置
UINT32 bssEndBoundary = ROUNDUP(bssEnd, MB);
LosArchMmuInitMapping mmuKernelMappings[] = {
{
.phys = SYS_MEM_BASE + textStart - KERNEL_VMM_BASE,//映射物理内存位置
.virt = textStart,//内核代码区
.size = ROUNDUP(textEnd - textStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),//代码区大小
.flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_EXECUTE,//代码段可读,可执行
.name = "kernel_text"
},
{
.phys = SYS_MEM_BASE + rodataStart - KERNEL_VMM_BASE,//映射物理内存位置
.virt = rodataStart,//内核常量区
.size = ROUNDUP(rodataEnd - rodataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),//4K对齐
.flags = VM_MAP_REGION_FLAG_PERM_READ,//常量段只读
.name = "kernel_rodata"
},
{
.phys = SYS_MEM_BASE + ramDataStart - KERNEL_VMM_BASE,//映射物理内存位置
.virt = ramDataStart,
.size = ROUNDUP(bssEndBoundary - ramDataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),
.flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE,//全局变量区可读可写
.name = "kernel_data_bss"
}
};
LosVmSpace *kSpace = LOS_GetKVmSpace();//获取内核空间
status_t status;
UINT32 length;
paddr_t oldTtPhyBase;
int i;
LosArchMmuInitMapping *kernelMap = NULL;//内核映射
UINT32 kmallocLength;
/* use second-level mapping of default READ and WRITE */
kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;//__attribute__((section(".bss.prebss.translation_table"))) UINT8 g_firstPageTable[MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS];
kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);//通过TTB虚拟地址查询TTB物理地址
status = LOS_ArchMmuUnmap(&kSpace->archMmu, KERNEL_VMM_BASE,
(bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT);//解绑 bssEndBoundary - KERNEL_VMM_BASE 映射
if (status != ((bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
//解绑失败
VM_ERR("unmap failed, status: %d", status);
return;
}
//映射 textStart - KERNEL_VMM_BASE 区
status = LOS_ArchMmuMap(&kSpace->archMmu, KERNEL_VMM_BASE, SYS_MEM_BASE,
(textStart - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |
VM_MAP_REGION_FLAG_PERM_EXECUTE);
if (status != ((textStart - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("mmap failed, status: %d", status);
return;
}
length = sizeof(mmuKernelMappings) / sizeof(LosArchMmuInitMapping);
for (i = 0; i < length; i++) {
//对mmuKernelMappings一一映射好
kernelMap = &mmuKernelMappings[i];
status = LOS_ArchMmuMap(&kSpace->archMmu, kernelMap->virt, kernelMap->phys,
kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT, kernelMap->flags);
if (status != (kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("mmap failed, status: %d", status);
return;
}
LOS_VmSpaceReserve(kSpace, kernelMap->size, kernelMap->virt);//保留区
}
//将剩余空间映射好
kmallocLength = KERNEL_VMM_BASE + SYS_MEM_SIZE_DEFAULT - bssEndBoundary;
status = LOS_ArchMmuMap(&kSpace->archMmu, bssEndBoundary,
SYS_MEM_BASE + bssEndBoundary - KERNEL_VMM_BASE,
kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE);
if (status != (kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
VM_ERR("unmap failed, status: %d", status);
return;
}
LOS_VmSpaceReserve(kSpace, kmallocLength, bssEndBoundary);
/* we need free tmp ttbase */
oldTtPhyBase = OsArmReadTtbr0();//读取TTB值
oldTtPhyBase = oldTtPhyBase & MMU_DESCRIPTOR_L2_SMALL_FRAME;
OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);//内核页表基地址写入CP15 c2(TTB寄存器)
ISB;
/* we changed page table entry, so we need to clean TLB here */
OsCleanTLB();//清空TLB缓冲区
(VOID)LOS_MemFree(m_aucSysMem0, (VOID *)(UINTPTR)(oldTtPhyBase - SYS_MEM_BASE + KERNEL_VMM_BASE));//释放内存池
}
LOS_ArchMmuMapは、L1およびL2ページテーブルエントリと、マッピングを実現するプロセス
を生成します。Mmuのマップは、仮想アドレスと実アドレスを変換するためのL1およびL2ページテーブルエントリを生成するプロセスです。コードを直接見てみましょう。コードはすべてを説明しています。 !!
//所谓的 map 就是 生成L1,L2页表项的过程
status_t LOS_ArchMmuMap(LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T paddr, size_t count, UINT32 flags)
{
PTE_T l1Entry;
UINT32 saveCounts = 0;
INT32 mapped = 0;
INT32 checkRst;
checkRst = OsMapParamCheck(flags, vaddr, paddr);//检查参数
if (checkRst < 0) {
return checkRst;
}
/* see what kind of mapping we can use */
while (count > 0) {
if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(vaddr) && //虚拟地址和物理地址对齐 0x100000(1M)时采用
MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(paddr) && //section页表项格式
count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) {
//MMU_DESCRIPTOR_L2_NUMBERS_PER_L1 = 0x100
/* compute the arch flags for L1 sections cache, r ,w ,x, domain and type */
saveCounts = OsMapSection(archMmu, flags, &vaddr, &paddr, &count);//生成L1 section类型页表项并保存
} else {
/* have to use a L2 mapping, we only allocate 4KB for L1, support 0 ~ 1GB */
l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);//获取L1页面项
if (OsIsPte1Invalid(l1Entry)) {
//L1 fault页面项类型
OsMapL1PTE(archMmu, &l1Entry, vaddr, flags);//生成L1 page table类型页表项并保存
saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);//生成L2 页表项目并保存
} else if (OsIsPte1PageTable(l1Entry)) {
//L1 page table页面项类型
saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);//生成L2 页表项目并保存
} else {
LOS_Panic("%s %d, unimplemented tt_entry %x\n", __FUNCTION__, __LINE__, l1Entry);
}
}
mapped += saveCounts;
}
return mapped;
}
STATIC UINT32 OsMapL2PageContinous(PTE_T pte1, UINT32 flags, VADDR_T *vaddr, PADDR_T *paddr, UINT32 *count)
{
PTE_T *pte2BasePtr = NULL;
UINT32 archFlags;
UINT32 saveCounts;
pte2BasePtr = OsGetPte2BasePtr(pte1);
if (pte2BasePtr == NULL) {
LOS_Panic("%s %d, pte1 %#x error\n", __FUNCTION__, __LINE__, pte1);
}
/* compute the arch flags for L2 4K pages */
archFlags = OsCvtPte2FlagsToAttrs(flags);
saveCounts = OsSavePte2Continuous(pte2BasePtr, OsGetPte2Index(*vaddr), *paddr | archFlags, *count);
*paddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
*vaddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
*count -= saveCounts;
return saveCounts;
}
OsMapL2PageContinousにはコメントがありません。怠惰になりすぎず、すばやく移動してください。ここで理解できるはずです。Hongmengカーネルソースコード分析(メモリアセンブリ)を組み合わせて、より深く理解することが最善です。