
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<Linux>
座右铭
路虽远,行则将至;事虽难,做则必成
目录
前言
今天我们的进程章节引来了完结,在之前的学习中我们初步认识了进程的结构,明白了进程是怎么切换的,怎么创建出一个进程并且等待他的返回值,这些知识点.今天我们将会来学习进程控制的相关的知识.
本文的主要的目标是学会怎么进行进程的切换.并且依靠这个知识点我们来模拟实现一下xshell
OK.
话不多说了.开始了我们今天学习的旅途了
进程的替换
进程的替换就是用来把这个进程的运行的任务替换成了另一个任务,但是我们需要了解一下进程替换的本质是什么.
进程替换实质
是当前进程取执行的吗?还是换了一个进程去执行的?
我们来看简单的一串代码
cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h> //fork
#include<sys/types.h> //pid_t
#include<sys/wait.h> //wait
#include<errno.h> //perror
#include<cstdio> //printf
#include<cstdlib> //exit
#include<cstring> //strerror
using namespace std;
int main()
{
pid_t id = fork();
if(id == -1)
{
perror("fork error\n");
exit(1);
}else if(id == 0)
{
//child process
printf("child process my id is %d\n",getpid());
printf("我要进行替换进程了,我将会把子进程替换为一个自己代码进程\n");
execl("/home/jiao/study/6.7/mycode","mycode",nullptr);
perror("execlp error\n");
exit(1);
}else
{
//parent process
printf("parent process my id is %d\n",getpid());
printf("我要等待子进程\n");
int sta;
int n=waitpid(id,&sta,0);
if(n==id&&(sta&0x7f)==0)
{
printf("子进程正常退出,退出码是:%d\n",(sta>>8)&0xff);
}else
{
printf("子进程异常退出,退出的信号是:%d\n",sta&0x7f);
}
}
return 0;
}
这是我们的主要的代码,下面这一串的代码是我们将要替换的程序的代码
cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h> //fork
#include<sys/types.h> //pid_t
#include<sys/wait.h> //wait
#include<errno.h> //perror
#include<cstdio> //printf
#include<cstdlib> //exit
#include<cstring> //strerror
using namespace std;
int main()
{
printf("我是一个C++的程序,我的pid是%d\n",getpid());
return 0;
}
我们来看看代码的执行的结果是不是真的执行了进程的替换

显然的是进程替换是成功的,那么我们来看看进程替换的实质是什么东西把/
我们的代码在编写的时候分别打印了一下替换前后的pid我们可以发现替换前后的pid是相同的,那么我们可以确定的是同一个进程来执行的程序,那么我们来修改一下我们的代码
cpp
#include<iostream>
#include<stdlib.h>
#include<unistd.h> //fork
#include<sys/types.h> //pid_t
#include<sys/wait.h> //wait
#include<errno.h> //perror
#include<cstdio> //printf
#include<cstdlib> //exit
#include<cstring> //strerror
using namespace std;
int main()
{
pid_t id = fork();
if(id == -1)
{
perror("fork error\n");
exit(1);
}else if(id == 0)
{
//child process
printf("child process my id is %d\n",getpid());
printf("我要进行替换进程了,我将会把子进程替换为一个自己代码进程\n");
execl("/home/jiao/study/6.7/mycode","mycode",nullptr);
printf("这句话会被执行吗???\n");
perror("execlp error\n");
exit(1);
}else
{
//parent process
printf("parent process my id is %d\n",getpid());
printf("我要等待子进程\n");
int sta;
int n=waitpid(id,&sta,0);
if(n==id&&(sta&0x7f)==0)
{
printf("子进程正常退出,退出码是:%d\n",(sta>>8)&0xff);
}else
{
printf("子进程异常退出,退出的信号是:%d\n",sta&0x7f);
}
}
return 0;
}

我们在替换进程的主要的代码中我们在替换语句的之后添加上了这样的代码那么他会执行吗?

答案非常的显然,这个语句是没有执行的,所以我们进一步的了解到了进程替换的实质
我们来看一下这一张图片
我们知道的是一个进程组成的部分是一个结构体和自己的代码和数据.
在上面我们发现的是这个进程的pid这个表明身份的信息没有改变,那么改变的只有一个东西,就是进程指向的数据和代码了.更确切地说就是这个结构体的内容没有发生明显的改变,其他的基本上变成了新的了
所以我们可以断言了
在进程替换的时候我们一般是没有机会接触到成功的返回值的,因为成功了就会跳入到了其他的代码中了,所以进程替换是只有失败的返回值,也就是说也只有在进程替换失败的时候,后面的语句才会有机会得到执行
现在我们知道了进程替换的实质大体就是改变了pcb指向的数据和代码.那么我们应该怎么进行进程替换呢?
进程替换的接口
主要的使用的接口有以下的几种:
cpp
#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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
我去这么多的函数接口我可记不住!!!!
没关系,我们来看看这些函数接口的特点:
首先这些函数的开头是统一的exec那么剩下的就是包含着特殊功能的字符了
- l:表示的是我们的参数需要填写的是链表的形式
- v:表示的是我们的参数是一个vector的数组
- p:表示的是这个函数接口会自己找默认的环境变量
- e:表示的是我们需要自己传递一个环境变量

那么这些字符进行组合就是一个个特殊的函数接口了
来实践一下
cpp
char* v[]={"ls","-l",nullptr};
//child process
printf("child process my id is %d\n",getpid());
printf("我要进行替换进程了,我将会把子进程替换为一个自己代码进程\n");
// execl("/home/jiao/study/6.7/mycode","mycode",nullptr);
execvp("ls",v);
printf("这句话会被执行吗???\n");
perror("execlp error\n");
exit(1);
cpp
char* v[]={"ls","-l",nullptr};
char* myenv[]={"PATH=/bin:/usr/bin", "TERM=console", NULL};
//child process
printf("child process my id is %d\n",getpid());
printf("我要进行替换进程了,我将会把子进程替换为一个自己代码进程\n");
// execl("/home/jiao/study/6.7/mycode","mycode",nullptr);
// execvp("ls",v);
execve("/bin/ls",v,myenv);
printf("这句话会被执行吗???\n");
perror("execlp error\n");
需要注意的是不管是我们的环境变量数组还是命令行的参数数组,数组的结尾都是以空指针结束的
但是真正进行了系统调用的不是我们上述的那么多的函数,本质上只有一个函数是进行系统调用了的.那就是execve函数
其他的函数都是通过变换变成了系统调用的格式之后才进行的进程替换的
具体的过程是这个图片

所以我们在执行的进程的时候我们需要两张表,一个就是命令的参数表和一个环境变量表
模拟实现xshell
首先我们需要简单的了解一下xshell的原理
我们在平常使用的时候我们是先输入一个指令,然后这个指令就会正常的运行.
我们用什么可以模拟出上面的功能呢???
我们今天模拟实现出这个xshell的主要的思路是
父进程创建出一个子进程然后我们的子进程分析我们输入的指令,然后进行进程替换执行我们的指令

首先我们的任务就是先输出一个提示行
也就是我们上面图片一样的提示符一样
构建命令行提示符
命令行的提示符中都包含了什么信息?
Linux 默认提示符一般分 [用户名@主机名 当前目录] $/# 几部分
这些的信息都可以从我们的环境变量中获取



现在我们得到了这些提示符的出处了所以我们来构建一下我们的命令行提示符
cpp
#include <iostream>
#include <stdlib.h>
#define Comand_size 1024
char *GetHostName()
{
char *hostname = getenv("HOSTNAME");
return hostname;
}
char *GetUserName()
{
char *UserName = getenv("USER");
return UserName;
}
char *GetPWD()
{
char *pwd = getenv("PWD");
return pwd;
}
void Makecommand(char *command, int size)
{
snprintf(command, size,"[%s@%s:%s]#", GetUserName(), GetHostName(), GetPWD());
}
void printfcommand()
{
char command[Comand_size];
Makecommand(command, sizeof(command));
printf("%s", command);
fflush(stdout);
}
int main()
{
printfcommand();
return 0;
}
上面的代码就是我们处理我们的命令行的提示符的代码

下面就是处理一下输入的命令行的处理了
首先就是判断我们输入的命令行是不是一个换行符
判断命令是否合法
在这个环节中我们需要把我们输入的命令的换行符变成终止符
cpp
bool GetCommand(char *out, int size)
{
char *c = fgets(out, size, stdin);
if (c == nullptr)
{
return false;
}
out[strlen(out) - 1] = 0; // 去除\n
if (strlen(out) == 0) // 判断是否是一个\n指令
{
return false;
}
// 有效指令
return true;
}
现在我们讲命令做了初步的处理了
但是呢我们输入的命令都是这样的类型的
ls -l -a
这样的所以我们需要用空格为休止符来把命令放到我们的命令数组中
构建命令数组
假设我们输入的命令是一个ls -l -a
那么经过上面的处理我们的命令现在是一个这样的状态
ls -l -a\0
那么我们处理的话就是把这个ls -a -l 分别放在字符串的数组中
这时候我需要介绍一个函数
strtok这个函数需要我们传入两个参数,一个是我们需要处理的字符串,第二个就是一个字符,
这个函数就会按照把第一个出现我们传入参数的字符的位置替换成\0
如果我们传入的第一个的参数是 空指针的话,这个函数就会从上次的位置继续工作
那么这个函数就可以帮助我们完成构建命令数组
cpp
bool AnalyseCommand(char *command)
{
//拆分指令到一个数组中
#define ELF " "
g_argv=0;
argv[g_argv++]=strtok(command,ELF);
while((bool)(argv[g_argv++]=strtok(nullptr,ELF)))
{}
g_argv--;
return true;
}
那么我们现在做好了命令数组的前置任务
现在我们就可以来进行最最重要的环节了
进程替换
现在我们可以使用进程替换的函数进行模拟实现xshell了
在这里我们选择的函数是execvp
因为我们并没有自己设置自己的环境变量所以我们使用的是默认的环境变量
子进程的环境变量会自动拷贝与父进程的环境变量
那么这就是我们的代码
cpp
void excute()
{
pid_t id=fork();
if(id==-1)
{
perror("fork error\n");
exit(1);
}else if(id==0)
{
//child
execvp(argv[0],argv);
}
int n=waitpid(id,nullptr,0);
(void)n;
return;
}
至此我们的函数的接口就全部实现完毕了,现在就是我们的主函数的编写了
首先就是我们打印一下命令行的提示符,然后从键盘中得到一个命令,处理命令然后执行我们的命令
下面就是全部的代码
cpp
#include <iostream>
#include <stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#define Command_size 1024
const int Maxargv=128;
char *argv[Maxargv];
int g_argv=0;
char *GetHostName()
{
char *hostname = getenv("HOSTNAME");
return hostname;
}
char *GetUserName()
{
char *UserName = getenv("USER");
return UserName;
}
char *GetPWD()
{
char *pwd = getenv("PWD");
return pwd;
}
void Makecommand(char *command, int size)
{
snprintf(command, size, "[%s@%s:%s]#", GetUserName(), GetHostName(), GetPWD());
}
void printfcommand()
{
char command[Command_size];
Makecommand(command, sizeof(command));
printf("%s", command);
fflush(stdout);
}
bool GetCommand(char *out, int size)
{
char *c = fgets(out, size, stdin);
if (c == nullptr)
{
return false;
}
out[strlen(out) - 1] = 0; // 去除\n
if (strlen(out) == 0) // 判断是否是一个\n指令
{
return false;
}
// 有效指令
return true;
}
bool AnalyseCommand(char *command)
{
//拆分指令到一个数组中
#define ELF " "
g_argv=0;
argv[g_argv++]=strtok(command,ELF);
while((bool)(argv[g_argv++]=strtok(nullptr,ELF)))
{}
g_argv--;
return true;
}
void excute()
{
pid_t id=fork();
if(id==-1)
{
perror("fork error\n");
exit(1);
}else if(id==0)
{
//child
execvp(argv[0],argv);
}
int n=waitpid(id,nullptr,0);
(void)n;
return;
}
int main()
{
while (1)
{
//打印命令行提示符
printfcommand();
char command[Command_size];
if (!GetCommand(command, sizeof(command)))
{
continue;
}
//分析指令
AnalyseCommand(command);
//执行命令
excute();
}
return 0;
}
在这里我们实现的xshell知识一个最简单的版本,
我们没有实现内建命令的执行,因为有的指令不能以子进程的身份去执行,比如cd
所以有的指令是需要父进程去执行的
下面是一个内建命令的例子
cpp
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#define Command_size 1024
const int Maxargv = 128;
char *argv[Maxargv];
int g_argv = 0;
char *GetHostName()
{
char *hostname = getenv("HOSTNAME");
return hostname;
}
char *GetUserName()
{
char *UserName = getenv("USER");
return UserName;
}
char *GetPWD()
{
char *pwd = getenv("PWD");
return pwd;
}
void Makecommand(char *command, int size)
{
snprintf(command, size, "[%s@%s:%s]#", GetUserName(), GetHostName(), GetPWD());
}
void printfcommand()
{
char command[Command_size];
Makecommand(command, sizeof(command));
printf("%s", command);
fflush(stdout);
}
bool GetCommand(char *out, int size)
{
char *c = fgets(out, size, stdin);
if (c == nullptr)
{
return false;
}
out[strlen(out) - 1] = 0; // 去除\n
if (strlen(out) == 0) // 判断是否是一个\n指令
{
return false;
}
// 有效指令
return true;
}
bool AnalyseCommand(char *command)
{
// 拆分指令到一个数组中
#define ELF " "
g_argv = 0;
argv[g_argv++] = strtok(command, ELF);
while ((bool)(argv[g_argv++] = strtok(nullptr, ELF)))
{
}
g_argv--;
return true;
}
void excute()
{
pid_t id = fork();
if (id == -1)
{
perror("fork error\n");
exit(1);
}
else if (id == 0)
{
// child
execvp(argv[0], argv);
printf("错误指令\n");
exit(1);
}
int n = waitpid(id, nullptr, 0);
(void)n;
return;
}
// 判断是否为内建命令
bool isandexcutebuiltin()
{
if (strcmp(argv[0], "cd") == 0)
{
if (g_argv == 1)
{
printf("cd: missing operand\n");
}
else if (g_argv > 2)
{
printf("cd: too many arguments\n");
}
else
{
if (chdir(argv[1]) == -1)
{
perror("cd");
}
}
return true;
}
// 还有很多的内建命令
// ...
return false;
}
int main()
{
while (1)
{
// 打印命令行提示符
printfcommand();
char command[Command_size];
if (!GetCommand(command, sizeof(command)))
{
continue;
}
// 分析指令
AnalyseCommand(command);
// 判断是否为内建命令
if (isandexcutebuiltin())
{
continue;
}
// 执行命令
excute();
}
return 0;
}
实现了cd命令
但是呢我们的程序没有实现重定向的实现
管道这些东西
在之后当我们学习了文件我们将会继续完善我们的xshell