[Python学习日记-84] 进程理论
简介
进程的概念
并发与并行的区别
进程并发的实现
简介
进程理论是计算机科学中一种重要的概念,用来描述操作系统中执行的程序实例。在操作系统中,每个程序的执行被称为一个进程。进程理论研究进程的创建、调度、通信、同步和终止等方面的原理和方法。本篇我们将介绍进程的概念、进程的创建和终止,以及并发与并行的区别,并了解并发的实现。
进程的概念
一、什么是进程
进程是计算机中正在运行的程序的实例,大白话说就是正在进行的一个过程或者说一个任务,而负责执行任务则是 CPU。
每个进程都有自己独立的内存空间,包括代码、数据和堆栈等,使其能够独立运行并与其他进程隔离。操作系统通过管理进程来分配资源,确保它们能够有效地运行并与其他进程协同工作。在多任务系统中,多个进程可以同时运行,共享计算机的处理器和内存资源。
二、进程与程序的区别
进程和程序是计算机领域中的两个重要概念,它们之间有一定的区别:
程序:程序是一组指令的集合,描述了在计算机上执行特定任务的步骤和逻辑。程序是静态的,它们通常存储在磁盘或内存中,并且不具有执行的状态。程序可以是源代码、可执行文件或脚本等形式。
进程:进程是程序在计算机中执行时的实例。当程序被加载到内存中,并被操作系统调度执行时,就会形成一个进程。每个进程都拥有独立的内存空间、堆栈和寄存器等资源,用于执行程序中的指令和处理数据。
总的来说,程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。
举个例子,在一家有名的西餐厅中,厨师们正在为迎接顾客而做准备,他们有菜单中每道菜相应的食谱,厨房里有所需的原料(土豆、胡萝卜、牛肉、鸡肉等)。
在这个比喻中:
- 菜单中每道菜相应的食谱就是程序(即用适当形式描述的算法)
- 厨师们就是处理器(即CPU)
- 做菜用的各种原材料就是输入的数据
- 进程就是厨师阅读食谱,取来各种原材料以及烹饪料理等一系列动作的总和
餐厅开始营业了,顾客陆续进场,各项作业都井然有序的执行着,突然间一位顾客被鲜嫩多汁的牛排卡住了喉咙,厨师想了想,处理被卡住喉咙的顾客比做其他菜更加重要,于是厨师就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示进行海姆立克急救法对顾客进行急救。
这里,我们看到处理机从一个进程(做菜)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当被食物卡住喉咙的顾客处理完之后,这位厨师又会回来继续从他离开时的那一步开始继续把菜做下去。
需要强调的是:同一个程序执行两次,那也是两个进程,例如,用 PoyPlayer(视频播放软件)打开两个视频,虽然都是同一个软件,但是一个播放的是哆啦A梦,另一个播放的是蜡笔小新。
三、同步\异步和阻塞\非阻塞
1、同步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
举个例子:
- multiprocessing.Pool 下的 apply(发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在 I/O 阻塞,总之就是无脑地等任务结束,这十分容易造成死锁现象,实际编程中应该避免这种等待)
- concurrent.futures.ProcessPoolExecutor().submit(func,).result()
- concurrent.futures.ThreadPoolExecutor().submit(func,).result()
2、异步
异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
举个例子:
- multiprocessing.Pool().apply_async()(发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果,该结果并不是最终的结果,可能是封装好的一个对象)
- concurrent.futures.ProcessPoolExecutor(3).submit(func,)
- concurrent.futures.ThreadPoolExecutor(3).submit(func,)
3、阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(例如,遇到 I/O 操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
举个例子:
- 同步调用:apply 一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走 CPU 的执行权限,那也是处于就绪态)
- 阻塞调用:当 socket 工作在阻塞模式的时候,如果没有数据的情况下调用 recv 函数,则当前线程就会被挂起,直到有数据为止
4、非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
总的来说,同步与异步针对的是函数(任务)的调用方式。同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态;异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。阻塞与非阻塞针对的是进程或线程。阻塞是当请求不能满足的时候就将进程挂起;非阻塞则不会阻塞当前进程。
四、进程的创建与终止
1、创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,例如,电饭煲中的控制器,一旦电饭煲启动,电饭煲中的所有进程都会启动,并存在于存储当中。而对于通用系统(Windows、Linux、MacOS,之类会跑很多应用程序的),需要有系统运行过程中创建或撤销进程的能力,主要分为四种形式创建新的进程:
- 系统初始化(查看进程:例如,Linux 中用 ps 命令查看,Windows 中用任务管理器查看。前台进程负责与用户交互,后台运行的进程则与用户无关,而运行在后台并且只有在需要时才唤醒的进程,被称为守护进程,例如,电子邮件、Web 页面、新闻、打印等)
- 一个进程在运行过程中开启了子进程(例如,nginx 开启多进程、os.fork、subprocess.Popen 等)
- 用户的交互式请求,而创建一个新进程(例如,用户双击 QQ)
- 一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论上述的哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的,而在不同系统中调用的方法也各不相同:
- 在 Unix 中该系统调用的是 fork,fork 会先创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在 shell 解释器进程中,执行一个命令就会创建一个子进程)
- 在 Windows 中该系统调用的是 CreateProcess,CreateProcess 既处理进程的创建,也负责把正确的程序装入新进程中
对于 Unix 和 Windows 创建的子进程有什么区别:
- 相同的是进程创建后,父进程和子进程有各自不同的地址空间(即多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程
- 不同的是在 Unix 中,子进程的初始地址空间是父进程的一个副本,也就是说子进程和父进程是可以有只读的共享内存区的。但是对于 Windows 来说,从一开始父进程与子进程的地址空间就是不同的
2、终止
对于进程的终止主要分为四种形式:
- 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在 Linux 中用 exit,在 Windows 中用 ExitProcess)
- 出错退出(自愿,执行命令 python a.py 时,命令中的 a.py 不存在)
- 严重错误(非自愿,执行非法指令,例如,引用不存在的内存地址、1/0 等,可以通过捕捉异常 try...except... 来规避)
- 被其他进程杀死(非自愿,例如,在 Linux 中使用 kill -9 杀死某个 pid)
五、进程的层次结构
在操作系统中,进程的层次结构是指多个进程之间的父子关系组成的层次结构。通常情况下,一个操作系统会以树状结构来组织多个进程之间的关系。
无论 Unix 还是 Windows,每个进程都有一个父进程(除了根进程,它是整个系统中的第一个进程),不同的是在 Unix 中所有的进程,都是以 init 进程为根,组成树形结构。父子进程共同组成一个进程组,这样当用户从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。
在 Windows 中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌,称为句柄,该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。
六、进程的状态
我们以一个 Unix 当中的命令为例
tail -f access.log |grep '404'
在执行程序 tail 时,开启了一个子进程来执行程序 grep,两个进程之间基于管道“|”通讯开启了另外一个子进程,将 tail 的结果作为 grep 的输入。
在这过程当中,进程 grep 在等待输入(即 I/O)时的状态被称为阻塞,此时 grep 命令是没有在运行的,在两种情况下会导致一个进程在逻辑上不能运行:
- 进程挂起是自身原因,遇到 I/O 阻塞,便要让出 CPU 让其他进程去执行,这样保证 CPU 一直在工作
- 与进程无关,是操作系统层面作出的操作,可能会因为一个进程占用时间过长,或者优先级等原因,进而调用其他的进程去使用 CPU
- 运行:当一个进程在CPU上运行时(单处理机处于运行态的进程只有一个,多进程在 CPU 上交替运行)
- 就绪:一个进程获得了除 CPU 外的一切所需资源,一旦得到处理机即可运行
- 阻塞:阻塞也称等待或睡眠状态,一个进程正在等待某一事件发生(例如,请求 I/O、等待 I/O 完成等)而暂时停止运行,此时即使把 CPU 分配给进程也无法运行,故称进程处于阻塞状态
并发与并行的区别
无论是并发还是并行,在用户看来都是“同时”运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是 CPU,CPU 来做这些任务只能一个一个来,即同一时刻只能执行一个任务。
一、并发
并发(Concurrency)是指在一个时间段内同时处理多个任务是伪并行,即看起来是同时运行。这些任务可以交替进行,每个任务都可能在不同的时间点被执行,但在任何时刻只有一个任务在被执行。在并发中,任务之间可能会互相影响或者依赖,需要通过调度算法来决定任务的执行顺序。单个 CPU 的情况下,可以利用多道技术来实现并发。
举个例子:
- 情人节约会:有一个海王他有三个女朋友,在情人节这天碰巧都要去同一个商场约会,那没办法你只能硬着头皮去了,我们把情人节这天和各个女朋友约会都看作是一个约会任务,这个海王被这三个任务共享,如果想要不出现修罗场的情况,那就要玩出并发恋爱的效果,具体操作应该是这样的,先跟“女友1”去看电影,看了一会说:“不好,我要拉肚子”,然后跑去跟“女友2”吃饭,吃了一会说:“那啥,我去趟洗手间”,然后跑去跟“女友3”开了个房,做了一回又跑回“女友1”哪里继续看电影,如此循环往复过完了情人节这天。
二、并行
并行(Parallelism)是指在同一时刻同时执行多个任务。在并行中,多个任务同时在不同的处理器上或者不同的计算机上并行执行,每个任务都有自己的执行流程。并行可以显著提高计算效率,尤其是在处理大规模数据或者复杂计算任务时。
举个例子:
- 假设银行有四个柜台,现在来了六个客户都要办理业务,这样同一时间有四个任务被执行,假设分配给了柜台1、柜台2、柜台3、柜台4,这个时候所有柜台都占满了,客户5和客户6只能等着,一旦客户1遇到需要等待家人拿资料过来,就会中断当前服务,此时客户5就拿到柜台1的时间片去办理业务了,而客户1资料到了之后银行会重新调用他,可能会被分配到四个柜台的其中一个去继续处理之前的业务
- 我们换个视角,从计算机的视角出发,四个柜台对应四个核,分别是 CPU1、CPU2、CPU3、CPU4,六个客户对应六个任务,这样同一时间有四个任务被执行,假设分别被分配给了 CPU1、CPU2、CPU3、CPU4,一旦任务1遇到 I/O 就被迫中断执行,此时任务5就拿到 CPU1 的时间片去执行(这就是单核下的多道技术,也就是说并行和多道技术并不冲突),而一旦任务1的 I/O 结束了,操作系统会重新调用它(要知道进程的调度、分配给哪个 CPU 运行,都是操作系统说了算),可能被分配给四个 CPU 中的任意一个去执行
所有现代计算机经常会在同一时间做很多件事,即使是一台个人电脑(无论是单核还是多核),都可以同时运行多个任务(一个任务可以理解为一个进程),例如后台运行着360杀毒的同时打开着 QQ 在聊天,还打开了 PotPlay 播放电影。这些被同时运行的进程都需要被管理,这也说明了一个支持多进程的多道程序系统是多么至关重要。
多道技术的回顾请查看这篇博客:操作系统的介绍
进程并发的实现
进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此操作系统需要维护一张表格来保存下终端的状态,这张表被称为进程表(Process Table),每个进程占用一个进程表项,这些表项被称为进程控制块
该表存放了进程状态的重要信息:程序计数器、堆栈指针、内存分配状况、所有打开文件的状态、帐号和调度信息,以及进程由运行态转为就绪态或者阻塞态时必须保存的信息,从而保证该进程在再次启动时,就像从未被中断过一样。