Linux之进程

先做一个进程的简单的介绍:

  • 课本概念: 程序的一个执行实例或正在执行的程序

  • 内核观点: 担当分配系统资源(CPU,内存)的实体

进程的概念

我们给出一个结论:

我们以前任何启动并运行程序的行为,都是由操作系统帮助我们将程序转化为进程,来完成特定的任务。

那我们平时自己写好的可执行程序,我们知道它本质其实就是一个二进制文件,那我们运行这个可执行程序,首先它被载入内存,其实就是把可执行程序里面的指令和数据加载到内存。 

那此时它就变成一个进程了吗?一个人进入到学校,他就是学校的一名学生了吗?

不是的。那同样的,一个可执行程序或应用程序被加载到内存里面,他就变成了被操作系统管理的进程了吗?好像也有点不合理。

那一个人如何才算学校的学生呢?

是不是它的学籍要在学校里,它的信息要在学校的教务管理系统上,这才是最关键的。然后第二个问题,我们可能同时运行多个程序,那他们都要加载到内存里,就好比学校里面有好多学生,那学校肯定要对这么多学生进行一个良好的管理。那同样的,操作系统也要对加载到内存的多个进程进行管理。

如何进行管理呢? ——先描述,再组织


PCB初认识(os是怎样管理进程的?) 

在冯诺依曼系统中讲到,管理的本质不是真正的管理实体本身,而是管理它的数据.虽然现在我们不知道什么是进程,但是我们可以通过先描述,再组织这一结论得出,操作系统管理进程肯定也是管理进程对应的数据.

我们每运行一个程序,除了要把它对应的指令和数据加载到内存,操作系统还会为它创建一个PCB来记录和管理进程信息(先描述),那操作系统要管理这么多的进程,就可以把所有进程的PCB用一个数据结构比如链表管理起来(再组织),那此后操作系统对于进程的管理就变成了对组织PCB的数据结构的管理

这个进程属性的结构体被称为PCB, 也叫进程控制块,PCB是进程存在的唯一标识。

PCB是这个结构体的总称,在Linux系统下,PCB具体叫做:struct task_struct

task_struct—PCB的一种

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

task_ struct里面包含了以下内容:

标示符: 描述本进程的唯一标示符,用来区别其他进程。


状态: 任务状态,退出代码,退出信号等。


优先级: 相对于其他进程的优先级。


程序计数器: 程序中即将被执行的下一条指令的地址。


内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针


上下文数据: 进程执行时处理器的寄存器中的数据


I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。


记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。


其他信息:..

可以在内核源代码里找到它,所有运行在系统里的进程都以task_struct链表的形式存在内核里。

然后呢我们上面提到:

可执行程序其实就是一个文件,而文件的话我们说过文件=内容+属性
那程序运行载入到内存里面的指令和数据是文件的内容。
然后呢我们有提到每个进程操作系统都会创建一个PCB来描述进程,可以理解为PCB就是进程的属性集合。

那请问这里的PCB即进程属性的结合跟可执行文件的属性有没有关系呢?

其实呢有关系,但是关系不大。
磁盘上文件的属性一般就是文件名、文件类型,位置,占用空间这些东西。
PCB呢,它是由操作系统动态创建和维护的一种内核数据结构,它里面包含的进程的属性都是操作系统自己获取和记录的,跟文件的属性不是一回事,没什么关系。
其实也稍微有点关系,后面会带大家看。

 那一个程序被加载到内存,他就是进程了吗?不是的
进程包括了程序加载到内存中的指令和数据,以及内核中与之关联的进程控制块(PCB)

 所以: 进程=程序加载到内存中的指令和数据(可执行程序)+内核中与之关联的进程控制块(PCB)


CPU对于进程列表的处理 

进程被链接在链表中会等待CPU去PCB找数据做处理,那么CPU怎么知道要处理哪些数据呢?这不得不提到进程排队的概念: 

把对应的PCB从链表中提取到队列中排队,PCB中的数据不会一次性被CPU处理完,它有时被处理有时在等待被处理,这是一种动态运行的特征,请看下图: 

讲这些概念是想让大家了解,操作系统内对于进程的控制十分复杂,不要认为一个进程的PCB只能在一张链表里,一个PCB可能链接到多个数据结构上,比如这里的PCB既链接在管理PCB的链表上,又链接在排队队列的队列中!

struct dlist
{
    struct dlist* prev;
    struct dlist* next;
}


struct task_struct
{
    //进程的各种属性
    //..

    struct dlist list;//系统所有进程所在的链表
    struct queue;//同时这个进程也可以在队列中

    //也可以在其他数据结构中
    //..
}

通过taks_struct内的数据结构的地址,通过偏移量的方式找到taks_struct的地址,然后访问成员变量,以链表为例:

#define curr(list) (struct task_struct*)(int)&list - (int)&(task_struct*)0->list
curr(list)->PCB内的元素

用链表的地址减偏移量就可以得到task_struct结构体的地址:

更多关于PCB的信息:

Linux系统PCB源码的阅读与分析_nr_task-CSDN博客


进程标识符:pid

每一个进程都有自己对应的pid,查看当前进程的信息:

使用指令: ps ajx

这样查看的是所有的进程

现在写一个死循环程序并运行,让它一直处于进程运行的状态: 

 #include<stdio.h>  
 #include<unistd.h>  
 int main()  
 {
     while(1)//死循环
     {
         printf("我现在是一个进程了\n");
         sleep(1);//休眠一秒                                                                                                                                         
     }                                       
     return 0;                           
 }

查看进程 

现在让程序运行起来,再去查看进程,那我们如何查看这个进程呢?

方法1: ps指令

在所有进程中搜索我刚刚写的可执行程序:

使用指令: ps ajx | grep mybin

 将进程信息的第一行打印出来:

使用指令: ps ajx | head -1

两条命令同时显示:ps axj | head -1 && ps axj | grep myprocess

简单解释一下这条命令,这是是逻辑与连接了两条命令,首先ps axj可以显示当前系统中所有进程的详细信息,但是我们不想看所有的,所以管道连接head -1就是去只显示ps axj展示出来的所有信息的第一行(即那个表头信息),然后&&后面又连接一条指令,其实就是过滤取出关键字mycode对应的进程信息.

同时运行两个程序就可以显示两个进程状态: 

我们看到有个PID,就是我们上面提到的进程的唯一标识符。
它们两个是不一样的,所以它们两个是不同的两个进程,虽然是同一个可执行程序运行生成的。



方法2: /proc文件

首先先介绍获取pid的函数

系统调用函数:getpid

 这里有一个系统调用的节后函数可以直接返回当前进程的pid,由于操作系统是由C语言编写的,所以可以直接在程序中调用此函数:

使用函数:getpid

 

/proc查看进程

还可以通过 /proc 系统文件夹查看进程信息 ,proc其实就是process的缩写,/proc 目录是 Linux 系统中的一个特殊目录,提供了有关当前运行进程和内核状态的信息。

需要注意的是,它跟普通的文件不一样,它不是一个真正的文件系统,而是通过内核在内存中维护的一个虚拟文件系统。只有当操作系统启动的时候,它才会存在,并不存在于磁盘上

proc是一个动态的目录结构,存放所有存在的进程,目录的名称,就是以这个进程的id命名的:

这些数字是蓝色的,我们知道蓝色表示它是一个目录/文件夹。
所以:一个进程被创建好,操作系统会自动在proc目录下创建一个以新增进程的PID命名的文件夹

我们可以进去看看: (这里是 cd /proc/3153, 因为每次程序运行pid都会变)

这些内容其实就是当前进程的相关属性信息, 其中cwd默认是文件当前目录,exe指向可执行程序的位置,

由exe的指向,可以得出结论1: 一个进程可以找到自己的可执行程序! (cwd在下面介绍)

我们当前是在proc里面这个进程PID对应的这个目录里面的,上面说了PID对应的目录是进程创建的时候才会在proc目录下新增的。


那如果我们把对应的进程终止(CTRL+c):

再想查看这个目录的内容就不行了:

上一级目录也回不去了:

因为进程终止,操作系统就会在proc目录下把这个进程PID对应的目录及其里面的内容删除掉。所以proc目录里面的内容是动态变化的。 

当前我再创建一个进程,查看/proc/5194进程目录下的 内容,没什么问题:

 此时在程序运行时我删除掉这个进程,发现进程依然在运行,但是再次查看进程目录下的内容,发现提示exe已经被删除了!

为什么删除exe文件后,程序还能运行呢?

因为删除的只是磁盘上的文件, 文件内容已经被拷贝到内存里了,所以删除了磁盘上的文件对加载到内存中的进程没有影响, 而进程没有结束自然也就可以访问进程目录了. 


bash也是一个进程

命令行解释器bash也是一个进程!
其次,我们发现上面每次运行起来进程的父进程都是bash,

所以:命令行启动的所有程序,最后变成进程其对应的父进程都是bash(也有特殊情况,我们目前先不考虑)。至于如何做到得,我们后面再说。

那为什么bash启动的程序,最终生成的进程它们的父进程都是bash呢?

之前提到在介绍shell时候提到——shell执行命令时,是创建子进程去执行的,所以上面我们发现进程的父进程都是bash。

那它为什么要这样做呢?

原因很简单,因为bash怕我们自己写到程序有问题,有bug。所以bash就创建子进程去执行来保证自己的安全。就像王婆自己去给小帅说媒怕不成功影响了自己的名声,所以找实习生去说。

那既然bash也是一个进程,那我们能不能把它干掉呢?

 在Linux下使用指令终止进程

在我们的程序运行时,可以在运行的地方按CTRL+c来结束进程,但是还有一种方法可以结束进程: 

使用指令: kill -9 要杀掉的进程id (注:这里的-9是信号参数,直接使用即可)

 我们把bash给kill掉呢?

我们kill之后会发现bash就不能正常工作了
那出现这种情况的话我们把xshell关掉重新登陆就行了。


父进程和子进程的概念

在使用ps指令查看进程详情时,除了pid我们可以看见左边还有一个ppid,这是parent pid的意思,也就是父进程的pid:

用grep -v "grep"可以过滤掉与grep有关的信息 

可以查看父进程id的函数:

使用函数: getppid()

 

 可以发现,每次运行时,子进程的id都在变化,然而父进程的id一直没变!这是因为在命令行中,父进程一般是命令行解释器: bash


进程的当前目录 

关于cwd:

我们经常听见一句话:"在当前目录下创建一个文件,在当前目录下....."
这个当前目录就是cwd指向的目录,当前目录全称是当前工作目录(current work diractory)也就是cwd, 默认情况下,进程启动所处的路径,就是当前路径并且Linux外壳的bash中, pwd指令其实就是从cwd中找到当前路径的!

 使用指令:chdir("路径"),可以更改进程的cwd

 现在把进程的cwd改为上一级目录:

可以看到当前进程的cwd改变了, 程序运行后在更改后的cwd下创建了test.txt

 结论2:每一个进程都要有自己的工作目录


Linux中创建进程的方式

1. 命令行中直接启动可执行程序.(对应windows下的双击exe文件) -----手动启动
2. 通过代码创建进程.(下面介绍)

启动进程的本质就是创建进程,一般是通过父进程通过系统调用接口创建子进程,构成一种父子关系而命令行中启动的进程都是由bash为父进程模拟创建子进程的! 

对于系统调用的拓展

我们使用的getpid和getppid是系统调用函数而在冯诺依曼体系中讲到,如何用户想要访问底层的数据必须经过系统调用这一门槛!

 pid和ppid的值是数据,那么数据是被存储在进程控制块PCB中的,而PCB的本质是一个结构体,所以操作系统只需要写一个函数,将结构体中的pid或ppid作为返回值返回给用户即可:


进程的创建 

启动一个进程, 如何理解这种行为?

启动进程本质就是系统多了一个进程, OS要管理的进程多了一个. 硬件层面是把可执行程序加载到了内存, 软件层面操作系统为了管理进程为该进程创建PCB为PCB初始化各种属性.

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

之前创建进程的方式都是手动执行(在命令行中输入./可执行程序), 一个进程在启动时有自己的父进程, 目前看来每个这样的进程它的父进程是bash, (因为都是在bash里面启动的), 而且每个进程都有自己的当前工作目录, 有自己的父子关系.

那么如果用代码创建进程呢?

用户使用代码创建进程叫系统调用, 我们可以通过系统调用来创建进程.

使用函数: fork

fork创建子进程

  1 #include <stdio.h>  
  2 #include <unistd.h>  
  3 int main()  
  4 {  
  5     printf("我是一个父进程,我的pid是:%d\n",getppid());  
  6   
  7     fork();  
  8   
  9     while(1)  
 10     {  
 11         printf("我是一个进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
 12         sleep(1);
 13     }
 14                                                                                                     
 15     return 0;                                                      
 16 }
~             

第一次打印对应的进程的PID刚好是第二次打印对应进程的PPID.
那这也证实了它们两个是父子进程关系,fork的作用就是创建当前进程的子进程,而PID为6485的这个进程就是被创建的子进程。 

总结:

如果没有fork的话, 那程序运行起来就只有一个进程, 这个进程是bash的子进程, 那就只有一个执行流, 所以死循环中只会重复打印一条语句

但是现在第一个打印后面有一个fork, 它去创建了一个当前进程的子进程, 所以就变成两个执行流, 死循环里的printf就被打印了两次, 且一次是父进程执行的, 另一次是子进程执行的.

结论:只有父进程执行fork之前的代码,fork之后的代码父进程和子进程都要执行 


fork的返回值 

fork成功的话,在父进程中返回子进程的PID,在子进程中返回0。

失败的话,-1在父进程中返回,不会创建任何子进程,并且正确设置了errno(C语言中一个用于表示错误码的全局变量,Linux内核时C语言写的)。
也就是说fork成功的话,返回值会有两个。

fork之后根据fork的返回值进行分流: 

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main()
  4 {
  5     printf("我是一个父进程,我的pid是:%d\n",getppid());
  6 
  7     //创建子进程,bash也是用c语言写的,命令行启动的进程,都是bash的子进程,大概估摸着,bash源代码中创建子进程    用的就是这个
  8     pid_t id = fork();
  9     //for之后,用if语句进行分流
 10     if(id < 0)
 11         return 1;
 12     else if(id == 0)
 13     {
 14         //child
 15         while(1)
 16         {
 17             printf("我是子进程,我的pid是%d,我的ppid是%d,返回的id是%d\n",getpid(),getppid(),id);
 18             sleep(1);
 19         }
 20     }
 21     else
 22     {
 23         //parent
 24         while(1)
 25         {
 26             printf("我是父进程,我的pid是%d,我的ppid是%d,返回的id是%d\n",getpid(),getppid(),id);
 27             sleep(1);
 28         }
 29     }
 30                                                                                                         
 31     return 0;
 32 }
~

我们也能查看到当前是有两个mycode进程的

但是我们之前写的代码出现过 else if 和 else 两个条件同时满足的吗?

并没有,但是这里if和elseif里面的语句都执行了,两个while循环同时在执行。

那为什么可以这样呢?

因为fork成功的话有两个返回值,所以在多执行流的情况下if语句分支是可以同时执行的。(为什么有两个返回值具体下面再分析)

目前可以得出一些结论:

fork成功之后,执行流会变成两个(父进程和子进程同时执行)
fork成功之后,父进程和子进程的执行顺序是不确定的,取决于操作系统的调度策略。
fork成功之后,父进程和子进程代码共享(我们上面fork之后父子进程都执行了第二个打印就可以证实这一点),通常我们要使用if语句进行代码块分流。


创建子进程的意义是什么?

了解了fork能创建子进程,那么创建子进程的意义是什么? 

创建子进程的意义就是为了让子进程执行和父进程不一样的代码,实现和父进程不一样的功能.

比如我们可以一边下载软件一边播放音乐,这两个过程就是不同的进程在执行! 


fork函数到底做了什么?

fork会创建子进程,系统中会多出一个子进程,操作系统以父进程为模板为子进程创建PCB,但是子进程中是没有代码和数据的,当前状态子进程和父进程共享代码和数据,所以fork之后,父子进程会执行一样的代码.

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

需要父进程来标识子进程的唯一性,需要得到子进程的pid

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

创建完成子进程只是一个开始, 创建完成子进程之后, 系统的其他进程, 父进程和子进程, 接下来是要被调度执行的!

当父子进程的PCB都被创建并在运行队列中排队的时候,哪一个进程的PCB先被调度,哪一个进程就先运行!

但是哪一个进程的PCB先被调度,默认情况下是不确定的,由各自PCB中的调度信息(时间片,优先级等) +调度器算法共同决定!

结论:

操作系统中,fork成功之后,父进程和子进程哪一个先运行完全是随机的,是不清楚的,因为fork成功创建子进程之后,父子进程谁先运行是取决于操作系统的调度策略.

为什么fork有两个返回值?

众所周知,C/C++函数只能有一个返回值,然而这里的fork函数既然也是C函数,为什么会有两个
返回值呢?

首先大家来思考一个问题:一个函数将要return的时候,它完成的主体功能是否已经执行完了?

是的.
比如有一个求和的函数,那当它return的时候,这个和肯定已经求出来了,而return是要把这个结果返回给函数调用的地方。

对于fork函数来说: 

那在fork最后将要return的时候,那它的主体功能即创建子进程当然已经完成了。所以此时的执行流就已经变成两个了,上面我们也说了,fork之后,父子进程是共享代码的。
那对于fork的return,他也是一句代码,一个语句啊。
所以这个return语句就会被父进程和子进程都执行,被执行了两次,而在我们看来就好像是fork返回了两个值。

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

fork成功之后,父子进程是共享一份代码的。比如我们上面演示的fork之后父子进程都执行了同一句printf语句。

再看这样一段代码:

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main()
  4 {
  5     printf("我是一个父进程,我的pid是:%d\n",getppid());
  6     int x = 100;                                                                                        
  7     //创建子进程,bash也是用c语言写的,命令行启动的进程,都是bash的子进程,大概估摸着,bash源代码中创建子进程    用的就是这个
  8     pid_t id = fork();
  9     //for之后,用if语句进行分流
 10     if(id < 0)
 11         return 1;
 12     else if(id == 0)
 13     {
 14         //child
 15         while(1)
 16         {
 17             printf("我是子进程,我的pid是%d,我的ppid是%d,返回的id是%d,x的地址%p\n",getpid(),getppid(),id,    &x);
 18             sleep(1);
 19         }
 20     }
 21     else
 22     {
 23         //parent
 24         while(1)
 25         {
 26             printf("我是父进程,我的pid是%d,我的ppid是%d,返回的id是%d,x的地址%p\n",getpid(),getppid(),id,    &x);
 27             sleep(1);
 28         }
 29     }
 30 
 31     return 0;
 32 }
~

两个进程打印对应的x的值和x的地址都是一样的,所以我们可以暂且认为父子进程的数据也是共享的。

然后再来看一个问题, 就比如我们现在电脑上打开了很多应用,那就对应了很多的进程。那如果现在把QQ退出了,会影响xshell吗。

这当然是不会的,凭我们平时的使用经验我们也知道。

所以程序的运行是具有独立性的!每个进程在执行时都相对独立,不会相互干扰或影响彼此的运行状态。
那同样的,对于父进程和子进程也是这样,我们可以验证一下:

杀死父进程,子进程依然在运行

杀死子进程,父进程依然在运行

对于父子进程来说,按照我们上面的分析,父子进程共享一份代码和数据,那它们是如何做到相互独立呢?

那首先对于代码来说,好像是没什么问题的。

因为在程序运行时,代码段通常被视为只读的,以确保程序的完整性和安全性.所以父子进程可以共享一份代码,就算其中一个进程被干掉了,那代码还是在的,也就可以实现代码层面的独立,一个进程结束不会影响另一个进程的执行。

但是数据呢?
一个进程在自己的执行流里执行代码的时候是可以修改代码里面的数据的(比如某个变量的值)

 现在我在子进程中修改x的值为0:

我们看到修改之后呢,它们打印的x的值不一样了,但是我们看到两个x的地址依然是一样的。那这里如何做到同一个变量地址相同但是值不同的.

对于父子进程的数据,并不是真正的共享一份,而是写时拷贝.
那写时拷贝的概念我们其实之前在C++里面string模拟实现那篇文章提到过.
其实就是只有在修改数据的时候才进行拷贝,然后修改你自己拷贝的数据,而不会影响原始数据。那这样就做到了在数据层面上可以实现进程间的独立性。 

所以,可以理解为:

当子进程被创建时,起初操作系统只为其分配一个新的进程控制块(PCB),用于维护子进程的相关信息。并不会立即复制父进程的整个地址空间,包括代码段和数据段。

相反,父进程的地址空间会被标记为共享,并且只有在子进程或父进程试图修改共享数据时,才会进行写时拷贝。这时,操作系统会将要修改的内存页复制到一个新的物理页中,然后对于的进程将修改后的数据写入这个新的页中,使得子进程和父进程的数据相互独立。

所以为什么一个变量同时接收两个值?

很简单,ret第二次接收的时候,相当于要对数据进行修改。
那这时会发生什么?这时就会发生写时拷贝

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 int main()
  4 {
  5     printf("我是一个父进程,我的pid是:%d\n",getppid());
  6 
  7     //创建子进程,bash也是用c语言写的,命令行启动的进程,都是bash的子进程,大概估摸着,bash源代码中创建子进程    用的就是这个
  8     pid_t id = fork();
  9     //for之后,用if语句进行分流
 10     if(id < 0)
 11         return 1;
 12     else if(id == 0)
 13     {
 14         //child
 15         while(1)
 16         {
 17             printf("我是子进程,我的pid是%d,我的ppid是%d,返回的id是%d,返回值id地址%p\n",getpid(),getppid(    ),id,&id);
 18             sleep(1);
 19         }
 20     }
 21     else
 22     {
 23         //parent
 24         while(1)
 25         {
 26             printf("我是父进程,我的pid是%d,我的ppid是%d,返回的id是%d,返回值id地址%p\n",getpid(),getppid(    ),id,&id);                                                                                              
 27             sleep(1);
 28         }
 29     }
 30 
 31     return 0;
 32 }

虽然看到这两个的地址是一样的,但是其实它们是两个不同的变量,占用不同的存储空间。那为什么地址看到的是一样的呢?
那其实这里我们看到的地址并不是底层真实的物理地址,那关于这方面的问题我们后面也会讲到,现在先了解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值