Linux系统编程之实现more命令

Linux系统编程之实现more命令

在学习内核以及内核编程的间隙,会时不时进行一些系统编程的练习,来更好的理解操作系统,边分析操作系统的职责,同时动手练习编写一些与操作系统紧密相连的程序,目的在于最终可编写实现自己意图功能的程序。

  • 系统编程与内核编程的不同?

    系统编程可以说是操作系统提供给用户程序的一组“特殊”接口,用户程序可以通过这组特殊的接口来获得操作系统内核提供的特殊服务。在linux中用户程序不能直接访问内核提供的服务,为了更好的保护内核,将程序的运行空间分为内核空间和用户空间,他们运行在不同的级别上。

  • 系统编程所使用的系统调用,或是说linux给予可供调用的系统资源?

    处理器、输入输出(I/O)、进程管理、内存、设备、计时器、进程间通信、网络。

此次编写第一个命令的实现:more

  • more命令的作用

    与cat类似,浏览文件内容,但是more为分页查看,最基本的指令为在分页浏览的时候

    • space:显示下一页内容
    • enter:显示下一行
    • q:结束显示
    • h:联机帮助
    • 在屏幕底部用反白字体显示文件百分比
  • more命令用法

    • more filename
    • command | more
    • more < filename

###main函数

#include <stdio.h>
#define PAGELEN 24
#define LINELEN 512

void do_more(FILE *);
int see_more();
int main(int ac ,char *av[])
{
	FILE *fp;
	if (ac == 1)
	do_more(stdin);
	else
	while (--ac)
	if ((fp = fopen(* ++av,"r"))!= NULL)  /* av ac??*/
	{
		do_more(fp);
		fclose(fp);
	
	}	
	else
		exit(1);
	return 0;

}

main函数的意图比较明确,一个选择分支结构,其中具体的参数问题解释见main函数有关问题的探讨

若main函数执行只有一个参数,则意味着无文件,那么把键盘输入到缓冲区里的数据用do_more函数读出来。

若有多个参数,则执行读文件时候的方法,即当用只读方式打开指针数组所指向的文件地址内容不为空时,do_more显示文件内容,而所指向地址为空时退出当前进程。

do_more函数与see_more函数

void do_more(FILE *fp)
{

	char line [LINELEN];
	int num_of_lines =0;
	int see_more(),reply;
	while(fgets(line,LINELEN,fp))
	{
	if(num_of_lines==PAGELEN)
	   {
	     reply =see_more();
	     if(reply==0)
	     break;
	     num_of_lines -=reply;
	   }
	if(fputs(line,stdout) ==EOF)
	exit(1);
	num_of_lines++;
	}

此函数的作用是使用fgets函数从所传递过来的文件中一行行读取数据,当屏幕满了时,调用see_more函数,做出相应处理,最终当输出的字符串为文件结尾时退出当前进程。

int see_more()
{
int c;
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;

}

至于相应处理,就是由see_more来控制,也就是之前提到的more命令中的相应命令,当然不能忘记反白的字体‘more?’

  • 若从键盘输入q则返回值为0,在do_more里break
  • 若从键盘输入空格,则返回行数,do_more中把行数重置为0,再显示接下来的文件。
  • 若从键盘输出回车,则返回1,do_more中行数只减1,那么只多显示1行。

看起来我们完成了!那么来试试它的效果

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux$ ./more1 more1.c
#include <stdio.h>
#define PAGELEN 24
#define LINELEN 512

void do_more(FILE *);
int see_more();
int main(int ac ,char *av[])
{
	FILE *fp;
	if (ac == 1)
	do_more(stdin);
	else
	while (--ac)
	if ((fp = fopen(* ++av,"r"))!= NULL)  /* av ac??*/
	{
		do_more(fp);
		fclose(fp);
	
	}	
	else
		exit(1);
	return 0;

}
 more?

 more?
void do_more(FILE *fp)
 more?
{
 more? 

	char line [LINELEN];
	int num_of_lines =0;
	int see_more(),reply;
	while(fgets(line,LINELEN,fp))
	{
	if(num_of_lines==PAGELEN)
	   {
	     reply =see_more();
	     if(reply==0)
	     break;
	     num_of_lines-=reply;
	   }

重定向数据源问题

看起来虽然有些小瑕疵,但是效果还行,可是有一个很关键的问题就是当我们的数据来源不是文件,而是管道重定向的时候,就有些问题了。

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux$ ls /bin | ./more1 
bash
brltty
bunzip2
busybox
bzcat
bzcmp
bzdiff
bzegrep
bzexe
bzfgrep
bzgrep
bzip2
bzip2recover
bzless
bzmore
cat
chacl
chgrp
chmod
chown
chvt
cp
cpio
dash
 more?date
 more?df
 more?dmesg
 more?domainname
 more?echo
 more?efibootdump
 more?egrep
 more?fgconsole
 more?findmnt
 more?fusermount
 more?grep
 more?gzexe
 more?hciconfig
 more?ip
 more?kbd_mode
 more?kmod
  • 可以看到,当数据来源为这种时候,它输出了24行但是并没有停下而是全部输出完了,这是为什么呢?

分析问题可以看到在see_more函数中,需要我们输入字符来进行下面的操作,但若我们使用的是管道重定向或输入重定向的话,就不会请求我们输入了,因为getchar()这个函数是读取标准输入的数据,现在我们用管道重定向或输入重定向将标准输入重定向了,所以getchar()函数会直接读取ls /bin这个命令输出。

  • 如何解决这个问题呢?

    我们可否把所有数据从标准输入读入,然后直接从键盘读用户的输入,也就是说把这两种数据流分开。Linux中有一个文件/dev/tty,此文件是显示器与键盘的描述文件,向这个文件写相当于显示在用户屏幕,读相当于从键盘获取用户输入,我们可以有效使用这一点,那么我们只需要改一下少量的do_more与see_more。

void do_more(FILE *fp)
{

	char line [LINELEN];
	int num_of_lines =0;
	int see_more(),reply;
	FILE *fp_tty              \\NEW
	fp_tty = fopen("/dev/tty","r");
	if(fp_tty==NULL)
		exit(1);
	while(fgets(line,LINELEN,fp))
	{
	if(num_of_lines==PAGELEN)
	   {
	     reply =see_more(fp_tty);
	     if(reply==0)
	     break;
	     num_of_lines =reply;
	   }
	if(fputs(line,stdout) ==EOF)
	exit(1);
	num_of_lines++;
	}

}

int see_more(FILE *cmd)         \\NEW
{
int c;
printf("\033[7m more?\033[m");
while ((c=getc(cmd)!=EOF))      \\NEW
	{
	if(c=='q')
	return 0;
	if(c==' ')
	return PAGELEN;
	if(c=='\n')
	return 1;
	
	}
	return 0;

}

这样就解决了输入重定向后不按规则的漏洞问题。

额外回车问题

我们平时使用more命令的时候是不需要输入字符后按回车生效的,同时输入的字符也不会回显,那么能否改变目前程序中这个问题呢。

这个问题刚提出的时候,我有些不以为然,那不就getch一下嘛,修改下函数不就好了,后来发现getch函数需要conio.h头文件的支持,但conio.h不是C标准库中的头文件,在ISO和POSIX标准中均没有定义,所以无法直接调用。那么现在有两种方法解决:

  • 在Linux下实现一下getch函数。

  • 通过修改终端属性属性

    这里采用的是第二种方法,利用"tcgetattr"和"tcsetattr"函数来实现修改终端属性。tcgetattr用于获取终端的相关参数,而tcsetattr函数用于设置终端参数。

#include<stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <termios.h>  
#define PAGELEN 24  
#define LINELEN 514  
void do_more(FILE *);  
int see_more(FILE *);  
int main(int ac, char *av[])  
{  
    FILE *fp;  
    if(ac==1)  
      do_more(stdin);  
    else  
    {  
        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 reply;  
    int number_line = 0;  
    FILE *fp_tty_in, *fp_tty_out;  
    fp_tty_in = fopen("/dev/tty", "r");  
    fp_tty_out = fopen("/dev/tty", "w");  
    struct termios initial_settings, new_settings;  
    tcgetattr(fileno(fp_tty_in), &initial_settings);//获取当前终端的属性。  
    new_settings = initial_settings;  
                                     //new_settings.c_lflag &= ~ICANON;设置终端为非标准模式  
    new_settings.c_lflag &= ~ECHO; //设置终端不回显  
    //设置读入一个字符,立即返回字符。  
    new_settings.c_cc[VMIN] = 1;   
    new_settings.c_cc[VTIME] = 0;  
    if(tcsetattr(fileno(fp_tty_in), TCSANOW, &new_settings) != 0) { // 重新配置终端接口  
        fprintf(stderr, "could not set attributes\n");  
    }  
    while(fgets(line, LINELEN, fp) != NULL)  
    {  
        if(number_line == PAGELEN)  
        {  
              
            reply = see_more(fp_tty_in);  
            if(reply == 0)   
              break;  
            number_line -= reply;  
        }  
        if( fputs(line, fp_tty_out) == EOF)  
        {  
            tcsetattr(fileno(fp_tty_in), TCSANOW, &initial_settings); // 恢复终端接口的配置  
             exit(1);  
        }  
        number_line ++;  
    }  
    tcsetattr(fileno(fp_tty_in), TCSANOW, &initial_settings);// 恢复终端接口的配置  
}  
int see_more(FILE *cmd)  
{  
    int c;  
    printf("\033[7m more? \033[m");  
    do {  
        c = fgetc(cmd);  
        if(c == 'q')  
          return 0;  
        if(c == ' ')  
          return PAGELEN;  
        if(c == '\n')  
          return 1;  
    }while(1);  
    return 0;  
}  

其实除过这两个最显著的问题,这次所写的more与系统所提供的还有一定差距,例如,如何显示已经读取的百分比,以及如何根据终端灵活的确定每页行数,这些问题会在以后的系统编程不断学习中得到进一步的解决。

猜你喜欢

转载自blog.csdn.net/weixin_43122409/article/details/83514589
今日推荐