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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

提起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_remoteput_page实现的最新的内核新增了两个APIpin_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住的操作

结束

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux