目录

Linux_17进程控制

Linux_17进程控制

前提回顾:

页表可以将无序的物理地址映射为有序的;

通过进程地址空间,避免将内存直接暴漏给操作系统;

cr3寄存器存放的有当前运行进程的页表的物理地址;

一、查看命令行参数和环境变量的地址

https://i-blog.csdnimg.cn/direct/c5870851f5dc4a6990fcb234fdfb1d91.png

因为命令行参数和环境变量都是字符串的形式,所以这里我们可以通过字符串直接打印地址;

https://i-blog.csdnimg.cn/direct/62a893145cc44ac18149dd4b95d41a47.png

通过验证最后可以知道:命令行参数和环境变量位于栈区的上方!

(子进程继承父进程的环境变量 — 因为进程地址空间存放的有!)

代码是不可写入的(无论是父进程还是子进程,映射到代码区,页表对应的权限关系是只读的!)

操作系统怎么识别到父子共享的数据块,此时需要进行写时拷贝呢?

https://i-blog.csdnimg.cn/direct/af00df22d129451a80e36f79e9a2ac11.png 如果我们要对当前的父进程的数据进行写入的时候,无论数据是只读还是可写,系统都会将其改为只读权限!(子进程也是只读),此时如果想写入就会触发系统权限的问题(不做异常处理),此时操作系统会对要访问的区域进行检查,如果权限是可写的就会进行写时拷贝!

通过fork创建多个进程

下面我们通过循环创建多个线程:

https://i-blog.csdnimg.cn/direct/50e7abc4325a42508f724398376163b4.png

父子进程(或兄弟进程)被创建出来后,谁先运行不可知!(由调度器决定!)

进程退出的场景都有哪些?

  • 代码运行完毕,结果正确;
  • 代码运行完毕,结果不正确;
  • 代码异常终止;

进程中,一般谁会关心我们的运行情况呢?(— 父进程!)

相比于正确的结果,我们一般关心的是为什么程序运行会出错?

在C语言中,可以用return的不同返回值数字,表征不同的出错原因(即退出码);

即:main函数的返回值得到本质表示的是:进程运行时是否是正确的结果,如果不是,可以用不同的数字来表示不同的出错原因!

使用 $? 可以获取上一个进程运行结束后的退出码(最近一个进程运行的退出码);

https://i-blog.csdnimg.cn/direct/1391cec651cf4684af5bfdf5a91e8218.png

这里第一次执行查看的时./myproc的进程码,接下来几次查看的都是echo的进程码!

错误码0、1…都是给计算机查看的,人自己查看可以通过Linux提供的接口将其转化为字符串(需要包含string.h这个头文件);

https://i-blog.csdnimg.cn/direct/82fda05f946442d381a7cc9b99b2a355.png

https://i-blog.csdnimg.cn/direct/5d0edb1f2f59446f9388a0b284d1a37e.png

这里当我们进行ls查看一个不存在的文件的时候,此时我们使用echo $? 打印的结果正好是错误代码中2!

我们也可以自定义错误码,而不是一定非要根据系统的体系走;

https://i-blog.csdnimg.cn/direct/190d7a809e1a4aeeb1a4408aa84a8ef6.png

此时返回错误码对应的自定义的下标即可!

C语言有个errno全局变量!(用于保存最近一次执行的错误码!)

当我们调用全局函数失败的时候,此时errno会返回对应的错误码!

https://i-blog.csdnimg.cn/direct/3ee965b7f94a40cba9c9a78a2585742b.png

https://i-blog.csdnimg.cn/direct/5dbbd375e3ac42c8b738bea9b60f13b4.png

此时我们可以打印errno对应的错误代码,并将其返回给父进程,让父进程直到此时调用有问题;

代码异常终止,我们可以认为:代码可能没有跑完(如果程序在main返回之前异常终止,此时我们认为该错误码没有意义,不关心退出码!);

进程的控制是由信号控制的:

https://i-blog.csdnimg.cn/direct/8912887b9ba549eaa0fd1f4774cc3aed.png

我们可以通过kill -l对对应的进程进行控制;

结论:进程出现异常,本质上就是进程收到了对应的信号!

https://i-blog.csdnimg.cn/direct/7e929f2f9dac46d58d845cddc010a90c.png

这里exit()在任何地方被调用后,都表示调用该进程直接退出!(不会执行后面的打印语句)

https://i-blog.csdnimg.cn/direct/958cd2dd8ca947dc9b3f757db97fecb5.png

此时如果我们进行return,函数会直接返回,然后在main里面继续打印printf的内容!

_exit()这是一个系统调用接口;exit是库函数!

https://i-blog.csdnimg.cn/direct/2a033302003d4520ace3a8854df0f4b3.png

exit()实际上是先将对应的缓存区和自定义的函数清理,然后再调用_exit() ;

https://i-blog.csdnimg.cn/direct/ffebe87e7d5c4e88b0f5e87a7633c4ee.png

此时我们提出一个问题:这个缓存区应该在内存的那个位置?

一定不在内核区,如果在内核区的话此时_exit()也会刷新缓存流!

二、进程等待

我们要通过等待进程,获取紫禁城的退出情况 — 直到我们给子进程布置的任务完成的怎么样,可以关心也可以不关系(不关心就设置为NULL)

异常实质上就是信号!(运行中报错)

如果我们对应的wait在调用的时候,此时子进程一直在执行工作,没有退出;

那么此时父进程就会阻塞等待子进程!(阻塞状态不仅发生在等待硬件 - 键盘、也会发生在等待软件 - 进程)

输出型参数:在函数调用的时候,由函数内部修改其值并返回调用者的参数(例如malloc)

status就是一个输出型参数!

父进程要拿子进程的状态数据,为什么必须要用wait等系统调用呢?

进程具有独立性!如果我们全局变量,此时子进程将这个全局变量进行修改,无法传递给父进程!(必须得通过系统调用来实现!)

https://i-blog.csdnimg.cn/direct/966ab788b5464b17aca4ceb016160a7b.png

前8位表示的是终止信号,(例如我们勇敢什么特殊信号杀掉进程);

第八位为标志位,此时我们暂不关心;

8~15表示的是退出状态(即子进程的退出状态码);

https://i-blog.csdnimg.cn/direct/c5997cdd164144b89ab90c423a2f4864.png

这里父进程在对应的数据和代码里面,通过系统调用接口,查询子进程的退出信息( exit_code, exit_signal ),其中这两个主要的信息包含在tast_struct里面!

因此waitpid函数的本质是: 读取子进程的task_struct的内核数据结构对象;

等待子进程返回信息的时候,什么时候会等待失败呢?

当等待的子进程不是对应的子进程!此时返回-1;

  • WIFEXITED(status):

    若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) —

    本质上检查的是信号位(也就是status & 0x7f — > 取小的七位!)

  • WEXITSTATUS(status):

    WIFEXITED非零,提取子进程退出码。

    (查看进程的退出码

    )也就是 (status»8)& 0xFF

用法如下所示:

https://i-blog.csdnimg.cn/direct/4b7c6cbf1b0743c484494df0c97cb7fd.png

waitpid中的option选项:

  • 当此时为0的时候,即为 阻塞等待方式;
  • WNOHANG(wait no 夯住了) — 非阻塞轮询!

阻塞等待方式:子进程此时一直为R状态运行,父进程需要等待,把父进程投递在等待队列中,

(我们可以认为子进程维护的PCB里面也维护了一个等待队列!— 为等待该进程的使用的)

引入非阻塞轮询的概念:

阻塞等待的时候,此时父进程只能等待,而不能先进行一会自己的工作;

但是非阻塞轮询此时会一直询问子进程的状态,如果没准备好,此时父进程会先忙自己的工作,过段时间再进行询问;

https://i-blog.csdnimg.cn/direct/df9c87043e4147bb964add1e934165ba.png

对于非阻塞轮询有三种返回条件:

  • ret_pid>0(也就是对应子进程的PID) | ret_pid <0,此时表示此时等待的条件已经就绪或者失败!
  • ret_pid = 0 表示还没有继续,此时会继续等待;

为了实现非阻塞轮询, 这里我们使用while循环来实现!

https://i-blog.csdnimg.cn/direct/2d41ff94920f4e63a90254dfda9312ab.png

信号是没有0号信号的!这是因为0号代表进程正常执行,没有异常,即没有信号干扰!

waitpid 中的 options 参数用于控制该函数的行为模式,允许父进程以更灵活的方式等待子进程的状态变化。以下是 options 参数的详细解析:

主要选项及其作用

  1. 0 (默认阻塞模式)

    • options 设置为 0 时, waitpid 会以 阻塞方式 等待子进程结束或状态变化。父进程会暂停执行,直到满足等待条件(如子进程终止)后才继续运行。
    • 适用场景 :需要父进程必须等待子进程结束后才能继续执行的简单场景 。
  2. WNOHANG (非阻塞模式)

    • 若子进程尚未结束或状态未变化, waitpid立即返回 ,而非阻塞父进程。此时返回值可能是:
      • 子进程 PID :子进程已结束,状态被成功收集。
      • 0 :子进程仍在运行,但未结束。
    • 适用场景 :父进程需要同时处理其他任务(如轮询多个子进程状态)时,避免阻塞以提高效率 。
  3. WUNTRACED (报告停止状态)

    • 当子进程因信号(如 SIGSTOP被暂停 (而非终止)时, waitpid 会返回其状态。
    • 需配合宏 WIFSTOPPED(status)WSTOPSIG(status) 解析状态,获取导致暂停的信号(如 SIGSTOP ) 。
  4. WCONTINUED (报告继续状态)

    • 当子进程因 SIGCONT 信号 恢复执行 时, waitpid 会返回其状态。需通过宏 WIFCONTINUED(status) 检测此状态 。

三、进程替换

excl类函数的作用:执行一个文件;

https://i-blog.csdnimg.cn/direct/9eb24b7ac2ca4fbc8877f50a9f668d09.png

标准写法要以NULL为结尾! (execl后面的代码不会执行!)

单进程的程序进行程序替换:

https://i-blog.csdnimg.cn/direct/54beed5258ec4cefa3c06d07fa547961.png

当我们名为mycommand的程序进行运行的时候,刚开始printf进行打印,执行到execl这一行命令的时候,此时会将ls的代码和数据移至内存当中(对原来的数据和代码进行覆盖)!此时整个页表左侧的结构和东西不变!(task_struct和进程地址空间和页表左侧)

当我们运行到程序替换所对应的程序的位置的时候:将新程序的数据替换老程序的数据;

https://i-blog.csdnimg.cn/direct/257c76759d7446f8a094e8ff8143759a.png

页表对应的左侧都没有发生变化!此时是页表对应的右侧的物理内存发生了变化,将新的程序加载到内存当中并替换页表;

execl如果执行成功!此时后面的代码都不会被执行!哪怕此时后面存在exit(0);

但是如果execl执行失败,此时函数会返回1,然后继续执行后面的代码!(execl后面的代码和数据也算是老程序的代码和数据)

问题:子进程执行程序替换的时候,是否会影响父进程?

当子进程进行进程替换的时候,不会影响父进程!此时子进程会进行写时拷贝!

程序替换不会创建子进程!只进行程序的代码和数据的替换工作!

程序替换的现象:

  • 程序替换之后,exec 之后的代码不会执行,替换失败呢,才有可能执行后面的代码;*
  • exec 函数只有失败才有返回值!!成功没有!*

问题:当我们加载新的程序的时候,CPU如何知道新的程序的入口地址?

  • Linux中的可执行程序是有格式的!(ELF),在可执行程序的表头中有可执行程序的入口地址;
  • 新进程的表头可以被CPU读取,进行替换新的进程;

execl接口介绍

https://i-blog.csdnimg.cn/direct/65f7c2dcff824967a5fe2011832766c4.png

list指的是像链表一样一个节点一个节点的进行传递(参数为可变参数);

所有的exec* 系列的函数的第一个参数都是为了找到执行该程序的地址;

找到该程序后应该怎么办?

主要是确定如何执行该程序,该程序需不需要覆盖选项?(命令行中如何传,此时我们就如何传)

execlp接口介绍

带p指的是:PATH,也就是说execlp会在自己默认的PATH中进行查找!

https://i-blog.csdnimg.cn/direct/0f0780ff7b2e4d5cb2d3b2f9fd6efeef.png 虽然这里显示我们调用写了两个ls,但是实际上!第一个参数ls是为了确定在哪里找到整个程序(即我们需要执行谁)!第二个ls是为了确定我们需要怎么进行执行!

execv接口介绍

这里的v指的是数组!

https://i-blog.csdnimg.cn/direct/7036b16ebea94d2a8e9053eac300a142.png

以NULL作为结尾!

https://i-blog.csdnimg.cn/direct/dc8a841cbac94f539fa9c8269b6f7b8e.png

实际上就是将我们对应的可变参数列表替换为了字符串指针数组!

https://i-blog.csdnimg.cn/direct/1d2ed24e71ee46738d8b06118aa72a55.png

这里在使用的时候我们需要先定义一个数组!

char* const myargv[]

https://i-blog.csdnimg.cn/direct/cfafadde41ca4c83bf2320a71b3abead.png

ls有main函数,那么ls的main函数有命令行参数吗?

https://i-blog.csdnimg.cn/direct/73347dcea8424f2ca49d75652148b310.png

有!这里ls的命令行参数是由myargv传入的!

execvp接口介绍

实际上就是vector + PATH!

https://i-blog.csdnimg.cn/direct/d9439fa6154645439ff1c15bc695758f.png

此时调用更加简单!

execle接口介绍

这里的e指的是env:也就是我们自己维护的环境变量!

补充点:对于C++的程序,后缀吹了.cpp,还可以为.cc或.cxx;

如何通过一个Makefile形成两个或者多个可执行文件?

https://i-blog.csdnimg.cn/direct/63c379d913b0480c81035e3a1abd9823.png

可以定义一个伪目标文件all;

此时形成这个伪目标文件需要先形成其他两个可执行程序!

https://i-blog.csdnimg.cn/direct/ca58273a98d1419ba5180610d43591f2.png

当前我们有两个可执行程序!此时我们想要通过mycommand来调用otherExe!

https://i-blog.csdnimg.cn/direct/c60c9d25a7a94bb883cdaf2c362fc6f8.png

我们可以通过下面的格式进行调用!

  • 第一个参数表示的是执行程序的路径(相对路径);
  • 第二个参数是我们需要执行什么程序;
  • 第三个参数是附加的选项(这里为NULL!);

问题:C/C++语言程序是否可以调用其他语言或脚本的程序?

答案:可以!

脚本语言开头都以#!为开头!后面对应的是脚本语言的解释器!

假如说我们当前有如下所示的脚本文件:

https://i-blog.csdnimg.cn/direct/3431ec7ec8fc41be99c8c023fa08c674.png

./shell脚本不能执行!(得通过bash来执行!)

bash  test.sh

此时如果我们想通过C/C++语言程序执行这个脚本,那我们应该怎么执行?

https://i-blog.csdnimg.cn/direct/378a94c3e725458ba32311f4860fb9b9.png 这里得第一个参数为bash解释器得地址!

第二个为需要执行的指令!(按照在命令行输入指令的形式)

如果当前我们有一个Python脚本也是对应的!

https://i-blog.csdnimg.cn/direct/738c44e4920d4936a85f888067f7a4ce.png

问题:无论是我们的可执行程序,还是脚本语言,为什么都能跨语言调用呢?

这是因为所有语言运行出来,本质上都是进程!

问题:我们的exec* 能调用系统指令,那么能不能执行我们自己的指令?

一个程序代码是否能通过exec*传递命令行参数,另一个可执行程序通过main中的命令行参数接受到可执行参数?

https://i-blog.csdnimg.cn/direct/dccfb877c84248379ac9f71217809c2d.png

https://i-blog.csdnimg.cn/direct/bc46b5031c0a47f3864bcf795a3cc127.png

这里我们没有传递环境变量,但是子进程依然可以显示环境变量!

问题:环境变量是什么时候传递给子进程的?

结论:环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了!(extern cahr** environ)

所以程序替换中,环境变量信息不会被替换!

问题:为什么程序被替换的时候,环境变量信息不回被替换?(deepseek)

1. 环境变量的存储位置与程序替换的覆盖范围

  • 环境变量存储区域 : 环境变量通常存储在进程地址空间的 栈区之上独立的环境变量表 中,与程序的代码段( .text )、数据段( .data )分离。程序替换仅覆盖代码段和数据段,而不会修改栈区和堆区等内存区域,因此环境变量所在区域未被触及。
  • 程序替换的本质 : 替换过程通过加载新程序的代码和数据到内存中,并更新页表映射,但不会修改进程的上下文信息(如环境变量、文件描述符等)。

2. 环境变量的继承机制

  • 父子进程的继承性 : 子进程通过 fork 创建时,会复制父进程的环境变量表。即使后续调用 exec 替换程序,子进程仍保留父进程传递的环境变量,除非显式指定新的环境变量数组。
  • 默认行为exec 系列函数(如 execlexecv 等)默认使用父进程的环境变量,仅当调用 execleexecve 时才会通过参数 envp[] 覆盖式传递 新环境变量。例如:
char *envp[] = {"PATH=/custom/path", NULL};
execle("/bin/program", "program", NULL, envp);  // 覆盖原有环境变量

3. 操作系统对进程上下文的管理

  • 进程控制块(PCB)的独立性 : 程序替换仅修改进程的代码和数据,而 PCB 中的优先级、环境变量指针等元数据保持不变。环境变量作为进程运行时的上下文信息,独立于代码逻辑。
  • 性能优化 : 环境变量的继承避免了重复加载和初始化,提升进程创建效率。若每次替换程序都重新加载环境变量,会增加系统开销。

问题:如果我们想给子进程传递环境变量,此时应该怎么进行传递呢?

  • 新增环境变量;

https://i-blog.csdnimg.cn/direct/9a1d131a83944810adb15a6a06172673.png

第一种情况:我们可以在bash上面新增一个环境变量,此时 mycommand是bash的子进程,会继承该环境变量;otherExe是mycommand的子进程,会继承其的环境变量,最后得到新增的环境变量!

第二种情况:我们不想让bash有该环境变量!

https://i-blog.csdnimg.cn/direct/5c540da2d8a149009f6edde456cf8405.png

https://i-blog.csdnimg.cn/direct/d6f7ea694e57496381f297980fee5566.png

需要在父进程中导入!

我们可以使用上面的系统调用接口新增环境变量!

第三种情况:通过exec* 系列带e的函数可以实现:

https://i-blog.csdnimg.cn/direct/bf9769221dc04bcab7276a6035824253.png

但是上面实际还是将父进程的环境变量传递过去,默认的也是将父进程的传过去!

  • 彻底替换;

https://i-blog.csdnimg.cn/direct/69a700fade7843fbad39870dbcc6f040.png

我们可以通过自定义环境变量,然后进行替换!

结论:这里execle采用的策略是覆盖而不是替换!

man的3号手册一般被称为库函数!(2号手册是系统调用!)

https://i-blog.csdnimg.cn/direct/817d3d5f0b334a1a86109e75a1ffcd9e.png

这里的execve实际上是系统调用接口!

https://i-blog.csdnimg.cn/direct/494ece02890b40769f3c0868477095c1.png

因此实际上其他库函数最后都是调用execve这个系统调用函数!

再谈shell

shell被称为外壳程序,shell/bash也被称为一个进程,执行命令的时候,本质上是创建子进程!

当我们在bash上面执行命令的时候,此时左侧对应的用户名 + 主机名 + 路径等,我们可以通过环境变量来获取(系统调用也可以!)

自定义简单shell

补充点1:C语言中,相邻字符串具有自动连接的特点!

如下所示:

printf("This is a very long string "
       "that spans multiple lines "
       "in the source code.");

输出结果: This is a very long string that spans multiple lines in the source code.

补充点二:fgets函数的用法(deepseek)

在Linux系统中, fgets() 是C标准库中用于安全读取字符串的函数,尤其适合处理文本文件的逐行读取需求。以下是其核心特性、使用方法和注意事项的综合解析:

  • 参数解析
    • str :指向字符数组的指针,用于存储读取的字符串。
    • n :最大读取字符数(包括结尾的 \0 ),即最多读取 n-1 个字符。
    • stream :输入流指针,可以是文件指针(如 fopen() 返回的)或标准输入( stdin ) 。

返回值

  • 成功时返回 str 指针;
  • 失败或到达文件末尾时返回 NULL ,需通过 feof()ferror() 判断具体原因 。

assert在编译的时候起效果,运行的时候没有效果!

注意点一:

对有的编译器来说,有时候创建变量若没有使用,则此时会出现警告甚至是报错,因此此时我们可以采用下面这种方式:

https://i-blog.csdnimg.cn/direct/52db6189c5c5446bad268c956f6dd1eb.png

通过(void)s 避免报错;

注意点二:

这里我们为了不想当输入完指令后,此时回车也会被记录到对应的指令中,因此此时我们可以进行下面的处理:

https://i-blog.csdnimg.cn/direct/dbb7039e48854887b1d1f0c67d419f51.png

这里的strlen会带上\n,因此此时我们将对应的\n替换为\0,即可实现!

注意点三:

为实现不同的功能,这里我们需要对字符串进行分割,这里我们调用一个函数strtok;

strtok 是C标准库中用于分割字符串的核心函数,常用于解析以特定分隔符分隔的文本数据。

#include <string.h>
char *strtok(char *str, const char *delim);
  • 参数
    • str :首次调用时传入待分割的字符串; 后续调用需设为 NULL ,以继续处理剩余部分。
    • delim :分隔符集合字符串,函数会将其中任意字符视为分隔符。
  • 返回值 :返回指向当前子串的指针,若无可分割内容则返回 NULL

该函数一次只会分割一次字串!

strtok的基本用法:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "apple,banana;cherry";
    char *token = strtok(str, ",;"); // 分隔符可以是逗号或分号

    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, ",;"); // 后续调用传入NULL
    }
    return 0;
}

这里需要注意的是,后续用来分隔的时候,需要传入NULL;

输出如下所示:

apple
banana
cherry

当我们执行mkdir等的时候,都可以正常运行,但是执行cd ..  或者 cd /等却不能正常运行!

这是因为这些命令我们都是通过fork之后的子进程来运行的!但是子进程的运行结果不会影响父进程!

因此这些命令需要父进程自己来运行!这些命令也就成为内建命令!

在自定义的shell时,内建命令需要我们自己罗列起来;

引用新的系统调用接口:chdir

在Linux系统中, chdir 函数是用于 更改进程当前工作目录 的核心接口,属于C标准库(libc)的一部分。

函数原型:

#include <unistd.h>
int chdir(const char *path);
  • 参数path 可以是 绝对路径 (如 /home/user )或 相对路径 (如 ../doc ) 。
  • 返回值 :成功返回 0 ,失败返回 -1 并设置 errno 以指示具体错误 。

函数的使用场景:

#include <stdio.h>
#include <unistd.h>

int main() {
    char buf[256];
    getcwd(buf, sizeof(buf));  // 获取当前目录
    printf("原目录: %s\n", buf);

    if (chdir("/tmp") == 0) {  // 切换目录到/tmp
        getcwd(buf, sizeof(buf));
        printf("新目录: %s\n", buf);
    } else {
        perror("chdir失败");
    }
    return 0;
}

输出结果如下所示:

原目录: /home/user
新目录: /tmp

引用函数调用接口:sprintf

sprintf 是 C 语言标准库中用于将 格式化数据写入字符数组 的核心函数,其作用是将变量、常量等数据按指定格式组合成一个字符串,并将结果存储到用户提供的缓冲区中。

函数原型与参数

int sprintf(char *str, const char *format, ...);
  • str :指向目标字符数组的指针,用于存储结果字符串。需确保缓冲区足够大,否则可能引发溢出 。
  • format :格式字符串,包含普通字符和格式说明符(如 %d%s )。格式说明符决定参数的类型和显示方式 。
  • ... :可变参数列表,参数数量与格式字符串中的 % 标签数量一致 。

示例场景:

int num = 123;
char buffer[20];
sprintf(buffer, "%d", num);  // 转换为 "123"

引用函数调用接口:getcwd

getcwd 是用于获取进程当前工作目录(Current Working Directory)的 C 标准库函数,其底层通过系统调用实现。

#include <unistd.h>
char *getcwd(char *buf, size_t size);
  • 参数
    • buf :存储路径的缓冲区,若为 NULLsize=0 ,函数自动分配内存(需手动 free 释放)。
    • size :缓冲区大小,若路径长度超过 size ,返回 NULL 并设置 errno=ERANGE
  • 返回值 :成功返回路径指针,失败返回 NULL

https://i-blog.csdnimg.cn/direct/bd86f955595c43c2ab9406578204e098.png

这里我们还需要处理export和echo打印环境变量!

这里export也需要是内建命令!如果不是内建命令,当我们到入环境变量时,通过fork子进程导入对应的环境变量,此时父进程无法显示!但是如果父进程执行该命令,此时由于继承环境变量,子进程也会有新的环境变量!

当我们们登录的时候,系统会帮我们启动一个shell进程,此时可以引出问题:shell本身的环境变量表是从哪里得来的?

命令行中所有执行的环境变量都是从bash得到的。

https://i-blog.csdnimg.cn/direct/e226561d4a834a01814d3cea546659be.png

在当前用户的家目录下,有这个.bash_profile这个文件!

即当用户登陆的时候,shell会读取用户目录下的.bash_profile文件,里面保存了导入环境变量的方式!