本章包含了操作系统和计算机组成相关的知识信息,Linux部分被专门分出去了。

1. 总体概念

1.1 操作系统的特性是什么?

四个特性:并发、共享、虚拟、异步。

  • 同一段时间内(时间片轮转算法)多个程序执行。
  • 系统中的资源可以被内存中多个并发执行的进线程共同使用。
  • 虚拟:通过时分复用以及空分复用(如虚拟内存)技术实现把一个物理实体虚拟为多个。
  • 异步:系统中的进程是以走走停停的方式执行的,且以一种不可预知的速度推进。(同步就是实时处理,比如打电话,异步就是分时处理,比如发短信)

1.2 操作系统的主要功能有哪些?

操作系统的本质是对资源的管理。包括了:

  • 处理器管理:以进程为单位分配资源
  • 存储器管理:也叫内存管理
  • 设备管理:完成所有的IO请求
  • 文件管理:包括磁盘存储空间管理,文件读写管理等等

1.3 什么是用户态与核心态?

从整体上讲,操作系统一般可分为内核(kernel)外壳(shell)两大部分。

当程序运行在3级特权级上时,就可以称之为运行在用户态。反之则是内核态。0级特权直接控制硬件,12级特权是系统程序,包括驱动等等,3级特权是用户程序。

用户态切换到内核态有三种情况:

  • 系统调用:用户态进程主动要求切换到内核态的一种方式。
  • 异常:当前运行进程切换到处理此异常的内核相关程序中
  • 外围设备中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。

2. 线程与进程

2.1 线程进程协程的区别

对于操作系统来说,一个任务就是一个进程(Process),比如使用word。而一个进程可能不只干一件事(比如word既要打字又要检查拼写),这种进程内的多个子任务就是线程(Thread)。

具体来说:

  • 进程是操作系统分配资源的单位,而线程是进程的一个实体,是CPU调度和分派的基本单位。
  • 线程没有独立的内存单元,不能够独立执行,必须依存在应用程序中。

引入线程的好处:线程快!创建、终止、切换都很快!

总结:

  1. 一个程序至少有一个进程,一个进程至少有一个线程。
  2. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存
  3. 虽然线程拥有单独的程序运行入口,出口,但不能独立执行。

协程与线程的区别:协程不再被内核调度,而是交给了程序自己,因此golang中专门引入了GMP模式,设立了专门的逻辑处理器。

2.2 进程有哪些状态,转换条件是什么?

就绪状态:进程获得了除CPU之外的一切所需资源

运行状态:一个CPU的一个核只能有一个进程处于运行状态。

阻塞状态,又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。

注意区别就绪状态和等待状态:就绪状态是指进程仅缺少处理机,只要获得处理机资源就立即执行;而等待状态是指进程需要其他资源(除了处理机)或等待某一事件

2.3 进程间通信

也就是所谓的IPC(Interprocess communication)问题,主要是指进程间交换数据的方式

进程是相互独立的,并不需要条件变量、互斥锁这些机制,要锁也是文件锁这种大锁。而线程需要互斥锁的原因是:线程之间的资源室共享的,需要程序员来完成变量级别的同步。

进程间通信分为低级通信高级通信

  • 低级通信:信号量
  • 高级通信:
    • 管道
    • 消息队列
    • 共享内存

信号量是一个计数器,防止多个进程将资源拿光,防止某进程正在访问共享资源时,其他进程也访问该资源。

管道是指用于连接一个读进程和一个写进程的一个共享文件,又名pipe文件,以字符流形式将数据写入文件。管道分为无名管道有名管道

  • 无名管道是半双工的通信方式,数据只能单向流动,只能在父子进程中流通;
  • 有名管道也是半双工,但是它允许无亲缘关系进程间通信。

消息队列指的是进程间的数据交换是以格式化的消息(Message)为单位的,再由消息组成的链表,形成队列。

共享内存指在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换。在对共享空间进行写/读操作时,需要使用同步互斥工具(如 P操作、V操作),对共享空间的写/读进行控制。

2.4 进程间同步

多进程虽然提高了系统资源利用率和吞吐量,但是由于进程的异步性可能造成系统的混乱。进程同步的任务就是对多个相关进程在执行顺序上进行协调

同步手段有:

  • 临界区
  • 互斥量
  • 信号量
  • 事件: 通过通知操作的方式来保持线程的同步

其中:互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多。

2.5 线程间同步

由于线程间的资源可以共享,同步的方式就会更加细致:

  • 互斥量
  • 信号量,只能用于一个资源的互斥访问,不能实现多个资源的多线程互斥问题
  • 读写锁,可以被多个读者拥有,但是只能被一个写者拥有的锁
  • 条件变量,线程 A 等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒
  • 原子操作
  • 通道

2.6 什么是临界区,如何解决冲突?

每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。

解决冲突的办法:

  • 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入,如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待;
  • 进入临界区的进程要在有限时间内退出
  • 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象。

2.7 线程的分类

内核级线程:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程

用户级线程:它仅存在于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在

2.8 线程池

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以池化技术能提升性能。

go实现线程池如下。创建两个通道,通道queue中传入任务函数,通道result传入结果:

  • 创建结构
  • 初始化
  • 开门接客
  • 关门送客
  • 辅助函数:添加任务、回调
type pool struct{
    Queue chan func() error
    Number int //协程数
    Total int //任务数

    result chan error
    finishCallback func()
}

func (p *pool) Init(number, total int){
    p.Queue=make(chan func() error,total)
    p.Number=number
    p.Total=total
    p.result=make(chan error,total)
}
// 开门接客
func (p *pool) Start(){
    // 开启Number个goroutine
    for i:=0;i<p.Number;i++{
        go func(){
            for {
                task, ok := <-p.Queue
                if !ok { //所有任务已经完成,跳出循环
                    break
                }
                err:=task() //执行任务,每个协程阻塞在此
                p.result<-err 
            }
        }()
    }

    // 获得每个work的执行结果
    for i:=0;i<p.Total;i++{
        res:=<-p.result //阻塞形式
        if res!=nil{
            fmt.Println(res)
        }
    }

    // main传入回调函数才可以执行
    if p.finishCallback!=nil{
        p.finishCallback()
    }
}

// 关门送客
func (p *pool) Stop() {
       close(p.Queue)
    close(p.result)
}
//添加任务
func (p *pool)addTask(task func() error){
    p.Queue<-task
}
//设置结束回调
func (p *pool) setFinishCallback(callback func()){
    p.finishCallback=callback

使用过程:

ool := new(GoroutinePool)
pool.Init(3, 10) //开启3个协程处理10个任务
for i:=0;i<10;i++{
    pool.addTask(func() error{
        //从taskName中获取任务要求
        return doTask(taskName[i])
    })
}

isFinish:=false
pool.setFinishCallback(func(){
    func(isFinish *bool){
        *isFinish=true
    }(&isFinish)
})

pool.Start()
for{
    if isFinished{
        break
    }
    time.Sleep(time.Millisecond*100)
}

pool.Stop()
fmt.Println("Finished!")

3. 内存

3.1 逻辑地址、线性地址和物理地址的区别?

逻辑地址(Logic Address)是指由程序产生的与段相关的偏移地址部分,因此一个逻辑地址由段标识符和段内偏移量组成,有时也称虚拟地址。比如,在C程序中,可以使用&操作读取指针变量本身的值,实际上这个值就是逻辑地址。逻辑地址和绝对的物理地址不相干。

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

物理地址(Physical Address)是CPU外部地址总线上的地址,也是地址变换的最终地址。

3.2 寻址方式有哪些?

寻址寻的都是物理地址。分三组:立即寻址+寄存器寻址;直接间接寻址;相对寻址+2个基变址寻址。

  • 立即寻址:操作数在指令中给出,作为指令的一部分。

    MOV AL,5
    
  • 寄存器寻址:操作数在CPU内部的寄存器中,指令指定寄存器。不需要访问存储器,所以速度快。

    MOV AX,BX
    
  • 直接寻址操作数在寄存器中,指令直接包含有操作数的有效地址。

    MOV AX,[8054]
    
  • 间接寻址:存储操作数的寄存器的地址另一个在寄存器中

    MOV AX,[SI]
    
  • 相对寻址:操作数在存储器中,形式为基地址+偏移地址。假设数据段DS地址为5000,DI为3678,最终地址为5000+3678+1223。

    MOV AX,[DI+1223H]
    
  • 基变址绝对寻址:操作数在寄存器中,形式为基地址寄存器+变址寄存器。BX基地址+DI变址得到操作数存放的地址。

    MOV AX,[BX][DI]
    
  • 基变址相对寻址:操作数在存储器中,形式为:基地址寄存器+变址寄存器+偏移地址

    MOV AX,[BX+DI-2]
    

3.3 什么是虚拟内存?

多任务会带来进程对内存的操作冲突,需要虚拟内存来解决。假设现在有一块物理内存,操作系统让两个进程共用这一块内存,彼此并不打扰。

3.4 什么是交换空间?

操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间。硬盘上的那块空间叫做交换空间(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。

用途:

  • 物理内存不足时一些不常用的页可以被交换出去,腾给系统。
  • 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。

3.5 什么是分页?

把内存空间划分为大小相等且固定的块,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。

访问分页系统中内存数据需要两次的内存访问 (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。

3.6 什么是分段?

分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。

分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。

3.7 分页分段的区别是什么?

  • 属性:页是信息的物理单位,对用户不可见,段是逻辑单位,用户可见。
  • 大小:分页固定,分段不固定
  • 决定权:分页在于系统,分段在于用户
  • 目的:分页有利于资源的利用,分段方便用户管理内存

3.8 有哪些页面置换算法?

缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

有时候操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。

  • 先进先出FIFO:总是选择在主存中停留时间最长(即最老)的一页置换
  • LRU:选择在最近一段时间里最久没有使用过的页面予以置换
  • LFU(least frequent):统计页的使用频率,选择在最近时期使用最少的页面作为淘汰页

4. 系统中断

4.1 中断的处理过程

  1. 保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈
  2. 开中断:以便执行中断时能响应较高级别的中断请求。
  3. 中断处理
  4. 关中断:保证恢复现场时不被新中断打扰
  5. 恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。

4.2 中断和轮询有什么区别?

轮询:CPU对特定设备轮流询问。中断:通过特定事件提醒CPU。

轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。