进程和线程的概念以及实现原理

进程的概念

进程就是一个运行起来的程序,存在的目的在于,可以让一个计算机运行多个程序,cup通过切换运行的进程来达到伪并行的目的。有了进程的概念就算CPU只有一个,操作系统也可以让程序达到并行的目的。
在这里插入图片描述
如上图所示:一个操作系统只有一个CPU,但是有四个进程(A,B,C,D),操作系统通过将多个进程交替放在CPU上执行,每个进程分到一段时间进行执行,就可以让用户感觉到程序是并行执行的。说白了在操作系统层次,进程就是运行起来程序的一个抽象

进程的创建

导致进程创建有以下几种情况:
1) 系统初始化,例如在系统内部初始化好了之后,会在最后创建一个shell进程。
2)执行了正在运行的进程所调用的进创建系统调用(fork)
3)用户请求创建一个新的进程
4)一个批处理作业的初始化

在这里呢,主要讲一下fork系统调用,因为其他的方式很好理解。
fork 这个系统调用会创建一个与调用进程相同的副本。两个进程拥有相同的存储映像,说白了就是和自己的父进程形式上一摸一样,但是呢,除了一部分的可以共享的东西除外,其他的子进程都独有。
fork的返回值: 子进程返回0,在父进程处返回子进程的PID。

进程终止

1)正常退出(自愿的)
2)错误退出(自愿的),比如用户输入错误
3)严重错误(非自愿的),比如内存和访问越界
4)被其他进程杀死(非自愿的),kill

进程三种状态

1)运行态
2)就绪态
3)阻塞态
在这里插入图片描述
运行态:此时进程正在CPU上面运行着。
就绪态:进程可运行,但是有获得 CPU 时间分片。
阻塞态: 与前两种状态不同这个进程现在不能运行,CPU 空闲时也不能运行。因为这个进程正在等待某个事情的发生。比如磁盘I/O,用户输入等等。
三种状态会涉及四种状态间的切换,在操作系统发现进程不能继续执行时会发生状态1的轮转,在某些系统中进程执行系统调用,例如 pause,来获取一个阻塞的状态。在其他系统中包括 UNIX,当进程从管道或特殊文件(例如终端)中读取没有可用的输入时,该进程会被自动终止。

转换 2 和转换 3 都是由进程调度程序(操作系统的一部分)引起的,进程本身不知道调度程序的存在。转换 2 的出现说明进程调度器认定当前进程已经运行了足够长的时间,是时候让其他进程运行 CPU 时间片了。当所有其他进程都运行过后,这时候该是让第一个进程重新获得 CPU 时间片的时候了,就会发生转换 3。

程序调度指的是,决定哪个进程优先被运行和运行多久,这是很重要的一点。已经设计出许多算法来尝试平衡系统整体效率与各个流程之间的竞争需求。

当进程等待的一个外部事件发生时(如从外部输入一些数据后),则发生转换 4。如果此时没有其他进程在运行,则立刻触发转换 3,该进程便开始运行,否则该进程会处于就绪阶段,等待 CPU 空闲后再轮到它运行。

进程在操作系统的实现

操作系统为了能调度进程的运行,在自己的内部为每个进程维护了一张表PCB表。内容包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。
在这里插入图片描述

线程

上面讲到了进程,程序在启动之后,只能干一件事情,比如说用户正在写一个文件,用户在第一个位置输入了一个字之后准备输入第二个,这时候程序要把字读进来,又要在显示屏调整原来所有字的位置,显然这样一个执行流根本忙不完,如果用多个进程那么会显的十分的麻烦。而线程的引入就可以解决这个问题,通俗一点来说,线程就是进程下的一个执行流。

线程的运用实例

现在考虑一个线程使用的例子:一个万维网服务器,客户机对页面的请求发送给服务器,服务器将所请求的页面发送回客户端。在多数 web 站点上,某些页面较其他页面相比有更多的访问。例如,索尼的主页比任何一个照相机详情介绍页面具有更多的访问,Web 服务器可以把获得大量访问的页面集合保存在内存中,避免到磁盘去调入这些页面,从而改善性能。这种页面的集合称为高速缓存(cache),高速缓存也应用在许多场合中,比如说 CPU 缓存。

在这里插入图片描述

上面是一个 web 服务器的组织方式,一个叫做分配线程(dispatcher thread) 的线程从网络中读入工作请求,在调度线程检查完请求后,它会选择一个空闲的(阻塞的)工作线程来处理请求,通常是将消息的指针写入到每个线程关联的特殊字中。然后调度线程会唤醒正在睡眠中的工作线程,把工作线程的状态从阻塞态变为就绪态。

工作线程启动后,它会检查请求是否在 web 页面的高速缓存中存在,这个高速缓存是所有线程都可以访问的。如果高速缓存不存在这个 web 页面的话,它会调用一个 read 操作从磁盘中获取页面并且阻塞线程直到磁盘操作完成。当线程阻塞在硬盘操作的期间,为了完成更多的工作,调度线程可能挑选另一个线程运行,也可能把另一个当前就绪的工作线程投入运行。

这种模型允许将服务器编写为顺序线程的集合,在分派线程的程序中包含一个死循环,该循环用来获得工作请求并且把请求派给工作线程。每个工作线程的代码包含一个从调度线程接收的请求,并且检查 web 高速缓存中是否存在所需页面,如果有,直接把该页面返回给客户,接着工作线程阻塞,等待一个新请求的到达。如果没有,工作线程就从磁盘调入该页面,将该页面返回给客户机,然后工作线程阻塞,等待一个新请求。

线程模型

理解进程的另一个角度是,用某种方法把相关的资源集中在一起。进程有存放程序正文和数据以及其他资源的地址空间。这些资源包括打开的文件、子进程、即将发生的定时器、信号处理程序、账号信息等。把这些信息放在进程中会比较容易管理。

另一个概念是,进程中拥有一个执行的线程,通常简写为 线程(thread)。线程会有程序计数器,用来记录接着要执行哪一条指令;线程还拥有寄存器,用来保存线程当前正在使用的变量;线程还会有堆栈,用来记录程序的执行路径。尽管线程必须在某个进程中执行,但是进程和线程完完全全是两个不同的概念,并且他们可以分开处理。进程用于把资源集中在一起,而线程则是 CPU 上调度执行的实体。

线程给进程模型增加了一项内容,即在同一个进程中,允许彼此之间有较大的独立性且互不干扰。在一个进程中并行运行多个线程类似于在一台计算机上运行多个进程。在多个线程中,各个线程共享同一地址空间和其他资源。在多个进程中,进程共享物理内存、磁盘、打印机和其他资源。因为线程会包含有一些进程的属性,所以线程被称为轻量的进程(lightweight processes)。多线程(multithreading)一词还用于描述在同一进程中多个线程的情况。

在这里插入图片描述
很容容易的理解每个线程都是进程的一个执行流,上图中a中每一个进程有一个线程(一个执行流),后面的一个进程有三个线程,三个执行流。

在这里插入图片描述
如图上图,左边为进程的内容,对所有的线程都是可见的,右边为线程独有的内容,线程与线程之间不可见。

POSIX线程控制

为了实现可以移植的线程程序,在IEEE标准1003.1c中定义了线程的标准,一共超过60个函数调用。这里给出常用的几个函数:
在这里插入图片描述
新的线程会通过 pthread_create 创建,新创建的线程的标识符会作为函数值返回。这个调用非常像是 UNIX 中的 fork 系统调用(除了参数之外),其中线程标识符起着 PID 的作用,这么做的目的是为了和其他线程进行区分。

当线程完成指派给他的工作后,会通过 pthread_exit 来终止。这个调用会停止线程并释放堆栈。

一般一个线程在继续运行前需要等待另一个线程完成它的工作并退出。可以通过 pthread_join 线程调用来等待别的特定线程的终止。而要等待线程的线程标识符作为一个参数给出。

有时会出现这种情况:一个线程逻辑上没有阻塞,但感觉上它已经运行了足够长的时间并且希望给另外一个线程机会去运行。这时候可以通过 pthread_yield 来完成。

下面两个线程调用是处理属性的。pthread_attr_init 建立关联一个线程的属性结构并初始化成默认值,这些值(例如优先级)可以通过修改属性结构的值来改变。

最后,pthread_attr_destroy 删除一个线程的结构,释放它占用的内存。它不会影响调用它的线程,这些线程会一直存在。

线程的实现方式

线程的实现方式一般分为两种,第一种实在用户空间中实现内存,另一种为在内核空间中实现。分别称为:用户态线程核心态线程
用户态线程:
把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构

在这里插入图片描述
用户态线程示意图如图所示,在进程自己的地址空间通过模拟操作系统调度进程的方式进行线程的调度。在进程空间维护一个表,每个表里放一个线程控制块。

核心态线程
在内核维护一个线程表,记录系统中所有线程。当某个进程希望创建一个新进程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。
在这里插入图片描述

内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。

由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把它标志为不可运行的状态,但是其内部结构没有受到影响。稍后,在必须创建一个新线程时,就会重新启用旧线程,把它标志为可用状态。

如果某个进程中的线程造成缺页故障后,内核很容易的就能检查出来是否有其他可运行的线程,如果有的话,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多,就会带来很大的开销。

两种线程实现的优缺点

在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用 pthread_yield 时,必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是本地过程,因而不需要切换到内核,也就不需要上下文切换,也不需要对内存高速缓存进行刷新,因为线程调度非常便捷,因此效率比较高。所以启动他们比进行内核调用效率更高。

在用户空间实现线程还有一个优势就是它允许每个进程有自己定制的调度算法。例如在某些应用程序中,那些具有垃圾收集线程的应用程序(知道是谁了吧)就不用担心自己线程会不会在不合适的时候停止,这是一个优势。用户线程还具有较好的可扩展性,因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大,容易造成问题。

尽管在用户空间实现线程会具有一定的性能优势,但是劣势还是很明显的,你如何实现阻塞系统调用呢?假设在还没有任何键盘输入之前,一个线程读取键盘,让线程进行系统调用是不可能的,因为这会停止所有的线程。所以,使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程。

与阻塞调用类似的问题是缺页中断问题,实际上,计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为缺页故障。而在对所需的指令进行读入和执行时,相关的进程就会被阻塞。如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘 I/O 完成为止,尽管其他的线程是可以运行的。

另外一个问题是,如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃 CPU,在一个单进程内部,没有时钟中断,所以不可能使用轮转调度的方式调度线程。除非其他线程能够以自己的意愿进入运行时环境,否则调度程序没有可以调度线程的机会。

而内核线程就不存在这样的问题。所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值