Linux 12mybash的实现
该文章将讲解在linux终端下实现自己的终端命令解释器
目录
核心概念
fork 复制进程:
exec进程替换:
获取用户信息:
getuid()
getpwuid(uid):
getcwd()
gethostname();
颜色实现
字符串分割函数strtok()
mybash实现步骤
1.创建打印命令终端提示符
2.键盘输入信息分解函数
3.主函数实现
完整代码:
在实现之前我们需要先了解如下概念:
核心概念
fork 复制进程:
核心:
1.fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
2.子进程会复制父进程的几乎所有资源(代码段、数据段、堆、栈、文件描述符表、信号处理方式等)。
3.若父进程未调用 wait()
或 waitpid()
回收子进程,子进程终止后会成为僵尸进程(保留 PID 和退出状态),需通过信号或进程管理机制避免。
具体想要学习fork可以参考L8-fotk进程复制
exec进程替换:
核心:
1.exec进程替换是指一个正在运行的进程,通过特定系统调用加载并执行新的程序代码,完全替换当前进程的代码段、数据段、堆和栈,仅保留进程 ID(PID)等核心属性。
2.进程替换调用函数
系统调用 | 功能描述 |
---|---|
execl | 以列表形式传递命令参数(参数个数固定) |
execlp | 与 execl 类似,但会在 PATH 环境变量中搜索可执行文件 |
execle | 以列表形式传递参数,并显式指定环境变量 |
execv | 以数组形式传递命令参数(参数个数可变) |
execvp | 与 execv 类似,会在 PATH 环境变量中搜索可执行文件 |
execvpe | 以数组形式传递参数,显式指定环境变量,且支持 PATH 搜索 |
execve | 最底层的系统调用,其他 exec 函数均为其封装,需指定程序路径、参数数组、环境数组 |
// "/usr/bin/ps",新程序的绝对路径或相对路径(必须明确指定路径)。
//"ps""-f"第一个参数通常是程序名(与 path 对应),后续是实际参数,最后以 (char*)0 结束。execl("/usr/bin/ps","ps","-f",(char*)0);
//同execl一样 path"ps"会自动从环境变量 PATH 中查找程序,无需指定完整路径。execlp("ps","ps","-f",(char*)0);
//允许自定义新程序的环境变量,而非使用当前进程的环境变量。程序的完整路径(同 execl)。
//最后一个参数 envp:自定义的环境变量数组(格式为 {"KEY=VALUE", ..., (char*)0})。char*myenvp[]={"KEY=VALUE",0};execle(path,"ps","-f",(char*)0,envp);
//path:程序的完整路径(同 execl)。
//argv:参数数组,格式为 {"程序名", "参数1", ..., (char*)0}。char*myargv[]={"ps","-f",0};execv(path,myargv);
//结合了 v(数组传参)和 p(自动查 PATH)的特性。
//file:程序名(从 PATH 中查找)。
//myargv:参数数组(同 execv)execvp(file,myargv);
//最底层的进程替换函数
//"usr/bin/ps/"第一个参数为可执行文件的路径(绝对路径或相对路径)
//myargv命令参数数组(如 ["ls", "-l", NULL]),数组末尾必须以 NULL 结束。
//envp环境变量数组execve("/usr/bin/ps",myargv,envp);
具体想要学习exec可以参考LIUNX 11进程替换
扩展:
获取用户信息:
getuid()
getuid()
是 Unix/Linux 系统中用于获取当前进程实际用户 ID(UID) 的系统调用函数,定义在 <unistd.h>
头文件中。实际用户 ID 用于标识进程的所有者,是系统进行权限检查的重要依据。
UID 是系统中用于唯一标识用户的整数:
0
:超级用户(root),拥有系统最高权限。
1~999
:系统用户(如 daemon、ftp 等,用于运行系统服务)。
1000+
:普通用户(由管理员创建的用户账号)。
getpwuid(uid):
#include<sys/types.h>
#include<pwd.h>
struct passwd *getpwuid(uid_t uid);
成功:返回指向 struct passwd 结构体的指针(该结构体由系统静态分配,后续调用可能覆盖内容)。
失败:返回 NULL,并设置 errno(如 UID 不存在时,errno 为 ESRCH)。
getcwd()
getcwd()
是 C 语言中用于获取当前工作目录(Current Working Directory)绝对路径的标准库函数。
#include <unistd.h>char *getcwd(char *buf, size_t size);
-
参数说明:
buf
:用于存储当前目录路径的缓冲区。若为NULL
,函数会自动分配足够大小的内存。size
:缓冲区的大小(字节数)。若buf
为NULL
,size
需设为0
。
-
返回值:
- 成功:返回指向存储路径的缓冲区指针(与
buf
相同,或函数分配的内存)。 - 失败:返回
NULL
,并设置errno
(如路径过长、权限不足)
- 成功:返回指向存储路径的缓冲区指针(与
gethostname();
gethostname()获取当前主机信息
颜色实现
// 定义颜色和样式常量
#define RESET "\033[0m"
#define RED "\033[31m"
#define GREEN "\033[32m"
#define BOLD "\033[1m"
#define YELLOW_BACKGROUND "\033[43m"
#define BLUE "\033[34m"
字符串分割函数strtok()
在 C 语言中,strtok
(string tokenize,字符串分割)是用于按指定分隔符拆分字符串的标准库函数,定义在 <string.h>
头文件中,广泛用于解析命令行参数、日志文件、CSV 数据等场景。其核心特点是通过 “替换分隔符为字符串结束符 \0
” 实现拆分,并通过静态变量记录分割位置,支持多轮分割。
char *strtok(char *str, const char *delim);
str
:待分割的字符串。
首次调用时:传入完整的待分割字符串(如 "a,b;c d"
)。
后续调用时:传入 NULL
,strtok
会通过内部静态变量读取上一次分割的 “中断位置”,继续分割剩余字符串。
delim
:分隔符集合(字符串形式),例如 ",; "
表示 “逗号、分号、空格” 均为分隔符。
返回值:
成功:返回当前拆分出的 “子串(token)” 的指针。
失败 / 分割结束:返回 NULL
(表示已无更多子串可拆分)。
- 首次调用时,
strtok
从str
开头跳过连续分隔符,找到第一个非分隔符字符(子串的起始位置)。 - 继续向后扫描,遇到第一个分隔符时,将该分隔符替换为
\0
(使当前子串终止),并记录下一个字符的位置(供后续调用使用)。 - 后续调用传入
NULL
时,strtok
从上次记录的位置继续重复上述过程,直到字符串末尾。
mybash实现步骤
1.创建打印命令终端提示符
实现结果:
我们封装一个Print函数用来实现终端打印函数
first:我们使用getuid函数获取当前实际用户UID并用char*变量a识别是普通用户还是超级管理员
second:用struct passwd* ptr接受当前用户UID的各种信息,用getpwuid函数实现
third:使用getcwd函数实现获取当前目录路径
forth:使用gethostname获取当前主机信息
fifth:在printf函数中添加颜色。
void Print(){
int uid=getuid();char*a="$";if(uid==0){a="#";}struct passwd*ptr=getpwuid(uid);if(ptr==NULL){printf("$");fflush(stdout);return;}char fixed_buf[1024];if (getcwd(fixed_buf, sizeof(fixed_buf)) == NULL) {perror("固定缓冲区获取目录失败(可能路径过长)");return 1;}char hostname[128];gethostname(hostname,127);printf("%s%s%s%s:~%s$%s%s%s ",BOLD,GREEN,ptr->pw_name,hostname,BLUE,fixed_buf,RESET,a);fflush(stdout);
}
2.键盘输入信息分解函数
first:buff为键盘输入函数,s为需要存入的参数数组
second:使用strtok进行分割
char* get_str(char*buff,char**s){if(buff==NULL||s==NULL){return NULL;}int i=0;char*a=strtok(buff," ");while(a!=NULL){s[i++]=a;a=strtok(NULL," ");}return s[0];
}
3.主函数实现
first:我们从键盘中输入需要执行的命令并将其保存在buff中,并将输入的\n换成\0
second:我们利用进程替换函数execvp只需要路径名和参数数组即可
third:我们利用自己实现的字符串分割函数,用get_str函数获取路径名,参数列表保存在myargv中
forth:使用fork创建新进程,pid==0execvp替换
fifth:使用wait()函数等待子进程结束
int main(){while(1){Print();char buff[128]={0};fgets(buff,127,stdin);buff[strlen(buff)-1]=0;if(buff[0]=='\0'){printf("\n");continue;}char*myargv[128]={0};char*cwd=get_str(buff,myargv);pid_t pid=fork();if(pid==-1){perror("pid err");exit(1);
}if(pid==0){execvp(cwd,myargv);perror("exec err");exit(0);}wait(NULL);
}exit(0);
}
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<pwd.h>
#include<sys/wait.h>
// 定义颜色和样式常量
#define RESET "\033[0m"
#define RED "\033[31m"
#define GREEN "\033[32m"
#define BOLD "\033[1m"
#define YELLOW_BACKGROUND "\033[43m"
#define BLUE "\033[34m"
char* get_str(char*buff,char**s){if(buff==NULL||s==NULL){return NULL;}int i=0;char*a=strtok(buff," ");while(a!=NULL){s[i++]=a;a=strtok(NULL," ");}return s[0];
}
void Print(){int uid=getuid();char*a="$";if(uid==0){a="#";}struct passwd*ptr=getpwuid(uid);if(ptr==NULL){printf("$");fflush(stdout);return;}char fixed_buf[1024];if (getcwd(fixed_buf, sizeof(fixed_buf)) == NULL) {perror("固定缓冲区获取目录失败(可能路径过长)");return 1;}char hostname[128];gethostname(hostname,127);printf("%s%s%s%s:~%s$%s%s%s ",BOLD,GREEN,ptr->pw_name,hostname,BLUE,fixed_buf,RESET,a);fflush(stdout);
}
int main(){while(1){Print();char buff[128]={0};fgets(buff,127,stdin);buff[strlen(buff)-1]=0;if(buff[0]=='\0'){printf("\n");continue;}char*myargv[128]={0};char*cwd=get_str(buff,myargv);pid_t pid=fork();if(pid==-1){perror("pid err");exit(1);
}if(pid==0){execvp(cwd,myargv);perror("exec err");exit(0);}wait(NULL);
}exit(0);
}