替换原理

fork创建子进程后执行的是和父进程相同的程序(但又可能执行不同的代码分支),如果想让子进程执行不同的程序,可以通过exec函数启动另一程序并替换执行; 当调用exec函数后,该进程的用户空间代码和数据完全被新程序替换; 调用exec函数并不创建新进程,所以调用前后的进程 id 不改变;

替换函数

  1. execl(执行文件名 + 变长数) 格式int execl(const char* path, const char* arg, ..., NULL);

    用法:该函数接受程序路径和一系列变长参数来执行新的程序,注意最后的参数必须以NULL结尾; 示例

execl("/usr/bin/ls", "ls", "-l", "-a", "/home", (char*)NULL);
// 注意,这里的"/home"是一个可选选项,用于指示其具体的目录,如果没有,则默认是当前目录
  1. execv(执行文件名 + 参数数组) 格式int execv(const char* path, char* const argv[]); 用法:与execl类似,但参数通过数组传递,适合动态构建参数列表的场景; 示例
char *args[] = { "ls", "-l", "/home", NULL };
execv("/bin/ls", args);
  1. execle(执行文件名 + 变长参数 + 环境变量) 格式int execv(const char* path, char* const argv[]); 用法:与execl类似,但最后允许指定一个环境变量数组 envp[],以控制新程序的环境。 示例
char *env[] = { "USER=guest", NULL };
execle("/bin/ls", "ls", "-l", "/home", (char *)NULL, env);
  1. execve(执行文件名 + 变长参数 + 环境变量) 格式int execve(const char* path, char* const ) 用法:该函数是最底层的exec的变体,接受路径、参数数组和环境变量数组;所有其他的exec函数都基于它实现; 示例
char *args[] = { "ls", "-l", "/home", NULL };
char *env[] = { "USER=guest", NULL };
execve("/bin/ls", args, env);
  1. execlp(执行可搜索路径 + 变长参数) 格式int execlp(const char *file, const char *arg, ..., NULL);; 用法:与 execl 相似,但 file 是可在环境变量 PATH 中搜索的可执行文件名,而不必指定完整路径; 示例
execlp("ls", "ls", "-l", "/home", (char *)NULL);
  1. execvp(执行可搜索路径 + 参数数组) 格式int execvp(const char *file, char *const argv[]); 用法:与execv相似,但file是可在PATH环境变量中搜索的文件名; 示例
char *args[] = { "ls", "-l", "/home", NULL };
execvp("ls", args);

总结

  1. lv的区别:带有l,只需提供变长参数即可;而v则需要提供参数数组;
  2. e的含义:需要提供自定义的环境变量;
  3. p的作用:可以直接使用可执行文件名,其会自动在PATH变量中查找相关文件,无需提供完整路径;

六大函数总结图如下: 屏幕截图 2024-09-07 103551.png 正如上面所述,真正的系统调用是execve,所有其他的函数都依赖于其实现,所以execveman手册的第二节,而其他的函数位于man手册的第三节; 屏幕截图 2024-09-07 103859.png

自制简易shell

有了上面一系列知识的铺垫,我们可以自己实现一个简易的shell程序,用于执行系统的部分可执行文件;

初步认识

首先,我们需要了解一般的shell的运作机理,简单来说,shell在执行用户命令时,会生成一个子进程用于执行,执行结束后,子进程自动销毁,而shell作为父进程则继续运行,接受用户的下一个命令,如此循环往复; 屏幕截图 2024-09-07 111123.png

框架实现

有了上面的认识,我们就可以使用之前介绍的exec函数来替换子进程的程序以运行用户的命令,所以一个shell程序,需要循环一下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立子进程(fork)
  4. 替换子进程(exec)
  5. 父进程等待子进程退出(wait) 下面是其简单实现:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>

#define NUM 1024
#define OPT_NUM 64


char lineCommand[NUM];
char* myargv[OPT_NUM];

int main(){
  while(1){
    printf("[用户名@主机名 当前路径] # ");
    fflush(stdout);

    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);

    assert(s != NULL);
    (void)s;
    lineCommand[strlen(lineCommand) - 1] = 0;

    myargv[0] = strtok(lineCommand, " ");

    int i = 1;
    while((myargv[i++] = strtok(NULL, " ")));
  
    if(strcmp(myargv[0], "cd") == 0){
      chdir(myargv[1]);
      continue;
    }
#ifdef DEBUG 
    for(int j = 0; myargv[j]; ++j){
      printf("%d : %s\n", j, myargv[j]);
    }
#endif
    pid_t id = fork();
    assert(id != -1);

    if(id == 0){
      execvp(myargv[0], myargv);
      exit(0);
    }
  
    waitpid(id, NULL, 0); 
  }

  return 0;
}

具体的代码解释以后再更新...

总结

以上就是Linux中进程程替换,介绍了几个替换函数以及进程替换的原理,并通过这些知识实现了一个简易的shell程序,用于执行用户的命令;需要注意的是,程序替换本身没有开启新进程,所以前后的进程ID是一致的;