满天星
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系统调用获取当前工作目录路径,然后调用open、read系统调用读取此目录文件以获取存储的目录项内容,主要问题是,直接读取一个目录,所获得的数据格式是什么?或者说如何解析获得的数据以获得此目录下的所有文件信息。
先看目录项结构:
-
-
在目录项结构中,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的文件系统有了更深入的理解,本次实验对我而言收获很大,尽管调试很辛苦,但是贵在坚持了下来,言尽于此,加油。