极客时间已完结课程限时免费阅读

参考答案 | 对答案,是再次学习的一个机会

参考答案 | 对答案,是再次学习的一个机会-极客时间

参考答案 | 对答案,是再次学习的一个机会

讲述:宇新

时长01:00大小931.52K

你好,我是编辑宇新。
春节将至,先给你拜个早年:愿你 2022 年工期变长,需求变少,技术水平更加硬核。
距离我们专栏更新结束已经过去了不少时间,给坚持学习的你点个赞。学习操作系统是一个长期投资,需要持之以恒,才能见效。无论你是二刷、三刷的朋友,还是刚买课的新同学,都建议你充分利用留言区,给自己的学习加个增益 buff。这种学习讨论的氛围,也会激励你持续学习。
今天这期加餐,我们整理了课程里的思考题答案,一次性发布出来,供你对照参考,查漏补缺。
建议你一定要先自己学习理解,动脑思考、动手训练,有余力还可以看看其他小伙伴的解题思路,之后再来对答案。

第 1 节课

Q:为了实现 C 语言中函数的调用和返回功能,CPU 实现了函数调用和返回指令,即上图汇编代码中的“call”,“ret”指令,请你思考一下:call 和 ret 指令在逻辑上执行的操作是怎样的呢?
A:一般函数调用的情况下 call 和 ret 指令在逻辑上执行的操作如下:
1. 将 call 指令的下一条指令的地址压入栈中;
2. 将 call 指令数据中的地址送入 IP 寄存器中(指令指针寄存器),该地址就是被调用函数的地址;
3. 由于 IP 寄存器地址设置成为被调用函数的地址,CPU 自然跳转到被调用函数处开始执行指令;
4. 在被调用函数的最后都有一条 ret 指令,当 CPU 执行到 ret 指令时,就从栈中弹出一个数据到 IP 寄存器,而这个数据通常是先前执行 call 指令的下一条指令的地址,即实现了函数返回功能。

第 2 节课

Q:以上 printf 函数定义,其中有个形式参数很奇怪,请你思考下:为什么是“…”形式参数,这个形式参数有什么作用?

A:在 C 语言中经常使用 printf(“%s :%d”,“number is :”,20);printf(“%x :%d”,0x10,20);printf(“%x,%x :%d”,0xba,0xff,20); 可以看出,这些 printf 函数参数个数都不同,因为 C 语言的特性支持变参函数。而“…”表示支持 0 个和多个参数,C 语言是通过调用者传递参数的,刚好支持这种变参函数。

第 3 节课

Q:其实我们的内核架构不是我们首创的,它是属于微内核、宏内核之外的第三种架构,请问这是什么架构?
A:我们的内核架构是混合内核架构,是介于微、宏架构之间的一种架构,这种架构保证了宏架构的高性能又兼顾了微架构的可移植、可扩展性。

第 4 节课

Q:Windows NT 内核属于哪种架构类型?
A:Windows NT 内核架构其实既不属于传统的宏内核架构,也不是新的微内核架构,说 NT 是微内核架构是错误的,NT 这种内核架构其实是宏内核的变种——混合内核。

第 5 节课

Q:请问实模式下能寻址多大的内存空间?
A:由于实模式下访问内存的地址是这样产生的:16 位段寄存器左移 4 位,加一个 16 位通用寄存器,最后形成了 20 位地址,所以只能访问 1MB 大的内存空间。

第 6 节课

Q:分页模式下,操作系统是如何对应用程序的地址空间进行隔离的?
A:操作系统会给每个应用程序都配置独立的一套页表数据。应用程序运行时,就让 CR3 寄存器指向该应用程序的页表数据。运行下一个应用程序时,则会执行同样的操作。

第 7 节课

Q:请你思考一下,如何写出让 CPU 跑得更快的代码?由于 Cache 比内存快几个数量级,所以这个问题也可以转换成:如何写出提高 Cache 命中率的代码?
A:第一,定义变量时,尽量让其地址与 Cache 行大小对齐。
int a __attribute__((aligned (64)));
int b __attribute__((aligned (64)));
第二,操作数据时的顺序,尽量和数据在内存中布局顺序保持一致。
int arr[M][M];
for(int i = 0; i < M; i++) {
for(int k = 0; k < M; k++) {
arr[i][k] = 0;
}
}
//而非这样
for(int i = 0; i < M; i++) {
for(int k = 0; k < M; k++) {
arr[k][i] = 0;
}
}
第三,尽量少用全局变量。

第 8 节课

Q:请用代码展示一下自旋锁或者信号量,可能的使用形式是什么样的?
A:最常规的形式是在设计共享数据结构时,在其中包含自旋锁或者信号量。
如下所示:
typedef struct s_DATA
{
spinlock_t d_lock;
sem_t d_sem;
int a;
int b;
long state;
}data_t;
data_t da;
do_da_write(data_t* d)
{
x86_spin_lock(&d->d_lock);
d->a = 0;
d->b = 1;
d->state = 2;
x86_spin_unlock(&d->d_lock);
}
do_da_sem_write(data_t* d)
{
krlsem_down(&d->d_sem);
d->a = 20;
d->b = 10;
d->state = 4;
krlsem_up(&d->d_sem);
}
do_da_write(&da);
do_da_sem_write(&da);

第 9 节课

Q:请试着回答:上述 Linux 的读写锁,支持多少个进程并发读取共享数据?这样的读写锁有什么不足?
A:第一个问题,根据上述描述,读写锁本质上就是一个计数器。锁变量的初始值为 0x01000000,即表示最多可以有 0x01000000 个进程同时获取读锁。
第二个问题,读写锁的不足是,如果一直有很多读取数据的进程占有读锁,因而可能导致修改数据的进程饥饿的情况。操作系统会加以控制,让修改数据的进程优先得锁。

第 10 节课

Q:请问,我们为什么要把虚拟硬盘格式化成 ext4 文件系统格式呢?
A:有两点原因。第一,GRUB 在加载系统映像文件时,能够识别 ext4 文件系统格式;二,我们在 Linux 下生成系统映像文件时,要复制到虚拟硬盘中去,所以这个文件系统格式必须被 Linux 所识别,那么选 ext4 就最合适。

第 11 节课

Q:请问 GRUB 头中为什么需要 _entry 标号和 _start 标号的地址?
A:这是 GRUB 规定的。GRUB 正是通过 _entry 标号和 _start 标号的地址,控制内核文件被加载到什么内存地址,又应该从什么内存地址开始运行。

第 12 节课

Q:请你想一下,init_bstartparm() 函数中的 init_mem820() 函数,这个函数到底干了什么?
A:init_mem820() 函数是把 e820map_t 结构数组复制到内核文件之后的内存空间,并且重新填写了机器信息结构。
它的代码如下:
void init_meme820(machbstart_t *mbsp)
{
//源e820map_t结构数组地址
e820map_t *semp = (e820map_t *)((u32_t)(mbsp->mb_e820padr));
//e820map_t结构数组元素个数
u64_t senr = mbsp->mb_e820nr;
//获取下一段空闲内存空间的首地址,即e820map_t结构数组的新地址
e820map_t *demp = (e820map_t *)((u32_t)(mbsp->mb_nextwtpadr));
//检查地址空间冲突
if (1 > move_krlimg(mbsp, (u64_t)((u32_t)demp), (senr * (sizeof(e820map_t)))))
{
kerror("move_krlimg err");
}
//复制
m2mcopy(semp, demp, (sint_t)(senr * (sizeof(e820map_t))));
//并重新填写了对应的机器信息结构字段
mbsp->mb_e820padr = (u64_t)((u32_t)(demp));
mbsp->mb_e820sz = senr * (sizeof(e820map_t));
mbsp->mb_nextwtpadr = P4K_ALIGN((u32_t)(demp) + (u32_t)(senr * (sizeof(e820map_t))));
mbsp->mb_kalldendpadr = mbsp->mb_e820padr + mbsp->mb_e820sz;
return;
}

第 13 节课

Q:请你画出 Cosmos 硬件抽象层的函数调用关系图。
A:Cosmos 硬件抽象层的函数调用关系图如下。

第 14 节课

Q:为什么要用 C 代码 mkpiggy 程序生成 piggy.S 文件,并包含 vmlinux.bin.gz 文件呢?
A:因为 mkpiggy 程序在读取 vmlinux.bin.gz 文件,知道了其长度等信息,它就会把这些信息保存在 piggy.S 文件相关的字段中。在解压 vmlinux.bin.gz 文件时,解压的代码需要用到这些信息。

第 15 节课

Q:你能指出上文中 Linux 初始化流程里,主要函数都被链接到哪些对应的二进制文件中了?
A:它们的链接结构如下。
1._start、main 函数链接在 setup.elf 文件中,而 setup.elf 文件生成了 setup.bin。
2.startup_32、startup_64、extract_kernel 链接在 linux/arch/x86/boot/compressed 目录下的 vmlinux 文件中,而这个文件生成了 vmlinux.bin。
3.Linux 内核的 startup_64、x86_64_start_kernel、start_kernel、arch_call_rest_init、rest_init、kernel_init、try_to_run_init_process、run_init_process 函数链接在顶层 linux 目录下的 vmlinux 中。这是一个 elf 格式的文件,由 objcoopy 去除符号信息后用压缩工具压缩,包含到 piggy.S 中,从而形成了 piggy.o,最终和其它文件一起生成了 vmlinux.bin。

第 16 节课

Q:我们为什么要以 2 的(0~52)次方为页面数来组织页面呢?
A:以 2 的(0~52)次方为页面数来组织页面,是为了每组连续的页面能对半分割。对半分割是为了保证连续的页面空间最大化,同时保证在下一次释放时,能最大可能地合并一个整体,这么做的目的只有一个:在满足最小、最大页面请求时,保证内存碎片的最小化。

第 17 节课

Q:请问在 4GB 的物理内存的情况下,msadsc_t 结构实例变量本身占用多大的内存空间?
A:4GB 有 1M 个页面,那就对应 1M 个 msadsc_t 结构,每个 msadsc_t 结构为 40 个字节,所以占用 40MB 的内存空间。

第 18 节课

Q:在内存页面分配过程中,是怎样尽可能保证内存页面连续的呢?
A:因为分配内存页面一开始就是连续的,然后在分配时始终以 2 的幂次分隔,所以能保证内存页面的最大连续性。

第 19 节课

Q:为什么我们在分配内存对象大小时,要按照 Cache 行大小的倍数分配呢?
A:因为这使得我们分配的内存对象的地址空间是和 Cache 行对齐的,那么这个内存对象中的数据就极有可能被 Cache 命中,从而大大提升程序的性能。

第 20 节课

Q:请问内核虚拟地址空间为什么有一个 0xFFFF800000000000~0xFFFF800400000000 的线性映射区呢?
A:内核的线性映射区 0xFFFF800000000000~0xFFFF800400000000,会映射到物理地址空间的 0~~0x400000000。因为内核本身运行在虚拟地址空间,本身使用虚拟地址,但是它又必须访问物理内存,所以有了这个线性映射区,就可以把这个区域的物理地址转换成虚拟地址,也可以直接把虚拟地址转换成物理地址。
另外,因为它们之间就是一个常数:0xFFFF800000000000。所以,内核就可以很方便地操作自身数据结构和设备寄存器。这个设备寄存器是物理地址,内核很方便就能转换为虚拟地址,然后通过这个虚拟地址访问设备寄存器。

第 21 节课

Q:请问,x86 CPU 的缺页异常,是第几号异常?缺页的地址保存在哪个寄存器中?
A:x86 CPU 的缺页异常,是 14 号异常。缺页的地址保存在 x86 CPU 的 CR2 寄存器中。

第 22 节课

Q:在默认配置下,Linux 伙伴系统能分配多大的连续物理内存?
A:Linux 伙伴系统能分配多大的连续物理内存,取决于 MAX_ORDER。MAX_ORDER 的值默认为 11,因为是 free_area 数组的下标,所以要 MAX_ORDER-1 = 10, 结果就是 2 << 10 = 1024,而 1024 个连续的页面(一个页面 4KB)是 4MB,即 Linux 伙伴系统能分配多大的连续物理内存是 4MB。

第 23 节课

Q:Linux 的 SLAB,使用 kmalloc 函数能分配多大的内存对象呢?
A:kmalloc 函数能分配 32MB 的内存对象。

第 24 节课

Q:各个进程是如何共享同一份内核代码和数据的?
A:只需要将每个进程的上半部分虚拟地址空间(0xFFFF800000000000~0xFFFFFFFFFFFFFFFF)的 MMU 页表设为相同的映射关系就行了,这样每个进程都可以共享内核的代码的数据,但是又不能读取和修改这部分地址空间中的数据,因为权限不够。

第 25 节课

Q:请问当调度器函数调度到一个新建进程时,为何要进入 retnfrom_first_sched 函数呢?
A:因为新建的进程内核中只有 CPU 默认的寄存器状态,没有从内核其它任何位置调用进入 krlschedul 函数。因此没有调用 krlschedul 函数的调用路径,所以无从返回,只能通过 retnfrom_first_sched 函数,强制初始化 CPU 寄存器状态,从而让进程开始运行。

第 26 节课

Q:我们让进程进入等待状态后,进程会立马停止运行吗?
A:进程不会立马停止运行,因为在调用 krlsched_wait 函数后,进程的上下文并没有切换。需要在 krlsched_wait 函数的外层,通过调用 krlschedul 函数进行进程调度,才能让该进程停止运行,进入等待状态。

第 27 节课

Q:想一想,Linux 进程的优先级和 Linux 调度类的优先级是一回事儿吗?
A:不是一回事儿。一个调度类管理着同一类的多个进程,而进程的优先级是该调度类下的各个进程间的优先级。

第 28 节课

Q:请你写出一个用来访问设备的接口函数,或者想一下访问一个设备需要什么参数。
A:比如打开一个设备的接口函数,如下所示:
int open(devid_t *devid, uint_t flgs);
必须至少要有设备的 devid 参数。里面要包含设备的类型和设备号,这样才能找到一个具体的设备。

第 29 节课

Q:请你写出帮驱动程序开发者自动分配设备 ID 接口函数。
A:很明显,这需要驱动程序提供一个设备类型,然后到设备表中搜索该设备类型还没有占用的设备 ID,最后返回这个设备 ID。代码如下所示:
drvstus_t krlnew_devid(devid_t *devid)
{
device_t *findevp;
drvstus_t rets = DFCERRSTUS;
cpuflg_t cpufg;
list_h_t *lstp;
devtable_t *dtbp = &osdevtable;//获取设备表
uint_t devmty = devid->dev_mtype;
uint_t devidnr = 0;
if (devmty >= DEVICE_MAX)
{
return DFCERRSTUS;
}
krlspinlock_cli(&dtbp->devt_lock, &cpufg);
if (devmty != dtbp->devt_devclsl[devmty].dtl_type)
{
rets = DFCERRSTUS;
goto return_step;
}
//检查这个设备类型链表是不是为空
if (list_is_empty(&dtbp->devt_devclsl[devmty].dtl_list) == TRUE)
{
rets = DFCOKSTUS;
devid->dev_nr = 0;
goto return_step;
}
//扫描该设备类型链表下的所有设备
list_for_each(lstp, &dtbp->devt_devclsl[devmty].dtl_list)
{
findevp = list_entry(lstp, device_t, dev_intbllst);
if (findevp->dev_id.dev_nr > devidnr)
{
//获取最大的设备号
devidnr = findevp->dev_id.dev_nr;
}
}
//新的设备号等于最大设备号加一
devid->dev_nr = devidnr++;
rets = DFCOKSTUS;
return_step:
krlspinunlock_sti(&dtbp->devt_lock, &cpufg);
return rets;
}

第 30 节课

Q:请你想一想,为什么没有 systick 设备这样周期性的产生中断,进程就有可能霸占 CPU 呢?
A:如果一个应用程序,它不调用任何系统接口,也不退出系统,就在主函数中执行一个死循环,这样这个进程一旦运行,内核将再也没有办法从应用手中夺回 CPU,其代码如下:
void main()
{
for(;;);
return;
}

第 31 节课

Q:为什么无论是我们加载 miscdrv.ko 内核模块,还是运行 App 测试,都要在前面加上 sudo 呢?
A:Linux 系统的安全是基于用户类型的,并且是多用户的系统,所以有些影响系统的操作,必须要 root 用户才能完成。比如你加载一个内核模块,这个内核模块是不是友好的?会不会干坏事?这需要管理员 root 用户评估;应用程序访问设备,同样是系统特权操作,也需要 root 用户,而 sudo 命令就是暂时让应用以 root 用户运行。

第 32 节课

Q:请问,我们文件系统的储存单位为什么要自定义一个逻辑储存块?
A:有两点考量:一是储存设备都按块为单位储存;二是为了文件系统代码的可移植性和可扩展性。因为储存设备的储存块大小各不相同,有 512B、1KB、2KB、4KB,我们自己定义一个逻辑储存块,就能很好地适应不同的储存设备。

第 33 节课

Q:请问,建立文件系统的超级块、位图、根目录的三大函数的调用顺序可以随意调换吗,原因是什么?
A:不能随意调换,因为建立位图要依赖于超级块,而建立根目录时需要依赖于位图,所以必须是先调用建立超级块的函数,然后调用建立位图的函数,最后调用建立根目录的函数。

第 34 节课

Q:请你想一想,我们这个简单的、小的,却五脏俱全的文件系统有哪些限制?
A:我们这个文件系统有如下限制:
不能创建目录,所有文件都在根目录“/”下,即文件路径名都是这样的形式:“/file”、“/file1”、“/file2”等;
每个文件最多只能分配一个储存块(4KB 大小);
暂不支持文件随机读写,一旦发生读写操作,我们的文件系统会把一个文件的全部数据都返回给请求者,或者更新该文件的全部数据。

第 35 节课

Q:请说一说 super_block,dentry,inode 这三个数据结构 ,一定要在储存设备上对应存在吗?
A:不一定要在储存设备上对应存在,具体的文件系统可以有自己的实现,但是在运行时刻必须要能转换成内存中对应的 super_block,dentry,inode 这三大数据结构。转换方法由具体文件系统实现。

第 36 节课

Q:我们这节课从宏观的角度分析了网络数据的运转,但是在内核中网络数据包怎么运转的呢?请你简单描述这个过程。
A:内核网络数据包处理流程如下:
网卡驱动初始化
中断注册
重要结构体初始化
网络收发包

第 37 节课

Q:我们已经了解到了操作系统内核和网络协议栈的关系,可是网络协议栈真的一定只能放在内核态实现么?
A:我们发现传统的收发方式有一些弊端,比如:内核态用户态切换会引入 Cache Miss、流水线失效、硬中断、锁、额外的拷贝等等额外开销。这些开销在 C10K 的并发规模可能无法体现出来,可是一旦到了 C10M 的规模,这些开销就不容小视了,于是 DPDK 这种用户态网络栈就应运而生了。

第 38 节课

Q:请思考一下,我们目前的互联网架构属于中心化架构还是去中心化架构呢?你觉得未来的发展趋势又是如何?
A:早期的传统互联网架构下,我们如果要配置交换机,一般都是直接用配置线连接交换机,然后命令行配置的,但出现什么问题可能就要跑到机房了。而且那个年代,中小型运营商也比较多,且分布式技术不够成熟。所以,诞生了如 OSPF、BGP、ISIS 之类的分布式、自组织的动态路由协议。
而随着分布式技术成熟,以及电信、互联网巨头逐渐聚集,我们现在逐渐演进到了以 Google B4 为代表的中心化架构的 SDN 上了,这也就是为什么万维网之父 Tim Berners-Lee 爵士会表示对今天的中心化 Web 非常不满,却还是搞出了开源的去中心化平台 Solid 项目的原因。
至于未来,个人认为随着以大数据、区块链为代表的去中心化架构逐渐成熟,也许互联网的基础架构会回归去中心化。当然为了实现这个目标,就需要我们大家一起努力了。

第 39 节课

Q:套接字也是一种进程间通信机制,它和其他通信机制有什么不同?
A:它可用于不同机器间的进程通信。

第 40 节课

Q:我们了解的 TCP 三次握手,发生在 socket 的哪几个函数中呢?
A:第一次握手:客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;
第二次握手:服务器监听到连接请求,即收到 SYN J 包,就会调用 accept 函数接收请求,向客户端发送 SYN K ,接着 ACK J+1,这时 accept 进入阻塞状态;
第三次握手:客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;服务器收到 ACK K+1 时,accept 返回。至此三次握手完毕,连接建立。

第 41 节课

Q:请问 int 指令后面的常数能不能大于 255,为什么?
A:int 指令后面的常数不能大于 255,因为 int 指令会经过中断门,后面的常数就是中断门的索引,而我们中断门最多 256(0~255)个,所以不能大于 255。

第 42 节课

Q:请说说 syscall 指令和 int 指令的区别是什么?
A:syscall 指令不需要经过中断门,执行 syscall 指令后的进入内核的入口地址,是内核在初始化时写入到特殊寄存器。这个寄存器应用程序不能访问,处理器在硬件层还对 syscall 指令执行逻辑做了一定的优化,而 int 要经过中断门进入到内核,做权限检查又还要读取内存,这会导致性能下降。

第 43 节课

Q:有了 KVM 作为虚拟化的基石之后,如果让你从零开始,设计一款像各大云厂商 IAAS 平台一样的虚拟化平台,还需要考虑哪些问题呢?
A:如果只是在一台物理机上开启多个虚拟机,KVM 确实已经做的很棒了,但是如果我们扩展到多个机架、多个机房,问题就变得更加复杂了。
我们除了要考虑之前讲过的网络问题,还需要考虑分布式环境下的计算、存储、消息传输、状态同步、动态迁移、扩缩容、镜像、身份认证、编排与调度、UI 管理面板等很多问题。
当然,业界也有一些开源解决方案,比如大名鼎鼎的 OpenStack,不过笔者觉得 OpenStack 由于设计实现得比较早,所以存在集群规模有限,部署、维护、二次开发复杂度高,历史包袱重等问题。和多位架构师沟通交流之后,我们正在尝试重新设计并实现一套更现代化的、轻量级的、IAAS 云平台,感兴趣的同学可以加入课程群多多交流。课程交流群点这里,按加群提示操作后加入。

第 44 节课

Q:在我们启动容器后,一旦容器退出,容器可写层的所有内容都会被删除。那么,如果用户需要持久化容器里的部分数据该怎么办呢?
A:可以通过实现 volume(数据卷),在容器文件系统里创建挂载点,把宿主机文件目录挂载到容器挂载点,启动过程中读取数据卷。

第 45 节课

Q:除了 ARM 指令集,如果想开发一款 CPU,我们还有更好的 RISC 指令集可选么?
A:RISC-V 是 2010 年加州大学柏克莱分校创建的开源指令集架构。由于这个指令集是完全开放,允许任何人用于任何目的而设计,还不需要付高昂的专利费,所以开源之后 IBM、高通、恩智浦、甲骨文、华为、阿里等知名公司也纷纷加入基金会,并且投入大量资源来进行研发与优化。由此可见,RISC-V 是一个非常有潜力的项目,我们也会在后续课程结束后,发起 Cosmos 配套的开源芯片研发项目,感兴趣的同学可以加入课程群一起多多交流。

第 45 节课

Q:请问,ARMv8 有多少特权级?每个特权级有什么作用?
A:ARMv8,有 4 个特权级,E0~E3, E0 运行 APP,E1 运行 OS, E2 运行虚拟机监控软件,E3 运行安全监视软件。
到这里,思考题答案公布完毕,同学们学习加油呀!
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 4

提建议

上一篇
结课测试 |这些操作系统的问题,你都掌握了么?
下一篇
LMOS来信:第二季课程带你“手撕”计算机基础
unpreview
 写留言

精选留言(3)

  • Zhang
    2023-01-31 来自河南
    感觉很有必要再刷一遍
  • ppd0705
    2022-03-13
    粗略的过了一遍+1,从去年7月底开始,还好没放弃 😭

    作者回复: 哈哈

  • ifelse
    2022-03-06
    粗略的过了一遍

    作者回复: 感觉怎么样

    共 3 条评论