LKD3文章目录
一.Linux内核简介
- 应用程序完成工作的基本方式:应用程序通过系统调用界面陷入内核
- 当硬件设备想和系统通信时,先发一个异步中断信号(有终端号)去打断处理器的执行,继而打断内核的执行;内核通过中断号查找相应的中断服务程序,并调用处理中断;
- 微内核:模块化,大部分服务在用户空间,内核通信:IPC通信,典型:windows
- 单内核:模块化设计,抢占式内核 运行在内核空间 典型:linux
- 内核版本号例如:2.2.6 中第一个逗号后的数字为偶数表示稳定版本,奇数表示开发版本
二.从内核出发
- linux kernel : https://www.kernel.org/
- 下载:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
- git pull
- 添加补丁:patch -pl < …/patch-x.y.z
内核源码树:
-
配置内核方法:
make config
make menuconfig
make gconfig -
make defconfig: 基于默认配置创建一个配置
克隆内核配置方法:
- 老版配置选项:CONFIG_IKCONFIG_PROC ==>把完整的压缩过的内核配置文件放在/proc/config.gz
- zcat /proc/config.gz > .config
- make oldconfig
- make
相对于应用层而言,内核开发特点:
- 内核编程时即不能访问C库也不能访问标准C头文件
- 必须使用GNU C
- 缺乏像用户空间那样的内存保护机制(内核中内存不分页,直接使用物理内存)
- 难以执行浮点运算
- 内核给每个进程只有一个很小的定长堆栈
- 内核支持异步中断,抢占和SMP,因此必须时刻注意同步和并发(自旋锁和信号量)
- 考虑可移植性的重要性
内核中C的扩展部分:
- 内联函数:以时间换空间:
static inline void wolf(unsigned long tail_size)
一般在头文件中定义(或者源文件开始的地方) - 内联汇编:
asm()指令嵌入汇编指令:
下边这条内联汇编用于执行x86处理器的rdtsc指令,返回时间戳(tsc)寄存器的值:
unsigned int loc,high;
asm volatile(“rdtsc” : “=a” (low), “=d” (high));
/low 和high分别包含64位时间戳的低32位和高32位/
-Q&A:
宏likely()与unlikely() 具体做了什么?
三.进程管理
- 每个线程都有一个独立的程序计数器,进程栈和一组进程寄存器。
- 内核调度的对象是线程,不是进程
- fork()系统调用从内核返回两次:一次回到父进程;另一次回到新产生的子进程;
- 内核把进程的列表存放在任务队列的双向循环的链表中,链表中每一项都是类型为task_struct进程描述符的结构,包含一个具体进程的所有信息
- linux的fork()使用写时拷贝,只有需要写入时,数据才会被复制,在此之前是只读方式共享:fork的实际开销是复制父进程的页表以及给子进程创建唯一的进程描述符
- 线程:::进程间共享资源的手段
- 创建线程与创建进程区别在于系统调用clone()时候传递的参数标志指明需要共享的资源
四.进程调度
- 内核的一个子系统
多任务系统可以分为两类:非抢占式多任务和抢占式多任务,linux提供的是抢占式多任务;有调度程序来决定什么时候停止一个进程的运行,以便其他进程得到运行的计划,这个强制挂起动作叫抢占;
非抢占式模式:除非进程自己退出,否则会一直执行下去,进程主动挂起自己的操作叫让步 - 进程在被抢占前能够运行的时间是预先设置好的,叫进程的时间片;
- 2.6版本用“完全公平调度算法”CFS取代O(l)
- 进程被分为 I/O消耗型(不需要长的时间片)和处理器消耗型(需要长时间片)
- 用两种不同优先级方式:一种是nice值 范围-20~+19,默认为0,越大优先级越低,可以通过ps -el查看N1列;
第二种 实时优先级,值可以配置,默认范围0~99,与nice意思相反,任何实时进程优先级都高于普通进程,ps -eo state,uid,pid,ppid,rtprio,time,comm查看RTPRIO列 -的表示不是实时进程 - CFS电镀算法实现:kernel/sched_fair.c:
-
时间记账 :当一个进程的时间片减少到0时,会被其他不为零的进程抢占
-
进程选择 :红黑树返回值即使下次调度的进程,如返回NULL,调度idle任务(__pick_next_entity()) 当fork函数执行时,进程被插入到红黑树中enqueue_entity() 进程堵塞或终止时,要在红黑树中删除进程 dequeue_entity()
-
调度器入口:schedule()
-
睡眠和唤醒
用户抢占产生:
从系统返回用户空间时
从中断处理程序返回用户空间时
五.系统调用
系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要作用:
1.为用户空间提供了一种硬件的抽象接口;
2.保证了系统的稳定和安全;
3.保护内核
linux中每个系统调用都关联一个系统调用号
x86-64 arch/i386/kernel/syscall_64.c中的sys_call_table中记录系统调用表中所有注册过的调用列表
- 应用程序通过软中断方式告诉内核自己需要系统调用(通过引发一个异常促使系统切换到内核态去执行异常程序)例如:x86系统定义的软中断号128,通过int $0x80指令(通过eax寄存器传递给内核)触发该中断然后系统切换到内核态执行第128号异常处理程序(系统调用处理程序)
- 系统调用步骤:
- 在系统调用表最后添加一项:
-
.long sys_foo /*338*/
- 添加系统调用号 <asm/unistd.h>:
-
#defing __NR_foo 338
- 编译进内核镜像(不能是模块形式:
-
放进kernel/下的一个相关文件即可,比如sys.c包含了各种系统调用:
-
XXX..c中
#include <adm/page.h>
/*返回每个进程的内核栈大小*/
admlinkage long sys_foo(void)
{
return THREAD_SIZE;
}
至此可以启动内核并在用户空间调用foo()系统调用了;
六.内核数据结构
通用数据结构:
链表
单向链表:
struct list_element{
void *data;
struct list_element *next;
};
双向链表
struct list_element{
void *data;
struct list_element *prev;
struct list_element *next;
};
环形链表
linux内核的标准链表是采用环形双向链表
内核提供了一系列函数操作链表<linux/list.h>
队列
生产者消费者模式<linux/kfifo.h>
映射?
关联数组,由唯一键组成的集合,每个建关联一个特定值,建到值的关联关系称为映射
二叉树
二叉搜索树BST
是一个节点有序的二叉树,顺序通常遵循:
1.根的左分支节点值都小于根节点值
2.根的右分支节点值都大于根节点值
3. 所有的子树也都是二叉树
红黑树(自平衡二叉树)六个属性?
1.所有节点要么着红色,要么黑色;
2.叶子节点都是黑色;
3.叶子节点不包含数据;
4.所有非叶子节点都有两个子节点;
5.如果一个节点是红色,则它的子节点都是黑色;
6.在一个节点到其叶子节点的路径中,如果总是包含同样数目的黑色节点,则该路径比其他路径是最短的;
数据结构如何选择
1.如果数据主要操作是遍历数据=链表;
2.如果代码符合生产者消费者模式=队列;
3.如果要映射一个UID到一个对象=映射;
4.需存储大量数据,并检索迅速=红黑树
七.中断和中断处理
中断服务程序=ISR(驱动程序中)
中断处理程序于普通函数区别:
1.需要按特定的类型声明
2.被内核调用来响应中断的,运行于我们称之为中断上下文(不可阻塞)的特殊上下文中
- 中断上半部:接收到一个中断,就立即开始执行,有严格时间限制的工作:例如应答接受的中断或复位硬件;
- 中断下半部:可以执行不着急的工作
放在中断上半部执行的任务:任务对时间敏感
任务和硬件相关
任务不能被其他中断打断
除此外 任务放到中断下半部
八.下半部和推后执行的工作
执行中断处理程序里边本身不执行的工作
内核提供三种下半部实现机制:(延迟内核工作的机制)
软中断
tasklet
工作队列
软中断
每个被注册时软中断就是该数组中的一项,最多32个
软中断都在do_softirq()执行,遍历每个处理程序,后执行
添加新的软中断方法
使用场景:在那些执行频率很高和连续性要求很高的情况下才需要使用
tasklet
通过软中断执行的
工作队列
将工作推后执行,交内核线程执行(进程上下文执行)允许重新调度或睡眠
九.内核同步介绍
锁(原子操作)防止因并发使临界资源发生未按编程者意愿的现象的机制
十.内核同步方法
十一.定时器和时间管理
jiffies记录子系统启动以来产生的街拍总数,初始0,后每次终端中断增加 jiffies=秒*HZ;
实时时钟(RTC)
持久存放系统时间,在xtime变量,系统启动时初始化xtime
十二.内存管理
内核管理内存的基本单位:物理页
用结构体表示每个页:
kmalloc()函数分配的内存物理空间连续–kfree()
vmalloc()虚拟地址连续,物理地址不连续
malloc()虚拟地址连续,物理地址不定
slab层–提高性能
为了缓存频繁申请内存释放内存的对象,对象使用时候在空链表存储对象,使用完释放对象结构又放回空闲链表,所以不会导致内存碎片
十三.虚拟文件系统
为系统与不同介质的通信或访问提供了便利
十四.块I/O层
字符设备:系统按字节流访问,比如键盘输入,输入英文单词;
块设备:随机访问大小块的物理设备
块设备
块设备的最小可寻址单元是扇区,大部分一个扇区大小是512字节;
一个块可以分很多扇区,但不能超过一页(内存中的页大小);
块到页的映射:buffer_head
内核中bio结构体表示块io的一些列操作;
io调度程序:合并与排列io请求–为了降低寻址时间;
2.6内核内嵌4种io调度程序,default是完全公平的io调度程序,启动时可以通过elecator=foo 覆盖default:
十五.进程地址空间
内核管理虚拟内存空间
进程空间可以分为:
1. 可执行文件代码的内存映像–代码段;
2. 可执行文件的已经初始化全局变量的内存映射–数据段
3. 未初始化的全局变量–bss段的零页(页面都为0);
4. 进程用户空间栈;
5. 堆
6. c库
内核用内存描述符表示进程的地址空间属性:mm_struct
进程与线程的本质区别:是否共享地址空间;
查看进程地址空间中的全部内存区域:
cat /proc/1426/maps
pmap 1426
应用程序访问虚拟内存–虚拟地址转化成物理地址–处理器解析访问物理地址:
地址转化需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,页表项指向物理地址
linux使用三级页表完成地址转换:基本硬件完成
顶级页表-页全局目录(PGD)包含pgd_t类型数组–>指向二级页目录的表项PMD(pmd_t)数组–>指向PTE中的表项–>页表pte_t类型的页表项–>指向物理页面:
十六.页高速缓存和页回写
页高速缓存是将磁盘的数据缓存到物理内存,对物理内存进行操作
写缓存三种方式:
1.不缓存:当对缓存中的数据进行写时,跳过缓存,直写操作磁盘;
2.写操作更新到缓存和磁盘;
3.回写:执行写操作写道缓存中后,将页缓存中写入的页面标记并加入相应链表,然后由回写进程周期的将被标记的页表写回到磁盘
脏页写回磁盘情况:flusher线程
1.当空闲内存低于特定阈值时,内核将脏页写回磁盘以释放内存;
2.脏页在内存时间超过阈值时;
3.用户进程调用sync()和fsync()系统调用时
内核会调用flusher_threads()唤醒flusher线程–调用bdi_writeback_all()写脏页数据到磁盘
十七.设备与模块
设备类型
sysfs:表示系统中设备树的一个文件系统;
linux系统中的设备类型:
字符设备;cdev,不可寻址,数据的流式访问,通过 字符设备节点的文件访问,用户空间通过直接访问设备节点与字符设备交互
块设备;blkdev,克寻址,支持重定位操作(对数据的随机访问),通过块设备节点的特殊文件访问,通常被挂在为文件系统;
网络设备:网卡+协议,通过套接字API接口访问;
杂项设备:miscdev–实际是简化的字符设备–对通用基本架构的一种折中;
伪设备:虚拟的仅提供访问内核功能的:比如:内核随机数发生器(dev/random和/dev/urandom访问),空设备(/dev/null访问),零设备(/dev/zero访问),满设备(/dev/full访问),内存设备