一般课本上对进程是这样定义的:一个可执行程序被执行后就变成了一个进程。
站在操作系统的角度则得出:进程是担当分配系统资源的实体。
而如果让我给进程一个定义,我应该会给出这个:
进程 == 内核数据结构( PCB) + 该进程对应的代码和数据。
操作系统是一个软硬件资源管理的软件,因此进程也要被操作系统(OS)管理。
那么操作系统是如何对进程进行管理的呢?
比如说我们要写一个学生管理系统,假设我们在里面要对学生成绩进行排序,那么我们就要先知道每个学生的成绩,进而进行操作。
操作系统对进程的管理也是一样的,需要知道进程的各种属性,我称其为对进程的描述,但是操作系统也可能并非直接对进程进行操作,而是对各种软硬件进行利用,进而进行操作,这个过程我理解为组织,因此 可得出操作系统对进程的整个操作:
进程操作 == 先描述 + 再组织
因此我们用什么描述进程?
在大部分操作系统的书籍上称描述进程的结构体为pcb,在linux操作系统下这个结构体叫做task_struct,这是在操作系统内核中创建的一种数据结构,其内部记载了进程的全部属性。
因此我们对进程的操作的目的是什么?
我们对进程操作,大体上是通过pcb的描述,从而对进程的代码和数据进行计算和处理。
那么我们对进程就有了初步的概念:
因此 进程 == 内核数据结构( PCB) + 该进程对应的代码和数据。
但是一台计算机上会同时有多个进程(你可以打开你的任务管理器,看到许多进程正在跑着),操作系统是如何将这么多进程组织起来的呢?
例如我的电脑:
各个进程的PCB结构体通过 链表 的形式连接起来。
其在整个计算机中 大概以这样的流程图存在:
大概有以下东西:
标示符: 描述本进程的唯一标示符,用来区别其他进程(这里的标示符指的是PID)。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
proc文件夹内的每个蓝色数字(进程的PID)标示的文件夹,就是关于每个进程的信息。
进入 12233 文件夹内 可以看到进程12233的各种信息
这条指令不仅用来查看 所有进程 ,我们也可以用grep指令 来组合起来 查看 指定进程。
我们都知道 bash外壳也属于一个 进程 ,我们用ps和grep组合起来查看一下bash进程
ps -axj| grep bash
同时我们也可以和 head -1 && ps -axj 组合起来 显示查看进程的第一行代码 也就是标识符号 。
ps -axj | head -1 && ps -axj | grep bash
同时我们也知道,命令行中的每一行命令,都是一个程序,使用时也就变成了一个进程,那么,grep命令也一样。
我们通过 grep -v 屏蔽的内容 的方式 屏蔽我们不想看到的进程
比如说屏蔽grep 进程
ps -axj | head -1 && ps -axj | grep bash | grep -v grep
直接输入 top 查看
按 q 退出
我们还可以在自己的程序中通过系统接口来查看进程的标识符。
getpid 返回的是当前进程的id。
getppid 返回的是当前进程父进程的id。
如:
代码结果为:
我们知道我们自己写的程序是通过bash来创建子进程执行的,所以使用命令行bash启动的程序,其父进程都是bash。
我们可以通过 fork 这个系统接口 来 创建一个进程 。
fork这个函数是一个很特殊的函数,他在成功创建子进程后有两个返回值,给父进程返回子进程pid,给子进程返回 0,如果创建失败那么就返回 -1。
为什么会这样呢?
归根结底是因为fork是个函数,这个函数在创建子进程的时候,是在函数中间直接创建了个子进程,那么我们都明白 进程 == 内核数据结构( PCB) + 该进程对应的代码和数据 ,那么这些东西,对于子进程来说,我们该如何获取呢? 、
答案是,直接继承父进程的所有东西,把所有的内容直接拷贝进 子进程(暂时这么理解,实际上是发生了写时拷贝),既然子进程继承了父进程的所有内容,那么,在子进程中,我们是不只是还在执行fork函数呢?只是 这时 创建子进程的代码已经执行完毕,但是,return 语句 一定还没有 发生 。
因此,当return时,父进程和子进程 分别return 两个内容 (这里我们应该是 通过 if else 语句 判断返回内容的)/
例如:
#include
#include
#include
using namespace std;
int main()
{
pid_t id=fork();
if(id<0)
{
cout<<"进程创建失败"<
执行结果为:
上面写的代码是一个死循环的代码,会一直跑下去,我们要如何终止该进程呢?
ctrl c 直接停止
kill -9 pid标识符 (通过信号)杀死进程
ctrl c 直接停止
kill -9 pid标识符 杀死进程
杀掉子进程
杀掉父进程
进程状态的概念: 进程状态反映进程执行过程的变化。
我们都知道,进程==PCB+代码和数据
那么进程状态就应该体现在PCB里面,由操作系统检测PCB得出该进程到底处于什么状态。
这些状态随着进程的执行和外界条件的变化而转换。 在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。
我们都知道,在计算机上,被称为 处理器的只有CPU,那么,是不是在CPU中运行的程序?才会被称为运行态呢?
答案是错误的。
在操作系统中,为可以直接被CPU调度的进程提供了一个运行队列,其一个进程的PCB在运行队列中,我们就称进程处于运行态。
运行态:在 运行队列中的 存在的进程。
这其实也很好理解,当我们去处理某些事情的时候,我们必须将某种顺序排列进行处理,运行队列便是这种顺序队列。
但是我们的任务越多队列越长,运行队列越长表示我的压力机就越大,为了缓解CPU压力,我们引入了就绪态。
当我们的进程需要的资源已经全部就绪,但是还没有处于运行队列的时候,我们就称这种状态为就绪态。
就绪态:当进程需要的资源除了CPU之外全部准备就绪的时候,我们就称这种状态为就绪态。
同样,就绪态也有一个就绪队列来维持。
当我们的CPU运行队列里面的进程越来越少时,就将就绪队列里面的进程按顺序一个个进入运行队列。
当我们的进程需要某种CPU之外的资源时,无法迅速得到该资源,但是我们又不能一直等待该资源(因为会拖慢整个队列的运行),那么这种进程就会进入阻塞状态。
比如说:当我们的进程处于运行队列的时候,此时进程刚好执行到scanf 函数,若是我们一直在屏幕上不输入内容的话,这样是不是就大量占用了CPU的资源?为了保证CPU的运行效率,此时我们就会把这个进程的状态改为 阻塞态。
阻塞:进程等待某种资源就绪的过程。
我们从上文中得知,运行状态有运行队列,那么阻塞状态有没有阻塞队列呢,假如我们启动了多个进程,这些进程都在执行scanf命令,并且 我们都很长时间没有输入,这些进程该怎么办?答案是像运行队列一样,一个个进入阻塞队列。
当然,每个给予资源的外部设备都有一个阻塞队列,这自然是 方便 对进程 先描述 在组织,不同的进程需要的资源不一样,如果把他们胡乱排在一个队列里,操作系统的效率变会下降,因此我们将每个外部设备整一个阻塞队列,这样就方便我们对进程整体管理。
在不少系统中进程只有上述三种状态,但在另一些系统中,又增加了一些新状态,最重要的是挂起状态:
挂起态可以理解为暂时被移出内存的进程, 我们都知道,计算机的资源都是有限的,当我们的电脑资源不足时,我们的操作者的感受是什么?
电脑运行缓慢,或者说卡顿。
这就是挂起态的基本场景,当我们的电脑资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存写入硬盘(进入挂起态),当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态(不太可能直接进入运行态)。
同时,我们只是将进程中的代码和数据移入硬盘,将PCB留在内存中,以便操作系统还能恢复进程。
挂起:当内存资源紧张时,操作系统通过合理安排,把一部分进程的代码和数据移入硬盘,仅仅把PCB留在内存中,此时进程就处于挂起态。
在Linux系统中,下面的状态在kernel源代码里定义:
/*
* 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 */
};
具体含义如下:
1、R状态(运行态)
Linux中创建了“运行队列”(双向循环链表)来调度(scheduling)进程。在该队列中的进程都是R状态,即:可运行状态。该状态有两种情况:
进程正在运行
进程正在运行队列中等待被调度。
示例:
#include
#include
using namespace std;
int main()
{
int n=1000000000;
int sum=0;
for(int i=0;i<=n;i++){
sum+=i;
}
cout<
ps:代码只是作为示例,有点小瑕疵别介意,看见右面那个r就行了
2、S状态(阻塞态)
Linux中创建了“等待队列”(双向循环链表)来维护正在休眠(或者说被挂起)的进程。当进程正在等待某事件完成时(比如等待文件有数据可读),OS就会将它标记成S状态,将其 从运行队列移除并加入等待队列。
当事件完成后,OS用信号将其唤醒并重新将它加入等待队列。
由于休眠状态的进程可以通过信号kill掉,因此该状态又被称为“可中断睡眠状态”。
示例:
#include
using namespace std;
int main()
{
int t;
cin>>t;// 我们将一直不输入 t 让他维持
return 0;
}
3、D状态(挂起态)
处于该状态的进程同样是在休眠,但是与S状态不同,D状态的进程不能被信号中断,也同样不能被kill,因此该状态又被称为”不可中断睡眠状态“。注:当进程正在等待设备IO时(磁盘IO、网卡IO等),操作系统为了保护它而将其设为D状态,避免进程IO被中断。
这个真不能示例,真出现这种情况,一般而言服务器也将近崩溃。
4、T状态(Stopped)
通过SIGSTOP发送停止信号可以将某一进程设为T状态,此时该进程会停止运行,直到使用SIGCONT信号将其唤醒。
kill -l 指令 列出所有控制进程的信号
示例:
#include
#include
#include
int main()
{
while(1)
{
;
}
return 0;
}
解除:
5.T状态(Tracing Stop)
又称为t状态,进程在调试断点处停止时会处于该状态。
这个我真不想示例,有兴趣可以自己用gdb调试一下。
6. X状态(Dead)
进程在真正死亡之前会处于X状态,接着就会被操作系统释放PCB,该过程非常短暂,很难被ps命令捕捉到。
Z状态(Zombie)
当进程退出时,会自动将自己退出时的相关信息写入PCB中,供操作系统或父进程进行读取。
在Linux下,对于一般的进程,其退出信息会由bash进程自动读取。
但是对于用户进程中创建的子进程,如果它先于父进程退出而父进程没有进行等待来读取子进程的退出信息,那么子进程就会处于Z状态,该子进程又被称为“僵尸进程”。
僵尸进程无法被kill,因为它是已死亡的进程。
Z状态的子进程需要父进程读取它的退出信息后才会释放PCB(即真正的死亡)。如果父进程创建了大量子进程而不进行等待,那么这些没有被释放的PCB就可能造成资源浪费。
示例如下:
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
int cnt=5;
while(cnt--)
{
cout<<"My is child"<<" "<
父进程回收子进程后退出
当父进程退出,而子进程依然在运行或者处于僵尸状态,那么这些子进程就被称为“孤儿进程”。
示例:
#include
#include
#include
int main()
{
pid_t id=fork();
if(id<0)
{
printf("fork fail\n");
return -1;
}
else if(id==0)
{
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else
{
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
可见 28054的 ppid一开始为18084 后来为1
init进程的pid为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。
孤儿进程会被init进程“领养”:
对于非僵尸孤儿进程,它们运行结束后会由init进程负责等待;
对于僵尸孤儿进程,init会在适当的时侯统一进行清理(比如系统资源非常短缺或者僵尸进程积累过多时)。
当我们人在生活的时候,肯定会优先做最需求的事物,你饿了按照常理说应该就要去吃饭,渴了就要去喝水,根据自身需要 选择对自己最有帮助的事物。
而计算机通过优先级来判断 哪个进程对自己最有帮助,从而先执行对自己最有利的进程。
优先级是系统用来衡量进程重要性和按序分配资源的重要标准。
优先级越高,进程就会越早得到资源。
使用top
命令查看进程优先级:
其中,
PR:优先级值(Priority),PR值越低说明优先级越高。(一般而言我们的进程基准值为80)
NI:nice值,取值范围[-20,19],用来在PR基准值的基础上调整进程的优先级。
PR = PR基准值 + NI,其中基准值为80,NI默认为0。
top 指令修改已经存在进程的nice值
1,top指令
2,输入r
3,输入要修改的进程的PID
4,输入新的nice值
注:修改nice值可能需要root权限。
注意:普通用户修改进程nice其取值范围是[-20,19],一共40个级别
按照上面说的我们的PRI值越小,优先级越高,那我们如果我们想要的进程优先级改的特别小,那cpu是不是就可以一直给我们的程序跑了,但Linux操作系统不允许这样做,这样就不能保证其他进程的公平性,但是万一有的时候必须修改一些优先级,所以才有有了我们的nice,给你一个范围,在这个范围里面可以随便去修改(但是不建议修改),我们看到我们一般程序的默认优先级是80,加上nice就是[60,99]。
(操作系统:我觉得你修改的优先级都是修的垃圾,给你个小范围自己玩玩就行了,别动我的东西)
如果是root用户,你们可以自己试一下自己的最大最小范围。