一、项目概述
在linux终端下我们可以看到用户的相关信息和主机信息等,并且在终端下,可以操作很多的命令(ls、ps、pwd等),并且在终端下可以运行自己的程序。因此本项目实现以下功能:
- 打印用户、主机等相关信息
- 可以在mybash中操作相关命令
- 各种命令的代码实现
二、显示信息
Linux系统下,打开一个终端后会出现以下界面,其中包括用户信息、主机信息和当前位置信息,首先实现这个功能,做到如下输出。
1、获得用户信息(终端中只需打印用户名和用户的权限(管理员打印'#' 普通用户打印'$'))
实现原理:
- 每个用户有自己对应的用户标识符UID,可以通过函数getuid来获得当前用户的UID。
getuid函数原型:
# include <stdio.h>
# include <sys/type.h>
//UID的数据类型为uid_t
uid_t getuid(void);
- 系统文件/etc/passwd包含一个用户账户数据库。它由行组成,每一行对应一个用户,包括用户名、加密口令、用户标识符、组标识符、全名、家目录和默认shell。
- 可以通过getpwuid函数来获得当前用户的相关信息。
getpwuid函数原型:
# include <sys/types.h>
# include <pwd.h>
//用户的相关信息存储在结构体passwd中
/*
passwd结构体中有以下成员:
char *pw_name 用户登录名
uid_t pw_uid 用户标识符
gid-t pw_gid 组标识符
char *pw_dir 用户家目录
chae *gecos 用户全名
char *shell 用户默认shell
*/
struct passwd *getpwuid(uid_t uid);//函数参数为用户标识符
- 获得了用户信息passwd之后,用户名就在该结构体中。
- 如果用户的UID、GID都为0,则该用户为管理员用户,否则为普通用户
2、打印主机信息(终端只需打印主机名)
实现原理:
- gethostname函数可以获取主机名
gethostname函数原型:
# include <stdio.h>
int gethostname(char *name, size_t namelen);//将主机名写出字符串数组name中
3、获取当前位置(终端只需输入当前路径的最后一项,并非完整路径)
实现原理:
- 通过getcwd函数获取当前的绝对路径
- 从路径尾部向前遍历站到绝对路径中的最后一项
4、代码实现
void print_sys_info()
{
char buff[128] = {0}; //用来存储要打印到终端的信息
strcpy (buff,"[");
struct passwd *pwd;
pwd = getpwuid(getuid()); //获取用户信息
if(pwd == NULL)
{
printf("mybash>>");
fflush(stdout);
return ;
}
strcat(buff,pwd->pw_name); //获取用户名
strcat(buff,"@");
char hostname[128] = {0};
gethostname(hostname,128); //获取主机名
strcat(buff,hostname);
strcat(buff," ");
char cur_dir[128] = {0};
getcwd(cur_dir,128); //获取当前位置的绝对路径
int i = 0;
while(cur_dir[i] != '\0') //遍历到绝对路径尾部
{
i++;
}
while(cur_dir[i] != '/') //从绝对路径尾部往前找第一个'/'
{
i--;
}
i++;
char cur_tmp[128] = {0};
int j = 0;
while(cur_dir[i] != '\0') //获取绝对路径的最后一项
{
cur_tmp[j] = cur_dir[i];
i++;
j++;
}
cur_tmp[j] = '\0';
strcat(buff,cur_tmp);
if (pwd->pw_uid == 0 && pwd->pw_gid ==0) 判断用户是否为管理员用户
{
strcat(buff,"]#");
}
else
{
strcat(buff,"]$");
}
printf("%s",buff);
fflush(stdout); //输出
}
三、主功能块
linux系统下bash终端可以执行其他命令或程序,核心原理是进程的复制和替换,具体步骤如下:
- bash进程作为父进程,bash进程设置循环永远存在
- 如果要执行相关命令操作,bash进程复制一个子进程
- 用相关命令的操作来替换该子进程,可以实现相关命令的操作
- 子进程实现完功能后退出,返回到父进程(即bash进程)
注意1:
- linux操作系统中常见命令执行时不需要输入绝对路径,因为其存在默认路径/etc/bin下,执行时内核会自动在该目录下查找命令的可执行程序
- 在本项目中,我们希望在bash中执行的也是自己实现的命令,因此,我们需要重新设置默认路径
注意2:
- cd命令不需要通过进程的复制替换实现,直接调用系统调用chdir即可
注意3:
- 需设置退出mybash的条件
- 当输入"exit"时,退出mybash
代码如下:
# include <stdio.h>
# include <stdlib.h>
# include <assert.h>
# include <string.h>
# include <unistd.h>
# include <error.h>
# include <fcntl.h>
# include <pwd.h>
#define MAX 10
#define PATH "/home/FUJIA/cy1706/bash/mybin/" //可执行程序默认存储路径
int main()
{
while(1)
{
char buff[128] = {0}; //用来存储用户输入的命令
print_sys_info(); //打印显示信息
fgets(buff,128,stdin); //获取用户输入
buff[strlen(buff) - 1] = 0; //删除结尾'\0'
char * myargv[MAX] = {0}; //存储命令参数
char * s = strtok(buff," "); //通过 " " 来对用户输入进行分割
if (s == NULL)
{
continue;
}
myargv[0] = s;
int i = 1;
while( (s = strtok(NULL," ")) != NULL) //依次将参数存入到myargv中
{
myargv[i++] = s;
}
if (strcmp(myargv[0],"exit") == 0) //如果输入 exit 则退出mybash
{
break;
}
else if (strcmp(myargv[0],"cd") == 0) //如果输入的命令是 cd 则直接调用chdir即可
{
chdir(myargv[1]);
continue;
}
pid_t pid = fork(); //复制子进程
assert(pid != -1);
if(pid == 0) //子进程完成进程的替换
{
char path[256] = {0};
//如果用户输入没有给定路径,则将默认路径传入
if (strncmp(myargv[0],"./",2) != 0 && strncmp(myargv[0],"/",1) != 0)
{
strcpy(path,PATH);
}
//将路径与命令名连接
strcat(path,myargv[0]);
//完成进程的替换
execv(path,myargv);
perror("execvp error");
exit(0);
}
wait(NULL);
}
exit(0);
}
四、常见命令的实现
在linux系统中对常见命令的操作是的bash非常实用,只有单独的bash并不能体现出其优势,linux中常见命令的可执行文件在/etc/bin中,本项目也实现了一些常见的命令,是的bash更加完善
1、pwd命令的实现
- 通过调用getcwd函数即可
代码如下:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <assert.h>
int main()
{
char buff[256] = {0};
getcwd(buff,256); //获得当前路径
printf("mypwd:%s\n",buff); //输出路径
exit(0);
}
2、ls命令的实现
扫描目录也存在一套完整的库函数,可以像操作文件一样对目录进行扫描。与目录操作有关的函数在dirent.h头文件中声明。与文件类似,文件的数据类型为FILE,目录的数据类型为DIR。与文件操作类似的还有目录的操作也分为打开目录,操作目录,关闭目录三步。
注意:
- struct stat是linux中用来描述文件属性的结构
- stat函数是用来获取文件状态
- S_ISDIR函数用来判断文件是否为目录的
代码如下:
# include <stdio.h>
# include <unistd.h>
# include <string.h>
# include <dirent.h>
# include <pwd.h>
# include <stdlib.h>
# include <fcntl.h>
# include <sys/stat.h>
int main()
{
char path[256] = {0};
getcwd(path,256);
DIR *pdir = opendir(path); //打开目录
if(pdir == NULL)
{
perror("opendir error");
exit(0);
}
struct dirent * p = NULL;
struct stat st; //用来存储文件信息
while((p = readdir(pdir)) != NULL) //遍历目录流
{
if(strncmp(p -> d_name,".",1) == 0) //目录流中存在 . 表示当前目录
{
continue;
}
stat(p->d_name,&st); //将文件名p->d_name的文件存储到st中
if ( S_ISDIR(st.st_mode)) //该文件为目录,以以下格式输出
{
printf("\033[1;34m%s \033[0m", p->d_name);
}
else
{
if(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) //该文件为可执行文件
{
printf("\033[1;32m%s \033[0m", p->d_name);
}
else
{
printf("%s ", p->d_name);
}
}
}
closedir(pdir); //关闭目录
printf("\n");
exit(0);
}
3、clear命令的实现
- 在终端中,ANSI定义了用于屏幕显示的Escape屏幕控制码,在printf函数调用时会以特定颜色或格式输出。
- ANSI中\033[2J 表示清屏
- \033[0;0H 表示光标置顶
代码如下:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <assert.h>
int main()
{
printf("\033[2J\033[0;0H");
exit(0);
}
4、su命令的实现
实现原理:
- 首先获得要切换到的用户x的信息
- 复制一个子进程b
- 用x的uid和gid改变子进程的uid和gid。
- 将b进程的家目录用x的家目录替换,用setenv函数实现,此时已经处于x用户状态
- 调用x用户的shell终端(替换进程),即可切换到x用户
注意:
- 如果su命令后没有别的参数,即为切换到管理员用户
代码如下:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <pwd.h>
# include <assert.h>
int main(int argc,char *argv[])
{
char *name = "root"; //默认指定要切换的用户是管理员用户
if(argc == 2) //当有两个参数的时候,说明su明后后边有指定要切换到的用户
{
name = argv[1];
}
struct passwd *p = getpwnam(name); //获取用户信息
if(p == NULL)
{
perror("su error!");
exit(0);
}
pid_t pid = fork(); //复制进程
assert(pid != -1);
if(pid == 0) //进人子进程
{
setgid(p->pw_gid); //修改当前进程的gid
setuid(p->pw_uid); //修改当前进程的uid
setenv("HOME",p->pw_dir,1); //修改环境变量,指定当前处于要切换到的用户的家目录下
execl(p->pw_shell,"p->pw_shell",(char*)0); //用p进程的终端替换该进程,使当前处于p用户的shell下,完成切换功能
perror("su execl errer!");
exit(0);
}
wait(NULL);
exit(0);
}
5、ps命令的实现
- 实现原理
进程的信息存储在/proc目录下,/proc目录中以数字命名的文件存储着一个进程的所有信息,通过stat系统调用可以获得文件中进程的信息
- 程序流程图
- stat结构体和stat函数
(1)stat结构体
stat是文件(夹)信息的结构体,定义如下:
struct stat
{
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
};
(2)stat函数
获取指定路径文件的信息,函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int stat(
const char *filename //文件或者文件夹的路径
, struct stat *buf //获取的信息保存在内存中
);
返回值:正确返回0
错误返回-1
- sprintf函数
将数据格式化输入到字符串。
代码如下:
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <sys/stat.h>
# include <pwd.h>
# include <sys/types.h>
# include <dirent.h>
# include <string.h>
# define MAX_LEN 20
typedef struct ps_info
{
char pname[MAX_LEN];
char user[MAX_LEN];
int pid;
int ppid;
char state;
struct ps_info *next;
}myps;
void uid_to_name(uid_t uid, struct ps_info *p1); //根据进程uid获取进程属主
myps *trav_dir(char dir[]); //获取进程信息,将所有进程信息存储在链表中
int read_info(char d_name[], struct ps_info *p1); //根据文件名获取某一进城的信息
void print_ps(struct ps_info *head); //打印进程信息
int is_num(char p_name[]); //判断文件名是否为数字
myps *trav_dir(char dir[])
{
DIR *dir_ptr;
myps *head, *p1, *p2;
struct dirent *direntp;
struct stat infobuf;
if((dir_ptr = opendir(dir)) == NULL) //打开目录
{
printf("open dir error");
}
else
{
head = (struct ps_info *)malloc(sizeof(struct ps_info )); //创建链表
p1 = head;
p2 = head;
while((direntp = readdir(dir_ptr)) != NULL) //遍历该目录
{
if((is_num(direntp->d_name)) == 0) //该文件是数字
{
if(p1 == NULL)
{
printf("malloc error\n");
exit(0);
}
if(read_info(direntp->d_name,p1) != 0) //获取当前进程信息
{
printf("read_info error");
exit(0);
}
//将该进程节点插入链表
p2->next = p1;
p2 = p1;
p1 = (struct ps_info*)malloc(sizeof(struct ps_info));
}
}
}
p2->next = NULL;
return head; //返回链表头结点
}
int read_info(char d_name[], struct ps_info *p1)
{
FILE *fd;
char dir[20];
struct stat infobuf;
sprintf(dir,"%s%s","/proc/",d_name); //将文件名和文件路径连接组成绝对路径存储到dir中
chdir("/proc"); //切换到proc目录
if(stat(d_name,&infobuf) == -1) //获取进程(文件)信息失败
{
printf("stat errro");
}
else
{
uid_to_name(infobuf.st_uid,p1); //获取进程属主
}
chdir(dir); //切换到该进程目录下
if((fd = fopen("stat","r")) < 0) //打开文件失败
{
printf("open the file error\n");
exit(0);
}
while(4 == fscanf(fd,"%d %s %c %d\n",&(p1->pid), p1->pname, &(p1->state), &(p1->ppid))) //从文件中读取进程各项信息
{
break;
}
fclose(fd); //关闭文件
return 0;
}
void uid_to_name(uid_t uid, struct ps_info *p1) //获取文件属主
{
struct passwd *getpwuid(),*pw_ptr;
static char numstr[10];
if((pw_ptr = getpwuid(uid)) == NULL) //通过uid获取用户信息
{
sprintf(numstr,"d",uid);
strcpy(p1->user,numstr);
}
else
{
strcpy(p1->user,pw_ptr->pw_name); //获取用户名
}
}
int is_num(char p_name[]) //判断文件是否是数字
{
int i,len;
len = strlen(p_name);
if(len == 0)
{
return -1;
}
for(i = 0; i <len; i++)
{
if(p_name[i] < '0' || p_name[i] > '9') //文件名的每一位都为数字才可
{
return -1;
}
}
return 0;
}
void print_ps(struct ps_info *head) //打印进程信息
{
myps *list;
printf("user\t\tPID\tSTATE\tPNAME\n");
for(list = head; list != NULL; list = list->next) //遍历链表
{
printf("%s\t\t%d\t%c\t%s\n",list->user,list->pid,list->state,list->pname);
}
}
int main()
{
myps *head;
myps *link;
head = trav_dir("/proc/");
if(head == NULL)
{
printf("traverse dir error\n");
}
print_ps(head);
while(head != NULL) //释放链表
{
link = head;
head = head->next;
free(link);
}
return 0;
}