gitbook/操作系统实战45讲/docs/483437.md
2022-09-03 22:05:03 +08:00

480 lines
26 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 参考答案 | 对答案,是再次学习的一个机会
你好,我是编辑宇新。
春节将至先给你拜个早年愿你2022年工期变长需求变少技术水平更加硬核。
距离我们专栏更新结束已经过去了不少时间给坚持学习的你点个赞。学习操作系统是一个长期投资需要持之以恒才能见效。无论你是二刷、三刷的朋友还是刚买课的新同学都建议你充分利用留言区给自己的学习加个增益buff。这种学习讨论的氛围也会激励你持续学习。
今天这期加餐,我们整理了课程里的思考题答案,一次性发布出来,供你对照参考,查漏补缺。
建议你一定要先自己学习理解,动脑思考、动手训练,有余力还可以看看其他小伙伴的解题思路,之后再来对答案。
### [第1节课](https://time.geekbang.org/column/article/369457)
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节课](https://time.geekbang.org/column/article/369502)
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节课](https://time.geekbang.org/column/article/372609)
Q其实我们的内核架构不是我们首创的它是属于微内核、宏内核之外的第三种架构请问这是什么架构
A我们的内核架构是混合内核架构是介于微、宏架构之间的一种架构这种架构保证了宏架构的高性能又兼顾了微架构的可移植、可扩展性。
### [第4节课](https://time.geekbang.org/column/article/374474)
QWindows NT内核属于哪种架构类型
AWindows NT内核架构其实既不属于传统的宏内核架构也不是新的微内核架构说NT是微内核架构是错误的NT这种内核架构其实是宏内核的变种——混合内核。
### [第5节课](https://time.geekbang.org/column/article/375278)
Q请问实模式下能寻址多大的内存空间
A由于实模式下访问内存的地址是这样产生的16位段寄存器左移4位加一个16位通用寄存器最后形成了20位地址所以只能访问1MB大的内存空间。
### [第6节课](https://time.geekbang.org/column/article/376064)
Q分页模式下操作系统是如何对应用程序的地址空间进行隔离的
A操作系统会给每个应用程序都配置独立的一套页表数据。应用程序运行时就让CR3寄存器指向该应用程序的页表数据。运行下一个应用程序时则会执行同样的操作。
### [第7节课](https://time.geekbang.org/column/article/376711)
Q请你思考一下如何写出让CPU跑得更快的代码由于Cache比内存快几个数量级所以这个问题也可以转换成如何写出提高Cache命中率的代码
A第一定义变量时尽量让其地址与Cache行大小对齐。
```plain
int a __attribute__((aligned (64))); 
int b __attribute__((aligned (64))); 
```
第二,操作数据时的顺序,尽量和数据在内存中布局顺序保持一致。
```plain
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节课](https://time.geekbang.org/column/article/377913)
Q请用代码展示一下自旋锁或者信号量可能的使用形式是什么样的
A最常规的形式是在设计共享数据结构时在其中包含自旋锁或者信号量。
如下所示:
```c++
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节课](https://time.geekbang.org/column/article/378870)
Q请试着回答上述Linux的读写锁支持多少个进程并发读取共享数据这样的读写锁有什么不足
A第一个问题根据上述描述读写锁本质上就是一个计数器。锁变量的初始值为0x01000000即表示最多可以有0x01000000个进程同时获取读锁。
第二个问题,读写锁的不足是,如果一直有很多读取数据的进程占有读锁,因而可能导致修改数据的进程饥饿的情况。操作系统会加以控制,让修改数据的进程优先得锁。
### [第10节课](https://time.geekbang.org/column/article/379291)
Q请问我们为什么要把虚拟硬盘格式化成ext4文件系统格式呢
A有两点原因。第一GRUB在加载系统映像文件时能够识别ext4文件系统格式我们在Linux下生成系统映像文件时要复制到虚拟硬盘中去所以这个文件系统格式必须被Linux所识别那么选ext4就最合适。
### [第11节课](https://time.geekbang.org/column/article/380507)
Q请问GRUB头中为什么需要\_entry标号和\_start标号的地址
A这是GRUB规定的。GRUB正是通过\_entry标号和\_start标号的地址控制内核文件被加载到什么内存地址又应该从什么内存地址开始运行。
### [第12节课](https://time.geekbang.org/column/article/381157)
Q请你想一下init\_bstartparm()函数中的init\_mem820()函数,这个函数到底干了什么?
Ainit\_mem820()函数是把e820map\_t结构数组复制到内核文件之后的内存空间并且重新填写了机器信息结构。
它的代码如下:
```plain
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节课](https://time.geekbang.org/column/article/381810)
Q请你画出Cosmos硬件抽象层的函数调用关系图。
ACosmos硬件抽象层的函数调用关系图如下。
![](https://static001.geekbang.org/resource/image/90/d7/9045b4fc2ece20b18e99b03b29be15d7.jpg?wh=3808x2771)
### [第14节课](https://time.geekbang.org/column/article/382733)
Q为什么要用C代码mkpiggy程序生成piggy.S文件并包含vmlinux.bin.gz文件呢
A因为mkpiggy程序在读取vmlinux.bin.gz文件知道了其长度等信息它就会把这些信息保存在piggy.S文件相关的字段中。在解压vmlinux.bin.gz文件时解压的代码需要用到这些信息。
### [第15节课](https://time.geekbang.org/column/article/383611)
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节课](https://time.geekbang.org/column/article/384366?)
Q我们为什么要以 2 的052次方为页面数来组织页面呢
A以2的052次方为页面数来组织页面是为了每组连续的页面能对半分割。对半分割是为了保证连续的页面空间最大化同时保证在下一次释放时能最大可能地合并一个整体这么做的目的只有一个在满足最小、最大页面请求时保证内存碎片的最小化。
### [第17节课](https://time.geekbang.org/column/article/384772)
Q请问在 4GB 的物理内存的情况下msadsc\_t 结构实例变量本身占用多大的内存空间?
A4GB有1M个页面那就对应1M个msadsc\_t结构每个msadsc\_t结构为40个字节所以占用40MB的内存空间。
### [第18节课](https://time.geekbang.org/column/article/385628)
Q在内存页面分配过程中是怎样尽可能保证内存页面连续的呢
A因为分配内存页面一开始就是连续的然后在分配时始终以2的幂次分隔所以能保证内存页面的最大连续性。
### [第19节课](https://time.geekbang.org/column/article/386400)
Q为什么我们在分配内存对象大小时要按照Cache行大小的倍数分配呢
A因为这使得我们分配的内存对象的地址空间是和Cache行对齐的那么这个内存对象中的数据就极有可能被Cache命中从而大大提升程序的性能。
### [第20节课](https://time.geekbang.org/column/article/387258)
Q请问内核虚拟地址空间为什么有一个 0xFFFF8000000000000xFFFF800400000000 的线性映射区呢?
A内核的线性映射区0xFFFF8000000000000xFFFF800400000000会映射到物理地址空间的0~0x400000000。因为内核本身运行在虚拟地址空间本身使用虚拟地址但是它又必须访问物理内存所以有了这个线性映射区就可以把这个区域的物理地址转换成虚拟地址也可以直接把虚拟地址转换成物理地址。
另外因为它们之间就是一个常数0xFFFF800000000000。所以内核就可以很方便地操作自身数据结构和设备寄存器。这个设备寄存器是物理地址内核很方便就能转换为虚拟地址然后通过这个虚拟地址访问设备寄存器。
### [第21节课](https://time.geekbang.org/column/article/388167)
Q请问x86 CPU 的缺页异常,是第几号异常?缺页的地址保存在哪个寄存器中?
Ax86 CPU的缺页异常是14号异常。缺页的地址保存在x86 CPU的CR2寄存器中。
### [第22节课](https://time.geekbang.org/column/article/389123)
Q在默认配置下Linux伙伴系统能分配多大的连续物理内存
ALinux伙伴系统能分配多大的连续物理内存取决于MAX\_ORDER。MAX\_ORDER的值默认为11因为是free\_area数组的下标所以要MAX\_ORDER-1 = 10, 结果就是2 << 10 = 1024而1024个连续的页面一个页面4KB是4MB即Linux伙伴系统能分配多大的连续物理内存是4MB
### [第23节课](https://time.geekbang.org/column/article/389880)
QLinux的SLAB使用kmalloc函数能分配多大的内存对象呢
Akmalloc函数能分配32MB的内存对象
### [第24节课](https://time.geekbang.org/column/article/390674)
Q各个进程是如何共享同一份内核代码和数据的
A只需要将每个进程的上半部分虚拟地址空间0xFFFF800000000000~0xFFFFFFFFFFFFFFFF的MMU页表设为相同的映射关系就行了这样每个进程都可以共享内核的代码的数据但是又不能读取和修改这部分地址空间中的数据因为权限不够
### [第25节课](https://time.geekbang.org/column/article/391222)
Q请问当调度器函数调度到一个新建进程时为何要进入 retnfrom\_first\_sched 函数呢
A因为新建的进程内核中只有CPU默认的寄存器状态没有从内核其它任何位置调用进入krlschedul函数因此没有调用krlschedul函数的调用路径所以无从返回只能通过retnfrom\_first\_sched函数强制初始化CPU寄存器状态从而让进程开始运行
### [第26节课](https://time.geekbang.org/column/article/392198)
Q我们让进程进入等待状态后进程会立马停止运行吗
A进程不会立马停止运行因为在调用krlsched\_wait函数后进程的上下文并没有切换需要在krlsched\_wait函数的外层通过调用krlschedul函数进行进程调度才能让该进程停止运行进入等待状态
### [第27节课](https://time.geekbang.org/column/article/393350)
Q想一想Linux 进程的优先级和 Linux 调度类的优先级是一回事儿吗
A不是一回事儿一个调度类管理着同一类的多个进程而进程的优先级是该调度类下的各个进程间的优先级
### [第28节课](https://time.geekbang.org/column/article/394084)
Q请你写出一个用来访问设备的接口函数或者想一下访问一个设备需要什么参数
A比如打开一个设备的接口函数如下所示
```plain
int open(devid_t *devid, uint_t flgs);
```
必须至少要有设备的devid参数里面要包含设备的类型和设备号这样才能找到一个具体的设备
### [第29节课](https://time.geekbang.org/column/article/394875)
Q请你写出帮驱动程序开发者自动分配设备ID接口函数
A很明显这需要驱动程序提供一个设备类型然后到设备表中搜索该设备类型还没有占用的设备ID最后返回这个设备ID代码如下所示
```plain
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节课](https://time.geekbang.org/column/article/395772)
Q请你想一想为什么没有 systick 设备这样周期性的产生中断进程就有可能霸占 CPU
A如果一个应用程序它不调用任何系统接口也不退出系统就在主函数中执行一个死循环这样这个进程一旦运行内核将再也没有办法从应用手中夺回CPU其代码如下
```plain
void main()
{
for(;;);
return;
}
```
### [第31节课](https://time.geekbang.org/column/article/396896)
Q为什么无论是我们加载miscdrv.ko内核模块还是运行App测试都要在前面加上sudo呢
ALinux系统的安全是基于用户类型的并且是多用户的系统所以有些影响系统的操作必须要root用户才能完成比如你加载一个内核模块这个内核模块是不是友好的会不会干坏事这需要管理员root用户评估应用程序访问设备同样是系统特权操作也需要root用户而sudo命令就是暂时让应用以root用户运行
### [第32节课](https://time.geekbang.org/column/article/397594)
Q请问我们文件系统的储存单位为什么要自定义一个逻辑储存块
A有两点考量一是储存设备都按块为单位储存二是为了文件系统代码的可移植性和可扩展性因为储存设备的储存块大小各不相同有512B1KB2KB4KB我们自己定义一个逻辑储存块就能很好地适应不同的储存设备
### [第33节课](https://time.geekbang.org/column/article/39869)
Q请问建立文件系统的超级块位图根目录的三大函数的调用顺序可以随意调换吗原因是什么
A不能随意调换因为建立位图要依赖于超级块而建立根目录时需要依赖于位图所以必须是先调用建立超级块的函数然后调用建立位图的函数最后调用建立根目录的函数
### [第34节课](https://time.geekbang.org/column/article/399700)
Q请你想一想我们这个简单的小的却五脏俱全的文件系统有哪些限制
A我们这个文件系统有如下限制
1. 不能创建目录所有文件都在根目录“/”即文件路径名都是这样的形式:“/file”、“/file1”、“/file2
2. 每个文件最多只能分配一个储存块4KB大小
3. 暂不支持文件随机读写一旦发生读写操作我们的文件系统会把一个文件的全部数据都返回给请求者或者更新该文件的全部数据
### [第35节课](https://time.geekbang.org/column/article/400424)
Q请说一说 super\_blockdentryinode 这三个数据结构 一定要在储存设备上对应存在吗
A不一定要在储存设备上对应存在具体的文件系统可以有自己的实现但是在运行时刻必须要能转换成内存中对应的super\_blockdentryinode这三大数据结构转换方法由具体文件系统实现
### [第36节课](https://time.geekbang.org/column/article/401467)
Q我们这节课从宏观的角度分析了网络数据的运转但是在内核中网络数据包怎么运转的呢请你简单描述这个过程
A内核网络数据包处理流程如下
1. 网卡驱动初始化
2. 中断注册
3. 重要结构体初始化
4. 网络收发包
### [第37节课](https://time.geekbang.org/column/article/402840)
Q我们已经了解到了操作系统内核和网络协议栈的关系可是网络协议栈真的一定只能放在内核态实现么
A我们发现传统的收发方式有一些弊端比如内核态用户态切换会引入Cache Miss流水线失效硬中断额外的拷贝等等额外开销这些开销在C10K的并发规模可能无法体现出来可是一旦到了C10M的规模这些开销就不容小视了于是DPDK这种用户态网络栈就应运而生了
### [第38节课](https://time.geekbang.org/column/article/404013)
Q请思考一下我们目前的互联网架构属于中心化架构还是去中心化架构呢你觉得未来的发展趋势又是如何
A早期的传统互联网架构下我们如果要配置交换机一般都是直接用配置线连接交换机然后命令行配置的但出现什么问题可能就要跑到机房了而且那个年代中小型运营商也比较多且分布式技术不够成熟所以诞生了如OSPFBGPISIS之类的分布式自组织的动态路由协议
而随着分布式技术成熟以及电信互联网巨头逐渐聚集我们现在逐渐演进到了以Google B4为代表的中心化架构的SDN上了这也就是为什么万维网之父Tim Berners-Lee爵士会表示对今天的中心化Web 非常不满却还是搞出了开源的去中心化平台 Solid项目的原因
至于未来个人认为随着以大数据区块链为代表的去中心化架构逐渐成熟也许互联网的基础架构会回归去中心化当然为了实现这个目标就需要我们大家一起努力了
### [第39节课](https://time.geekbang.org/column/article/404724)
Q套接字也是一种进程间通信机制它和其他通信机制有什么不同
A它可用于不同机器间的进程通信
### [第40节课](https://time.geekbang.org/column/article/405781)
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节课](https://time.geekbang.org/column/article/406633)
Q请问 int 指令后面的常数能不能大于255为什么
Aint 指令后面的常数不能大于255因为int指令会经过中断门后面的常数就是中断门的索引而我们中断门最多2560255所以不能大于255
### [第42节课](https://time.geekbang.org/column/article/407343)
Q请说说syscall指令和int指令的区别是什么
Asyscall指令不需要经过中断门执行syscall指令后的进入内核的入口地址是内核在初始化时写入到特殊寄存器这个寄存器应用程序不能访问处理器在硬件层还对syscall指令执行逻辑做了一定的优化而int要经过中断门进入到内核做权限检查又还要读取内存这会导致性能下降
### [第43节课](https://time.geekbang.org/column/article/408124)
Q有了KVM作为虚拟化的基石之后如果让你从零开始设计一款像各大云厂商IAAS平台一样的虚拟化平台还需要考虑哪些问题呢
A如果只是在一台物理机上开启多个虚拟机KVM确实已经做的很棒了但是如果我们扩展到多个机架多个机房问题就变得更加复杂了
我们除了要考虑之前讲过的网络问题还需要考虑分布式环境下的计算存储消息传输状态同步动态迁移扩缩容镜像身份认证编排与调度UI管理面板等很多问题
当然业界也有一些开源解决方案比如大名鼎鼎的OpenStack不过笔者觉得OpenStack由于设计实现得比较早所以存在集群规模有限部署维护二次开发复杂度高历史包袱重等问题和多位架构师沟通交流之后我们正在尝试重新设计并实现一套更现代化的轻量级的IAAS云平台感兴趣的同学可以加入课程群多多交流课程交流群点[这里](https://jinshuju.net/f/I4XbfK)按加群提示操作后加入
### [第44节课](https://time.geekbang.org/column/article/408927)
Q在我们启动容器后一旦容器退出容器可写层的所有内容都会被删除那么如果用户需要持久化容器里的部分数据该怎么办呢
A可以通过实现volume数据卷在容器文件系统里创建挂载点把宿主机文件目录挂载到容器挂载点启动过程中读取数据卷
### [第45节课](https://time.geekbang.org/column/article/409790)
Q除了 ARM 指令集如果想开发一款 CPU我们还有更好的 RISC 指令集可选么
ARISC-V是2010年加州大学柏克莱分校创建的开源指令集架构由于这个指令集是完全开放允许任何人用于任何目的而设计还不需要付高昂的专利费所以开源之后IBM高通恩智浦甲骨文华为阿里等知名公司也纷纷加入基金会并且投入大量资源来进行研发与优化由此可见RISC-V是一个非常有潜力的项目我们也会在后续课程结束后发起Cosmos配套的开源芯片研发项目感兴趣的同学可以加入课程群一起多多交流
### [第45节课](https://time.geekbang.org/column/article/410396)
Q请问ARMv8有多少特权级每个特权级有什么作用
AARMv8有4个特权级E0E3, E0运行APPE1运行OS E2运行虚拟机监控软件E3运行安全监视软件
到这里思考题答案公布完毕同学们学习加油呀