Linux(4)(下)

https://blog.csdn.net/qscftqwe/article/details/156114541
上节课内容,大家需要的话可以点击看一看的

一.进程替换

替换原理

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

总结:

  • 进程替换不会创建新的进程(因为进程的id并未改变)。

  • 当执行任意一种exec函数时,由于该进程的代码和数据都被新进程替换,因此exec后面的代码都随着替换无法执行了,但是如果替换失败就继续执行接下来的代码

  • exec系列函数只可能失败返回(返回值-1)

  • exec系列如果一个参数是可变的那么该参数的结尾要是NULL,如果是一个参数列表则该参数列表结尾也要是NULL

    cpp 复制代码
    // 参数一个一个写,最后必须加 NULL!
    execl("/bin/ls", "ls", "-l", "/home", NULL);
    
    char *args[] = 
    {
        "ls",       // argv[0]
        "-l",       // argv[1]
        "/home",    // argv[2]
        NULL        // 必须!标记结束
    };
    execv("/bin/ls", args);

替换函数

cpp 复制代码
#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[]);

//系统函数
int execve(const char *path, char *const argv[], char *const envp[]);

总结各个参数

  • path:必须提供可执行文件的完整路径

  • file:用户输入可执行文件即可,系统会自动在 PATH 环境变量中搜索

  • char *arg:为可执行文件的命令行参数

  • char *const argv[]:为可执行文件的命令行参数列表

  • char *const envp[]:为可执行文件的环境变量列表

1.1 进程替换知识补充

xec系列调用
  • exec系列同样也可以调用自己实现重启

  • exec系列同样可以调用脚本语言

  • 脚本语言运行的本质是通过其解释器来解释其文件的代码

    • 之所以可以运行其他语言是因为这些都是进程

    • 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。

1.2 如何给子进程传环境变量

如果想给子进程传递环境变量,有几种传递

  • 一:什么都不写(直接继承父类的环境变量)

  • 二:putenv(对其进程和子进程有效)

    cpp 复制代码
    putenv("PRIVATE_ENV=66666")
    
    //在当前进程的环境变量中,添加或修改一个名为 PRIVATE_ENV 的变量,其值为 66666
  • 三:exec系列

    • myenv采用的是替换不是追加!

      cpp 复制代码
      char *const myenv[] = 
      {
          "MYVAL=111",
          "MYPATH=/usr/bin/XXX",
          NULL
      };
      execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);

      当execle替换成功后,命令行参数 是a,w,v。myeny是替换的环境变量,也就是说otherExe的环境变量只提供myenv里面的,这个很危险的!

二.简易shell

在制作这个之前,需要和大家先分享一下会用到的函数,提前讲解然后更方便大家观看代码理解意思。

函数介绍:

1.getenv

cpp 复制代码
// getenv是获取环境变量
#include <stdlib.h>
char *getenv(const char *name);
//参数:
//	name:你要查询的环境变量名(字符串)
//返回值:
//	如果找到该环境变量,返回指向其值的字符串指针
//	如果未找到,返回 NULL。

2.getcwd

cpp 复制代码
// 获取当前工作目录(Current Working Directory)的绝对路径
#include <unistd.h>

char *getcwd(char *buf, size_t size);

// 参数:
//   buf  - 存储路径的缓冲区;若为 NULL,函数自动分配内存(需 free)
//   size - 缓冲区大小(字节);若 buf 为 NULL,通常设为 0

// 返回值:
//   成功:返回指向路径字符串的指针(buf 或动态分配地址)
//   失败:返回 NULL,并设置 errno(如 ERANGE、ENOENT 等)

3.strcpy

cpp 复制代码
// 将源字符串复制到目标缓冲区(包括结尾的 '\0')
#include <string.h>

char *strcpy(char *dest, const char *src);

// 参数:
//   dest - 目标缓冲区,必须足够大以容纳 src 的全部内容(含 '\0')
//   src  - 源字符串,必须是以 '\0' 结尾的有效 C 字符串

// 返回值:
//   成功:返回 dest 的起始地址
//   注意:不检查缓冲区溢出,使用不当易导致安全问题(建议用 strncpy 或 strcpy_s 替代)

4.fflush

cpp 复制代码
// 强制将输出流的缓冲区内容立即写入目标设备(如终端、文件等)
#include <stdio.h>

int fflush(FILE *stream);

// 参数:
//   stream - 指向输出流的 FILE 指针(如 stdout、或以写/追加模式打开的文件);
//            若传入 NULL,则刷新所有输出流。

// 返回值:
//   成功:返回 0
//   失败:返回 EOF,并设置 errno(例如磁盘满、I/O 错误等)

知识补充
// 1. 全缓冲(Full buffering)
//    - 特点:缓冲区满 或 显式调用 fflush() / fclose() 时才冲刷
//    - 典型场景:普通文件(如 fopen 打开的文件)

// 2. 行缓冲(Line buffering)
//    - 特点:遇到换行符 '\n'、缓冲区满、或显式冲刷时写入
//    - 典型场景:终端设备上的 stdout(当连接到 tty 时)

// 3. 无缓冲(No buffering)
//    - 特点:每次 I/O 操作立即写入,不使用缓冲区
//    - 典型场景:stderr(默认无缓冲,确保错误信息及时输出)

fflush 不是"避免差异",而是"消除延迟",让输出行为变得确定和即时。

5.fgets

cpp 复制代码
// 从输入流中读取一行字符串(最多 n-1 个字符),并保留换行符(如果读到)
#include <stdio.h>

char *fgets(char *str, int n, FILE *stream);

// 参数:
//   str    - 指向目标缓冲区的指针,用于存储读取的字符串
//   n      - 最多读取的字符数(实际最多读 n-1 个,留一个位置给 '\0')
//   stream - 输入流(如 stdin、或以读模式打开的文件)

// 返回值:
//   成功:返回 str(即传入的缓冲区指针)
//   失败或到达文件结尾(EOF)且未读取任何字符:返回 NULL
//   注意:若读到换行符 '\n',它会被包含在结果中;字符串始终以 '\0' 结尾

6.strcspn

cpp 复制代码
// 计算字符串 str1 开头连续
// 不包含在字符串 str2 中的字符个数(即首次出现 str2 中任一字符的位置)
#include <string.h>

size_t strcspn(const char *str1, const char *str2);

// 参数:
//   str1 - 要被扫描的源字符串
//   str2 - 包含要查找的"禁止字符"集合的字符串

// 返回值:
//   返回 str1 起始位置到第一个出现在 str2 中的字符之间的字符数量;
//   如果 str1 中没有任何字符出现在 str2 中,则返回 str1 的全长(即 strlen(str1))

7.strtok

cpp 复制代码
// 将字符串 str 按照分隔符集合 delim 进行分割,每次调用返回一个子串(token)
#include <string.h>

char *strtok(char *str, const char *delim);

// 参数:
//   str   - 首次调用时传入要分割的源字符串(会修改原字符串);
//           后续调用传入 NULL,表示继续分割上次未处理完的部分。
//   delim - 包含所有分隔符字符的字符串(如 " \t\n" 表示空格、制表符、换行符为分隔符)

// 返回值:
//   成功:返回指向当前 token(子串)的指针(该子串末尾已被 '\0' 截断)
//   分割完毕或输入为空:返回 NULL
//   注意:该函数使用内部静态指针保存状态,因此不是线程安全的;
//   	原字符串会被修改(分隔符被替换为 '\0'),不能用于字符串字面量(如 strtok("a,b", ",") 是未定义行为)

8.fork(不做讲解,这个不懂要去前面看)

9.execvp

cpp 复制代码
// 执行指定名称的程序,使用当前环境变量,并自动在 PATH 中搜索可执行文件
#include <unistd.h>

int execvp(const char *file, char *const argv[]);

// 参数:
//   file  - 要执行的程序名(可以是相对路径、绝对路径或仅文件名);
//           若不含 '/',则在环境变量 PATH 列出的目录中依次查找。
//   argv  - 以 NULL 结尾的字符串数组,argv[0] 通常为程序名,后续为命令行参数。

// 返回值:
//   成功时:**不会返回**(当前进程映像被新程序替换)
//   失败时:返回 -1,并设置 errno

10,11,12(不做讲解,不懂要去前面看)

13 strcmp

cpp 复制代码
// 比较两个 C 字符串的字典序(按字符 ASCII 值逐个比较)
#include <string.h>

int strcmp(const char *s1, const char *s2);

// 参数:
//   s1 - 第一个以 '\0' 结尾的字符串
//   s2 - 第二个以 '\0' 结尾的字符串

// 返回值:
//   若 s1 == s2(内容完全相同):返回 0
//   若 s1 在字典序中小于 s2:返回一个负整数(< 0)
//   若 s1 在字典序中大于 s2:返回一个正整数(> 0)

14 chdir

cpp 复制代码
// 更改当前进程的工作目录(Current Working Directory)
#include <unistd.h>

int chdir(const char *path);

// 参数:
//   path - 目标目录的路径(可以是绝对路径或相对路径)

// 返回值:
//   成功:返回 0
//   失败:返回 -1,并设置 errno

15 setenv

cpp 复制代码
// 设置或修改环境变量的值
#include <stdlib.h>

int setenv(const char *name, const char *value, int overwrite);

// 参数:
//   name      - 环境变量名(不能包含 '=',且不应为 NULL)
//   value     - 环境变量的值(若为 NULL,则等效于设置为空字符串 "")
//   overwrite - 控制是否覆盖已存在的同名变量:
//               非 0:覆盖已有值;
//               0:若变量已存在,则不做任何操作,保持原值。

// 返回值:
//   成功:返回 0
//   失败:返回 -1,并设置 errno

代码实现

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

#define LEFT "[" // 命令提示符左侧的起始符号
#define RIGHT "]"

#define LABEL "#" // 命令提示符末尾的标识符

#define DELIM " \t\n" // 用于分割命令行参数的分隔符:空格、制表符、换行符

#define LINE_SIZE 1024 // 缓冲区长度
#define ARGC_SIZE 32   // 单条命令最多支持的参数个数(包括命令本身)
#define EXIT_CODE 127  // 标准"command not found"退出码

int lastcode = 0; // 保存上一条命令的退出状态码(exit status)
                  // 初始为 0(表示"成功"),用于支持 `echo $?` 功能

int quit = 0; // 控制主循环是否退出的标志
              // 0 表示继续运行 shell
              // 非 0(如 1)时表示收到 exit 命令或 EOF(Ctrl+D),应退出主循环

const char *getusername() // 获取执行者名称
{
    const char *user = getenv("USER"); // getenv是获取环境变量(1)
    return user ? user : "unknown";
}

const char *gethostname() // 获取主机名
{
    const char *host = getenv("HOSTNAME");
    return host ? host : "localhost";
}

void interact(char *cline, int size)
{
    char pwd[LINE_SIZE]; // 存放当前工作目录路径

    // 获取当前工作目录并存入 pwd 缓冲区;成功时返回 pwd 指针,失败返回 NULL(2)
    if (getcwd(pwd, sizeof(pwd)) == NULL)
    {
        perror("getcwd");
        strcpy(pwd, "?"); // 将pwd设置为空表示路径未知(3)
    }
    // 打印执行者,主机名,当前工作目录
    printf(LEFT "%s @%s %s" RIGHT "" LABEL " ", getusername(), gethostname(), pwd);
    fflush(stdout); // 冲刷缓冲区,避免数据在缓冲区,没有打印出来(4)

    if (fgets(cline, size, stdin) == NULL) // 从标准输入读取一行文本内容(5)
    {
        // EOF (Ctrl+D) or error(文件结束了或者输入错误)
        memset(cline, 0, size); // 设为空
        quit = 1;               // 准备退出
        return;
    }

    // 将第一个 \n 的位置替换为字符串结束符 \0,安全去除换行符(6)
    cline[strcspn(cline, "\n")] = '\0';
}

// 函数:将用户输入的一行命令字符串(cline)按空白符分割成多个参数
int splitstring(char cline[], char *_argv[])
{
    // 初始化 argv 为 NULL,防止上一次调用残留的指针被误用
    for (int i = 0; i < ARGC_SIZE; i++)
    {
        _argv[i] = NULL;
    }

    int i = 0;

    // 将字符串按指定分隔符拆分成多个"token"(片段)这个最后会自动插入\0(7)
    char *token = strtok(cline, DELIM);
    while (token != NULL && i < ARGC_SIZE - 1)
    {
        _argv[i++] = token;

        // 继续调用 strtok(NULL, ...) 获取下一个 token
        // strtok 内部会记住上次分割的位置
        token = strtok(NULL, DELIM);
    }
    _argv[i] = NULL; // 显式在末尾添加 NULL 指针,因为参数列表必须以 NULL 结尾
    return i;
}

// 函数:执行一个非内建命令(如 ls、grep 等)
// 通过创建子进程并调用 execvp(这是给程序替换函数) 来运行外部程序
void NormalExecute(char *_argv[])
{
    pid_t id = fork(); // 创建子进程(8)
    if (id < 0)
    {
        perror("fork"); // 创建失败
        return;
    }
    // 子进程
    else if (id == 0)
    {
        execvp(_argv[0], _argv); //_argv[0]是命令名,_argv是参数列表(9)

        // 注意:execvp 成功后不会返回!只有失败才会继续往下执行
        perror(_argv[0]); // 打印命令执行失败原因
        _exit(EXIT_CODE); // exit会冲刷缓冲区这样可能会影响到父进程
    }
    // 父进程
    else
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0); // 阻塞式进程等待(10)

        // 检查是否成功回收目标子进程
        if (rid == id)
        {
            // 判断子进程是否正常退出(即调用了 exit 或 main 返回)(11)
            if (WIFEXITED(status))
            {
                // 提取子进程的实际退出码(0~255)(12)
                // 保存到全局变量 lastcode,供后续 echo $? 使用
                lastcode = WEXITSTATUS(status);
            }
            else
            {
                lastcode = 1; // 被信号终止(采取简化处理统一退出码为1)
            }
        }
    }
}

// 函数:判断并执行内建命令(如 cd、export、echo、exit 等)
int buildCommand(char *_argv[], int _argc)
{
    if (_argc == 0 || _argv[0] == NULL)
    {
        return 1; // 空命令,视为内建(不执行外部程序)
    }

    // cd
    // strcmp(_argv[0], "cd")判断_argv[0]是否等于cd(13)
    if (_argc >= 1 && strcmp(_argv[0], "cd") == 0)
    {
        // 如果cd无参(那么_argc的大小就为1,默认为家目录)
        // 如果有参_argc大小为2,那么_argv[1]则为参数
        const char *path = (_argc == 2) ? _argv[1] : getenv("HOME");

        // chdir 是一个改变当前工作目录,成功为0,失败为-1(14)
        if (chdir(path) == 0) 
        {
            // 切换成功:更新环境变量 PWD(当前工作目录)
            char pwd[LINE_SIZE];
            if (getcwd(pwd, sizeof(pwd)))
            {
                // 将当前进程的环境变量 PWD的值设置为 pwd 所指向的字符串值(15)
                // 并且如果 PWD的值存在,则强制覆盖它
                setenv("PWD", pwd, 1);
            }
            //注意前者chdir是改真实的,setenv是改给我看的
        }
        else
        {
            // 切换失败
            perror("chdir");
        }
        return 1;
    }
    // 内建命令:export
    //  支持两种形式:
    //    export VAR=value     → 设置新环境变量
    //    export VAR           → 将已存在的 VAR 导出到环境(简化版)
    if (_argc == 2 && strcmp(_argv[0], "export") == 0)
    {
        // 在_argv[1] 这个字符串中查找第一个出现的 '=' 字符,并返回指向它的指针
        char *eq = strchr(_argv[1], '=');
        if (eq)
        {
            // 是第一种形式

            // 临时将 '=' 替换为 '\0',把字符串拆成两部分
            *eq = '\0'; // 这是修改字符串 Var\0value

            //_argv[1] = Var
            // eq+1 = value
            // 设置环境变量 VAR=value(若已存在则覆盖旧值)
            setenv(_argv[1], eq + 1, 1);

            *eq = '='; // 恢复字符串把'\0'改成=
        }
        else
        {
            // 是第二种形式
            if (getenv(_argv[1]) == NULL)
            {
                //如果不存在,那就创造一个,用""字符作为其值,符合bash操作
                setenv(_argv[1], "", 1);
            }
        }
        return 1;
    }

    // echo
    if (_argc >= 1 && strcmp(_argv[0], "echo") == 0)
    {
        // 仅支持简单形式如 $PATH,不支持 ${} 或 $$ 等扩展
        if (_argc == 2 && strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
        }

        // 特殊情况:echo $XXX(尝试打印环境变量)
        else if (_argc == 2 && _argv[1][0] == '$')
        {
            const char *name = _argv[1] + 1; // 跳过 '$
            const char *val = getenv(name);  // 获取环型变量
            if (val)
            {
                printf("%s\n", val);
            }
            else
            {
                printf("\n"); // 变量未定义,输出空行
            }
        }
        else
        {
            // 支持多参数 echo(简单版)
            for (int i = 1; i < _argc; i++)
            {
                printf("%s", _argv[i]);
                if (i < _argc - 1)
                    printf(" ");
            }
            printf("\n");
        }
        lastcode = 0; // 简易版:所有 echo 成功后 $? 都为 0
        return 1; // echo 是内建命令,已处理
    }

    // exit
    if (_argc == 1 && strcmp(_argv[0], "exit") == 0)
    {
        quit = 1;
        return 1;
    }
    return 0;//语法要求
}

int main()
{
    char commandline[LINE_SIZE];
    char *argv[ARGC_SIZE];

    while (!quit)
    {
        interact(commandline, sizeof(commandline));
        if (commandline[0] == '\0')
            continue;

        int argc = splitstring(commandline, argv);
        if (argc == 0)
            continue;

        int is_builtin = buildCommand(argv, argc);
        if (!is_builtin)
        {
            NormalExecute(argv);
        }
    }

    return 0;
}

三.shell补充

在继续学习新知识前,我们来思考函数和进程之间的相似性,然后再看看下图

实现简易shell代码中的绝大部分函数我已经标记出来,在上面给大家分享了如何使用,然后大家就根据我分享的和代码的注释来理解这个shell实现,可以加深我们对之前学习的函数进行一个复习的作用,然后理解代码也可以提升我们的理解程度。

相关推荐
敲敲了个代码5 小时前
从硬编码到 Schema 推断:前端表单开发的工程化转型
前端·javascript·vue.js·学习·面试·职场和发展·前端框架
Shanxun Liao6 小时前
Cenots 7.9 配置多台 SSH 互信登陆免密码
linux·运维·ssh
j_xxx404_6 小时前
Linux:第一个程序--进度条|区分回车与换行|行缓冲区|进度条代码两个版本|代码测试与优化
linux·运维·服务器
looking_for__7 小时前
【Linux】Ext系列文件系统
linux
我命由我123457 小时前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
OliverH-yishuihan7 小时前
开发linux项目-在 Windows 上 基于“适用于 Linux 的 Windows 子系统(WSL)”
linux·c++·windows
Fern_blog9 小时前
鸿蒙学习之路
学习
南棱笑笑生9 小时前
20251224给飞凌OK3588-C开发板适配Rockchip原厂的Buildroot【linux-6.1】系统时确认ssh服务【内置dropbear】
linux·c语言·ssh·rockchip
I · T · LUCKYBOOM9 小时前
30.Firewalld-Linux
linux·运维·安全