Linux修炼:进程控制(二)
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》
感谢您打开这篇博客!希望这篇博客能为您带来帮助,也欢迎一起交流探讨,共同成长。
目录
1、进程替换
1.1、替换原理
1.2、替换方式
1.2.1、execl
1.2.2、execl
1.2.3、execlp
1.2.4、execvp
1.2.5、execvpe
2、简易Shell命令行解释器
1、进程替换
在之前的学习中,我们通过fork创建的子进程,执行的仍然是父进程代码的一部分,那么有没有什么办法,让fork创建的子进程,执行全新的一段代码呢?这时候,就需要我们今天要介绍的进程替换了。
1.1、替换原理
进程程序替换是指在一个正在运行的进程中,将当前执行的程序完全替换为另一个新程序。替换后,原进程的代码段、数据段、堆栈等内存空间会被新程序覆盖,但进程的PID、文件描述符等资源保持不变。新程序从它的main函数开始执行,原程序的执行逻辑被彻底终止。
说通俗一点,就是把虚拟内存通过页表映射的物理内存中的代码段和数据段,进行替换,替换为要执行的程序。
1.2、替换方式
我们可以通过exec系列的函数进行替换。用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程 序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
1.2.1、execl
我们可以通过execl函数来进行替换:
int execl(const char *path, const char *arg0, ..., (char *)0);
其中,第一个参数为想要替换的文件的路径(你要执行谁),剩下的为可变参数(你想怎么执行)。可变参数部分为给程序传递的命令行选项。可变参数部分必须以NULL结尾。
我们来使用一下,把进程替换为linux系统中的ls命令:
#include<iostream>
#include<unistd.h>int main()
{printf("这是一个进程:%d\n",getpid());//替换sleep(1);execl("/usr/bin/ls","ls","-a","-l","-n",NULL); printf("运行结束\n");return 0;
}
执行结果和直接执行ls命令的结果相同。
程序替换一旦替换成功,后续代码便不再执行。
exe系列的函数,如果执行成功,便没有返回值,如果失败,返回值为-1。
进程之间具有独立性,所以说当子进程被替换后,不会影响父进程。
我们都知道代码编译之后要通过加载器加载到内存,那么其实这个加载器本质上还是对操作系统程序替换接口的封装。通过替换的方式,可以把磁盘中的可执行代码替换到内存中。
1.2.2、execl
我们接下来介绍一下exe系列的其他接口:
int execv(const char *path, char *const argv[]);
这个接口和execl没有本质区别,只是传参方式不同而已。同样的,argv这个数组也必须以NULL结尾。
现在我们可以通过这个接口实现一个简易的加载器,只要运行这个加载器,我们就可以把特定的文件加载到内存,换句话说,我们就可以执行这个可执行文件。
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main(int argc,char* argv[])
{pid_t pid;pid=fork();if(pid<0){fprintf(stderr,"Fork failed\n");exit(EXIT_FAILURE);}else if(pid==0){char** myargv=&argv[1];execv(myargv[0],myargv);}return 0;
}
1.2.3、execlp
int execlp(const char *file, const char *arg, ..., (char *) NULL);
这个接口与之前接口的最大区别是,我们不需要传入绝对或相对路径了,我们只需要传入文件(可执行文件)名字就可以了。执行指定的命令中,让execlp会自己在环境变量PATH中寻找指定的程序。
1.2.4、execvp
int execvp(const char *file, char *const argv[]);
execvp与execv的区别和execlp和execl的区别是一样的,我们都不需要传入文件路径,只需要文件名即可。即原来传路径的参数变成传想要执行的文件名。
1.2.5、execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
execvpe 与其他 exec函数的主要区别在于它可以接受一个自定义的环境变量数组envp,而不是继承调用进程的环境变量。这使得它可以更灵活地控制子进程的环境。
现在我们使用一下execvpe:
首先我们新建一个文件mycmd,我们通过mycmd实现打印环境变量的效果:
#include<iostream>
#include<cstdio>int main(int argc,const char *argv[],const char* env[])
{int i=0;for(;i<argc;i++){printf("argv[%d]:%s\n",i,argv[i]);//打印出进程的所有命令行参数}for(i=0;;i++){if(env[i]==NULL) return 0; printf("env[%d]:%s\n",i,env[i]);}return 0;
}
接着我们在执行另一个文件myexec,运行之后会创建新进程并用mycmd替换掉子进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{pid_t pid;pid=fork();if(pid<0){fprintf(stderr,"Fork failed\n");exit(EXIT_FAILURE);}else if(pid==0){char* const myargv[]={(char* const)"mycmd",NULL};char* const myenv[]={(char* const)"abcd",(char* const)"world",NULL};execvpe("./mycmd",myargv,myenv);}return 0;
}
我们总结一下exec系列函数的特点与区别:

只有execve是系统调用,其他的几个函数最终都调用了execve。
2、简易Shell命令行解释器
子进程的作用有两个:
(1)让子进程执行父进程的代码。
我们简单说几个典型的应用场景:多任务并行处理,进程池模式,安全沙箱环境,热更新机制,批量数据处理...
(2)让子进程执行全新的程序
基于此我们可以实现接下来我们要介绍的命令行解释器myshell,除此之外,还有许多应用场景,比如,服务器管理,插件系统,安全沙箱
我们采用C语言和C++混编的方式,首先我们创建好myshell.cc,main.cc,myshell.h三个文件,其中.cc结尾的文件是C++语言源文件,在Linux系统上我们偏好以.cc为后缀的文件作为C++代码源文件,而在windows系统上我们源文件的后缀偏好以.cpp为结尾,这两个没有本质区别。

首先,我先给出myshell.h和main.cc两个文件,因为我们的重点是myshell.cc文件,在这个文件中我们实现了简易的命令行解释器需要用到的几个接口。
myshell.h:
#ifndef __MYSHELL_H__
#define __MYSHELL_H__#include<stdio.h>#define ARGS 64
//讲解
extern char* gargv[];void Debug();
void InitGlobal();
void PrintfCommandPrompt();
bool GetCommandString(char cmd_str_buff[],int len);
bool ParseCommandString(char cmd[]);
void ForkAndExec();
bool BuiltInCommandExec();
#endif
我们简单介绍一下ifdef和ifndef:
#ifndef 和 #ifdef 是 C/C++ 中的预处理指令,用于条件编译。它们允许在编译时根据条件决定是否包含某段代码,常用于头文件保护和功能开关控制。
ifdef 的作用
#ifdef 用于检查某个宏是否已定义。如果宏已定义,则编译其后的代码块;否则跳过。
#ifdef MACRO_NAME// 如果 MACRO_NAME 已定义,则编译此部分代码
#endif
常见用途
- 检查平台或编译器特性(如
_WIN32、__linux__)。 - 功能模块的开关控制(如
ENABLE_DEBUG)。
ifndef 的作用
#ifndef 是 #ifdef 的反向操作,检查宏是否未定义。如果未定义,则编译其后的代码块;否则跳过。
#ifndef MACRO_NAME// 如果 MACRO_NAME 未定义,则编译此部分代码
#endif
常见用途
- 头文件保护(防止重复包含)。
- 默认配置的兜底逻辑。
示例(头文件保护)
// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H// 头文件内容
#endif
当然,如果我们用C++写的话头文件保护可以用#pragma once。
两者的区别
| 指令 | 条件 | 典型用途 |
|---|---|---|
#ifdef | 宏已定义时编译 | 功能开关、平台检测 |
#ifndef | 宏未定义时编译 | 头文件保护、默认配置 |
接着我们看一下main.cc,由于真正的软件都是持续运行的,我们不能让这个命令行解释器一运行就结束所以我们把他写成死循环,main函数中循环执行以上几个函数:
#include"myshell.h"#define SIZE 1024int main()
{char commandstr[SIZE];while(true){//初始化操作InitGlobal();//输出命令行提示符PrintfCommandPrompt();//获取用户输入的命令if(!GetCommandString(commandstr,SIZE))continue;//解析命令ParseCommandString(commandstr);//如果是内建命令(如cd)要让shell自己执行if(BuiltInCommandExec()) {continue;}//printf("echo %s\n",commandstr);//执行命令ForkAndExec();}
}
然后我们分别讲解一下这几个函数:
第一个是初始化函数,目的是把我们定义的全局变量初始化:
//命令行参数表
char* gargv[ARGS]={NULL};
int gargc=0;
int lastcode=0;
char pwd[1024];void InitGlobal(){gargc=0;memset(gargv,0,sizeof(gargc));
}
第二个函数是打印命令行提示符:
static std::string GetUserName()
{//static修饰私有函数,限制本函数只能在该文件内使用 std::string username=getenv("USER");if(username.empty()){return "None";}else return username;
}static std::string GetHostName()
{std::string hostname=getenv("HOSTNAME");return hostname.empty()?"None":hostname;
}
static std::string GetPwd()
{//std::string pwd=getenv("PWD");//return pwd.empty()?"None":pwd;//使用getcwd获取当前的工作路径,并更新环境变量//?char temp[1024];getcwd(temp,sizeof(temp));snprintf(pwd,sizeof(pwd),"PWD=%s",temp);putenv(pwd);//截取当前文件夹名std::string pwd_label=temp;const std::string pathsep="/";auto pos=pwd_label.rfind(pathsep);if(pos==std::string::npos){return "None";}pwd_label=pwd_label.substr(pos+pathsep.size());return pwd_label.empty()?"/":pwd_label;
}void PrintfCommandPrompt()
{//这里用的环境变量,也可以用系统调用来获取std::string user=GetUserName();std::string hostname=GetHostName();std::string pwd=GetPwd();printf("[%s@%s %s]#",user.c_str(),hostname.c_str(),pwd.c_str());
}
我们分别封装三个函数,GetUserName,GetHostName,GetPwd来获取用户名,主机名,当前工作目录,然后打印在终端。前两个函数分别调用接口通过环境变量来获取,第三个接口由于执行cd命令,环境变量不会同步进行改变(因为我们用的是myshell的环境变量表,环境变量表需要myshell自己维护),所以我们调用getcwd,并且手动更新环境变量表。
以上三个函数都用static修饰,目的是限制函数的使用范围,让函数只能在本文件中使用。
第三个操作是读取输入,我们使用fgets来读取,并且把读取到的字符串的最后一个字符'\n'改为'\0'。
bool GetCommandString(char cmd_str_buff[],int len)
{if(cmd_str_buff==NULL||len<=0) return false;char* res=fgets(cmd_str_buff,len,stdin);if(res==NULL){return false;}cmd_str_buff[strlen(cmd_str_buff)-1]='\0';return true;
}
第四个操作是解析命令,我们需要把命令中的各个选项解析到数组中,方便后续调用exec系列接口。
bool ParseCommandString(char cmd[]) {//"ls -a "->"ls" "-a"//第一次从头,把截取的最后那个地方设置为\0,第二次传null,原理就是把空格设置为\0if(cmd==NULL)return false;
#define SEP " "gargv[gargc++]=strtok(cmd,SEP);//整个数组,最后以NULL结尾while((bool)(gargv[gargc++]=strtok(NULL,SEP)));//回退一下。命令行参数的格式,减到null,即gargc等于实际的元素个数(除最后一个null)gargc--;#ifdef DEBUGprintf("gargc:%d\n",gargc);for(int i=0;i<gargc;i++) {printf("gargv[%d]:%s\n",i,gargv[i]);}
#endif return true;
}
我们使用strtok切割字符串:
strtok 函数概述
strtok 是 C 标准库中的一个字符串分割函数,用于将字符串按照指定的分隔符拆分成多个子字符串(标记)。该函数定义在头文件 <string.h> 中。
函数原型
char *strtok(char *str, const char *delimiters);
- str: 待分割的字符串,首次调用时传入需要分割的字符串,后续调用传入
NULL。 - delimiters: 分隔符字符串,包含所有可能的分隔字符。
工作原理
strtok 通过修改原始字符串来实现分割,每次调用会在分割位置插入 \0(字符串终止符),并返回当前标记的起始地址。首次调用时传入待分割字符串,后续调用传入 NULL 以继续分割同一字符串。
第五个函数,我们创建子进程,通过进程替换接口执行命令:
void ForkAndExec()
{pid_t id=fork();if(id<0){perror("fork");}else if(id==0){//子进程execvp(gargv[0],gargv);exit(0);}else{//父进程等待子进程int status=0;pid_t rid=waitpid(id,&status,0);if(rid>0){lastcode=WEXITSTATUS(status);}}
}
我们执行命令,并将子进程的退出码记录到lastcode这个全局变量中。
第六个函数,我们处理一下内建命令(如cd,echo)的执行操作。这类命令由Shell自身实现,通常用于执行基础的系统操作或控制Shell行为,例如环境变量管理、目录切换等。由于内建命令需要由myshell执行而不能让子进程执行,所以我们得单独写个函数:
bool BuiltInCommandExec()
{std::string cmd=gargv[0];bool ret=false;if(cmd=="cd"){if(gargc==2){//gargc是最后一个元素的下标ret=true;std::string target=gargv[1];if(target=="~"){ret=true;chdir(GetHomePath().c_str());}else{ret=true;chdir(gargv[1]);}}else if(gargc==1) {ret=true;chdir(GetHomePath().c_str());}else{//doNothing}}else if(cmd=="echo"){ if(gargc==2){std::string args=gargv[1];if(args[0]=='$'){if(args[1]=='?'){printf("%d\n",lastcode);lastcode=0;ret=true;}else{const char* name=&args[1];printf("%s\n",getenv(name));lastcode=0;ret=true;}}else{printf("%s\n",args.c_str());lastcode=0;ret=true;}}}return ret;
}
好了,今天的内容就分享到这,我们下期再见!

