操作系统2:进程

进程

image-20200201111513234.png
CPU受到IO影响,影响了使用率

但如果是多任务的话:

image-20200201111722757.png
即一个程序执行IO操作的时候,另外一个使用CPU,从而实现CPU100%的利用率

image-20200201112420353.png
image-20200201112528224.png
进程包括1.正在执行的程序 2.处理的数据 3.现在的状态

这三个维度上只要有一个维度不一样,就是不同的进程

进程在计算机中以进程映像的形式存在

image-20200201113058002.png
全局量和静态量放在公共数据段

image-20200202091013419.png

进程控制块(PCB)

image-20200202091453187.png
PCB几乎包含所有和进程相关的信息

cpu schedule:cpu调度

Accounting information:记账信息,记录进程的资源使用情况

I/O status information:io状态信息

这些信息加起来至少有好几百个字节,非常庞大,所以这种数据结构是很复杂的。每个进程都要有一个PCB,他们放置在内存中的结果是十分庞大的

为了解决这个问题,有操作系统会将PCB分为两块,频繁使用的部分放在内存中,剩下的放在外存中。但是显然,这是影响效率的

现在的办法是尽量少地在PCB中放置信息,例如进程共享的信息不要放到PCB中

image-20200202093921940.png
process number 进程号:进程唯一

memory limits:使用内存的上下限的值

lists of open files:已经打开的文件的列表

program counter:程序计数器,也就是PC,记录程序执行到了哪一步

PCB源码分析

image-20200202111411367.png
用struct定义了PCB,光定义就有100多行

image-20200202111709569.png
对成员变量的访问有些时候是直接使用偏移量进行的,为的是加快访问速度,这就要求成员变量的位置不能变化,要严格按照最初的定义

进程状态字:

image-20200202111901541.png
tash_running:就绪状态

等待状态分为两个,task_uninterruptible的进程只能在资源有效的时候将其唤醒,不允许被通信信号(signal)等唤醒

task_stopped:进程暂停,可以用于调试,来看程序执行到某一阶段时候的结果

这里说的进程状态只能描述一部分,实际情况下还有比这更加复杂的情况,例如进程正在创建、将代码从外存读入内存这样的过程,无法用上面的状态来描述,实际的操作系统都要处理这样的问题,进程标志可以部分解决:

进程标志:

可以看做是进程状态字的细化描述,用来描述一些过渡信息

image-20200202113030629.png
image-20200202113253175.png
一个标志

image-20200202113435585.png
可以执行其他unix操作系统生成的程序

image-20200202164229559.png
289行的这个符号如果置为1,那么在从内核态切换回用户态的时候就会做一次系统调度

290行是对进程进行trace

image-20200202164516808.png
linux的内核没有独立的存储空间,而是借用了用户进程空间的1/4作为存储空间,所以不同的进程都可以通过系统调用进入同样的内核态,这样做的缺点就是所有的进程都可以访问到内核空间的地址范围,所以可能发生多个进程同时进入内核态的情况(这特别是在早期的Linux中是不允许的)。为了解决这个问题,就要加内核锁

image-20200202164957278.png
counter起到了进程优先级的作用

image-20200202165218136.png
进程的优先级在进程被创建的时候由系统确定,但是long nice表示了进程自身对于自身优先权的调整,可以在一定范围内表达对自己优先级进行调整的期望(但是是否执行由操作系统确定)

image-20200202165615541.png
确定该进程的调度策略。调度策略如果选择不当,效率就会受影响

image-20200202170024690.png
struct mm_struct 指向内存管理的所有信息,指针结构如下:

image-20200202170143746.png
image-20200202170329244.png
表示进程是否占用CPU、占用的是哪个CPU

image-20200202170429215.png
就绪队列由双向链表存储,存储的是PCB

所有的进程都保存在312行的双向链表中。早期Linux使用的是数组来保存这样的进程,但是缺点是数组的大小是有上限的

image-20200202170922837.png
DOS的可执行文件有两种,一种是.com,一种是.exe,.com中的内存位置都是写好的,所以程序必须装入预定的内存位置;.exe文件就没有这种要求了,它的变量地址是在操作系统装载程序进内存的时候才确定的,而程序中有哪些变量是需要编译程序和操作系统相互约定的。在Windows平台,这种约定好的格式是exe,而在Linux,则有.out、script等多种,所以才会有这一位来表示可执行文件的格式

image-20200202171545022.png
进程组:由一组在某些方面的一致的进程组成

image-20200202171657117.png

  • 原始父进程
  • 父进程
  • 子进程
  • 新、老兄弟进程

image-20200202171842032.png

进程创建

image-20200202172039814.png
创建子进程的一种常用做饭是复制一份父进程的资源调用给子进程:

image-20200202213824956.png
也有一种方式,是创建空的子进程,没有父进程的资源,只创建了一个PID

image-20200202214307872.png
上图这种就是,父进程创建子进程后,先让子进程执行,父进程等待至子进程执行完再执行。当然也可以是父进程和子进程都处于等待状态抢CPU的时间片,这就是另外一种策略了

进程创建实例

unix中通过fork创建子进程:

image-20200202214636789.png
fork()创建子进程,获取pid.这里要注意,子进程中对于父进程的代码也是完全拷贝的,唯一的区别就是因为子进程同时继承了父进程的寄存器的值,特别是PC,所以在fork之前的语句子进程并不会再执行一次,而只会从fork执行。fork()函数在子程序中的返回值是0,程序靠此区分父程序和子程序应该执行的不同代码。至于fork()为什么能在父进程中返回子进程的PID,而在子进程中只返回0:

来源:http://www.voidcn.com/article/p-hzkbtzqv-bgd.html

fork()是linux下创建进程的函数,这里通过linux 0.11分析下fork()创建进程后,子进程是如何返回的,但并不打算分析完整的fork()。

  fork()是1个系统调用(int 0x80),主要由find_empty_process和copy_process两个内核函数组成。

  当调用fork()时(int 0x80),cpu会自动将调用fork()时的代码段cs和fork()指令的下一句指令地址eip压栈,在执行copy_process时,将此cs,eip作为copy_process的部分参数,而copy_process内将复制父进程的TSS给子进程,也即将cs,eip做为子进程的入口地址。

  但是copy_process并不是将父进程的TSS完全复制给子进程,而是将子进程TSS段内的eax字段设置为0,而eax正是函数fork()的返回值。

  copy_process中最后将子进程设置为就绪态(TASK_RUNNING),父进程就从fork()中返回。

  NOTE!子进程实际上并没有执行fork() ,fork()只不过是创建了一个新进程,而它被调度执行时候的eax,cs,eip早已被精心设计过了。

也就是说,PC在子程序初始化的时候指向的下一条语句是fork的return语句,而eax寄存器是被置零的,所以返回值是0

进程树:

image-20200202221321301.png

进程终止

image-20200202221354831.png
注意子进程在终止时会传递数据给父进程,所以说子进程和父进程也不是真正意义上的独立

进程合作

image-20200202221639066.png
进程间通信方法:

  • signal
  • 管道
  • 共享内存
  • 消息传递

案例

image-20200202224944714.png
生产者和消费者是独立的、没有信息交流的,生产和消费只由缓冲区的空或者满影响

image-20200202225107581.png
image-20200202225139881.png
image-20200202225333939.png
其实也不用特别纠结上面的实现,毕竟有了In\out\SIZE三个变量,怎么都好实现,上面的实现,特别是生产者判断满的实现挺别扭的

进程间通信

image-20200202231501187.png
比较好的实现是消息系统

image-20200202231748821.png

进程调度

将各种状态的进程的PCB拉成不同的队列

进程调度有轮转法、先进先出算法、基于优先权的轮转法等算法

image-20200202094836973.png
就绪状态:等待CPU时间片。等待状态的进程获取资源后进入就绪状态

如果我们使用的是单核CPU,同一时间内处于执行状态的进程最多只有一个

而对于处于等待状态的进程却又不一样:虽然都是处于等待状态,但是等待的资源是不一样的,所以都拉在一条等待队列上是不合适的,所以合理的设计是按等待的资源进行排队,有多少种、每种内有多少个资源就有多少个队列

image-20200202094742965.png
job queue:作业队列,还没有装入到内存的、待执行的程序,它因为还没有装入到内存,还不是进程,所以不属于上面进程管理的内存,而是作业管理的内容

image-20200202100244218.png
在cpu执行的过程中请求资源,进入等待状态,得到资源后进入就绪状态、等待下一次执行

time slice expired:时间片到期,直接进入就绪状态

fork a child:创建一个子进程,先让子进程执行,等待子进程结束

上下文切换

image-20200202103340977.png
由于CPU内的寄存器速度比内存要快很多,所以程序都希望把数据放到寄存器中,例如C编译器优化时也是如此,而一个问题是寄存器数量有限,所以只能把使用频繁的变量放入寄存器;同样的,切换进程的时候很可能不得不需要把寄存器中的内存进行保存和切换

image-20200202103758956.png
切换进程时,将寄存器中的数据保存到内存中的PCB中,放入PCB中的寄存器部分中:

image-20200202093921940

线程

概述

Why 线程?

  • 父进程和子进程资源有很多重叠
  • 父进程和子进程上下文切换时有很多额外开销
  • 子进程占用资源很大

划重点:

  • 进程是一个能独立运行的基本单位,同时也是系统分配资源基本单位。
  • 线程是CPU调度的基本单位
  • 属于同一进程的线程,堆是共享的,栈是私有的。
  • 属于同一进程的所有线程都具有相同的地址空间。

对于UNIX,其在70年代就已经得到应用,而线程这一概念此时尚未出现,这就导致其内核必须大范围重写,windows由于开发较晚,早期就支持了线程,影响不大。一些数据库软件在操作系统还没有开发出实现了线程的版本之前在用户态内自己的数据库软件中实现了整套的线程,是为用户级线程(User Level Threads).最著名的就是Java threads(因为Java虚拟机就是在用户态的**。当然Java虚拟机在支持线程的操作系统上可以实现系统的线程管理,但是如果不支持,它也是有自己的一套用户态的办法的

Kernel level threads,内核级线程,例如windows从2000开始就支持线程了。

进程和线程的区别及其应用场景

来源:https://blog.csdn.net/weixin_39731083/article/details/82015830

一. 两者区别
进程是分配资源的基本单位;线程是系统调度和分派的基本单位。
属于同一进程的线程,堆是共享的,栈是私有的。
属于同一进程的所有线程都具有相同的地址空间。

多进程的优点:
①编程相对容易;通常不需要考虑锁和同步资源的问题。
②更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
③有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。
多线程的优点:
①创建速度快,方便高效的数据共享
共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术。
②较轻的上下文切换开销 - 不用切换地址空间,不用更改寄存器,不用刷新TLB。
③提供非均质的服务。如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。

二. 应用场景

  1. 多进程应用场景
    nginx主流的工作模式是多进程模式(也支持多线程模型)
    几乎所有的web server服务器服务都有多进程的,至少有一个守护进程(守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束)配合一个worker进程,例如apached,httpd等等以d结尾的进程包括init.d本身就是0级总进程,所有你认知的进程都是它的子进程;
    chrome浏览器也是多进程方式。 (原因:①可能存在一些网页不符合编程规范,容易崩溃,采用多进程一个网页崩溃不会影响其他网页;而采用多线程会。②网页之间互相隔离,保证安全,不必担心某个网页中的恶意代码会取得存放在其他网页中的敏感信息。)
    redis也可以归类到“多进程单线程”模型(平时工作是单个进程,涉及到耗时操作如持久化或aof重写时会用到多个进程)

  2. 多线程应用场景
    线程间有数据共享,并且数据是需要修改的(不同任务间需要大量共享数据或频繁通信时)。
    提供非均质的服务(有优先级任务处理)事件响应有优先级。
    单任务并行计算,在非CPU Bound的场景下提高响应速度,降低时延。
    与人有IO交互的应用,良好的用户体验(键盘鼠标的输入,立刻响应):多线程去做,有部分结果就可以有响应,不需要等待所有结果出来
    案例:
    桌面软件,响应用户输入的是一个线程,后台程序处理是另外的线程;
    memcached

  3. 选什么?
    ①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
    这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的②需要进行大量计算的优先使用线程(CPU频繁切换)
    所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
    这种原则最常见的是图像处理、算法处理。

③强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

④可能要扩展到多机分布的用进程,多核分布的用线程

⑤都满足需求的情况下,用你最熟悉、最拿手的方式
至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。
————————————————
版权声明:本文为CSDN博主「SJLin96」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39731083/article/details/82015830

TCB(线程控制块)

来源:https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E6%8E%A7%E5%88%B6%E5%9D%97/3026392

img

线程控制块(Thread Control Block,TCB)是与进程的控制块(PCB)相似的子控制块,只是TCB中所保存的线程状态比PCB中保存少而已

多线程

image-20200203004229184.png
这里的对应关系指的是用户态下的线程和内核态下的线程的对应关系。

  • 一对一:一个TCB对应一个用户态中的线程,一般使用该模型

  • 一对多:应用的场景例如操作系统没有实现线程,没有TCB的情况下,在用户态中实现线程。这不仅要求用户态中要实现线程的资源分配,还使得线程的资源经过了两次分配,会造成效率上的损失。最大的问题是,如果线程出错,会导致整个进程出错挂起,影响所有线程

  • 多对多:用户态中的线程数和内核态中的TCB数量不一致。好处是因为有多个线程在内核态,争抢CPU时间片的能力加强了

    image-20200203110210139.png

  • two level:同时提供一对一和多对多模型供选择,也可以做一对多

    image-20200203110311498.png
    image-20200203110412097.png

线程的实现难点

image-20200203110523421.png
举几个来说

  1. image-20200203110615344.png
    如果某一线程调用exec()来执行其他程序、替换掉现在的代码段,那么会对所有线程造成影响,这是允许的吗,这也是一个问题

  2. image-20200203110822235.png
    线程撤销了,线程使用的资源是否要撤销,也是一个问题

  3. signal handling:接受到signal后是一个线程响应还是所有线程响应

  4. Thread pools:如果按照进程一样的方式,每次创建或者销毁一个进程就在内核态中创建和销毁一个TCB,会大大影响效率,所以需要使用进程池,免去频繁创建和销毁对象

猜你喜欢

转载自www.cnblogs.com/jiading/p/12289032.html