linux操作系统——进程(一) 进程概念

什么是进程呢?

运行起来的程序?在内存中的程序?    这样理解进程说明你还并不能把进程的概念描述清楚。

进程vs程序

程序是我们在编写代码之后编译链接生成的可执行程序。那进程呢?是把磁盘中的可执行程序通过加载拷贝的方式加载到内存中就是进程嘛?

其实在我们的程序加载到内存 之前一定会有一个老大哥——操作系统加载到内存中,而程序是操作系统将其进行加载拷贝到内存当中的。再来看,我们平常使用计算机的时候打开任务管理器发现不止一个进程在运行,如下图

linux操作系统——进程(一) 进程概念_第1张图片

是不是有多个进程在运行啊。那么说明操作系统内可能会同时存在多个“进程”,那么既然这么多“进程”,操作系统要不要管理所有的“进程”呢?答案是。那么操作系统如何管理呢?结合我们前面冯诺依曼体系结构那篇文章的理解我们知道,应该先描述,再组织。操作系统是用C语言写的,所以操作系统要想管理进程,那么就必须用C语言来描述进程,通过进程的各种重要属性来描述进程,我们很容易想到结构体。这么多进程,结构体里面可以放一些结构体指针将他们链接起来,那么对进程的管理就转变成了对链表的增删查改

linux操作系统——进程(一) 进程概念_第2张图片

上图中的PCB叫进程控制块,全称process control  block.

所以进程=可执行程序+内核数据结构(目前以PCB为例)

相比于程序,进程多了在内存中的内核数据结构,而这里的的内核数据结构就是为了方便操作系统对进程做管理。

进程概念:

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

描述进程-PCB:

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

task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

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

已经初步有了进程的框架了,那么我们下面开始来了解task_struct的核心字段都有哪些?

首先我们来谈谈进程的唯一标识符,叫做pid, 即process  id.

下面我们在linux系统下编写一段代码来演示进程的pid,包括如何获取进程的pid,如何通过指令查看进程的pid.

首先我创建一个自动化构建工具,输入touch Makefile

回车之后这个文件创建完毕,然后我们再创建一个mycode.c文件

然后vim Makefile

写上以下代码:

linux操作系统——进程(一) 进程概念_第3张图片

这样就可以通过简单的指令来编译代码,重新编译前可以将生成的可执行程序删除再编译,这样会更方便。

然后在mycode.c中写上以下代码:

linux操作系统——进程(一) 进程概念_第4张图片

  1 #include
  2 #include                                                    
  3 int main()
  4 {
  5     int i = 0;
  6     while(i<=100)
  7     {
  8         printf("我已经是一个进程了。。。。:%d\n",getpid());
  9         sleep(1);
 10     }
 11     return 0;
 12 }

保存退出之后输入 make指令会生成一个mycode的可执行程序,然后通过指令./mycode运行该程序

在linux系统下呢可以通过系统调用接口获取进程的pid,需要包含头文件#include

linux操作系统——进程(一) 进程概念_第5张图片

运行之后是这样

linux操作系统——进程(一) 进程概念_第6张图片

然后我们可以通过(我用的是云服务器xshell)这个窗口右键复制SSH渠道可以再弹出一个相同用户的不同窗口,同样可以对该系统进行操作

linux操作系统——进程(一) 进程概念_第7张图片

然后输入这行命令就可以看到对应进程的PID的信息了,这里我们可以看到是1630

linux操作系统——进程(一) 进程概念_第8张图片

然后我输入kill -9 1630

把这个进程杀死了,另一个窗口的该进程则被停下来了

linux操作系统——进程(一) 进程概念_第9张图片

kill掉之后我们再去那个窗口查看这个进程是否还存在

linux操作系统——进程(一) 进程概念_第10张图片

发现该pid为1630的进程不见了

然后呢,我再通过运行该进程再进行查看

linux操作系统——进程(一) 进程概念_第11张图片

继续输入查看的指令发现又出现了。

linux操作系统——进程(一) 进程概念_第12张图片

而且pid变化了,变成了1946  每次运行分配的pid都不一样。

但是他的PPID我们发现并没有改变。

所以呢,我们可以很清楚的知道这就是进程。

我们运行的所有的指令,软件,自己写的程序,都是进程。

下面我们以windows为例,再解释以下进程

一开始我没有打开计算器的时候,没有计算器这个进程。

打开之后我们立马可以看到计算器这个进程。

linux操作系统——进程(一) 进程概念_第13张图片

想要更加了解Linux操作系统里面的进程PCB结构体的话可以去

linux内核源代码中下载源代码进行查看

进程还有自己的父进程id ,命令行中,父进程一般是        命令行解释器,也就是bash.

linux中创建进程的方式:

1.命令行中直接启动进程----手动启动

2.通过代码来进行创建

启动进程,本质就是创建进程,一般都是通过父进程来创建的!

我们前面说到进程PCB是由操作系统创建的,进程PCB的确是操作系统创建的,但是PCB创建出来的时候里面的各种属性都是没有的,所以该进程一般以父进程为模板,把父进程的属性填充到子进程的PCB当中,有点像我们对象的初始化。

我们命令行 启动的进程都是bash的子进程。

那么如何获取ppid呢?

通过一个系统调用接口:getppid()

linux操作系统——进程(一) 进程概念_第14张图片

输入这行命令ls /proc/会显示一个进程的动态的目录结构

linux操作系统——进程(一) 进程概念_第15张图片

再查看根目录下的目录结构

linux操作系统——进程(一) 进程概念_第16张图片

这个proc就是process(进程的缩写)

我们看到上面那张图呢,有很多蓝色的数字,这些数字我们可以很容易的猜测到它可能就是我们进程的pid

现在呢,我把一个进程给启动起来

linux操作系统——进程(一) 进程概念_第17张图片

然后呢我们直接复制SSH渠道再打开一个窗口,在另一个窗口输入命令ls /proc/4682

linux操作系统——进程(一) 进程概念_第18张图片

我们只查看目录

然后我们把这个进程按住ctrl+c停止掉

linux操作系统——进程(一) 进程概念_第19张图片

再去查看这个目录

linux操作系统——进程(一) 进程概念_第20张图片

发现这个目录的这个进程不见了

然后重新运行该进程

linux操作系统——进程(一) 进程概念_第21张图片

pid变了,先查看该进程在proc下的目录,然后ctrl+c之后再查看一次,发现运行的时候有,一旦ctrl+c他就找不到了

linux操作系统——进程(一) 进程概念_第22张图片

所以我们可以得出一个结论,proc目录其实是一个动态的目录结构,存放所有存在的进程,就是以这个进程的id命名的。

下面我继续让这个进程跑起来

linux操作系统——进程(一) 进程概念_第23张图片

然后在另一个窗口通过make clean指令把该可执行程序删除了,

linux操作系统——进程(一) 进程概念_第24张图片

然后再去/proc目录结构查看该进程的情况发现,进程知道自己的可执行程序被删除了

linux操作系统——进程(一) 进程概念_第25张图片

以上说明,一个进程能够找到自己的可执行程序。

就好比,你从自己的老家进入了大城市生活,你还是记得自己老家的位置。

上面我们都是通过手动启动进程,下面我们通过了解一下用代码创建进程,在使用和特征上介绍一下创建进程的函数fork(),这是一个系统调用函数。

启动一个进程,如何理解这种行为呢?本质就是操作系统多了一个进程,OS要管理的进程也就多了一个,进程=可执行程序+task_struct对象(内核对象)。

创建一个进程,就是系统中要申请内容,保存当前的可执行程序+task_struct对象,并将tast_struct对象添加到进程列表中。

linux操作系统——进程(一) 进程概念_第26张图片

下面我们来编写一段代码,调用一下fork()函数。

linux操作系统——进程(一) 进程概念_第27张图片

在第一个窗口执行该代码生成的可执行程序,然后在第二个窗口执行以下命令:

while :; do ps ajx | head -1 &&  ps ajx | grep myprocess | grep -v grep; sleep 1;echo "---------------";done

linux操作系统——进程(一) 进程概念_第28张图片

停止之后:

linux操作系统——进程(一) 进程概念_第29张图片

分析这几个进程之间的关系:

linux操作系统——进程(一) 进程概念_第30张图片

结合目前:只有父进程执行fork()之前的代码(一定的),fork之后,父子进程都要执行后续代码!

这个函数返回值的描述是,如果该函数调用成功了,那么子进程的pid会被返回给父进程,子进程返回一个0,如果函数调用失败了,那么会给父进程返回一个-1,子进程不会被创建。

我们发现,一个函数竟然有两个返回值???

下面我们先验证一下fork的返回值。

修改代码为如下图中代码:

linux操作系统——进程(一) 进程概念_第31张图片

linux操作系统——进程(一) 进程概念_第32张图片

fork代码的一般写法:

1.我们为什么要创建子进程?我们想让子进程协同父进程一起完成一些工作,这些工作是单进程解决不了的。

2.我们创建子进程是为了让父进程跟子进程做一样的事情吗?

不是,我们创建子进程,就是为了让子进程跟父进程做不一样的事情,执行不一样的代码。

那么该如何验证呢?

可以通过判断fork的返回值,判断谁是子进程,谁是父进程,然后让他们执行不同的代码片段。

linux操作系统——进程(一) 进程概念_第33张图片

然后我们编译这段代码,然后运行。可以看到如下结果

linux操作系统——进程(一) 进程概念_第34张图片

下面我们从以下几个问题来分析fork的原理:

1.fork干了什么事情?

linux操作系统——进程(一) 进程概念_第35张图片

上图说到,既然父进程跟子进程会共享代码和数据,会执行相同的代码,那么fork之前的代码呢,子进程能看到吗?答案是能看到。

为什么子进程不从头开始执行呢?

为什么我们的程序从上往下按照顺序去执行

pc/eip寄存器执行fork完毕,eip指向的是fork后续的代码,而eip也会被子进程继承。所以只执行从fork之后的代码。

2.为什么fork会有两个返回值?

linux操作系统——进程(一) 进程概念_第36张图片

根据我们之前对函数的理解,如果一个函数已经执行到return 语句了,那么它的核心工作做完了嘛?答案是:已经做完了。

linux操作系统——进程(一) 进程概念_第37张图片

前面我们说到子进程跟父进程代码和数据会共享,那么return之前,子进程已经被创建出来了,而return 语句也是代码,所以return 语句也会被子进程共享,所以父进程子进程被调度都会执行return 语句,从而造成了fork函数有两个返回值。

真实情况是,操作系统是通过一些寄存器做到的返回值返回两次。但是上面跟易于我们理解。

3.为什么fork的两个返回值,会给父进程返回子进程的pid,给子进程返回0呢?

 在现实生活中呢,父亲:儿子=1:n.  也就是说父亲可以有多个儿子,而这些儿子的父亲只有一个,父亲有多个儿子的时候需要给他们都取个名字代表他们的唯一性,同样的父进程要管理子进程就需要标识他们唯一性的pid所以子进程要给父进程返回自己的pid才便于父进程管理,而子进程他们的父亲只有一个,不需要进行区分,有唯一性,所以只需要返回0表示自己创建成功即可。

4.fork之后,父子进程谁先运行?

 创建完成子进程只是一个开始,创建完成子进程之后,系统的其他进程,父进程,子进程接下来是要被调度执行的,当父子进程的PCB都被创建并在运行队列中排队的时候,哪一个进程先运行是不确定的,因为哪一个进程的PCB先被选择调度,哪个进程就先运行,然后谁会先被调度是由各自PCB中的调度信息(比如说 时间片,优先级等)+调度器算法共同决定的,简而言之就是操作系统自主决定。

5.如何理解同一个变量,会有不同的值?

linux操作系统——进程(一) 进程概念_第38张图片

linux操作系统——进程(一) 进程概念_第39张图片

如果启动一个qq,启动微信,启动浏览器,这些都是进程,杀掉微信,qq还在吗?浏览器进程还在吗?很明显,进程还在。

如果父子进程,父进程被杀掉,子进程还在吗?子进程被杀掉,父进程还在嘛? 答案是:还在

结合以上两点,我们可以推出,进程之间运行的时候是具有独立性的,无论是什么关系?那么系统是如何做到的呢?

进程的独立性,首先是表现在有各自的PCB,进程之间不会相互影响 !而代码又是只读的,不会影响!但是数据父子进程是会修改的!代码共享,数据各个进程必须想办法各自私有一份。

但是一般如果不对数据进行写入的话,数据是不会发生拷贝的,只有当写入时,才会进行拷贝,也就是写时拷贝。

linux操作系统——进程(一) 进程概念_第40张图片

fork()要return 一个返回值给id赋值:返回的本质是写入,id是父进程的变量,保存的也是数据,返回的时候发生了写时拷贝,所以id变量也被拷贝了,id的值被不同的fork函数给赋值了,所以造成了同一个变量有两个返回值。

但是呢

我们执行如下代码:

linux操作系统——进程(一) 进程概念_第41张图片

运行之后:

linux操作系统——进程(一) 进程概念_第42张图片

我们发现:

怎么可能同一个变量,同一个地址,会有不同的内容?我们在这里只能得出一个结论:这个地址,绝对不是物理地址!!!

6.重新复盘代码,重新理解!

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