进程和线程

概念与区别

从本质上来说,一个进程就是一个正在执行的程序,它是系统进行资源分配和调度的基本单位,是操作系统结构的基础。每个进程都有自己的地址空间,包括可执行程序,程序的数据,栈,一组寄存器(程序计算器,栈指针以及其他运行程序需要的信息

线程有时被称为轻量级进程,是程序执行的最小执行流,它是进程的一个实体,是系统独立调度和分派的基本单位

进程和线程的区别:

  1. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  2. 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
  3. 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  4. 线程是处理器调度的基本单位,但是进程不是。
  5. 两者均可并发执行。

进程和线程占有的资源

首先我们知道线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源,不包含堆栈,如果要修改共享空间里的资源,需要加锁。

进程的话占有堆栈

进程

进程的创建和终止

进程的创建主要有四个原因:系统初始化,正在运行的进程执行了创建进程的系统调用,用户请求创建一个进程以及批处理作业的初始化。

常见的就是一个进程调用了 fork() 函数创建新的进程。

操作系统创建一个新的进程的过程

  1. 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB( PCB 是有限的)。若 PCB 申请失败则创建失败。
  2. 为进程分配资源,为新进程的程序和数据、以及用户栈分配必要的内存空间(在 PCB 中体现)。注意:这里如果资源不足(比如内存空间),并不是创建失败,而是处于”等待状态“,或称为“阻塞状态”,等待的是内存这个资源。
  3. 初始化PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。
  4. 如果进程就绪队列能够接纳新进程,就将新进程插入到就绪队列,等待被调度运行。

注:PCB是进程存在的唯一标识,它包含进程标识符(内部标识符:每个进行唯一的一个数字标识符,外部标识符:创建者提供),处理机状态,进程调度信息和进程控制信息。

进程终止的一些原因:工作完成正常退出,出错退出,严重错误,被其他进程杀死。

进程的状态和控制原语

进程有三种状态:运行态,阻塞态,就绪态。这三种状态的转换是:

就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。

阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。

运行:当一个进程在处理机上运行时,则称该进程处于运行状态。

注意不可能存在直接从阻塞态转换到执行态。

除了这三个基本状态还有一个挂起状态,新建状态,终止状态。

引起挂起状态的原因:终端用户的请求,父进程请求,负荷调节的需要,操作系统的需要。

用于控制进程的原语有:

  1. 创建原语(Create):创建一个就绪状态的进程,使进程从创建状态变迁为就绪状态。

  2. 阻塞原语(Block):使进程从执行状态变迁为阻塞状态。

  3. 唤醒原语(Wakeup):使进程从阻塞状态变迁为就绪状态。

  4. 挂起原语(Suspend):将指定的进程或处于阻塞的进程挂起

Java的Runnable状态与操作系统中进程运行状态的关系

RUNNABLE 状态对应了传统的 readyrunning 以及部分的 waiting状态,也就是上面的三种状态,但是操作体系中其实是有五种状态的。

进程间通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

管道

管道是由调用pipe函数来创建

1
2
3
4
#include <unistd.h>
int pipe (int fd[2]);
//返回:成功返回0,出错返回-1
// fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。fd[1]的输出是fd[0]的输入。

实现进程通信的方式

  • 父进程创建管道,得到两个⽂件描述符指向管道的两端
  • 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
  • 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。

管道读取数据的几种情况:

  • 读端不读,写端一直写
  • 写端不写,但是读端一直读
  • 读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]
  • 读端读了一部分数据,不读了且关闭fd[0],写端一直在写且fd[1]还保持打开状态。

对应的处理:

  • 如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
  • 如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
  • 而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样。

管道的特点:

  • 管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
  • 管道只允许单向通信。
  • 管道内部保证同步机制,从而保证访问数据的一致性。
  • 面向字节流
  • 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。

信号量

信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

工作原理:

由于信号量只能进行两种操作等待和发送信号,即P(sv)V(sv),他们的行为是这样的:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

在信号量进行PV操作时都为原子操作(单条指令的执行是不会被打断的,因为它需要保护临界资源)

与信号量相关的函数:

1
2
3
4
5
6
7
8
9
10
// 创建信号量,返回:成功返回信号集ID,出错返回-1
int semget(key_t key,int nsems,int flags)

// 删除和初始化信号量
int semctl(int semid, int semnum, int cmd, ...);

// 改变信号量的值
int semop(int semid, struct sembuf *sops, size_t nops);

// 对应的参数的含义:https://blog.csdn.net/skyroben/article/details/72513985

消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。 用户可以从消息队列中读取数据和添加消息,其中发送进程添加消息到队列的末尾,接收进程在队列的头部接收消息,消息一旦被接收,就会从队列中删除。

消息队列常用的一些函数有:

  • msgget创建或者打开消息队列,
  • msgsnd添加消息,
  • msgrcv读取消息,
  • msgctl 控制消息队列,
  • ftok由于文件路径工程ID生成的标准key

共享内存

共享内存就是允许两个或多个进程共享一定的存储区。就如同malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝。

但是共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步

共享内存的涉及到的函数:

1
2
3
4
5
// 创建共享内存,成功返回共享内存的ID,出错返回-1  
int shmget(key_t key, size_t size, int shmflg);

// 操作共享内存,成功返回0,出错返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

共享内存优缺点:

  • 优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。

  • 缺点:共享内存没有提供互斥同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段比如信号量等来进行进程间的同步工作。

为什么需要进程间通信

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

线程间通信

同步

指多个线程通过Synchronized关键字这种方式来实现线程间的通信。

比方说由于线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了通信。

这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

while轮询的方式

参考:Java多线程通信方式

wait/notify机制

通过进程调用对应的函数,通知对应另外的线程从而实现线程的通信。

比方说当条件未满足时,线程A调用wait()放弃CPU,并进入阻塞状态,当条件满足时,线程B调用 notify()通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。

进程同步

进程同步是一个操作系统级别的概念,有两种形式的制约:间接性制约(同一个系统的进程需要共享着某种资源),直接性制约(源于进程间的合作)表示是在多道程序的环境下,存在着不同的制约关系,为了协调这种互相制约的关系,实现资源共享和进程协作,从而避免进程之间的冲突,引入了进程同步。 比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒。

临界资源

在操作系统中,进程是占有资源的最小单位,但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机

对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。

对于临界区的访问过程分为四个部分:

  • 进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞
  • 临界区:在临界区做操作
  • 退出区:清除临界区被占用的标志
  • 剩余区:进程与临界区不相关部分的代码

进程互斥

进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。只有当使用临界资源的进程退出临界区后,这个进程才会解除阻塞状态。

比如进程 B 需要访问打印机,但此时进程 A 占有了打印机,进程 B 会被阻塞,直到进程 A 释放了打印机资源,进程B 才可以继续执行。

实现临界区互斥的基本方法

  • 通过硬件实现临界区最简单的办法就是关 CPU 的中断
  • 信号量实现:常见的 P,V 操作

进程同步与进程通信区别

  • 进程同步:控制多个进程按一定顺序执行;
  • 进程通信:进程间传输信息。

进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。

如何正确的停止一个线程

结束一个线程有一个最基本的方法:Thread.stop() 方法,但是这个方法已经是被建议不要使用的方法(会立即释放该线程所持有的所有的锁导致数据得不到同步的处理,出现数据不一致的问题;即刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中,并抛出 ThreadDeath 异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。)。

正确的是使用中断,也就是使用 Thread.interrupt() 方法,严格的讲,线程中断不会使线程立即退出,而是给线程发送一个通知,告诉目标线程,有人需要你退出啦!至于目标线程接到通知后如果处理,则完全由目标线程自行决定。

所以置为中断状态,还需要增加中断处理逻辑程序,不然就没有作用。需要使用 Thread.isInterrupted() 判断线程是否被中断,然后进入中断处理逻辑代码。

中断和异常

所谓中断是指CPU对系统发生的某个事件作出的一种反应:CPU暂停正在执行的程序,保留现场后自动地转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序。

引起中断的事件称为中断源,中断源向CPU提出进行处理的请求称为中断请求。它是由CPU以外的事件引起的中断,如I/O中断、时钟中断、控制台中断等。

引入中断的目的:实现并发活动,实现实时处理,故障自动处理

异常来自 CPU 的内部事件或程序执行中的事件引起的过程。如由于CPU本身故障、程序故障和请求系统服务的指令引起的中断等。

进程隔离

它是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件)的技术。这个技术是为了避免进程 A 写入进程B 的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程 A 的虚拟地址和进程 B 的虚拟地址不同,这样就防止进程 A 将数据信息写入进程 B。

虚拟内存

虚拟内存:虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。

虚拟地址空间

通过虚拟内存的概念,操作系统为每一个进程提供完全一致的内存视图,这个内存视图的地址空间,叫虚拟地址空间。CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。

多线程、多进程的区别及适用场景

多线程占相比于多进程占用内存少、CPU利用率高,创建销毁,切换都比较简单,速度很快。多进程相比于多线程共享数据复杂,需要将进程间通信。但是同步简单,多线程因为数据共享简单,导致同步复杂。多进程编程调试都比多线程简单。进程之间互相不影响,一个线程挂掉将导致整个进程挂掉。多进程适合多核,多机分布,多线程适合多核分布。

举个例子,谷歌浏览器是使用多进程来实现的,浏览器中你打开的每个页面,都是一个进程。如果一个页面崩溃了,不会影响其他页面(进程相互独立)。但是谷歌浏览器占用内存相比于其他浏览器多,实际应用中,打开页面太多,占用内存较大。其他浏览器采用多线程来实现,每个页面就是一个线程,所以一个页面崩溃,会导致整个浏览器崩溃。