【CUDA编程】CUDA内存模型

news2024/11/29 4:31:42

文章目录

  • 1. 内存结构
  • 2. GPU device内存
    • 2.1 寄存器(Registers)
    • 2.2 本地内存(Local Memory)
    • 2.3 共享内存(Shared Memory)
    • 2.4 常量内存(Constant Memory)
    • 2.5 纹理内存(Texture Memory)
    • 2.6 全局内存(Global Memory)
  • 3. CPU Host内存

1. 内存结构

在CUDA中可编程内存的类型有:

  • 寄存器(Registers)
  • 本地内存(Local Memory)
  • 共享内存(Shared Memory)
  • 常量内存(Constant Memory)
  • 纹理内存(Texture Memory)
  • 全局内存(Global Memory)

CUDA中的内存模型分为以下几个层次:

  • thread:每个线程都用自己的registers(寄存器)和local memory(局部内存)
  • block:每个线程块(block)内都有自己的shared memory(共享内存),所有线程块内的所有线程共享这段内存资源
  • grid:每个grid都有自己的global memory(全局内存),constant memory(常量内存)和texture memory(纹理内存),不同线程块的线程都可使用。其中常量内存和纹理内存为只读内存空间

线程访问这几类存储器的速度是:register > shared memory >Constant Memory > Texture Memory > Local Memory and Global Memory。下面这幅图表示这些内存在计算机架构中的所在层次。

在这里插入图片描述

2. GPU device内存

2.1 寄存器(Registers)

内核函数中声明且没有其他修饰符修饰的变量通常是存放在GPU的寄存器中,比如下面代码中的线程索引变量i。寄存器通常用于存放内核函数中需要频繁访问的线程私有变量,这些变量与内核函数的生命周期相同,内核函数执行完毕后,就不能再对它们进行访问了。

特点:每个线程私有,速度快

__global__ void VectorAddGPU(const float *const a, const float *const b,
                             float *const c, const int n) {
  int i = blockDim.x * blockIdx.x + threadIdx.x;  //变量i 在寄存器中
  if (i < n) {
    c[i] = a[i] + b[i]; 
  }
}

寄存器是GPU中访问速度最快的内存空间,但是一个SM中寄存器的数量比较有限,一旦内核函数使用了超过硬件限制的寄存器数量,则会使用本地内存来代替多占用的寄存器,这种寄存器溢出的情况会带来性能上的不利影响,实际编程过程中我们应该避免这种情况。

使用nvcc的编译选项maxrregcount可以控制内核函数使用的寄存器的最大数量:

-maxrregcount=32

2.2 本地内存(Local Memory)

当register耗尽时,数据将被存储到local memory。如果每个线程中使用了过多的寄存器,或声明了大型结构体或数组,或编译器无法确定数组大小,线程的私有数据就会被分配到local memory中。,可能存放到本地内存中的变量有:

  • 编译时使用未知索引引用的本地数组
  • 可能会占用大量寄存器空间的较大本地结构体或者数组
  • 任何不满足内核函数寄存器限定条件的变量

特点:每个线程私有;没有缓存,慢。
 
溢出到本地内存中的变量 本质上与全局内存在同一块区域

2.3 共享内存(Shared Memory)

在内核函数中被__shared__修饰符修饰的变量被存储到共享内存中。每个SM都有一定数量由线程块分配的共享内存,它们在内核函数内进行声明,生命周期伴随整个线程块,一个线程块执行结束后,为其分配的共享内存也被释放以便重新分配给其他线程块进行使用。线程块中的线程通过使用共享内存中的数据可以实现互相之间的协作,不过使用共享内存必须调用如下函数进行同步:

void __sybcthreads()

该函数为线程块中的所有线程设置了一个执行障碍点,使得同一线程块中的所有线程必须都执行到该障碍点才能往下执行,这样就可以避免一些潜在的数据冲突。

特点:block中的线程共有;访问共享存储器几乎与register一样快.

共享内存的定义方式有两种:静态共享内存和动态共享内存,静态共享内存在创建时候指明大小,态内存可以不指明大小。

#include <stdio.h>
__global__ void staticReverse(int *d, int n)
{
  __shared__ int s[1000];//静态共享内存
  int t = threadIdx.x;
  int tr = n-t-1;
  s[t] = d[t];  //从global memory拷贝写入shared memory

  //因为数组s是所有线程共享的,如果不做同步执行下面语句则可能出现数据竞争问题
  __syncthreads();	//调用同步函数,只有当前block中所有线程都完成之后,再往下走
  //从shared memory读,然后写回到global memory
  d[t] = s[tr];
}

__global__ void dynamicReverse(int *d, int n)
{
  extern __shared__ int s[];//动态共享内存
  int t = threadIdx.x;
  int tr = n-t-1;
  s[t] = d[t];
  __syncthreads();
  d[t] = s[tr];
}
//目的:将一个数组中的数据前后交换,实现倒序
int main(void)
{
  const int n = 1000;
  int a[n], r[n], d[n];
  
  for (int i = 0; i < n; i++) {
    a[i] = i;
    r[i] = n-i-1;
    d[i] = 0;
  }

  int *d_d;
  cudaMalloc(&d_d, n * sizeof(int)); 
  
  // run version with static shared memory
  cudaMemcpy(d_d, a, n*sizeof(int), cudaMemcpyHostToDevice);
  float time_gpu;
  cudaEvent_t start_GPU,stop_GPU;
  cudaEventCreate(&start_GPU);
  cudaEventCreate(&stop_GPU);
  cudaEventRecord(start_GPU,0);
  staticReverse<<<1,n>>>(d_d, n);//函数调用
  cudaEventRecord(stop_GPU,0);
  cudaEventSynchronize(start_GPU);
  cudaEventSynchronize(stop_GPU);
  cudaEventElapsedTime(&time_gpu, start_GPU,stop_GPU);
  printf("\nThe time from GPU:\t%f(ms)\n", time_gpu);
  cudaDeviceSynchronize();
  cudaEventDestroy(start_GPU);
  cudaEventDestroy(stop_GPU);
  
  cudaMemcpy(d, d_d, n*sizeof(int), cudaMemcpyDeviceToHost);
  //check
  for (int i = 0; i < n; i++) {
    if (d[i] != r[i]) 
      printf("Error: d[%d]!=r[%d] (%d, %d)\n", i, i, d[i], r[i]);
  }
    
  
  // run dynamic shared memory version
  cudaMemcpy(d_d, a, n*sizeof(int), cudaMemcpyHostToDevice);

  cudaEventCreate(&start_GPU);
  cudaEventCreate(&stop_GPU);
  cudaEventRecord(start_GPU,0);
  dynamicReverse<<<1,n,n*sizeof(int)>>>(d_d, n);//函数调用
  cudaEventRecord(stop_GPU,0);
  cudaEventSynchronize(start_GPU);
  cudaEventSynchronize(stop_GPU);
  cudaEventElapsedTime(&time_gpu, start_GPU,stop_GPU);
  printf("\nThe time from GPU:\t%f(ms)\n", time_gpu);
  cudaDeviceSynchronize();
  cudaEventDestroy(start_GPU);
  cudaEventDestroy(stop_GPU);
  cudaMemcpy(d, d_d, n * sizeof(int), cudaMemcpyDeviceToHost);
  for (int i = 0; i < n; i++) 
    if (d[i] != r[i]) printf("Error: d[%d]!=r[%d] (%d, %d)\n", i, i, d[i], r[i]);
}

输出:

The time from GPU:	0.015424(ms)

The time from GPU:	0.004672(ms)

__syncthreads() 是轻量级的,并且是以block 级别做同步。

2.4 常量内存(Constant Memory)

常量变量用__constant__修饰符进行修饰,它们必须在全局空间内和所有内核函数之外进行声明,对同一编译单元中的内核函数都是可见的。常量变量存储在常量内存中,内核函数只能从常量内存中读取数据。

特点:只读;有缓存;空间小(64KB)

注:定义常数存储器时,需要将其定义在所有函数之外,作用于整个文件 。

常量内存必须在host端代码中使用下面的函数来进行初始化

cudaError_t cudaMemcpyToSymbol(const void* symbol, const void* src,size_t count);

下面的例子展示了如何声明常量内存并与之进行数据交换:

__constant__ float const_data[256];
float data[256];
cudaMemcpyToSymbol(const_data, data, sizeof(data));
cudaMemcpyFromSymbol(data, const_data, sizeof(data));

常量内存适合用于线程束中的所有线程都需要从相同的内存地址中读取数据的情况,比如所有线程都需要的常量参数,每个GPU只可以声明不超过64KB的常量内存。

2.5 纹理内存(Texture Memory)

纹理内存驻留在设备内存中,并在每个SM的只读缓存中缓存。纹理内存是一种通过指定的只读缓存访问的全局内存,是对二维空间局部性的优化,所以使用纹理内存访问二维数据的线程可以达到最优性能。

特点:具有纹理缓存,只读。

2.6 全局内存(Global Memory)

全局内存是GPU中容量最大、延迟最高的内存空间,其作用域和生命空间都是全局的。一个全局内存变量可以在host代码中使用cudaMalloc函数进行动态声明,或者使用__device__修饰符在device代码中静态地进行声明。全局内存变量可以在任何SM设备中被访问到,其生命周期贯穿应用程序的整个生命周期。

特点:所有线程都可以访问;没有缓存

下面的例子展示了如何静态声明并使用全局变量:

#include <cuda_runtime.h>
#include <stdio.h>

__device__ float dev_data;

__global__ void AddGlobalVariable(void) {
  printf("device, global variable before add: %.2f\n", dev_data);
  dev_data += 2.0f;
  printf("device, global variable after add: %.2f\n", dev_data);
}

int main(void) {
  float host_data = 4.0f;
  cudaMemcpyToSymbol(dev_data, &host_data, sizeof(float)); //host拷贝数据值device
  printf("host, copy %.2f to global variable\n", host_data);
  AddGlobalVariable<<<1, 1>>>();
  cudaMemcpyFromSymbol(&host_data, dev_data, sizeof(float));//device拷贝数据值host
  printf("host, get %.2f from global variable\n", host_data);
  cudaDeviceReset();
  return 0;
}

上面的代码中需要注意的是,变量dev_data只是作为一个标识符存在,并不是device端的全局内存变量地址,所以不能直接使用cudaMemcpy函数把host上的数据拷贝到device端。不能直接在host端的代码中使用运算符&对device端的变量进行取地址操作,因为它只是一个表示device端物理位置的符号。但是在device端可以使用&对它进行取地址

不过我们可以使用如下函数来获取它的地址:

cudaError_t cudaGetSymbolAddress(void** devPtr, const void* symbol);

这个函数用于获取device端的全局内存物理地址,获取地址后,经过改造上述函数代码可改为:

#include <cuda_runtime.h>
#include <stdio.h>

__device__ float dev_data;

__global__ void AddGlobalVariable(void) {
  printf("device, global variable before add: %.2f\n", dev_data);
  dev_data += 2.0f;
  printf("device, global variable after add: %.2f\n", dev_data);
}

int main(void) {
  float host_data = 4.0f;
  float *dev_ptr = NULL;
  cudaGetSymbolAddress((void **)&dev_ptr, dev_data);
  cudaMemcpy(dev_ptr, &host_data, sizeof(float), cudaMemcpyHostToDevice);//host拷贝数据值device
  printf("host, copy %.2f to global variable\n", host_data);
  AddGlobalVariable<<<1, 1>>>();
  cudaMemcpy(&host_data, dev_ptr, sizeof(float), cudaMemcpyDeviceToHost);//device拷贝数据值host
  printf("host, get %.2f from global variable\n", host_data);
  cudaDeviceReset();
  return 0;
}

注意:在CUDA编程中,一般情况下device端的内核函数不能访问host端声明的变量,host端的函数也不能直接访问device端的变量,即使它们是在同一个文件内声明的。

3. CPU Host内存

对CUDA架构而言,主机端的内存被分为两种,一种是可分页内存(pageable memroy)页锁定内存(page-locked或 pinned)

  • 可分页内存 Pageable

可分页内存是使用malloc()或者new在主机上分配

  • 页锁定内存 Pinned(Page-locked)

页锁定内存是使用CUDA函数cudaMallocHost 或者cudaHostAlloc在主机内存上分配,cudaFreeHost()来释放

注意:cudaMalloc()是在GPU上分配内存

页锁定内存的重要属性是主机的操作系统将不会对这块内存进行分页和交换操作,确保该内存始终驻留在物理内存中。由于每个页锁定内存都需要分配物理内存,并且这些内存不能交换到磁盘上,所以页锁定内存比使用标准malloc()分配的可分页内存更消耗内存空间

下面是页锁定内存与可分页内存的拷贝时间的比较。

//锁页内存(page-locked或 pinned)与可分页内存(pageable memroy)比较

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include "iostream"
#include <stdio.h>

using namespace std;

#define COPY_COUNTS 10
#define MEM_SIZE 25 * 1024 * 1024

//函数功能:拷贝到device再拷贝回host,重复执行10次
//页锁定内存
float cuda_host_alloc_test(int size, bool up)
{
	//耗时统计
	cudaEvent_t start, stop;
	float elapsedTime;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);

	int *a, *dev_a;
	//在主机上分配页锁定内存
	cudaMallocHost((void **)&a, size * sizeof(*a));
	//在设备上分配内存空间
	cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));
	//计时开始
	cudaEventRecord(start, 0);

	for (int i = 0; i < COPY_COUNTS; i++)
	{
		//从主机到设备复制数据
		cudaMemcpy(dev_a, a, size * sizeof(*dev_a), cudaMemcpyHostToDevice);
		//从设备到主机复制数据
		cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
	}
	cudaEventRecord(stop, 0);
	cudaEventSynchronize(stop);
	cudaEventElapsedTime(&elapsedTime, start, stop);

	cudaFreeHost(a);
	cudaFree(dev_a);
	cudaEventDestroy(start);
	cudaEventDestroy(stop);

	return (float)elapsedTime / 1000;
}

//可分页内存
float cuda_host_Malloc_test(int size, bool up)
{
	//耗时统计
	cudaEvent_t start, stop;
	float elapsedTime;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);
	int *a, *dev_a;

	//在主机上分配可分页内存
	a = (int *)malloc(size * sizeof(*a));

	//在设备上分配内存空间
	cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));

	//计时开始
	cudaEventRecord(start, 0);

	//执行从copy host to device 然后再 device to host执行100次,记录时间
	for (int i = 0; i < COPY_COUNTS; i++)
	{
		//从主机到设备复制数据
		cudaMemcpy(dev_a, a, size * sizeof(*dev_a), cudaMemcpyHostToDevice);
		//从设备到主机复制数据
		cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
	}
	cudaEventRecord(stop, 0);
	cudaEventSynchronize(stop);
	cudaEventElapsedTime(&elapsedTime, start, stop);

	free(a);
	cudaFree(dev_a);
	cudaEventDestroy(start);
	cudaEventDestroy(stop);

	return (float)elapsedTime / 1000;
}

int main()
{
	float allocTime = cuda_host_alloc_test(MEM_SIZE, true);
	cout << "页锁定内存: " << allocTime << " s" << endl;
	float mallocTime = cuda_host_Malloc_test(MEM_SIZE, true);
	cout << "可分页内存: " << mallocTime << " s" << endl;
	return 0;
}

输出:

页锁定内存: 0.332271 s
可分页内存: 0.364879 s

自己测试下来,页锁定内存并没有起到多大的作用。。。


参考:
https://developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/

https://blog.csdn.net/chongbin007/article/details/123838980?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166753182216782395390699%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=166753182216782395390699&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-22-123838980-null-null.142^v63^control,201^v3^control_2,213^v1^t3_control2&utm_term=cuda内存&spm=1018.2226.3001.4187?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件管理的功能

文章目录什么是文件文件的属性文件内部的数据如何组织起来文件之间应该如何组织起来操作系统应该向上提供哪些功能从上往下看&#xff0c;文件应该如何存放在外存其他需要由操作系统实现的文件管理功能什么是文件 文件就是一组有意义的信息/数据集合 文件的属性 文件名&#x…

万字手撕AVL树 | 上百行的旋转你真的会了吗?【超用心超详细图文解释 | 一篇学会AVL】

说在前面 今天这篇博客&#xff0c;是博主今年以来最最用心的一篇博客。我们也很久没有更新数据结构系列了&#xff0c;几个月前博主用心深入的学习了这颗二叉平衡搜索树&#xff0c;博主被它的查找效率深深吸引。 AVL树出自1962年中的一篇论文《An_algorithm_for_the_organi…

计算机毕业设计(附源码)python职业高中智慧教学系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;我…

ASCII纯文本绘制流程图

我们使用纯文本写代码&#xff0c;有了Markdown又可以使用纯文本写文档&#xff0c;那么图片&#xff0c;能不能使用纯文本描述呢&#xff1f; Text Flow是什么&#xff1f; Text Flow&#xff1a;一个强大的在线ASCII流程图绘制工具&#xff0c;是程序员大佬们很喜爱的制作流…

【AI绘图】咒术师的评级指南

成为咒术师之路 python版本要选用3.9.7 C盘或系统缓存目录预留5G空间 咒术师评级 以下内容仅供参考。。 三级咒术师 理解咒言的使用&#xff0c;正向咒言&#xff0c;逆向咒言&#xff0c;构图要素的表达 二级咒术师 能够对咒物做后期调整&#xff0c;校正手部 一级咒术师…

Redis

1.概念:redis是一款高性能的NOSQL系列的非关系型数据库 关系型数据库&#xff1a;数据之间有关联关系&#xff0c;数据存储在硬盘的文件上 非关系型数据库&#xff1a;数据之间没有关联关系&#xff0c;数据存储在内存中 是一款用C语言开发…

猿创征文|一文吃透JAVA初学者的开发工具

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;乐趣国学的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;CSDN活动专…

【算法】网络最大流问题,三次尝试以失败告终

文章目录开始基本思路&#xff1a;“反悔”机制干活尝试一&#xff1a;深度优先搜索尝试二&#xff1a;少走弯路尝试三&#xff1a;最短增广路径&#xff0c;广度优先还是没ac记两个小bug1. 数组越界2. 写错变量名小结最后一个版本的代码&#xff08;C&#xff09;定义类与函数…

谷雨妹子要出国

文 / 谷雨&#xff08;微信公众号&#xff1a;王不留&#xff09; 作为出差在外的实施团队中的唯一一位女生&#xff0c;我可以独享一个单间&#xff0c;晚上的备考时候不会受影响&#xff0c;心里倒有点美嗞嗞的。 目前工作状态是 996&#xff08;早上九点到晚上九点&#xf…

常用短信平台一览,记得收藏哦

市面上的短信平台很杂很多&#xff0c;小到几个人的公司、大到腾讯、阿里这样的巨无霸都在做&#xff0c;但常用的就那么几个&#xff0c;因而用户的选择也存在不少的困惑。 在我看来&#xff0c;我觉得选择短信平台、在我看来有这几个需要的注意地方&#xff1a; 1、价格 无论…

Java:Session 会话详解

在介绍本篇的主角之前, 我们先复习一下 Cookie 为了实现在游览器的持久性存储和安全性考虑, 游览器提供了一个机制—— Cookie , Cookie 的储存空间很有限, 不同的游览器Cookie空间上限也不同, 一般总上限是 4k 个字节左右 (例如 Firefox), 其储存也只是按照域名进行分块存储, …

在众多编程语言中,我为什么要学Python?

前言 编程语言排行榜三剑客Java、C、C&#xff0c;长期统治榜首&#xff0c;今日python重回榜首 &#xff08;文末送福利&#xff09; python的前世今生 1、最新动态 TIOBE排行榜是根据互联网上有经验的程序员、课程和第三方厂商的数量&#xff0c;并使用搜索引擎&#xff…

【算法篇-动态规划】手撕各大背包问题 —— 01背包

背包问题1. 最基础的背包 —— 01背包 &#xff08;必看&#xff09;1.1 分析1.2 状态转移方程 和 边界条件1.3 代码1.3.1 代码模拟1.4 空间复杂度的优化1.4.1 错误的优化方式1.4.2 正确的优化方式1.5 终极版优化总结本文章参考自 B站 董晓算法 董晓算法 1. 最基础的背包 ——…

Linux下git和gdb的使用

&#x1f680;每日鸡汤&#xff1a;生活不相信眼泪&#xff0c;即使你把眼泪流成珍珠&#xff0c;灰暗的生活也不会因此而闪光。 目录 一、使用git命令行 1.1安装git、配置仓库 Ⅰ.gitignore Ⅱ.git 1.2git的基本使用 二、Linux调试器-gdb 2.1、gdb的使用 2.2、 debug与…

矩阵求导简记

很多机器学习算法都需要求解最值&#xff0c;比如最小二乘法求解样本空间相对拟合曲线的最短距离&#xff0c;最值的求解往往通过求导来计算&#xff0c;而机器学习中又常用矩阵来处理数据&#xff0c;所以很多时候会涉及到矩阵的求导。矩阵求导就像是线性代数和微积分的结合&a…

熬夜肝出囊括Java后端95%的面试题解析

为大家整理了一版java高频面试题&#xff0c;其实&#xff0c;一直有大佬在面试&#xff0c;不是在面试&#xff0c;就是在面试的路上&#xff0c;2022其实不是个适合跳槽的年份&#xff0c;稳稳当当当然好&#xff0c;但是&#xff0c;也别委屈自己呀&#xff0c;话不多说&…

Kotlin编程实战——与Java互操作(10)

一 概述 Kotlin 中调用 Java 代码Java 中调用 Kotlin 二 Kotlin 中调用 Java 代码 Getter 和 Setter返回 void 的方法将 Kotlin 中是关键字的 Java 标识符进行转义空安全与平台类型Java类型映射kotlin类型Kotlin 中的 Java 泛型Java 可变参数 三 Java 中调用 Kotlin 属性实…

【ELM预测】基于matlab探路者算法优化极限学习机预测(含前后对比)【含Matlab源码 2204期】

一、探路者算法简介 提出的一种新兴的智能优化算法&#xff0c;该算法的思想起源于群体动物的狩猎行为&#xff0c;种群中的个体分为探路者和跟随者两种角色。算法的寻优过程模拟了种群寻找食物的探索过程&#xff0c;利用探路者、跟随者两种角色不同的位置更新方式以及角色间…

NR/5G - PUSCH repetition次数

--- R15 DCI format 0-1 PUSCH 38.214中的描述&#xff0c;DCI format 0-1调度的PUSCH&#xff0c;包括C-RNTI/MCS-C-RNTI动态DCI调度PUSCH以及CS-RNTI&#xff0c;NDI1时候指示的Configured Grant的重传调度PUSCH&#xff0c;通过PUSCH-Config中的pusch-AggregationFactor指示…

谷粒学院——Day02【环境搭建和讲师管理接口开发】

前后端分离概念 传统单体结构 前后端分离结构 前后端分离就是将一个单体应用拆分成两个独立的应用&#xff1a;前端应用和后端应用&#xff0c;以JSON格式进行数据交互。 后台讲师管理模块环境搭建 一、数据库设计 数据库 guli_edu 数据库 guli_edu.sql # # Structure fo…