第一章 linux内核简介
1.1 追寻linux的足迹:linux简介
*起源于unix/minix,C,Internet + Linux Torvalds等开发者的努力
*遵循GPL的非商业化发行
*主要包括内核,C库,编译器,工具集,系统工具等
1.2 操作系统和内核简介
*内核需要实现中断处理,调度,内存管理,网络,IPC等功能
*内核空间/用户空间
*应用程序和内核通信:应用程序->C库->系统调用->内核
+-----------+ +-----------+ +-----------+
+ 应用程序1 + + 应用程序2 + + 应用程序3 +
+-----------+ +-----------+ +-----------+
| | |
V V V
+---------------------------------------+
+ 系统调用接口 +
+---------------------------------------+
| | |
V V V
+-----------------------+---------------+
+ 内核子系统 + +
+-----------------------+ +
+ 设备驱动程序 +
+---------------------------------------+
| | |
V V V
+---------------------------------------+
+ 硬件设备 +
+---------------------------------------+
*上下文
-运行于内核空间,处于进程上下文,代表某个进程执行
-运行于内核空间,处于中断上下文,处理某个特定中断
-运行于用户空间,执行用户进程
1.3 linux内核与传动unix内核比较
*单内核(宏内核)/微内核
*linux是单内核+模块/抢占/内核线程(PS:操作系统设计中的哲学思想,折衷)
*linux特点:
-动态加载模块
-支持SMP
-支持抢占
-线程的实现方法
-面向对象的设备模型
-自由,摒弃unix中不好的东西,会不断吸取好的东西
1.4 内核版本
*版本命名规则
aa.bb.cc
| | |
主版本<----+ V +---->修订版本
从版本(奇数为开发版,偶数为稳定版)
1.5 社区
*LKML:http://lkml.org/
*linux kernel newbies:http://kernelnewbies.org/
*linux kernel source:htt://www.kernel.org/
1.6 小节
*enjoy it
第二章 从内核出发
2.1 获取源代码
2.1.1 安装内核源代码
$wget -c http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.24.tar.bz2
$tar xjf linux-2.6.24.tar.bz2
$cd linux-2.6.24
$make defconfig
$make all
$su -c "make modules_install install"
摘自:http://kernelnewbies.org/KernelBuild
原代码有两种压缩方式gzip和bzip2,bzip2压缩效果更好
2.1.2 使用补丁
For example's sake, you have unpacked a tarball of Linux 2.4.0, and you want to apply Linus' patch-2.4.1.bz2, which you have
placed in /usr/src
$bzip2 -dc /usr/src/patch-2.4.1.bz2 | patch -p1 --dry-run
摘自:http://kernelnewbies.org/FAQ/HowToApplyAPatch
2.2 内核源代码树
目录/文件 | 描述
+-----------------------------------------------+
+ arch | 体系结构相关代码 +
+ crypto | crypto API +
+ Doumentation | 文档 +
+ drivers | 设备驱动程序 +
+ fs | VFS和各种文件系统 +
+ include | 内核头文件 +
+ init | 内核引导和初始化 +
+ ipc | 进程间通信 +
+ kernel | 核心子系统 +
+ lib | 通用内核库函数 +
+ mm | 内存公里子系统和VM +
+ net | 网络子系统 +
+ scripts | 编译内用脚本 +
+ security | linux安全模块 +
+ sound | 语音子系统 +
+ usr | 早期用户空间代码(ramfs) +
+ COPYING | 内核许可证(GPL) +
+ CREDITS | 开发者列表 +
+ MAINTAINERS | 维护者列表 +
+ Makefile | 内核Makefile文件 +
+-----------------------------------------------+
2.3 编译内核
*配置内核:make config,make menuconfig,make xconfig,make gconfig
yes/no/module
请参考:./readme
http://kernelnewbies.org/KernelBuild
减少输出垃圾信息:make > /dev/null
衍生多个作业: make -j2,make -j4,...
2.4 内核开发的特点
*没有C库
*使用GNU C
*没有用户空间的内存保护
*不实用浮点数
*内核栈空间有限
*内核支持中断,抢占,SMP,因此需要注意同步和并发
*需要考虑可移植性
第三章 进程管理
进程/线程的概念
虚拟内存(使进程觉得自己独占系统内存)/虚拟处理器(使进程觉得自己独享处理器)
3.1 进程描述符及任务结构
*任务队列:task_struct的双向循环链表
*task_struct位于include/linux/sched.h
3.1.1 分配进程描述符
*通过slab分配器分配,达到着色缓存(cache coloring)的目的
*寻址进程的task_struct
+---------------+
+ 内 +
+ +
+ 核 +
+ +
+ 栈 +
+---------------+
+ thread_info +
+---------------+ -->task_struct
内核栈的底端为thread_info,thread_info的第一个成员
指向进程的task_sturct
3.1.2 进程描述符的存放
*PID:进程标识符,管理员可以修改/proc/sys/kernel/pid_max来改变
系统中允许的进程数目
*current宏
请参考:http://kernelnewbies.org/FAQ/get_current
3.1.3 进程状态
*TASK_RUNNING
*TASK_INTERRUPTIBLE
*TASK_UNINTERRUPTIBLE
*TASK_ZOMBIE
*TASK_STOPPED
*状态迁移图
+---------------+
fork创建任务 + TASK_ZOMBIE +
| +---------------+
| A
| +-----------------------+ 任务 |
|任务创建 | context_switch | 退出 |
| | V |
| +---------------+ +---------------+ |
+--> + TASK_RUNNING + + TASK_RUNNING + ------+
+--> + 未占有CPU + + 占有CPU + ------+
| +---------------+ +---------------+ |
| A | |
| | 任务被抢占 | 等待 |
|事件 +-----------------------+ 事件 |
|发生 |
|唤醒 |
| +-----------------------+ |
+----------- + TASK_INTERRUPTIBLE + <-------------+
+ TASK_UNINTERRUPTIBLE +
+-----------------------+
3.1.4 设置当前进程状态
*set_task_state(task, state)
*set_current_state(state)
3.1.5 进程上下文
*内核代表进程执行
*用户空间执行->系统调用/异常->陷入内核,此时处于进程上下文
3.1.6 进程家族树
*task_struct的parent,sibling,children域
*task_sturct的tasks域
*init进程
*内核通用数据结构list,include/linux/list.h
3.2 创建进程
*fork() exec()
3.2.1 写时拷贝
*copy-on-write,只有在写入的时候,数据才被复制。
3.2.2 fork()
*fork()
->sys_fork
->do_fork()
->...
3.3.3 vfork()
*vfork()
->sys_vfork()
->do_fork()
->...
*不推荐用vfork(),而使用fork()
3.3 线程在linux中的实现
*linux中线程是共享资源的进程
*clone()共享资源的标志,include/linux/sched.h
*内核线程,运行在内核空间,没有独立的地址空间,完成特殊的任务
如,ksoftirqd,kswapd,pdflush
3.4 进程终结
*exit()
->sys_exit
->do_exit()
->...
*释放进程拥有的资源,但是此时没有释放task_struct
3.4.1 删除进程描述符
*wait()
->wait4()
*父进程执行wait4(),等待子进程退出,然后子进程的task_struct才被释放
3.4.2 孤儿进程造成的进退维谷
*如果父进程在子进程之前退出,需要为子进程寻找新的父进程
do_exit()
->notify_parent()
->forget_origial_parent()
3.5 进程小结
*操作系统最重要的抽象:进程
第四章 进程调度
*调度程序,选择哪个进程占有CPU,原则就是最大限度的利用CPU
*抢占式多任务/非抢占是式多任务
*linux采用了O(1)的调度算法
4.1 策略
*调度策略是在什么时候让哪个进程运行
4.1.1 I/O消耗型和CPU消耗型进程
*CPU消耗型,降低优先级,否则会影响系统对用户的响应
*I/O消耗型,提高优先级,尽量满足用户的快速响应
*而一个进程可能同时具有两种特性,有时候需要消耗大量的CPU时钟,
而有时候又要等待用户介入进行一些操作,调度策略就是在这种矛盾中
寻找平衡,这里有体现了哲学中的矛盾思想
4.1.2 进程优先级
*linux采用动态优先级调度,所谓动态,是指调度程序从等待I/O时间
和占用处理器的时间推测,从而动态增减优先级(nice值)。
*linux提供两组优先级,实时优先级和nice值。第一组,nice值,-20~+19。
实时优先级,0~99。
4.1.3 时间片
*默认时间片的规定,太大,则系统交互性差,太小,明显会增加进程
切换时处理器的消耗。这里有体现了哲学中的矛盾思想。
*linux默认时间片为100ms。
更低优先级 更高优先级
|<----------------------|---------------------->|
| 更低交互性 | 更高交互性 |
V V V
最小 默认 最大
5ms 100ms 800ms
4.1.4 进程抢占
*当进程变为TASK_RUNNING的时候,内核检查优先级是否高于当前进程
如果是则当前进程被抢占,进程时间片为0的时候,也会唤醒调度程序
抢占当前进程
4.1.5 调度策略的活动
*为交互性强的进程分配更高的优先级,高优先级可以抢占低优先级,提高
系统整体性能
4.2 linux调度算法
*kernel/sched.c
*调度算法实现了目标
-O(1)调度
-SMP扩展性,每个处理器有自己的锁和运行队列
-SMP亲和力
-加强交互性
-保证公平
-可优化到多处理上,每个处理器有多个进程在运行(FIXME:不太理解这是什么意思)
4.2.1 可执行队列
*运行队列runqueuq,可执行进程的链表,每个处理器一个
*cpu_rq宏,this_rq宏
*运行队列锁,操作运行队列时候需要所锁定
*task_rq_lock(),task_rq_unlock(),this_rq_lock(),this_rq_unlock
4.2.2 优先级数组
*活动数组,过期数组,prio_array
*优先级数组是实现O(1)调度算法的核心
struct prio_array{
int nr_active; /* 任务数目 */
unsigned long bitmap[BITMAP_SIZE]; /* 优先级位图 */
struct list_head queue[MAX_PRIO]; /* 优先级队列 */
}
sched_find_first_bit(),查找bitmap中的最高优先级。
查找bitmap的结果作为queue的索引,取得最高优先级的task_struct。
4.2.3 重新计算时间片
*每次从活动数组移动到过去数组之前就计算好时间片。从而当活动数组中
没有进程的时候,只需要切换活动数组和过期数组。
if (!array->nr_active){
rq->active = rq->expired;
rq->expired = array;
}
4.2.4 schedule()
*schedule()
->context_switch()
->switch_to()
4.2.5 计算优先级和时间片
*优先级计算:nice+effective_prio()
*时间片:task_timeslice(),按照优先级进行缩放
*交互很强的进程时间片用完之后,如果没有过期数组饥饿,
可以被重新放如活动数组
4.2.6 睡眠和唤醒
*DECLARE_WAITQUEUE()创建等待队列
add_wati_queue()加入等待队列
设置task状态为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE
如果设置了TASK_INTERRUPTIBLE,判断是否是收到信号的伪唤醒
判断等待条件是否为真,如果是取消睡眠,否则调用schedule()
当进程被唤醒后,检查是等待条件是否为真,如果是,退出循环,否则再次调用schedule(),一直重复这个动作
等待条件满足后,设置task状态为TASK_RUNNING,并且调用remove_wait_queue()移出等待队列
4.2.7 负载平衡
*load_balance(),只有在SMP的时候才是有效的,负责在各个处理器之间平衡运行队列
4.3 抢占和上下文
*need_resched标志
*内核在想要调度的时候,需要设这这个标志,如进程时间片耗尽(scheduler_tick),更高优先级变为TASK_RUNNING(try_to_wake_up),从内核空间返回到用户空间,从中断中返回的是时候,都需要检查need_resched标志
4.3.1 用户抢占
*从系统调用返回到用户空间,需要检查need_resched标志
*从中断处理程序返回到用户空间,需要检查need_resched标志
4.3.2 内核抢占
*引入preempt_count标志内核是否持有锁,如果没有并且need_resched标志被设置,则可以发生内核抢占
*从中断处理程序总返回到内核空间之前
*当内核再一次具有可抢占的时候
*显示调用schedule()
*内核任务被阻塞
4.4 实时
*SCHED_FIFO
*SCHED_RR
*SCHED_NORMAL(通常进程)
4.5 与调度相关的系统调用
4.5.1 与调度策略和优先级相关的系统调用
*sched_setscheduler(),sched_getscheduler()
*sched_setpara(),sched_getpara()
*sched_get_priority_max(),sched_set_priority_min()
4.5.2 与处理器绑定有关的系统调用
*sched_setaffinity()
4.5.3 放弃处理器时间
*sched_yield()
4.6 调度程序小结
*在各种矛盾中折衷
第五章 系统调用
*提供用户空间和内核空间通信的一组接口
*除了异常和陷入外,系统调用是用户空间程序进入内核的唯一合法入口
5.1 API POSIX C库
*提供机制而不是策略,设计是需要抽象出需要提供的功能,而至于外部如何使用这些功能则不用关心
*用户空间程序和内核通信
+---------------+ +-----------------------+ +---------------+
+ 调用printf() + ---> + C库中printf C库中write+ ---> + write系统调用 +
+---------------+ +-----------------------+ +---------------+
应用程序-------------------->C库------------------------->内核
5.2 系统调用
*名称为sys_bar()
*所有的系统调用为asmlinkage的,从内核栈上取得参数
*通常0表示成功,失败的时候可以通过peeror()取得全局的errno
5.2.1 系统调用号
*entry.S中系统调用函数表的位置
5.2.2 系统调用性能
*上现文切换高效,系统调用实现简洁
5.3 系统调用处理程序
*int 0x80产生异常,系统切换到内核态,执行异常处理程序
*system_call()异常处理程序
5.3.1 指定恰当的系统调用
*系统调用号存放在EAX寄存器中
5.3.2 参数传递
*ebx,ecx,edx,esi,edi存放5个参数
*返回指存放在eax
5.4 系统调用的实现
*良好的设计是关键,简洁,通用,可移植
*参数验证,内核需要对系统调用作严格的检查,特别是用户空间的指针
-copy_from_user()可以睡眠
-copy_to_user()可以睡眠
-suser()检查用户是否具有超级用户权限
-capable()更细粒度的检查权限
5.5 系统调用上下文
*系统调用上下文属于进程上下文,可以睡眠可抢占
*系统调用需要可重入
5.5.1 绑定一个系统调用的最后步骤
*在系统调用表最后追加一个表项
*定义系统调用号,位于asm/unistd.h文件
*定义系统调用函数,任何文件都可,但是不能编译为模块
5.5.2 从用户空间访问系统调用
*long open(const char * filename, int flags, int mode)
*#define NR_open 5
_syscall3(long, open, const char*, filename, int, flags, int, mode)
最新的版本已经不支持
5.5.3 为什么不通过系统调用的方式实现
*虽然linux中,追加系统调用非常容易,但是不提倡用这种方式,因为需要
系统调用号,稳定后的内核就被固化了,需用追加到各种体系结构中等
5.6 系统调用小结
*API 系统调用 陷入内核 系统调用号,参数传递...
第六章 中断和中断处理程序
*轮询(polling)
*中断(interrupt
6.1 中断
*中断是外设产生的经PIC送到CPU的异步中断信号
*异常是CPU内部执行指令出现错误或者特殊情况时产生的同步中断信号
6.2 中断处理程序
*中断处理程序interrupt handler(ISR)
*内核调用来响应外部设备,需要响应快速,并且需要尽快完成
*上半部于下半部(top half and bottom half)
6.3 注册中断处理程序
*/kernel/irq/manage.c
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
Flags:
IRQF_SHARED Interrupt is shared
IRQF_DISABLED Disable local interrupts while processing
IRQF_SAMPLE_RANDOM The interrupt can be used for entropy
dev_id用于区分同一跟中断线上的共享中断
此函数可能会引起睡眠,不能在中断上下文和不允许阻塞的代码中使用
释放中断:void free_irq(unsigned int irq, void *dev_id)
6.4 编写中断处理程序
*即是编写request_irq()中的handler函数
linux中断处理程序无须可重入,因为一个中断在处理的时候,这个中断在所有处理器上都是被屏蔽的
6.4.1 共享的中断处理程序
*request_irq()中的flag必须是SA_SHIRQ,dev_id必须唯一,需要硬件设备支持和中断处理程序中判断逻辑
6.4.2 中断处理程序实例
*请参考drivers/char/rtc.c
6.5 中断上下文
*执行中断处理程序或者下半部,称为中断上下文,中断处理程序不能睡眠,有严格的执行时间要求,尽量少的内存使用
6.6 中断处理程序的实现
*设备产生中断->经总线到中断控制器->没有屏蔽送到CPU->中断内核->do_IRQ->有中断处理程序->handle_irq_event->运行该线上的所有中断处理程序->ret_from_intr->被中断的代码
/proc/interrupts,porcfs文件系统是虚拟文件系统,存在于内核内存,安装在/proc目录下,代码位于fs/proc目录下
6.7 中断控制
6.7.1 禁止和激活中断
*禁止和激活当前处理器上的本地中断
local_irq_disable(),local_irq_enable()
*禁止之前保存中断状态,激活后恢复中断状态
unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);
6.7.2 禁止指定中断线
*disable_irq()/enable_irq()
disable_irq_nosync()/synchornize_irq()
不能禁止共享中断线,disable和enable次数一致
6.7.3 中断系统的状态
*中断禁止还是激活:irqs_disable(),禁止返回0
内核处于中断上下文:in_interrupt()
内核在执行中断处理程序:in_irq()
6.8 别打断我,马上结束
*中断打断了其它代码,需要快速执行完成,但是又有很多工作要作,所以内核提供了下半部机制(bottom half),在下一章讨论
第七章 下半部和推后执行的工作
7.1 下半部
*执行和中断相关的但是中断处理程序不执行的那部分工作
中断处理程序需要完成对硬件的应答,数据的拷贝,硬件的控制等,其余可留给下半部执行
划分上半部(中断处理程序)和下半部的基本原则:时间敏感,硬件相关,不能被中断打断,要放在中断处理程序中,其余部分都可放在下半部中
7.1.1 为什么要用下半部
*关这中断时间过长会影响系统的相应能力,下半部仅仅是将任务推迟一点,通常在中断处理程序返回就会执行,下半部在开中断的条件下执行
7.1.2 下半部的环境
*实现下半部有不同的方法,老的BH,任务队列,灵活性和性能都不理想,已被摒弃
2.6提供了三种下半部的实现方法:软中断(softirqs),tasklet和工作队列
另外,内核定时器可以把任务推后到固定的时间执行
7.2 软中断
7.2.1 软中断的实现
*代码位于kernel/softirq.c
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
include/linux/interrupt.h
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
软中断处理程序:mysoftirq->action(mysoftirq)
执行软中断:从硬件中断代码中返回,从ksoftirqd内核线程,内核中显式检查执行待处理的软中断
do_softirq()
7.2.2 使用软中断
*软中断只保留给系统中对时间要求最为严格的子系统使用,目前为SCSI和网络子系统
tasklet和内核定时器都是建立在软中断之上
分配索引:位于include/linux/interrupt.h中的枚举
注册处理程序: open_softirq()
触发软中断:raise_softirq()
7.3 tasklet
*利用软中断实现的下半部机制
7.3.1 tasklet的实现
*include/linux/interrupt.h
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};