Linux系统编程【1】——编写more命令
背景介绍
笔者知识背景
笔者接触Linux快一年了。理论知识方面:学习了操作系统基础知识,了解进程调度、内存分配、文件管理、磁盘I/O这些基本的概念。
实操方面:会使用Linux简单命令,在嵌入式系统设计课程实验中完成Linux内核编译和烧写、在信息安全实践课程实验上基于Linux操作系统完成HTTPS原理实操、CSRF、XSS、点击劫持的攻防,以及在Linux操作系统的云服务器上部署自己的博客。
如果仅仅是将Linux操作系统作为工具,使用它提供的功能,掌握简单的命令和上网搜索这一技能就基本能满足需要了。但是笔者更想了解其背后的机理,也想将之前所学的理论知识落实到具体实现上,于是便有了这一系列博客。
什么是系统编程
通俗的讲就是“写操作系统”。
它与“普通编程”的不同之处在于,“普通程序”通过调用系统提供的功能,来实现用户需求的功能,对于系统提供的功能,只管使用就是,无需关注实现。在用户看来,就像是“普通程序”直接在操作各种硬件资源。
而“系统编程”则需要实现那些系统调用,对下层的硬件资源进行管理。所以在进行系统编程时,必须对系统的结构和工作方式有更深入的了解。管理下层各种硬件资源,将硬件资源抽象为接口,提供系统调用供上层的“普通程序”使用。所以操作系统是那个在背后默默付出的人,把下面的“脏活累活”都处理好,为上层提供稳定的服务(泪目)。
more命令
more命令的作用
在linux控制台中输入man more,可以看到more的使用说明。"man"是"manuals"的缩写,即“使用手册”的意思。
more的语法形式是:more [options] file...
options是可选的参数,现在忽略这个参数,之后只考虑最简单的more file...如何实现。more后面可以跟多个文件名,用空格分隔。
more的作用就是显示指定的文件的内容,一次显示一屏幕,然后在底部提示用户按键,用户按"q"就直接退出,按空格就显示下一屏幕的内容,按"Enter"则显示下一行的内容。
如何实现more命令
确定参数的传递
Linux主要是用C语言编写的,那就用C语言来实现more命令。
其输入形式是more filename1 filename2 ... ,然后按回车运行more命令。笔者发现其与普通的C语言程序运行方式不同。普通的C语言程序,比如"hello.c"编译之后生成的可执行文件假设为"hello",那么用"./hello"命令就可以执行程序,后面没有跟上任何参数。
这里就需要了解main函数的参数了。
#include<stdio.h>
int main(int argc,char* argv[]){
return 0;
}
一般main函数参数为空,实际上里面可以含有两个参数。argc表示传递的参数个数,char* argv[]表示传入的是指向字符串的指针。argv[0]指向自身运行目录路径和程序名,如more程序放在/home/lularible/more路径下,那么argv[0]就指向"/home/lularible/more"这个字符串。argv[1]指向第一个参数,依次类推。
所以我们就可以直接拿到argv中的指向文件名指针,以便后续进行文件读取操作。
辅助函数的设计
现在已经获得了指向文件名的字符串指针,那么就可以拿着这个指针打开目标文件,然后进行读取显示,接着等待用户操作,直到文件显示完毕或者用户主动退出。
通过以上分析,我们需要:
- 1.打开目标文件函数
- 2.读取显示文件内容函数
- 3.处理用户输入函数
其中,打开目标函数文件函数在目前看来是难点,所幸的是系统为我们提供了这个函数"fopen"。笔者猜测该函数是通过DFS或者BFS来遍历目录树,从而查找目标文件的,待后续验证。
假设通过打开目标文件函数已经获得了目标文件指针,那么读取显示文件函数就通过这个指针将一段文件内容暂存起来并显示,等用户输入空格或回车后继续搬运后面的文件内容。
处理用户输入函数最简单,通过判断输入的是什么,返回不同的值。
初级版本的"more"代码实现
//more01.c
#include<stdio.h>
#include<stdlib.h>
#define PAGELEN 24 //每一页显示24行
#define LINELEN 512 //每次获取512字节
//读取文件并显示的函数声明
void do_more(FILE*);
//处理用户的下一步输入的函数声明
int see_more();
int main(int ac,char* av[]) //ac表示参数个数,av存储指向参数字符串的指针。av[0]指向more这个程序名,av[1]指向第一个参数
{
FILE* fp;
//如果只输入一个more命令,就从标准输入中读取内容来显示
if(ac == 1)
do_more(stdin);
else{
//依次显示每个文件的内容
while(--ac){
//以只读的方式打开文件
if((fp = fopen(*++av,"r"))!=NULL){
do_more(fp);
fclose(fp);
}
else{
exit(1);
}
}
}
}
void do_more(FILE* fp)
{
//首次读取并显示PAGELEN行,然后等待用户输入
char line[LINELEN];
int num_of_lines = 0;
int reply;
//不断读取文件内容
while(fgets(line,LINELEN,fp)){
//当显示行数到达最大值,暂停读取,等待用户输入
if(num_of_lines == PAGELEN){
reply = see_more();
//用户输入'q',直接退出
if(reply == 0)
break;
//用户输入空格,将行计数清0,输入回车,将行计数减1
num_of_lines -= reply;
}
//显示一行内容
if(fputs(line,stdout) == EOF)
exit(1);
num_of_lines++;
}
}
int see_more()
{
int c;
//反显输出more提示
printf("\033[7m more? \033[m");
while((c = getchar())!= EOF){
if(c == 'q')
return 0;
if(c == ' ')
return PAGELEN;
if(c == '\n')
return 1;
}
return 0;
}
初级版本缺陷
初级版本的"more"程序,实现了读取指定文件内容,但是与真正的"more"命令有一些主要差距。
- 差距1:没有显示出已显示内容占总内容的百分比
- 差距2:在"more?"提示下,用户输入指令后还需按回车才继续运行,而不是按下指令直接运行
- 差距3:每一次的"more?"提示都会显示在屏幕上,并随着文本内容上移,应当在用户输入指令后将提示内容去掉,以免影响阅读原文件
- 差距4:当使用|进行输出重定向时,直接就读取文件内容作为用户输入了,而不是从键盘读取用户输入
改进版的"more"代码实现
- 对于差距1,只需要获取到当前读取的文件的大小和已经显示的内容大小,问题就解决了。定义getFileSize函数,传入一个参数FILE*,返回文件大小。另外每读取一行,就将已显示的内容大小那个变量增加strlen(line)。
long getFileSize(FILE* fp){
long size;
fseek(fp, 0L, SEEK_END); //将读写指针指向文件尾
size = ftell(fp); //获得读写指针之前的长度
rewind(fp); //重置读写指针指向文件头
return size;
}
-
对于差距2,笔者网上搜索类似功能后找到了一个可靠的办法,即改变终端的设置,之后在代码中体现
-
对于差距3,目前还没有找到满意的能去除已显示的内容的功能。但可以通过控制光标的位置以及利用空格覆盖的方法,可以做到同等效果
-
对于差距4,可以从/dev/tty这个文件中读取键盘输入
源代码如下:
//more02.c
#include<stdio.h>
#include<sys/stat.h>
#include<termios.h>
#include<string.h>
#include<stdlib.h>
#define PAGELEN 24 //每一页显示24行
#define LINELEN 512 //每次获取512字节
//函数声明区
//读取文件并显示的函数声明
void do_more(FILE*);
//处理用户的下一步输入的函数声明
int see_more(FILE* cmd,long fileSize,long curSize);
//改变终端设置,使用户输入不显示,且无需按回车就可以执行
void changeMode(int mode);
//获取目标文件的大小
long getFileSize(FILE*);
//全局变量区
//保存原本的终端输入输出设置
static struct termios old;
int main(int ac,char* av[]) //ac表示参数个数,av存储指向参数字符串的指针。av[0]指向more这个程序名,av[1]指向第一个参数
{
FILE* fp;
//如果只输入一个more命令,就从标准输入中读取文件来显示
if(ac == 1)
do_more(stdin);
else{
//获得原本的终端输入输出设置,保存在old变量中,以便还原,"0"是键盘的文件描述符
tcgetattr(0,&old);
//依次显示每个文件的内容
while(--ac){
//以只读的方式打开文件
if((fp = fopen(*++av,"r"))!=NULL){
do_more(fp);
fclose(fp);
}
else{
exit(1);
}
}
}
return 0;
}
void do_more(FILE* fp)
{
char line[LINELEN];
int num_of_lines = 0;
int reply;
long fileSize = getFileSize(fp); //获取文件总大小
long curSize = 0; //当前已显示的内容大小
FILE* fp_tty;
fp_tty = fopen("/dev/tty","r");
if(fp_tty == NULL){
exit(1);
}
while(fgets(line,LINELEN,fp)){
curSize += strlen(line);
if(num_of_lines == PAGELEN){
changeMode(0);
reply = see_more(fp_tty,fileSize,curSize);
changeMode(1);
if(reply == 0){
putchar('\n');
break;
}
putchar('\r'); //将光标移到该行最左边
printf(" "); //输出一连串空格覆盖提示句
putchar('\r'); //再次移动光标到该行最左边
num_of_lines -= reply;
}
if(fputs(line,stdout) == EOF)
exit(1);
num_of_lines++;
}
}
int see_more(FILE* cmd,long fileSize,long curSize)
{
int c;
double percent = (double)curSize / (double)fileSize;
percent *= 100;
printf("\033[7m --more?--(%d\%) \033[m", (int)percent);
while((c = getc(cmd))!= EOF){
if(c == 'q')
return 0;
if(c == ' ')
return PAGELEN;
if(c == '\n')
return 1;
}
return 0;
}
void changeMode(int mode)
{
struct termios new;
new = old;
new.c_lflag &= ~(ICANON | ISIG);
new.c_cc[VTIME] = 0;
new.c_cc[VMIN] = 1;
if(mode == 0){
new.c_lflag &= ~ECHO; //不显示输入的值
tcsetattr(0, TCSANOW, &new); //输入之后立即执行,不需要按回车键
}
if(mode == 1){
tcsetattr(0, TCSADRAIN, &old); //还原设置
}
}
long getFileSize(FILE* fp){
long size;
fseek(fp, 0L, SEEK_END); //将读写指针指向文件尾
size = ftell(fp); //获得读写指针之前的内容大小
rewind(fp); //重置读写指针指向文件头
return size;
}
命令执行方式
先用如下的命令将源文件编译成可执行的程序"more02"。
gcc more02.c -o more02
然后在同一个目录下可以使用"./more02 filename"来执行。
那么如何做到像真正的more命令那样,直接输入"more filename"来执行?
这就涉及到“环境变量”了,Linux终端输入$export$PATH
命令,可以显示环境变量的配置路径,如下图:
可以看到,/bin这个路径已经被配置好了,这个路径下有很多Linux命令,在任意文件夹中输入这些命令名称都可以执行。
所以可以将"more02"这个可执行文件放入已经配置好的环境变量路径下,如"/bin"路径。
笔者在自己的用户名路径下创建了"bin"文件夹,并在其中编写和编译了"more"程序,系统自动的将该路径配置为环境变量路径了,那么输入"more02 filename"也能正常执行。
总结
虽然改进版的"more"命令弥补了初版的"more"命令缺陷,但与真正的more命令还存在很大差距。比如对文件进行类型和权限检查,没有实现附带的可选参数等。
在实现"more"命令的代码编写中,笔者发现还是需要调用一些已有的函数来辅助,比如fopen函数,而并非完全自己从零实现。跳出来看,more函数也可以被其他更高层次的函数所调用。所以在不同的层面上,所需考虑的问题也就不同。本文的重点还是在于通过实现"more"命令,来增加对linux的了解。
参考资料
《Understanding Unix/Linux Programming A Guide to Theory and Practice》
欢迎大家转载本人的博客(需注明出处),本人另外还有一个个人博客网站:[https://www.lularible.cn],欢迎前去浏览。