1.带你看懂Linux内核空间内存申请函数kmalloc.、数源实现kzalloc、数源实现 vmalloc的数源实现区别(一篇就够了)
2.详谈Linux内核《系统调用》(1)———kmalloc/ Kfree实现与分析
3.kmalloc描述
4.kmallocLINUX内核中物理内存分配函数分析
5.kmalloc/vmalloc相关功能(linux系统)
6.值得一看的LINUX内核内存管理kmalloc,vmalloc
带你看懂Linux内核空间内存申请函数kmalloc.、kzalloc、数源实现 vmalloc的数源实现区别(一篇就够了)
kmalloc()函数用于在内核空间中申请内存。其选择的数源实现源码 查看原评论内存分配策略与内存使用场景紧密相关。例如,数源实现进程上下文相关代码在使用kmalloc()时可以选择GFP_KERNEL标志,数源实现允许代码在进程上下文内睡眠等待内存分配完成;而需要在中断处理程序、数源实现软中断或任务队列中执行的数源实现代码则应使用GFP_ATOMIC标志,以避免阻塞中断流程;对于DMA相关的数源实现内存申请,kmalloc()允许使用GFP_DMA标志,数源实现以确保内存分配满足DMA传输需求。数源实现
kzalloc()函数是数源实现kmalloc()的优化版本,专为在中断上下文中使用而设计。数源实现它同样支持GFP_ATOMIC标志,确保中断上下文中的代码可以高效执行,同时减少中断处理的延迟。这使得kzalloc()成为在处理中断时申请内存的首选方法,因为它能够避免不必要的阻塞和内存分配的延迟,从而提高系统的整体性能。
vmalloc()函数用于在内存的非连续区域中分配内存。与kmalloc()和kzalloc()不同,vmalloc()不会影响系统的内存对齐规则。它特别适用于那些需要在内存中预留固定大小且不连续的区域的应用场景,例如大型数据结构、虚拟内存管理或者其他需要大量非连续内存的需求。使用vmalloc()可以避免内存对齐问题,同时提供更大的灵活性和控制,特别是在需要为特定应用预留大量内存资源的情况下。
总结起来,cpsapp源码kmalloc()、kzalloc()和vmalloc()在内存申请时各有侧重。kmalloc()适用于大多数进程上下文相关的内存需求,并提供灵活的内存分配策略;kzalloc()专为中断上下文优化,确保在中断处理时的高效执行;而vmalloc()则用于特定应用需要大量、非连续内存的场景。选择合适的内存分配函数,可以显著提高内核程序的性能和稳定性。
详谈Linux内核《系统调用》(1)———kmalloc/ Kfree实现与分析
kmalloc和kfree是Linux内核中用于动态内存分配的函数。kmalloc的主要参数包括要分配的内存块大小以及分配标志。size参数确定分配的内存块大小,最小为或字节,最大为K。flags参数则决定了分配的内存是在内核内存、用户内存还是其他类型的内存中,以及在分配时是否需要考虑特定的内存使用限制。其中GFP_KERNEL用于内核内存分配,GFP_USER用于用户内存分配,GFP_ATOMIC在中断上下文中进行无阻塞分配,GFP_HIGHUSER用于高端内存分配,GFP_NOIO和GFP_NOFS用于禁止特定类型的I/O或文件系统调用。
kmalloc通过__builtin_constant_p函数判断size是否为常数,如果为常数且超过slab缓存最大大小,会调用kmalloc_large进行大内存分配。然后调用kmalloc_order_trace,kmalloc_order,以及alloc_pages进行内存分配。如果size不是常数,会调用__kmalloc,然后经过一系列函数调用最终通过alloc_pages_nodemask进行实质性的fluentd源码内存分配。
kfree函数用于释放由kmalloc分配的内存。它首先检查释放对象地址是否有效,然后禁用中断,执行额外的释放检查,获取内存所属的缓存,并判断是否为NUMA架构。如果为NUMA架构,会根据释放对象所在的内核节点与当前CPU所属的内存节点是否相同来决定是就地释放还是释放到其他节点。最后,kfree会释放内存片段,更新缓存状态,并释放page到伙伴子系统,同时调整缓存中的可用对象数量。
通过kmalloc和kfree的交互,Linux内核能够灵活地在内核空间和用户空间中分配和释放内存,满足各种应用需求。这些函数的实现涉及内存管理的多个层面,包括常数检测、页分配、内存节点判断以及缓存管理,展示了内核在资源分配上的高效性和灵活性。
kmalloc描述
在设备驱动程序和内核模块中,内存分配通常不使用malloc,而是采用kmalloc、vmalloc或get_free_pages直接申请。kmalloc特殊之处在于它为DMA操作分配的是物理上连续的内存,这对于需要高速数据传输的硬件设备至关重要。它的内存大小限制在k到字节之间,其中字节被页描述符占用。kmalloc的hashmaput源码使用方法可参考khg文档。
对于内存映射的I/O接口和硬件RAM,它们通常位于F以上的地址空间,不能直接访问。驱动程序需通过kernel函数vremap实现内存的重新映射。对于需要大块连续内存的硬件DMA,kmalloc只能提供kB的内存,而不能保证交换到文件中。
Linux的存储管理系统支持MMU处理器,为进程提供了4GB的内存空间,分为用户空间和内核空间。用户空间地址范围从0到3GB,内核空间从3GB开始,包括物理内存映射区域(如内核镜像、mem_map等)和vmalloc区域。例如,M的VMware虚拟内存会映射到3GB到3GB+M之间,中间有8M的间隙以防止地址溢出。
kmalloc和get_free_pages申请的内存位于物理内存映射区域,虚拟和物理地址之间有固定的偏移,可以通过virt_to_phys()函数进行简单的转换,该函数定义为:
c
unsigned long virt_to_phys(volatile void * address) {
return ((unsigned long)(address) - PAGE_OFFSET);
}
对应的函数phys_to_virt()则用于将物理地址转化为内核虚拟地址:
c
void * phys_to_virt(unsigned long address) {
return ((void *)((unsigned long)(address) + PAGE_OFFSET));
}
这两个函数都在include\asm-i\io.h文件中提供。
kmallocLINUX内核中物理内存分配函数分析
在Linux内核中,内存分配主要通过kmalloc()和__get_free_page()两种函数来实现。kmalloc()主要用于小内存的连续虚拟地址分配,它基于slab机制,实际上在页分配器的基础上进行更细粒度的内存划分。通过追踪kmalloc()的实现,我们可以看到它首先调用__cache_alloc(),进而通过kmem_cache_alloc()等函数,最终在cache_grow()中通过kmem_getpages()和alloc_pages_node()等步骤,mmcv源码利用__alloc_pages()来获取物理页面,从而确保分配的是物理地址。
相比之下,__get_free_page()函数负责整页的物理内存分配,它基于buddy机制,分配最小粒度为一页。该函数直接调用__alloc_pages()来分配物理内存,分配过程涉及zonelist和zone的管理,通过free_area_init_nodes()和free_area_init_node()等函数,将物理页面与zonelist关联起来。当分配完成后,物理页面会被映射到虚拟地址空间,具体映射起点是PAGE_OFFSET,这是通过引导代码中的VMM初始化来实现的,如setup_arch()和paging_init()等函数。
kmalloc()和__get_free_page()分配的物理地址映射到虚拟地址空间后,为了正确处理虚拟地址,内核提供了virt_to_phys()和phys_to_virt()函数进行转换。这两个函数通过简单的加减PAGE_OFFSET操作即可完成转换,因为内核已经将物理地址映射到了固定的虚拟地址区域。这解释了为何分配到的虚拟地址需要经过virt_to_phys()转换,因为它们之间存在固定的一一对应关系。因此,kmalloc()和__get_free_page()返回的是虚拟地址,但可以通过减去PAGE_OFFSET得到物理地址。
kmalloc/vmalloc相关功能(linux系统)
了解 kmalloc 功能,需先认识 slab 内存池。slab 是为应对内核中频繁分配和释放小型内存需求而设计的,内核为每一个核心数据结构创建专属的内存池。这些内存池管理着从物理内存页中划分出的大小相同的内存块,用于存储特定的内核对象。
在处理通用小内存分配时,kmalloc 内存池体系应运而生。内核通过预先创建多个特定尺寸的 slab cache 来应对不同尺寸的通用内存块申请需求。通过 kmem_cache_create 函数,可以根据需要指定内存块尺寸来创建 kmem_cache。
在 kmalloc 体系中,通过数组 kmalloc_info[][] 来定义通用内存块尺寸分布信息,从而确定最佳内存块尺寸的选取规则。内核定义了一个 size_index[] 数组,存放申请内存块大小在 字节以下的 kmalloc 内存池选取规则。每个元素表示在 kmalloc_info[] 数组中的索引,对应最佳合适尺寸的通用 slab cache。
kmalloc 函数执行流程:首先通过 kmem_cache_create 函数创建 kmem_cache,接着根据申请的内存块大小创建 object,这些 object 由 kmem_cache 管理,最后将 kmem_cache 添加到 slab_cache 链表中。
kmalloc 与 vmalloc 功能对比:kmalloc 与 kzalloc 类似,分配的内存大小有限制(KB),且可以保证分配的内存物理地址是连续的。kmalloc 可以是原子过程,分配内存开销小,执行较快。而 vmalloc 分配连续的虚拟地址空间,无尺寸限制,但不能保证物理地址连续,且分配过程可能产生阻塞。vmalloc 可以睡眠,不宜在中断上下文中调用。kmalloc 相比 vmalloc,速度快,适用于频繁分配和释放小型内存需求场景。
值得一看的LINUX内核内存管理kmalloc,vmalloc
在设备驱动程序或内核模块中进行动态内存分配时,通常使用 kmalloc 和 vmalloc 函数而非 malloc。kmalloc 和 vmalloc 分配的内存类型和使用方式存在显著差异。
kmalloc 用于从物理上连续的低端内存区域分配小块(一般不超过 k)内存,分配的内存地址为物理连续的线性地址,适合于需要连续内存以进行直接内存访问(DMA)操作的设备。释放 kmalloc 分配的内存时使用 kfree 函数。
相比之下,vmalloc 用于从虚拟上连续但物理上可能不连续的高端内存区域分配较大块(一般为大块内存)内存,适合在内存资源紧张时使用。vmalloc 分配的内存仅在逻辑上连续,物理地址无需连续,因此不能直接用于 DMA 操作。释放 vmalloc 分配的内存时使用 vfree 函数。
总结两者区别:
1. kmalloc 分配的是低端内存,而 vmalloc 分配的是高端内存,当内存资源紧张时才会使用低端内存。
2. kmalloc 分配的物理地址是连续的,而 vmalloc 分配的物理地址可能不连续。
3. kmalloc 分配的内存适用于小块内存需求,而 vmalloc 分配的内存适用于大块内存需求。
在 DMA 工作中,为了减轻 CPU 负载,数据传输由 DMA 控制器在快速设备与主存储器之间直接控制完成。在 DMA 模式下,CPU 只需下达指令给 DMA 控制器,由其处理数据的传输,并在传输完成后反馈信息给 CPU,从而显著提高数据传输效率。
在测试代码中,通过将数据放置在 地址,并在 d 物理地址读取数据,可以观察到 kmalloc 分配的物理地址位于低端内存区域,而 vmalloc 分配的物理地址位于高端内存区域。通过 DMA 传输的数据通常位于低端内存区域。
kmalloc 函数的原型为 static __always_inline void *kmalloc(size_t size, gfp_t flags),其中 flags 参数主要决定内存的分配类型,如 GFP_KERNEL、GFP_USER 和 GFP_ATOMIC 等,分别用于内核、用户空间和不允许睡眠的原子分配。
内核编程时,应遵守中断上下文不可睡眠的原则,使用 is_in_interrupt_context() 函数检查是否处于中断处理程序中,以避免睡眠。
字符设备中的几个函数分析
1.在内核中, dev_t 类型(在 <linux/types.h>中定义)用来持有设备编号 — 主次部分都包括.其中dev_t 是 位的量, 位用作主编号, 位用作次编号
1 #ifndef _LINUX_TYPES_H
2 #define _LINUX_TYPES_H
3
4 #include <asm/types.h>
5
6 #ifndef __ASSEMBLY__
7 #ifdef __KERNEL__
8
9 #define DECLARE_BITMAP(name,bits) /
unsigned long name[BITS_TO_LONGS(bits)]
#endif
#include <linux/posix_types.h>
#ifdef __KERNEL__
typedef __u __kernel_dev_t;
typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t; //用来持有设备编号的主次部分
typedef __kernel_ino_t ino_t;
typedef __kernel_mode_t mode_t;
...
2.在 <linux/kdev_t.h>中的一套宏定义. 为获得一个 dev_t 的主或者次编号, 使用:
2.1设备编号的内部表示
MAJOR(dev_t dev);
MINOR(dev_t dev);
2.在有主次编号时, 需要将其转换为一个 dev_t, 可使用:
MKDEV(int major, int minor);
在linux/kdev_t.h中有下了内容
...
4 #define MINORBITS
5 #define MINORMASK ((1U << MINORBITS) - 1)
6
7 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
8 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
9 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))//高为表示主设备号,低位表示次设备号
...
3.分配和释放设备编号register_chrdev_region函数
下面摘自文件fs/char_dev.c内核源代码
/
*** register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count; //计算分配号范围中的最大值+=
dev_t n, next;
for (n = from; n < to; n = next) { /*每次申请个设备号*/
next = MKDEV(MAJOR(n)+1, 0);/*主设备号加一得到的设备号,次设备号为0*/
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:/*当一次分配失败的时候,释放所有已经分配到地设备号*/
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
这里, from是要分配的起始设备编号. from 的次编号部分常常是 0, 但是没有要求是那个效果. count是你请求的连续设备编号的总数. 注意, 如果count 太大, 要求的范围可能溢出到下一个次编号;但是只要要求的编号范围可用, 一切都仍然会正确工作. 最后, name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中.如同大部分内核函数, 如果分配成功进行, register_chrdev_region 的返回值是 0. 出错的情况下, 返回一个负的错误码, 不能存取请求的区域.
4.下面是char_device_struct结构体的信息
fs/char_dev.c
static struct char_device_struct {
struct char_device_struct *next; // 指向散列冲突链表中的下一个元素的指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号的范围大小
const char *name; // 处理该设备编号范围内的设备驱动的名称
struct file_operations *fops; // 没有使用
struct cdev *cdev; /* will die指向字符设备驱动程序描述符的指针*/
} *chrdevs[MAX_PROBE_HASH];
/
** Register a single major with a specified minor range.
*
* If major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If major > 0 this function will attempt to reserve the passed range of
* minors and will return zero on success.
*
* Returns a -ve errno on failure.
*/
/
*** 该函数主要是注册注册注册主设备号和次设备号
* major == 0此函数动态分配主设备号
* major > 0 则是申请分配指定的主设备号
* 返回0表示申请成功,返 回负数说明申请失败
*/
static struct char_device_struct
*__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{ /*以下处理char_device_struct变量的初始化和注册*/
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
//kzalloc()分配内存并且全部初始化为0,
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
if (cd == NULL)
//ENOMEM定义在include/asm-generic/error-base.h中,
// #define ENOMEM /* Out of memory */
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* temporary */
if (major == 0) { //下面动态申请主设备号
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i—) {
//ARRAY_SIZE是定义为ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
//#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
if (chrdevs[i] == NULL)
//chrdevs是内核中已经注册了的设备好设备的一个数组
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;//这里得到一个位使用的设备号
}
//下面四句是对已经申请到的设备数据结构进行填充
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;/*申请设备号的个数*/
strlcpy(cd->name, name, sizeof(cd->name));
/*以下部分将char_device_struct变量注册到内核*/
i = major_to_index(major);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major || //chardevs[i]设备号大于主设备号
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) || //chardevs[i]主设备号等于主设备号,并且此设备号大于baseminor
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
//在字符设备数组中找到现在注册的设备
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
/*所申请的设备好号能够满足*/
cd->next = *cp;/*按照主设备号从小到大顺序排列*/
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
以上程序大体上分为两个步骤:
1.char_device_struct类型变量的分配以及初始化~行
2.将char_device_struct变量注册到内核,行页到行
1.char_device_struct类型变量的分配以及初始化
(1)首先,调用 kmalloc 分配一个 char_device_struct 变量cd。
检查返回值,进行错误处理。
(2)将分配的char_device_struct变量的内存区清零memset。
(3)获取chrdevs_lock读写锁,并且关闭中断,禁止内核抢占,write_lock_irq。
(4)如果传入的主设备号major不为0,跳转到第(7)步。
(5)这时,major为0,首先需要分配一个合适的主设备号。
将 i 赋值成 ARRAY_SIZE(chrdevs)-1,其中的 chrdevs 是包含有个char_device_struct *类型的数组,
然后递减 i 的值,直到在chrdevs数组中出现 NULL。当chrdevs数组中不存在空值的时候,
ret = -EBUSY; goto out;
(6)到达这里,就表明主设备号major已经有合法的值了,接着进行char_device_struct变量的初始化。
设置major, baseminor, minorct以及name。
2.将char_device_struct变量注册到内核
(7)将 i 赋值成 major_to_index(major)
将major对取余数,得到可以存放char_device_struct在chrdevs中的索引
(8)进入循环,在chrdevs[i]的链表中找到一个合适位置。
退出循环的条件:
(1)chrdevs[i]为空。
(2)chrdevs[i]的主设备号大于major。
(3)chrdevs[i]的主设备号等于major,但是次设备号大于等于baseminor。
注意:cp = &(*cp)->next,cp是char_device_struct **类型,(*cp)->next是一个char_device_struct
*类型,所以&(*cp)->next,就得到一个char_device_struct **,并且这时候由于是指针,所以
对cp赋值,就相当于对链表中的元素的next字段进行操作。
(9)进行冲突检查,因为退出循环的情况可能造成设备号冲突(产生交集)。
如果*cp不空,并且*cp的major与要申请的major相同,此时,如果(*cp)->baseminor < baseminor + minorct,
就会发生冲突,因为和已经分配了的设备号冲突了。出错就跳转到ret = -EBUSY; goto out;
()到这里,内核可以满足设备号的申请,将cd链接到链表中。
()释放chrdevs_lock读写锁,开中断,开内核抢占。
()返回加入链表的char_device_struct变量cd。
()out出错退出
a.释放chrdevs_lock读写锁,开中断,开内核抢占。
b.释放char_device_struct变量cd,kfree。
c.返回错误信息
下面程序出自fs/char_dev.c
动态申请设备号
...
/
*** alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
/* dev:
仅仅作为输出参数,成功分配后将保存已分配的第一个设备编号。
baseminor:
被请求的第一个次设备号,通常是0。
count:
所要分配的设备号的个数。
name:
和所分配的设备号范围相对应的设备名称。
b.返回值:
成功返回0,失败返回负的错误编码
*/
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
...