个人技术分享

目录

一、进程等待

1 进程等待的必要性

2 进程等待的实现方法:

①wait接口

② pid_t waitpid(pid_t pid, int *wstatus, int options)

③ 子进程的status

3 进程等待方式:阻塞等待与非阻塞等待

二、进程替换 

1.什么是程序替换

 2.最简单的程序替换代码接口

3.利用子进程来实现程序替换:多进程替换

4. 学习6种exec

 1.int execlp(const char *file, const char *arg, ...);

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

 3 int execvp(const char *file, char *const argv[]);

4 int execle(const char *path, const char *arg, ...,char *const envp[]);

首先我们先了解如果使用makefile生成两个可执行文件:

总结:


一、进程等待

1 进程等待的必要性

子进程退出,父进程如果对子进程的退出信息不做理会,子进程就会变成“僵尸进程”,进程一但变成僵尸状态,就刀枪不入,KILL-9命令也无法关闭,誰也没有办法杀死一个已经死去的进程,会造成内存泄漏。其次,我们创建子进程就是为了让子进程完成某种任务,而作为调用者需要知道子进程运行的结果,结果对不对,运行状态正常不正常。所以父进程要采取进程等待的方式来获取子进程退出时候的信息。回收子进程资源。

那么对于进程来说, 进程退出的时候应该保存那些信息呢:

进程退出时,应该只释放部分资源,pcb里面要保存进程退出时候的信息,所以应该至少保留一个pcb,实际上也是如此。那么当没有进程来读取这个退出信息时,我们将当前进程的状态称为“Z”状态,也叫作僵尸状态。所以,我们需要知道进程退出时候的退出信息,代码的执行结果被保存到进程的pcb里面,保存了那些字段呢?

这是linux内核代码:

/*task stste*/
int exit_code,exit_signal;
int  pdeath_sigal;/* the sigal sent when the parent dies*/
usigned int personality;
unsigend did_exec:1;

所以,进程退出时要保存的信息是进程的退出码和进程的退出信号是几号信号。

2 进程等待的实现方法:

①wait接口

 #include <sys/types.h>
 #include <sys/wait.h>

pid_t wait(int *wstatus);

pid_t waitpid(pid_t pid, int *wstatus, int options);

 int  waitid(idtype_t  idtype, id_t id, siginfo_t *infop,int options);

1

int *wstatus 叫做输出型参数

wait接口在使用的时候某认进程阻塞等待。阻塞等待:子进程不退,父进程就一直等待子进程执行完拿到结果。其次,wait等待任意一个子进程。返回值.>0,等待成功,返回的是子进程的pid,<0,表示等待失败,

我们先来验证一下 回收进程的过程:

 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>  
  4 #include<sys/types.h>  
  5 #include<sys/wait.h>
  6 int main()
  7 {
  8 pid_t id = fork();
  9 if(id==0)
 10 {
 11   //子进程
 12   int cnt = 5;
 13   while(cnt)
 14   {
 15     printf("子进程正在运行,pid:%d.ppid:%d\n",getpid(),getppid());
 16     sleep(1);
 17     cnt--;
 18   }
 19   printf("子进程准备退出\n");
 20   exit(0);//终止子进程
 21 }
 22 
 23 sleep(10);//为了看到5秒钟的僵尸进程
 24 printf("父进程开始回收\n");
 25 //父进程
 26 pid_t rid = wait(NULL);//阻塞等待
 27 if(rid>0)
 28 {
 29   printf("等待成功,rid:%d\n",rid);
 30 }
 31 
 32 printf("父进程回收僵尸成功\n");
 33 sleep(3);                                                                       
 34   return 0;                                                
 35 }   

 如果我们将父进程睡眠时间全部取消,可能连僵尸进程都看不见直接就结束了,这是因为父进程一直处于阻塞等待。那么这个现象可以引出一个问题:

fork之后父进程和子进程誰先执行,这个答案不一定,有可能是父进程有可能是子进程,这是由调度器说了算,但是在fork代码里,父进程应该最后退出。

那么我们创建子进程更多的是关心子进程工作做得怎么样了,结果怎么样,而不是仅仅只是关心回收,如何得到进程的执行情况和退出时候的信息,下面介绍第二个接口:

② pid_t waitpid(pid_t pid, int *wstatus, int options)

首先,waitpid肯定是能够进行进程回收的工作的。接下来看一下参数和返回值的意义:

 pid_t pid:具体要等待的进程的pid

int *wstatus:这个是输出型参数,可以通过这个拿到子进程的退出结果

int options:两种,一个是0,一个是非阻塞(此次测试默认为0,表示阻塞等待)

返回值:

返回值.>0,等待成功,返回的是子进程的pid,<0,表示等待失败,

返回值:

当正常返回的时候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。

 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>  
  4 #include<sys/types.h>  
  5 #include<sys/wait.h>  
  6 int main()  
  7 {  
  8 pid_t id = fork();  
  9 if(id==0)  
 10 {  
 11   //子进程  
 12   int cnt = 5;  
 13   while(cnt)  
 14   {  
 15     printf("子进程正在运行,pid:%d.ppid:%d\n",getpid(),getppid());  
 16     sleep(1);  
 17     cnt--;  
 18   }  
 19   printf("子进程准备退出\n");  
 20   exit(1);//终止子进程                                                          
 21 }
 22 
 23 //父进程
 24 int status = 0;//子进程的退出吗和退出信号会放入status里面
 25 pid_t rid = waitpid(id,&status,0);//阻塞等待                             
 26 if(rid>0)                                                                
 27 {                                                                        
 28   printf("等待成功,rid:%d, status:%d\n",rid,status);                    
 29 }                                                                        
 30                                                                          
 31   return 0;                                                              
 32 }

③ 子进程的status

我们获取子进程的退出信息是获取两方面的信息:退出信号和退出码

所以两个数字可以看做是局部性的位图来使用的,所以status有自己的格式。

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

0-7位:衡量进程退出时,收到的信号编号(进程出异常时)

8-15位:进程正常退出时候的退出码。

两个数字就可以拿到这个进程退出时候的信息。

7-8中间这个位置叫做core dump 标志位。(信号补充)

这也就能够解释上面的status为什么是256了。

如果要获取退出码和退出信号,需要对status做位操作。

那么我们要知道进程退出的一个情况,正常思路就是先看进程有没有收到信号,然后在看退出码,

那么我们就从staus中提取一下:

staus & ox7F(16进制整数:0000 0000 0000 0000 0000 0000 0111 1111)这就提取到了低七位的数据,也就是终止信号。

(staus>>8)&0xff:拿到退出码数据

23   //父进程                                                 
 24   int status = 0;//子进程的退出吗和退出信号会放入status里面
 25   pid_t rid = waitpid(id,&status,0);//阻塞等待                             
 26   if(rid>0)                                                                 27   {                                                                        
 28     printf("等待成功,rid:%d, status:%d , exit_sigal:%d .exit_code:%d\n",rid,sta    tus,status&0xtF,(status>>8)&0xFF);                    
 29   }                                                                        
 30                                                                            
 31     return 0;                                                              
 32   }

现在我们在代码中写一个除0的错误:

 //子进程 
 12     int a = 10;
 13     int cnt = 5;  
 14     while(cnt)  
 15     {  
 16       a = a/0;     

 难道,每次我们为了获取退出码和退出信号,每次都要做位操作吗?

是不需要的,linux操作系统给我们提供了两个宏:

status:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。

     (查看进程是否是正常退出)这个宏用于判断进程的退出信号,为0正常退出,不为0为假

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。

        (查看进程的退出码)专门用于提取退出码

一般我们会这么写:

 //父进程
   26   int status = 0;//子进程的退出吗和退出信号会放入status里面
   27   pid_t rid = waitpid(id,&status,0);//阻塞等待                             
   28   if(rid>0)                                                                
   29   {   
   30     if(WIFEXITED(status))
   31     {
E> 32     printf("等待成功,rid:%d, status:%d , exit_code:%d\n",rid,status,WEXITSTAT      US(status);                                                                   
   33 
   34   }
   35   else
   36   {
   37 printf("子程序出异常\n");
   38   }
   39   }
   40                                                                            
   41     return 0;                                                              
   42   }

这就是通过宏获取进程的退出信息。

为什么不定义几个全局变量来进行操作,而要使用waitpid这个系统调用呢?

int sub_exitcode = 0;
    7 int sub_exitsigo = 0;                                    
    8    int main()                                            
    9    {                                                     
   10    pid_t id = fork();                                    
   11    if(id==0)                                             
   12  {                                                       
   13     //子进程                                             
   14     int a = 10;                                          
   15     int cnt = 5;                                         
   16     while(cnt)                                           
   17     {                                                    
W> 18       a = a/0;                                           
   19       printf("子进程正在运行,pid:%d.ppid:%d\n",getpid(),getppid());  
   20       sleep(1);                                          
   21       cnt--;                                             
   22     }                                                       23     printf("子进程准备退出\n");                                                  24     sub_exitcode = 1;   25     exit(1);//终止子进程                                                                                                              
   26   }                             

当子进程的进程出异常,修改全局变量,但是代码是来自于父进程的,一但要发生数据修改就会发生写实拷贝,由于进程具有独立性,父进程也无法读取子进程修改的值。就是说父进程看不到。如果将代码定义在子进程的内部,子进程也无法将数据交给父进程,进程具有独立性。所以父子进程无法直接交互然后修改或者读取对方的数据后并让对方看到。所以通过系统调用让对方看到。

        子进程运行有自己的pcb,页表,进程地址空间等。当进程运行结束退出,大多数的数据会被释放,但是进程的退出信号和进程退出码会被暂时保留,保留在进程的pcb中,而status,是用户自己定义的变量,属于用户空间用户层。如果我们想要获得数据,将退出信号和退出码写到这个变量里面,本质上来说是将操作系统的数据拿到了用户层。操作系统是不会允许用户之间访问内核数据结构的。只能通过系统调用的方式来获得,也就是说只能通过witpid这种系统调用接口来读取子进程退出的信息,父进程通过系统调用获取访问子进程的退出信息。

3 进程等待方式:阻塞等待与非阻塞等待

上面演示的代码中,子进程在运行的时候,父进程在进行阻塞等待。但是父进程也可以进行非阻塞等待。阻塞等待没有什么问题,但是就是在等待这段期间,父进程什么工作都没有做。而父进程的阻塞等待与waitpid这个系统调用有关系:

我们先来说一下阻塞与非阻塞:

当父进程调用waitpid时,父进程会一直等待子进程运行结束过后才会返回,更深层的说,当父进程调用这个接口,发现子进程状态还未就绪就是还没有运行完,父进程将自己状态设置为非r状态,连入到等待队列中,等待子进程,这是阻塞等待。而非阻塞等待,waitpid的第三个参数设置为0,表示阻塞等待,其中,这个参数的设置还可以是系统提供的一个选项,

WHOHANG,这是一个大写而且是一个选项,必然是一个宏,那么其作用是:父进程等待子进程的时候,如果子进程没有就绪,waitpid会以出错的形式或者其他返回值返回比如0.返回值大于01表示等待成功,=0表示调用函数成功,子进程未退出,<0表示等待失败。那么如果以wnohang的方式等待这种等待方式就称为非阻塞等待。

WNOHANG:w:wait .HANG:有时电脑会突然卡住,操作系统黑屏蓝屏,我们叫做操作系统宕机,如果访问电脑,电脑没挂,操作系统也还在,但是就是没有反应,我们将电脑这个状态叫做被hang住了,所以阻塞等待实际上就是hang住了。

阻塞等待:结果不就绪,进程不退出,这就是阻塞调用

非阻塞调用:多次调用,每调用一次得到一个返回值,不管返回值好不好,不影响父进程做其他工作。也称为基于非阻塞的轮询访问。(循环套用非阻塞调用接口,重复检测)优点:轮询期间父进程可以做其他事情。

一般这样使用:


   int main()  
  9    {  
 10    pid_t id = fork();  
 11    if(id==0)  
 12  {  
 13     //子进程 
 14 //    int a = 10;
 15     int cnt = 5;  
 16     while(cnt)  
 17     {  
 18     //  a = a/0;
 19       printf("子进程正在运行,pid:%d.ppid:%d\n",getpid(),getppid());  
 20       sleep(1);  
 21       cnt--;  
 22     }  
 23     printf("子进程准备退出\n");  
 24     sub_exitcode = 1;
 25     exit(1);//终止子进程                                                            
 26   }  
 27   
 28   //父进程  
 29   int status = 0;//子进程的退出吗和退出信号会放入status里面  
 30   while(1)  
 31   {  
 32   
 33     
 34   pid_t rid = waitpid(id,&status,WNOHANG);//阻塞等待                               
 35   if(rid>0)                                                                  
 36   {     
 37     printf("等待成功,rid:%d, status:%d , exit_code:%d\n",rid,status,WEXITSTATUS(status));  
 38     break;//等待成功就退出  
 39   }  
 40   else if(rid == 0)  
 41   {  
 42     printf("子进程正在运行,还没有结束,你可以做其他的事情\n");  
 43   }  
 44   else                                                                                                                                                                                     
 45    {  
 46   perror("wairpid:");  
 47    }  
 48   }
 49        
 50     return 0;                                                              
 51     }
~

那么对于父进程等待的时候可以做其他的事情,我们可以怎么设计呢:

 10 //*********任务*******************
 11 void printlog()
 12 {
 13   printf("这个函数是输出一个日志\n");
 14 }
 15 
 16 void PrintNet()
 17 {
 18   printf("这个函数执行一个网络任务\n");
 19 }
 20 void FlushNpc()
 21 {
 22   printf("这个函数应用于刷新野怪\n");
 23 
 24 }
 25 void InitTask()
 26 {
 27   tasks[0] = printlog;
 28 
 29   tasks[1] = PrintNet;
 30   tasks[2] = FlushNpc;
 31   tasks[3] = NULL;
 32 }
 33 
 34 void excuteTAsk()
 35 {
 36   //执行任务
 37   int i = 0;
 38   for(i = 0;tasks[i];i++)
 39   {
 40     tasks[i]();
 41   }
 42 }
 43 
 44                                                                                 
 45 int main()  
 46    {
 47   InitTask();//初始化任务
 48 
 49    pid_t id = fork();  
 50    if(id==0)  
 51  {  
 52     //子进程 
 53     int cnt = 5;  
 while(cnt)  
 55     {  
 56       printf("子进程正在运行,pid:%d.ppid:%d\n",getpid(),getppid());  
 57       sleep(1);                                                                 
 58       cnt--;  
 59     }  
 60     printf("子进程准备退出\n");  
 61     exit(1);//终止子进程                                                              
 62   }
 63  
 64   //父进程
 65   int status = 0;//子进程的退出吗和退出信号会放入status里面
 66   while(1)
 67   {
 68 
 69   
 70   pid_t rid = waitpid(id,&status,WNOHANG);//阻塞等待                                 
 71   if(rid>0)                                                                
 72   {   
 73     printf("等待成功,rid:%d, status:%d , exit_code:%d\n",rid,status,WEXITSTATUS    (status));
 74     break;//等待成功就退出
 75   }
 76   else if(rid == 0)
 77   {
 78     printf("子进程正在运行,还没有结束,你可以做其他的事情\n");
 79     printf("父进程任务开始执行\n");
 80     excuteTAsk();
 81     printf("父进程单个任务结束\n");
 82   }
 83   else
 84    {
 85   perror("wairpid:");
 86    }
 87   }
 88                                                                            
 89     return 0;                                                              
 90     }
~

 这就是进程等待的部分。

二、进程替换 

1.什么是程序替换

目前我们的程序只能执行自己进程的代码。如果我们创建的子进程想要执行其他程序的代码的话,可以通过程序替换来实现。原理如下图:

如果现在要执行另外一个程序,将新的程序的代码和数据直接在内存中替换老的代码和数据,栈区和堆区该初始化的初始化,该清空的就清空,页表要重新映射就重新映射,地址空间的范围大小该调整就调整掉。此时pcb被cpu调度,cpu不关心执行的是那个程序,只要有代码跑就行。这个时候cpu会执行新的程序,这就叫做程序替换。

正经一点说:

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换的过程中,没有创建新的进程。因为我们并没有重新创建pcb,也没有页表这样的数据结构释放重新建立,修改是一定的,清空也是可以的,但是并没有将内核的数据结构清空了再重来,页表的映射等可能会改变。

我们将数据和代码进行替换,就意味着要访问内存数据,所以系统替换一定要伴随着系统调用:

有六种以exec开头的函数,统称exec函数:

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

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

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

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

int execve(const char *path, char *const argv[], char *const envp[]);

 2.最简单的程序替换代码接口

用我们的程序包装一下系统的指令:

const char *pathname:要执行的程序,第一个参数为带路径带文件名:

...:第三个参数为可变参数列表,可以传递多个参数,就像printf和scanf函数一样。那这个理解就像我们平时使用命令选项的个数不确定,所以这里我们可以这样做:命令行怎么写,我们就怎么传,最后用NULL结尾。这个参数的目的是告诉接口这个程序你想怎么执行就像命令行指令一样带各种选择执行不同的功能一样。

 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/types.h>
  4 #include<sys/wait.h>
  5 
  6 int main()
  7 {
  8   printf("i am a process,pid:%d\n",getpid());
  9   printf("exec begin..\n");
 10   execl("/user/bin/ls","ls","-a","-l",NULL);
 11   printf("exac end....\n");                                                     
 12 }          

 execl("/usr/bin/top","top",NULL);         

细节1:

程序替换一但成功,exec*后续的代码不再执行

细节2:

如果失败返回-1,错误码表示错误原因。成功的时候没有返回值。exec只有失败返回值,没有成功返回值。如果程序替换成功,就不会再执行原来进程剩下的代码,如果执行失败,后续才会继续执行。

失败的情况:

替换程序的路径错误,给了一个不存在的路径

细节3:替换的时候没有创建子进程,替换完成,不创建新的程序。

#include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/types.h>
  4 #include<sys/wait.h>
  5 
  6 #include<unistd.h>
  7 int main()
  8 {
  9   printf("i am a process,pid:%d\n",getpid());
 10   printf("exec begin..\n");
 11   sleep(5);
 12 
 13  execl("/usr/bin/top","top",NULL);
 14 
 15   printf("exac end....\n");
 16   exit(1);                                                    
 17 }
~

现象:前五秒检测到运行起来的mytest,但是五秒后没有,原因是发生了程序替换,程序名字变成了top所以没有查到,但是此时进程的pid为:32732

然后查询更换后的进程,可以查到,而且进程的pid也为32732

细节4:

创建一个进程的时候,是先创建进程的pcb,页表等还是先将程序加载到内存呢?应该是先加载好进程的pcb、页表、进程地址空间等。就像我们高中毕业后,在我们还没有来到学校的时候,我们的信息,学号呀,档案等等都会在学校先创建好,我们开学随时来学校报到就好了。所以,进程最重要的工作不是加载,而是在创建时先创建pcb等数据结构,再加载。先将代码数据等加载到内存可能页表等都还没有办法映射。如果先建立数据结构,当调度进程的时候,操作系统也可以通过缺页中断,动态的调度程序的代码和数据。

        程序是如何加载到内存的?

我们所谓的进程替换,好像就是将新程序的代码放到代码区,数据放到数据区。如果原来的进程在代码和数据区有内容就替换,如果没有就创建。这样来看,程序替换的本质就是程序加载。

程序为什么要加载到内存?

 根据冯诺依曼体系结构,将程序的代码和数据加载到内存,才能方便cpu的读取和调度。

层序如何加载到内存?通过程序替换实现。   

程序替换是将磁盘里面的数据拷贝到内存,是将外设的数据拷贝到内存,这个工作需要操作系统来做,需要调用系统调用接口。那么程序替换就相当于是一个加载器。

细节5:补充

 execl("/user/bin/ls","ls","-a","-l",NULL);

这是一种标准传参,一板一眼,当然也可以用很多非标准传参。

 execl("/user/bin/ls","、user/bin/ls","-a","-l",NULL);

3.利用子进程来实现程序替换:多进程替换

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 
  7 int main()                                                  
  8 {                                                           
  9   printf("i am a process,pid:%d\n",getpid());               
 10   pid_t id = fork();                                        
 11   if(id == 0)                                               
 12   {
 13     sleep(3);
 14     printf("exec bejin......\n");
 15     execl("/user/bin/ls","ls","-a","-l",NULL);                                  
 16     exit(1);                             
 17   }                                                            
 18   pid_t rid = waitpid(id,NULL,0);                              
 19   if(rid>0)                                                    
 20   {                                                            
 21     printf("等待成功\n");                                      
 22   }                                                            
 23                                                                
 24   return 0;                                                    
 25 } 

这样做的原因,用子进程去执行进程替换,父进程等待结果。

因为我们可以通过父进程拿到执行的结果

其次,由于进程具有独立性,子进程发生程序替换的时候,要修改原来的代码和数据,就要发生写实拷贝。写实拷贝的本质是重新开辟空间。所以代码和数据都会发生写实拷贝。

shel是如何运行起来一个指令的:

        shell通过fork创建子进程,让后shell这个进程通过wait_pid来等待子进程也就是指令的执行结果。子进程也会继承shell的代码,但是并不影响shell.当输入指令的时候,子进程发生程序替换,替换输入的指令,然后执行完成,返回执行结果。

4. 学习6种exec

有六种以exec开头的函数,统称exec函数:

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

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

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

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

int execve(const char *path, char *const argv[], char *const envp[]);

此外还有一个:

规律:

命名中带P的:不用告诉系统,要替换的目标程序在哪里,只用告诉文件名。系统替换时会自己去PATH环境变量中去找到。

命名最后带e:表示跟环境变量有关。

 1.int execlp(const char *file, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

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

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

 const char *path:想要执行程序的相对或者绝对路径

char *const argv[]:这是一个指针数组,类似于main函数的参数,传入命令行参数的一张表,这个表由自己构建。

此时我们的ls里面也是有main函数的,ls里面的命令行参数李彪参数就是由这里来传入的,如果我们自己要实现一个shell,那么这个命令行参数列表就是用我们自己来写的。

 3 int execvp(const char *file, char *const argv[]);

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

两个参数分别是程序名和参数列表

4 int execle(const char *path, const char *arg, ...,char *const envp[]);

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

铺垫:

exce*族的函数可以执行系统的指令,也就是程序,那么exec可以执行用户写的程序吗?

结论:无论什么语言,只要可以在linux下跑,都是可以调用的。

都可以调用原因:无论程序用什么语言写,运行过后都会变成程序,都是进程。都有自己的代码和数据,我们都平等的替换,exce不管你是什么语言,只能看到二进制的数据。和语言没有关系。

我们现在编写一个c++的文件:

可以使用 .cc /.cpp/.cxx都是c++文件的后缀名

那些现在我们想用C语言写的程序调用c++写的程序可以办到吗?

首先我们先了解如果使用makefile生成两个可执行文件:

当我们这样书写的时候,无论怎么写都只能生成一个可执行文件,原因是makefile从上往下扫描,默认只会形成一个可执行程序,我们现需要这样书写:

成功生成两个可执行文件:

现在我们使用C语言程序调用c++写的程序

C语言文件中我们这样书写:

c++文件中,我们将main函数的命令行参数带上来看一下时候使我们调用程序传入的:

编译运行:

成功使用一个C语言写的程序调度了一个c++语言写的程序。当然也可以调用php,shell/python等语言都是可以的。

接下来我们就可以基于这两个程序研究父子进程之间环境变量的传递,也就可以说明带e的exec*函数了。

接下来我们修改一下c++程序,也就是要替换的目标程序内容:

子进程程序获得了父进程程序的所有环境变量。(此时是程序替换哦) ,但是父进程中并没有传入环境变量:

子进程没有传环境变量,但是子进程却默认拿到了,这是怎么回事呢?

 命令行参数和环境变量是在我们的进程地址空间中的,那么子进程也会拿到父进程进程地址空间中的数据。被子进程所继承,经过页表映射到子进程内存。所以环境变量可以通过进程地址空间继承的方式,让所有的子进程拿到。但是,我们说程序替换是会替换掉原先进程的代码和数据的,但是实验现象表明:不会替换环境变量的数据。

所以,如果我们想要子进程继承全部的环境变量,是可以直接拿到的。

如果父进程想在原先环境变量的基础上新增环境变量给子进程:

我们的程序也是bash的子进程,所以在bash中是不存在我们新创建的环境变量的:

当我们运行起来:

孙子进程是可以拿到的。新增环境变量不会影响前者但是会影响后者。

如果想要设置全新的环境变量:

使用接口:

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

所以:接口的作用就是设置全新的环境变量列表给子进程,这是一种覆盖式传递。

总结:这些接口功能并没有太大区别,都是程序替换,只是使用上的区别,传参方式不同。(用户可能在各种各样的场景下进行替换)但是,只有:

 execve是系统调用接口,来自二号手册。所以其余几个接口底层都会封装这个接口。

总结:

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

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