命令解释器开发(2)

1 篇文章 0 订阅
1 篇文章 1 订阅
本文详细介绍了如何在Linux0.11中实现getcwd系统调用,通过遍历i节点回溯到根目录获取当前工作目录,并实现了ls功能,展示目录内容。实验过程包括系统调用注册、i节点操作和目录项解析等关键步骤。
摘要由CSDN通过智能技术生成

满天星

2023.6.1

写在前面:本文提供实现思路,部分核心代码已给出,需要自己完善

一、实验目的和内容

实验目的

  • 掌握系统调用的实现方法,掌握文件系统实现技术。

实验内容

增加 getcwd 系统调用

  • 接口定义如下:
long getcwd(char * buf, size_t size);
/* 功能:获取当前工作目录;
 * 输入:
 - char *buf:一块缓存区,用于保存当前工作目录的字符串。
 - size:buf 缓存区的大小。
* 返回值:成功执行,则返回当前工作目录的字符串的指针。失败,则返回 NULL*/
  • 并通过调用该系统调用来为上一实验开发的命令解释器增加“当前工作目录提示”功能,即 在命令提示符前显示进程当前工作目录的全路径名。

实验3基础下:实现ls内部命令

  • 为命令解释器增加一个内部命令 ls(不能通过外部命令功能实现),其功能是显示目录的内容。

二、操作方法与实验步骤

(一) 理论知识

1.实现getcwd系统调用

  • 我们知道:Linux 0.11 系统采用的是 MINIX 文件系统。MINIX文件系统结构如下:        

  • 在Linux 0.11文件系统中,每个文件或目录名都对应着一个 i 节点。 每个 i 节点结构中存放着对应文件或目录的相关信息:

  • 其中,i_num保存着此文件或目录的i节点号,而**i_zone[9]**中存储着文件或目录的相关内容,对于目录而言,其存储的目录信息就存储在此数组中,以目录项的结构存储,其数据结构为:

  • 即:在目录的i_zone[9]数组中存储着目录下所有文件对应的i节点号和文件名。

  • 在每个目录中还包含两个特殊的文件目录项,它们的名称分别固定是 '.' 和 '..'。'.'目录项中给出了当前目录的 i 节点号,而**'..'目录项中给出了当前目录父目录的 i 节点号**。这也为我们完成本实验提供了实现思路:通过访问进程task_struct结构成员pwd获得当前工作目录的m_inode结构指针:

    struct task_struct {
    /* these are hardcoded - don't touch */
    	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    	long counter;
    	long priority;
    	long signal;
    	struct sigaction sigaction[32];
    	long blocked;	/* bitmap of masked signals */
    /* various fields */
    	int exit_code;
    	unsigned long start_code,end_code,end_data,brk,start_stack;
    	long pid,father,pgrp,session,leader;
    	unsigned short uid,euid,suid;
    	unsigned short gid,egid,sgid;
    	long alarm;
    	long utime,stime,cutime,cstime,start_time;
    	unsigned short used_math;
    /* file system info */
    	int tty;		/* -1 if no tty, so it must be signed */
    	unsigned short umask;
    	struct m_inode * pwd;
    	struct m_inode * root;
    	struct m_inode * executable;
    	unsigned long close_on_exec;
    	struct file * filp[NR_OPEN];
    /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    	struct desc_struct ldt[3];
    /* tss for this task */
    	struct tss_struct tss;
    };
    

    其中m_inode是i节点结构在内存中使用的数据结构:

    struct m_inode {
    	unsigned short i_mode;
    	unsigned short i_uid;
    	unsigned long i_size;
    	unsigned long i_mtime;
    	unsigned char i_gid;
    	unsigned char i_nlinks;
    	unsigned short i_zone[9];
    /* these are in memory also */
    	struct task_struct * i_wait;
    	unsigned long i_atime;
    	unsigned long i_ctime;
    	unsigned short i_dev;
    	unsigned short i_num;
    	unsigned short i_count;
    	unsigned char i_lock;
    	unsigned char i_dirt;
    	unsigned char i_pipe;
    	unsigned char i_mount;
    	unsigned char i_seek;
    	unsigned char i_update;
    };
    
  • 从而可以获取当前工作目录的i节点号(即i_num),查看当前i节点结构(task_struct->pwd所指)的**i_zone[9]**数组中存储的’..’对应的目录项以获取父目录的i节点号,继而查看父目录i节点号对应inode结构获取其父目录i节点号……,依次向上回溯到根目录(task_struct->root所指),我们可以获取当前工作目录到根目录上的所有中间目录的i节点号(结构)。当到达根目录时,就可以向下遍历,根据获取的i节点号(结构)进行匹配查询,便可获取中间目录的文件名,将文件名进行拼接即得到当前工作目录,当然在回溯过程中,我们可以存储每个中间目录m_inode结构的指针,整个过程可以如下图所示(以/usr/root作当前工作目录为例):

2.实现ls功能

  • 在实现getcwd系统调用的基础上,首先明确当前目录是一个文件,可以通过调用getcwd获取其文件路径,继而调用read系统调用获取其存储的内容(目录项),对获取的数据进行解析处理即可实现ls功能。

(二)具体实现

1.增加getcwd系统调用

(1)添加系统调用

  • 修改unistd.h增添系统调用号

    #define __NR_getcwd 91
    
  • 修改sys.h

    /*add sysytem_call*/
    extern int sys_getcwd();
    ...
    fn_ptr sys_call_table[] = { sys_setup,..., sys_getcwd};
    
  • 修改systemcall.s中总系统调用数

    nr_system_calls = 92
    

(2)在/linux/kernel中编写getcwd.c文件

  • 文件核心代码如下:

    while (node != current->root){
        _path[n_path++] = node;
        entries = node->i_size / (sizeof (struct dir_entry)); /*目录项数*/
        /*查看目录项,寻找父目录i节点号*/
        if (!(block = node->i_zone[0])){
            printk("error while get block\\n");
            return NULL;
        }
    	  if (!(bh = bread(i_dev,block))){
            printk("error while read block\\n");
        return NULL;
        }
        de = (struct dir_entry *) bh->b_data;
        i = 0;
        while (i < entries){
          if ((char *)de >= BLOCK_SIZE+bh->b_data) { /*BLOCK_SIZE:块大小 1024*/
    	    brelse(bh);
    	    bh = NULL;
    	    if (!(block = bmap(node, i/DIR_ENTRIES_PER_BLOCK)) ||
    	        !(bh = bread(node->i_dev,block))) {
    		    i += DIR_ENTRIES_PER_BLOCK;
    		    continue;
    	    }
    	    de = (struct dir_entry *) bh->b_data;
        }
            /*匹配父目录项*/
            if (mstrcmp(de->name, (char*)"..")) { 
                i_num = de->inode; /*切换为父目录i节点号*/
                break;
        }
            de++;
            i++;
        }
        brelse(bh);
        node = iget(i_dev, i_num);
    } /*跳出循环说明node指向根目录*/
    
    • 在这段代码中,我们用_path[]数组记录向上回溯时过程中目录的m_inode结构指针,具体实现**思路是:通过访问node(指向回溯过程中正在访问目录m_inode结构指针)获取节点i_zone[0]数据块号,通过i节点所在设备号和设备块号调用bread函数(buffer.c中)以从设备上读取目标数据块,(其功能为:根据指定的设备号和数据块号,首先在高速缓冲区中申请一块缓冲块,如果该缓冲块中已经包含有有效的数据就直接返回该缓冲块指针(缓冲头),否则就从设备中读取指定的数据块到该缓冲块中并返回缓冲块指针。)**我们查看bh对应数据结构:
    struct buffer_head {
    	char * b_data;			/* pointer to data block (1024 bytes) */
    	unsigned long b_blocknr;	/* block number */
    	unsigned short b_dev;		/* device (0 = free) */
    	unsigned char b_uptodate;
    	unsigned char b_dirt;		/* 0-clean,1-dirty */
    	unsigned char b_count;		/* users using this block */
    	unsigned char b_lock;		/* 0 - ok, 1 -locked */
    	struct task_struct * b_wait;
    	struct buffer_head * b_prev;
    	struct buffer_head * b_next;
    	struct buffer_head * b_prev_free;
    	struct buffer_head * b_next_free;
    };
    
    • 可以得到bh数据区指针de,进而在数据区遍历匹配文件名为“..”(父目录)的目录项,并修改i_num为父目录i节点号。获取父目录i节点号后,调用iget函数(inode.c中)从指定设备号的设备中读取指定节点号的i节点,并更新node节点,进行下一次循环操作。以上代码过于熟悉?没错是仿照find_entry函数写的,在namei.c中,find_entry中相应部分如下:
    static struct buffer_head * find_entry(struct m_inode ** dir,
    	const char * name, int namelen, struct dir_entry ** res_dir)
    {
    	int entries;
    	int block,i;
    	struct buffer_head * bh;
    	struct dir_entry * de;
    	struct super_block * sb;
    
    #ifdef NO_TRUNCATE
    	if (namelen > NAME_LEN)
    		return NULL;
    #else
    	if (namelen > NAME_LEN)
    		namelen = NAME_LEN;
    #endif
    	entries = (*dir)->i_size / (sizeof (struct dir_entry));
    	*res_dir = NULL;
    	if (!namelen)
    		return NULL;
    /* check for '..', as we might have to do some "magic" for it */
    	if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') {
    /* '..' in a pseudo-root results in a faked '.' (just change namelen) */
    		if ((*dir) == current->root)
    			namelen=1;
    		else if ((*dir)->i_num == ROOT_INO) {
    /* '..' over a mount-point results in 'dir' being exchanged for the mounted
       directory-inode. NOTE! We set mounted, so that we can iput the new dir */
    			sb=get_super((*dir)->i_dev);
    			if (sb->s_imount) {
    				iput(*dir);
    				(*dir)=sb->s_imount;
    				(*dir)->i_count++;
    			}
    		}
    	}
    	if (!(block = (*dir)->i_zone[0]))
    		return NULL;
    	if (!(bh = bread((*dir)->i_dev,block)))
    		return NULL;
    	i = 0;
    	de = (struct dir_entry *) bh->b_data;
    	while (i < entries) {
    		if ((char *)de >= BLOCK_SIZE+bh->b_data) {
    			brelse(bh);
    			bh = NULL;
    			if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
    			    !(bh = bread((*dir)->i_dev,block))) {
    				i += DIR_ENTRIES_PER_BLOCK;
    				continue;
    			}
    			de = (struct dir_entry *) bh->b_data;
    		}
    		if (match(namelen,name,de)) {
    			*res_dir = de;
    			return bh;
    		}
    		de++;
    		i++;
    	}
    	brelse(bh);
    	return NULL;
    }
    
    • 其实基本一模一样,需要注意的是,bread函数向缓冲区申请了一个缓冲块,使用完之后需要及时通过brelse函数(buffer.c中)释放,因为缓冲块资源是有限的,不及时释放可能导致缓冲块资源不足的情况。

    前面对向上回溯到根目录的核心代码进行了解释,下面对向下遍历获取文件名过程代码进行讲解:

    _path[n_path] = node; /*最终n_path下标指向根目录i节点指针*/
    
    /*以下代码功能是根据从当前工作目录回溯到根目录时记录的i节点信息,
    依次向下遍历这些i节点获取每一个目录文件对应文件名,最后将这些名字
    拼接起来,即得到当前工作目录的工作路径*/
    
    while (n_path > 0){
        node  = _path[n_path--]; /*通过设备号和i节点号获取i节点信息*/
        entries = node->i_size / (sizeof (struct dir_entry)); /*目录项数*/
        /*查看目录项,寻找目标i节点号对应目录项*/
        if (!(block = node->i_zone[0])){
            printk("error while get block1\\n");
        return NULL;
        }
      if (!(bh = bread(i_dev,block))){
            printk("error while read block1\\n");
        return NULL;
        }
        de = (struct dir_entry *) bh->b_data;
        i = 0;
        while (i < entries){
            if ((char *)de >= BLOCK_SIZE+bh->b_data) { /*BLOCK_SIZE:块大小 1024*/
    	    brelse(bh);
    	    bh = NULL;
    	    if (!(block = bmap(node, i/DIR_ENTRIES_PER_BLOCK)) ||
    	        !(bh = bread(node->i_dev,block))) {
    		    i += DIR_ENTRIES_PER_BLOCK;
    		    continue;
    	    }
    	    de = (struct dir_entry *) bh->b_data;
        }
            if (de->inode == _path[n_path]->i_num){ /*找到对应i节点*/
                mstrcat(res, "/");
                mstrcat(res, de->name);
                break;
            }
            de++;
            i++;
        }
        brelse(bh);
        iput(node);
    }
    
    • 不难看出,这段代码的主要功能就是通过回溯过程中记录在**_path[]**数组中的目录m_inode结构指针,对存储的目录项通过i节点号进行匹配查找并获取文件名拼接存储于res数组中记录结果,具体实现与回溯过程相似。 最终经过向下遍历目录节点,获得拼接后存储到res数组中的工作目录。值得注意的是,我们在向上回溯的过程中,通过 iget函数 获取了i节点,在此过程中需要将其对应的i节点引用计数值减一(若i节点链接计数值为0时,则释放该i节点占用的所有磁盘逻辑块),通过调用iput函数来实现。
    • 不难看出,这段代码的主要功能就是通过回溯过程中记录在**_path[]数组中的目录m_inode结构指针,对存储的目录项通过i节点号进行匹配查找并获取文件名拼接存储于res数组中记录结果,具体实现与回溯过程相似。 最终经过向下遍历目录节点,获得拼接后存储到res数组中的工作目录。值得注意的是,我们在向上回溯的过程中,通过 iget函数 获取了i节点,在此过程中需要将其对应的i节点引用计数值减一(若i节点链接计数值为0时,则释放该i节点占用的所有磁盘逻辑块),通过调用iput函数**来实现。

    下一步就是将res中存储的当前工作目录写回buf中:

    int len = mstrlen(res);
    int k = 0;
    while(k < size){
    	put_fs_byte('\\0', buf+k);
    	k++;
    }
      if (len >= size){
          printk("buf size error\\n");
          return NULL;
      }
    else if(len == 0){
    	put_fs_byte('/', buf);
    }
    else{
    	k = 0;
          while(k < len){
    		put_fs_byte(res[k], buf+k);
    		k++;
    	}
    }
    
    • 值得注意的是,在对buf进行修改时,不能通过下标对字符数组进行修改,即buf[i]=x的形式,可能会引起段错误(程序访问了未分配的内存地址),报错信息如下:

    • 需要使用put_fs_byte函数,因为在内核态中,字符数组是以指针的形式存在的,而且指针所指向的内存区域可能已经被释放或者被其他进程占用了。因此,直接使用下标的形式对字符数组进行修改可能会导致未定义的行为或程序崩溃。而使用 put_fs_byte函数来实现对字符数组的修改,可以保证操作的安全性和正确性。put_fs_byte函数是用于将一个字节写入文件系统的函数,它可以将要写入的数据强制类型转换为 unsigned char 类型,然后将数据写入指定的内存地址中。通过使用 put_fs_byte函数来修改字符数组,可以保证修改的数据类型正确,并且不会越界访问到其他内存区域。

    修改makefile文件,并重新编译内核

    OBJS  = sched.o system_call.o traps.o asm.o fork.o \\
    	panic.o printk.o vsprintf.o sys.o exit.o \\
    	signal.o mktime.o sem.o getcwd.o
    
    ### Dependencies:
    getcwd.s getcwd.o: getcwd.c ../include/unistd.h ../include/linux/fs.h \\
      ../include/linux/sched.h ../include/asm/segment.h
    
    • 将getcwd系统调用运用到自写的命令解释器上,完善“当前工作目录提示’’功能:

      printf("[%s]:", getcwd(dirname, sizeof(dirname)));
      

      修改工作平凡而无需展现。 写到这里,增加getcwd系统调用的工作也就圆满完成了。

2.实现ls功能

  • 实际上,相较于添加getcwd系统调用而言,实现ls功能显得没那么有挑战性,通过前面的分析,我们可以调用getcwd系统调用获取当前工作目录路径,然后调用openread系统调用读取此目录文件以获取存储的目录项内容,主要问题是,直接读取一个目录,所获得的数据格式是什么?或者说如何解析获得的数据以获得此目录下的所有文件信息。

    先看目录项结构:

  • 在目录项结构中,i节点号占2个字节,文件名占14个字节,故整个目录项结构占16个字节,我们通过调用read进行读取时传入了一个字符数组,那么存储在字符数组中的正是一个个大小为16字节的文化目录项结构。那么在一个目录项中,我们所需要的是在16个字节中的后14个字节,于是对字符数组中每16个字节中的后14个字节解析出来即是对应的每一个文件名。综上所述,代码如下:

    i = 0, k = 0;
    while (i < n){
    	i += 2;
    	j = 0;
    	while (j < 14 && (i+j) < n){
    			res[k][j] = buf[i+j];
    			j++;
    	}
    	k++;
    	i = i+j;
    }
    i = 0, j = 1;
    while(i < k){
    	printf("%-12s\\t", res[i]);
    	if (j % 3 == 0)
    		printf("\\n");
    	j++;
    	i++;	
    }
    printf("\\n");
    

    init\main.c

  • 完善main.c修改,重新编译内核

三、实验结果与分析

实验结果展现

可以看到,功能顺利实现,本实验圆满成功。

四、问题与建议

  • 从刚开始的毫无思绪,逼着自己硬“吃下”《linux内核完全注解0.12内核,赵烔》关于文件系统的注解,我逐渐抓住了实现的思路,也对linux0.11的文件系统有了更深入的理解,本次实验对我而言收获很大,尽管调试很辛苦,但是贵在坚持了下来,言尽于此,加油。
  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
C语言实现的SCPI命令解释器可以用于控制和管理各种仪器设备,例如示波器、信号发生器、电源等。下面是一个基本的SCPI命令解释器的设计: 1. 定义SCPI命令格式,包括命令头和参数部分,例如:“MEASure:VOLTage:DC?”。 2. 使用C语言编写解析器函数,将接收到的SCPI命令解析为命令头和参数部分,例如:“MEASure:VOLTage:DC”和“?”。 3. 根据不同的命令头,调用相应的处理函数,例如针对“MEASure:VOLTage:DC”命令头,调用相应的函数进行电压测量操作。 4. 在处理函数中,根据参数部分的不同,执行相应的操作,例如根据“?”返回电压值。 5. 将结果返回给用户或者其他系统,例如将测量结果显示在屏幕上或者将结果发送到其他设备。 基于Python的SCPI命令解释器的设计与C语言类似,但是可以使用Python的高级特性和库来简化开发过程。 1. 定义SCPI命令格式,包括命令头和参数部分,例如:“MEASure:VOLTage:DC?”。 2. 使用Python的正则表达式库或者字符串操作函数,将接收到的SCPI命令解析为命令头和参数部分,例如:“MEASure:VOLTage:DC”和“?”。 3. 根据不同的命令头,使用Python的面向对象编程特性,调用相应的处理类和方法,例如针对“MEASure:VOLTage:DC”命令头,调用相应的类进行电压测量操作。 4. 在处理类中,根据参数部分的不同,执行相应的操作,例如根据“?”返回电压值。 5. 使用Python的网络编程库或者GUI库,将结果返回给用户或者其他系统,例如将测量结果显示在屏幕上或者将结果发送到其他设备。 总之,无论是基于C语言还是Python的SCPI命令解释器,都需要定义命令格式、编写解析器、实现处理函数、返回结果等基本步骤,并且需要根据具体应用场景选择合适的语言和库。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

满天星..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值