Ⅰ.主结构
操作系统分为shell 和 kernel,shell为上层应用软件提供接口,kernel面向计算机内部;shell可以被视为一个中介,任务就是执行用户与内核交互相关的命令,在不同的操作系统上,shell有不同的类型,比如终端形式和GUI,虽然形式不同,但其内部的逻辑都是一样的:等待用户的命令–>执行命令–>返回结果–>回到等待状态;
环境
VMWare上搭建的MINIX3操作系统
宏定义及全局变量
argc和argv[]必不可少;history[][]用于存放历史命令;buf[]存储用户键入字符串
#include <unistd.h> //提供对 POSIX 操作系统 API 的访问功能的头文件
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <cerrno>
#include <dirent.h>
#define buffsize 255
#define maxargs 128 //每条命令最大参数
#define maxline 128 //最大命令数
char prompt[]="myshell> ";
char buf[buffsize];
int argc;
char* argv[maxargs];
char curPath[buffsize];
char history[maxline][buffsize];
int cmd_num = 0;
主体逻辑
- Shell主体结构是一个while循环,不断地接受用户键盘输入行并给出反
馈(getbuf函数)。 - 接收到用户输入后,调用parsline函数解析命令,同时构建argv数组便于之后的传参;
- 命令解析成功后,调用eval函数执行命令;命令分为两种,第一种是program命令,包括重定向(>),管道(|),后台运行(&),以及运行程序。第二种是内置命令,包括cd,history,mytop等;
- 执行eval()时,先判断是否是管道/重定向/后台命令,之后再判断是否是内置命令,如果都不是,说明是运行程序类的命令,此时fork子进程,并调用execvp执行程序。
int main()
{
while(1)
{
printf("%s",prompt);
if(getcommand(buf))
{
if(strcmp(buf,"exit")==0)
{
exit(0);
break;
}
else
{
strcpy(history[cmd_num],buf);
cmd_num++;
parseline(buf);
eval(argc,argv);
}
}
}
return 0;
}
int getcommand(char *buf)
{
memset(buf,0,buffsize);
int length;
fgets(buf,buffsize,stdin);
length = strlen(buf);
buf[length-1] = '\0';
return strlen(buf);
}
void parseline(char* cmd)
{
int i,j,k;
int len = strlen(cmd);
int num=0;
for(i=0;i<maxargs;i++)
{
argv[i] = NULL;
}
char tmp[buffsize];
j=-1;
for(i=0;i<=len;i++)
{
if(cmd[i]==' '||i==len)
{
if(i-1>j)
{
cmd[i]='\0';
argv[num++] = cmd+j+1;
}
j = i;
}
}
argc = num;
argv[argc] = NULL;
}
//以下是伪代码
void eval(int argc,char* argv[])
{
pid_t pid,pid2;
int i,j;
int bg_flag = 0;
/*program_command*/
if '>' then outRedirect(argc,i,argv);
if '<' then inRedirect(argc,i,argv);
if '>>' then reoutRedirect(argc,i,argv);
if '|' then:
fork();
if pid == 0 then Pipecommand(buf);
else waitpid();
if '&' then bgflag = 1;
/*内置命令*/
if(strcmp(argv[0],"cd")==0)
{
int res = CD(argc);
if(res==0) printf("cd指令错误\n");
}
else if(strcmp(argv[0],"history")==0)
{
int res = HISTORY(argc);
if(res ==0) printf("history指令错误\n");
}
else if(strcmp(argv[0],"mytop")==0)
{
mytop();
}
else
{
fork();
if pid==0 then:
if(bgflag==1):/*后台执行程序方法*/
else: execvp()
else : waitpid();
}
}
Ⅱ.program命令
运行程序
执行程序是shell最基本的功能,我们可以使用exec系函数来完成,用fork创建新进程,用exec初始执行新的程序,exit和wait处理终止和等待终止,这是最基本的进程控制原语;exec有七个不同的函数供我们使用,由于被执行的是内置命令脚本,所以直接调用execvp();
int execvp(const char *filename,char *const argv[])
- 如果filename中包含/,则将其视为路径名
- 否则按path环境变量,在目录中寻找可执行文件,如果该文件不是由连接编辑器产生的机器可执行文件,则将其视为脚本文件,并尝试/bin/sh
exec是不返回的,当命令不是后台命令时,父进程应该等待子进程结束,再接收下一条命令,这一要求可以由waitpid()实现,(后台命令的情况在后面讨论)。而为了防止其成为僵尸子进程,父进程需要接收其识别码,便于报错;
if((pid=fork())==-1)
{
printf("子进程创建失败\n");
return ;
}
else if(pid == 0)
{
if(bg_flag)
{
/*后台命令处理方法*/
}
execvp(argv[0], argv);
printf("%s: 命令输入错误\n", argv[0]);
exit(1);
}
else
{
if(bg_flag)
{
/*后台命令处理方法*/
}
int status;
waitpid(pid, &status, 0); // 等待子进程返回
int err = WEXITSTATUS(status); // 读取子进程的返回码
if (err) {
printf("Error %d: %s\n", errno,strerror(err));
}
}
重定向
重定向命令可以分为三种:
- ’>’ 覆盖写
- ’<’ 文件输入
- ’>>’ 追加写
由于三个操作的函数逻辑是一样的,所以这里以覆盖写操作为例。
首先在eval函数中要识别命令是覆盖写命令,只需要遍历argv即可,如果匹配到了’>’,则调用处理覆盖写的函数outRedirect;
dup2函数:
谈到重定向,就不得不谈到dup2函数。
int dup2(int fd,int fd2)
dup2允许将一个未使用的文件描述符定向到一个已打开的文件.若参数fd2已经被程序使用,则系统就会将fd2所指的文件关闭,若fd2等于fd,则返回fd2,而不关闭fd2所指的文件。dup2所复制的文件描述符与原来的文件描述符共享各种文件状态。共享所有的锁定,读写位置和各项权限或flags等。
所以当fd为目标文件,fd2为标准输出时,系统会先关闭标准输出,然后令标准输出指向目标文件。
open函数:
想要重定向输出,还需要知道目标文件的信息,而目标文件的路径可以以字符串的形式从argv中读出。所以调用open函数获取文件的文件描述符:
int open(const char* path,int oflag,.../*mode_t mode*/)
path参数是文件的路径;oflag可用来说明此函数的多个选项,用下列一个或多个常量进行或运算构成oflag参数;
- O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 读写打开
- O_EXEC 只执行打开
- O_SEARCH 只搜索打开
以上5个参数只能选择一个,以下常量则可多个搭配
- O_APPEND 每次写都追加到尾端
- O_CREAT 若不存在则创建
- O_TRUNC 每次打开后长度截断为0
易知覆盖写的oflag为 O_WRONLY | O_TRUNC | O_CREAT
当oflag中存在O_CREAT时,函数还要有第三个参数:mode:mode指定了新文件的访问权限位,共有11位,其中有9位的文件访问权限位。每3位为一组,所以mode为八进制码,例如0777换算为2进制则是0111111111,即用户、同组用户、其他用户均可以对其进行读写执行操作;
int outRedirect(int argc,int inter,char* argv[])
{
char outFile[buffsize];
memset(outFile, 0x00, buffsize);
int i,j;
int redi_num = 0;
for(i=0;i<argc;i++)
{
if(strcmp(argv[i],">")==0)
{
redi_num += 1 ;
}
}
if(redi_num != 1)
{
printf("wrong Redirect command\n");
return 0;
}
if(inter==argc-1)
{
printf("lack of output-file name\n");
return 0;
}
if(argc-inter>2)
{
printf("to many file\n");
return 0;
}
strcpy(outFile,argv[inter+1]);
for(j=inter;j<argc;j++)
{
argv[inter] =NULL;
}
pid_t pid;
if((pid=fork())==-1)
{
printf("子进程创建失败\n");
return 0;
}
else if(pid == 0)
{
int fd;
int oldfd;
fd = open(outFile, O_WRONLY|O_CREAT|O_TRUNC, 777);
// 文件打开失败
if (fd < 0) {
printf("open error\n");
exit(1);
}
if(dup2(fd, STDOUT_FILENO) == -1)
{
printf("dup2 error\n");
exit(1);
}
execvp(argv[0], argv);
if (fd != STDOUT_FILENO) {
close(fd);
}
printf("%s: 命令输入错误\n", argv[0]);
exit(1);
}
int status;
waitpid(pid, &status, 0); // 等待子进程返回
int err = WEXITSTATUS(status); // 读取子进程的返回码
if (err) {
printf("Error %d: %s\n", errno,strerror(err));
}
return 0;
}
管道
管道命令的实现与重定向类似,主要用到pipe()和dup()函数;
int pipe(int fd[2])//成功返回0,出错返回-1
管道只能在具有共同祖先的进程下使用,所以需要fork出父子两个进程,分别完场前后两个命令,然后用管道将前一条命令进程的输出作为下一条命令进程的输入。
pipe函数经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开,fd[1]的输入是fd[0]的输出;
int Pipecommand(char* buf)
{
int fd[2];
int i;
int pos;
pid_t pid;
for(i=0;i<strlen(buf);i++)
{
if(buf[i]=='|'&&buf[i+1]==' ')
{
pos = i;
break;
}
}
char readpipe[buffsize];
memset(readpipe,0,buffsize);
char writepipe[buffsize];
memset(writepipe,0,buffsize);
for(i=0;i<strlen(buf);i++)
{
if(i<pos)
{
writepipe[i] = buf[i];
}
else if(i>pos)
{
readpipe[i-pos-1] = buf[i];
}
}
writepipe[pos]='\0';
readpipe[strlen(buf)-pos-1] = '\0';
if (pipe(fd) < 0) {
printf("pipe failed\n");
exit(1);
}
pid = fork();
if (pid < 0) {
printf("创建子进程失败\n");
exit(1);
}
if(pid==0){
close(fd[0]); //关闭写功能
close(1); //关闭标准输出
dup(fd[1]);//fd[1]管道写入端,映射到标准输出1
close(fd[1]); //用不到fd[1]
parseline(writepipe);
execvp(argv[0], argv);
return 0;
}else{
int status;
waitpid(pid, &status, 0); // 等待子进程返回
int err = WEXITSTATUS(status); // 读取子进程的返回码
if (err) {
printf("Error: %s\n", strerror(err));
}
close(fd[1]);
close(0);
dup(fd[0]);//fd[0]管道读入端,映射到标准输入0
close(fd[0]);
parseline(readpipe);
execvp(argv[0], argv);
return 0;
}
}
后台运行
之前介绍执行程序时,提到先fork子进程,在子进程中exec,然后父进程等待子进程结束后再返回。
但是对于后台命令,则不希望shell一直等待子进程结束,而是忽略他,让他在后台慢慢运行: 为了屏蔽键盘和控制台,子进程的标准输入、输出映射成
/dev/null。子进程调用signal(SIGCHLD,SIG_IGN),作用是令父进程忽略sigchild的信号,从而使minix接管此进程。因此Shell可以避免调用wait/waitpid直接运行下一条命令。
if((pid=fork())==-1)
{
printf("子进程创建失败\n");
return ;
}
else if(pid == 0)
{
if(bg_flag)
{
int fd1 = open("/dev/null", O_RDONLY);
dup2(fd1, 0);
dup2(fd1, 1);
dup2(fd1, 2);
signal(SIGCHLD, SIG_IGN);
}
execvp(argv[0], argv);
printf("%s: 命令输入错误\n", argv[0]);
exit(1);
}
else
{
if(bg_flag)
{
printf("[process id %d]\n", pid);
return ; //若为后台程序,则输出进程号
}
int status;
waitpid(pid, &status, 0); // 等待子进程返回
int err = WEXITSTATUS(status); // 读取子进程的返回码
if (err) {
printf("Error %d: %s\n", errno,strerror(err));
}
}
Ⅲ.内置命令
cd
int CD(int argc)
{
int res ;
if(argc == 2)
{
if(!chdir(argv[1])) res = 1;
else res = 0;
}
else{
res = 0;
}
if(getcwd(curPath, buffsize)==NULL)
{
printf("getcwd failed\n");
res =0;
}
else printf("%s\n",curPath);
return res;
}
exit
int main()
{
while(1)
{
printf("%s",prompt);
if(getcommand(buf))
{
if(strcmp(buf,"exit")==0)
{
exit(0); //输入exit则退出
break;
}
else
{
strcpy(history[cmd_num],buf);
cmd_num++;
parseline(buf);
eval(argc,argv);
}
}
}
return 0;
}
history
int HISTORY(int argc)
{
if(argc!=2)
{
printf("history failed\n");
return 0;
}
int res = 1;
int n = atoi(argv[argc-1]);
int i;
for (i = n; i > 0; i--) {
if( cmd_num - i < 0)
{
continue;
}
printf("%s\n", history[cmd_num - i]);
}
return res;
}
mytop
mytop命令输出:
- 总体内存大小,空闲内存大小,缓存大小。
- 总体CPU使用占比。计算方法:得到进程和任务总数量total_proc,对每一个proc的ticks累加得到总体ticks,再计算空闲的ticks,最终可得到CPU使用百分比。
/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=ticks/ (u32_t)60。
void mytop() {
FILE *fp = NULL;
char buff[255];
fp = fopen("/proc/meminfo", "r"); // 以只读方式打开meminfo文件
fgets(buff, 255, (FILE*)fp); // 读取meminfo文件内容进buff
fclose(fp);
// 获取 pagesize
int i = 0, pagesize = 0;
while (buff[i] != ' ') {
pagesize = 10 * pagesize + buff[i] - 48;
i++;
}
// 获取 页总数 total
i++;
int total = 0;
while (buff[i] != ' ') {
total = 10 * total + buff[i] - 48;
i++;
}
// 获取空闲页数 free
i++;
int free = 0;
while (buff[i] != ' ') {
free = 10 * free + buff[i] - 48;
i++;
}
// 获取最大页数量largest
i++;
int largest = 0;
while (buff[i] != ' ') {
largest = 10 * largest + buff[i] - 48;
i++;
}
// 获取缓存页数量 cached
i++;
int cached = 0;
while (buff[i] >= '0' && buff[i] <= '9') {
cached = 10 * cached + buff[i] - 48;
i++;
}
// 总体内存大小 = (pagesize * total) / 1024 单位 KB
int totalMemory = pagesize / 1024 * total;
// 空闲内存大小 = pagesize * free) / 1024 单位 KB
int freeMemory = pagesize / 1024 * free;
// 缓存大小 = (pagesize * cached) / 1024 单位 KB
int cachedMemory = pagesize / 1024 * cached;
printf("totalMemory is %d KB\n", totalMemory);
printf("freeMemory is %d KB\n", freeMemory);
printf("cachedMemory is %d KB\n", cachedMemory);
unsigned int procsnum, tasksnum;
int total_n;
if ((fp = fopen("/proc/kinfo", "r")) == NULL)
{
fprintf(stderr, "opening /proc/kinfo failed\n");
exit(1);
}
if (fscanf(fp, "%u %u", &procsnum, &tasksnum) != 2)
{
fprintf(stderr, "reading from /proc/kinfo failed");
exit(1);
}
fclose(fp);
total_n = (int)(procsnum + tasksnum);
DIR *d;
struct dirent *dir;
d = opendir("/proc");
int totalTicks = 0, freeTicks = 0;
if (d) {
while ((dir = readdir(d)) != NULL) { // 遍历proc文件夹
if (strcmp(dir->d_name, ".") != 0 &&
strcmp(dir->d_name, "..") != 0) {
char path[255];
memset(path, 0x00, 255);
strcpy(path, "/proc/");
strcat(path, dir->d_name); // 连接成为完成路径名
struct stat s;
if (stat (path, &s) == 0) {
if (S_ISDIR(s.st_mode)) { // 判断为目录
strcat(path, "/psinfo");
FILE* fp = fopen(path, "r");
char buf[255];
memset(buf, 0x00, 255);
fgets(buf, 255, (FILE*)fp);
fclose(fp);
// 获取ticks和进程状态
int j = 0;
for (i = 0; i < 4;) {
for (j = 0; j < 255; j++) {
if (i >= 4) break;
if (buf[j] == ' ') i++;
}
}
// 循环结束, buf[j]为进程的状态, 共有S, W, R三种状态.
int k = j + 1;
for (i = 0; i < 3;) { // 循环结束后k指向ticks位置
for (k = j + 1; k < 255; k++) {
if (i >= 3) break;
if (buf[k] == ' ') i++;
}
}
int processTick = 0;
while (buf[k] != ' ') {
processTick = 10 * processTick + buff[k] - 48;
k++;
}
totalTicks += processTick;
if (buf[j] != 'R') {
freeTicks += processTick;
}
}else continue;
}else continue;
}
}
}
printf("CPU states: %.2lf%% used,\t%.2lf%% idle\n",
(double)((totalTicks - freeTicks) * 100) / (double)totalTicks,
(double)(freeTicks * 100) / (double)totalTicks);
return;
}