哈工大操作系统实验总结

实验地址, https://www.lanqiao.cn/courses/115/learning/?id=374, 在现做实验, 好处是环境提前都配好了, 不足之处是敲代码有网络延迟, 环境无法保存.总的来说比较方便.

教科书, <<Linux内核完全注释 修正版 V5.0 >> (赵炯), 后面简称<<注释>>, <<汇编语言>>(王爽)

 

实验1 熟悉实验环境

这个实验主要是介绍实验环境的基本情况, 宿主机使用ubuntu, 通过boch模拟器来模拟Linux0.11的运行.

主要用到了下面的命令:

cd /home/shiyanlou/oslab/  #cd 是change directory 表示更换文件夹, 后面跟的是更改的文件地址, 如果不是以斜杠 '/'开头的话默认是当前文件夹, 和'./'表示相同的意思, '/'开头表示根目录

tar -zxvf hit-oslab.tar.gz -C /home/shiyanlou  #tar是打包命令, 在"鸟哥的linux私房菜"里有详细的介绍, 这里的-z表示用zip格式压缩或则解压, -x表示extract就是解压, -v表示显示压缩或者解压的文件细节, -f后面跟处理的tar文件(无论是压缩还是解压), -C后面跟的是指定的解压地址, C为大写

make all  #这里的make是一个编译命令, 当前所在文件夹必须有Makefile文件, 关于Makefile文件在<<linux内核完全注释(第五版)>>里3.6节有详细的介绍, 这个知识个人认为非常有必要掌握

sudo ./mount-hdc  # 挂载hdc硬盘命令, 必须使用sudo (super user do)来完成, 否则权限不够; 并且当前目录下的hdc文件夹的主要文件映射是在 /hdc/usr/root/ -> /root; ./run进入boch模拟之后的默认其实目录是/home/usr/root, 所以为了交换文件方便, 尽量放在/hdc/usr/root里

sudo umount hdc 跟上一个命令对应, 在./run运行boch前要umount

 

实验2 操作系统的引导

这个实验主要是介绍操作系统从电源上电到启动的整个过程, 里面涉及到了 .s 文件(也就是汇编文件)的修改, 所以要对汇编有一定的了解, 了解这个可以使用王爽的<<汇编语言>>(主要介绍实模式下的一些汇编操作), 和<<注释>>里的第三章, 讲了AT&T语法和8086实模式汇编语法的区别, 以及C语言内嵌汇编的格式, 不弄懂这些的话后面很多汇编语言都会看不懂

下面是一些实验过程自己的一些问题和想法

#6.2完成bootsect.s的屏幕输出功能
#.org 510    这一句的意思是下面的代码从第510个字节开始算, 后面跟了一个.word 0xAA55, 正好印证了引导扇区是512个字节
#.word 0xAA55    这一句是约定的引导扇区的地址, 没有别的意思

dd bs=1 if=bootsect of=Image skip=32     #这里的dd是linux的备份命令, 查了一下是disk destroyer的缩写, 在<<鸟哥>>里第九章有介绍, bs表示block size 一个块的大小, if 是 input file, of是output file, skip 32 表示跳过前32个字节, 这句命令在linux0.11目录下的Makefile里也有, 所以手工编译链接的时候需要你进行这一步处理

jnc     # 汇编命令, jump no carry, 没有进位时跳转
jmpi    # 段间跳转命令, 所以后面必须有两个操作数, 一个表示段, 一个表示段内偏移
lds si, memory    # load ds, 从memory中获取32位的信息到 ds:si中


 

这个实验重点需要弄明白操作系统的引导整个过程, 在<<注释>>一书中第六章有详细介绍, 个人感觉核心就是这张图

实验3 系统调用

这个实验介绍了如何从用户态进入到内核态并且调用内核代码的过程, 以及如何修改和添加内核代码

操作系统实现系统调用的五段论, 结合一个例子表示如下:

1.应用程序调用库函数(API);
2.API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
3.内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
4.系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
5.中断处理函数返回到 API 中;
API 将 EAX 返回给应用程序

# 以系统调用 close.c 为例
第一步, 应用程序调用 int close(int fd)     # 这里是用户态, 用户调用close()函数是系统调用的API

第二步, API将系统调用号NR_close存到eax里作为系统中断int 0x80的第一个参数, 根据参数调用_NR_table里对应的系统调用        # 这里是通过_syscall1(int, close, int , fd)这个宏定义将close变成__NR_close, __NR_close在文件sys.h中定义了其序号以及相应的处理函数, 这个时候刚进入内核状态, 后面进行内核栈的切换操作, 切换操作就是让 ds, es 指向核心地址空间 0x10, 用户地址空间是0x17

第三步, 查sys_call_table, 调用对应的内核函数sys_close        #  这时候已经进入内核态, 并进行内核栈的切换
第四步, sys_close在内核态处理, 返回的结果在eax中
第五步, 内核态切换为用户态, 此时返回值是用户态的返回值

这个实验在网上看到很多都有点小bug, 在iam.c里赋值之前要把buffer的值全部设置为0, 否则会留下上一次设置的痕迹

 

实验4 进程运行轨迹的跟踪与统计

这个实验开始进入到操作系统的核心部分, 多进程编程,

实验涉及的文件和个人理解如下:

process.c     # 这是个样本程序, 实现了一个cpuio_bound函数, 用来模拟一个进程是cpu耗时为主还是IO耗时为主, 从而来观察得出一个进程的不同状态的持续时间

/var/process.log      # 通过在main.c的init()之前, 将文件描述符为3的输出关联到process.log, 因此后面用到fprintk就可以将输出指定在process.log里

(void) open("/var/process.log", O_CREAT | O_TRUNC | O_WRONLY, 0666)      # 这里的 O_CREAT是打开文件的flag设置, 定义在/usr/include/asm-generic/fcntl.h  O_CREAT 表示文件不存在就创建, O_TRUNC 表示文件存在就截为0(即清空), O_WRONLY 表示只写write only

/kernel/printk.c         # 这里面的门道有点多, 一个一个来

va_list args         # 这个是用来专门对应 const char* fmt 里的输出格式 , 如 "%d %s", ... 
struct file* file    # 这个struct file* 是在后面的文件系统里会学习到, 是一个文件类型的指针
struct m_inode* inode    # 这个inode是磁盘里的一个struct m_inode数据结构指针, 是一个文件节点, 里面存有当前文件对应的数据块block索引以及下一级目录的索引

count = vsprintf(logbuf, fmt, args)        # 这里是将fmt, args的结果输入到定义的内核缓存logbuf中, vsprintf返回输入的字节数, 这里的count表示输入了多少个字节到logbuf里

addl $8, %%esp\n\t        # 这里是栈的回滚, 前面pushl %logbuf 和 pushl %1正好占据了8个字节, 将esp加8就相当于抹去这两个数据, 后面的addl $12 也是相同的道理

/6.4jiffies 滴答
set_intr_gate(0x20, &timer_interrupt)        # 这里就是吧timer_interrupt这个时钟中断设置到IDT的0x20位置

/6.5寻找状态切换点
struct task_struct *p        # 这个struct task_struct 就是老师课件里提到的PCB(进程控制块 process control block), 里面有一个进程的各种信息, 栈, 寄存器, 上下文等, 定义在/include/linux/sched.h中

*p = current        # current是一个全局变量, 表示当前的进程控制块PCB  

实验中容易出错的地方有

1. 进程0会不断调用sys_pause, 如果不对这个做处理的话python文件会报错, 重复行, 也就是某一个进程的状态跟上一次一样

2. switch_to(next) 也要对next任务和当前任务current的pid进行比较, 如果是同一个任务就不需要做更改

 

实验5 基于内核栈切换的进程切换

这个实验难度比较大, 主要在于代码量较大, 而且得对内核栈有一定的了解, 稍有不慎就会出错, 

这个实验做完要记住GDT表的整体结构

一些代码的注释如下:

/6.3 schedule 与 switch_to
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)    #    (*p)->counter > c表示p的时间片比当前最大的时间片大, 所以当前p的优先级最高, 下一个调度的任务就是*p, 
    c = (*p)->counter, next = i, pnext = *p;        # next 是下一个任务的编号, 所有的任务是定义的一个NR_TASKS

//.......

switch_to(pnext, LDT(next));
/6.4 实现switch_to
pushl %ebp
movl %esp, %ebp        # 这两句是栈帧操作, %ebp保存当前栈的栈帧位置, 因为栈是往下扩展的, 一个栈的栈帧是地址的最高值

movl 8(%ebp), %ebx        # %ebp是当前栈帧的地址, %ebp+4是保存的上一个栈帧的地址, %ebp+8就是switch_to的第一个参数, 回想一下上一节switch_to(pnext, LDT(next)), 因此这里是把pnext的值赋给了%ebx, 也就是要切换的下一个任务的指针

je 1f        # 这里的1是一个标号, 对应的下面的1:    , f表示forward,向前跳转到1的位置

movl tss, %ecx
addl $4096, %ebx    # %ebx表示下一个任务的指针, 增加4096相当于开辟了4096个字节的空间, 也就是一页的大小, 可以作为内核PCB的空间
movl %ebx, ESP0(%ecx)    # 这里就把%ebx也就是任务的指针存到了任务0的内核栈指针那里, 所有的内核任务都共用这一个地址

movl %esp,KERNEL_STACK(%eax)    #    这个%eax其实也是pnext指针, KERNEL_STACK(%eax)就是下一个任务的内核栈地址

还需要了解的是任务状态段TSS(task state segment)的结构

以及PCB(task_struct)的结构, 在源代码里可以看到, 或者看另一个博主的博文, 讲的也挺详细

https://blog.csdn.net/qq_29503203/article/details/54618275

 

实验6 信号量的实现和应用

这个实验大概是对考研和找工作中帮助比较大的一个实验, 主要是通过信号量来讲述进程间通信和锁的原理

首先需要我们设计一个sem_t的数据结构, 根据视频里讲的, 一个信号量结构要有两个基本成员, 一个是信号量的名称(char* name), 一个是信号量里的值(int value), 进程间的通信就通过这个信号量来进行阻塞和唤醒;

还有一个是任务的队列, 这里可以在信号量sem_t里加入一个 sem_t_queue的指针, 也可以直接利用实验指导里的隐式队列;

其次要理解生产者消费者这个程序的PV顺序,  在互斥信号量MUTEX里(临界区), 要尽量使用少的别的信号量资源, 使用的越多越容易造成死锁,  实验的思考题里就是这个问题的一种情况 

/6.1 信号量
Producer()
{
    // 生产一个产品 item;

    // 空闲缓存资源
    P(Empty);

    // 互斥信号量
    P(Mutex);

    // 将item放到空闲缓存中;
    V(Mutex);

    // 产品资源
    V(Full);
}

Consumer()
{
    P(Full);
    P(Mutex);

    //从缓存区取出一个赋值给item;
    V(Mutex);

    // 消费产品item;
    V(Empty);
}
/6.2 多进程共享文件

使用标准 C 的文件操作函数要注意,它们使用的是进程空间内的文件缓冲区,父进程和子进程之间不共享这个缓冲区        # 这句话其实隐含的说明了进程的内存空间是独立的, 父进程和子进程之间不会共享, 所以需要写完之后马上fflush一下, 手动把要写入的东西立即写入磁盘

建议直接使用系统调用进行文件操作    # 这句话说明系统调用可以使用统一的内核空间, 这样相当于直接使用了共享内存

/6.4 原子操作, 睡眠和唤醒

这里用到的原子操作是开关中断(sti(), cli()), 缺点是只能在单核上有效, 因为开关中断是CPU单核上的一个引脚的置1或者0
课堂上还讲了利用硬件的原子操作test&set(sem), 也就是P,V操作

string.h里要用到strcmp来进行信号量名字(sem_t ->name)的比较

 

实验7 地址映射与共享

这个实验的第一部分是直观地展示了一个逻辑地址如何通过层层映射到物理内存上的

逻辑地址上的ds是一个段选择子(segment selector), 是在LDT上面的偏移, 要找LDT就得找GDT, 所以过程是  查GDTR(寄存器)-->找到GDT的地址->查LDTR(寄存器)对应的位数-->找到LDT在GDT地址的偏移-->找到GDT偏移处的64位数据, 取出包含LDT物理地址的32位信息也就是真正的段基址-->根据段基址+段偏移3004就能得到逻辑地址ds:3004的物理地址;

段选择子ds的值其实也是有讲究的, 都说ds=0x17表示用户态, 主要就是0x17的二进制是00010111, 后两位表示特权级, 这里是11表示3, 正好是用户态特权级3, 第2位表示TI, TI为1表示去LDT查, 那不就正好是用户态去LDT查数据, 而内核态去GDT查数据了嘛; 因此0x10表示内核态就说得通了, 0x10的二进制是00010000,后两位表示特权级为0, 内核态的特权级, TI位为0, 表示在GDT上查数据, 印证了内核态的数据在GDT上查

通过线性地址查物理地址那就简单了, 利用页表知识, 线性地址的32位分为页目录+页表项+页内偏移, 页目录项和页表项的大小都是一个32位的数, 所以要根据页目录基址+页目录号*4来找到对应的页表, 页表基址+页表号*4得到页号, 最后+页内偏移就得到了物理地址

第二个实验的核心部分其实是找到空闲的物理页面, 把物理页面存放在内核的一个共享空间table里, 让两个进程根据shmid来获取同一块共享内核内存, 从而达到共享内存的目的, 其余部分跟实验6的是相似的, 这里面学习到了&这个字符的使用方法, 以及进程空间64M的内存是如何分布的

 

实验8 终端设备的控制

这个实验主要是学习如何处理键盘输入和显示输出的过程, 为了让按下F12就能实现所有字符为'*', 就得找到这个输出'*'的出口在哪里, 根据视频里老师的介绍, 要在console.c这个文件里修改

还要找到F12对应的输入码是多少, 在输入缓冲区中判断一下出现了F12的输入码(ESC, [, [, L好像是),  一旦出现, 就令console_write函数里写到console的字符为"*"即可, 这种方法确保了只有按下F12才能改变成'*'

参考了这位博主的做法https://blog.csdn.net/weixin_45666853/article/details/105278345

 

实验9 PROC文件系统的实现

这个实验主要是学习文件系统中block, inode的概念, 通过挂载相应的inode实现PROC这个虚拟文件, 

主要要知道mkdir() 和mknod()这两个函数的用法来创造节点, 然后要知道task_struct的结构从而获取每个pid对应的state, start_time, count等信息

hdinfo的获取方法目前还是有点不大明白, 整体上看是通过遍历统计所有使用过的block和inode, 然后用总的block和inode去减

其余步骤没有特别难理解的地方, 按照实验指导一步一步来就能得到结果

 

总结

9个实验花了差不多1个月的时间, 主要前半个月花在了看王爽的<<汇编语言>>上了, 对实模式的汇编有了一定的了解, 但其实这个系列的实验对汇编的要求在<<注释>>一书中都能找到比较全面的解析, 所以其实可以更快完成, 完成过程中也参考了不少别人的做法, 刚接触这个玩意儿的时候还是一头雾水, 不知道从哪里开始下码, 做到后面几个实验的时候就逐渐有了点感觉, 可以独立写一大段代码, 唯一头疼的地方还是debug, 用GDB来找BUG实在是很困难.

 

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值