Windows内存管理(二):内存架构 浅谈一二

《Windows内存管理(一):Windows性能监视器(PerfMon)》

Windows内存管理是一个复杂的主题,涉及多个层次和组件。以下是一个分层的概述。

Image

1、虚拟内存管理

Windows使用虚拟内存来给每个进程提供一个看似连续的内存空间,实际上这些空间可能分散在物理内存和磁盘上。

1.1 虚拟地址空间

每个Windows进程都有自己的虚拟地址空间,32位系统通常是4GB64位系统则远大于此。

  • PS:所以,我们选择项目编译和运行在32位还是64位系统上,绝不是拍拍脑袋随便决定的。当虚拟内存空间大小成为项目瓶颈时,很显然选择64位的操作系统,我们的项目也对应使用64位编译工具链进行编译.

每个进程都有自己的用户空间需要管理,当我们使用VirtualAlloc等函数申请一块固定的地址空间时,首先需要确认这块空间是否被占用,如果该空间已被占用则申请失败 用户空间并非像内核空间一样通过一块链表去管理已占用的线性地址空间(效率低), 而是通过搜索二叉树 申请内存的两种方式:

通过VirtualAlloc/VirtualAllocEx申请:Private Memory(独享物理页)
通过CreateFileMapping进行映射:Mapped Memory(共享物理页)

1.2 页面文件

当物理内存不足时,Windows会使用页面文件(pagefile.sys)作为虚拟内存的扩展。

1.3 代码示例

VituaAlloc
LPVOID VituaAlloc{
        LPVOID IpAddress,   //要分配的内存区域的地址
        DWORD dwSize,       //分配的大小
        DWORD fiAllocationType, //分配的类型
        DWORD fiProtect     //该内存的初始保护属性
};
#include <windows.h>

int main() {
    // 分配虚拟内存
    LPVOID lpvBase = VirtualAlloc(
        NULL,                 // 系统选择地址
        1024 * 1024,          // 分配1MB空间
        MEM_COMMIT | MEM_RESERVE, // 分配并提交
        PAGE_READWRITE);      // 可读写保护

    if (lpvBase == NULL) {
        // 处理错误
    }

    // 使用内存...

    // 释放内存
    if (!VirtualFree(lpvBase, 0, MEM_RELEASE)) {
        // 处理错误
    }

    return 0;
}
Mapped Memory
共享内存
#define MapFileName "共享内存"
//内核对象:1、物理页 2、文件
HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,MapFileName);

//将物理页与线性地址进行映射
LPTSTR g_IpBuff=(LPTSTR)MapViewOfFile(g_hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);
*(PDWORD)g_IpBuff = 0x12345678;

printf("%p",g_IpBuff);
共享文件
HANDLE g_hFile = CreateFile("C:\\NOTEPAD.EXE",GENERIC_READIGENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
​
HANDLE g_hMapFile = CreateFileMapping(g_hFile,NULL,PAGE_READWRITE,O,BUFSIZ,NULL);
LPTSTR g_IpBuff=(LPTSTR)MapViewOfFile(g_hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);
*(PDWORD)g_IpBuff = 0x12345678;
镜像文件
HMODULE hModule = ::LoadLibrary("C:INOTEPAD.EXE");
1、LoadLibrary就是通过内存映射的方式实现的
2、为了避免影响到别人,属性为:写拷贝

2、物理内存管理

Windows通过内存管理器来管理物理内存,它负责页面的分配和回收。

2.1 工作集

工作集是指一个进程当前在物理内存中的页面集合。

2.2 代码示例:锁定内存页

#include <windows.h>

int main() {
    // 分配内存
    LPVOID lpvBase = VirtualAlloc(NULL, 1024 * 1024, MEM_COMMIT, PAGE_READWRITE);

    // 锁定内存页
    if (!VirtualLock(lpvBase, 1024 * 1024)) {
        // 处理错误
    }

    // 使用内存...

    // 解锁内存页
    VirtualUnlock(lpvBase, 1024 * 1024);

    // 释放内存
    VirtualFree(lpvBase, 0, MEM_RELEASE);

    return 0;
}

3. 内存分页

Windows使用分页系统来管理内存,非活跃的页面可以被移动到磁盘上的页面文件。

3.1 页面错误

  • 硬页面错误:当一个进程访问的页不在物理内存中时,操作系统必须从磁盘中读取该页到物理内存,这个过程称为硬页面错误。
  • 软页面错误:当一个进程访问的页在物理内存中但不在进程的工作集中,或者需要写入一个共享页时发生。

4. 内存池

在Windows内核中,内存管理是通过两种主要类型的内存池来实现的:非分页池(Non-Paged Pool)和分页池(Paged Pool)。这些池是操作系统内核用来分配内存给内核模式下的驱动程序和内核系统的。

非分页池(Non-Paged Pool)

  • 定义:非分页池是一块内存区域,它的内容始终驻留在物理内存中,不会被分页到磁盘上。
  • 用途:通常用于存储不能被分页出去的数据,如必须在任何时候都能快速访问的数据结构。
  • 特点:
  • 由于它不会被分页到磁盘,因此可以在任何IRQL(中断请求级别)下访问。
  • 适用于中断服务例程(ISR)和其他高IRQL级别的代码。
  • 限制:非分页池的大小是有限的,因为它占用的是宝贵的物理内存。

分页池(Paged Pool)

  • 定义:分页池是可以被分页到磁盘上的内存区域。
  • 用途:用于存储不需要在高IRQL下访问的数据。
  • 特点:
  • 当系统内存不足时,它的内容可以被分页到磁盘上。
  • 只能在PASSIVE_LEVEL IRQL(即IRQL=0)下访问,这是大多数用户模式代码和一些低优先级的内核模式代码运行的级别。
  • 优势:相对于非分页池,分页池可以更大,因为它不受物理内存大小的限制。
    管理和监控
  • 工具:可以使用Windows性能监视器(Performance Monitor)来监控非分页池和分页池的使用情况。
  • API:内核模式驱动程序可以使用内核API,如ExAllocatePoolWithTag来指定从哪个池分配内存,以及分配的内存块的标签。

4.1 代码示例:内核模式内存分配

#include <ntddk.h>

void DriverEntry() {
    // 分配非分页池内存
    PVOID pNonPagedMemory = ExAllocatePoolWithTag(NonPagedPool, 1024, 'Tag1');

    // 分配分页池内存
    PVOID pPagedMemory = ExAllocatePoolWithTag(PagedPool, 1024, 'Tag2');

    // 使用内存...

    // 释放内存
    ExFreePool(pNonPagedMemory);
    ExFreePool(pPagedMemory);
}

在这个示例中,ExAllocatePoolWithTag函数用于分配内存,并且每个分配都有一个标签,这有助于调试和跟踪内存使用。
注意事项

  • 过度使用非分页池可能会导致系统的物理内存耗尽,从而引起性能问题或系统崩溃。
  • 驱动程序开发者应当谨慎使用非分页池,并确保释放不再需要的内存。

理解和正确使用这两种内存池对于开发稳定的内核模式代码至关重要。

5、总结

Windows内存管理是一个多层次的架构,它涵盖了从用户空间的虚拟内存分配到内核空间的物理内存和内存池管理。理解这些层次和它们如何交互对于开发高效、稳定的Windows应用程序和驱动程序至关重要。

开发者可以通过各种API与这些层次交互,例如使用 VirtualAlloc VirtualFree 在用户空间分配和释放内存,或者在内核模式下使用 ExAllocatePool ExFreePool。正确地管理内存不仅可以提高应用程序的性能,还可以避免内存泄漏和其他内存相关的错误。

此外,开发者还需要考虑到内存的访问模式,比如内存页的锁定(VirtualLock)和解锁(VirtualUnlock),以及对非分页池和分页池的使用,这些都会影响程序的性能和稳定性。

最后,高级内存管理还可能涉及到对页面文件的管理、理解工作集的概念、处理页面错误以及优化内存使用和访问模式。通过工具如Windows性能监视器(Performance Monitor)和资源监视器(Resource Monitor),开发者可以监控应用程序的内存使用情况,从而做出相应的优化。

6 、拓展知识

几个常识性但很重要的问题

那么,我们在这里拓展性的提几个问题:

  • 1、有个常识性的问题,我们知道以32位.exe应用程序可以在64位的操作系统上运行,为何反过来却不行呢?

答: 在Windows上,32位应用程序可以在64位操作系统上运行,这是因为64位Windows操作系统提供了一种名为WOW64(Windows 32-bit on Windows 64-bit)的兼容层。WOW64是一个兼容性环境,它模拟了32位Windows操作系统,允许32位应用程序在64位系统上运行,而无需任何修改。

WOW64实现了以下功能:

  1. API转换:将32位应用程序的系统调用转换为适用于64位系统的调用。
  2. 文件系统重定向:将32位应用程序对系统文件夹的访问重定向到对应的32位版本文件夹。
  3. 注册表重定向:将32位应用程序对注册表的访问重定向到对应的32位视图。

相反,64位程序不能在32位操作系统上运行,原因包括:

  1. 处理器模式:64位程序需要在64位处理器模式下运行,这种模式支持更宽的寄存器和新的指令集,这些在32位模式下是不可用的。
  2. 内存寻址:64位程序设计为利用大于4GB的地址空间,而32位操作系统受限于最多4GB的地址空间(实际可用内存通常更少)。当然,32位程序跑64位系统,会浪费一部分地址空间;
  3. 操作系统内核:64位程序需要64位操作系统内核提供的服务和接口,这些服务和接口在32位操作系统中不存在。

因此,由于硬件和软件架构的根本差异,64位应用程序无法在32位操作系统上直接运行。

  • 2、在windows上如何为每个进程设置更大的虚拟内存空间?

Windows操作系统中,可以通过几种方法来设置进程可以分配的最大虚拟内存大小:
方法1. 系统属性设置
可以通过修改系统属性来设置所有进程可用的虚拟内存总量,即页面文件的大小。这不是针对单个进程,而是针对整个系统。

  • 打开“控制面板”。
  • 点击“系统和安全”。
  • 点击“系统”。
  • 在左侧面板中,点击“高级系统设置”。
  • 在“系统属性”窗口中,切换到“高级”选项卡。
  • 在“性能”部分,点击“设置”按钮。
  • 在“性能选项”窗口中,切换到“高级”选项卡。
  • 在“虚拟内存”部分,点击“更改”按钮。
  • 取消勾选“自动管理所有驱动器的分页文件大小”,然后可以为每个驱动器设置自定义大小。

方法2. 通过程序设置
如果你正在开发软件,可以在程序中使用Windows API来限制特定进程的最大虚拟内存大小。例如,可以使用SetProcessWorkingSetSize函数来设置进程的工作集大小,这影响了进程的物理内存使用量,间接影响虚拟内存的使用。

#include <windows.h>

BOOL SetMaxMemoryUsage(HANDLE hProcess, SIZE_T dwMinimumWorkingSetSize, SIZE_T dwMaximumWorkingSetSize) {
    return SetProcessWorkingSetSize(hProcess, dwMinimumWorkingSetSize, dwMaximumWorkingSetSize);
}

在这个例子中,hProcess是进程的句柄,dwMinimumWorkingSetSizedwMaximumWorkingSetSize是你希望设置的工作集的最小和最大大小。

方法3:通过Job对象
Windows提供了Job对象,可以用来管理多个进程的资源使用。可以创建一个Job对象,将一个或多个进程与之关联,并设置资源限制。

#include <windows.h>

int main() {
    // 创建Job对象
    HANDLE hJob = CreateJobObject(NULL, NULL);

    // 将进程关联到Job对象
    AssignProcessToJobObject(hJob, GetCurrentProcess());

    // 设置Job对象的内存限制
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {0};
    jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY;
    jeli.ProcessMemoryLimit = sizeInBytes; // 设置进程内存限制

    SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli));

    // ... 运行进程 ...

    // 关闭Job对象句柄
    CloseHandle(hJob);

    return 0;
}

在这个例子中,sizeInBytes是你希望限制的内存大小。
注意

  • 设置进程的最大虚拟内存大小可能会影响程序的性能和稳定性。
  • 这些设置通常由系统管理员或有经验的用户进行,因为不当的设置可能会导致系统或应用程序运行不稳定。

在进行任何更改之前,请确保你了解这些设置的含义以及它们对系统的潜在影响。

  • 3、在应用层,我们使用栈,还是通过new 或者 malloc 方法为对象分配的内存,都指的是虚拟内存么?
  • 答:是的,当你在应用层使用 newmalloc 分配内存时,你实际上是在分配虚拟内存。在Windows操作系统中,每个进程都有自己的虚拟地址空间,这个地址空间是由操作系统管理的,它将虚拟地址映射到物理内存上。
    当你使用 newmalloc 请求内存时,操作系统会在进程的虚拟地址空间中找到一块足够大的未使用区域,并将其标记为已使用。然后,当你首次访问这块内存时,操作系统会将虚拟地址映射到物理内存上。这个过程通常被称为按需分页(demand paging)。
    这种虚拟内存系统的好处是,每个进程都有一致的地址空间视图,不需要关心物理内存的实际布局。此外,操作系统可以通过页面交换(swapping)机制,将不常用的内存页面移动到磁盘上,从而有效地管理有限的物理内存资源。

其他补充:

Windows分页系统是操作系统内存管理的核心组成部分,它允许物理内存(RAM)的有效使用,并为每个进程提供一个大的、连续的虚拟地址空间。以下是Windows分页系统的详细解释:
虚拟内存和物理内存

  • 虚拟内存:为每个进程提供的抽象内存空间,它通过分页系统映射到物理内存。
  • 物理内存:实际的RAM,其容量通常远小于虚拟内存空间。
    分页机制
  • 分页:操作系统将虚拟内存分割成固定大小的块,称为“页”(通常大小为4KB),物理内存同样分割成页框。
  • 页表:用于维护虚拟页到物理页框的映射关系。
    分页和换页
  • 分页(Paging):将不活跃的内存页从物理内存移动到磁盘上的页面文件。
  • 换页(Swapping):将整个进程的地址空间或大块内存从物理内存交换到磁盘,或者反过来。
    页面文件
  • 页面文件(Page File):一个位于硬盘上的系统文件(pagefile.sys),用作虚拟内存的扩展。
    内存管理单元(MMU)
  • MMU:硬件组件,负责虚拟地址到物理地址的转换。
    页面错误
  • 硬页面错误:当一个进程访问的页不在物理内存中时,操作系统必须从磁盘中读取该页到物理内存,这个过程称为硬页面错误。
  • 软页面错误:当一个进程访问的页在物理内存中但不在进程的工作集中,或者需要写入一个共享页时发生。
    工作集
  • 工作集:一个进程实际在物理内存中的页集合。
    内存压力
  • 内存压力:当物理内存使用接近或达到上限时,操作系统会通过换出页到磁盘来减轻压力。
    内存管理API
  • VirtualAlloc/VirtualFree:应用程序用来在虚拟地址空间中分配和释放内存的API。
    内存管理策略
  • 需求分页:只有当页面被访问时,它才会被加载到物理内存。
  • 预读(Prefetching):操作系统预测将要使用的页面并提前加载到物理内存。
  • 页面置换算法:如最近最少使用(LRU)算法,用于决定哪些页面应该被换出到磁盘。
    性能监控
  • 性能监视器(Performance Monitor):可以用来监控虚拟内存使用情况、页面文件使用情况、页面错误等。
    优化
  • 内存优化:开发者和系统管理员可以通过调整页面文件大小、监控内存使用模式、优化应用程序的内存使用等方式来优化内存使用。

Windows分页系统的设计允许多个进程共享有限的物理内存资源,同时提供了足够的虚拟内存空间,以便运行大型应用程序。通过需求分页、页面置换算法和内存压力管理,Windows能够在物理内存受限的情况下维持系统的稳定性和性能。


参考文献
1、https://zhuanlan.zhihu.com/p/670251533
2、https://zhuanlan.zhihu.com/p/34753439
3、https://learn.microsoft.com/en-us/sysinternals/downloads/vmmap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/304741.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java学习笔记-day01-Flowable工作流入门

课程来源B站大佬波哥的课程 本文仅做笔记&#xff0c;课件需要的联系B站大佬获取 0.前置 相关话术 流程定义&#xff08;Process Definition&#xff09;&#xff1a; 描述业务流程的定义&#xff0c;通常使用BPMN&#xff08;Business Process Model and Notation&#xff09…

没有货源是不是就没办法在家做抖店?打包发货怎么完成?解答如下

我是王路飞。 有人问了我一个问题&#xff1a;无货源模式的抖店&#xff0c;自己一个人在家里做不了是吧&#xff1f;毕竟打包发货这些问题怎么解决呢&#xff1f; 店铺要是发货不及时被平台罚款怎么办&#xff1f;产品有质量问题怎么解决呢&#xff1f;店铺一直不出单怎么办…

并发程序设计--D5~D7线程的创建、回收和取消清理

一个进程中的多个线程共享以下资源 可执行指令 静态数据 进程中打开的文件描述符 当前工作目录 用户ID 用户组ID 每个线程私有的资源 线程ID PC&#xff08;程序计数器&#xff09;和相关寄存器 堆区和栈区 错误号&#xff08;errno&#xff09; 优先级 执行状态和属性 线程的…

2024阿里云服务器ECS实例全方位介绍_优缺点大全

阿里云服务器ECS英文全程Elastic Compute Service&#xff0c;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如经济型e实例、通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云服务器网aliyu…

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -用户信息修改实现

锋哥原创的uniapp微信小程序投票系统实战&#xff1a; uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

数字化解决方案在市政交通大显身手

Bentley 应用程序促进了创新设计解决方案&#xff0c;预计可在 25 年内节省 3,200 万美元 大规模现代化计划 大学大道位于爱荷华州锡达福尔斯市繁忙的社区&#xff0c;是一条重要的六车道高速公路&#xff0c;长约两英里&#xff0c;每天通行的车辆有 2 万多辆。该道路已有 60…

java编程中,保证接口幂等性的实现方案讨论

一、什么是幂等性 数学中的幂等是指f(x) f(f(x))&#xff0c;编程领域的术语是指同一个操作&#xff0c;在重复提交的情况下&#xff0c;最终产生的影响是不变的。举例说&#xff1a; 提交订单时&#xff0c;用户在购物车界面&#xff0c;重复点击“下单”&#xff0c;服务端…

vxe-树形结构层级过多横向滚动条设置

一. 问题 在使用vxe-table中的树形结构的时候&#xff0c;如果树形结构的层级过多的话的会导致层级很深的层级&#xff0c;鼠标点击无法选中&#xff0c;对应的内容显示不完全。 层级过多后 二. 解决 方法1&#xff1a;动态的计算所占行的宽度&#xff0c;选取最大宽度给表格列…

统信UOS虚拟机安装VirtualBox扩展使用USB功能

为什么要安装VirtualBox扩展包&#xff1f; 安装 Oracle VM VirtualBox 扩展包的原因是&#xff0c;它提供了对 USB 2.0、USB 3.0、远程桌面协议 VRDP&#xff08;VirtualBox Remote Desktop Protocol&#xff09;等实用功能的支持&#xff0c;以增强 VirtualBox 的功能。这些…

MIT_线性代数笔记:第 25 讲 对称矩阵和正定性

目录 对称矩阵 Symmetric matrices实特征值 Real eigenvalues正定矩阵 Positive definite matrices 对称矩阵是最重要的矩阵之一&#xff0c;其特征值为实数并且拥有一套正交特征向量。正定矩阵的性质则更好。 对称矩阵 Symmetric matrices 包含特殊性质的矩阵&#xff0c;例如…

CCF模拟题 202309-1 坐标变换(其一)

问题描述 试题编号&#xff1a; 202309-1 试题名称&#xff1a; 坐标变换&#xff08;其一&#xff09; 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 512.0MB 问题描述&#xff1a; 对于平面直角坐标系上的坐标&#xff08;x,y&#xff09;&#xff0c;小P定义了一个包含…

【Flutter 开发实战】Dart 基础篇:常用运算符

在Dart中&#xff0c;运算符是编写任何程序的基本构建块之一。本文将详细介绍Dart中常用的运算符&#xff0c;以帮助初学者更好地理解和运用这些概念。 1. 算术运算符 算术运算符用于执行基本的数学运算。Dart支持常见的加、减、乘、除、整除以及取余运算。常见的算数运算符如…

从虚拟到现实:数字孪生驱动智慧城市可持续发展

随着科技的飞速发展&#xff0c;智慧城市已经成为未来城市发展的重要趋势。数字孪生技术作为智慧城市建设中的关键技术之一&#xff0c;正在发挥着越来越重要的作用。本文将探讨数字孪生如何从虚拟走向现实&#xff0c;驱动智慧城市的可持续发展。 一、数字孪生技术&#xff1…

【CV】计算两个向量的夹角,并使用 OpenCV 可视化弧线

背景 基于人体/动物,骨骼点数据,计算关节角度 1. 原理 计算两个向量的夹角,我们已三个点为例,BA 向量和BC向量,求 B 的角度。若为四个点,延长交叉即可。 2. 效果 效果图如下 3. 核心代码 def compute_vector_angle(a, b, c):"""计算两个向量 ba 和…

Unity中BRP下的深度图

文章目录 前言一、在Shader中使用1、在使用深度图前申明2、在片元着色器中 二、在C#脚本中开启摄像机深度图三、最终效果 前言 在之前的文章中&#xff0c;我们实现了URP下的深度图使用。 Unity中URP下使用屏幕坐标采样深度图 在这篇文章中&#xff0c;我们来看一下BRP下深度…

【PB续命07】JDBC连接达梦数据库

JDBC(Java DataBase Connectivity) 称为Java数据库连接&#xff0c;它是一种用于数据库访问的应用程序API&#xff0c;由一组用Java语言编写的类和接口组成&#xff0c;有了JDBC就可以用同一的语法对多种关系数据库进行访问&#xff0c;而不用担心其数据库操作语言的差异。 有了…

无线网卡怎么连接台式电脑?正确操作步骤分享!

“我在使用电脑时经常都需要用到网络&#xff0c;请问大家在使用无线网卡时怎么将它与台式电脑进行连接的呢&#xff1f;” 使用电脑的用户在进行网上冲浪时都需要先连接网络。如果不想使用网线&#xff0c;无线网卡不仅可以为用户提供网络服务&#xff0c;在使用时该更加灵活和…

实战:低代码表单引擎助力文件上传与数据处理

在当今的信息化时代&#xff0c;数据已成为企业的重要资产。为了更好地管理和利用这些数据&#xff0c;许多企业开始采用表单上传组件来导入和处理数据。通过使用表单上传组件&#xff0c;用户可以方便地将文件上传至系统中&#xff0c;然后进行后续的数据处理和分析。这种方式…

Java零基础教学文档第一篇:JavaSE(3)

接上期后续 本期继续分享尚未结束的JavaSE章节 JavaSE属于初入门阶段&#xff0c;内容很多~ 但很基础&#xff0c; 大家需保持耐心&#xff0c;慢慢的学~ 争取你们学习的速度&#xff01; 跟上我更新的速度哦~ 今日新篇章 流程控制语句 【主要内容】 if单选结构 if双选结构…

适合培训协会搭建的培训机构管理系统开发方案

一、项目背景与目标 &#xff08;一&#xff09;项目背景 培训学校教务管理系统是培训机构数字化管理的必备系统&#xff0c;该系统功能大大提升机构办学的管理效率、提升机构在家长心中的专业度&#xff0c;市面上的培训机构管理系统收费越来越贵&#xff0c;为了给协会内培…