操作系统(一)进程与线程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xxzxxzdlut/article/details/72229028

进程

1.程序与进程的区别

-程序:
作为静态文件存储在计算机系统的硬盘等存储空间中
-进程:
处于动态条件下由操作系统维护的系统资源管理实体
1. 能更好地描述并发,程序不能
2. 由程序和数据两方面组成,是竞争计算机系统有限资源的基本单位,也是进程处理机调度的基本单位
3. 程序是静态的概念,进程是程序在处理机上一次执行的过程,是动态的
4. 进程有生存周期,是短暂的,程序是长久的
5. 一个程序可以作为多个进程的运行程序,一个进程也可以运行多个程序

程序相当于做蛋糕的食谱,而进程相当于厨师阅读食谱制作蛋糕的一系列动作总合

一个程序运行两遍,算作两个进程
守护进程——停留在后台处理活动,与特定的用户没有关系,再有请求到来的时候被唤醒

2.创建新的进程

  1. 创建时机:
    在所从事的工作可以很容易的划分成若干相关的但没有相互作用的进程的时候,创建新的进程特别有效果

  2. 创建的系统调用:
    unix中: fork创建子进程(副本)——>exec子进程从其他程序初始化(开始干其他的事情)。
    fork之后原有的进程与子进程分开了,两个地址空间,但是确实可以共享其创建者的资源,此时执行写时复制,此时子进程是父进程的精确副本(完全一样)。fork调用返回子进程的进程标识符—-相当于指向子进程的指针,在子进程中此值为0(因为子进程没有他的子进程),使用这个PID可以区分两个进程。

    如果父进程正在执行多线程,在编译时编译器会查看子进程是否使用了exec系统调用,如果没有,子进程将拥有父进程的全部线程,如果使用了则只拥有调用了fork的线程

exec系统调用会使得进程的核心映像被一个文件替换,此时子进程与父进程完全不一样了。

3.进程的状态:

  1. 运行态(进程实际占用cpu)
  2. 就绪态(条件全部具备,因为其他进程在运行而暂时停止)
  3. 阻塞态/等待态(因为缺少运行所需条件而停止,外部条件不满足不运行,如等待I/O)

操作系统最底层就是调度程序——理想的模型

4.进程的实现:

  1. 为了实现进程模型,系统维护着一种表格——进程表。每个进程的进程控制块PCB占用一个进程表表项,包括了进程状态的重要信息。
  2. 操纵系统通过中断的方式切换进程,操作系统是由中断驱动的。

中断的步骤:

  1. 硬件压入堆栈程序计数器等,对当前进程来说,通常是在进程表项中。
  2. 硬件从中断向量装入新的程序计数器。
  3. 汇编语言过程保存寄存器的值。
  4. 汇编语言过程设置新的堆栈。
  5. C中断服务例程运行(典型的读和缓冲输入)
  6. 调度程序决定下一个将运行的进程
  7. C过程返回至汇编代码
  8. 汇编语言过程开始运行新的当前进程

线程:

使用多线程的原因:

  1. 将程序分解,是程序设计模型简单,多个并行实体共享同一个地址空间和所有可用数据(例如文字处理的时候,多个线程对文件进行交互,格式处理,备份)。
  2. 比进程更加轻量级,更容易创建和撤销。
  3. 如果存在大量的计算和大量的I/O处理,允许重叠进行会加快速度,提高性能。
  4. 共享资源

线程是资源调度的基本单位,进程是资源分配的基本单位,进程用于把资源集中到一起,而线程是CPU上被调度的实体。

在同一个进程中运行多个线程,是对在一台计算机上运行多个进程的模拟。

每个线程都可以访问进程地址空间的每一个内存地址,线程之间不设保护,不可能也没必要。

线程和进程一样拥有运行,就绪,阻塞,终止状态。

每个线程拥有自己的栈,因为通常每个线程调用不同的过程,有不同的执行历史
共享:堆,全局变量,静态变量,文件
私有:栈,寄存器

实现线程的方式:

  1. 将线程包放在用户空间中(快),内核对线程一无所知,从内核角度,每个进程为单线程。每个进程需要专用的线程表,和进程表类似。
    优点:
    1. 可以在不支持线程的操作系统上运行。
    2. 切换速度快,不需要陷入内核,上下文转换。
    3. 允许每个进程拥有自己的调度算法。
    问题:
    1.如何实现阻塞系统调用。一个线程使用阻塞系统调用会阻塞全部线程。解决方法:
    1) 使用非阻塞的系统调用,需要修改操作系统。
    2)使用包装器(在系统调用周围进行检查),如果某个调用会阻塞,通知。需要重写部分系统调用库,不高效不优雅。
    2.页面故障问题,由于系统不知道是多线程,当一个线程引起页面故障的时候,系统会阻塞整个进程,尽管其他线程是可用的。
    3.在单独的进程中没有时钟中断,一个线程可能永久运行,解决方法:向运行时系统申请每秒一次的时钟中断,生硬,无序。

2.将线程包放在内核中,在内核中有记录系统中所有线程的线程表,线程创建或者撤销的时候,通过系统调用来更新线程表。
当一个线程阻塞的时候,系统可以选择所有进程中的任一就绪线程执行。而用户级线程只有在进程被剥夺CPU时才能选择其他进程中的线程。
缺点:系统调用开销太大,特别是在线程的操作(创建,撤销)多的时候。

3.混合实现,使用内核级线程,然后将用户级线程与某些或者全部内核线程进行多路复用。

  1. 多对一:许多用户级线程映射到同一个内核线程。效率高,但一个线程执行了阻塞系统调用,整个进程将会阻塞。线程管理由线程库在用户空间管理。内核一次只能调用一个线程,没有提高并行性。
  2. 一对一:并行性好,缺点开销大,每一个用户线程就要建立一个内核进程。限制线程数。
  3. 多对多:多路复用。不限制线程数,并行性好。

内核只识别内核级线程,而内核级线程操作用户级线程集合。

调度程序激活机制
保持内核级线程优良特性的前提下改进速度。线程包位于用户空间。
内核给每个进程分配一定数量的虚拟处理器,通常为一,进程可以自己申请,也可以在用完之后返回。运行时系统将线程部署到虚拟处理器上。
当内核知道一个线程被阻塞之后,内核通知进程运行时系统,并在堆栈以参数形式传递有问题的线程编号和所发生事件的一个描述。之后在一个已知地址启动运行时系统,使其重新调度线程。
这个过程为upcall
运行时是如何给自己的线程分配cpu的。如何调度的,这个是通过上行调用(upcall)来完成的。在cpu中用户空间为上层,内核为下层,常规调用应该是上层调用下层,下层不应该调用上层,upcall就是指内核调用用户空间。

弹出式线程:分布式系统中消息到来的时候创建新的线程对其进行处理。没有历史,没有寄存器堆栈等内容,所以速度很快。

进程间通信

两种基本模式:

  1. 共享内存(速度快)
  2. 消息传递

竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程的精确时序,这种情况称为竞争条件。

互斥:确保一个进程正在使用一个共享变量或者文件的时候,其他进程不能做同样的操作。

同步:保证某种时间的顺序发生或不发生

临界区:对共享内存进行访问的程序片段。

临界区问题的要求:

  1. *互斥
  2. *前进,有空让进,多种选一,不能被临界区之外的进程阻塞
  3. *有限等待
  4. 不对cpu的速度和数量有任何假设。

实现互斥的解决方案:

隐藏的前提:多个进程需要访问一些共享内存
解决方案:
1。某些共享数据结构,如信号量,可以放在内核中,只能由系统调用访问
2。让进程之间共享部分地址空间

  1. 屏蔽中断,在每个进程进入临界区之后屏蔽中断,防止进程切换。
    问题:把屏蔽中断的的权利交给用户不安全,也许中断不再打开,系统死机。

  2. 严格轮换法:使用循环测试一个公共变量的值,该值唯一,不符合条件的进程连续循环——忙等待。使用忙等待的锁称之为自旋锁

    忙等待的问题:优先级翻转,低优先级的进程处于临界区中,高优先级的进程处于就绪,当高优先级的进程可以运行时开始忙等待,而低优先级的进程此时无法离开临界区。高优先级进程将永远忙等待下去。

    问题:一个进程比另一个进程速度慢了很多的情况下,会导致快的进程空循环等待,违背了前进的原则。

  3. Peterson解法:
#define FALSE 0
#define TRUE 1
#define N 0

int turn;//全局变量,表示现在轮到谁
int interested[N];//所有值初始化为FALSE

void enter_region(int progress)
{
    int other;//代表其他进程
    other=1-progress;
    interested[progress]=TRUE;//表明当前进程想进入临界区
    turn=progress;//只有最后修改的进程能使turn等于其进程号
    while(turn==other&&interested[other]==TRUE);//如果turn不等于自己进程号
                                                //或者另外一个进程不想进入则当前进程进入临界区

}
临界区;
void leave_region(int progress)
{
    intrested[progress]=FALSE;//离开临界区,声明自己不想进入了。
}

问题:忙等待

4.TSL指令:测试并加锁指令,需要硬件支持。执行该指令会锁住内存总线,以至于只有执行该指令的进程可以对共享内存做出修改。
XCHG指令:原子性的交换两个位置的内容,用来设置锁。
两个指令都能确保只有一个cpu对共享内存进行操作。
问题:忙等待

解决方法:使用通信原语:sleep,wakeup,无法进入临界区的时候将会阻塞而不是忙等待。
原语:用机器语言书写,在系统态下运行,执行中不可中断。

信号量

通信原语使用时一般会用到if语句,当系统在这里切换进程的时候,会导致wakeup信号丢失,以至于消费者一直睡眠,生产者将缓冲区填满之后也进入睡眠。
信号量是设置一个整型变量来累计唤醒次数。对信号量有两种操作:down(P)和up(V)(一般化后的sleep和wankeup)。

down:检查信号量的值是否为零,若不是则减一,若为零则睡眠(阻塞)。

up:对信号量的值加一,如果有多个进程在一个信号量上睡眠,则系统随机选择一个使其解除睡眠状态,此时信号量的值仍为零,但是在其上睡眠的进程数减少。

down和up都是原子操作:一组相关联的操作要么都不间断地执行,要么都不执行。通过屏蔽全部中断来实现,因为只有几条指令,不会出现问题。

互斥量

如果不需要信号量的计数功能,可以用信号量的简化版本——互斥量。
互斥量只需要用一个二进制数表示,0表示解锁,1表示枷锁。使用TSL或者XCHG指令实现

用法

同步信号量分开写在两个进程中
互斥信号量写在一个进程中
同时出现的时候:先同步再互斥
先P后V

pthread中的互斥,条件信号量的使用

Linux中多线程编程,互斥量和条件变量的使用

提取码:zoav
条件变量不会存在内存中,如果一个信号量传递给一个没有线程在等待的条件变量,信号将会丢失。wait必须在signal之前

管程

**一个管程是一个由过程,变量以及数据结构组成的一个集合。进程可以在任何需要的时候调用管程中的过程,但他们不能在管程之外定义的过程中调用管程中的数据结构。**

*是一种语言概念,c语言不支持,java支持,只要将synchronized加入到方法申明中即可

重要特性:任意时刻管程中只能有一个活跃进程

管程是编程语言的一部分,由编译器来负责进入管程的互斥。编程人员无需知道编译器是怎样实现互斥的。一般是使用一个互斥量或二元信号量。
以及使用条件变量来使进程在无法运行的时候被阻塞。

消息传递

上面的所有方法都是设计用来解决访问公共内存的一个或多个cpu上的互斥问题的,如果情况变为一个分布式系统具有多个cpu,而且每个cpu拥有自己的私有内存,他们之间通过局域网链接,那么这些原语都将失效。
1,信号量太低级了。2,管程只能在少数几种编程语言中可以使用。并且这些原语都没有提供机器间信息交换的方法。

所以我们需要消息传递:使用两条原语:send,receive

问题:
  1. 不可靠的消息传递的成功通信
  2. 进程命名的二义性问题
  3. 身份认证问题
  4. 性能问题
对消息进行编址
  1. 为每个进程分配一个唯一的地址,让消息按进程的地址编址
  2. 引入新的数据结构——信箱。
    对于信箱的使用:
    1。信箱创建时确定消息的数量。使用时send和receive调用的是信箱的地址。对满的信箱发消息的进程将会被挂起。
    2。彻底取消缓冲。如果send在receive之前执行,则发送进程被阻塞,直到receive执行,消息直接从发送者复制到接受者,不用任何缓冲。
    反过来也是一样。实现容易,但灵活性不行。因为发送者和接受者必须一步步紧紧相接。

屏障

用于进程组。在应用中划分了阶段,规定除非所有进程都就绪准备着手下一阶段,否则任何进程都不能进入下一阶段。可以通过在每个阶段的结尾设置屏障来实现。

调度

长期调度:进程被提交到硬盘的缓冲池中,由长期调度程序选择调入内存。
短期调度(cpu调度):从内存中准备执行的进程中选择执行。
中期调度(分时系统中):将进程从内存中移出,减少多道程序的道数。称为交换

进程行为

  • 使用cpu——计算
  • 一个进程等待外部设备完成工作而阻塞——I/O活动

进程分类:

  • 计算密集型——将大部分时间放在计算上。
  • I/O密集型——将大部分时间放在等待I/O上

    随着cpu越来越快,更多的进程倾向为I/O密集型,其结果为I/O密集型的进程调度处理更为重要。以便发出磁盘请求并保持磁盘运转。

调度时机

  1. 进程创建后,父子进程的执行顺序
  2. 进程退出时,选择一个进程运行
  3. 进程阻塞的时候。
  4. I/O中断的时候,是否让等待该I/O的进程执行

可以利用时钟中断来做出调度决策,根据如何处理时钟中断可分为非抢占式算法(忽略时钟中断,运行直到进程放弃cpu)抢占式调度算法(固定时段挂起进程)

####重要评价标准

  1. 周转时间=等待时间+执行时间
  2. 带权周转时间=周转时间/执行时间

    调度算法的目标

    所有系统:

    • 公平——给每个进程公平的CPU份额
    • 策略强制执行——调度程序拥有强制执行权利
    • 平衡——保持系统的所有部分忙碌,CPU密集型+I/O密集型

批处理系统:

  • 吞吐量——每小时最大作业数
  • 周转时间——从提交时刻到完成时刻的统计平均时间
  • CPU利用率——保持CPU始终忙碌

交互式系统

  1. 响应时间——快速响应要求
  2. 均衡性——满足用户的期望,调整调度顺序

实时系统
3. 满足截止时间——避免丢失数据
4. 可预测性——在多媒体系统中避免品质降低

调度算法

用于批处理系统中:

FCFS(先来先服务):

按照请求CPU的顺序使用CPU。
缺点:1.周转时间与响应时间无法保证 2。对短作业不利

SJF(最短作业优先)不可抢占:

要求运行时间可以预知,最短的作业放在最前。只有在所有进程可同时运行的时候才是最优化的。平均等待时间最短,平均周转时间最短。

最短剩余时间优先,可抢占:

最短作业优先的可抢占版本。
缺点:1。对长作业不利

最高响应比(HRN)

先来先服务和最短作业优先的综合平衡。
R=(W+T)/T W:等待时间 T:执行时间

交互式系统中:

轮转调度:

每个进程被分配一个时间段,称之为时间片。时间片结束的时候,剥夺CPU分配给其他进程。如果时间片结束之前进程阻塞或结束,立刻切换进程。
实现方式:维护一张可运行进程的列表。
时间片太短会导致太多进程切换,降低了CPU的效率;太长则可能造成对短交互请求的响应时间太长。20~50ms通常是比较合适的折中。
平均周转时间长。

优先级调度

每个进程被分配不同的优先级,优先级高的先运行。
为了防止高优先级的进程一直运行,在每一个时钟中断的时候降低优先级,如果比次高优先级进程低,则切换。不及时调整老化将会出现饥饿
对于I/O密集型的进程,为了让它获得高优先级,以保持I/O设备活跃,将其优先级设置为上一个时间片使用CPU时间所占时间片比例的倒数,使用CPU时间越短,优先级越高。

多级队列:

为CPU密集型的进程分配较长的时间片比频繁分配较短的时间片效率要高。所以设立优先级类,优先级越低,每次分配的时间片个数就多。(I/O密集型优先级别高)
允许改变优先级:多级反馈队列调度

最短进程优先

等待命令,执行命令,把每一条指令的执行看做是一个独立“”作业“”,运行最短作业来使响应时间最短。
实现方式:老化—>通过当前测量值和先前估计值进行加权平均而得到下一个估计值。
T0->1/2T0+1/2T1->1/4T0+1/4T1+1/2T2…………

保证调度

向用户作出明确的保证,然后实现。将进程自创建以来的时间除以进程数。计算出真正获得的CPU时间和应得的CPU时间之比。如0.5说明只获得了应有的时间的一半。

彩票调度

向进程提供系统资源的彩票,需要做出决策的时候,就随机抽出一张彩票,拥有该彩票的进程获得资源。按照进程的重要程度给予进程不同的彩票数量。协作进程之间彩票可以交换。

公平分享调度

按照用户数量平均分配资源。

用于实时系统中

在实时系统中,迟到的正确应答比没有应答更加糟糕
。。。。

策略与机制

机制:要干什么
策略:如何去干
将调度算法以某种形式参数化,参数可以由用户进程填写。

线程调度

用户级线程

内核不知道有线程的存在,内核给予进程时间片,进程中的线程调度程序选取线程运行,但是因为多道线程中没有时间中断,该线程会一直执行直到结束。但是该线程的不合群行为不会影响到其他的进程

内核级线程

内核选择一个线程运行,且不关心它是属于哪一个进程。

区别

二者区别在于性能,用户级的线程切换需要少量的机器指令,内核级线程需要完整的上下文切换,但是一旦线程阻塞在I/O上时,不需要像用户级线程那样挂起整个进程。
用户级线程能使用专为应用程序定制的线程调度程序,能将并行度最大化。而内核从来不了解每个线程的作用。

猜你喜欢

转载自blog.csdn.net/xxzxxzdlut/article/details/72229028