<Linux> 进程控制

embedded/2024/9/24 4:28:51/

目录

一、进程创建

1. fork函数

2. fork函数返回值

3. 写时拷贝

4. fork常规用法

5. fork调用失败原因

6. 如何创建多个子进程?

二、进程终止 

1. 进程退出场景

2. 进程退出码 

3. errno

4. 进程异常退出

5. 进程常见退出方法

5.1 return退出 

5.2 exit退出

5.3 _exit退出 

小结

三、进程等待

1. 进程等待的必要性

2. 进程等待的方法

2.1 wait 方法

2.2 waitpid

3. 获取子进程status

4. options参数

5. wait / waitpid原理

四、进程程序替换

1. 替换原理

2. 7个替换函数

execl

execlp

execv

execvp

make形成多个可执行程序

execle 

新增环境变量 

覆盖环境变量

小结:


一、进程创建

1. fork函数

         fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核工作:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容(PCB、进程地址空间、页表)拷贝至子进程
  • 添加子进程到系统进程列表当中(将PCB链入系统运行队列中)
  • fork返回,开始调度器调度

fork简单样例:

        31685进程创建子进程31686,fork函数对父进程返回子进程pid,对子进程返回0表示创建成功,如果返回-1表示创建失败

2. fork函数返回值

  • 对子进程返回0表示创建成功,-1表示失败
  • 对父进程返回子进程的pid

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

        一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

为什么fork函数有两个返回值?

        父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。

        也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句父子共享,这就是fork函数有两个返回值的原因

        fork的具体细节我们在进程概念处已经讲过,此处不再赘述

3. 写时拷贝

        通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本

         当一个进程要创建子进程时,在进程地址空间内原只读数据权限不变,而可写数据权限更变为只读。显然,当子进程拷贝父进程的进程地址空间后,两者都为只读权限,所以父子进程任何一方要修改数据时,不会发生异常处理,而是触发写时拷贝,开辟新的空间来存放新的数据,再修改该数据的页表映射即可

4. fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

5. fork调用失败原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

6. 如何创建多个子进程?

        循环一定次数即可,这里主函数处父进程会提前结束,所以让父进程睡眠1000s等待子进程

Z+、defunct表示僵尸进程,因为父进程没有获取子进程信息,所以子进程一直处于僵尸状态 

二、进程终止 

1. 进程退出场景

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

正不正确将统一采用进程的退出码来判定,即main函数的return值

当进程异常退出后,进程的退出码就没有意义了,我们要关心的就是为什么异常,以及发生了什么异常

2. 进程退出码 

我们写C、C++语言时,在主函数内总是return 0;这是为什么呢?

        这里的0指的是进程的退出码,表征进程的运行结果是否正确。本质上表示该进程运行完成时是否正确的结果,如果不是,可以用不同的数字表示出错原因。

        main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。

那么这个0被谁拿到了呢?

        被父进程即bash拿到了,可以用下面的指令查看命令行中最近一个进程退出时的退出码

        所以如果连续两次 echo $? 那么第二次的echo将会输出0,因为最近一个进程就是上一个echo,该进程是正常退出的,所以退出码是0

echo $?

进程中,谁会关心“我”运行的情况呢?

        一般而言,是该进程的父进程要关心!因为父进程创建子进程就是要让子进程去干一些事情,所以子进程运行结束,父进程是需要知道运行的结果如何。但是父进程不会关心子进程为什么运行成功了,而是会关心他为什么运行失败或异常!

        所以子进程可以return的不同的数字,来表明运行结果——退出码

这些退出码我们不了解含义,所以有strerror函数可以返回这些退出码的退出码描述

        以2号退出码为例,我们ls一个不存在的文件

ls myfile.txt

 

        结果显示No such file or directory,这正是2号退出码的解释,也就是说ls这个进程运行出错了,想bash返回了2号退出码

        同样的,我们也可以编写我们自己的错误解释,一个指针数组即可完成

3. errno

        C语言中,提供了一个全局变量errno — number of lasr error,即最近一次的错误码,这是因为C语言有许多库函数,如果出错了那就需要出错原因

示例:申请大概4G空间

4. 进程异常退出

代码错误导致进程运行时异常退出

        进程出现异常,本质时我们的进程收到了对应的信号。当进程异常退出时,操作系统会向该进程发送错误信号

例如,我们的程序中有分母为0的错误,对应信号为8号SIGFPE,野指针错误对应11号SIGSEGV

向进程发生信号导致进程异常退出

例如,一个正常运行的进程,我们主动向该进程发信号,从而使该进程异常退出

5. 进程常见退出方法

 正常终止(可以通过 echo $? 查看进程退出码):  

1. 从main返回

2. 调用exit

3. _exit  

5.1 return退出 

        在main函数内return退出进程是我们最常用的方法

5.2 exit退出

 可以看下面样例的输出

void fun()
{printf("hello world!\n");printf("hello world!\n"); printf("hello world!\n");exit(13);
}int main()
{printf("hello Linux!\n");fun();//exit(12);return 12;
}
输出
echo $?
13

exit 函数在退出进程前会做一系列工作:

  1. 执行用户通过atexit或on_exit定义的清理函数
  2. 关闭所有打开的刘,所有的缓存数据均被写入
  3. 调用_exit函数终止进程 

printf 一定是先把数据写入缓冲区中,合适的时候(\n等)再进行刷新

例如,exit 终止进程前会换新缓冲区

可以看到即使没有\n,最终也刷新了缓冲区数据打印在屏幕上

5.3 _exit退出 

        同样的,_exit 也可以在代码中任意位置退出进程,但是它并不会在退出进程前做任何收尾工作

例如,使用_exit 函数终止进程,缓冲区不会刷新输出

        侧面证明了缓冲区并不在操作系统的内核部分,因为如果在内核,那么 _exit 也应该刷新缓冲区,操作系统是不会容忍任何浪费空间效率的行为

小结
  • exit 、_exit 在任意地方被调用都表示调用进程直接退出
  • return 是当前函数返回,只有在main函数内部return表示进程退出,因为主函数结束后,会将main函数的返回值作为 exit 函数的参数调用 eixt 函数
  • _exit 是系统调用,exit是库函数
  • 使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit 系统调用 终止进程,而_exit 会直接终止进程,不会做任何收尾工作

三、进程等待

        是什么:通过系统调用 wait / waitpid,来进行对子进程状态检测与回收功能

        为什么:因为僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄露问题

        怎么办:父进程通过wait、waitpid进程僵尸子进程的回收

1. 进程等待的必要性

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

2. 进程等待的方法

2.1 wait 方法

        wait 要包含两个头文件,形参我们暂时传NULL

举例,父进程wait 等待僵尸子进程 

        当父进程wait后,子进程由僵尸进程转为真正的死亡进程。但是如果父进程创建了多个子进程,那么wait返回的是哪一个子进程呢?又该如何等待呢?

当gcc版本低时 ( for循环内不能定义变量),可以在编译时加上 -std=c99

举例,多个子进程的wait,循环wait即可

        要实现多个子进程的wait,直接循环N次,判断wait的返回值是否大于0即可

wait阻塞

        如果在上面的样例中,我们使子进程死循环,一直不推出,那么父进程的wait还有效吗?

无效!父进程的wait将会处于阻塞状态,父进程一直运行,一直等待子进程的退出

如果子进程不退出,父进程默认在wait时,也就不返回,默认叫做阻塞状态

2.2 waitpid

        wait所能提供的功能是waitpid的子集

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

与之前举例效果相同

3. 获取子进程status

wait以及waitpid都涉及到了一个整形指针status参数,它的作用是什么呢?

        该参数是一个输出型参数,是父进程用来获取子进程退出信息的方式,父进程可以不获取子进程信息,直接将status参数处传NULL即可,这表示父进程不关心子进程的退出状态信息,但是父进程不能没有获取子进程信息的方式!

         通过传入指针的形式,在函数内部可以修改该整形变量,从而达到传递信息的功能

        所以,我们会在父进程内定义一个整型变量status,将它的地址传入wait或waitpid函数中

status整形变量是如何存储信息的呢?

         

        我们不能简单的将status视为普通的整形来看待,因为status的不同bit位所代表的信息不同。

        status是int型变量占4字节,共有32bit位,高位的16bit位不使用,而从低位开始的7bit位,它们存储该进程退出的异常码(可以使用kill -l查看),因为异常码范围是【1,64】,所以只需要7个bit位。

        第 8 bit 位为 core dump 标志,我们在后面的学习中再谈。

      【9, 16】bit位存储进程的退出状态码

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitSignal = status & 0x7F;      //退出信号
exitCode = (status >> 8) & 0xFF; //退出码0xFF:全1
0x7F:0111 1111

如果将子进程死循环,那么父进程的 waitpid 将会一直等待子进程的退出,当我们

        kill -9 子进程pid

子进程立即死亡,父进程打印出的exit sig即为9

系统提供了两个宏简化status内两个信息获取

   status:

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

4. options参数

阻塞等待 —— 0

        如果在wait、waitpid函数中,指定pid子进程如果没有退出,例如还在R状态,那么父进程只能一直等,并从R状态转为S状态,并脱离CPU上的运行队列,进入子进程PCB中的进程等待队列(父进程处于等待软件就绪),直至子进程退出转为Z状态,父进程获取退出信息,被唤醒,再次修改进程状态为R,进入运行队列

非阻塞轮询

使用宏WNOHANG 

  1 #include <stdio.h>2 #include <unistd.h>3 #include <stdlib.h>4 #include <sys/types.h>5 #include <sys/wait.h>6 7 //#define N 108 //9 //void runChild()10 //{11 //   int cnt = 3;12 //   while (cnt--)13 //   {14 //        printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());15 //        sleep(1);16 //   }17 //18 //}19 //20 int main()21 {22 //    for (int i = 0; i < N; i++)23 //    {24 //        pid_t id = fork();25 //        if (id == 0)26 //        {27 //            runChild();28 //            exit(i);29 //        }30 //        printf("create child process: %d success\n", id);31 //    }32 //33 //    sleep(5);34 //35 //    for (int i = 0; i < N; i++)36 //    {37 //        int status = 0;38 //        pid_t id = waitpid(-1, &status, 0);39 //        if (id > 0)40 //        {41 //            printf("wait %d success, exitCode: %d\n", id, WEXITSTATUS(status));42 //        }                                                                                                                            43 //    }44 //45 //    sleep(3);46 //   47     48     pid_t id = fork();49    50    if (id < 0)51    {52        perror("fork");53        return 1;54    }55    else if (id == 0)56    {57        //printf("子进程\n");58        int cnt = 3;59        while (cnt)60        {61            printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);62            sleep(1);63            --cnt;64        }65        exit(1);66    }67    else68    {69        //int cnt = 5;70        printf("父进程\n");71        //while (cnt)    72        //{                                                                                                                             73        //    printf("i am parent, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);74        //    sleep(1);75        //    cnt--;76        //} 77     78        //pid_t ret = wait(NULL);79        80        //轮询81        while (1)82        {83          int status = 0;84          //pid_t ret = waitpid(id, &status, 0);85          pid_t ret = waitpid(id, &status, WNOHANG);     //非阻塞86          if (ret > 0)87          {88               //printf("wait success, ret: %d, status: %d, exitsig: %d, exitcode: %d\n", ret, status, status&0x7F, (status >> 8) & 0xFF    );89               if (WIFEXITED(status))90               {91                   printf("进程是正常跑完的,退出码: %d\n", WEXITSTATUS(status));92               }93               else94               {95                   printf("进程出异常了\n");96               }97             break;98          }99          else if (ret < 0)
100          {
101              printf("wait failed!\n");
102             break;
103          }
104          else
105          {
106               //ret == 0
107               printf("子进程还没有退出,我再等等...\n");
108               sleep(1);
109          }
110         }
111        sleep(3);
112    }
113 
114 
115     return 0;
116 }

在父进程空闲时加入任务,父进程最先开始创建多进程,父进程也是最后退出进程

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>#define TASK_NUM 10
typedef void(*task_t)();task_t tasks[TASK_NUM];void task1(){printf("这是一个执行打印日志的任务, pid: %d\n", getpid());}void task2(){printf("这是一个中检测网络健康状态的任务, pid: %d\n", getpid());}void task3(){printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());}//在InitTask前声明一下Add函数int AddTask(task_t t);void InitTask(){for (int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;AddTask(task1);AddTask(task2);                                                                                                                    AddTask(task3);}int AddTask(task_t t){int pos = 0;for (; pos < TASK_NUM; pos++){if (!tasks[pos])break;}if (pos == TASK_NUM)return -1;tasks[pos] = t;return 0;}void DelTask(){}void Cheack()
{}void UpdateTask()
{}//执行任务
void ExecuteTask()
void ExecuteTask()
{for (int i = 0; i < TASK_NUM; i++){                                                                                                                                  if (!tasks[i]) continue;tasks[i]();}}int main(){pid_t id = fork();if (id < 0){perror("fork");return 1;}else if (id == 0){//printf("子进程\n");int cnt = 3;while (cnt){printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);--cnt;}exit(1);}else{//pid_t ret = wait(NULL);InitTask();//AddTask(task1);//AddTask(task2);//AddTask(task3);//轮询while (1){int status = 0;//pid_t ret = waitpid(id, &status, 0);                                                                                        pid_t ret = waitpid(id, &status, WNOHANG);     //非阻塞if (ret > 0){//printf("wait success, ret: %d, status: %d, exitsig: %d, exitcode: %d\n", ret, status, status&0x7F, (status >> 8) & 0xFF    );if (WIFEXITED(status)){printf("进程是正常跑完的,退出码: %d\n", WEXITSTATUS(status));}else{printf("进程出异常了\n");}break;}else if (ret < 0){printf("wait failed!\n");break;}else{//ret == 0//printf("子进程还没有退出,我再等等...\n");//sleep(1);//启动父进程自己的任务 ExecuteTask();usleep(500000);}}sleep(3);}return 0;}

5. wait / waitpid原理

        僵尸子进程可以丢掉代码和数据,但是绝不能丢掉代码控制快task_struct,因为task_struct内部有 sigcode、exitcode 两个字段值记录子进程退出时的退出码和异常信号。

        因为操作系统不相信用户,并且进程之间具有独立性,所以 wait、waitpid本质上都是由操作系统先判断子进程是否为僵尸进程,再完成读取子进程的task_struct内核数据结构,并将进程的Z状态改为X状态

        进程等待失败的情况就是该进程不是父进程的子进程

四、进程程序替换

1. 替换原理

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

        如果要让子进程执行不同的代码,就要用到程序替换技术

例如,单进程的进程程序替换

        ./mycommand 后,操作系统为该进程创建PCB、进程地址空间、页表,将程序对应的代码和数据从硬盘加载到内存中,完成页表的映射,当程序执行到 execl 函数时(例如ls),那么操作系统会将磁盘中ls对应的代码和数据直接替换mycmmand进程的代码和数据(包括堆栈等等,但是不替换环境变量),但是PCB和进程地址空间等结构不变,再从 ls 起始代码开始执行,也就是说使用 execl 后,execl 后面的代码将不再执行

当父进程fork创建子进程后,让子进程执行execl,那么子进程的程序替换会有影响到父进程吗?

        答案是不会!因为进程具有独立性,可是我们一致认为代码是只读的,他怎么会被修改替换呢?因为这件事并不是绝对的!对于 execl 而言,它的操作者是操作系统,在替换代码时,发现是代码是只读权限,那么操作系统就会触发写时拷贝,开辟新空间存放代码,再修改页表的映射

         子进程被创建初始即为13853,直至被wait回收也是13853,这就证明了execl并不是创建新的进程

        如果不是父子进程场景,那么直接替换;如果是父子进程场景,那么触发写时拷贝。

注意:

  • 程序替换并不会创建新的进程,只是将代码和数据进行了替换​​​​​​​ 
  • 程序替换成功后,exec*(表示exec这一系列函数)后续的代码不会执行,因为已经被覆盖替换;exec*函数只有失败返回值,成功就没有返回值 

CPU是如何知道新替换的程序入口地址是什么?

         涉及编译原理,Linux中形成的可执行程序都是有格式的——ELF,在可执行程序最开始有一个表,表明该可执行程序有哪些段(代码段、数据段等等)并写好地址,可执行程序的入口地址就在表头中

        所以,操作系统在替换进程的代码和数据的同时,也获取了该可执行程序的其他信息(程序执行的入口地址)

程序替换后,父进程怎么等待子进程?

        替换后父子关系不变,父进程等待的是PCB,PCB没变,替换的程序也会有相应的进程终止信息,所以没有影响

2. 7个替换函数

三号手册:6个库函数

二号手册:1个系统调用

        六个库函数底层是都调用该系统调用,完成程序替换

我们主要讲解其中五个:

#include <unistd.h>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[]);
execl

execl 函数在当前进程中加载并运行指定的程序,替换当前进程的映像为新程序的映像。如果 execl 函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果 execl 函数执行失败,它将返回 -1,并设置全局变量 errno 以指示错误原因

        execl 中的 l 表示的是list,列表的意思,将各个参数像链表一样链接起来,最后指向NULL

它的用法与命令行的用法相同,只是将空格分隔符转为逗号

ls -l -aexecl("/usr/bin/ls", "-l", "-a", NULL)

        执行一个程序第一件事就是找到程序所在位置,而第一个参数的意义就是找到程序的路径,在哪个绝对路径执行哪个指令;剩下的参数就是告诉bash命令行怎么执行

注意:

  • execl 函数不会继承调用它的进程的环境变量,除非显式地通过其他方式(如 execve 函数)传递它们。
  • 如果 execl 函数执行失败,它通常会返回 -1 并设置 errno。常见的错误原因包括文件不存在、文件不是可执行文件、权限不足等。
  • 由于 execl 函数的参数是变参的,因此在编写代码时需要特别注意参数列表的结束标志 NULL
execlp

execlp 函数在当前进程中查找并执行指定的程序。它首先会检查 PATH 环境变量,以确定要执行程序的位置。找到程序后,它会替换当前进程的映像(包括代码和数据段)为新程序的映像,并开始执行新程序。如果 execlp 函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果 execlp 函数执行失败,它将返回 -1,并设置全局变量 errno 以指示错误原因

        这里的p指的是PATH环境变量,它会自动去默认的PATH环境变量中查找

execlp("ls", "ls", "-a", "-l", NULL);

第一个ls参数是让要找到的程序,表示要执行谁,后面的ls表示要执行什么 ,怎么执行

  • file:要执行的新程序的名称(不是路径),该函数会在 PATH 环境变量指定的目录列表中查找该程序。
  • arg:传递给新程序的参数列表,第一个参数 arg 通常被新程序作为 argv[0](即程序名称),后续参数是新程序的参数,列表必须以 NULL 指针结束。注意,这里的参数是变参(variadic arguments),意味着你可以传递任意数量的参数,直到遇到 NULL 为止。然而,在实际编程中,由于 C 语言不支持直接传递可变数量的参数并在函数内部识别它们的结束,因此通常最后一个参数使用 (char *)NULL(或简单地 NULL,如果编译器可以隐式转换)来明确指示参数列表的结束。但请注意,在某些情况下,可能需要显式地强制类型转换 NULL 为 (char *)NULL,以避免类型不匹配的问题。然而,现代编译器通常能够处理这种类型转换,因此直接使用 NULL 也是可以的。

​​​​​​​​​​​​​​注意:

  • 第一个参数(即程序名称)可以写任何内容,但通常写为程序的实际名称以便于日志记录或错误处理。然而,这个名称并不会影响程序的执行路径,因为 execlp 会根据 PATH 环境变量来查找程序。
  • 参数列表必须以 NULL 指针结束,以指示参数列表的结束。
  • 如果 execlp 函数执行失败,它将返回 -1 并设置 errno。常见的错误原因包括文件不存在(即 PATH 环境变量中没有找到指定的程序)、权限不足等
 execv

execv函数用于在当前进程中加载并运行指定的程序,替换当前进程的映像(包括代码和数据段)为新程序的映像。如果execv函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果execv函数执行失败,它将返回-1,并设置全局变量errno以指示错误原因

        这里的v指的是vector,第二个参数是字符串指针数组,就是命令行参数,这个参数就是要让我们自己主动填写指针数组,再将指针数组传进函数内(我们在命令行参数时已经讲过argv),注意最后一个数据要填NULL

  • file:要执行的新程序的名称(不是路径),该函数会在 PATH 环境变量指定的目录列表中查找该程序。
  • arg:传递给新程序的参数列表,第一个参数 arg 通常被新程序作为 argv[0](即程序名称)后续参数是新程序的参数列表必须以 NULL 指针结束

        第一个参数依旧是指定路径,第二个参数依旧是怎么执行。

        操作系统会将 argv 传到 ls 内的main函数的argv表,形成指令选项,exev与execl的区别就是exev我们直接编写好了指针数组argv,而execl还要操作系统帮忙

execv就是加载器,将我们的可执行程序从磁盘导入到内存,也能传递命令行参数,将形参接收的argv传递给其他main函数

注意:

  • execv函数不会继承调用它的进程的环境变量,除非显式地通过其他方式(如execve函数)传递它们。
  • 如果execv函数执行失败,它通常会返回-1并设置errno。常见的错误原因包括文件不存在、文件不是可执行文件、权限不足等。
  • execv函数的参数argv数组必须以NULL结尾,且数组中的每个元素都应该是有效的字符串
execvp

        同execv,只是将路径改为直接在PATH环境变量中搜索

make形成多个可执行程序

.PHONY 是 Makefile 中使用的一个特殊目标(target),它用于声明该目标是一个“伪目标”(phony target)。伪目标并不是一个真实的文件名,Makefile 不会对它进行文件存在性的检查,也就是说,伪目标总是会被执行,无论它是否比它的依赖新。

        使用 .PHONY 的目的是为了明确地告诉 make 工具,某个目标是“伪”的,避免因为目标名称恰好与某个文件名相同而导致 make 工具产生误解。这对于定义一些只执行命令而不产生文件的清理操作(如 cleandistclean 等)特别有用。

  • make:设置伪目标all,all 依赖 mycommand 和 otherExe,所以make时会自顶向下寻找第一个依赖关系,all 关系链依赖两个文件,所以此时 make 会编译两个文件,又因为all 没有依赖方法,所以两个文件推导完成后直接结束。如果不设置伪目标all,那么make时只会自顶向下找到第一个依赖关系,例如mycommand,它的依赖文件存在,所以执行完依赖方法就结束了,不会再编译生成otherExe
  • clean:rm后直接跟两个可执行程序名即可

execle 

既然exec系列函数可以调用系统命令,那么他能调用我们的命令吗?(我们自己的可执行程序) 

        答案是可以的!我们在 mycommand.c 中通过 execl 调用了otherExe.cpp 可执行程序

可能有疑问的地方就是我们说过命令行怎么写,我们就怎么传参,那么我们之前在命令行执行可执行程序都是 ./otherExe 那么为什么此时 execl 的第二个参数没有带 ./ 呢?

        这是因为之前加 ./ 是为了让系统能找到该程序所在位置,知道工作路径在哪,如今 execl 的第一个参数目的就是为了让系统找到该可执行程序所在位置,这个工作已经做完了,所以可以不用带 ./,当然加上了也没有错

那么,C能调用C++程序,同样的C也能调用java、python、脚本等语言编写的程序。

例如,调用脚本文件(需要用解释器bash执行命令——bash test.sh) 

无论是可执行程序还是脚本,为什么 execl 能跨语言调用呢? 

    exec系列函数本身并不关心所加载的程序是用什么语言编写的。它们只负责将指定的程序文件(如可执行文件或脚本,如果系统配置了相应的解释器)加载到当前进程的地址空间中,并从该程序的入口点开始执行。这意味着,无论是用C、C++、Python、Java还是其他任何语言编写的程序,只要它们被编译或解释为可在当前操作系统上运行的格式,就可以通过exec系列函数来执行

         因为所有的语言运行起来,本质都是进程 ,只要是进程都可以被调度,就可以使用execl替换调用

ps:所以我们就可以在某些地方挂羊头卖狗肉(execl调用别的代码)

趁此机会,再验证一下execv

替换后,环境变量会发生什么?

        环境变量也是默认传递的,因为环境变量也是数据,也在进程地址空间,创建子进程后,子进程拷贝父进程的进程地址空间,所以环境变量就被子进程继承下去了 

        又我们之前再环境变量处讲过一个第三方的全局变量char** environ,它已经被父进程初始化了,创建子进程后,它也被子进程继承下去了,因为子进程并不修改该值,只是简单的使用查询,所以也就不会触发写时拷贝。

        所以我们不传参,该进程也能拿到环境变量但是execl程序替换之后,它替换了代码和数据但唯独没有替换环境变量,即环境变量没有被替换,而是被保留下来了

新增环境变量 

        所以如果想给子进程传递环境变量,该怎么传递? 

方法一:直接在bash的环境变量内添加环境变量

        因为mycommand和otherExe都是bash的子进程,所以它们都继承了bash的环境变量,会让子进程都获取到

方法二:在父进程内部调用 putenv

        即直接在父进程的环境变量内部添加,而bash内部没有,查不到

        bahs内部没有 

方法三:execle搭配environ

覆盖环境变量

 方法:execle

注意函数细节,NULL不要忘

此方法直接覆盖原环境变量

小结:

  • 这些函数原型看起来很容易混,但只要掌握了规律就很好记。
    l(list) : 表示参数采用列表
    v(vector) : 参数用数组
    p(path) : p 自动搜索环境变量 PATH
    e(env) : 表示自己维护环境变量 ​​​​​​​
  • 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。


http://www.ppmy.cn/embedded/98381.html

相关文章

ORACLE ADG 主库的归档日志不能主动传递到备库

主库有三个节点 &#xff0c;其中两个节点传递没有问题&#xff0c;唯独节点二的归档日志不能主动传递到备库&#xff0c;都是在备库恢复需要的时候一个个传递到备库。下面是备库的日志。 Media Recovery Waiting for thread 2 sequence 1204582 …

后端开发刷题 | 跳台阶问题

描述 一只青蛙一次可以跳上1级台阶&#xff0c;也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法&#xff08;先后次序不同算不同的结果&#xff09;。 数据范围&#xff1a;1≤n≤40 要求&#xff1a;时间复杂度&#xff1a;O(n) &#xff0c;空间复杂度&#…

知乎搜索技巧大公开, 三种方法帮你找到有价值的内容

本文将揭秘知乎搜索的三大高效技巧&#xff0c;帮助你从信息海洋中快速定位到真正有价值的内容。通过这些实用策略&#xff0c;无论是专业研究还是日常学习&#xff0c;都能让你的知乎之旅更加高效与充实。 正文 一、善用高级搜索语法&#xff0c;精准定位 在知乎搜索框中&a…

linux 提权

linux 提权 linux 提权linux 本机信息枚举常见命令find命令大全 linux 自动枚举工具内核提权利用步骤漏洞来源 sudo提权suid 提权capabilities(功能) 提权计划任务提权环境变量提权NFS&#xff08;网络文件共享&#xff09;提权 linux 提权 linux 本机信息枚举 常见命令 hos…

安卓中设置渐变字体和描边字体

1.CommonFontSpan abstract class CommonFontSpan : ReplacementSpan() {/** 测量的文本宽度 */private var mMeasureTextWidth 0foverride fun getSize(paint: Paint,text: CharSequence?,start: Int,end: Int,fontMetricsInt: FontMetricsInt?): Int {mMeasureTextWidth…

学习ComfyUI的一个不错网站:www.comflowy.com/basics

学习ComfyUI&#xff0c;不仅仅是照搬别人的工作流来使用&#xff0c;重要的是能自己搭建工作流&#xff0c;而要能够熟练搭建&#xff0c;离不开对工作流中节点&#xff08;特别是重要节点&#xff09;的透彻理解。比如我自己&#xff0c;原来对 Lora 就十分陌生&#xff0c;不…

SSH 隧道方式连接 MySQL 服务器

SSH 隧道方式连接 MySQL 服务器 1 安装 MySQL 客户端工具1.1 Navicat1.2 MySQL Workbench1.2.1 查看本机系统类型1.2.2 安装 Visual C 20191.2.3 安装 MySQL Workbench 2 SSH 隧道方式连接数据库2.1 Navicat2.1.1 SSH 连服务器2.1.2 本地连数据库 2.2 MySQL Workbench 本文介绍…

[数据集][目标检测]街灯检测数据集VOC+YOLO格式1893张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1893 标注数量(xml文件个数)&#xff1a;1893 标注数量(txt文件个数)&#xff1a;1893 标注…