提交所有LaTeX文件。
This commit is contained in:
65
操作系统/实验报告/Lab1.tex
Normal file
65
操作系统/实验报告/Lab1.tex
Normal file
@@ -0,0 +1,65 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mylabname}{系统软件启动过程}
|
||||
\renewcommand{\mydate}{2024年3月26日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\myitem{完成相关实验内容后,回答以下问题:}{
|
||||
\questionandanswer[]{
|
||||
为何要开启A20? uCore OS是如何开启A20的?
|
||||
}{
|
||||
如果不开启A20会导致地址线第21位永远为0,无法完整进行内存寻址。uCore OS通过向8042键盘控制器的命令端口和数据端口发送命令和数据来开启A20。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
试分析段描述符表(GDT )的结构,说明段描述符中每个字段的含义以及作用。uCore OS是如何初始化GDT表的?
|
||||
}{
|
||||
% 段选择子的每个元素为16位,前13位是INDEX,表示这个段选择子在GDT数组或LDT数组的索引号;第14位是Table Indicator,这个值为0表示查找GDT,1则查找LDT;最后两位是Request Privilege Level,表示以什么样的权限去访问段。
|
||||
\begin{center}
|
||||
\includegraphics[width=1\linewidth]{imgs/2024-03-26-21-17-25.png}
|
||||
\end{center}
|
||||
uCore OS初始化了一个空的表项、一个内核代码段、一个内核数据段。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
实模式和保护模式有何不同?uCore OS是如何使能和进入保护模式的?
|
||||
}{
|
||||
实模式的寻址范围不超过1M,并且没有分段机制等,而保护模式的寻址范围则没有1M的限制,并且可以实现分段机制等。uCore OS通过把控制寄存器的0位设置成1来使能和进入保护模式。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
Bootloader是如何利用ELF文件头的相关属性加载和运行uCore OS Kernel的?
|
||||
}{
|
||||
首先判断文件头的e\_magic是否等于ELF\_MAGIC,之后根据e\_phoff找到程序头表的位置,在程序头表中找到分段数和各个分段的偏移位置、大小,接着把各个分段加载到内存中,最后跳转到e\_entry入口开始执行。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
分析中断描述符表(IDT)的结构,说明中断描述符中每个字段的含义以及作用。 uCore OS是如何实现中断机制的?
|
||||
}{
|
||||
中断描述符表中的每个表项称为中断描述符,它可以确定相应的中断处理程序的位置,中断描述符的结构和每个字段的含义以及作用如下:
|
||||
\begin{center}
|
||||
\includegraphics[width=1\linewidth]{imgs/IDT1.png}
|
||||
\includegraphics[width=1\linewidth]{imgs/IDT2.png}
|
||||
\end{center}
|
||||
它确定了段选择子、段内偏移、该段是否已调入内存、该中断的特权级、中断类型(中断门或者陷阱门)等。
|
||||
|
||||
uCore OS将每个中断门都初始化为带中断号参数的调用(最后调用trap.c中的内容),之后把系统调用的中断门的特权级设置为用户态,最后加载中断描述符表寄存器。
|
||||
}
|
||||
}
|
||||
\myitem{程序设计与实现的基本思路}{
|
||||
\item 大部分实现的功能在trap.c文件中,首先从实验视频中可以得知,在触发中断后会进入trap.c的执行流程,之后根据不同的中断号执行不同的流程,由于题目要求通过键盘中断实现计时器,因此主要关注键盘中断和时钟中断;
|
||||
\item 根据题意,在键盘按下S时开始,P暂停,E停止,C继续,A正计时,B倒计时。那么首先考虑键盘中断,这里很明显很适合用switch语句,根据键盘中断后得到的字符不同,执行不同的流程;
|
||||
\item 这里还需要注意整个功能是存在不同的状态的(其实是因为单线程才会有这么多状态,如果多线程都不需要考虑状态,事件驱动就行),首先很明显可以知道有正在计时状态和停止状态。然后我们考虑这两个状态会在什么时候互相转化,从停止状态转到开始状态可以是按下S或者C,从开始状态转到停止状态可以是按下P或E,或者是倒计时到0了。而且题目需要实现正计时和倒计时,这是两种不同的模式,因此还需要一个状态变量记录当前的模式,通过按A和B切换;
|
||||
\item 还要注意,当按了B后进入倒计时,此时要输入时间,但是这时候还没有gets等函数的实现,需要自己通过键盘中断进行输入,那么在键盘中断的时候就不仅需要捕获字母,也需要捕获数字,但我们不希望在输入数字的时候不小心输入了字母就打断了数字输入,这时候就需要增加状态了,需要一个状态表示正在输入数字,在按下B后切换到这个状态,当按下Enter后切换到计时状态或停止状态;
|
||||
\item 当倒计时到0的时候,应该切换到停止状态,并且还需要输出一条信息表示当前计时到0了,但是这里就要注意,从计时状态切换到停止状态时不应该输出这条信息,因此这里需要多加一个中间状态,用来表示倒计时结束的准备输出,这个状态在下一次时钟中断(可以类比数字逻辑电路中,同步时序电路,在时钟有效边沿到来时)转换到停止状态,并且输出这条信息;
|
||||
\item 关于检测频率,原先的计时器在每次时钟中断时执行ticks++,而时钟中断频率在clock.c文件中设置的是时钟频率除以100,也就是每0.01秒触发一次,尝试过改成除以1000,但会导致走时变慢,应该是因为触发中断太频繁了时间开销太大,也尝试过改成除以10,又会导致走时过快,看来还是原始的除以100最准时;
|
||||
\item 既然每0.01秒触发一次中断,那么如果是每TICK\_NUM次中断打印一次时间,直观感受就是精度为 TICK\_NUM * 0.01 秒,按照题目要求那么TICK\_NUM应该取10,当然为了提升精度也可以取更小,比如1;
|
||||
\item 当键入退格时字符为$\backslash$b,用于在输入数字时进行退格;当键入回车时字符为$\backslash$n,用于确定倒计时的时间;循环打印时间时需要使用$\backslash$r,表示将光标回到当前行的开头,这样再打印时间就会把之前的时间覆盖掉;
|
||||
\item 当然还存在一些bug,比如从10.000倒计时到9.990时就会无法覆盖最后一个0,导致看起来是9.9900;还有时间精度还是不准的问题;欢迎给出修复建议(可以通过Issue的方式,下面会给出网址)。
|
||||
}
|
||||
\myitem{代码}{
|
||||
\item \url{https://gitea.shuishan.net.cn/10213903403/os_kernel_lab}
|
||||
\item 也可以看上传的附件。
|
||||
}
|
||||
注:\mycircle{1} 要求实验报告以及代码以附件形式提交。
|
||||
\mycircle{2} 实验报告提交的截止期为4月1日。
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
156
操作系统/实验报告/Lab2.tex
Normal file
156
操作系统/实验报告/Lab2.tex
Normal file
@@ -0,0 +1,156 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mychapternum}{2}
|
||||
\renewcommand{\mylabname}{物理内存管理}
|
||||
\renewcommand{\mydate}{2024年4月16日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\item{\textbf{研读理解相关代码后,回答以下问题:}}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
uCore如何探测物理内存布局,其返回结果e820映射结构是什么样的?分别表示什么?
|
||||
}{}
|
||||
{\kaishu
|
||||
首先探测物理内存布局可以通过BIOS中断或直接探测,其中BIOS中断调用方法通常只能在实模式下完成,直接探测的方法必须在保护模式下完成,这里$\mu$Core 是在实模式下通过BIOS中断调用完成的。具体可以分为以下三步骤:
|
||||
\begin{enumerate}
|
||||
\item 设置一个存放内存映射地址描述符的物理地址(在此为0x8000);
|
||||
\item 将e820作为参数传递给INT 15h中断;
|
||||
\item 通过检测eflags的CF位来判断探测是否结束。如果CF位为0,则表示探测没有结束,那么就需要设置存放下一个内存映射地址描述符的物理地址,返回步骤2继续进行;否则物理内存检测就此结束。
|
||||
\end{enumerate}
|
||||
|
||||
其返回结果e820的结构为:
|
||||
\begin{minted}[fontsize=\zihao{-5}]{C}
|
||||
struct e820map {
|
||||
int nr_map;
|
||||
struct {
|
||||
uint64_t addr;
|
||||
uint64_t size;
|
||||
uint32_t type;
|
||||
} __attribute__((packed)) map[E820MAX];
|
||||
};
|
||||
\end{minted}
|
||||
其中\mintinline{C}{nr_map}表示内存映射地址描述符的数量,\mintinline{C}{addr}表示系统内存块基地址,\mintinline{C}{size}表示系统内存块大小,\mintinline{C}{type}表示内存类型。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
uCore中的物理内存空间管理采用什么样的方案?跟我们理论课中的哪个方案相似?有何不同之处?
|
||||
}{
|
||||
$\mu$Core 中的物理内存管理采用段页式系统,但是简化了分段机制,将逻辑地址直接恒等映射到线性地址,之后使用分页机制将线性地址通过分页映射到物理地址。由于使用了分页机制,因此需要对空闲物理块管理,对空闲物理块的管理使用了链表来管理,链表按照地址排序。
|
||||
|
||||
跟我们理论课中的“段页式存储管理”和“使用链表管理存储空间”的方案相似,不同之处在于分段机制简化了,并且只使用链表管理了空闲物理块(已分配的物理块在页表中有记录,所以不需要管理)。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
Page数据结构中每个字段含义与作用是什么?如何表示某物理块分配与否?字段property的作用是什么?如何表示 property是否有效?
|
||||
}{}
|
||||
{\kaishu
|
||||
\begin{minted}[fontsize=\zihao{-5}]{C}
|
||||
struct Page {
|
||||
int ref; // page frame's reference counter
|
||||
uint32_t flags; // array of flags that describe the status of the page frame
|
||||
unsigned int property; // the num of free block, used in first fit pm manager
|
||||
list_entry_t page_link; // free list link
|
||||
};
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{ref}字段表示此物理块被引用的个数,如果大于1代表这个物理块对应的可能是共享内存;\mintinline{C}{flags}的0位表示此物理块是否已被分配,0位为1代表已被分配;1位代表此描述符的\mintinline{C}{property}字段是否有效,1位为1代表有效。\mintinline{C}{property}只有在此物理块是空闲块时才有效,表示从此物理块开始连续的空闲物理块的数量。
|
||||
|
||||
用\mintinline{C}{flags}的0位表示此物理块是否已被分配,0位为1代表已被分配。
|
||||
|
||||
\mintinline{C}{property}的作用是表示从此物理块开始连续的空闲物理块的数量。
|
||||
|
||||
用\mintinline{C}{flags}的1位表示\mintinline{C}{property}是否有效,1位为1代表有效。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
uCore现有代码已实现的物理块分配首次适应算法以及物理块回收算法有没有问题或者错误?如有的话,请简述相关的问题或者错误,并修改相应的代码。
|
||||
}{
|
||||
\mintinline{C}{default_init_memmap}函数中的\mintinline{C}{p->flags = 0;},这里应该是把\mintinline{C}{flags}的0位设置成0,而不是把整个\mintinline{C}{flags}都设置成0。所以应该修改成\mintinline{C}{p->flags &= ~1;}。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
你认为在uCore的四个物理块基本分配方案基础上有没有进一步优化的可能?如有的话,请简要说明你的相关优化方案。
|
||||
}{
|
||||
四个物理块基本分配方案难道是FF(first fit,首次适应)、BF(best fit,最佳首次适应)、WF(worst fit,最坏首次适应)、NF(next fit,循环首次适应)?有进一步优化的可能,比如合并空闲的内存块:在系统空闲时可以检测所有已分配的物理块,将已分配但最近未使用的物理块合并到相邻的位置(最近使用的物理块不能乱动,不然会影响性能还可能出现问题)。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
\mintinline{C}{get_pte}、\mintinline{C}{get_page}函数的作用是什么?它们的输入与返回分别是什么?
|
||||
}{
|
||||
\mint{C}|pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create)|
|
||||
\mintinline{C}{get_pte}的作用是根据逻辑地址返回页表指针,它的输入为页目录的起始地址、逻辑地址、页表是否已被调入内存,返回为页表指针。
|
||||
\mint{C}|struct Page *get_page(pde_t *pgdir, uintptr_t la, pte_t **ptep_store)|
|
||||
\mintinline{C}{get_page}的作用是根据逻辑地址返回对应的物理块描述符,它的输入为页目录的起始地址、逻辑地址、以及可能需要存储的页表指针的地址,返回为物理块描述符。
|
||||
}
|
||||
\end{enumerate}
|
||||
\item \textbf{程序设计与实现的基本思路}
|
||||
\begin{enumerate}
|
||||
{\kaishu
|
||||
\item 关于物理内存管理的部分是在kern/mm/中(这里的mm应该是memory map 内存映射的意思?),循环首次适应算法是分配物理块的算法,所以观察到分配物理块的部分在\mintinline{C}{default_pmm.c}中实现,那么只需要关注这个文件的内容。
|
||||
\item 原先的代码中实现的是首次适应算法(first fit),这里要改成循环首次适应算法(next fit),只需要记录下每次分配的指针,每次从该指针的位置开始继续查找下一个空闲块。这里使用了\mintinline{C}{list_entry_t *alloc_le = &free_list;}作为全局变量来记录每次分配的指针。(为什么不用静态局部变量?是因为它的测试函数的问题,后面会提到)这里的\mintinline{C}{alloc_le}和\mintinline{C}{default_alloc_pages}中的\mintinline{C}{le}是同样的作用,只是全局变量名字不能和已有的一样所以改了个名。
|
||||
\item 这里新建的物理块分配函数使用\mintinline{C}{next_fit_alloc_pages}命名和原来的区分,在这个函数中首先需要初始化\mintinline{C}{alloc_le}指向链表头结点,但这个初始化又必须执行且只执行一次,所以这里使用了分支语句,
|
||||
\begin{minted}{C}
|
||||
if (alloc_le == &free_list)
|
||||
alloc_le = list_next(&free_list);
|
||||
\end{minted}
|
||||
|
||||
\item 接下来就开始从\mintinline{C}{default_alloc_pages}中的循环着手,原先的循环条件是\\ \mintinline{C}{while((le=list_next(le)) != &free_list)},表示每次查看下一个链表表项,直到到达末尾(由于是循环链表,所以末尾就是头结点即\mintinline{C}{&free_list})。那么这里就需要修改成循环到下一次遇到\mintinline{C}{alloc_le}。假设链表共有$n$项,那么最多需要执行$n+1$次循环,并且最后一次循环不满足条件退出。那能不能直接固定循环$n$次呢?这里是不行的,还是它的测试函数的问题,也在后面提到。因此首次循环和最后一次循环,循环变量指向的都是同一个位置,但是首次循环需要满足条件,最后一次循环需要不满足条件,这应该怎么实现呢?这就是下面要介绍的中途改变循环结束指针的方式。
|
||||
|
||||
\begin{minted}[fontsize=\zihao{-5}]{C}
|
||||
static struct Page *
|
||||
next_fit_alloc_pages(size_t n) {
|
||||
list_entry_t *start = alloc_le;
|
||||
list_entry_t *end = &free_list;
|
||||
while(1) {
|
||||
if (alloc_le == end) { // 最多两次触发此条件
|
||||
if (end != start) { // 第二次触发le == end的时候这里就不满足了
|
||||
end = start; // 如果循环到链表末尾了就把结束的指针改成循环开始的位置
|
||||
alloc_le = list_next(alloc_le); // 并且这个&free_list不应该被分配
|
||||
continue;
|
||||
}
|
||||
break; // 这时候就是真正的循环完了也没有剩余空间
|
||||
}
|
||||
...
|
||||
alloc_le = list_next(alloc_le);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
首先,用\mintinline{C}{start}记录循环指针开始的位置,用\mintinline{C}{end}记录头结点的位置,之后进入循环,在首次循环到末尾的时候(即\mintinline{C}{if (alloc_le == end)},把\mintinline{C}{end}修改到\mintinline{C}{start}的位置,之后第二次触发\mintinline{C}{if (alloc_le == end)}的时候,就可以退出循环了。也就是把循环遍历分成了两段,假设循环指针当前位置为$k$,共有$n$个链表节点,这就是分成了$k \cdots n$的前一段和$1 \cdots k$的后一段。
|
||||
|
||||
\item 上文多次提到原先的测试代码的问题,这里它的测试代码在\mintinline{C}{default_check}和\\ \mintinline{C}{basic_check}这两个函数中,这两个函数的目的是检测内存的分配与回收的方案是否正确,具体方法是多次分配回收,例如先分配3个物理块,之后把空闲物理块链表清空,这时候空闲物理块是多少?已经清空了所以是0个对不对?那么这时候要再分配一个物理块呢?应该是无法分配的是吧,但这里就出现问题了,循环首次适应的指针这时候在哪呢?还在之前分配了的位置,所以实际上这时候还是能分配到空闲物理块的,这就出现问题了,它的\mintinline{C}{assert(alloc_page() == NULL);}就不通过了。
|
||||
|
||||
分配了3个物理块后的情况如下图所示,红色矩形代表已经被分配的物理块,蓝色矩形代表未被分配的物理块,左侧的圆形代表链表头结点,prev和next表示头结点的前驱和后继节点。此时已经分配了3个物理块,所以\mintinline{C}{alloc_le}记录的位置在第三个物理块后。
|
||||
|
||||
\includexopp[1.2]{2.2.1}
|
||||
|
||||
但是之后出现了\mintinline{C}{list_init(&free_list);}这一行,这个函数的定义如下
|
||||
\begin{minted}[fontsize=\zihao{-5}]{C}
|
||||
/* *
|
||||
* list_init - initialize a new entry
|
||||
* @elm: new entry to be initialized
|
||||
* */
|
||||
static inline void
|
||||
list_init(list_entry_t *elm) {
|
||||
elm->prev = elm->next = elm;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
可见此函数只是把链表的前驱节点和后继节点都指向了自己,也就变成了下图:
|
||||
|
||||
\includexopp[1.2]{2.2.2}
|
||||
|
||||
这时候似乎没什么问题,但注意它的清空不彻底,再分配物理空间的时候,\mintinline{C}{alloc_le}还在原先的位置,所以仍然能分配成功,这就导致\mintinline{C}{assert(alloc_page() == NULL);}不通过了。所以解决方案也很简单,只需要把\mintinline{C}{alloc_le}也指向头结点就行了,如下图所示:
|
||||
|
||||
\includexopp[1.2]{2.2.3}
|
||||
|
||||
还要注意它在清空链表之后还有个恢复的操作,所以这个\mintinline{C}{alloc_le}也需要恢复,而且在\mintinline{C}{default_check}和\mintinline{C}{basic_check}中都有这样的操作,因此两个函数,每个函数一次保存一次恢复,所以需要添加四行\mintinline{C}{alloc_le = &free_list;}。
|
||||
|
||||
% 为什么楷体的仓会变成仑啊??????
|
||||
\item 时间仓促,如有错误敬请指出(在下方的链接中)。
|
||||
}
|
||||
\end{enumerate}
|
||||
\myitem{代码}{
|
||||
\item \url{https://gitea.shuishan.net.cn/10213903403/os_kernel_lab}
|
||||
\item 也可以看上传的附件。
|
||||
}
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
113
操作系统/实验报告/Lab3.tex
Normal file
113
操作系统/实验报告/Lab3.tex
Normal file
@@ -0,0 +1,113 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mychapternum}{3}
|
||||
\renewcommand{\mylabname}{虚拟内存管理}
|
||||
\renewcommand{\mydate}{2024年5月7日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\item{\textbf{研读理解相关代码后,回答以下问题:}}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
uCore 中的段页式存储管理方案与理论课中讲述的方案有何不同?
|
||||
}{
|
||||
$\mu$Core 中简化了分段机制,将逻辑地址直接恒等映射到线性地址,之后使用分页机制将线性地址通过分页映射到物理地址。由于使用了分页机制,因此需要对空闲物理块管理,对空闲物理块的管理使用了链表来管理,链表按照地址排序。
|
||||
|
||||
跟我们理论课中的“段页式存储管理”和“使用链表管理存储空间”的方案相似,不同之处在于分段机制简化了,并且只使用链表管理了空闲物理块(已分配的物理块在页表中有记录,所以不需要管理)。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
试简述uCore中缺页异常的处理过程,它与理论课中讲述的过程有何不同?
|
||||
}{
|
||||
在vmm.c的注释中有这样一行:
|
||||
\mint{C}| * CALL GRAPH: trap--> trap_dispatch-->pgfault_handler-->do_pgfault|
|
||||
显然,在$\mu$Core中,缺页异常从\mintinline{C}{trap}触发,之后通过\mintinline{C}{trap_dispatch}根据中断号(\mintinline{C}{tf->tf_trapno}分发给不同的处理程序,这里是缺页中断(\mintinline{C}{T_PGFLT}),所以分发给\mintinline{C}{pgfault_handler},再调用\mintinline{C}{do_pgfault},之后在\mintinline{C}{do_pgfault}中找到一个pte,并且分配内存或者从交换分区换入一个页面,或者由于访问权限不正确而直接返回失败。
|
||||
|
||||
与理论课中讲述的过程的不同是进行了简化,取消了快表,也就是只有一级索引而没有二级索引。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
为了支持页面置换,在数据结构Page中添加了哪些字段,其作用是什么?
|
||||
}{}
|
||||
{\kaishu
|
||||
\begin{minted}[fontsize=\zihao{-5}]{C}
|
||||
struct Page {
|
||||
int ref; // page frame's reference counter
|
||||
uint32_t flags; // array of flags that describe the status of the page frame
|
||||
unsigned int property; // the num of free block, used in first fit pm manager
|
||||
list_entry_t page_link; // free list link
|
||||
list_entry_t pra_page_link; // used for pra (page replace algorithm)
|
||||
uintptr_t pra_vaddr; // used for pra (page replace algorithm)
|
||||
};
|
||||
\end{minted}
|
||||
|
||||
添加了\mintinline{C}{pra_page_link}和\mintinline{C}{pra_vaddr}这两个字段,\mintinline{C}{pra_page_link}是用来将物理块组织成FIFO队列(使用链表实现)用来进行页面置换的,\mintinline{C}{pra_vaddr}是物理块对应的虚拟地址,用来记录访问哪个虚拟地址的时候产生的缺页,以便进行页面置换。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
请描述页目录项页(PDE)和页表目录项(PTE)的组成部分对uCore实现页面置换算法的潜在用处。请问如何区分未映射的页表表项与被换出页的页表表项?请描述被换出页的页表表项的组成结构。
|
||||
}{
|
||||
用处在于记录某个页面在内存中还是在外存对换区中,以及对应物理地址(在内存中)或者offset(在外存对换区中),在下次页面换入或换出时确保页面被放到正确的位置上。
|
||||
|
||||
由于未映射的页表表项全是0,所以被换出的页的offset从1开始以区分。这样如果页表表项全是0就说明是未映射,如果在offset(高24位)中存在1,那么就是被换出的。
|
||||
|
||||
被换出页的页表表项复用了pte的结构,在\mintinline{C}{memlayout.h}中有这样一行:
|
||||
\mint{C}|typedef pte_t swap_entry_t; //the pte can also be a swap entry|
|
||||
|
||||
再根据示意图:
|
||||
\begin{center}
|
||||
\includegraphics[width=0.5\linewidth]{imgs/2024-05-10-08-49-47.png}
|
||||
\end{center}
|
||||
|
||||
可以看到其高24位为offset,代表在外存对换区里的位置;存在位(最低位)为0,表示此页面不存在对应的物理块;中间的7位都保留暂不使用。
|
||||
}
|
||||
\end{enumerate}
|
||||
\item \textbf{程序设计与实现的基本思路}
|
||||
\begin{enumerate}
|
||||
{\kaishu
|
||||
\item 在$\mu$Core中原先实现的是FIFO页面置换算法,这里要改成第二次机会页面置换算法,可以发现页面的组织方式不需要更改,仍然使用链表,每次插入页面时仍然是添加到链表头部不需要更改,只需要更改换出页面的算法即可。
|
||||
|
||||
\begin{center}
|
||||
\includegraphics[width=0.9\linewidth]{imgs/2024-05-10-09-00-25.png}
|
||||
\end{center}
|
||||
|
||||
\item 课上讲的这张示意图,右侧是链表的头部,左侧是链表的尾部,每次插入的页面在最右侧即链表的头部。当需要换出页面时,从左侧(链表尾部)取出一个页面,如果这个页面的访问位是1,那么将其访问位改为0,并放到右侧(链表头部)。接着再从左侧(链表尾部)检测下一个页面,直到找到一个访问位为0页面的作为换出的页面。所以可以写出代码如下:
|
||||
|
||||
\begin{minted}{C}
|
||||
// 从后往前,最后面的是最早访问的
|
||||
list_entry_t *current = head->prev;
|
||||
list_entry_t *le = NULL;
|
||||
for (; current != head; current = current->prev) {
|
||||
pte_t *ptep = get_pte(mm->pgdir, le2vma(current, vm_start), 0);
|
||||
if (*ptep & PTE_A) { // 访问位为1
|
||||
*ptep &= !PTE_A; // 清除访问位
|
||||
list_del(current); // 从链表中移除
|
||||
list_add(head, current); // 添加到链表头部
|
||||
} else {
|
||||
le = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{le}即为最终的找到的用来换出的页面。
|
||||
|
||||
\item 当然,还需要完善一些问题。当页面只有一个并且访问位为1时,从链表中移除此页面再放入后链表结构并未改变,所以下一次的\mintinline{C}{current}指向链表头结点,循环退出了,但此时\mintinline{C}{le}还是空的!但期望的返回结果应该是这一个页面,所以需要对这种情况进行处理:
|
||||
|
||||
\begin{minted}{C}
|
||||
if (le == NULL) {
|
||||
le = head->prev; // 循环一遍找不到就最后一个
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
当循环一遍仍然找不到可以换出的页面的时候就选择最后一个页面(和FIFO一样)作为换出的页面。当然,其实使用第二次机会页面置换算法的话,页面大于一个的时候必定能找到一个换出的页面的(请自行验证),而且也不会出现页面为0个情况(总不能页表数量为0吧),所以其实这样就只是解决了页面只有一个时的异常。
|
||||
|
||||
\item 测试代码\mintinline{C}{_fifo_check_swap}仍然需要修改,因为使用第二次机会页面置换算法,缺页次数肯定与FIFO算法不尽相同。测试代码中的\mintinline{C}{pgfault_num}即为缺页次数。
|
||||
|
||||
\item 由于时间紧迫,一些细节可能没有考虑周到,如有发现错误欢迎在下方的链接中提Issue。
|
||||
}
|
||||
\end{enumerate}
|
||||
\myitem{代码}{
|
||||
\item \url{https://gitea.shuishan.net.cn/10213903403/os_kernel_lab}
|
||||
\item 也可以看上传的附件。
|
||||
}
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
295
操作系统/实验报告/Lab4.tex
Normal file
295
操作系统/实验报告/Lab4.tex
Normal file
@@ -0,0 +1,295 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mychapternum}{4}
|
||||
\renewcommand{\mylabname}{内核线程管理}
|
||||
\renewcommand{\mydate}{2024年5月17日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\item{\textbf{研读理解相关代码后,回答以下问题:}}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
请简单说明\mintinline{C}{proc_struct}中各个成员变量含义以及作用。它与理论课中讲述的PCB结构有何不同之处?
|
||||
}{}
|
||||
{\kaishu
|
||||
\setlength{\parskip}{2em}
|
||||
\begin{minted}[]{C}
|
||||
struct proc_struct {
|
||||
enum proc_state state; // Process state
|
||||
int pid; // Process ID
|
||||
int runs; // the running times of Proces
|
||||
uintptr_t kstack; // Process kernel stack
|
||||
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
|
||||
struct proc_struct *parent; // the parent process
|
||||
struct mm_struct *mm; // Process's memory management field
|
||||
struct context context; // Switch here to run process
|
||||
struct trapframe *tf; // Trap frame for current interrupt
|
||||
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
|
||||
uint32_t flags; // Process flag
|
||||
char name[PROC_NAME_LEN + 1]; // Process name
|
||||
list_entry_t list_link; // Process link list
|
||||
list_entry_t hash_link; // Process hash list
|
||||
};
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{state}是进程的状态,\mintinline{C}{pid}是进程的ID,\mintinline{C}{runs}是进程的被调度到的次数,\mintinline{C}{kstack}是进程的堆栈,\mintinline{C}{need_resched}用于非抢占式调度,表示进程是否运行结束可以把CPU重新调度给其他进程,\mintinline{C}{parent}是进程的父进程,\mintinline{C}{mm}是进程的内存管理的部分的指针,\mintinline{C}{context}是进程的上下文,即
|
||||
\begin{minted}{C}
|
||||
struct context {
|
||||
uint32_t eip;
|
||||
uint32_t esp;
|
||||
uint32_t ebx;
|
||||
uint32_t ecx;
|
||||
uint32_t edx;
|
||||
uint32_t esi;
|
||||
uint32_t edi;
|
||||
uint32_t ebp;
|
||||
};
|
||||
\end{minted}
|
||||
\mintinline{C}{tf}是进程的中断帧,记录了中断的相关信息,\mintinline{C}{cr3}是进程的页目录表的指针,\mintinline{C}{flags}是进程的各种标志位,\mintinline{C}{name}是进程的名称,\mintinline{C}{list_link}是所有进程的链表表项,\mintinline{C}{hash_link}是具有同一个哈希值的进程的链表表项。
|
||||
|
||||
它与理论课中讲述的PCB结构的不同之处在于没有优先级、进程组、进程运行时间、进程使用的CPU时间、进程的子进程的CPU时间、文件管理(因为还没实现文件系统?)。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
试简单分析 uCore中 内核线程的创建过程。(要求说明处理流程,相关的主要函数、它们的功能以及调用关系。)
|
||||
}{}
|
||||
{\kaishu
|
||||
\setlength{\parskip}{2em}
|
||||
\begin{center}
|
||||
\includegraphics[width=1\linewidth]{imgs/2024-05-17-19-18-00.png}
|
||||
\end{center}
|
||||
\begin{minted}{C}
|
||||
// proc_init - set up the first kernel thread idleproc "idle" by itself and
|
||||
// - create the second kernel thread init_main
|
||||
void
|
||||
proc_init(void) {
|
||||
int i;
|
||||
|
||||
list_init(&proc_list);
|
||||
for (i = 0; i < HASH_LIST_SIZE; i ++) {
|
||||
list_init(hash_list + i);
|
||||
}
|
||||
|
||||
if ((idleproc = alloc_proc()) == NULL) {
|
||||
panic("cannot alloc idleproc.\n");
|
||||
}
|
||||
|
||||
idleproc->pid = 0;
|
||||
idleproc->state = PROC_RUNNABLE;
|
||||
idleproc->kstack = (uintptr_t)bootstack;
|
||||
idleproc->need_resched = 1;
|
||||
set_proc_name(idleproc, "idle");
|
||||
nr_process ++;
|
||||
|
||||
current = idleproc;
|
||||
|
||||
int pid = kernel_thread(init_main, "Hello world!!", 0);
|
||||
if (pid <= 0) {
|
||||
panic("create init_main failed.\n");
|
||||
}
|
||||
|
||||
initproc = find_proc(pid);
|
||||
set_proc_name(initproc, "init");
|
||||
|
||||
assert(idleproc != NULL && idleproc->pid == 0);
|
||||
assert(initproc != NULL && initproc->pid == 1);
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
主要的内核线程创建过程在\mintinline{C}{proc_init}中,首先调用\mintinline{C}{list_init}初始化全部进程的链表和相同哈希的进程列表,然后调用 \mintinline{C}{alloc_proc}给当前进程分配一个进程控制块,并且设置相关属性,其中调用\mintinline{C}{set_proc_name}设置当前进程的名称为\mintinline{C}{"idle"},之后调用\mintinline{C}{kernel_thread}创建一个\mintinline{C}{init_main}进程,并调用\mintinline{C}{find_proc},再调用\mintinline{C}{set_proc_name}将其名称设置为\mintinline{C}{init}。
|
||||
|
||||
\begin{minted}{C}
|
||||
// kernel_thread - create a kernel thread using "fn" function
|
||||
// NOTE: the contents of temp trapframe tf will be copied to
|
||||
// proc->tf in do_fork-->copy_thread function
|
||||
int
|
||||
kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
|
||||
struct trapframe tf;
|
||||
memset(&tf, 0, sizeof(struct trapframe));
|
||||
tf.tf_cs = KERNEL_CS;
|
||||
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
|
||||
tf.tf_regs.reg_ebx = (uint32_t)fn;
|
||||
tf.tf_regs.reg_edx = (uint32_t)arg;
|
||||
tf.tf_eip = (uint32_t)kernel_thread_entry;
|
||||
return do_fork(clone_flags | CLONE_VM, 0, &tf);
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{kernel_thread}的功能是通过调用一个函数创建一个新的线程,由于新的线程是由当前线程创建的,这里就调用了\mintinline{C}{do_fork}函数。
|
||||
|
||||
\begin{minted}{C}
|
||||
/* do_fork - parent process for a new child process
|
||||
* @clone_flags: used to guide how to clone the child process
|
||||
* @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
|
||||
* @tf: the trapframe info, which will be copied to child process's proc->tf
|
||||
*/
|
||||
int
|
||||
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
|
||||
int ret = -E_NO_FREE_PROC;
|
||||
struct proc_struct *proc;
|
||||
if (nr_process >= MAX_PROCESS) {
|
||||
goto fork_out;
|
||||
}
|
||||
ret = -E_NO_MEM;
|
||||
//LAB4:EXERCISE2 YOUR CODE
|
||||
/*
|
||||
* Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
|
||||
* MACROs or Functions:
|
||||
* alloc_proc: create a proc struct and init fields (lab4:exercise1)
|
||||
* setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
|
||||
* copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags
|
||||
* if clone_flags & CLONE_VM, then "share" ; else "duplicate"
|
||||
* copy_thread: setup the trapframe on the process's kernel stack top and
|
||||
* setup the kernel entry point and stack of process
|
||||
* hash_proc: add proc into proc hash_list
|
||||
* get_pid: alloc a unique pid for process
|
||||
* wakeup_proc: set proc->state = PROC_RUNNABLE
|
||||
* VARIABLES:
|
||||
* proc_list: the process set's list
|
||||
* nr_process: the number of process set
|
||||
*/
|
||||
|
||||
// 1. call alloc_proc to allocate a proc_struct
|
||||
// 2. call setup_kstack to allocate a kernel stack for child process
|
||||
// 3. call copy_mm to dup OR share mm according clone_flag
|
||||
// 4. call copy_thread to setup tf & context in proc_struct
|
||||
// 5. insert proc_struct into hash_list && proc_list
|
||||
// 6. call wakeup_proc to make the new child process RUNNABLE
|
||||
// 7. set ret vaule using child proc's pid
|
||||
if ((proc = alloc_proc()) == NULL) {
|
||||
goto fork_out;
|
||||
}
|
||||
|
||||
proc->parent = current;
|
||||
|
||||
if (setup_kstack(proc) != 0) {
|
||||
goto bad_fork_cleanup_proc;
|
||||
}
|
||||
if (copy_mm(clone_flags, proc) != 0) {
|
||||
goto bad_fork_cleanup_kstack;
|
||||
}
|
||||
copy_thread(proc, stack, tf);
|
||||
|
||||
bool intr_flag;
|
||||
local_intr_save(intr_flag);
|
||||
{
|
||||
proc->pid = get_pid();
|
||||
hash_proc(proc);
|
||||
list_add(&proc_list, &(proc->list_link));
|
||||
nr_process ++;
|
||||
}
|
||||
local_intr_restore(intr_flag);
|
||||
|
||||
wakeup_proc(proc);
|
||||
|
||||
ret = proc->pid;
|
||||
fork_out:
|
||||
return ret;
|
||||
|
||||
bad_fork_cleanup_kstack:
|
||||
put_kstack(proc);
|
||||
bad_fork_cleanup_proc:
|
||||
kfree(proc);
|
||||
goto fork_out;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{do_fork}函数中,先调用\mintinline{C}{alloc_proc}分配一个进程控制块,再调用\mintinline{C}{setup_kstack}分配一个内核堆栈,再调用\mintinline{C}{copy_mm}将父进程的内存空间复制或者共享给子进程,再调用\mintinline{C}{copy_thread}设置子进程的中断处理,并设置上下文的变量。之后关中断,将新的进程控制块插入到全部进程链表与相同哈希链表中,再开中断。然后把子进程设置成状态为\mintinline{C}{RUNNABLE},返回子进程的\mintinline{C}{pid}。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
试简单分析 uCore中 内核线程的切换过程。(要求说明处理流程,相关的主要函数、它们的功能以及调用关系。)
|
||||
}{}
|
||||
{\kaishu
|
||||
\setlength{\parskip}{2em}
|
||||
切换线程主要使用的是\mintinline{C}{schedule}函数。
|
||||
|
||||
\begin{minted}{C}
|
||||
void
|
||||
schedule(void) {
|
||||
bool intr_flag;
|
||||
list_entry_t *le, *last;
|
||||
struct proc_struct *next = NULL;
|
||||
local_intr_save(intr_flag);
|
||||
{
|
||||
current->need_resched = 0;
|
||||
last = (current == idleproc) ? &proc_list : &(current->list_link);
|
||||
le = last;
|
||||
do {
|
||||
if ((le = list_next(le)) != &proc_list) {
|
||||
next = le2proc(le, list_link);
|
||||
if (next->state == PROC_RUNNABLE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (le != last);
|
||||
if (next == NULL || next->state != PROC_RUNNABLE) {
|
||||
next = idleproc;
|
||||
}
|
||||
next->runs ++;
|
||||
if (next != current) {
|
||||
proc_run(next);
|
||||
}
|
||||
}
|
||||
local_intr_restore(intr_flag);
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
在\mintinline{C}{schedule}中,我们要找到下一次调度的线程,即找到状态为\mintinline{C}{RUNNABLE}的线程,如果当前的线程为\mintinline{C}{idleproc}即CPU空闲的线程则从线进程链表头开始找(因为\mintinline{C}{idleproc}线程不在线程链表中),否则从当前线程的下一个线程开始找。找到需要调度的\mintinline{C}{RUNNABLE}线程后,如果新的线程和老的线程不是同一个线程,则需要调用\mintinline{C}{proc_run}进行切换。
|
||||
|
||||
\begin{minted}{C}
|
||||
// proc_run - make process "proc" running on cpu
|
||||
// NOTE: before call switch_to, should load base addr of "proc"'s new PDT
|
||||
void
|
||||
proc_run(struct proc_struct *proc) {
|
||||
if (proc != current) {
|
||||
bool intr_flag;
|
||||
struct proc_struct *prev = current, *next = proc;
|
||||
local_intr_save(intr_flag);
|
||||
{
|
||||
current = proc;
|
||||
load_esp0(next->kstack + KSTACKSIZE);
|
||||
lcr3(next->cr3);
|
||||
switch_to(&(prev->context), &(next->context));
|
||||
}
|
||||
local_intr_restore(intr_flag);
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
在\mintinline{C}{proc_run}中,调用\mintinline{C}{load_esp0}设置任务状态段,调用\mintinline{C}{lcr3}加载新的进程的页表,之后调用\mintinline{C}{switch_to}保存老的进程的上下文,恢复新的进程的上下文。
|
||||
|
||||
\begin{minted}{asm}
|
||||
switch_to: # switch_to(from, to)
|
||||
|
||||
# save from's registers
|
||||
movl 4(%esp), %eax # eax points to from
|
||||
popl 0(%eax) # save eip !popl
|
||||
movl %esp, 4(%eax)
|
||||
movl %ebx, 8(%eax)
|
||||
movl %ecx, 12(%eax)
|
||||
movl %edx, 16(%eax)
|
||||
movl %esi, 20(%eax)
|
||||
movl %edi, 24(%eax)
|
||||
movl %ebp, 28(%eax)
|
||||
|
||||
# restore to's registers
|
||||
movl 4(%esp), %eax # not 8(%esp): popped return address already
|
||||
# eax now points to to
|
||||
movl 28(%eax), %ebp
|
||||
movl 24(%eax), %edi
|
||||
movl 20(%eax), %esi
|
||||
movl 16(%eax), %edx
|
||||
movl 12(%eax), %ecx
|
||||
movl 8(%eax), %ebx
|
||||
movl 4(%eax), %esp
|
||||
|
||||
pushl 0(%eax) # push eip
|
||||
|
||||
ret
|
||||
\end{minted}
|
||||
|
||||
在\mintinline{C}{switch_to}中,从\mintinline{C}{esp+4}的位置加载\mintinline{C}{eax}指针(即\mintinline{C}{&(prev->context)}),并把老的线程中当前的寄存器中的值都放到这个指针所在的\mintinline{C}{context}结构体中,再从\mintinline{C}{esp+8}的位置加载\mintinline{C}{eax}指针(即\mintinline{C}{&(next->context)}),并从这个指针的位置加载新的线程中的上下文寄存器的值,其中\mintinline{C}{eip}需要用\mintinline{C}{popl}和\mintinline{C}{pushl}来设置,不能使用\mintinline{C}{movl}来设置。
|
||||
}
|
||||
\end{enumerate}
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
130
操作系统/实验报告/Lab5.tex
Normal file
130
操作系统/实验报告/Lab5.tex
Normal file
@@ -0,0 +1,130 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mychapternum}{5}
|
||||
\renewcommand{\mylabname}{用户进程管理}
|
||||
\renewcommand{\mydate}{2024年5月26日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\item{\textbf{研读理解相关代码后,回答以下问题:}}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
用户进程与内核线程有何不同?为何要引入用户进程?
|
||||
}{
|
||||
用户进程所在的段的特权级是3,而内核进程所在的段的特权级是0。
|
||||
|
||||
引入用户进程是为了确保安全。一些代码和数据只能在内核态访问而不能在用户态访问,例如操作系统进行页面管理和进程管理的代码。如果用户的代码出错,错误地修改了操作系统的代码段或数据段,会造成严重的错误。而引入用户进程后,用户进程在特权级3下运行,无法修改内核进程所在段,只要操作系统不崩溃,用户进程出现错误仍不影响操作系统正常运行。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
uCore是如何实现系统调用的?这样的设计有何优点?
|
||||
}{
|
||||
\begin{center}
|
||||
\includegraphics[width=0.5\linewidth]{imgs/Interrupt_Procedure_Call.jpg}
|
||||
\end{center}
|
||||
|
||||
在用户态(特权级3)通过 \mintinline{asm}{int 0x80} 指令,根据中断描述符表找到中断处理程序的段选择子以及偏移量,根据段选择子在段描述符表中找到对应的段描述符,再把段描述符的地址加上偏移量即可找到最终需要执行的代码,并且以特权级0执行。其中参数(即中断号)和返回值的传递通过 \mintinline{asm}{eax} 寄存器实现。
|
||||
|
||||
这样设计的优点是在用户进程处于用户态的情况下能以内核态执行系统调用,也就是说操作系统可以在切换到内核态之前先进行检查用户是否有权限执行此系统调用,如果没有权限可以提前拦截,避免用户进程错误地或恶意地切换到内核态。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
分析 uCore中 用户进程的创建、调度以及执行的过程。( 要求说明处理流程,相关的主要函数、它们的功能以及调用关系。)
|
||||
}{}
|
||||
{\kaishu\setlength{\parskip}{2em}
|
||||
在 \mintinline{C}{init_main} 中加入了一行 \mintinline{C}{int pid = kernel_thread(user_main, NULL, 0);} , \mintinline{C}{user_main} 的定义如下:
|
||||
\begin{minted}{C}
|
||||
// user_main - kernel thread used to exec a user program
|
||||
static int
|
||||
user_main(void *arg) {
|
||||
#ifdef TEST
|
||||
KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE);
|
||||
#else
|
||||
KERNEL_EXECVE(exit);
|
||||
#endif
|
||||
panic("user_main execve failed.\n");
|
||||
}
|
||||
\end{minted}
|
||||
可以看到这个进程是用来创建用户进程的,其中调用了 \mintinline{C}{kernel_execve} :
|
||||
\begin{minted}{C}
|
||||
// kernel_execve - do SYS_exec syscall to exec a user program called by user_main kernel_thread
|
||||
static int
|
||||
kernel_execve(const char *name, unsigned char *binary, size_t size) {
|
||||
int ret, len = strlen(name);
|
||||
asm volatile (
|
||||
"int %1;"
|
||||
: "=a" (ret)
|
||||
: "i" (T_SYSCALL), "0" (SYS_exec), "d" (name), "c" (len), "b" (binary), "D" (size)
|
||||
: "memory");
|
||||
return ret;
|
||||
}
|
||||
\end{minted}
|
||||
其中又调用了系统调用子功能 \mintinline{C}{SYS_exec} ,它对应的函数为
|
||||
\begin{minted}{C}
|
||||
static int
|
||||
sys_exec(uint32_t arg[]) {
|
||||
const char *name = (const char *)arg[0];
|
||||
size_t len = (size_t)arg[1];
|
||||
unsigned char *binary = (unsigned char *)arg[2];
|
||||
size_t size = (size_t)arg[3];
|
||||
return do_execve(name, len, binary, size);
|
||||
}
|
||||
\end{minted}
|
||||
其中又调用了 \mintinline{C}{do_execve}
|
||||
\begin{minted}{C}
|
||||
// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
|
||||
// - call load_icode to setup new memory space accroding binary prog.
|
||||
int
|
||||
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
|
||||
struct mm_struct *mm = current->mm;
|
||||
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
|
||||
return -E_INVAL;
|
||||
}
|
||||
if (len > PROC_NAME_LEN) {
|
||||
len = PROC_NAME_LEN;
|
||||
}
|
||||
|
||||
char local_name[PROC_NAME_LEN + 1];
|
||||
memset(local_name, 0, sizeof(local_name));
|
||||
memcpy(local_name, name, len);
|
||||
|
||||
if (mm != NULL) {
|
||||
lcr3(boot_cr3);
|
||||
if (mm_count_dec(mm) == 0) {
|
||||
exit_mmap(mm);
|
||||
put_pgdir(mm);
|
||||
mm_destroy(mm);
|
||||
}
|
||||
current->mm = NULL;
|
||||
}
|
||||
int ret;
|
||||
if ((ret = load_icode(binary, size)) != 0) {
|
||||
goto execve_exit;
|
||||
}
|
||||
set_proc_name(current, local_name);
|
||||
return 0;
|
||||
|
||||
execve_exit:
|
||||
do_exit(ret);
|
||||
panic("already exit: %e.\n", ret);
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
这里的 \mintinline{C}{do_execve} 就释放了当前内核进程的内存空间,并且调用了 \mintinline{C}{load_icode}
|
||||
\begin{minted}{C}
|
||||
/* load_icode - load the content of binary program(ELF format) as the new content of current process
|
||||
* @binary: the memory addr of the content of binary program
|
||||
* @size: the size of the content of binary program
|
||||
*/
|
||||
static int
|
||||
load_icode(unsigned char *binary, size_t size) {
|
||||
...
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
这里 \mintinline{C}{load_icode} 就会创建一个新的页目录,把内核空间中共用的部分复制过去,之后从程序文件(ELF格式)中读取程序头,根据各个段的地址在内存中分配空间,并相应设置权限,然后从程序文件中的内容复制到内存中,之后将当前进程的页目录设置为新创建的这个页目录。
|
||||
|
||||
最后设置当前中断帧的代码段,由于这里的代码段的特权级为3,所以(通过 \mintinline{asm}{eax} )返回之后继续执行代码就是以用户的特权级执行了。
|
||||
}
|
||||
\end{enumerate}
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
298
操作系统/实验报告/Lab67.tex
Normal file
298
操作系统/实验报告/Lab67.tex
Normal file
@@ -0,0 +1,298 @@
|
||||
\documentclass[a4paper]{ctexart}
|
||||
\input{mypreamble}
|
||||
\renewcommand{\mychapternum}{6、7}
|
||||
\renewcommand{\mylabname}{调度器、同步互斥}
|
||||
\renewcommand{\mydate}{2024年6月10日}
|
||||
|
||||
\begin{document}
|
||||
\mytitle
|
||||
\begin{enumerate}
|
||||
\item \textbf{完成相关实验内容后,回答以下问题:}
|
||||
|
||||
\textbf{Lab6:}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
分析sched\_class中各个函数指针的用法,并结合Round Robin 调度算法描述ucore的调度执行过程。
|
||||
}{}
|
||||
{\kaishu\setlength{\parskip}{2em}
|
||||
先看代码:
|
||||
\begin{minted}{C}
|
||||
// The introduction of scheduling classes is borrrowed from Linux, and makes the
|
||||
// core scheduler quite extensible. These classes (the scheduler modules) encapsulate
|
||||
// the scheduling policies.
|
||||
struct sched_class {
|
||||
// the name of sched_class
|
||||
const char *name;
|
||||
// Init the run queue
|
||||
void (*init)(struct run_queue *rq);
|
||||
// put the proc into runqueue, and this function must be called with rq_lock
|
||||
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);
|
||||
// get the proc out runqueue, and this function must be called with rq_lock
|
||||
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc);
|
||||
// choose the next runnable task
|
||||
struct proc_struct *(*pick_next)(struct run_queue *rq);
|
||||
// dealer of the time-tick
|
||||
void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc);
|
||||
/* for SMP support in the future
|
||||
* load_balance
|
||||
* void (*load_balance)(struct rq* rq);
|
||||
* get some proc from this rq, used in load_balance,
|
||||
* return value is the num of gotten proc
|
||||
* int (*get_proc)(struct rq* rq, struct proc* procs_moved[]);
|
||||
*/
|
||||
};
|
||||
\end{minted}
|
||||
|
||||
\mintinline{C}{name} 即为调度类的名称, \mintinline{C}{init} 用来初始化运行队列(数据结构不一定是链表,可以是任何数据结构), \mintinline{C}{enqueue} 是(创建新的进程的时候)进程入队的函数; \mintinline{C}{dequeue} 是(进程结束的时候)进程出队的函数; \mintinline{C}{pick_next} 用来在就绪(RUNNABLE)状态的进程中选出下一个将要调度的进程; \mintinline{C}{proc_tick} 是在时钟中断时需要执行的函数。
|
||||
|
||||
\mintinline{C}{labcodes_answer/lab6_result} 中实现的是stride调度算法,而Round Robin调度算法可以在 \mintinline{C}{labcodes/lab6} 中找到:
|
||||
\begin{minted}{C}
|
||||
struct sched_class default_sched_class = {
|
||||
.name = "RR_scheduler",
|
||||
.init = RR_init,
|
||||
.enqueue = RR_enqueue,
|
||||
.dequeue = RR_dequeue,
|
||||
.pick_next = RR_pick_next,
|
||||
.proc_tick = RR_proc_tick,
|
||||
};
|
||||
\end{minted}
|
||||
|
||||
对于Round Robin(时间片轮转),$\mu$Core 的调度执行过程如下:
|
||||
调用 \mintinline{C}{RR_init} 初始化一个队列。之后每次触发时钟中断时,进入 \mintinline{C}{trap_dispatch} ,根据 \mintinline{C}{labcodes_answer/lab7_result/kern/trap/trap.c} :
|
||||
\begin{minted}{C}
|
||||
/* LAB6 YOUR CODE */
|
||||
/* IMPORTANT FUNCTIONS:
|
||||
* run_timer_list
|
||||
*----------------------
|
||||
* you should update your lab5 code (just add ONE or TWO lines of code):
|
||||
* Every tick, you should update the system time, iterate the timers, and trigger the timers which are end to call scheduler.
|
||||
* You can use one funcitons to finish all these things.
|
||||
*/
|
||||
\end{minted}
|
||||
|
||||
应该更新系统计时器,并且调用调度器的中断处理函数 \mintinline{C}{RR_proc_tick} :
|
||||
\begin{minted}{C}
|
||||
static void
|
||||
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
|
||||
if (proc->time_slice > 0) {
|
||||
proc->time_slice --;
|
||||
}
|
||||
if (proc->time_slice == 0) {
|
||||
proc->need_resched = 1;
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
在中断处理函数中更新进程还剩余的时间片( \mintinline{C}{proc->time_slice} ,如果时间片用完了就把进程标记为需要重新调度。执行完 \mintinline{C}{trap_dispatch} 之后,在 \mintinline{C}{trap} 函数中有这样几行:
|
||||
\begin{minted}{C}
|
||||
if (current->need_resched) {
|
||||
schedule();
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
这就是在当前进程需要重新调度时再次执行 \mintinline{C}{schedule} ,再看 \mintinline{C}{schedule} 的代码:
|
||||
\begin{minted}{C}
|
||||
void
|
||||
schedule(void) {
|
||||
bool intr_flag;
|
||||
struct proc_struct *next;
|
||||
local_intr_save(intr_flag);
|
||||
{
|
||||
current->need_resched = 0;
|
||||
if (current->state == PROC_RUNNABLE) {
|
||||
sched_class_enqueue(current);
|
||||
}
|
||||
if ((next = sched_class_pick_next()) != NULL) {
|
||||
sched_class_dequeue(next);
|
||||
}
|
||||
if (next == NULL) {
|
||||
next = idleproc;
|
||||
}
|
||||
next->runs ++;
|
||||
if (next != current) {
|
||||
proc_run(next);
|
||||
}
|
||||
}
|
||||
local_intr_restore(intr_flag);
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
这里当前进程是 \mintinline{C}{PROC_RUNNABLE} 状态,所以会把当前进程放入队列(末尾),之后把下一个进程(从队首)取出,如果下一个进程不是idle进程,就执行下一个进程。下一个进程继续执行直到时间片用完(或提前结束或进入等待状态),这样就实现了时间片轮转。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
如何在uCore OS中设计实现“多级反馈队列调度算法”?请给出概要设计,鼓励给出详细设计。
|
||||
}{
|
||||
\begin{itemize}
|
||||
\item 初始化函数:初始化所有的多级队列,每个队列有不同的时间片大小;
|
||||
\item 入队函数:新的进程应该进入最高优先级的队列;
|
||||
\item 出队函数:进程在哪个队列就从哪个队列里出队;
|
||||
\item 选出下一个调度的进程:按照优先级从高到低遍历每个队列,找到一个不为空的队列后,选出队首的进程;
|
||||
\item 时钟中断时执行:如果当前进程的时间片用完,这时候就需要把这个进程从当前队列中取出,放到更低的优先级队列中;如果已经是最低优先级队列,就按照时间片轮转的方式,从队首取出放到队尾;
|
||||
\item 时钟中断时,如果比当前进程更高的优先级队列中存在进程,那么将当前进程的状态改为 \mintinline{C}{PROC_RUNNABLE} 并调用 \mintinline{C}{schedule} 进行重新调度,这是为了实现高优先级对低优先级的抢占式调度;
|
||||
\item IO中断时,将当前进程标记为等待IO;等到调用 \mintinline{C}{wakeup_proc} 唤醒进程时,如果此进程被标记了等待IO,那么此时将进程放到最高优先级队列,此时该进程会被优先调度,这是为了确保IO密集型的进程优先得到调度。
|
||||
\end{itemize}
|
||||
}
|
||||
\end{enumerate}
|
||||
\textbf{Lab7:}
|
||||
\begin{enumerate}
|
||||
\questionandanswer[]{
|
||||
uCore OS与理论课的信号量机制的实现方案有何不同?请给出 理论课中的信号量机制的实现方案 的概要设计。
|
||||
}{}
|
||||
{\kaishu\setlength{\parskip}{2em}
|
||||
理论课中的信号量机制的实现方案直接简单地关闭中断,修改信号量的值,再开中断。但是在$\mu$Core中在进入临界区之后要进行进程的等待和唤醒。也就是在 \mintinline{C}{__down} 函数中,关闭中断后,访问 \mintinline{C}{sem->value} ,如果此信号量的值大于0那么正常修改后开中断。否则,说明进程需要等待,这时重新开启中断后会将进程放入等待队列,并且触发一次进程调度。
|
||||
\begin{minted}{C}
|
||||
bool intr_flag;
|
||||
local_intr_save(intr_flag);
|
||||
if (sem->value > 0) {
|
||||
sem->value --;
|
||||
local_intr_restore(intr_flag);
|
||||
return 0;
|
||||
}
|
||||
wait_t __wait, *wait = &__wait;
|
||||
wait_current_set(&(sem->wait_queue), wait, wait_state);
|
||||
local_intr_restore(intr_flag);
|
||||
|
||||
schedule();
|
||||
\end{minted}
|
||||
|
||||
当该进程被唤醒,并且被再次调度到时,就将其移出等待队列:
|
||||
\begin{minted}{C}
|
||||
local_intr_save(intr_flag);
|
||||
wait_current_del(&(sem->wait_queue), wait);
|
||||
local_intr_restore(intr_flag);
|
||||
|
||||
if (wait->wakeup_flags != wait_state) {
|
||||
return wait->wakeup_flags;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
在 \mintinline{C}{__up} 中,关闭中断后,如果当前的等待队列中有进程,那么需要唤醒队首的一个进程,并且将信号量的值加一,之后再开中断。
|
||||
\begin{minted}{C}
|
||||
if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
|
||||
sem->value ++;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
理论课中的信号量机制的实现方案只需要在 \mintinline{C}{up} 里关中断,修改信号量的值,开中断,在 \mintinline{C}{down} 里关中断,修改信号量的值,开中断。
|
||||
}
|
||||
\questionandanswer[]{
|
||||
用户级信号量与内核级信号量有何不同?请给出在现有的内核级信号量基础上实现用户级信号量的概要设计。
|
||||
}{}
|
||||
{\kaishu\setlength{\parskip}{2em}
|
||||
用户级信号量的创建、访问、修改都是在用户态完成的,而内核级信号量的创建、访问、修改都是在内核态完成的。
|
||||
|
||||
在现有的内核级信号量基础上实现用户级信号量,最简单的方式是使用内核级信号量对一个用户空间中的变量当作临界资源访问,比如可以在用户空间中这样实现PV操作(以下P用acquire表示,V用release表示)
|
||||
|
||||
\begin{minted}{C}
|
||||
#define SLEEP_TIME 10 // 检测间隔
|
||||
|
||||
typedef struct {
|
||||
int value; // 用户级信号量的值
|
||||
semaphore_t sem; // 内核级信号量
|
||||
} user_semaphore;
|
||||
|
||||
void acquire(*user_semaphore) {
|
||||
while (1) { // 不断检测用户级信号量的值
|
||||
down(user_semaphore->sem); // 进入临界区
|
||||
if (user_semaphore->value > 0) {
|
||||
user_semaphore->value--; // 修改用户级信号量
|
||||
up(user_semaphore->); // 退出临界区
|
||||
return;
|
||||
}
|
||||
up(user_semaphore->sem); // 退出临界区
|
||||
do_sleep(SLEEP_TIME); // 用户进程将自己挂起
|
||||
}
|
||||
}
|
||||
|
||||
void release(*user_semaphore) {
|
||||
down(user_semaphore->sem); // 进入临界区
|
||||
user_semaphore->value++; // 修改用户级信号量
|
||||
up(user_semaphore->sem); // 退出临界区
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
这里不需要init函数,因为创建变量的时候可以直接给value赋初值。
|
||||
|
||||
可以作个类比,由于不能一直处于关中断的状态(万一因为故障没有开中断),所以用关中断实现内核级信号量用来实现内核线程的同步互斥;由于不能一直处于内核态(权限过高不安全),所以用内核级信号量实现用户级信号量用来实现用户进程或线程的同步互斥。
|
||||
|
||||
当然也可以使用系统调用来实现用户级信号量,这里就不详述了。
|
||||
}
|
||||
\end{enumerate}
|
||||
\item \textbf{程序设计与实现的基本思路}
|
||||
|
||||
{\kaishu
|
||||
虽然是任选一题完成,但是写完一个后顺手就把另一个写了。
|
||||
|
||||
\textbf{Lab6:}
|
||||
\begin{enumerate}
|
||||
\item 彩票进程调度算法,还是比较容易的,每个进程设置了一个新的属性: \mintinline{C}{uint32_t lab6_ticket_start;}
|
||||
用来表示进程的第一个彩票的位置,并且使用原先就有的 \mintinline{C}{uint32_t lab6_priority;} 来表示进程的彩票个数(优先级越高彩票越多)。也就是 \mintinline{C}{lab6_ticket_start} 到 \mintinline{C}{lab6_ticket_start + lab6_priority} 这一部分都是进程拥有的彩票。
|
||||
\item 但是后来发现这样的话在进程退出时很麻烦,彩票中间空了一段的话就很难随机抽取彩票了,所以不用 \mintinline{C}{uint32_t lab6_ticket_start;} 了,而是在选择下一个进程时把队列中的进程彩票数量(优先级)加起来和随机到的彩票比较;
|
||||
\item 在运行队列中加了一个 \mintinline{C}{uint32_t lab6_total_num;} 属性,用来记录目前所有的进程总共有多少彩票;
|
||||
\item init函数没什么改的,enqueue函数里需要每次把 \mintinline{C}{lab6_total_num} 加上当前进程的优先级,dequeue函数也是每次把 \mintinline{C}{lab6_total_num} 减去当前进程的优先级。这里要注意优先级可能为0,但是如果一个进程有0张彩票的话那这个进程永远不会被调度到了,所以这里优先级加了1。但在 \mintinline{C}{lab6_set_priority} 这个函数中已经有类似的操作了:
|
||||
\begin{minted}{C}
|
||||
//FOR LAB6, set the process's priority (bigger value will get more CPU time)
|
||||
void
|
||||
lab6_set_priority(uint32_t priority)
|
||||
{
|
||||
if (priority == 0)
|
||||
current->lab6_priority = 1;
|
||||
else current->lab6_priority = priority;
|
||||
}
|
||||
\end{minted}
|
||||
|
||||
不过如果创建进程时没有调用 \mintinline{C}{lab6_set_priority} 那么默认的优先级好像就是0,所以自己计算的时候加一还是有必要的。
|
||||
\item proc\_tick函数也不需要改,彩票调度也是每过一个时间片发一次彩票;
|
||||
\item 最重要的就是 \mintinline{C}{ticket_pick_next} 函数了,每次调用时先随机抽取一张彩票 \mintinline{C}{target_index} :
|
||||
\mint{C}|uint32_t target_index = rand() % rq->lab6_total_num;|
|
||||
然后遍历队列,每次把 \mintinline{C}{temp_ticket_num} 加上当前进程的优先级(加一),如果加上之后达到了 \mintinline{C}{target_index} ,那么也就说明这个彩票在这个进程拥有的彩票范围之间,那么就选择这个进程:
|
||||
\begin{minted}{C}
|
||||
struct proc_struct *p;
|
||||
int32_t temp_ticket_sum = 0;
|
||||
while (le != &rq->run_list)
|
||||
{
|
||||
p = le2proc(le, run_link);
|
||||
temp_ticket_sum += p->lab6_priority + 1;
|
||||
if (temp_ticket_sum >= target_index)
|
||||
break;
|
||||
le = list_next(le);
|
||||
}
|
||||
\end{minted}
|
||||
\item 之后就是要检验这个调度是否有问题了,只需要在 \mintinline{C}{proc.c} 中创建多个优先级不同的进程即可,会自动完成调度。(这里优先级为了看起来方便就使用了 \mintinline{C}{(rand() % 100) * 100} ,也可以设置其他的优先级)
|
||||
\begin{minted}{C}
|
||||
static int
|
||||
my_test_user_main(void *arg) {
|
||||
int num = (int)arg;
|
||||
int priority = (rand() % 100) * 100;
|
||||
cprintf("process %d, priority %d\n", num, priority);
|
||||
lab6_set_priority(priority);
|
||||
cprintf("process %d end\n", num);
|
||||
}
|
||||
\end{minted}
|
||||
\begin{minted}{C}
|
||||
int i = 0;
|
||||
for (; i < 10; i++) {
|
||||
int pid = kernel_thread(my_test_user_main, (void *)i, 0);
|
||||
struct proc_strucht *proc = find_proc(pid);
|
||||
}
|
||||
\end{minted}
|
||||
\end{enumerate}
|
||||
\textbf{Lab7:}
|
||||
\begin{enumerate}
|
||||
\item 读者写者问题的同步关系,更简单了,课件中已经给了代码,只需要稍微改改就能用:
|
||||
\begin{center}
|
||||
\includegraphics[width=0.8\linewidth]{imgs/2024-06-10-22-08-03.png}
|
||||
\end{center}
|
||||
|
||||
\item 原先的代码在 \mintinline{C}{lab7_result/kern/sync/check_sync.c} 实现了哲学家就餐问题,这里就新建了一个 \mintinline{C}{read_write_sync.c} 实现读者写者问题,并在 \mintinline{C}{proc.c} 中将原先的 \mintinline{C}{check_sync} 改为 \mintinline{C}{read_write_sync} 。
|
||||
\item 这里的读者写者问题只有一个缓冲区,信号量定义了两个, \mintinline{C}{semaphore_t mutex} 用来互斥,与课件中的 \mintinline{C}{mutex} 一样, \mintinline{C}{semaphore_t synchronization} 用来同步,相当于课件中的 \mintinline{C}{wrt} , \mintinline{C}{read_pos_write_neg} 相当于课件中的 \mintinline{C}{readcount} 。
|
||||
\item 但是对于信号量的初始化的操作要在主进程里做而不能在定义时赋初值,这里的主进程就是 \mintinline{C}{read_write_sync} ,在主进程中随机创建读者进程和写者进程(直到达到目标数量),并随机延迟,这样就能体现出读者和写者在不同情况下的互斥与同步是否正确;
|
||||
\item 读者和写者的读操作和写操作也用 \mintinline{C}{do_sleep} 来模拟延迟,并且在每个读者或写者完成所有操作后打印读取的结果或写入的结果便于观察。
|
||||
\end{enumerate}
|
||||
}
|
||||
\myitem{代码}{
|
||||
\item \url{https://gitea.shuishan.net.cn/10213903403/os_kernel_lab}
|
||||
\item 也可以看上传的附件。
|
||||
}
|
||||
\end{enumerate}
|
||||
\end{document}
|
||||
115
操作系统/实验报告/mypreamble.tex
Normal file
115
操作系统/实验报告/mypreamble.tex
Normal file
@@ -0,0 +1,115 @@
|
||||
\usepackage[margin=1in]{geometry}
|
||||
\usepackage{longtable}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{zhnumber} % change section number to chinese
|
||||
% \usepackage{enumerate}
|
||||
\usepackage{enumitem}
|
||||
\usepackage{titlesec}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{environ} % 加了这个再\def\myitem就不报错了
|
||||
% \usepackage[outputdir=./latex-output]{minted}
|
||||
\usepackage{float} % https://blog.csdn.net/qq_32623363/article/details/101095168
|
||||
\usepackage{fp}
|
||||
\usepackage{graphicx} % 原来includegraphics要使用参数要用graphicx,只是用graphics是没法带参数的
|
||||
\usepackage{tabularx}
|
||||
\usepackage{array}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{multirow}
|
||||
\usepackage{url}
|
||||
\usepackage{color}
|
||||
\usepackage{mylatex}
|
||||
\usepackage{totpages} % 不加这个会导致总页数出错
|
||||
\pagestyle{fancyplain}
|
||||
\fancyhead{}
|
||||
\fancyhead[C]{华东师范大学计算机科学与技术学院上机实践报告}
|
||||
\fancyfoot[C]{第 \thepage 页\quad 共 \ref{TotPages} 页}
|
||||
\renewcommand\thesection{\zhnum{section}}
|
||||
\renewcommand \thesubsection {\arabic{subsection}}
|
||||
\setlist[1]{label=\zhnum{enumi}、}
|
||||
\setlist[2]{listparindent=2em, labelindent=-1em, leftmargin=*, label=\arabic{enumii}、\ }
|
||||
\setlist[3]{listparindent=2em, leftmargin=*, label=(\arabic{enumiii})\ }
|
||||
\definecolor{shadecolor}{RGB}{204,232,207}
|
||||
\definecolor{bg}{rgb}{0.95,0.95,0.95}
|
||||
\setminted{breaklines=true, frame=single} % , bgcolor=bg
|
||||
\setmintedinline{bgcolor={}}
|
||||
\setitemize[1]{label=\textbullet}
|
||||
|
||||
\newcommand{\mydate}{
|
||||
2023年11月3日
|
||||
}
|
||||
|
||||
\newcommand{\mychapternum}{
|
||||
1
|
||||
}
|
||||
|
||||
\newcommand{\mylabname}{
|
||||
实验名称
|
||||
}
|
||||
|
||||
\newcommand{\myname}{
|
||||
姓名
|
||||
}
|
||||
|
||||
\newcommand{\mystudentnum}{
|
||||
e.g. 12345678902
|
||||
}
|
||||
|
||||
\input{myprivatepreamble}
|
||||
|
||||
\newcommand{\mytitle}{
|
||||
\title{\fontsize{15}{0}华东师范大学计算机科学与技术学院上机实践报告\vspace{-2em}}
|
||||
\date{}
|
||||
\maketitle
|
||||
|
||||
\begin{center}
|
||||
\begin{tabularx}{\textwidth}{XXl}
|
||||
\toprule
|
||||
\textbf{课程名称}:操作系统 & \textbf{年级}:2022级 & \textbf{上机实践日期}:\mydate \\
|
||||
\textbf{指导教师}:李东 & \textbf{姓名}:\myname & \textbf{学号}:\mystudentnum \\
|
||||
\multicolumn{3}{l}{\textbf{实验名称}:实验\zhnumber{\mychapternum}\quad\mylabname} \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
\end{center}
|
||||
\addtocounter{table}{-1}
|
||||
\vspace{1em}
|
||||
}
|
||||
|
||||
\newcommand{\myitemx}[3][]{
|
||||
\item \textbf{#2}
|
||||
\begin{enumerate}[#1]
|
||||
#3
|
||||
\end{enumerate}
|
||||
}
|
||||
|
||||
% https://blog.csdn.net/u010801696/article/details/79477226
|
||||
\def\UrlBreaks{\do\A\do\B\do\C\do\D\do\E\do\F\do\G\do\H\do\I\do\J
|
||||
\do\K\do\L\do\M\do\N\do\O\do\P\do\Q\do\R\do\S\do\T\do\U\do\V
|
||||
\do\W\do\X\do\Y\do\Z\do\[\do\\\do\]\do\^\do\_\do\`\do\a\do\b
|
||||
\do\c\do\d\do\e\do\f\do\g\do\h\do\i\do\j\do\k\do\l\do\m\do\n
|
||||
\do\o\do\p\do\q\do\r\do\s\do\t\do\u\do\v\do\w\do\x\do\y\do\z
|
||||
\do\.\do\@\do\\\do\/\do\!\do\_\do\|\do\;\do\>\do\]\do\)\do\,
|
||||
\do\?\do\'\do+\do\=\do\#}
|
||||
|
||||
\renewcommand{\thefigure}{\mychapternum-\arabic{figure}}
|
||||
\renewcommand{\thetable}{\mychapternum-\arabic{table}}
|
||||
|
||||
|
||||
% https://mirrors.pku.edu.cn/ctan/info/svg-inkscape/InkscapePDFLaTeX.pdf
|
||||
% 这个只有PDFLaTeX才能用,filemod也是
|
||||
% \newcommand{\executeiffilenewer}[3]{%
|
||||
% \ifnum\pdfstrcmp{\pdffilemoddate{#1}}%
|
||||
% {\pdffilemoddate{#2}}>0%
|
||||
% {\immediate\write18{#3}}\fi%
|
||||
% }
|
||||
|
||||
|
||||
% 该命令用于控制 p{} 的情况
|
||||
\newcolumntype{P}[1]{>{\RaggedRight\hspace{0pt}}p{#1}} % 使用过程中,将p{4cm}换成P{4cm},小写改成大写即可!
|
||||
% 该命令用于控制 X 的情况
|
||||
\newcolumntype{Z}{>{\centering\let\newline\\\arraybackslash\hspace{0pt}}X} % 使用过程中,将Z 换成 X,即可!
|
||||
|
||||
% 可利用 RaggedLeft Centering替换RaggedRight,实现靠右和居中 [代码对大小写敏感!!!!!!!!!!!!!!!!!!!!!!!!!!!!]
|
||||
|
||||
% 原文链接:https://blog.csdn.net/wanjiac/article/details/107494424
|
||||
|
||||
\setminted{fontsize=\zihao{6}, baselinestretch=1}
|
||||
Reference in New Issue
Block a user