【LInux】linux控制(进程替换,自主shell的实现详解)

本文是小编巩固自身而作,如有错误,欢迎指出!

目录

一、进程替换

进程替换的核心原理

exec系列函数

execl

execlp

execv

execle

execvpe

[exec 系列程序替换总结](#exec 系列程序替换总结)

[1. 本质是什么](#1. 本质是什么)

[2. 核心作用](#2. 核心作用)

[3. 6 个函数统一规律](#3. 6 个函数统一规律)

二、自主myshell的实现

1.交互界面,命令行的复现

2.字串的分隔问题,解析命令行

strtok的使用

3.常规命令的执行

4.内建命令


一、进程替换

进程替换的核心原理

进程替换 (Process Replacement),在类 Unix 系统(如 Linux)中,指一个进程不创建新进程、不改变 PID ,完全替换自身的代码段、数据段、堆栈 ,转而执行另一个全新程序的机制。核心是 "换程序,不换进程"

进程 = 内核结构(PCB、PID、文件描述符) + 用户空间(代码、数据、堆、栈)

  • 保留:PID、PPID、进程控制块(PCB)、打开的文件描述符、当前工作目录、用户 ID 等。
  • 替换:清空原用户空间,加载新程序的代码、数据、BSS 段,重建页表与内存映射。
  • 执行 :成功后从新程序入口(_startmain)开始执行,原进程后续代码不再执行
  • 返回成功不返回失败返回 -1 并设置 errno

exec系列函数

我们在系统自带的man手册中可以看到exec相关函数族的用法,下面我们详细解释.

execl

cpp 复制代码
#include <unistd.h>

// 原型
int execl(const char *path, const char *arg, ... /* NULL */);
  1. path

    • 要执行的程序完整路径
    • 例如:/bin/ls/usr/bin/python3
    • 不能只写 ls,必须写全路径
  2. arg(以及后面所有 ...)

    • 传给新程序的命令行参数
    • 第一个 arg = 程序自己的名字argv[0]
    • 后面是真正的参数
    • 最后必须写 NULL 表示结束
cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("我是原来的程序,即将替换成 ls...\n");

    // 程序替换:把当前进程换成 /bin/ls
    execl("/bin/ls", "ls", "-l", NULL);

    // 下面这句话 **只有 execl 失败时才会打印**
    perror("execl 失败");
    return 1;
}

execlp

cpp 复制代码
int execlp(const char *file, const char *arg, ... /* NULL */);
  • file

    • 程序文件名,不用写路径
    • 系统会自动去 PATH 里找
    • ls 就行,不用 /bin/ls
  • arg...

    • 和 execl 完全一样
    • 第一个是程序名
    • 最后必须 NULL

execv

cpp 复制代码
int execv(const char *path, char *const argv[]);
  • path

    • 程序完整路径,同 execl
  • argv[]

    • 字符串指针数组

    • 格式:

      cpp 复制代码
      argv[0] = 程序名
      argv[1] = 参数1
      argv[2] = 参数2
      ...
      最后一个 = NULL
      cpp 复制代码
      #include <stdio.h>
      #include <unistd.h>
      
      int main() {
          // 构造参数数组:必须以 NULL 结尾
          char *argv[] = {
              "ls",   // argv[0]
              "-l",   // argv[1]
              NULL    // 结束标志
          };
      
          printf("准备替换成 ls...\n");
      
          // 执行程序替换
          execv("/bin/ls", argv);
      
          // 失败才会走到这里
          perror("execv failed");
          return 1;
      }

execle

cpp 复制代码
int execle(const char *path,
           const char *arg, ...,
           NULL,
           char *const envp[]);
  • path

    • 要执行程序的完整路径 ,如 /bin/ls
    • 不会自动搜索 PATH
  • arg, ...

    • 命令行参数列表
    • 第一个 arg = argv[0](程序名)
    • 后面是参数
    • 必须以 NULL 结束参数列表
  • envp[]

    • 自定义环境变量数组

    • 格式:"VAR=VALUE"

    • 数组最后一项必须是 NULL

    • 新进程只使用这些环境变量 ,不继承父进程

      cpp 复制代码
      #include <stdio.h>
      #include <unistd.h>
      
      int main()
      {
          // 自定义环境变量:必须以 NULL 结尾
          char *env[] = {
              "NAME=test_exec",
              "AGE=22",
              "PATH=/bin",  // 保证能找到系统命令
              NULL
          };
      
          printf("execle 替换程序...\n");
      
          // execle(路径, argv[0], 参数..., NULL, 环境变量数组);
          execle(
              "/bin/env",   // 要执行的程序
              "env",        // argv[0]
              NULL,         // 参数结束
              env           // 自定义环境变量
          );
      
          perror("execle 失败");
          return 1;
      }

execvpe

cpp 复制代码
int execvpe(const char *file,
            char *const argv[],
            char *const envp[]);
  • file

    • 程序名,如 ls
    • p → 会自动在 PATH 中搜索
  • argv[]

    • 参数数组
    • argv[0] = 程序名
    • 最后一项必须是 NULL
  • envp[]

    • 自定义环境变量数组
    • 格式同上
    • 结尾必须 NULL
  • l:参数用列表(逗号分隔)
  • v:参数用数组
  • p:自动搜 PATH
  • e:自定义环境变量(不继承

exec 系列程序替换总结

1. 本质是什么

  • 进程不变,程序换掉
  • PID 不变、文件描述符不变、进程身份不变
  • 把当前进程的代码、数据、堆栈全部替换成新程序
  • 成功不返回,失败返回 -1

2. 核心作用

让一个进程运行另一个程序 ,最经典搭配:fork() 创建子进程 → exec 替换成新程序这就是 shell 执行命令、系统启动程序的底层原理。

3. 6 个函数统一规律

  • l :参数用列表,逗号分隔,结尾 NULL
  • v :参数用数组,argv [],结尾 NULL
  • p :根据 PATH 搜索命令,不用写全路径
  • e :使用自定义环境变量,不继承父进程

二、自主myshell的实现

1.交互界面,命令行的复现

我们可以看到,在我们xshell的界面,是这样的命令行

他们都是这样的格式

bash 复制代码
[\u@\h \W]\$ 
  • \u:用户名
  • \h:短主机名
  • \W:当前工作目录的基名(仅显示最后一级目录)
  • \$:普通用户 /root 提示符切换

那我们要复现出这样的提示符,首先就得包含上述内容

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

char* getusrname()
{
  return getenv("USER");
}

char* gethostname()
{
  return getenv("HOSTNAME");
}

char* getpwd()
{
  return getenv("PWD");
}

int main()
{
  printf("[%s@%s %s]# \n", getusrname(), gethostname(), getpwd());

  return 0;
}

可以看到我们的提示符已经实现,那么我们要怎么实现一个可以输入命令的命令行呢?

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024

char commandline[LINE_SIZE];

char* getusrname()
{
  return getenv("USER");
}

char* gethostname()
{
  return getenv("HOSTNAME");
}

char* getpwd()
{
  return getenv("PWD");
}

void interact(char* cline, int size)
{
  printf(FORMATE, getusrname(), gethostname(), getpwd());
  fflush(stdout);

  fgets(cline, size, stdin);

  // 删掉换行符(必须保留!)
  cline[strcspn(cline, "\n")] = '\0';
  
  // 测试:把输入的命令打印出来
  printf("%s\n", cline);
}

int main()
{
  while(1)
  {
    interact(commandline, sizeof(commandline));
  }

  return 0;
}

在上述代码中,重点在于以下几点:

1. 死循环 while (1):

这就是Shell 能一直让你输命令的原因!

作用:

  • 输完一条命令 → 循环回来
  • 再显示提示符 → 再输
  • 再执行 → 再回来
  • 永远不退出

类比:

你用的终端是不是输完 ls 还能输 pwd ?就是因为里面有个 while (1) 死循环

2. 为什么要去掉换行符?

cpp 复制代码
cline[strcspn(cline, "\n")] = '\0';

原因只有一句话:

fgets 会把你按的回车键(Enter)一起读进来!

\n = 换行符,不是命令的一部分

不删会怎样?

  • 执行 ls 会变成 ls\n
  • 系统找不到这个命令
  • 直接报错,无法运行

3. fgets 是什么?怎么用?

cpp 复制代码
fgets(cline, size, stdin);
  • cline:读到哪里去(存命令的数组)
  • size:最多读多少字符
  • stdin:从键盘读(标准输入)

  • 读到回车就停止
  • 会把回车 \n 一起读进去(所以要删)
  • 安全、不会越界

2.字串的分隔问题,解析命令行

上一个模块,我们可以将用户的输入写入到一个字符数组commandline中了,那么接下来我们就要解析一下用户的输入,如果用户的输入带选项的指令,那么选项和指令之间,选项和选项之间都是以空格为分隔符,例如ls -a -l,所以我们应该按照空格为分隔符进行分隔用户的输入,即字符数组commandline

而为了实现切割的目的,我们使用c语言的strtok

strtok的使用

cpp 复制代码
char *strtok(char *str, const char *delim);
  1. char *str

要被分割的字符串

  1. const char *delim

分隔符(你想按什么切)

可以是:

  • 单个字符:" "(空格)
  • 多个字符:" ;\t\n"(空格、分号、制表符、换行)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024

char commandline[LINE_SIZE];
// 用来存放分割后的命令和参数
char *args[64];

char* getusrname()
{
    return getenv("USER");
}

char* gethostname()
{
    return getenv("HOSTNAME");
}

char* getpwd()
{
    return getenv("PWD");
}

// 新增:用 strtok 分割命令行
void parse_command()
{
    int i = 0;
    // 第一次分割:传原字符串
    args[i] = strtok(commandline, " ");
    
    // 继续分割,直到分割完毕
    while(args[i] != NULL)
    {
        i++;
        args[i] = strtok(NULL, " ");
    }
}

void interact(char* cline, int size)
{
    printf(FORMATE, getusrname(), gethostname(), getpwd());
    fflush(stdout);

    fgets(cline, size, stdin);

    // 去掉换行符
    cline[strcspn(cline, "\n")] = '\0';
    
    // 测试打印你输入的内容
    printf("%s\n", cline);
}

int main()
{
    while(1)
    {
        // 1. 交互获取命令
        interact(commandline, sizeof(commandline));

        // 2. 使用 strtok 解析命令
        parse_command();

        // 测试:打印分割后的结果
        printf("--- 分割后 ---\n");
        for(int i=0; args[i]!=NULL; i++)
        {
            printf("args[%d] = %s\n", i, args[i]);
        }
    }

    return 0;
}

3.常规命令的执行

我们上两个模块我们已经可以接收用户输入,将用户输入的字符串解析出来,那么接下来就是根据解析出来的命令和选项去执行命令了,对于普通命令,是由bash创建子进程,子进程去执行普通命令,由于我们有命令就是argv[0],但是我们没有路径,我们有命令行参数argv,子进程进行程序替换execvp即可

cpp 复制代码
// 执行命令(核心!)
void execute_cmd()
{
    // 空命令直接跳过
    if(args[0] == NULL)
        return;

    pid_t pid = fork();  // 创建子进程

    if(pid == 0)
    {
        // 子进程:执行命令
        execvp(args[0], args);

        // 如果 execvp 执行失败,才会走到这里
        perror("exec error");
        exit(1);
    }
    else if(pid > 0)
    {
        // 父进程:等待子进程结束
        wait(NULL);
    }
    else
    {
        // 创建进程失败
        perror("fork error");
    }

整体思路就是通过我们的输入,把输入分割成一个个块,然后通过execvp在系统中环境变量中寻找相关命令。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024

char commandline[LINE_SIZE];
char *args[64];

char* getusername()
{
    return getenv("USER");
}

char* gethost()
{
    return getenv("HOSTNAME");
}

char* getmypwd()
{
    return getenv("PWD");
}

void interact(char* cline, int size)
{
    printf(FORMATE, getusername(), gethost(), getmypwd());
    fflush(stdout);

    fgets(cline, size, stdin);
    cline[strcspn(cline, "\n")] = '\0';
   
}

void parse_command()
{
    int i = 0;
    args[i] = strtok(commandline, " ");

    while(args[i] != NULL)
    {
        i++;
        args[i] = strtok(NULL, " ");
    }
}

void execute_cmd()
{
    if(args[0] == NULL)
        return;

    pid_t pid = fork();

    if(pid == 0)
    {
        execvp(args[0], args);
        perror("exec error");
        exit(1);
    }
    else if(pid > 0)
    {
        wait(NULL);
    }
    else
    {
        perror("fork error");
    }
}

int main()
{
    while(1)
    {
        interact(commandline, sizeof(commandline));
        
        // 空输入跳过
        if(strlen(commandline) == 0)
            continue;
            
        parse_command();
        execute_cmd();  

    return 0;
}

4.内建命令

在上述学习中,我们已经了解自主实现的shell要运用系统本身自带的命令该怎么做,现在我们看看怎么在自主实现的shell中实现。我们现在就以自建命令cd为例子

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024

char commandline[LINE_SIZE];
char *args[64];

char* getusername()
{
    return getenv("USER");
}

char* gethost()
{
    return getenv("HOSTNAME");
}

char* getmypwd()
{
    return getenv("PWD");
}

void interact(char* cline, int size)
{
    printf(FORMATE, getusername(), gethost(), getmypwd());
    fflush(stdout);

    fgets(cline, size, stdin);
    cline[strcspn(cline, "\n")] = '\0';
}

void parse_command()
{
    int i = 0;
    args[i] = strtok(commandline, " ");

    while(args[i] != NULL)
    {
        i++;
        args[i] = strtok(NULL, " ");
    }
}

// ======================
// 仅实现 cd 内建命令
// ======================
int builtin_cd()
{
    // 如果不是 cd,返回 0,继续执行系统命令
    if (strcmp(args[0], "cd") != 0)
        return 0;

    // 是 cd,自己执行
    if (args[1] == NULL)
    {
        // 没参数 → 回到家目录
        chdir(getenv("HOME"));
    }
    else
    {
        // 有参数 → 切换到目标目录
        chdir(args[1]);
    }

    // 更新 PWD 环境变量,让提示符路径刷新
    char buf[1024];
    getcwd(buf, sizeof(buf));
    setenv("PWD", buf, 1);

    return 1;
}

void execute_cmd()
{
    if(args[0] == NULL)
        return;

    pid_t pid = fork();

    if(pid == 0)
    {
        execvp(args[0], args);
        perror("exec error");
        exit(1);
    }
    else if(pid > 0)
    {
        wait(NULL);
    }
}

int main()
{
    while(1)
    {
        interact(commandline, sizeof(commandline));
        if(strlen(commandline) == 0) continue;

        parse_command();

        // 如果是 cd,执行完直接跳过系统命令
        if(builtin_cd())
            continue;

        // 其他命令正常执行
        execute_cmd();
    }

    return 0;
}
  • cd 是内建命令,必须在 Shell 进程自身执行。
  • 通过 strcmp 判断命令是否为 cd。
  • 使用 chdir () 系统调用切换目录。
  • 处理无参数(回到家目录)和有参数两种情况。
  • 更新 PWD 环境变量,保证提示符路径刷新。
  • cd 执行完毕后,不再进入 fork/exec 流程

本次分享就到这里结束了,后续会继续更新,感谢阅读!

相关推荐
IMPYLH3 小时前
Linux 的 ls 命令
linux·运维·服务器·bash
笨笨饿3 小时前
33_顺序表(待完善)
linux·服务器·c语言·嵌入式硬件·算法·学习方法
Agent产品评测局3 小时前
企业发票管理自动化落地,验真归档全流程实现方法:2026企业级智能体选型与实测指南
运维·网络·人工智能·ai·chatgpt·自动化
wwj888wwj3 小时前
Ansible基础(复习1)
linux·运维·ansible
DYuW5gBmH3 小时前
Anthropic 开源 Bloom:基于 LLM 的自动化行为评估框架
运维·microsoft·自动化
yj_xqj3 小时前
Linux network启动报错 && nmcli 的使用
linux·运维·服务器
程序猿编码4 小时前
eBPF代理:让SSH进程“溯源”,找到背后的客户端IP
linux·tcp/ip·ssh·ebpf
Shepherd06194 小时前
【IT 实战】解决 TP-Link USB 无线网卡在 Linux/PVE 下识别为存储设备的问题
linux·运维·服务器
认真的薛薛4 小时前
GPU运维:vllm启动大模型参数解析
运维·数据库·vllm