从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就完成了。觉得不错的小伙伴给个一键三连吧。

相关推荐
干饭小白9 分钟前
ffmpeg使用流程
笔记
coder_lorraine16 分钟前
【Linux系列】Linux Snap 安装与使用指南:高效管理应用的神器
linux·运维
LLLLYYYRRRRRTT20 分钟前
9. Linux 交换空间管理
linux·数据库·redis
zhuyan10831 分钟前
【ROS2】常用命令
linux·运维·服务器
DARLING Zero two♡41 分钟前
【Linux操作系统】简学深悟启示录:进程初步
linux·运维·服务器
努力一点9481 小时前
ubuntu22.04系统实践 linux基础入门命令(三) 用户管理命令
linux·运维·服务器·人工智能·ubuntu·gpu算力
chennalC#c.h.JA Ptho1 小时前
iPad os
经验分享·笔记·架构·电脑
明月清了个风1 小时前
工作笔记-----IAP的相关内容
arm开发·笔记·iap·嵌入式软件·程序升级
Virgil1392 小时前
【DL学习笔记】各种卷积操作总结(深度可分离、空洞、转置、可变形)
笔记·深度学习·学习
打不了嗝 ᥬ᭄2 小时前
进程间通信
linux·运维·服务器