281 lines
13 KiB
Markdown
281 lines
13 KiB
Markdown
|
# 加餐福利 | 课后思考题答案合集
|
|||
|
|
|||
|
你好,我是程远,好久不见。
|
|||
|
|
|||
|
距离我们的专栏更新结束,已经过去了不少时间。我仍然会在工作之余,到这门课的留言区转一转,回答同学的问题。大部分的疑问,我都通过留言做了回复。
|
|||
|
|
|||
|
除了紧跟更新的第一批同学,也很开心有更多新朋友加入到这个专栏的学习中。那课程的思考题呢,为了给你留足思考和研究的时间,我选择用加餐的方式,给你提供参考答案。
|
|||
|
|
|||
|
这里我想和你说明的是,我这里给你提供的参考答案,都是我能够直接给你特定答案的问题。至于操作类的题目,有的我引用了同学回复的答案。
|
|||
|
|
|||
|
另外一类操作题,是为了帮你巩固课程内容知识的,相信你可以从课程正文里找到答案。我还是建议你自己动手实战,这样你的收获会更大。
|
|||
|
|
|||
|
## 必学部分思考题
|
|||
|
|
|||
|
[第2讲](https://time.geekbang.org/column/article/309423)
|
|||
|
|
|||
|
Q:对于这一讲的最开始,有这样一个C语言的init进程,它没有注册任何信号的handler。如果我们从Host Namespace向它发送SIGTERM,会发生什么情况呢?
|
|||
|
|
|||
|
A:即使在宿主机上向容器1号进程发送SIGTERM,在1号进程没有注册handler的情况下,这个进程也不能被杀死。
|
|||
|
|
|||
|
这个问题的原因是这样的:开始要看内核里的那段代码,“ !(force && sig\_kernel\_only(sig))”,
|
|||
|
|
|||
|
虽然由不同的namespace发送信号, 虽然force是1了,但是sig\_kernel\_only(sig)对于SIGTERM来说还是0,这里是个&&, 那么 !(1 && 0) = 1。
|
|||
|
|
|||
|
```shell
|
|||
|
#define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
|
|||
|
#define SIG_KERNEL_ONLY_MASK (\
|
|||
|
rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[第3讲](https://time.geekbang.org/column/article/310060)
|
|||
|
|
|||
|
Q:如果容器的init进程创建了子进程B,B又创建了自己的子进程C。如果C运行完之后,退出成了僵尸进程,B进程还在运行,而容器的init进程还在不断地调用waitpid(),那C这个僵尸进程可以被回收吗?
|
|||
|
|
|||
|
A:这道题可以参考下面两位同学的回答。
|
|||
|
|
|||
|
Geek2014用户的回答:
|
|||
|
|
|||
|
> 这时C是不会被回收的,只有等到B也被杀死,C这个僵尸进程也会变成孤儿进程,被init进程收养,进而被init的wait机制清理掉。
|
|||
|
|
|||
|
莫名同学的回答:
|
|||
|
|
|||
|
> C应该不会被回收,waitpid仅等待直接children的状态变化。
|
|||
|
|
|||
|
> 为什么先进入僵尸状态而不是直接消失?觉得是留给父进程一次机会,查看子进程的PID、终止状态(退出码、终止原因,比如是信号终止还是正常退出等)、资源使用信息。如果子进程直接消失,那么父进程没有机会掌握子进程的具体终止情况。
|
|||
|
|
|||
|
> 一般情况下,程序逻辑可能会依据子进程的终止情况做出进一步处理:比如 Nginx Master 进程获知 Worker 进程异常退出,则重新拉起来一个Worker进程。
|
|||
|
|
|||
|
[第4讲](https://time.geekbang.org/column/article/310804)
|
|||
|
|
|||
|
Q:请你回顾一下基本概念中最后的这段代码,你可以想一想,在不做编译运行的情况下,它的输出是什么?
|
|||
|
|
|||
|
```shell
|
|||
|
#include <stdio.h>
|
|||
|
#include <signal.h>
|
|||
|
|
|||
|
typedef void (*sighandler_t)(int);
|
|||
|
|
|||
|
void sig_handler(int signo)
|
|||
|
{
|
|||
|
if (signo == SIGTERM) {
|
|||
|
printf("received SIGTERM\n\n");
|
|||
|
// Set SIGTERM handler to default
|
|||
|
signal(SIGTERM, SIG_DFL);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
int main(int argc, char *argv[])
|
|||
|
{
|
|||
|
//Ignore SIGTERM, and send SIGTERM
|
|||
|
// to process itself.
|
|||
|
|
|||
|
signal(SIGTERM, SIG_IGN);
|
|||
|
printf("Ignore SIGTERM\n\n");
|
|||
|
kill(0, SIGTERM);
|
|||
|
|
|||
|
//Catch SIGERM, and send SIGTERM
|
|||
|
// to process itself.
|
|||
|
signal(SIGTERM, sig_handler);
|
|||
|
printf("Catch SIGTERM\n");
|
|||
|
kill(0, SIGTERM);
|
|||
|
|
|||
|
|
|||
|
//Default SIGTERM. In sig_handler, it sets
|
|||
|
//SIGTERM handler back to default one.
|
|||
|
printf("Default SIGTERM\n");
|
|||
|
kill(0, SIGTERM);
|
|||
|
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
A:可以参考用户geek 2014同学的答案。输出结果如下:
|
|||
|
|
|||
|
Ignore SIGTERM
|
|||
|
Catch SIGTERM
|
|||
|
received SIGTERM
|
|||
|
Default SIGTERM
|
|||
|
|
|||
|
[第5讲](https://time.geekbang.org/column/article/311054)
|
|||
|
|
|||
|
Q:我们还是按照文档中定义的控制组目录层次结构图,然后按序执行这几个脚本:
|
|||
|
|
|||
|
* [create\_groups.sh](https://github.com/chengyli/training/blob/main/cpu/cgroup_cpu/create_groups.sh)
|
|||
|
* [update\_group1.sh](https://github.com/chengyli/training/blob/main/cpu/cgroup_cpu/update_group1.sh)
|
|||
|
* [update\_group4.sh](https://github.com/chengyli/training/blob/main/cpu/cgroup_cpu/update_group4.sh)
|
|||
|
* [update\_group3.sh](https://github.com/chengyli/training/blob/main/cpu/cgroup_cpu/update_group3.sh)
|
|||
|
|
|||
|
那么,在一个4个CPU的节点上,group1/group3/group4里的进程,分别会被分配到多少CPU呢?
|
|||
|
|
|||
|
A:分配比例是: 2 : 0.5 : 1.5
|
|||
|
|
|||
|
**可以参考geek 2014的答案:**
|
|||
|
|
|||
|
> group1 的shares为1024,quota 3.5,尝试使用4,
|
|||
|
|
|||
|
> group2的shares默认为1024,quota设置为-1,不受限制,也即是,如果CPU上只有group2的话,那么group2可以使用完所有的CPU(实际上根据group3和group4,group2最多也就能用到1.5+3.5 core)
|
|||
|
|
|||
|
> 故而,group1和group2各分配到2。把group2分到的2CPU,看作总量,再次分析group3和group4。group3和group3尝试使用的总量超过2,所以按照shares比例分配,group3使用1/(1+3) \* 2 = 0.5,group4使用3/(1+3) \* 2 = 1.5
|
|||
|
|
|||
|
[第6讲](https://time.geekbang.org/column/article/313255)
|
|||
|
|
|||
|
Q:写一个小程序,在容器中执行,它可以显示当前容器中所有进程总的CPU使用率。
|
|||
|
|
|||
|
A:上邪忘川的回答可以作为一个参考。
|
|||
|
|
|||
|
```shell
|
|||
|
#!/bin/bash
|
|||
|
cpuinfo1=$(cat /sys/fs/cgroup/cpu,cpuacct/cpuacct.stat)
|
|||
|
utime1=$(echo $cpuinfo1|awk '{print $2}')
|
|||
|
stime1=$(echo $cpuinfo1|awk '{print $4}')
|
|||
|
sleep 1
|
|||
|
cpuinfo2=$(cat /sys/fs/cgroup/cpu,cpuacct/cpuacct.stat)
|
|||
|
utime2=$(echo $cpuinfo2|awk '{print $2}')
|
|||
|
stime2=$(echo $cpuinfo2|awk '{print $4}')
|
|||
|
cpus=$((utime2+stime2-utime1-stime1))
|
|||
|
echo "${cpus}%"
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[第8讲](https://time.geekbang.org/column/article/315468)
|
|||
|
|
|||
|
Q:在我们的例子[脚本](https://github.com/chengyli/training/blob/main/memory/oom/start_container.sh)基础上,你可以修改一下,在容器刚一启动,就在容器对应的Memory Cgroup中禁止OOM,看看接下来会发生什么?
|
|||
|
|
|||
|
A:通过“**memory.oom\_control**”禁止OOM后,在容器中的进程不会发生OOM,但是也无法申请出超过“memory.limit\_in\_bytes”内存。
|
|||
|
|
|||
|
```shell
|
|||
|
# cat start_container.sh
|
|||
|
#!/bin/bash
|
|||
|
docker stop mem_alloc;docker rm mem_alloc
|
|||
|
|
|||
|
docker run -d --name mem_alloc registry/mem_alloc:v1
|
|||
|
|
|||
|
sleep 2
|
|||
|
CONTAINER_ID=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i mem_alloc | awk '{print $1}')
|
|||
|
echo $CONTAINER_ID
|
|||
|
|
|||
|
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/memory/ -name "*$CONTAINER_ID*")
|
|||
|
echo $CGROUP_CONTAINER_PATH
|
|||
|
|
|||
|
echo 536870912 > $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
|||
|
echo 1 > $CGROUP_CONTAINER_PATH/memory.oom_control
|
|||
|
cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[第10讲](https://time.geekbang.org/column/article/317216)
|
|||
|
|
|||
|
Q:在一个有Swap分区的节点上用Docker启动一个容器,对它的Memory Cgroup控制组设置一个内存上限N,并且将memory.swappiness设置为0。这时,如果在容器中启动一个不断读写文件的程序,同时这个程序再申请1/2N的内存,请你判断一下,Swap分区中会有数据写入吗?
|
|||
|
|
|||
|
A:Memory Cgroup参数memory.swappiness起到局部控制的作用,因为已经设置了memory.swappiness参数,全局参数swappiness参数失效,那么容器里就不能使用swap了。
|
|||
|
|
|||
|
[第11讲](https://time.geekbang.org/column/article/318173)
|
|||
|
|
|||
|
Q:在这一讲OverlayFS的[例子](https://github.com/chengyli/training/blob/main/filesystem/overlayfs/test_overlayfs.sh)的基础上,建立2个lowerdir的目录,并且在目录中建立相同文件名的文件,然后一起做一个overlay mount,看看会发生什么?
|
|||
|
|
|||
|
A:这里引用上邪忘川同学的实验结果。
|
|||
|
|
|||
|
实验过程如下,结果是lower1目录中的文件覆盖了lower2中同名的文件, 第一个挂载的目录优先级比较高
|
|||
|
|
|||
|
```shell
|
|||
|
[[root@localhost ~]# cat overlay.sh
|
|||
|
#!/bin/bash
|
|||
|
|
|||
|
umount ./merged
|
|||
|
rm upper lower1 lower2 merged work -r
|
|||
|
|
|||
|
mkdir upper lower1 lower2 merged work
|
|||
|
echo "I'm from lower1!" > lower1/in_lower.txt
|
|||
|
echo "I'm from lower2!" > lower2/in_lower.txt
|
|||
|
echo "I'm from upper!" > upper/in_upper.txt
|
|||
|
# `in_both` is in both directories
|
|||
|
echo "I'm from lower1!" > lower1/in_both.txt
|
|||
|
echo "I'm from lower2!" > lower2/in_both.txt
|
|||
|
echo "I'm from upper!" > upper/in_both.txt
|
|||
|
|
|||
|
sudo mount -t overlay overlay \
|
|||
|
-o lowerdir=./lower1:./lower2,upperdir=./upper,workdir=./work \
|
|||
|
./merged
|
|||
|
[root@localhost ~]# sh overlay.sh
|
|||
|
[root@localhost ~]# cat merged/in_lower.txt
|
|||
|
I'm from lower1!
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[第12讲](https://time.geekbang.org/column/article/318978)
|
|||
|
|
|||
|
Q:在正文知识详解的部分,我们使用"xfs\_quota"给目录打了project ID并且限制了文件写入的数据量。那么在做完限制之后,我们是否能用xfs\_quota命令,查询到被限制目录的project ID和限制的数据量呢?
|
|||
|
|
|||
|
A:xfs\_quota不能直接得到一个目录的quota大小的限制,只可以看到project ID上的quota限制,不过我们可以用[这段程序](https://github.com/chengyli/training/blob/main/filesystem/quota/get_projectid.c)来获得目录对应的project ID。
|
|||
|
|
|||
|
```shell
|
|||
|
# xfs_quota -x -c 'report -h /'
|
|||
|
...
|
|||
|
Project ID Used Soft Hard Warn/Grace
|
|||
|
---------- ---------------------------------
|
|||
|
#0 105.6G 0 0 00 [------]
|
|||
|
#101 0 0 10M 00 [------]
|
|||
|
|
|||
|
# ./get_proj /tmp/xfs_prjquota
|
|||
|
Dir: /tmp/xfs_prjquota projectid is 101
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
[第13讲](https://time.geekbang.org/column/article/320123)
|
|||
|
|
|||
|
Q:这是一道操作题,通过这个操作你可以再理解一下 blkio Cgroup与 Buffered I/O的关系。
|
|||
|
|
|||
|
在Cgroup V1的环境里,我们在blkio Cgroup V1的例子基础上,把fio中“-direct=1”参数去除之后,再运行fio,同时运行iostat查看实际写入磁盘的速率,确认Cgroup V1 blkio无法对Buffered I/O限速。
|
|||
|
|
|||
|
A: 这是通过iostat看到磁盘的写入速率,是可以突破cgroup V1 blkio中的限制值的。
|
|||
|
|
|||
|
[第17讲](https://time.geekbang.org/column/article/324122)
|
|||
|
|
|||
|
Q:在这节课的最后,我提到“由于ipvlan/macvlan网络接口直接挂载在物理网络接口上,对于需要使用iptables规则的容器,比如Kubernetes里使用service的容器,就不能工作了”,请你思考一下这个判断背后的具体原因。
|
|||
|
|
|||
|
A:ipvlan/macvlan工作在网络2层,而iptables工作在网络3层。所以用ipvlan/macvlan为容器提供网络接口,那么基于iptables的service服务就不工作了。
|
|||
|
|
|||
|
[第18讲](https://time.geekbang.org/column/article/324357)
|
|||
|
|
|||
|
Q:在这一讲中,我们提到了Linux内核中的tcp\_force\_fast\_retransmit()函数,那么你可以想想看,这个函数中的tp->recording和内核参数 /proc/sys/net/ipv4/tcp\_reordering是什么关系?它们对数据包的重传会带来什么影响?
|
|||
|
|
|||
|
```shell
|
|||
|
static bool tcp_force_fast_retransmit(struct sock *sk)
|
|||
|
{
|
|||
|
struct tcp_sock *tp = tcp_sk(sk);
|
|||
|
|
|||
|
return after(tcp_highest_sack_seq(tp),
|
|||
|
tp->snd_una + tp->reordering * tp->mss_cache);
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
A: 在TCP链接建立的时候,tp->reordering默认值是从/proc/sys/net/ipv4/tcp\_reordering(默认值为3)获取的。之后根据网络的乱序情况,进行动态调整,最大可以增长到/proc/sys/net/ipv4/tcp\_max\_reordering (默认值为300)的大小。
|
|||
|
|
|||
|
[第20讲](https://time.geekbang.org/column/article/327107)
|
|||
|
|
|||
|
Q:我在这一讲里提到了rootless container,不过对于rootless container的支持,还存在着不少的难点,比如容器网络的配置、Cgroup的配置,你可以去查阅一些资料,看看podman是怎么解决这些问题的。
|
|||
|
|
|||
|
A:可以阅读一下[这篇文档](https://github.com/containers/podman/blob/master/rootless.md)。
|
|||
|
|
|||
|
## 专题加餐
|
|||
|
|
|||
|
[专题03](https://time.geekbang.org/column/article/340142)
|
|||
|
|
|||
|
Q:我们讲ftrace实现机制时,说过内核中的“inline函数”不能被ftrace到,你知道这是为什么吗?那么内核中的“static函数”可以被ftrace追踪到吗?
|
|||
|
|
|||
|
A:inline函数在编译的时候被展开了,所以不能被ftrace到。而static函数需要看情况,如果加了编译优化参数“-finline-functions-called-once”,对于只被调用到一次的static函数也会当成inline函数处理,那么也不能被ftrace追踪到了。
|
|||
|
|
|||
|
[专题04](https://time.geekbang.org/column/article/340934)
|
|||
|
|
|||
|
Q:想想看,当我们用kprobe为一个内核函数注册了probe之后,怎样能看到对应内核函数的第一条指令被替换了呢?
|
|||
|
|
|||
|
A:**首先可以参考莫名同学的答案:**
|
|||
|
|
|||
|
> 关于思考题,想到一个比较笨拙的方法:gdb+qemu调试内核。先进入虚拟机在某个内核函数上注册一个kprobe,然后gdb远程调试内核,查看该内核函数的汇编指令(disass)是否被替换。应该有更简单的方法,这方面了解不深。
|
|||
|
|
|||
|
另外,我们用gdb远程调试内核看也可以。还可以通过 /proc/kallsyms找到函数的地址,然后写个kernel module把从这个地址开始后面的几个字节dump出来,比较一下probe函数注册前后的值。
|
|||
|
|