<Linux> 进程控制

目录

一、进程创建

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 10
  8 //
  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    else
 68    {
 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               else
 94               {
 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 -a
    
execl("/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节。这些函数之间的关系如下图所示。

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值