开始学习内核了,第一本内核书籍选取了《Linux内核设计与实现》这本书,这本只有300多页的薄薄的书当然是不能涵盖所有内核知识的,但是它的作用是一本提纲掣领为以后的内核学习理顺思路的书籍,里面涉及的东西很多,但是又讲得较浅,初学者好理解,是一本不错的入门书籍。
Unix的几个特点
系统调用少
所有东西被当做文件对待
内核和系统工具软件用C写成,移植性强
一套简单但稳定的进程间通信元语,策略和机制分离
Linux是类UNIX系统但它不是Unix,它没有直接只用Unix的源码,有的实现与Unix大相径庭,但同样保证了应用程序编程接口的一致
操作系统是指在整个系统中负责完成最基本功能和系统管理的那些功能
内核通常由负责中断响应的中断服务程序,负责管理多个进程从而共享处理器时间的调度程序,负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成
内核一般处于系统态,拥有收保护的内存空间和访问硬件设备的所有权限,内核空间
应用程序通过系统调用来与内核通信
应用被称为通过系统调用在内核空间运行,而内核被称为运行于进程上下文中,这种交互关系-应用程序通过系统调用界面陷入内核-是应用程序完成工作的基本行为方式
许多操作系统的中断服务程序,包括Linux都不在上下文中执行,他们在一个与所有进程无关的、专门的中断上下文中运行,保证快速响应快速退出
运行于用户控件,执行用户进程
运行于内核空间,处于进程上下文,代表某个特定的进程执行
运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断
页机制(MMU)管理内存,加强对内存的保护,保证每个进程都可以运行于不同的虚拟地址空间上
Linux是一个单内核,运行在单独的内核地址空间上,不过Linux汲取了微内核的精华:其模块化设计、抢占式内核、支持内核线程以及动态装载内核模块的能力,而且还避免了微内核设计上性能的损失,所有的事情运行在内核态,直接调用函数无需消息传递
版本号
从版本号决定是稳定版本还是开发版本,偶数是稳定版,基数是开发版本
源码树目录
编译内核
配置内核
配置选项,CONFIG开头,配置选项要么是二选一要么是三选一,yes,no,module
module一位这该配置项被选定编译的时候这部分的功能的实现代码是以模块(一种可以动态安装的独立代码段)形式生成
yes表示把代码编译仅主内核映像中
驱动程序一般都用三选一的配置项
字符界面配置 make config
基于ncurse库的图形界面工具 make menuconfig
基于qt的图形工具 make xconfig
根据默认的配置创建 make defconfig
验证和更新配置make oldconfig
安装模块 make modules_install 可以把所有已编译的模块安装到/lib/modules
内核开发的特点
与应用程序开发的不同
头文件
基本的头文件位于include目录下
体系结构相关的头文件集位于arch/
printk与printf的区别,printk允许通过指定一个标志来设置优先级,syslogd会根据这个优先级标志来决定在什么地方显示这条系统消息
GNU C
内核开发者总是要用到许多gcc提供的语言拓展部分
1、内联(inline)函数
可消除函数调用和返回带来的开销
但会使代码变长
对时间要求高而本身长度短的函数写成内联函数
2、内联汇编
使用asm()指令嵌入汇编代码
底层和对执行时间要求严格的地方
3、分支声明
gcc内建了一条指令用于优化条件选择语句
likely()想把一个分支标志为通常为真的选择
unlikely()标记为绝少发生的分支
容积小而固定的栈
内核栈的准确大小随体系结构而改变
同步与并发
使用自旋锁和信号量来解决竞争
可移植性
保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于移植性
进程就是处于执行器的程序
执行线程(thread)是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器
内核调度的对象是线程而不是进程
Linux的线程实现很特别:对进程和线程并不特别区分,对Linux而言,线程是一种特殊的进程
同一进程的线程间可以共享虚拟内存但每个都拥有各自的虚拟处理器
通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec函数组创建新的地址空间并把新的程序载入其中
进程描述符和任务结构
内核把进程的列表存放在叫做任务队列的双向循环链表中,结构为task_struct,很大,32位系统一个进程的结构体1.7k
Linux通过slab分配器分配task_struct,2.6版本后用slab分配器动态生成task_struct所以只需在栈底(向下增长栈)或栈顶(向上增长栈)创建一个struct thread_info
进程描述符(pid)
pid_t的隐含类型
这个值越小转一圈就越快
可以修改上限 /kernel/pid_max
进程状态
五种状态:
TASK_RUNNING:可执行、正在执行、在运行队列中等待执行
TASK_INTERRUPTIBLE(可中断):正在睡眠(被阻塞),等待某些条件的达成,达后会设置为运行状态
TASK_UNINTERRUPTIBLE(不可中断):接收到信号也不会被唤醒或准备投入运行
_TASK_TRACE:被其他进程跟踪的进程
_TASK_STOPPED:停止执行,没有投入运行也不能投入运行
设置进程的状态set_task_state(task,state)、set_current_state(task,state)
进程上下文
一般程序在用户控件执行,当一个程序执行了系统调用或者触发了某个异常就陷入了内核空间,此时称内核“代表进程执行”并处于进程上下文中
系统调用或者异常处理是对内核明确定义接口的,对内核的访问都必须通过这些接口
for_each_process(task)宏提供了依次访问整个任务队列的能力,每次访问,任务指针都指向链表的下一个元素
创建进程
Linux的fork使用写时拷贝页实现,写时拷贝是一种可以推迟拷贝甚至免除拷贝数据的计数,内核此时不赋值整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入的时候数据才会被复制,在此之前只是以只读方式共享,在页根本不会被写入(fork之后立刻调用exec)的情况下不发生复制
fork()
fork() vfork() __clone()库函数都根据各自需要的参数标志去调用clone(),然后clone()去调用do_fork()
do_fork()完成创建中的大部分工作,该函数还调用了copy_process()
copy_process()工作流程:
1、调用dup_task_struct为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时,父子进程的描述符完全相同
2、检查并确保创建这个子进程后,当前用户所拥有的进程数没有超出给它分配的资源的限制
3、子进程着手使自己与父进程区别开,初始化进程描述符中许多成员
4、子进程被设置为TASK_UNINTERRUPTIBLE以保证不会投入运行
5、调用copy_flags更新task_struct的flags成员
6、调用alloc_pid为新进程分配PID
7、根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间
8、copy_process()做扫尾工作并返回一个指向子进程的指针
vfork()
除了不拷贝父进程页表项外,vfork()的系统调用与fork()功能相同
子进程作为一个单独的线程在它的地址空间里运行,父进程被阻塞,知道子进程退出或执行exec()
现在在执行fork()时引入了写时拷贝页并且明确了子进程先执行,vfork的好处就仅限于不拷贝父进程的页表项了
另外,由于vfork()语义非常微妙,不建议使用
实际是通过想clone()传递一个特殊标志来进行的
1、调用copy_process()时,task_struct的vfor_done成员被设置为NULL
2、执行do_fork()时,如果给定特殊标志则vfor_done指向一个特定地址
3、子进程先开始执行后,父进程不是马上恢复执行而是一直等待,直到子进程通过vfor_done指针向它发送信号
4、调用mm_release()时,该函数用于进程退出内存空间,并且检查vfor_done是否为空,如果不为空,则会向父进程发送信号
5、回到do_fork(),父进程醒来并返回
如果一切顺利,子进程在新的地址空间里运行而父进程也恢复了再原地址空间的运行,这样,开销确实降低了,不过它的实现不优良
线程技术
现代编程技术中常用的一种抽象概念
提供了再同一程序内共享内存地址空间运行的一组线程
支持并发程序设计,在多处理器上,也能保证真正的并行处理
Linux上线程仅仅被视为一个与其他进程共享某些资源的进程吗,在其他系统上线程被抽象成一个耗费较少资源、运行迅速的执行单元
创建线程
线程的创建和普通进程创建类似,只不过调用clone()时传递一些参数标志来指明所有共享的资源
与调用fork差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序
一个普通的fork()实现
vfork()的实现
clone参数标志
内核线程(kernel thread)
独立运行在内核空间的标准进程
与普通进程间的区别在于内核线程没有独立的地址空间(指向地址空间mm指针被设置为NULL)
只在内核空间运行,不切换到用户空间去
内核线程也只能由其他内核线程创建,内核通过从kthreadd内核进程中衍生出所有新的内核线程
内核线程启动后就一直运行直到调用do_exit()退出或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task_struct结构的地址
进程终结
方式:显式调用exit();隐式地从某个程序的主函数返回;接收到它既不能处理也不能忽略的信号或异常
无论怎么终结,绝大部分都要靠do_exit(),它所作的工作
1、将task_struct中的标志成员设置为PF_EXITING
2、调用del_timer_sync()删除任一内核定时器,确保没有定时器在排队,也没有定时器处理程序在运行
3、如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输入记账信息
4、接下来调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说这个地址空间没有被共享)就彻底释放它们
5、调用exit_sem()函数,如果进程排队等待IPC信号,它则离开队列
6、调用exit_files()和exit_fs(),分别递减文件描述符、文件系统数据的引用计数
7、把存放在task_struct的成员exit_code中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作
8、调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init线程看,并把进程状态设置为EXIT_ZOMBIE
9、do_exit()调用schedule()切换到新的进程
到这里它所占用的内存为内核栈、thread_info结构和task_struct,此时进程向它的父进程提供信息,父进程检索到信息后,由进程所持有的剩余内存被释放
需要释放进程描述符时,会调用release_task()
1、调用__exit_signal,该函数调用__unhash_process,后者又调用detach_pid从pidhash上删除该进程,同时也要从任务列表中删除该进程
2、__exit_signal释放当前僵尸进程所使用的所有剩余资源,并进行最终统计和记录
3、如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task就要通知僵尸的领头进程的父进程
4、调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存