0


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

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

进程的创建、终止、等待、程序替换

本节重点

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

1. 进程的创建

1.1 fork函数初识

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

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

#include<unistd.h>pid_tfork(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?

父亲:孩子 = 1:n, 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就作为错误的进程的退出码,因此不同的错误对应的退出码也是不同的。

退出码的意义: 0:success, !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_twait(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种函数,为了提前演示,就在这里拿出一个函数看看进程程序替换究竟是什么样子。

intexecl(constchar*path,constchar*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>`intexecl(constchar*path,constchar*arg,...);//l(list) : 表示参数采用列表intexeclp(constchar*file,constchar*arg,...);//p(path) : 有p自动搜索环境变量PATHintexecle(constchar*path,constchar*arg,...,char*const envp[]);//e(env) : 表示自己维护环境变量intexecv(constchar*path,char*const argv[]);//v(vector) : 参数用数组intexecvp(constchar*file,char*const argv[]);//vp就是v和p的结合

一、函数解释

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

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

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

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

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

image-20221224154245937

image-20221224154059177

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

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

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

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.cc

image-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命令行解释器

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

image-20221227183321087

下面就来编写myshell.c:image-20221227183236194

编译运行:image-20221227183547762

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

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

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

因此在这里touch一个新的

myproc.c

来解释:image-20221227185532441

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

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

  • 改变当前路径:chdir函数

image-20221227190942767

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

编译运行:image-20221227191548842

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

/home/cfy

的这个路径中。

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

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

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

image-20221227200258246

image-20221227200404272

这样就补充了之前的不足。像cd这种不需要让我们的子进程来执行,而是让shell自己执行的命令,被称为内建/内置命令。 接下来还没完,实现最后一个问题:echo内建命令。对于echo我们知道,通过

echo $?

能够活获得最近一次进程的退出码和终止信号。最终代码:image-20221227203636105

image-20221227205910943

完结!

标签: linux 运维

本文转载自: https://blog.csdn.net/NEFUT/article/details/128461468
版权归原作者 每天都要进步呀~ 所有, 如有侵权,请联系我们删除。

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

还没有评论