第一章 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; };