【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,每一个数字目录里面的内容包含的都是这个进程的动态属性,一旦进程退出,该目录会被系统自动移除。
在众多属性中,我们来简单了解两个:cwd
和 exe
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 的学习路上一起进步!