文章目录

  • 17.1 APIC设备模拟
    • apicR3Construct
    • apicR3InitState
    • apicRZConstruct:
    • apicR3Reset
    • apicR3Destruct
  • 17.2 设置APIC Base MSR
  • 17.3 APIC Page访问的相关函数
    • apicReadMmio
    • apicWriteMmio
    • apicSetTprEx
    • apicSetLvtEntry
    • apicSetIcrHi/apicSetIcrLo
    • apicSetSvr :
    • apicSetEsr
    • apicSetTimerDcr
    • apicSetTimerIcr
    • apicSetEoi
    • apicSetLdr
    • apicSetDfr
    • APICGetTpr
  • 17.4 APIC Timer相关的函数
    • apicStartTimer
    • apicStopTimer
    • apicHintTimerFreq
    • APICGetTimerFreq
    • apicR3TimerCallback

17.1 APIC设备模拟

VirtualBox里,把APIC作为一个R0的PNP设备来模拟:

const PDMDEVREG g_DeviceAPIC =
{/* .u32Version = */             PDM_DEVREG_VERSION,/* .uReserved0 = */             0,/* .szName = */                 "apic",/* .fFlags = */                 PDM_DEVREG_FLAGS_DEFAULT_BITS | PDM_DEVREG_FLAGS_RZ | PDM_DEVREG_FLAGS_NEW_STYLE| PDM_DEVREG_FLAGS_REQUIRE_R0 | PDM_DEVREG_FLAGS_REQUIRE_RC,/* .fClass = */                 PDM_DEVREG_CLASS_PIC,/* .cMaxInstances = */          1,/* .uSharedVersion = */         42,/* .cbInstanceShared = */       sizeof(APICDEV),/* .cbInstanceCC = */           0,/* .cbInstanceRC = */           0,/* .cMaxPciDevices = */         0,/* .cMaxMsixVectors = */        0,/* .pszDescription = */         "Advanced Programmable Interrupt Controller",
#if defined(IN_RING3)/* .szRCMod = */                "VMMRC.rc",/* .szR0Mod = */                "VMMR0.r0",/* .pfnConstruct = */           apicR3Construct,/* .pfnDestruct = */            apicR3Destruct,/* .pfnRelocate = */            apicR3Relocate,/* .pfnMemSetup = */            NULL,/* .pfnPowerOn = */             NULL,/* .pfnReset = */               apicR3Reset,/* .pfnSuspend = */             NULL,/* .pfnResume = */              NULL,/* .pfnAttach = */              NULL,/* .pfnDetach = */              NULL,/* .pfnQueryInterface = */      NULL,/* .pfnInitComplete = */        apicR3InitComplete,/* .pfnPowerOff = */            NULL,....
#elif defined(IN_RING0)/* .pfnEarlyConstruct = */      NULL,/* .pfnConstruct = */           apicRZConstruct,/* .pfnDestruct = */            NULL,/* .pfnFinalDestruct = */       NULL,/* .pfnRequest = */             NULL,...
#elif defined(IN_RC)/* .pfnConstruct = */           apicRZConstruct,...
#else
# error "Not in IN_RING3, IN_RING0 or IN_RC!"
#endif/* .u32VersionEnd = */          PDM_DEVREG_VERSION
};

并且在PDMR0Init初始化的时候加入到g_PDMDevModList里,PDMR0DeviceCreateReqHandler函数会遍历g_PDMDevModList,create APIC devices。(虚拟机设备的创建过程在PDM一章里说明)

static const PDMDEVREGR0 *g_apVMM0DevRegs[] =
{&g_DeviceAPIC,
};/*** Module device registration record for VMMR0.*/
static PDMDEVMODREGR0 g_VBoxDDR0ModDevReg =
{/* .u32Version = */ PDM_DEVMODREGR0_VERSION,/* .cDevRegs = */   RT_ELEMENTS(g_apVMM0DevRegs),/* .papDevRegs = */ &g_apVMM0DevRegs[0],/* .hMod = */       NULL,/* .ListEntry = */  { NULL, NULL },
};VMMR0_INT_DECL(void) PDMR0Init(void *hMod)
{RTListInit(&g_PDMDevModList);g_VBoxDDR0ModDevReg.hMod = hMod;RTListAppend(&g_PDMDevModList, &g_VBoxDDR0ModDevReg.ListEntry);
}

实现代码在VMM\VMMAlll\APICAll.cpp,VMM\VMMR3\APIC.cpp中

apicR3Construct

R3的初始化函数

DECLCALLBACK(int) apicR3Construct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg)
{//读取配置,是否支持IOAPICint rc = pHlp->pfnCFGMQueryBoolDef(pCfg, "IOAPIC", &pApic->fIoApicPresent, true);//获取MAX APIC模式uint8_t uMaxMode;rc = pHlp->pfnCFGMQueryU8Def(pCfg, "Mode", &uMaxMode, PDMAPICMODE_APIC);switch ((PDMAPICMODE)uMaxMode){case PDMAPICMODE_NONE:case PDMAPICMODE_APIC:case PDMAPICMODE_X2APIC:break;default:return VMR3SetError(pVM->pUVM, VERR_INVALID_PARAMETER, RT_SRC_POS, "APIC mode %d unknown.", uMaxMode);}pApic->enmMaxMode = (PDMAPICMODE)uMaxMode;//向PNP设备管理器里注册APIC设备rc = PDMDevHlpApicRegister(pDevIns);//如果支持x2APIC,加入x2APIC对应的MSR寄存器到MSRRange数组里if (pApic->enmMaxMode == PDMAPICMODE_X2APIC){rc = CPUMR3MsrRangesInsert(pVM, &g_MsrRange_x2Apic);AssertLogRelRCReturn(rc, rc);}else{//不支持x2APIC,加入一个会产生GP的handlerc = CPUMR3MsrRangesInsert(pVM, &g_MsrRange_x2Apic_Invalid);AssertLogRelRCReturn(rc, rc);}apicR3SetCpuIdFeatureLevel(pVM, pApic->enmMaxMode);//初始化APIC的相关数据rc = apicR3InitState(pVM);//注册MMIO范围,GCPhysApicBase开头的XAPICPAGE结构体大小都是MMIO地址//APIC内存的读写调用到apicWriteMmio/apicReadMmio这两个函数PAPICCPU pApicCpu0 = VMCPU_TO_APICCPU(pVM->apCpusR3[0]);RTGCPHYS GCPhysApicBase = MSR_IA32_APICBASE_GET_ADDR(pApicCpu0->uApicBaseMsr);rc = PDMDevHlpMmioCreateAndMap(pDevIns, GCPhysApicBase, sizeof(XAPICPAGE), apicWriteMmio, apicReadMmio,IOMMMIO_FLAGS_READ_DWORD | IOMMMIO_FLAGS_WRITE_DWORD_ZEROED, "APIC", &pApicDev->hMmio);//给每个VCPU 创建APIC timer for (VMCPUID idCpu = 0; idCpu < pVM->cCpus; idCpu++){PVMCPU   pVCpu    = pVM->apCpusR3[idCpu];PAPICCPU pApicCpu = VMCPU_TO_APICCPU(pVCpu);rc = PDMDevHlpTimerCreate(pDevIns, TMCLOCK_VIRTUAL_SYNC, apicR3TimerCallback, pVCpu, TMTIMER_FLAGS_NO_CRIT_SECT,pApicCpu->szTimerDesc, &pApicCpu->hTimer);}//注册SSM的callbackrc = PDMDevHlpSSMRegister(pDevIns, APIC_SAVED_STATE_VERSION, sizeof(*pApicDev), apicR3SaveExec, apicR3LoadExec);
}

apicR3InitState

//APIC Pending-Interrupt Bitmap (PIB). 里面保存了所有pending的中断
//pending的中断有两种类型,一种是edge trigglemode,一种是level trigglemode
typedef struct APICPIB
{uint64_t volatile au64VectorBitmap[4];uint32_t volatile fOutstandingNotification;uint8_t           au8Reserved[APIC_CACHE_LINE_SIZE - sizeof(uint32_t) - (sizeof(uint64_t) * 4)];
} APICPIB;
static int apicR3InitState(PVM pVM)
{//分配保存edge trigglemode用的内存,这个内存同时映射到R0和R3//计算需要多少个页面,每个VCPU都有自己的PIBpApic->cbApicPib    = RT_ALIGN_Z(pVM->cCpus * sizeof(APICPIB), PAGE_SIZE);size_t const cPages = pApic->cbApicPib >> PAGE_SHIFT;if (cPages == 1){SUPPAGE SupApicPib;RT_ZERO(SupApicPib);SupApicPib.Phys = NIL_RTHCPHYS;//分配1个page大小的页面int rc = SUPR3PageAllocEx(1 /* cPages */, 0 /* fFlags */, &pApic->pvApicPibR3, &pApic->pvApicPibR0, &SupApicPib);if (RT_SUCCESS(rc)){pApic->HCPhysApicPib = SupApicPib.Phys;}}else//分配物理地址连续的cPage大小页面pApic->pvApicPibR3 = SUPR3ContAlloc(cPages, &pApic->pvApicPibR0, &pApic->HCPhysApicPib);if (pApic->pvApicPibR3){RT_BZERO(pApic->pvApicPibR3, pApic->cbApicPib);for (VMCPUID idCpu = 0; idCpu < pVM->cCpus; idCpu++){//给每个VCPU分配申请一个Virtual APIC pageSUPPAGE SupApicPage;RT_ZERO(SupApicPage);SupApicPage.Phys = NIL_RTHCPHYS;pApicCpu->cbApicPage = sizeof(XAPICPAGE);int rc = SUPR3PageAllocEx(1 /* cPages */, 0 /* fFlags */, &pApicCpu->pvApicPageR3, &pApicCpu->pvApicPageR0, &SupApicPage);if (RT_SUCCESS(rc)){pApicCpu->HCPhysApicPage = SupApicPage.Phys;//根据CPUID获取自己的PIB内存地址uint32_t const offApicPib  = idCpu * sizeof(APICPIB);pApicCpu->pvApicPibR0      = (RTR0PTR)((RTR0UINTPTR)pApic->pvApicPibR0 + offApicPib);pApicCpu->pvApicPibR3      = (RTR3PTR)((RTR3UINTPTR)pApic->pvApicPibR3 + offApicPib);//初始化APICRT_BZERO(pApicCpu->pvApicPageR3, pApicCpu->cbApicPage);apicResetCpu(pVCpu, true /* fResetApicBaseMsr */);}}}
}

apicRZConstruct:

R0的设备初始化函数

static DECLCALLBACK(int) apicRZConstruct(PPDMDEVINS pDevIns)
{PAPICDEV pThis = PDMDEVINS_2_DATA(pDevIns, PAPICDEV);PVMCC    pVM   = PDMDevHlpGetVM(pDevIns);pVM->apicr0.s.pDevInsR0 = pDevIns;int rc = PDMDevHlpSetDeviceCritSect(pDevIns, PDMDevHlpCritSectGetNop(pDevIns));//设置APIC设备rc = PDMDevHlpApicSetUpContext(pDevIns);//设置MMIO内存读写的callback函数rc = PDMDevHlpMmioSetUpContext(pDevIns, pThis->hMmio, apicWriteMmio, apicReadMmio, NULL /*pvUser*/);return VINF_SUCCESS;
}static DECLCALLBACK(int) pdmR0DevHlp_ApicSetUpContext(PPDMDEVINS pDevIns)
{pGVM->pdm.s.Apic.pDevInsR0 = pDevIns;return VINF_SUCCESS;
}static DECLCALLBACK(int) pdmR0DevHlp_MmioSetUpContextEx(PPDMDEVINS pDevIns, IOMMMIOHANDLE hRegion, PFNIOMMMIONEWWRITE pfnWrite,PFNIOMMMIONEWREAD pfnRead, PFNIOMMMIONEWFILL pfnFill, void *pvUser)
{PGVM pGVM = pDevIns->Internal.s.pGVM;//调用IOM里的函数设置mmio内存对应的read/write的函数int rc = IOMR0MmioSetUpContext(pGVM, pDevIns, hRegion, pfnWrite, pfnRead, pfnFill, pvUser);return rc;
}

apicR3Reset

R3的重置函数

DECLCALLBACK(void) apicR3Reset(PPDMDEVINS pDevIns)
{for (VMCPUID idCpu = 0; idCpu < pVM->cCpus; idCpu++){PVMCPU   pVCpuDest = pVM->apCpusR3[idCpu];PAPICCPU pApicCpu  = VMCPU_TO_APICCPU(pVCpuDest);//如果当前CPU开启了APIC timer,先停掉timerif (PDMDevHlpTimerIsActive(pDevIns, pApicCpu->hTimer))PDMDevHlpTimerStop(pDevIns, pApicCpu->hTimer);apicResetCpu(pVCpuDest, true /* fResetApicBaseMsr */);//clear APIC相关的中断信息apicClearInterruptFF(pVCpuDest, PDMAPICIRQ_HARDWARE);}
}//重新初始化每个APIC CPU
void apicResetCpu(PVMCPUCC pVCpu, bool fResetApicBaseMsr)
{//初始化IpiapicInitIpi(pVCpu);PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);pXApicPage->version.u.u8MaxLvtEntry = XAPIC_MAX_LVT_ENTRIES_P4 - 1;pXApicPage->version.u.u8Version     = XAPIC_HARDWARE_VERSION_P4;if (fResetApicBaseMsr)apicResetBaseMsr(pVCpu);pXApicPage->id.u8ApicId = pVCpu->idCpu;
}
//重置APICBase MSR的值,其实就是保存到ApicCPU的全局变量里
static void apicResetBaseMsr(PVMCPUCC pVCpu)
{PAPICCPU pApicCpu     = VMCPU_TO_APICCPU(pVCpu);PAPIC    pApic        = VM_TO_APIC(pVCpu->CTX_SUFF(pVM));//msrbase设置成默认值(fee00000)uint64_t uApicBaseMsr = MSR_IA32_APICBASE_ADDR;//0号CPU设置成启动的核: BSP: the bootstrap processorif (pVCpu->idCpu == 0)uApicBaseMsr |= MSR_IA32_APICBASE_BSP;//非APICMODE_NONE,表示开启了LAPICif (pApic->enmMaxMode != PDMAPICMODE_NONE){//设置APICBASE enableuApicBaseMsr |= MSR_IA32_APICBASE_EN;//设置CPUMCPUMSetGuestCpuIdPerCpuApicFeature(pVCpu, true /*fVisible*/);}//设置ApicBase到ApicCpu中ASMAtomicWriteU64(&pApicCpu->uApicBaseMsr, uApicBaseMsr);
}

apicR3Destruct

//释放掉apicR3InitState里申请的内存
static void apicR3TermState(PVM pVM)
{//释放PIB内存if (pApic->pvApicPibR3 != NIL_RTR3PTR){size_t const cPages = pApic->cbApicPib >> PAGE_SHIFT;if (cPages == 1)SUPR3PageFreeEx(pApic->pvApicPibR3, cPages);elseSUPR3ContFree(pApic->pvApicPibR3, cPages);pApic->pvApicPibR3 = NIL_RTR3PTR;pApic->pvApicPibR0 = NIL_RTR0PTR;}//释放Virtual APIC pagefor (VMCPUID idCpu = 0; idCpu < pVM->cCpus; idCpu++){PVMCPU   pVCpu    = pVM->apCpusR3[idCpu];PAPICCPU pApicCpu = VMCPU_TO_APICCPU(pVCpu);pApicCpu->pvApicPibR3 = NIL_RTR3PTR;pApicCpu->pvApicPibR0 = NIL_RTR0PTR;if (pApicCpu->pvApicPageR3 != NIL_RTR3PTR){SUPR3PageFreeEx(pApicCpu->pvApicPageR3, 1 /* cPages */);pApicCpu->pvApicPageR3 = NIL_RTR3PTR;pApicCpu->pvApicPageR0 = NIL_RTR0PTR;}}
}

17.2 设置APIC Base MSR

VMM_INT_DECL(int) APICSetBaseMsr(PVMCPUCC pVCpu, uint64_t u64BaseMsr)
{APICMODE enmOldMode = apicGetMode(pApicCpu->uApicBaseMsr);APICMODE enmNewMode = apicGetMode(u64BaseMsr);uint64_t uBaseMsr   = pApicCpu->uApicBaseMsr;//如果修改了APIC模式if (enmNewMode != enmOldMode){switch (enmNewMode){//关闭APICcase APICMODE_DISABLED:{//重置APIC CPU信息apicResetCpu(pVCpu, false /* fResetApicBaseMsr */);uBaseMsr &= ~(MSR_IA32_APICBASE_EN | MSR_IA32_APICBASE_EXTD);//通知CPUM APIC已被关闭CPUMSetGuestCpuIdPerCpuApicFeature(pVCpu, false /*fVisible*/);break;}//切换成xAPICcase APICMODE_XAPIC:{//只能从disable模式切换到xAPIC模式if (enmOldMode != APICMODE_DISABLED){return apicMsrAccessError(pVCpu, MSR_IA32_APICBASE, APICMSRACCESS_WRITE_INVALID);}uBaseMsr |= MSR_IA32_APICBASE_EN;//设置开启APICCPUMSetGuestCpuIdPerCpuApicFeature(pVCpu, true /*fVisible*/);break;}case APICMODE_X2APIC:{//配置不支持x2APIC,返回错误if (pApic->enmMaxMode != PDMAPICMODE_X2APIC){return apicMsrAccessError(pVCpu, MSR_IA32_APICBASE, APICMSRACCESS_WRITE_INVALID);}//只能从xAPIC模式切换到x2APIC模式if (enmOldMode != APICMODE_XAPIC){return apicMsrAccessError(pVCpu, MSR_IA32_APICBASE, APICMSRACCESS_WRITE_INVALID);}uBaseMsr |= MSR_IA32_APICBASE_EN | MSR_IA32_APICBASE_EXTD;//u32ApicId设置成当前VCPUID,x2APIC不支持软件设置APIC IDPX2APICPAGE pX2ApicPage = VMCPU_TO_X2APICPAGE(pVCpu);ASMMemZero32(&pX2ApicPage->id, sizeof(pX2ApicPage->id));pX2ApicPage->id.u32ApicId = pVCpu->idCpu;//LDR initialization occurs when entering x2APIC mode.pX2ApicPage->ldr.u32LogicalApicId = ((pX2ApicPage->id.u32ApicId & UINT32_C(0xffff0)) << 16)| (UINT32_C(1) << pX2ApicPage->id.u32ApicId & UINT32_C(0xf));break;}}}
}

17.3 APIC Page访问的相关函数

apicReadMmio

DECLCALLBACK(VBOXSTRICTRC) apicReadMmio(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off, void *pv, unsigned cb)
{VBOXSTRICTRC rc = VBOXSTRICTRC_VAL(apicReadRegister(pDevIns, pVCpu, offReg, &uValue));
}
//大部分APIC寄存器直接读取
DECLINLINE(VBOXSTRICTRC) apicReadRegister(PPDMDEVINS pDevIns, PVMCPUCC pVCpu, uint16_t offReg, uint32_t *puValue)
{PXAPICPAGE   pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);uint32_t     uValue = 0;VBOXSTRICTRC rc = VINF_SUCCESS;switch (offReg){case XAPIC_OFF_ID:case XAPIC_OFF_VERSION:case XAPIC_OFF_TPR:case XAPIC_OFF_EOI:case XAPIC_OFF_RRD:case XAPIC_OFF_LDR:case XAPIC_OFF_DFR:case XAPIC_OFF_SVR:case XAPIC_OFF_ISR0:    case XAPIC_OFF_ISR1:    case XAPIC_OFF_ISR2:    case XAPIC_OFF_ISR3:case XAPIC_OFF_ISR4:    case XAPIC_OFF_ISR5:    case XAPIC_OFF_ISR6:    case XAPIC_OFF_ISR7:case XAPIC_OFF_TMR0:    case XAPIC_OFF_TMR1:    case XAPIC_OFF_TMR2:    case XAPIC_OFF_TMR3:case XAPIC_OFF_TMR4:    case XAPIC_OFF_TMR5:    case XAPIC_OFF_TMR6:    case XAPIC_OFF_TMR7:case XAPIC_OFF_IRR0:    case XAPIC_OFF_IRR1:    case XAPIC_OFF_IRR2:    case XAPIC_OFF_IRR3:case XAPIC_OFF_IRR4:    case XAPIC_OFF_IRR5:    case XAPIC_OFF_IRR6:    case XAPIC_OFF_IRR7:case XAPIC_OFF_ESR:case XAPIC_OFF_ICR_LO:case XAPIC_OFF_ICR_HI:case XAPIC_OFF_LVT_TIMER:
#if XAPIC_HARDWARE_VERSION == XAPIC_HARDWARE_VERSION_P4case XAPIC_OFF_LVT_THERMAL:
#endifcase XAPIC_OFF_LVT_PERF:case XAPIC_OFF_LVT_LINT0:case XAPIC_OFF_LVT_LINT1:case XAPIC_OFF_LVT_ERROR:case XAPIC_OFF_TIMER_ICR:case XAPIC_OFF_TIMER_DCR:{//直接读取Virtual APIC page里的值uValue = apicReadRaw32(pXApicPage, offReg);break;}case XAPIC_OFF_PPR:{//获取当前进程的优先级uValue = apicGetPpr(pVCpu);break;}case XAPIC_OFF_TIMER_CCR:{//获取APIC时间计时器rc = apicGetTimerCcr(pDevIns, pVCpu, VINF_IOM_R3_MMIO_READ, &uValue);break;}case XAPIC_OFF_APR:{#if XAPIC_HARDWARE_VERSION == XAPIC_HARDWARE_VERSION_P4/* Unsupported on Pentium 4 and Xeon CPUs, invalid in x2APIC mode. */Assert(!XAPIC_IN_X2APIC_MODE(pVCpu));
#else
# error "Implement Pentium and P6 family APIC architectures"
#endifbreak;}default:{//设置错误标记rc = PDMDevHlpDBGFStop(pDevIns, RT_SRC_POS, "VCPU[%u]: offReg=%#RX16\n", pVCpu->idCpu, offReg);apicSetError(pVCpu, XAPIC_ESR_ILLEGAL_REG_ADDRESS);break;}}*puValue = uValue;return rc;
}

apicWriteMmio

APIC寄存器的写操作就要复杂很多,很多项都需要特殊处理

DECLINLINE(VBOXSTRICTRC) apicWriteRegister(PPDMDEVINS pDevIns, PVMCPUCC pVCpu, uint16_t offReg, uint32_t uValue)
{VMCPU_ASSERT_EMT(pVCpu);Assert(offReg <= XAPIC_OFF_MAX_VALID);Assert(!XAPIC_IN_X2APIC_MODE(pVCpu));VBOXSTRICTRC rcStrict = VINF_SUCCESS;switch (offReg){case XAPIC_OFF_TPR:{rcStrict = apicSetTprEx(pVCpu, uValue, false /* fForceX2ApicBehaviour */);break;}case XAPIC_OFF_LVT_TIMER:
#if XAPIC_HARDWARE_VERSION == XAPIC_HARDWARE_VERSION_P4case XAPIC_OFF_LVT_THERMAL:
#endifcase XAPIC_OFF_LVT_PERF:case XAPIC_OFF_LVT_LINT0:case XAPIC_OFF_LVT_LINT1:case XAPIC_OFF_LVT_ERROR:{rcStrict = apicSetLvtEntry(pVCpu, offReg, uValue);break;}case XAPIC_OFF_TIMER_ICR:{rcStrict = apicSetTimerIcr(pDevIns, pVCpu, VINF_IOM_R3_MMIO_WRITE, uValue);break;}case XAPIC_OFF_EOI:{rcStrict = apicSetEoi(pVCpu, uValue, VINF_IOM_R3_MMIO_WRITE, false /* fForceX2ApicBehaviour */);break;}case XAPIC_OFF_LDR:{rcStrict = apicSetLdr(pVCpu, uValue);break;}case XAPIC_OFF_DFR:{rcStrict = apicSetDfr(pVCpu, uValue);break;}case XAPIC_OFF_SVR:{rcStrict = apicSetSvr(pVCpu, uValue);break;}case XAPIC_OFF_ICR_LO:{rcStrict = apicSetIcrLo(pVCpu, uValue, VINF_IOM_R3_MMIO_WRITE, true /* fUpdateStat */);break;}case XAPIC_OFF_ICR_HI:{rcStrict = apicSetIcrHi(pVCpu, uValue);break;}case XAPIC_OFF_TIMER_DCR:{rcStrict = apicSetTimerDcr(pVCpu, uValue);break;}case XAPIC_OFF_ESR:{rcStrict = apicSetEsr(pVCpu, uValue);break;}case XAPIC_OFF_APR:case XAPIC_OFF_RRD:{//暂时的不支持这两个寄存器写入
#if XAPIC_HARDWARE_VERSION == XAPIC_HARDWARE_VERSION_P4
#else
# error "Implement Pentium and P6 family APIC architectures"
#endifbreak;}/* Read-only, write ignored: */case XAPIC_OFF_VERSION:case XAPIC_OFF_ID:break;/* Unavailable/reserved in xAPIC mode: */case X2APIC_OFF_SELF_IPI:/* Read-only registers: */case XAPIC_OFF_PPR:case XAPIC_OFF_ISR0:    case XAPIC_OFF_ISR1:    case XAPIC_OFF_ISR2:    case XAPIC_OFF_ISR3:case XAPIC_OFF_ISR4:    case XAPIC_OFF_ISR5:    case XAPIC_OFF_ISR6:    case XAPIC_OFF_ISR7:case XAPIC_OFF_TMR0:    case XAPIC_OFF_TMR1:    case XAPIC_OFF_TMR2:    case XAPIC_OFF_TMR3:case XAPIC_OFF_TMR4:    case XAPIC_OFF_TMR5:    case XAPIC_OFF_TMR6:    case XAPIC_OFF_TMR7:case XAPIC_OFF_IRR0:    case XAPIC_OFF_IRR1:    case XAPIC_OFF_IRR2:    case XAPIC_OFF_IRR3:case XAPIC_OFF_IRR4:    case XAPIC_OFF_IRR5:    case XAPIC_OFF_IRR6:    case XAPIC_OFF_IRR7:case XAPIC_OFF_TIMER_CCR:default:{//只读的寄存器读取需要设置异常标记rcStrict = PDMDevHlpDBGFStop(pDevIns, RT_SRC_POS, "APIC%u: offReg=%#RX16\n", pVCpu->idCpu, offReg);apicSetError(pVCpu, XAPIC_ESR_ILLEGAL_REG_ADDRESS);break;}}return rcStrict;
}

下面一个一个看所有的set函数

apicSetTprEx

static int apicSetTprEx(PVMCPUCC pVCpu, uint32_t uTpr, bool fForceX2ApicBehaviour)
{bool const fX2ApicMode = XAPIC_IN_X2APIC_MODE(pVCpu) || fForceX2ApicBehaviour;if (   fX2ApicMode&& (uTpr & ~XAPIC_TPR_VALID))return apicMsrAccessError(pVCpu, MSR_IA32_X2APIC_TPR, APICMSRACCESS_WRITE_RSVD_BITS);PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);//写入TprpXApicPage->tpr.u8Tpr = uTpr;//更新pprapicUpdatePpr(pVCpu);//检查是否有pending的中断可以唤醒,发送中断到VCPU中apicSignalNextPendingIntr(pVCpu);return VINF_SUCCESS;
}
//设置了Tpr或者有有中断被唤醒的时候,都会调用这个函数更新Ppr
static void apicUpdatePpr(PVMCPUCC pVCpu)
{PXAPICPAGE    pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);//获取当前正在处理的中断的最高优先级uint8_t const uIsrv      = apicGetHighestSetBitInReg(&pXApicPage->isr, 0 /* rcNotFound */);uint8_t       uPpr;//PPR赋值成TPR的值和ISR中最大优先级中较大的一个if (XAPIC_TPR_GET_TP(pXApicPage->tpr.u8Tpr) >= XAPIC_PPR_GET_PP(uIsrv))uPpr = pXApicPage->tpr.u8Tpr;elseuPpr = XAPIC_PPR_GET_PP(uIsrv);pXApicPage->ppr.u8Ppr = uPpr;
}
//唤醒下一个高优先级中断
static void apicSignalNextPendingIntr(PVMCPUCC pVCpu)
{VMCPU_ASSERT_EMT_OR_NOT_RUNNING(pVCpu);PCXAPICPAGE pXApicPage = VMCPU_TO_CXAPICPAGE(pVCpu);if (pXApicPage->svr.u.fApicSoftwareEnable){//从irr里获取优先级最高的pending中断int const irrv = apicGetHighestSetBitInReg(&pXApicPage->irr, -1 /* rcNotFound */);if (irrv >= 0){uint8_t const uVector = irrv;uint8_t const uPpr    = pXApicPage->ppr.u8Ppr;//如果这个中断优先级大于当前CPU的中断优先级,设置标记位表示有中断可以被唤醒if (   !uPpr||  XAPIC_PPR_GET_PP(uVector) > XAPIC_PPR_GET_PP(uPpr)){apicSetInterruptFF(pVCpu, PDMAPICIRQ_HARDWARE);}}}
}

apicSetLvtEntry

static VBOXSTRICTRC apicSetLvtEntry(PVMCPUCC pVCpu, uint16_t offLvt, uint32_t uLvt)
{PCAPIC pApic = VM_TO_APIC(pVCpu->CTX_SUFF(pVM));if (offLvt == XAPIC_OFF_LVT_TIMER){//如果是APIC Timer,不支持Tsc Deadline模式,但是Lvt里又设置了Tsc Deadline模式if (   !pApic->fSupportsTscDeadline&& (uLvt & XAPIC_LVT_TIMER_TSCDEADLINE)){//x2APIC返回错误if (XAPIC_IN_X2APIC_MODE(pVCpu))return apicMsrAccessError(pVCpu, XAPIC_GET_X2APIC_MSR(offLvt), APICMSRACCESS_WRITE_RSVD_BITS);//xAPIC模式直接去掉这个bituLvt &= ~XAPIC_LVT_TIMER_TSCDEADLINE;}}//获取7个LVT里的顺序uint16_t const idxLvt = (offLvt - XAPIC_OFF_LVT_START) >> 4;//检查输入的值是否正确,不同的LVT中断,LVT里有效位是不同的。具体有效位见上一篇的图3if (   XAPIC_IN_X2APIC_MODE(pVCpu)&& (uLvt & ~g_au32LvtValidMasks[idxLvt]))return apicMsrAccessError(pVCpu, XAPIC_GET_X2APIC_MSR(offLvt), APICMSRACCESS_WRITE_RSVD_BITS);//获取LVT MaskuLvt &= g_au32LvtValidMasks[idxLvt];//如果没有开启SoftwareEnable(只运行硬件中断)需要设置上XAPIC_LVT_MASKPXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);if (!pXApicPage->svr.u.fApicSoftwareEnable)uLvt |= XAPIC_LVT_MASK;//检查通过,写入LVT寄存器apicWriteRaw32(pXApicPage, offLvt, uLvt);return VINF_SUCCESS;
}

apicSetIcrHi/apicSetIcrLo

ICR写入相当于发送一个IPI中断,先写入高位,后写入低位,写入低位的时候发送IPI中断

static VBOXSTRICTRC apicSetIcrHi(PVMCPUCC pVCpu, uint32_t uIcrHi)
{//写入寄存器高位PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);pXApicPage->icr_hi.all.u32IcrHi = uIcrHi & XAPIC_ICR_HI_DEST;return VINF_SUCCESS;
}
static VBOXSTRICTRC apicSetIcrLo(PVMCPUCC pVCpu, uint32_t uIcrLo, int rcRZ, bool fUpdateStat)
{//写入寄存器低位PXAPICPAGE pXApicPage  = VMCPU_TO_XAPICPAGE(pVCpu);pXApicPage->icr_lo.all.u32IcrLo = uIcrLo & XAPIC_ICR_LO_WR_VALID;//发送IPI中断return apicSendIpi(pVCpu, rcRZ);
}

apicSetSvr :

当处理器将其PPR(任务优先级)提高到大于或等于当前断言处理器INTR信号的中断级别时,可能会出现特殊情况。如果在发出INTA周期时,要分配的中断已被屏蔽(由软件编程),则本地APIC将传递一个虚假的中断向量。分配假中断向量不会影响ISR,因此该向量的处理程序应在没有EOI的情况下返回。

static int apicSetSvr(PVMCPUCC pVCpu, uint32_t uSvr)
{uint32_t   uValidMask = XAPIC_SVR_VALID;PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);//如果开启了禁止EOI broadcast,加上XAPIC_SVR_SUPRESS_EOI_BROADCAST标记if (pXApicPage->version.u.fEoiBroadcastSupression)uValidMask |= XAPIC_SVR_SUPRESS_EOI_BROADCAST;//x2APIC模式不允许非有效位是1if (   XAPIC_IN_X2APIC_MODE(pVCpu)&& (uSvr & ~uValidMask))return apicMsrAccessError(pVCpu, MSR_IA32_X2APIC_SVR, APICMSRACCESS_WRITE_RSVD_BITS);//直接写入SVR寄存器apicWriteRaw32(pXApicPage, XAPIC_OFF_SVR, uSvr);//如果关闭了软件中断,需要重置LVT里的maskif (!pXApicPage->svr.u.fApicSoftwareEnable){pXApicPage->lvt_timer.u.u1Mask   = 1;
#if XAPIC_HARDWARE_VERSION == XAPIC_HARDWARE_VERSION_P4pXApicPage->lvt_thermal.u.u1Mask = 1;
#endifpXApicPage->lvt_perf.u.u1Mask    = 1;pXApicPage->lvt_lint0.u.u1Mask   = 1;pXApicPage->lvt_lint1.u.u1Mask   = 1;pXApicPage->lvt_error.u.u1Mask   = 1;}
}

apicSetEsr

static int apicSetEsr(PVMCPUCC pVCpu, uint32_t uEsr)
{//x2APIC模式不允许非有效位是1if (   XAPIC_IN_X2APIC_MODE(pVCpu)&& (uEsr & ~XAPIC_ESR_WO_VALID))return apicMsrAccessError(pVCpu, MSR_IA32_X2APIC_ESR, APICMSRACCESS_WRITE_RSVD_BITS);//这边只是清除所有内部错误,并没有真正设置ESR寄存器PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);pXApicPage->esr.all.u32Errors = apicClearAllErrors(pVCpu);return VINF_SUCCESS;
}

apicSetTimerDcr

Timer Divide Configuration Register (DCR).

static VBOXSTRICTRC apicSetTimerDcr(PVMCPUCC pVCpu, uint32_t uTimerDcr)
{//如果是x2APIC模式,只接受有效位有值的输入if (   XAPIC_IN_X2APIC_MODE(pVCpu)&& (uTimerDcr & ~XAPIC_TIMER_DCR_VALID))return apicMsrAccessError(pVCpu, MSR_IA32_X2APIC_TIMER_DCR, APICMSRACCESS_WRITE_RSVD_BITS);//写入DCR值PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);apicWriteRaw32(pXApicPage, XAPIC_OFF_TIMER_DCR, uTimerDcr);return VINF_SUCCESS;
}

apicSetTimerIcr

timer’s Initial-Count Register (ICR).

static VBOXSTRICTRC apicSetTimerIcr(PPDMDEVINS pDevIns, PVMCPUCC pVCpu, int rcBusy, uint32_t uInitialCount)
{PAPIC      pApic      = VM_TO_APIC(pVCpu->CTX_SUFF(pVM));PAPICCPU   pApicCpu   = VMCPU_TO_APICCPU(pVCpu);PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);// TSC-deadline mode不使用ICR,忽略if (   pApic->fSupportsTscDeadline&& pXApicPage->lvt_timer.u.u2TimerMode == XAPIC_TIMER_MODE_TSC_DEADLINE)return VINF_SUCCESS;TMTIMERHANDLE hTimer = pApicCpu->hTimer;VBOXSTRICTRC rc = PDMDevHlpTimerLockClock(pDevIns, hTimer, rcBusy);if (rc == VINF_SUCCESS){pXApicPage->timer_icr.u32InitialCount = uInitialCount;//设置CCR的值等于ICRpXApicPage->timer_ccr.u32CurrentCount = uInitialCount;//如果ICR不等于0,启动Timerif (uInitialCount)apicStartTimer(pVCpu, uInitialCount);else//ICR等于0,停止TimerapicStopTimer(pVCpu);PDMDevHlpTimerUnlockClock(pDevIns, hTimer);}return rc;
}

apicSetEoi

中断完成设置

static VBOXSTRICTRC apicSetEoi(PVMCPUCC pVCpu, uint32_t uEoi, int rcBusy, bool fForceX2ApicBehaviour)
{bool const fX2ApicMode = XAPIC_IN_X2APIC_MODE(pVCpu) || fForceX2ApicBehaviour;if (   fX2ApicMode&& (uEoi & ~XAPIC_EOI_WO_VALID))return apicMsrAccessError(pVCpu, MSR_IA32_X2APIC_EOI, APICMSRACCESS_WRITE_RSVD_BITS);int isrv = apicGetHighestSetBitInReg(&pXApicPage->isr, -1 /* rcNotFound */);//获取下一个需要处理的中断if (isrv >= 0){uint8_t const uVector      = isrv;//是否是电平触发的中断bool const fLevelTriggered = apicTestVectorInReg(&pXApicPage->tmr, uVector);if (fLevelTriggered){//广播EOIVBOXSTRICTRC rc = PDMIoApicBroadcastEoi(pVCpu->CTX_SUFF(pVM), uVector);if (rc == VINF_SUCCESS){ /* likely */ }elsereturn rcBusy;//clear TMR寄存器 (中断完成)apicClearVectorInReg(&pXApicPage->tmr, uVector);//LINT0上的, 电平触发,fixedmode的中断需要在收到EOI命令之后清除Remote IRR标记uint32_t const uLvtLint0 = pXApicPage->lvt_lint0.all.u32LvtLint0;if (   XAPIC_LVT_GET_REMOTE_IRR(uLvtLint0)&& XAPIC_LVT_GET_VECTOR(uLvtLint0) == uVector&& XAPIC_LVT_GET_DELIVERY_MODE(uLvtLint0) == XAPICDELIVERYMODE_FIXED){ASMAtomicAndU32((volatile uint32_t *)&pXApicPage->lvt_lint0.all.u32LvtLint0, ~XAPIC_LVT_REMOTE_IRR);}}//清除ISR寄存器(当前中断已处理完成)apicClearVectorInReg(&pXApicPage->isr, uVector);//更新PprapicUpdatePpr(pVCpu);//选择下一个要处理的中断apicSignalNextPendingIntr(pVCpu);}
}

apicSetLdr

设置Logical Destination Register (LDR).

static VBOXSTRICTRC apicSetLdr(PVMCPUCC pVCpu, uint32_t uLdr)
{//直接写入Virtual APIC-pagePCAPIC pApic = VM_TO_APIC(pVCpu->CTX_SUFF(pVM));PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);apicWriteRaw32(pXApicPage, XAPIC_OFF_LDR, uLdr & XAPIC_LDR_VALID);return VINF_SUCCESS;
}

apicSetDfr

Destination Format Register (DFR).

static VBOXSTRICTRC apicSetDfr(PVMCPUCC pVCpu, uint32_t uDfr)
{uDfr &= XAPIC_DFR_VALID;uDfr |= XAPIC_DFR_RSVD_MB1;PXAPICPAGE pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);apicWriteRaw32(pXApicPage, XAPIC_OFF_DFR, uDfr);return VINF_SUCCESS;
}

APICGetTpr

VMMDECL(int) APICGetTpr(PCVMCPUCC pVCpu, uint8_t *pu8Tpr, bool *pfPending, uint8_t *pu8PendingIntr)
{VMCPU_ASSERT_EMT(pVCpu);if (APICIsEnabled(pVCpu)){PCXAPICPAGE pXApicPage = VMCPU_TO_CXAPICPAGE(pVCpu);if (pfPending){//获取IRR里pending中断中最高的一个优先级*pfPending = apicGetHighestPendingInterrupt(pVCpu, pu8PendingIntr);}//返回这个中断的TPR寄存器里的值*pu8Tpr = pXApicPage->tpr.u8Tpr;return VINF_SUCCESS;}*pu8Tpr = 0;return VERR_PDM_NO_APIC_INSTANCE;
}

17.4 APIC Timer相关的函数

当ICR写入时,会调用这些函数

apicStartTimer

void apicStartTimer(PVMCPUCC pVCpu, uint32_t uInitialCount)
{PCXAPICPAGE    pXApicPage   = APICCPU_TO_CXAPICPAGE(pApicCpu);uint8_t  const uTimerShift  = apicGetTimerShift(pXApicPage);uint64_t const cTicksToNext = (uint64_t)uInitialCount << uTimerShift;//最终调用到TM里的TMTimerSetRelative,这个函数在TM(Time Manager)里介绍PDMDevHlpTimerSetRelative(pDevIns, pApicCpu->hTimer, cTicksToNext, &pApicCpu->u64TimerInitial);apicHintTimerFreq(pDevIns, pApicCpu, uInitialCount, uTimerShift);
}

apicStopTimer

static void apicStopTimer(PVMCPUCC pVCpu)
{//调用TM里的TMTimerStop函数停止定时器PDMDevHlpTimerStop(pDevIns, pApicCpu->hTimer);pApicCpu->uHintedTimerInitialCount = 0;pApicCpu->uHintedTimerShift = 0;
}

apicHintTimerFreq

告诉TM当前APIC的频率

void apicHintTimerFreq(PPDMDEVINS pDevIns, PAPICCPU pApicCpu, uint32_t uInitialCount, uint8_t uTimerShift)
{//Timer启动之后只会设置一次if (   pApicCpu->uHintedTimerInitialCount != uInitialCount|| pApicCpu->uHintedTimerShift        != uTimerShift){uint32_t uHz;if (uInitialCount){//开启了定时器,调用TMTimerGetFreq函数获取频率uint64_t cTicksPerPeriod = (uint64_t)uInitialCount << uTimerShift;uHz = PDMDevHlpTimerGetFreq(pDevIns, pApicCpu->hTimer) / cTicksPerPeriod;}elseuHz = 0;//调用TMTimerSetFrequencyHint设置时间频率PDMDevHlpTimerSetFrequencyHint(pDevIns, pApicCpu->hTimer, uHz);pApicCpu->uHintedTimerInitialCount = uInitialCount;pApicCpu->uHintedTimerShift = uTimerShift;}
}

APICGetTimerFreq

获取当前APIC的频率

VMM_INT_DECL(int) APICGetTimerFreq(PVMCC pVM, uint64_t *pu64Value)
{PVMCPUCC pVCpu = pVM->CTX_SUFF(apCpus)[0];if (APICIsEnabled(pVCpu)){PCAPICCPU pApicCpu = VMCPU_TO_APICCPU(pVCpu);// 调用TMTimerGetFreq函数获取频率*pu64Value = PDMDevHlpTimerGetFreq(VMCPU_TO_DEVINS(pVCpu), pApicCpu->hTimer);return VINF_SUCCESS;}return VERR_PDM_NO_APIC_INSTANCE;
}

apicR3TimerCallback

时间片到之后的callback

static DECLCALLBACK(void) apicR3TimerCallback(PPDMDEVINS pDevIns, PTMTIMER pTimer, void *pvUser)
{PXAPICPAGE     pXApicPage = VMCPU_TO_XAPICPAGE(pVCpu);uint32_t const uLvtTimer  = pXApicPage->lvt_timer.all.u32LvtTimer;if (!XAPIC_LVT_IS_MASKED(uLvtTimer)){//时间片到, 根据LVT Timer里的信息发送中断到目标VCPUuint8_t uVector = XAPIC_LVT_GET_VECTOR(uLvtTimer);apicPostInterrupt(pVCpu, uVector, XAPICTRIGGERMODE_EDGE, 0 /* uSrcTag */);}//根据Timer mode决定是否重置定时器XAPICTIMERMODE enmTimerMode = XAPIC_LVT_GET_TIMER_MODE(uLvtTimer);switch (enmTimerMode){case XAPICTIMERMODE_PERIODIC:{//PERIODIC需要重置定时器并重新把u32CurrentCount赋值uint32_t const uInitialCount = pXApicPage->timer_icr.u32InitialCount;pXApicPage->timer_ccr.u32CurrentCount = uInitialCount;if (uInitialCount){//重新启动定时器apicStartTimer(pVCpu, uInitialCount);}break;}case XAPICTIMERMODE_ONESHOT:{//时间片到,不会重置,所以只设置u32CurrentCount成0pXApicPage->timer_ccr.u32CurrentCount = 0;break;}case XAPICTIMERMODE_TSC_DEADLINE:{//暂时不支持创建TSC deadline的timerbreak;}}
}

参考资料:

https://blog.csdn.net/omnispace/article/details/61415994
https://blog.csdn.net/ustc_dylan/article/details/4132046
Intel指令手册

Virtualbox源码分析17 APIC虚拟化2.APIC设备模拟相关推荐

  1. Virtualbox源码分析4:VMM虚拟化实现源码分析1

    文章目录 Virtualbox源码分析4:VMM虚拟化框架实现源码分析 4.1 VMX原理 4.1.1 VMX的状态转化: 4.1.2 VMCS 4.1.3 VMExit:VMX异常 Virtualb ...

  2. Virtualbox源码分析16 APIC虚拟化1 APIC概念和初始化

    文章目录 中断是什么 中断是如何被发送给CPU的 16.1 xAPIC and x2APIC 16.2 Local APIC Page里的寄存器和对应的重要概念 APIC ID 中断优先级类型的寄存器 ...

  3. Virtualbox源码分析10 CPU manager2:APIs

    文章目录 10.1 R3Init相关APIs(CPUM.cpp) CPUMR3Init 初始化CPUM CPUMR3Term CPUMR3InitCompleted CPUMR3ResetCpu CP ...

  4. Virtualbox源码分析22 NEM(Hyper-V兼容)3 Emulation Thread

    Native execution manager (Emulation Thread ) 文章目录 Native execution manager (Emulation Thread ) 22.1. ...

  5. Virtualbox源码分析23 NEM(Hyper-V兼容)4 VMExit

    Native execution manager (VMExit) 文章目录 Native execution manager (VMExit) 23.1 EPT内存管理 23.1.1分配内存 23. ...

  6. Virtualbox源码分析21 NEM(Hyper-V兼容)2 Hyper-V初始化和VM创建销毁

    文章目录 Native execution manager (Hyper-V兼容) 21.1 NEM初始化/Term 相关API 21.1.1 NEMR3InitConfig 21.1.2 NEMR3 ...

  7. Virtualbox源码分析20 NEM(Hyper-V兼容)1 Hyper-V架构和API介绍

    文章目录 Native execution manager (Hyper-V兼容) 20.1 winhvplatform.dll里的APIs 20.2 VID.dll 的导出函数 20.3 直接调用V ...

  8. apache dubbo 源码分析系列汇总

    Dubbo(读音[ˈdʌbəʊ])是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成.后面捐献给了知名的开源社区 ...

  9. 001-Golang1.17源码分析之slice

    Golang1.17源码分析之slice-001 Golang1.17 学习笔记001 一.slice 核心概念:切片和数组之间.切片和切片之间都是共享内存的.只要切片不发生扩容,那么都是操作的同一个 ...

最新文章

  1. 上海世博会信息化的8大看点
  2. Linux学习笔记13:把网卡名字都修改成eth*
  3. Scrapy 爬虫实例 抓取豆瓣小组信息并保存到mongodb中
  4. 【C 语言】文件操作 ( 配置文件读写 | 读取配置文件 | 函数接口形参 | 读取配置文件的逐行遍历操作 | 读取一行文本 | 查找字符 | 删除字符串前后空格 )
  5. Django框架之跨站请求伪造
  6. Java基础提升篇:理解String 及 String.intern() 在实际中的应用
  7. 快速部署Linkis1.0文档
  8. USB转串口 FT232/PL2303/CH340 比较
  9. 【lua学习】4.表
  10. 小程序开发(7)-之获取手机号、用户信息
  11. git+coding.net记录篇
  12. 问界M7开启交付 邹市明成精英车主
  13. Windows上搭建安卓的JAVA开发环境(Ecli...
  14. 中国电信最快apn里面的服务器,电信4g网速最快的apn接入点(电信4g承载系统哪个快)...
  15. 【AE教程】AI文件导入AE方法
  16. Confluence 6 教程:空间高手
  17. DataCastle[职位预测竞赛]冠军——我们都爱苍老师
  18. html5+JS制作音乐播放器
  19. 替代满足、稀缺冲动、从众效应、思考快与慢就不怕退货吗?
  20. a股历史30年的大盘价_中国股市历史图(中国股市30年走势图)

热门文章

  1. 推荐一个程序员必备官方 App ,名字叫:力扣
  2. 【计算机网络】第九章:无线网络
  3. kepserver在设备上添加项目失败_隔空投送存储项目失败怎么办
  4. Github如何绑定域名
  5. 极客时间 资源_极客学校:学习Windows 7 –资源访问
  6. 微软官方提供的免费正版的虚拟机
  7. 苹果手机怎么清除缓存_手机里的文件如何彻底删除?教你清除缓存的方法
  8. 不忘初心,牢记使命——SSM始于Maven,终于Maven(关于Maven的大总结)
  9. Expected response code 250 but got code “501“, with messa php laravel 发邮件 smtp qq邮箱 阿里云
  10. AIIA-2021版《电信行业人工智能应用白皮书》