1.1 Linux内核简介
从上图得知,Linux由用户空间和内核空间两部分组成。
为什么Linux系统会被划分为用户空间与内核空间?
现代CPU通常实现了不同的工作模式,以ARM为例,实现了7种工作模式:
用户模式(usr)、快速中断(fiq)、外部中断(irq)、管理模式(svc)、数据访问中止
(abt)、系统模式(sys)、未定义指令异常(und)
X86也实现了4个不同的级别:Ring0—Ring3。Ring0下,可以执行特权指令,可以访问IO设备等,在Ring3则有很多限制。
Linux系统利用了CPU的这一特性,使用了其中的两级来分别运行Linux内核与应用程序,这样使操作系统本身得到充分的保护。例如:如果使用X86,用户代码运行在Ring3,内核代码运行在Ring0。
内核空间与用户空间是程序执行的两种不同状态,通过系统调用和硬件中断能够完成从用户空间到内核空间的转移。
Linux内核如何构成的?
Linux内核架构
系统调用接口
SCI 层为用户空间提供了一套标准的系统调用函数来访问Linux内核,搭起了用户空间到内核空间的桥梁。
进程管理
进程管理的重点是创建进程(fork、exec),停止进程(kill、exit),并控制它们之间的通信(signal 或者 POSIX 机制)。进程管理还包括控制活动进程如何共享CPU,即进程调度。
内存管理
内存管理的主要作用是控制多个进程安全地共享内存区域。
网络协议栈
内核协议栈为Linux提供了丰富的网络协议实现。
虚拟文件系统(VFS)
VFS隐藏各种文件系统的具体细节,为文件操作提供统一的接口。
设备驱动
Linux 内核中有大量代码都在设备驱动程序中,它们控制特定的硬件设备。
1.2 Linux内核源代码
目录结构
Linux内核源代码采用树形结构进行组织,非常合理地把功能相关的文件都放在同一个子目录下,使得程序更具可读性。
arch目录
arch是architecture的缩写。内核所支持的每种CPU体系,在该目录下都有对应的子目录。每个CPU的子目录,又进一步分解为boot,mm,kernel等子目录,分别包含控制系统引导,内存管理,系统调用等。
| --x86 /* 英特尔cpu及与之相兼容体系结构的子目录*/
| |--boot /*引导程序*/
| | |--compressed /*内核解压缩*/
| |--tools /*生成压缩内核映像的程序*/
| |--kernel /*相关内核特性实现方式,如信号处理、时钟处理*/
| |--lib /*硬件相关工具函数*/
block目录
部分块设备驱动程序
crypto目录
加密、压缩、CRC校验算法
documentation
内核的文档
drivers目录
设备驱动程序
fs目录
存放各种文件系统的实现代码。每个子目录对应一种文件系统的
实现,公用的源程序用于实现虚拟文件系统vfs。
| |--devpts /*/dev/pts虚拟文件系统*/
| |--ext2 /*第二扩展文件系统*/
| |--fat /*MS的fat32文件系统*/
| |--isofs /*ISO9660光盘cd-rom上的文件系统*/
include目录
内核所需要的头文件。与平台无关的头文件在include/linux 子目
录下,与平台相关的头文件则放在相应的子目录中。
init目录
内核初始化代码
ipc目录
进程间通信的实现代码
kernel目录
Linux大多数关键的核心功能都是在这个目录实现。(调度程序,进程控制,模块化)
lib目录
库文件代码
mm目录
mm目录中的文件用于实现内存管理中与体系结构无关的部分
net目录
网络协议的实现代码
| |--802 /*802无线通讯协议核心支持代码*/
| |--appletalk /*与苹果系统连网的协议*/
| |--ax25 /*AX25无线INTERNET协议*
| |--bridge /*桥接设备*/
| |--ipv4 /*IP协议族V4版32位寻址模式*/
| |--ipv6 /*IP协议族V6版*/
samples
一些内核编程的范例
scripts
配置内核的脚本
security
SElinux的模块
sound
音频设备的驱动程序
usr
cpio命令实现
virt
内核虚拟机
1.3 Linux内核配置与编译
Linux内核具有可定制的优点,具体步骤如下:
1. 清除临时文件、中间文件和配置文件.
• make clean
remove mostgenerated files but keep the config
• make mrproper
remove all generatedfiles + config files
• make distclean
mrproper + remove editorbackup and patch files
2、确定目标系统的软硬件配置情况,比如CPU的类型、网卡的型号,所需支持的网络协议等。
3、使用如下命令之一配置内核:
make config:基于文本模式的交互式配置。
make menuconfig:基于文本模式的菜单型配置。(推荐使用)
make oldconfig:
使用已有的配置文件(.config),但是会询问新增的配置选项。
make xconfig:
图形化的配置(需安装图形化系统)。
make menuconfig 是最为常用的内核配置方式,使用方法如下:
1、使用方向键在各选项间移动;
2、使用“Enter”键进入下一层选单;每个选项上的高亮字母是键盘快捷方式,使用它可以快速地到达想要设置的选单项。
3、在括号中按“y”将这个项目编译进内核中,按“m”编译为模块,按“n”为不选择(按空格键也可在编译进内核、编译为模块和不编译三者间进行切换),按“h”将显示这个选项的帮助信息,按“Esc”键将返回到上层选单。
内核配置通常在一个已有的配置文件基础上,通过修改得到新的配置文件Linux内核提供了一系列可供参考的内核配置文件,位于Arch/$cpu/configs
4、编译内核:
make zImage
make bzImage
区别:在X86平台,zImage只能用于小于512K的内核
*如需获取详细编译信息,可使用:
make zImage V=1
make bzImage V=1
** 编译好的内核位于arch/<cpu>/boot/目录下 **
5、编译内核模块:编译内核配置时选择M的项
make modules
6、安装内核模块
vmakemodules_install
**将编译好的内核模块从内核源代码目录copy
至/lib/modules下*
7、制作init ramdisk
mkinitrdinitrd-$version $version
例:
mkinitrdinitrd-2.6.29 2.6.29
*$version 可以通过查询/lib/modules下的目录得到
内核安装(X86平台)
1、cp arch/x86/boot/bzImage
/boot/vmlinuz-$version
2、cp $initrd /boot/
3、修改/etc/grub.conf 或者 /etc/lilo.conf
** $version 为所编译的内核版本号**
1.4 Linux内核模块开发
什么是内核模块?
Linux内核的整体结构非常庞大,其包含的组件也非常多,如何使用需要的组件呢:
方法一:把所有的组件都编译进内核文件,即:zImage或bzImage,但这样会导致两个问题:一是生成的内核文件过大;二是如果要添加或删除某个组件,需要重新编译整个内核。
有没有一种机制能让内核文件(zImage或bzImage)本身并不包含某组件,而是在该组件需要被使用的时候,动态地添加到正在运行的内核中呢?
有,Linux提供了一种叫做“内核模块”的机制,就可以实现以上效果。
内核模块具有如下特点:
• 模块本身并不被编译进内核文件(zImage或者bzImage)
• 可以根据需求,在内核运行期间动态的安装或卸载。
范例(hello world)
#include<linux/init.h>
#include<linux/module.h>
static inthello_init(void)
{
printk(KERN_WARNING"Hello,world !\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_INFO"Goodbye, world\n");
}
module_init(hello_init);
module_exit(hello_exit);
1、模块加载函数(必需)
安装模块时被系统自动调用的函数,通过module_init宏来指定
2、模块卸载函数(必需)
卸载模块时被系统自动调用的函数,通过module_exit宏来指定
模块的编译
在Linux 2.6下编译模块,多使用makefile
范例 makefile 分析
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KDIR :=/lib/modules/2.6.18-53.el5/build
all:
make -C $(KDIR)M=$(PWD) modules
clean:
rm -f *.ko *.o*.mod.o *.mod.c *.symvers
endif
范例 多文件makefile 分析
ifneq($(KERNELRELEASE),)
obj-m := mymodule.o
mymodule-objs := file1.ofile2.o file3.o
else
KDIR :=/lib/modules/2.6.18-53.el5/build
all:
make -C $(KDIR)M=$(PWD) modules
clean:
rm -f *.ko *.o*.mod.o *.mod.c *.symvers
endif
安装与卸载
加载 insmod (insmod hello.ko)
卸载 rmmod (rmmod hello)
查看 lsmod
加载 modprobe (modprobe hello)
modprobe 如同 insmod, 也是加载一个模块到内核。它的不同之处在于它会根据文件/lib/modules/<$version>/modules.dep来查看要加载的模块, 看它是否还依赖于其他模块,如果是,modprobe 会首先找到这些模块, 把它们先加载到内核。
对比应用程序,内核模块具有以下不同:
应用程序是从头(main)到尾执行任务,执行结束后从内存中消失。内核模块则
是先在内核中注册自己以便服务于将来的某个请求,然后它的初始化函数结束,此时模块仍然存在于内核中,直到卸载函数被调用,模块才从内核中消失。
模块可选信息
1、许可证申明
宏MODULE_LICENSE用来告知内核, 该模块带有一个许可证,没有这样的说明,加载模块时内核会抱怨。有效的许可证有"GPL“、"GPLv2"、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MPL/GPL"和"Proprietary"。
2、作者申明(可选)
MODULE_AUTHOR(“SimonLi");
3、模块描述(可选)
MODULE_DESCRIPTION("HelloWorld Module");
4、模块版本(可选)
MODULE_VERSION("V1.0");
5、模块别名(可选)
MODULE_ALIAS("asimple module");
6、模块参数
通过宏module_param指定模块参数,模块参数用于在加载模块时传递参数给模块。
module_param(name,type,perm)
name是模块参数的名称,type是这个参数的类型,
perm是模块参数的访问权限。
type常见值:
bool:布尔型 int:整型 charp:字符串型
perm 常见值:
S_IRUGO:任何用户都对/sys/module中出现的该参数具有读权限
S_IWUSR:允许root用户修改/sys/module中出现的该参数
例如:
int a = 3;
char *st;
module_param(a,int,S_IRUGO);
module_param(st,charp,S_IRUGO);
.ko PK .o
Before Linux 2.6, auser space program would interpret
the ELF object(.o)file and do all the work of linking it to
the running kernel,generating a finished binary image.
The program wouldpass that image to the kernel and the
kernel would dolittle more than stick it in memory. In
Linux 2.6, thekernel does the linking. A user space
program passes thecontents of the ELF object file
directly to thekernel. For this to work, the ELF object
image must containadditional information. To identify
this particular kindof ELF object file, we name the file
with suffix".ko"("kernel object") instead of ".o"
内核符号导出
/proc/kallsyms 记录了内核中所有导出的符号的名字与地址。
内核符号的导出使用:
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名)
其中EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块。
常见问题:版本不匹配
内核模块的版本由其所依赖的内核代码版本所决定, 在加载内核模块时,insmod程序会将内核模块版本与当前正在运行的内核版本比较,如果不一致时,就会出现类似下面的错误:
insmod hello.ko
disagrees aboutversion of symbol struct_module
insmod: errorinserting 'hello.ko': -1 Invalid module format
解决方法:
1、使用 modprobe --force-modversion 强行插入
2、确保编译内核模块时,所依赖的内核代码版本等同于当前正在运行的内核。
**可通过uname –r 察看当前运行的内核版本**
内核打印
Printk是内核中出现最频繁的函数之一,通过将Printk与Printf对比,将有助于大家理解。
相同点:
•打印信息
不同点:
• Printk在内核中使用,Printf在应用程序中使用
• Printk允许根据严重程度,通过附加不同的“优先级”来对消息分类。
在<linux/kernel.h>中定义了8种记录级别。按照优先级递减的顺序分别是:
KERN_EMERG“<0>”
用于紧急消息,常常是那些崩溃前的消息。
KERN_ALERT“<1>”
需要立刻行动的消息。
KERN_CRIT“<2>”
严重情况。
KERN_ERR “<3>”
错误情况。
KERN_WARNING“<4>”
有问题的警告
KERN_NOTICE“<5>”
正常情况,但是仍然值得注意
KERN_INFO“<6>”
信息型消息
KERN_DEBUG“<7>”
用作调试消息
没有指定优先级的printk默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,
它是一个在kernel/printk.c中定义的整数。
在2.6.29内核中
#defineDEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */
控制台优先级配置
/proc/sys/kernel/printk
6417
• Console_loglevel
•Default_message_loglevel
•Minimum_console_level
•Default_console_loglevel
优先级使用:
printk(KERN_INFO"Name:%s\n",name);
或printk(" (6)Name:%s\n",name);
如果要在控制台打印信息,优先级必须高于6
1.5 Linux内核启动流程
1.6 内存管理
内存是Linux内核所管理的最重要的资源之一,内存管理子系统是操作系统中最重要的部分之一。对于立志从事内核开发的工程师来说,熟悉Linux的内存管理系统非常重要。
地址类型
物理地址:物理地址是指出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。
线性地址(虚拟地址):线性地址又名虚拟地址,在32位CPU架构下,可以表示4G的地址空间,用16进制表示就是0x00000000到0xffffffff。
逻辑地址:程序代码经过编译后在汇编程序中使用的地址。
地址转换
CPU要将一个逻辑地址转换为物理地址,需要两步:首先CPU利用段式内存管理单元,将逻辑地址转换成线性地址,再利用页式内存管理单元,把线性地址最终转换为物理地址。
段式管理(16位CPU)
16位CPU内部拥有20位的地址线,它的寻址范围就是2的20次方,也就是1M的
内存空间。但是16位CPU用于存放地址的寄存器(IP,SP......)只有16位,因此只能访问65536个存储单元,64K。
为了能够访问1M的内存空间,CPU就采用了内存分段的管理模式,并在CPU内部加入了段寄存器。16位CPU把1M内存空间分为若干个逻辑段,每个逻辑段的要求如
下:
1、逻辑段的起始地址(段地址)必须是16的倍数,即最后4个二进制位必须全为0。
2、逻辑段的最大容量为64K(why?)
物理地址的形成方式:
由于段地址必须是16的倍数,所以值的一般形式为XXXX0H,即前16位二进制位是变化的,后四位是固定的0,鉴于段地址的这种特性,可以只保存前16位二进制位来保存整个段基地址,所以每次使用时要用段寄存器左移补4个0(乘以16)来得到实际的段地址。
在确定了某个存储单元所属的段后,只是知道了该存储单元所属的范围(段地址 ->
段地址+65536),如果想确定该内存单元的具体位置,还必须知道该单元在段内的偏移。有了段地址和偏移量,就可以唯一的确定内存单元在存储器中的具体位置。
逻辑地址=段基地址+段内偏移量,由逻辑地址得到物理地址的公式为:
PA = 段寄存器的值 * 16 + 逻辑地址
段寄存器是为了对内存进行分段管理而增加的,16位CPU有四个段寄存器,程序可同时访问四个不同含义的段。
1) CS + IP : 用于代码段的访问,CS 指向存放程序的段基址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令。
2) SS + SP :用于堆栈段的访问,SS指向堆栈段的基地址,SP指向栈顶,可以通过SS和SP两个寄存器直接访问栈顶单元的内存物理位置。
3) DS + BX :用于数据段的访问。 DS中的值左移四位得到数据段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。
4) ES + BX :用于附加段的访问。 ES中的值左移四位得到附加段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。
段式管理(32位CPU)
32位pc的内存管理仍然采用“分段”的管理模式,逻辑地址同样由段地址和偏移量两
部分组成,32位pc的内存管理和16位pc的内存管理有相同之处也有不同之处,因为
32位pc采用了两种不同的工作方式:实模式和保护模式。
1)实模式
在实模式下,32位CPU的内存管理与16位CPU是一致的。
2) 保护模式
段基地址长达32位,每个段的最大容量可达4G,段寄存器的值是段地址的“选择器”(Selector),用该“选择器” 从内存中得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移量,这与16位CPU的物理地址计算方式完全不同。
32位CPU内有6个段寄存器,其值在不同的模式下具有不同的含义:
1、在实模式下:
段寄存器的值*16就是段地址
2、在保护模式下:
段寄存器的值是一个选择器,间接指出一个32位的段地址
分页管理
从管理和效率的角度出发,线性地址被分为固定长度的组,称为页(page),例如32位的机器,线性地址最大可为4G,如果用4KB为一个页来划分,这样整个线性地址就被划分为2的20次方个页。
另一类“页”,称之为物理页,或者是页框、页桢。分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与线性地址页是相同的。
1、分页单元中,页目录的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2、每一个进程,都有其独立的虚拟地址空间,运行一个进程,首先需要将它的页目录地址放到cr3寄存器中,将其他进程的保存下来。
3、每一个32位的线性地址被划分为三部份:页目录索引(10
位):页表索引(10位):偏移(12位)
依据以下步骤进行地址转换:
1、装入进程的页目录地址(操作系统在调度进程时,把这个地址装入CR3)
2、根据线性地址前十位,在页目录中,找到对应的索引项,页目录中的项是一个页表的地址
3、根据线性地址的中间十位,在页表中找到页的起始地址
4、将页的起始地址与线性地址的最后12位相加,得到物理地址
这样的二级模式是否能够覆盖4G的物理地址空间?为什么?(通过计算得出结论)
页目录共有:2^10项,也就是说有这么多个页表;每个目表对应了:2^10页;每个页中可寻址:2^12个字节。2^32 = 4GB
Linux内存管理
Linux内核的设计并没有全部采用Intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。
所有段的基地址均为0
由此可以得出,每个段的逻辑地址空间范围为0-4GB。因为每个段的基地址为0,因此,逻辑地址与线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在Linux中所提到的逻辑地址和线性地址(虚拟地址),可以认为是
一致的。看来,Linux巧妙地把段机制给绕过去了,而完全利用了分页机制。
Linux页式管理
前面介绍了i386的二级页管理架构,不过有些CPU(Alpha 64位)使用三级,甚至四级架构,Linux 2.6.29内核为每种CPU提供统一的界面,采用了四级页管理架构,来兼容二级、三级、四级管理架构的CPU。
这四级分别为:
1. 页全局目录 (Page Global Directory):即pgd,是多级页表的抽象最高层。
2. 页上级目录(Page Upper Directory):即pud。
3. 页中间目录(Page Middle Directory):即pmd,是页表的中间层。
4. 页表(Page Table Entry):即 pte。
1.7 进程控制
什么是进程?什么是程序?进程和程序的区别在哪里?
程序是存放在磁盘上的一系列代码和数据的可执行映像,是一个静止的实体。
进程是一个执行中的程序。它是动态的实体。
进程四要素
1.有一段程序供其执行。这段程序不一定是某个进程所专有,可以与其他进程共用。
2. 有进程专用的内核空间堆栈。
3. 在内核中有一个task_struct数据结构,即通常所说的“进程控制块”。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。
4. 有独立的用户空间。
进程描述
在Linux中,线程、进程都使用struct task_struct来表示,它包含了大量描述进程/线程的信息,其中比较重要的有:
pid_t pid;
进程号,最大值10亿
volatile long state/* 进程状态 */
1.
TASK_RUNNING
进程正在被CPU执行,或者已经准备就绪,随时可以执行。当一个进程刚被创建时,就处于TASK_RUNNING状态。
2.
TASK_INTERRUPTIBLE处于等待中的进程,待等待条件为真时被唤醒,也可以被信号或者中断唤醒。
3.TASK_UNINTERRUPTIBLE
处于等待中的进程,待资源有效时唤醒,但不可以由其它进程通过信号(signal)或中断唤醒。
4. TASK_STOPPED
进程中止执行。当接收到SIGSTOP和SIGTSTP等信号时,进程进入该状态,接收到SIGCONT信号后,进程重新回到TASK_RUNNING。
5. TASK_KILLABLE
Linux2.6.25新引入的进程睡眠状态,原理类似于TASK_UNINTERRUPTIBLE,但是可以被致命信号(SIGKILL)唤醒。
6. TASK_TRACED
正处于被调试状态的进程。
7. TASK_DEAD
进程退出时(调用do_exit),state字段被设置为该状态。
int exit_state /*进程退出时的状态*/
EXIT_ZOMBIE(僵死进程)
表示进程的执行被终止,但是父进程还没有发布
waitpid()系统调用来收集有关死亡的进程的信息。
EXIT_DEAD(僵死撤销状态)
表示进程的最终状态。父进程已经使用wait4()或
waitpid()系统调用来收集了信息,因此进程将由系
统删除。
struct mm_struct *mm
进程用户空间描述指针,内核线程该指针为空。
unsigned int policy
该进程的调度策略。
int prio
优先级,相当于 2.4 中 goodness() 的计算结果,在
0--(MAX_PRIO-1) 之间取值(MAX_PRIO 定义为 140),其中
0—(MAX_RT_PRIO-1)(MAX_RT_PRIO 定义为100)属于实时进程范围,MAX_RT_PRIO-MX_PRIO-1 属于非实时进程。
数值越大,表示进程优先级越小。
int static_prio
静态优先级,与 2.4 的 nice 值意义相同。nice 值仍沿用 Linux 的传统,在 -20 到 19 之间变动,数值越大,进程的优先级越小。nice 是用户可维护的,但仅影响非实时进程的优先级。进程初始时间片的大小仅决定于进程的静态优先级,这一点不论是实时进程还是非实时进程都一样,不过实时进程的static_prio 不参与优先级计算。nice 与static_prio 的关系如下:
static_prio =MAX_RT_PRIO + nice + 20
内核定义了两个宏用来完成这一转换:PRIO_TO_NICE()、
NICE_TO_PRIO
structsched_rt_entity rt
rt->time_slice
时间片,进程的缺省时间片与进程的静态优先级(在 2.4
中是 nice 值)相关,使用如下公式得出:
MIN_TIMESLICE +((MAX_TIMESLICE - MIN_TIMESLICE) *
(MAX_PRIO-1 -(p)->static_prio) / (MAX_USER_PRIO-1))
内核将 100-139 的优先级映射到200ms-10ms 的时间片上去,优先级数值越大,则分配的时间片越小。
Task_struct位置(2.4)
Task_struct位置(2.6)
在Linux中用current指针指向当前正在运行的进程的task_struct。
进程创建
进程销毁
进程销毁可以通过几个事件驱动 — 通过正常的进程结束、通过信号或是通过对 exit 函数的调用。不管进程如何退出,进程的结束都要借助对内核函数 do_exit的调用。
1.8 进程调度
什么是调度?
从就绪的进程中选出最适合的一个来执行。
学习调度需要掌握哪些知识点?
1、调度策略
2、调度时机
3、调度步骤
调度策略
SCHED_NORMAL(SCHED_OTHER):普通的分时进程
SCHED_FIFO :先入先出的实时进程
SCHED_RR:时间片轮转的实时进程
SCHED_BATCH:批处理进程
SCHED_IDLE: 只在系统空闲时才能够被调度执行的进程
调度类
调度类的引入增强了内核调度程序的可扩展性,这些类(调度程序模块)封装了调度策略,并将调度策略模块化。
CFS 调度类(在 kernel/sched_fair.c 中实现)用于
以下调度策略:SCHED_NORMAL、SCHED_BATCH 和 SCHED_IDLE。
实时调度类(在 kernel/sched_rt.c 中实现)用于
SCHED_RR 和 SCHED_FIFO 策略。
struct sched_class {
struct sched_class*next;
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int wakeup);
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int sleep);
void (*yield_task)(struct rq *rq, struct task_struct *p);
void(*check_preempt_curr) (struct rq *rq, struct task_struct *p);
struct task_struct *(*pick_next_task)(struct rq *rq);
void(*put_prev_task) (struct rq *rq, struct task_struct *p);
unsigned long(*load_balance) (struct rq *this_rq, int this_cpu, struct rq *busiest, unsigned
long max_nr_move,unsigned long max_load_move, struct sched_domain *sd, enum
cpu_idle_type idle,int *all_pinned, int *this_best_prio);
void(*set_curr_task) (struct rq *rq); void (*task_tick) (struct rq *rq, structtask_struct *p);
void (*task_new)(struct rq *rq, struct task_struct *p);
};
pick_next_task:选择下一个要运行的进程
调度时机
调度什么时候发生?即:schedule()函数什么时候被调用?
调度的发生有两种方式:
1、主动式
在内核中直接调用schedule()。当进程需要等待资源等而暂时停止运行时,会把状态置于挂起(睡眠),并主动请求调度,让出CPU。
主动放弃cpu例:
1. current->state= TASK_INTERRUPTIBLE;
2. schedule();
2、被动式(抢占)
用户抢占(Linux2.4、Linux2.6),内核抢占(Linux2.6)
用户抢占
用户抢占发生在:
从系统调用返回用户空间。
从中断处理程序返回用户空间。
内核即将返回用户空间的时候,如果
need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。
ENTRY(ret_from_exception)//异常返回
get_thread_info tsk
mov why, #0
b ret_to_user
__irq_usr: //在用户态收到中断
usr_entry
kuser_cmpxchg_check
......
......
......
b ret_to_user
ENTRY(ret_to_user)
ret_slow_syscall:
disable_irq @disable interrupts
ldr r1, [tsk,#TI_FLAGS]
tst r1,#_TIF_WORK_MASK
bne work_pending
work_pending:
tst r1,#_TIF_NEED_RESCHED
bne work_resched
work_resched:
bl schedule
内核抢占
在不支持内核抢占的系统中,进程/线程一旦运行于内核空间,就可以一直执行,直到它主动放弃或时间片耗尽为止。这样一些非常紧急的进程或线程将长时间得不到运行。
在支持内核抢占的系统中,更高优先级的进程/线程可以抢占正在内核空间运行的低优先级进程/线程。
在支持内核抢占的系统中,某些特例下是不允许内核抢占的:
内核正进行中断处理。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。
内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。
进程正持有spinlock自旋锁、writelock/readlock读写锁等,当持有这些锁时,不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。
内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。
为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占计数。这一变量被设置在进程的thread_info结构中。每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_count就减1,同时进行可抢占的判断与调度。
内核抢占可能发生在:
中断处理程序完成,返回内核空间之前。
当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等。
调度标志:TIF_NEED_RESCHED
作用:
内核提供了一个need_resched标志来表明是否需要重新执行一次调度。
设置:
当某个进程耗尽它的时间片时,会设置这个标志;
当一个优先级更高的进程进入可执行状态的时候,也会设置这个标志。
调度步骤
Schedule函数工作流程如下:
1). 清理当前运行中的进程;
2). 选择下一个要运行的进程;(pick_next_task 分析)
3). 设置新进程的运行环境;
4). 进程上下文切换 。
1.9 系统调用
定义
Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。
区别
系统调用和普通的函数调用非常相似,区别仅仅在于,系统调用由操作系统内核实现,运行于内核态;而普通的函数调用由函数库或用户自己提供,运行于用户态。
库函数
Linux系统还提供了一些C语言函数库,这些库对系统调用进行了一些包装和扩展,这些库函数与系统调用的关系非常紧密。
系统调用数
在2.6.29 版内核中,共有系统调用332个,可在arch/arm/include/asm/unistd.h中找到它们。
使用系统调用
#include<time.h>
main()
{
time_t the_time;
the_time=time((time_t*)0); /*调用time系统调用*/
printf("Thetime is %ld\n",the_time);
}
/* 从格林尼治时间1970年1月1日0:00开始到现在的秒数。 */
工作原理
一般情况下,用户进程是不能访问内核的。它既不能访问内核所在的内存空间,也不能调用内核中的函数。系统调用是一个例外。其原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令,这个指令会让用户程序跳转到一个事先定义好的内核中的一个位置:
在Intel CPU中,这个指令由中断0x80实现。
在ARM中,这个指令是SWI。
进程可以跳转到的内核位置是
ENTRY(vector_swi)<entry-common.S>。这个过程检查系统调用号,这个号码告诉内核进程请求哪种服务。然后,它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。接着,就调用函数,等返回后,做一些系统检查,最后返回到进程。
实现系统调用
向内核中添加新的系统调用,需要执行 3个步骤:
1. 添加新的内核函数
2. 更新头文件 unistd.h
3. 针对这个新函数更新系统调用表calls.S
1. 在kernel/sys.c中添加函数:
asmlinkage intsysMul(int a, int b)
{
int c;
c = a*b;
return c;
}
/* asmlinkage:使用栈传递参数 */
2. 在arch/arm/include/asm/unistd.h中添加如下代码:
#define __NR_sysMul361
3.在arch/arm/kernel/calls.S中添加代码,指向新实现的系统调用函数:
CALL(sysMul)
#include<stdio.h>
#include<linux/unistd.h>
main()
{
int result;
result =
syscall(361,1, 2);
printf("result= ", result);
}
1.10 proc文件系统
什么是proc文件系统?
实例:通过 /proc/meminfo,查询当前内存使用情况。
结论:proc文件系统是一种在用户态检查内核状态的机制。
Proc文件
子目录/文件名
apm 高级电源管理信息
bus总线以及总线上的设备
devices可用的设备信息
driver已经启用的驱动程序
interrupts中断信息
ioports端口使用信息
version 内核版本
特点
每个文件都规定了严格的权限可读?可写?哪个用户可读?哪个用户可写?
可以用文本编辑程序读取(more命令,cat命令,vi程序等等)
不仅可以有文件,还可以有子目录。
可以自己编写程序添加一个/proc目录下的文件。
文件的内容都是动态创建的,并不存在于磁盘上。
内核描述
structproc_dir_entry {
{
。。 。。。。。。。。。。。。。。。。。
read_proc_t*read_proc;
write_proc_t*write_proc;
。。。。。。。。。。。。。。。。。。。
}
创建文件
structproc_dir_entry* create_proc_entry (const char
*name,mode_tmode,struct proc_dir_entry *parent)
功能:
创建proc文件
参数:
name :要创建的文件名
mode :要创建的文件的属性 默认0755
parent :这个文件的父目录
创建目录
structproc_dir_entry * proc_mkdir (const char *name,struct
proc_dir_entry*parent)
功能:
创建proc目录
参数:
name :要创建的目录名
parent :这个目录的父目录
删除目录/文件
voidremove_proc_entry (const char *name,struct
proc_dir_entry*parent)
功能:
删除proc目录或文件
参数:
name :要删除的文件或目录名
parent :所在的父目录
读写
为了能让用户读写添加的proc文件,需要挂接上读写回调函数:
read_proc
write_proc
读操作
int read_func (char*buffer,char **stat,off_t
off,int count,int*peof,void *data)
参数:
buffer :把要返回给用户的信息写在buffer里,最大不超过PAGE_SIZE
stat :一般不使用
off :偏移量
count :用户要取的字节数
peof :读到文件尾时,需要把*peof置1
data :一般不使用
写操作
int write_func(struct file *file,const char
*buffer,unsignedlong count,void *data)
参数:
file :该proc文件对应的file结构,一般忽略。
buffer :待写的数据所在的位置
count :待写数据的大小
data :一般不使用
实现流程
实现一个proc文件的流程:
(1)调用create_proc_entry创建一个struct proc_dir_entry。
(2)对创建的struct proc_dir_entry进行赋值:read_proc,mode,owner,size,write_proc 等等。
1.11 内核异常
定义
Oops 可以看成是内核级的Segmentation Fault。应用程序如果进行了非法内存访问或执行了非法指令,会得到Segfault信号,一般的行为是coredump,应用程序也可以自己截获Segfault信号,自行处理。如果内核自己犯了这样的错误,则会打出Oops信息。
分析步骤
1. 错误原因提示
2. 调用栈(对照反汇编代码)
3. 寄存器
1.12 进程地址空间
虚拟内存
Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间是大小为3G,用户看到和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。
Linux将4G的虚拟地址空间划分为两个部分——用户空间与内核空间。用户空间从0到0xbfffffff,内核空间从3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用访问内核空间。
进程空间
用户空间对应进程,所以每当进程切换,用户空间就会跟着变化。
每个进程的用户空间都是完全独立、互不相干的。把同一个程序同时运行10次(为了能同时运行,让它们在返回前睡眠100秒),会看到10个进程使用的线性地址一模一样。
创建进程fork()、程序载入execve()、动态内存分配malloc()等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。
实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了物理地址上。
内核内存分配
在应用程序中,常使用malloc函数进行动态内存分配,而在Linux内核中,通常使用kmalloc来动态分配内存。
kmalloc 原型是:
#include<linux/slab.h>
void *kmalloc(size_tsize, int flags)
参数:
size:要分配的内存大小。
flags:分配标志, 它控制 kmalloc 的行为。
GFP_ATOMIC
用来在进程上下文之外的代码(包括中断处理)中分配内存,从不睡
眠。
GFP_KERNEL
进程上下文中的分配。可能睡眠。(16M-896M)
__GFP_DMA
这个标志要求分配能够 DMA 的内存区(物理地址在16M以下的页帧)
__GFP_HIGHMEM
这个标志表示分配的内存位于高端内存。(896M以上)
最常用的标志是GFP_KERNEL,它的意思是该内存分配是由运行在内核态的进程调用的。也就是说,调用它的函数属于某个进程的,当空闲内存太少时,kmalloc函数会使当前进程进入睡眠,等待空闲页的出现。
如果kmalloc是在进程上下文之外调用,比如在中断处理,任务队列处理和内核定时器处理中。这些情况属于中断上下文,不能进入睡眠,这时应该使用优先权GFP_ATOMIC。
如果模块需要分配大块的内存,那使用面向页的分配技术会更好
get_zeroed_page(unsignedint flags)
返回指向新页面的指针,并将页面清零。
__get_free_page(unsignedint flags)
和get_free_page类似,但不清零页面。
__get_free_pages(unsignedint flags,unsigned int order )
分配若干个连续的页面,返回指向该内存区域的指针,但也不清零这段内存区域。
当程序用完这些页, 可以使用下列函数之一来释放它们:
voidfree_page(unsigned long addr)
voidfree_pages(unsigned long addr, unsigned
long order)
**如果释放的和先前分配数目不等的页面,会导致系统错误**
1.13 内核地址空间
内核空间
内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。
高端内存
物理内存896MB以上的部分称之为高端内存。
内核空间分布
直接内存映射区(Direct Memory Region)
从3G开始,最大896M的线性地址区间,我们称作直接内存映射区,这是因为该区域的线性地址和物理地址之间存在线性转换关系
线性地址=3G + 物理地址
例:
物理地址区间0x100000-0x200000映射到线性空间就是
3G+0x100000-3G+0x200000。
动态内存映射区(Vmalloc Region)
该区域的地址由内核函数vmalloc来进行分配,其特点是线性空间连续,但对应的物理空间不一定连续。vmalloc分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。
永久内存映射区(PKMap Region )
对于896MB以上的高端内存,可使用该区域来访问,访问方法:
1. 使用alloc_page(__GFP_HIGHMEM)分配高端内存页
2. 使用kmap函数将分配到的高端内存映射到该区域
固定映射区(Fixing Mapping Region)
PKMap区上面,有4M的线性空间,被称作固定映射区,它和4G顶端只有4K的隔离带。固定映射区中每个地址项都服务于特定的用途,如ACPI_BASE等。
1.14 内核链表
链表是一种常用的数据结构,它通过指针将一系列数据节点连接成一条数据链。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
通常链表数据结构至少包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型。
内核链表
在Linux内核中使用了大量的链表结构来组织数据。这些链表大多采用了[include/linux/list.h]中实现的一套精彩的链表数据结构。
链表数据结构的定义:
struct list_head
{
struct list_head*next, *prev;
};
list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双向循环链表。
链表操作
Linux内核中提供的链表操作主要有:
初始化链表头
INIT_LIST_HEAD(list_head*head)
插入节点
list_add(structlist_head *new, struct list_head *head)
list_add_tail(structlist_head *new, struct list_head *head)
删除节点
list_del(structlist_head *entry)
提取数据结构
list_entry(ptr,type, member)
已知数据结构中的节点指针ptr,找出数据结构,
例:list_entry(aup, struct autofs, list)
遍历
list_for_each(struclist_head *pos, struc list_head *head)
例:
struct list_head*entry;
struct list_headcs46xx_devs; //链表头
list_for_each(entry,&cs46xx_devs)
{
card =list_entry(entry, struct cs_card, list);
if(card->dev_midi == minor)
break;
}
1.15 内核定时器
度量时间差
时钟中断由系统的定时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确定,HZ是一个与体系结构无关的常数,可配置(50-1200),在X86平台,默认值为1000。
每当时钟中断发生时,全局变量jiffies(unsigned long)就加1,因此jiffies记录了自linux启动后时钟中断发生的次数。驱动程序常利用jiffies来计算不同事件间的时间间隔。
延迟执行
如果对延迟的精度要求不高,最简单的实现方法如下--忙等待:
unsigned longj=jiffies + jit_delay*HZ;
while (jiffies<j)
{
/* do nothing */
}
内核定时器
定时器用于控制某个函数(定时器处理函数)在未来的某个特定时间执行。内核定时器注册的处理函数只执行一次--不是循环执行的。
内核定时器被组织成双向链表,并使用struct timer_list结构描述。
struct timer_list {
struct list_headentry /*内核使用*/;
unsigned longexpires; /*超时的jiffies值*/
void(*function)(unsigned long); /*超时处理函数*/
unsigned long data;/*超时处理函数参数*/
struct tvec_base*base; /*内核使用*/
};
操作定时器的有如下函数:
voidinit_timer(struct timer_list *timer);
初始化定时器队列结构。
void add_timer(structtimer_list * timer);
启动定时器。
int del_timer(structtimer_list *timer);
在定时器超时前将它删除。当定时器超时后,系统会自动地将它删除。
1.16 交叉工具链
安装
进行嵌入式开发前,首先需安装交叉工具链,步骤如下:
1. 解压工具链到某一目录下
例:
tar xvzfarm-linux-gcc-4.3.2.tar.gz –C /
2. 修改/etc/profile ,添加
pathmunge/usr/local/arm/4.3.2/bin
3. 执行source /etc/profile
使用
编译器:arm-linux-gcc
arm-linux-gcchello.c –o hello
反汇编工具:arm-linux-objdump
arm-linux-objdump –D–S hello
ELF文件查看工具:arm-linux-readelf
arm-linux-readelf –ahello
arm-linux-readelf –dhello 查看hello使用的动态库
1.17 嵌入式Linux产品开发流程
嵌入式Linux应用程序
1、QT图形化应用程序开发
2、网络应用程序开发
协议:
Http、Wap、Smpp、PPPoe......
SSL
移动通讯知识 GPRS、GSM
1.18 嵌入式Linux内核制作
系统组成
嵌入式Linux系统由Linux内核与根文件系统两部分构成,两者缺一不可。
内核制作
制作嵌入式平台使用的Linux内核,方法和制作PC平台的Linux内核基本一致,下面使用对比的方式介绍如何制作用于mini2440开发板的内核。
1、清除原有配置与中间文件
x86: make distclean
arm: make distclean
2、配置内核
x86: make menuconfig
arm: make menuconfigARCH=arm
/*nfs与ramdisk启动的区别*/
3、编译内核
x86: make bzImage
arm: make uImageARCH=arm
CROSS_COMPILE=arm-linux-
1.19 根文件系统制作
根文件系统是Linux启动时使用的第一个文件系统。没有根文件系统,Linux将无法正常启动。
根文件系统由一系列目录组成,目录中包含了应用程序、C库、以及相关的配置文件。
1、创建根文件系统的目录
mkdir rootfs
cd rootfs
mkdir bin dev etclib proc sbin sys usr mnt tmp var
mkdir usr/binusr/lib usr/sbin lib/modules
2、创建设备文件
cd dev/
mknod -m 666 consolec 5 1
mknod -m 666 null c1 3
cd ..
3、安装/etc
tar etc.tar.gz –C/xxx/rootfs
4、编译内核模块
进入Linux内核目录(linux 2.6.29)
make modulesARCH=arm
CROSS_COMPILE=arm-linux-
5、安装内核模块
make modules_installARCH=arm
INSTALL_MOD_PATH=/xxx/rootfs
6、配置busybox Busybox: 嵌入式开发中的瑞士军刀
进入busybox目录执行
make menuconfig
a.进入 Busybox Settings
build Options->
1.选中 “Build busybox as a static binary”, 静态链接
2.Cross Compilerprefix (arm-linux-)
InstallationOptions->
1.选中 “Don‘t use /usr”, 选中该项可以避免busybox 被安装到宿主系统的/usr目录下,破坏宿主系统
2.BusyboxInstallation Prefix (/xxx/rootfs)
该选项表明编译后的busybox的安装位置
7、编译、安装busybox
make ARCH=armCROSS_COMPILE=arm-linux-
make install
Ramdisk
基于前面步骤制作好的根文件系统,可进一步制作ramdisk,步骤如下:
1、genext2fs -b 8192 -d /xxx/rootfs ramdisk
** genext2fs 为产生ramdisk的工具 **
2、 gzip -9 -f ramdisk
1.20 Linux文件系统介绍
定义
Linux支持多种文件系统类型,包括ext2、ext3、vfat、jffs、romfs和nfs等,为了对各类文件系统进行统一管理,Linux引入了
虚拟文件系统VFS(Virtual File System),为各类文件系统提供一个统一的应用编程接口。
文件系统架构
文件系统类型
根据存储设备的硬件特性、系统需求,不同的文件系统类型有不同的应用场合。在嵌入式Linux应用中,主要的存储设备为
RAM 和FLASH,常用的基于存储设备的文件系统类型包括:jffs2, yaffs, cramfs,ramdisk, ramfs等。
基于FLASH的文件系统
Flash(闪存)作为嵌入式系统的主要存储媒介,主要有NOR和NAND两种技术。Flash存储器的擦写次数是有限的,NAND闪存还有特殊的硬件接口和读写时序。因此,必须针对Flash的硬件特性设计符合应用要求的文件系统。
JFFS
JFFS文件系统最早是由瑞典Axis
Communications公司基于Linux2.0内核为嵌入式系统开发的文件系统。JFFS2是RedHat公司基于JFFS开发的闪存文件系统,最初是针对RedHat公司的嵌入式产品eCos开发的嵌入式文件系统,所以JFFS2也可以用在Linux, uCLinux中。
JFFS2
Jffs2: 日志闪存文件系统版本2 (Journalling
Flash FileSystem v2)
主要用于NOR型flash,基于MTD驱动层,特点是:可读写、支持数据压缩的日志型文件系统,并提供了崩溃/掉电安全保护等。缺点主要是当文件系统已满或接近满时,因为垃圾收集的关系而使jffs2的运行速度大大放慢。
jffsx不适合用于NAND闪存主要是因为NAND闪存的容量一般较大,这样导致jffs为维护日志节点所占用的内存空间迅速增大。另外,jffsx文件系统在挂载时需要扫描整个FLASH,以找出所有的日志节点,建立文件结构,对于大容量的NAND闪存会耗费大量时间。
Yaffs
yaffs/yaffs2 (YetAnother Flash FileSystem)是专为NAND型flash而设计的一种日志型文件系统。与jffs2相比,它减少了一些功能(例如不支持数据压缩),所以速度更快,挂载时间很短,对内存的占用较小。另外,它还是跨平台的文件系统,除了Linux和eCos,还支持WinCE, pSOS和ThreadX等。
yaffs与yaffs2的主要区别在于,前者仅支持小页(512 Bytes) NAND闪存,后者则可支持大页(2KB) NAND闪存。同时,yaffs2
在内存空间占用、垃圾回收速度、读/写速度等方面均有大幅提升。
Cramfs
Cramfs是Linux的创始人 Linus 参与开发的一种只读的压缩文件系统,它也基于MTD驱动程序。Cramfs文件系统以压缩方
式存储,在运行时解压缩,所有的应用程序要求被拷到RAM里去运行。另外,它的速度快,效率高,其只读的特点有利于保护文件系统免受破坏,提高了系统的可靠性。
基于RAM的文件系统
Ramdisk是将一部分固定大小的内存当作分区来使用。它并非一个实际的文件系统,而是一种将实际的文件系统(如ext2)装入内存的机制。将一些经常被访问而又无需更改的文件通过Ramdisk放在内存中,可以明显地提高系统的性能。
NFS
NFS (Network FileSystem)是由Sun开发的一种在不同机器之间通过网络共享文件的技术。在嵌入式Linux系统的开发调试阶
段,可以利用该技术在主机上建立基于NFS的根文件系统,挂载到嵌入式设备,可以很方便地修改根文件系统的内容。