操作系统
计算机启动的过程
x86架构的启动过程
开机时先处于实模式,实模式的寻址方式为CS:IP(CS<<4+IP)
,CS为段寄存器,IP为指针寄存器(相当于偏移地址)
ox7c00处的引导扇区代码bootsect.s
,主要功能如下:
0x90200处的setup.s
,主要功能如下:
其中,0x10中断读取光标位置,0x15中断读取扩展内大小
system模块从head.s
开始执行,head.s
的主要功能是在保护模式下进行初始化,主要包括:
- 重新初始话
idt
、gdt
- 进入C语言的main函数
main函数的工作主要是xx_init
:内存、中断、设备、时钟、CPU等内容的初始化
main函数最后if(!fork()){init();}
,永远不会退出。
操作系统的接口
命令行:命令行也是一段程序(即shell也是一段程序)
OS启动的最后,会执行一个shell程序
/*
shell调用:1. scanf 2. 申请CPU 3. 执行命令
*/
int main(int argc, char* argv[]){
char cmd[20];
while(1){
scanf("%s", cmd);
if(!fork()){
exec(cmd);
}else{
wait();
}
}
}
操作系统接口:接口表现为函数调用,又由操作系统提供,所以称为系统调用
POSIX标准
系统调用的实现
-
内核程序和用户程序隔离,区分内核态和用户态
-
当前程序执行在什么态:由于CS:IP是当前指令,所以用CS的最低两位来表示,0是内核态,3是用户态。
内核态可以访问任何数据,用户态不能访问内核数据。
DPL:目标内存段特权级 CPL:当前内存段特权级;对比CPL和DPL,当DPL大于等于CPL时,才可以访问。
硬件提供了进入内核的方法, 对于x86架构,中断是进入内核的唯一方法。
- 用户程序中包含一段含有
int
中断指令的代码(x86架构为int 0x80
) - 操作系统写中断处理,获取想调程序的编号
- 操作系统根据编号执行相应的代码
操作系统历史
在计算机的发展过程中,当计算机应用在金融行业时,需要计算机进行读写和计算,但是由于IO读写设备速度相对于CPU较慢,如果让CPU一直等待输入完成,然后进行运算,使得CPU利用效率较低。所以考虑CPU进行切换和调度,提高CPU利用率。进而发展出多进程,之后又出现了图像界面和文件管理,出现了操作系统的文件操作.
CPU管理
进程:刻画运行的程序
设置初始PC指针,CPU自动取指执行,PC指针自加加。如果只是顺序的执行,还会出现操作系统历史发展中CPU利用率低的问题,因此,在单个CPU上采用多道程序交替执行的方式(并发)提高CPU利用率。
交替执行的过程中,涉及到PC指针的跳转,跳转返回需要回复之前的状态,因此采用PCB(Process Control Block)记录一个程序的状态。
多进程
多进程如何组织:PCB+状态+队列
进程状态图:
多进程如何切换:
进程的调度、FIFO、Priority
多进程的影响:
负面影响:通过对一个物理地址进行不同的操作,破坏数据。通过多进程的地址空间分离可解决该问题。具体是通过映射表将进程中的逻辑地址映射为不同的物理地址。
进程同步:进程间的合作。
用户级线程
进程=(内存)资源+指令执行序列
资源不动,而只切换指令序列(实质就是映射表不变,而PC指针变):线程切换
线程:保留了并发的优点,避免了进程切换的代价。
网页浏览器使用多线程的思想。例如:一个线程用来接收数据,一个线程用来显示文字,一个线程用来处理图片,一个线程用来显示图片等。
实现:创建线程pthread_create
,切换线程Yield
,
两个用户级线程所需要的:两个TCB、两个用户栈、切换的PC在栈中
用户级线程**,Yield是用户程序,当进程的某个线程阻塞的时候,整个进程都会阻塞,因为在内核看来,整个进程是一个整体,一个阻塞即整个进程阻塞。
内核级线程,ThreadCreate
是系统调用,进入系统内核,Schedule
进行调度。
内核级线程
内核级线程的并发性更好,多进程、用户级线程不能发挥多核的价值
两个内核级线程所需要的:两个 TCB、两套栈(一套栈包括一个用户栈和一个内核栈)、切换的PC在栈中
内核级线程切换流程:用户执行程序,出现函数调用,进行用户栈的入栈,当通过系统调用产生int 0x80
中断进入内核时,将用户栈的信息压入内核栈进行保存;如果当前线程变成阻塞态态,则调用switch_to
函数在内核中进行切换(切换TCB),切换到另一个内核栈的指针,切到对应得内核程序,最后通过ret
返回到该内核栈对应得用户栈中。
用户级线程、核心级线程得对比:
CPU调度策略
周转时间:从任务进入到结束的时间
响应时间:从操作到发生响应的时间
吞吐量:完成的任务量
IO约束型:CPU使用时间少,IO阻塞时间长
CPU约束型:CPU使用时间长,IO阻塞时间少
前台任务关注响应时间,后台任务关注周转时间
CPU调度算法:
- SJF:短作业优先,该方法的周转时间最小
- RR:轮转机制
- 优先级调度
实际的schedule函数
void Schedule(void){
while(1){
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while(--i){
if((*p->state == TASK_RUNNING)&&(*p)->counter>c){
c=(*p)->counter, next = i;
}
}
if(c) break;
for(p=&LAST_TASK;p>&FIRST_TASK;--p) (*p)->counter = ((*p)->counter>>1)+(*p)->priority;
}
switch_to(next);
}
进程同步与信号量
生产者-消费者实例
只是单纯的信号,不足以表示足够的信息,例如多少个生产者等等,所以需要信号量。
信号量临界区保护
如果进程同时修改信号量,会引发错误,产生竞争错误;所以需要对信号量进行保护,当一个进程对信号量进行写操作的时候,需要对信号量进行上锁操作。在代码中,一次只允许一个进程进入的该进程的那段代码称为临界区,临界区可起到保护信号量的作用。
保护代码的基本原则:1. 互斥进入 2. 有空进入 3. 有限等待
实际中临界区保护的方法:
- 面包店算法,排号
- 关中断,阻止调度产生
- 临界区保护的硬件原子指令法,一条机器指令完成上、开锁的功能,这样就不会调度出去
死锁处理
多个进程由于互相等待对方持有的资源而造成的谁都执行的情况叫死锁。
死锁的四个必要条件:1.互斥使用 2.不可抢占 3.请求和保持 4.循环等待
死锁的处理方法:1. 死锁预防 2.死锁避免 3.死锁检测+回复 4.死锁忽略
银行家算法课实现死锁预防。
内存使用
将程序放在内存中,PC指向开始地址;CPU取指执行,但是实际指令的跳转地址可能和内存的实际物理地址不同,因此,程序运行过程中需要重定位。重定为有编译时重定位和载入时重定位;编译时重定位,程序只能放在指定的固定位置,载入时重定位,程序一旦载入内存就不能改动。但是,这两种方式在实际中无法满足对载入程序进行移动的需求。因此,引入运行时重定位。