◆ 博主名称 晓此方-CSDN博客大家好欢迎来到晓此方的博客。⭐️Linux系列个人专栏 【主题曲】Linux⭐️此方的GitHub github_此方⭐️Re系列专栏我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)文章目录概要序論一、进程的概念1.1进程的课本概念1.2进程的实际概念1.2.1什么是PCB/task_struct1.2.2操作系统内部组织的进程链表1.2.3输出两个结论1.3初见系统调用与查看我们的进程1.3.1系统调用存在哪里1.3.2第一个系统调用1.3.3查看一个进程1.3.4进程与工作目录1.4进程父子关系1.4.1进程树1.4.2子进程的创建1.4.3一个函数为什么可以有两个返回值1.4.4 else 和 if else 为什么可以同时执行1.4.5 一个局部变量为什么可以同时等于 0 和大于 01.4.6补充一个有趣的进程——守护进程了解概要序論Hello大家好我是此方上文我们初步探讨了[进程]这个话题了解了冯诺依曼体系结构和软硬件体系的层状结构最重要的还是一句话“先描述再组织。”今天将正式开始对进程概念的理解。好的现在我们开始吧。一、进程的概念1.1进程的课本概念课本概念程序的一个执行实例正在执行的程序等。内核观点担当分配系统资源CPU时间内存的实体。当前进程 内核数据结构(task_struct) 自己的程序代码和数据。如果说进程的概念我就这么讲的话未免也太不负责任了以上的概念实际上是已经了解它的人提出的精辟的总结不适合我们直接拿来讲。那么进程——到底是什么1.2进程的实际概念1.2.1什么是PCB/task_struct我们上一篇文章中了解到程序没有被运行起来的时候都是一个个在磁盘上的文件在运行起来之后就要被加载进内存。此时操作系统OS必然要对多个被加载到内存中的程序进行管理。管理的核心逻辑是先描述再组织。先描述操作系统描述进程的各种数据创建了一个PCBPCB是什么PCB全称是Process Control Block即进程控制块。PCB就可以理解为进程的一个“户口本”。在 Linux 操作系统中用于描述进程的结构体叫做struct task_struct(PCB和task_struct是抽象和具体的关系)。它包含了进程的所有属性。我们举例几个后面的文章中都会讲,实际上这里面有上百个描述信息标示符描述本进程的唯一标示符用来区别其他进程。状态任务状态退出代码退出信号等。优先级相对于其他进程的优先级。程序计数器程序中即将被执行的下一条指令的地址。内存指针包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下文数据进程执行时处理器的寄存器中的数据[休学例子要加图CPU寄存器]。I / O状态信息包括显示的I/O请求,分配给进程的I / O设备和被进程使用的文件列表。记账信息可能包括处理器时间总和使用的时钟数总和时间限制记账号等。1.2.2操作系统内部组织的进程链表再组织操作系统再每一个task_struct里面都放了一个指针于是每一个PCB连接在一起。形成了一个链表。最终对进程的管理最终就变成了对链表的增删查改。增加加入一个新的PCB然后用它指向我们新的被加载进来的代码与数据删除一个代码运行结束将我们的一个对应的PCB删除从链表中)操作不是直接去操作我们的代码与数据而是去找我们的PCB用PCB去修改我们的代码与数据但是我们还是不是很清楚进程的链表到底是什么试想一个场景我们在面试的时候排成一个长队然后一个一个找面试官。比喻面试官为CPU那么我们就是PCB。但是实际上这种模型不符合实际准确的来说不是我们的人在排队而是我们的简历在排队——也即是:我们人是数据和代码而简历就是对应我们人的PCB。于是先描述再组织的结构就清晰了。1.2.3输出两个结论没错关于进程我们首先要输出两个颠覆你想象的概念进程PCB加载到内存的代码和数据而不是代码和数据。操作系统对进程的管理操作系统对链表的增删查改。这也是解耦。1.3初见系统调用与查看我们的进程1.3.1系统调用存在哪里看完上面的理论部分这会是我们第一次触碰系统调用。首先得知道它在那里查找。如下的man手册前者是系统调用后者是库函数调用。1.3.2第一个系统调用1#includestdio.h2#includeunistd.h3#includesys/types.h5intmain()6{7while(1)8{9sleep(1);10printf(我是一个进程我的pid: %d\n,getpid());// 获取当前进程的pid11}12}unistd.h包含了各种系统服务的函数原型是访问系统调用如getpid的门户。sys/types.h提供了进程 ID 等数据类型的定义如pid_t。sleep让进程进入休眠状态。后面进程状态回讲getpid通过系统调用直接向内核申请获取当前进程的唯一标识符PID。那么除了getpid以外我还要补充一个系统调用getppid获取父进程的pid。1#includestdio.h2#includeunistd.h3#includesys/types.h5intmain()6{7while(1)8{9sleep(1);10printf(我是一个进程我的pid: %d\n,getppid());// 获取当前进程的父进程pid11}12}1.3.3查看一个进程那么进程创建起来了让他跑起来后如何查看它呢我们可以使用ps axj命令。该命令会列出系统中所有进程的详细状态信息。为了更高效地观察进程我们通常会结合管道和脚本进行优化基础过滤使用grep 文件名筛选特定进程。去除干扰加上grep -v grep过滤掉过滤指令本身的进程。保留标题栏使用head -1显示ps输出的首行属性说明。实时监控脚本使用while循环构建一个简单的自动化脚本while:;dopsaxj|head-1psaxj|grepmyprocess|grep-vgrep;sleep1;donepid不会一成不变当我们ctrl c杀死进程后再次运行看到的pid就不一样了。补充杀死进程的方法还有一个通过信号杀死kill -9 进程pid。但是细心的你肯定发现了有一个pid是始终不变的查看它我们发现它就是bash。所以说命令行解释器本质上也是一个进程它是一个会在你的终端不断的打印一个譬如[zbcbite…]$并且不断的阻塞等待你输入的一个进程。操作系统对每一个用户都会创建一个bash。bash前面的一横杠表示“远程登录”除了上述的方式查看进程还有一种方式通过 /proc 目录查看进程系统把你的一个一个进程都转化称为了文件。进程结束文件消失。或者打开一个指定的进程的文件夹/proc/21381, 好我要补充两个小知识。1.3.4进程与工作目录这两个是什么exe -/home/whb/code/code/merge_class/lesson12/myprocess cwd -/home/whb/code/code/merge_class/lesson12第一个是进程的路径让进程知道自己是哪里来的。它的代码和数据在磁盘的什么位置把它干掉不会影响进程的正常运行。一般情况下后面讲挂起的时候再讲这个地址的重要性当然你给他干掉了它会跳红警告。第二个是进程的当前工作目录。文件拼接当我们在代码中使用相对路径打开文件如fopen(hello.txt, w);时系统会自动将cwd的路径与文件名进行拼接从而确定文件在磁盘上的位置。这也是为什么新建文件默认会出现在可执行文件同级目录的原因——因为进程启动时的cwd默认继承自父进程通常是 shell所在的路径。动态修改chdir系统调用 进程可以通过调用chdir接口来动态改变自己的工作目录。#includeunistd.hchdir(/home/whb);// 将进程的工作目录切换到 /home/whbfopen(hello.txt,a);// 此时文件将创建在 /home/whb 目录下有没有这么想过其实shell也是一个进程于是cd命令很可能内部就封装了一个chdir事实确实如此。1.4进程父子关系1.4.1进程树我们的进程是一个单亲繁殖系统类似于B树我们还没有学一颗多叉树从根结点出发延伸出多个子进程。譬如我们一切的命令都有一个共同的父进程bash。1.4.2子进程的创建那么进程应该如何创建呢我们先来介绍一个创建子进程的系统调用fork。头文件#include unistd.h快速了解一下这个系统调用一句话核心fork()就是进程的“分身术”——调用一次原地复制出一个几乎一模一样的子进程让程序实现真正的“分头行动”。作用*创建分身产生子进程继承父进程的代码和数据。*并发处理让父子进程同时运行互不干扰地处理不同任务。*环境隔离子进程在独立空间运行崩溃不影响父进程。我们写一个代码然后慢慢讲#includeiostream#includeunistd.h#includestring#includesys/types.husingnamespacestd;voidFunc_Test_Process(){pid_t _idfork();if(_id0){stringstrerror(fork fail);throwstrerror;}elseif(_id0){while(1){printf(这是一个子进程子进程的pid是: %d ,子进程的ppid是: %d .\n,getpid(),getppid());sleep(1);}}elseif(_id0){while(1){printf(这是一个父进程父进程的pid是: %d ,父进程的ppid是: %d .\n,getpid(),getppid());sleep(1);}}}intmain(){try{Func_Test_Process();}catch(stringstr){coutstrendl;}}如上父子进程都拿着各自的_id去执行对应的代码。我们的父进程创建一个子进程默认是将父进程的PCB中的数据拷贝一份给子进程并修改一部分内容比如pid和ppid。于是子进程自然而然地就会指向父进程的那段数据与代码。于是自然而然地子进程也会去执行父进程后面的那一段代码。相当于有两个进程同时执行代码。这里父进程获取到子进程的pid并且子进程获取的id结果是0.为什么父进程必须获得每一个子进程的pid方便做管理子进程不必获得父进程的pid因为子进程可以用ppid如果你看到这里一定被很多问题困惑着没错它确实颠覆了你从C到C到数据结构以来的代码经验。于是我们有三个问题需要解决这些问题看似都违反了C的语言规则。else和ifelse为什么可以同时执行一个局部变量为什么可以同时0和0一个函数为什么可以有两个返回值1.4.3一个函数为什么可以有两个返回值我们首先来解决第三个问题一个函数为什么可以有两个返回值为什么fork可以返回两个值首先我想要问你一个问题一个函数到达了return它的核心功能做完了没有答案是做完了fork内部也是如此也就是说fork在return之前就已经完成了申请新的pcb拷贝父pcb给子进程子pcb放入进程list甚至放入调度队列中那么精确到语句层面子进程被创建的时间结点一定在return之前于是fork内部必然会有两个进程返回两个值。1.4.4 else 和 if else 为什么可以同时执行在之前的认知里if else和else永远互斥。但在 Linux 系统编程中这个规则被“进程”这个维度打破了。我们要明确一个概念代码是共享的但执行流是独立的。正如前面所说当fork函数核心逻辑执行完毕时系统里已经存在两个独立的执行流父进程和子进程。父进程拿着fork返回的子进程 PID继续向下执行。它遇到了if(_id 0)逻辑判断为真于是进入该分支。子进程拿着fork返回的 0也继续向下执行。它遇到了if(_id 0)逻辑判断为真于是进入该分支。所以并不是“在一个进程里同时执行了两个分支”而是两个进程各自执行了属于自己的那个分支。由于它们几乎同时在屏幕上打印才给了你一种“代码逻辑被违背”的错觉。我只能先讲到这里这个问题还有疑点我必须放在进程地址空间里面讲。1.4.5 一个局部变量为什么可以同时等于 0 和大于 0这可能是最令你崩溃的地方同一个变量_id怎么可能既是 0 又是进程号这里涉及到了 Linux 内存管理的核心技术——写时拷贝。1.初始状态fork刚完成时子进程的页表指向的物理内存和父进程是同一块。也就是说它们最初确实共享同一个_id的内存空间。2.发生写入当fork准备返回结果并写入_id变量时操作系统发现有两个进程尝试操作这块内存。3.触发拷贝为了保证进程的独立性互不干扰操作系统会为子进程重新开辟一块物理空间把父进程的内容拷贝过去然后修改子进程页表的映射关系。你在父进程里看_id它映射的是一块存着子进程 PID 的物理内存你在子进程里看_id它映射的是另一块存着 0 的物理内存。由上我们其实还能得出一个结论进程具有独立性。两个进程的PCB数据结构是独立的。代码不可修改后面讲进程程序替换的时候说明这里其实说的不对即使共享也不会影响数据又有写时拷贝护着。1.4.6补充一个有趣的进程——守护进程了解采纳自Gemini守护进程和精灵进程指的是同一种东西。中文术语守护进程。英文术语Daemon原意为“精灵”、“守护神”故部分翻译称之为精灵进程。什么是守护进程守护进程是生存期长的一种进程。它们通常在系统引导装入时启动在系统关闭时终止。它的三个核心特征后台运行它没有控制终端TTY你无法在屏幕上直接看到它的输入输出。独立于终端即使你关闭了当前登录的 Shell 窗口它依然在运行不受用户登录、注销的影响。周期性/等待性它通常在等待某个事件发生比如 Web 服务器等待请求或周期性执行任务比如磁盘清理。好的本期内容就到这里如果对你有帮助还不要忘记点赞三联支持。我是此方我们下期再见。bye!