一,进程的课本概念

课本概念:系统的一个执行实例,正在执行的程序

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

进程 = 内核数据结构(task_struct)+自己的程序代码和数据

二,进程的实际概念

2.1什么是PCB/task_struct

程序在没有被运行起来的时候都是一个个在磁盘上面的文件,运行起来后就要被加载进内存

此时,操作系统必然要对多个被加载到内存中的程序进行管理

管理的核心逻辑是:先描述,再组织

先描述,操作系统描述进程的各种数据,创建了一个PCB,PCB是什么

PCB全称是Process Control Block ,即进程控制块

在Linux操作系统中,用于描述进程的结构体叫做struct task_struct(PCB和task_struct是抽象和具体的关系),它包含了进程的所有属性,举例如下:

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

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

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

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

内存指针:包括程序代码和进程相关数据的指针

2.2操作系统内部组织的进程链表

操作系统在每一个task_struct里面放了一个指针,于是每一个PCB连接在一起,形成了一个链表

于是,对进程的管理,就变成了对链表的增删查改

操作的本质不是直接去操作我们的代码与数据,而是去找我们的PCB,用PCB去修改我们的代码和数据

举个简单的例子:我们去找工作,面试,一群人按顺序去找面试官,比如面试官为CPU,那这个时候我们的简历就是PCB,我们人就是数据和代码,这就是先描述再组织的结构

关于进程我们还有两个结论:

进程 == PCB + 加载到内存的代码和数据,而不是代码和数据

操作系统对进程的管理 == 操作系统滴哦链表的增删查改,这也是解耦

三,系统调用与进程查看

3.1系统调用存在哪里

在man手册里面查看,前者是系统调用,后者是库函数调用

3.2系统调用

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
5 int main()
6 {
7     while(1)
8     {
9         sleep(1);
10        printf("我是一个进程!,我的pid: %d\n", getpid()); // 获取当前进程的pid
11    }
12 }

unistd.h:包含了各种系统服务的函数原型,是访问系统调用(如getpid)的门户

sys/types.h:提供进程ID等数据类型的定义(如pid_t)

sleep:让进程进入休眠状态

getpid:通过系统调用直接向内核申请获取当前进程的唯一标识符(PID)

除了getpid外,我们还补充一个getppid,获取父进程的pid

3.3查看一个进程

创建一个进程之后,我们可以通过使用ps axj命令,该命令会列出系统中所有进程的详细状态信息

为了更加高效的观察,我们会通常结合管道和脚本进行优化:

1.基础过滤:使用grep‘文件名’筛选特定进程

2.去除干扰:加上grep -v grep过滤掉过滤指令本身的进程。这是因为当我们执行ps axj | grep 文件名时,grep命令本身也是一个进程,也会出现在ps的输出结果中,从而干扰我们的观察。而grep -v grep中的-v选项表示反向匹配,即排除包含"grep"关键字的行,这样就能把grep命令自身的进程过滤掉,只保留我们真正关心的进程信息。

3.保留标题栏:使用head-1显示ps输出的首行属性说明

PID不会一成不变,当我们crtl+c杀死进程后,再次运行看到的pid就不一样了,杀死进程的方式还有一种,kill -9进程pid

但是始终有一个pid是始终不变的

        查看它,我们发现这个就是bash,所以说命令解释器本质也是一个进程,它是一个会在你的终端不断打印而且不断阻塞等待你输入的一个进程,操作系统对每一个用户都会创建一个bash

除了上述的方式查看进程,还有一种办法:通过/proc目录查看进程,系统把一个个进程都转化为了文件,进程结束,文件消失

或者指定打开一个进程的文件夹/proc/21381

3.4进程与工作目录

exe -> /home/whb/code/code/merge_class/lesson12/myprocess
cwd -> /home/whb/code/code/merge_class/lesson12

第一个是进程的路径,让进程知道自己是从哪里来的,它的代码和数据在磁盘的什么位置

第二个是进程的当前的工作目录,当我们使用相对路径打开文件,系统会自动将cwd的路径与文件名进行拼接,从而确定文件在磁盘上的位置

#include <unistd.h>
chdir("/home/whb"); // 将进程的工作目录切换到 /home/whb
fopen("hello.txt", "a"); // 此时文件将创建在 /home/whb 目录下

这也是为什么新建文件会出现在可执行文件同级目录的原因---因为进程启动时cwd默认继承父进程(通常是shell)所在的路径

动态修改:chdir系统调用:进程可以通过chdir接口来动态改变自己的工作目录

#include <unistd.h>
chdir("/home/whb"); // 将进程的工作目录切换到 /home/whb
fopen("hello.txt", "a"); // 此时文件将创建在 /home/whb 目录下

在shell是本质进程的情况下,cd内部其实就是封装了一个chdir函数

四,进程的父子关系

4.1进程树

        我们的进程是一个单亲繁殖系统,类似B树,一颗多叉树从根节点出发延申出多个子进程,譬如我们一切命令都有一个共同的父进程bash

4.2子进程的创建

        我们来介绍一个创建子进程的系统调用fork()

头文件#include<unistd.h>

一句话核心:fork()就是进程的分身术--调用一次原地复制出一个一模一样的子进程,让程序真正分头行动

作用:

创建分身:产生子进程,继承父进程的代码和数据

并发处理:让父进程同时运行,互不干扰地处理不同任务

环境隔离:子进程在独立空间运行,崩溃不影响父进程

#include<iostream>
#include<unistd.h>
#include<string>
#include<sys/types.h>
using namespace std;
void Func_Test_Process()
{
	pid_t _id = fork();
	if(_id < 0)
	{
		string strerror("fork fail");
		throw strerror;
	}
	else if(_id == 0)
	{
		while(1)
		{
			printf( "这是一个子进程,子进程的pid是: %d ,子进程的ppid是: %d .\n",getpid(),getppid());
			sleep(1);		 
		}
	}
	else if(_id > 0)
	{

		while(1)
		{
			printf( "这是一个父进程,父进程的pid是: %d ,父进程的ppid是: %d .\n",getpid(),getppid());
			sleep(1);
		}		 
	}
}
int main()
{
	try
	{
		Func_Test_Process();
	}
	catch(string&& str)
	{
		cout<<str<<endl;
	}
}

        如上,父子进程都拿着各自的-id去执行对应的代码

        我们的父进程创建一个子进程,默认是将父进程PCB中的数据拷贝一份给子进程,并且修改一部分内容,比如pid和ppid。于是子进程就会自然而然的指向父进程的那段数据和代码,于是子进程与会去执行父进程后面的那一段代码,相当于有两个进程同时执行代码

值得注意的是

        父进程必须获得子进程的pid才能方便管理

        子进程不必获得父进程的pid,因为子进程可以使用ppid

之后我们还有问题:

  • else和ifelse为什么可以同时执行?
  • 一个局部变量为什么可以同时=0和>0?
  • 一个函数为什么可以有两个返回值?

4.3一个函数为什么可以有两个返回值

        首先一点,一个函数达到了return,其核心功能就算是完成了,fork也是如此,fork在return之前就完成了任务

1.申请新的pcb

2.拷贝父pcb给子进程

3.子pcb放入进程list中

4.甚至放入调度队列中

        因此我们得到,子进程的创建时间点一定在return之前,于是fork内部必然会有两个进程,两个返回值

4.4为什么else和if else可以同时执行

        在我们的认知内,if else和else永远互斥,但在Linux系统编程中,进程改变了这一点

        还有一个概念:代码是共享的,但是执行流是独立的

        当fork函数执行完成之后,系统里就存在两个独立的执行流(父进程和子进程)

       父进程:拿着fork返回的子进程pid,继续向下执行,遇到if分支,进行逻辑判断后进入分支

        子进程:拿着fork返回的0,继续向下执行,遇到if分支,进行了不同的逻辑判断,之后进入到了不同分支内

        综上,其实并不是在一个进程内执行了两个分支,而是两个进程各自执行了属于自己的分支

4.5为什么一个局部变量可以同时等于0和大于0

        同一个变量_id,为什么可以即是0又是进程pid

这里涉及到了linux的内存管理技术,写时拷贝

1.初始状态:fork刚完成时,紫禁城的页表指向的物理内存和父进程是同一块,也就是说,他们最初确实共享一个_id的内存空间

2.发生写入:当fork准备返回结果并写入_id变量时,操作系统会发现有两个进程尝试操作这块内存

3.触发拷贝:为了保证进程结果的独立性(互不干扰),操作系统会为子进程重新开辟一块物理空间,把父进程的内容拷贝过去,然后修改子进程页表的映射关系。

        你在父进程看到_id,它映射的是一块存着子进程PID的物理内存,你在子进程看-id,它映射的是另一块存着0的物理内存

由上,我们还能得出一个结论:进程具有独立性,两个进程的PCB的数据结构是独立的。即使共享也不会影响,数据又有写时拷贝守护

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐