【Linux修炼】11.进程的创建、终止、等待、程序替换

本节重点

进程的创建终止等待进程的程序替换和进程地址空间强相关

1. 进程的创建

1.1 fork函数初识

在之前的进程创建中已经提到过fork因此在这里的初识是在原有基础上进一步了解。

在linux中fork函数是非常重要的函数它从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
//返回值子进程中返回0父进程返回子进程id出错返回-1

那么在调用fork函数之前只有一个进程当进程调用fork时当控制转移到内核中的fork代码后内核做

  • 分配新的内存块和内核数据结构给子进程内核数据结构PCB地址空间+页表构建对应的映射关系
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回开始调度器调度

对于第三点的添加系统进程列表我们在之前的进程的章节中介绍是由链表存储而实际上当时是为了便于理解操作系统实际上没有那么笨其实际上是由哈希表存储的通过struct task_struct类型的指针数组存储当运行需要的进程时则将会通过指针找到对应的进程控制块。image-20221213194017235

1.2 fork的返回值问题

对于这个问题从三个层次去理解。

1. 如何理解fork函数有两个返回值问题image-20221213212137243

对于fork函数当调用时fork函数内部会有两个执行流对应父进程和子进程当fork函数内部代码执行完毕后子进程也就被创建好了并有可能在OS的运行队列中准备被调度了父进程和子进程各自执行return这样在main()函数中调用fork函数时从fork返回的两个执行流就会分别执行main()调用fork之后的代码因此我们之前所了看到的两个结果就是父子进程对应的执行流所造成的。

2. 如何理解fork返回之后给父进程返回子进程pid给子进程返回0

父亲孩子 = 1n n>=1因此孩子找父亲具有唯一性。而由于子进程多父进程想具体调用某一个子进程时就需要这个子进程得有一个名字才能调用这个子进程因此给父进程返回对应子进程的pid。

3. 如何理解同一个id值怎么会保存两个不同的值让if else if同时执行

对于pid_t id = fork()我们知道返回的本质就是写入所以谁先返回谁就先写入对应的id由于进程具有独立性因此进程就会进行写时拷贝上一篇详细描述了因此同一个id地址是一样的但内容却不同。

1.3 写时拷贝

上一篇的进程地址空间中我们已经提到过什么是写时拷贝但不是单独分一个专题去写的因此这里总结一下写时拷贝。

通常父子代码共享父子再不写入时数据也是共享的当任意一方试图写入便以写时拷贝的方式各自一份副本。虚拟内存就是进程地址空间

即当我们不修改数据时父子进程的虚拟内存所对应的物理内存都是同一块物理地址内存当子进程的数据被修改那么就会将子进程修改所对应数据的物理内存出进行写时拷贝在物理内存中拷贝一份放在物理内存的另一块空间将子进程虚拟内存与这个新的地址通过页表进行关联。

1.4 创建多个进程

创建多个进程可以使用如下代码image-20221216214436332

由于开的进程过多会导致整个OS崩掉只需要重启服务器就可以解决了。

2. 进程终止

2.1 进程退出码

我们在C/C++中在代码最后都会写上return 0;对于这个返回值我们称它为进程退出码。对于正确的进程一般都以0作为进程退出码而非0就作为错误的进程的退出码因此不同的错误对应的退出码也是不同的。

退出码的意义 0success, !0表示失败。0具体是多少即表示不同的错误。——数字对人不友好对计算机友好。

对于如下代码image-20221216234348431

这个函数的返回值是4950因此退出码是1。当进程执行之后可以通过一个命令查看具体的进程退出码echo $?image-20221216234537868

但当继续执行这个命令时发现结果是0这是因为这个命令只会显示最近一次的进程退出码而下一个为0的原因就是echo本身也是一个进程并且正确执行因此显示的是0。

在这里回顾一下之前的函数strerror(n),n为自然数即n的不同的值就代表着不同的错误。那我们就可以执行这样的一段代码

for(int i=0; i<200; i++)
{
    printf("%d: %s\n", i, strerror(i));
}

image-20221216235229653

执行结果发现只有0代表着success其他的都对应不同的错误并且有133个不同的错误一共有134个进程结果。

image-20221216235417643

而对于我们指定指令的随意选项造成的错误No such or diectory就就对应着数值为2的错误。

总结一下

  • ./mytest ———— 运行一个进程
  • echo $? ———— $?永远记录最近一个进程在命令行中执行完毕时对应的退出码main->return?😉

进程退出的情况

  1. 代码跑完了结果正确 ——— return 0
  2. 代码跑完了结果不正确———return !0; 退出码这个时候起效果。确定对应的错误
  3. 代码没跑完程序异常了退出码无意义。

那么进程如何退出呢接下来就来解释一下前两种情况

2.2 进程如何退出

1. main函数return返回

这也是我们经常用的方式

2. 任意地方调用 exit(code)退出

code为退出码下面就演示一下image-20221217170442003

结果显而易见当我们查看这个进程是如何结束的直接观察退出码image-20221217170552816

此外在函数内部exit时进程也会直接结束函数也不会有返回值下面就来看看这个例子image-20221217170837686

image-20221217170911136

到exit语句就会将进程结束后面的代码也就不会再去执行了。

3. _exit()退出

我们看一下_exit()是如何退出的。image-20221217172856383

image-20221217172955263

我们发现其也是和exit()一样的功能。事实上_exit()是系统调用的函数也就是OS而exit()是库函数库函数是OS之上的函数调用exit实际上就是exit内部调用_exit但二者之间也会有区别我们将换行符去掉来演示一下exitimage-20221217175319034

结果

11.27_1

可以看出进程结束后会刷新缓冲区打印的结果暂停2秒也会显示出来。再来看看_exit:image-20221217175722684

11.27_12

这样并没有打印出结果也就是说_exit并没有刷新缓冲区。

因此总结一下二者

  1. exit终止进程主动刷新缓冲区
  2. _exit终止进程不会刷新缓冲区

image-20221217180924329

因此用户级的缓冲区一定在系统调用之上具体位置会在基础IO的时候说明。image-20221217181708006

3. 进程等待

3.1 进程等待的原因

  • 之前讲过子进程退出父进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏。
  • 另外进程一旦变成僵尸状态那就刀枪不入“杀人不眨眼”的kill -9 也无能为力因为谁也没有办法杀死一个已经死去的进程。
  • 最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是不对或者是否正常退出。
  • 父进程通过进程等待的方式回收子进程资源获取子进程退出信息

总结进程为什么要等待回收子进程资源获取子进程退出信息即通过进程等待的方式解决僵尸进程的问题。

3.2 进程等待的方法

1. 回收子进程资源wait

我们需要了解wait这个函数通过man 2 wait打开手册image-20221218002055645

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值
    成功返回被等待进程pid失败返回-1。
参数
    输出型参数获取子进程退出状态,不关心则可以设置成为NULL

了解了关于wait的信息之后就试着使用一下wait()

image-20221218010034204

这段代码的目的是想演示僵尸状态下的子进程被回收的结果

即子进程先在循环中sleep10秒而父进程sleep15秒这样当子进程运行完毕exit时父进程在子进程结束的5s内不会回收子进程这就造成子进程变成Z僵尸状态当5s之后父进程就会通过wait回收子进程ret的接收的值就是子进程的进程退出码。最后得sleep(5)是为了让父进程再破案一段时间从而更好的观察状态。

那么这段代码我们编辑完成之后赋值ssh渠道进行观察进程的状态

11.27_12

一开始右侧执行脚本观察状态同时左侧运行mytest我们发现当子进程正在执行时子进程和父进程都处于S+状态当子进程执行完毕没有被父进程回收时的那5秒子进程就变成了Z+状态当父进程执行时通过调用wait将子进程回收子进程就结束了最后的5秒只剩下父进程处于S+状态。这就是父进程通过进程等待回收了僵尸进程子进程。

2. 获取子进程的退出信息waitpid

通过man 2 waitpid查询waitpid的信息image-20221218011450153

pid_ t waitpid(pid_t pid, int *status, int options);
返回值
    当正常返回的时候waitpid返回收集到的子进程的进程ID
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数
    pid
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0.等待其进程ID与pid相等的子进程。
    status:
        WIFEXITED(status): 若为正常终止子进程返回的状态则为真。查看进程是否是正常退出
        WEXITSTATUS(status): 若WIFEXITED非零提取子进程退出码。查看进程的退出码
    options:
    WNOHANG: 若pid指定的子进程没有结束则waitpid()函数返回0不予以等待。若正常结束则返回该子进程的ID。

接下来就按照参数的顺序进行演示

image-20221218014105344

1. 获取子进程status

  • wait和waitpid都有一个status参数该参数是一个输出型参数由操作系统填充。
  • 如果传递NULL表示不关心子进程的退出状态信息。
  • 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16比特位

而上面所说的实际上就是对于这个拿到子进程的退出结果实际上并不能直接反应出我们想要的结果其结果是一个复合类型我们需要将其进行拆分

image-20221218013950204

对于32个bit位在这里只有尾部16个bit位是有意义的因此我们将这些拿出来即0~7位返回0代表正常的终止信号返回0证明没有出问题8~15次低8位代表子进程对应的退出码。

执行image-20221218014641040

这样就能很好的观察终止信号和子进程的退出码。

3.若代码没跑完结果异常了在子进程中添加一个错误image-20221218014857017

image-20221218015211680

不同的错误通过信号的值可找到对应的错误。下面是struct task_struct的源码我们发现对于进程退出码和终止信号都在这个PCB中。

image-20221218020328806

即我们可以总结成一张图image-20221218020240820

上述的过程我们也再总结一下

  1. 让OS释放子进程的僵尸状态
  2. 获取子进程的退出结果如果子进程不结束父进程就会一直处于阻塞等待等待子进程退出

2. WIFEXITED(status)和WEXITSTATUS(status):

image-20221219135201557

运行一下结果image-20221219135249955

成功接收到了子进程正常退出的退出码。那如果子进程不是正常退出呢我们将cnt改成50这样会有充足的时间杀掉子进程让其异常image-20221219135856038

3.3 再谈进程退出

  1. 进程退出会变成僵尸会把自己的退出结果写入到自己的task_struct中
  2. wait/waitpid 是一个系统调用即以OS的身份进行因此OS也有能力去读取子进程的status。

即前两条都意味着子进程的退出信号和退出结果都保留在子进程的PCB中。

3.4 进程的阻塞和非阻塞等待

在此之前我们先以一个例子解释阻塞和非阻塞

在一所学校中有张三和李四这么两个人张三经常逃课因此什么也不会李四认真听讲学的非常好。考试周到了张三约好李四让其辅导张三并想着帮了这么大的忙得请李四吃顿饭。于是张三给李四打电话“李四现在有时间吗下楼请你吃个饭。”李四说“等我20多分钟我看完这本书就下去。”于是张三答应了下来但这期间张三并没有挂电话想着能够等待他看完的消息。现实中并不会出现这样的情况即便是舔狗也不会就这样两头电话打着双方却都很安静过了20多分钟李四看完了就这样二人通过电话彼此收到了消息。

过了几天之后张三考的还不错为了感谢李四的帮助想再请李四吃个饭这次李四仍然说请等我一会我处理完事情就下楼。而张三对与上次一直打电话但两头都沉默这种情况感觉很是尴尬于是这次就先挂了电话。张三一会看看书一会打打游戏又时不时的给李四打电话了解处理事情的进度就这样打了10几次电话后李四说我下楼了并且已经看到你了张三很是高兴便和李四出去吃饭了。

对于上面的这个例子张三第一次打电话并没有挂断电话就这样一直检测李四的状态这种状态实际上就是阻塞状态。

而对于第二次打电话并没有一直接通打的每一次电话都是一种状态检测如果李四没有就绪那么就挂断过一段时间再次检测而这种每一次打电话实际上都是一个非阻塞状态——而这多次非阻塞就是一个轮询的过程。因此打电话就相当于系统调用wait/waitpid张三就相当于父进程李四就相当于子进程。

对于阻塞等待我们上面已经演示过那么下面就直接上非阻塞状态的过程image-20221219164651999

对于这段代码设计理念是这样的子进程在执行期间父进程则会一直等待并通过while的方式去轮询非阻塞状态直到子进程退出。image-20221219165015178

如果子进程出异常了那么父进程也能够抓到为了演示这种情况我们在子进程中增加一个野指针的错误image-20221219165325701

image-20221219165436959

此时的退出码为0代表的是子进程的退出码而终止信号是11号错误对于异常的进程退出他的退出码是没有意义的所以我们返回为0的退出码也不看。

那什么时候会等待失败呢id错误的时候会等待失败。image-20221219170204832

image-20221219170308065

阻塞状态VS非阻塞状态

非阻塞状态有什么好处

**不会占用父进程的所有精力可以在轮询期间干干别的。**那么就来用代码演示一下image-20221219172342340

image-20221219194129020

但也不是非阻塞是最好的这两个状态是并行存在的并没有好坏之分。

因此这里我们再来回顾一下:

  • 进程等待是什么

通过系统调用让父进程等待子进程的一种方式。

  • 进程为什么要等待

答释放子进程僵尸获取子进程状态。退出码退出信号

  • 进程怎么等待

通过wait/waitpid通过指定方式阻塞或者非阻塞的方式进行等待。

4. 进程的程序替换

创建子进程的目的

  1. 想让子进程执行父进程代码的一部分执行父进程对应磁盘代码中的一部分

  2. 想让子进程执行一个全新的程序让子进程想办法加载磁盘是指定的程序执行新程序的代码和数据这就是进程的程序替换

4.1 见见猪跑

在这一小节中包含6种函数为了提前演示就在这里拿出一个函数看看进程程序替换究竟是什么样子。

int execl(const char *path, const char *arg, ...);//将指定的程序加载到内存中让指定进程进行执行

对于一个程序加载到内存去执行首先是找到这个程序然后通过不同的选项去以不同的方式去执行这与环境变量是一样的。因此对于此execl函数来讲第一个参数path就代表找到程序对应的路径第二个就代表选项选哪种方式运行程序的选项而后面的...

我们为他引入一个新的名词可变参数列表。顾名思义我们在C语言中的scanf以及printf类的函数无论传入多少个参数都没有限制实际上就是可变参数列表的作用因此excel里的可变参数列表的作用就是让我们能在传入选项参数时能够传入任意数量的选项。如 cmd 选项1选项2……

知道了这个函数功能之后开始操作

一、构建环境

  • 首先新建一个目录exec并将上一级的Makefile拷贝到当前目录下cp ../Makefile .
  • 然后打开Makefile,将里面的文件名替换成我们想要创建的文件名%s/mychild/myexec/gimage-20221224123751669
  • 编写代码函数execl的头文件是unistd.himage-20221224134759926

二、编译执行

image-20221224135152957

我们发现其就有了ls指令的功能ls也是一个程序。

三、修改完善

当然我们也可以将其增加选项命令执行对应的功能image-20221224135601339

执行之后对比正常的ls -a -l命令image-20221224135708508

发现二者无异。那么这就叫做进程的程序替换。

但是我们发现第一个printf打印出来了但是execl后面的printf却没有打印出来这是为什么呢通过下面理解

4.2 理解原理是什么、为什么、怎么办

image-20221224141101733

当我们执行代码时就会创建进程地址空间与物理内存磁盘之间形成映射关系当执行上面的代码时就是这样执行第一个printf会照常打印到了execl函数时就会发生进程的程序替换也就是说我们所编写的代码会被我们调用的execl对应磁盘内部的代码覆盖即将指定程序的代码和数据覆盖自己的代码和数据执行这个新的代码和数据所以我们明白了为什么execl后面的printf没有执行。

那在进程程序替换的时候有没有创建新的进程呢实际上是没有我们一开始所创建的虚拟空间并不会变化。

  • execl函数的返回值问题

我们知道只要是一个函数调用就有可能失败就是没有替换成功就是没有替换而对于这exec系列的函数失败了返回-1程序不被替换因此execl下面的代码也会继续执行。下面就演示一下随便打一个不存在的路径或者程序image-20221224142357412

image-20221224142509023

execl下面的代码也就正常执行了。而exec系列的函数调用成功是没有返回值的也不需要返回值因为进程被替换之候原本的代码就没有意义了即便返回了一个值也不会有什么作用还会有额外的开销。

  • 多进程的问题

这次我们通过fork创建子进程并在子进程执行对应的execl函数image-20221224145800438

image-20221224145919200

如果我们仍随便打一个不存在的位置或者程序那么code的值就会变成-1。那这个时候子进程调用的execl会影响父进程吗答案当然是否定的进位进程具有独立性下面就来理解一下具体是什么原因

image-20221224150806207

当只存在一个父进程时就会创建出上面这样的映射关系当fork函数开始执行子进程生成就会创建出子进程的PCB以及对应的虚拟内存、页表与父进程共享对应的物理内存image-20221224151416388

而当子进程调用execl时由于子进程发生改变本着进程直之间具有独立性的原则子进程就会发生写时拷贝将共享的数据段和代码段在物理内存的另一个位置进行写时拷贝并与新的位置形成映射这样便不会影响到父进程。此外我们也可以看出数据和代码都可以发生写时拷贝。image-20221224151841925

总结 虚拟地址空间+页表保证进程独立性一旦有执行流想替换代码或者数据就会发生写时拷贝

4.3 一个一个调用对应的方式

除了execl还有其他类似的接口六种以exec开头的函数,统称exec函数我们通过man execl查看image-20221224152256002

主要

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);//l(list) : 表示参数采用列表
int execlp(const char *file, const char *arg, ...);//p(path) : 有p自动搜索环境变量PATH
int execle(const char *path, const char *arg, ...,char *const envp[]);//e(env) : 表示自己维护环境变量
int execv(const char *path, char *const argv[]);//v(vector) : 参数用数组
int execvp(const char *file, char *const argv[]);//vp就是v和p的结合

一、函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

二、函数的具体原理及演示

下面就来演示其他几个例子

  1. execlp(const char *file, const char *arg, …)

ppath不用告诉我程序的路径只有告诉这个函数传入的名字就会自动的在环境变量PATH中进行可执行程序的查找。

image-20221224154245937

image-20221224154059177

上面的两个ls是不重复的第一个ls代表着要执行谁第二个ls代表着要怎么执行。

  1. int execv(const char *path, char *const argv[])

vvector可以将所有的执行参数放入数组中统一传递而不用进行使用可变参数方案

image-20221225231924397

image-20221225231958343

  1. int execvp(const char *file, char *const argv[])

v, p 与相当于两个组合一起

image-20221225232410038
每一个字符串强转成char*就不会有警告这样也可以正常编译
image-20221225232439000

  • 调用自己创建的程序

在这里我们已经看过了上面的几个调用方式事实上我们所调用的都是系统程序接下来就通过exec类的函数调用自己写的程序

在同一个目录中touch mybin.c并编写如下代码image-20221225234824621

我们需要用生成的myexec调用这个程序生成的mybin因此在Makefile中也需要改成能够同时生成myexec和mybin的指令对于Makefile文件只会生成第一个程序因此在这里这样改就可以同时生成image-20221225235128698

这样处理之后再将原myexec.c中的内容少加改动注mybin不是环境变量中的内容因此不能用带p的函数image-20221225235247076

处理完毕之后看看结果image-20221225235509159

这样就通过myexe.c调用了自己创建的mybin程序了。

对于这种调用方式是没有语言之间的隔阂的即我们可以通过C语言调用C++、Java、Python等等其他类型的语言当然也可以反过来调所以下面就演示一下用C语言的myexec.c调用一下C++写的程序吧

  • C调用C++程序

首先touch mycpp.ccimage-20221226000053068

通过指令g++ -o mycpp mycpp.cc就可以生成mycpp程序再将myexec.c中的路径改成mycpp的路径image-20221226000431638

下面就可以执行了image-20221226000553917

因此只要是程序就都可以调用。

  1. int execle(const char *path, const char *arg, …,char *const envp[])

e自定义环境变量

image-20221226003330612

再将我们的myexec.c内部修改成对应的函数调用:

image-20221226003355043

处理完毕之后看看结果image-20221226003524196

发现这样使用之后系统内部的环境变量使用不了只能使用自定义的。这是因为我们的函数的最后一个参数的原因最后的一个参数就是传入的环境变量没有传入就不会使用因此如果我们在myexec中将最后一个位置的参数改成environ(前面添加extern char** environ)的话就会反过来我们自定义的环境变量就不会生效只有系统的才会生效。但是我们想让两者同时生效这就引入了一个前几章提到的函数putenv image-20221226004340685

image-20221226004612588

这样就满足了我们的需求。

  • 问对于execle函数和main函数在进程调用的时候是谁先被调用呢

在我们之前的代码中main函数通常是这样的参数VS上没有是因为编译器在编译时自动生成image-20221226011041361

答exec先被调用。解释exec系列的函数的功能是将我们的程序加载到内存中我们知道一个程序要想运行必须加载到内存中让CPU去执行而对于LinuxOS来说程序加载是通过exec系列的函数加载到内存中的因此Linux中的exec系列函数也被称为加载器。因此我们可以改变问题的方式程序是先加载呢还是先执行main呢毫无疑问一定是先加载所以也就解释通了对于exec系列的函数和main函数一定是execz系列的函数先被调用。

那main也作为函数也需要被传参exec系列的函数和main函数的参数有什么关联呢image-20221226012332641

事实上他们的参数就是这种一一对应的映射关系即main函数被exec调用这是我们看不到的。

而对于exec系列中不带有env参数的那些函数照样能够拿到默认的环境变量是怎么拿的呢environ通过地址空间的方式让子进程拿到的。

对于虚拟地址空间我们回忆一下从下到上依次是代码区、已初始化、未初始化、堆区、栈区再往上就是命令行参数和环境变量而对于这个命令行参数和环境变量就是通过第三方变量environ这个虚拟地址以这个地址作为起始就可以拿到所有的环境变量如果需要的话也可以通过这个虚拟地址传入到main函数参数里去使用。

以上就是程序替换的全部内容。但是除了代码块中的那几个函数还剩下一个execvpe现在就可以返回上面观察函数参数参数类型都是一样的只不过是组合的形式出现而已因此也没必要再说明了。

此外呢上面的exec类的函数有了各种组合观察规律发现缺了一种组合execve那我们直接 man execve查看对应的信息发现其是单独出现在二号手册而上面的那些函数都是在三号手册最终得出一个结论execve是唯一一个系统调用的接口而上面的那些函数都是在execve基础上进行的封装封装是为了让我们有很多的选择性提供给不同的替换场景

现在就可以总结一下函数的特征image-20221226014404823

image-20221226014418251

在使用中忽略一些参数其实也是对的但为了理解最好不要那样做

4.4 应用场景模拟shell命令行解释器

我们将子进程的代码中的替换注释掉在添加成这样image-20221226015055934

不传入argv[0]的原因是argv[0]代表我的程序myexec这样的话就会出现死循环的情况因为会一直调用所以为了跳过我们从第二个元素argv[1]的地址开始。

image-20221226015629598

那如果我们将第一个./myexec去掉发现不就是相当于自己写了一个shell吗因此下面我们来编写shell命令行解释器

新建目录myshelltouch一个myshell.c 并编辑Makefile

image-20221227183321087

下面就来编写myshell.cimage-20221227183236194

编译运行image-20221227183547762

这样就可以很好的模拟出shell命令行解释器了但还有一个问题就是返回上一级路径时对于我们这个代码是这样的情况image-20221227183915764

但是按照正常的命令行来说应该是变化的因此下面就来尝试解决这个问题

  • 首先我们要知道什么是当前路径

因此在这里touch一个新的myproc.c来解释image-20221227185532441

复制ssh渠道并观察执行image-20221227190511620

当前进程的工作目录就是当前路径。 因此若是想实现路径的改变就需要实现进程工作目录的改变说到这里大家也应该明白这个当前进程的工作目录也是可以修改的。

  • 改变当前路径chdir函数

image-20221227190942767

下面不废话直接演示其是如何改变当前路径的image-20221227191311143

编译运行image-20221227191548842

我们发现这样就将这个进程的路径改变了也就是说如果我们再通过这个进程创建文件就会创建到此时这个/home/cfy的这个路径中。

那回到一开始为什么我们自己写的shellcd 的时候路径没有变化呢

在上面实现的shell模拟代码中我们fork出了子进程子进程有自己的工作目录因此cd更改的是子进程的工作目录子进程执行完毕继续用的是父进程就是我们的shell因此在这个过程中父进程也就是shell的工作目录并没有发生变化。

  • 将编写的模拟shell进行修改——修改当前路径

image-20221227200258246

image-20221227200404272

这样就补充了之前的不足。像cd这种不需要让我们的子进程来执行而是让shell自己执行的命令被称为内建/内置命令。 接下来还没完实现最后一个问题echo内建命令。对于echo我们知道通过echo $? 能够活获得最近一次进程的退出码和终止信号。最终代码image-20221227203636105

image-20221227205910943

完结

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux