本实验依赖实验1/2/3/4/5/6/7。请把你做的实验1/2/3/4/5/6/7的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6” /“LAB7”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab8的测试应用程序,可能需对已完成的实验1/2/3/4/5/6/7的代码进行进一步改进。
不太需要改写什么
文件打开大致流程
- 用户态应用程序:
- 用户态的应用程序通过系统调用(例如
open
系统调用)来请求内核打开文件。- 系统调用接口层:
- 在系统调用接口层,例如
sys_open
函数,内核获取用户态应用程序传递的参数,例如文件路径名(path
)和打开标志(open_flags
)。- 虚拟文件系统层(VFS):
- 在 VFS 层,内核根据传递的文件路径名找到对应的文件系统(例如 SFS 文件系统)。
- VFS 层会调用具体文件系统的打开函数(例如
sfs_open
)来进行文件的打开操作。- 文件系统层(例如 SFS 文件系统):
- 在文件系统层,打开函数(例如
sfs_open
)会获取文件路径名对应的 inode,并创建struct file
结构体,用于表示打开的文件。该结构体包含了与文件相关的信息,如文件的 inode、文件读写位置等。- 打开函数还可能进行权限检查,确保应用程序有权访问该文件。
- 返回文件描述符:
- 在 VFS 层,打开文件后会为该文件创建一个文件描述符,并返回给用户态应用程序。文件描述符是一个整数,用于标识文件的打开状态。
- 应用程序的文件操作:
- 用户态应用程序在获取到文件描述符后,可以使用该文件描述符进行文件读写等操作,通过系统调用(例如
read
、write
)来请求内核进行相应的操作。
下面给出读操作有关sfs_io_nolock函数的调用关系
- 用户程序调用
sys_read
系统调用:
- 用户程序通过
sys_read
系统调用来请求读取文件内容。- 系统调用处理:
- 内核根据系统调用号,将控制权转交给系统调用处理函数
sys_read
。- 文件描述符解析:
sys_read
函数解析文件描述符,并找到对应的struct file
结构体。- 文件读取调用:
sys_read
函数调用文件读取函数file_read
。file_read
函数首先检查文件读取权限,并调用文件系统抽象层函数vfs_read
。- 虚拟文件系统抽象层(VFS):
vfs_read
函数根据文件描述符,调用 VFS 层的文件读取函数vop_read
。vop_read
函数根据文件节点(struct vnode
)类型,调用不同文件系统的文件读取函数,如sfs_read
(用于 SFS 文件系统)。- SFS 文件系统读取:
sfs_read
函数根据文件节点中的文件读写偏移量(文件指针),找到相应的磁盘块,并调用sfs_io_nolock
函数读取磁盘块的数据。sfs_io_nolock
:
sfs_io_nolock
函数负责实际的读取磁盘块操作。- 它调用
sfs_read_block
函数从磁盘中读取指定的数据块。- 磁盘读取:
sfs_read_block
函数利用设备驱动层的接口函数,将磁盘块数据从硬盘读取到内存缓冲区。- 数据传递:
sfs_read_block
函数将读取的数据传递给上层的sfs_read
函数。- 数据传递至文件读取函数:
sfs_read
函数将读取的数据传递给vop_read
函数。- 数据传递至系统调用函数:
vop_read
函数将读取的数据传递给vfs_read
函数。- 数据传递至系统调用处理函数:
vfs_read
函数将读取的数据传递给sys_read
函数。- 返回用户空间:
sys_read
函数将读取的数据传递回用户程序空间,并返回读取的字节数。
sfs_io_nolock 实现
static int
sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write)
{
bool aligned = 0;
if ((blkoff = offset % SFS_BLKSIZE) != 0)
{
aligned = 1;
size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset);
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0){
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0){
goto out;
}
alen += size; buf += size;
if (nblks == 0) goto out;
}
if(nblks - aligned > 0){
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno + aligned, &ino)) != 0){
goto out;
}
if ((ret = sfs_block_op(sfs, buf, ino, nblks - aligned)) != 0){
goto out;
}
buf += (nblks - aligned) * SFS_BLKSIZE;
alen += (nblks - aligned) * SFS_BLKSIZE;
}
if ((size = endpos % SFS_BLKSIZE) != 0 ){
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno + nblks, &ino)) != 0){
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0){
goto out;
}
alen += size;
}
struct sfs_disk_inode
结构体指针 din
,并对文件进行合法性检查,如文件类型不为目录等。endpos
,并进行参数合法性检查,确保读写操作在有效的范围内。write
),选择对应的缓冲区操作函数 sfs_buf_op
和块操作函数 sfs_block_op
,分别对应于读操作和写操作。offset
和结束位置 endpos
,按块为单位进行读写操作。alen
。dirty
)。这个写起来真的好多好难啊,代码还是不放了,答案也有(鄙人不会,只能抄答案),下面给出一些该函数实现流程吧
- 首先,检查当前进程的
mm
是否为空。mm
是进程的内存管理结构,用于管理进程的虚拟地址空间。如果当前进程的mm
不为空,说明内存管理结构已经被占用,这意味着当前进程已经运行了其他程序或者已经加载了其他程序,因此抛出 panic,表示异常情况。- 创建新的
mm
结构,用于存放当前进程的虚拟地址空间。首先,调用mm_create()
函数创建一个新的mm
结构,并将返回的指针存储在mm
变量中。- 为新的
mm
结构设置页目录表(PDT),通过setup_pgdir()
函数进行页表项的设置。该函数的目的是为了为用户程序提供一个独立的虚拟地址空间,并将用户程序的代码、数据和栈等内容映射到不同的物理页上。- 解析 ELF 可执行文件头部,读取 ELF 文件的信息,并进行合法性检查。首先,定义一个
elf
结构体和一个指向该结构体的指针elfp
,用于存储 ELF 文件的头部信息。然后,通过load_icode_read()
函数从磁盘上读取 ELF 可执行文件的头部信息到elfp
中。接着,检查 ELF 文件的魔数是否正确,如果不正确则说明该 ELF 文件不合法,抛出-E_INVAL_ELF
错误。- 遍历 ELF 可执行文件的 program header 表,对 TEXT/DATA/BSS 段进行处理。首先,定义一个
ph
结构体和一个指向该结构体的指针php
,用于存储 program header 表项的信息。然后,通过循环遍历 ELF 文件的每一个 program header 表项。- 对 TEXT/DATA 段进行分配虚拟地址空间和读取操作。对于每一个 program header 表项,首先检查其类型是否为
ELF_PT_LOAD
,表示该段是可加载的段。然后,检查p_filesz
是否大于p_memsz
,如果是则说明 ELF 文件不合法,抛出-E_INVAL_ELF
错误。接着,判断p_filesz
是否为 0,如果为 0 则说明该段没有数据需要读取,直接跳过。如果以上条件都不满足,说明该段需要加载到用户空间。- 为 TEXT/DATA 段在用户空间分配虚拟地址空间,并将对应的数据从 ELF 文件中读取到用户空间。首先,根据 program header 表项中的虚拟地址
p_va
和大小p_memsz
以及权限信息p_flags
,调用mm_map()
函数将该段映射到用户空间的合适位置。接着,计算出每一页的偏移和大小,并逐页分配物理内存,并将数据从 ELF 文件中读取到相应的物理页中。- 对 BSS 段进行分配虚拟地址空间和清零操作。对于每一个 program header 表项,同样检查其类型是否为
ELF_PT_LOAD
,表示该段是可加载的段。然后,判断p_memsz
是否大于p_filesz
,如果大于,则说明 BSS 段需要进行清零初始化。接着,为 BSS 段在用户空间分配虚拟地址空间,并将对应的内存空间清零。- 为用户栈设置虚拟地址空间,并将命令行参数和用户栈的布局写入用户空间。首先,定义一些变量来辅助设置用户栈。然后,根据命令行参数计算出所需的栈空间大小,并调用
mm_map()
函数将用户栈映射到用户空间的合适位置。接着,根据命令行参数的长度和个数,将参数写入用户栈中。- 切换当前进程的页目录表到新创建的用户页目录表,完成从内核空间到用户空间的切换。首先,增加新创建的
mm
结构的引用计数,然后将当前进程的mm
指针指向新创建的mm
结构,将当前进程的 CR3 寄存器设置为新的页目录表的物理地址,最后通过lcr3
指令切换到用户态的页目录表。- 最后,设置用户程序的执行环境。首先,设置中断帧
tf
,包括代码段和数据段选择子、栈指针esp
、用户程序的入口地址eip
,以及标志寄存器eflags
。然后,返回 0,表示加载用户程序成功。如果在加载过程中发生错误,会进行相应的清理工作,并返回相应的错误码。这样,
load_icode
函数负责将 ELF 可执行文件加载到用户空间中,设置用户程序执行的环境,并切换到用户态,使用户程序开始执行。
顺便解释一下主要elf格式信息
ELF头部(ELF Header):位于文件的开头,用于描述整个ELF文件的结构和属性。包含了与ELF文件有关的一些基本信息,如文件的类型(可执行文件、共享库或目标文件)、目标硬件平台、入口点地址等。
程序头表(Program Header Table):位于ELF头部指定的偏移位置,用于描述可执行程序或共享库中各个程序段(Program Segment)的属性和位置。每个程序段描述了一个逻辑上连续的内存区域,包含了一段代码或数据。程序头表在目标文件中可以不存在,但在可执行文件和共享库中通常存在。
节头表(Section Header Table):位于ELF头部指定的偏移位置,用于描述目标文件中各个节(Section)的属性和位置。节是ELF文件的组成单位,包含了一些编译器产生的信息,如代码、数据、符号表等。
符号表(Symbol Table):位于一个特定的节中,用于描述目标文件中定义和引用的符号(变量、函数等)的信息。符号表提供了在程序中查找和链接符号的功能。
字符串表(String Table):位于一个特定的节中,用于存储目标文件中使用的字符串,如符号表中的符号名称、节的名称等。通过字符串表,可以根据字符串的偏移值找到相应的字符串。
代码段和数据段:ELF文件中的代码段(Code Segment)和数据段(Data Segment)存储了程序的指令和数据。在可执行文件中,代码段包含了程序的机器指令,数据段包含了程序使用的全局变量和常量等数据。
BSS段:位于数据段之后,用于存储未初始化的全局变量和静态变量。BSS段在ELF文件中并不占据实际的存储空间,而是在程序加载到内存时由系统初始化为零
唔,至此,本人对操作系统的方方面面有了大致的框架,也了解到许多底层知识,中断描述符呀,虚拟内存,同步互斥等等,对自己提升真的好多。ucore结束了,但是操作系统还没结束
说实话,本人水平有限,对于ucore很多地方确实还是不太理解,没有很好掌握操作系统的内部机理,只是大致有了一个整体框架,框架内部的细节还是没有填满,最近由于处理事情较多,过段时间写CSAPP的时候,再接再厉,希望到时候我的基础会扎实一些
祝贺你通过自己的努力,完成了ucore OS lab1-lab8!
这段话还是恭喜你们吧,哈哈