UNIX提出了四个文件系统抽象概念:文件(file)、目录项(dentry)、索引节点(inode) 和安装点(mount point)
- 文件:UNIX文件中的内容可理解为是一有序字节buffer,文件都有一个方便应用程序识别的文件名称(也称文件路径名)。典型的文件操作有读、写、创建和删除等。
- 目录项:目录项不是目录(又称文件路径),而是目录的组成部分。在UNIX中目录被看作一种特定的文件,而目录项是文件路径中的一部分。如一个文件路径名是“/test/testfile”,则包含的目录项为:根目录“/”,目录“test”和文件“testfile”,这三个都是目录项。一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点(见下面的描述)位置。
- 索引节点:UNIX将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点。
- 安装点:在UNIX中,文件系统被安装在一个特定的文件路径位置,这个位置就是安装点。所有的已安装文件系统都作为根文件系统树中的叶子出现在系统中。
上述抽象概念形成了UNIX文件系统的逻辑数据结构,并需要通过一个具体文件系统的架构设计与实现把上述信息映射并储存到磁盘介质上,从而在具体文件系统的磁盘布局(即数据在磁盘上的物理组织)上具体体现出上述抽象概念。比如文件元数据信息存储在磁盘块中的索引节点上。当文件被载入内存时,内核需要使用磁盘块中的索引点来构造内存中的索引节点。
ucore模仿了UNIX的文件系统设计,ucore的文件系统架构主要由四部分组成:
- 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得ucore内核的文件系统服务。
- 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个同样的抽象函数指针列表和数据结构屏蔽不同文件系统的实现细节。
- Simple FS文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口
- 外设接口层:向上提供device访问接口屏蔽不同硬件细节。向下实现访问各种具体设备驱动的接口,比如disk设备接口/串口设备接口/键盘设备接口等。
假如应用程序操作文件(打开/创建/删除/读写),首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发给某一具体文件系统(比如SFS文件系统),具体文件系统(Simple FS文件系统层)把应用程序的访问请求转化为对磁盘上的block的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。
ucore文件系统总体结构
从ucore操作系统不同的角度来看,ucore中的文件系统架构包含四类主要的数据结构, 它们分别是:
- 超级块(SuperBlock),它主要从文件系统的全局角度描述特定文件系统的全局信息。它的作用范围是整个OS空间。
- 索引节点(inode):它主要从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置。它的作用范围是整个OS空间。
- 目录项(dentry):它主要从文件系统的文件路径的角度描述了文件路径中的一个特定的目录项(注:一系列目录项形成目录/文件路径)。它的作用范围是整个OS空间。对于SFS而言,inode(具体为struct sfs_disk_inode)对应于物理磁盘上的具体对象,dentry(具体为struct sfs_disk_entry)是一个内存实体,其中的ino成员指向对应的inode number,另外一个成员是file name(文件名).
- 文件(file),它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。它的作用范围是某一具体进程
在ucore中把目录看成是一个特殊的文件,所以opendir和closedir实际上就是调用与文件相关的open和close函数。
对应ucore上,具体的过程如下:
- 1、 以打开文件为例,首先用户会在进程中调用 safe_open() 函数,然后依次调用如下函数 open->sys_open->syscall,从而引发系统调用然后进入内核态,然后会由 sys_open 内核函数处理系统调用,进一步调用到内核函数 sysfile_open,然后将字符串 "/test/testfile" 拷贝到内核空间中的字符串 path 中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。
- 2、 在VFS层,系统会分配一个 file 数据结构的变量,这个变量其实是 current->fs_struct->filemap[] 中的一个空元素,即还没有被用来打开过文件,但是分配完了之后还不能找到对应对应的文件结点。所以系统在该层调用了 vfs_open 函数通过调用 vfs_lookup 找到 path 对应文件的 inode,然后调用vop_open函数打开文件。然后层层返回,通过执行语句 file->node=node;,就把当前进程的 current->fs_struct->filemap[fd](即 file 所指变量)的成员变量 node 指针指向了代表文件的索引节点 node。这时返回 fd。最后完成打开文件的操作。
- 3、 在第2步中,调用了 SFS 文件系统层的 vfs_lookup 函数去寻找 node,这里在 sfs_inode.c 中我们能够知道 .vop_lookup = sfs_lookup。
- 4、看到 sfs_lookup 函数传入的三个参数,其中 node 是根目录“/”所对应的 inode 节点;path 是文件的绝对路径(例如“/test/file”),而 node_store 是经过查找获得的 file 所对应的 inode 节点。 函数以“/”为分割符,从左至右逐一分解path获得各个子目录和最终文件对应的 inode 节点。在本例中是分解出 “test” 子目录,并调用 sfs_lookup_once 函数获得 “test” 子目录对应的 inode 节点 subnode,然后循环进一步调用 sfs_lookup_once 查找以 “test” 子目录下的文件 “testfile1” 所对应的 inode 节点。当无法分解 path 后,就意味着找到了testfile1对应的 inode 节点,就可顺利返回了。
- 5、而我们再进一步观察 sfs_lookup_once 函数,它调用 sfs_dirent_search_nolock 函数来查找与路径名匹配的目录项,如果找到目录项,则根据目录项中记录的 inode 所处的数据块索引值找到路径名对应的 SFS 磁盘 inode,并读入 SFS 磁盘 inode 对的内容,创建 SFS 内存 inode。
// file&dir接口层定义了进程在内核中直接访问的文件相关信息
struct file {
enum {
FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
} status; //访问文件的执行状态
bool readable; //文件是否可读
bool writable; //文件是否可写
int fd; //文件在filemap中的索引值
off_t pos; //访问文件的当前位置
struct inode *node; //该文件对应的-内存inode指针
int open_count; //打开此文件的次数
};
// proc_struct结构中描述了进程访问文件的数据接口files_struct
struct files_struct {
struct inode *pwd; //进程当前执行目录的内存inode指针
struct file *fd_array; //进程打开文件的数组
atomic_t files_count; //访问此文件的线程个数
semaphore_t files_sem; //确保对进程控制块中fs_struct的互斥访问
};
//当创建一个进程后,该进程的files_struct将会被初始化或复制父进程的files_struct。当用户进程打开一个文件时,将从fd_array数组中取得一个空闲file项,然后会把此file的成员变量node指针指向一个代表此文件的inode的起始地址。
index node是位于内存的索引节点,它是VFS结构中的重要数据结构,因为它实际负责把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统。其定义如下:
struct inode {
union { //包含不同文件系统特定inode信息的union成员变量
struct device __device_info; //设备文件系统内存inode信息
struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息
} in_info;
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type; //此inode所属文件系统类型
atomic_t ref_count; //此inode的引用计数
atomic_t open_count; //打开此inode对应文件的个数
struct fs *in_fs; //抽象的文件系统,包含访问文件系统的函数指针
const struct inode_ops *in_ops; //抽象的inode操作,包含访问inode的函数指针
};
ucore目前支持如下几种类型的文件:
- 常规文件:文件中包括的内容信息是由应用程序输入。SFS文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节。
- 目录:包含一系列的entry,每个entry包含文件名和指向与之相关联的索引节点(index node)的指针。目录是按层次结构组织的。
- 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名。
- 设备文件:不包含数据,但是提供了一个映射物理设备(如串口、键盘等)到一个文件名的机制。可通过设备文件访问外围设备。
- 管道:管道是进程间通讯的一个基础设施。管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接受数据。
在lab8中关注的主要是SFS支持的常规文件、目录和链接中的 hardlink 的设计实现。SFS文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中。SFS通过索引节点来管理目录和常规文件,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其它控制信息都保存在索引节点中。可以有多个文件名可指向一个索引节点。
第三个磁盘(即disk0,前两个磁盘分别是 ucore.img 和 swap.img)用于存放一个SFS文件系统(Simple Filesystem)。通常文件系统中,磁盘的使用是以扇区(Sector)为单位的,但是为了实现简便,SFS 中以 block (4K,与内存 page 大小相等)为基本单位。第0个块(4K)是超级块(superblock),它包含了关于文件系统的所有关键参数,当计算机被启动或文件系统被首次接触时,超级块的内容就会被装入内存。其定义如下:
struct sfs_super {
uint32_t magic; /* magic number, should be SFS_MAGIC */
uint32_t blocks; /* # of blocks in fs */
uint32_t unused_blocks; /* # of unused blocks in fs */
char info[SFS_MAX_INFO_LEN + 1]; /* infomation for sfs */
};
可以看到,包含一个成员变量魔数magic,其值为0x2f8dbe2a,内核通过它来检查磁盘镜像是否是合法的 SFS img;成员变量blocks记录了SFS中所有block的数量,即 img 的大小;成员变量unused_block记录了SFS中还没有被使用的block的数量;成员变量info包含了字符串"simple file system"。
第1个块放了一个root-dir的inode,用来记录根目录的相关信息。有关inode还将在后续部分介绍。这里只要理解root-dir是SFS文件系统的根结点,通过这个root-dir的inode信息就可以定位并查找到根目录下的所有文件信息。
从第2个块开始,根据SFS中所有块的数量,用1个bit来表示一个块的占用和未被占用的情况。这个区域称为SFS的freemap区域,这将占用若干个块空间。为了更好地记录和管理freemap区域,专门提供了两个文件kern/fs/sfs/bitmap.[ch]来完成根据一个块号查找或设置对应的bit位的值。
最后在剩余的磁盘空间中,存放了所有其他目录和文件的inode信息和内容数据信息。需要注意的是虽然inode的大小小于一个块的大小(4096B),但为了实现简单,每个 inode 都占用一个完整的 block。
在sfs_fs.c文件中的sfs_do_mount函数中,完成了加载位于硬盘上的SFS文件系统的超级块superblock和freemap的工作。这样,在内存中就有了SFS文件系统的全局信息。
sfs_disk_inode记录了文件或目录的内容存储的索引信息,该数据结构在硬盘里储存,需要时读入内存。sfs_disk_entry表示一个目录中的一个文件或目录,包含该项所对应inode的位置和文件名,同样也在硬盘里储存,需要时读入内存。
磁盘索引节点:SFS中的磁盘索引节点代表了一个实际位于磁盘上的文件。首先我们看看在硬盘上的索引节点的内容:
struct sfs_disk_inode {
uint32_t size; // 如果inode表示常规文件,则size是文件大小
uint16_t type; // inode的文件类型
uint16_t nlinks; // 此inode的硬链接数
uint32_t blocks; // 此inode的数据块数的个数
uint32_t direct[SFS_NDIRECT]; // 此inode的直接数据块索引值(有SFS_NDIRECT个)
uint32_t indirect; // 此inode的一级间接数据块索引值
};
//通过上表可以看出,如果inode表示的是文件,则成员变量direct[]直接指向了保存文件内容数据的数据块索引值。indirect间接指向了保存文件内容数据的数据块,indirect指向的是间接数据块(indirect block),此数据块实际存放的全部是数据块索引,这些数据块索引指向的数据块才被用来存放文件内容数据。
//ucore 里 SFS_NDIRECT 是 12,即直接索引的数据页大小为 12 * 4k = 48k;当使用一级间接数据块索引时,ucore 支持最大的文件大小为 12 * 4k + 1024 * 4k = 48k + 4m。数据索引表内,0 表示一个无效的索引,inode 里 blocks 表示该文件或者目录占用的磁盘的 block 的个数。indiret 为 0 时,表示不使用一级索引块。(因为 block 0 用来保存 super block,它不可能被其他任何文件或目录使用,所以这么设计也是合理的)。
//对于普通文件,索引值指向的 block 中保存的是文件中的数据。而对于目录,索引值指向的数据保存的是目录下所有的文件名以及对应的索引节点所在的索引块(磁盘块)所形成的数组。数据结构如下:
/* file entry (on disk) */
struct sfs_disk_entry {
uint32_t ino; // 索引节点所占数据块索引值
char name[SFS_MAX_FNAME_LEN + 1]; // 文件名
};
//操作系统中,每个文件系统下的 inode 都应该分配唯一的 inode 编号。SFS 下,为了实现的简便(偷懒),每个 inode 直接用他所在的磁盘 block 的编号作为 inode 编号。比如,root block 的 inode 编号为 1;每个 sfs_disk_entry 数据结构中,name 表示目录下文件或文件夹的名称,ino 表示磁盘 block 编号,通过读取该 block 的数据,能够得到相应的文件或文件夹的 inode。ino 为0时,表示一个无效的 entry。此外,和 inode 相似,每个 sfs_dirent_entry 也占用一个 block。
内存中的索引节点
/* inode for sfs */
struct sfs_inode {
struct sfs_disk_inode *din; /* on-disk inode */
uint32_t ino; /* inode number */
uint32_t flags; /* inode flags */
bool dirty; /* true if inode modified */
int reclaim_count; /* kill inode if it hits zero */
semaphore_t sem; /* semaphore for din */
list_entry_t inode_link; /* entry for linked-list in sfs_fs */
list_entry_t hash_link; /* entry for hash linked-list in sfs_fs */
};
//可以看到SFS中的内存inode包含了SFS的硬盘inode信息,而且还增加了其他一些信息,这属于是便于进行是判断否改写、互斥操作、回收和快速地定位等作用。需要注意,一个内存inode是在打开一个文件后才创建的,如果关机则相关信息都会消失。而硬盘inode的内容是保存在硬盘中的,只是在进程需要时才被读入到内存中,用于访问文件或目录的具体内容数据
Inode的文件操作函数
static const struct inode_ops sfs_node_fileops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_openfile,
.vop_close = sfs_close,
.vop_read = sfs_read,
.vop_write = sfs_write,
……
};
//上述sfs_openfile、sfs_close、sfs_read和sfs_write分别对应用户进程发出的open、close、read、write操作。其中sfs_openfile不用做什么事;sfs_close需要把对文件的修改内容写回到硬盘上,这样确保硬盘上的文件内容数据是最新的;sfs_read和sfs_write函数都调用了一个函数sfs_io,并最终通过访问硬盘驱动来完成对文件内容数据的读写。
Inode的目录操作函数
static const struct inode_ops sfs_node_dirops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_opendir,
.vop_close = sfs_close,
.vop_getdirentry = sfs_getdirentry,
.vop_lookup = sfs_lookup,
……
};
//对于目录操作而言,由于目录也是一种文件,所以sfs_opendir、sys_close对应户进程发出的open、close函数。相对于sfs_open,sfs_opendir只是完成一些open函数传递的参数判断,没做其他更多的事情。目录的close操作与文件的close操作完全一致。由于目录的内容数据与文件的内容数据不同,所以读出目录的内容数据的函数是sfs_getdirentry,其主要工作是获取目录下的文件inode信息。
###sfs_io_nolock实现
每次通过 sfs_bmap_load_nolock 函数获取文件索引编号,然后调用 sfs_buf_op 完成实际的文件读写操作。
/*
* sfs_io_nolock - Rd/Wr a file contentfrom offset position to offset+ length disk blocks<-->buffer (in memroy)
* @sfs: sfs file system
* @sin: sfs inode in memory
* @buf: the buffer Rd/Wr
* @offset: the offset of file
* @alenp: the length need to read (is a pointer). and will RETURN the really Rd/Wr lenght
* @write: BOOL, 0 read, 1 write
*/
static int
sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) {
struct sfs_disk_inode *din = sin->din;
assert(din->type != SFS_TYPE_DIR);
off_t endpos = offset + *alenp, blkoff;
*alenp = 0;
// calculate the Rd/Wr end position
if (offset < 0 || offset >= SFS_MAX_FILE_SIZE || offset > endpos) {
return -E_INVAL;
}
if (offset == endpos) {
return 0;
}
if (endpos > SFS_MAX_FILE_SIZE) {
endpos = SFS_MAX_FILE_SIZE;
}
if (!write) {
if (offset >= din->size) {
return 0;
}
if (endpos > din->size) {
endpos = din->size;
}
}
int (*sfs_buf_op)(struct sfs_fs *sfs, void *buf, size_t len, uint32_t blkno, off_t offset);
int (*sfs_block_op)(struct sfs_fs *sfs, void *buf, uint32_t blkno, uint32_t nblks);
if (write) {
sfs_buf_op = sfs_wbuf, sfs_block_op = sfs_wblock;
}
else {
sfs_buf_op = sfs_rbuf, sfs_block_op = sfs_rblock;
}
int ret = 0;
size_t size, alen = 0;
uint32_t ino;
uint32_t blkno = offset / SFS_BLKSIZE; // The NO. of Rd/Wr begin block
uint32_t nblks = endpos / SFS_BLKSIZE - blkno; // The size of Rd/Wr blocks
//LAB8:EXERCISE1 YOUR CODE HINT: call sfs_bmap_load_nolock, sfs_rbuf, sfs_rblock,etc. read different kind of blocks in file
/*
* (1) If offset isn't aligned with the first block, Rd/Wr some content from offset to the end of the first block
* NOTICE: useful function: sfs_bmap_load_nolock, sfs_buf_op
* Rd/Wr size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset)
* (2) Rd/Wr aligned blocks
* NOTICE: useful function: sfs_bmap_load_nolock, sfs_block_op
* (3) If end position isn't aligned with the last block, Rd/Wr some content from begin to the (endpos % SFS_BLKSIZE) of the last block
* NOTICE: useful function: sfs_bmap_load_nolock, sfs_buf_op
*/
// 找到内存文件索引对应的 block 的编号 ino
if(offset % SFS_BLKSIZE){
size = nblks ? SFS_BLKSIZE - offset % SFS_BLKSIZE : (endpos - offset);
if((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) goto out;
alen += size;
if(nblks == 0) goto out;
buf += size;
++blkno;
--nblks;
}
//读取中间部分的数据,将其分为 size 大小的块,然后一次读一块直至读完
while(nblks > 0){
if((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) goto out;
if((ret = sfs_block_op(sfs, buf, blkno, 1)) != 0) goto out;
alen += SFS_BLKSIZE, buf += SFS_BLKSIZE, ++blkno, --nblks;
}
//读取第三部分的数据
if(endpos % SFS_BLKSIZE){
if((ret = sfs_bmap_load_nolock(sfs, sin, blkno, ino)) != 0) goto out;
if((ret = sfs_buf_op(sfs, buf, endpos % SFS_BLKSIZE, blkno, 0)) != 0) goto out;
alen += endpos % SFS_BLKSIZE;
}
out:
*alenp = alen;
if (offset + alen > sin->din->size) {
sin->din->size = offset + alen;
sin->dirty = 1;
}
return ret;
}
读 elf 文件变成从磁盘上读,而不是直接在内存中读
proc->filesp = NULL; //初始化fs中的进程控制结构
完整的 alloc_proc 函数的实现:
//LAB8:EXERCISE2 YOUR CODE HINT:need add some code to init fs in proc_struct, ...
static struct proc_struct *alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //进程状态为为初始化
proc->pid = -1; //进程ID为-1
proc->runs = 0; //进程运行时间为0
proc->kstack = 0; //内核栈为0
proc->need_resched = 0; //进程不需要调度
proc->parent = NULL; //父进程为空
proc->mm = NULL; //内存管理为空
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL; //中断帧为空
proc->cr3 = boot_cr3; //cr3寄存器
proc->flags = 0; //标记
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0; //等待状态
proc->cptr = proc->optr = proc->yptr = NULL; //相关指针初始化
proc->rq = NULL; //运行队列
list_init(&(proc->run_link)); //运行队列链表
proc->time_slice = 0; //进程运行的时间片
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL; //进程池
proc->lab6_stride = 0;
proc->lab6_priority = 0; //优先级
proc->filesp = NULL; //初始化fs中的进程控制结构
}
return proc;
}
// load_icode - called by sys_exec-->do_execve
static int
load_icode(int fd, int argc, char **kargv) {
/* LAB8:EXERCISE2 YOUR CODE HINT:how to load the file with handler fd in to process's memory? how to setup argc/argv?
* MACROs or Functions:
* mm_create - create a mm
* setup_pgdir - setup pgdir in mm
* load_icode_read - read raw data content of program file
* mm_map - build new vma
* pgdir_alloc_page - allocate new memory for TEXT/DATA/BSS/stack parts
* lcr3 - update Page Directory Addr Register -- CR3
*/
/* (1) create a new mm for current process
* (2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
* (3) copy TEXT/DATA/BSS parts in binary to memory space of process
* (3.1) read raw data content in file and resolve elfhdr
* (3.2) read raw data content in file and resolve proghdr based on info in elfhdr
* (3.3) call mm_map to build vma related to TEXT/DATA
* (3.4) callpgdir_alloc_page to allocate page for TEXT/DATA, read contents in file
* and copy them into the new allocated pages
* (3.5) callpgdir_alloc_page to allocate pages for BSS, memset zero in these pages
* (4) call mm_map to setup user stack, and put parameters into user stack
* (5) setup current process's mm, cr3, reset pgidr (using lcr3 MARCO)
* (6) setup uargc and uargv in user stacks
* (7) setup trapframe for user environment
* (8) if up steps failed, you should cleanup the env.
*/
if(current->mm == NULL) panic("load_icode : mm is busy currently.\n");
int ret = _E_NO_MEM;
struct mm_struct* mm;
// (1) create a new mm for current process
if((mm = mm_create()) == NULL) goto bad_mm;
// (2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
if(setup_pgdir(mm) != 0) goto bad_pgdir_cleanup_mm;
// (3) copy TEXT/DATA/BSS parts in binary to memory space of process
struct Page *page;
struct elfhdr elf;
if(elf.e_magic != ELF_MAGIC){
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}
uint32_t vm_flags, perm, phnum;
struct proghdr __ph, *ph = &__ph;
//(3.4) find every program section headers
for(phnum = 0; phnum < elf.e_phnum; ++phnum){ //e_phnum 代表程序段入口地址数目,即多少各段
off_t phoff = elf->e_phoff + sizeof(struct proghdr) * phnum; //循环读取程序的每个段的头部
if ((ret = load_icode_read(fd, ph, sizeof(struct proghdr), phoff)) != 0) {// 读取program header
goto bad_cleanup_mmap;
}
if (ph->p_type != ELF_PT_LOAD) {
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {
continue ;
}
//(3.5) call mm_map to build vma related to TEXT/DATA
vm_flags = 0, perm = PTE_U;//建立虚拟地址与物理地址之间的映射
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;// 根据 ELF 文件中的信息,对各个段的权限进行设置
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {// 将这些段的虚拟内存地址设置为合法的
goto bad_cleanup_mmap;
}
off_t offset = ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
ret = -E_NO_MEM;
//(3.6) callpgdir_alloc_page to allocate page for TEXT/DATA, read contents in file
// and copy them into the new allocated pages
// callpgdir_alloc_page to allocate pages for BSS, memset zero in these pages
//复制数据段和代码段
end = ph->p_va + ph->p_filesz; //计算数据段和代码段终止地址
//(3.6.1) copy TEXT/DATA section of bianry program
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {// 为 TEXT/DATA 段逐页分配物理内存空间
ret = -E_NO_MEM;
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
//每次读取size大小的块,直至全部读完
if ((ret = load_icode_read(fd, page2kva(page) + off, size, offset)) !=
0) { //load_icode_read 通过 sysfile_read 函数实现文件读取,将磁盘上的 TEXT/DATA 段读入到分配好的内存空间中去
goto bad_cleanup_mmap;
}
start += size, offset += size;
}
//建立BSS段
end = ph->p_va + ph->p_memsz; //同样计算终止地址
if (start < la) {// 如果存在 BSS 段,并且先前的 TEXT/DATA 段分配的最后一页没有被完全占用,则剩余的部分被BSS段占用,因此进行清零初始化
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {// 如果 BSS 段还需要更多的内存空间的话,进一步进行分配
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {// 为 BSS 段分配新的物理内存页
ret = -E_NO_MEM;
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
//每次操作 size 大小的块
memset(page2kva(page) + off, 0, size);// 将分配到的空间清零初始化
start += size;
}
}
sysfile_close(fd);//关闭文件,加载程序结束
//(4)建立相应的虚拟内存映射表
vm_flags = VM_READ | VM_WRITE | VM_STACK;// 设置用户栈的权限
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {// 将用户栈所在的虚拟内存区域设置为合法的
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
//(5)设置用户栈
mm_count_inc(mm);// 切换到用户的内存空间,这样的话后文中在栈上设置参数部分的操作将大大简化,因为具体因为空间不足而导致的分配物理页的操作已经交由page fault处理了,是完全透明的
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));
//(6)处理用户栈中传入的参数,其中 argc 对应参数个数,uargv[] 对应参数的具体内容的地址
uint32_t argv_size=0, i;
for (i = 0; i < argc; i ++) {// 先算出所有参数加起来的长度
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}
uintptr_t stacktop = USTACKTOP - (argv_size/sizeof(long)+1)*sizeof(long);
char** uargv=(char **)(stacktop - argc * sizeof(char *));
argv_size = 0;
for (i = 0; i < argc; i ++) { //将所有参数取出来放置 uargv
uargv[i] = strcpy((char *)(stacktop + argv_size ), kargv[i]);
argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + 1)+1;
}
stacktop = (uintptr_t)uargv - sizeof(int); //计算当前用户栈顶
*(int *)stacktop = argc;
//(7)设置进程的中断帧
struct trapframe *tf = current->tf;// 设置中断帧
memset(tf, 0, sizeof(struct trapframe));//初始化 tf,设置中断帧
tf->tf_cs = USER_CS;// 需要返回到用户态,因此使用用户态的数据段和代码段的选择子
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = stacktop;// 栈顶位置为先前计算过的栈顶位置,注意在C语言的函数调用规范中,栈顶指针指向的位置应该是返回地址而不是第一个参数,这里让栈顶指针指向了第一个参数的原因在于,在中断返回之后,会跳转到ELF可执行程序的入口处,在该入口处会进一步使用call命令调用主函数,这时候也就完成了将 Return address 入栈的功能,因此这里无需画蛇添足压入返回地址
tf->tf_eip = elf->e_entry;// 将返回地址设置为用户程序的入口
tf->tf_eflags = FL_IF;// 允许中断,根据 IA32 的规范,eflags 的第 1 位需要恒为 1
ret = 0;
out:
return ret; //返回
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}
load_icode 主要是将文件加载到内存中执行,从上面的注释可知分为了一共七个步骤:
- 1、建立内存管理器
- 2、建立页目录
- 3、将文件逐个段加载到内存中,这里要注意设置虚拟地址与物理地址之间的映射
- 4、建立相应的虚拟内存映射表
- 5、建立并初始化用户堆栈
- 6、处理用户栈中传入的参数
- 7、最后很关键的一步是设置用户进程的中断帧
- 8、发生错误还需要进行错误处理。