内存锁定(memory locking)是确保进程保留在主内存中并且免于分页的一种方法。在实时环境中,系统必须能够保证将进程锁定在内存中,以减少数据访问、指令获取、进程之间的缓冲区传递等的延迟。锁定内存中进程的地址空间有助于确保应用程序的响应时间满足实时要求。作为一般规则,时间关键的进程(time-critical process)应该被锁定到内存中。
虚拟地址空间(virtual address space)被划分为固定大小的单元(fixed-sized unit),称为页(page)。每个进程通常占用多个页,这些页在进程执行时独立地移入和移出主内存。通常,当进程执行时,进程页的子集驻留在主内存(primary memory)中。
由于可用的主内存量是有限的,因此分页通常会牺牲一些页即要移入页,必须移出其他页。如果要替换的页在执行期间被修改,则该页将被写入文件区域。该页根据需要被带回主内存,并且在内核检索该页时延迟执行。
分页通常对当前进程是透明的。通过增加物理内存的大小或将页锁定到内存中可以减少分页量。但是,如果进程非常大或者页频繁调入和调出,则调页所需的系统开销可能会降低效率。
对于实时应用程序来说,拥有足够的内存比非实时应用程序更重要。实时应用程序必须确保进程被锁定到内存中,并且有足够的内存可供实时进程和系统使用。对于关键的实时任务来说,由于分页引起的延迟通常是不可接受的。
内存锁定适用于进程的地址空间。只有映射到进程地址空间的页才能被锁定到内存中。当进程退出时,页将从地址空间中删除,并且锁也被删除。
内存锁定有两个主要应用:实时算法和高安全性数据处理。
页锁定内存无法从RAM移动到交换文件,确保该内存始终驻留在物理内存中。与传统内存相比,它提高了GPU的PCI-Express(CPU和GPU之间的总线) I/O速度。
NVIDIA的GPU是一个协处理器(co-processor),GPU内核启动、数据初始化和传输均由CPU进行.
CUDA中有四种类型的内存分配:
1.Pageable memory(可分页内存):主机中分配的内存默认是可分页内存。该内存位置的数据可供主机使用。为了将此数据传输到设备,CUDA运行时会将此内存拷贝到临时固定内存,然后传输到设备内存。因此,存在两次内存传输。因此,这种类型的内存分配和传输速度很慢。GPU无法直接从可分页主机内存访问数据。涉及的函数:malloc, cudaMalloc, cudaMemcpy
2.Pinned memory(固定内存):也称为页锁定,数据可以直接在主机固定存储器中初始化。与malloc分配的常规可分页主机内存相反。通过这样做,我们可以避免像可分页内存中那样的两次数据传输。这将使该过程更快,但会牺牲主机性能。当数据在固定存储器中初始化时,主机处理的存储器可用性会降低。涉及的函数:cudaMallocHost, cudaMalloc, cudaMemcpy
优点:
(1).对于某些设备,页锁定主机内存和设备内存之间的拷贝可以与内核执行同时执行。
(2).在某些设备上,页锁定主机内存可以映射到设备的地址空间,从而无需将其拷贝到设备内存或从设备内存中拷贝。
(3).在具有前端总线(front-side bus)的系统上,如果主机内存被分配为页锁定,则主机内存和设备内存之间的带宽(bandwidth)会更高。
注:页锁定主机内存不会缓存在非I/O一致Tegra设备上。此外,非I/O一致Tegra设备不支持cudaHostRegister。
3.Mapped memory(映射内存)或Zero copy memory(零拷贝内存):零拷贝内存是映射到设备地址空间的固定内存。主机和设备都可以直接访问该内存。涉及的函数:cudaHostAlloc, cudaHostGetDevicePointer
优点:
(1).当设备内存不足时,可以利用主机内存。
(2).可以避免主机和设备之间显式的数据传输。
(3).提高PCI-Express传输速率。
缺点:由于它被映射到设备地址空间,因此数据不会被拷贝到设备内存中。传输将在执行期间发生,这将大大增加处理时间。
4.Unified memory(统一内存):这将创建一个托管内存池,其中来自该内存池的每个分配都可以在主机和设备上使用相同的地址或指针进行访问。底层系统将数据迁移到主机和设备。涉及的函数:cudaMallocManaged
优点:无需为所需的设备显式分配和恢复内存。这降低了编程复杂性。
缺点:在内存管理方面添加了额外的指令.
RAM(Random Access Memory):随机存取存储器,是一种计算机存储器,它用于临时存储数据和指令,以便在计算机运行时快速访问。RAM可以随时读取和写入数据,而且读取和写入的速度非常快。相比之下,硬盘、闪存等存储设备的数据访问速度要慢得多。RAM通常用于计算机的主存(内存),以便CPU可以快速访问指令和数据。它也用于缓存和其他高速存储设备,以加快数据访问速度。
linux下的用于内存锁定的函数mlock/mlockall, munlock/munlockall声明如下:
int mlock(const void *addr, size_t len);
int munlock(const void *addr, size_t len);
int mlockall(int flags);
int munlockall(void);
mlock:锁定从addr开始并持续len字节的地址范围内的页。当调用成功返回时,所有包含指定地址范围的页都保证驻留在RAM中,直到稍后解锁。
munlock:解锁从addr开始并持续len字节的地址范围内的页。在此调用之后,包含指定内存范围的所有页都可以由内核再次移动到外部交换空间。
mlockall:锁定映射到调用进程的地址空间的所有页。This includes the pages of the code, data and stack segment, as well as shared libraries, user space kernel data, shared memory, and memory-mapped files. 当调用成功返回时,所有映射的页都保证驻留在RAM中,直到稍后解锁。
munlockall:解锁映射到调用进程地址空间的所有页.
mlock和mlockall函数分别将调用进程的部分或全部虚拟地址空间(virtual address space)锁定到RAM中,防止该内存被分页到交换区域(preventing that memory from being paged to the swap area)。munlock和munlockall执行相反的操作,分别解锁调用进程的部分或全部虚拟地址空间,以便在内核内存管理器(kernel memory manager)需要时可以再次交换指定虚拟地址范围内的分页。
内存锁定和解锁以整页(whole page)为单位进行。
这些函数调用成功时返回0;失败时返回-1,设置errno来指出错误。
注意:
1.内存锁定不会被通过fork创建的子进程继承(注:现代的Unix系统可能并非这样)。
2.如果通过munmap取消映射,则会自动删除地址范围(address range)上的内存锁定。
3.如果锁定了非常多的内存,有些程序可能会因为缺少内存(real memory)而根本无法运行,或者导致系统运行速度更慢。
4.内存锁定不堆叠(memory locks do not stack),不能两次锁定特定页。
5.内存锁定持续存在,直到进程退出或应用程序调用相应的munlock或munlockall函数解锁它为止。
以上内存主要整理自:
1.https://man7.org/linux/man-pages/man2/mlock.2.html
2.https://medium.com/analytics-vidhya/cuda-memory-model-823f02cef0bf
3.https://www3.physnet.uni-hamburg.de/physnet/Tru64-Unix/HTML/APS33DTE/DOCU_005.HTM
4.https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#page-locked-host-memory
在Windows上可使用VirtualAlloc、VirtualLock、VirtualUnlock、VirtualFree函数进行内存锁定。
以下为测试代码:
int test_memory_locking()
{constexpr size_t size{ 1024 };
#ifdef _MSC_VER// 1. allocate memoryauto p = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);if (p == nullptr) {std::cerr << "Error: VirtualAlloc: " << GetLastError() << "\n";return -1;}// 2. lock memory pageif (!VirtualLock(p, size)) {std::cerr << "Error: VirtualLock: " << GetLastError() << "\n";return -1;}// 3. use lock memory// 4. unlock memory pageif (!VirtualUnlock(p, size)) {std::cerr << "Error: VirtualUnlock: " << GetLastError() << "\n";return -1;}// 5. free memoryif (!VirtualFree(p, 0, MEM_RELEASE)) {std::cerr << "Error: VirtualFree: " << GetLastError() << "\n";return -1;}
#elsechar data[size];// 1. get configuration information at run timeauto page_size = sysconf(_SC_PAGE_SIZE);if (page_size == -1) {std::cerr << "Error: sysconf: " << strerror(errno) << "\n";return -1;}std::cout << "page size: " << page_size << "\n";// 2. lock memory pageif (mlock(data, size) == -1) {std::cerr << "Error: mlock: " << strerror(errno) << "\n";return -1;}// 3. use lock memory// 4. unlock memory pageif (munlock(data, size) == -1) {std::cerr << "Error: munlock: " << strerror(errno) << "\n";return -1;}
#endifreturn 0;
}
执行结果如下图所示:
GitHub:https://github.com/fengbingchun/Messy_Test