第一周总结
1.存储程序计算机 + 函数调用堆栈 + 中断机制
2.堆栈:C语言程序运行时候必须的一个记录调用路径和参数的空间(函数调用框架/提供局部变量/传递参数/保存返回地址)
不同指令可能实现相同的功能
3.堆栈相关:esp ebp ;操作: push pop;
ebp在C语言中用作记录当前函数调用的基址
cs:eip总是指向下一条的指令地址:顺序;跳转/分支
第二周总结
thread(存ip sp)
pcb(定义进程管理相关的数据结构)
my_schedule(调度器)
内核初始化 0号调度
初始化0号进程
创建更多的进程
启动0号进程
ret之后0号进程启动
循环1000万次,才有机会判断一下是否需要调度
设置时间片大小,时间片用完时设置一下调度标志。
第三周总结
1.两把宝剑:中断-上下文的切换(保存现场和恢复现场) 进程-上下文的切换
2.linux-3.18.6
arch/x86目录下的代码是我们重点关注的;
内核启动相关代码基本在init目录下;(start_kernel函数相当于普通C程序的main函数)
linux内核的核心代码在kernel目录下
1、内核被称作是管理者或者是操作系统核心,它一般处于系统态,拥有受保护的内存空间和访问硬件设施的所有权限。这种系统态和被保护起来的内存空间,统称为内核空间。
2、在系统中运行的应用程序通过系统调用来和内核通信:
运行于用户空间,执行用户进程
运行于内核空间,处于进程上下文,代表某个特定的进程执行
运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断
1.4linux内核和传统unix内核的比较
1、linux支持动态加载内核模块
2、linux支持对称多处理的(SMP)机制
3、linux内核可以抢占
4、linux不区分线程和其他一般进程,所以进程都一样
第四周总结
一、用户态.内核态.中断
(上周课件有学习到)
1、地址空间是一个显著的标志(是逻辑地址,不是物理地址)
2、CPU每条指令的读取都是通过cs:eip这两个寄存器;0xc00000000以上的地址空间只能用内核态访问
3、中断处理是从用户态进入内核态的主要方式(系统调用是特殊的中断)
4、保护用户态的寄存器上下文(用户栈顶地址、当时的状态字、当时cs:eip的值)
5、中断第一步:保存现场(push) 最后一步:恢复现场(pop)
二、系统调用概述
1、系统调用的意义
1.操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用:把用户从底层的硬件编程中释放出来;极大的提高了系统的安全性;使用户程序具有可移植性
2.API和系统调用
应用编程接口(API)和系统调用是不同的:API只是一个函数定义,而系统调用会通过软中断向内核发出一个明确的请求
3.Libc库定义的一些API引用了封装例程,唯一的目的就是发布系统
一般每个系统调用对应一个封装例程
库再用这些封装例程定义出用户的API
*不是每个API都对应一个特定的系统调用(API可能直接提供用户态的服务);也可能调用几个系统调用;不同的API可能调用同一个系统调用
*返回值:大部分封装例程返回一个整数(其含义依赖于相应的系统调用);-1在多数情况下表示内核不能满足进程的请求;Libc定义的errno变量包含特定的出错码
2、系统调用程序及服务例程
1.在linux中通过执行int $0x80来执行系统调用
2.传参:进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数(使用eax寄存器)
3、参数传递
1.系统调用也需要输入输出参数;
2.system_call是linux中所有系统的调用的入口点,每个系统调用至少有一个参数,即为用eax传递的系统调用号
4、系统调用的内核代码
1.系统调用分派表(dispatch table)存放在sys_call_table数组
5、系统调用的机制初始化
(了解代码)
6.系统调用的三层皮
xyz()
system_call()
sys_xyz()
7.中断向量0x80与system_call绑定起来;系统调用号将xyz和system_xyz()关联起来
第五周总结
5.1与内核通信
1.系统调用在用户空间进程和硬件设备之间添加一个中间层。作用有三个:它为用户空间提供了一种硬件的抽象接口;系统调用保证了系统的稳定和安全;系统调用是用户空间访问内核的唯一手段;
**除异常和陷入外,它们是内核唯一的合法入口;
5.2API、POSIX、和C库
1.应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程;内核只跟系统调用打交道;
5.3系统调用
1.系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long
2.系统调用号sys_ni_syscall(),除了返回,这个错误号就是专门针对无效的系统调用而设的
3.系统调用表中的所有注册过的系统调用的列表,存储在sys_call_table中,都明确的定义了这个表,在x86-64中,它定义于arch/i386/kernel/syscall 64.c
4.系统调用的性能:linux上下文切换时间是一个重要的原因,进出内核都被优化的简洁高效;另一个原因是系统调用处理程序和每个系统调用本身也很简洁
5.4系统调用处理程序
1.通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序(也就是系统调用处理程序),中断号是128,通过int $0x80指令触发该中断
2.系统调用号是通过eax寄存器传递给内核的
3.参数传递:把这些参数也存放在寄存器里;用单独的寄存器存放指向所有这些参数在用户空间地址的指针
5.5系统调用的实现
1.实现系统调用:注意可移植性和健壮性
2.参数验证:与文件I/O相关的系统调用必须检查文件描述符是否有效;与进程相关的函数必须检查提供的PID是否有效;还有就是检查用户提供的指针是否有效(接收指针之前必须保证:指针指向的内存区域属于用户空间;指针指向的内存区域在进程的地址空间里;进程决不能绕过内存访问控制);
3.copy_to_users和copy_from_users()都有可能引起阻塞;进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存
5.6系统调用上下文
1.内核在执行系统调用的时候处于进程上下文,current指针指向当前任务(也就是引发系统调用的那个进程);在进程上下文中,内核可以休眠
2.绑定一个系统调用最后步骤:首先,在系统调用表的最后加入一个表项;再者,系统调用号都必须定义于<asm/unitsd.h>;系统调用编译成内核映象
3.从用户空间访问系统调用
第六周总结
1.操作系统内核的三大功能:进程管理(核心),内存管理,文件系统;
1. fork()用户态创建子进程
2. fork()在父进程和子进程中各返回一次
2.理解方法
1. fork()是一个系统调用
2. fork() vfork() clone()最终都可以在do_fork来实现进程的创建
3.复制当前进程实现创建新进程
3.浏览相关关键代码
1.复制、给新进程分配、修改PID、进程链表等;
2.sys_fork sys_clone sys_vfork 最终都return do_fork
3.alloc_thread_info_node 地址空间,分配实际内核空间
4.p=dup_init_task 复制进程
5.childregs= *current_pt_regs();复制内核堆栈
6.return_from_fork(返回用户态)
拷贝内核堆栈数据和指定新进程的第一条指令进程
4.gdb跟踪调试
3.1进程
1.进程:进程就是处于执行期的程序,实际上,进程就是正在执行的程序代码的实时结果;
2.执行线程,简称线程,是进程中活动的对象(每个线程拥有独立的程序计数器、进程栈、和一组进程寄存器),内核调度的对象是线程,不是进程
3.进程提供的两种虚拟进制:虚拟处理器和虚拟内存
4.程序本身不是进程
5.调用fork(),该系统调用通过复制一个现有进程来创建一个全新的进程;接这调用exce()函数,可以创建新的地址空间,并把程序载入其中;最后,程序通过exit()系统调用退出调用。这个函数会终结进程并将其占用的资源释放掉
*fork()实际上是由clone()系统调用实现的
3.2进程描述符及任务结构
1.双向循环链表中,每一项都是类型为task_struct,称为进程描述符的结构;该结构定义在<sched.h>文件中
2.进程描述符中包含的数据能完整地描述一个正在执行的程序
3.每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际的task_struct的指针
4.进程描述符的PID的最大值实际就是系统中允许同时存在的进程的最大数目;
5.进程描述符中的state域描述了进程的当前状态;必然处于五个状态的一种:
task_running
task_interruprtion
task_uninterruption
task_traced
task_stopped
*调整某个进程的状态,这时使用set_task_state函数:
6.进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口
系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或者多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中
3.3进程创建
1.首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源的统计量;exec()函数负责读取可执行文件并将其载入地址空间开始运行;
2.linux通过clone()系统调用实现fork();do_fork完成创建中的大部分工作,定义在kernel/fork.c文件中
3.除了不拷贝父进程的页表项外,vfork()系统调用和fork功能相同;vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的
3.4线程在linux中的实现
1.创建线程:与普通进程的创建类似,只不过调用clone()时候需要传递一些参数标志来指明需要共享的资源,参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类
2.内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它们只在内核空间进行,从来不切换到用户空间。内核进程和普通进程一样,可以被调度,也可以被抢占
3.kthread内核进程通过clone()系统调用而创建。新的进程将运行threadfn函数,传递的参数为data
3.5进程终结
1.在调用do_exit()之后,尽管线程已经僵死不能再运行,但是系统还是保留了它的进程描述符。
2.wait()这一组函数通过唯一的一个系统调用wait4()来实现
3.linux如何存放(task_struct)和表示进程(thread_info);创建(fork());,实际上最终clone()
第七周总结
7.1编译器驱动程序
大部分编译器驱动程序,它代表用户在需要时调用语言预处理器,编译器,汇编器,和链接器.
1.驱动程序首先运行C预处理器cpp,将C源程序main.c翻译成一个ASCII码的中间文件main.i;
2.驱动程序运行C编译器cc1,将main.i翻译成ASCII汇编语言文件main.s;
3.驱动程序运行汇编器as,将main.s翻译成一个可重定位目标文件main.o;
4.运行链接器1d,将.o文件和一些必要的系统目标文件组合起来,创建一个可执行目标文件p;
7.2静态链接
输入的可重定位目标文件由各种不同的代码和数据节组成。指令在一个节中,初始化的全局变量在另一个节,未初始化的变量在另一个节;
构造可执行文件,链接器必须完成:
1.符号解析(目的是将每个符号引用刚好和一个符号定义联系起来)
2.重定位(把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得他们指向这个存储器位置)
*目标文件纯粹是字节块的集合
7.3目标文件
目标文件有三种形式:
1.可重定位目标文件
2.可执行目标文件
3.共享目标文件(特殊的可重定位目标文件)
*一个目标模块就是一个字节序列;而一个目标文件就是存放在磁盘文件中的目标模块
7.4可重定位目标文件
使用的是unix可执行和可链接格式ELF目标文件格式
ELF头以一个16字节的序列开始,生成该文件的系统的字的的大小和字节顺序;头部剩下的部分 .....有节头部表,,,,,,
夹在ELF头和节头部表之间的都是节;
.text
.bss(未初始化的数据)
7.5符号和符号表
每个可重定位目标模块m都有一个符号表,包含m所定义和引用的符号的信息,在链接器的上下文中,有三种不同的符号:
1.由m定义并能被其他模块引用的全局符号
2.由其他模块定义并被模块m引用的全局符号
3.只被模块m定义和引用的本地符号
*定义带有C static 属性的本地过程变量是不在栈中管理的;
符号表由汇编器构造。name是字节串表的字节偏移;value是距定义目标的节的起始位置的偏移;size是目标的大小;每个符号都和目标文件的某一个节相关联,section字段表示,该字段也是一个到节头部表的索引:
1.ABS代表不该被重定位的符号;
2.UNDEF代表未定义的符号;
3.COMMON表示还未被分配位置的未初始化的数据目标;
7.6符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。编译器还确保静态本地变量,它们也会有本地链接器符号,拥有唯一的名字
1.如何解析多重定义的全局符号
编译器向汇编器输出每个全局符号,或者强或者弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里,函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号,有三个规则:
规则1、不允许有多个强符号;
规则2、如果有一个强符号和多个弱符号,那么选择强符号;
规则3、如果有多个弱符号,那么随机选择一个;
2.与静态库链接
将所有相关的目标模块打包成一个单独的文件称作静态库,可以用作链接器的输入。当链接器构造一个输出的的可执行文件,只需要拷贝静态库里被应用程序引用的模块。
特点:减少了可执行文件在磁盘和存储器中的大小;
*静态库以一种称为存档的的特殊文件格式存放的磁盘中;存档文件由后缀.a标识
3.如何使用静态库来解析引用
链接器维持一个可重定位的目标文件E,一个未解析的符号U,和一个在前面输入文件中已定义的符号集合D;
链接器会判断F是一个目标文件还是一个存档文件
7.7重定位
1.重定位节和符号定义;重定位节中的符号引用(依赖于重定位条目);
2.ELF中两个基本的重定位类型:R_386_PC32;R_386_32
3.重定义符号引用分为PC相对引用和绝对引用
7.8可执行目标文件
1.ELF包括程序的入口点,也就是当程序运行时的第一条指令的地址
2.ELF可执行文件被设计得很容易加载到存储器,可执行文件的连续的片被映射到连续的存储器段。段头部表描述了这种映射关系
7.9加载可执行的目标文件
7.10动态链接共享库
共享库是一个目标模块, 在运行时,可以加载到任意的存储器地址,并且和一个在存储器中的程序链接起来,这个过程就是动态链接
7.11从应用程序中加载和链接共享库
7.12与位置无关的代码PIC(引用、函数调用)
7.13处理目标文件的工具
第八周总结
1.进程调度与其时机分析
分类:
第一种分类 I/O-bound:频繁的进行I/O;会花很多时间等待I/O操作完成
CPU-bound:计算密集型;需要大量cpu时间进行计算
第二种分类 批处理进程:不必与用户交互,通常在后台进行;不行很快响应(编译程序,科学计算)
实时进程:有实时需求,不被低优先级的进程阻塞;响应时间短,稳定(视频/音频,机械)
交互式进程:经常和用户交互;花很多时间等待用户输入,响应时间快(shell;文本编辑器)
linux中的调度是多种调度策略和算法的混合:
是基于分时和优先级的;
进程的优先级是动态的;
*内核中的调度算法相关代码使用了类似OOD中的策略模式
进程调度的时机:
schedule函数:在运行队列找到一个进程,把CPU分配给它(直接调用或者分散标记need_reched)
中断处理过程中,直接调用schedule(),或者返回用户态schedule()根据标记来返回;
内核线程直接调用schedule()进行线程切换(内核线程是只有内核态没有用户态的特殊进程);
用户态无法主动调度,仅能陷入内核态后的某个时机点进行调度(只能被动调度);
2.进程上下文切换相关代码分析
进程的切换:
挂起正在CPU上执行的进程,与中断时保存现场不同,中断前后是同一个进程上下文中,由用户态向内核态进行,包含了进程执行所需要的所有信息(用户地址空间;控制信息;硬件上下文)
schedule()选择一个新进程来运行,调用context_switch进行上下文切换,这个宏调用switch_to进行关键上下文切换
next=pick_next_task
context_switch(上下文切换)
switch_to(pre,next,last)
*next_ip一般是$1f,对于新创建的子进程是ret_from_fork
二、linux系统的一般执行过程
1.一般执行过程
正在运行X→发生中断→SAVE_ALL(保存现场)→中断处理过程或中断返回前调用schedule(),其中的switch_to进行上下文切换→标号1后开始运行进程Y→RESTORE_ALL(恢复现场)→iret_pop→继续运行用户态Y
2.特殊情况
内核线程发生中断没有进程用户态,内核态转换;
内核线程主动调用schedule(),只有进程上下文切换,没有中断文上下文切换;
创建子进程的系统调用在子进程的执行起点,以及返回用户态(next_ip=ret_from_fork)
加载一个新的可执行程序后返回到用户态的情况(如exceve)
3.内核是各种中断处理过程和内核进程的集合
三、linux系统架构和执行过程概览
1.架构概览
2.执行ls命令→确定命令→fork生成一个shell本身的拷贝→exce将ls的可执行文件装入内存→从系统调用返回
3.从CPU和内存的角度看linux的执行过程