进程和线程的区别

1.进程是资源分配的基本单位。
所有与该进程有关的资源,如外部设备、缓冲区队列等,都被记录在PCB中,以表示该进程拥有这些资源。
同一进程的所有线程共享该进程的所有资源。
2。线程是分配处理机的基本单位。                          它与资源分配无关,即真正在处理机上运行的是线程。
3。线程是进程的子集。                                      一个线程只能属于一个进程,而一个进程可以有多个线程。
4。线程的执行过程中需要协作同步。                  不同进程的线程间要利用消息通信的方法实现同步。

Linux下的线程

Linux的内核级线程也称为系统级线程。
Linux的内核级线程和其他操作系统的内核实现不同,它可以同时支持内核级线程和用户级线程。
大多数操作系统单独定义描述线程的数据结构,采用独立的线程管理方式,提供专门的线程调度,这些都增加了内核和调度程序的复杂性。
而在Linux中,将线程定义为“执行上下文”,它实际只是进程的另外一个执行上下文而已,而进程采用同样的表示、管理、调度方式。

这样Linux内核并不需要区分线程和进程,只需要一个进程或线程数组,而且调度程序也只有进程的调度程序,内核的实现相对简单得多,而且节约系统用于管理方面得时间开销。

进程的几种状态

按照惯常五种模型状态,进程一共分为五种状态:

创建:指的是一个进程被创建完成之后的一个状态。进程由程序代码数据和进程控制块(PCB)组成。当操作系统发现了要创建新进程的事件,使用进程创建原语 Creat() 创建一个新的进程。进程的创建过程分为五个步骤。
         1 申请空白PCB:为新的进程分配一个PID,并向PCB集合中索取一个空白PCB。
         2分配进程需要的资源:主要是为新进程的程序、数据以及用户栈分配必要的内存空间。
        3   初始化PCB:①初始化标识信息;②初始化处理机状态信息;③初始化处理机控制信息。
         4   放入就绪队列:如果进程就绪队列能够容纳新进程,就将进程放入就绪队列。
就绪:进程具备运行条件,等待系统分配处理器以便运行。
执行:进程占有处理器正在运行。
阻塞:又称为等待(wait)、阻塞(blocked)、睡眠(sleep)态,通常有四类事件会导致进程挂起。进程要处于阻塞状态的时候,会先调用进程阻塞原语block把自己阻塞。
           1 请求系统服务:正在执行的进程请求系统级别的服务,但是不能立即满足。常见的比如打印机请求,当打印机被占用时申请,进程将会处于阻塞,只有打印机释放才会被唤醒。
           2 启动某种操作:当进程启动了某种操作,而且该操作完成之后才能继续进程。比如IO操作,通常进程要在IO之后才能继续正常运行。如果进程在IO的时候占用了大量的时间,那么进程在此                时会进入阻塞状态,等待操作完成之后再唤醒进程。
  3  新数据尚未到达:当进程需要等待外界提供的新数据才能继续工作,进程会处于阻塞状态。比如,具有处理顺序的两个进程,A进程和B进程共同处理数据,A进程处理数据的第一阶段,B进程处理数据的第二阶段,只有当A进程处理完数据之后,B进程才能进行处理。此时B进程在进行等待的时候只能处于阻塞状态,避免无故消耗过多的资源。
4 无新工作可做:有些进程每次处理自己的工作,当等待新的工作到来之前,会处于阻塞状态。
终止:结束一个进程。进程终止的原因通常有三种。
  1 正常终止:这种情况下,进程通常会在最后安排一条指令去通知OS本进程已经结束运行。
   2异常终止:出现故障或者错误导致的终止。比如:越界错误、保护错、特权指令错、IO故障等。
   3外界终止:进程应外界的请求而停止运行。比如:用户或操作系统干预、父进程请求、父进程终止。


存储管理方式

内存管理方式分为连续分配管理非连续分配管理。连续分配管理分为单一连续分配、固定分区分配、动态分区分配以及动态重定位分区分配四种;非连续分配管理又叫离散分配方式,如果离散分配的基本单位是页Page,则称为分页存储管理方式;如果离散分配的基本单位是段Segment,则称为分段存储管理方式。

连续分配管理方式允许一个用户程序分配一个连续的内存空间。

单一连续分配:多用于单用户单任务的操作系统中,是一种简单的存储管理方式。单一连续分配中,会将存储空间分配为系统区和用户区。系统区存放系统进程,用户区存放用户进程,这样的管理可以避免用户进程影响到系统进程。这种分配方式中,比如0-a的地址空间存放系统区,那么a+1-n的地址空间都存放用户区。
固定分区分配:是一种最简单的可运行多道程序的存储管理方式。固定分区分配首先要划分分区,之后进行内存分配

  内存分区分为分区大小相等和分区大小不等两种。分区大小相等的情况下,如果进程大小不相等容易造成内存浪费或者内存不够进程无法运行的问题,所以通常用于进程内存大小相等的情况中。分区大小不相等,就是根据常用的大小(较多的小分区,适量的中分区,少量的大分区)进行分区,这样就可以更好地利用内存空间,但是这样的方式需要维护一个分区使用表。
内存分配要维护一张分区使用表,通常按照进程大小进行排序。每次在进行内存分配的时候,要查看哪些分区能够容纳该进程。


动态分区分配:根据进程的需要,动态地分配内存空间。这样的分配方式涉及到分区中的数据结构、分区分配算法以及分区的分配和回收操作三个问题。

    1分区分配中的数据结构主要分为空闲分区表和空闲分区链两种。
空闲分区表维护空闲分区的序号、空闲分区的起始地址以及空闲分区的大小数据。空闲分区链相当于一个双向链表,维护空闲分区;为了方便检索在分区尾部设置一个分区的状态位以及分区大小。

   2  分区分配算法主要有五种方法。
首次适应算法(First Fit),利用空闲分区链实现,将空闲分区按照地址递增(注意与后面的最佳适应区分,这里按照的是地址递增)进行排序,然后根据进程的大小从链首查找空闲分区链,第一个能够适应的就分配。
循环首次适应算法(Next Fit),不是每次都从链首开始查找,而是从上一次找到的空闲分区的下一个空闲分区开始查找,直到查找到一个能够使用的分区,之后动态地分配内存(会导致后续大空闲分区变少)。
最佳适应算法(Best Fit),将空闲分区链按照大小进行排序,找到第一个适应的空闲分区即可(最大限度利用空闲分区)。
最坏适应算法(Worst Fit),与最佳适应相反,排序之后每次挑选最大的分区使用,对中小作业有利,不易产生碎片。
快速适应算法(Quick Fit),根据大小将空闲分区进行分类,维护多个空闲分区链;这样的好处是可以加快检索,相比一条链能够更快地检索目标进程。

3  分区分配和回收:主要操作是内存的分配和内存的回收。内存分配中,若空闲分区内存大小-用户进程内存大小<=预设不可切分内存大小,则进行一次内存的分配;否则将剩余的内存空间放到空闲内存链中,继续下次的使用。内存回收,主要看是否和空闲分区相邻,如果相邻就直接合并;否则就建立新的表项,维护回收区的内存起始地址和大小。


动态重定位分区分配:在连续分配方式中,必须把系统进程或者用户进程装入一个连续的内存空间中。这个时候可能会因为程序的大小与分区的大小不一致的问题产生内存碎片。这个时候我们要想插入新的进程,即使碎片空间总和支持进程,也无法再分配空间,所以我们要把内存空间进行一个整理。



整理内存地址,是将程序的内存地址整理成在物理上相邻的状态。
程序使用的地址在分区装载之后仍然是相对地址,要想将相对地址转换为相邻物理地址,必然会影响到程序的执行。
为了不影响程序的执行,需要在硬件上提供对程序的内存地址转换支持,于是引入重定位寄存器,用它来存放程序在内存中的起始地址。

非连续分配管理方式允许将一个程序分散地装入不相邻的内存分区。根据内存分区的大小分为分页式存储管理方式和分段式存储管理方式。

页存储:页存储首先需要将一个进程的逻辑内存空间划分为多个内存大小相等的页,然后将物理内存空间划分为相等的大小个数的物理块Block(页框Frame)。分配的时候将多个页面放入多个不相邻的物理块中。这样的划分之后,通常进程的最后一页存不满,会产生内存碎片——”页内碎片“。

     页面大小:页面大小的选定需要适当。
如果过小,虽然可以减少页内碎片的产生,但是需要更大的页表,而且页面切换更频繁;如果太大就会产生较大的内存碎片。
所以大小的选择应该适中,通常为2的整数次幂,范围为512B至8KB。

    页表:页表主要保存进程占用的页数,同时存储逻辑内存空间页到物理内存块的映射。


地址转换:页表要存储地址映射,那么首先得有一个地址变换机构。基本地址变换机构,主要用来建立逻辑地址到物理内存空间地址的映射。
传统系统中,主要使用寄存器来存放页表(寄存器速度快,有利于变换地址),那么每一个页表都需要寄存器来存放,成本过高。

进程数量过多的情况下,首先将页表的起始地址和页表长度存放在PCB中,之后在进程运行的时候再将数据读取到PTR(Page-Table Register,页表寄存器)。
读取数据的时候,地址转换机构会将相对地址转换为页号以及页内地址两部分,之后根据页号检索页表。
检索之前会将页号和页表长度进行比较,如果页号大于或等于页表长度,则说明出现了访问越界,会出现越界中断。


具有块表的地址转换机构:页表存放在内存中,那么将要先查页表,计算得出物理地址之后访问物理地址,是两次检索过程。

为了提高地址变换速度,可在地址变换机构中增设一个具有并行查寻能力的特殊高速 缓冲寄存器,又称为“联想寄存器”(Associative Memory),或称为“快表”。

对于32位操作系统,使用两级页表结构式合适的;
但是对于64位操作系统,建议采用多级页表。

段存储:使用段存储而不再使用页存储,第一个原因是提高内存的利用率,第二个原因是满足开发者在编程中的多种需求。主要是满足编程中的几个新的需求:方便编程(简化逻辑地址访问方式)、信息共享(段式信息的逻辑单位)、信息保护(对信息的逻辑单位实现保护)、动态增长(分段更加满足动态增长的需求)、动态链接(链接装载使用段更符合需求)。

分段:作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息。
段表:为每个分段分配一个连续的分区,进程每个段可以离散地存放到不同分区中,所以也需要维护一张段表,使得进程能够查到逻辑地址对应的物理地址,这个表就是段映射表,简称段表。
地址变换机构:为了实现逻辑地址到物理地址的映射,系统设置了一个段表寄存器,用于存放段表地址和段表长度,如果段号大于段表长度,则表示发生了越界访问,产生越界中断;若未越界,找出该段对应段表项的位置,读出内存中的起始地址;再检查段内地址是否超过段长,如果超过段长,发出越界中断;否则将基址与段内地址相加,得出内存的物理地址。
分段和分页的主要区别:①页是信息的物理单位,段是信息的逻辑单位;②页的大小由系统决定,段的大小不固定;③分页作业地址空间是一维的,分段作业地址空间则是二维的。即页的地址空间可以用一个符号来标识,而段的地址空间不仅要知道段名还要知道段内地址。


段页存储:分页存储能提高内存利用率,分段存储能满足用户的更多需求。为了各取所长,产生了段页存储的管理方式。

基本原理:段页存储,就是既分段也分页,先分段再分页。地址结构由段号、段内页号、页内地址三部分组成。
地址变换过程:首先也需要准备一个段表寄存器,存放段表起始地址和段表长。首先将段号和段表长相比较是否越界;再利用段表起始地址和段表号来找到段表项在段表中的位置,从而得到页表地址;再之后使用段内页号来获取页表项的位置,找出物理块号,用物理块号和页内地址来构成物理地址。

虚拟存储器

在之前介绍的几种存储管理方式中,它们要求将一个作业全部装入内存之后才能运行。这样的机制势必会产生某些问题。①如果有单个作业的内存很大,那么该作业将无法被全部装入内存中,导致作业无法被运行;②如果内存中已经存在很多作业,而这些作业已经占据了大量内存,那么新的作业将无法被装载到内存中,也无法被运行。

这样的状况下,无疑要增加内存来解决。但是直接增加物理内存显然会增加成本,另一种方式就是在逻辑上扩充内存,也就是使用虚拟内存。

虚拟存储器的引入:在虚拟存储器的引入之前,要考虑传统的存储器在处理作业的时候有无不必要的操作。
一次性和驻留性:传统存储器在执行作业之前,首先会将一个作业全部装载到内存中,一次性读入,这就是一次性;此外,作业被装载入内存中之后,会一直存在直到作业结束。
局部性原理:程序的执行呈现局部性原理,在一短时间内,程序的执行只局限于某个部分,相应访问的存储也局限于某个部分。根据局部性原理可以得出,一次性和驻留性耗费的大量内存是不必要的。
虚拟存储器:是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储器系统。虚拟存储器引入之后,OS会首先将程序可能执行的部分读取到内存中(预读性),如果程序执行的时候缺少了必要的内容(缺页或缺段),那么OS就会将程序请求的内容读取到内存中(请求调入)。如果此时内存不够,无法装载新的页,那么OS会将内存中暂时不用的部分替换成将要装载的部分(置换),满足新的程序的运行需求。


虚拟存储器的实现:虚拟存储器拥有请求调入和置换两大功能,那么在系统中是如何实现这一功能的?主要依赖于两个系统:分页请求系统和请求分段系统。
分页请求系统:在分页系统上增加了请求调入和置换功能。允许将程序的少数需要运行的页面装载进内存,之后将
请求分段系统:在分段系统的基础上增加了请求调段及分段置换功能形成的段式虚拟存储系统。
虚拟存储器的特征:多次性(作业被分成多次调入内存)、对换性(允许作业在运行的时候调入调出)、虚拟性(能从逻辑上扩充内存容量)
IPC通信方式
IPC,Inter-Process Communication,进程间通信。

全双工和半双工:全双工指的是在接收数据的时候也能够发送数据,两者同步进行。半双工同一时刻只能进行一个步骤。浅显的比喻,全双工好比是双向双车道,同一时刻允许两辆相向行驶的车辆通过;而半双工好比是单向单车道,有相向的两辆车行驶的时候只能等待其中一方先行,另一方才能通行。

无名管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
高级管道popen:将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
消息队列Message Queue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储Shared Storage:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号Signal:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
套接字Socket:套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

线程间通信方式

在Linux中,线程之间的通信与同步主要有下面的几种方式:

互斥锁(mutex):互斥锁有两种状态,主要为unlock和lock两种。线程在对共享数据段进行读写的时候,首先将该互斥锁设置为lock状态,之后在其他线程进行访问该数据段的时候,判断一下互斥锁的状态,如果没有上锁则可以进行操作,否则只能等待互斥锁释放。
条件变量:使用互斥锁可能会带来死锁情况。条件变量通常应该和互斥锁一起使用,互斥锁用在短期锁定,主要用来保证临界区的互斥进入;条件变量用于线程的长期等待,主要等待线程需要的资源变为可用资源、
信号量机制:信号量不仅可以用于进程之间的同步,也可以用于线程之间的同步,主要有两种方式:私用信号量和公用信号量。
私用信号量(private semaphore):主要是指该信号量是在进程中独有的,同一进程中的线程需要通信的时候,在进程空间中创建一个私用信号量,该信号量对于OS和其他进程来说是不可见的,所以私用。如果持有该私用信号量的线程意外关闭或者正常关闭却没有释放该私用信号量,会造成该私用信号量无法释放
公用信号量(public semaphore):公用信号量主要是实现不同进程间以及不同进程的线程之间的同步通信而设置的。因为拥有公开的名字以及存放在受保护的OS存储区中,又因为由系OS分配空间并进行管理,所有又叫做系统信号量(实现思想与分布式锁很相近,一样需要被所有的组件获取到,并且能够互斥访问)。因为是由系统管理的信号量,所以进程或者线程持有的时候关闭却没有释放系统信号量,也不会造成系统信号量无法释放的问题。
线程间通信主要跟线程的实现有关系,Java中的线程间通信方式主要有以下几种:

volatile

volatile主要实现变量的可见性。在多线程中的环境中,会设定有共享变量。根据JIT的优化,每一个工作线程都会有各自对应的工作内存,操作共享变量的时候会先将变量读取到工作内存中,等待线程结束之后再把变量读回到主内存中。这样的环境下,共享变量的修改就不能及时地被其他线程感知到,使用volatile关键字可以保证变量的修改能即时被其他线程感知。


等待/通知机制
主要是使用Object中提供的两个方法:wait()/notify()。等待/通知机制就是一个等待一个通知,线程A等待线程B,线程B在一定条件下通知线程A可以不再进行等待了。通常wait()需要放在一个while循环里面,因为wait是要满足一定条件的。这样的机制也可以理解为生产者/消费者模型。

join方式

这种方式通常用于主线程和子线程之间的通信。通常主线程如果要处理的任务比较快速,子线程要处理的任务时间非常长,那么主线程main往往在子线程之前结束。如果要让主线程在子线程完毕之后再结束并且获取子线程中的数据,就需要使用ziThread.join()方法。

Thread.sleep()不会释放锁,但会让出CPU资源;Thread.join()会释放锁,因为底层使用wait方法实现。

ThreadLocal

在Java中,如果想要所有线程使用一个共享变量,那么只需要将变量定义为一个public static类型即可。如果单个线程想为自己单独开辟一个独立的空间,并且创建自己独有的共享变量,那么可以使用ThreadLocal类来实现。ThreadLocal类可以类比为超市中的储物柜,每个人都可以存储自己的私人物品,虽然都放置在一起,但是不会出现错拿的情况,存取也比较方便。

虚拟地址、逻辑地址、线性地址、物理地址的区别

两个内存的区别:物理内存和虚拟内存

物理内存:指的是插在主板上的内存条,是一种计算机物理组件。
虚拟内存:在硬盘上划分一块页面文件充当内存,主要用来存放程序在运行时暂时不需要的部分。

四种地址的区别:虚拟地址、逻辑地址、线性地址、物理地址

虚拟地址:指由程序产生的由段选择符和段内偏移地址组成的地址。
逻辑地址:指由程序产生的段内偏移。有时候直接把逻辑地址当做虚拟地址。
线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。
物理地址:指CPU外部地址总线上寻址物理内存的地址信号,是地址变换的最终结果。

Linux常用命令

参考文档:史上最全的Linux常用命令汇总(超全面!超详细!)收藏这一篇就够了!_万里羊的博客-CSDN博客_linux常用命令归纳

LRU和LFU

LRU,Least Recently Used,最近最少使用。
  • LRU在实现的时候,可以使用Java中的LinkedHashMap这个集合。该集合在插入的时候是有序的,可以根据插入顺序实现遍历。在实现LRU的时候,可以将访问到的数据直接插入到集合的最后,这样就实现了LRU的基础功能了。
  • 该集合实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,方法默认是直接返回false,不会移除元素,所以需要重写该方法。即当缓存满后(判断集合长度和既定缓存区大小)就移除最不常用的数。
  • 具体实现中,就将新插入的元素和访问的元素插入到链表的最前端。判断长度是否超出既定设置的最大长度,如果超出了就移除链表尾部数据。

LFU,Least Frequently Used,最不经常使用。

  • LFU淘汰一定时期内被访问次数最少的元素。如果元素的一定时间内的访问次数相同时,则比较他们的最新一次的访问时间。LFU中淘汰的条件是一定时间内使用最少的元素,实现中需要维护一个数据使用的Map以及保存访问次数以及时间的Map。
  • 实现中的主要难点在于put方法,在put之前需要先判断是否存在这个key。如果存在,则将Count的Map次数+1并且更新访问时间的Map中的时间。如果不存在,需要判断当前长度是否超出既定的缓存值。超出了就要进行淘汰,先比较谁使用的最少,再比较谁上次被访问到的时间离现在最久(访问最少最久的被淘汰)。
  • 两者的区别在于:LRU表示的是最长时间没用到;LFU表示的是最少使用次数。
参考文档:LRU、LFU算法java实现_foye12的博客-CSDN博客_java lfu

用户态和内核态

为什么要划分用户态和内核态?

因为要对程序进行不同的权限访问的管理,防止一些程序能够获取其他程序的数据或者系统数据,并将其上传到网络之中。简单来说,就是进行权限管理,防止用户进程将系统数据或者用户隐私泄露,是OS安全性上的一种保障。

用户态和内核态
用户态:程序运行时只能访问受限的内存部分,且不允许访问外围设备,且占用CPU的能力可被剥夺
内核态:程序运行时能够访问内存中的所有部分,且能够访问外围所有设备,CPU可在程序之间进行切换


用户态和内核态的切换
用户进程在运行的时候,有时候会不可避免地需求用到内核态才能用到的一些资源。比如在运行的时候会请求IO设备,这个时候就要进行用户态到内核态的转换。

但是这个切换之后又不能够让用户态的程序访问到它所应该访问到的其他资源信息,就需要将自己需要访问的资源请求发送给OS来处理,这个过程就称为系统调用,在CPU的实现中被称为陷阱指令(Trap Instruction)。

请求过程:①程序将请求的数据参数放入到寄存器中,或者使用参数创建一个堆栈(stack Frame);②系统在寄存器或者堆栈中找到用户态程序请求的参数,将CPU切换为内核态并且跳转到内存指定位置的指令,并处理程序执行的请求;③系统调用结束之后,OS会重置CPU为用户态并且返回系统调用的结果。

线程私有元素

进程中的所有线程拥有共享变量,只需要将变量设置为全局变量即可。但是如果线程想要拥有自己的私有变量,就需要创建线程私有数据TSD(Thread-specific Data)来解决。线程私有元素对于线程内部的各个接口来说是可见的,但是对于其他线程来说是不可见的。

线程私有技术使用了一键多值的技术,一个key对应多个值。访问数据值的时候都是通过key来进行访问的。使用线程私有元素需要对每一个线程创建一个关联的key,Linux中主要由四个接口来实现:

  • pthread_key_create:创建一个Key,从TSD中分配一个赋值给key供以后的使用。Key创建之后是一种全局变量,各个线程需要往Key中填入不同的值,这就形成了一个同名不同值的全局变量,是一种一键多值的结构。
  • pthread_setspecific:为指定键值设置线程私有数据
  • pthread_getspecific:从指定键读取线程的私有数据
  • pthread_key_delete:删除一个键,线程数据的释放必须要在Key删除之前

僵尸、孤儿进程

进程在创建的时候,会出现父进程和子进程的问题。子进程正常情况下是由父进程创建的,子进程的结束和父进程的运行是一个异步过程,父进程无法得知子进程的结束时间。当子进程完成工作之后,父进程会调用wait和waitpid系统调用来取得子进程的终止状态。

举个类似的例子,当你工作干完想要下班,这个时候应该由你上级领导来给你批复,如果你工作完成了,你的上级领导才会对你下班做出批复(wait或者waitpid),同意你下班之后你就可以离开工作岗位了。

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

孤儿进程,就是你们整个部门中,部门领导都已经下班或者辞职了,此时你们整个部门处于无领导状态,而你们还需要进行工作(孤儿进程)。此时应该由一个专门负责无领导部门的人(init)来对你们部门进行暂时管理,并对你们的工作状态完成收集。

僵尸进程,与孤儿进程相反,部门领导把你们招进来,之后你们全体私下罢工但是部门领导出差了不知道,仍然在进行命令下达,部门领导也没有问其他部门领导你们的状态(wait或waitpid),那么在整个部门的信息中你们仍处于在职状态。此时部门领导就是个光杆司令,相当于僵尸进程。


孤儿进程和僵尸进程的危害

孤儿进程,还是上面的例子,如果你们领导下班或辞职,但是部门会马上交由专员管理(init),尽管此时领导并没有那么专业,但是实际上不会影响到整个公司的运作的,只需要由专员对你们逐一进行管理(下班批复或者辞职批复)即可。所以说孤儿进程实际上不会造成危害。
僵尸进程,部门领导招你们进来之后,你们因为某些原因辞职回家,部门领导却不知道也不对你们进行管理(批复),这样的行径就会给公司带来负面影响(比如你们散播工作体验以及直系领导的相关消息)。此时公司高层知道之后,解决问题的办法就是开除你们部门领导,否则这样的人存在会对公司造成负面影响。
参考文档:[孤儿进程与僵尸进程总结] - Rabbit_Dale - 博客园 (cnblogs.com)

死锁产生以及死锁避免、死锁解决

死锁的定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。

死锁检测需要用到资源分配图,可以直观地看到资源的分配情况以及是否造成死锁。

死锁产生的四个必要条件

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源,P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源
死锁解决两种方式:①剥夺资源;②撤销进程。

死锁产生之后是很难解决的,所以最好的方法应该是死锁避免,只要破坏死锁产生的四个必要条件中的任意一个,就可以避免死锁的产生。

银行家算法

Banker’s Algorithm,是一个死锁避免的算法。以银行借贷系统为基础,判断系统运行的安全性。主要实现依赖四种数据结构:

  1. 可利用资源向量Available,是一个含有m个元素的数组,每一个元素代表一种可利用的资源数目
  2. 最大需求矩阵Max,n*m的矩阵,表示系统中存在n个进程,每一个进程对于m类资源的最大需求
  3. 分配矩阵Allocation,n*m的矩阵,表示了系统给每个进程当前已经分配的资源数
  4. 需求矩阵Need,n*m的矩阵,表示了每一个进程还需要的各种资源数
如果存在一个由系统中所有进程构成的安全序列P1,…,Pn,则系统处于安全状态。安全状态一定是没有死锁发生。为了保证系统进程安全,系统在分配资源的时候应该遵循下面几点原则:

  1. 进程申请的资源最大需求量不超过系统现有可利用资源时就代表可以运行这个进程
  2. 进程申请的资源系统可以分批次调给进程,但是系统申请的资源不能超过最大需求量
  3. 系统现有资源不能满足程序申请的资源的时候,系统可以推迟分配,但是总能够使得进程在有限的时间内得到所需要的资源
  4. 当进程获取所有的资源之后,进程运行结束之后能够在一定时间内将资源归还给系统

进程调度算法

  1. 时间片轮转(RR):给每个进程设置一个时间片,每个进程依次执行一个时间片的时间,时间片用完就自动切换到下一个进程,不管该进程是否运行完。
  2. 响应比优先(HRRF):响应比的计算方法:(等待时间+运行时间)/(运行时间)= 响应比。这个响应比越大,就表明该进程可以越早地被执行完毕,就越先被执行。
  3. 先来先服务(FIFO):按照进程进入就绪队列的顺序执行进程,这样的调度弊端就是后来的很短的线程会一直得不到运行。整个调度算法就相当于日常排队,谁先来就先处理谁的业务,不管你的业务是长还是短,是紧急还是不紧急。
  4. 最短进程优先(SJF):按照进程的长短顺序执行,短的先执行,长的后执行。
  5. 最短剩余时间优先(SRTF):按照进程运行的剩余时间来排序,越短的剩余时间就越先执行,这样的
其中,SJF和SRTF属于非可抢占式进程调度,其余属于抢占式进程调度。

参考文档:五种进程调度的算法实现(一) - bajdcc - 博客园 (cnblogs.com)

线程控制方法

线程调度方法分为分时响应和抢占式两种。

线程在运行的过程中状态应该是可控的,在Java中,提供了四种方法来控制线程的状态。

  • wait():等待方法,属于Object类的方法。wait执行的时候会释放锁,只有执行了notify()方法之后才可以将线程唤醒。wait方法必须获得线程锁,所以在执行的时候是在synchronized代码块中的,所以相应的notify唤醒方法也应该写在同步代码块中。
synchronized (obj) {
    System.out.println("thread1 start");
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("thread1 end");
}



sleep():休眠方法,属于Thread类的方法,主要传参的参数是时间,单位默认是ms。在线程执行休眠的时候,不会释放锁,但是会阻塞线程,暂时将线程的交出去。一旦结束休眠时间,便又会获得执行权,所以在休眠的时间内其实是有保持监控状态的。sleep执行的时候可以让更低优先级的线程获得执行机会。

synchronized (obj) {
    System.out.println("thread1 start");
    try {
        obj.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("thread1 end");
}


yield():暂停方法,属于Thread类的方法,主要也是暂停线程,与sleep不一样的是它在运行的时候不会阻塞线程,而是让线程处于就绪状态。处于就绪状态的线程在进入可执行状态的时候会立马进入执行状态,所以yield方法不一样的是在执行的时候只会让更高优先级的线程获取执行机会。

synchronized (obj) {
    System.out.println("thread1 start");
    Thread.yield();
    System.out.println("thread1 end");
}



join():挂起方法,属于Thread类中的方法,主要作用是挂起线程,等待该线程结束之后才继续执行程序。通常用于要设定线程执行的前后顺序的场景中才会用到,比如后续的程序处理要等待这个线程运行完毕之后传来的数据才能执行的场景中。

try {
    t1.join();
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}