回顾:
1.linux内核等待队列机制
目的:应用程序通过系统调用,进入内核空间,一旦发现驱动操作的设备或者数据不可用,由内核让进程进行休眠和唤醒操作。
等待队列机制编程的实现过程:
1.方法1
分配等待队列头
初始化等待队列头
分配进程的容器,内核表示当前进程用current指向当前进程的struct task_struct结构体对象!
初始化进程容器
将当前进程添加到睡眠队列中
设置当前进程的休眠状态
让进程进入休眠:schedule/schedule_timeout
等待被唤醒
一旦被唤醒,要判断唤醒的原因:
signal_pending:判断进程是否接收到了信号
设置当前进程的状态为运行
从睡眠队列中移除被唤醒进程
msleep/ssleep/down/down_interruptible都是利用等待队列来实现的!
2.方法2
分配等待队列头
初始化等待队列头
wait_event/wait_event_interruptible/wait_event_timeout/wait_event_interruptible_timeout
唤醒的方法:
不管是方法1还是方法2的休眠,唤醒的方法都是一样!
wake_up/wake_up_interruptibel
*****************************************************************************************
Day11
linux内核阻塞和非阻塞:
阻塞:进程的状态要不为忙,要不就为休眠
忙等待
休眠等待:利用等待队列机制
非阻塞:如果应用程序进入内核空间,发现设备或者数据不可用,不会休眠也不会忙等待,而是立即返回到用户空间!
linux系统访问文件默认采用阻塞方式;
问:如果应用程序访问设备或者文件等采用非阻塞如何实现?
答:只需在打开open设备或者文件时,指定一个操作选项即可:O_NONBLOCK,例如:
int fd = open("/dev/myled",O_RDWR|O_NONBLOCK);
问:虽然应用程序在打开设备时,指定了O_NONBLOCK,但是操作设备只能在内核空间的驱动中完成,那么驱动如何直到应用程序是采用非阻塞呢?如果一旦驱动发现应用采用非阻塞方式来访问设备,进程发现设备不可用,那么进程立即返回到用户空间,如果发现设备可用,操作设备!
答:每当应用程序调用open打开设备文件时,通过软中断,陷入内核空间,找到对应的内核函数sys_open,sys_open创建struct file结构体对象来描述设备文件打开以后的状态信息,其中file结构体中有一个unsigned long f_flags来保存open打开文件时指定的属性(O_RDWR|O_NONBLOCK),底层驱动的接口函数,例如:
int led_open(struct inode *inode, structfile *file)
int led_close(struct inode *inode, structfile *file)
ssize_t led_close(struct file *file,...)
...
所以底层驱动只需要通过file指针就能够获取f_flags操作属性,那么底层驱动只需要通过一下代码就可以判断应用是采用阻塞还是非阻塞:
if (file->f_flags & O_NONBLOCK) {
//采用非阻塞
if(如果设备不可用){
//立即返回到用户空间
return -EAGAIN;
}{
//如果设备可用,操作设备
}
} else {
//采用阻塞
//采用等待队列
}
总结:以后设备驱动只要阻塞的实现,必须添加非阻塞的实现!
案例:给之前的按键驱动添加非阻塞的功能!
**********************************************************
linux内核内存相关内容:
1.内存空间和IO空间:
X86架构:有两类总线
一类总线的位宽为16位,硬件地址空间范围64K,如果将外设接到这个总线上,那么CPU访问这个外设通过in,out指令来完成访问;这个地址空间称之为IO空间;
另一类总线的位宽是32位,硬件地址空间范围是4G,如果将外设接到这个总线上,比如内存,GPIO寄存器,nandflash控制器等,那么CPU访问这写外设通过地址或者指针的形式来访问;这个地址空间称之为内存空间;
ARM结构:只有内存空间;无IO空间一说!
2.物理地址和虚拟地址
明确:CPU最终访问的地址都是物理地址;
明确:不管是在内核空间还是用户空间,程序访问的地址都是虚拟地址;
虚拟地址的优点:
1.如果物理内存比如只有1G,采用虚拟地址这个策略,能够让用户看到比实际的物理内存大很多的内存空间;
2.由于CPU最终访问的地址是物理地址,所以在进行虚拟地址和物理地址转换的时候,可以对地址进行有效性和权限的检查,保证系统的安全;
3.4G虚拟地址空间的划分:
用户空间:0x00000000~ 0xBFFFFFFF
内核空间:0xC0000000~0x FFFFFFFF
4.用户进程都有自己独立的3G空间,互补干涉;但是内核空间的1G,所有进程都共享;
5.用户空间和内核空间要进行数据交互,必须通过系统调用
3.MMU
MMU是CPU自带的硬件逻辑单元,一旦有这个硬件逻辑单元,那么CPU就可以采用虚拟地址策略,MMU的主要职责:
1.将虚拟地址转换成对应的物理地址,根据页表
2.检查地址的有效性和权限,决定是访问操作这个地址还是给CPU发送一个异常信号!
4.TLB
TLB是MMU自带的硬件逻辑单元,类似"Cache",它用于页表的缓存,每当MMU进行地址转换的时候,首先到TLB中找转换关系,如果找到,进行转换,如果没有找到,就从主存中取出页表信息进行地址转换,更新TLB,为下一次转换做准备!
5.uclinux
运行在不带MMU,不支持MMU的CPU上;
处理的地址都是物理地址;
ARM7,比如三星s3c4510,s3c44b0等;
FPGA处理器+ARM核
DSP处理器
6.物理页
linux内核管理物理内存,单位不是按字节,按页来进行管理,一页默认是4K;
内核描述每一个物理页都是使用structpage结构体,如果这个物理地址和一个虚拟地址做好了映射,这个结构体中的virtual字段保存映射的虚拟地址;
内核在初始化时,给每一个物理页创建一个struct page结构体来描述每一个物理页的信息,所占用的内存为:
sizeof(struct page) * 1G/4K
7.用户空间,内核空间和物理内存的映射关系:
用户空间3G和物理内存的映射是一个动态的过程,也就是说用户需要访问某个物理内存,就动态创建物理内存和用户虚拟地址的映射(建立页表),如果不再使用,接触映射关系(销毁页表),这个访问方式效率不高!
用户空间能够访问的物理内存最大也只有3G;
内核空间1G和物理内存的映射是在内核初始化时就已经建立一一映射的关系,一旦建立好,后续访问无需动态建立页表,加快地址的访问速度。
物理和虚拟地址的一一映射:
物理地址 虚拟地址
0x0 0xC0000000
0x1 0xC0000001
0X2 0xC0000002
... ...
问题:内核虚拟地址为1G,如果物理内存大于1G,如果让内核访问其他的物理内存呢?
答:linux内核将1G的虚拟地址划分为若干个区域来实现访问所有的物理内存:
X86:将1G的内核虚拟地址分别划分为4个区域:
直接内存映射区:
映射关系:在内核初始化(不是动态映射),如果物理内存大于1G,内核将内核的1G虚拟内存的前896M虚拟内存和物理内存进行一一映射;1G的虚拟内存还剩余128M;
起始地址:0xC0000000
大小:如果物理内存大于1G,那么直接内存映射区的大小就为896M,如果物理内存的大小就是直接内存映射区的大小
别名:低端内存
动态内存映射区
映射关系:如果需要访问某一个物理地址(寄存器地址,内存地址),只需动态的建立物理地址和动态内存映射区中虚拟地址的映射关系(页表)即可,每当程序访问这个内核的动态内存映射区的虚拟地址其实最终也是在访问对应的物理地址(MMU);
如果不在使用对应物理地址,一定要解除地址映射!
起始地址:随着物理内存的大小变化而变化,如果物理内存小于896M,比如512M,起始地址=0xC0000000 + 物理内存的大小,如果物理内存大于896M,起始地址0xC000000+896M
默认的大小为120M
永久内存映射区:
映射关系:它也是实现物理地址和内核虚拟地址的一个动态映射,只是如果在某些时刻,如果访问这个物理地址的频率很大,如果频繁的建立页表和销毁页表,那么访问效率不高,于是可以将这个物理地址映射到永久内存映射区,一经映射,无需再销毁对应的映射关系,加快地址的访问速度。当然可以人为的销毁这个映射关系。使用kmap函数进行映射,这个映射有可能会休眠,所以不能在中断上下文中使用。
大小:4M
固定内存映射区:
和永久内存映射区的目的是一样的,区别仅仅是固定内存映射区的虚拟地址在映射的时候,可以用在中断上下文中。
大小:4M
注意:直接内存映射区又叫低端内存;
动态内存映射区+永久内存映射区+固定内存映射区=高端内存;
S5PV210处理器1G内核虚拟地址的划分:
异常向量表:
vector : 0xffff0000 - 0xffff1000 ( 4 kB)
固定内存映射区:
fixmap : 0xfff00000 - 0xfffe0000 ( 896kB)
DMA内存映射区:
DMA : 0xff000000 -0xffe00000 ( 14 MB)
动态内存映射区:
vmalloc : 0xf4800000 - 0xfc000000 ( 120 MB)
直接内存映射区:
lowmem : 0xc0000000 - 0xf4000000 ( 832MB)
注意:ARM架构本身的异常向量表的入口地址有两个:
0地址还有0xFFFF0000
**********************************************************
linux内核分配内存的方法:
1.kmalloc/kfree
void *kmalloc(size_t size, gfp_t flags);
函数功能:从低端内存(直接内存映射区)分配,物理上连续,虚拟上也连续,一次性最小分配32字节,新版本内核(2.6.33以后)最大只能分配4M,之前内核最大只能是128K。
参数:
size:大小,最小是32字节,最大是4M/128K
flags:分配内存的行为
GFP_KERNEL:如果在分配内存时,指定了这个宏,就等于告诉内核请努力的帮我分配好内存,如果内存不足,会导致休眠,所以这个宏不能用于中断上下文中。
GFP_ATOMIC:如果在分配内存时,指定了这个宏,就等于告诉内核,如果内存不足,不做休眠,而是理解返回一个-ENOMEM,可以用在中断上下文中;
返回值:就是分配的内核的虚拟内存的起始地址
void kfree(void *addr); //释放内存
2.__get_free_pages()/free_pages;
unsigned long __get_free_pages(gfp_t gfp_mask, unsignedint order);
函数的功能:这个函数由kmalloc实现,它也是从低端内存分配,物理,虚拟上都连续,最小为1页,最大是4M
参数:
gfp_mask:分配内存的行为GFP_KERNEL,GFP_ATOMIC
order:如果order=0,分配1页,order=1,分配2页,order=2,分配4页...
返回值:就是分配的内核的虚拟内存的起始地址
//释放内存
void free_pages(unsigned long addr, unsigned int order);
3.vmalloc/vfree:
void *vmalloc(unsigned long size);
函数功能:分配内存从动态内存映射区分配,虚拟上连续的,但物理地址不一定连续!理论上最大能分配120M
size:大小
默认的分配行为是阻塞方式(会休眠)
返回值:分配的内核虚拟内存的起始地址
释放内存
vfree(void *addr);
4.内存分配的其他方法:
4.1.在驱动中定义一个全局数组,例如
static char buf[5*1024*1024]; //BSS段,不影响文件的大小
static char buf[5*1024*1024] = {0xaa, 0x55, 0xaa, 0x55};//数据段,会影响ko文件的大小,最终影响ko文件的加载速度!
4.2.通过设置内核的启动参数调整将动态内存映射区的大小,例如:
setenv bootargs root=/dev/nfs ...vmalloc=256M //将动态内存映射区的大小由120M调整到256M
4.3.通过设置内核的启动参数,添加mem=8M来将物理内存高地址的8M预留出来,然后驱动通过ioremap函数将这个物理内存映射到直接内存映射区中再使用。
**********************************************************
问题:如何在设备驱动中,访问设备的物理地址呢?
例如设备驱动访问GPIO对应的寄存器来实现对灯的操作!
答:
千万明确一点:不管是在内核空间还是在用户空间,软件一律不能去直接访问物理地址,只能访问用户虚拟地址或者内核虚拟地址,如果驱动要访问物理地址,思路就是将物理地址映射到内核的虚拟地址上即可,一旦映射成功,以后访问这个内核虚拟地址就是在访问物理地址!
这里使用大名鼎鼎:ioremap函数;
函数原型:
void *ioremap(unsiged long phy_addr, intsize);
函数功能:
给定一个物理地址,将这个物理地址映射到内核的虚拟地址上,映射到内核的动态内存映射区的虚拟地址上。这个过程是动态映射的过程(动态建立页表)
参数说明:
phy_addr:要访问的物理地址
size:映射的内存区域的大小
返回值:就是映射的内核虚拟地址,一旦有这个映射好的内核虚拟地址,以后用户访问这个内核虚拟地址就是在访问对应的物理地址;
例如:物理起始地址为0x10000000,地址空间为12字节;
void *addr = ioremap(0x10000000, 12);
注意:映射完毕以后,物理上连续,虚拟上也连续!
映射的关系:
物理地址 虚拟地址
0x10000000 addr
0x10000004 addr+4
0x10000008 addr+8
... ...
注意:一个物理地址可以有多个虚拟地址;
一旦不在使用物理地址,一定要解除地址映射:
iounmap(void *addr); //addr就是映射的内核虚拟地址
案例:利用ioremap实现操作LED对应的寄存器来操作LED
案例:分析4.0代码的作用和功能!
**********************************************************
linux内核链表:
传统链表的特点:
struct fox {
inta;
intb;
structfox *next;
structfox *prev;
};
1.传统链表的指针域的数据类型和节点的数据类型保持一致!
2.传统链表的指针域永远指向下一个节点的首地址或者前一个节点的首地址!
3.传统链表头一般都是拿首节点作为链表头!
注意:不管什么链表,一般链表都要进行初始化,插入,删除,遍历,合并等操作。
缺点:
由于传统链表的指针域和节点的数据类型保持一致,并且链表的操作都需要插入,删除和遍历,那么都需要定义一组操作函数,那么节点的数据类型不一样,最终导致不同的链表都有自己独立的操作函数,例如现在有三个链表:
struct fox, struct dog, struct cat ,那么最终以上三个链表都有自己独立的三个操作函数,最终导致代码量相当的庞大和难以维护!
总结:链表的操作关键操作的指针域!
linux内核链表特点:
struct list_head {
structlist_head *next, *prev;
};
1.内核链表的指针域和节点的数据类型不必一致,内核链表的指针域只跟struct list_head相关;
2.内核链表的指针域永远指向下一个或者前一个节点的指针域,不在指向节点的首地址;
3.内核链表头一般用struct list_head来表示,内核链表头只包含指针域,不包含数据域!
4.内核链表具有通用信,因为链表操作都是通过指针域来进行,所以既然内核链表的指针域都跟struct list_head相关,所以内核的双链表的操作只需要一组函数即可!这些函数操作方法都是定义在内核源码的include/linux/list.h
linux内核链表的使用步骤:
1.定义链表头
struct list_head fox_head;
2.初始化链表头
INIT_LIST_HEAD(&fox_head);
或者1+2:
LIST_HEAD(fox_head);
3.声明描述实物的结构体,然后将struct list_head嵌入这个结构体中:
struct fox{
inta;
intb; //数据域
structlist_head list; //添加指针域
};
struct dog{
intc;
intd;
structlist_head list; //指针域
};
4.分配节点:
用户空间:malloc
内核空间:kzalloc =kmalloc + memset();
struct fox *fox1 = kzalloc(sizeof(structfox), GFP_KERNEL);
struct fox *fox2 = kzalloc(sizeof(structfox), GFP_KERNEL);
struct fox *fox3 = kzalloc(sizeof(structfox), GFP_KERNEL);
5.节点的插入操作
list_add(struct list_head *new, struct list_head *head);
功能:插入一个新节点到链表头中去
new:新节点的指针域的首地址
head:链表头的首地址
例如:
list_add(&fox1->list, &fox_head);
list_add(&fox2->list, &fox_head);
list_add(&fox3->list, &fox_head);
添加以后的顺序:链表头-》fox3->fox2->fox1
list_add_tail(struct list_head *new,
struct list_head *head);
功能:插入一个新节点到链表头中去
new:新节点的指针域的首地址
head:链表头的首地址
例如:
list_add_tail(&fox1->list, &fox_head);
list_add_tail(&fox2->list, &fox_head);
list_add_tail(&fox3->list, &fox_head);
添加以后的顺序:链表头-》fox1->fox2->fox3
6.删除链表节点:
list_del(struct list_head *new);
功能:删除节点
new:要删除节点的指针域的首地址
list_del(&fox1->list);
list_del(&fox2->list);
list_del(&fox3->list);
7.遍历链表
#define list_for_each(pos, head) \
for(pos = (head)->next;
pos != (head); \
pos = pos->next)
功能:遍历链表,本质上是一个for循环
pos: 保存每一个节点的指针域的首地址
head:链表头
#define list_for_each_safe(pos, n, head) \
for(pos = (head)->next, n = pos->next;
pos != (head); \
pos = n, n = pos->next)
功能:遍历链表
pos:保存要访问的节点的指针域的首地址
n:保存下一个节点的指针域的首地址
head:链表头
总结:如果仅仅是遍历链表,以上两个方法都行,但是如果在变量链表的时候,进行了对链表的删除节点操作,必须使用后者!否则造成内核崩溃!
问:pos是保存节点的指针域的首地址,如何访问这个节点的 数据域呢?
8.通过节点的指针域的首地址获取节点的首地址,从而访问节点的数据域
#define list_entry(ptr, type, member) \
container_of(ptr,type, member)
container_of使用:
功能:如果已知结构体某一个成员的地址,通过这个宏能够获取结构体的首地址;
ptr:已知的成员的地址,pos = &fox1->list
type:结构体名,struct fox
member:成员的变量名 list
返回值:就是结构体的首地址
例如:
struct fox *fox = container_of(pos, structfox, list);
访问数据域:fox->a,fox->b
案例:在用户空间统计3个狐狸
案例:在内核空间统计5个员工