os实验报告2-shell的实现

Shell 在 Minix3中的实现

目的

Shell主体结构是一个while循环,不断地接受用户键盘输入行并给出反馈。Shell 将输入行分解成单词序列,根据命令名称分为二类分别处理 ,即shell内置命令(例如cd,history,exit)和 program 命令 (例如/bin/目录下的ls,grep 等) 。识别为shell内置命令后,执行对应操作。接受program命令后,利用minix自带的程序创建一个或多个新进程,并等待进程结束。如果末尾包含&参数,Shell可以不等待进程结束,直接返回。

Program命令

运行程序:利用fork调用创建进行子进程,利用execvp调用将minix程序装载到该进程,并赋予运行参数,最后Shell利用wait/waitpid调用等待子进程结束。(参见UNIX高级编程8.3,8.7和8.10节)

参考资料:

  1. Shell主要是为用户提供一个命令解释器,接收用户命令(如ls等),然后调用相应的应用程序。实现的Shell支持后台进程的运行。
  2. 在计算内存和CPU总体使用情况时,可参考top命令实现 https://github.com/0xffea/MINIX3/blob/master/usr.bin/top/top.c
  3. 推荐《UNIX环境高级编程》查找详细的系统调用使用方法。

1.实现带参数的程序运行功能

program arg1 arg2 ... argN

2. 重定向功能, 将文件作为程序的输入/输出

  • “>” 表示覆盖写 program arg1 arg2 … argN > output-file
  • “>>” 表示追加写 program arg1 arg2 … argN >> output-file
  • “<” 表示文件的输入 program arg1 arg2 … argN < input-file

重定向:minix为每个进程赋予键盘输入和控制台输出的文件描述符默认为0和1。子进程装载程序前,利用close(0 or 1)将默认输入或者输出关闭,再调用dup(fd)将某个打开文件的文件描述fd映射到标准输入或输出。(参见UNIX高级编程3.12节)

3. 管道符号 “|”

在程序间传递数据 programA arg1 … argN | programB arg1 … argN

若有n个子进程组成管道流,Shell在fork前先用pipe调用创建n-1对管道描述符,关闭不需要的读写端。Shell运行fork后,每个子进程利用dup将前一个管道的读端映射到标准输入,后一个管道的写端映射到标准输出。(参见UNIX高级编程15.2节)

在实现管道时,需要用到pipe和dup系统调用等,重定向标准向输入输出(参见教材1.4.3节)。

4. 后台符号&

表示此命令将以后台运行的方式执行 program arg1 arg2 … argN &

后台运行:为了屏蔽键盘和控制台,子进程的标准输入、输出映射成/dev/null。子进程调用signal(SIGCHLD,SIG_IGN),使得minix接管此进程。因此Shell可以避免调用wait/waitpid直接运行下一条命令。

Shell 内置命令

1. 工作路劲移动命令 cd

提示: 因为Shell也是一个程序,启动时minix会分配一个当前工作目录,利用chdir系统调用可以移动Shell的工作目录。

2. 程序运行统计mytop

提示:参考minix终端输入top命令的输出信息,在minix系统/proc文件夹中通过open/read系统调用输出进程信息。

  • /proc/meminfo中,查看内存信息,每个参数对应含义依次是页面大小pagesize总页数量total 空闲页数量free最大页数量largest缓存页数量cached。可计算内存大小:$(pagesize ~*~total)/1024$,同理算出其他页内存大小。
  • /proc/kinfo中,查看进程和任务数量
  • /proc/pid/psinfo中,例如/proc/107/psinfo文件中,查看pid为107的进程信息。每个参数对应含义依次是:版本version类型type端点endpt名字name状态state阻塞状态blocked动态优先级priority,滴答ticks,高周期highcycle,低周期lowcycle,内存memory,有效用户ID effuid,静态优先级nice等。其中会用到的参数有:类型,状态,滴答。进程时间$time=\frac{ticks}{(u32_t)60}$。

输出内容:

  • 总体内存大小,空闲内存大小,缓存大小
  • 总体CPU使用占比。计算方法:得到进程和任务总数量total_proc,对每一个proc的ticks累加得到总体ticks,再计算空闲的ticks,最终可得到CPU使用百分比。

3. shell 退出命令 exit

提示: 退出Shell的while循环,结束Shell的main函数。

4. history n 显示最近执行的n条指令

提示: 保存Shell每次的输入行,打印所需字符串即可

测试用例

1
2
3
4
5
6
7
8
9
10
cd /your/path
ls -a -l
ls -a -l > result.txt
vi result.txt
grep a < result.txt
ls -a -l | grep a
vi result.txt &
mytop
history n
exit

内容与设计思想

这个实验主要是在Minix里面自己实现一个简单的shell,并实现一些基本的命令。

我会将整个project分割成很多小片段来一一实现。

使用环境

虚拟机: Guest OS

虚拟机软件: Oracle VirtualBox

物理机: Windows10

实验过程

首先我们先介绍一下 main函数的实现思路,然后再详细分析main函数中调用的各种函数

宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define _PATH_PROC "/proc/"
#define PSINFO_VERSION 0
#define TYPE_TASK 'T'
#define TYPE_SYSTEM 'S'
#define TYPE_USER 'U'
#define STATE_RUN 'R'
#define TIMECYCLEKEY 't'
#define ORDERKEY 'o'

#define USED 0x1
#define IS_TASK 0x2
#define IS_SYSTEM 0x4
#define BLOCKED 0x8

#define ORDER_CPU 0
#define ORDER_MEMORY 1
#define ORDER_HIGHEST ORDER_MEMORY

#define TC_BUFFER 1024 /* Size of termcap(3) buffer */
#define TC_STRINGS 200 /* Enough room for cm,cl,so,se */


/* name of cpu cycle types, in the order they appear in /psinfo. */
//cpu 周期的种类,
const char *cputimenames[] = { "user", "ipc", "kernelcall" };
#define CPUTIMENAMES (sizeof(cputimenames)/sizeof(cputimenames[0]))

#define CPUTIME(m, i) (m & (1L << (i)))

全局变量

1
2
3
4
5
6
7
8
9
char *message;  //用于输出当前的路径
int whilecnt=0; //用于记载已输入的命令条数,为history n做准备
char msg[40][256];//用于记载已输入的命令信息,为history n做准备
unsigned int nr_procs, nr_tasks;//进程数和任务数
int nr_total;//进程和任务总数
int order = ORDER_CPU;//
int blockedverbose = 0;
char *Tclr_all;
int slot=1; //这个变量是进程结构体在数组中的位置

main函数

在main函数中,主要是实现一个循环。

首先我们要定义一些参数:

wordcount 即输入命令中的单词的个数

wordmatrix 即一个单词数组,每一行存储一个单词,每一列头字符为\0

buf 即存放我们命令的缓冲数组。

首先我们需要判断系统中还有没有足够的空间来存放下我们的语句。当空间不足的时候,malloc函数会返回NULL指针,然后我们就返回 malloc failed 并推出shell。这里,perror 是C语言标准库 <stdio.h> 的一个函数,它会将一个描述性错误消息输出到stderr .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char **argv){
//...
int i;
int wordcount = 0;
char wordmatrix[100][256];
char **arg = NULL;
char *buf = NULL; //用户输入

if((buf = (char *)malloc(256))== NULL){
perror("malloc failed");
exit(-1);
}
//...
}

接下来是循环的主体。 首先我们利用memset函数将buf 数组初始化清零。

然后我们要给 message申请一段空间。这里,message是全局变量

,通过 getcwd这个函数来获取当前所在的路径并通过printf打印出来。比如说,当前我正在 proj1_shell 文件夹当中启动 我们的shell,如下图所示:

然后我们需要将 message 释放掉,因为每次循环我们都会重新申请message数组,如果不将其释放会产生很多垃圾、占用我们的虚拟机空间。

既然下来我们就要利用getOrder()函数从命令行中读取命令,并将其存储在我们刚刚声明的 buf数组当中。还需要将buf数组拷贝到msg二维数组中做一个备份,当收到history n的命令后,会从这个数组中查看之前输入过的命令。

如果输入的是exit命令,那么就直接跳出循环。

然后我们对 wordmatrix 二维数组的每一行的开头都设置为\0 (头插法),为接下来解析命令做准备。 anaOrder()即 analyze order的缩写,其目的就是解析buf中的命令,并将结果存入到wordmatrix数组当中去。

将命令全部解析完成之后,我们调用 execommand函数,也就是执行我们解析完的命令。这也是我们待会要着重分析的函数。因为所有的 内置命令、Program 命令都在这个函数中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(int argc, char **argv){
//...
while(1){
memset(buf, 0, 256); //将buf所指的空间清零
message = (char *)malloc(100);
getcwd(message, 100);
printf("os_shell:%s # ", message);
free(message);
getOrder(buf);
strcpy(msg[whilecnt],buf);//whilecnt一开始默认为0
whilecnt++;
if( strcmp(buf, "exit\n") == 0 ){
break;
}
for(i = 0; i < 100; i++){
wordmatrix[i][0] = '\0';
}
wordcount = 0;
anaOrder(buf, &wordcount, wordmatrix);
exeCommand(wordcount, wordmatrix);
}
//...
}

最后,当循环结束的时候,我们对buf数组进行回收。

1
2
3
4
5
6
7
8
int main(int argc, char **argv){
//...
if(buf != NULL){
free(buf);
buf = NULL;
}
return 0;
}

getOrder函数

getOrder函数是从命令行窗口读取我们输入的命令、并将命令存放在buf数组当中。

首先我们要了解我们输入的命令长什么样: 有一些单词、符号,中间用空格隔开,然后以\n换行符号结尾。

我们使用 getchar()函数来单字符读取命令依次存储到buf数组,buf数组最后两个符号分别是\0(结束符)和\n (换行符)。

如果输入的命令过长,导致计数器已经到达256了,那么就会报错并退出函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void getOrder(char *buf){
int cnt = 0;
int c = getchar();
while(cnt < 256 && c != '\n'){
buf[cnt++] = c;
c = getchar();
}

buf[cnt++]='\n';
buf[cnt]='\0';
if(cnt == 256){
exit(-1);
}
}

anaOrder函数

这个函数会解析buf中的命令,所谓解析,就是利用空格符,将buf中的命令一个单词一个单词的拆开,然后存放到wordmatrix当中去。

这个函数的整体架构是由一个双重循环组成的。大循环解析整个命令,小循环提取出命令中每一个word。

首先我们令 数组p和数组q都等于buf数组。

如果p数组一开始就是回车,说明我们什么命令都没输入或者命令已经解析完成,那么就直接跳出循环;如果命令前面字符都是空格,那么我们就从非空格字符的地方开始解析。

当排除了空白输入以及空格符号之后,我们令q=p ,计数器number等于0。然后一直读取到一个单词结束,这时候 number就等于这个单词的长度。接下来我们利用 strncpy 函数将p中的前number+1个字节复制到wordmatrix中属于这个单词的位置当中(*wordcount从0开始)——为什么要number+1呢?因为这时候第number+1个字符要么是''要么是'\n' 因此也要复制进去,方便之后将最后一个字符改成\0结束符。

最后,将单词计数器wordcount加上1,然后令p等于q,相当于砍掉了一个单词。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void anaOrder(char* buf, int* wordcount, char (*wordmatrix)[256]){
char *p = buf;
char *q = buf;
int number = 0;
int i;
while(1){
if(p[0] == '\n'){
break;
}
if(p[0] == ' '){
p++;
}
else{
q = p;
number = 0;
while((q[0] != ' ') && (q[0] != '\n')){
number++;
q++;
}
strncpy(wordmatrix[*wordcount], p, number + 1);
wordmatrix[*wordcount][number] = '\0';
*wordcount = *wordcount + 1;
p = q;
}
}
}

exeCommand函数

exeCommand函数是整个shell中最关键也是最复杂的函数。因代码过长,我们将其拆分成数个小部分一一分析。

准备阶段

这一部分我们定义了一些变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void exeCommand(int wordcount, char (*wordmatrix)[256]){
pid_t pid; // 进程号
char *file; // 文件名称
int correct = 1;// 格式判断标志
int way = 0; // 符号分类器
int status; // 程序
int i;
int fd;
char *word[wordcount + 1];
char *wordnext[wordcount + 1];//存放管道运算符后面的命令

//将命令取出
for(i = 0; i < wordcount; i++){
word[i] = (char *)wordmatrix[i];
}
word[wordcount] = NULL;
//...
}

然后,我们通过一个循环将wordmatrix中的每个单词都复制到word指针数组当中去。并将最后一个指针设置为NULL

cd

如果我们命令的第一个单词为cd ,那么我们就看第二个word是什么。如果第二个word是NULL,说明我们的命令就是cd\n那就直接返回;否则我们就利用 chdir这个函数改变当前的目录、转到新的目录,如果返回-1就说明该目录不存在,于是我们调用perror抛出一个错误。

1
2
3
4
5
6
7
8
9
10
11
12
void exeCommand(int wordcount, char (*wordmatrix)[256]){
//...
if(strcmp(word[0], "cd") == 0){//返回0代表word[0] = "cd"
if(word[1] == NULL){
return ;
}
if(chdir(word[1]) == -1){
perror("cd");
}
return ;
}
}

history n

如果我们命令的第一个word 是 history ,那么我们就要显示最近执行的n条指令。 这n条命令在main函数的时候已经被存到msg数组当中去了,我们现在要做的就是个打印的操作。

比如说我输入的命令是history 15,那么这时候 word[1] 就是 (char*)15 ,然后我们就需要将字符数组转换为整数——这里我们使用atoi函数。我们在main函数中用whilecnt来作为 命令的计数器, whilecnt的值就是当前msg 数组中存放的命令数量+1, 所以我们输出的5条是从 whilecnt-1开始计算,一直到 whilecnt-6结束,然后将命令依次打印输出。

注意,history和mytop 既可以创建一个子进程让其运行,也可以直接在父进程中运行。 但是因为我将program命令和shell内置命令放在一个 execommand函数中实现了。 如果这里不创建一个子进程,直接在父进程中运行,那么后面会出现奇奇怪怪的打印问题,因此为了安全和方便起见,我们这里也将其放到一个子进程当中去运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    if((pid = fork()) < 0){
printf("fork error\n");
return ;
}
if(strcmp(word[0],"history")==0){
if(pid==0){
char *cnum = word[1];
int num = atoi(cnum);
printf("%d", num);
for (int i = whilecnt - 1; i > whilecnt - num - 1; i--) {
printf("command %d:", i);
printf("%s", msg[i]);
}
exit(0);
}
}

效果如下:

mytop

这个函数需要输出的东西异常的多:

  • 总体内存大小、空闲内存大小、缓存大小、总体CPU使用占比

我们用不同的函数来实现这些信息

1
2
3
4
5
6
7
8
9
10
11
12
if(strcmp(word[0], "mytop") == 0){
if(pid==0){
int cputimemode = 1;
getkinfo();
print_memory();
get_procs();
if (prev_proc == NULL)
get_procs();
print_procs(prev_proc, proc, cputimemode);
exit(0);
}
}

这个函数就是打印出当前主存的使用情况。

首先我们打开 meminfo这个文件:

我们发现这里有5个数据,依次是:页面大小、总的页数、空闲页数、最大连续页、缓存页数量

那么这里我们就可以以此计算总的内存、空闲内存、最大页的内存、缓存。计算公式为:$(PAGESIZE * TOTAL)/1024$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int print_memory()
{
FILE *fp;
unsigned int pagesize;
unsigned long total, free, largest, cached;

if ((fp = fopen("/proc/meminfo", "r")) == NULL)
return 0;

if (fscanf(fp, "%u %lu %lu %lu %lu", &pagesize, &total, &free,
&largest, &cached) != 5) {//如果不是五个参数就说明读取失败
fclose(fp);
return 0;
}

fclose(fp);

printf("main memory: %ldK total, %ldK free, %ldK contig free, "
"%ldK cached\n",
(pagesize * total)/1024, (pagesize * free)/1024,
(pagesize * largest)/1024, (pagesize * cached)/1024);

return 1;
}

到这一步,我们可以试试看效果如何:

getkinfo();

这个函数就是读取/proc/kinfo得到总的进程和任务数。首先我们来看看 /proc/kinfo 中的记载的记录:

其中,256就代表进程数,5代表任务数量

然后我们将这两个数字读入。并返回 进程数与任务数量 之和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 void getkinfo(){
FILE *fp;

if ((fp = fopen("/proc/kinfo", "r")) == NULL) {
fprintf(stderr, "opening " _PATH_PROC "kinfo failed\n");
exit(1);
}

if (fscanf(fp, "%u %u", &nr_procs, &nr_tasks) != 2) {
fprintf(stderr, "reading from " _PATH_PROC "kinfo failed\n");
exit(1);
}

fclose(fp);

nr_total/*进程和任务总数*/ = (int) (nr_procs + nr_tasks);
}
get_procs();

这个函数的作用是将所有需要的信息放在结构数组proc[] 中,每个元素都是一个进程结构体

首先我们在文件一开始已经定义了一个 proc 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct proc {
int p_flags; //proc的类型:系统/用户
endpoint_t p_endpoint; //端点,可以看做是ID
char p_name[PROC_NAME_LEN+1]; //名字
pid_t p_pid; //进程号
u64_t p_cpucycles[CPUTIMENAMES];//cpu周期
endpoint_t p_blocked; //阻塞状态
int p_priority; //动态优先级
time_t p_user_time; //用户时间
vir_bytes p_memory; //内存
uid_t p_effuid; //有效用户ID
int p_nice; //静态优先级
};

struct proc *proc = NULL,*prev_proc = NULL;//申请两个 结构数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void get_procs(){
struct proc *p;
int i;
//总之,这部分就是要将两个数组proc和prev_proc 都“装满”进程
p = prev_proc;
prev_proc = proc;
proc = p;

if (proc == NULL) {
//分配内存
//每个进程分配一个结构体
//分配nr_total个单位proc结构体内存空间,并把指针赋予proc
proc = malloc(nr_total * sizeof(proc[0]));
//如果内存申请失败,就报错
if (proc == NULL) {
fprintf(stderr, "Out of memory!\n");
exit(1);
}
}
//首先将所有的proc的flag置0
for (i = 0; i < nr_total; i++)
proc[i].p_flags = 0;

parse_dir();
}
parse_dir()

这个函数的作用是获取到/proc/ 下的所有进程pid

也就是获取这些文件夹的名字:

打开/proc文件夹之后,我们用一个循环来遍历目录及其子目录。

首先我们看一下 dirent结构

1
2
3
4
5
6
7
struct dirent{
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type;/* type of file; not supported by all file system types */
char d_name[256]; /* filename */
};

然后通过一个for循环,依次读取/proc目录下的子目录项,并将目录的名字转换成十进制整数。然后执行parse_file()操作以获取这个目录中的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void parse_dir(){
DIR *p_dir;
struct dirent *p_ent;//目录项
pid_t pid;
char *end;//end是指向第一个不可转换的字符位置的指针

if ((p_dir = opendir("/proc")) == NULL) {
perror("opendir on " _PATH_PROC);
exit(1);
}

for (p_ent = readdir(p_dir); p_ent != NULL; p_ent = readdir(p_dir)) {
pid = strtol(p_ent->d_name, &end, 10);//strtol函数将字符串转换为十进制长整数

if (!end[0] && pid != 0)
parse_file(pid);
}

closedir(p_dir);
}
parse_file()

这个函数的作用是获取每一个进程信息,并判断信息是否可用。传入的参数是这个进程的进程号码。将有用的信息传入到一个结构数组中去,这个结构数组就是 之前声明的 *p,里面每一个元素就是每一个进程/任务。

这里我们要分清楚任务和进程的区别,才能在接下来中更清楚得区分他们。 首先,一个进程可能涉及到多个任务,一个任务也可对应多个进程。他们的一些信息都存放在 psinfo文件当中。但是,进程是表示资源分配的基本单位,又是调度运行的基本单位。为了避免重复运算,我们这里计算所有的进程,但是不计算任务。

首先我们设置一些变量。这些变量就是我们接下来要从进程的 /psinfo 读取的。

1
2
3
4
5
6
7
8
9
10
11
void parse_file(pid_t pid)
{
char path[PATH_MAX], name[256], type, state;//路径、名字、种类、状态
int version, endpt, effuid; //版本、端点、有效用户ID
unsigned long cycles_hi, cycles_lo; //高周期、低周期
FILE *fp;
struct proc *p;
int i;
sprintf(path, "/proc/%d/psinfo", pid);//将path设置为 "/proc/(pid)/psinfo"的格式
//...
}

然后我们通过fopen()函数来读取("r")该进程的/psinfo文件

下面两张图片分别是任务的 psinfo 以及 进程的 psinfo

上图是一个任务,版本是0,类型为T(TASK), 端点是-5,任务名叫做 asyncm,状态是 W(WORK),我们发现在任务下面,动态优先级、嘀嗒、周期、ID都为0,

在这个编号为107的进程中:

版本是 0,(第一次读取)

类型为U(即USER),端点是32859 (第二次读取)

名字叫devmand , 状态是 ‘S’, 阻塞状态为 1,动态优先级为7,滴答为 25,高周期1186,低周期4 (第三次读取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
void parse_file(pid_t pid)
{
//...
if ((fp = fopen(path, "r")) == NULL)
return;

if (fscanf(fp, "%d", &version) != 1) {
fclose(fp);
return;
}
//判断version是否为0,如果不为0就会报错
if (version != PSINFO_VERSION) { //0
fputs("procfs version mismatch!\n", stderr);
exit(1);
}
//读取文件中的第二个、第三个参数,分别是文件类型和端点
if (fscanf(fp, " %c %d", &type, &endpt) != 2) {
fclose(fp);
return;
}
//每当读取了一个合法的任务/进程,slot就会加1
slot++;

//判断endpoint的值是否合理 在0到nr_total的范围内
if(slot < 0 || slot >= nr_total) {
fprintf(stderr, "top: unreasonable endpoint number %d\n", endpt);
fclose(fp);
return;
}
//slot为该进程结构体在数组中的位置
p = &proc[slot];//把slot地址赋值给p

//判断这个进程的类型到底是 TASK类型还是SYSTEM类型
if (type == TYPE_TASK)//如果是TASK类型,数值为 'T'
p->p_flags |= IS_TASK;//0b0010
else if (type == TYPE_SYSTEM)//如果是SYSTEM类型,数值为'S'
p->p_flags |= IS_SYSTEM;//0b0100
//除了这两者之外,就是 User 类型的进程了
//将endpt和pid存入结构体
p->p_endpoint = endpt;
p->p_pid = pid;
//读取名字 状态 阻塞状态 动态优先级 进程时间 高周期 低周期
if (fscanf(fp, " %255s %c %d %d %lu %*u %lu %lu",
name, &state, &p->p_blocked, &p->p_priority,
&p->p_user_time, &cycles_hi, &cycles_lo) != 7) {

fclose(fp);
return;
}
//将读取到的name复制给p->name ,最多复制 sizeof(p->p_name)-1 位
strncpy(p->p_name, name, sizeof(p->p_name)-1);
//然后将名字的最后一个字符改成0
p->p_name[sizeof(p->p_name)-1] = 0;
//如果不是正在运行的进程,那就打一个阻塞的标志
if (state != STATE_RUN)//如果数值为'R',就说明这个程序在运行
p->p_flags |= BLOCKED;
//通过和 0x1 按位或,表明已经处理完毕。
p->p_flags |= USED;

fclose(fp);
}

在这里,我们简单查看一下 各个 task 的名字以及他们的内存,我们发现他们的端点都是负的,原因我还没搞清楚。

我们再打印一部分的进程的信息:

首先我们在一开始定义一个结构体tp,用来存放进程结构体数组和它的嘀嗒

1
2
3
4
struct tp{
struct proc *p;
u64_t ticks;
}

这个函数输出CPU使用时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

void print_procs(struct proc *proc1, struct proc *proc2, int cputimemode)
{
int p, nprocs;
u64_t systemticks = 0;
u64_t userticks = 0;
u64_t total_ticks = 0;
int blockedseen = 0;
static struct tp *tick_procs = NULL;
//tp结构体的数组tick_procs,对所有的进程和任务(即上面读出来的nr_total)计算ticks

if (tick_procs == NULL) {//如果这个数组是空的话,就要给其分配内存
tick_procs = malloc(nr_total * sizeof(tick_procs[0]));
//申请内存下不来,就报错
if (tick_procs == NULL) {
fprintf(stderr, "Out of memory!\n");
exit(1);
}
}
//总数是nr_total,对所有的进程/任务进行遍历
for(p = nprocs = 0; p < nr_total; p++) {
u64_t uticks;
//如果当前进程标志不是used(代表没在parse_file中”备案“) 就continue 看下一个进程。
if(!(proc2[p].p_flags & USED))
continue;
//每次proc2数组都向前加一个proc单位
tick_procs[nprocs].p = proc2 + p;
//更新实时的嘀嗒 嘀嗒=
tick_procs[nprocs].ticks = cputicks(&proc1[p], &proc2[p], cputimemode);
total_ticks = total_ticks + cputicks(&proc1[p], &proc2[p], cputimemode);
//判断是否为 systemtick 和 usertick
if(!(proc2[p].p_flags & IS_TASK)) {//首先排除改对象是一个任务
if(proc2[p].p_flags & IS_SYSTEM)
//如果是系统进程,那么就加到 systemticks 上去
systemticks = systemticks + tick_procs[nprocs].ticks;
//否则就是用户进程,那么就加到 userticks 上去
else
userticks = userticks + tick_procs[nprocs].ticks;
}
//计数器自增1
nprocs++;
}
//如果所有的嘀嗒等于0,那么就直接返回
if (total_ticks == 0)
return;

//打印user system和总占用情况
printf("CPU states: %6.2f%% user, ", 100.0 * userticks / total_ticks);
printf("%6.2f%% system", 100.0 * systemticks / total_ticks);
printf("%6.2f%% in total\n", 100.0 * (systemticks+userticks) / total_ticks);
}
cputicks

这个函数计算每个进程的运行周期

滴答并不是简单的结构体中的滴答,因为在写文件的时候需要更新。需要通过当前进程来和该进程一起计算

endp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
u64_t cputicks(struct proc *p1, struct proc *p2, int timemode)
{
int i;
u64_t t = 0;
//计算每个进程proc的滴答,通过proc和当前进程prev_proc做比较
for(i = 0; i < CPUTIMENAMES; i++) {
//如果endpoint相等,则在循环中分别计算
if(p1->p_endpoint == p2->p_endpoint)
t = t + p2->p_cpucycles[i] - p1->p_cpucycles[i];
else
t = t + p2->p_cpucycles[i];
}
return t;
}

后台、重定向、管道

查看是否有 | ,>, >>, < , & 符号

首先我们要给不同的符号打上标签:

  • & 后台符号, way = 5
  • > 输出重定向 覆盖写 ,way = 1
  • < 输入重定向,way=2
  • | 管道符号, way = 3
  • >> 追加写,way=4
预处理1-分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
void exeCommand(int wordcount, char (*wordmatrix)[256]){
//...
for(i = 0; i < wordcount; i++){
if(strncmp(word[i], "&", 1) == 0){
way=5;
word[wordcount - 1] = NULL;
}
}

for(i = 0; word[i] != NULL; i++){
if(strcmp(word[i], ">") == 0){
way = 1;
// 如果重定向符号后面没有单词了,说明重定向了个空气,那么我们将correct置0,表明格式错误
if(word[i + 1] == NULL){
correct=0;
}
}
if(strcmp(word[i], ">>") == 0){
way = 4;
// 如果重定向符号后面没有单词了,说明重定向了个空气,那么我们将correct置0,表明格式错误
if(word[i + 1] == NULL){
correct=0;
}
}
if(strcmp(word[i], "<") == 0){
way = 2;
// 如果第一个字符就是输入重定向,说明也重定向了个空气,置correct = 0
if(i == 0){
correct=0;
}
}
if(strcmp(word[i], "|") == 0){
way = 3;
// 对于管道符号,前后必须都非空,否则就管道了个空气,置correct = 0
if(word[i+1] == NULL){
correct=0;
}
if(i == 0){
correct=0;
}
}
}
//correct=0,说明命令格式不对
if(correct==0){
printf("wrong format\n");
return ;
}

//...
}

格式错误时,例子如下:

预处理2-进一步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void exeCommand(int wordcount, char (*wordmatrix)[256]){
//...
if(way == 1){
//输出重定向,这里我们将file设置成 >符号 后面一个单词。也就是我们的目的地(destination)
for(i = 0; word[i] != NULL; i++){
if(strcmp(word[i], ">") == 0){
file = word[i+1];
word[i] = NULL;
}
}
}

if(way == 2){
//输入重定向,同上,设置来源地(source)
for(i = 0; word[i] != NULL; i++){
if(strcmp(word[i], "<") == 0){
file = word[i + 1];
word[i] = NULL;
}
}
}

if(way == 3){
//把管道符后面的部分存入wordnext中,管道后面的部分是一个可执行的shell命令
for(i = 0; word[i] != NULL; i++){
if(strcmp(word[i], "|") == 0){
word[i] = NULL;
int j;
// 将管道符号后面的命令存放到 wordnext 数组中去
for(j = i + 1; word[j] != NULL; j++){
wordnext[j-i-1] = word[j];
}
wordnext[j-i-1] = NULL;
}
}
}
if(way == 4){
//输出重定向的追加写,操作方式和覆盖写一样
for(i = 0; word[i] != NULL; i++){
if(strcmp(word[i], ">>") == 0){
file = word[i+1];
word[i] = NULL;
}
}
}
//...
}
switch case

这里我们直接使用了 execvp()来执行一些 ls -a -l 之类的命令

execvp()

定义函数:int execvp(const char *file, char * const argv []);

函数说明:execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名, 找到后便执行该文件, 然后将第二个参数argv 传给该欲执行的文件。

open()

此外我们还要学习一下 open函数的使用方法: open的第一个参数是要打开的文件路径名称;第二个参数是 flag,就是以何种方式打开文件;最后一个参数是 mode,也就是说文件被新建的话,我们要为其指定权限(8进制)

dup2()

dup2也是一个很有用的函数,定义如下:

1
2
#include <unistd.h>
int dup2(int oldfd, int newfd);

这个函数的功能就是讲原本要打印在屏幕上的一些信息,写入到指定的文件当中去。 文件描述符的 0 1 2 分别对应着标准输入、标准输出、标准错误。

所以当我 dup2(fd, 1) 实际上是在说我将本来要输出在terminal中的信息重定向输入到fd所指向的文件当中去,在重定向输出中非常有用; 同理 dup2(fd,0) 就代表着本来从terminal中输入的信息重定向到 fd文件当中读入,这对重定向输入非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
 
switch(way){
case 0:
//当 way=0 的时候,说明输入的命令中不含> < | &,说明是一些 ls 之类的命令
if(pid == 0){
if(!(checkCommand(word[0]))){
printf("%s : command not found\n", word[0]);
exit(0);
}
execvp(word[0], word);
exit(0);
}
break;

case 1:
//输入的命令中含有输出重定向符
if(pid == 0){
if( !(checkCommand(word[0])) ){
printf("%s : command not found\n", word[0]);
exit(0);
}
// 0644 代表文件权限时 rw-r--r--, 也就是用户可读可写
/*这里的操作就是:以读写的方式打开文件;
如果文件已经存在就删除原有的数据;如果文件不存在就创建一个新的文件*/
fd = open(file, O_RDWR|O_CREAT|O_TRUNC, 0644);
//打开后,我们利用dup2将输出的东西重定向到fd文件当中去
dup2(fd, 1);
execvp(word[0], word);
exit(0);
}
break;

case 2:
//输入的命令中含有输入重定向<
if(pid == 0){
if( !(checkCommand (word[0])) ){
printf("%s : command not found\n", word[0]);
exit(0);
}
//用只读的方式打开文件
fd = open(file, O_RDONLY);
//重定向输入
dup2(fd, 0);
execvp(word[0], word);
exit(0);
}
break;

case 3:
//输入的命令中含有管道符|
if(pid == 0){
int pid2;
int status2;
int fd[2];//创建一个文件索引数组,分别对应管道前后的两个文件
//创建一个管道
if(pipe(fd)<0){
printf("pipe error\n");
exit(0);
}
//需要创建另外一个子进程
if( (pid2 = fork()) < 0 ){
printf("fork2 error\n");
return ;
}
/*
在另一个子进程中,我们将重定位输入改为管道的后面那个文件。
然后我们执行管道前面那条指令,并将结果输入到后面那个文件当中去
*/
else if(pid2 == 0){
if( !(checkCommand(word[0])) ){
printf("%s : command not found\n", word[0]);
exit(0);
}
dup2(fd[1], 1);
execvp(word[0], word);
//不用管道之后要关闭管道文件
close(fd[1]);
exit(0);
}
// 父进程需要等待子进程结束
if(waitpid(pid2, &status2, 0) == -1){
printf("wait for child process error\n");
}
//查找管道后面的指令是否可执行
if( !(checkCommand(wordnext[0])) ){
printf("%s : command not found\n", wordnext[0]);
exit(0);
}
//若可执行, 则将管道后面的标准输入改成前面执行的结果。
dup2(fd[0], 0);
//然后执行管道后面的指令,并在前端输出结果。
execvp (wordnext[0], wordnext);
//不用管道之后要关闭管道文件,否则无法回到父进程中。
close(fd[0]);
exit(0);
}
break;
case 4:
if(pid == 0){
if( !(checkCommand(word[0])) ){
printf("%s : command not found\n", word[0]);
exit(0);
}
//如果是追加写符号,我们只需要将 O_TRUNC改为O_APPEND 即可,其他保持不变
fd = open(file, O_RDWR|O_CREAT|O_APPEND, 0644);
dup2(fd, 1);
execvp(word[0], word);
exit(0);
}
break;
case 5:
signal(SIGCHLD,SIG_IGN);//若命令中有&,表示后台执行,父进程直接返回,不等待子进程结束
if(pid==0){
printf("[process id: %d]\n",pid);
// dev/null 相当于一个垃圾桶,我们将输入和输出都移动到这个"垃圾桶"当中去
int trash = open("/dev/null",O_RDONLY);
dup2(trash,0);
dup2(trash,1);
if( !(checkCommand(word[0])) ){
printf("%s : command not found\n", word[0]);
exit(0);
}
//然后交给后台去执行该命令
execvp(word[0],word);
}
break;
default:
break;
}

//父进程等待子进程结束
if(waitpid(pid, &status, 0) == -1){
printf("wait for child process error\n");
}
}
checkCommand函数

这个函数的目的就是在 ./ ;/bin;/usr/bin 这三个文件夹下查找可执行的命令文件。比如说在bin文件夹下有很多shell本来就有的命令:

这么多的可执行的命令文件,如 ls,cat,我们的目的就是判断我们输入的命令是否是其中之一,是的话返回1,否的话返回0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int checkCommand(char *command){
DIR *dp;
struct dirent *dirp;
char *path[] = {"./", "/bin", "/usr/bin", NULL};
//如果我输入的命令是 ./可执行文件, 那么我们就要跳过 './' 这两个字符,然后查找可执行文件
if( strncmp(command, "./", 2) == 0 )
command = command + 2;


int i = 0;
while(path[i] != NULL)
{
if( (dp= opendir(path[i])) ==NULL )
printf("can not open /bin \n");
while( (dirp = readdir(dp)) != NULL )
{
// 找到了就返回 1,然后交给 execvp() 函数去执行。
if(strcmp(dirp->d_name, command) == 0)
{
closedir(dp);
return 1;
}
}
closedir(dp);
i++;
}
return 0;
}

代码重构

将所有的函数、声明都放在一个文件中不易维护代码,找起来也比较麻烦。因此我们将 main.c 文件拆分成几个头文件:

我们将一些头文件引用、宏和函数定义放到 main.h头文件中,把mytop中要调用的函数放到mytop.h 头文件中,将检查命令函数 放到checkcommand.h 头文件。

最后main.c文件只留下一个while循环,比较简洁直观。

测试结果

  • cd /your/path

  • ls -a -l > result.txt + vi result.txt

  • grep a < result.txt

  • ls -a -l | grep a

  • vi result.txt &

这边,对 vi 指令进行后台操作,会报错。这是因为 vim 的标准输入和输出必须在一个终端当中。将其丢到后台去执行是不对的。

  • mytop

  • history n

  • exit

总结

这个project 的工程量很大。而且实现起来是比较困难的。因此,如何将整个project拆分成一个一个小任务就显得尤其重要。我先实现了 cd、mytop、history n 这类shell内置命令。然后再实现了 | ,\& ,> ,>> 等 Program命令。

此外,如何调试文件也显得十分重要。为此我尝试了很多工作流,最后选择了 mac中的Terminus、Transmit软件 远程连接 windows上开的Minix虚拟机,并通过clion修改、编写代码。这样虽然比在windows上直接连接繁琐一点。但是在mac下各种安装不了虚拟机的情况下,这已经不失为一种高效的解决方法了。

不足

没有实现 mytop 的实时更新

没有实现 多重管道

在未来需要继续完善。

-------------本文结束,感谢您的阅读-------------