Linux kernel Memory Pin机制的实现以及测试

news2024/9/30 9:21:22

提起Memory Pin机制,就不得不提到swap的概念,这两个概念息息相关,为了避免在CPU忙碌的时候,也就是在缺页异常发生的时候,临时搜索可供换出的内存页面并加以换出,Linux内核定期地检查系统的空闲页面数量是否小于预定义的极限,一旦发现空闲页面数太少,就预先将若干页面换出,以减轻缺页异常发生时系统所承受的负担,当然,由于无法确切地预测页面的使用,即使这样做了也还可能出现缺页异常发生时内存依然没有足够的空闲页面。但是,预换出毕竟能减少空闲页面不够用的利率。并且通过选择适当的参数,比如每隔多久换出一次,每次换出多少页,可以使临时寻找要换出页面的情况很少发生,为此,linux内核设置了一个专伺定期将页面换出的守护进程kswapd.kswapd的分析参考博客:

https://blog.csdn.net/tugouxp/article/details/119896712?spm=1001.2014.3001.5502

swap的原理是,当内存不足的时候,把最近很少访问的没有存储设备支持的物理页(其实就是匿名页)数据暂时保存到交换区,释放内存空间,当交换区中的存储页被访问的时候,再把数据从交换页读取到内存中。

Pin Memory

交换功能并不是在所有场景下就是需要的,以CUDA为例,熟悉cuda的同学一定知道cudaMallocHost函数,cudaMallocHost和malloc分配的都是主机端内存,但是他们是有区别的。cudaMallocHost函数用于分配页锁定内存,使用方法如下:

cudaMallocHost((void**)&pdataA, MATRIX_M * MATRIX_N * sizeof(int));
cudaHostGetDevicePointer((void**)&pdata_gpuA, (void*)pdataA, 0);

使用malloc分配的内存是swapable(交换页)的(malloc的都是匿名页),而上面的代码例子中,调用cudaHostGetDevicePointer的目的,实质是强制让分配得到的页面不参与页交换,目的是让一片用户

buffer永驻内存,从而提高系统应用效率。

下图是nvidia关于函数cudaHostGetDevicePointer的官方文档,可以明显看到pin memory的字眼。

如何操作一片用户memory为Pin Memory?

Linux内核提供了完善的pin memory API接口供开发者调用,可以将一块malloc得到的匿名内存区域设置为为pin memory,防止其被交换出去。

关于pin memory 操作的API,稍微老一点的内核是通过get_user_page, get_user_page_remote,put_page实现的,最新的内核新增了两个API,pin_user_pages和unpin_user_pages,用来完成PIN的功能。

下面我们就基于内核提供的API,实现一个将用户malloc内存pin住的用例,用例包含两个部分,分别为内核模块和用户态测试代码。

内核实现部分:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/spinlock.h>
#include <linux/blkdev.h>
#include <linux/module.h>  
#include <linux/fs.h>  
#include <linux/errno.h>  
#include <linux/mm.h>  
#include <linux/cdev.h>  
#include <linux/miscdevice.h>
#define MISC_NAME   "miscdriver"

static int temp_data = 0;

static int misc_open(struct inode *inode, struct file *file)
{
    printk("misc_open.\n");
    return 0;
}

static void page_count_output(struct page** page, int cnt)
{
    int i;

    for(i = 0; i < cnt; i ++)
    {
        printk("%s line %d, page count %d, page map count %d.\n", __func__, __LINE__, page_count(page[i]), page_mapcount(page[i]));
    }
}

static long misc_ioctl( struct file *file, unsigned int cmd, unsigned long arg)
{    
    switch(cmd)
    {
        case 0x100:
            if(copy_from_user(&temp_data,  (int *)arg, sizeof(int))) 
                return -EFAULT;
            break;
        
        case 0x101:
            if(copy_to_user( (int *)arg, &temp_data, sizeof(int))) 
                return -EFAULT;
            break;

        case 0x102:
        {
            int pined = 4;
            int ret, i;
            int page_cache_pins = 0;
            struct page *user_pages[4];

            ret = get_user_pages(arg, pined, FOLL_WRITE | FOLL_LONGTERM, user_pages, NULL);
            if(ret == pined) {
                printk("%s line %d, pined 4 user pages success.\n", __func__, __LINE__);
            } else {
                printk("%s line %d, pined 4 user pages failure.\n", __func__, __LINE__);
                return -EFAULT;
            }

            page_cache_pins = PageTransHuge(user_pages[0]) && PageSwapCache(user_pages[0]) ? 100:1;
            printk("%s line %d, arg = 0x%lx, %d, %d.\n", __func__, __LINE__, arg, page_has_private(user_pages[0]), page_cache_pins);

            page_count_output(user_pages, pined);
            //unpined
            for( i = 0; i < pined; i ++ ) {
                put_page(user_pages[i]);
            }
            page_count_output(user_pages, pined);

            break;
        }
    }
    
    //printk(KERN_NOTICE"ioctl CMD%d done!\n",temp);    
    return 0;
}


static const struct file_operations misc_fops = {
    .owner          =   THIS_MODULE,
    .open           =   misc_open,
    .unlocked_ioctl = misc_ioctl,
};

static struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = MISC_NAME,
    .fops  = &misc_fops,
};


static int __init misc_init(void)
{
    int ret;
    
    ret = misc_register(&misc_dev);
    if (ret)
    {
        printk("misc_register error.\n");
        return ret;
    }

    return 0;
}

static void __exit misc_exit(void)
{
    misc_deregister(&misc_dev);
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("czl");

测试用例:

#include <stdio.h>  
#include <fcntl.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/types.h>  
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main(void)
{
    int fd;
    int ret;
    int wdata, rdata;
    void *ptr;

    fd = open("/dev/miscdriver", O_RDWR);
    if( fd < 0 ) {
        printf("open miscdriver WRONG!\n");
        return 0;
    }

    ret = ioctl(fd, 0x101, &rdata);
    printf("ioctl: ret=%d rdata=%d\n", ret, rdata);

    wdata = 42;
    ret = ioctl(fd, 0x100, &wdata);

    ret = ioctl(fd, 0x101, &rdata);
    printf("ioctl: ret=%d rdata=%d\n", ret, rdata);

    ptr = malloc(16 * 1024);
    if(ptr == NULL) {
        printf("%s line %d, malloc failure.\n", __func__, __LINE__);
    ret = -1;
    }

    printf("%s line %d, ptr = %p.\n", __func__, __LINE__, ptr);

    ret = ioctl(fd, 0x102, (unsigned long)ptr);

    free(ptr);
    
    close(fd);
    return ret;
}

Makefile:

ifneq ($(KERNELRELEASE),)
obj-m:=miscdriver.o
else
KERNELDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
 
clean:
    rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.mod .*.cmd *.order
endif

运行结果:

Memory Pin机制是通过page->_refcount成员发挥作用的,其核心逻辑上对_refcount进行递增操作,使其不满足swapout的条件,在swap的关键流程节点pageout一步中,会对page是否swapable进行判断,以此方式来阻止指定页面被交换出去。关键流程如下图所示:

为何CUDA用的HOST内存一定要PIN的?

PIN内存不能换出,linux kernel内核函数pageout函数再进行页面判断的时候,会调用is_page_cache_freeable检查页面是否符合换出条件,如果发现是pin page memory,就直接返回不会调用swapper_writepage将页面换出。

方式则是很老套的检查page计数,关于page计数的逻辑后续在分析,总之,通过这个函数过滤,经过pin的内存就不会再被swap out掉了。

从注释中可以知道,可以swap出去的页面的引用计数有三个特征。

1.由分配(isolated)发起的引用计数+1,在alloc_pages的调用栈中.(prep_new_page函数)

2.由page cache引起的计数器递增+1,在handle page fault处理的过程__lru_cache_add中。

3.作为buffer cache 页面指向buffer_head结构时 +1.

所以作为匿名页面,只有1和2条件满足,可以交换出去的页面引用计数为2,对于buffer cache,则引用计数为3,比如文件交换到back file,此时的计数为3.

注意这个条件是充分且必要的,即便对于那种驱动分配的页面,由于其没有2和3,引用计数为1,也不会发生交换出去的操作。

交换的最终目的是页面的回收,并非内存中所有的页面都是可以交换出去的,只有与用户空间建立了映射关系的物理页面才会被换出去,而内核空间中的内核所占用的页面则常驻内存。这部分就包括用alloc_pages分配的页面。

从这个角度看,用户态进程的堆空间和代码空间(page private不为空,refcount为3)都可以swap出去。

那么,为什么GPU端一定要PIN memory呢?原因除了提高效率之外,恐怕最重要的一点是,当GPU访问的PAGE被换出后,无法像CPU端那样支持将page swap in进来。CPU操作系统支持page fault,并且MMU page walk也支持检测这种换出类型的swap pte item并上报CPU,这一套逻辑,GPU都不一定具备,所以,cuda用的HOST内存,一定要pin 住的。

主机(CPU)数据分配的内存默认是可分页的。GPU不能直接访问可分页的主机内存,所以当从可分页内存到设备内存的进行数据传输时,CUDA驱动必须首先分配一个临时的不可分页的或者固定的主机数组,然后将主机数据拷贝到固定数组里,最后再将数据从固定数组转移到设备内存,如下图所示:

pin memory的释放

在4.x-5.6的内核上,pin memory的释放是通过put_page实现的,在最新的内核上则新增了一个佳作unref_pin_page的API专门负责 pin memory的释放,重点是pin meomory是通过page结构的引用计数来实现的,这一点只有在put_page的函数实现中比较明显,见下图:

用户态mlock/munlock函数和和memory pin的联系

关于用户态常用的操作memory的函数总结如下:

void *malloc (size_t);
void free (void *);
void *mmap (void *, size_t, int , int , int , off_t);
int munmap (void *, size_t);

int mprotect (void *, size_t, int);
int msync (void *, size_t, int);

int mlock (const void *, size_t);
int munlock (const void *, size_t);
int mlockall (int);
int munlockall (void);

void *mremap (void *, size_t, size_t, int, ...);
int remap_file_pages (void *, size_t, int, size_t, int);

其中的mlock/munlock做的事情本质上和上面的用例类似,都是将一片用户内存作为pin memory防止交换的发生。mlock/munlock在musl libc中中的实现如下:

在内核中,则是通过get_free_page/put_page实现的。

内核中其它模块应用pin memory的例子

窃以为,Linux内核所有模块中对Memory Buffer的管理最复杂,花样最多的,应该是V4L2模块了,这并不是随口乱讲,要知道,最早的DMABUF机制的开发者,就是V4L2模块的维护者。在V4L2模块中,涉及了非常多的用户态和内核态共享buffer的实现要求。自然关于memory pin机制,在V4L2中也有出现,下面几张图展示了用户态调用V4L2_MEMORY_USERPTR 将buffer pin住的操作:

结束

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

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

相关文章

九大数据分析方法-单指标分析方法与多指标分析方法

文章目录1 单指标分析方法1.1 周期性分析法1.2 结构分析法1.3 分层分析法2 多指标分析方法2.1 矩阵分析法2.2 指标拆解法本文来源&#xff0c;为接地气的陈老师的知识星球&#xff0c;以及付同学的观看笔记。1 单指标分析方法 顾名思义&#xff0c;用单个数据指标进行数据分析…

RocketMQ 简介

一、简介 官方简介&#xff1a; l RocketMQ是一款分布式、队列模型的消息中间件&#xff0c;具有以下特点&#xff1a; l 能够保证严格的消息顺序 l 提供丰富的消息拉取模式 l 高效的订阅者水平扩展能力 l 实时的消息订阅机制 l 亿级消息堆积能力 二、网络架构 三、特性 1. na…

05-jquery基本过滤器

2.5过滤器 过滤器是一个字符串&#xff0c;用了筛选dom对象&#xff0c;过滤器是和选择器一起使用。在选择dom对象后&#xff0c;再进行过滤筛选。 .5.1基本过滤器 使用dom对象在数组中的位置&#xff0c;作为过滤条件。 1 选择数组中第一个dom成员。 语法&#xff1a;$(“选…

【nginx】全面实战-Mac

▒ 目录 ▒&#x1f6eb; 导读需求1️⃣ 安装brew替换为阿里源修复报错No such file or directorybrew install nginx常用文件及目录常用命令2️⃣ nginx配置配置结构3️⃣ web服务器默认服务器自定义静态服务器4️⃣ 反向代理配置及介绍5️⃣ 负载均衡配置及介绍负载均衡的策略…

杂记:python和pyinstaller从头安装步骤(附安装包的备份)

pyinstaller 简介 知道的就跳过本章 python 属于脚本语言&#xff0c;只要有 python 就能运行 .py 文件。而 pyinstaller 是可执行文件文件生成工具&#xff0c;约等于编译工具。 以 windows 为例&#xff0c;在 A 计算机上生成的 exe&#xff0c;复制到 B 计算机可以直接运行…

字节跳动青训营--前端day3

文章目录前言一、写好JavaScript的一些原则二、各司其职三、组件封装四、过程抽象前言 仅以此文章记录学习 一、写好JavaScript的一些原则 各司其职&#xff1a;让HTML、CSS和JavaScript职能分离组件封装&#xff1a;好的UI组件具备正确性、扩展性、复用性过程抽象&#xff1…

JVM堆内存分配策略(深入理解Java虚拟机第三章)

堆内存模型&#xff1a; 年轻代&#xff1a; 根据分代算法&#xff0c;默认小于15岁的对象称作年轻代&#xff0c;年轻代分为Eden区、幸存者区(Survivor Form&#xff0c;Survivor To),三者比例为&#xff1a;8&#xff1a;1&#xff1a;1 Eden 分区&#xff1a;对象出生分区…

Java 实现几种 异步的实现方式

前言 异步执行对于开发者来说并不陌生&#xff0c;在实际的开发过程中&#xff0c;很多场景多会使用到异步&#xff0c;相比同步执行&#xff0c;异步可以大大缩短请求链路耗时时间&#xff0c;比如&#xff1a;发送短信、邮件、异步更新等&#xff0c;这些都是典型的可以通过…

FL水果21最新版本电脑编曲软件FL Studio更新

电脑编曲软件也就是我们常说的宿主软件&#xff0c;英文简称DAW。 FL Studio俗称水果&#xff0c;是一款开发初衷为了电子音乐制作的宿主软件。内置了非常多优秀的合成器以及效果器插件&#xff0c;极为适合于电子音乐的编排。同时FL Studio支持第三方音源插件导入&#xff0c;…

[RootersCTF2019]ImgXweb

目录 信息收集 JWT伪造 工具使用 寻找秘钥 curl 补充知识 信息收集 进入查看源码未发现重要信息 注册admin失败&#xff0c;猜测应该需要垂直越权 先随意注册个账户coleak 登录后发现可以文件上传&#xff0c;上传shell后发现不能连接&#xff0c;执行命令也没有回显…

Linux常用命令——tailf命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) tailf 在屏幕上显示指定文件的末尾若干行内容&#xff0c;通常用于日志文件的跟踪输出。 补充说明 tailf命令几乎等同于tail -f&#xff0c;严格说来应该与tail --followname更相似些。当文件改名之后它也能继…

微服务注册中心-Eureka

微服务注册中心-Eureka微服务注册中心-Eureka一、注册中心&#xff08;Eureka&#xff09;二、Euraka实现1.eureka-server端&#xff08;1&#xff09;新建项目&#xff0c;引入pom依赖&#xff08;2&#xff09;编写启动类&#xff0c;引入开启注册中心的注解&#xff08;3&am…

py第八章 面向对象 笔记

8.1类与对象的基础运用类是抽象的&#xff0c;对象是类的实例8.1.1类的定义class 类名:属性名属性值def 方法名(self):方法体类名:大驼峰命名法&#xff1a;首字母一般为大写方法参数列表中的第一个参数是一个指代对象的默认参数selfclass Car:wheels4def drive(self):print(行…

非常好用,绝对未来黑马的 Python 开源测试框架 PySimpleTest

非常好用&#xff0c;绝对未来黑马的 Python 开源测试框架 PySimpleTest 简单介绍 PySimpleTest 是一个非常简洁开源的python代码测试框架&#xff0c;作者是 https://github.com/Time-Coder?tabstars 你可以在以下网站找到PyPI索引&#xff1a;https://pypi.org/project/P…

CSS 排行榜

CSS 排行榜 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>排行榜</title><style type"text/css">* {margin: 0;padding: 0;}/* 容器 */.rank-container {width: 400px;color: #333;font-size: 14p…

go 常用命令

巩固学习最好的方法是通过go help看文档 GO语言规范文档 终端执行命令 go help environment GOBIN The directory where ‘go install’ will install a command. go 命令使用 go <command> [arguments]command&#xff1a; The commands are:bug start a b…

十、MySQL 聚合函数、分组查询及过滤分组

文章目录一、聚合函数1.1 COUNT()函数1.2 SUM()函数1.3 AVG()函数1.4 MAX()函数1.5 MIN()函数二、分组查询及过滤分组2.1 创建分组2.2 使用HAVING过滤分组2.3 WHERE和HAVING的对比前置知识&#xff1a; 一、数据库开发与实战专栏导学及数据库基础概念入门 二、MySQL 介绍及 MyS…

你知道这些快捷键吗?

今天就给大家带来常用的电脑快捷键&#xff0c;让你的办公和学习效率加倍&#xff0c;一起来看看吧&#xff01; 快捷键一&#xff1a;快速切换窗口 想要快速切换电脑正在使用的窗口程序&#xff0c;就可以使用“AltTab”键即可快速切换。先按住“Alt”按键&#xff0c;再按“T…

GO语言的实战学习(猜谜游戏和在线词典)| 青训营笔记

一.GO语言的实战学习 1.1 前言 在上文我们急速学习了Go语言的入门&#xff0c;今天我们来学习一下Go语言的实战 本专栏代码&#xff0c;源码打包下载地址如下&#xff1a; https://download.csdn.net/download/weixin_52908342/87389481 二.猜谜游戏 1.导入依赖包&#xff…

模拟实现一个简单的命令行解释器(shell)

目录 前言 环境变量与本地变量 和环境变量相关的命令 获取环境变量的三种方法 第一种 第二种 第三种 进程地址空间 页表 为什么存在进程地址空间 第一 第二 第三 进程控制 进程的产生 进程终止 进程等待 进程替换 模拟实现一个shell 前言 我们通过各种指令来实现…