Linux命令行解释程序

发布于:2022-12-21 ⋅ 阅读:(455) ⋅ 点赞:(0)

  

目录

头文件

实验步骤

主函数main

确定shell的运行模式

代码

核心函数eval

分析和计算(evaluate)用户输入,执行内部命令或可执行文件

代码

命令行解析函数parseline

建立argv数组,返回命令属性(foreground / background)

代码

内部命令处理函数builtin_command

判断命令属性,运行内部命令

代码

信号处理函数sigchld_handler

保持errno不变,回收所有子进程

代码

实现内部命令

pwd:显示当前目录

ls:显示指定目录下的所有文件

cat:显示文件内容

rm:删除文件

touch:建立空文件

exit:终止当前进程(直接调用了_exit系统调用)

创建专属命令——myshell

编写myshell脚本

更改脚本权限

技术难点和解决方案

如何正确的加载和运行一个可执行文件

如何为execve系统调用生成合适的参数

如何一次性完成对数十个C程序的编译和链接

makefile文件的编写

变量OBJS

目标main

目标clean

具体内容

测试

myshell命令

前台进程

后台进程

内部命令

pwd

ls

cat

touch

rm

exit


头文件

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <dirent.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXARGS	 128
#define	MAXLINE	 8192  /* Max text line length */

void eval(char* cmdline);
int builtin_command(char** argv);
int parseline(char* buf, char** argv);

void print_working_directory();
void list(char* filename);
void concatenate(char* filename);
void remove_file(char* filename);
void touch(char* filename);
void directly_systemcall_exit();

实验步骤

主函数main

确定shell的运行模式

  1. 使用signal系统调用设置SIGCHLD信号的处理函数(回收僵尸进程)
  2. 打印出提示符
  3. 从键盘(标准输入)获取一条命令
  4. 分析和运行该命令

代码

int main() {
	char cmdline[MAXLINE];

	if (signal(SIGCHLD, sigchld_handler) == SIG_ERR)
		printf("signal error\n");

	while (1) {
		printf("> ");
		fgets(cmdline, MAXLINE, stdin);
		if (feof(stdin)) exit(0);

		eval(cmdline);
	}
}

 

核心函数eval

分析和计算(evaluate)用户输入,执行内部命令或可执行文件

  1. 首先对输入的命令进行语法分析(parse),并建立argv数组
  2. 然后检查argv数组的第一项argv[0],检查其是否为内部命令(builtin command)
  3. 如果是内部命令,那么执行该命令
  4. 如果不是内部命令,那么将其视作可执行文件,调用fork系统调用创建一个子进程,然后在子进程中调用execve系统调用来为该可执行文件创建进程上下文
  5. 对于为可执行文件生成的进程,检查其为前台进程(foreground)还是后台进程(background),如果是前台进程,那么在父进程中调用waitpid系统调用来等待其终止;如果是后台进程,输出其进程号和当前命令行,回到主函数

代码

void eval(char* cmdline)
{
	char* argv[MAXARGS];	//argument list execve()
	char buf[MAXLINE];		//holds modified command line
	int bg;					//should the job run in bg or fg?
	pid_t pid;				//process id

	strcpy(buf, cmdline);
	bg = parseline(buf, argv);
	if (argv[0] == NULL) return;	//ignore empty lines

	if (!builtin_command(argv)) {
		if ((pid = fork()) == 0) {	//child runs user job
			if (execve(argv[0], argv, NULL) < 0) {
				printf("%s: Command not found.\n", argv[0]);
				exit(0);
			}
		}

		/* parent waits for foreground job to terminate */
		if (!bg) {
			int status;
			if (waitpid(pid, &status, 0) < 0)
				printf("waitfg: waitpid error\n");
		}
		else printf("%d %s", pid, cmdline);
	}
	return;
}

命令行解析函数parseline

建立argv数组,返回命令属性(foreground / background)

  1. 用空格作为分隔符,来划分用户输入的字符串,每个子串首地址作为argv数组的一项,并将argv数组尾元素的下一个位置设为NULL
  2. 检查argv数组的最后一个字符串是否为“&”,如果是,那么该命令生成一个后台进程(bg=1);否则该命令生成一个前台进程(bg=0)

代码

/* parseline - Parse the command line and build the argv array */
int parseline(char* buf, char** argv)
{
	char* delim;		//points to first space delimiter
	int argc;			//number of args
	int bg;				//background job?

	buf[strlen(buf) - 1] = ' ';		//replace trailing '\n' with space
	while (*buf && (*buf == ' ')) ++buf;	//ignore leading spaces

	/* build the argv list */
	argc = 0;
	while ((delim = strchr(buf, ' '))) {
		argv[argc++] = buf;
		*delim = '\0';
		buf = delim + 1;
		while (*buf && (*buf == ' ')) ++buf;	//ignore spaces
	}
	argv[argc] = NULL;

	if (argc == 0) return 1;	//ignore blank line

	/* should the job run in the background? */
	if ((bg = (*argv[argc - 1] == '&')) != 0)
		argv[--argc] = NULL;

	return bg;
}

 

内部命令处理函数builtin_command

判断命令属性,运行内部命令

  1. 如果输入参数是内部命令,那么运行该命令,并返回1(true)
  2. 否则返回0(false)

代码

/* if first arg is a builtin command, run it and return true */
int builtin_command(char** argv)
{

	if (!strcmp(argv[0], "pwd")) {
		print_working_directory();
	}
	else if (!strcmp(argv[0], "ls")) {
		list(argv[1]);
	}
	else if (!strcmp(argv[0], "cat")) {
		concatenate(argv[1]);
	}
	else if (!strcmp(argv[0], "rm")) {
		remove_file(argv[1]);
	}
	else if (!strcmp(argv[0], "touch")) {
		touch(argv[1]);
	}
	else if (!strcmp(argv[0], "exit")) {
		directly_systemcall_exit();
	}
	else if (!strcmp(argv[0], "&"));	// ignore singleton &
	else return 0;

	return 1;
}

 

信号处理函数sigchld_handler

保持errno不变,回收所有子进程

  1. 保存errno变量
  2. 调用waitpid系统调用,第一个参数设置为-1,则等待集合为当前进程的所有子进程;第二个参数设置为NULL,忽略子进程的状态信息;第三个参数设置为0,表示默认状态,即挂起当前进程的执行,直到等待集合中一个子进程的终止
  3. 恢复errno变量

代码

void sigchld_handler(int sig) {
	int olderrno = errno;

	while (waitpid(-1, NULL, 0) > 0);

	if (errno != ECHILD) {
		printf("waitpid error\n");
		exit(1);
	}
	
	errno = olderrno;
}

 

实现内部命令

pwd:显示当前目录

void print_working_directory() {
	char dirname[MAXLINE];
	getcwd(dirname, MAXLINE);
	printf("%s\n", dirname);
}

ls:显示指定目录下的所有文件

void list(char* filename) {
         DIR* dir = opendir(filename);
         struct dirent* ptr;
         int i;
         while (ptr = readdir(dir)) {
                  printf("%s\n", ptr->d_name);
         }
         closedir(dir);
}

cat:显示文件内容

void concatenate(char* filename) {
         int ch;
         FILE* fp;

         if ((fp = fopen(filename, "r")) == NULL) {
                  printf("Can't open %s\n", filename);
                  exit(1);
         }
         while ((ch = getc(fp)) != EOF) {
                  putc(ch, stdout);
         }
         fclose(fp);
}

rm:删除文件

void remove_file(char* filename) {
         int ret = remove(filename);
         if (ret) {
                  printf("Remove %s failed.\n", filename);
         }
         else {
                  printf("Remove %s sucessed.\n", filename);
         }
}

touch:建立空文件

void touch(char* filename) {
         FILE* fp = fopen(filename, "w");
         fclose(fp);
}

exit:终止当前进程(直接调用了_exit系统调用)

void directly_systemcall_exit() {
         _exit(0);
}

 

创建专属命令——myshell

(本地用户的家目录为/home/kku19099/)

可以看到PATH环境变量的最后一项为/home/kku19099/bin,因此编写一个名为myshell的shell脚本,放入家目录中的bin目录中,即可通过直接在命令行输入myshell来运行我们的命令行解释程序。

编写myshell脚本

myshell脚本所作的工作

  1. 用一个变量记录下当前目录
  2. 跳转到我们命令行解释程序所在的目录
  3. 使用make清理之前的残留目标文件,生成一个新的可执行目标文件main
  4. 执行main
  5. 返回原目录 

具体内容

  

更改脚本权限

增加用户可执行权限:

然后回到根目录,输入myshell就可以运行我们自己的shell了

技术难点和解决方案

如何正确的加载和运行一个可执行文件

解决

  1. 使用fork系统调用生成一个子进程
  2. 在子进程中调用execve系统调用来为可执行文件创建进程上下文

如何为execve系统调用生成合适的参数

解决:

      execve函数接受三个参数,分别为可执行文件名,参数列表和环境变量列表

可执行文件名是我们已有的,环境变量列表可设置为NULL,那么主要工作就是准备合适的参数列表,这需要分析用户的输入来创建argv数组,将argv数组作为execve函数的第二个参数。

如何一次性完成对数十个C程序的编译和链接

解决:

      使用make进行宏编译,这需要了解make指令的用法,make指令的行为,以及最重要的——makefile文件的编写。

makefile文件的编写

解决:

变量OBJS

因目标文件众多,所以使用一个变量OBJS来记录所有的目标文件的名称,之后可由OBJS来替换所有目标文件的出现

目标main

  1. 编译所有源文件,生成对应的目标文件
  2. 链接所有目标文件,生成可执行目标文件main
  3. 列出所有目标文件和可执行目标文件main

目标clean

  1. 清除掉所有目标文件和可执行目标文件main
  2. 列出当前目录下所有文件

具体内容

测试

myshell命令

前台进程

/home/kku19099/bin/ans_yn.sh是一个和用户交互的可执行文件

后台进程

/home/kku19099/bin/cal_1_100.sh计算1+2+…+100

内部命令

pwd

ls

cat

touch

rm

exit

 


网站公告

今日签到

点亮在社区的每一天
去签到