1 内存寻址
- 三种地址:逻辑地址:segment + offset;线性地址:32位无符号数;物理地址:用于芯片级内存寻址。
逻辑地址经过分段单元转为线性地址。段寄存器保存segment(16位)。其中cs有两位表示特权级,0级为内核态,3级为用户态。首先从段选择符的TI域找到段描述符存放的位置(GDT还是LDT),然后根据段选择符的索引部分找到段描述符,最后和offset结合得到线性地址。
线性地址经过分页单元转为物理地址。32位线性地址分为三部分:页目录(top10),页表(top10),偏移量(last12)。页表项的几个重要参数:present(是否在主存中),dirty(是否被修改过),acessed(是否被访问过),read/write(读写权限),U/S(权限) - 写回策略:只修改高速缓存内的页表,只有当信号到来时才讲cache中的页表写回到RAM中。TLB(快表)作为存放页表的高速缓存
- Linux的分页采用三级页表。每一个进程有自己的页全局目录和页表集合。Linux的内核代码保存在保留frame 1中(frame 0用于BIOS启动)。进程地址空间将后1GB留给内核态。
2 进程
- 进程由进程描述符表示进程状态。存储于进程地址空间的最低位。
- 为了有效的搜索进程,进程描述符以双向链表的形式储存。为了进程调度,进程描述符保存在一个队列中。为了方便通过pid查询进程,内核维护一个hash链表。为了在进程空闲的时候及时更新task数组,内核维护一个空闲进程列表。
- 进程亲属关系需要注意:如果没有父进程,那么祖先进程是1(init)。
- 进程切换:硬件上下文,硬件支持,代码,浮点寄存器(根据新进程是否使用来决定保存与否,设置TS标志位)。switch_to宏执行进程切换:挂起prev进程描述符,调用schedule将next载入CPU。
- 内核线程特点:每个kernel thread对应一个内核函数,只运行在内核态,只能使用大于PAGE_OFFSET的地址空间。0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程。其中1号内核进程调用执行init函数并演变成1号用户态进程(init进程)
3 中断和异常
-
同步中断(异常):指令运行结束后产生的中断;异步中断(中断):其他设备依照CPU时钟信号产生的中断。
中断分为可屏蔽中断(IO设备发起的)和不可屏蔽中断(硬件故障等)
异常分为处理器检测异常(异常地址保存在栈寄存器eip中)和编程异常(软中断)
0-31标识异常和不可屏蔽中断;32-47标识可屏蔽中断;其余为软中断(linux中128作为系统调用) -
中断描述符表(IDT)有256 X 8个字节。中断门和陷阱门区别在于会不会关闭可屏蔽中断
异常处理程序结构:- 保存要用的寄存器到堆栈中
- 调用C函数处理
- ret_from_exception退出高级语言。
内核使用设备不可用异常(进程切换)和缺页异常(内存管理)来提高效率
中断处理程序结构:- 保存中断请求(IRQ)的值和寄存器
- 给正在服务IRQ的中断控制器(PIC)发出应答
- 执行中断服务历程(ISR)
- ret_from_intr结束
-
对于优先级不高的中断,等到内核处理完系统调用,异常,中断和进程切换后空闲了才执行。下半部分由上半部分触发。
4 定时测量(timing measurement)
- 内核两种定时测量:持续计时器(time,ftime,gettimeofday) 和 定时器(settimer,alarm) 基于固定频率的振荡器和计数器实现的电路完成定时测量。
- 内核交互的时钟:
实时时钟(RTC):独立于CPU,独自供电,固定频率。
时间戳计数器(TSC):64位,汇编中通过rdtsc读取,每个时钟信号到来+1
可编程间隔定时器器(PIT):定时发送中断。Linux给PC的第一个PIT设置为每10ms产生一次中断,称为tick,tick决定系统运行节拍。 - 定时中断程序需要自己完成更新自系统启动以来所经过的时间,其他交给下半部分TIMER_BH。
PIT中断服务例程:如果有TSC寄存器,保存TSC寄存器的值和中断与中断例程之间的延迟。调用do_timer_interrupt(1. 调用do_timer 2. 每11分钟调整一次RTC)
do_timer 参数:jiffies 自系统启动以来的节拍数(32位无符号);lost_ticks 自xtime更新后的节拍数;lost_ticks_system 自xtime更新后再内核态的节拍数
TIMER_BH:- 更新时间和日期。更新xtime,自197001010时以来的秒数存放于xtime.tv_sec。
- 更新资源统计使用数。update_times关闭中断。 保存并清空 lost_ticks,保存并清空 lost_ticks_system,calc_load(ticks)更新CPU使用统计数,update_process_times(ticks,system)。进程描述符的counter域记录还能使用的节拍数,就是通过update_process_times更新。
- linux维护三种定时器(对定时器的检查总存在与下半部分,不适合时间要求严苛的应用):
- 静态定时器。内核,32个,到时间后执行fn域指向的函数。在timer_active中存储标志。存放于timer_struct中。
- 动态定时器。内核,存储于timer_list中,双向循环列表。为了插入和删除的效率,采用tvesc的结构,分为5个子tv,存储在下一个节拍会过期的所有动态定时器,每个子tv包含 2 8 2^8 28个节拍。
- 间隔定时器。用户态创建。
5 内存管理
- Linux采用4KB大小的page frame。page frame的描述符由一下域组成:
- count。有多少进程在使用这个页,如果空闲设为0。
- flags。32位的描述页框状态。
- 外碎片处理方法:Buddy系统算法
把所有空闲页框分为10个块链表。由连续的1,2,,,512个页框组成。
分配:从小到大分配,如果是由比需求大的块链表分配,则把空余的页框向下填充。(e.g. 128如果由块10分配,则把剩余的384分为256和128并入块9和块8)
实现Buddy算法的数据结构:
free_data[0 or 1][K]:大小为 2 K 2^K 2K的块的双向循环链表。
map:第K项位图的描述大小为 2 K 2^K 2K的状态。
清晰的图讲解可见buddy数据结构和位图 - 内碎片处理方法:slab分配器,是分配cache中内存的方法。
释放slab的条件:- slab中所有的对象都是空闲的。
- buddy系统不能满足新的请求。
slab着色:为了防止cache冲突
- 非连续内存区管理
避免外碎片,但是打乱了内核页表。因此linux很保守的在用。
6 进程的地址空间
- 与进程地址空间有关信息保存在mm_struct中。线性区保存在vm_area_struct中。线性区是地址空间的一部分。
当进程有大量线性区时,保存在AVL树中;反之,保存在链表中。
分配线性区:do_mmap。释放线性区:do_munmap。 - do_page_fault
- 堆管理。用于动态内存分配,由内存描述符的start_brk和brk域管理。
7 系统调用
linux调用系统调用前,必须执行int 128发出中断。
- 系统调用的参数传递一般是先传入到寄存器,在拷贝到内核态的栈上面。
对地址参数的验证:是否小于PAGE_OFFSET(即没有落入内核保留地址空间中) - 动态地址检查和修正。异常表记录了访问地址空间的指令的线性地址,以及对应的修正码。
- linux为内核函数提供了系统调用的封装。以参数数位末尾数字,从system0到5,不能调用超过5个参数的system call。
8 信号
名字前缀为SIG的宏标识信号。
- 信号的作用:
- 让进程知道发生某一特定事件
- 强制进程执行代码中的信号处理程序
- 进程对信号的应答:
- 显示的忽略
- 执行默认调用
- 执行信号处理函数
- 与信号有关的系统调用:
- kill(pid,sig)。
当pid=0,发送给调用进程的同组进程
当pid=-1,发送给非swapper(0),init(1),和current进程。
当pid<-1,发送给进程组-pid中的所有进程 - sigaction(sig,act,oact),改变信号的操作,act为新操作表,oact为可选参数,输出原来的操作表。
- sigpending() 检查挂起的阻塞信号
- sigprocmask() 修改阻塞信号集合
- sigsuspend() 挂起进程
- kill(pid,sig)。
9 进程调度
- 进程的两种优先级:
- 静态优先级。用户给实时进程设置的,调度算法不管。优先级高于动态优先级。
- 动态优先级。基本时间片+当前剩余时间片,只针对普通进程。
- do_fork函数会将父进程的时间片平分给父子进程。
- 调用schedule函数的两种形式:
- 直接调用。current进程需要立即阻塞。
- 松散调用。将current的need_resched设置为1,等待合适的时机调用。
- goodness函数表示进程的优先度。返回值为c。
c=-1000,永远不调度这个进程。
c=0,用完时间片的进程。
0<c<1000,用了部分时间片的普通进程。
c>=1000,实时进程。
10 内核同步
- 内核同步技术:
- 内核态的进程的非抢占性。这意味着:
- 占有内核态的进程不会被抢占。
- 可被中断,中断结束后需要返还控制权。
- 原子操作。以下是原子操作:
- 访问零次或一次内存的指令。
- 单处理器的从内存读,更新,写数据。
- 多处理器的前缀有lock的从内存读,更新,写数据。(锁住总线防止窃用)
- 关中断。较短的临界区代码可以通过关中断实现同步。
- 锁。 较长的临界区代码通过锁实现同步。
- 内核态的进程的非抢占性。这意味着:
- 多处理下的自旋锁:
当进程发现锁被其他进程占用时,进行“旋转”,执行一条tight指令。
优点在于多处理器下系统负载小,减少了上下文切换的开销。 - 处理器间中断(IPI)用来CPU之间通信,可以群发、发给自己、发给自己以外的其他CPU和发给特定CPU。
能实现的操作:- RESCHEDULE_VECTOR。强制某CPU调用schedule函数。
- INVALIDATE_TLB_VECTOR。重置TLB,内核修改某进程页表时用。
- STOP_CPU_VECTOR。强制停止自己以外的CPU,内核检测到无法解决的错误时使用。
- LOCAL_TIMER_VECTOR。定时中断发给所有CPU。
- CALL_FUNCTION_VECTOR。强制自己以外的CPU执行函数。
11 虚拟文件系统
- VFS是用户程序与实际操作系统之间的一层抽象。
- VFS通过通用文件模型来实现抽象。对于函数调用,则是通过内核调用文件数据结构中f_op域中对应的函数指针来实现。
举例:file → \rightarrow →f_op → \rightarrow →read(…)
通用文件模型包含:- superblock。存放已安装文件系统的信息。
- inode。每个inode对象有个inode号, 唯一标识指定文件。
inode有两种,一种是VFS的inode,一种是具体文件系统的inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的inode调进填充内存中的inode,这样才是算使用了磁盘文件inode。 - file。存放文件与进程交互的信息,仅当进程访问文件时存在与内核中。
- dentry。存放目录项文件链接的信息。
- 软链接:有一个目录项,指向独立INODE,INODE不存文件数据,存指向真实文件的路径,可以在不同分区
硬链接:有一个目录项,没有独立INODE,指向原始INODE,并且原始INODE的引用计数加1 ,不能在不同分区。
12 管理IO设备
- CPU和IO设备之间有三个层次的硬件结构:
- IO端口
IO端口可以被映射为实际物理地址,方便用对内存操作的指令操作。 - IO接口
将IP端口中的值翻译成对应的命令和数据。 - 设备管理器
复杂设备需要设备管理器将来自IO接口命令解释。比如磁盘控制器接受到写命令,解释为具体磁道调整命令。
- IO端口
- 监控IO操作的两种方法:轮询和中断。
- 通过引用计数器来决定是否为设备文件被多少进程访问。值为0是调用open,则需要分配IRQ。调用release后值为0,释放IRQ。