Linux——进程的概念

✅<1>主页::我的代码爱吃辣
<2>知识讲解:Linux——进程
☂️<3>开发环境:Centos7
<4>前言:进程是我们学习操作系统的第一个非常重要的概念,它是担当分配系统资源(CPU时间,内存)的实体。

目录

一.什么进程

二.进程的描述——PCB

三.组织进程

四.查看进程

 五.杀掉进程

 六.获取进程的pid

七.创建子进程

1.fork ()

2.为什么一个fork函数会有两个返回值呢?

3. 创建子进程的本质

4.创建子进程之后

八.进程的状态

1.运行状态:

2.阻塞状态

3.挂起状态

4.睡眠状态(S状态)

5.磁盘休眠状态(D)

6.停止状态(T)

7.死亡状态(X)

8.僵死状态(Z)

 九.孤儿进程


一.什么进程

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

 简单来说:一个加载到内存的程序就是一个进程。所以说进程的与程序的本质区别就是,程序是一个存储在磁盘上的文件,而进程是加载到内存的。

我们运行的一段代码可以说是进程,我们双击打开一个游戏也是一个进程。

这是对进程的粗粒度的理解。

二.进程的描述——PCB

我们知道当我们使用计算机的时候会有大量的进程被打开,那么操作系统是如何管理进程的么?这里谈到管理,首先我们知道管理的前提是对对象先描述,后组织。那么操作系统是如何描述一个进程的呢?

操作系统对进程的描述就是,PCB。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct.

task_ struct内容分类:

标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息

 进程的完整的定义:内核数据结构(PCB)+   对应的代码和数据。

三.组织进程

使用PCB描述完进程,就可以使用合适的数据结构将进程的PCB来进行组织,以后操作系统对进程的管理,就转换成了对数据结构的管理。这个数据结构就是,链表。

Linux——进程的概念_第1张图片

四.查看进程

1.方法一:

命令:ps axj 

查看当前所有的进程。

Linux——进程的概念_第2张图片

 也可以通过配合 grep 使用,筛选出我们想看到的进程。

先准备一段死循环代码:

Linux——进程的概念_第3张图片

 我们编译以后执行它。

Linux——进程的概念_第4张图片

查看进程:

进程的pid:

进程的pid用于区分进程,是每一个就进程有且仅有一个的标识符,相当于我们的学号,或者是公民分身证。

进程的ppid:

进程的ppid,是指其父进程的pid。

 如果一个进程的pid 是另一个进程的ppid ,我们就称该进程是另一个进程的父进程,这两个进程就是具有父子关系。

方法二:

ls /proc/进程pid

我们还可以从根目录下的proc目录来查看进程,我们的进程也可以看成是一个目录,所以再一次应证了,linux下一切皆是文件。

 五.杀掉进程

kill -l ---查看kill指令选项
kill -9 进程pid ————杀掉指定的进程

 kill -9 + 进程pid 就是向指定进程发送9号信号,杀死该进程。

Linux——进程的概念_第5张图片

 六.获取进程的pid

我们可以利用与进程相关的系统调用,来获得进程的pid,和ppid。

相关的系统调用函数是:

getpid()
getppid()

Linux——进程的概念_第6张图片

代码:

  1 #include
  2 #include
  3 #include                                                                                                                                
  4 int main()
  5 {
  6 
  7   int count=0;
  8   while(1)
  9   {
 10     printf("我是一个进程%d,我的pid是%d,我的ppid是%d\n",count++,getpid(),getppid());
 11     sleep(1);
 12   }
 13 
 14   return 0;
 15 }

Linux——进程的概念_第7张图片 在我们每一个运行我们的程序的时候,他每次变成进程以后的他的pid都是不一样的,但是他的ppid却是不变的:

Linux——进程的概念_第8张图片

 简单来说就是:当他的ppid始终不变,也就意味着他的父进程始终都是一个。其实这个父进程就是bash。即使我们的命令行解释器。

Linux——进程的概念_第9张图片

 所以我们得到一个结论:命令行启动的程序,其父进程一般都是bash。

七.创建子进程

1.fork ()

Linux——进程的概念_第10张图片

Linux——进程的概念_第11张图片

#include
#include
#include
int main()
{
    pid_t pid=fork();                                                                                                                                
    printf("我是一个进程\n");
    sleep(1);
    return 0;
}

 我们最直观的发现就是我们的打印语句被执行了两次。

我们可以查看一下进程的pid 和ppid:

#include
#include
#include
int main()
{
    pid_t pid = fork();
    printf("我是一个进程,我的pid是%d,我的ppid是%d\n", getpid(), getppid());
    sleep(1);
    return 0;
}

我们发现我们的打印结果呈现的是两个进程的打印效果,并且这两个进程是一对父子关系,结合我们的men手册发现,fork的作用就是创建子进程,fork对于父进程返回创建的子进程的pid,对于子进程返回0。

2.为什么一个fork函数会有两个返回值呢?

这里通常当我们的fork函数准备开始返回的时候,那么函数的主体功能已经完成了,即当fork返回时子进程已经创建完成了,在执行返回语句时,已经是双执行流了,所以返回语句就会被父子进程都执行,所以也就出现了,一个函数出现两个返回值的现象。

Linux——进程的概念_第12张图片

首先如果我们发现fork之后就会有两个进程,这两个进程还是一对父子关系,并且他们还同时都在执行我们写的同一段代码。那么我们就可以通过让他们返回值的不同,让父子进程执行不同的代码。

例如:

#include
#include
#include
int main()
{
	pid_t pid = fork();
	if (pid == 0)
	{
		while (1)
		{
			printf("我是一个子进程,我的pid%d,我的ppid%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else if (pid > 0)
	{	
		while (1)
		{
			printf("我是一个父进程,我的pid%d,我的ppid%d\n", getpid(), getppid());
			sleep(1);	     }
		}
	}
	return 0;
}

Linux——进程的概念_第13张图片

所以在fork之后父子进程是代码共享的,数据也是共享的。一般情况下在没有对数据进行写操时,父子进程对于数据都是共享使用的,如果有父子进程中的其中一个对数据进行写操作,就会触发写时拷贝机制,来保证父子进程不会相互影响。

写时拷贝:故名思意,在写操作时发生的拷贝,当一个进程需要修改数据时,就会触发,目的是为了,修改的数据不会对另一个进程产生影响,而且不修改数据就不发生拷贝,提高了整的效率。从这里可以体现出进程是具有独立性的。

3. 创建子进程的本质

我们一个进程:就是内核数据结构+其进程对应的代码和数据,创建一个子进程的本质就是,在创建一个子进程的PCB,并且在数据不发生修改时,父子进程共享代码,如果发生修改就会触发写时拷贝。

4.创建子进程之后

  1. fork之后,执行流会变成两个即父子进程。
  2. fork之后,父子进程谁先调度是不确定的,完全由调度器来决定。
  3. fork之后,代码只可读且代码共享,通常可以根据fork返回值,进行执行流分流。

八.进程的状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:

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.运行状态:

运行状态,从字面来说我们会认为运行状态就是进程被cpu执行的状态,实际上并非如此。运行状态是指:进程已经准备好了被cpu调度,在cpu中的运行队列中准备被cpu调度,那么进程其实就是描述进程的PCB和进程对应的代码和数据,其实就是进程的PCB在cpu的运行队列中被维护,此时这个进程就是运行状态。

Linux——进程的概念_第14张图片

举例:

#include
int main()
 {
   while(1)
   {
     ;                                                                                                                                                
   } 
   return 0;
 }

R运行状态(running): 并不意味着进程一定在被CPU执行着,它表明进程要么是在运行中要么在运行队列中。
里。

2.阻塞状态

进程阻塞了,也就是执行流不推进了,从我们用户的角度来说阻塞状态其实就是卡了,从操作系统的角度来说:阻塞一定是进程因为等待某种资源的就绪,而产生的进程不推进的状态。

为什么会有进程阻塞:

进程通过等待的方式,等具体的资源被别人用完以后,再被自己使用,而这里的资源就可以是硬件资源比如,网卡,io设备等。在等待资源就绪的时候,cpu可以执行资源已经就绪的进程,而从使得cpu可以最大限度的发挥他的运算性能。

我们之前见过操作系统的管理方式,先描述,再组织。

那么操作系统,对硬件资源的管理也是通过先描述再组织的方式实现管理的,每一硬件资源也都有自己抽象出来的结构体,在结构体中就会有等待相应的等待队列,即计算的每一个硬件对于进程而言都是一种资源,当我们缺少某种资源时,就需要到对应硬件的等待队列中去等待资源就绪,资源就绪以后进而让cpu执行。而这种在等待资源的过程,我们也叫做阻塞。

Linux——进程的概念_第15张图片

 注意:阻塞状态是一种进程不推进的总成,在内核中没有具体的一个状态叫做阻塞状态,但是有一批状态都叫做阻塞状态。

3.挂起状态

挂起状态,就是一种特殊的阻塞状态。当进程阻塞的时候,代码和数据还在内存中加载着,如果此时内存比较紧张,操作系统就会考虑将正在阻塞的进程的代码和数据,暂时放回磁盘中。此时我们称该进程被挂起。

Linux——进程的概念_第16张图片

4.睡眠状态(S状态)

意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

这个睡眠状态是我们可以查看到的,查看进程的状态:

ps -axj | grap [进程名] 

测试代码:

#include 
#include 

int main()
{
    while (1)
    {
        printf("我是一个进程,我的pid:%d\n", getpid());
        sleep(1);
    }

    return 0;
}
ps -axj | head -n1 && ps -axj |grep test | grep -v grep

Linux——进程的概念_第17张图片

注意:

我们这里代码是一个死循环的代码,为什么我们查到的状态却是S状态?

因为R状态其实是有的,只不过CPU执行太快了,R状态只在一瞬间出现,其他的状态都是在等待显示器的状态,即S状态。

我们使用Ctrl + C 就可以终止我们当前的进程。

5.磁盘休眠状态(D)

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

无法演示,简单描述一下场景:

当我们的磁盘比较容量比较紧张的时候,此时恰巧内存也出现了比较紧张的情况,如果此时有一个进程的一部分数据需要刷新到磁盘上,又因为磁盘的交互速度比较慢,待刷新的数据又比较多,进程又处于占有内存资源的状态,但是有没有实际的工作,此时我们的操作系统。由于内存资源比较紧张,操作系统是不能容忍,没有工作的进程长时间占有内存资源的。操作系统就会将该进程杀死(Linux内核是会杀进程的)。由于磁盘比较紧张,数据刷新失败了,上层进程又被操作系统给杀死了,就会导致这部分数据丢失。

这个场景中出现的角色:进程,磁盘,操作系统,都是没有错的,错的是设计者。解决这个问题也很简单,只要这个进程不被杀死,即使数据刷新失败,数据也不会丢失。

所以该进程只要被设置为D状态,那么该进程就不会被杀死,即使我们使用,kill -9 命令也无法杀死D状态进程,在这个状态的进程通常会等待IO的结束,所以叫做不可中断休眠。

注意:当系统中出现了,D状态的进程,那么必然意味着我们的磁盘非常紧张,内存也好不到哪去。

6.停止状态(T)

T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

Linux——进程的概念_第18张图片

启动程序:

Linux——进程的概念_第19张图片

此时进程的状态我们查询看到的就是S+状态(前面有解释)。

 发送 19 号信号 :

Linux——进程的概念_第20张图片

 查看进程状态:

 发送 18 号信号:

注意:

  1. 首先当发送19号信号以后,进程由S+状态变成T状态。
  2. 当我们给进程发送18号信号以后,进程状态已经由T变成了S,没有了之前的+。这就代表我们的进程由一个前台进程变成了一个后台进程。
  3. 后台进程我们可以继续使用指令,不会影响正常的操作,但是会一直在命令行执行,且无法使用Ctrl  + c 终止进程,可以使用指令  kill  -9  杀死后台进程。

7.死亡状态(X)

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。 

8.僵死状态(Z)

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

 见一见僵尸进程:

测试代码:

#include 
#include 
#include 
int main()
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
    }
    else if (pid > 0) // parent
    {
        printf("父进程 begin -30秒\n");
        sleep(30);
        exit(0);
    }
    else if (pid == 0)
    {
        printf("子进程 begin -5秒\n");
        sleep(5);
        exit(0);
    }

    return 0;
}

Linux——进程的概念_第21张图片

僵尸进程危害:

  1. 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
  3. 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  4. 内存泄漏?是的!

 九.孤儿进程

  1. 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  2. 父进程先退出,子进程就称之为“孤儿进程”
  3. 孤儿进程被1号init进程领养,当然要有init进程回收喽。

 见一见孤儿进程:

Linux——进程的概念_第22张图片

 

你可能感兴趣的:(linux,linux,服务器)