《Chapter 1 & 2》
1 机制与策略分离:
机制(mechanism)和策略(policy):
a. 机制是指需要提供什么功能,策略是指如何使用这些功能。
b. 机制是实现具体功能,策略是组合已有功能。
c. 机制是战术,策略是战略。
策略与机制是动态与静态的关系。机制意味着固定和自动,像是一个一个的积木块。策略是组合方法,怎样使用已有的积木搭出一个作品。
策略往往是在机制的基础上进行具体问题的调整。
2 单内核与微内核:
Linux是单内核,但吸取了微内核的诸多优点:模块化设计,内核可抢占,支持内核线程,动态装在内核模块。至今,Linux是模块化,多线程,内核本身可调度的OS,实用主义再次占了上风。
3 Git:Linus写的分布式的版本控制工具。
4 内核源码树
目录 | 描述 |
arch | 特定体系结构的源码 |
block | 块IO设备层 |
crypto | 加密API |
Documentation | 内核源码文档 |
drivers | 设备驱动程序 |
firmware | 使用某些驱动程序而需要的设备固件 |
fs | VFS和各种文件系统 |
include | 内核头文件 |
init | 内核引导和初始化 |
ipc | 进程间通信代码 |
kernel | 核心子系统(像调度程序这样的) |
lib | 通用内核函数 |
mm | 内存管理子系统和VM |
net | 网络子系统 |
samples | 示例,示范代码 |
scripts | 编译内核所用的版本 |
security | Linux安全模块 |
sound | 语音子系统 |
usr | 早期用户空间代码(所谓的initramfs) |
tools | 在Linux开发中有用的工具 |
virt | 虚拟化基础结构 |
5 编译内核
yes/no/module – module指编译成模块。
编译命令:make (没错,就是一个make)。
以前用过Ubuntu下的Linux2.4~2.6有其他的编译安装命令,也很简单。
6 内核只给每个内核进程一个很小的定长的堆栈,所以一定要节省内存。
7 像所有自视清高的Unix内核一样,Linux内核使用C语言写的。
8 C语言中的static:
static修饰的全局变量放在静态变量区,只能被当前.c文件访问。
static修饰的局部变量也会被放在静态变量区,但仍然只能由该局部函数访问。
static修饰的函数只能在当前.c文件中被访问。
9 内联汇编:
asm volatile(“rdtsc”:”=a(low)”,”=d(high)”);
《Chapter 3 进程管理》
1 线程是CPU调度的基本单位,进程是操作系统分配资源的基本单位。例如:同一个进程的线程共享一个虚拟内存,但是有各自的虚拟CPU(分时)。
2 PCB: task_struct 32位机器上1.7K大小,包括打开的文件,地址空间,挂起的信号,进程状态等。
slab机制存放 task_struct –-- 预分配和重复使用。
3 进程状态:TASK_RUNNING / TASK_INTERRUPTABLE / TASK_UNINTERRUPTIBLE / TASK_TRACED / TASK_STOPED
set_task_state(task,state);
4 进程调用系统调用方式进入内核,内核“代表进程执行”并处于进程上下文中;VS
中端上下文:中断上下文中,进程不代表进程执行,而是执行一个终端处理程序,此时不存在进程的上下文。
5 fork()写时复制(copy-on-write COW)
fork()通过clone()这个系统调用实现,clone需要指定父子进程需要共享哪些资源。
clone()调用do_fork(),do_fork()调用copy_process()。copy_process()复制一个task_struct,分配pid,根据参数指定的共享哪些资源,进行复制。
6 内核线程(kernel thread):内核线程与普通的进程的区别在于内核线程没有独立的地址空间(指向地址空间的mm指针被设置为NULL)。内核线程可以像普通进程一样被调度被抢占。内核线程也是由其他线程(kthreadd)创建的。
struct trask_struct *kthread_create(int *(threadfn)(void *data), void *data,const char namefmt[],…);
新创建的内核线程需要被唤醒(wake_up_process)。
7 进程终结:
do_exit() ->
del_timer_sync();
exit_mm();
sem_exit();
exit_file();
exit_fs();
exit_notify();//给进程找养父
schedule();//切换到新进程
do_exit()永不返回。此时进程不能再运行,处于EXIT_ZOMBIE退出状态,但仍占用一部分内存(内核栈,thread_info,task_struct),来向父进程提供信息。父进程检测到信息(或者通知内核这些信息不需要了),这部分内存就被释放了。
如果孤儿进程没有养父,孤儿进程内存将会被浪费。
《Chapter 4 进程调度》
1 Linux调度的发展:
Linux2.4及其之前都只有一个很简陋的调度算法。Linux2.5版本开发了一个O(1)的调度算法(名字就被称为O(1)调度程序),涉及到静态时间片算法和针对每处理器队列。该调度程序扩展性良好(几十个核)。O(1)调度程序存在一个问题,对于那些响应时间敏感的程序有一些先天不足,这使得O(1)调度程序在服务器上运行良好,但在桌面上表现不佳。Linux2.6.23中,O(1)调度被换成了CFS(完全公平调度)。
2 调度策略通常需要在两个矛盾的目标之间寻找平衡:进程响应迅速(响应时间),最大系统利用率(高吞吐量)。Linux常常倾向于更多调度IO消耗型进程。
IO密集型期望较短的时间片(多切换几次,迅速响应,每次响应不需要很长的时间片);
计算密集型期望较长的时间片(少切换几次,连续计算一阵子,时间片长些比较好);
3 两种优先级:nice值 和 实时优先级。
4 公平调度CFS:
4.1 Unix系统调度存在的问题:
a. 若将nice值映射到时间片,则必需将nice单位值对应到处理器的绝对时间。例如,nice=0,19两个进程,对应时间片是100ms,5ms,则105ms内切换一次。nice=0,0对应时间片是100ms,100ms,则100ms切换一次,各50%。
nice=19,19对应时间片是5ms,5ms,则10ms切换一次,各50%。
高nice的进程常常是后台进程且多为计算密集型,低nice的进程常常是前台程序且多为IO密集型。于是跟初衷相背。
b. nice对应到时间片的问题:0-100ms,1-95ms, … 18-10ms, 19-5ms。同样是差1,nice=0,1与nice=18,19时间差别的比例很大。
c. nice映射到时间片,还会造成 依赖定时器节拍长度 的问题。
d. 调度器为了优化交互任务而唤醒进程的问题。这种系统中,为了使进程更快地投入运行,会对新唤醒的进程提升优先级(即使它们的时间片)。这给了某些睡眠/唤醒用例一个机会,使得它们可以利用这一点获得更多的处理器时间。
4.2 CFS的动机和逻辑:
CFS的出发点:进程调度的效果应该如同系统具备一个理想中的完美多任务处理器。
标准unix的调度模型中,进程P1先运行5ms,P2再运行5ms。但它们任何一个运行时都占用100%的时间片。而在理想情况下,完美的多任务处理器模型应该是这样的:我们能在10ms内同时运行两个进程,它们各自使用处理器一半的能力。
CFS允许每个进程运行一段时间,循环轮转,选择运行时间最少的进程作为下一个运行进程,而不失依靠nice值来计算时间片了。nice在CFS中被用作进程运行时间的比重。
4.3 CFS的实现:
kernel/sched_fair.c
四个组成部分:时间记账,进程选择,调度器入口,睡眠和唤醒。
4.3.1 时间记账:
标准Unix调度模型是每个进程一个时间片,每次时钟中断将时间片减少,减到0时进程被切换。CFS中没有时间片的概念(但仍需要处理时间记账)。
进程调度实体: struct sched_entry (可由task_struct->se获得)。
vruntime是se的一个字段(u64),用来记录一个进程已经运行的时间和还应该运行多长时间。该运行时间(花在运行上的时间和)的计算是经过了所有可运行进程总数的标准化(或者说是加权化)。虚拟时间以ns为单位,与节拍无关。注意:如果存在一个完美的处理器,则进程的vruntime应该是一致的。
记账功能由 update_curr(struct cfs_rq *cfs_rq)函数实现(该函数在定时器中断时调用)。
4.3.2 进程选择:
如前所述,如果存在一个完美的处理器,则任意时刻各个进程的vruntime应该是一致的。在现实处理器中,只能是一个一个地运行,所以vruntime是有差别的。因此,CFS在选择下一个进程的时候,会挑选一个具有最小vruntime的进程。这是CFS调度算法的核心:选择具有最小vruntime的进程。
具体实现:数据结构-红黑树。CFS使用红黑树来组织可运行进程队列,并使用这种数据结构迅速选择具有最小vruntime的进程。其上的操作有1.挑选下一个进程;2.向RBTree中加入进程(进程从阻塞变为可运行,或者刚刚fork创建);3.从树中删除进程(进程从可运行变为阻塞,或者进程执行结束)。
4.3.3 调度器入口:
调度器类;pick_next_task
4.3.4 睡眠和唤醒。
4.4 抢占和上下文切换:
每当一个新的进程被调度进来,schedule就会调用content_switch()函数来进行上下文切换。
content_switch()主要完成两项基本工作:1.调用switch_mm()将虚拟内存从上一个进程切换到新进程;2.调用switch_to(),将上一个处理器状态切换到新进程的处理器状态(保存/恢复 栈信息,寄存器信息等)。
什么时候调用schedule():内核设置了一个标记need_resched;某进程应被抢占时,更高优先级进程进入可运行状态时都会设置need_resched;在返回用户空间及从中断返回时会检查该标记;need_resched曾是一个全局变量,后来因为current一般都在cache中,所以每个进程中都加了这个标志位。
用户抢占:内核即将返回用户空间时(中断处理程序/系统调用之后返回用户空间),如果need_sched被设置,则调用schedule()于是发生用户抢占。
内核抢占:Linux完整支持内核抢占;如果重新调度是安全的(安全=没有持有锁),内核抢占就可能发生。调度前除了检查need_resched外,还要检查preempt_count==0(使用锁加1,释放锁减1)。如果内核被阻塞或者显式调用schedule()时,内核抢占也会发生。总之,内核抢占发生的时机:1.中断处理程序正在执行,且返回内核空间之前;2.内核代码再一次具有可抢占性的时候;3.内核任务显式调用schedule()的时候。4.内核任务阻塞。
4.5 实时调度策略:
SCHED_FIFO,SCHED_RR:实时调度;静态优先级。
SCHED_NORMAL:普通的非实时调度。
处理器绑定(Process Affinity):使进程在同一个处理器上执行。task_struct中的cpu_allowed标记(每个位对应一个CPU),有get/set*系统调用获取和设置这些标记位。
《Chapter 5 系统调用》
5.1 与内核通信
5.2 API,POSIX,C库
5.3 系统调用
5.4 系统调用处理程序
5.5 系统调用的实现
capable()函数用来检查是否有权对特定资源进行操作。例如:
capable(CAP_SYS_BOOT)
5.6 系统调用上下文
内核在执行系统调用时处于进程上下文,current指针指向当前任务。
《Chapter 6 内核数据结构》
使用Linux内核中已有的数据结构,不要重复制造轮子。
6.1链表
6.2队列
6.3映射
6.4二叉树(红黑树)
《Chapter 7 中断和中断处理》
一个设备中断处理代码是设备的驱动程序的一部分。
又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这是相互抵触的两个目标;所以将中断处理程序分为两个部分:上半部(top half,做严格限时的工作)和下半部(bottom half,可以稍后执行的工作)。
注册中断处理程序:
int request_irq( unsigned int irq, /* 中断号 */
irq_handler_t handler, /* 函数指针 */
unsigned long flag, /* 标记 */
const char *name, /* 设备名称,如keyboard*/
void *dev); /*用于共享中断号的设备进行区分*/
typedef irqreturn_t (*irq_handler_t)(int,void*);
void free_irq(unsigned int irq, void* dev);
static irqreturn_t intr_handler(int irq, void* dev);
中断上下文:
进程上下文是一种内核所处的操作模式,此事内核代表进程执行。
中断上下文与进程没有瓜葛,因为没有后备进程,所以中断上下文不可以睡眠(否则怎么唤醒),所以中断上下文不能调用某些可能导致睡眠的函数。
内核栈大小一般是两页(8K)。
中断从硬件到内核的路由图:
中断控制接口:
local_irq_disable();
lcoal_irq_enable();
local_irq_save(flags);
local_irq_restore(flags);
disable_irq(unsigned int irq);
disable_irq_nosync(unsigned int irq);
enable_irq(unsigned int irq);
synchronize_irq(unsigned int irq); //等待一个中断处理程序的退出
in_interrupt(); //返回正在执行的中断类型
in_irq();
《Chapter 7 下半部和退后执行的工作》
Linux2.6内核提供了三种不同形式的下半部实现机制:软中断,tasklet,工作队列。
软中断:编译期间静态分配的。由结构体softirq_action表示。
struct softirq_action {
//函数指针(软中断处理程序)
void (*action)(struct softirq_action *);
}
static struct softirq_action softirq_vec[NR_SOFTIRQS];
软中断处理程序
调用: my_softirq->action(my_softirq);
注:action参数为整个结构体,是为了将来在结构体中增加新的字段时方便。
软中断执行时机:
1. 从一个硬件中断代码返回时;
2. 在ksoftieqd内核线程中;
3. 在那些显式检查和执行待处理的软中断代码中(例如 网络子系统)
软中断留给系统中最严格以及最重要的下半部使用。目前只有网络子系统和SCSI直接使用软中断;此外,内核定时器和tasklet都是建立在软中断之上的。
tasklet:
比软中断接口简单,锁保持要求也低。基于两种softirq(仅仅优先级不同)。
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
}
工作队列(work queue):
工作队列是由一个内核线程来执行的下半部机制。它总是运行在一个进程上下文中,因此可以睡眠(是否需要睡眠是用来决定选择tasklet还是工作队列的基本方法)。
比较:
下半部机制 | 上下文 | 顺序执行保障 |
软中断 | 中断 | 无 |
tasklet | 中断 | 同类型不能执行 |
工作队列 | 进程 | 无(和进程一样被调度) |
</END HERE>