《现代操作系统》读书笔记 — 进程和线程

一、进程和线程

什么是进程

进程是程序执行的一个过程,程序指的是我们通常意义上的代码,一个程序可以被执行多次,也就产生了多个进程。进程是操作系统资源分配和调度的基本单位,是操作系统结构的基础。每个进程都有属于自己的地址空间。

操作系统中的进程一般由三部分组成:①进程控制块PCB;②数据段;③正文段。

UNIX系统为了节省进程控制块所占的内存空间,把每个进程控制块分成两部分。一部分常驻内存,不管进程是否正占有处理器运行,系统经常会对这部分内容进行查询和处理,常驻部分内容包括:进程状态、优先数、过程特征、数据段始址、等待原因和队列指针等,这是进行处理器调度时必须使用的一些主要信息。另一部分非常驻内存,当进程不占有处理器时,系统不会对这部分内容进行查询和处理,因此这部分内容可以存放在磁盘的对换区中,它随用户的程序和数据部分换进或换出内存。

UNIX系统把进程的数据段又划分成三部分:用户栈区(供用户程序使用的信息区);用户数据区(包括用户工作数据和非可重入的程序段);系统数据区(包括系统变量和对换信息)。

正文段是可重入的程序,能被若干进程共享。为了管理可共享的正文段,UNIX设置了一张正文表,每个正文段都占用一个表目,用来指出该正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。

什么是线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以有多个线程,这些线程都共享该进程的系统资源。其中内核级的线程实现又被称为轻量级进程。进程和线程都是为了并行而存在,那么为什么要有线程的概念呢?

  1. 多个并行的实例之间想要共享同一个地址空间以及数据,这是多进程无法做到的。
  2. 线程比进程更轻量,创建和销毁的代价更低。在许多系统中,创建一个线程比创建一个进程要快10~100倍。

线程的实现类型

线程的实现类型有三种,分别是用户级线程和内核级线程,以及他们组合混合型线程。

1. 用户级线程

在用户级线程的实现中,对于操作系统内核来说,一个进程只有一个线程。操作系统调度的时候只负责调度这个进程,进程内部的多线程有进程自己来管理并调度。

优点:

  • 由于用户级线程仅存在于用户空间中,所以不需要在内核态和用户态之间切换,创建、销毁、切换线程的速度非常快
  • 我们可以写程序自己实现线程的调度算法
  • 独立于操作系统,可以在不支持线程的操作系统中使用

缺点:

  • 由于操作系统内核无法识别进程中的多线程,因此无法充分的利用多核CPU的特性
  • 程序运行时,一个线程的阻塞可能会导致整个进程的阻塞
  • 在一个进程的内部,没有时钟中断,无法用轮转调度的方式调度线程
2. 内核级线程

内核级线程也被称为是轻量级进程,由操作系统内核来创建和撤销,因此操作系统可以感知到线程的存在,并针对线程进行调度。在内核级线程模型中,操作系统会维护一个创建的线程表用来调度线程。

优点:

  • 充分利用多核CPU的特性
  • 进程中的一个线程阻塞时,不会造成进程阻塞

缺点:

  • 调度线程时需要切换到内核态,代价稍微比较高
  • 如果程序依赖于内核级线程,但是操作系统不支持的话,则可能导致程序无法运行
3. 混合型线程

混合型线程就是用户级线程和内核级线程的混合实现。程序在自己的用户空间创建管理线程,但是可以将一些线程映射在内核级线程上面。这样就同时拥有了两个线程模型的优点。

Java 就是使用的混合型线程:

java在jdk 1.2之前基于用户线程实现,在1.2之后,基于操作系统的原生线程模型来实现,在每个平台上都不尽相同,比如在windows和linux下都是采用一对一的线程模型实现,在Solaris平台,采用都是一对一或者多对多来实现(solaris 同时支持一对一和多对多)。

进程和线程的区别

  • 进程和线程可以理解为父子关系,一个进程可以有多个线程,但是一个线程只会属于一个进程。
  • 进程是资源分配和调度的基本单位,而线程是操作系统进行调度的最小单位(有的操作系统没有实现线程,因此就是以进程为单位进行调度)。
  • 每个进程都有自己的一个用户地址空间,而线程则共享进程的用户地址空间
  • 进程创建调度开销比较大,线程创建销毁调度开销较小

二、进程间通信

进程间通信方式(IPC)

由于每个进程都有自己的一个地址空间,因此进程间无法共享数据。那么进程间要怎么实现通信呢?下面介绍一下linux使用的几种进程间通信的方式

1. 管道

管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

管道是内核管理的一个缓冲区,一般为4K大小,它被设计成为环形的数据结构,以便管道可以被循环利用。可以理解为阻塞队列,一端由一个进程输入,一端由一个进程输出。

在linux中,又分为无名管道和有名管道。无名管道用于用于有亲缘关系的进程间使用,使用的块是内存缓冲区。有名管道可以突破管道只能用于具有亲缘关系的进程间使用的限制,主要是将管道放到磁盘文件中来实现的。

2. 共享内存

就是拿出一块能被所有进程访问的内存块,来达到数据共享的目的。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。由于多个进程同时访问同一块内存,所以需要做好进程间同步的措施。

3. 信号量

信号量就是一个计数器,主要用来同步各个进程之间对共享资源的访问,也就是一种锁机制。我们可以通过down操作将信号量减1,当信号量是0时,进程进入休眠。通过up将信号量加1,同时唤醒在这个信号量上面休眠的进程。

互斥量就是信号量中的其中一种,它的值只有0和1,一般用来加锁和释放锁。加锁成功则设置为1,加锁失败则休眠。释放锁的时候将互斥量的值设置为0,并唤醒那些等待的进程中的一个进程。

4. 消息队列

消息队列是由消息的链表,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5. socket

我们常用的socket也可以实现进程之间的通信。只要进程监听操作系统的某个端口,其他进程就可以通过ip+端口来和这个进程进行通信。

上面四种的通信方式其实归根究底都是和共享内存有关,但是socket却是使用网络接口来实现的通信。另外socket还可以跨机器进行进程通信。

进程间同步

当多个进程共同访问共享数据时,就要考虑进程同步问题。

一个典型的例子就是假设A、B进程都要访问一个变量a=0,然后将a从内存取出来之后+1,再放回内存。如果A和B没有做同步,可能会导致A和B同时去获取变量a的信息,然后+1,再放回去的情况,最后结果还是1(正常情况应该是2)。这样就会造成最终数据和预想的不一致的情况。

临界区

为了解决这个问题,必须保证A和B在对a变量进行操作时是互斥的。这里就引入了一个临界区的概念,我们把对共享内存进行访问的程序片段称作临界区。任何两个进程不能同时处于其临界区。

要实现临界区就要保证进程之间的互斥,那么有哪些办法可以保证进程之间的互斥呢?

临界区的实现方案
1. 屏蔽中断

进程可以在进入临界区之后屏蔽所有的中断,保证访问共享内存时不会有其他的进程也进入临界区中。这个方案并不好,因为这样要把屏蔽中断的权利交给用户进程,有一定的隐患。另外,如果是多核处理器的话,屏蔽中断也无法保证不会有其他进程进入临界区。

2. 锁变量 — TSL指令和XCHG指令

可以设定一个共享变量,其初始值为0。如果一个进程想要进入临界区,就要先检查其值是否为0,是的话将值设置为1,并进入临界区,否则不允许进入。——其实就是一个互斥量(信号量)

但是共享变量还是会有进程同步的问题,那么怎么保证不会出现多个进程同时检查共享变量的情况呢?这就需要从硬件层面上来解决了。

在某些计算机中,特别是那些设计为多处理器的计算机,都有下面这条指令

TSL RX,LOCK

这个指令被称为测试并加锁,它可以先将内存地址的值读出来,然后再写入一个非0的值到内存地址中。读和写操作是不可分割的,保证了指令的原子性。执行该指令的时候会锁住CPU总线,以禁止其他的CPU在本指令结束前访问内存。

下面是通过TSL指令实现的访问临界区的汇编伪代码

enter_region:
	TSL REGISTER,LOCK
	CMP REGISTER,#0		//判断从内存读出来的数是否为0
	JNE enter_region    //若读出来的数不是0,说明锁已经被设置,所以循环
	RET     //返回调用者,进入临界区
leave_region:
	MOVE LOCK,#0	//在锁变量中存入0
	RET		//返回调用者

一个可替代TSL的指令是XCHG,它原子性的交换了两个位置的内容。实现临界区的原理和TSL差不多。

管程

使用信号量做进程之间的同步,会使up(S)和down(S)操作大量分散在各个进程中,不易管理,易发生死锁。因此引入了管程的概念。

管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。任何一个时刻,管程只能由一个进程使用。

进入管程时的互斥由编译器负责完成(如何实现互斥都写好了),相比程序员自己去实现互斥,出错的可能性要小很多。所以我们无须关心编译器是如何实现互斥的,只需要将所有的临界区转换成管程过程即可。

**在管程中执行的进程,如果发现自身无法继续执行下去(可能要等待外部某些条件达成),可以通过执行wait操作阻塞自己,同时推出管程,这样其他的进程就可以进入管程中。**另外,其他进程可以通过在某个共享变量上执行signal操作来唤醒那些进入wait的进程。

java的同步原语synchronized就是使用了管程。在java的同步块中,我们执行Object.wait()时会释放锁,这样其他的线程就可以获取到锁。同时可以通过Object.notify()来通知在wati中的线程。

三、结尾

最近在看《现代操作系统》这本书,刚看完进程和线程这一章,边看边记录,也就写了这篇博客。这本书太厚了,内容也很多,所以只能把一些自认为关键的点写出来。

看完这一章,特别是进程同步那一块的内容,让我对java锁的实现原理有了更深的了解。

本来还想写一下关于进程调度的算法,但是想了想这部分看的时候也没有很认真去钻研过,真写下来内容也有点多,可能大部分还是复制过来的,加上网上资料一查一大堆,实在没有写下来的必要。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值