🌛什么是进程
有小伙伴就会问了,什么是进程呢?
进程=内核关于进程的相关数据结构+当前代码的内容和数据
什么是进程?
早期的计算机一次只能执行一个程序,这种程序完全控制系统,并且访问所有系统资源。到了现代,计算机系统允许加载多个程序到内存,以便于并发执行。这就要求操作系统对各种程序提供更严格的控制和更好地划分和规划。这些需求引发了进程概念的产生,大白话来说,进程就是正在执行的程序,是现代分时操作系统的工作单元。
操作系统的复杂程度决定它可以为用户带来更好地体验感。虽然它主要关注的是执行用户程序,但是它也要顾及各种系统任务。因此系统会由一组进程组成,操作系统进程和用户进程;操作系统进程执行系统代码,而用户进程执行用户代码。
通过 CPU 的多路复用,所有这些进程可以并发执行。通过在进程之间切换 CPU,操作系统能使计算机更为高效。
前面说,进程是执行的程序,这是一种非正式的说法。进程不只是程序代码(文本段或代码段),通常还包含以下内容:
当前活动,如程序计数器的值和处理器寄存器的内容等。
进程堆栈(包括临时数据,如函数参数、返回地址和局部变量)和数据段(包括全局变量)。
堆,这是在进程运行时动态分配的内存。
⭐什么是PCB?
从操作系统理解进程概念-------先描述,后组织
为了使参与并发执行的程序能独立的运行,必须为之配置一个专门的数据结构-----task_struct,称为进程控制块(PCB)。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程(也就是组织)进程。 相应地,由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)。所谓创建进程,实质上是创建进程映像中的PCB;而撤销进程,实质上是撤销进程的PCB。值得注意的是,进程映像是静态的,进程则是动态的。
进程的唯一标志—PCB
综上所述,进程的定义大体分为以下几点
进程是程序的一次执行
进程是一个程序及其数据在处理机上顺序执行时所发生的活动
进程是具有独立功能的程序在一个数据集合上运行过程,它是系统进行资源分配和调度的一个独立单位
进程的特征
进程是由多程序的并发执行而引出的,它和程序是两个截然不同的概念。进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求。
动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程 的程序并发执行,以提高资源利用率。
独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。
异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须 配置相应的进程同步机制。
结构性:每个进程都配置一个PCB对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。
进程的状态和转换
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
🌛查看进程
#include<stdio.h> int main() { while(1) { printf("我是一个进程\n"); sleep(1); } return 0; }
- 这是一个简单的进程(myprocess)
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
- 这是查看进程的指令!
🌛如何通过系统调用查看进程PID
我们还可以用系统调用getpid()获取当前进程的pid,getppid()获取当前进程的父进程的PID。
首先通过man手册查询这两个函数如何使用:
$ man getpid
运行截图:
注意,返回值类型未pid_t其实就是size_t。通过一下代码进行测试:
#include<stdio.h> #include<sys/types.h> int main() { while(1) { printf("我是一个进程,我的PID是:%d,我的父进程是:%d\n",getpid(),getppid()); sleep(1); } return 0; }
运行截图:
🌛fork
首先我们通过man手册认识一下fork;
$ man fork
运行截图:
简单说明就是fork用来创建子进程,在父进程中,fork的返回值为子进程的PID;在子进程中返回值为0。
#include<stdio.h> #include<sys/types.h> int main() { printf("fork调用之前的内容.....\n"); fork(); printf("fork调用之后的内容.....\n"); return 0; }
运行截图:
我们可以看出这里fork调用之后的内容打印了两遍。原因是当调用fork之后,子进程被创建,父进程与子进程同时在运行,于是父进程打印了一遍fork调用后的内容,子进程也打印了一遍fork调用后的内容。
fork的功能很强大,我们一般需要与if配合使用进行分流。还记得上面提到的fork的返回值吗?子进程返回值为0,父进程返回值为子进程PID,可以此作为分流的依据。例如:
#include<stdio.h> #include<sys/types.h> int main() { pid_t ret = fork(); if(ret==0) { // 子进程 while(1) { printf("我是子进程,我的PID是:%d,我的父进程是:%d\n",getpid(),getppid()); sleep(1); } } else { // 父进程 while(1) { printf("我是父进程,我的PID是:%d,我的父进程是:%d\n",getpid(),getppid()); sleep(1); } } return 0; }
运行截图:
此刻父子进程都在运行。那么问题来了——
- 请问为什么此时if与else竟然能够同时执行?也就是fork为什么会有两个返回值?
进程间是互相独立的
例如qq与微信同时运行,两个并无关联,互不影响。
我们知道进程=内核数据结构(PCB)+代码和数据。当子进程创建时,操作系统会为子进程创建一个PCB记录子进程的状态信息等。同时子进程会与父进程共同使用一份代码和数据,若有任意一个执行流(只父子进程)修改数据时,操作系统会为该进程将代码和数据拷贝一份,再进行修改,此动作我们称之为写时拷贝,写时拷贝同样为非常重要的知识。
最后,因为进程具有独立性,同样父子进程也具有独立性,且由于写时拷贝的存在,父进程中调用fork会返回子进程的PID,所以else执行了,这是父进程的行为;子进程中会fork会返回0,所以if执行了,这是子进程的行为。父子进程互不影响
🌞认识进程状态
进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变化,使得进程的状态也在不断地发生变化(一个进程会经历若干种不同状态)。通常进程有以下五种状态,前三种是进程的基本状态。
运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行。
阻塞状态:又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
创建状态:进程正在被创建,尚未转到就绪状态。创建进程通常需要多个步骤:首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息;然后由系统为该进程分 配运行时所必需的资源;最后把该进程转入到就绪状态。
结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行。当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和 回收等工作。
Linux中进程状态一般有:
R(运行状态):并不意外着真正的在运行(指正在被CPU调度);
S(休眠状态):进程在等待获取某种资源,此状态还被称为可中断休眠;
D(磁盘休眠状态):在这个状态的进程也是在休眠,但是不可被中断,因此又称过该状态为不可中断休眠;
T(暂停状态):可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X(死亡状态):这个状态只是一个返回状态,你不会在任务列表里看到这个状态;
Z(僵尸状态):当一个子进程没有被父进程“回收”,该进程就会处于僵尸状态;
🌛查看进程状态
指令:
ps axj | head -n1 && ps axj | grep myprocess | grep -v grep
🌛R状态
当你在电脑上同时运行很多程序,例如你敲代码的时候,还听着某个软件播放的歌曲,或者在浏览器之间来回切换。请问此时这些所有的应用都在CPU运行吗?
答案是,并不是这样的。
在CPU进行工作的时候,会存在一个进程运行的队列。队列维护的内容是一个个task_struct结构体的指针。在该队列中维护的进程都处于R状态,且等着被CPU所调度。
⭐例如:
根据上图,问题来了,为什么在该程序执行时,并没有看到所谓的R状态呢?
原因是由于CPU运算速度太快了,我们基本很难看到R状态。该进程死循环的在屏幕上打印。我们都知道此时的屏幕是一种外设,而CPU的计算速度相比较外设的访问速度根本不在一个量级。所以,该进程死循环的在屏幕上打印内容,有99.9%的时间都在访问外设,剩下的时间是CPU在做计算。在进程访问外设的时候,CPU并不会傻傻的原地等待,而是转头却做别的事,当该进程访问外设成功后,CPU再对它进行调度。那么有什么办法等看到R状态呢?我们将上面的代码略作修改:
#include<stdio.h> #include<unistd.h> int main() { while(1) { //printf("hello myprocess\n"); } return 0; }
运行截图:
如上图所示,当我们不再访问外设,而是只不停地做重复的运算,此时CPU会一直被调度,就能看到R状态了。
🌛S状态
S状态称为休眠状态。休眠状态本质是一种阻塞。
阻塞:进程因为等待某种资源就绪而表现出的不推进的状态。
例如,当一个进程运行到一半,需要从磁盘上获取很大的一块数据,那么就要花费较久的时间。此时OS的处理方式是,让该进程继续等待它要的数据,但是要求你不能在等待资源的时候还占用着CPU,于是该进程就被OS安排到某个地方进行等待,这时该进程就处于S状态。运行截图:
如上图所示,当进程等待用户从键盘上输入的数据时,它就处于睡眠状态。
🌛D状态
D状态也是一种休眠状态,但是它又有个名字叫做磁盘休眠状态或者不可中断休眠。那么如何看待S状态与D状态的区别呢?
🌛T状态
T状态称为停止状态,就是让某个进程暂停一下。
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 int count = 0; 7 while(1) 8 { 9 //printf("hello myprocess\n"); 10 printf("我再运行吗??%d\n",count++); 11 sleep(1); 12 } 13 return 0; 14 }
当程序开始运行后,此时向进程发送暂停的信号:
$ kill -18 (进程PID)
运行截图:
此外,我们还可以发送继续的信号让该进程继续执行:
$ kill -18 (进程PID)
运行截图:
注意:
进程继续在运行了。但是我们发现有一个地方好像和之前不一样了,S后面是不是一直有一个+号来着?我们也不知道+是干嘛的,只知道他现在好像消失了。
“+” 代表在前台运行,没有”+“表示在后台运行;
之前我们在终止一个程序时,习惯使用Ctrl + c ,但是现在好像对于后台在运行的进程失效了,此时我们需要掌握一条新的指令来”杀掉“进程:
$ kill -9 (进程PID)
🌛t状态:
例如在调试时,我们设置了几个断点。当进程在该断点处停下来时,该进程就处于暂停状态。
可以看见我们在第十行打了断点,此时观看进程状态:
我们在调试中运行一下:
观看运行状态:
🌛X状态
- X状态为死亡状态是一个瞬时状态不易观察,暂且认为它不重要;
🌛Z状态
- Z状态被称为僵尸状态。顾名思义,一个进程死了(退出了)但没有”收尸“,就成了”僵尸“。具体一点,当一个进程退出时如果它的父进程没有读取到该进程退出时返回的退出状态码,该进程就会变成僵尸进程。
⭐僵尸进程
#include<stdio.h> #include<unistd.h> int main() { pid_t id = fork(); if(id == 0) { while(1) { printf("我是子进程,我在运行,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } else if(id > 0) { while(1) { printf("我是父进程,我在运行,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } return 0; }
当我们运行程序后,能看到程序正常的在运行;
此时当我们执行指令将子进程”杀“掉,子进程就会变成僵尸进程;
$ kill -9 (子进程PID)
其中我们能看到一个英文单词——defunct就是僵尸的意思。
⭐僵尸进程的危害
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(即PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存。
🌛孤儿进程
所谓孤儿进程,故名思义,和现实生活中的孤儿有点类似,当一个进程的父进程结束时,但是它自己还没有结束,那么这个进程将会成为孤儿进程。
运行该程序,我们使用kill命令”杀“掉父进程,此时再来查看进程信息:
如上图所示,子进程发生了两个变化。一是子进程的PPID,二是子进程变为在后台运行了。
当子进程的父进程挂掉之后,子进程会被1号进程领养。该进程也被称为孤儿进程。
那么为什么要进行领养呢?
原因是孤儿进程会被init进程(1号进程)的进程收养,当然在子进程结束时也会由init进程完成对它的状态收集工作,因此一般来说,孤儿进程并不会有什么危害.