【Linux】进程替换与shell的模拟实现

目录

一、进程替换

1.1 进程替换的概念

1.2 替换函数

二、命令行解释器-Shell

2.1 shell的实现与运行

2.2 步骤讲解


一、进程替换

1.1 进程替换的概念

当我们使用 fork 函数创建子进程后,父子进程各自执行父进程代码的一部分。那如果创建的子进程想要执行一个全新的程序呢?那我们便可以通过进程替换来实现该功能。

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

子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新程序的启动历程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

1.2 替换函数

其中有六中以exec开头的函数,统称exec函数:

#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[]);

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果函数调用出错返回-1
  • 所以exec函数只有出错的返回值没有成功的返回值

命名理解

  • l(list):表示参数采用列表
  • v(vector):参数用数组
  • p(path):有 p 自动搜索环境变量PATH
  • e(env):表示自己维护环境变量

 事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve,所以 execve 在 man手册第2节,其他函数在第3节,这些函数之间的关系如下图所示:

首先我们来介绍 execl 函数。

其中第一个参数 path 为路径,我们需要传入路径+目标文件名;第二个参数 arg,是可变参数列表,我们可以向其中传入不定的参数;其中 arg 中的参数,我们在命令行上是如何执行代码的,就在其参数中如果传参,注意:最后一个参数必须是NULL,表示传参结束。

结果:(没执行程序结束处,转而执行了 ls 命令)

 

因为程序替换会替换掉当前调用替换接口的进程,所以这些程序替换接口的调用往往配合着进程创建(fork)一起使用。

为什么要创建子进程?

为了不影响父进程,我们想让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能!如果不创建新的子进程,那么被替换的进程只能是当前运行的父进程;如果创建了,替换的进程就是子进程,而不影响父进程。

说了这么多,我们可以举一个配合使用的代码例子:

 运行结果:

 然后我们再来看看 execv 函数是如果调用的

该函数要求第二个参数传入指针数组,数组中存放着调用的命令以及选项。

运行结果:

接下来是 execlp 函数的使用

其中这个p表示,该函数会自己再环境变量PATH中进行查找,不用告知路径。

结果如下:

execvp 函数的使用:

第一个参数会自动在环境变量中进行查找,第二个参数传入指针数组即可。

关于执行 execle 函数

首先我们要先实现一个功能:使用C/C++程序,如果调用其他语言的可执行程序。

我们首先编写一个python脚本,然后使用进程替换接口进行调用:

结果如下: 

关于execle的第三个参数 envp[] ,即环境变量,应该如何传递呢?

第一个参数传入要执行文件的路径,第二个参数为可变参数列表,第三个参数为环境变量,可以是自己设置的,也可以是主函数中传入的。

代码如下(myproc、mycmd、makefile):

结果:

二、命令行解释器-Shell

学习了fork和程序替换的众多接口,我们便可以来实现一个Linux中的命令行解释器shell。

其运行原理就是通过让子进程执行命令,父进程等待&&解析指令。

2.1 shell的实现与运行

这里先把代码附上,然后我们来逐步讲解

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define NUM 1024
#define SIZE 32
#define SEP " "    
char cmd_line[NUM];
char* g_argv[SIZE];
int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程,不退出
	while (1)
	{
        //1.打印出命令提示信息  [root@localhost myshell]#
		printf("[root@localhost myshell]#");
		fflush(stdout);       
		sleep(1);
		memset(cmd_line, '\0', sizeof(cmd_line));
        //2.获取用户的键盘输入[输入的为各种指令和选项 "ls -a -l "]
		if (fgets(cmd_line, sizeof(cmd_line), stdin) == NULL)
		{
			continue;
		}
		cmd_line[strlen(cmd_line) - 1] = '\0';
		//3.命令行字符串解析:"ls -a -l "  -> "ls"  "-a"  "-l"
		g_argv[0] = strtok(cmd_line, SEP);
		//给ls加颜色
		if (strcmp(g_argv[0], "ls") == 0)
		{
			g_argv[index++] = "--color=auto";
		}
		while (g_argv[index - 1])
		{
			g_argv[index++] = strtok(NULL, SEP);
		}
        //4.父进程执行内置命令
		if (strcmp(g_argv[0], "cd") == 0)
		{
			if (g_argv[1] != NULL)chdir(g_argv[1]);
		}
        //5.fork(),子进程执行任务
		pid_t id = fork();
		if (id == 0)
		{
			printf("下面功能是子进程执行的:\n");
			execvp(g_argv[0], g_argv);
			exit(1);
		}
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if (ret > 0)
			printf("exit code:%d\n", WEXITSTATUS(status));
	}
	return 0;
}

我们来看看运行结果:

 好的,接下来我们来分析myshell。

2.2 步骤讲解

1. 打印命令提示符

在linux中,我们用户是这样输入的

 这种实现是基于printf'打印出用户和盘符后,调用了fflush立即刷新了缓冲区,然后我们在后面继续输入实现的,然后调用sleep函数让其打印稍显自然,不要立即弹出。

printf("[root@localhost myshell]#");
fflush(stdout);       //立即刷新缓冲区
sleep(1);

2.获取用户输入

我们将用户输入的数据存放到一个全局变量数组中,使用fgets函数进行接受(getline、gets也行),因为用户最有的输入会有一个\n,所以我们还要对该\n进行删除。

//2.获取用户的键盘输入[输入的为各种指令和选项 "ls -a -l "]
memset(cmd_line, '\0', sizeof(cmd_line));
if (fgets(cmd_line, sizeof(cmd_line), stdin) == NULL)
{
	continue;
}
//清除用户输入的\n
cmd_line[strlen(cmd_line) - 1] = '\0';

3.命令行字符串解析

接受了字符串后我们就要对其进行解析,例如"ls -a -l "  -> "ls"  "-a"  "-l"。 

就是将其拆分为字串,然后存放到一个指针数组中。我们就可以使用 strtok 函数来切割。

其中SEP为" ",因为strtok第二个参数是以什么字符串进行切割,所以传入" "。

strtok函数第一次切割传入字符串,第二次以及后续切割直接传入NULL即可。

//3.命令行字符串解析:"ls -a -l "  -> "ls"  "-a"  "-l"
g_argv[0] = strtok(cmd_line, SEP);
//给ls加颜色
if (strcmp(g_argv[0], "ls") == 0)
{
	g_argv[index++] = "--color=auto";
}
while (g_argv[index - 1])
{
	g_argv[index++] = strtok(NULL, SEP);
}

4.内置命令

内置命令就是让父进程(shell)自己执行的命令,其本质就是shell中的一个函数调用。如果用户输入cd ..,就是改变当前 myshell 的路径,让子进程进行切换路径是无法让所在路径进行切换的,所以这里我们要进行额外的处理。

if (strcmp(g_argv[0], "cd") == 0)
{
	if (g_argv[1] != NULL)chdir(g_argv[1]);
}

其中 chdir 是一个系统调用,传入路径,就可以让当前进程进入该路径。

5.fork()子进程执行

接下来就是子进程执行的部分,我们可以直接使用fork创建出一个子进程,然后用 execvp 通过调用环境变量,因为输入的指令本来就以及被添置进了环境变量,然后我们传入我们切割好的指针数组,就可以很好的进行进程替换。

然后使用waitpid让父进程阻塞等待子进程执行完毕。

pid_t id = fork();
if (id == 0)
{
	printf("下面功能是子进程执行的:\n");
	execvp(g_argv[0], g_argv);
	exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
printf("exit code:%d\n", WEXITSTATUS(status));

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Brant_zero2022

素材免费分享不求打赏,只求关注

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值