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(对其进程和子进程有效)
cppputenv("PRIVATE_ENV=66666") //在当前进程的环境变量中,添加或修改一个名为 PRIVATE_ENV 的变量,其值为 66666三:exec系列
myenv采用的是替换不是追加!
cppchar *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实现,可以加深我们对之前学习的函数进行一个复习的作用,然后理解代码也可以提升我们的理解程度。


