从exec到Shell:深度解析Linux进程等待,程序替换与自主Shell实现

一、进程等待

1. 进程等待的必要性

这个前面是说过的哦。

1、回收子进程资源

2、获取子进程的退出信息

2. 什么是进程等待

让父进程通过等待的方式,回收子进程的PCB,Z,如果需要,获取子进程的退出信息

3. 怎么做

复制代码
//等待任意一个子进程,成功的话返回子进程的pid,失败返回-1
pid_t wait(int* status);//这里status暂时设置为NULL,不关心

父进程调用wait,表示父进程等待任意一个子进程

1、如果子进程没有退出,父进程wait的时候,就会阻塞

2、如果子进程退出了,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题



通过这段代码,就可以验证父进程等待子进程是否成功,成功的话子进程的僵尸状态就没有了。子进程会从阻塞状态变为僵尸状态,最后被父进程回收。

上面只回收了一个子进程,如何回收多进程呢?

多进程中,父进程往往最先创建,最后退出。fork之前,只有一个父进程,fork之后,父进程需要对所有的子进程进行回收。

复制代码
//pid为-1,表示等待任意进程,pid > 0,等待进程id与pid相等的进程
//status输出型参数,目的是为了带出一些数据
//options为0,阻塞等待,options为WNOHANG,非阻塞等待
//等待成功,返回子进程的pid,等待失败,返回0
//既没有等待成功也没有失败返回0(子进程在运行,暂时未退出)
pid_t waitpid(pid_t pid, int* status, int options);


子进程的退出码为1,可是status为什么是256呢?这里的status不仅仅是退出码。

status是一个int(整型),32个bit,高16位不考虑 。剩下的16位,次8位是子进程的退出码,所以,进程的退出码取值范围是[0,255]低7位是终止信号,还有一位是core dump(这个后面再讲)。

进程正常终止,终止信号为0,子进程退出码为1,但是后面有8个0,所以status为256。要想拿到子进程的退出码exit_code = (status >> 8) & 0xFF

上面所述都是基于进程正常终止的情况。那么,如果是进程异常呢?我们都知道,进程异常了,退出码是没有意义的。因此,我们的重点应该是进程为什么会异常

是因为程序出现了问题,导致OS给你的进程发送信号了

复制代码
kill -l //查看所有的进程信号

退出信号没有0号信号 。所以我们是如何判断进程是否是正常运行结束呢?status->信号的数字 == 0

我们用两个数字来表示子进程的执行情况

1.进程的退出码

2.进程的退出信号

那我们要如何拿到进程的退出信号呢 ?status低7位表示的是进程信号,exit_signal = status & 0x7F

但是获取进程退出码,进程退出信号的方式太麻烦,所以操作系统提供了两个宏,WIFEXITED(status),WEXITSTATUS(status),分别用来表示进程是否正常退出(正常退出返回非零值,反之,返回0)和获取进程退出状态码

不知道大家有没有疑问呢。waitpid(pid_t pid, int* status, int options)中 pid > 0表示的是等待与pid相等的子进程,是因为父进程可以拿pid找到该子进程

可是,如果是-1呢?父进程怎么知道要等待哪一个子进程。那是因为父进程的task_struct里有对于子进程进行管理,等待子进程就看哪一个是僵尸状态就可以了

我们可以通过控制代码来验证进程等待的效果。

上面所说都是阻塞等待。现在,该解释什么是非阻塞等待了。

像scanf函数就是典型的阻塞等待,资源没有就绪,就会一直卡住。而非阻塞等待,就是资源没有就绪的情况,它并不会一直卡在哪里,会去做别的事情的同时也会继续等待资源就绪

例子:明天就是C语言考试,你的好朋友李四是一个学霸,你需要借助李四的笔记来复习应对明天的考试,但是李四也要用笔记来复习。但是他说等他复习好了,就可以把笔记借给你。这时候你是很开心的,但是你也很慌张,因为平时你没有好好学习。所以你就不停的给李四打电话,问他复习好了吗?他说没有,这时候你就把电话挂断了,你就去刷抖音了,刷大学生期末挂科应该怎么办?过了一会儿,你又给李四打电话,李四说还没好呢,你又去做自己的事情了。

这就是非阻塞等待。有时候你会听到非阻塞等待比阻塞等待更高效就是这个原因

二、进程程序替换

fork之后,父子进程各自执行父进程代码的一部分,那如果子进程就想执行一个全新的程序呢

进程的程序替换来完成这个功能

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中

1. 初识程序替换函数

复制代码
//pathname 你想执行的程序(路径+文件名)
//arg 我们要执行程序的程序名称
//... 给程序传递的命令行选项,必须以NULL结尾(参数包)
//失败了,返回-1
int execl(const char* pathname, const char* arg, ...);


可以看到,程序是替换成功了的。但是,有一个问题,execl 结束之后,为什么没有打印后续的 printf 呢

那是因为你的进程已经执行另一个程序的代码了,你自己的代码已经没有了

总结程序替换函数,一旦调用成功,后续代码不再执行,因为已经没有了

那如果失败呢?

程序替换,如果成功,不需要也不会有返回值,失败返回 -1

也就是说exe系列的函数,只要返回,必然失败

那么,如果我们想用子进程进行程序替换呢?父子进程代码是共享的,数据以写时拷贝的方式各自私有。如果用子进程程序替换,是不是也影响到了父进程呢?不是说好进程之间具有独立性吗

我们可以认为,fork之后,父子进程的代码和数据都以写时拷贝的方式各自私有。这样就不会影响父进程了,子进程会加载新的代码和数据(物理内存上新开一段空间),更改页表与物理内存的映射关系

2. 关联linux历史知识

1、命令行上的命令是bash的子进程,那它和bash是共享代码和数据的,但是不同的命令有不同的功能。这是为什么呢

我们已经知道,子进程是由父进程fork创建出来的,那么父进程不就可以对子进程进行程序替换,进程等待...等操作了吗

2、二进制文件,先加载程序到内存,为什么呢

由冯诺依曼体系结构决定的

进程 = PCB(内核数据结构)+ 自己的代码和数据

我们都知道,进程是先有数据结构的,在加载代码和数据,甚至是需要的时候在加载(惰性加载),那么你的程序,是如何加载到内存的呢?

加载的本质是为了变成进程 ,那么是怎么加载的呢?我们使用的exe系列的函数不就是相当于一种"加载器"吗?程序从磁盘上拷贝到内存,不就是硬件到硬件吗,只有操作系统有这个权利,所以加载一定要调用系统调用或者是对系统调用做封装

3. 详解程序替换函数

复制代码
//pathname 依旧是路径+文件名
//argv[] 与execl后半部分的参数一致,只不过是用数组组织起来
int execv(const char* pathname, char* const argv[]);

子进程执行程序替换,我们不需要自己的可执行程序名,因此从命令行参数表下标为1的参数开始

复制代码
//执行指定的命令,需要让execlp自己在环境变量PATH中寻找指定的程序
int execlp(const char* file, const char* arg, ...);


复制代码
int execvp(const char* file, char* const argv[]);


上面执行的都是系统命令,那么,可不可以执行我们自己的命令呢?

execl 执行 mycmd的时候,之所以第二个参数不用带./是因为第一个参数已经表明了路径系统已经能够找到该文件了

复制代码
//argv[]命令行参数表
//envp[]环境变量表
//这两张表可以是系统的,也可以自定义
int execvpe(const char* file, char* const argv[], char* const envp[]);



execvpe函数传递环境变量表,会默认摒弃旧的环境变量,使用你自己设置的全新的环境变量表,如果你要用系统提供的,就传入系统的环境变量表。

那如果我们既想使用系统的环境变量表又想使用自定义的呢?

复制代码
//成功返回0,失败返回非0
int putenv(char* string);//默认的环境变量中新增一项


程序替换函数一共有7个。这六个都是execve衍生出来的。剩下的就不多做介绍了。

三、mini shell

myshell.h

c 复制代码
#ifndef _SHELL_H_
#define _SHELL_H_

#include<cstdio>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<ctype.h>


#define MAX 1024

#define ARGS 64

#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3


void InitGlobal();
void PrintCommandLinePrompt();
bool GetUserCommand(char usercommand[], int len);
void CheckRedir(char usercommand[]);
bool PraseUserCommand(char usercommand[]);
bool BuiltInCommandExec();
bool ForkAndExec();
#endif

myshell.cc

c 复制代码
#include "myshell.h"

int gargc = 0; //解析用户命令存储的数组下标
char* gargv[ARGS] = {NULL};//故意设置为全局的,方便使用,安全起见,保证命令行参数表全局有效
int exit_code = 0;
char pwd[MAX] = {0};
int redir;
std::string filename;


void InitGlobal()
{
	gargc = 0;
	memset(gargv,0,sizeof(gargv));
	redir = NONE_REDIR;
	filename = "";

}

static std::string GetUserName()
{
	std::string username = getenv("USER");
	return username.empty()? "None": username;
}

static std::string GetHostName()
{
	char name[256] = {0};
	if(gethostname(name, sizeof(name)) == 0)
	{
		std::string hostname = name;
		return hostname;
	}
	else
	{
		perror("gethostname failed");
		return "None";
	}
}

static std::string GetPwd()
{
	//std::string pwd = getenv("PWD");
	//return pwd.empty()? "None": pwd;
	char temp[MAX];
	char* ptr = getcwd(temp,sizeof(temp));
	
	//将ptr指针里面的内容和PWD=(这四个字符)一起写到pwd数组里,putenv会在环境变量中查找PWD,然后进行覆盖。
	snprintf(pwd, sizeof(pwd), "PWD=%s", ptr);
	putenv(pwd);
	std::string str = temp;
	std::string delim = "/";
	int pos = str.rfind(delim);
	std::string s = str.substr(pos+delim.size());
	return s.empty()? "/": s;

}

std::string GetHomePath()
{
	std::string home = getenv("HOME");
	return home.empty()? "/": home;
}


void PrintCommandLinePrompt()
{
	std::string username = GetUserName();
	std::string hostname = GetHostName();
	std::string pwd = GetPwd();

	printf("[%s@%s %s]$ ",username.c_str(), hostname.c_str(), pwd.c_str());
}



bool GetUserCommand(char usercommand[], int len)
{
	if(usercommand == NULL || len <= 0)
		return false;
	//fgets函数会自动在字符串的末尾加上'\0'
	char* res = fgets(usercommand, len, stdin);
	if(res == NULL)
	{
		perror("fgets failed");
		return false;
	}
	//去掉末尾的换行符(\n)
	usercommand[strlen(usercommand) - 1] = 0;
	return strlen(usercommand) == 0? false: true;
}


#define TrimeSpace(start) do{\
	while(isspace(*start)){\
		start++;\
	}\
}while(0)


//检查是否有重定向
void CheckRedir(char usercommand[])
{
	char* start = usercommand, *end = usercommand + strlen(usercommand) - 1;
	while(start <= end)
	{
		if(*start == '>')
		{
			*start = '\0';
			if(*(start+1) == '>')
			{
				redir = APPEND_REDIR;
				start += 2;
				TrimeSpace(start);
				filename = start;
				break;
			}
			else
			{
				redir = OUTPUT_REDIR;
				start += 1;
				TrimeSpace(start);
				filename = start;
				break;
			}
		}
		else if(*start == '<')
		{
			redir = INPUT_REDIR;
			*start = '\0';
			start++;
			TrimeSpace(start);
			filename  = start;
			break;
		}
		else
		{
			start++;
		}
	}
}


bool PraseUserCommand(char usercommand[])
{
	if(usercommand == NULL)
		return false;
	//解析
	//"ls -l -s" -------->  "ls", "-l", "-a"
#define SEP " " 	
	gargv[gargc++] = strtok(usercommand, SEP);
	//最后一次解析失败,会存储NULL,但是我们不需要NULL
	char* str = strtok(NULL,SEP);
	while(str)
	{
		gargv[gargc++] = str;
		str = strtok(NULL, SEP);
	}
//#define DEBUG
#ifdef DEBUG
	printf("gargc:%d\n",gargc);
	for(int i = 0; i < gargc; ++i)
		printf("gargv[%d]:%s\n",i,gargv[i]);
#endif
	return true;
}


bool BuiltInCommandExec()
{
	std::string cmd = gargv[0];
	bool ret = false;
	if(strcmp(cmd.c_str(), "cd") == 0)
	{
		if(gargc == 2)
		{
			char* str = gargv[1];
			if(strcmp(str, "~") == 0)
			{
				ret = true;
				const char* home = GetHomePath().c_str();
				chdir(home);
			}
			else
			{
				ret = true;
				chdir(str);
			}
		}
		else if(gargc == 1)
		{
			ret = true;
			chdir(GetHomePath().c_str());
		}
		else
		{
			//TODO
		}
	}
	else if(cmd == "echo")
	{
		if(gargc == 2)
		{
			std::string args = gargv[1];
			if(args[0] == '$')
			{
				if(args[1] == '?')
				{
					ret = true;
					printf("%d\n",exit_code);
					exit_code = 0;
				}
				else
				{
					const char* name = &args[1];
					printf("%s\n",getenv(name));
					ret = true;
				}
			}
			else
			{
				ret = true;
				printf("%s\n",args.c_str());
			}
		}
	}
	return ret;
}

bool ForkAndExec()
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		return false;
	}
	else if(id == 0)
	{
		//更改文件描述符指向的文件,从而让子进程只需要执行系统命令就可以达到重定向的功能
		if(redir == OUTPUT_REDIR)
		{
			int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
			dup2(output, 1);
		}
		else if(redir == INPUT_REDIR)
		{
			int input = open(filename.c_str(), O_RDONLY);
			dup2(input, 0);
		}
		else if(redir == APPEND_REDIR)
		{
			int append = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND);
			dup2(append, 1);
		}
		else
		{
			//nothing to do
		}

		execvp(gargv[0],gargv);
		exit(0);
	}
	else
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, 0);
		if(rid > 0)
		{
			//exit_code = WIFEXITED(status);
			exit_code = WEXITSTATUS(status);
		}
	}
	return true;
}

main.cc

c 复制代码
#include"myshell.h"

int main()
{
	char UserCommand[MAX];
	while(true)
	{
		//打印命令行提示符
		PrintCommandLinePrompt();

		//获取用户输入的命令
		if(!GetUserCommand(UserCommand, sizeof(UserCommand)))
			continue;

		InitGlobal();
		//printf("echo %s\n",UserCommand);
		
		CheckRedir(UserCommand);

		//解析用户命令
		PraseUserCommand(UserCommand);

		//检查内建命令
		if(BuiltInCommandExec())
			continue;

		//执行命令,不能让父进程自己去执行程序替换,否则父进程程序替换之后就结束了,而shell是一个死循环软件,应该让子进程去执行
		ForkAndExec();
	}
	return 0;
}

Makefile

c 复制代码
myshell:myshell.cc main.cc
	g++ -o $@ $^ -g
.PHONY:clean
clean:
	rm -f myshell

运行结果:

至此,我们简易版的mini_shell就完成了。觉得不错的小伙伴给个一键三连吧。

相关推荐
超级大只老咪4 小时前
快速进制转换
笔记·算法
嵩山小老虎5 小时前
Windows 10/11 安装 WSL2 并配置 VSCode 开发环境(C 语言 / Linux API 适用)
linux·windows·vscode
Fleshy数模5 小时前
CentOS7 安装配置 MySQL5.7 完整教程(本地虚拟机学习版)
linux·mysql·centos
a41324475 小时前
ubuntu 25 安装vllm
linux·服务器·ubuntu·vllm
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.7 小时前
Keepalived VIP迁移邮件告警配置指南
运维·服务器·笔记
一只自律的鸡7 小时前
【Linux驱动】bug处理 ens33找不到IP
linux·运维·bug
17(无规则自律)7 小时前
【CSAPP 读书笔记】第二章:信息的表示和处理
linux·嵌入式硬件·考研·高考
!chen7 小时前
linux服务器静默安装Oracle26ai
linux·运维·服务器
ling___xi8 小时前
《计算机网络》计网3小时期末速成课各版本教程都可用谢稀仁湖科大版都可用_哔哩哔哩_bilibili(笔记)
网络·笔记·计算机网络
REDcker8 小时前
Linux 文件描述符与 Socket 选项操作详解
linux·运维·网络