进程和线程

Posted csushl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程和线程相关的知识,希望对你有一定的参考价值。

 

1.概念和区别

概念

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

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

进程和线程的区别

 

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、CPU等。但是进程之间的资源是独立的。进程切换时,消耗的资源大,效率低,所以涉及到频繁的切换时,使用线程好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。

  • 执行过程:每个独立的进程有一个程序运行的入口,顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  • 线程是处理器调度的进本单位,但进程不是

  • 两者都可并发执行。

2.进程和线程占有的资源

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

? 进程占有堆栈。

3.进程

进程的创建和终止

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

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

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

? 1.为新进程分配一个唯一的进程标识号,并申请一个空白的PCB(PCB是有限的)。若PCB申请失败则创建失败。

? 2.为进程分配资源,为新进程的程序和数据,以及用户栈分配必要的内存空间(在PCB中体现)。注意:如果资源不足(比如内存空间),并不是创建失败,而是处于“等待状态”,或则称为“阻塞状态”,等待的是内存这个资源。

? 3.初始化PCB,主要包括初始化标志信息,初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。

? 4.如果进程就绪队列能够接纳新锦成,或将新进程插入到就绪队列,等待被调度运行。

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

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

进程的状态和控制原语

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

 

 技术图片

 

 

 

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

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

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

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

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

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

用于控制进程的原语有:

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

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

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

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

     

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

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

技术图片

 

 

 

5.进程间通信

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

管道

所谓管道,就是用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件,又名"pipe"文件。

管道是由pipe函数来创建

#include<unistd.h>
int pipe (int fd[2]);
//返回:成功返回0,出错返回-1    
// fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。fd[1]的输出是fd[0]的输入。那么此时这个管道对于用户程序就是一个文件,可以通过read(fd [0]);或者write(fd [1])进行操作。pipe函数调用成功返回0,否则返回-1。

实现进程通信的方式

  • 父进程创建管道,得到两个文件描述符指向管道的两端

  • 父进程fork出子进程,子进程也有两个文件描述符指向同一管道

  • 父进程关闭fd[0],子进程关闭fd[1],即父进程关闭管道读端,子进程关闭管道写端,(因为管道只支持单向通信)。父进程可以往管道里写,子进程可以往管道里读,管道是用环形队列实现的数据从写端流入,从读端流出,这样就实现了进程间通信。

管道读取数据的几种情况(读、写具有互斥性):

  • 读端不读,写端一直写

  • 写端不写,但是读端一直读

  • 读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]

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

读/写必须确定对方是否存在,只有确定对方存在时,才能进行通信。

对应的处理:

  • 如果一个管道的写端一直在写,而读端的引?计数是否?于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;

  • 如果一个管道的读端一直在读,而写端的引?计数是否?于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;

  • 而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到?件末尾?样。

四个特殊情况:

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样

  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。

  4. 如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

管道的特点:

  • 管道只允许具有血缘关系的进程间通信,如父子进程间的通信。

  • 管道只允许单向通信。

  • 管道内部保证同步机制,从而保证访问数据的一致性。

  • 面向字节流

  • 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。

管道通信的同步:指当写(输入)进程把一定的数量的数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读一空pipe时,也应该睡眠等待,直到写进程将数据写入管道后,才将之唤醒。

匿名管道

匿名管道主要用于本地父进程和子进程之间的通信,在父进程中的话,首先是要创建一个匿名管道,在创建匿名管道成功后,可以获取到对这个匿名管道的读写句柄,然后父进程就可以向这个匿名管道中写入数据和读取数据了,但是如果要实现的是父子进程通信的话,那么还必须在父进程中创建一个子进程,同时,这个子进程必须能够继承和使用父进程的一些公开的句柄,为什么呢?因为在子进程中必须要使用父进程创建的匿名管道的读写句柄,通过这个匿名管道才能实现父子进程的通信,所以必须继承父进程的公开句柄。同时在创建子进程的时候,必须将子进程的标准输入句柄设置为父进程中创建匿名管道时得到的读管道句柄,将子进程的标准输出句柄设置为父进程中创建匿名管道时得到的写管道句柄。然后在子进程就可以读写匿名管道了。

信号量

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

工作原理:

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

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

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

与信号量相关的函数:

// 创建信号量,返回:成功返回信号集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() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝。

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

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

// 创建共享内存,成功返回共享内存的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);
?
 

共享内存优缺点:

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

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

 

6.为什要进程通信

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

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

 

7.线程间通信

同步

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

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

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

while轮询的方式

参考:Java多线程通信方式

wait/notify机制

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

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

 

8.进程同步

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

临界资源

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

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

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

  • 进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞

  • 临界区:在临界区做操作

  • 退出区:清除临界区被占用的标志

  • 剩余区:进程与临界区不相关部分的代码

 

9.进程互斥

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

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

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

  • 通过硬件实现临界区最简单的办法就是关 CPU 的中断

  • 信号量实现:常见的 P,V 操作

 

10.进程同步与进程通信区别

进程同步:控制多个进程按一定的顺序执行;

进程通信:进程间传输信息。

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

 

11.如何正确的停止一个线程

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

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

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

 

12.中断和异常

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

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

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

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

13.进程隔离

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

虚拟内存

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

虚拟地址空间

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

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

对比维度多进程多线程总结
数据共享、同步 数据共享复杂、需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁,切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会相互影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适用于多核、多机分布式;如果一台机器不够,扩展到多态机器比较简单 是英语多核分布式 进程占优

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

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

 

技术图片

以上是关于进程和线程的主要内容,如果未能解决你的问题,请参考以下文章

多个请求是多线程吗

python多线程

多线程编程

python中的多线程和多进程编程

进程和线程和协程之间的关系

[Python3] 043 多线程 简介