进程控制(Linux)

进程控制

  • 一、进程创建
    • 1. 再识fork
    • 2. 写时拷贝
  • 二、进程终止
    • 前言——查看进程退出码
    • 1. 退出情况
      • 正常运行,结果不正确
      • 异常退出
    • 2. 退出码
      • strerror和errno
      • 系统中设置的错误码信息
      • perror
      • 异常信息
    • 3. 退出方法
      • exit和_exit
  • 三、进程等待
    • 1. 解决等待的三个问题
    • 2. 系统调用
      • wait
        • 参数为NULL
        • 使用status参数
        • 小结
      • waitpid
    • 3. 阻塞和非阻塞等待
    • 4. 进程等待的原理
  • 四、进程替换
    • 1. 概念
    • 2. exec函数族
      • ①execl
      • ②execlp
      • ③execle
      • ④execv
      • ⑤execvp
      • ⑥execvpe
      • 小结
    • 3. 系统调用——execve
    • 4. 总结

一、进程创建

1. 再识fork

在初始进程这篇博客中,浅谈了fork这个函数,在进程地址空间这篇中,也解释了一些关于fork返回值的问题。在这里再认识一下fork函数。

fork函数: 在Linux中,fork可以从已存在的进程中创建一个新的进程,新进程为子进程,原进程为父进程。

#include <unistd.h>
pid_t fork(void);
//返回值:如果成功,给父进程返回子进程PID,给子进程返回0,如果失败返回-1

来一段代码测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    int main()    
{    printf("Before:pid id %d\n", getpid());    pid_t pid = fork();    if(pid < 0)                                                                                                                                                                                                                             {    perror("fork");    exit(-1);    }    printf("After:pid is %d, fork return %d\n", getpid(), pid);    sleep(1);    return 0;    
} 

上述代码运行结果:
结果

fork运行的逻辑结构:
逻辑结构

结论: fork之前,父进程独立执行。fork之后,父子进程两个执行流分别执行。那个进程先执行由调度器决定

fork常规用法:

  1. 父进程希望复制自己,使父子进程同时执行不同的代码段。eg:父进程等待客户端请求,生成子进程处理请求。
  2. 一个进程要执行一个不同的程序。(进程替换,后面讲)

2. 写时拷贝

在进程地址空间这篇博客中也对写时拷贝进行了说明

OS为了提高效率,在创建子进程时会使用写时拷贝。本质就是按需申请,不会浪费系统资源

通过触发页表的可读权限,后续判断写入错误,发生写时拷贝,并更改权限

二、进程终止

前言——查看进程退出码

程序在执行完,无论正不正确都有一个退出码,可以查看

查看方式:echo $? 这个命令的意思就是查看 ? 的内容。
注:

  1. 查看最近一个进程或是命令运行结束的退出码,退出码保存在 ? 中。
  2. 先看程序是否异常,异常结束的退出码无意义

先实践一下:

#include <stdio.h>    int main()    
{    printf("hello Linux!\n");                                                                                                                             return 0;    
}

查看退出码:
查看退出码
退出码是0,程序正常运行结束

1. 退出情况

  1. 正常运行,结果正确
  2. 正常运行,结果不正确
  3. 代码异常终止

第一种情况就不再实验

正常运行,结果不正确

先看程序是否异常,如果没有异常再看退出码。

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    int main()    
{    int *p = (int*)malloc(1000*1000*1000*4);    if(p == NULL)    {    printf("error msg:%s\n", strerror(errno));                                                                                                      exit(errno);    }    return 0;    
} 

查看退出码:
查看退出码
退出码是12,可见结果不正确。我们也打印了错误信息Cannot allocate memory

异常退出

退出码无意义。
进程出现异常,本质是我们的进程收到对应的信号

#include <stdio.h>     int main()    
{    int a = 1, b = 0;    a = a / b;                                                                                                                                                                                                                              return 0;    
}  

查看运行结果:
运行结果
注: 第二次执行echo $? 发现结果是0。原因是上一次echo $?也是程序,并且是正常运行得到正常结果

在前面也说程序没有执行完就结束,是因为OS对进程发送了异常的信号。
测试一下:

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    int main()    
{    while(1)    {    printf("PID: %d\n", getpid());    sleep(1);                                                                                                                                                                                                                           }    return 0;    
} 

测试结果:
测试结果
对3330进程发送了11号信号,所以左边的会话弹出段错误,并且程序结束。

2. 退出码

父进程和用户可能会关心这个进程的运行结果,如果出错,那就要了解原因,而退出码对应着错误原因。退出码是程序执行结果和错误类型的一种有效表示方式。

C语言提供了两个函数和一个全局变量显示程序的错误信息。

strerror和errno

  1. strerror:查看系统对应的错误码信息
  2. errno:全局变量,strerror想要查看错误码信息,需要的错误码就是从errno来,程序出现结果不正确,错误码被设置,也就是errno被设置。
  3. 我们也可以自己设计一套对应的退出码

头文件:#include <string.h>
函数声明:char* strerror(int errnum)
参数:错误码(全局变量errno存放的就是错误码)
返回值:错误码对应的信息

代码:

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    int main()    
{    int *p = (int*)malloc(1000*1000*1000*4);    if(p == NULL)    {    printf("error msg:%s\n", strerror(errno));                                                                                                      exit(errno);    }    return 0;    
} 

运行结果:
运行结果

系统中设置的错误码信息

一共133个非常多

测试代码:

#include <stdio.h>    
#include <string.h>    int main()    
{    for(int i = 0; i < 140; i++)    {    printf("strerror[%d] -> msg:%s\n", i, strerror(i));                                                                                                                                                                                 }    return 0;    
}  

运行结果: (注:没有全部截下来,太长了)
运行结果

perror

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>                                                                                                                                                                                                                        int main()                            
{                                           int *p = (int*)malloc(4*1000*1000*1000);if(p == NULL)                           {                                 perror("malloc error");       exit(errno);                                                                                                                                                                                                       }                                                                                                                                                                                                                      return 0;                                                                                                                                                                                                              
} 

测试结果:
测试结果
直接打印出错误信息。
可以这样理解:strerror+errno+printf == perror

异常信息

kill -l 查看信号
kill -[信号编号] [进程PID] 对指定进程发生信号

查看所有信号:
查看信号

SIGHUP:挂起信号,通常由终端关闭或网络连接中断引起。用于通知进程重新加载配置文件或进行清理操作。
SIGINT:中断信号,通常由终端用户通过键盘输入Ctrl+C发送。用于请求进程终止。
SIGQUIT:退出信号,通常由终端用户通过键盘输入Ctrl+\发送。与SIGINT类似,但会生成核心转储文件。
SIGILL:非法指令信号,表示进程执行了一个非法的机器指令。
SIGTRAP:跟踪陷阱信号,用于调试目的。
SIGABRT:异常终止信号,通常由调用abort函数引起。
SIGBUS:总线错误信号,表示进程访问了无效的内存地址。
SIGFPE:浮点异常信号,表示进程执行了一个非法的浮点运算。
SIGKILL:强制终止信号,用于立即终止进程。不能被忽略或捕获。
SIGUSR1:用户自定义信号。
SIGSEGV:段错误信号,表示进程访问了无效的内存段。
SIGUSR2:用户自定义信号。
SIGPIPE:管道破裂信号,表示进程向一个已关闭的管道写入数据。
SIGALRM:闹钟信号,用于定时器操作。
SIGTERM:终止信号,用于请求进程正常终止。
SIGSTKFLT:协处理器栈错误信号。
SIGCHLD:子进程状态改变信号,用于通知父进程子进程的状态发生了改变。
SIGCONT:继续信号,用于恢复被SIGSTOP或SIGTSTP暂停的进程的执行。
SIGSTOP:停止信号,用于暂停进程的执行。
SIGTSTP:终端停止信号,通常由终端用户通过键盘输入Ctrl+Z发送。用于请求进程暂停。
SIGTTIN:后台读取信号,表示后台进程试图从终端读取数据。
SIGTTOU:后台写入信号,表示后台进程试图向终端写入数据。
SIGURG:紧急条件信号,表示进程收到了一个紧急数据。
SIGXCPU:CPU时间限制信号,表示进程超过了CPU时间限制。
SIGXFSZ:文件大小限制信号,表示进程试图创建一个超过文件大小限制的文件。
SIGVTALRM:虚拟定时器信号。
SIGPROF:性能分析器定时器信号。
SIGWINCH:窗口大小改变信号,表示终端窗口大小发生了改变。
SIGIO:异步I/O信号。
SIGPWR:电源故障信号。
SIGSYS:无效系统调用信号,表示进程执行了一个无效的系统调用。

信号部分,再进行介绍

3. 退出方法

exit和_exit

  1. exit
#include <stdio.h>    
#include <stdlib.h>    int main()    
{    printf("hello Linux");    exit(0);    return 0;    
} 

代码运行结果:
运行结果

  1. _exit
#include <stdio.h>    
#include <unistd.h>    int main()    
{    printf("hello Linux");    _exit(0);                                                                                                                                                                                                                               return 0;    
}  

代码运行结果:
运行结果

结论:通过结果发现exit刷新了缓冲区,_exit没有刷新缓冲区

exit和_exit的底层关系:
exit和_exit的底层关系

总结:

  1. exit是库函数,_exit是系统调用接口
  2. exit会刷新缓冲区,_exit不会刷新缓冲区
  3. C语言的exit的实现底层封装系统调用接口_exit
  4. 系统调用接口不刷新缓冲区,所以得出缓冲区不在内核中,在用户空间

注: exit和return的区别

  1. exit是直接退出进程,而return是函数的返回值
  2. 在main函数中的return与exit的作用相同。在其它函数return和exit作用不同

三、进程等待

1. 解决等待的三个问题

  1. 是什么?
    通过系统调用wait/waitpid,对子进程进行状态检测与回收功能
  1. 为什么?
    • 僵尸进程无法被kill -9杀死(因为僵尸进程已经死了),需要通过进程等待来杀掉它,进而解决内存泄漏问题。
    • 可以通过进程等待,获取子进程的退出情况,可以了解子进程的任务完成情况。
    • 总而言之,父进程可以通过等待,回收子进程资源,获取子进程退出信息,无论父进程是否关心这个信息。
  1. 怎么办?
    父进程通过调用wait/waitpid进行子进程回收。

2. 系统调用

wait

头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>函数声明:
pid_t wait(int *status);参数:输出型参数,执行结束status会带出进程的退出状态(包括错误信息和异常信息)返回值:成功返回所等待进程的pid,失败返回-1
参数为NULL

eg1:(简单使用)

#include <stdio.h>    
#include <unistd.h>    
#include <errno.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
#include <sys/types.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //child    printf("I am child, pid: %d\n", getpid());    }    else if(id < 0)    {    perror("fork:");    exit(errno);    }    else    {    sleep(5);    pid_t ID = wait(NULL);    if(ID == id)                                                                                      {    printf("等待成功!\n");    }    else    {    printf("等待失败!\n");    }    }    sleep(3);    return 0;    
} 

预期结果:创建子进程,子进程执行完代码先等待三秒,然后父进程等待五秒,其中差额的两秒子进程进入僵尸状态,然后父进程休眠完之后等待成功,再等待三秒结束进程。

实验结果: (符合预期结果)
实验结果

eg2:(多个子进程进行等待)

#include <stdio.h>                                                                                    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <errno.h>    
#include <stdlib.h>    #define N 3    void RunChild()    
{    int cnt = 5;    while(cnt--)    {    printf("I am Child Process, pid: %d, ppid: %d\n", getpid(), getppid());    sleep(1);    }    
}      int main()    
{    for(int i = 0; i < N; i++)    {    pid_t id = fork();    if(id == 0)    {    RunChild();    exit(i);    }    printf("create child process:%d success\n", id); //这句话只有父进程才会执行    }    sleep(10);    //开始等待    for(int i = 0; i < N; i++)    {    pid_t id = wait(NULL);if(id > 0){printf("回收进程:%d->%d\n", i, id);}}sleep(5);return 0;
}

实验结果:在回收子进程的过程中,等到谁就释放谁
实验结果

也可以使用while :; do ps axj | head -1; ps axj | grep test | grep -v grep;sleep 1; done;查看进程状态

使用status参数

使用输出型参数status的原因:

  1. 因为进程间的独立性,所以父进程不能直接访问子进程
  2. 通过返回值返回的信息成功要返回子进程pid,无法再返回别的信息。

status:

  1. 该参数可以不设置,就如同上面直接用NULL,表示不关心子进程的退出状态信息。
  2. 正常设置该参数,OS会根据该参数,将子进程的退出信息反馈父进程。
  3. status也不能用简单的整型看待,要从位图看待(只研究低16位)

退出信息位分布:(低16位)
status低16位

来两个小demo测试一下:

正常终止:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("I am child, pid: %d\n", getpid());exit(11);}else if(id > 0){//fatherint status = 0;pid_t ID = wait(&status);if(ID == id){printf("子进程的退出信息:%d\n", status);printf("等待成功!\n");}else{printf("等待失败!\n");}}else{perror("fork");exit(11);}return 0;
}

运行结果:
运行结果

异常终止:

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>int main()
{pid_t id = fork();if(id == 0){//childprintf("I am child, pid:%d\n", getpid());//出现异常int *p = NULL;*p = 0;}else if(id > 0){int status = 0;pid_t ID = wait(&status);if(ID == id){printf("子进程的退出信息:%d\n", status);                                                                                                                                                                  printf("等待成功!\n");}else{printf("等待失败!\n");}}else{perror("fork:");exit(errno);}return 0;
}

运行结果:
运行结果

小结

根据上文的测试:大致可以观察到测试结果和退出信息的位分布结果是一致的。

如何从位中获得具体的退出状态或者终止信号

  1. 退出状态:(status >> 8) & 0xFF
  2. 终止信号:(status & 0x7F)

系统也提供了一些宏,来帮助获取退出状态与终止信号

  1. 判断是否正常退出:WIFEXITED
    获取退出状态:WEXITSTATUS
  2. 判断是否被信号终止:WIFSIGNALED
    获取信号信息:WTERMSIG

根据上面的小结,再做一些小实验:

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <errno.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //child             printf("I am child, pid:%d\n", getpid());                                    //出现异常                                                                   int *p = NULL;                                                               *p = 0;                                                                      }                                                                              else if(id > 0)                                                                {                                                                              int status = 0;                                                              pid_t ID = wait(&status);                                                    if(ID == id)                                                                 {                                                                            if(WIFEXITED(status))                                                      {                                                                          //printf("正常退出,退出状态:%d\n", WEXITSTATUS(status)); //和下面一条语句结果一样printf("正常退出,退出状态:%d\n", (status >> 8) & 0xFF);                }                                                                          if(WIFSIGNALED(status))                                                    {                                                                          //printf("异常退出,终止信号:%d\n", WTERMSIG(status));   //和下面一条语句结果一样                                                                                                                       printf("异常退出,终止信号:%d\n", status & 0x7F);  }                                                                                 printf("等待成功!\n");                                                            }                                                                                   else                                                                                {                                                                                   printf("等待失败!\n");                                                            }                                                                                   } else {perror("fork:");exit(errno);}return 0;
}

运行结果:
运行结果

注: 这个测试,只是测试了异常终止的代码,正常结束的程序就不再赘述了。

waitpid

头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>函数声明:
pid_t waitpid(pid_t pid, int *status, int options);参数:pid:1. pid = -1,等待任意一个子进程。与wait的作用一样2. pid > 0。等待与pid相等的子进程status:(该参数在wait部分已经详细介绍)WIFEXITED:正常终止的子进程,则为真。(查看进程是否正常退出)WEXITSTATUS:WEXITSTATUS不为0,提取子进程退出码。(查看进程的退出码)options:WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不进行等待。若正常结束,则返回该子进程pid0:进行阻塞等待WUNTRACED:如果子进程进入暂停状态就立刻返回。返回值:1. 当正常返回的时候waitpid返回的是收集到子进程的PID2. 如果设置了第三个参数WNOHANG,而调用waitpid发现没有已退出的子进程可收集,则返回03. 调用出错,则返回-1,errno也会被设置

注: 在这里就不再对waitpid进行实验,在下一节内容顺带实验

3. 阻塞和非阻塞等待

阻塞等待:

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <errno.h>    int main()    
{    pid_t pid = fork();    if(pid < 0)    {    perror("fork");    exit(errno);    }    else if (pid == 0)    {    //child    printf("child Running, pid:%d\n", getpid());    sleep(5);    exit(140);    }    else    {    int status = 0;    pid_t ret = waitpid(-1, &status, 0);   //第一个参数代表任意子进程都可以再次等待,第三个参数就是阻塞等待    printf("wait......5s..\n");                                                                                                                                                                                  if(WIFEXITED(status) && ret == pid)    {    printf("等待成功,ret_code:%d\n", WEXITSTATUS(status));    }    else    {    printf("等待失败!\n");    exit(errno);    }    }    return 0;    
} 

运行结果:
运行结果

非阻塞等待:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>#define TASK_NUM 10typedef 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());
}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 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");exit(errno);}else if(id == 0){//childint cnt = 5;while(cnt--){printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);sleep(1);}exit(11);}else {int status = 0;InitTask();while(1)  //非阻塞轮询{pid_t ret = waitpid(id, &status, WNOHANG);  //阻塞只需要该WNOHANG为0即可if(ret > 0){if(WIFEXITED(status)){printf("代码正常跑完,退出码:%d\n", WEXITSTATUS(status));}else {printf("代码异常!\n");}break;}else if(ret < 0){printf("wait failed!\n");break;}else  //轮询,处理别的任务 {ExecuteTask();sleep(1);}}}return 0;
}

代码运行结果:
运行结果

总结: 阻塞等待和非阻塞等待

  1. 阻塞等待:直到子进程被等待成功,才会继续执行,否则就一直处于阻塞状态
  2. 非阻塞等待:无论能不能等待到子进程都进行返回,等待成功返回子进程pid,失败返回0
  3. 非阻塞等待相比阻塞等待,可以在子进程没结束的时候,做一些自己的事情。所以也称作非阻塞轮询
  4. wait函数就是阻塞等待,子进程不结束,就一直等

4. 进程等待的原理

  1. 子进程运行结束,父进程没有等待之前,子进程是为僵尸状态,其代码和数据先被释放,PCB保留在OS。
  2. 称为僵尸进程的子进程的PCB中有这样的两个变量,int exit_code, exit_signal
  3. 父进程调用系统调用wait进行等待,因为进程间的独立性,实际由OS获取这个两个变量,并写入输出型参数当中。

结构图

四、进程替换

1. 概念

进程替换:进程替换需要通过调用exec系列的函数,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。注:不会创建新的进程,会更新PCB,但是如进程的PID值这样的不会改变。很像是被小说中的大能夺舍,变得是灵魂,不变的是肉体。

2. exec函数族

exec系列函数:头文件:#include <unistd.h>返回值:1. 函数调用成功,则加载新的程序,开始执行代码,不返回2. 替换失败,返回-1.按照原先的代码继续运行。错误码被设置

①execl

函数声明:int execl(const char *path, const char *arg, ...);参数:1. path:指的是所要打开文件具体的路径2. arg:所要打开的文件名3. ...:可变参数列表,传的是具体选项,且以NULL结尾

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //进行进程替换    int ret = execl("/usr/bin/ls", "ls", "-l", NULL);    //int ret = execl("ls", "ls", "-l", NULL);        //这个填的路径是相对路径,就是相对当前路径来说//执行ls -l命令    if(ret == -1)    {    printf("进程替换失败!\n");    }    }                                                                                                                return 0;    
} 

测试结果: (进程替换成功)
测试结果

②execlp


函数声明:int execlp(const char *file, const char *arg, ...);
参数:1. file:指的是所要打开文件的路径,若不加路径,可以在当前路径和PATH环境变量下的路径寻找2. arg:所要打开的文件名3. ...:可变参数列表,传的是具体选项,且以NULL结尾	

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    int main()    
{    pid_t id = fork();    if(id == 0)    {    //进行进程替换    int ret = execlp("ls", "ls", "-l", NULL);                                                                      //执行ls -l命令    if(ret == -1)    {    printf("进程替换失败!\n");    }    }    return 0;    
}

测试结果:
测试结果

③execle

函数声明:int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:1. path:指的是所要打开文件具体的路径2. arg:所要打开的文件名3. ...:可变参数列表,传的是具体选项,且以NULL结尾4. envp:新的环境变量数组,即新执行程序的环境变量

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    int main()    
{    printf("before exec: USER=%s, HOME=%s\n", getenv("USER"), getenv("HOME"));    char *const env[] = {(char *const)"USER=KKK", (char *const)"HOME=hhhh", NULL};    int ret = execle("./process", "process", NULL, env);                                                                                                                                                                                    if(ret == -1)    {    perror("execle");    exit(11);    }    printf("After\n");    return 0;    
}   

替换的程序:

#include <stdio.h>                                                                                                                                                                                                                          
#include <cstdlib>    
#include <unistd.h>    int main()    
{    printf("USER=%s\n", getenv("USER"));    printf("HOME=%s\n", getenv("HOME"));    return 0;    
}  

测试结果:
运行结果

注意:因为一次编译了两个程序,所以我们在编译makefile时,要借用为目标,让其推到自己编译好两个程序

makefile文件:

.PHONY:ALL                                                                                                                                                
ALL:test process    
process:process.cpp    g++ -o $@ $^    test:test.c    gcc -o $@ $^ -std=c99    
.PHONY:clean    
clean:    rm -f test process 

④execv

函数声明:int execv(const char *path, char *const argv[]);
参数:1. path:替换程序的路径2. argv[]:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。

测试:

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    int main()    
{    char *const argv[] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL };                                                                 pid_t id = fork();    if(id == 0)    {    int ret = execv("/usr/bin/ls", argv);    if(ret == -1)    {    printf("进程替换失败!\n");    }    }    return 0;    
} 

测试结果:
测试结果

⑤execvp

函数声明:int execvp(const char *file, char *const argv[]);
参数:1. file:所要打开的文件路径(绝对和相对路径)。也可以在PATH环境变量下寻找2. argv:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。

测试:

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h>      #define ARR_NUM 4                                                                                                                                         int main()      
{      char *const argv[ARR_NUM] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL};      pid_t id = fork();      if(id == 0)      {      int ret = execvp("ls", argv);      if(ret == -1)      {      printf("进程替换失败!\n");      }      }      return 0;      
}  

测试结果:
测试结果

⑥execvpe

该函数接口是GNU扩展,所以在使用的时候要加上#define _GNU_SOURCE

函数声明:int execvpe(const char *file, char *const argv[], char *const envp[]);
参数:1. file:指的是所要打开文件的路径,也可在环境变量PATH下寻找2. argv:指的是所要打开文件的名及选项3. envp:要传入的环境变量数组

测试代码:(test进程,其中子进程替换成process进程)

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    #define ARR_NUM 3    extern char** environ;    int main()    
{    char *const argv[ARR_NUM] = { (char *const)"process", NULL };    char *str =(char *)"MY_VAL=449220104";                                                                                                                  putenv(str);    pid_t id = fork();    if(id == 0)    {    int ret = execvpe("./process", argv, environ);    if(ret == -1)    {    printf("进程替换失败!\n");    }    }    return 0;    
} 

process:

#include <iostream>    
using namespace std;    int main(int argc, char *argv[], char *env[])                                                                                                           
{    for(int i = 0; env[i]; i++)    {    cout << "env[" << i << "]: " << env[i] << endl;    }    return 0;    
}  

测试结果比较长,这里就不展示了。前面再介绍execle函数时,也说了如何编译多个程序

小结

上面我们介绍了exec系列的六种函数接口。这六个函数接口都包含在库文件中,而在这六个接口的底层,无疑调用了系统调用,这个系统调用接口就是execve。

六个函数的命名理解:
l (list):表示参数采用列表
v (vector):参数用数组
p (path):有p自动搜索环境变量PATH
e (env):表示自己维护环境变量

函数接口参数格式是否带路径是否使用当前环境变量
execl列表不是
execlp列表
execle列表不是需要自己组装环境变量
execv数组不是
execvp数组
execvpe数组需要自己组装环境变量
execve(系统调用)数组不是需要自己组装环境变量

代码:

  #include <unistd.h>    int main()    {    char *const argv[] = { "ps", "-axj", NULL };    char *const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };    execl("/bin/ps", "ps", "-axj", NULL);    //带p的,可以使用环境变量PATH    execlp("ps", "ps", "-axj", NULL);    //带e的,需要自己组装环境变量    execle("ps", "ps", "-axj", NULL, envp);    execv("/bin/ps", argv);    //带p的,可以使用环境变量PATH    execvp("ps", argv);    //带e的,需要自己组装环境变量    execve("/bin/ps", argv, envp);    //带p和e的    execvpe("ps", argv, envp);                                                                                                                            return 0;    }    

六个函数的关系图:(去掉了GNU扩展的那个函数)
关系图

3. 系统调用——execve

函数声明:int execve(const char *path, char *const argv[], char *const envp[]);
参数:1. path:替换程序的路径2. argv:保存的是参数列表,可执行文件和参数保存到字符串数组中,以NULL结尾3. envp:新的环境变量数组

测试:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main() {char *const argv[] = { "ls", "-l", NULL };char *const envp[] = { "PATH=/bin", NULL };int ret = execve("/bin/ls", argv, envp);if (ret == -1) {perror("execve");exit(EXIT_FAILURE);}return 0;
}

测试结果:
测试结果

4. 总结

  1. 子进程被进程替换之后,不会影响父进程。
    虽然说父子进程共享代码和数据,而进程替换也是替换代码和数据,但是本质是由OS再开一块物理空间,将页表的映射关系更改即可
  1. C代码可以替换C++程序是因为:操作系统对于正在运行的程序来说,无论是什么语言编写的,它们最终都会被操作系统视为进程来执行。最终都会被编译成机器码,以二进制形式存储在可执行文件中,当操作系统加载可执行文件并创建进程时,它会将这些机器码加载到内存中,并按照指令的顺序执行。
  1. 环境变量:子进程默认继承父进程的环境变量,但是进程的独立性,所以父子进程的环境变量也是相互独立的,子进程环境变量的修改不会影响父进程。eg:Shell的环境变量就是在用户登陆时从配置文件.bash_profile中加载
  1. 进程的入口:Linux形成的可执行程序是有格式的ELF格式,ELF格式定义了可执行文件的结构和布局。可执行程序被加载到内存时,OS会先读取可执行文件的表头。表头中包含程序的入口地址(程序开始执行的第一个指令的内存地址)
小知识:讲这个的原因,是因为说到了C代码可以替换C++程序,所以进行扩展
test.sh --> 全称.Shell --> Shell脚本
Shell脚本就是把Linux命令放到一个文件
开头-->#!(shebang-->用于指定脚本文件的解释器。作用就是告诉OS用那个解释器执行脚本文件) 所以要紧跟脚本语言对应的解释器。-----------------------------------
test.py:
#!/usr/bin/python3
print("hello Python!")
-----------------------------------
test.sh:
#!/usr/bin/bash
function myfun()
{cnt=1while [ $cnt -le 10]doecho "hello $cnt"let cnt++done
}
echo "hello 1"
ls -a -l
myfun
-----------------------------------替换上面的程序:
execl("/usr/bin/bash", "bash", "test.sh", NULL);
execl("/usr/bin/python3", "python3", "test.py", NULL);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/253577.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity_ShaderGraph节点问题

Unity_ShaderGraph节点问题 Unity版本&#xff1a;Unity2023.1.19 为什么在Unity2023.1.19的Shader Graph中找不见PBR Master节点&#xff1f; 以下这个PBR Maste从何而来&#xff1f;

项目02《游戏-09-开发》Unity3D

基于 项目02《游戏-08-开发》Unity3D &#xff0c; 本次任务是做抽卡界面&#xff0c;获取的卡片增添在背包中&#xff0c;并在背包中可以删除卡片&#xff0c; 首先在Canvas下创建一个空物体&#xff0c;命名为LotteryPanel&#xff0c;作为抽卡界面&#xff0c; …

Redis的数据类型Hash使用场景实战

Redis的数据类型Hash使用场景 常见面试题&#xff1a;redis在你们项目中是怎么用的&#xff0c;除了String数据类型还使用什么数据类型&#xff1f; 怎么保证缓存和数据一致性等问题… Hash模型使用场景 知识回顾&#xff1a; redisTemplate.opsForHash() 方法是 Redis 的 …

【Nginx】nginx入门

文章目录 一、Web服务器二、Nginx三、Nginx的作用Web服务器正向代理反向代理 四、CentOS上安装Nginx(以CentOS 7.9为例) 一、Web服务器 Web 服务器&#xff0c;一般是指“网站服务器”&#xff0c;是指驻留于互联网上某种类型计算机的程序。Web 服务器可以向 Web 浏览器等客户…

Centos 7.5 安装 NVM 详细步骤

NVM&#xff08;Node Version Manager&#xff09;是一个用于管理Node.js版本的工具&#xff0c;它可以让你轻松地在多个版本之间切换。NVM 通过下载和管理 Node.js 的多个版本&#xff0c;为用户提供了一种灵活的方式来使用不同版本的 Node.js。如果你需要更多关于NVM的信息&a…

C语言操作符详解

操作符的分类 • 算数操作符 &#xff1a; 、 - 、 * 、 / 、 % • 移位操作符 &#xff1a; << 、 >> • 位操作符 &#xff1a; & 、 | 、 ^ • 赋值操作符 &#xff1a; 、 、 - 、 * 、 / 、 % 、 << 、 >> 、 & 、 |…

阅读《极客时间 | Kafka核心技术与实战》(一)【Kafka入门】

阅读《极客时间 | Kafka核心技术与实战》 为什么要学习Kafka消息引擎系统ABC一篇文章带你快速搞定Kafka术语我应该选择哪种Kafka&#xff1f;聊聊Kafka的版本号 为什么要学习Kafka 如果你是一名软件开发工程师的话&#xff0c;掌握 Kafka 的第一步就是要根据你掌握的编程语言去…

移动光猫gs3101超级密码及改桥接模式教程

文章目录 超级管理员账号改桥接模式路由器连接光猫&#xff0c;PPPOE拨号即可&#xff01;附录&#xff1a;如果需要改桥接的话不知道拨号密码咋办打开光猫Telnet功能Telnet 登录 参考文章 移动光猫吉比特GS3101超级账号获取更改桥接 移动光猫gs3101超级密码及改桥接模式教程 …

单片机——FLASH(2)

文章目录 flash &#xff08;stm32f40x 41x的内存映射中区域详解&#xff09;flash写数据时 flash &#xff08;stm32f40x 41x的内存映射中区域详解&#xff09; Main memory 主存储区 放置代码和常数 System memory 系统存储区 方式bootloader代码 OTP区 一次性可编程区 选项…

AR人脸106240点位检测解决方案

美摄科技针对企业需求推出了AR人脸106/240点位检测解决方案&#xff0c;为企业提供高效、精准的人脸识别服务&#xff0c;采用先进的人脸识别算法和机器学习技术&#xff0c;通过高精度、高速度的检测设备&#xff0c;对人脸进行快速、准确地定位和识别。该方案适用于各种应用场…

spring boot和spring cloud项目中配置文件application和bootstrap加载顺序

在前面的文章基础上 https://blog.csdn.net/zlpzlpzyd/article/details/136060312 日志配置 logback-spring.xml <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"10000000 seconds" debug…

Flink流式数据倾斜

1. 流式数据倾斜 流式处理的数据倾斜和 Spark 的离线或者微批处理都是某一个 SubTask 数据过多这种数据不均匀导致的&#xff0c;但是因为流式处理的特性其中又有些许不同 2. 如何解决 2.1 窗口有界流倾斜 窗口操作类似Spark的微批处理&#xff0c;直接两阶段聚合的方式来解决…

【开源】SpringBoot框架开发超市账单管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统设计3.1 总体设计3.2 前端设计3.3 后端设计在这里插入图片描述 四、系统展示五、核心代码5.1 查询供应商5.2 查询商品5.3 新增超市账单5.4 编辑超市账单5.5 查询超市账单 六、免责说明 一、摘要 1.1 项目介绍 基于…

Adobe Camera Raw for Mac v16.1.0中文激活版

Adobe Camera Raw for Mac是一款强大的RAW格式图像编辑工具&#xff0c;它能够处理和编辑来自各种数码相机的原始图像。以下是关于Adobe Camera Raw for Mac的一些主要特点和功能&#xff1a; 软件下载&#xff1a;Adobe Camera Raw for Mac v16.1.0中文激活版 RAW格式支持&…

EMQX Enterprise 5.3 发布:审计日志、Dashboard 访问权限控制与 SSO 一站登录

EMQX Enterprise 5.3.0 版本已正式发布&#xff01; 新版本带来多个企业特性的更新&#xff0c;包括审计日志&#xff0c;Dashboard RBAC 权限控制&#xff0c;以及基于 SSO&#xff08;单点登录&#xff09;的一站式登录&#xff0c;提升了企业级部署的安全性、管理性和治理能…

C遗漏知识(个人向)

之前C语言遗漏的一些。 数据在内存中的存储 原码、反码、补码 整数的2进制表⽰⽅法有三种&#xff0c;即 原码、反码和补码 正整数的原、反、补码都相同。 负整数的三种表⽰⽅法各不相同。 原码&#xff1a;直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。 反码&…

Project 2019下载安装教程,保姆级教程,附安装包和工具

前言 Project是一款项目管理软件&#xff0c;不仅可以快速、准确地创建项目计划&#xff0c;而且可以帮助项目经理实现项目进度、成本的控制、分析和预测&#xff0c;使项目工期大大缩短&#xff0c;资源得到有效利用&#xff0c;提高经济效益。软件设计目的在于协助专案经理发…

云计算 - 弹性计算技术全解与实践

一、引言 在过去的十年里&#xff0c;云计算从一个前沿概念发展为企业和开发者的必备工具。传统的计算模型通常局限于单一的、物理的位置和有限的资源&#xff0c;而云计算则通过分布式的资源和服务&#xff0c;为计算能力带来了前所未有的"弹性"。 弹性&#xff1a;…

浅谈分布式系统

常见基本概念 在正式引入架构演进之前&#xff0c;先了解几个比较重要的概念做前置介绍。 应用(Application)/系统(System) 一个应用&#xff0c;就是一个/一组服务器程序 模块(Module)/组件(Component) 一个应用&#xff0c;里面有很多个功能。每个独立的功能&#xff0c;就可…

Unity AnimationRigging无法修改权重?

个人理解&#xff0c;已解决无法修改权重的问题: unity自带的动画系统是在FixUpdate和Update之后LateUpdate之前执行&#xff0c;如果在这FixedUpdate或Update函数内更新AnimationRigging内的权重后&#xff0c;内部动画系统会覆盖权重的修改&#xff0c;导致无法正确更新&…