【从浅学到熟知Linux】进程控制下篇=>进程程序替换与简易Shell实现(含替换原理、execve、execvp等接口详解)

在这里插入图片描述

🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见

文章目录

  • 进程程序替换
    • 什么是程序替换及其原理
    • 替换函数
      • execl
      • execlp
      • execle
      • execv
      • execvp
      • execvpe
      • execve
  • 替换函数总结
  • 实现简易Shell


进程程序替换

什么是程序替换及其原理

父进程创建子进程的目的只有一个:让子进程帮助父进程完成某些任务。如果要让子进程执行与父进程不同的代码有两种方式↓↓↓

  • 通过if分支判断语句决定父子进程各自的执行代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
	pid_t id = fork();
	if(id < 0)//进程创建错误
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		int cnt = 5;
		while(cnt)
		{
			printf("child process %d is doing something different from parent process!\n");
			cnt--;
		}
		exit(0);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(ret == id)
		{
			printf("parent wait child process success! exitcode = %d\n", WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

  • 通过进程程序替换,让子进程执行与父进程完全不同代码

下面仅是演示代码,关于进程程序替换的详细内容将在下文介绍↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)
	{
		execlp("top", "top", NULL);
		exit(2);
	}
	else
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("wait success! exitcode = %d\n", WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述
从上面可知,用fork创建子进程后可以执行的程序和父进程相同的程序(但可能执行不同的代码分支),也可以通过调用exec系列函数接口来执行另一个程序。

当程序调用exec系列函数中的一个时,该进程的用户空间的代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并没有创建新进程,所以调用exec前后该子进程的id并没有改变。调用了exec函数后,会对子进程的数据和代码做写入,此时会发生写时拷贝(即子进程不再与父进程共享代码和数据,而是在物理空间中拥有自己独立的代码和数据)。

在这里插入图片描述
在这里插入图片描述

★ps:CPU如何得知替换后程序的入口?Linux中形成的可执行文件是有格式的(即ELF),可执行文件的表头包含可执行程序的入口地址、页表、mm_struct(程序地址空间)等。

替换函数

下面了解一下exec系列函数->替换换函数共有7个,先看一下它们,对它们有一个大致印象↓↓↓(除了execve在2号man手册,其余均位于3号man手册)
在这里插入图片描述
在这里插入图片描述

下面给出每个exec系列函数的使用方法↓↓↓

execl

int execl(const char* path, const char* arg, ...)
execl的第一个参数需要传入可执行文件的绝对路径,例如,可以传入/usr/bin/ls。而余下参数为可变参数,传入形式就和我们使用命令行命令一样,先给出命令名称,再给出命令行参数,最终以NULL结尾,例如,“ls”、“-a”、“-l”、NULL。

★ps:execl的l表示list(列出)的意思,即需要列出每个命令行参数。

下面给出接口使用示例↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)
	{
		execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
		exit(2);
	}
	else
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

execlp

int execlp(const char* file, const char* arg, ...);
execlp第一个参数如果是存在于PATH环境变量中的可执行文件(如命令等),可以直接写出可执行文件名称即可,不用写绝对路径,因为execlp在执行时,会在PATH环境变量中的各个目录下查找对应的可执行文件;但如果是不存放于PATH环境变量中的各个目录下的可执行文件,则需要使用绝对路径。而余下参数为可变参数,传入形式就和我们使用命令行命令一样,先给出命令名称,再给出命令行参数,最终以NULL结尾,这与execl相同。

★ps:exec系列函数中,主要带有p的,如果可执行文件存在于PATH环境变量中,均不需要使用绝对路径,只需要给出可执行文件名即可。

下面给出接口使用示例↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)
	{
		execlp("ls", "ls", "-a", "-l", NULL);
		exit(2);
	}
	else
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

★ps:如果我们的exec系列函数能够执行系统命令,那如何执行我们自己编写的可执行程序呢?下面演示C语言程序调用C++程序↓↓↓

excute.cpp↓↓↓

#include <iostream>
using namespace std;

int main()
{
	for(int i = 0; i < 5; i++)
	{
		cout << "Jammingpro is coding..." << endl;
	}
	return 0;
}

execlp2.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)
	{
		execlp("./excute", "excute", NULL);
		exit(2);
	}
	else
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

execle

int execle(const char *path, const char *arg,..., char * const envp[]);
该接口函数的第一个参数path需要使用传入可执行文件的绝对路径,紧接其后的arg与execl用法一致,传入形式就和我们使用命令行命令一样,先给出命令名称,再给出命令行参数,最终以NULL结尾。最后一个参数envp表示环境变量。

子进程默认继承父进程的环境变量,那环境变量是什么时候传给子进程的呢?环境变量也是数据,创建子进程的时候,环境变量就被子进程继承下去了。即使对代码和数据修改时发生写时拷贝,也不会影响父子进程共享同一片环境变量空间。所以发生程序替换时,环境变量信息不会被替换。如果需要对子进程的环境变量做修改可以选择execle、execvpe、execve,这3个exec系列接口。这里先通过execle接口介绍execle、execvpe、execve的两个应用场景↓↓↓

  • 希望子进程增加新增或覆盖某些环境变量

下面代码中,父进程fork创建子进程后,子进程在进行程序替换前,给自己增加了一个"Jammingpro=666"的环境变量。子进程能获取该环境变量,而父进程无法获取该环境变量。

execle_test.c↓↓↓

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char* s = getenv("Jammingpro");
	if(s != NULL) printf("%s\n", s);
	return 0;
}

execle1.c↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		extern char **environ;
		putenv("Jammingpro=666");//给子进程新增环境变量,该语句子进程会执行
		execle("./execle_test", "execle_test", NULL, environ);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		char* s = getenv("Jammingpro");
		if(s != NULL) printf("%s", s);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

  • 安全考虑及定制化场景

出于安全考虑,不希望子进程获取父进程的环境变量;或者因为子进程需要定制与父进程完全不一样的环境变量。可以定义一个字符指针数组,在该数组中存储子进程专属的环境变量,当使用execle、execvpe、execve将该环境变量传入时,会直接覆盖从父进程继承下来的环境变量。

下面代码中,给子进程创建专门的环境变量,子进程此时可以获取专门的环境变量"Jammingpro=666",但无法获取父进程的环境变量PWD,因为使用exec带e的接口时,直接覆盖了子进程从父进程那里继承的环境变量。而父进程能获取从bash继承下来的环境变量,而无法获取子进程专属的环境变量。

execle_test2.c↓↓↓

#include <stdio.h>
#include <stdlib.h>

int main()
{
	char* s = getenv("Jammingpro");
	if(s != NULL) printf("%s\n", s);
	else printf("Don't have Jammingpro\n");

	s = getenv("PWD");
	if(s != NULL) printf("%s\n", s);
	else printf("Don't have PWD\n");
	return 0;
}

execle2.c↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* env[] = {"Jammingpro=666", "xiaoming=888"};
		execle("./execle_test2", "execle_test2", NULL, env);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		char* s = getenv("Jammingpro");
		if(s != NULL) printf("%s\n", s);
		else printf("Don't have Jammingpro\n");
	
		s = getenv("PWD");
		if(s != NULL) printf("%s\n", s);
		else printf("Don't have PWD\n");
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

★ps:关于putenv与exec带e系列接口原理探索
子进程和父进程的PCB内都有一个环境变量表指针,当子进程刚创建时,子进程的环境变量表指针与父进程指向同一个位置,一旦子进程调用putenv尝试对环境表做修改,此时则会发生写时拷贝。
在这里插入图片描述
在这里插入图片描述
环境变量表是是一个字符指针数组,也就是说:环境变量表中并不会直接存储对应的环境变量,而是存储各个环境变量的存储地址。当我们使用putenv时,本质是将我们定义环境变量(字符串或char类型数组)的首地址存储到该环境变量中。如果定义一个char env[100] = "Jammingpro=666",再执行putenv(env),则会将env的首地址存储到环境变量表中。此时不可以修改env数组中的内容,一旦修改,则对应的环境变量会跟着发生变化。

在这里插入图片描述
putenv只是修改环境变量表中某个表项的指向。但如果我们使用的是exec带e系列函数,则会直接修改进程PCB中环境变量表指针的指向。

execv

int execv(const char *path, char *const argv[]);
该接口第一个参数需要传入可执行程序的绝对路径,第二参数需要传入命令行参数,不同的是,这里并不是使用可变参数列表的方式,而是使用字符指针数组的方式。例如,我们需要执行ls命令,则第一个参数需要传入/usr/bin/ls,第二个参数需要先定义一个字符指针数组char* opts[] = {"ls", "-a", "-l", NULL},再将该数组作为第二参数传入。

★ps:exec系列函数中,带v的,则第二个参数需要以字符指针数组的形式传入命令行参数。

下面给出接口使用示例↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* opts[] = {"ls", "-a", "-l", NULL};
		execv("/usr/bin/ls", opts);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

execvp

int execvp(const char *file, char *const argv[]);
该接口第一个参数传入可执行文件,如果该可执行文件可以在PATH环境变量中找到,则不需要使用绝对路径,否则需要使用绝对路径;第二个参数需要以字符指针数组的形式传入命令行参数。

下面给出接口使用示例↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* opts[] = {"top", NULL};
		execvp("top", opts);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

execvpe

int execvpe(const char *file, char *const argv[],char *const envp[]);
该接口第一个参数传入可执行文件,如果该可执行文件可以在PATH环境变量中找到,则不需要使用绝对路径,否则需要使用绝对路径;第二个参数需要以字符指针数组的形式传入命令行参数;第三个参数需要传入环境变量。

下面给出接口使用示例(给子进程传入自定义环境变量)↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* opts[] = {"top", NULL};
		char* env[] = {"Jammingpro=666"};
		execvpe("./execle_test", opts, env);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

execve

int execve(const char *filename, char *const argv[],char *const envp[]);
该接口第一个参数需要传入可执行程序的绝对路径,第二个参数需要以指针数组的方式传入命令行参数,第三个参数需要传入环境变量。

下面给出该接口的使用方式↓↓↓

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* opts[] = {"top", NULL};
		char* env[] = {"Jammingpro=666"};
		execve("./execle_test", opts, env);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述

替换函数总结

上面总结上面介绍的各个替换函数↓↓↓

函数名参数格式PATH中可执行程序是否需要带绝对路径是否使用当前环境变量
execl列表
execlp列表不是
execle列表不是,需自己组装环境变量
execv数组
execvp数组不是
execvpe数组不是不是,需自己组装环境变量
execve数组不是,需自己组装环境变量

上面的exec系列函数中,如果带有p的,则第一个参数的可执行文件若存在于PATH环境变量中,则只需要填写可执行文件名;如果带有v的,则需要以字符指针数组的形式传入命令行参数;如果是带e的,则需要自己组装环境变量。

上面的各个接口统称为加载器,它们为即将替换进来的可执行程序加载入参数列表、环境变量等信息。下面我们使用execvpe接口给自定义可执行程序传入命令行参数及环境变量,该可执行程序将会把命令行参数及环境变量打印至显示器↓↓↓

printInfo.c

#include <stdio.h>

int main(int argc, char* argv[]; char* env[])
{
	printf("传入%d个命令行参数,分别是:\n", argc);
	int i = 0;
	for(; argv[i]; i++)
	{
		printf("[%d]->%s\n", i, argv[i]);
	}
	printf("--------------------------------------\n");
	printf("环境变量分别是:\n");
	i = 0;
	for(; env[i]; i++)
	{
		printf("[%d]->%s\n", i, env[i]);
	}
	return 0;
}

execvpe2.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	extern char **environ;
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork");
		exit(1);
	}
	else if(id == 0)//子进程
	{
		char* opts[] = {"printInfo", NULL};
		char* env[] = {"Jammingpro=666", "xiaoming=888"};
		execvpe("./printInfo", opts, env);
		exit(2);
	}
	else//父进程
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, 0);
		if(WIFEXITED(status))
		{
			printf("Wait %d success! exitcode is %d\n", ret, WEXITSTATUS(status));
		}
	}
	return 0;
}

在这里插入图片描述
上述各个接口中,只有execve是系统调用,其他均是对该系统调用接口的封装。这也就是为什么execve位于2号手册,而其他接口函数位于3号手册的原因。

实现简易Shell

我们来模拟实现一个Shell,这个Shell具有一些常用的简易功能。下面我们用一张图了解一下Linux中bash(Shell的一种)的执行过程↓↓↓
在这里插入图片描述

一个shell程序需要循环做的事如下:
1.获取命令行
2.解析命令行
3.创建子程序
4.替换子程序
5.父进程等待子进程退出

下面我们先来完成第一步获取命令行↓↓↓

#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 LABLE "#"

#define COM_LEN 1024	//输入命令行长度
#define PWD_LEN 128		//当前路径长度

char command[COM_LEN];	//命令行缓冲区
char pwd[PWD_LEN];		//当前路径缓冲区

//获取用户名
const char* getUser()
{
  return getenv("USER");
}

//获取当前路径
const char* getPWD()
{
  getcwd(pwd, sizeof(pwd));
  return pwd;
}

//获取主机名
const char* getHostName()
{
  return getenv("HOSTNAME");
}

//输出提示信息&&获取用户输入
void getCommand()
{
  printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getHostName(), getUser(), getPWD());
  char* s = fgets(command, sizeof(command), stdin);
  assert(s != NULL);
  (void)s;
  
  s[strlen(command) - 1] = '\0';
  printf("%s\n", command);
}

int main()
{
  while(1)
  {
    getCommand();
  }
  return 0;
}

在这里插入图片描述

下面我们需要对获取的字符串进行切割↓↓↓

★ps:下面spliteString中使用了条件编译,当编译时带上-DDEBUG,就可以输出分隔的结果

#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 LABLE "#"
#define SEP " \t"

#define COM_LEN 1024
#define ARG_LEN 64
#define PWD_LEN 128

char command[COM_LEN];
char *argv[ARG_LEN];
char pwd[PWD_LEN];
int argc = 0;

const char* getUser()
{
  return getenv("USER");
}

const char* getPWD()
{
  getcwd(pwd, sizeof(pwd));
  return pwd;
}

const char* getHostName()
{
  return getenv("HOSTNAME");
}

//输出提示信息&&获取用户输入
void getCommand()
{
  printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getHostName(), getUser(), getPWD());
  char* s = fgets(command, sizeof(command), stdin);
  assert(s != NULL);
  (void)s;
  
  s[strlen(command) - 1] = '\0';
}

//分隔字符串
void spliteString()
{
  argc = 0;
  argv[argc++] = strtok(command, SEP);
  while(argv[argc++] = strtok(NULL, SEP));
#ifdef DEBUG 
  int j = 0;
  for(;argv[j]; j++)
  {
    printf("[%d]->%s\n", j, argv[j]);
  }
#endif
}

int main()
{
  while(1)
  {
    getCommand();
    spliteString();
  }
  return 0;
}

在这里插入图片描述

使用normalExcute创建子进程,使用程序替换的方式,让子进程执行指定程序,如果子进程替换失败则返回EXIT_CODE。父进程等待子进程,并将子进程退出码保存在exitcode中。

#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 LABLE "#"
#define SEP " \t"
#define EXIT_CODE 66

#define COM_LEN 1024
#define ARG_LEN 64
#define ENV_LEN 32
#define PWD_LEN 128

char command[COM_LEN];
char *argv[ARG_LEN];
char pwd[PWD_LEN];
int argc = 0;
int exitcode = 0;

const char* getUser()
{
  return getenv("USER");
}

const char* getPWD()
{
  getcwd(pwd, sizeof(pwd));
  return pwd;
}

const char* getHostName()
{
  return getenv("HOSTNAME");
}

//输出提示信息&&获取用户输入
void getCommand()
{
  printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getHostName(), getUser(), getPWD());
  char* s = fgets(command, sizeof(command), stdin);
  assert(s != NULL);
  (void)s;
  
  s[strlen(command) - 1] = '\0';
}

//分隔字符串
void spliteString()
{
  argc = 0;
  argv[argc++] = strtok(command, SEP);
  while(argv[argc++] = strtok(NULL, SEP));
#ifdef DEBUG 
  int j = 0;
  for(;argv[j]; j++)
  {
    printf("[%d]->%s\n", j, argv[j]);
  }
#endif
}

void normalExcute()
{
  pid_t id = fork();
  assert(id != -1);
  if(id == 0)
  {
    exitcode = 0;
    execvp(argv[0], argv);
    exit(EXIT_CODE);
  }
  else 
  {
    int status = 0;
    pid_t id = waitpid(id, &status, 0);
    exitcode = WEXITSTATUS(status);
  }
}

int main()
{
  while(1)
  {
    getCommand();
    spliteString();
    normalExcute();
  }
  return 0;
}

在这里插入图片描述

截至到这里,我们已经实现了能够执行大部分命令的Shell程序。但如果我们执行cd命令,当前路径却没有改变。这是为什么呢?

在这里插入图片描述

当子进程被创建后,由子进程执行cd命令,则修改环境变量PWD时,子进程会发生写时拷贝,保证父子进程的独立性。因而,子进程的PWD改变,不会影响父进程。像这样的命令需要使用内建命令的方式解决,即遇到这类命令时,不让子进程执行,而是由父进程自己执行对应的函数。下面通过设置buildExcute函数,不能交由子进程执行的命令进行了特殊处理↓↓↓

#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 LABLE "#"
#define SEP " \t"
#define EXIT_CODE 66

#define COM_LEN 1024
#define ARG_LEN 64
#define ENV_LEN 32
#define PWD_LEN 128

char *env[ENV_LEN];
char command[COM_LEN];
char *argv[ARG_LEN];
char pwd[PWD_LEN];
int argc = 0;
int envNum = 0;
int exitcode = 0;

const char* getUser()
{
  return getenv("USER");
}

const char* getPWD()
{
  getcwd(pwd, sizeof(pwd));
  return pwd;
}

const char* getHostName()
{
  return getenv("HOSTNAME");
}

//输出提示信息&&获取用户输入
void getCommand()
{
  printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getHostName(), getUser(), getPWD());
  char* s = fgets(command, sizeof(command), stdin);
  assert(s != NULL);
  (void)s;
  
  s[strlen(command) - 1] = '\0';
}

//分隔字符串
void spliteString()
{
  argc = 0;
  argv[argc++] = strtok(command, SEP);
  while(argv[argc++] = strtok(NULL, SEP));
#ifdef DEBUG 
  int j = 0;
  for(;argv[j]; j++)
  {
    printf("[%d]->%s\n", j, argv[j]);
  }
#endif
}

void normalExcute()
{
  pid_t id = fork();
  assert(id != -1);
  if(id == 0)
  {
    exitcode = 0;
    execvp(argv[0], argv);
    exit(EXIT_CODE);
  }
  else 
  {
    int status = 0;
    pid_t id = waitpid(id, &status, 0);
    exitcode = WEXITSTATUS(status);
  }
}

int buildExcute()
{
  if(argc == 3 && strcmp(argv[0], "cd") == 0)
  {
    int ret = chdir(argv[1]);
    if(ret != -1) exitcode = 0;
    else exitcode = EXIT_CODE;
    return 1;
  }
  else if(argc == 3 && strcmp(argv[0], "export") == 0)
  {
  	env[envNum] = (char*)malloc(sizeof(argv[1]) + 1);
    strcpy(env[envNum], argv[1]);
    int ret = putenv(env[envNum++]);
    if(ret == 0) exitcode = 0;
    else exitcode = EXIT_CODE;
    return 1;
  }
  else if(argc == 3 && strcmp(argv[0], "echo") == 0)
  {
    if(strcmp(argv[1], "$?") == 0) printf("%d\n", exitcode);
    else printf("%s\n", argv[1]);
    return 1;
  }
  else if(strcmp(argv[0], "ls") == 0)
  {
    argv[argc - 1] = (char*)"--color=auto";
    argv[argc] = NULL;
  }
  return 0;
}

int main()
{
  while(1)
  {
    getCommand();
    spliteString();
    int ret = buildExcute();
    if(!ret) normalExcute();
  }
  return 0;
}

至此,能执行大部分命令的Shell程序就大功告成了。但该程序并没有所有内建命令做处理。
在这里插入图片描述

★ps:当我们登录Linux的时候,就要启动一个shell进程,该进程会从用户home目录的.bash_profile中获取环境变量,从而读入环境变量。从上面的自定义实现shell程序中可知,export导入环境变量仅在当前shell进程有效。如果需要设置永久性环境变量,需要修改系统的环境变量配置文件。

★ps:现象:程序替换成功后,exec系列函数后的代码不会被执行;若替换失败,则继续向下执行。这也解释了exec系列函数为什么只有失败返回值,而没有正确返回值。一旦正确,后序代码不会执行,该正确返回值也无法使用。
在这里插入图片描述

🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/549104.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3D视觉引导麻袋拆垛破包 | 某大型化工厂

客户需求 此项目为大型化工厂&#xff0c;客户现场每日有大量麻袋拆垛破包需求&#xff0c;麻袋软包由于自身易变形、码放垛型不规则、运输后松散等情况&#xff0c;无法依靠机器人示教位置完成拆垛。客户遂引入3D视觉进行自动化改造。 工作流程&#xff1a; 3D视觉对紧密贴合…

公司文件加密软件有监视功能吗?

公司文件加密软件不仅提供了强大的文件加密能力&#xff0c;还具备了监视功能&#xff0c;确保文件在使用过程中的安全性。华企盾DSC数据防泄密系统中的监控功能体现在以下几个方面&#xff1a; 加密文件操作日志&#xff1a;记录所有加密文件的申请、审批、扫描加解密、自动备…

数据分析

数据分析流程 数据分析开发流程一般分为下面5个阶段&#xff0c;主要包含&#xff1a;数据采集、数据处理、数据建模、数据分析、数据可视化 数据采集&#xff1a; 数据通常来自于企业内部或外部&#xff0c;企业内部数据可以直接从系统获得&#xff0c;外部数据则需要购买&a…

【面试经典 150 | 链表】删除链表的倒数第 N 个结点

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;统计节点个数方法二&#xff1a;双指针 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本…

Python 爬虫:如何用 BeautifulSoup 爬取网页数据

在网络时代&#xff0c;数据是最宝贵的资源之一。而爬虫技术就是一种获取数据的重要手段。Python 作为一门高效、易学、易用的编程语言&#xff0c;自然成为了爬虫技术的首选语言之一。而 BeautifulSoup 则是 Python 中最常用的爬虫库之一&#xff0c;它能够帮助我们快速、简单…

【Java探索之旅】数组使用 初探JVM内存布局

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java编程秘籍 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一、数组的使用1.1 元素访问1.2 数组遍历 二、JVM的内存布局&#x1f324;️全篇总结 …

Java面试题目和答案【终极篇】

Java面向对象有哪些特征,如何应用 ​ 面向对象编程是利用类和对象编程的一种思想。万物可归类,类是对于世界事物的高度抽象 ,不同的事物之间有不同的关系 ,一个类自身与外界的封装关系,一个父类和子类的继承关系, 一个类和多个类的多态关系。万物皆对象,对象是具体的世…

Linux 删除文件或文件夹命令(新手)

一、删除文件夹 rm -rf 路径/目录名 1 强制删除文件夹及其子文件。 二、删除文件/文件夹&#xff1a;rm 命令 rm 删除命令&#xff0c;它可以永久删除文件系统中指定的文件或目录。 rm [选项] 文件或目录 选项&#xff1a; -f&#xff1a;强制删除&#xff08;force&am…

我们试用了6款最佳Appium替代工具,有些甚至比Appium更好

Appium是一款知名的自动化测试工具&#xff0c;用于在iOS、Android和Windows等移动平台上运行测试。就开源移动测试自动化工具而言&#xff0c;虽然替代品有限&#xff0c;但它们确实存在。我们找到了一些优秀的Appium替代品&#xff0c;它们也可以满足自动化测试要求&#xff…

聚道云软件连接器助力医疗器械有限公司打通金蝶云星辰与飞书

摘要 聚道云软件连接器成功将金蝶云星辰与飞书实现无缝对接&#xff0c;为某医疗器械有限公司解决采购订单、付款单同步、审批结果回传、报错推送等难题&#xff0c;实现数字化转型升级。 客户介绍 某医疗器械有限公司是一家集研发、生产、销售为一体的综合性医疗器械企业。…

BackTrader 中文文档(一)

原文&#xff1a;www.backtrader.com/ 主页 欢迎来到 backtrader&#xff01; 原文&#xff1a;www.backtrader.com/ 一个功能丰富的 Python 框架&#xff0c;用于回测和交易 backtrader允许您专注于编写可重复使用的交易策略、指标和分析器&#xff0c;而不必花时间构建基础…

打一把王者的时间,学会web页面测试方法与测试用例编写

一、输入框 1、字符型输入框&#xff1a; &#xff08;1&#xff09;字符型输入框&#xff1a;英文全角、英文半角、数字、空或者空格、特殊字符“~&#xff01;#&#xffe5;%……&*&#xff1f;[]{}”特别要注意单引号和&符号。禁止直接输入特殊字符时&#xff0c;…

Web App 入门指南:构建预测模型 App 的利器(shiny)

Web App 入门指南&#xff1a;构建预测模型 App 的利器 简介 近年来&#xff0c;随着机器学习和人工智能技术的快速发展&#xff0c;预测模型在各行各业得到了广泛应用。为了方便地部署和使用预测模型&#xff0c;将模型构建成 Web App 是一种非常好的选择。Web App 无需下载…

27.8k Star,AI智能体项目GPT Pilot:第一个真正的人工智能开发者(附部署视频教程)

作者&#xff1a;Aitrainee | AI进修生 排版太难了&#xff0c;请点击这里查看原文&#xff1a;27.8k Star&#xff0c;AI智能体项目GPT Pilot&#xff1a;第一个真正的人工智能开发者&#xff08;附部署视频教程&#xff09; 今天介绍一下一个人工智能智能体的项目GPT Pilot。…

Postman 环境变量配置初始调用登录脚本赋值Token

效果 新建环境 切换 Environments 标签下 点击上面加号增加环境变量 使用环境变量 使用{{变量名}}引用变量使用 Pre-request Script 全局 一般授权接口都需要再调用接口前&#xff0c;进行登录授权&#xff0c;这里使用了全局的请求前脚本调用。 脚本示例 // 基础地址 var…

前端跨域怎么办?

如果网上搜到的方法都不可行或者比较麻烦&#xff0c;可以尝试改变浏览器的设置&#xff08;仅为临时方案&#xff09; 1.新建一个Chrome浏览器的快捷方式 2.鼠标右键&#xff0c;进入属性&#xff0c;将以下命令复制粘贴到目标位置&#xff08;可根据Chrome实际存放位置修改…

数据结构DAY4--哈希表

哈希表 概念&#xff1a;相当于字典&#xff0c;可以根据数据的关键字来寻找相关数据的查找表。 步骤&#xff1a;建立->插入->遍历->查找->销毁 建立 建立数据&#xff0c;形式随意&#xff0c;但一般为结构体&#xff08;储存的数据量大&#xff09;&#xff…

vivado AXI 接口事件

AXI 接口事件 在 Vivado 硬件管理器中 &#xff0c; 如果使用 System ILA IP 对设计 AXI 接口进行调试 &#xff0c; 那么“波形 (Waveform) ”窗口会显示对 应于 System ILA 所探测的接口的接口插槽、事件和信号组。正如下图所示 &#xff0c; “ Waveform ”窗口会显示…

牛客2024 【牛客赛文X】春招冲刺 ONT84 子数组的最小值之和【中等 单调栈 Java、Go、PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/a7401d0dd4ec4071a31fd434e150bcc2 思路 单调栈解决的问题单调栈解决的问题是在一个数组中想知道所有数中&#xff0c; 左边离他近的比他大的和右边离他近的比他大的数 思考的问题&#xff1a;如果知道所有数上…

移植speexdsp到OpenHarmony标准系统④

五、在OpenHarmony编译体系下增量编译Speexdsp 建议先增量编译生成三方库的动态链接库和可执行文件,验证是否成功把三方库加入OpenHarmonybian编译体系。 成功编译出so和可执行文件&#xff0c;即成功把三方库加入到ohos编译体系。之后还要验证三方库在ohos运行&#xff0c;功…