aboutsummaryrefslogtreecommitdiffstats
path: root/notes.md
diff options
context:
space:
mode:
authorWe-unite <3205135446@qq.com>2024-08-19 19:41:01 +0800
committerWe-unite <3205135446@qq.com>2024-08-22 14:12:01 +0800
commitf9f8f35ccd8b505a827d40f95c52ed039512b79d (patch)
tree241c393f6b865958191df802cd112f26d40dddc4 /notes.md
parentae4957b41156d576e849ec0424edd4d89d8d49f2 (diff)
downloadgodo-f9f8f35ccd8b505a827d40f95c52ed039512b79d.tar.gz
godo-f9f8f35ccd8b505a827d40f95c52ed039512b79d.zip
Write documents of the program.
Add README.md on the design of the whole program, and how its every part(listener, filter) works, finally how to compile and use them. Besides, notes.md records the things and technology learned in this program, such as how to read kernel src, how the pthread_create/fork/ clone syscall works on processes and threads, the techs used to make docker container works well, and books to be read. Good good study, day day up.
Diffstat (limited to '')
-rw-r--r--notes.md318
1 files changed, 318 insertions, 0 deletions
diff --git a/notes.md b/notes.md
new file mode 100644
index 0000000..932e811
--- /dev/null
+++ b/notes.md
@@ -0,0 +1,318 @@
1# 背景说明
2
3本文档对[godo](https://git.qin-juan-ge-zhu.top/godo)编写过程中新了解到的技术、遇到的问题进行简要说明,以备所需。
4
5# 系统调用
6
7As is universually acknowledged, 操作系统、尤其是类 Unix 操作系统,以系统调用的形式对应用程序提供服务。系统调用是名称,有系统调用号与之对应(同一版本的内核在不同架构的 cpu 上,系统调用号可能不一样)。有的时候我们需要了解一些内核行为,但却不知道从何下手。可以通过查看内核源码来学习。
8
9系统调用可以在源码中查找到。由于本项目使用的是 centos 7,内核版本 3.10.0-1160、cpu 为 x86-64 架构,兹以该版本内核为例说明。
10
11要查看 fork 的系统调用号,查看`arch/x86/syscalls/syscall_64.tbl`。想要查看其具体的实现,则在源码根目录下执行`grep -rInP "SYSCALL_DEFINE\d\(fork"`,其中 SYSCALL_DEFINE+数字是 kernel 中定义的宏,展开即完整的函数声明。通过这种查找办法,我们可以快速地定位内核中对系统调用的处理函数,查看其工作原理。查看其他的内核相关内容也可以采取类似办法,即先用 grep 定位大致范围、看都有什么地方用到,然后找到真正起作用的地方,读相关代码。
12
13使用这些系统调用有两种办法:
14
15- 在 C 语言中直接调用同名函数,但大概率经过了 glibc 的封装
16- 手动封装。如下:
17
18```c
19#include <stdio.h>
20#include <sys/syscall.h>
21#include <sys/types.h>
22#include <sys/wait.h>
23#include <unistd.h>
24
25int main(){
26 pid_t pid = syscall(SYS_fork);
27 // syscall是一个变参函数,第一个参数是系统调用号,接下来的是系统调用的各个参数
28 // syscall定义在 unistd.h
29 // SYS_fork定义在 sys/syscall.h
30 if(pid == 0) {
31 printf("Child!\n");
32 } else {
33 printf("Parent!\n");
34 }
35 return 0;
36}
37```
38
39这种封装方式与经常被用来当作 os 教材的 Linux-0.11/0.12 有所区别。Linux-0.11 环境上,unistd.h 大致如下:
40
41```c
42#ifndef _UNISTD_H
43#define _UNISTD_H
44
45...
46#include <sys/stat.h>
47#include <sys/times.h>
48#include <sys/utsname.h>
49#include <utime.h>
50
51#ifdef __LIBRARY__
52
53#define __NR_setup 0 /* used only by init, to get system going */
54#define __NR_exit 1
55#define __NR_fork 2
56#define __NR_read 3
57#define __NR_write 4
58#define __NR_open 5
59#define __NR_close 6
60...
61
62#define _syscall0(type,name) \
63 type name(void) \
64{ \
65long __res; \
66__asm__ volatile ("int $0x80" \
67 : "=a" (__res) \
68 : "0" (__NR_##name)); \
69if (__res >= 0) \
70 return (type) __res; \
71errno = -__res; \
72return -1; \
73}
74
75#define _syscall1(type,name,atype,a) \
76type name(atype a) \
77{ \
78long __res; \
79__asm__ volatile ("int $0x80" \
80 : "=a" (__res) \
81 : "0" (__NR_##name),"b" ((long)(a))); \
82if (__res >= 0) \
83 return (type) __res; \
84errno = -__res; \
85return -1; \
86}
87
88...
89#endif /* __LIBRARY__ */
90...
91
92#endif
93```
94
95可以看到,Linux-0.11 上,封装的一般方法为:
96
97```c
98#define __LIBRARY__ // 一定要在unistd.h之前
99#include <unistd.h>
100#include <stdio.h>
101
102syscall0(int, fork); // 宏替换后这就是个名为fork的函数的具体实现了
103int main() {
104 if(fork() == 0) {
105 printf("Child!\n");
106 } else {
107 printf("Parent!\n");
108 }
109 return 0;
110}
111```
112
113但是无论如何,一般情况下不推荐手动封装,这不是 release 版该有的做法。
114
115此外,从汇编代码来看,Linux-0.11 所用的 80386 芯片,不提供专门的系统调用指令,因而该系统使用的是`int 0x80`中断指令,通过注册中断处理函数进行对应处理;而现代 x86 提供了专门的 syscall 指令,Linux 系统直接用该指令进行系统调用。
116
117## 系统调用中的进程与线程
118
119一般地,在 Linux 系统上,我们以 pid 指代进程号,而进程可以有多个线程。很显然,真正被调度执行的单元应该是线程,换言之,**是 thread 而非 process 真正地对应着内核中 tasks 表里的一个 task,而每个 task 才具有独一无二的 id**。
120
121### 常见系统调用的分析
122
123看看这个:
124
125```c
126extern int pthread_create (pthread_t *__restrict __newthread,
127 const pthread_attr_t *__restrict __attr,
128 void *(*__start_routine) (void *),
129 void *__restrict __arg) __THROWNL __nonnull ((1, 3));
130```
131
132`pthread_create`函数的第一个参数,就是一个 pthread_t 类型的指针,处理后将 task 的 id 写到指针指向的区域。
133
134让我们来看一段简单的代码:
135
136```c
137// test.c
138#include <stdio.h>
139#include <pthread.h>
140#include <sys/syscall.h>
141#include <sys/types.h>
142#include <unistd.h>
143
144void *test(void *args) {
145 printf("Hello, I'm %d\n", getpid());
146}
147
148int main() {
149 pthread_t pthid;
150 int pid;
151 pthread_create(&pthid, NULL, test, NULL);
152 printf("main: thread %ld\n", pthid);
153 pthread_join(pthid, NULL);
154 if ((pid = fork()) == 0) {
155 printf("Hello, I'm %d\n", getpid());
156 return 0;
157 }
158 printf("main: child process %d\n",pid);
159 if ((pid = syscall(SYS_fork)) == 0) {
160 printf("Hello, I'm %d\n", getpid());
161 return 0;
162 }
163 printf("main: child process %d\n",pid);
164 return 0;
165}
166```
167
168当我们使用`strace ./test`来查看上述代码时,会发现情况如下:
169
170```c
171clone(child_stack=0x7f3dd28bbff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f3dd28bc9d0, tls=0x7f3dd28bc700, child_tidptr=0x7f3dd28bc9d0) = 21756
172write(1, "main: thread 139903502108416\n", 29) = 29
173clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3dd308e9d0) = 21757
174--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21757, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
175write(1, "main: child process 21757\n", 26) = 26
176fork() = 21758
177--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21758, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
178write(1, "main: child process 21758\n", 26) = 26
179exit_group(0) = ?
180+++ exited with 0 +++
181```
182
183从这样的输出里,我们可以清晰地看到,无论是`pthread_create`还是`fork`(指库函数),本质上都是封装了`clone`系统调用,即使 Linux 本身提供了专门的 fork 系统调用。也许这是 glibc 和 Linux 都想在添加功能的基础上保证代码兼容性?花开两朵各表一枝了属于是。
184
185这一结论也可以从 glibc 的代码中得到验证:
186
187```c
188// 文件 glibc-2.18/nptl/sysdeps/unix/sysv/linux/pt-fork.c
189pid_t
190__fork (void)
191{
192 return __libc_fork ();
193}
194strong_alias (__fork, fork)
195
196
197// 文件 glibc-2.18/nptl/sysdeps/unix/sysv/linux/fork.c
198pid_t
199__libc_fork (void)
200{
201 ... // 一堆不知所云的代码
202#ifdef ARCH_FORK
203 pid = ARCH_FORK ();
204#else
205# error "ARCH_FORK must be defined so that the CLONE_SETTID flag is used"
206 pid = INLINE_SYSCALL (fork, 0);
207#endif
208 ... // 又是一堆不知所云的代码
209}
210
211// 文件 glibc-2.18/nptl/sysdeps/unix/sysv/linux/x86_64/fork.c
212#define ARCH_FORK() \
213 INLINE_SYSCALL (clone, 4, \
214 CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, 0, \
215 NULL, &THREAD_SELF->tid)
216
217// 文件 glibc-2.18/sysdeps/unix/sysv/linux/x86_64/syscall.S
218
219/* Please consult the file sysdeps/unix/sysv/linux/x86-64/sysdep.h for
220 more information about the value -4095 used below. */
221 .text
222ENTRY (syscall)
223 movq %rdi, %rax /* Syscall number -> rax. */
224 movq %rsi, %rdi /* shift arg1 - arg5. */
225 movq %rdx, %rsi
226 movq %rcx, %rdx
227 movq %r8, %r10
228 movq %r9, %r8
229 movq 8(%rsp),%r9 /* arg6 is on the stack. */
230 syscall /* Do the system call. */
231 cmpq $-4095, %rax /* Check %rax for error. */
232 jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
233 ret /* Return to caller. */
234
235PSEUDO_END (syscall)
236```
237
238可以看到,fork 库函数实际上是掉入了`__libc_fork`,在经过各种处理之后,如果 glibc 中该平台的相关代码里定义了 ARCH_FORK 宏,则调用之;否则会直接调用`INLINE_SYSCALL`(这是 glibc 各个平台的代码里都有的宏);而如果直接调用`syscall`函数手动封装系统调用,则调用什么就是什么。`syscall`函数调用过程涉及延迟绑定等问题,就不是这里的重点了,而且我也没太搞明白,有机会单开一篇吧。
239
240### 进程与线程
241
242对于一个进程而言,它有很多线程,每个线程有一个号,但整个进程都有主线程的号,称为 tgid,只有一个 tgid 能真正地代表一个进程,而 pid 事实上是 task 的编号。
243
244对于 netlink connector 而言,它听到的 fork 并不是 fork,而是 clone;对于 audit,也只能听到 clone 而听不到 fork。这是因为在内核中,fork 也是通过调用 clone 的处理函数来进行的。clone 创建的是一个 task,至于具体是进程还是线程,取决于用的 flag 参数,参见 manual 手册。
245
246因而,无论使用 connector 还是 audit,拿到的都是 pid,只不过 connector 可以直接拿到 tgid、据此确定是进程还是线程,而 audit 只能拿到 pid,需要从 clone 的参数里查看是进程还是线程,且拿不到 tgid。这也就是我在项目中选择使用 connector 听进程消息的原因。
247
248干巴巴说了这么多,其实就是想说,pid 也许在不同的语境下有不同含义。
249
250# docker 使用的技术
251
252## cgroup
253
254Linux 下用来控制进程资源的东西。没学明白,留缺。姑且抄点书上的内容来占个位置吧。
255
256cgroup 是 control group 的简写,是 Linux 内核提供的一个特性,用于限制和隔离一组进程对系统资源的使用,也即做进程的 QoS。控制的资源主要包括 cpu、内存、block IO、网络带宽等。该特性自 2.6.24 开始进入内核主线,目前各大发行版都默认打开了该特性。
257
258从实现角度,cgroup 实现了一个通用的进程分组框架,而不同类型资源的具体管理由各个 cgroup 子系统实现。截至内核 4.1,已经实现的子系统及其作用如下:
259
260| 子系统 | 作用 |
261| ---------- | ----------------------------------------- |
262| devices | 设备权限控制 |
263| cpuset | 分配指定的 cpu 和内存节点 |
264| cpu | 控制 cpu 占用率 |
265| cpuacct | 统计 cpu 使用情况 |
266| memory | 限制内存使用上限 |
267| freezer | 冻结暂停 cgroup 中的进程 |
268| net_cls | 配合 tc(traffic controller)限制网络带宽 |
269| net_prio | 设置进程网络流量优先级 |
270| huge_tlb | 限制 huge_tlb 的使用 |
271| perf_event | 允许 Perf 工具基于 cgroup 分组做性能监测 |
272
273cgroup 原生接口通过 cgroupfs 提供,类似于 procfs 和 sysfs,是一种虚拟文件系统。具体使用与分析参见《Docker 进阶与实战》。
274
275## namespace
276
277namespace 是将内核的全局资源做封装,使每个 namespace 拥有独立的资源,进程在各自的 namespace 中对相同资源的使用不会互相干扰。比如主机名 hostname 作为全局资源,执行 sethostname 系统调用会影响到其他进程;内核通过实现 UTS namespace,将不同进程分割在不同的 namespace 中,实现了隔离,一个 namespace 修改主机名不影响别的 namespace。
278
279目前内核实现了以下几种 namespace:
280
281| namespace | 作用 |
282| --------- | ----------------------------------- |
283| IPC | 隔离 System V IPC 和 POSIX 消息队列 |
284| Network | 隔离网络资源 |
285| Mount | 隔离文件系统挂载点 |
286| PID | 隔离进程 ID |
287| UTS | 隔离主机名和域名 |
288| User | 隔离用户 ID 与组 ID |
289
290对 namespace 的操作主要通过`clone/setns/unshare`三个系统调用来实现。详细的使用也不写了,没用过的东西就不全抄。记得读书和自己实验,补到这里。
291
292### 文件系统
293
294众所周知,docker 的文件系统是分层的,有镜像文件等一堆东西。文件系统分为若干层,在开启 docker 的时候会被联合挂载到同一个点下,作为 docker 的根目录。这叫做联合挂载,即将多个目录和文件系统挂载到同一个目录下,其中可能有覆盖等。
295
296docker 进程运行在宿主机的内核上,但是根文件系统又要用 docker 自己挂载的目录,且后来的进程也需要进入该目录。这里采用的技术是 pivot_root,该系统调用允许进程切换根目录。
297
298在根目录挂载完成之后,docker 拉起一个初始 shell(正如 Linux-0.11 启动的时候也会有一个 shell 干活),这是 docker 中第一个进程,它调用 pivot_root 切换根目录。在切换完成之后,当我们执行 docker exec 时,这是一个 docker 的新的进程,但该进程不再 pivot_root,而是打开第一个进程的 namespace,通过 setns 系统调用,将自己的 namespace 设置为与其相同。由于 mnt 的 namespace 的存在,进程的根目录也就与第一个进程一样了。
299
300# 书籍列表
301
302**毕业之前读完这些属实是有点难为人了,一个比一个硬,一次性啃完能给我门牙崩了;但是定点投放耗材市场之后,估计也不会有啥精力琢磨这些玩意了**。能读一点是一点吧。
303
304感觉自己现在已经染上班味了,绝症,没得治。
305
306- SRE:Google 运维解密
307- Linker and Loader
308- 有空自己解析一下 ELF?
309- Docker 进阶与实战
310- containerd 原理剖析与实战
311- Linux 内核源码情景分析
312- [LFS](https://www.linuxfromscratch.org/lfs/) 网站,自己从软件包开始搭建 Linux
313- 构建嵌入式 Linux 系统
314
315也许我应该把它们列入进阶版:
316
317- gcc 技术大全
318- 黑客调试技术大全