文章目录
一、进程概念
1.进程的概念
我们通常在课本看到对进程的描述是这样的:一个运行起来(被加载到内存)的程序就叫做一个进程,或者称一个在内存中的程序就被称为进程。进程和程序相比具有动态属性
我们知道,计算机系统为了提高整个计算机的效率,在数据的层面,CPU不会直接和外设打交道,而只会和内存进行交互,同样,外设也只会和内存交互;此外,我们编写的C/C++代码经过编译之后形成的二进制可执行程序在本质上就是一个存放在磁盘上的一个文件,所以当我们需要运行这个程序的时候,就必须先将这个文件加载到内存,这是因为CPU需要从内存中读取程序中的代码和数据进行运算(算数运算和逻辑运算)
在我们的程序加载到内存之后,操作系统需要对程序进行管理,而操作系统对程序的管理本质上是对数据的管理,管理方法为先描述,再组织,所以操作系统会将这个程序的属性用一个结构体来进行表示,然后为每一个进程都创建一个结构体对象,最后再将所有的结构体对象使用某一种数据结构组织起来,比如链表,此时,操作系统对进程的管理就变成了堆数据结构的管理的某个节点的管理,在操作系统中,这个用于描述和组织进程的数据结构被称为进程控制块-PCB
2.进程的描述-PCB
进程控制块PCB(process control block):操作系统中用于描述进程的工具,进程信息被放在进程控制块的数据结构中,可以理解为进程属性的集合;Linux操作系统下的PCB是: task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
task_struct可以使用下面的结构体来表示:
c
struct task_struct
{
// 进程的所有属性
// 进程对应的代码和数据的地址
// 写一个进程的地址
struct task_struct* next;
}
Linux中task_struct的部分源码如下:
我们了解task_struct之后,我们可以得出这样的结论:进程=内核数据结构(task_struct)+进程对应的磁盘代码
二、进程相关的基本操作
1.组织进程
所有运行在系统里的进程都以task_struct链表的形式存在内核里。
在Linux中,进程是由操作系统调度和管理的执行中程序。每个进程都被分配了一个唯一的PID(进程标识符),并且可以包含多个线程。
Linux通过使用进程控制块(PCB)来组织进程。 PCB是一个数据结构,用于存储有关进程状态的信息,如进程ID,优先级,内存映像和打开文件等。 Linux将它们链接成单向链表以跟踪所有处于活动状态的进程。此外,还有其他几个数据结构,如等待队列、可用内存空间和保护时钟中断处理程序等,用于在进程执行过程中监视和管理资源。
总之,在Linux中,操作系统通过组织进程控制块来进行进程调度,使得可以对其进行有效的管理和监视
2.查看进程
我们可以通过下面两种方式来查看进程:
1.ps axj 指令配合grep 和管道查看指定进程
c
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("我是一个进程\n");
sleep(1);
}
return 0;
}
2.在"/proc"系统文件夹中查看所有进程
3.结束进程
对于我们自己编写的普通进程来说,我们可以使用[ctrl+c]来结束,也可以使用kill指令,指定-9选择来结束:
4.通过系统调用获取进程标示符
我们可以使用操作系统给我们提供的系统调用接口 getpid() 与 getppid() 来获取进程id和父进程id(进程id是一个进程的唯一标识)
我们可以通过man指令来查看这两个接口的用法:
注:对于函数的返回值pid_t,我们把它当做int即可,打印进程id的时候也使用%d
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("我是一个进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
我们可以看到,我们通过getpid() 和gitppid() 函数得到的值确实是我们进程对应的id,同时,我们发现myproc的父进程是bash,即shell外壳,所以我们可以得出这样的结论shell为了防止自身崩溃,并不会自己去执行指令,而是会派生子进程去执行
另外,同一个程序重新被运行时它的进程id之后它的id和之前就可能不一样了,这是因为它的代码和数据需要重新从磁盘中加载到内存中,但是它的父进程的id不会改变,因为他们都是通过bash来完成的,需要改变的话,退出重新启动服务器即可
5.通过系统调用创建进程-fork初识
我们可以使用系统调用接口fork来创建子进程:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id==0)
{
// 子进程
while(1)
{
printf("子进程:pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id > 0)
{
// 父进程
printf("父进程:pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(2);
}
else
{
perror("fork mail");
return 1;
}
}
我们可以看到,子进程的id为0,ppid是父进程的id,父进程的id是子进程的pid,父进程的ppid是bash,即对于父进程,fork函数返回的是子进程的id,对于子进程,fork返回0
三、进程状态
1.普遍操作系统层面的进程状态
在普遍的操作系统的层面,即站在操作系统学科的角度来说,进程的状态可能有如下几种:运行,新建,就绪,挂起,阻塞,等待,停止,挂机,死亡;其中我们最为重要的是理解以下这三种状态:运行 ,阻塞 ,挂起
或许我们只是听过上面我们所说的进程状态,但是什么是进程状态呢,我们知道,当一个程序被加载到内存变成进行之后,操作系统就需要对该进程进行管理,即为该进程创建PCB对象,而进程状态的本质就是PCB内部的一个属性,用一个整形变量来表示,不同的整形变量就对应不同的进程状态
运行状态
我们知道,程序被加载到内存之后就变成了进程,但是我们可能需要有多个进程需要等待执行,而一般我们的CPU只有一个,而我们的外设资源(硬件)的个数也是远小于进程的数量,此时,操作系统为了合理的分配CPU以及各种硬件资源的分配和更好的调度各个进程,就会为CPU创建一个运行队列,为每一个硬件创建一个等待队列,而让某一个进程处于运行状态的本质就是将该进程的PCB放入到CPU的运行队列中,然后将PCB维护进程状态的变量值修改为相应的值,比如0;
进程PCB里面有进程的各种属性,以及进程对应的代码和数据的地址,所以CPU从运行队列中取出PCB之后 ,就可以根据PCB来得到进程的各种数据和指令,然后执行相应的运算,所以进程处于运行状态并不意味着该进程此刻正在被运行,只要该进程处于CPU的运行队列中我们就称为该进程处于运行状态,这是因为CPU是纳秒级的芯片,运算的速度非常的快,所以只要进程处于CPU的运行队列中,我们就可以认为该进程处于运行状态
阻塞状态
我们知道,计算机的各种硬件资源也是有限的,但是需要使用这些硬件资源的进程却有很多,在同一个时刻硬件只能为一个进程服务,比如同一时刻只能有一个进程向磁盘写入数据,所以为了解决这个问题,当有多个进程需要使用同一个硬件的时候,操作系统会将进程的PCB放入硬件的等待队列中,等待硬件为其服务。进程在等待队列中等待硬件的状态称为阻塞状态,阻塞状态本质上就是将进程的PCB从CPU的运行队列剥离出来,放入硬件的等待队列中,然后将PCB中维护进程状态的变量修改为对应的值,比如1;待该进程获得对应的硬件资源之后,再将该进程放入CPU的运行队列中。此外,并不是只有等待硬件资源进程才会处于阻塞状态,一个进程等待另一个进程就绪,一个进程等待某种软件资源就绪都会处于阻塞状态
挂起状态
我们已经知道,阻塞状态的进程需要等待某种资源,所以它对应的代码和数据在短期内并不会被执行,此时他们扔存在内存中就相当于浪费了内存资源,但是如果当前操作系统处于高IO的时候,就会使得很多进程都在等待外设资源,可能就会导致内存不足,此时,操作系统就会选择将某些处于阻塞状态的进程对应的代码和数据拷贝到磁盘中,然后释放内存中的代码,从而将对应的空间释放出来,让其他的程序进入到内存中让CPU为其执行
所以挂起状态就可以理解为当内存空间不足时,操作系统将在等待资源的进程对应的代码和数据放大磁盘中的状态就称为挂起状态,挂起状态不会移动进程的PCB,只会移动该进程对应的代码和数据。挂起状态不是释放进程,因为该进程对应的PCB仍然处在某种硬件的等待队列之中,当该进行获得对应的资源之后,操作系统就会将该进程的代码和数据从磁盘中加载到内存中继续执行,这个过程的本质就是对内存数据的换入换出,同时阻塞不一定挂起,挂起也不一定是阻塞,也可能是新建挂起,就绪挂起,甚至是运行挂起
【总结】
1.一个CPU有一个运行队列
2.让进程 入队列,本质是将该进程的task_struct结构体对象放入运行队列中
3.进程PCB在runqueue,就是运行状态®,不是这个进程正在运行,才是运行状态,状态是进程内部的一个属性,保存在task_struct中,用一个整形变量来进行表示
4.不要以为你的进程只会等待(占用)CPU资源,你的进程,也随时随地需要外设资源
5.所谓的进程的不同状态,本质是进程在不同的队列中,等待某种资源
2.Linux操作系统的进程状态
上面我们所讲述的是普遍操作系统中进程的状态,下面我们学习具体操作系统Linux的进程的状态:
Linux内核源码中对进程状态的定义如下:
c
/*
* 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 */
};
我们可以看到,在Linux中,进程一共有7种状态,分别为运行,浅度睡眠,深度睡眠(磁盘休眠),暂停,追踪暂停,死亡和僵尸状态
运行状态®
运作状态即进程的PCB位于CPU的运行队列中:
睡眠状态(S)
Linux下的睡眠状态其实就相当于阻塞状态,进程需要等待某种资源:
我们使用ps axj 指令查看进程状态只能查看进程的某一时刻的状态,而外设的速度是远低于CPU的,所以我们就会发现,myprocess也在执行加法运算,但是我们每次查询时都处于阻塞状态,因为99%的时间都在等待硬件资源就绪,只有1%的时间在进行加法运算以及执行打印,所以我们查询的结果基本都是睡眠状态
暂停状态(t)
暂停状态也是阻塞状态的一种,我们可以使用kill 命令,使用-19选项来让一个进程从一个状态变为暂停状态:
我们也可以使用kill -18 让一个进程处于暂停状态回复为原来的状态
但是我们发现,我们将myprocess暂停或者continue之后,进程状态前面的+号消失了,事实上,进程状态后面的+号代表着一个进程是前台进程,没有+号就代表该进程是一个后台进程,对于前台进程我们可以使用ctrl+c将其终止,也可以使用kill命令来完成,但是对于后台进程,我们只能通过kill命令来完成:
追踪暂停状态(t)
追踪暂停状态是一种特殊的暂停状态,进程处于次状态表示该进程正在被追踪,比如我们使用gdb调试进程:
深度睡眠状态(D)
我们知道,当我们的内容不足的时候,操作系统会将一部分进程挂起来节省资源,但是如果内存空间严重不足,挂起已经解决不了问题的时候,操作系统就会自动杀死一些进程
我们以一个例子来说明:当前有一个进程需要向磁盘写入一些数据,这些数据是10万名用户近半年来的转账记录,非常的重要,该进程访问磁盘,让磁盘将这些数据写入磁盘,但是在磁盘写入这些数据期间,该进程属于阻塞状态,因为它要等待磁盘给他返回一个结果,即是否将这些数据写入成功,但是此时,内容严重不足,操作系统将该进程杀掉了,磁盘此时写入失败了,告诉该进程的时候,进程却无法回应,因为该进程已经被杀掉,磁盘只能将这些数据丢弃然后为其他进程提供服务,此时,这些数据就丢弃了
为了防止这种情况的发生,Linux操作系统就设计出了一个深度睡眠(D)状态,处于深度睡眠状态的进程既不能被用户杀掉,也不能被操作系统杀掉,只能通过断电,或者等待进程自己醒来,我们需要注意的是,深度睡眠一般只会发生在高IO的情况下,且如果操作系统通过杀掉进程也无法解决问题的时候,此时操作系统也即将崩溃了
死亡状态(X)
死亡状态代表着一个进程结束运行,该进行对应的PCB以及代码和数据全部都被操作系统回收
僵尸状态(Z)
我们创建一个进程的目的是为了让其为我们完成某种任务,既然是完成任务,那么就应该在进程结束前告诉返回任务的执行结果,供父进程或者操作系统进行读取,所以当一个进程退出的时候,不能立即释放该进程的全部资源,其中对于进程的代码和数据来说,操作系统可以直接将其释放,因为该进程已经退出,不会再被执行了,但是该进程的PCB会被保留,这是因为PCB中存放着该进程的各种状态的代码,特别是退出状态的代码
我们以一个简单的例子来更深入的理解:假如你在操场上跑步,你前面跑步的一个人突然倒下,你上去发现他没有了呼吸,此时你就打电话报警,警察来了之后发现这个人确实已经死亡,但是警察不会立即安排该死者的家属安排后事,而是会通过法医对他进行检查,判断他是因为什么原因死亡,是过度劳累,还是他人谋害,确定死亡原因之后才会通知家属处理后事
上述中跑步的那个人就相当于一个进程,警察就相当于操作系统或者父进程,在进程结束后会对其进行检查
所以僵尸状态就是进程在退出时等待父进程或者操作系统来读取退出状态代码,然后再释放PCB的一种状态
【总结】
我们可以看到,具体的Linux操作系统的进程状态和普遍的操作系统学科上的进程状态是不一定相同的,比如Linux操作系统中没有阻塞和挂起状态,阻塞状态而是通过浅度睡眠,深度睡眠,暂停,追踪暂停等状态来表现出来,而进程处于这些状态时是否会被挂起,我们是不知道的,对于用户来说,操作系统没有必要将挂起状态暴露出来,因为用户不关心一个进程是否处于挂起状态
四、两种特殊的进程状态
1.僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,该进程的PCB就一直得不到释放并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
进程退出的时候,不能立即释放该进程对应的资源,需要保存一段时候,让父进程或操作系统来读取
我们通过下面的代码进行说明:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.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);
}
}
else
{
perror("fork fail");
exit(-1);
}
return 0;
}
我们可以看到,当我们kill子进程之后,由于父进程中还没有对子进程的退出状态进行读取,所在子进程变成了Z状态,并且子进程还提示了defunct(失效的,不再使用的),此时,如果父进程一直不对子进程读取,那么子进程就会变成僵尸状态
僵尸进程危害
1.占用系统资源:维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护僵尸进程占用系统资源,导致系统运行缓慢,甚至崩溃。
2.安全漏洞:僵尸进程可能成为黑客攻击的入口,从而导致系统被入侵,数据泄露等安全问题。
3.影响系统性能:进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。僵尸进程会消耗系统的资源,导致系统性能下降,从而影响用户的使用体验。
4.影响系统稳定性:一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间僵尸进程会占用程序的资源,导致程序无法正常运行,从而影响用户的使用体验。
2.孤儿进程
父进程先退出,子进程就称之为"孤儿进程",此时孤儿进程就会被1号init进程领养
我们可以发现,父进程退出之后并没有变成Z状态,因为父进程的父进程是bash,bash会读取父进程的退出状态,子进程被领养后变成了后台进程
【注意】
1.父进程先退出的情况一定存在
2.子进程会被操作系统领养--1号进程
3.操作系统为什么要领养?如果不领养,那么子进程退出的时候,对应的僵尸,就没有人能回收了
4.被领养的进程--孤儿进程
5.如果是前台进程创建的进程,如果孤儿了,会自动变成后台进程
五、进程优先级
1.什么是优先级
我们首先要和权限进行区分,权限决定的是一件事能不能被执行,而优先级是在能被执行的情况下,这件事情是先做还是后做的问题
cpu资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
2.为什么要有优先级
在我们的计算机系统是有限的,比如CPU只有一个或者多个,但始终是小于进程的数量,硬件如显示器,磁盘,显卡等等都只有一个,而内存中很多进程都要占用资源,所以我们需要指定优先级来合理的分配资源
3.Linux中优先级的特点
Linux中优先级的表示和维护通过两个变量PRI(priority)和NI(nice)来完成,每个进程的默认PRI为80,NI为0,我们 可以通过修改NI的值来调整进程的优先级的大小,NI的变动范围为[-20,19],PRI与NI的值越小,那么进程的优先级就越高
在Linux系统中,我们可以通过ps --l命令来查看进程的优先级:
对于ps -l的其他信息如下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
我们可以使用top命令更改已存在进程的nice:
top 进入top后按"r"-->输入进程PID-->输入nice值
最终的优先级=老的优先级+nice值(老的优先级为80)
六、进程的其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰(每个进程的代码数据和PCB都是独立的,一个进程的死亡不会影响其他进程,包括父子进程,子进程崩溃不会影响父进程)
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
七、进程切换
一般的电脑只有一个CPU,那么在同一个时刻就只有一个进程正在被运行,但是,我们在使用电脑的时候,在同一个时间会运行多个程序,并且这些程序都能够正常的运行,这依靠的是进程转换
进程转换:CPU同一时刻只能运行一个进程,但是CPU的速度很快,所以位于CPU运行队列中的每一个进程都只运行一个时间片,每个进程运行完一个时间片后就会被放到运行队列的尾部或者可能退出,在运行队列中的进程就等待下一次的运行,这样使得在一个时间段内有多个进程都能被运行
CPU在进行进程切换的时候需要进行上下文保护和上下文恢复
我们的进程在运行时会产生非常多的临时文件,同时CPU中存在一套寄存器硬件,当程序进行时,进程的PCB会被放入CPU内的 寄存器中,此时CPU就可以通过PCB得到进程代码和数据的地址,CPU在运行进程时产生的大量的临时数据也会被保存到寄存器中,所以我们在进行进程切换的时候需要进行程序的上下文保护和上下文恢复,即程序停止运行时会将寄存器里面的数据保存起来,进程重新运行时将保存是数据重新放入到寄存器中,以便我们能够接着上次运行的地方接着运行
我们举一个简单的例子来更好的理解:
我们在大学的时候可以选择参加入伍,我们入伍的时候需要告诉学校,保留我们的学籍,否则等我们回来的时候可能就被退学了,这就是上下文保护;我们回来也不是直接去上课,而是需要向学校进行回报,将我们的学籍处理好,继续进行学习,这就是上下文恢复
【总结】
CPU寄存器硬件被所有的进程共享,但是当CPU运行某一具体的进程的时候,CPU寄存器的数据只属于该进程,同时,我们进行上下文保护时保存的是寄存器中的数据,而不是寄存器硬件
进程在切换的时候,要进行进程的上下文保护
当进程在恢复运行的时候,要进行进程的上下文恢复
在任何时候,CPU里面的寄存器里面的数据,看起来是在大家都看得见的寄存器上,但是,寄存内的数据,只属于当前运行的进程,寄存器被所有进程所共享,寄存器内的数据,是每个进程各自私有的--上下文数据
要执行一个程序(指令),先找到这个程序 ./myprocess -> ./ -> 当前路径 ->找到程序