【Linux系统】—— 进程概念

【Linux系统】------ 进程概念

  • [1 什么是进程](#1 什么是进程)
  • [2 task_struct](#2 task_struct)
  • [3 如何查进程标识符:pid](#3 如何查进程标识符:pid)
    • [3.1 系统调用 getpid](#3.1 系统调用 getpid)
    • [3.2 ps 指令在系统中查看进程信息](#3.2 ps 指令在系统中查看进程信息)
    • [3.3 以文件的方式查看进程](#3.3 以文件的方式查看进程)
      • [3.3.1 exe](#3.3.1 exe)
      • [3.3.2 cwd](#3.3.2 cwd)
    • [3.4 杀掉进程](#3.4 杀掉进程)
    • [3.5 父进程](#3.5 父进程)
      • [3.5.1 获取父进程 pid](#3.5.1 获取父进程 pid)
      • [3.5.2 谁是父进程](#3.5.2 谁是父进程)
  • [4 用代码创建子进程](#4 用代码创建子进程)
    • [4.1 子进程如何创建](#4.1 子进程如何创建)
    • [4.2 fork 的返回值](#4.2 fork 的返回值)
      • [4.2.1 为什么 fork 要给父子返回各自的不同返回值?](#4.2.1 为什么 fork 要给父子返回各自的不同返回值?)
      • [4.2.2 为什么一个函数会返回两次?](#4.2.2 为什么一个函数会返回两次?)
      • [4.2.3 为什么一个变量,既 == 0, 又 >0 ?导致 if 和 else 同时成立](#4.2.3 为什么一个变量,既 == 0, 又 >0 ?导致 if 和 else 同时成立)

1 什么是进程

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

通过前面的学习我们知道,我们编译好的二进制可执行程序不在运行时是存储在磁盘中的。想要运行这个程序,程序运行之前要先将这个文件加载到内存中,为什么要加载,这是由冯诺依曼体系结构决定的。那么加载到了内存中的这个可执行文件就叫进程吗?先不回答,我们继续往下看

现在我们是加载一个程序,但现实中往往是多个程序加载到内存,所以在内存中的同一时刻,往往同时存在非常多的可执行程序。包括操作系统本身也是一款软件,它自己要加载到内存中

那么多的代码和数据,操作系统肯定要对多个被加载到内存中的程序进行管理进行管理

如果仅仅只有各个可执行的代码和数据,操作系统能进行管理吗?做不到 !举个例子:在操作系统的视角里,并不能区分这些代码和数据是属于哪个可执行程序的

那么如何管理呢?

先描述,再组织!

操作系统为了管理这些代码和数据,将代码和数据的各个属性用 struct 结构体 聚合起来定义一个结构体,再给每一个加载到内存中的可执行构建一个该 struct结构体 对象,将该可执行的对象填好到该对象中,就有了对应的结点,最后用数据结构(双链表)将所有结点组织起来,最终在操作系统内形成了一个程序列表,我们把这个程序列表称为进程列表。

所以什么是进程?

进程 = 加载到内存中的代码和数据 + 内核数据结构对象。程序本身并不是进程

在操作系统学科中,我们将描述可执行程序的这个 struct结构体 称之为:PCB(process control block),在 Linux 下,这个结构体具体叫:task_struct

虽然我们现在还不知道 task_struct 有什么属性,但先告诉大家:进程的所有属性,都可以直接或者间接通过 task_struct 找到

在 Linxu 中具体一点,进程 = PCB + 自己的代码和数据

在操作系统内部,对进程的管理,全部会转化为对数据结构(主要是双链表)的增删查改

为帮助小伙伴们进一步理解 PCB,下面举一个例子:

找工作时,我每个人都有自己的一份简历,简历上面试者我们的各种信息,面试官最终会收到一堆简历。我们要找工作,本质上不是我们在找工作,而是我们的简历在找工作。我们在排队,本质是我们的简历在排队;淘汰某个学生本质是淘汰了它的简历。所以一旦一个可执行程序加载到内存中,那这个可执行程序自己是最不重要的,重要的是它对应的PCB

2 task_struct

task_struct 的内容特别多:

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

3 如何查进程标识符:pid

先说结论:我们历史上的所有的指令 、工具、自己的程序等运行起来全都是进程!

我们运行的 ls、pwd、top、grep 等等所有的命令在系统里都是进程,只不过 ls 运行特别快,一启动就退出,而 top 是启动后要手动 q 退出。所以系统中要执行我们的任务,全都是通过进程来执行的。

在 Linux 中我们用户是以进程的方式来访问操作系统的。

我们可以将用户当做一名老师,操作系统当做学生。老师给学生布置任务让学生去完成。

所以进程也叫任务,所以 PCB 在 Linux 中叫 task_struct。我们所说的"进程",是我们将 task 这个单词翻译成进程,其实"进程"在国外都叫任务(task)

我们写一个自己的程序:

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    while(1)
    {   
        printf("我是一个进程!\n");
        sleep(1);
    }   
    return 0;
}

3.1 系统调用 getpid

既然这个我们自己想的程序是进程,那么他运行起来肯定有自己的 PCB 和自己的代码和数据。

既然他是一个进程,那么这个进程相关的属性值我们怎么获取呢?

我们来认识一下第一个系统调用(在 man 手册第二章):getpid


getpid:

  • 头文件:<sys/types.h> 和 <unistd.h>
  • 功能:获取当前进程的 pid (标示符)。通俗讲就是哪个进程调我,我就获取哪个进程的 pid
  • 返回值:pid_t 相当于 int
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

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

3.2 ps 指令在系统中查看进程信息

在系统层面,我们也有对应的指令去直接查找当前系统中启动的进程有哪些

使用指令: 「ps axj」

因为当前系统中启动的进程非常多,我们只想看到刚刚启动起来的进程:ps axj | grep code

可是进程有那么多的属性信息,这些信息是啥我都不知道。因此我们先把第一行进程的属性提出来:ps axj | head -1

Linux 中同时执行两条指令有两种方法:在两条指令中间加 "; " 或 "&&"

但查出来的第二个进程是什么东西呢?

当我们去查进程的时候,对应的 grep 指令总会被显示出来。因为整条命令从左向右查的时候,grep 也是个命令,grep 命令一旦跑起来它自己也是个进程,而它自己的过滤关键字也会包含 "code",所以也会自己吧自己查出来

如果不想见到 grep 的进程,可以 grep -v 选项反向查找

  • 指令:ps axj | head -1 && ps axj | grep code | grep -v grep
      

3.3 以文件的方式查看进程

Linux 中一切皆文件 ,所以进程在 Linux 中也以文件的形式展现出来

我们可以通过文件的方式去查看进程:查看 Linux 中的一个目录结构:proc 目录来查看

我们知道,蓝色显示的都是目录,这些目录名都是特定进程的 pid

proc目录中记录的是当前系统中所有进程的信息,该目录下所有的文件,没有数字目录代表着都是特定进程的 pid,每一个数字目录里面的内容包含的都是这个进程的动态属性,一旦进程退出,该目录会被系统自动移除。

在众多属性中,我们来简单了解两个:cwdexe

3.3.1 exe

exe 记录的是当前进程对应的可执行文件。

也就是说一个进程启动时是知道自己是从哪里来的,是启动哪个指令才有了我这个进程。

它的PCB我记录这个可执行文件的绝对路径和程序名

如果将这个可执行文件删除,这个进程还在跑吗?

还在跑!

因为删除的是磁盘 上的可执行文件,而进程启动时这个文件的拷贝已经在内存 了。所以删除 exe 的这个可执行文件并不会影响当前进程

3.3.2 cwd

cwd 即:current work dir

cwd 记录的是:当前可执行程序所在的路径

以前我们写 C/C++ 使用 fopen 打开一个文件时,往往可以只用加上文件名,并不一定要加上路径:fopen("test.txt", "w"),如果是新建文件就会在当前路径下新建。为什么呢?

答案就是进程在启动时记录了自己的当前路径

3.4 杀掉进程

只要是一个进程,我们就可以杀掉它

法一:ctrl + c


法二:kill -9 pid

同时也可以看到,每次启动同一个程序其 pid 都是不同的。Linux 分配 pid 是通过一个线性递增 的一个整型值分配的。两次启动进程的 pid 不是连续可能是因为进程退出到再启动期间,系统又启动其他进程了

3.5 父进程

Linux 中所有的进程都是被它的父进程创建的!

子进程都是由父进程创建,父进程可以创建多个子进程,所以 Linux 中所有的进程是一颗进程树

3.5.1 获取父进程 pid

我们可以用 getppid 系统调用来获得父进程的 pid。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

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

3.5.2 谁是父进程

我们频繁的将进程启动再杀死,发现每次 pid 都会变化,但是父进程的 pid 一直不变。

所以父进程到底是谁?

上面我们讲了如何查进程,我们自己动手查一下

父进程是bash!

bash 是什么?bash 就是我们之前提到过的命令行解释器 (文章链接: 【Linux系统】------ 初识 shell 与 Linux 中的用户 )。

所以命令行解释器bash(王婆)它自己就是一个进程

并且我们启动自己的程序都是 bash 的子进程(王婆和实习生)

每次我们登录我们的云服务器时,操作系统会给每一个用户分配一个 bash ( bash 前面带 "-" 表示远程登录),由 bash 给我们做命令行解释。

所以我们现在知道了 bash 就是一个进程,那命令行又是什么?

命令行就是 bash 打出来的一个字符串。该字符串被打出来后就在这里等,卡在这里,相当于 sacnf。我们输入的所有的命令都是以字符串的形式交给 bash,bash 拿到命令后再做相关处理。

我们输入的命令 ls/pwd/mkdir/top,他们的父进程全都是 bash。

所以 bash 是如何创建子进程的呢?我们下面一起来学习

4 用代码创建子进程

4.1 子进程如何创建

在代码中我们先创建进程可以用系统调用:fork

fork系统调用的功能就是创建子进程

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("父进程开始运行,pid:%d\n", getpid());
    fork();
    printf("进程运行,pid:%d\n", getpid());
    return 0;
}

为什么会有上述现象?

进程 == 代码和数据 + PCB。

所以父进程有自己的 PCB 和代码数据。

而 fork 是用来创建子进程的,我们虽然不知道它是怎么创建的,但进程 == 代码和数据 + PCB。

所以子进程也一定要有自己的 PCB 和 代码数据。

创建子进程,其PCB一般是从父进程哪里拷贝过来,当然个别属性如 pid 等会有所区别;其代码和数据则指向父进程的代码和数据

所以子进程再被调度时,它就会执行父进程之后的代码。

刚创建出的子进程没有自己的代码和数据,因为目前没有程序新加载

那为什么子进程不执行前面的代码,而是往后执行呢?

因为前面的代码已经执行过了,子进程虽然能看到,但也只能两个与父进程两个执行流分别往后执行

就好比有一个人把你的简历抄了一份,但他忘了改电话,所以两份简历都指向同一个人,就是你自己

4.2 fork 的返回值

现在我们知道 fork 的功能是创建子进程,那它的返回值是什么呢?

如果成功,fork 会将子进程的 pid 返回给父进程,将 0 返回子进程,失败返回 -1

这意思是 fork 会有两个返回值吗?还真是。

先不去理解 fork 的两个返回值,现在我想让父进程和子进程执行不同的代码逻辑

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("父进程开始运行,pid:%d\n", getpid());
    pid_t id = fork();
    if(id < 0)//小于0 ,表示失败,直接退出
    {   
        perror("fork");
        return 1;
    }   
    if(id == 0)//返回值为0,子进程代码逻辑
    {   
        while(1)
        {   
            printf("我是一个子进程!我的 pid 是:%d,我的父进程 pid 是:%d\n", getpid(), getppid());
            sleep(1);
        }   
    }   
    else//父进程代码逻辑
    {   
        while(1)
        {   
            printf("我是一个父进程! pid :%d,ppid 是:%d\n", getpid(), getppid());
            sleep(1);
        }   
    }   
    return 0;
}

相信大家有很多疑惑:我们之前学 C/C++ 时,什么时候见过一个函数有两个返回值的;什么时候见过 if 和 else 能同时执行的;什么时候见过一个变量(id)有两份值(0 和 大于0)

4.2.1 为什么 fork 要给父子返回各自的不同返回值?

为什么给子进程返回 0,而给父进程返回子进程的 pid 呢?

因为在 Linux 系统中,父进程 :子进程 = 1 : n

即任何一个父进程可以无数多个孩子,而一个子进程只能有一个父亲

把子进程的 pid 返回给父进程,是因为父进程要通过不同的 pid 来区分不同的子进程,方便未来对不同的子进程进行管理;而子进程不需要专门来获得父进程的 pid,因为子进程一个 getppid 就能获得

4.2.2 为什么一个函数会返回两次?

首先问大家一个问题:一个函数执行到 return了,那这个函数的主体逻辑做完了吗?

很显然:函数的核心功能已经做完了

fork 函数本质是系统调用,所以上述我们写的 C 代码一旦调用 fork,就会进入 fork 对应的函数。我们虽然不知道 fork 的具体实现,但是知道子进程的大致创建过程:它要申请新的 PCB ,拷贝父进程的 PCB ,将子进程的 PCB 放入调度队列中等等工作

当 fork 函数走到 return 时(还没执行 return),子进程已经创建完成,甚至已经被调度了。

而 fork 函数的 return 也是一条语句,也已经被父进程和子进程共享了,return 会被父进程执行也会被子进程执行,所以 return 会被返回两次,所以 fork 有两个返回值

4.2.3 为什么一个变量,既 == 0, 又 >0 ?导致 if 和 else 同时成立

第三个问题,我们要学习了虚拟地址空间后才能真正解答,现在我们只能简单说一部分。

代码是只读的,父进程和子进程共享代码 ,这一点没有问题。

问题是:数据也是共享的吗?

先问大家:我们刷着抖音,抖音崩了,会不会影响我们在后台打开的微信?我们用着 Excel ,会不会影响打开的 VS2022 ?每一款软件,启动的时候都是进程,但我们发现现实生活照一个进程挂了并不会影响其他进程。

结论:进程具有独立性

即便是父子进程关系,父进程挂了,子进程一点事都没有

既然进程是独立的,要是数据是共享的话,子进程不就能修改父进程的数据?进程不就不独立了

结论:父子进程在数据层面上默认是共享的,但是父子任意一个一但尝试去修改数据,那么操作系统就会把这个要修改的数据在底层拷贝一份,让目标进程修改这个拷贝数据。这种技术较写实拷贝

代码验证:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int g_val = 100;

int main()
{
    printf("父进程开始运行,pid:%d\n", getpid());
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    if(id == 0)
    {
        printf("我是一个子进程!我的 pid 是:%d,我的父进程 pid 是:%d,g_val 值为:%d\n", getpid(), getppid(), g_val);
        sleep(5);
        while(1)
        {
            printf("子进程修改变量:%d -> %d\n", g_val, g_val += 10);
            printf("我是一个子进程!我的 pid 是:%d,我的父进程 pid 是:%d\n", getpid(), getppid());
            sleep(1);
        }   
    }   
    else
    {   
        while(1)
        {   
            printf("我是一个父进程! pid :%d,ppid 是:%d, g_val的值为:%d\n", getpid(), getppid(), g_val);
            sleep(1);
        }
    }
    return 0;
}

pid_t id = fork()中的 id 本身也是变量,return 返回值,返回的本质就是写入变量。不管 return 时父和子那个先 return,哪个先修改 id 变量,最终 id 变量都会发生写实拷贝,父和子就拿到了不同的值


好啦,本期关于 进程概念 的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在 Linux 的学习路上一起进步!

相关推荐
HZero.chen28 分钟前
Linux字符串处理
linux·string
张童瑶30 分钟前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功12334 分钟前
什么是SELinux
linux
石小千40 分钟前
Linux安装OpenProject
linux·运维
柏木乃一1 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-30901 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu
百年渔翁_肯肯1 小时前
Linux 与 Unix 的核心区别(清晰对比版)
linux·运维·unix
胡闹541 小时前
Linux查询防火墙放过的端口并额外增加需要通过的端口命令
linux·运维·windows
lc9991022 小时前
简洁高效的相机预览
android·linux
SongJX_3 小时前
DHCP服务
linux·运维·服务器