操作系统(1)

Posted by BY KiloMeter on May 6, 2019

操作系统的特征:

1、并发:计算机系统中存在多个运行的程序,需要OS的管理和调度

2、共享:“同时”访问,互斥共享

3、虚拟:利用多道程序设计技术,让每个用户都觉得有一台计算机为自己服务

4、异步:程序的执行并不是一贯到底,而是走走停停,OS需要保证程序的正确运行。

中断、异常和系统调用

中断(来源于外设):来自不同的硬件设备的计时器和网络的中断

系统调用(来源于应用程序):应用程序主动向操作系统发出服务请求

异常(来源于不良的应用程序):非法指令或其他坏的处理状态(如内存出错)

中断如何实现?

中断的实现是通过软件和硬件一齐实现的。

硬件:CPU设置中断标记(将内部、外部事件设置中断标记,还要对中断产生对应的中断事件ID)

软件:保存当前程序的状态,根据CPU传递的中断事件ID,查到对应中断处理程序的地址,然后跳转到该地址执行对应的程序。例如网卡来了个数据包,那么这个中断处理程序需要取出这个数据包进行相应的处理。处理完后清除中断标记,并恢复之前保存的程序状态并继续执行程序。

异常:

异常产生之后,CPU也会得到对应的异常编号,此时操作系统也会保存当前的状态,把异常编号交给操作系统,有操作系统处理该异常,决定是否要杀死产生异常的程序,还是继续执行下去。

系统调用:

系统调用是操作系统提供的一层接口,为了让应用程序可以使用操作系统的一些服务。

总结:系统调用,中断和异常跨越了应用程序和操作系统的边界,跨越这个边界付出了一定的代价,获得的是整个操作系统的可靠运行。

代价都有什么:

1、在执行时间上的开销,这三种都会使得CPU从用户态转换成内核态。

2、需要建立中断号或者系统调用号对应的中断例程服务和系统调用历程服务,这个表格需要在操作系统设计初期就设定好

3、操作系统有自己的堆栈,需要额外维护这个堆栈。CPU在内核态和用户态之间来回切换是,需要保存好当时的堆栈情况,以便于后面恢复。

4、内存数据的拷贝也需要时间的开销。不能简单地让应用程序访问内核态的数据,得把内核态的数据拷贝到用户态(比如读取文件,文件读取的内容先是保存在内核态,再拷贝到用户态)。

内存管理

内存是由一个个的内存块组成,不同机器的内存块大小不一样,有些机器是按照字节编址,即一个字节对应一个地址,有些机器是按字编制,比如32位机,字长32位,即4个字节一个地址

地址的长度取决于内存块的数量,打个比方,假设现在有一台4GB内存的电脑,是按照字节编址的,那么一共有4*2^30个内存块,也就是说,需要有32位的二进制数才能表示一个地址。

操作系统中内存管理的不同方法

  • 程序重定位
  • 分段
  • 分页
  • 虚拟内存
  • 按需分页虚拟内存

内存管理的实现高度依赖于硬件,如MMU(内存管理单元,功能是实现虚拟地址到物理地址的转换)

内碎片和外碎片:

内碎片,指的是分配给应用程序,但是应用程序使用不到的(比如应用程序只需要500字节的空间,但是分配给了600字节的空间,多出来的100字节应用程序用不到,只有在应用程序结束后才能释放出来)

外碎片,指的是还未分配的内存空间,由于太小了而无法分配给其他应用程序使用

内存分配策略

首次适配:需要n字节,按照地址从小到大进行查询,分配第一个满足要求的空闲内存块所对应的n字节空间,该方法的优点是速度快,缺点是可能会对大内存块进行切割,容易产生外碎片

最佳适配:选择符合条件,但是相差最小的内存块。该方法的优点是不会去分割大的内存块,缺点也是容易产生外碎片

最差适配:和最佳适配相反,选取相差最大的内存块。优点是避免产生大量琐碎的内存块,缺点是一开始就切割大的内存块,之后如果有大内存空间的需求可能无法满足。

上面这三种算法各有利弊,没有最好的算法,各有自己的应用场景

上面三种都是管理连续内存空间,下面是非连续内存分配的方法

为什么需要非连续内存分配方法,是因为上面的连续内存空间管理方法都存在着问题,内存利用率低,有内存碎片问题。

非连续分配的优点有:

  • 一个程序的物理地址空间是非连续的
  • 更好地对内存进行利用和管理
  • 允许共享代码和数据
  • 支持动态加载和动态链接

非连续分配的问题在于如何建立虚拟地址和物理地址之间的转换

有分页和分段两种方法

分段需要解决的问题是:如何进行分段,分段之后如何进行寻址

将逻辑地址对应的空间对应到分散的物理地址空间上。

分段的寻址需要利用到硬件方面的支持,

逻辑地址由段号+段内偏移量 组成,逻辑地址转换成物理地址的过程如下:

取出段号,段号就是段表的索引,获取对应段的基地址(也就是起始地址),先比对下偏移量是否超出长度,如果超出了范围,发生异常,否则基地址和段内偏移量拼接成物理地址。

段式用的比较少,现在多用的是页式

分页和分段最大的区别就是在于分段的段大小是不固定的,而分页的页大小是固定的

分页同样需要解决寻址问题,物理地址同样使用页号+页内偏移量组成

页表存在的问题:现在的机器一般是64位,也就是说逻辑地址有64位,假设每页有1024字节,那么页表中一共有2^64/1024 = 2^54项,页表无法存放这么多项内容。此外,还有问题就是,如果页表比较大。CPU无法存放,那么就得存放在内存当中,这样的话,每次要查找内存中数据,都得访问两次内存(第一次访问页表获取页帧号,第二次访问真正的内存数据)

如果解决这个问题?有两种方法,缓存和间接访问

缓存:通过TLB(快表)实现,TLB位于CPU内部,存放着经常访问的页表项。

进程控制

内核态和用户态之间的切换是通过中断实现的,通过设置CPU的程序状态字(PSW)的标志位来进行切换

进程由程序段、数据段、PCB三部分组成

PCB是进程存在的唯一标志

操作系统内部维护着各个状态的进程队列,如就绪队列,阻塞队列,运行队列等。

进程的状态可以根据PCB中的信息进行判断。

进程控制:现在假设一个场景,一个正在运行的进程,被打断后放入了阻塞队列,之后进入了就绪队列,如果在这个时候,放入到就绪队列中时,刚好获取到CPU而进入了运行队列,但是该进程的PCB中进程状态位仍未发生改变,这种情况就会出现问题。这个问题产生的根源就是PCB放入其他队列以及修改PCB中的状态位这两个指令是分开执行的,如果想要避免上面这种情况的发生,需要使用到原语(和原子性是差不多的,这几个指令要么全都执行,要么都不执行),原语在底层的实现就是在要一起执行的这些指令开始前加上一条关中断指令,并在这些指令结束之后加上一条开中断指令,中间的指令在执行的过程中,如果遇到其他中断,这些中断会暂时被忽略。

进程通信

为了系统的安全,进程之间是不能直接访问彼此的数据的,因此需要进程通信。

进程通信可以分为三大类:共享存储、消息传递、管道通信

共享存储

共享存储:在内存中开辟一块进程共享的内存块,通过该内存块实现信息的传递。(注意:进程对该共享内存块的访问必须是互斥的,这里可以通过同步互斥工具,如PV操作来实现)。

共享存储又分为基于数据结构的共享和基于存储区的共享

基于数据结构的共享:这种方式只能在共享空间里面放置特殊数据结构的数据,比如长度为10的数组,进程间数据的交换通过这个数组实现,这种共享方式比较慢,限制多,是一种低级通信方式

基于存储区的共享:这种方式在内存中画出一块共享存储区,数据的形式,内存的位置都由进程来决定,而不是由操作系统来决定,这种共享方式速度比较快,是一种高级通信方式

管道通信

“管道”指的是用于连接读写进程的一个共享文件,又名pipe文件,实际上是在内存中开辟了一个固定大小的缓冲区。

注:管道只能进行半双工通信,如果要实现全双工通信的话,需要使用两个管道

各个进程对管道的访问也必须是互斥的

数据是以字符流的形式写入管道的,当管道写满后,写进程的write()方法会被阻塞,等待读进程将数据取走,取完后,读进程的read()也被阻塞。

如果没写满,不允许读,没读空,不允许写

管道中的数据一旦被读取后,就会从管道中被抛弃,因此如果由多个读进程读取同一个管道的话,可能会出现读错数据的情况。

消息传递

进程间的数据交换以格式化的消息为单位,进程间通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。消息分为消息头部分和消息体部分。消息头包含了发送进程ID,接收进程ID,消息类型,消息长度等信息。

消息传递分为直接通信和间接通信

直接通信:每个进程都有一个消息缓冲队列,直接通信是把消息直接发送到进程的消息缓冲队列中

间接通信:类似于管道通信,进程间维护一个信箱,消息先发送到信箱上,再去信箱上进行消息读取。和管道通信的区别就是,由于消息头部分由接收进程ID等信息,因此不会发生取错消息的情况