上篇文章:Linux:进程程序替换
前言
本篇文章主要用于串联之前的Linux文章内容,加深学习印象
目录
实现原理
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。所以要写一个shell,需要循环以下过程:
1.获取命令行
2.解析命令行
3.建立一个子进程 (fork)
4.替换子进程 (execvp)
5.父进程等待子进程退出 (wait)

输出命令行
命令行示例:

获得用户名等信息使用getenv:

此时myshell.c:
1 #include "myshell.h"
2
3 #include <stdlib.h>
4 #include <string.h>
5 #include <unistd.h>
6
7 // 命令行相关
8 static char username[32]; // 用户名
9 static char hostname[64]; // 主机名
10 static char cwd[256]; // 当前路径名
11
12 static void GetUserName()
13 {
14 char *_username = getenv("USER");
15 strcpy(username,(_username ?_username : "None"));
16 }
17
18 static void GetHostName()
19 {
20 char *_hostname = getenv("HOSTNAME");
21 strcpy(hostname,(_hostname ?_hostname : "None"));
22 }
23
24 static void GetCwdName()
25 {
26 char *_cwd = getenv("PWD");
27 strcpy(cwd,(_cwd ?_cwd : "None"));
28 }
29
30 void PrintPrompt() // 提示符
31 {
32 GetUserName();
33 GetHostName();
34 GetCwdName();
35 printf("[%s@%s %s]# ", username, hostname, cwd);
36 fflush(stdout);
37 }
38
39 void Bash()
40 {
41 while(1)
42 {
43 // 第一步:输出命令行
44 PrintPrompt();
45 sleep(1);
46
47 }
48 }
main.c
1 #include "myshell.h"
2
3 int main()
4 {
5 Bash();
6 return 0;
7 }
myshell.h
1 #pragma
2
3 #include <stdio.h>
4
5 void Bash();
此时的输出样例,没有换行,路径显示为绝对路径

获取用户输入
首先要明确,在等待用户输入时,不能使用scanf输入,因为我们打入的ls -a -l实际上是一个字符串"ls -a -l",而scanf以空格作为分隔符。所以我们使用fgetc:

特殊处理:要让我们输入命令时命令本身不带\n
见下方代码,因为stdin带有\n,但是我们要让自己输入的命令字符串不带\n,\n应该由printf中的内容提供

修改后:
此处commandline不可能等于零,因为在fgets中至少会带有一个\n

结果:

解析字符串
char *argv[]做为命令行参数

解析命令是重复进行的事件,所以先将全局变量做初始清空
**接口strtok:**可以将传入的字符串进行你的要求切割(一次返回一个token)


第一次,传入字符串本身,第二次之后,如果还想切割后续字符串,参数为NULL->把正在切割的字符串记录下来->使用static变量即可
测试下方代码:

为空时显示为1:

再修改代码:

执行命令

75 void Execute()
76 {
77 pid_t id = fork();
78 if(id < 0)
79 {
80 perror("fork");
81 return;
82 }
83 else if(id == 0)
84 {
85 // child
86 // 程序替换
87 execvp(argv[0], argv);
88 exit(1);
89 }
90 else{
91 // father
92 pid_t rid = waitpid(id, NULL, 0);
93 (void)rid;
94 }
95 }
内建命令
我们的cd等命令并没有作用,原因是通过fork函数,这些命令都由子进程执行,所以只会影响子进程而不是父进程,所以应该通过内建命令(bash内部的函数)由父进程执行。


获得实时路径
根据上述运行结果,发现提示符相关的内容并没有根据路径的变化而变化,原因是我们使用getenv获取环境变量的方式,并不会实时改变。

所以我们不采用环境变量,使用getcwd获得



优化


记录退出码

122 int CheckBuiltinAndExecute()
123 {
124 int ret = 0;
125 if(strcmp(argv[0], "cd") == 0)
126 {
127 // 内建命令
128 ret = 1;
129 if(argc == 2)
130 {
131 chdir(argv[1]);
132 }
133 }
134 else if(strcmp(argv[0], "echo") == 0)
135 {
136 ret = 1;
137 if(argc == 2)
138 {
139 if(argv[1][0] == '$')
140 {
141 if(strcmp(argv[1], "$?") == 0)
142 {
143 printf("%d\n", lastcode);
144 lastcode = 0;
145 }
146 else
147 {
148 // env
149 }
150 }
151 else
152 {
153 printf("%s\n", argv[1]);
154 }
155 }
156 }
157 return ret;

环境变量




优化环境变量

将上述的环境变量放在bash中,启动时从系统env拷贝所有环境变量的指针到自定义的_environ数组中。
源码
myshell.c
#include "myshell.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
// 提示符相关
static char username[32]; // 用户名
static char hostname[64]; // 主机名
static char cwd[256]; // 当前路径名
static char commandline[256];
// 命令行相关
static char *argv[64];
static int argc = 0;
static const char *sep = " ";
// 与退出码相关
static int lastcode = 0;
// 环境变量相关
static char **_environ;
static int envc = 0;
static void GetUserName()
{
char *_username = getenv("USER");
strcpy(username,(_username ?_username : "None"));
}
static void GetHostName()
{
char *_hostname = getenv("HOSTNAME");
strcpy(hostname,(_hostname ?_hostname : "None"));
}
static void GetCwdName()
{
//char *_cwd = getenv("PWD");
//strcpy(cwd,(_cwd ?_cwd : "None"));
char _cwd[256];
getcwd(_cwd, sizeof(_cwd));
if(strcmp(_cwd, "/") == 0)
{
strcpy(cwd, _cwd);
}
else
{
int end = strlen(_cwd) - 1;
while(end >= 0)
{
if(_cwd[end] == '/')
{
strcpy(cwd, &_cwd[end+1]);
break;
}
end--;
}
}
}
static void PrintPrompt() // 提示符
{
GetUserName();
GetHostName();
GetCwdName();
printf("[%s@%s %s]# ", username, hostname, cwd);
fflush(stdout);
}
static void GetCommandLine()
{
if(fgets(commandline, sizeof(commandline), stdin) != NULL)
{
commandline[strlen(commandline) - 1] = 0;
//printf("debug: %s\n", commandline);
}
}
static void ParseCommandLine()
{
// 清空
argc = 0;
memset(argv, 0, sizeof(argv));
// 判空
if(strlen(commandline) == 0)
return;
// 解析
argv[argc] = strtok(commandline, sep);
while((argv[++argc] = strtok(NULL, sep)));
//printf("argc: %d\n", argc);
//int i = 0;
//for(; argv[i]; i++)
//{
// printf("argv[%d]: %s\n", i, argv[i]);
//}
}
void Execute()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if(id == 0)
{
// child
// 程序替换
execvp(argv[0], argv);
exit(1);
}
else{
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
(void)rid;
lastcode = WEXITSTATUS(status);
}
}
// 1: yes
// 0: no,普通命令让后续执行
int CheckBuiltinAndExecute()
{
int ret = 0;
if(strcmp(argv[0], "cd") == 0)
{
// 内建命令
ret = 1;
if(argc == 2)
{
chdir(argv[1]);
}
}
else if(strcmp(argv[0], "echo") == 0)
{
ret = 1;
if(argc == 2)
{
if(argv[1][0] == '$')
{
if(strcmp(argv[1], "$?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
// env
}
}
else
{
printf("%s\n", argv[1]);
}
}
}
else if(strcmp(argv[0], "env") == 0)
{
ret = 1;
int i = 0;
for(; i < envc; i++)
{
printf("%s\n", _environ[i]);
}
}
else if(strcmp(argv[0], "export") == 0)
{
ret = 1;
if(argc == 2)
{
char *mem = (char*)malloc(strlen(argv[1])+1);
strcpy(mem,argv[1]);
_environ[envc++] = mem;
_environ[envc] = NULL;
}
}
return ret;
}
static void LoadEnv()
{
extern char **environ;
for(envc = 0; environ[envc]; envc++)
{
_environ[envc] = environ[envc];
}
_environ[envc] = NULL;
}
void Bash()
{
// 环境变量相关,由bash维护,从系统配置文件读,我们从系统bash拷贝
static char *env[64];
_environ = env;
// 获取环境变量
LoadEnv();
while(1)
{
// 第一步:输出命令行
PrintPrompt();
// 第二步:等待用户输入,获取用户输入
GetCommandLine();
// 第三步:解析字符串
ParseCommandLine();
if(argc == 0)
continue;
// 第四步:内建命令
if(CheckBuiltinAndExecute())
{
continue;
}
// 第五步:执行命令
Execute();
}
}
main.c
#include "myshell.h"
int main()
{
Bash();
return 0;
}
myshell.h
#include "myshell.h"
int main()
{
Bash();
return 0;
}
[xxx404@VM-16-10-centos test2_2]$ cat myshell.h
#pragma
#include <stdio.h>
void Bash();
本章完。