内核模块之内存分配 2.9 内核模块之定时器 2.11 内核模块之锁、信号量 » 内核模块之内存分配
内存子系统在内核中算是一个比较难的部分。。。内容多,较复杂。。。
首先说下vmalloc与kmalloc的不同点。。。
vmalloc 和 kmalloc 拿到的内存地址都是内核虚拟地址。 不同点在于通过vmalloc分配的内存是通过页表映射的,只是逻辑上连续, 物理上不一定连续。kmalloc是线性映射的,逻辑上连续,物理上也是连续的
下面是基本的内存函数的使用:
ioremap的使用:
//-------------------------------------------------------------------
// ioremap.c
//
// This module illustrates use of the kernel's 'ioremap()' and
// 'iounmap()' functions.
//
// NOTE: Written and tested with Linux kernel version 2.6.22.5
//
// programmer: ALLAN CRUSE
// written on: 27 SEP 2007
//-------------------------------------------------------------------
#include <linux/module.h> // for init_module()
#include <linux/proc_fs.h> // for create_proc_info_entry()
#include <asm/io.h> // for ioremap(), iounmap()
#define LOCAL_APIC_BASE 0xFEE00000
char modname[] = "ioremap";
void *lapic;
int _cr3;
int my_get_info( char *buf, char **start, off_t off, int count,int *eof,void *data )
{
int i, pgdir, pgtbl, cpu, len;
cpu = ioread32( lapic + 0x20 ) >> 24;
asm(" push %eax \n mov %cr3, %eax \n mov %eax, _cr3 \n popl %eax ");
len = 0;
len += sprintf( buf+len, "\n LOCAL APIC registers " );
len += sprintf( buf+len, "for processor %d \n", cpu );
for (i = 0; i < 0x400; i+=16)
{
int val = ioread32( lapic + i );
if ( ( i % 128 ) == 0 )
len += sprintf( buf+len, "\n %04X:", i );
len += sprintf( buf+len, " %08X", val );
}
len += sprintf( buf+len, "\n" );
len += sprintf( buf+len, "\n" );
pgdir = ((int)lapic >> 22)&0x3FF;
pgtbl = ((int)lapic >> 12)&0x3FF;
len += sprintf( buf+len, " APIC virtual-address = %08X ", (int)lapic );
len += sprintf( buf+len, " CR3=%08X ", _cr3 );
len += sprintf( buf+len, " pgdir+0x%03X ", pgdir << 2 );
len += sprintf( buf+len, " pgtbl+0x%03X ", pgtbl << 2 );
len += sprintf( buf+len, "\n" );
len += sprintf( buf+len, "\n" );
return len;
}
static int __init my_init( void )
{
printk( "<1>\nInstalling \'%s\' module\n", modname );
lapic = ioremap( LOCAL_APIC_BASE, PAGE_SIZE );
if ( !lapic ) return -ENODEV;
printk( " LOCAL APIC at physical address %08X", LOCAL_APIC_BASE );
printk( " remapped to virtual address %08X \n", (int)lapic );
create_proc_read_entry( modname, 0, NULL, my_get_info,NULL );
return 0; //SUCCESS
}
static void __exit my_exit(void )
{
iounmap( lapic );
remove_proc_entry( modname, NULL );
printk( "<1>Removing \'%s\' module\n", modname );
}
module_init( my_init );
module_exit( my_exit );
MODULE_LICENSE("GPL");
$ ./mmake ioremap.c
$ sudo insmod ioremap.ko
$ dmesg
效果:
物理内存地址或者物理总线地址转换为内核虚拟地址。
注意的地方:
1.参数是物理地址
2.转化结果不要再去用virt_to_phys 函数转物理地址。
ioremap的结果一个可能是走页表的,另一个可能会转到特定的地址段,就是说和物理地址不是绝对的线性关系。
virt_to_phys只对线性内核空间有效,在driver中也不建议用phys_to_virt和virt_to_phys。
kmalloc的使用:
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
#define MEM_KMALLOC_SIZE 8092
char *mem_spvm;
static int __init kmalloc_init(void)
{
mem_spvm = (char *) kmalloc( MEM_KMALLOC_SIZE, GFP_KERNEL);
if(mem_spvm == NULL)
printk("<0> kmalloc failed !\n");
else
printk("<0> kmalloc successfully! \naddr = 0x%1x\n",(unsigned long)mem_spvm);
return 0;
}
static void __exit kmalloc_exit(void)
{
if(mem_spvm != NULL)
{
kfree(mem_spvm);
printk("<0> kfree ok!\n");
}
printk("<0> exit !\n");
}
module_init(kmalloc_init);
module_exit(kmalloc_exit);
$ ./mmake kmalloc.c
$ sudo insmod kmalloc.ko
$ dmesg
[15032.864679] kmalloc successfully! [15032.864680] addr = 0xef962000
vmalloc的使用:
#include <linux/vmalloc.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
#define MEM_VMALLOC_SIZE 8092
char * mem_spvm;
static int __init vmalloc_test(void)
{
mem_spvm = (char *) vmalloc(MEM_VMALLOC_SIZE);
if(mem_spvm == NULL)
printk("<0> vmalloc failed!\n");
else
printk("<0> vmalloc successfully! addr = 0x%lx\n",(unsigned long)mem_spvm);
return 0;
}
static void __exit vmalloc_exit(void)
{
if(mem_spvm !=NULL)
{
vfree(mem_spvm);
printk("<0> vfree successd ! \n");
}
printk("<0> exit !\n");
}
module_init(vmalloc_test);
module_exit(vmalloc_exit);
$ ./mmake vmalloc.c
$ sudo insmod vmalloc.ko
$ dmesg
vmalloc successfully! addr = 0xf83d4000
kmem_cache_alloc的使用:
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
struct kmem_cache * my_cachep;
void * object = NULL;
static int __init kmem_cache_alloc_init(void)
{
// 创建一个名为slab的缓存
my_cachep = kmem_cache_create("my_cache",35,0,SLAB_HWCACHE_ALIGN,NULL);
if(my_cachep == NULL)
printk("<0> kmem_cache_create failed !\n");
else
{
object = kmem_cache_alloc(my_cachep,GFP_KERNEL);
if(object == NULL)
printk("<0> kmem_cache_alloc failed \n");
else
printk("<0> alloc object is 0x%lx\n",(unsigned long) object);
}
return 0;
}
static void __exit kmem_cache_alloc_exit(void)
{
if(object)
{
kmem_cache_free(my_cachep,object);
printk("<0> free object successfuly !\n");
}
if(my_cachep)
{
kmem_cache_destroy(my_cachep);
printk("<0> destroy my_cachep successfully!\n");
}
printk("<0> exit!\n");
}
module_init(kmem_cache_alloc_init);
module_exit(kmem_cache_alloc_exit);
$ ./mmake kmem_cache_alloc.c
$ sudo insmod kmem_cache_alloc.ko
$ dmesg
[20003.802765] vmalloc successfully! addr = 0xf83d4000 [21273.655775] alloc object is 0xef16bf40
get_free_pages 的使用:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gfp.h>
MODULE_LICENSE("GPL");
unsigned long addr = 0;
static int __init __get_free_pages_init(void)
{
addr = __get_free_pages(GFP_KERNEL,3);
if(!addr)
{
return -ENOMEM;
}
else
printk("<0> __get_free_pages Successfully!,\naddr = 0x%lx\n",addr);
return 0;
}
static void __exit __get_free_pages_exit(void)
{
if(addr)
{
free_pages(addr,3);
printk("<0> free_pages ok !\n");
}
printk("<0> exit!\n");
}
module_init(__get_free_pages_init);
module_exit(__get_free_pages_exit);
$ ./mmake get_free_page.c
$ sudo insmod get_free_page.ko
$ dmesg
[21995.400602] __get_free_pages Successfully!, [21995.400605] addr = 0xc7e80000
虚拟映射和mmap()
虚存映射
我们知道,程序是存储在磁盘上到静态文件;进程是对程序到一次运行过程。在进程开始运行时,
进程的代码和数据等内容必须装入到进程用户空间到适当区域。这些区域也就是所谓的代码段和数据段等,
而被装入的数据和代码等内容被称为进程的可执行映像。从上面都描述中可以发现,
进程在运行时并不是将程序一下子就装入到物理内存,而只是将程序装入到进程的用户空间,
这个装入的过程称为虚存映射。
一个源程序在成为可执行文件的过程中会经历预处理、编译、汇编和链接四个阶段。
因此,进程要成功运行不仅要在其用户空间装入进程映像,也要装入该进程所用到到函数库以及链接程序等。
所以,一个进程到用户空间就被分为若干个内存区域。linux使用mmstruct结构来描述一个进程到用户地址空间
,使用vm_area_struct结构来描述进程地址空间中的一个内存区域。因此,
一个vm_area_struct结构可能代表进程到数据段,也可能代表链接程序到代码段等。
进程的虚存映射所做的只是将磁盘上到文件映射到该进程的用户地址空间,
并没有建立虚拟内存到物理内存的映射。当某个可执行映像映射到进程用户空间并开始执行时,
只有很少一部分虚拟页被装入了物理内存。在进程后续到执行过程中,如果需要访问到数据并不在物理内存中,
则产生一个缺页中断(其实是异常),将所需页从交换区或磁盘中调入物理内存,
这个过程即虚拟内存中到请页机制。
进程到虚存区
那么对于一个任意的进程,我们可以通过下面到方法查看其地址空间中到内存区域。
我们先看一个简单的测试程序:
#include < stdio.h >
#include < stdlib.h >
int main()
{
int i=1;
char *str=NULL;
printf("hello,world!\n");
str=(char *)malloc(sizeof(char)*1119);
sleep(1000);
return 0;
}
这个程序中使用到了malloc函数,因此str变量存储于堆中。我们通过打印/proc/3530/maps文件,即可看到该进程的内存空间划分。其中3530是该进程的id。
edsionte@edsionte-desktop:~$ cat /proc/3530/maps
0014a000-00165000 r-xp 00000000 08:07 398276 /lib/ld-2.11.1.so
00165000-00166000 r--p 0001a000 08:07 398276 /lib/ld-2.11.1.so
00166000-00167000 rw-p 0001b000 08:07 398276 /lib/ld-2.11.1.so
001d8000-0032b000 r-xp 00000000 08:07 421931 /lib/tls/i686/cmov/libc-2.11.1.so
0032b000-0032c000 ---p 00153000 08:07 421931 /lib/tls/i686/cmov/libc-2.11.1.so
0032c000-0032e000 r--p 00153000 08:07 421931 /lib/tls/i686/cmov/libc-2.11.1.so
0032e000-0032f000 rw-p 00155000 08:07 421931 /lib/tls/i686/cmov/libc-2.11.1.so
0032f000-00332000 rw-p 00000000 00:00 0
00441000-00442000 r-xp 00000000 00:00 0 [vdso]
08048000-08049000 r-xp 00000000 08:09 326401 /home/edsionte/test
08049000-0804a000 r--p 00000000 08:09 326401 /home/edsionte/test
0804a000-0804b000 rw-p 00001000 08:09 326401 /home/edsionte/test
08958000-08979000 rw-p 00000000 00:00 0 [heap]
b78ce000-b78cf000 rw-p 00000000 00:00 0
b78dd000-b78e0000 rw-p 00000000 00:00 0
bfa6a000-bfa7f000 rw-p 00000000 00:00 0 [stack]
每一行信息依次显示的内容为内存区域其实地址-终止地址,访问权限,偏移量,主设备号:次设备号,inode,文件。
上面的信息不但包含了test可执行对象的各内存区域,而且还分别显示了 /lib/ld-2.11.1.so(动态连接程序)
文件和/lib/tls/i686/cmov/libc-2.11.1.so(C库)文件的内存区域信息。
我们从某个内存区域的访问权限上可以大致判断该区域的类型。各个属性符号的意义为:
r-read,w-write,x-execute,s-shared,p-private。因此,r-x一般代表程序的代码段,即可读,可执行。
rw-可能代表数据段,BSS段和堆栈段等,即可读,可写。堆栈段从行信息的文件名就可以区分;
如果某行信息的文件名为空,那么可能是BSS段。另外,上述test进程共享了内核动态库,
所以在00441000-00442000行处文件名显示为vdso(Virtual Dynamic Shared Object)。
mmap系统调用
通过mmap系统调用可以在进程到用户空间中创建一个新到虚存区。该系统调用到原型如下:
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
该函数可以将以打开的文件映射到进程用户空间到一片内存区上,执行成功后,该函数返回这段映射区到首地址。
用户得到这片虚存的首地址后,就可以像访问内存那样访问文件。
该系统调用的参数说明如下:
addr:映射到用户地址空间到起始地址;
length:映射区以字节为单位到长度;
prot:对映射区到访问模式。包括PROTEXEC(可执行),PROTREAD (可读),PROTWRITE(可写),
PROT_NONE(文件不可访问)。这个访问模式不能超过所映射文件到打开模式。
比如被映射的文件打开模式为只读,那么此处到访问模式不能是可读写的。
flags:这个字段比较灵活,不同到标志有不同的功能,具体如下:
MAP_SHARED:创建一个可被子进程共享的映射区;
MAP_PRIVATE:创建一个“写实复制”的映射区;
MAP_ANONYMOUS:创建一个匿名到映射区,该虚存区与进程无关;
fd:所要映射到进程用户空间的文件描述符,该文件必须为以打开的文件;
offset:文件的起始映射偏移量;
mmap()举例
在该程序中,首先以只读方式打开文件test.c,再通过该文件返回到文件描述符和mmap函数将test.c文件映射到当前进程到用户地址空间中。成功执行mmap函数后,buf被赋值为所映射的虚存区的首地址。注意,mmap函数返回的是void型指针,而buf是char型指针。将mmap返回值赋值给buf变量时,自动将void转化为char型。
最后,就像平常我们使用一个char型指针变量那样,依次打印出buf中到数据。
#include < stdio.h >
#include < sys/mman.h >
#include < fcntl.h >
int main()
{
int i,fd;
char *buf = NULL;
fd = open("./test.c", O_RDONLY);
if(fd < 0)
{
printf("open error\n");
return -1;
}
buf = mmap(NULL, 12, PROT_READ, MAP_PRIVATE ,fd, 0);
for(i = 0;i < 12;i++)
{
printf("%c",buf[i]);
}
printf("\n");
return 0; }
try一下!