驱动程序中的物理内存通过mmap机制映射到用户空间,用户空间得到虚拟内存地址然后进行相关数据的读写操作
引言
在用户空间中,我们并不能直接访问驱动程序空间中的数据,但有时用read和write函数去调用驱动中的read操作函数和write操作函数会感觉麻烦且低效,这个时候我们可以先把驱动程序中的数据的物理内存地址得到,然后再利用用户空间中的内核函数mmap
和内核空间中的函数remap_pfn_range
映射到用户空间中,映射完成后就得到了数据的虚拟地址的指针,然后就可以用指针的形式对数据进行操作了。
完整源代码
驱动程序hello_drv.c
中的代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/pgtable.h>
#include <linux/mm.h>
#include <linux/slab.h>
/* 1. 确定主设备号 */
static int major = 0;
static char *kernel_buf;
static struct class *hello_class;
static int bufsiz = 1024*2;
#define MIN(a, b) (a < b ? a : b)
/* 3. 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(bufsiz, size));
return MIN(bufsiz, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_mmap(struct file *file, struct vm_area_struct *vma)
{
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置Cache(高速缓存)属性*/
// vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/* 设置Cache(高速缓存)属性 */
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* map */
if (remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
printk("mmap remap_pfn_range failed\n");
return -ENOBUFS;
}
return 0;
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 2. 定义自己的file_operations结构体 */
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
.mmap = hello_drv_mmap,
};
/* 4. 把file_operations结构体告诉内核:注册驱动程序 */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init hello_init(void)
{
int err;
// kernel_buf = kmalloc(bufsiz, GFP_KERNEL);
// 由于kmalloc分配的物理内存不一定是页对齐的,所以改用 __get_free_pages
// __get_free_pages 能保证分配的物理内存一定是页对齐的
kernel_buf = (char *)__get_free_pages(GFP_KERNEL, get_order(bufsiz));
strcpy(kernel_buf, "This is an old string."); // 字符串末尾会自动加上 '\0'哈
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv);
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello_01"); /* /dev/hello_01 */
return 0;
}
/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数 */
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
//kfree(kernel_buf);
if (kernel_buf) {
// 使用 free_pages 释放内存
free_pages(kernel_buf, get_order(bufsiz)); // 根据原始分配大小计算页数
// printk(KERN_INFO "Memory freed at address: %lx\n", kernel_buf);
}
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
测试程序map_test.c中的代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
/*
* ./hello_drv_test
*/
int main(int argc, char **argv)
{
int fd;
char *buf_map_pointer;
int len; // 利用read函数从从驱动程序空间中的kernel_buf中读取到数据长度
int map_size = 1024 * 2; // 映射的内存大小
char read_buffer_1[1024]; // 存储读到的数据
char read_buffer_2[1024]; // 存储读到的数据
char read_buffer_3[1024]; // 存储读到的数据
char read_buffer_4[1024]; // 存储读到的数据
/* 1. 打开设备文件 */
fd = open("/dev/hello_01", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello_01\n");
return -1;
}
/* 2. mmap 映射内存 */
/* MAP_SHARED : 多个进程都调用mmap映射同一块内存时, 对内存的修改大家都可以看到感受到,
* 就是说多个进程实际上访问的都是同一块内存。
* MAP_PRIVATE : 进行映射操作时,以要映射的内存区域为模板,重新分配一个与这个模板相同大小的内存区域进行映射,
* 假如之前模板区域有数据,那么模板区域的数据并不会复制到这个新分配的区域,
* 映射完成后数据的读写操作都在这个新分配的区域进行,
* 其它进程如果也是映射到模板内存区域,那是看不到这块新分配的内存区域的数据的,
* 相当于这个新分配的内存区域这个进程私有的。
*/
// 采用MAP_SHARED形式映射的语句
// buf_map_pointer = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 采用MAP_PRIVATE形式映射的语句
buf_map_pointer = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (buf_map_pointer == MAP_FAILED)
{
printf("can not mmap file /dev/hello_01\n");
close(fd);
return -1;
}
/*-----------------------------------------*/
/* 第1次读取数据:使用用户空间中的read函数调用驱动中的read操作函数读取驱动程序中kernel_buf里的数据 */
len = read(fd, read_buffer_1, sizeof(read_buffer_1) - 1); // 注意:结合驱动程序中的read操作函数来看,这里的第2个参数表示要读取的数据长度
// 假如复制得到的数据并没有以'\0' 结尾,则要确保以 '\0' 结尾,因为这里是当成字符串处理
// 详情请参阅我的博文 https://blog.csdn.net/wenhao_ir/article/details/144671071
read_buffer_1[sizeof(read_buffer_1) - 1] = '\0';
/* 第1次打印读取到的内容 */
/* 假如读到的以字符串开头的数据中字符串的长度很短,远远小于sizeof(read_buffer)的长度,那也无所谓,因为printf会以第1个遇到的 '\0' 截断数据。*/
printf("The first read(use read_fun): %s\n", read_buffer_1);
/*-----------------------------------------*/
/*-----------------------------------------*/
/* 第2次读取数据:从映射内存读取字符串 */
strncpy(read_buffer_2, buf_map_pointer, sizeof(read_buffer_2) - 1); // 注意:strncpy遇到'\0'便会停止复制,所以第3个参数代表最大复制长度
// 假如复制得到的数据并没有以'\0' 结尾,则要确保以 '\0' 结尾,因为这里是当成字符串处理
// 详情请参阅我的博文 https://blog.csdn.net/wenhao_ir/article/details/144671071
read_buffer_2[sizeof(read_buffer_2) - 1] = '\0';
/* 第2次打印读取到的内容 */
printf("The second read(use map_pointer): %s\n", read_buffer_2);
/*-----------------------------------------*/
/*-----------------------------------------*/
/* 写入字符串到映射的内存区域 */
printf("\nNow write a string to the mapping area through the mapping pointer....\n\n");
strcpy(buf_map_pointer, "This is a new string."); // strcpy函数会自动在末尾加上'\0'
/*-----------------------------------------*/
/*-----------------------------------------*/
/* 第3次读取数据:使用用户空间中的read函数调用驱动中的read操作函数读取驱动程序中kernel_buf里的数据 */
len = read(fd, read_buffer_3, sizeof(read_buffer_3) - 1); // 注意:结合驱动程序中的read操作函数来看,这里的第2个参数表示要读取的数据长度
// 假如复制得到的数据并没有以'\0' 结尾,则要确保以 '\0' 结尾,因为这里是当成字符串处理
// 详情请参阅我的博文 https://blog.csdn.net/wenhao_ir/article/details/144671071
read_buffer_3[sizeof(read_buffer_3) - 1] = '\0';
/* 第3次打印读取到的内容 */
/* 假如读到的以字符串开头的数据中字符串的长度很短,远远小于sizeof(read_buffer)的长度,那也无所谓,因为printf会以第1个遇到的 '\0' 截断数据。*/
printf("The Third read(use read_fun): %s\n", read_buffer_3);
/*-----------------------------------------*/
/*-----------------------------------------*/
/* 第4次读取数据:从映射内存读取字符串 */
strncpy(read_buffer_4, buf_map_pointer, sizeof(read_buffer_4) - 1); // 注意:strncpy遇到'\0'便会停止复制,所以第3个参数代表最大复制长度
// 假如复制得到的数据并没有以'\0' 结尾,则要确保以 '\0' 结尾,因为这里是当成字符串处理
// 详情请参阅我的博文 https://blog.csdn.net/wenhao_ir/article/details/144671071
read_buffer_4[sizeof(read_buffer_4) - 1] = '\0';
/* 第4次打印读取到的内容 */
printf("The fourth read(use map_pointer): %s\n", read_buffer_4);
/*-----------------------------------------*/
/* 6. 释放映射和关闭文件 */
munmap(buf_map_pointer, map_size);
close(fd);
return 0;
}
关键函数mmap()详解
mmap
是一个用于内存映射的系统调用函数,允许将一个文件或设备的内容映射到进程的虚拟内存空间中。通过这种方式,程序可以直接通过指针访问文件内容或设备内存,而不需要反复调用 read
或 write
。
mmap
的原型
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明
-
addr
- 映射的起始地址,通常传
NULL
,由内核选择合适的虚拟地址。 - 如果指定了非
NULL
值,内核会尽量将映射放在指定地址,但不一定能保证。
- 映射的起始地址,通常传
-
length
- 映射区域的大小(字节数)。通常需要是页面大小的倍数(4KB 或系统的页大小)。
-
prot
- 映射区域的访问权限,可以组合使用以下宏:
PROT_READ
:页面可读。PROT_WRITE
:页面可写。PROT_EXEC
:页面可执行。PROT_NONE
:页面不能访问。
- 映射区域的访问权限,可以组合使用以下宏:
-
flags
- 控制映射区域的属性,常见值包括:
MAP_SHARED
:共享映射,映射时不会以被映射对象为模板,去映射一块新分配的私有内存区域。详细的解释请见我在运行测试程序时对运行结果的解释。MAP_PRIVATE
:私有映射。映射时会以被映射对象为模板,去分配一块新的私有内存区域然后对这块新分配的内存区域进行映射操作,数据的读写都在这块新的私有内存区域进行。详细的解释请见我在运行测试程序时对运行结果的解释。MAP_ANONYMOUS
:匿名映射,不与设备文件关联,常用于分配私有内存。
- 控制映射区域的属性,常见值包括:
-
fd
- 被映射文件的文件描述符(通过
open
获取)。 - 如果
flags
包含MAP_ANONYMOUS
,则fd
应设为-1
。
- 被映射文件的文件描述符(通过
-
offset
- 文件映射的起始偏移量,必须是页大小的倍数。
返回值
- 成功:返回映射区域的起始地址(虚拟地址)。
- 失败:返回
MAP_FAILED
(即(void *) -1
),错误原因通过errno
获取。
代码详解
首先在用户空间的测试程序中,调用内核函数mmap()
,并传入相关映射参数表示我要进行映射参数,相关代码如下:
buf_map_pointer = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
关于这个函数的各参数的意义请参考上面对“函数mmap()的详解”。
函数mmap()
会去调用驱动程序提供的mmap
操作函数,并向操作函数传入参数struct file *file
和struct vm_area_struct *vma)
,相关代码如下:
然后咱们自然就是要看mmap操作函数代码的代码了,如下:
static int hello_drv_mmap(struct file *file, struct vm_area_struct *vma)
{
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置属性: cache, buffer */
// vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/* 设置属性: noncache */
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* map */
if (remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
printk("mmap remap_pfn_range failed\n");
return -ENOBUFS;
}
return 0;
}
首先要获得驱动程序中的数组kernel_buf
的物理地址,因为后面利用函数remap_pfn_range
在进行物理地址映射到用户空间的操作时,需要用到物理地址,代码如下:
unsigned long phy = virt_to_phys(kernel_buf);
从后面对函数remap_pfn_range
的介绍中,我们知道在这里,物理地址phy
要求是页对齐的,关于什么是页对齐的,请参考我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144985338
那怎么保证这里的phy
是页对齐的呢?
那显然就要在kernel_buf
上的初始化上做文章了,在本文的模块初始化函数的代码中,我使用了内核函数__get_free_pages
来实现kernel_buf
对应的物理地址是页对齐的,相关代码如下:
kernel_buf = (char *)__get_free_pages(GFP_KERNEL, get_order(bufsiz));
其中 get_order(bufsiz)
会根据 bufsiz 计算需要的页数,当页数不够1页时它会向上取整到1,当不够2页时,它会向上取整到2页…以此类推。
在开发板提供的教程中,用的是kmalloc
函数得到kernel_buf
的值,但kmalloc
函数分配内存空间时,并不能保证对应的物理地址是页对齐的(一页为4KB),只能保证是行对齐的(一行为64字节),只能当kmalloc
函数要分配的大小为4KB(即4096字节)的整数倍时,它分配得到的kernel_buf
在物理内存上才是页对齐的,而我这里设备的bufsiz
的大小只有2048字节,代码如下:
虽然有可能kmalloc
函数分配得到的kernel_buf
的值为页对齐的,但为了程序的健壮性,还是建议使用函数__get_free_pages
实现kernel_buf
的初始化。
函数__get_free_pages
分配的内存在模块卸载时需要被释放,否则系统会报错,释放代码如下:
if (kernel_buf) {
// 使用 free_pages 释放内存
free_pages(kernel_buf, get_order(bufsiz)); // 根据原始分配大小计算页数
// printk(KERN_INFO "Memory freed at address: %lx\n", kernel_buf);
}
然后设置映射时的Cache(高速缓存)属性、写缓冲器属性:
/* 设置Cache(高速缓存)属性 */
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
关于映射时的Cache(高速缓存)属性设置、写缓冲器属性的设置详细介绍请参见我的另一篇博文:
https://blog.csdn.net/wenhao_ir/article/details/145354997
上面这篇博文还详细介绍了什么叫Cache(高速缓存)、什么叫写缓冲器;还介绍了CPU核心、Cache(高速缓存)、写缓冲器、主存之间的关系。
上面这篇博文有时间的话建议看一看。当你看了上面这篇博文后,就对上面这句代码有详细的了解了,故这里略去对它的介绍。
Cache(高速缓存)属性设置完之后,就利用函数remap_pfn_range
将物理地址映射到虚拟地址了,代码如下:
remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot)
remap_pfn_range
是一个 Linux 内核中的函数,用于将物理页帧号(Page Frame Number, PFN)范围映射到用户空间虚拟地址空间。该函数通常用于设备驱动程序中,帮助将物理内存(如设备寄存器或共享内存区域)映射到用户进程的地址空间。
函数原型
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size,
pgprot_t prot);
参数含义:
-
vma
:- 一个指向
vm_area_struct
结构的指针,描述用户进程的虚拟内存区域。 - 包含映射的起始地址、大小和属性。
- 来自于用户空间mmap函数的参数传递。
- 一个指向
-
addr
:- 用户空间的虚拟地址起始位置(通常是
vma->vm_start
)。
- 用户空间的虚拟地址起始位置(通常是
-
pfn
:- 起始的物理页帧号。
- 注意:物理地址需要右移
PAGE_SHIFT
位来转换为页帧号(phy >> PAGE_SHIFT
)。 - 由于remap_pfn_range没有别的参数表示物理地址的信息,所以从对这个参数的处理可以看出,你要映射的物理地址应该是页对齐的,关于什么叫页对齐的?请参见 https://blog.csdn.net/wenhao_ir/article/details/144985338
-
size
:- 映射的大小(字节数)。通常用
vma->vm_end - vma->vm_start
表示该区域的大小。
- 映射的大小(字节数)。通常用
-
prot
:- 内存页面的权限和属性,通常使用
vma->vm_page_prot
。 - 可通过宏(如
pgprot_noncached
或pgprot_writecombine
)修改页面属性。
- 内存页面的权限和属性,通常使用
返回值:
- 成功时返回
0
。 - 失败时返回负值错误码(如
-EINVAL
、-ENOBUFS
等)。
代码分析
remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);
分析参数:
-
vma->vm_start
:- 用户进程的虚拟地址起始位置(映射区域起点)。
-
phy >> PAGE_SHIFT
:phy
是设备的物理地址。- 通过右移
PAGE_SHIFT
位将物理地址转换为物理页帧号(PFN)。 - 示例:假设
phy = 0x203000
(物理地址),PAGE_SIZE = 4096 = 0x1000 = 2的12次方
,则phy >> PAGE_SHIFT = 0x203
(页帧号)。 - 由于remap_pfn_range没有别的参数表示物理地址的信息,所以从对这个参数的处理可以看出,你要映射的物理地址应该是页对齐的,关于什么叫页对齐的?请参见 https://blog.csdn.net/wenhao_ir/article/details/144985338
-
vma->vm_end - vma->vm_start
:- 映射区域的大小,单位为字节。
- 示例:如果
vm_start = 0x40000000
,vm_end = 0x40001000
,则大小为0x1000
(即 4 KB)。
-
vma->vm_page_prot
:- 页面的属性和权限。
- 通过设置
pgprot_noncached
或pgprot_writecombine
可决定页面的缓存行为。 - 例如:
pgprot_noncached(vma->vm_page_prot)
:非缓存模式。pgprot_writecombine(vma->vm_page_prot)
:写合并模式。
工作流程
-
虚拟地址到物理地址的映射:
- 通过
remap_pfn_range
将指定的物理内存(以页为单位)映射到用户空间虚拟地址区域。 - 用户进程访问
vma->vm_start
开始的虚拟地址时,内核会自动将该访问请求映射到对应的物理地址。
- 通过
-
保护属性设置:
- 通过
vma->vm_page_prot
,决定虚拟内存的缓存行为(如缓存、写合并、非缓存)。
- 通过
-
系统检查:
- 内核会检查映射区域的大小和权限,确保映射合法。
至此,整个工程中的关键代码就解释清楚了。
Makfile文件的内容
# 使用不同的开发板内核时, 一定要修改KERN_DIR
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o map_shared_test map_shared_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f map_shared_test
obj-m += hello_drv.o
交叉编译出驱动模块和可执行程序
源码复制到Ubuntu中。
make
将交叉编译出的hello_drv.ko
和map_test
复制到NFS文件目录中,备用。
注意:由于测试程序中要测试两种不同的映射方式,所以在进行测试时要分别交叉编译两次,两次的map_test
是不一样的,但hello_drv.ko
是一样的。
加载驱动模块
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/mmap/hello_drv.ko
检查设备文件生成没有
ls /dev/
有了:
运行测试程序
以MAP_SHARED
形式映射内存区域的测试程序
相关测试程序的代码如下:
/mnt/mmap/map_test
运行结果如下:
运行结果分析:
第1次读取数据分析:
驱动程序初始化时向处于内核空间中的数组kernel_buf
写入了字符串This is an old string
,所以第一次用read操作函数读数组kernel_buf
的数据时能读到字符串This is an old string
。
第2次读取数据分析:
由于这个测试程序中采用MAP_SHARED
的形式进行的内存映射,所以映射的内存区域就是驱动程序中数组kernel_buf
的内存区域,所以映射完成之后就能用映射指针buf_map_pointer
直接读出数组kernel_buf
的数据了,所以能读出驱动程序初始化时向处于内核空间中的数组kernel_buf
写入的字符串This is an old string
。
第3次和第4次读取数据分析:
第3次和第4次读取数据之前用映射得到的指针向映射区域完成了一次数据写入,由于这个测试程序中采用MAP_SHARED
的形式进行的内存映射,所以映射的内存区域就是驱动程序中数组kernel_buf
的内存区域,而不是像MAP_PRIVATE
形式那样另外新分配一个私有内存区域,所以对映射区域完的数据写入相当于就是修改数组kernel_buf
中的数据,所以第3次和第4次读出的字符串就是新写入的字符串This is a new string.
以MAP_PRIVATE
形式映射内存区域的测试程序
测试之前请先重启开发板,把以MAP_SHARED
形式映射内存区域的测试程序的运行痕迹给清理掉,否则会影响接下来的测试。
测试之前请先重启开发板,把以MAP_SHARED
形式映射内存区域的测试程序的运行痕迹给清理掉,否则会影响接下来的测试。
测试之前请先重启开发板,把以MAP_SHARED
形式映射内存区域的测试程序的运行痕迹给清理掉,否则会影响接下来的测试。
相关测试程序的代码如下:
/mnt/mmap/map_test
运行结果如下:
运行结果分析:
第1次读取数据分析:
驱动程序初始化时向处于内核空间中的数组kernel_buf
写入了字符串This is an old string
,所以第一次用read操作函数读数组kernel_buf
的数据时能读到字符串This is an old string
。
第2次读取数据分析:
由于这个测试程序中采用MAP_PRIVATE
的形式进行的内存映射,所以处于内核空间中的数组kernel_buf
只是作为映射的模板,此时数组kernel_buf
存在的意义只在于提供映射大小而已,连数据都不提供,此时用户空间通过mmap
函数获得的虚拟内存指针指向的内存区域并不是数组kernel_buf
的内存区域,而是一块和数组kernel_buf
有相同大小的另一块新分配的区域,注意这一块新区域里面并没有数组kernel_buf
里的数据,而是一块未经初始化的区域,所以在这里第2次读到的值为空。
我们不妨在下文中把这块未经初始的区域称为叫某个进程的私有内存区域。
第3次和第4次读取数据分析:
第3次和第4次读取数据之前用映射得到的指针向映射区域完成了一次数据写入,这样,私有内存区域中有就有数据了,但私有内存区域和kernel_buf
对应的内存区域是相互独立的两块区域,所以此时第3次读数据时读到的kernel_buf
中的字符串仍然为This is an old string.
,第4次读数据时则读到的是向私有内存区域写入的数据This is a new string.
卸载驱动模块
rmmod hello_drv
经测试,卸载一段时间后,没有任何报错,且系统仍然能正常运行,所以代码没有问题。
附完整工程文件
https://pan.baidu.com/s/1onvR1HArgV-CAmL5LdLMRg?pwd=npak