背景介绍

笔者知识背景

笔者接触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],欢迎前去浏览。

Linux系统编程【1】——编写more命令的更多相关文章

  1. Linux系统编程【3.1】——编写ls命令

    ls命令简介 老规矩,直接在终端输入:man ls (有关于man命令的简介可以参考笔者前期博客:Linux系统编程[1]--编写more命令) 可以看到,ls命令的作用是显示目录中的文件名,它带有可 ...

  2. Linux系统编程【2】——编写who命令

    学到的知识点 通过实现who命令,学到了: 1.使用man命令寻找相关信息 2.基于文件编程 3.体会到c库函数与系统调用的不同 4.加深对缓冲技术的理解 who命令的作用 who命令的使用 在控制终 ...

  3. Linux系统编程【3.2】——ls命令优化版和ls -l实现

    前情提要 在笔者的上一篇博客Linux系统编程[3.1]--编写ls命令中,实现了初级版的ls命令,但是与原版ls命令相比,还存在着显示格式和无颜色标记的不同.经过笔者近两天的学习,基本解决了这两个问 ...

  4. linux系统编程综合练习-实现一个小型的shell程序(一)

    之前已经花了不少篇幅学习了linux系统编程的很多知识点:文件与io.进程.信号.管道,而零散的知识点,怎么能够综合的串接起来是学习的一个很重要的目的,当然最好的方式就是用所学的知识点做一个项目了,所 ...

  5. Linux 系统编程 学习:01-进程的有关概念 与 创建、回收

    Linux 系统编程 学习:01-进程的有关概念 与 创建.回收 背景 上一讲介绍了有关系统编程的概念.这一讲,我们针对 进程 开展学习. 概念 进程的身份证(PID) 每一个进程都有一个唯一的身份证 ...

  6. Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

    Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号 背景 上一讲我们介绍了Unix IPC中的2种管道. 回顾一下上一讲的介绍,IPC的方式通常有: Unix IPC包括:管道 ...

  7. Linux系统编程温故知新系列 --- 01

    1.大端法与小端法 大端法:按照从最高有效字节到最低有效字节的顺序存储,称为大端法 小端法:按照从最低有效字节到最高有效字节的顺序存储,称为小端法 网际协议使用大端字节序来传送TCP分节中的多字节整数 ...

  8. Linux系统编程@进程通信(一)

    进程间通信概述 需要进程通信的原因: 数据传输 资源共享 通知事件 进程控制 Linux进程间通信(IPC)发展由来 Unix进程间通信 基于System V进程间通信(System V:UNIX系统 ...

  9. Linux C 程序 文件操作(Linux系统编程)(14)

    文件操作(Linux系统编程) 创建一个目录时,系统会自动创建两个目录.和.. C语言实现权限控制函数 #include<stdio.h> #include<stdlib.h> ...

  10. 读书笔记之Linux系统编程与深入理解Linux内核

    前言 本人再看深入理解Linux内核的时候发现比较难懂,看了Linux系统编程一说后,觉得Linux系统编程还是简单易懂些,并且两本书都是讲Linux比较底层的东西,只不过侧重点不同,本文就以Linu ...

随机推荐

  1. 与HBase对比,Cassandra的优势特性是什么?

    在1月9日Cassandra中文社区开年活动开始之前的闲聊时间,活动的四位嘉宾就"HBase和Cassandra的对比"这一话题展开了讨论.   总的来说,HBase和Cassan ...

  2. 检查Mysql主从状态

    .检查MySQL主从同步状态 #!/bin/bash USER=bak PASSWD=123456 IO_SQL_STATUS=$(mysql -u$USER -p$PASSWD -e  show s ...

  3. 三种梯度下降算法的区别(BGD, SGD, MBGD)

    前言 我们在训练网络的时候经常会设置 batch_size,这个 batch_size 究竟是做什么用的,一万张图的数据集,应该设置为多大呢,设置为 1.10.100 或者是 10000 究竟有什么区 ...

  4. MySQL全面瓦解20:可编程性之流程控制语句

    背景 说到流程控制语句,我们在程序语法中用的比较多,比如C#的if..else...,while...,?: 等.同样的,在MySQL中,也有一些流程控制的语法,方便我们在写函数.存储过程的时候对逻辑 ...

  5. 消息队列之kafka

    消息队列之activeMQ 消息队列之RabbitMQ 1.kafka介绍 kafka是由scala语言开发的一个多分区,多副本的并且居于zookeeper协调的分布式的发布-订阅消息系统.具有高吞吐 ...

  6. Android事件分发机制三:事件分发工作流程

    前言 很高兴遇见你~ 本文是事件分发系列的第三篇. 在前两篇文章中,Android事件分发机制一:事件是如何到达activity的? 分析了事件分发的真正起点:viewRootImpl,Activit ...

  7. 理解js闭包9大使用场景

    1.返回值(最常用) //1.返回值 最常用的 function fn(){ var name="hello"; return function(){ return name; } ...

  8. Redis 实战 —— 09. 实现任务队列、消息拉取和文件分发

    任务队列 P133 通过将待执行任务的相关信息放入队列里面,并在之后对队列进行处理,可以推迟执行那些耗时对操作,这种将工作交给任务处理器来执行对做法被称为任务队列 (task queue) . P13 ...

  9. 一体化的Linux系统性能和使用活动监控工具–Sysstat

    [转]原文出处: Tecmint-Kuldeep Sharma   译文出处:Linux Story-天寒   欢迎分享原创到伯乐头条 在监控系统资源.系统性能和使用活动方面,Sysstat的确是一个 ...

  10. Java——集合框架之ArrayList,LinkedList,迭代器Iterator

    概述--集合框架 Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(Java Collection ...