目录
进程替换
六种替换函数
自主shell的编写
shell解析命令行字符串的过程
创建makefile
打印输出命令行
获取与分割命令行字符串
多次执行命令
完善工作
完整代码
进程替换
原因:fork创建子进程后,执行的是和父进程相同的代码,但有时可能需要子进程执行不同的代码分支,故子进程往往要调用exec函数来执行另一个程序
基本概念:新程序的代码和数据替换(覆盖)原进程的代码和数据,对原进程的PCB会有稍许修改
注意事项:在一个进程替换后不会创建新进程
单进程发生替换时:
多进程发生替换时:
- 也是execl函数的使用 ,当然除了调用linux自身的程序,还可以调用自己写的程序,比如execl("自定义程序路径","自定义程序名称",NULL)
六种替换函数
基本概念:有六种以exec开头的函数,统称exec函数
包含头文件:<unistd.h>
返回值:调用成功则直接加载新程序代码不返回,调用失败则返回-1
注意事项:不管是数组还是列表都以NULL结尾,只有execve函数是系统调用接口,其余五个替换函数是execve函数的封装(为了支持不同的应用场景)
int execl(const char *path, const char *arg, ...);
//(要执行程序的路径,以命令列表的形式决定如何执行该程序)
int execlp(const char *file, const char *arg, ...);
//(要执行程序的名称,以命令列表的形式决定如何执行该程序),编译器自动寻找路径
int execv(const char *path, char *const argv[]);
//(要执行程序的路径,以字符指针数组的形式决定如何执行该程序)
int execvp(const char *file, char *const argv[]);
//(要执行程序的名称,以字符指针数组的形式决定如何执行该程序),编译器自动寻找路径
int execle(const char *path, const char *arg, ...,char *const envp[]);
//(要执行程序的名称,以字符指针数组的形式决定如何执行该程序,选择环境变量)
//系统调用接口:
int execve(const char *path, char *const argv[], char *const envp[]);
//(要执行程序的路径,以字符指针数组的形式决定如何执行该程序,选择环境变量)
l(list) | 参数采用命令列表的形式 |
v(vector) | 参数采用数组的形式 |
p(path) | 编译器自动搜索执行路径 |
e(env) | 用户自行维护环境变量 |
execv函数:
execvp函数(execlp函数用法相同):
execle函数(execlpe函数用法相同):
- putenv:向环境变量表中新增环境变量
- getenv:获取当前环境变量表中的环境变量
自主shell的编写
shell解析命令行字符串的过程
创建makefile
打印输出命令行
命令行 = [当前用户名 + 当前主机名 + 当前目录名] >
snprintf函数:
函数原型:int snprintf(char *str, size_t size, const char *format, ...)
参数:目标字符串,写入数据的最大长度(包括\0),要写入的字符串
返回值:预写入字符串的长度,而非实际写入字符串长度
功能:将一个或多个字符串格式化后写入指定数组
注意事项:指定数组要足够大,防止内存泄漏
获取与分割命令行字符串
fgets函数:
函数原型:char *fgets(char *str,int num,FILE *stream);
包含头文件:<stdio.h>
参数:要写入的字符数组,读取num个字符直至到达换行符或文件末尾,文件流指针
返回值:执行成功时返回读取的字符串的指针,如果到达文件末尾或发生错误,则返回
NULL
功能:按行获取
注意事项:换行符会使 fgets 停止读取,但它仍被函数视为有效字符,并包含在复制到 str 的字符串中,同时读取结束后会自动添加\0
strtok函数:
函数原型:char * strtok(char * str,const char * delimiters);
包含头文件:<string.h>
参数:源字符串,分割标识字符串
返回值:分割后的字符串的首地址
功能:依据指定标志分割字符串
注意事项:分割标识是字符串不是字符,且在
strtok()
函数的后续调用中,需要将第一个参数设置为 NULL ,它可以让函数从上一次结束位置开始查找下一个匹配项(函数内的特殊实现)
多次执行命令
完善工作
问题1:无法切换路径
原因:不能识别内建命令,cd指令还是子进程在执行,但实际应该是父进程执行的
chdir函数:
函数原型: int chdir(const char *path);
头文件:<unistd.h>
参数:path可以是绝对目录或者相对目录
返回值:成功返回0,失败返回-1
功能:改变当前工作路径
getcwd函数:
函数原型:char *getcwd(char *buf,size_t size);
头文件:<unistd.h>
参数:存放当前工作目录的绝对路径的字符指针,绝对路径大小
返回值:指向绝对路径的字符指针
功能:获取当前目录的绝对路径的大小
在已经定义了<stdlib.h>头文件的前提下却仍然报错“implicit declaration of function ‘putenv’”
则可以在包含头文件前定义_SVID_SOURCE
或_XOPEN_SOURCE:
#define _XOPEN_SOURCE #include <stdlib.h>
或者在编译时(使用
-D
),比如:gcc -o output file.c -D_XOPEN_SOURCE
有点太复杂了懒得描述了,直接上源码
完整代码
#define _XOPEN_SOURCE//用于处理putenv函数缺少声明的问题,且必须定义在stdlib.h之前
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define SIZE 512 //命令行最多字符数
#define NUM 32 //命令行指令数组中最多的指令数
#define TAG " " //标识符
#define ZERO '\0' //处理双重回车问题
#define SkipPath(p) do{p += strlen(p)-1; while(*p != '/') p--;}while(0)//循环遍历到只打印工作路径的最右值
//该函数处理的是传入指针的指向,原来该指针指向一大串字符串,现在我们只让它指向其中最右的一个子串
char cwd[SIZE*2];//环境变量
int lastcode = 0;//错误码
char *Argv[NUM];//命令行字符串数组
//获取当前用户名接口
const char * GetUserName()
{
const char * name = getenv("USER");
if(name == 0) return "None";
return name;
}
//获取当前主机名接口
const char * GetHostName()
{
const char * hostname = getenv("HOSTNAME");
if(hostname == 0) return "None";
return hostname;
}
//获取当前工作路径接口
const char * GetCwd()
{
const char * cwd = getenv("PWD");
if(cwd == 0) return "None";
return cwd;
}
//组合命令行并打印
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
//以格式化的形式向命令行数组中写入
SkipPath(cwd);//指向获取当前工作路径的最右侧的子串内容
snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
//如果子串长度为1,则cwd是\,如果不为1则打印/后的内容
printf("%s",line);
fflush(stdout);
printf("\n");
}
//获取命令行字符串接口
int GetUserCommand(char usercommand[],size_t n)
{
char * str = fgets(usercommand,n,stdin);
if(str == NULL)return -1;
usercommand[strlen(usercommand)-1] = ZERO;//输入命令行字符串时会多输入一个\n,即字符串+'\n'+'\0',这会导致多打一行,现在我们在获取完全部字符串后直接在该字符串后加上'\0'覆盖掉读取到的'\n'
return strlen(usercommand);
}
//分割字符串接口
void SplitCommand(char usercommand[],size_t n)
{
Argv[0] = strtok(usercommand,TAG);
int index = 1;
while((Argv[index++] = strtok(NULL,TAG)));//循环赋值并判断
}
//执行命令接口
void ExecuteCommand()
{
pid_t id = fork();
if(id == 0)
{
//child
execvp(Argv[0],Argv);//(要执行的程序,怎么执行该程序)
exit(errno);//execvp函数替换失败后会返回一个错误码,退出的退出码就是该错误码
}
else
{
//father
int status = 0;
pid_t rid = waitpid(id,&status,0);//父进程阻塞等待
if(rid > 0)
{
lastcode = WEXITSTATUS(status);//子进程的退出码lastcode
if(lastcode != 0)printf("%s:%s:%d\n",Argv[0],strerror(lastcode),lastcode);
}
}
}
//获取家目录接口
const char * GetHome()
{
const char * home = getenv("HOME");
if(home == NULL) return "/";//找不到就放到根目录下
return home;//根目录不为空则返回家目录
}
//执行内建命令cd
void Cd()
{
const char *path = Argv[1];//选择怎么执行cd程序
if(path == NULL)//如果只有一个单独的cd指令,则path存放家目录信息
path = GetHome();
chdir(path);//更改当前工作目录
//刷新环境变量(即刷新[]最后的一个内容)
char temp[SIZE*2];//临时缓冲区
getcwd(temp,sizeof(temp));//获取当前工作目录的绝对路径
snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
putenv(cwd);//向环境变量表中写入
}
//检测是否是内建指令
int CheckBuildin()
{
int yes = 0;
const char * enter_cmd = Argv[0];//指向数组中存放的要执行的程序名
if(strcmp(enter_cmd,"cd") == 0)
{
yes = 1;
Cd();//是内建命令就执行
}
else if(strcmp(enter_cmd,"echo") == 0 && strcmp(Argv[1],"$?") == 0)
{
yes = 1;
printf("%d\n",lastcode);//打印退出码
lastcode = 0;
}
return yes;//返回判断结果
}
int main()
{
int quit = 0;
while(quit == 0)//多次执行命令
{
//1、获取命令行并打印
MakeCommandLineAndPrint();
//2、获命令行取字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n <= 0) return 1;//获取字符串失败或者获取字符串长度为0时退出,退出码为1
//3、分割命令行字符串
SplitCommand(usercommand,sizeof(usercommand));
//4、检测是否是内建命令
n = CheckBuildin();//这里的指令还是由父进程执行的
if(n) continue;//如果是内建命令就在检测函数中执行完内建目命令后返回循环开始处,而不是执行普通命令
//5、执行命令
ExecuteCommand();//此时才会创建子进程
}
}
~over~