悠扬随风 发表于 2025-4-15 07:54:24

【Linux篇章】踏入 Linux 进程控制的奇幻迷宫,解锁体系调理奥秘(秒懂版)

https://i-blog.csdnimg.cn/direct/ffced872536a410197fa170658811e76.jpeg本篇小鸡汤:人们会常常欺骗你,是为了让你明白,有时间,你唯一应该信赖的人是你自己  。     
      冲冲冲!!!!!!!!!!     https://i-blog.csdnimg.cn/direct/265657e0253447b5bdd90d2db47f9572.png         https://i-blog.csdnimg.cn/direct/04ea3becca7f43c8879332210380c3de.png        https://i-blog.csdnimg.cn/direct/8259ddc4d5894cb8b2df03a95357e7d9.png羑悻的小杀马特.-CSDN博客羑悻的小杀马特.擅长C/C++题海汇总,AI学习,c++的不归之路,等方面的知识,羑悻的小杀马特.关注算法,c++,c语言,ubuntu,linux,数据结构领域.https://g.csdnimg.cn/static/logo/favicon32.icohttps://blog.csdn.net/2401_82648291?type=lately                                               
   欢迎拜访:羑悻的小杀马特.-CSDN博客
本篇主题:进程控制
制作日期:2025.04.14
隶属专栏:Linux之旅 
 https://i-blog.csdnimg.cn/direct/115c1033b6df49d48821d2bd4156f961.gif
https://i-blog.csdnimg.cn/direct/1d97e3c2f82646e1b56f0017512871ab.png

目录
一·进程创建:
1.1对fork的认识:
1.2写时拷贝:
1.3fork用途及失败缘故原由:
二·进程终止:
 2.1退出码:
2.2_exit函数:
2.3exit函数:
三·进程等待:
3.1为何要进程等待:
3.2wait函数: 
3.3waitpid函数:
3.4status怎样提取子进程退出信息:
3.5waitpid的阻塞等待与非阻塞等待:
阻塞等待:
非阻塞等待:
四·进程中的程序替换:
4.1何为程序替换:
4.2六大替换函数(语言封装):
4.2.1execl:
4.2.2execlp:
4.2.3execle:
4.2.4execv:
4.2.5execvp:
4.2.6execvpe:
4.2.7上述函数总结及巧记:
4.3替换底层调用实现原理:
五.应用进程控制模仿实现自定义shell命令行解释器:


一·进程创建:

1.1对fork的认识:

   在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。
https://i-blog.csdnimg.cn/direct/5cfe861e39e343eea9e9894e6ef4208d.png
返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1。

 调用fork:
   进程调⽤fork,当控制转移到内核中的fork代码后,内核会:
1·分配新的内存块和内核数据结构给⼦进程。
2·将⽗进程部分数据结构内容拷⻉⾄⼦进程。
 3·添加⼦进程到体系进程列表当中。 4·fork返回,开始调理器调理。
下面我们来演示一下: 
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
      //child
      int count = 5;
      while(count)
      {
            printf("我是子进程,pid:%d 我正在运行: %d\n",getpid(), count);
            sleep(1);
            count--;
      }
    }
    else
    {
      while(1)
      {
            printf("我是父进程,我正在运行%d...\n",getpid());
            sleep(1);
      }
    }

    return 0;
}
 首先,我们创建了子进程然后父进程与子进程同时举行,两者id各差别。
https://i-blog.csdnimg.cn/direct/2e99d4390e054dfc90f8a0cad04e3dd3.png
因此;我们就可以把fork后理解成分流:返回的id=0的子进程是一个流而id=子进程pid 父进程是一个流;各自干各自的事。
1.2写时拷贝:

首先我们先看一个例子来了解什么事写时拷贝:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int gval = 100;

int main()
{
    printf("父进程开始运行,pid: %d\n", getpid());
    pid_t id = fork();
    if(id < 0)
    {
      perror("fork");
      return 1;
    }
    else if(id == 0)
    {
      printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
      sleep(5);
      // child
      while(1)
      {
            sleep(1);
            printf("子进程修改变量: %d->%d", gval, gval+10);
            gval+=10; // 修改
            printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());
      }
    }
    else
    {
      //father
      while(1)
      {
            sleep(1);
            printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
      }
    }
    printf("进程开始运行,pid: %d\n", getpid());

}   代码解释:
我们定义全局变量gval;然后fork分出子进程;子进程不断修改它然后打印;父进程也在打印。 
https://i-blog.csdnimg.cn/direct/2d45faeedc3a4b0090410413171a0102.png
是不是有个疑问;明显子进程修改了gval但是父进程却没有变;这里就涉及到我们子父进程之间的写时拷贝了(虽然上面说了是直接拷贝给子一份;但是照旧有相干规矩的)。
   通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当恣意⼀⽅试图写⼊,便以写时拷⻉的⽅式各⾃⼀份副本:
也就是子进程对应的要修改的位置会重新给它开一份空间;让它自己对自己的区域进程操作。
 下面看张图:
https://i-blog.csdnimg.cn/direct/400f60df978f414db74102fd732ffd57.png
因此,我们就可以理解为:
对于fork前存在的量;如果不修改;那么就是一份;如果一方修改那么就会写时拷贝;另一方仍旧稳定。对于fork后的量就是自己两个进程各自维护;该咋样咋样。 
    因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!
  写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率。
1.3fork用途及失败缘故原由:

而我们利用fork就是为了让父进程去分配给子进程让他去执行程序,任务等(用到了程序替换也就是后面我们要讲的) 。
对于利用fork而言一样寻常不会失败;除非体系中有太多的进程与现实⽤⼾的进程数超过了限制。
 
二·进程终止:

进程终⽌的本质是释放体系资源,就是释放进程申请的相干内核数据结构和对应的数据和代码。
   进程退出进程退出场景:
 进程常⻅退出⽅法:代码运⾏完毕,结果正确
 从main返回
 代码运⾏完毕,结果不正确
 调⽤exit(c库封装)
 代码异常终⽌
 调用_exit(体系)
  异常退出也就是我们手动的了:ctrl+c,--->信号终⽌。

 2.1退出码:

概念:
   退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令竣事以后,我们可以知道命令是成功完成的照旧以错误竣事的。其基本思想是,程序返回退出代码0 时表⽰执⾏成功,没有问题。代码1或0 以外的任何代码都被视为不成功。
 下面一张图主要说明Linux_Shell中的主要退出码:
https://i-blog.csdnimg.cn/direct/9b588d301fd04326a77e5807fb25d3e7.png
   退出码 0 表⽰命令执⾏⽆误,这是完成命令的抱负状态。

退出码1 我们也可以将其解释为“不被允许的操作”。例如在没有sudo权限的环境下使⽤
yum;再例如除以0 等操作也会返回错误码 1。

130 ( SIGINT 或 ^C )和143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中n 代表终⽌码。
可以使⽤strerror函数来获取退出码对应的描述。(c的函数)
#include <string.h>

char *strerror(int errnum); printf("Error opening file: %s\n", strerror(errno));在多线程环境中,strerror 不是线程安全的,发起利用线程安全版本的 strerror_r 函数。
这里目前作为了解即可。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

void *thread_function(void *arg) {
    char errmsg;
    // 使用 strerror_r 获取错误信息
    if (strerror_r(EACCES, errmsg, sizeof(errmsg)) != 0) {
      perror("strerror_r");
    } else {
      printf("Thread: Error message for EACCES: %s\n", errmsg);
    }
    return NULL;
}

int main() {
    pthread_t thread;
    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
      perror("pthread_create");
      return 1;
    }
    if (pthread_join(thread, NULL) != 0) {
      perror("pthread_join");
      return 1;
    }
    return 0;
}
2.2_exit函数:

https://i-blog.csdnimg.cn/direct/513dd3dfa4524c869be4213a24dc1945.png
虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现返回值是255(这是被处理的。后面我们在进程等待模块会谈到)。
   普及个指令echo$?:打印上一个进程的退出码。
https://i-blog.csdnimg.cn/direct/d424a7bf798c49e6810cb8758ae3fd05.png
正常退出就是0否则就黑白0。
其次就是有个特殊点就是:它不会刷新缓冲区 :
int main()
{
printf("hello");
_exit(0);
} 就啥也不打印;因为没有\n(c语言规定会遇到它刷新)即hello还停顿在缓冲区。 
   任何地方调用exit,体现进程竣事!!并返回给父进程bash,子进程的退出码!! 
我们一样寻常就是传null或者0给status。
也可以理解成这个函数是体系自己的;调用它就体现进程终止掉了。
2.3exit函数:

它是c库封装(体系的_exit)的一个函数,它会默认清空缓冲区;也就是不会发生像上面那样的环境。
https://i-blog.csdnimg.cn/direct/b42b402d055b4353a6198d9041d52d3d.png
int main()
{
printf("hello");
exit(0);
}    此时就会打印hello了。
exit最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:
 https://i-blog.csdnimg.cn/direct/50a3a246a398431991429545580ceff6.png
   1·执⾏⽤⼾通过atexit或on_exit定义的清理函数。
2. 关闭全部打开的流,全部的缓存数据均被写⼊。3. 调⽤_exit
因此这里我们可以简单理解成: 
   exit封装了_exit;然后调用exit会先调清理函数,清空缓冲区等;最后才是调用体系的_exit函数。
    比如我们c语言main函数最后常用return 0竣事其实相称于exit(0)发挥的作用。 
三·进程等待:

3.1为何要进程等待:

⼦进程退出,⽗进程如果不管不顾,就大概造成‘僵⼫进程’的问题,进⽽造成内存走漏。
另外,进程⼀旦变成僵⼫状态,kill-9也不行,因为谁也没有办法杀死⼀个已经死去的进程。
最后,⽗进程派给⼦进程的任务完成的怎样,我们必要知道。如:⼦进程运⾏完成,结果对照旧不对,或者是否正常退出。
   父进程等待子进程为了干什么???
1·采取⼦进程资源。
2·获取⼦进程退出信息。
3.2wait函数: 

https://i-blog.csdnimg.cn/direct/49c8e1e771bf4348a09b6e760808e199.png
对于返回值:成功返回被等待进程pid,失败返回-1。(父进程会在这不停等着子进程)
 输出型参数(wstatus):获取⼦进程退出状态,不关⼼则可以设置成为NULL(传地点)
首先来讲;它是可以等待恣意子进程的;如果不想利用父进程看相干信息等;这个输出型参数就可以设置成null。
其实它就等同于我们后面讲的waitpid(-1,&status,0)(阻塞等待状态);后面会讲到。
下面我们举个例子来说一下用法:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>

int main( )

{
pid_t pid;

if ( (pid=fork()) == -1 )
perror("fork"),exit(1);

if ( pid == 0 ){
   printf("i am child process\n");
sleep(5);
exit(10);
} else {
int st;
int ret = wait(&st);


printf("exit signal: %d,coredump:%d",st&0X7F,(st>>7)&1);


}
}
   这个代码就是子进程休眠一会就退出;而父进程就在这等着;最后举行采取相干资源信息等;对于怎样从st这个输出型参数中提取出信息;后面3.4我们会讲到;这里就是可以利用位运算得到上面的信息。 
运行:
https://i-blog.csdnimg.cn/direct/ea9661c94da2417dbe21ee6c444535d4.png 因为它是程序正常竣事退出;故退出信号就是0(对于正常退出状态低七位都是0);而coredump对于正常退出的进程也是0。
下面形象看一下st是怎样被子进程退出后的exit的码填充的:
https://i-blog.csdnimg.cn/direct/7e6e03310a5e4a08a3de06055a0bc3fa.png
3.3waitpid函数:

https://i-blog.csdnimg.cn/direct/b12fab2bc37a4eeeadb43fdf758ddd3c.png
对于第一个参数pid:
 如果想像wait一样那么就传-1﹔相称于等待恣意子进程﹔传某子进程pid就是指定子进程 。
对于第二个参数wstatus:
同上面我们讲的wait了(用于父进程查察子进程):
这里补充一下其实我们还有专门的函数举行提取(也就不用像上面wait演示的手动提取状态等):
   WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查察进程是否是正常退出)【查察低七位】也就是我们对应的:(st&0x7F)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查察进程的退出码)【查察高八位】在保证WIFEXITED为0也就是子进程是正常退出的环境下才故意义。 也就是我们对于的:((st>>8)&0XFF)
对于第三个参数options:
   这里可以分为填0(阻塞状态:就是父进程只能在这行代码等着子退出):此时pid_t返回-1或者得到子的pid。
或者WNHONG(非阻塞状态:就是父进程还可以执行后面的代码等):子进程还在继续就返回0,调用出错就返回-1成功就返回子pid(异常终止或者正常退出) 
后序会在3.5讲到。
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误地点。
 下面就是必要注意的:
   ①如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,而且释放资源,得到⼦进程退出信息。
② 如果在恣意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程大概阻塞。
③如果不存在该⼦进程,则⽴即出错返回。
 这里我们就先不代码演示了;留在3.5解说阻塞和非阻塞再对waitpid演示。
3.4status怎样提取子进程退出信息:

wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作体系填充。
如果转达NULL,表⽰不关⼼⼦进程的退出状态信息。否则,操作体系会根据该参数,将⼦进程的退出信息反馈给⽗进程。
status不能简单的当作整形来对待,可以当作位图来对待,详细细节如下图(只研究status低16⽐特位):
https://i-blog.csdnimg.cn/direct/17714d81cd2d43df89d9fedcc6538062.png
 下面请看博主联合上面的代码以及自己理解画的图:
https://i-blog.csdnimg.cn/direct/c43c37952c144b6dba2518a4f7113a96.png
比如我们就以_exit(-1)为例说明一下:
这里的-1变成补码截取低八位储存就是status的储存;接着从status中提取:
首先-1会变成补码储存也就是全都是1;那么由于是正常终止status低八位全为0;其次status低16位剩下的前八位就都是1(提取的存入的-1的补码的低八位)故提取八个1就是255;因此我们echo$?就是255了。
   下面总结一下:
只要是正常终止那么低8位都是0;就可以查察退出状态;反之,异常的话;这个coredump会被标记;然后就会存在终止信号(也就是低7位不为0;此时就只需查察终止信号;而退出状态(退出码)就没故意义了) 。

因此根据差别的退出分为两种提取环境;我们既可以用上面的位运算提取也可以用上面给定的专属的函数举行提取查察相干信息等。即WIFEXITED(status)/WEXITSTATUS(status)或者(st&0x7F)/((st>>8)&0XFF)

3.5waitpid的阻塞等待与非阻塞等待:

下面就是我们要测试的waitpid代码了:
阻塞等待:

也就是我们的wait或者waitpid(-1,&status,0);此时父进程会在这不停等着;啥也不干。
下面我们来测试一下:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>


int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}
else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
}
else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
printf("child pid:%d\n",pid);
printf("child ret:%d\n",ret);

//if( WIFEXITED(status) && ret == pid ){
// printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
// }
if((status&0X7F)==0&& ret == pid ){
printf("wait child 5s success, child return code is :%d\n",status>>8&0XFF);
}

else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;}
   代码解释:
 子进程休眠五秒后退出;而父进程不停在这阻塞等待并采取它。
https://i-blog.csdnimg.cn/direct/a444352eb6a5497c81015d7f49f539d9.png

运行:
https://i-blog.csdnimg.cn/direct/bbff97262a6b46d380f54a155d84ff8d.png
因为我们的exit传的是257;也就是我们的status它的低16位分别是前八位00000001末八位00000000(正常退出都是0);然后我们提取前八位天然就是1了。 
 如果要是异常退出;我们可以再开一个窗口给杀死子进程即可;下面演示一下:
https://i-blog.csdnimg.cn/direct/d9b69d08bda741adb825e5a66ecf58df.png
这里我们可以在里面利用(status&0X7F) 把终止信号打印出来;这里就不演示了。
非阻塞等待:

下面就是我们演示非阻塞(waitpid(-1, &status, WNOHANG));父进程等待的过程可以干其他事变(如果子进程还在运行就返回0):
下面我们就来测试一下:
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
pid_t pid;

pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}while(ret == 0);

if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
   代码解释:
这里和上面阻塞差不多;只是父进程等待过程还举行其他的打印工作等;直到成功比及就返回。
 https://i-blog.csdnimg.cn/direct/9831f541e8c54a39b058f8ae7e25847b.png
这里采取的轮巡方式;执行完回来看看子进程是否被成功等回(用的dowhile 结构完成)
 运行:
https://i-blog.csdnimg.cn/direct/919773a9a49b448e9cb222e187af70ff.png
同理如果信号终止也就是异常的我们仍旧可以像阻塞等待那样更改一下代码举行信号杀死举行打印信号等。
小结:
   这里分析一下父进程的等待:当子进程还在运行时;父进程可以选择阻塞等;也可以返回后去做自己的事;当接收到子进程竣事的信号(可以是exit的正常终止也可以是异常;然后通过wait或者waitpid把它记录到status;随后我们能得到它的相干信息;最后子进程被采取)。

这里父进程对子进程:1·资源采取2·得到一定信息
四·进程中的程序替换:

4.1何为程序替换:

fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序替换来完成这个功能!
   程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地点空间中!
⽤fork创建⼦进程后执⾏的是和⽗进程类似的程序(但有大概执⾏差别的代码分⽀),⼦进程每每要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,重新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。
https://i-blog.csdnimg.cn/direct/b1c00b2118884dc6bfe90ef4d602bba4.png
 
   下面我们来抽象理解一下:
我们可以把程序替换理解成:当一个进程如果想举行程序替换;它必然调用exec接口函数(后面我们会讲到);然后这个函数就会找到对应的程序;相称于把对应代码展开覆盖下去【那么如果成功调用后我们exec函数后面的代码就都被覆盖掉不大概执行了;如果调用失败就返回-1;后面不覆盖接着执行】然后我们根据转达的执行程序;对应命令行参数等转达给对应main中执行里面的代码。
 下面我们以我们命令行输入的ls -l -a为例子用程序替换解释一下:
   首先ls就是我们一个可执行程序;然后-l -a是一些命令行参数;首先,当我们调用这个命令的时间;那么父进程会让子进程去执行;因此子进程就会调用对应的exec函数举行程序替换;即找到对应ls的代码然后把-l -a作为命令行参数传给main中的argv数组;然后也就是argv=ls;argv=-l;argv=-a;然后对应的替换后的main中按照这个参数是啥举行分块执行对应的代码;进而来完成我们想要的操作。
总结一下:
对于程序替换;我们可以含糊理解成绩是把对应执行程序代码替换覆盖到当前以及后面的位置;然后根据传进来的命令行参数来执行对应的代码即可(此时环境变量也就是environ指针指向的是我们传进来的环境变量【后面会讲到是什么】)。 
4.2六大替换函数(语言封装):

https://i-blog.csdnimg.cn/direct/0e2d8bdb4690462c9a50bf67863057bc.png
 这里我们想必execvpe而言照旧execvp更多一些。
首先;先普及一下:这些函数它们除了带p的都会默认向当前目录举行查找指定程序(如果当前没有的话就按照给的路径或者环境变量PATH中找(有p)否则直接返回-1;替换失败);对于带p的;它只会在PATH中找(或者表明当前目录如./t)否则就失败。
下面我们来一次讲一下它们的用法(不要忘记对应要以NULL末端):
4.2.1execl:

int execl(const char *path, const char *arg, ...); 这里我们所必要转达的就是对应文件的路径以及 链表形式的 程序名字,命令行参数等。
当然了对于path如果要替换的程序是当前目录就可以直接文件名字否则就要加上路径了。
下面我们来执行一下对应我们上面解释的ls -l -a命令(用程序替换):
https://i-blog.csdnimg.cn/direct/1e8d4c49b0c9485289ad3b36ccc6c28c.png
https://i-blog.csdnimg.cn/direct/ffc602486de4409ca6a0ce56b7422709.png 这里如果我们ls在当前目录的话path就不用全写;直接ls即可(因为默认都会当前目录找一下)。
   巧记:这里execl只有 l;说明我们要以链表形式转达(勿忘NULL)而无e说明只需传命令行参数;环境变量参数无需传。
4.2.2execlp:

int execlp(const char *file, const char *arg, ...); 下面利用一下:
PATH中没有但是表明了当前目录: 
https://i-blog.csdnimg.cn/direct/a5a0b7dadedd4ed28a59814647a6623f.png 可执行程序t的源代码:
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[], char *env[])//argc自动识别多少个命令参数
{
   printf("hello,world\n");
    for(int i = 0; i < argc; i++)
    {
      printf("argv[%d]: %s\n", i, argv);
    }

    printf("\n");

    for(int i = 0; env; i++)
    {
      printf("env[%d]: %s\n", i, env);
    }

    return 0;
}
资助我们打印所转达给被替换程序的命令行参数数组以及环境变量数组。 
运行下: 
https://i-blog.csdnimg.cn/direct/59ba82975d9a4d5a9e9460da96603815.png
这里我们会发现明显没传环境变量数组;但是它却打印了;因为底层默认如果没有维护自己的环境变量数组就会默认把environ(也就是全局的)传给execve(体系的程序替换函数)函数接着就到了main中了(也就是我们所看到的)。 
   巧记: 这里由于我们加了p故它可以除了当前目录找还可以去环境变量表了;因此我们path就不用输入只必要输入文件名即可;而无e说明只需传命令行参数;环境变量参数无需传。
4.2.3execle:

int execle(const char *path, const char *arg, ...,char *const envp[]); 同上面大差不大;来测试一下:
https://i-blog.csdnimg.cn/direct/bc66a8e4029a45c6a3be600bebc3c9d0.png
https://i-blog.csdnimg.cn/direct/85b35ba4edb142cb87cbadc7fc666941.png
这里就必要我们手动转达自己维护的环境变量数组了(有e)。 
 这里大概会有个疑问;明显没有路径(p)为什么直接一个文件就ok;因为上面说了它(不带p)默认都会先在当前目录查找一下的。
测试结果:
https://i-blog.csdnimg.cn/direct/589fe2674c324a9a8ef16323e2cec1ec.png
完成了env的覆盖了(后面原剖析讲到)。
   巧记:有l故命令行参数以链表形式(勿忘NULL);其次就是有e故必要自己手动转达维护的环境变量数组。 
4.2.4execv:

int execv(const char *path, char *const argv[]); 这里和上面一样只不外把命令行参数从list的形式变成了vector了。
演示一下:
https://i-blog.csdnimg.cn/direct/f8eda27d9ed740469a7f519b2fe698f4.png
https://i-blog.csdnimg.cn/direct/405ef024ce0d432b8d11475500199ef0.png
https://i-blog.csdnimg.cn/direct/edb0e9b96a7240708ab1e6d4c34b4eae.png
同上面。 
   巧记:有v故把命令行参数以数组形式传入;无e故直接无需转达环境变量数组。
4.2.5execvp:

int execvp(const char *file, char *const argv[]);  https://i-blog.csdnimg.cn/direct/57ead4828da94f11a5194c645be7ec83.png
这里我们直接给它表明当前目录;如果要是填别的目录的程序要么直接把路径和程序都加上要么直接写个程序名(但是PATH要导入它的路径【利用getenv和putenv导入】)因此这样还不如直接用不带p的
https://i-blog.csdnimg.cn/direct/9673bc2bd4394170a8a97f9cbaef9352.png
下面我们演示一个PATH本身就存在这个可执行程序路径的:
 https://i-blog.csdnimg.cn/direct/786c477bb9f5400583f967510e4dd47a.png
这样;只要文件名本身就可以执行:
https://i-blog.csdnimg.cn/direct/d520fb043ee046d2b981ab4b7cbc729e.png
    巧记:带v故命令行参数以数组形式;带p故直接文件名即可(但是要保证PATH中一定存在对应路径)否则就要传绝对路径咯
4.2.6execvpe:

这个不经常用但是我们照旧演示一下:
https://i-blog.csdnimg.cn/direct/8943329f5ad847d49269d161e126606e.png
https://i-blog.csdnimg.cn/direct/c372cd68cd674271b33254e816943305.png
同理只不外是多了个环境变量数组而已。
   巧记:有v故命令行参数用数组形式;有p故如果PATH可以找到就直接传文件即可;有e故必要环境变量数组。 
4.2.7上述函数总结及巧记:

这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。如果调⽤出错则返回-1;所以exec函数只有出错的返回值⽽没有成功的返回值。
上面每个函数例子后面都有提到影象方法。
   l(list):表⽰参数采⽤列表。
v(vector):参数⽤数组。
p(path):有p⾃动搜索环境变量PATH。 e(env):表⽰⾃⼰维护环境变量。
https://i-blog.csdnimg.cn/direct/8a4d284f20da423e97bc8ef0db27f923.png 对于execvpe因为和execvp差不多;只是多了个必要重组自己环境变量而已。
最后一个是体系的函数;也就是前六个函数最后都会调用execve。(后面讲原剖析提到)
4.3替换底层调用实现原理:

下面我们将详细讲一下关于上面六个函数是怎样完成底层调用的:
下面我们先看张图:
https://i-blog.csdnimg.cn/direct/2bfe5c0049344d87a34134ebd34aa075.png
我们不难明白最后它都能转化成execve函数了(体系自己的):
int execve(const char *path, char *const argv[], char *const envp[]); https://i-blog.csdnimg.cn/direct/9bfa21aa74de4444b761d71a21f2e771.png
 下面我们就来抽象解释一下(通俗易懂):
首先命令行参都要变成数组形式然后传给execve的第二个参数然后有p的就去PATH找到路径与文件传给它第一个参数;其次就是没有自己维护的环境变量数组的就用全局环境变量指针的environ传给第三个参数;否则就是用自己维护的环境变量表给第三个参数(但是这样就会覆盖了;即原来全局的都没了换成了我们传入的了)。
下面就是执行execve函数了:
根据第一个参数展开要执行文件的代码;然后把后两个参数分别传给该文件的main函数参数里;接着执行展开的代码就ok了。
必要注意的就是:
   我们替换后得到的那个程序它的代码中的环境变量是被完全覆盖掉的(被我们转达的environ或者自己维护的环境变量数组覆盖)也就是被替换后的main中我们利用environ访问的是我们转达进来的环境变量数组。
五.应用进程控制模仿实现自定义shell命令行解释器:

下面就是我们应用本篇所解说的进程控制相干的知识来实现的小项目;见博主的这篇文章:传送门:Linux命令行解释器的模仿实现_linux命令模仿-CSDN博客



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux篇章】踏入 Linux 进程控制的奇幻迷宫,解锁体系调理奥秘(秒懂版)