Linux | 相关知识点 | 可以通过点击 | 以下链接进行学习 | 一起加油! |
---|---|---|---|---|
初识指令 | 指令进阶 | 权限管理 | yum包管理与vim编辑器 | GCC/G++编译器 |
make与Makefile自动化构建 | GDB调试器与Git版本控制工具 | Linux下进度条 | 冯诺依曼体系与计算机系统架构 |
进程是操作系统中资源分配和调度的核心单位,而 fork() 函数是创建子进程的关键工具。本节将简要介绍进程的概念,并通过 fork() 探讨其实际应用。
🌈个人主页:是店小二呀
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈初阶数据结构专栏: 初阶数据结构
🌈高阶数据结构专栏: 高阶数据结构
🌈Linux专栏: Linux
🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
文章目录
- 一、进程概念
- 1.1 进程理解
- 二、如何描述和管理进程
- 2.1 进程本质
- 2.2 PCB描述进程状态
- 2.2.1 task_struct
- 2.2.2 task_ struct内容分类
- 2.3 组织进程
- 2.3.1 不同数据结构配合进程需求
- 四、查看进程状态
- 4.1 查看所有进程指令
- 4.2 /proc 目录
- 五、通过系统调用获取进程标识符
- 5.1 父子进程
- 5.1.2 父进程意义
- 5.3 使用系统调用接口getpid、getppid
- 六、系统调用创建进程
- 6.1 fork函数(分叉/创建进程)
- 6.2 fork函数返回值
- 6.2.1 fork() 的返回值
- 6.2.1 为什么fork可以返回两次
- 6.2.3 变量同时接收两个返回值
- 6.3 fork()函数后进程关系
- 6.4 父子进程运行顺序
- 6.5 创建子进程意义
- 6.6 进程具有独立性
- 6.7 Bash父进程简介
一、进程概念
进程是计算机操作系统中的一个基本概念,指的是一个程序在计算机中的一次执行过程。它是操作系统中资源分配和调度的基本单位。进程和程序的主要区别在于:程序是静态的代码,而进程是程序在运行时的动态实例。
1.1 进程理解
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:已经加载到内存中程序,担当分配系统资源(CPU,内存)的实体
当我们运行程序,需要数据和代码加载到内存中,内存会对进程中数据和代码进行处理。其中操作系统中不单单只存在一个进程,而是同时存在多个进程,操作系统需要对这些进程进行管理,那么就需要用到"先描述,在组织",进行更好地管理。
二、如何描述和管理进程
2.1 进程本质
我们将磁盘中可执行程序的数据和代码被加载到内存,就可以说明"进程是可执行程序中数据和代码"吗?这里有一系列问题:占据内存多少空间、被调用多少时间、当前进程状态如何?
结果很显然,单从内存只有可执行程序的代码和数据完全不足描述上述需要的信息。这说明了"操作系统将可执行程序加载到内存中,被加载到内存中的代码和数据,根本不是进程,只是进程对应的代码和数据"
2.2 PCB描述进程状态
操作系统为了管理已经被加载进来所有进程,需要创建用于描述进程的结构体对象"PCB进程控制块(process ctrl block)",本质是对于进程属性进行描述。
2.2.1 task_struct
在Linux中描述进程的结构体是"task_struct"。task_struct属于Linux中内核数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
2.2.2 task_ struct内容分类
PCB是对操作系统学科的统称,其中"struct task_struct"对Linux进程块具体的称呼
- 【标示符】: 描述本进程的唯一标示符,用来区别其他进程。
- 【状态】: 任务状态,退出代码,退出信号等
- 【优先级】: 相对于其他进程的优先级
- 【程序计数器】: 程序中即将被执行的下一条指令的地址
- 【内存指针】: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(可执行程序执行需要对应的数据和代码,所有PCB对象必须需要一个指针指向这块空间)
- 【上下文数据】: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器
- 【I/O状态信息】: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。- 【其他信息】
【总结】:进程 = 内核数据结构对象PCB + 对应代码和数据
2.3 组织进程
操作系统做管理并不是把你的可执行程序加载进来做直接管理,而是对于pcb进行管理。
操作系统对进程进行管理,本质上是对PCB做管理,并不关心进程加载内存中代码和数据。只需要找到PCB对象,通过PCB内存指针找到对应的代码和数据,交给CPU处理就行。
每个进程都有属于自己的PCB,为了更好地管理不同进程的PCB,一般通过指针将PCB进行连接,形成链表。根据不同进程的需求,会采用不同的数据结构进行存储,通过对应指针信息来将它们更好地管理。
其中在Linux中"task_struct"主要是以"双向链表形式组织"
2.3.1 不同数据结构配合进程需求
进程管理的关键在于将其放入适合的组织数据结构中。不同的数据结构具有独特的特性,决定了其背后的算法选择,而算法的差异又对应于不同的应用场景。因此,合理的数据结构选择是优化进程管理的基础
【相关场景】:调度运行进程,本质就是让进程控制块"task_struct"(简历)进行排队,而不是这个代码和数据在排队,跟面试投简历一般,面试官查看每一份简历
在操作系统中,可能存在存储进程指针的运行队列和等待队列。如果需要将某个进程移动到特定队列,可以通过修改队列指针的链接结构来实现,从而达到更灵活的进程管理目的。
四、查看进程状态
开机时操作系统(OS)从外部存储设备(如硬盘或固态硬盘)加载到内存中,这是启动过程的一个重要步骤
4.1 查看所有进程指令
ps ajx
是用于查看系统中所有进程状态的命令。
具体参数含义如下:
ps
:显示当前系统中的进程信息。a
:显示所有用户的进程(不仅仅是当前用户的进程)j
:显示进程的控制终端、进程组等信息。x
:显示没有控制终端的进程(包括后台进程)
【场景演示】这里想拿到头部属性信息和进程信息,可以使用指令级联。通过指令ps axj | head -1 && ps axj | grep myprocess
查看可执行程序该进程状态信息。
【问题】:为什么会出现grep进程信息
如果指令需要被执行需要变成进程才能运行,grep指令被执行本质商也是进程被执行,所以grep本身这个进程也被查出来。
4.2 /proc 目录
/proc 目录中的文件是内存级文件,关机时会消失,开机后会重新生成。它主要提供动态运行进程的可视化信息。
其中,蓝色表示目录文件,因为一个进程可能包含许多相关信息。我们可以尝试进入这些目录查看具体内容。
【删除执行中程序】
如果删除可执行程序,会发现这个进程依旧运行。
因为原则上一个程序要被变成进程调度,会在内存里面有存在一份。就算你将可执行程序从磁盘上删除了,由于内存存在备份,所以进程还是可以运行。
总结:
exe 表示当前进程对应的可执行文件路径,说明进程能够找到自己需要执行的代码(已被可视化展现)。
cwd 表示当前进程的工作目录(Current Working Directory)。因此,当你使用
fopen
创建新文件时,默认路径就是当前目录,而这个默认位置正是由该进程的cwd
决定的。说明这个启动的myprocess进程,是由这个指定的绝对路径下的这个程序加载形成的。
五、通过系统调用获取进程标识符
在内核结构体 task_struct
中,存在一个 pid
属性,用于表示进程的唯一标识符。
pid
是无符号整数类型(unsigned int pid
),用于区分不同的进程。- 它是操作系统封装的一种数据类型,实际对应无符号整数。
用户无法直接访问操作系统内核中与进程相关的 PCB(进程控制块)数据结构。如果用户需要获取进程的 pid
、ppid
,必须通过操作系统提供的系统调用"getpid()、getppid()"来完成。
5.1 父子进程
如果是pid属性表示进程的唯一标识符,那么ppid属性表示什么呢?
ppid
的含义: Parent Process ID
通过结果显示,ppid
属性表示父进程的唯一标识符。可以观察到,在我们的可执行程序中,父进程的 ppid
通常不会发生变化,但是pid会发生变化,因为它表示的是基础命令行进程(如 bash)的父进程标识。
5.1.2 父进程意义
在 Linux 权限管理相关内容中,以“王婆和实习生”的例子可以更好地理解为什么 bash 需要创建子进程。
bash 命令行的作用主要有两个方面:
- 解释命令:将用户输入的指令解析为可执行操作。
- 阻止非法操作:限制用户进行不符合权限的行为。
每一条指令或可执行程序其实都对应一个独立的进程。因此,bash 命令行会先创建一个子进程来执行对应的指令,而自己则保持运行,等待处理其他命令。
这种设计的优点是:即使子进程崩溃,也不会影响 bash 命令行本身的运行,从而确保可以继续处理其他指令。
5.3 使用系统调用接口getpid、getppid
按照上述说法,进程每次启动对应pid都不一样,ppid通过不会发生变化,但是当我们重新启动机器时,再次执行该程序,会发现ppid发生了变化,可以说明父进程或bash命令行在打开机器时候就创建好的进程。
【调用监考脚本】:while :; do ps ajx | head -1 ;ps ajx | grep mycode | grep -v grep;echo "----------------------------";sleep 1;done
如果不想显示与 grep
指令相关的进程信息,可以使用 -v
参数来排除,会过滤掉包含 grep
本身的进程信息。
六、系统调用创建进程
6.1 fork函数(分叉/创建进程)
用户想创建进程,但是创建进程需要创建内核数据结构,但是用户是没有权限对内核数据结构进行任何的增删查改,用户没有权限在操作系统内新增一个test_struct
,所以操作系统也需要提供相对于的系统调用fork()
。
6.2 fork函数返回值
6.2.1 fork() 的返回值
- 成功创建子进程时:
- 父进程中:
fork()
会返回子进程的PID
(正整数)。- 子进程中:
fork()
会返回数值0
。
- 创建子进程失败时:
fork()
会返回-1
给父进程,表示子进程创建失败。总结:
fork()
有两个返回值,分别传递给父进程和子进程,用于区分执行的上下文(父进程还是子进程),以及处理可能的错误情况。
6.2.1 为什么fork可以返回两次
由于子进程会继承父进程的代码,因此 return ret
这一段代码会被父子进程分别执行一次,从而导致两次返回结果
6.2.3 变量同时接收两个返回值
由于子进程在修改数据发生了写实拷贝,导致了数据存在两份内容,进程具有独立性,父进程和子进程通过if else 分流去执行共享的代码。
【为父进程返回子进程PID和为子进程返回数值0】
- 为父进程返回子进程的 PID:通过返回值,父进程可以知道子进程的唯一标识符(PID)。
- 为子进程返回数值 0:子进程通过返回值知道自己是新创建的进程。
【显示效果】:
6.3 fork()函数后进程关系
通过我们的实时监控,会发现PID和PPID是相同的,是存在父子关系。
fork
之后,父进程和子进程会共享代码,但各自拥有独立的数据空间。
- 【创建一个进程的本质】:
- 系统中多了一个新的进程,对应一个新的内核
task_struct
。- 子进程拥有自己的代码和数据空间,但默认情况下,继承了父进程的代码和数据。
- 【数据加载机制】:
父进程的代码和数据是从磁盘加载的,子进程通过fork
默认继承了这些内容,但有独立的内存空间。- 【调用关系】:
子进程会获取调用它的父进程的id
信息(导致子进程PPID为父进程PID)。
6.4 父子进程运行顺序
当使用fork()函数创建出子进程,是先运行子进程还是父进程呢?这里先运行谁,是需要通过调度器去决定的。一般电脑只有单个CPU,调度器会根据当前进程中选择一个合适的进程放到CPU当中,进程之间会竞争CPU资源,所以调度器会遵循自己的一套原则来保证进程之间的公平性(进程优先级、时间戳)去决定。
6.5 创建子进程意义
如果父进程和子进程执行的后续代码完全相同,就没有实际意义。通常,父进程和子进程会各自执行代码的不同部分:父进程负责处理一部分逻辑,子进程负责处理另一部分逻辑。两个进程并发运行,从而提高程序的整体执行效率。
【问题】:问题在于fork()之后,父子进程执行后续代码完全相同,如何分开执行不同模块的功能呢?
fork
之后,父进程和子进程的代码虽然共享,但它们的执行路径可以通过 fork
的返回值来区分,从而保证父子进程执行不同的代码逻辑。
fork
的返回值:
- 在父进程中,
fork
返回子进程的PID
(正整数)。- 在子进程中,
fork
返回0
。- 如果
fork
失败,返回-1
给父进程。
通过检查 fork
的返回值,可以让父进程和子进程执行不同的代码逻辑,例如:
pid_t pid = fork();
- if (pid > 0) {// 父进程执行的逻辑}
- else if (pid == 0) { // 子进程执行的逻辑}
- else {// 错误处理逻辑}
这种方式确保了父子进程可以执行各自的任务,实现功能分工。
6.6 进程具有独立性
任何平台上,进程都具有独立性,这意味结束了某个进程不会影响其他进程,
在数据结构层面上,进程是并行的,你是你的我是我的;在逻辑上,它们是存在父子关系,但是仅仅是在数据结构指针层面上的关心。由于进程的代码和数据是从磁盘中加载进来的,所以子进程只能用父进程的代码,对于"数据"子进程也必须使用父进程的数据。
理论上,由于代码本身具有不可被修改属性,只有只读属性,所以父子支持共享代码。数据不一样,数据是可以被修改的,这样导致"子进程必须拷贝一份相同的数据"且独立出来。
问题在于父进程的数据可能非常多,但是可能子进程只是共享了其中一部分代码,不一定需要使用到父进程的所有数据。如果子进程无脑将这些数据拷贝过来,会存在大量资源浪费,效率也会大幅度减低。
6.7 Bash父进程简介
在系统启动时,操作系统被加载到内存,同时负责启动对应的bash父进程。当用户在命令行输入指令时,bash会为每条指令创建一个子进程,由子进程负责执行该指令。即使指令执行失败,也不会影响bash父进程的正常运行,这种机制有效保护了bash父进程,使其能够专注于命令行解释的核心工作。
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是Linux笔记,希望对你在学习Linux旅途中有所帮助!