【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 的学习路上一起进步!

相关推荐
WZF-Sang7 分钟前
Linux——信号
linux·运维·服务器·c++·学习·进程·信号
瞌睡不来22 分钟前
(学习总结29)Linux 进程概念和进程状态
linux·学习·操作系统·进程
优秀是一种习惯啊8 小时前
Linux 内核源码阅读——ipv4
linux·网络
源远流长jerry9 小时前
NIC数据包的接收与发送
linux·网络
南棱笑笑生10 小时前
20250321在荣品的PRO-RK3566开发板的buildroot系统下使用ll命令【直接编译进IMG】
linux·服务器·数据库
IT 忘本10 小时前
如何在 Linux 系统中部署 FTP 服务器:从基础配置到安全优化
linux·服务器·安全
ROC_bird..10 小时前
linux_vim
linux·vim
GalaxyPokemon11 小时前
LINUX基础 [二] - 进程概念
linux·运维·服务器
C嘎嘎嵌入式开发11 小时前
Linux中mutex机制
linux·运维·服务器
猫咪-952711 小时前
常考计算机操作系统面试习题(二)(中)
linux·操作系统