在内核空间分配内存
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一般的,用户空间使用函数malloc在堆上分配内存空间,同样的,在内核空间同样有一套类似的函数来分配空间。下面的知识会涉及页式管理的内存机制,如果不懂的要先复习一下,在S3C2440数据手册的MMU部分有介绍。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、内核空间和用户空间有什么不同
学c语言的时候应该学过,从用户空间看,每个进程都傻乎乎的以为自己有4G的内存空间,其中位于高地址(3G-4G)的1G空间给内核用,另外的3G(0-3G)都是它一个人独占的。所以用户空间很慷慨的把3G的空间分了好几个区域,如堆、栈、代码段等。其中,malloc()分配的空间位于堆,而程序中的自动变量,如你在函数内定义的“int i”,它是放在栈上,同时。用户空间的栈是可变栈,即随着数据的增多,对应函数的栈空间也会增多。
跟每个用户空间的进程不一样,内核只有1G的空间,同时,除了自己本身有进程运行外,内核还要允许用户空间进程调用系统调用进入内核空间去执行。所以,内核对此相当吝啬,它规定在内核中的每个进程都只有4KB或8KB(32位下)的定长栈。出于这样的原因,大的数据结构就不能在栈中分配,只能请求内核分配新的空间来存放数据,如函数kmalloc()。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、内存的基本单位是字节吗?
在介绍分配内存空间的函数前,我们还要了解一下内存是怎么被划分的。
内核不仅知道用户空间中看到的1G内核空间是假的,它还知道实际的物理内存是多少(我的开发板是64M)。所以,内核的其中一个任务就是,当这段虚假内存中的数据需要调用时,内核把这段虚拟内存与实际的物理内存对应上,运行完后又把两段内存的对应关系撤销掉给另外的虚拟内存用。
既然知道虚拟内存与物理内存的关系,那它们是怎么对应的,难道是一个一个字节?如果这样子做的话内核肯定觉得崩溃。
页是内存管理的基本单位。内存管理器(MMU,用于虚拟地址与物理地址之间的转换)通常以页为单位进行出来。页是内存管理的最小单位。在32位的系统中,一页的大小为4KB。所以,64M的物理内存将被分为16384个页。每一个物理页对应地用一个struct page来维护,注意,该结构体是用来维护物理页,而不是虚拟也,结构体记录该页是否被使用,对应的虚拟地址是多少等信息。
由于内存访问的限制,内核又把内存分成了3个区。
如有些硬件的访问只能在24位的地址空间寻址,出于这总访问限制,linux把前16MB划分为ZONE_DMA——用于直接内存访问(MDA)。
在x86体系里,高于896M的内存空间称为高端内存,这段内存区域的页和普通的内存页操作后有差异,这段区域划分为ZONE_HIGHMEM。
剩下的,加载这两段区域之间的就是我们平时用的普通内存区域——ZONE_NORMAL。
这这里要注意一下:
1)这些分区是指linux自己分的,当然,如果普通分区不够用,当然也可以占用其他区的空间。
2)分区的大小是根据体系结构而定的,一般的ARM下,ZONE_NORMAL就是所有的可用内存区域。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、分配内存时使用的标记gfp_mask
在讲如何分配内存之前,先讲一下分配内存时将会用到的gfp_mask。简单地讲,这个标记指定了分配内存时的要求。具体分三类:
行为修饰符:表示内核应当如何分配内存,如指定不能休眠等。
区修饰符:指定内存将要分配到上面讲的三个区中的哪一个。
类型标记:这包含了上面两种修饰符(或运算),这些标记是为了让用户更好地去使用。
标记有很多,我这里不一一介绍,需要的可以自己查阅《linux内核设计与实现(第三版)》P238页。这里我讲两个常用的类型标记:
1)GFP_KERNEL:最常用的标记,用于可睡眠的进程上下文。
2)GFP_ATOMIC:使用了这个标记,内存分配函数不会引起随眠。
3)GFP_USER:当需要给用户空间分配内存空间时使用该标记。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、分配内存的第一种方法——按页分配
这是内核提供的一种请求内存的底层机制,都是以页为单位分配内存。以下函数包含在<linux/gfh.h>
这分为两个步骤:
1、请求内核分配页,获得物理页对应的结构体struct page:
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
使用:
该函数用于申请(1<<other)——即2的other次方个连续物理页,gfp_mask用于指定分配的方式,一般使用GFP_KERNEL或GFP_ATOMIC。注意:函数会引起睡眠
返回值:
成功返回一个指针,指向这连续物理页的第一个struct page结构体,失败返回NULL。
2、分配页后还不能直接用,需要得到该页对应的虚拟地址:
void *page_address(struct page *page)
其实这个函数就是获取page的成员virtual,但千万不要直接访问,需要使用这个函数。函数返回的是物理页对应的虚拟地址,注意,如果你申请了多个物理页,分配的物理页是连续的,对应的虚拟地址也是连续的。
上面的两个步骤其实可以合成一个函数:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
这个函数的传参和alloc_pages的一样,不过它直接返回申请的物理页对应的虚拟地址。
当然,无论使用上面的哪种方法,当内存不用时,需要调用函数释放:
1、如果你使用上面的第一种方法:
void __free_pages(struct page *page, unsigned int order)
2、如果你使用的是第二种方法:
void free_pages(unsigned long addr, unsigned int order)
下面来个程序:
/*5th_mm/5th_mm_1/1st/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <linux/mm.h>
5
6 struct page *p;
7 char *s;
8
9 static int __init test_init(void) //模块初始化函数
10 {
11 unsigned long virt, phys;
12
13 #define SWITCH 0 //通过定义这个来切换校验这两种不同的方法
14 #if SWITCH
15 //alloc 2 pages
16 p = alloc_pages(GFP_KERNEL, 1);
17 if (NULL == p){ //必须检验错误
18 printk("alloc page error!\n");
19 return - ENOMEM;
20 }
21 s = page_address(p);
22 #else
23 s = (char *)__get_free_pages(GFP_KERNEL, 1);
24 if (NULL == s){
25 printk("alloc page error!\n");
26 return - ENOMEM;
27 }
28 #endif
29
30 phys = __pa((unsigned long)s); //通过虚拟地址获得对应的物理地址
31 virt = (unsigned long)__va(phys); //通过物理地址获得对应的虚拟地址
32 printk("<p->virtual, s>[%p]\n", s); //打印获得的虚拟地址
33 printk("<phys>[%p]\n", (void *)phys); //打印对应的物理地址
34 printk("<virt>[%p]\n", (void *)virt); //再打印虚拟地址,其实就是分配函数返回的地址
35
36 memcpy(s, "hello mm", 20);
37
38 printk("hello kernel\n");
39 return 0;
40 }
41
42 static void __exit test_exit(void) //模块卸载函数
43 {
44 #if SWITCH
45 __free_pages(p, 1);
46 #else
47 free_pages((unsigned long)s, 1);
48 #endif
49
50 printk("good bye kernel\n");
51 }
52
53 module_init(test_init);
54 module_exit(test_exit);
55
56 MODULE_LICENSE("GPL");
57 MODULE_AUTHOR("xoao bai");
58 MODULE_VERSION("v0.1");
再检验一下:
[root: 1st]# insmod test.ko
<p->virtual, s>[c3968000] //虚拟地址
<phys>[33968000] //对应的实际地址
<virt>[c3968000]
hello kernel [hello mm] //打印出来了,hello美眉!
上面我分配了两页后什么也没做,当然,如果你要是只分配一页,内核有贴心函数:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask),0)
另外还有一个函数,不仅给你分配一页空间,还帮你清零了,特别适用于给用户空间分配内存。
nsigned long get_zeroed_page(gfp_t gfp_mask)
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
五、分配内存的第二种方法——kmalloc()
kmalloc()的用法和malloc差不多,只是多了一个我前面介绍的标志gfp_flag。
上函数,需要包含头文件<linux/slab.h>
void *kmalloc(size_t size, gfp_t flags)
成功返回指向这块内存的地址(虚拟地址),失败返回NULL。这里注意一下,返回的内存大小不一定是size,因为内存的分配是基于页来分配的,有时需要地址对齐之类,所有分配的内存地址可能比size大。函数同样会引起睡眠,如果不能睡眠需要使用GFP_ATOMIC。
分配的内存必须释放,使用函数:
void kfree(const void *objp)
上个程序:
/*5th_mm_1/2nd/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <linux/mm.h>
5
6 char *s;
7
8 static int __init test_init(void) //模块初始化函数
9 {
10 s = kmalloc(20, GFP_KERNEL);
11 memcpy(s, "hello mm", 20);
12
13 printk("hello kernel [%s]\n", s);
14 return 0;
15 }
16
17 static void __exit test_exit(void) //模块卸载函数
18 {
19 kfree(s);
20 printk("good bye kernel\n");
21 }
22
23 module_init(test_init);
24 module_exit(test_exit);
25
26 MODULE_LICENSE("GPL");
27 MODULE_AUTHOR("xoao bai");
28 MODULE_VERSION("v0.1");
再验证一下:
[root: 2nd]# insmod test.ko
hello kernel [hello mm] //又打印出来了
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
六、其他的内存分配函数——vmalloc
有时候,内核不一定会有很大的一块连续物理内存,这时候kmalloc就不能处理这种情况了,它只能是分配连续的物理内存。需要用以下的函数vmalloc。
分配:
void * vmalloc(unsigned long size);
同样的,成功返回首地址,失败返回NULL,切记这个函数会引起睡眠。而且没有标志可选。
释放:
void vfree(void *addr);
当指定的size没有真够大的连续空间时,这个函数就会像捡破烂一样,东捡一块西捡一块,凑成满足大小的物理内存,并连在一起形成连续的虚拟内存再把首地址返回,所以这个函数的操作很麻烦,出于性能的考虑,能不用的话尽量不用。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
七、slab层
为了方便一些频繁被使用的数据,内核有了slab层的概念。大概意思就是,在告诉内存空间里面,内核定义多了slab(其实就是一页),这些页可以预先定义成用来存放什么数据。这样的话就方便了,当有这样的数据要存放,进程就可以申请放在slab层,这样的话就省去了内存分配和释放的操作。具体的介绍请看《linux内核设计与实现(第三版)》P245。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2.操作硬件——IO内存
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
在之前章节的驱动,都没有对硬件进行操作,接写来将从我之前学的裸板驱动开始,讲解在linux系统下如何访问硬件。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、IO端口与IO内存
介绍之前可以看看以下的博客:http://blog168.chinaunix.net/link.php?url=http://blogold.chinaunix.net%2Fu2%2F66435%2Fshowart_2137870.html
x86体系和ARM体系的寻址方式是有差别的:
在x86下,为了能够满足CPU高速地运行,内存与CPU之间通过北桥相连并通过地址方式访问,而外设通过南桥与CPU相连并通过端口访问。
在ARM下也实现了类似的操作,通过两条不同的总线(AHB BUS和APB BUS)来连接不同访问速度的外设。但是它与x86不同,无论是内存还是外设,ARM都是通过地址访问。
因为这两种访问方式的不同,linux分出了两种不同的访问操作:
以地址方式访问硬件——使用IO内存操作。
以端口方式访问硬件——使用IO端口操作。
在ARM下,访问寄存器就像访问内存一样——从指定的寄存器地址获取数据,修改。所以,ARM下一般是使用IO内存的操作。但这并不是说IO端口的操作在ARM下不能用,它们的代码差不多,只是没有使用的必要,下面也将介绍IO内存操作。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、如何使用IO内存获得硬件的地址
之前已经说过,不能在linux使用实际的物理地址,要对指定的物理地址进行操作,必须要先将物理地址与虚拟地址对应,通过虚拟地址访问。于是有了以下的物理地址映射函数:
#include <asm/io.h>
void *ioremap(unsigned long phys_addr, unsigned long size);
其实这也是上一节介绍的内存分配的一种方式,它同样会建立新页表来管理虚拟地址。函数传入两个参数,需要访问的物理内存(寄存器)的首地址phys_addr和这段内存区域的大小size,返回与该段物理地址对应的虚拟地址。这段地址可以多次被映射,当然,每次映射的虚拟地址也不一样。
对应的也有撤销映射关系的函数:
void ioumap(void *addr);
接下来,我将会从一个裸板的ARMled驱动开始,讲解linux下的操作和裸板有什么不一样。
我的ARM裸板程序是在linux下编写的,我不知道这跟win下使用ADS有什么区别,在裸板驱动中,一般我是通过这样的办法来操作寄存器的:
首先,先给个地址定义个容易记的名字:
#define GPECON *(volatile unsigned long *) 0x56000040
接着,我就要操作这个GPECON寄存器了:
*GPECON &= ~(3 << 24); //将24和25位清零
*GPECON |= (1 << 24); //将24和25位分别赋值为1、0
可以看到,操作寄存器其实就是拿个地址出来进行操作。其实在linux下也是一样,只是操作的时候不能使用物理地址,需要用映射出来的虚拟地址。
上个函数,这个程序我将要点亮连在我开发板上的led灯,这个灯接在我开发板的GPE12上,如果需要下载程序运行,需要改一下接口。
/*5th_mm_2/1st/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <asm/io.h> //上面介绍的函数需要包含该头文件
5
6 volatile unsigned long virt, phys; //用于存放虚拟地址和物理地址
7 volatile unsigned long *GPECON, *GPEDAT, *GPEUP; //用与存放三个寄存器的地址
8
9 void led_device_init(void)
10 {
11 phys = 0x56000000; //1、指定物理地址
12 virt = (unsigned long)ioremap(phys, 0x0c); //2、通过ioremap获得对应的虚拟地址
13 //0x0c表示只要12字节的大小
14 GPECON = (unsigned long *)(virt + 0x40); //3、指定需要操作的三个寄存器的地址
15 GPEDAT = (unsigned long *)(virt + 0x44);
16 GPEUP = (unsigned long *)(virt + 0x48);
17 }
18
19 void led_configure(void) //led配置函数
20 {
21 *GPECON &= ~(3 << 24); //配置GPE12为输出端口
22 *GPECON |= (1 << 24); //先清零再赋值
23
24 *GPEUP |= (1 << 12); //禁止上拉电阻
25 }
26
27 void led_on(void) //点亮led
28 {
29 *GPEDAT &= ~(1 << 12);
30 }
31
32 void led_off(void) //灭掉led
33 {
34 *GPEDAT |= (1 << 12);
35 }
36
37 static int __init test_init(void) //模块初始化函数
38 {
39 led_device_init();
40 led_configure();
41 led_on();
42 printk("hello led!\n");
43 return 0;
44 }
45
46 static void __exit test_exit(void) //模块卸载函数
47 {
48 led_off();
49 iounmap((void *)virt); //注意,即使取消了映射,通过之前的虚拟地址还能访问硬件,
50 printk("bye\n"); //但不是肯定可以,只要该虚拟地址被内核改动后就不行了。
51 }
52
53 module_init(test_init);
54 module_exit(test_exit);
55
56 MODULE_LICENSE("GPL");
57 MODULE_AUTHOR("xoao bai");
58 MODULE_VERSION("v0.1");
从上面的程序可以看到,除了获得地址有点和裸板驱动不一样外,寄存器的操作还是一样的。
接下来验证一下:
[root: 1st]# insmod test.ko
hello led! //这时候灯亮了
[root: 1st]# rmmod test
bye //灯灭了
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、改进函数,使用更好的内存访问接口
为了实现更好的移植性,上面的程序就有缺陷了。内核建议,尽量使用内核提供的内存访问接口:
#include <asm/io.h>
//从内存读取数据,返回值是指定内存地址中的值
unsigned int ioread8(void *addr)
unsigned int ioread16(void *addr)
unsigned int ioread32(void *addr)
//往指定内存地址写入数据
void iowrite8(u8 value, void *addr)
void iowrite16(u16 value, void *addr)
void iowrite32(u32 value, void *addr)
一般常用的是32位内存存取接口。
接下来就改进一下函数,其实实质没有改变,上面的函数是根据对应的平台体系结构编写的,这样可以提高驱动的移植性。
/*5th_mm_2/1st/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <asm/io.h>
5 #include <asm/sizes.h>
6
7 volatile unsigned long virt, phys;
8 volatile unsigned long *GPECON, *GPEDAT, *GPEUP;
9 unsigned long reg;
10
11 void led_device_init(void)
12 {
13 phys = 0x56000000;
14 virt = (unsigned long)ioremap(phys, SZ_16); //这里只是想介绍一下,在asm/sizes.h中有一下
15 //定义好用来表示内存大小的宏,这里其实我只
16 GPECON = (unsigned long *)(virt + 0x40); //需要12个字节,并不需要16个字节。
17 GPEDAT = (unsigned long *)(virt + 0x44);
18 GPEUP = (unsigned long *)(virt + 0x48);
19 }
20
21 void led_configure(void)
22 {
23 //*GPECON &= ~(3 << 24);
24 //*GPECON |= (1 << 24);
25 reg = ioread32(GPECON);
26 reg &= ~(3 << 24);
27 reg |= (1 << 24);
28 iowrite32(reg, GPECON);
29
30 //*GPEUP |= (1 << 12);
31 reg = ioread32(GPEUP);
32 reg &= ~(3 << 12);
33 iowrite32(reg, GPEUP);
34 }
35
36 void led_on(void)
37 {
38 //*GPEDAT &= ~(1 << 12);
39 reg = ioread32(GPEDAT);
40 reg &= ~(1 << 12);
41 iowrite32(reg, GPEDAT);
42 }
43
44 void led_off(void)
45 {
46 //*GPEDAT |= (1 << 12);
47 reg = ioread32(GPEDAT);
48 reg |= (1 << 12);
49 iowrite32(reg, GPEDAT);
50 }
51
52 static int __init test_init(void) //模块初始化函数
53 {
54 led_device_init();
55 led_configure();
56 led_on();
57 printk("hello led!\n");
58 return 0;
59 }
60
61 static void __exit test_exit(void) //模块卸载函数
62 {
63 led_off();
64 iounmap((void *)virt);
65 printk("bye\n");
66 }
67
68 module_init(test_init);
69 module_exit(test_exit);
70
71 MODULE_LICENSE("GPL");
72 MODULE_AUTHOR("xoao bai");
73 MODULE_VERSION("v0.1");
会发现发现,程序将原来直接访问内存的一句话变成了3句话,其他都没有改变。
我就不验证了,效果其实是一样的。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、再改进一下程序:
在使用IO内存映射操作之前,其实还可以添加一个步骤:分配内存区域。
#include <ioport.h>
struct resource *request_mem_region(unsigned long start, unsinged long len, char *name)
该函数从start开始分配len字节长的内存空间。如果成功,返回一个结构体指针,但这结构体我们没必要用,如果失败返回NULL。成功后,可以在.proc/iomem查看到name的信息。
其实调用request_mem_region()不是必须的,但是建议使用。该函数的任务是检查申请的资源是否可用,如果可用则申请成功,并标志为已经使用,其他驱动想再申请该资源时就会失败。
如果不再使用,需要调用释放函数:
void release_mem_region(unsigned long start, unsigned long len)
现在把这两个函数加上去:
/*5th_mm_2/3rd/test.c*/
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <asm/io.h>
5 #include <linux/ioport.h>
6
7 volatile unsigned long virt, phys;
8 volatile unsigned long *GPECON, *GPEDAT, *GPEUP;
9 unsigned long reg;
10 struct resource *led_resource;
11
12 void led_device_init(void)
13 {
14 phys = 0x56000000;
15 virt = (unsigned long)ioremap(phys, 0x0c);
16
17 GPECON = (unsigned long *)(virt + 0x40);
18 GPEDAT = (unsigned long *)(virt + 0x44);
19 GPEUP = (unsigned long *)(virt + 0x48);
20 }
21
22 void led_configure(void)
23 {
24 reg = ioread32(GPECON);
25 reg &= ~(3 << 24);
26 reg |= (1 << 24);
27 iowrite32(reg, GPECON);
28
29 reg = ioread32(GPEUP);
30 reg &= ~(3 << 12);
31 iowrite32(reg, GPEUP);
32 }
33
34 void led_on(void)
35 {
36 reg = ioread32(GPEDAT);
37 reg &= ~(1 << 12);
38 iowrite32(reg, GPEDAT);
39 }
40
41 void led_off(void)
42 {
43 reg = ioread32(GPEDAT);
44 reg |= (1 << 12);
45 iowrite32(reg, GPEDAT);
46 }
47
48 static int __init test_init(void) //模块初始化函数
49 {
50 led_device_init();
51
52 led_resource = request_mem_region(phys, 0x0c, "LED_MEM");
53 if(NULL == led_resource){
54 printk("request mem error!\n");
55 return - ENOMEM;
56 }
57
58 led_configure();
59 led_on();
60 printk("hello led!\n");
61 return 0;
62 }
63
64 static void __exit test_exit(void) //模块卸载函数
65 {
66 if(NULL != led_resource){
67 led_off();
68 iounmap((void *)virt);
69 release_mem_region(phys, 0x0c);
70 }
71 printk("bye\n");
72 }
73
74 module_init(test_init);
75 module_exit(test_exit);
76
77 MODULE_LICENSE("GPL");
78 MODULE_AUTHOR("xoao bai");
79 MODULE_VERSION("v0.1");
写完就得验证一下:
[root: 3rd]# insmod test.ko
hello led! //灯亮了
[root: 3rd]# cat /proc/iomem
19000300-19000310 : cs8900
19000300-19000310 : cs8900
。。。。
56000000-5600000b : LED_MEM //看到了
57000000-570000ff : s3c2410-rtc
57000000-570000ff : s3c2410-rtc
5a000000-5a0fffff : s3c2440-sdi
[root: 3rd]# rmmod test
bye //灯灭了
[root: 3rd]# cat /proc/iomem //LED_MEM不见了
19000300-19000310 : cs8900
19000300-19000310 : cs8900
。。。。。。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
有时候会觉得,每次访问硬件都要先通过ioremap来获取虚拟地址,其实有没有一种一劳永逸的方法,只要一次的操作,以后就能通过这个地址来访问硬件。答案是“有”,这就是接下来要介绍的IO内存静态映射。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、静态IO是怎么建立的
Io静态映射发生在内核启动的时候,接下来通过内核源代码来分析,如果你的开发板是mini2440或者时候mini2440的内核配置文件,可以跟着我同样修改。注意:我的开发板只是使用mini2440的配置文件,外围电路跟mini2440不一样。
注:以下代码在内核目录linux-2.6.29/arch/arm/mach-s3c2440/mach-mini2440.c。
静态映射的建立方法,是在内核启动的时候,读取struct map_desc结构体里面的成员:
/*arch/arm/include/asm/mach/map.h*/
14 struct map_desc {
15 unsigned long virtual; //存放以后需要操作的虚拟地址,由自己定义
16 unsigned long pfn; //需要操作的硬件的物理地址对应的页帧号,即物理地址右移12
17 unsigned long length; //需要映射的大小
18 unsigned int type; //类型
19 };
这里要说明两个成员:
1)物理地址的页帧号pfn:如果你了解linux的页式管理,那你应该知道,一个页的大小是4096B(2 << 12),所以一个地址的31-12位是用来表示一个地址对应的页帧号。对应的,一个物理地址,只要右移12位就能得到对应的页帧号,也可以使用函数:
#define __phys_to_pfn(paddr) ((paddr) >> PAGE_SHIFT) //其实也是右移12位
2)类型type:有下面这些类型定义:
/*include/asm/mach/map.h*/
21 /* types 0-3 are defined in asm/io.h */
22 #define MT_UNCACHED 4
23 #define MT_CACHECLEAN 5
24 #define MT_MINICLEAN 6
25 #define MT_LOW_VECTORS 7
26 #define MT_HIGH_VECTORS 8
27 #define MT_MEMORY 9
28 #define MT_ROM 10
其中,MT_UNCACHED是我们常用的,表示该地址不放在缓冲区cached中。要知道,为了方便内存的访问,内核会将一些经常使用的内存数据放在cached中,但是这样在访问寄存器时就不行了,如果寄存器改变了,内核读取数据是从cached中读取数据,而不在寄存器读取,这样的做法是不合理的。
首先,我们需要往这个结构体中填充我们需要访问的地址。
本来这个结构体是空的。
/*arch/arm/mach-s3c2440/mach-mini2440.c*/
45 static struct map_desc mini2440_iodesc[] __initdata = {
46 };
修改成:
45 static struct map_desc mini2440_iodesc[] __initdata = {
46 {
47 .virtual = 0xeeee0000,
48 .pfn = __phys_to_pfn(0x56000000), //0x56000
49 .length = SZ_4K, //这里我直接映射一页
50 .type = MT_UNCACHED
51 },
52 };
填充结构体后,我们再看看启动时通过调用什么函数:
/*linux-2.6.29/arch/arm/mach-s3c2440/mach-mini2440.c*/
264 static void __init mini2440_map_io(void)
265 { //就是这个函数,将我刚才修改的结构体的成员进行静态映射
266 s3c24xx_init_io(mini2440_iodesc, ARRAY_SIZE(mini2440_iodesc));
267 s3c24xx_init_clocks(12000000);
268 s3c24xx_init_uarts(mini2440_uartcfgs, ARRAY_SIZE(mini2440_uartcfgs));
269 }
这个函数里面有一个重要的函数——iotable_init(),其实大部分的工作都由这个函数来完成,实现静态映射。
既然修改了内核,就需要重新编译内核:
make bzImage
通过上面这几步,我们就实现了这样的一个操作,可以通过虚拟地址0xeeee0000来访问一页的物理地址。既然知道了虚拟地址和物理地址之间的关系,就不需要再用ioremap了。
为了更好的规范,我们使用一个头文件来定义寄存器的访问地址,方便编程时使用:
/*5th_mm_3/1st/test_map_io.h*/
1 #ifndef _TEST_H
2 #define _TEST_H
3
4 typedef volatile unsigned long * s3c_reg_t;
5
6 #define S3C2440_VA 0xeeee0000 //我们已经知道静态映射的虚拟地址
7
8 #define S3C2440_BASE(x) (S3C2440_VA + (x))
9 #define S3C2440_GPEBASE S3C2440_BASE(0x40)
10 #define S3C2440_GPECON S3C2440_BASE(0x40) //这就是我们要操作的寄存器
11 #define S3C2440_GPEDAT S3C2440_BASE(0x44)
12 #define S3C2440_GPEUP S3C2440_BASE(0x48)
13
14
15 #endif /* _TEST_H */
然后再修改一下前一节的2nd函数,去掉ioremap部分:
1 #include <linux/module.h>
2 #include <linux/init.h>
3
4 #include <asm/io.h>
5 #include "test_map_io.h"
6
7 s3c_reg_t *GPECON, *GPEDAT, *GPEUP;
8 unsigned long reg;
9
10 void led_device_init(void)
11 {
12 GPECON = (s3c_reg_t *)S3C2440_GPECON;
13 GPEDAT = (s3c_reg_t *)S3C2440_GPEDAT;
14 GPEUP = (s3c_reg_t *)S3C2440_GPEUP;
15 }
16
17 void led_configure(void)
18 {
19 reg = ioread32(GPECON);
20 reg &= ~(3 << 24);
21 reg |= (1 << 24);
22 iowrite32(reg, GPECON);
23
24 reg = ioread32(GPEUP);
25 reg &= ~(3 << 12);
26 iowrite32(reg, GPEUP);
27 }
28
29 void led_on(void)
30 {
31 reg = ioread32(GPEDAT);
32 reg &= ~(1 << 12);
33 iowrite32(reg, GPEDAT);
34 }
35
36 void led_off(void)
37 {
38 reg = ioread32(GPEDAT);
39 reg |= (1 << 12);
40 iowrite32(reg, GPEDAT);
41 }
42
43 static int __init test_init(void) //模块初始化函数
44 {
45 led_device_init();
46 led_configure();
47 led_on();
48 printk("hello led!\n");
49 return 0;
50 }
51
52 static void __exit test_exit(void) //模块卸载函数
53 {
54 led_off();
55 printk("bye\n");
56 }
57
58 module_init(test_init);
59 module_exit(test_exit);
60
61 MODULE_LICENSE("GPL");
62 MODULE_AUTHOR("xoao bai");
63 MODULE_VERSION("v0.1");
除了红笔部分,和删除的ioremap相关函数,其他部分都没有改动,效果还是一样,加载灯亮,卸载灯灭。