文章目录
- 进程的状态
- Linux下的进程状态
-
- [(一)运行状态(R - running)](#(一)运行状态(R - running))
- [(二)睡眠状态(S - sleeping)](#(二)睡眠状态(S - sleeping))
- [(三)磁盘休眠状态(D - disk sleep)](#(三)磁盘休眠状态(D - disk sleep))
- [(四)停止状态(T - stopped)](#(四)停止状态(T - stopped))
- [(五)僵死状态(Z - zombie)](#(五)僵死状态(Z - zombie))
- (六)孤儿进程
进程的状态
为了弄清楚正在运行的进程是什么意思,我们需要先来了解进程的不同的状态代表着什么。在一个系统中进程可以有几个状态。但是在这之前我们要明确几个知识点来辅助我们进行理解。
并行和并发
并行(Parallelism)
定义:多个进程在多个CPU下分别、同时进行运行。
需要有多个CPU核心或者多个处理器来支持。例如,在一个具有四核CPU的计算机上,可以同时运行四个进程,每个进程在一个独立的CPU核心上运行,它们之间互不干扰,可以同时进行各自的任务处理。
并发(Concurrency)
定义:指多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进。
操作系统通过快速地在多个进程之间切换CPU的执行权来实现并发。例如,在一个单核CPU的计算机上,同时运行着多个程序,如浏览器、音乐播放器和文本编辑器等。操作系统会给每个进程分配一小段时间片,在这个时间片内进程可以执行一些指令,当时间片用完后,操作系统会暂停该进程,保存其当前的运行状态(上下文),然后切换到另一个进程执行,如此循环往复,给用户一种多个程序同时运行的感觉。(因为CPU切换和运行的速度非常快且时间片足够小用户体会不到就会给用户一种多个程序同时运行的感觉)
简单来说就是:
CPU执行进程代码,不是将一个代码执行完毕之后再开始执行下一个代码,而是给每一个进程预分配出一个时间片,基于时间片进行调度轮转(单个CPU下)
物理和逻辑
在这里想说一个小点,物理层面上是一个CPU调度轮转多个进程。而在逻辑上我们可以将并发理解成,假设4个进程并发在一个CPU上,那么我们就可以理解成一个CPU被分成4份来使用,就好比一个100分的CPU被分成4个25分的CPU。这种物理与逻辑上面的的不同是为了更方便来让我们理解计算机中抽象的概念,像是堆的物理上数组来存储,而逻辑结构是二叉树的特殊规则等。
时间片
在这里我们明确两种操作系统就可以:
分时操作系统:Linux/Windows民用级别的操作系统,用的都是分时操作系统。
实时操作系统:汽车的系统等。
至于原因,在我们日常生活中所用的操作系统我们会同时进行很多任务而这些任务有没有明确的优先级,所以为了调度任务的公平,让所有任务都流畅的推进,就采用分时操作系统。在汽车系统中,安全相关的功能如刹车辅助系统、防抱死系统等需要实时操作系统的支持。这些系统必须能够在极短的时间内对传感器检测到的危险信号做出反应,例如,当车辆的传感器检测到前方有障碍物需要紧急刹车时,刹车系统相关的进程必须立即获得 CPU 资源进行制动操作,而不能等待其他非关键任务(如音乐播放、导航等)的时间片结束(总不能CPU说等会等音乐的时间片过去的我再进行刹车系统( ̄▽ ̄)")。同时,在汽车系统中,一些非安全关键的功能如多媒体播放、导航等可以采用分时操作系统来运行,以充分利用系统资源,提高用户体验。
简单就是让大家更好的体会时间片这个概念。
进程具有独立性
进程的独立性是指在多进程运行环境下,每个进程都具有以下特点:
1. 资源独享
内存空间独立
每个进程都有自己独立的地址空间,这意味着一个进程无法直接访问另一个进程的内存区域。例如,进程 A 中的变量存储在其自身的虚拟地址空间内,进程 B 无法直接读取或修改这些变量的值。这种内存空间的独立性是通过操作系统的内存管理机制来实现的,操作系统会为每个进程分配一段独立的内存区域,并通过地址映射等技术将虚拟地址转换为物理地址,确保进程之间不会相互干扰。
其他资源独立
除了内存空间,进程在使用其他系统资源时也具有一定的独立性。例如,进程在使用文件资源时,每个进程都有自己的文件描述符表,用于记录该进程打开的文件信息。即使多个进程打开了同一个文件,它们对文件的操作也是相互独立的,各自维护自己的文件读写位置等信息。
2. 执行互不干扰
指令执行顺序独立
每个进程都按照自己的指令流顺序执行,不受其他进程的影响。例如,进程 A 可能正在执行一段循环代码,而进程 B 可能在执行一个文件读取操作,它们各自按照自己的程序逻辑推进,不会因为另一个进程的存在而改变自己的执行顺序。
时间片分配独立
在操作系统采用时间片轮转调度算法的情况下,每个进程获得的时间片是独立分配的。也就是说,进程 A 的执行时间片用完后,它会被暂停,等待下一次时间片分配,而这个过程与其他进程的时间片分配情况无关。
等待的本质
在操作系统中每一个CPU都有一个执行队列struct runqueue
而struct runqueue
里面包含队列属性其中有task struct *head
,在进程的算法中当我们启动一个进程我们就把新创建的进程tack_struct
链入到task struct *head
中,当我们进程变多时会链接的越来越多,而CPU在运行的时候会直接找到struct runqueue
根据FIFO的调度算法会选择进程的代码地址放到CPU的寄存器中进行执行,而执行完之后会将刚才执行的tack_struct
链接到整个链表的尾部,这样循环往复就形成基于时间片进行轮转的FIFO的调度算法,如下图:
运行
那么什么是一个进程处于运行状态呢?
在之前我以为的是只有在CPU上运行的才是处于运行状态
通过上面的铺垫我们可以引出只要是在运行队列中的,这个进程就是处于运行状态,因为它已经是准备好的可以随时被CPU进行调度
阻塞
我们一般进程中我们的代码中大多数都会包含IO操作,eg:等待键盘数据,读文件等等。以scanf
为例,在我们执行程序的时候到scanf
时候但是我们并没有在键盘上输入任何数据的时候(这个时候我们叫键盘数据没有准备好,即用户没有按键盘),那么此时CPU就不能一直执行这个程序,所以这个程序就会进入阻塞状态中。
操作系统管理硬件和上面进程类似,都是先描述在组织,设计一个链表进行连接每个硬件,如图:
那么我的代码只有scanf
,当我们调用的时候他的内部一定是会封装调用的(因为scanf
的本质是访问键盘),而管理硬件的是操作系统,在整个的系统中只有操作系统最清楚硬件的状态,那么说白了scanf
的封装就是让操作系统查键盘的状态,那么当键盘没有数据的时候我们的进程的状态就是阻塞 。在类里面有一个等待队列task_struct *wait_queue
就是说在查询硬件的状态的时候发现是没有数据的CPU就会将这个进程扔进等待队列,简单理解就是CPU将这个进程从运行队列扔到我们的设备struct device
中等待
所以我们把在设备上的等待状态叫做阻塞状态
而在之后用户想通了终于输入了,操作系统就知道了就会把这个进程(键盘中第一个等待进程)从等待队列中取出再放到CPU上运行
标记
所以总结:
综上所述运行 和阻塞 的本质就是将不同的进程处于不同的队列中
那么回到标题,阻塞最直观的表现就是等待,等待的本质就是:连入目标设备,CPU的不调度!
挂起等待
前提:当内存资源严重不足时!!!
在操作系统读取运行队列的task_struck
的时候进入阻塞状态,那么这个进程被扔进struck device
的等待队列中,但是在等待的期间你的这个进程根本不会被调度,但是你这个进程的PCB占用一点内存和你的代码和数据相比较更加占用内存,而在这个期间当内存资源严重不足时!!! 所以操作系统为了保证自己的正常运行(操作系统也是一个大的软件)想到的一个解决方案,那么在阻塞的进程即占用内存又不执行所以操作系统会选择这些 进程的代码和数据换出到磁盘当中 而当被换出的这个进程准备好之后操作系统不仅会把这个进程的PCB放到运行队列中(改变状态),还要把刚才换出的程序和数据换回来(换入)。而在磁盘中有一个区域是swap
分区,用来存放换出的所有数据。
所以一个进程一旦进入阻塞状态并且还被换出到磁盘中,那么此时的状态就是挂起(严格来说应该是阻塞挂起状态)
1.所以阻塞挂起的目的就是内存严重不足时在阻塞这个状态的背景下,通过将内存挂起到外设上面,从而节省内存
2.更严重的时候还会有运行挂起,就是把运行队列挂起到外设,来缓解内存不足
3.我们从冯诺依曼体系可以知道内存的作用就是提升整机的速度,这样挂起磁盘和内存交换数据必然会影响整体运行速度,所以这是在用时间来换空间没有办法的办法
一般在正常的生产环境下swap的功能都会被禁掉,因为空间不够生产环境可以加(优化软件,或者加服务器),但是时间不行。一般机器在内存不足时候或者连挂起都解决不了的话为了保证操作系统正常运行会直接杀掉其他进程(软件战术性闪退)
Linux下的进程状态
上面我们学习的都是概念,那么真正进入操作系统之后的状态不同的操作系统也不同
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
(一)运行状态(R - running)
运行状态并不意味着进程一定在实际运行中。它表明进程要么正在CPU上执行指令,要么在运行队列里等待被调度到CPU上执行。在Linux内核中,这是一个动态的状态,多个进程可能处于这个状态,但只有一个进程能在某个时刻真正占用CPU资源。
(二)睡眠状态(S - sleeping)
睡眠状态意味着进程在等待某个事件完成。这种睡眠有时候也叫做可中断睡眠(interruptible sleep)。例如,进程可能在等待用户输入、某个文件的读取完成或者网络数据的接收。当等待的事件发生时,进程会被唤醒,重新进入运行队列或者直接开始执行。
(三)磁盘休眠状态(D - disk sleep)
磁盘休眠状态有时候也叫不可中断睡眠状态(uninterruptible sleep)。处于这个状态的进程通常在等待I/O操作的结束,比如磁盘写入或读取操作。在此期间,进程不会被信号中断,直到I/O操作完成。这是为了确保磁盘操作的完整性和数据一致性。
(四)停止状态(T - stopped)
进程可以通过发送SIGSTOP信号进入停止状态。这个被暂停的进程可以通过发送SIGCONT信号让其继续运行。这种状态常用于调试目的或者当系统需要暂时停止某个进程的执行,但又不想终止它时。
(五)僵死状态(Z - zombie)
僵死状态是一个比较特殊的情况。当进程退出并且父进程没有读取到子进程退出的返回代码时,就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。僵死进程会占用系统资源,因为相关的数据结构仍然需要维护在内存中,如果大量产生僵死进程,可能会导致内存泄漏等问题。
(六)孤儿进程
与僵死进程相关的一个概念是孤儿进程。当父进程提前退出,而子进程后退出时,子进程就会成为孤儿进程。孤儿进程会被1号init进程领养,由init进程负责回收相关资源。