day51-Linux内核内存

day51-Linux内核内存

一、内存映射

  linux内存映射是一种将文件或设备的一部分或全部映射到进程的虚拟地址空间的技术。这样,进程可以直接访问文件或设备的内容,而不需要通过系统调用或缓冲区复制。linux内存映射有两种类型:文件映射和匿名映射。

  文件映射是将一个文件的一部分或全部映射到进程的虚拟地址空间的过程。文件映射可以用于实现共享内存,即多个进程可以访问同一个文件的同一部分,从而实现数据的共享和同步。文件映射也可以用于提高文件操作的性能,因为进程可以直接读写文件内容,而不需要通过系统调用或缓冲区复制。文件映射可以通过mmap系统调用来创建,通过munmap系统调用来销毁。

  匿名映射是将一块没有关联任何文件或设备的内存区域映射到进程的虚拟地址空间的过程。匿名映射可以用于实现动态内存分配,即进程可以根据需要申请和释放内存空间,而不需要事先确定内存大小。匿名映射也可以用于实现私有内存,即进程可以独占一块内存区域,而不需要与其他进程共享。匿名映射可以通过mmap系统调用来创建,通过munmap系统调用来销毁。

  linux内存映射是一种强大而灵活的技术,它可以提高进程对文件和设备的访问效率,也可以提供多种内存管理功能。linux内存映射的原理和实现涉及到多个方面,如虚拟内存、页表、页缓冲、页故障、写时复制等,这些方面将在后续的文章中详细介绍。




二、内存分配

  Linux内存映分配是一个复杂而重要的主题,它涉及到多种技术和机制,如虚拟内存、物理内存、分段、分页、请求分页、缺页异常、内存区域、内存管理单元等。本文将简要介绍Linux内存映分配的基本原理和过程,帮助读者了解Linux如何管理和使用内存资源。

  首先,我们需要区分两种不同的地址空间:虚拟地址空间和物理地址空间。虚拟地址空间是进程看到的地址空间,它是由操作系统提供的一种抽象,使得每个进程都有一个独立的、连续的、4GB大小的地址空间。物理地址空间是实际存在的内存芯片上的地址空间,它是有限的、不连续的,并且可能大于或小于4GB。为了充分利用和保护物理内存,Linux采用了虚拟内存技术,即通过一种映射关系,将虚拟地址转换为物理地址,从而实现对物理内存的访问。

  在Linux中,每个进程的虚拟地址空间被划分为两部分:用户空间和内核空间。用户空间是进程自己使用的地址空间,它占据了虚拟地址空间的低端3GB(从0x00000000到0xBFFFFFFF),并且每个进程都有一个不同的用户空间。用户空间通常包含以下几个区域:

  • 代码段:用来存放可执行文件的操作指令,它是只读的。
  • 数据段:用来存放可执行文件中已初始化的全局变量和静态变量,它是可读可写的。
  • BSS段:用来存放可执行文件中未初始化的全局变量和静态变量,在内存中被清零,它也是可读可写的。
  • 堆:用来存放动态分配的内存,它的大小可以动态增长或缩减,由malloc等函数分配和free等函数释放,它也是可读可写的。
  • 栈:用来存放函数调用时的局部变量、参数、返回值等信息,它是后进先出(LIFO)的数据结构,由系统自动分配和释放,它也是可读可写的。

  内核空间是操作系统使用的地址空间,它占据了虚拟地址空间的高端1GB(从0xC0000000到0xFFFFFFFF),并且每个进程都共享同一个内核空间。内核空间通常包含以下几个区域:

  • 直接映射区:用来直接映射物理内存中前896MB(从0x00000000到0x37FFFFFF)的区域,它们之间有一个固定的偏移量PAGE_OFFSET(0xC0000000),即线性地址=PAGE_OFFSET+物理地址。这样做的好处是可以快速地访问物理内存,并且保证了物理内存中前16MB(ZONE_DMA)可以被DMA设备直接访问。
  • 高端内存线性地址空间:用来映射物理内存中超过896MB的区域,它们之间没有固定的偏移量,而是通过一种动态的、临时的映射机制来实现,即高端内存映射。这样做的原因是因为物理内存的大小可能大于4GB,而内核空间的大小只有1GB,所以无法直接映射所有的物理内存。高端内存线性地址空间又分为以下三个子区域:
    • 动态内存映射区:用来映射不连续的、大块的物理内存,由vmalloc等函数分配和vfree等函数释放,它们之间没有固定的映射关系,而是通过页表来实现。
    • 永久内核映射区:用来映射连续的、小块的物理内存,由kmap等函数分配和kunmap等函数释放,它们之间有一个固定的映射关系,即线性地址=PKMAP_BASE+物理地址/PAGE_SIZE。
    • 临时内核映射区:用来临时映射连续的、小块的物理内存,由kmap_atomic等函数分配和kunmap_atomic等函数释放,它们之间也有一个固定的映射关系,即线性地址=FIX_KMAP_BASE+物理地址/PAGE_SIZE。

Linux内存映分配的过程大致如下:

  • 当进程需要分配内存时,它会调用相应的系统函数,如malloc、vmalloc、kmap等。
  • 系统函数会根据请求的大小、属性、位置等条件,选择合适的内存区域和分配器,如堆、栈、slab、伙伴系统等。
  • 分配器会从相应的内存区域中寻找或创建一个合适的空闲块,并返回给系统函数。
  • 系统函数会将空闲块对应的虚拟地址返回给进程,并建立虚拟地址和物理地址之间的映射关系,如修改页表、设置TLB等。
  • 进程就可以通过虚拟地址访问分配到的物理内存了。



三、共享内存mmap

  mmap是一种在Linux系统中将文件或设备映射到内存的方法。它可以提高文件操作的效率,避免不必要的数据拷贝,以及实现进程间通信等功能。mmap函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

//addr:是指定映射区域的起始地址,如果为NULL,则由系统自动分配;

//length:是指定映射区域的大小,单位是字节;prot是指定映射区域的保护属性,可以是PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)或PROT_NONE(不可访问)的组合;

//flags:是指定映射区域的类型和选项,可以是MAP_SHARED(共享映射)、MAP_PRIVATE(私有映射)、MAP_FIXED(固定地址映射)、MAP_ANONYMOUS(匿名映射)等的组合;

//fd:是指定要映射的文件或设备的文件描述符,如果为-1,则表示不映射任何文件或设备;

//offset:是指定要映射的文件或设备的偏移量,单位是字节。

//mmap函数成功时返回映射区域的起始地址,失败时返回MAP_FAILED,并设置errno。

使用完毕后,需要调用munmap函数来解除映射,其原型如下:

1
2
3
int munmap(void *addr, size_t length);

//addr和length分别是mmap函数返回的地址和长度。munmap函数成功时返回0,失败时返回-1,并设置errno。

mmap的使用

内核层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct file_operations fops = {
.mmap = mychr_mmap,
};

static int mychr_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long page;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);

/*标记这段虚拟内存映射为IO区域,并阻止系统将该区域包含在进程的存放转存中*/
vma->vm_flags |= VM_IO;
/*标记这段区域不能被换出, 在驱动设备中虚拟页和物理页的关系应该是长期的,应该保留起来,不能被其他的虚拟页换出*/
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;

//得到物理地址
page = virt_to_phys(kbuf);

//将用户空间的一个vma虚拟内存区映射到以page开始的一段连续物理页面上
if(remap_pfn_range(vma, start, page>>PAGE_SHIFT, size, PAGE_SHARED)) /*虚拟区域保护属性*/
return -1;
return 0;
}

应用层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#define KSIZE 4096

int main(int argc, char *argv[])
{
int fd = open("/dev/mychr", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
printf("open success!\n");

char *vaddr = mmap(NULL, KSIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == vaddr)
{
perror("mmap");
return -1;
}

printf("vaddr = %s\n", vaddr);

close(fd);
munmap(vaddr, KSIZE);

return 0;
}





内核层完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//1. 头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mm.h>

#define CHAR_MAJOR 500 //主设备号
#define CHAR_MINOR 0 //次设备号
static dev_t devno; //设备号
static int count = 1; //设备数量
static struct class *mycls;
static struct device *mydev;
static struct cdev mycdev; //字符设备
static char *kbuf = NULL;

static int mychr_open(struct inode *id, struct file *fp)
{
//动态分配空间
kbuf = kmalloc(4096, GFP_KERNEL);
strcpy(kbuf, "hello world!");
printk("open ok!\n");
return 0;
}

static int mychr_close(struct inode *id, struct file *fp)
{
//释放空间
kfree(kbuf);
printk("close ok!\n");
return 0;
}

static ssize_t mychr_read(struct file *fp, char __user *ubuf, size_t len, loff_t *off)
{
int ret;
//从设备中读取数据到用户空间

ret = copy_to_user(ubuf, kbuf, len);
if (0 != ret)
return -1;

return 0;
}

static ssize_t mychr_write(struct file *fp, const char __user *ubuf, size_t len, loff_t *off)
{
int ret;
ret = copy_from_user(kbuf, ubuf, len);
if (0 != ret)
return -1;
return 0;
}

static int mychr_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long page;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);

/*标记这段虚拟内存映射为IO区域,并阻止系统将该区域包含在进程的存放转存中*/
vma->vm_flags |= VM_IO;
/*标记这段区域不能被换出, 在驱动设备中虚拟页和物理页的关系应该是长期的,应该保留起来,不能被其他的虚拟页换出*/
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;

//得到物理地址
page = virt_to_phys(kbuf);

//将用户空间的一个vma虚拟内存区映射到以page开始的一段连续物理页面上
if(remap_pfn_range(vma, start, page>>PAGE_SHIFT, size, PAGE_SHARED)) /*虚拟区域保护属性*/
return -1;
return 0;
}


static struct file_operations fops = {
.owner = THIS_MODULE,
.open = mychr_open,
.release = mychr_close,
.read = mychr_read,
.write = mychr_write,
.mmap = mychr_mmap,

};

//2. 定义加载与卸载函数
static int __init mychr_init(void)
{
/*一. kernel*/
int ret;

//1. 申请设备号
#if 1
//a) 静态申请设备号
devno = MKDEV(CHAR_MAJOR, CHAR_MINOR);
ret = register_chrdev_region(devno, count, "mychrdev");
#else
//b) 动态申请设备号
ret = alloc_chrdev_region(&devno, 0, count, "mychrdev");
#endif
if (0 != ret)
return -1;

//2. 初始化字符设备
cdev_init(&mycdev, &fops);

//3. 添加设备
ret = cdev_add(&mycdev, devno, count);
if (0 != ret)
goto err;

//4. 创建设备节点
//1) 手动创建: mknod /dev/设备 c 主设备号 次设备号
//2) 代码创建
mycls = class_create(THIS_MODULE, "mychrcls");
if (IS_ERR(mycls))
goto err1;

mydev = device_create(mycls, NULL, devno, NULL, "mychr");
if (IS_ERR(mydev))
goto err2;

/*二. hardware*/

printk("Mychr init ok!\n");
return 0;

err2:
//销毁类
class_destroy(mycls);
err1:
//删除设备
cdev_del(&mycdev);
err:
//注销设备号
unregister_chrdev_region(devno, count);
return -1;
}

static void __exit mychr_exit(void)
{
//销毁设备节点
device_destroy(mycls, devno);
//销毁类
class_destroy(mycls);
//删除设备
cdev_del(&mycdev);
//注销设备号
unregister_chrdev_region(devno, count);
printk("Mychr exit ok!\n");
}

//3. 声明加载与卸载函数
module_init(mychr_init);
module_exit(mychr_exit);

//4. 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Liu Jing");
MODULE_DESCRIPTION("This is a sample chrdev driver!");

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2020-2024 nakano-mahiro
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信