1、初始化命令行解释器
在计算机系统中,操作系统内核是一个很大得到软件模块,用户不能直接去使用。因此计算机提供一个外壳shell,用户能够通过这个外壳执行各种应用程序,从而完成操作系统各种功能。
shell:命令解释器。允许用户交互式的输入命令并解释执行,并且可以调用出相应的应用程序运行,从而让用户能够使用内核的功能去操作计算机
如mac中的bash ,window中的cmd Linux中的shell
目标:设计一个命令行的shell应用程序,解释用户输入的命令并解释执行。
shell应用程序,内置一个命令表,包含可以被执行的若干指令。当命令不存在于命令表中,尝试从磁盘加载应用程序
2、实现help命令
以help命令,演示如何在shell中执行内置命令
如何解析命令字符串,按照int argv, char argv的方式
1)通过遍历内置命令表:command list
2)help命令的实现包括获取用户输入,解析输入缓冲区中的字符,以及执行相应的命令
具体过程:
使用strchr函数查找特定字符位置,并将其设置为0,以清理输入字符
定义strtok函数,用于分割字符串、提取命令和参数
ARGV和ARGC用于传递参数和参数数量给命令执行函数
命令的查找与执行
通过查找命令表,找到用户输入的命令对应的函数指针
如果命令是内置命令,直接执行相应函数
如果命令在磁盘上,需要从磁盘加载并执行相应的子应用程序
命令的输入与解析
使用特定长度的输入函数fgets获取标准输入,避免数据量过大的问题
解析输入字符串,取出不必要的回车和换行字符,以便后续处理
利用strtok函数按照空格分割输出字符串,提取命令和参数
将提取出的参数字符串转为指针数组,传递给相应的内置命令执行函数
3、执行echo命令并给输出加颜色
加了信息输出时使用不同颜色的功能,以及演示了如何在程序中解析命令行的参数选项
显示颜色和清屏
两项功能是通过发送ESC转义序列完成,根本实现颜色的控制和清屏,只需要发送相应的转义序列。
命令选项的解析
使用getopt函数(Newlib提供)进行命令行的参数解析。
int getopt(int argc, char * const argv[], const char *optstring);
使用echo命令时,使用的形式为echo -n 重复数量 -h 待重复的字符串,其中有-n和-h为选项,且-n的后边必须加上一个数量值。
因此在调用getopt时,optstring需要设置为n:h(n后面的:表示n必须有几个参数值)
while ((ch = getopt(argc, argv, "n:h")) != -1) {
}
在设置optstring后,每次调用时,getopt函数会逐个扫描argv相应的选项,如n或者h
如果返回的是期望的选项,如n或者h,我们可以从全局变量optarg中获取对应的值
如果返回的是?,则表示解析出的选项不可识别,例如echo -x,此时optopt的值将会被设置成x
当返回时-1时,表示已经没有参数,即对于echo -n 重复数量 -h 字符串,表示已经扫描完所有以-开头的选项,后续已经没有其它选项。
while ((ch = getopt(argc, argv, "n:h")) != -1) {
switch (ch) {
case 'h':
puts("echo echo any message");
puts("Usage: echo [-n count] msg");
optind = 1; // getopt需要多次调用,需要重置
return 0;
case 'n':
count = atoi(optarg);
break;
case '?':
if (optarg) {
fprintf(stderr, "Unknown option: -%s\n", optarg);
}
optind = 1; // getopt需要多次调用,需要重置
return -1;
}
}
getopt被调用时,getopt会调整argv中各参数的顺序。因此当getopt优先返回-1时,optind指向了第一个非选项的参数的索引,即待重复的字符串在argv中索引号。因此,我们此时可以通过argv[optind]来获取待重复的字符串。
由于getopt将会在其它命令的执行时重复使用,因此需要在退出echo命令的执行时,将optind重置。因为optind 是 argv 中要处理的下一个元素的索引,该变量为全局变量,getopt会依赖此值。
3、为进程增加exit接口
在应用程序的执行过程中,程序可能会主动结束执行,或者从main返回,因此需要给进程一个主动退出执行的接口。应用程序通常以main函数开头,通过return语句退出函数,但也可以使用EXIT库函数退出。
exit系统调用的功能是允许进程在任意位置主动退出,结束执行,任何属于该进程的打开的文件描述符都会被关闭。因为exit已经有newlib库实现,我们只需要实现更底层的exit
exit系统调用接口实现:
- 添加系统调用表配置项
- sysy_exit系统调用的实现应停止进程执行,回收相关资源,内存
- 进程退出前会关闭打开的文件,将状态信息保存到进程的task结构中
- 应用程序返回时,应直接调用exit函数终止进程,且exit函数应包含应用程序的返回操作。
4、为进程增加wait接口
在shell加载程序运行时,需要程序的执行结果,因此需要调用wait来获取子程序的执行结果
int wait(int* status);
wait系统调用的功能:
- 回收子进程的所有资源
- 获取子进程的运行结果,如果调用的子进程还未结束,则会等待
僵尸进程(并未进行处理)
如果父进程没有主动调用wait来回收子进程的资源,则子进程会成为僵尸进程。
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源
5、让子进程继承父进程已打开的文件
考虑到fork的功能是创建一个完全相同的子进程,因此需要将增加的打开文件进行复制。如果直接内存复制在进程间是不可行的,因为两个进程的状态是独立的
文件表的复制与引用计数
- 文件表的复制通过遍历父进程的文件表并复制其内容
- 引用计数用于跟踪文件的打开次数,确保多个进程访问时文件不会被意外关闭
- 子进程创建时,需要增加被打开的文件的引用计数。
进程间文件访问冲突解决
- 多个进程同时往控制台写数据时,需要解决写冲突。
- 通过在写操作前加锁,确保一次只允许一个进程写入。
- 锁机制保护了控制台写操作,避免了多个进程同时写入的冲突