Linux 内核的阻塞
阻塞亦即等待,有两种,一种忙等待,一种休眠等待。
应用层阻塞,多数是指休眠等待。
阻塞的实现,一般都是基于等待队列机制。
非阻塞:事件如果得不到满足,不会休眠等待,而是立即返回到用户空间。
Linux 系统默认的实现是阻塞方式,但是,linux系统也要求,驱动编程必须支持非阻塞方式的实现。
那么,阻塞,我们可以利用等待队列完成,那么非阻塞的方式呢?
非阻塞的实现
应用层,用户空间需要显式操作。可以在open时给定 O_NONBLOCK,或者,调用fcntl 函数设置。
驱动的行为:
如果指定了阻塞,那么事件不满足,进行休眠
如果指定了非阻塞,那么时间不满足,不休眠,立刻返回用户空间
那么应用层这样的设置,是怎么下来的呢?
每当用户层open一个设备时,内核会创建一个struct file对象,来描述这个文件被打开以后的状态属性,如果指定了 O_NONBLOCK,那么这个字段会被保存到该对象的 f_flags 域中。底层驱动操作时,用户自实现的比如 write read这样的callback中该struct file 对象的地址会被传下来,驱动层便可以通过该参数,去访问 f_flags 域,获取到该 O_NONBLOCK 标识。
if ((file->f_flags & O_NONBLOCK) != 0x00)
{
// 存在 O_NONBLOCK 标识
// 非阻塞方式
if (condition)
{
// 满足条件,进行处理
}
else
{
// 不满足条件,直接返回
return -EAGAIN;
}
}
else
{
// 阻塞方式
// 采用等待队列实现
}
Linux 内核的内存访问(32bit机器为例)
首先,需要明确一些Linux 系统的一些实现规则
1. 不管是用户空间还是内核空间,程序访问的地址都是虚拟地址
2. CPU最终访问的地址必然是物理地址,虚拟地址转换成物理地址是通过MMU硬件逻辑单元
3. 有些CPU没有MMU,也能运行衍生版的linux(uclinux)
4. 4G虚拟内存,用户空间每个进程独占前3G虚拟内存(0x0~0xBFFFFFFF)
5. 4G虚拟内存,内核空间占后1G虚拟内存(0xC0000000~0xFFFFFFFF)
6. 用户空间只能最大访问3G的物理内存
7. 内核空间能够访问所有的物理内存
8. 一个物理地址可以有多个虚拟地址,一个虚拟地址不能对应多个物理地址
虚拟内存和物理内存的映射关系
虚拟内存分为:用户虚拟内存和内核虚拟内存
用户虚拟内存和物理内存的映射采用动态映射(用户需要访问某块物理内存,内核会动态创建用户虚拟内存和物理内存的映射关系(页表)),如果将来不再访问, 需要将这种映射关系解除,动态映射的缺点在于内存访问的效率不高!
内核虚拟内存和物理内存的映射采用静态映射(又称一一映射),在内核启动初始化的时候就已经完成这种一一映射关系,将来内核只需直接访问即可,无需再次建立映射关系,加快内存的访问效率。
内核虚拟内存如果真的与物理内存严格的一一对应,内核访问内存的效率得到提高,但是内核只能访问1G的物理内存,无法访问多余的物理内存,内核如何访问其余的物理内存或者任何一个设备的物理地址呢?
内核将1G的内核虚拟内存进行分区划分,既满足内存访问的效率,又能满足内核能够访问所有的物理内存。
具体划分地址由低到高如下(对于X86架构):
直接内存映射区
内核初始化时,将1G内核虚拟内存的前896M跟物理内存的前896M进行一一映射,这块虚拟内存的访问效率高。这块内核虚拟内存区域又称低端内存(lowmem)
动态内存映射区
如果要访问其余的物理内存或者任何一个物理地址,可以将物理内存和物理地址跟动态内存映射区的内核虚拟内存或者地址进行映射(建立页表),如果不在使用,记得要解除地址映射;缺点是内存的访问效率不高,但是能够保证访问所有的物理内存和地址,默认大小为120M。
永久内存映射区(kmap) / 固定内存映射区(kmap_atomic)
永久就是固定,固定就是永久,如果对某块物理内存或者某个物理地址要频繁的访问,可以将此地址跟永久或者固定内存映射区的虚拟内存进行映射,不再使用时,可以不用解除地址映射,优点是即可访问其余的物理内存或者地址也加快内存的访问效率。
两者不同的是:前者用于进程上下文,后者用于中断上下文,大小:各占4M。
动态内存映射区+永久+固定 = 高端内存
Linux内核内存分配的方法
kmalloc/kfree
void *kmalloc(size_t size, gfp_t flags)
函数功能:
从直接内存映射区分配内存,申请的物理,虚拟都连续,内存的大小为最小为32字节,最大为4M
参数:
size,大小
flags,指定分配内存时的行为,一般指定为:
1. GFP_KERNEL:告诉内核,请努力帮我把这次内存分配搞定,如果内存不足,会导致休眠,不能用于中断上下文,成功概率比较大
2. GFP_ATOMIC:如果分配内存,内存不足,不会导致休眠而是立即返回,可以用于中断上下文,成功概率比较小
返回值:
返回分配的内核起始虚拟地址
void kfree(void *addr)
__get_free_pages/free_pages
1page=1页=4K
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
从直接内存映射区分配;物理,虚拟都连续;最小为1页,最大为4M。
mask:指定分配内存时的行为,一般指定为:
GFP_KERNEL:告诉内核,请努力帮我把这次内存分配搞定,如果内存不足,会导致休眠,不能用于中断上下文,成功概率比较大
GFP_ATOMIC:如果分配内存,内存不足,不会导致休眠。立即返回,可以用于中断上下文,成功概率比较小
order: 0 分配 1页,1 分配 2页,2 分配 3页...
返回值:
返回分配的内核起始虚拟地址,注意数据类型的转换!
vmalloc/vfree
void *vmalloc(int size)
从动态内存映射区分配内存;虚拟上连续,物理上不一定连续;如果内存不足,会导致休眠,不能用于中断上下文;最大理论上默认120M。
返回值:返回起始的虚拟地址
void vfree(void *addr) //释放内存
全局数组,申请内存
比如: static char g_buf[5*1024*1024];
另:
1. 在内核的启动参数中指定vmalloc=250M,告诉内核将内核的动态内存映射区的大小由默认的120M扩展到250M。
setenv bootargs root=/dev/nfs nfsroot=... vmalloc=250M
saveenv
boot
2. 在内核的启动参数中指定mem=10M,告诉内核将物理内存的最后10M预留出来,预留给驱动单独使用,利用ioremap函数进行映射即可访问
setenv bootargs root=/dev/nfs nfsroot=... mem=10M
saveenv
boot
将来驱动利用ioremap函数将这10M的物理内存进行映射,即可访问
Linux 内核 ioremap
说明:
1. 不论是在用户空间还是在内核空间,一律不允许访问物理地址
2. 如果要访问外设的物理地址必须要进行地址映射,将物理地址要不映射到用户的虚拟地址上,要不映射到内核的虚拟地址上,一旦映射完毕,将来访问这个虚拟地址就是在访问对应的物理地址
如何将设备的物理地址和物理内存映射到内核空间的虚拟地址和内存上呢?
利用ioremap函数即可完成映射。
void *ioremap(unsigned long phys_addr, int size)
函数功能:
将物理内存或者地址映射到内核的虚拟内存或者地址上
将来访问这个内核虚拟内存或者地址就是在访问对应的物理内存和地址
说白了,就是把物理内存或者其他的一些硬件寄存器之类的映射到了内核的内存上。如果映射到用户空间的内存?后续mmap!
参数:
phys_addr,要映射的物理起始地址
size,物理内存的大小
"物理内存":不单单指内存条,外设对应的寄存器也通常称之为物理内存
返回值:
返回映射的对应内核起始的虚拟地址
解除地址映射:iounmap(映射的内核起始虚拟地址);
上下文
前面一直总是进程上下文,中断上下文。
中断上下文,典型的几个:
1. request_irq 注册的中断服务函数。
2. 基于软中断的 tasklet。
3. 软中断中注册的callback。
4. 信号处理函数(也是不允许休眠的)
以上的场景,其实都是通过特定的API,调用,挂载callback,至于callback的执行时机,我们是不能确定的,执行的上下文,都是“别人”那边场景。
所以,在内核编程中,注册 callback的时候,一定要小心,我们注册的函数,是否是中断上下文,这些其实都是需要一一甄别的,其实也不多,也就那几个...
为什么驱动中的函数,会有进程上下文的概念?
一般的驱动函数,比如前文中的 hello 的驱动,其中的函数,如果我们不是注册特定的callback,或者使用tasklet之类,一般都是运行在进程上下文中的,因为fops中的callback函数,如 open release,都是应用进程通过 open close 等等调用的,其实这就是所谓的进程上下文。