├── 00507E52.png
├── 02295DAE.png
├── Linux内核.md
├── Linux内核设计与实现.md
├── Linux性能调优.md
├── Linux系统编程.md
├── README.md
├── image-20200531102202520.png
├── image-20200531102311595.png
├── image-20200531110908683.png
├── image-20200531123204686.png
├── image-20200531123538872.png
├── image-20200531162345047.png
├── image-20200531162554050.png
├── image-20200531163039995.png
├── image-20200531163630230.png
├── image-20200531164328810.png
├── image-20200604082129145.png
├── image-20200604082802402.png
├── image-20200604084503749.png
├── image-20200604090111563.png
├── image-20200615090347978.png
├── image-20200618083457485.png
├── image-20200618084630339.png
├── image-20200619090453579.png
├── image-20200619103702377.png
├── image-20200619104102379.png
├── image-20200619104457849.png
├── image-20200619111240263.png
├── image-20200619111413066.png
├── image-20200619112545758.png
├── image-20200619113142088.png
├── image-20200619114904496.png
├── image-20200619135038953.png
├── image-20200619135129369.png
├── image-20200619135204831.png
├── image-20200619140433905.png
├── image-20200619142443132.png
├── image-20200619144135833.png
├── image-20200619145306992.png
├── image-20200619145720447.png
├── image-20200619145815123.png
├── image-20200622083824843.png
├── image-20200622085138977.png
├── image-20200622090154215.png
├── image-20200622090919581.png
├── image-20200622091237024.png
├── image-20200623100711987.png
├── image-20200623102542286.png
├── image-20200623191143202.png
├── image-20200623191624065.png
├── image-20200623193750366.png
├── image-20200623194904111.png
├── image-20200703193455046.png
├── image-20200705165658500.png
├── image-20200705170712936.png
├── image-20200705170755922.png
├── image-20200705171053845.png
├── image-20200707091040150.png
├── image-20200707091125308.png
├── image-20200707091245724.png
├── image-20200707091319282.png
├── image-20200707091505999.png
├── image-20200707092746157.png
├── image-20200707093319597.png
├── image-20200707093832701.png
├── image-20200707094443166.png
├── image-20200707095202477.png
├── image-20200707095225496.png
├── image-20200713152306980.png
├── image-20200713153253447.png
├── image-20200713153304300.png
├── image-20200713153340086.png
├── image-20200713153412875.png
├── image-20200713153434116.png
├── image-20200713153535101.png
├── image-20200713154924924.png
├── image-20200713160114414.png
├── image-20200713160800427.png
├── image-20200713164051426.png
├── image-20200713165518219.png
├── image-20200713174143283.png
├── image-20200713185213374.png
├── image-20200713192813123.png
├── image-20200720194433868.png
├── image-20200720195417684.png
├── image-20200720195445895.png
├── image-20200720200359909.png
├── image-20200721082112889.png
├── image-20200721083726563.png
├── image-20200721084613884.png
├── image-20200721085534908.png
├── image-20200721090550123.png
├── image-20200727084714720.png
├── image-20200727085803749.png
├── image-20200727090201863.png
├── image-20200729082148633.png
├── image-20200729083424801.png
├── image-20200729083533669.png
├── image-20200729083648597.png
├── image-20200729084944972.png
├── image-20200730083703350.png
├── image-20200730085636637.png
├── image-20200730085816908.png
├── image-20200730090512169.png
├── image-20200730091212221.png
├── image-20200731081843643.png
├── image-20200731082550667.png
├── image-20200731090358996.png
├── image-20200731091121429.png
├── image-20200731091142602.png
├── image-20200802092331057.png
├── image-20200802092411500.png
├── image-20200802092849165.png
├── image-20200802093257426.png
├── image-20200802093500875.png
├── image-20200802095755049.png
├── image-20200802100106115.png
├── image-20200802101629272.png
├── image-20200802103414354.png
├── image-20200802103603545.png
├── image-20200802104015706.png
├── image-20200802104445502.png
├── image-20200802111837958.png
├── image-20200802111902259.png
├── image-20200802112524577.png
├── image-20200802112647741.png
├── image-20200802112815989.png
├── image-20200802113302644.png
├── image-20200802113325385.png
├── image-20200802113445670.png
├── image-20200802113750230.png
├── image-20200802114225988.png
├── image-20200802114732166.png
├── image-20200802120202420.png
├── image-20200803084218092.png
├── image-20200803084315826.png
├── image-20200803084515122.png
├── image-20200803085509088.png
├── image-20200803085745108.png
├── image-20200803085807606.png
├── image-20200803090535044.png
├── image-20200803101934382.png
├── image-20200803125209144.png
└── image-20200803125215783.png
/00507E52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/00507E52.png
--------------------------------------------------------------------------------
/02295DAE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/02295DAE.png
--------------------------------------------------------------------------------
/Linux内核.md:
--------------------------------------------------------------------------------
1 | [TOC]
2 |
3 |
4 | # Linux内核
5 |
6 | ## 3 进程管理
7 |
8 | ### 3.1 进程
9 |
10 | > 进程:处于执行期的程序以及相关资源(打开的文件、挂起的信号、内核内部数据、处理器状态等)的总称
11 | >
12 | > 线程:是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,不是进程
13 | >
14 | > Linux不区分进程和线程,对它来说,线程只不过是一种特殊的进程而已
15 |
16 | 现代操作系统的两种**虚拟机制**:
17 |
18 | * 虚拟处理器:给进程一种假象,让它觉得自己在独享处理器
19 | * 虚拟内存:让进程在分配和管理内存时觉得自己拥有整个系统的内存资源
20 |
21 | **注意**:线程之间可以共享虚拟内存,但是都拥有自己的虚拟处理器
22 |
23 | ### 3.2 进程描述
24 |
25 | 内核把进程的列表存放在叫做**任务队列**的双向循环链表中。链表中的每一项类型为`task_struct`,称为**进程描述符**的结构,描述了一个具体进程的所有信息
26 |
27 | #### 3.2.1 分配进程描述符
28 |
29 | Linux通过slab分配器分配`task_struct`结构,这样能够**对象复用**和**缓存着色**。
30 |
31 | 每个任务的`thread_info`结构在它的内核栈尾端分配,其中`task`域存放的是指向该任务实际`task_struct`的指针
32 |
33 | ```c
34 | struct thread_info {
35 | struct task_struct *task;
36 | struct exec_domain *exec_domain;
37 |
38 | ...
39 | };
40 | ```
41 |
42 |
43 |
44 | #### 3.2.2 进程描述符的存放
45 |
46 | 在内核中,访问任务需要获取指向`task_struct`结构的指针,通过`current`宏查找到当前进程的进程描述符,这个查找的**速度**很重要
47 |
48 | 有的硬件体系结构拿出一个专门的寄存器存放当前进程的`task_struct`指针,而有些像x86的体系结构(寄存器不太富余),就只能在**内核栈的尾部**创建`thread_info`结构,通过计算偏移量间接找到`task_struct`结构
49 |
50 | #### 3.2.3 进程状态
51 |
52 | 进程描述符中的`state`域描述了进程的当前状态。系统中进程的状态包括:
53 |
54 | * **TASK_RUNNING**(运行或就绪):进程是可执行的
55 | * **TASK_INTERRUPTIBLE**(可中断睡眠)
56 | * **TASK_UNINTERRUPTIBLE**(不可中断睡眠)
57 | * **__TASK_TRACED**:被其他进程跟踪的进程,例如ptrace调试的程序
58 | * **__TASK_STOPPED**:被暂停执行的任务,通常在接收到**SIGSTOP**,**SIGTSTP**,**SIGTTIN**,**SIGTTOU**等信号时
59 |
60 |
61 |
62 | #### 3.2.4 设置当前状态
63 |
64 | 使用`set_task_state(task,state)`函数
65 |
66 | #### 3.2.6 进程家族树
67 |
68 | 所有的进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本(initsctript)并执行其他的相关程序,最终完成系统启动的整个过程
69 |
70 | 每个`task_struct`结构都包含一个指向其父进程`task_struct`结构的`parent`指针,还包含一个`children`的子进程链表
71 |
72 | ### 3.3 进程创建
73 |
74 | Unix将进程的创建分解到两个单独的函数中去执行:fork()和exec()
75 |
76 | 首先,fork()通过拷贝当前进程创建一个子进程,exec()负责读取可执行文件并将其载入地址空间开始运行
77 |
78 | #### 3.3.1 写时拷贝
79 |
80 | Linux的fork()使用**写时拷贝**页实现,是一种推迟甚至免除拷贝数据的技术,在创建子进程时,内核并不复制整个进程地址空间,而是让父子进程共享一个拷贝,只有在写入的时候,数据才会被复制。在页根本不会被写入的情况下,例如fork()之后马上exec(),进程的地址空间就不用复制了
81 |
82 | fork()的实际开销:复制父进程的页表以及给子进程创建唯一的进程描述符
83 |
84 | #### 3.3.2 fork()
85 |
86 | Linux通过clone()实现fork(),clone()通过一系列参数指明父子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone(),然后在clone()中调用do_fork()
87 |
88 | do_fork()调用copy_process()函数,然后让进程运行。copy_process()函数的过程:
89 |
90 | * 调用`dup_task_struct()`为进程创建一个内核栈、thread_info结构和task_struct,与父进程的值相同,此时父子进程的描述符是相同的
91 | * 检查创建子进程后,当前用户拥有的进程数不超过分配资源限制
92 | * 子进程开始将自己与父进程区分开:进程描述符内的很多成员清0或者初始化,大部分的数据未被修改
93 | * 子进程状态设置为UNINTERRUPTIBAL,保证它不会投入运行
94 | * copy_process()调用copy_flags()更新task_struct的flags成员:其中代表进程是否拥有超级用户权限的PF_SUPERPRIV标志清0,代表进程还没有调用exec()函数的PF_FORKNOEXEC的标志被设置
95 | * 调用alloc_pid()为子进程分配PID
96 | * 根据clone()传递进来的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。(一般这些资源会被进程的**所有线程共享**)
97 | * 最后,copy_process()做扫尾工作并返回一个指向子进程的指针
98 |
99 | copy_process()返回到do_fork()函数,如果copy_process()返回成功,新创建的子进程被唤醒并投入运行
100 |
101 | **注意**:内核有意选择子进程首先执行,因为一般子进程会调用exec()函数,这样可以避免写时拷贝的额外开销
102 |
103 | #### 3.3.3 vfork()
104 |
105 | 除了**不拷贝父进程的页表项**,vfork()和fork()的功能相同
106 |
107 | ### 3.4 线程在Linux中的实现
108 |
109 | Linux实现线程的机制非常独特,从内核的角度来说,并没有线程这个概念,Linux把所有的**线程当做进程**来实现。线程仅仅被视为一个与其他进程共享某些资源的进程,拥有唯一隶属于自己的`task_struct`。
110 |
111 | Windows和Sun Solaris等系统都提供了专门支持线程的机制(将线程称为**轻量级进程**),相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于Linux,线程只是**进程间共享资源的一种手段**。
112 |
113 | 举例说明:对于一个包含四个线程的进程,在提供专门线程支持的系统,通常会有一个包含指向四个不同线程的指针的进程描述符,该描述符负责描述像地址空间、打开的文件等共享资源。而Linux只是创建四个进程并分配四个普通的`task_struct`结构,并指定它们共享某些资源。
114 |
115 | #### 3.4.1 创建线程
116 |
117 | 线程的创建和普通进程类似,只是需要在调用clone()时传递一些参数标志来指明共享的资源:
118 |
119 | ```c
120 | clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
121 | ```
122 |
123 | 调用的结果和fork()差不多,只是父子进程**共享地址空间、文件系统资源、打开的文件描述符和信号处理程序**
124 |
125 | 传递给clone()的参数标志决定了**新创建进程的行为方式和父子进程之间共享的资源种类**,详见**P29 表3-1**
126 |
127 | #### 3.4.2 内核线程
128 |
129 | > 内核线程用于内核在后台执行一些任务,他们是独立运行在内核空间的标准进程
130 | >
131 | > 内核线程和普通进程的区别是:**内核线程没有独立的地址空间**(指向地址空间的mm指针为NULL),它们只在内核空间运行,不切换到用户空间。
132 |
133 | 例如软中断ksoftirqd和flush都是内核线程的例子
134 |
135 | 内核是通过从kthreadd内核进程衍生出所有新的内核线程,从现有内核线程创建一个新的内核线程的方法如下:
136 |
137 | ```c
138 | struct task_struct *thread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
139 | ```
140 |
141 | 新的内核线程是由kthreadd进程通过clone()系统调用创建,它们将运行threadfn函数,传递的参数是data,进程命名为namefmt。
142 |
143 | 新创建的进程处于不可运行状态,需要通过wake_up_process()唤醒来运行
144 |
145 | ```c
146 | struct task_struct *thread_run(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
147 | ```
148 |
149 | `thread_run`方法先调用`thread_run`方法,然后调用`wake_up_process()`
150 |
151 | 内核线程启动后一直运行直到调用`do_exit()`退出,或者内核的其他部分调用`kthread_stop()`退出(传递给kthread_stop()的参数是kthread_create()返回的task_struct结构地址)
152 |
153 | ```c
154 | int kthread_stop(struct task_struct *k)
155 | ```
156 |
157 | ### 3.5 进程终结
158 |
159 | 进程终结的几种情况:
160 |
161 | * 显式的调用`exit()`
162 | * 隐式地在某个程序的主函数返回(C语言在main()函数的返回点防止调用exit()的代码)
163 | * 接收到不能处理也不能忽略的信号或者异常时,被动地终结
164 |
165 | 不管进程如何终结,大部分都是靠`do_exit()`(定义在kernel/exit.c)来完成,它的主要工作包括:
166 |
167 | * 将task_struct的标志成员设置为PF_EXITING
168 | * 调用`del_timer_sync()`删除任一内核定时器,确保没有定时器在排队,且没有定时器处理程序在运行
169 | * 如果BSD记账功能开启,调用`acct_update_intergrals()`来输出记账信息
170 | * 调用`exit_mm()`函数释放进程占用的mm_struct,如果没有别的进程在使用它(没有被共享),就彻底释放它们
171 | * 调用`sem__exit()`函数,如果进程排队等候IPC信号,则它离开队列
172 | * 调用`exit_files()`和`exit_fs()`来分别递减文件描述符和文件系统数据的引用计数,如果引用计数降为0,就可以直接释放
173 | * 接着将存放在task_struct的exit_code成员中的任务退出代码置为`exit()`提供的退出代码。**退出代码存放在这里供父进程检索**
174 | * 调用`exit_notify()`向父进程发送信号,给自己的子进程重新找**养父**(为进程组的其他线程或者init进程),并把进程的状态(task_struct中的exit_state)设置为EXIT_ZOMBIE
175 | * 调用`schedule()`切换到新的进程,因为EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程执行的最后一段代码。`do_exit()`永不返回
176 |
177 | 至此,与进程相关联的所有资源都释放掉了(假设该进程是这些资源的唯一使用者),进程不可运行且处于EXIT_ZOMBIE状态,他占用的所有内存包括**内核栈、thread_info结构和task_struct结构**。此时存在的唯一目的就是向它的父进程提供信息用于检索,父进程通知内核都是无关的信息后,进程所持有的剩余内存被释放,归还给系统使用
178 |
179 | #### 3.5.1 删除进程描述符
180 |
181 | 在调用`do_exit()`后,进程处于僵死状态不再运行,但是系统保留了它的进程描述符,这样可以**让系统能在进程中杰后仍能获取它的信息**。可以看到,**进程终结时所做的清理工作和进程描述符的删除是分开执行的**,在父进程获得已终结的子进程信息后,通知内核它不关注这些信息后,子进程的task_struct结构被释放
182 |
183 | 回收子进程状态是通过wait()一族函数实现,他们都是通过唯一的系统调用`wait4()`来实现,它首先会挂起调用它的进程,直到有一个子进程退出,此时函数返回子进程的PID,调用**该函数提供的指针指向子进程的退出代码**
184 |
185 | 当需要释放进程描述符时,会调用`release_task()`函数,它的工作包括:
186 |
187 | * 调用`__exit_signal()`,该函数调用`_unhash_process()`,后者再调用`detach_pid()`从pidhash上删除该进程,同时从任务列表中删除该进程
188 | * `__exit_signal()`函数释放目前僵死进程所使用的所有剩余资源,进行最终统计和记录
189 | * 如果这个进程是进程组的最后一个进程,且领头进程(进程组首进程)已经死掉,那么`release_task()`就通知僵死的领头进程的父进程
190 | * `release_task()`调用`put_task_struct()`释放进程内核栈和thread_info结构所占的内存页,并释放task_struct占用的slab高速缓存
191 |
192 | 至此,进程描述符和所有进程独享的资源全部释放
193 |
194 | #### 3.5.2 孤儿进程造成的进退维谷
195 |
196 | 当父进程在子进程之前推出时,需要保证子进程找到一个新的父进程,否则这些孤儿进程就会在退出时一直处于僵死状态。
197 |
198 | 解决方法是:给子进程在当前进程组找一个进程作为父亲,如果不行,就让init进程作为父进程
199 |
200 | 在`do_exit()`中会调用`exit_nodify()`,该函数调用`forget_original_parent()`,后者再调用`find_new_reaper()`进程寻父过程。
201 |
202 | 代码中会遍历两个链表:**子进程链表和ptrace子进程链表**,给每个子进程设置新的父进程。
203 |
204 | **注意**:当一个进程被跟踪时,它的临时父亲被设置为调试进程,如果他们真正的父进程退出,系统会为它及其兄弟进程找一个父进程。以前的内核版本中需要遍历系统所有的进程来找到这些子进程,现在只需要遍历这个单独ptrace的子进程链表,减轻了遍历的时间消耗
205 |
206 | ## 4 进程调度
207 |
208 | > 进程调度程序:在可运行态进程之间分配有限处理器时间资源的**内核子系统**。
209 |
210 | ### 4.1 多任务
211 |
212 | > 多任务操作系统是同时并发地交互执行多个进程的操作系统,能使多个进程处于阻塞或者睡眠状态,这些任务位于内存中,但是并不处于可运行状态,他们利用内核阻塞自己,直到某一时间(键盘输入、网络数据等)发生。
213 |
214 | 多任务系统分为两类:
215 |
216 | * 非抢占式多任务
217 | * 抢占式多任务
218 |
219 | Linux提供了抢占式的多任务模式,由调度程序决定什么时候停止一个进程的运行,以便其他进程得到运行机会,这个强制的挂起动作叫做抢占。
220 |
221 | 时间片:可运行进程在被抢占之前预先设置好的处理器时间段。
222 |
223 | 非抢占任务模式下,除非进程自己主动停止运行,否则他会一直运行。进程主动挂起自己的操作称为**让步**(yielding)
224 |
225 | 非抢占任务模式的缺点:调度程序无法对每个进程该执行多长时间做出统一规定,进程独占的CPU时间可能超出预期,另外,一个绝不做出让步的悬挂进程就能使系统崩溃
226 |
227 | ### 4.2 Linux的进程调度
228 |
229 | 2.6内核系统开发初期,为了提供对交互程序的调度性能,引入新的调度算法,最为著名的是**反转电梯最后期限调度算法**(RSDL),在2.6.3版本替代了**O(1)调度算法**,最后被称为**完全公平调度算法(CFS)**
230 |
231 | ### 4.3 策略
232 |
233 | #### 4.3.1 I/O消耗型和CPU消耗型的进程
234 |
235 | CPU消耗型进程把时间大多用在了执行代码上,不属于I/O驱动类型,从系统响应速度考虑,调度策略往往是降低它们的调度频率,而延长其运行时间
236 |
237 | 调度策略的主要矛盾是:**进程响应迅速和最大系统利用率(高吞吐量)**
238 |
239 | Unix系统的调度程序更倾向于I/O消耗型程序,以提供更好的响应速度。Linux为了保证交互式应用和桌面系统的性能,对进程的响应做了优化(缩短响应时间),更倾向于调度I/O消耗型进程。
240 |
241 | #### 4.3.2 进程优先级
242 |
243 | 调度程序总是选择时间片为用尽而且优先级最高的进程运行
244 |
245 | Linux采用了两种不同的优先级范围:
246 |
247 | * 第一种用nice值,范围-20~+19,默认值0;越大的nice值优先级越低。相比高nice值(低优先级)的进程,低nice值(高优先级)的进程可以获得更多的处理器时间
248 | * 第二种是实时优先级,数值可配置,默认范围是0~99,数值越大优先级越高。任何实时进程的优先级都比普通进程高,实时优先级和nice优先级处于互不相交的范畴
249 |
250 | #### 4.3.3 时间片
251 |
252 | 调度策略选择合适的时间片并不简单,时间片太短会增加进程切换的处理器消耗,太长会导致系统的交互响应变差
253 |
254 | Linux的CFS调度器没有直接分配时间片到进程,它是将**处理器的使用比**划分给进程,所以进程所获得的时间片时间是和**系统负载(系统活跃的进程数)**密切相关的
255 |
256 | Linux中新的CFS调度器,它的进程抢占时机取决于**新的可运行程序消耗了多少处理器使用比**。如果消耗的处理器使用比比当前进程小,则新进程投入运行(当前进程被强占),否则,推迟运行。
257 |
258 | **总而言之,CFS会先根据进程的nice值预期设定每个进程的cpu使用比,而在进程调度时,需要将新的被唤醒进程实际消耗的cpu使用比和当前进程比较,如果更小,则抢占当前进程,投入运行,否则,推迟运行**
259 |
260 | ### 4.4 Linux调度算法
261 |
262 | #### 4.4.1 调度器类
263 |
264 | Linux调度器以模块提供,允许不同类型的进程针对性地选择调度算法,这种模块化结构成为**调度器类**,它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
265 |
266 | 完全公平调度(CFS)是一个针对普通进程的调度类,称为**SCHED_NORMAL**,具体算法实现定义在文件kernel/sched_fair.c中
267 |
268 | #### 4.4.2 Unix系统中的进程调度
269 |
270 | 传统Unix系统调度:进程启动会有默认的时间片,具有高优先级的进程将运行的更频繁,而且被赋予更多的时间片。存在的问题如下:
271 |
272 | * nice映射到时间片,就会将nice单位值对应到处理器的绝对时间,这样将会导致进程切换无法最优化进行,同时会导致进程获得的处理器时间很大程度上取决于其nice初始值。场景实例详见**P40**
273 | * 时间片一般为系统定时器节拍的整数倍,它会随着定时器节拍改变
274 |
275 | CFS采用的方法是:**完全摒弃时间片而是分配给进程一个处理器使用比重**,确保了调度中恒定的公平性,切换频率是在动态变化中
276 |
277 | #### 4.4.3 公平调度
278 |
279 | > 完美的多任务系统:每个进程获得1/*n*的处理器时间(*n*是指可运行进程的数量),同时调度给他们无限小的时间周期(交互性会很好)
280 |
281 | CFS的做法:**允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程**,在所有进程总数基础上计算一个进程应该运行多久,不在依靠nice值计算绝对的时间片,而是作为**进程获得的处理器运行比的权重**,越高的nice值获得更低的处理器使用权重。
282 |
283 | 每个进程按照其权重在全部可运行进程中所占比例的“时间片”来运行,由于越小的调度周期(重新调度所有可运行进程所花的时间)交互性会越好,也就更接近完美的所任务,CFS为调度周期设定一个目标(无限小的调度周期近似值)。
284 |
285 | 当可运行任务数量区域无限大时,他们所获得的处理器使用比和时间片将趋近于0(这会增加CPU的切换消耗)。因此,CFS引入每个进程获得的时间片底线,称为最小粒度。而当进程数非常多时,由于这个最小粒度的存在,调度周期会比较长,因此CFS并非完美的多任务。
286 |
287 | **总之,在CFS中任何进程所获得的的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的,nice值对时间片的作用不再是算数加权,而是几何加权,CFS是近乎完美的多任务**
288 |
289 | ### 4.5 Linux调度的实现
290 |
291 | Linux调度主要关注四个部分:
292 |
293 | * 时间记账
294 | * 进程选择
295 | * 调度器入口
296 | * 睡眠和唤醒
297 |
298 | #### 4.5.1 时间记账
299 |
300 | 1. 调度器实体结构
301 |
302 | CFS不再有时间片的概念,但是它会维护每个进程运行的时间记账,需要确保每个进程在分配给它的处理器时间内运行。CFS使用**调度器实体**(文件中的struct_sched_entity中)来追踪进程运行记账
303 |
304 | ```c
305 | struct sched_entity {
306 | struct load_weight load;
307 | struct rb_node run_node;
308 | struct list_head group_node;
309 | ...
310 | u64 vruntime;
311 | ...
312 | };
313 | ```
314 |
315 | 调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符task_struct内
316 |
317 | 2. 虚拟实时
318 |
319 | vruntime变量存放进程的虚拟运行时间,这个数值的计算是经过所有可运行进程总数的标准化,以ns为单位,与定时器节拍无关
320 |
321 | 定义在kernel/sched_fair.h文件中的update_curr()函数实现记账功能,它是系统定时器周期性调用,无论进程是在可运行态还是阻塞状态
322 |
323 | ```c
324 | static void update_curr(struct cfs_rq *cfs_rq)
325 | {
326 | ...
327 | __update_curr(cfs_rq, curr, delta_exec)
328 | ...
329 | }
330 | ```
331 |
332 | #### 4.5.2 进程选择
333 |
334 | **CFS**算法调度核心:**当CFS需要选择下一个运行进程时,选择具有最小vruntime的进程**
335 |
336 | **CFS**使用红黑树组织可运行进程队列,红黑树的键值为vruntime,检索对应节点的时间复杂度为对数级别
337 |
338 | 1. 挑选下一个任务
339 |
340 | CFS选择进程的算法为:运行rbtree中最左边叶子结点代表的那个进程。实现的函数是`__pick_next_entity()`,定义在kernel/sched_fair.c中
341 |
342 | ```c
343 | static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
344 | {
345 | struct rb_node *left = cfs_rq->rb_leftmost;
346 |
347 | if(!left)
348 | return NULL;
349 |
350 | return rb_entry(left, struct sched_entity, run_node)
351 | }
352 | ```
353 |
354 | **注意**:如果该函数返回值为NULL,说明树中没有任何节点,代表没有可运行进程,CFS调度器选择idle任务运行
355 |
356 | 2. 向树中加入进程
357 |
358 | 当**进程变为可运行状态(被唤醒)或者通过fork()调用第一次创建进程时**,会将进程加入到rbtree。`enqueue_entity()`函数实现了这个过程,代码详见P45
359 |
360 | 3. 从树中删除进程
361 |
362 | 删除动作发生在**进程阻塞(变为不可运行状态)或者终止(结束运行)**,是由函数`dequeue_entity()`函数完成
363 |
364 | #### 4.5.3 调度器入口
365 |
366 | 进程调度的入口函数是`schedule()`,定义在kernel/sched.c文件,**它是内核其他部分调用进程调度器的入口**。
367 |
368 | `schedule()`通常需要和一个调度类相关联,它会先找到一个最高优先级的调度类,后者要有自己的可运行进程队列,然后这个调度类决定下一个可运行的进程。
369 |
370 | 因此,`schedule()`函数的逻辑比较简单,它的主要逻辑就是调用`pick_next_task()`,这个函数会以优先级为序,从高到低一次检查每个调度器类,从最高优先级的调度类中选择下一个运行的进程。详细代码如下图
371 |
372 |
373 |
374 | 每个调度类都实现了`pick_next_task()`函数,它会返回指向下一个可运行进程的指针,在CFS中`pick_next_task()`会调用`pick_next_entity()`,该函数会调用 [4.5.2节](#4.5.2 进程选择) 提到的`__pick_next_entity()`
375 |
376 | **函数优化**:由于CFS是普通进程的调度类,而系统绝大多数进程是普通进程。函数使用了一个小技巧,当所有可运行进程数等于CFS类对应的可运行进程数时,直接返回CFS调度类的下一个运行进程
377 |
378 | #### 4.5.4 睡眠和唤醒
379 |
380 | 睡眠(或阻塞)的进程处于一个特殊的不可运行状态。
381 |
382 | 进程睡眠时,进程把自己标记为休眠状态,从可执行进程对应的红黑树中移出,放入等待队列,然后调用`schedule()`调度下一个进程;唤醒的过程相反:进程被设置成可执行状态,然后从等待队列移到可执行红黑树中
383 |
384 | 1. 等待队列
385 |
386 | 等待队列是由**等待某些事件发生的进程组成的简单链表**,内核用`wake_queue_head_t`代表等待队列
387 |
388 | 进程加入等待队列的详细过程和代码详见**P50**
389 |
390 | 2. 唤醒
391 |
392 | 唤醒操作通过函数`wake_up()`进行,它会唤醒等待队列上的所有进程,它调用函数`try_to_wake_up()`将进程状态设置为**TASK_RUNNING**,调用`enqueue_task()`将此进程放入红黑树,如果被唤醒的进程比当前正在执行的进程优先级高(这里不是指nice值,而是根据CFS调度的cpu使用比规则得出的结果),还要设置进程的need_resched标志。
393 |
394 | **注意**:通常哪段代码促使等待条件达成,它就要负责调用`wake_up()`函数。例如,当磁盘数据到来时,VFS需要负责对等待队列调用`wake_upe()`。
395 |
396 | 如下图是每个调度程序状态之间的关系
397 |
398 |
399 |
400 |
401 | ### 4.6 抢占和上下文切换
402 |
403 | 上下文切换由定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来投入运行的时候,schedule()会调用函数context_switch(),后者完成两项工作:
404 |
405 | * 调用声明在中的switch_mm()函数,它负责将虚拟内存从上一个进程映射切换到新进程中
406 | * 调用声明在的switch_to(),负责从上一个进程的处理器状态切换到新进程的处理器状态,其中包括**保存、恢复栈信息和寄存器信息**
407 |
408 | 内核提供一个`need_resched`标志标明是否需要重新执行一次调度,2.2以前放在全局变量,2.2~2.4在每个进程的进程描述符中(由于current宏速度很快并且进程描述符通常是在高速缓存中,访问`task_struct`内的数值比全局变量更快),而在2.6版本中,它放在thread_info结构体中,用一个特别的标志变量的一位来表示。
409 |
410 | `need_resched`标志被设置的时机:
411 |
412 | * 当某个进程应该被抢占时,scheduler_tick()函数会设置这个标志
413 | * 当一个优先级更高的进程进入可运行状态时,try_to_wake_up()也会设置这个标志
414 |
415 | 然后内核检查该标志,确认被设置后,会调用schedule()切换到一个新进程
416 |
417 | #### 4.6.1 用户抢占
418 |
419 | 内核在中断处理程序或者系统调用返回后,都会检查`need_resched`标志。从中断处理程序或者系统调用返回的返回路径都是跟体系结构相关,在entry.S(包含内核入口和退出的代码)文件通过汇编实现
420 |
421 | 当内核将返回用户空间的时候,如果`need_resched`标志被设置,会导致schedule()调用,会发生用户抢占
422 |
423 | 因此,用户抢占发生在以下情况:
424 |
425 | * 系统调用返回用户空间时
426 | * 中断处理程序返回用户空间时
427 |
428 | #### 4.6.2 内核抢占
429 |
430 | > 在没有内核抢占的系统中,调度程序没有办法在一个内核级的任务正在执行时重新调度,内核中的任务以协作方式调度,不具备抢占性,内核代码一直执行到完成(返回用户空间)或者阻塞为止
431 |
432 | 在2.6版本,Linux内核引入抢占能力,只要重新调度是**安全的**(没有持有锁的情况),内核可以在任何时间抢占正在执行的任务。
433 |
434 | 在每个进程的thread_info结构中加入preempt_count计数,代表进程使用锁的个数。
435 |
436 | * 在中断返回内核空间的时候,会检查need_resched和preempt_count,如果need_resched被设置且preempt_count为0,则可以进行安全的抢占,调度程序schedule()会被调用,否则,中断直接返回当前进程
437 | * 如果进程持有的所有锁被释放,preempt_count会减为0,此时释放锁的代码会检查need_resched标志,如果被设置,则调用schedule()
438 |
439 | 因此,内核抢占发生在:
440 |
441 | * 中断处理程序正在执行,且返回内核空间之前
442 | * 进程在内核空间释放锁的时候
443 | * 内核任务显式的调用schedule()
444 | * 内核中的任务阻塞
445 |
446 | ### 4.7 实时调度策略
447 |
448 | > Linux提供了一种软实时的工作方式
449 | >
450 | > 软实时的定义:内核调度进程尽力使进程在规定时间到来前运行,但是内核不能总是满足这些进程的要求
451 | >
452 | > 硬实时的定义:保证在一定条件下,可以完全满足进程在规定的时间内完成操作
453 |
454 | Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,普通的、非实时的调度策略是SCHED_NORMAL。实时策略不被CFS调度器管理,而是被一个特殊的实时调度器管理
455 |
456 | SCHED_FIFO实现了**简单的、先入先出的调度算法**,它不使用时间片,SCHED_RR和前者大致相同,不同点在于它使用时间片,是一种**实时轮转调度算法**
457 |
458 |
459 |
460 | ## 5 系统调用
461 |
462 | ### 5.1 与内核通信
463 |
464 | 系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要作用是:
465 |
466 | * 为用户空间提供了硬件的抽象接口
467 | * 保证了系统的稳定和安全,可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决
468 |
469 | 系统调用是用户空间访问内核的**唯一手段**,除了**异常和陷入**之外,它是内核唯一的合法入口
470 |
471 | ### 5.2 API、POSIX和C库
472 |
473 | 应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来变成,一个API定义了一组应用程序使用的编程接口,可以实现为一个或多个系统调用,或者完全不使用任何系统调用
474 |
475 | POSIX、API、C库以及系统调用的关系如下图
476 |
477 |
478 |
479 | ### 5.3 系统调用
480 |
481 | #### 5.3.1 系统调用号
482 |
483 | 在Linux中,每个系统调用被赋予了一个系统调用号。
484 |
485 | 系统调用号的特点:
486 |
487 | * 系统调用号一旦分配就不能再有变更,否则编译好的程序有可能崩溃
488 | * 如果系统调用被删除,所占用的系统调用号不允许被回收利用,否则以前编译过的代码会调用这个系统调用,但是却调用的另一个系统调用,Linux使用“未实现”系统调用`sys_ni_syscall()`来填补这种空缺,它除了返回`-ENOSYS`外不做任何工作
489 |
490 | 系统调用表sys_call_table,为每一个有效的系统调用指定了唯一的系统调用号
491 |
492 | #### 5.3.2 系统调用的性能
493 |
494 | Linux系统调用比其他许多操作系统都要快,原因是:
495 |
496 | * Linux很短的上下文切换时间,进出内核被优化的很简洁
497 | * 系统调用处理程序和每个系统调用本身也很简洁
498 |
499 | ### 5.4 系统调用处理程序
500 |
501 | 通知内核的机制通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
502 |
503 | 在x86系统上预定义的软中断是中断号128,通过int $0x80指令触发中断,这条指令触发一个异常导致系统切换到内核态并执行第128号异常处理程序(这个异常处理程序就是系统调用处理程序),它的名字是system_call()
504 |
505 | #### 5.4.1 指定恰当的系统调用
506 |
507 | 因为所有系统调用陷入内核的方式都一样,所以需要把系统调用号传给内核用于区分每种系统调用。
508 |
509 | 在x86上,系统调用号通过eax寄存器传递给内核,在陷入内核之前,用户空间把相应系统调用号放入eax中,system_call()函数将给定的系统调用号与NR_syscalls作比较来检查其有效性,如果大于或等于NR_syscalls,就返回-ENOSYS,否则,执行相应的系统调用
510 |
511 | ```asm
512 | call *sys_call_table(, %rax, 8)
513 | ```
514 |
515 | #### 5.4.2 参数传递
516 |
517 | 系统调用额外的参数也是存放在寄存器传递给内核。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前5个参数,如果超过5个参数,需要用单独的寄存器存放**所有指向这些参数在用户空间地址的指针**
518 |
519 | 给用户空间的返回值也通过寄存机传递,在x86系统中,它存放在eax寄存器
520 |
521 |
522 |
523 | ### 5.5 系统调用的实现
524 |
525 | #### 5.5.1 实现系统调用
526 |
527 | 实现的几个原则:
528 |
529 | * 尽量提供单一功能
530 | * 系统调用应该提供标志参数以确保**向前兼容**,扩展了功能和选项
531 | * 系统调用要考虑通用性,**提供机制而不是策略**
532 |
533 | #### 5.5.2 参数验证
534 |
535 | 由于系统调用在内核空间执行,为了保证系统的安全和稳定,系统调用必须仔细检查所有的参数是否合法
536 |
537 | 其中,最重要的一项就是**检查用户提供的指针是否有效**。在接收一个用户空间的指针之前,内核必须保证:
538 |
539 | * 指针指向的内存区域属于用户空间,进程决不能哄骗内核去读内核空间的数据
540 | * 指针指向的内存区域在进程的地址空间中,进程决不能哄骗内核去读其他进程的数据
541 | * 内存应该标记为对应的读、写或者可执行的权限,进程决不能绕过内存访问权限
542 |
543 | 内核提供两个方法完成**必须的检查和内核空间与用户空间之间数据的来回拷贝**,分别为`copy_to_user()`和`copy_from_user()`
544 |
545 | 如果执行失败,这两个函数返回没能完成拷贝的数据的字节数;如果成功,返回0。两个方法都有可能**阻塞**,当包含用户数据的页被换出到磁盘而不再物理内存上时,就可能发生,此时进程休眠,知道缺页异常程序将改页换入物理内存
546 |
547 | **注意**:内核无论何时都不能轻率地接受来自用户空间的指针!
548 |
549 | **最后一项检查**:进程是否有对应系统调用的合法权限,如`reboot()`系统调用,需要确保进程拥有CAP_SYS_REBOOT功能
550 |
551 | ### 5.6 系统调用上下文
552 |
553 | 内核在执行系统调用事处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程
554 |
555 | 在进程上下文中,内核可以休眠(比如在系统调用阻塞或者调用schedule()时),这说明了:
556 |
557 | * 系统调用可以使用内核提供的绝大多数功能
558 | * 系统调用的进程可以被其他进程抢占,新的进程可以使用相同的系统调用,因此需要保证系统调用是可重入的
559 |
560 | #### 5.6.1 绑定一个系统调用的最后步骤
561 |
562 | 注册一个正式的系统调用的过程为:
563 |
564 | * 在系统调用表的最后加入一个表项,从0开始递增
565 | * 对于所支持的各种体系结构,系统调用号都必须定义于中
566 | * 系统调用必须被编译进内核映像(不能被编译成模块),只需要将它放进kernel/下的相关文件就可以
567 |
568 | 例如,一个虚构的系统调用foo()注册的过程:
569 |
570 | 1. 将sys_foo加入到系统调用表中,对于大多数体系结构,该表位于entry.s文件,形式如下图,将新的系统调用`.long sys_foo`加入表的末尾
571 |
572 |
573 |
574 | 2. 将系统调用号加入到,格式如下图:
575 |
576 |
577 |
578 | 然后在该表中加入一行
579 |
580 | ```c++
581 | #define __NR_foo 338
582 | ```
583 |
584 | 3. 最后,实现foo()系统调用,把实现代码放进kernel/sys.c文件中(asmlinkage限定词是一个编译指令,通知编译器仅从栈中提取该函数的参数)
585 |
586 | ```c
587 | asmlinkage long sys_foo()
588 | {
589 | return THREAD_SIZE
590 | }
591 | ```
592 |
593 | #### 5.6.2 从用户空间访问系统调用
594 |
595 | 用户程序除了通过标准头文件和C库链接来使用系统调用之外,Linux本身提供了一组宏,用于直接访问系统调用,它会设置好寄存器并调用陷入指令,这些宏的形式为_syscall*n()*(n的范围是0~6,代表传递给系统调用的参数个数)
596 |
597 | 例如,`open()`系统调用的定义是:
598 |
599 | ```c
600 | long open(const char *filename, int flags, int mode)
601 | ```
602 |
603 | 不依靠库的支持,直接调用此系统调用的宏形式为:
604 |
605 | ```c
606 | # define NR_open 5
607 | _syscall3(long, open, const char*, filename, int, flags, int, mode)
608 | ```
609 |
610 | 每个宏都有2+2*n个参数,第一个参数对应系统调用的返回类型,第二个参数是系统调用的名称,之后就是系统调用参数顺序排列的每个参数的类型和名称。该宏会扩展成内嵌汇编的C函数,汇编语言执行将系统调用号压入寄存器并触发软中断陷入内核的过程
611 |
612 | ## 6 内核数据结构
613 |
614 | ## 7 中断和中断处理
615 |
616 | ## 8 下半部和推后执行的工作
617 |
618 | ## 9 网络
619 |
620 | ### 9.1 网络实现的分层模型
621 |
622 | 内核网络子系统的实现与TCP/IP模型很相似,相关的C语言代码划分为不同层次,各层次都有明确定义的任务,各个层次只能通过明确定义的接口与上下紧邻的层次通信(这样设计的优点:可以组合使用各种设备、传输机制和协议),如下图是内核对于分层结构的实现
623 |
624 | 
625 |
626 | 该子系统处理了大量特定于协议的细节,穿越各层的代码路径中有大量的函数指针,没有直接的函数调用(因为各个层次存在多个组合关系)。
627 |
628 | ### 9.2 网络命名空间
629 |
630 | ### 9.3 套接字缓冲区
631 |
632 | 内核采用**套接字缓冲区**用于在网络实现的各个层次之间交换数据,无须来回复制分组数据,提高了性能
633 |
634 | 其结构定义如下
635 |
636 | 
637 | 
638 |
639 | #### 9.3.1 使用套接字缓冲区管理数据
640 |
641 | 套接字缓冲区通过其中包含的各种指针与一个内存区域相关联,网络分组的数据位于该区域。
642 |
643 | 套接字缓冲区的基本思想是:通过操作指针来增删协议首部
644 |
645 | * head和end指向数据在内存中的起始和结束位置
646 |
647 | * data和tail指向协议数据区域的起始和结束位置
648 |
649 | 
650 |
651 | * mac_header指向MAC协议首部的起始,network_header和transport_header分别指向网络层和传输层协议首部的起始;在字长32位的系统上,数据类型sk_buff_data_t表示各种类型为简单指针的数据
652 |
653 | ```c
654 | typedef unsigned char *sk_buff_data_t;
655 | ```
656 |
657 | * data和tail使得在不同协议层之间传递数据时,无须显式地复制操作,如下图展示了分组的合成方式
658 |
659 | 
660 |
661 | 在一个新分组产生时,TCP层首先在**用户空间**分配内存来容纳该分组数据(首部和净荷),分配的空间大于数据实际需要的长度,因此较低的协议层可以进一步增加首部
662 |
663 | 然后分配一个套接字缓冲区,使得head和end分别指向上述内存区的起始和结束地址,而TCP数据位于data和tail之间
664 |
665 | 在套接字缓冲区传递到互联网络层时,必须增加一个新层,只需要向已经分配但尚未占用的那部分内存写入数据即可,除了data之外所有的指针都不变,data现在指向IP首部的起始处,下面的各层会重复这样的操作,直至分组完成通过网络发送
666 |
667 | 为了保证套接字缓冲区的长度尽可能小,在64位CPU上,将sk_buff_data_t改为整型变量,由于整型变量占用的内存只有指针变量的一半(前者4字节,后者8字节),该结构的长度缩减了20字节。
668 |
669 | ```c
670 | typedef unsigned int sk_buff_data_t;
671 | ```
672 |
673 | 其中data和head仍然是常规的指针,而所有sk_buff_data_t类型的成员是前两者的偏移,如指向传输层的首部指针计算如下:
674 |
675 | 
676 |
677 | #### 9.3.2 管理套接字缓冲区数据
678 |
679 | 除了前述的指针外,套接字缓冲区还包括用于处理相关的数据和管理套接字缓冲区自身的其他成员,主要成员如下:
680 |
681 | * tstamp:保存了分组到达的时间
682 | * dev指定了分组的网络设备
683 | * iif:输入设备的接口索引号
684 | * sk:指向处理该分组套接字对应的socket实例的指针
685 | * dst:改分组接下来通过内核网络实现的路由
686 | * next和prev:将套接字缓冲区保存在一个双链表中(没有用内核标准链表实现,使用了手工实现的版本)
687 | * qlen:指定了等待的长度
688 |
689 | sk_buff_head和sk_buff的next和prev用于创建一个循环链表,套接字缓冲区的list成员指向表头,如下图
690 |
691 | 
692 |
693 | ### 9.4 网络访问层
694 |
695 | 网络访问层主要负责在计算机之间传输信息,与网卡的设备驱动程序直接协作
696 |
697 | ### 9.4.1 网络设备的表示
698 |
699 | 在内核中,每个网络设备都表示为net_device结构的实例,在分配并填充该实例之后,必须用net/dev.c中的register_device函数将其注册到内核。该函数完成一些初始化任务,并将该设备注册到通用设备机制内,这会创建一个sysfs项/sys/class/net/,关联到该设备对应的目录
700 |
701 | 
702 |
703 |
704 | 网络设备不是全局的,是按照命名空间进行管理,每个命名空间(net实例)有如下3个实例可用:
705 |
706 | * 所有的网络设备都保存在一个单链表中,表头为dev_base
707 | * 按设备名散列:辅助函数dev_get_by_name(struct net *net, const char *name)根据设备名在该散列表上查找网络设备
708 | * 按接口索引散列:辅助函数dev_get_by_index(struct net *net, int ifindex)根据给定的接口索引查找net_device的实例
709 |
710 | net_device结构包含了与特定设备相关的所有信息,该结构非常复杂,如下图为部分成员
711 |
712 | 
713 |
714 | 一些成员定义了与网络层和网络访问层相关的设备属性:
715 |
716 | * mtu指定了一个传输帧的最大长度
717 | * type保存了设备的硬件类型
718 | * dev_addr存储了设备的硬件地址(如以太网的MAC地址),addr_len指向该地址的长度,broadcast是广播地址
719 | * ip_ptr、ip6_ptr、atalk_ptr等指针指向特定于协议的数据
720 |
721 | net_device的大多数成员都是函数指针,执行与网卡相关的典型任务,这些成员表示了与下一个协议层的抽象接口,实现同一组接口访问所有的网卡,而网卡的驱动程序负责实现细节
722 |
723 | #### 9.4.2 接收分组
724 |
725 | 所有现代的设备驱动程序都使用中断来通知内核有分组到达。网卡驱动程序对特定于设备的中断设置了一个处理例程,每当中断被引发时,内核都会调用中断处理程序,将数据从网卡传输到物理内存(通过DMA方式能够将数据从网卡传输到物理内存),或者通知内核在一定时间后进行处理
726 |
727 | 如下图是一个分组到达网络适配器之后,该分组穿过内核到达网络层函数的路径
728 |
729 | 
730 |
731 | 分组是在中断上下文中接收的,处理例程只能执行一些基本任务,避免系统其他任务延迟太长时间
732 |
733 | 在中断上下文中,数据由3个短函数处理,执行下列任务:
734 |
735 | * net_interrupt是设备驱动程序设置的中断处理程序,它用于确定中断是否真的由接收到的分组所引发的,如果确实如此,则控制转到net_rx
736 | * net_rx函数也是特定于网卡,首先创建一个套接字缓冲区,并指向一块物理内存,分组的内容接下来从网卡传输到缓冲区(也就是物理内存),然后分析首部数据,确定分组数据所使用的网络层协议
737 | * 接下来调用netif_rx,与前两个方法不同,netif_rx不是特定于网络驱动程序的,该函数位于net/core/dev.c函数中,调用该函数,标志着控制由特定于网卡的代码转到**网络层的通用接口部分**。该函数的作用在于,将接收的分组放置到一个特定于CPU的等待队列上,并退出中断上下文
738 |
739 | 内核在全局定义的softnet_data数组中管理进出分组的等待队列,数组类型为softnet_data,为提高多处理器系统的性能,对每个CPU都会创建等待队列,支持分组的并行处理。不需要显式的使用锁机制,因为每个CPU只会处理自身的队列,不会干扰其他CPU的工作
740 |
741 | softnet_data结构如下,inpput_pkt_queue使用前面提到的sk_buff_head表头,对所有进入的分组建立一个链表
742 |
743 | 
744 |
745 | netif_rx在结束之前将软中断NET_RX_SOFTIRQ标记为即将执行,然后退出中断上下文,接下来net_rx_action用于该软中断的处理程序,其代码流程图如下(这里是简化版本)
746 |
747 | 
748 |
749 | 在一些准备工作之后,工作转移到process_backlog,该函数在循环中处理下列步骤
750 |
751 | * __skb_dequeue从等待队列中移除一个套接字缓冲区,该缓冲区管理着一个接收到的分组
752 | * 由netif_receive_skb函数分析分组类型,以便根据分组类型将分组传递到网络层的接收函数(即传输到网络系统的更高一层),该函数遍历可能负责当前分组类型的所有网络层函数,逐个调用deliver_skb函数
753 | * 接下来deliver_skb函数使用特定于分组类型的处理程序func,承担对分组更高一层的处理
754 |
755 | 新的协议通过dev_add_pack增加,各个数组项的类型为struct packet_type,定义如下
756 |
757 | 
758 |
759 | func是该结构的主要成员,指向网络层函数的指针,如果分组的类型合适,则将其传递给该函数
760 |
761 | #### 9.4.3 发送分组
762 |
763 | net/core/dev.c中的dev_queue_xmit用于将分组放置到发出分组的队列上,在分组放置到等待队列上一定时间后,分组将发出,这是由特定于适配器的函数hard_start_xmit完成,在每个net_device结构中都以函数指针出现
764 |
765 | ### 9.5 网络层
766 |
767 | #### 9.5.1 IPv4
768 |
769 | IP分组使用的协议首部如下图
770 |
771 | 
772 |
773 | 在内核源码中,该首部由iphdr结构实现
774 |
775 | 
776 |
777 | ip_rcv函数是网络层的入口点,分组向上传过内核的路线如下图
778 |
779 | 
780 |
781 | #### 9.5.2 接收分组
782 |
783 | 在分组转发到ip_rcv(packet_type -> func)时,必须检查接收到的信息,主要是检查计算的校验和与首部中的校验和是否一致,还会检查分组是否达到了IP首部的最小长度,协议是否是IPv4(IPv6的例程是另一个)
784 |
785 | 在进行一些检查之后,内核并不立即继续对分组的处理。而是调用一个netfilter挂钩,使用户可以对分组进行操作,当内核到达一个挂钩位置时,将在用户空间调用对该标记支持的例程,接着在另一个内核函数继续内核端的处理
786 |
787 | 下一步,需要判断分组目的地是本地还是远程计算机,从而判断需要将分组转发到更高层或是转到互联网络层的输出路径上
788 |
789 | ip_route_input负责选择路由,判断路由的结果是选择一个函数,进行进一步的处理,可用的函数分别是ip_local_deliver和ip_forward,分别对应向更高一层传递和转发到另一台计算机
790 |
791 | #### 9.5.3 交付到本地传输层
792 |
793 | 如果分组的目的地是本地计算机,ip_local_deliver会找到一个合适的传输层函数,将分组转发过去
794 |
795 | 1. **分片合并**
796 |
797 | 该函数的第一项任务是通过ip_defrag重新组合分片分组的各个部分,对应的代码流程图如下
798 |
799 | 
800 |
801 | 内核在一个独立的缓存中管理一个分组的各个分片,改缓存称为**分片缓存**。在缓存中,属于同一个分组的各个分片保存在一个独立的等待队列中,直至改分组的所有分片都到达
802 |
803 | 接下来调用ip_find函数,它使用基于分片ID、源地址、目标地址、分组协议标识的散列值,检查是否为对应的分组创建了等待队列。如果没有,则建立一个新的队列,并将当前处理的分组置于其上,否则返回现存队列的地址,以便ip_frag_queue将分组置于队列上
804 |
805 | 在分组的所有分片都进入缓存,ip_frag_reasm将各个分片组合起来;如果分片尚未全部到达,ip_defrag返回NULL指针,终止互联网络层的分组处理,在所有分组都到达后,将恢复处理
806 |
807 | 2. **交付到传输层**
808 |
809 | 接下来返回到ip_local_deliver,在分片合并完成后,调用netfilter挂钩NF_IP_LOCAL_IN,恢复在ip_local_deliver_finish函数中的处理,会根据分组的协议标识符确定一个传输层的函数,将分组传递到该函数,所有基于互联网络层的协议都有一个net_protocol结构的实例,该结构定义如下
810 |
811 | ```c
812 | struct net_protocol {
813 | int (*handler)(struct sk_buff *skb);
814 | void (*err_handler)(struct sk_buff *skb, u32 info);
815 | ...
816 | };
817 | ```
818 |
819 | * handler是协议例程
820 | * 在接收到ICMP错误信息并需要传递到更高层时,需要调用err_handler
821 |
822 | inet_add_protocol标准函数将上述结构的实例指针存储到inet_protos数组,通过散列的方法确定存储具体协议的索引位置
823 |
824 | 在套接字缓冲区中通过指针移动“删除”IP首部后,剩下的工作就是调用传输层对应的接收例程,其函数指针存储在inet_protocol的handler字段中。例如接收TCP分组的tcp_v4_rcv例程
825 |
826 | #### 9.5.4 分组转发
827 |
828 | 当需要将分组转发到其他计算机时,分组的目标地址分为两类:
829 |
830 | * 目标计算机在某个本地网络中,发送计算机与该网络有连接
831 | * 目标计算机是远程计算机,不连接本地网络,只能通过网关访问
832 |
833 | 第二种情况会复杂很多,需要找到剩余路由中的第一个站点,将分组转发到该站点,不仅需要计算机所属本地网络结构的相关信息,还需要相邻网络和相关的外出路径的信息。该信息由**路由表**提供,由内核通过多种数据结构管理,会在[9.5.5](####9.5.5 发送分组)节讨论
834 |
835 | 在接收分组时,调用的ip_route_input函数充当路由实现的接口,它能够识别出分组是交付到本地还是转发出去,同时能找到通向目标地址的路由(目标地址存储在套接字缓冲区的dst字段)
836 |
837 | ip_forward函数的处理流程如下图
838 |
839 | 
840 |
841 | * 首先,根据TTL字段检查是否允许传输到下一跳,如果TTL<=1,则丢弃该分组,否则,将TTL减一,ip_decrease_ttl负责该工作,修改TTL的同时分组校验和也会修改
842 | * 接下来调用netfilter挂钩NF_IP_FORWARD,之后在ip_forward_finish中恢复处理,接下来的工作委托给两个函数
843 | * 如果分组包含额外的选项,则在ip_forward_options函数中处理
844 | * dst_output函数将分组传递到在路由期间选择并保存在skb->dst->output的发送函数,通常使用ip_output,该函数将分组传递到与目标地址匹配的网络适配器
845 |
846 | #### 9.5.5 发送分组
847 |
848 | 内核会提供通过互联网络层发送分数据的函数给较高层的协议(传输层)使用,其中ip_queue_xmit是比较常见的一个,代码流程图如下
849 |
850 | 
851 |
852 | * 首先查找可用于该分组的路由,在发送第一个分组时,内核需要查找一个新的路由(在下文讨论)
853 | * 接下来ip_send_check为分组生成校验和
854 | * 内核调用netfilter挂钩NF_IP_LOCAL_OUT
855 | * 接下来调用dst_output函数,还函数基于调用skb->dst->output函数(在确定路由期间找到),后者位于套接字缓冲区中,与目标地址无关,通常该函数指向ip_output,本地产生和转发的分组将在该函数汇总
856 |
857 | 1. **转移到网络访问层**
858 |
859 | ip_output函数的代码流程图如下
860 |
861 | 
862 |
863 | * 首先调用netfilter挂钩NF_IP_POST_ROUTING
864 | * 接下来是ip_finish_output
865 | * 如果分组长度不大于MTU,则无须分片,直接调用ip_funish_output2,该函数检查套接字缓冲区是否有足够空间容纳产生的硬件首部,如果不够,则使用skb_realloc_headroom分配额外的空间;否则调用ip_fragment实现分组的分片
866 | * 最后调用路由层设置的函数dst->neighbour->output,该函数指针通常指向dev_queue_xmit
867 |
868 | 2. **分组分片**
869 |
870 | ip_fragment将IP分组划分为更小的单位,如下图
871 |
872 | 
873 |
874 | 3. **路由**
875 |
876 | 每个接收到的分组分为3类:
877 |
878 | * 目标是本地主机
879 | * 目标是当前主机连接的计算机
880 | * 目标是远程计算机,只能经由中间系统到达
881 |
882 | 对于第3种情况,必须根据路由选择信息来查找网关系统,分组需要通过网关发送
883 |
884 | 内核使用散列表来加速路由的工作,路由的起始点是ip_route_input函数,它首先会在路由缓存中查找路由
885 |
886 | ip_route_input_slow用于根据内核的数据结构建立一个新的路由,它调用fib_lookup函数,后者的隐式返回值(一个用作参数的指针)指向一个fib_result结构的实例。fib代表信息转发库,是一个表,用于管理内核保存的路由选择信息
887 |
888 | 路由结果关联到一个套接字缓冲区,其中的dst成员指向一个dst_entry结构的实例,该实例的内容在路由期间查找,其结构定义如下
889 |
890 | 
891 |
892 | * Input和output分别用于处理进入和外出的分组,根据分组类型,会将input和output指向不同的函数:
893 |
894 | * 对需要交付到本地的分组,input设置为ip_local_deliver,而output设置为ip_rt_bug(该函数只会向内核日志输出一个错误信息)
895 | * 对于需要转发的分组,input设置为ip_forward,output设置为ip_output函数
896 |
897 | * dev指定了用于处理该分组的网络设备
898 |
899 | * neighbour成员存储了计算机在本地网络的IP和硬件地址,可以通过网络访问层直接到达
900 |
901 | 
902 |
903 | dev保存了网络设备的数据结构而ha是设备的硬件地址,output是指向适当的内核函数的指针,在通过网络适配器传递分组时调用
904 |
905 | neighbour实例由内核中实现ARP的ARP层创建,ARP协议负责将IP地址转换为硬件地址,由于dst_dentry实例有一个成员指向neighbour,网络访问层的代码在分组通过网络适配器离开当前系统时可以调用output函数
906 |
907 | #### 9.5.6 netfilter
908 |
909 | netfilter是一个Linux内核框架,可以根据动态定义的条件来过滤和操作分组
910 |
911 | 1. **扩展网络功能**
912 |
913 | netfilter框架向内核添加了下列能力:
914 |
915 | * 根据状态及其他条件,对不同数据流方向(进入、外出、转发)进行**分组过滤**
916 | * **NAT(网络地址转换)**,根据某些规则来转换源地址和目标地址
917 | * 分组处理和操作,根据特定的规则拆分和修改分组
918 |
919 | 可以在运行时向内核载入模块来增强netfilter功能,一个定义好的规则集,告诉内核在何时使用各个模块的代码
920 |
921 | netfilter实现由两部分组成:
922 |
923 | * 内核代码中的挂钩,位于网络实现的核心,用于调用netfilter代码
924 | * netfilter模块,其代码挂钩内核调用,但独立于其余的网络代码
925 |
926 | iptables由网络管理员用来配置防火墙、分组过滤器和类似功能,这些是定义在neifilter框架上的模块
927 |
928 | 2. **调用挂钩函数**
929 |
930 | 在通过挂钩执行netfilter代码时,网络层的函数将会中断。挂钩将一个函数分为两部分,前一部分在netfilter代码调用前运行,后一部分在其后执行
931 |
932 | netfilter挂钩通过中的NF_HOOK宏调用,如果内核启用的netfilter支持,该宏定义如下:
933 |
934 | 
935 |
936 | * pf是指调用的netfilter挂钩源自哪个协议族(IPv4层的所有调用都使用PF_INET)
937 | * hook是挂钩编号,如NF_IP_FOREARD和NF_IP_LOCAL_OUT,定义在中
938 | * skb代表所处理的套接字缓冲区
939 | * indev和outdev是指向网络设备的net_device实例的指针,分别通过二者进入和离开内核(值可以为NULL)
940 | * okfn是一个函数指针,在netfilter挂钩结束时执行
941 |
942 | 该宏在展开时,首先迂回到NF_HOOK_THRESH和nf_hook_thresh,然后执行nf_look_slow来处理netfilter挂钩,最后调用结束netfilter处理的okfn函数;其中的nf_hook_slow函数会遍历所有注册的netfilter挂钩并调用它们
943 |
944 | 以IP转发为例,挂钩调用的代码如下
945 |
946 | 
947 | 
948 |
949 | 其中指向的okfn是ip_forward_finish,如果没有为PF_INET和NF_IP_FORWARD注册netfilter挂钩,那么控制直接传递到该函数,否则,执行相关的netfilter代码,控制转入ip_forward_finish
950 |
951 | 3. **扫描挂钩表**
952 |
953 | 如果注册了挂钩函数,则会调用nf_hook_slow函数,所有挂钩都保存在二维数组nf_hooks中
954 |
955 | 
956 |
957 | NPPOTO指定了系统支持的协议族的最大数目(各个协议族的符号常数,例如PF_INET和PF_DECnet,保存在include/linux/socket.h),每个协议可以定义NF_MAX_HOOKS个挂钩链表,默认值是8个
958 |
959 | 该表的list_head元素作为双链表表头,双链表可容纳nf_hooks_ops实例
960 |
961 | 
962 |
963 | 主要成员:
964 |
965 | * list:将结构连接到双链表
966 |
967 | * owner:指向所属模块的module数据结构的指针
968 |
969 | * hook:一个指向挂钩函数的指针,需要的参数和NF_HOOK宏相同
970 |
971 | 
972 |
973 | * pf和hooknum指定了协议族和与挂钩相关的编号
974 |
975 | * 链表中的挂钩是按照优先级升序排列(比如,可以确保分组数据的处理总是在过滤器操作之前进行)
976 |
977 | 
978 |
979 | 可以根据协议族和挂钩编号从nf_hook数组中选择适当的链表,接下来的工作委托给of_iterate,该函数会保存所有链表元素,并调用hook函数
980 |
981 | 4. **激活挂钩函数**
982 |
983 | 每个hook函数都返回下列值之一:
984 |
985 | * NF_ACCEPT:表示接受分组
986 | * NF_STOLEN:表示挂钩函数“窃取”了一个分组并处理该分组,此时分组已与内核无关,不必在调用其他挂钩,还需要取消其他协议层的处理
987 | * NF_DROP:通知内核丢弃该分组,和CF_STOLEN一洋,其他挂钩和协议层也不需要处理,同时套接字缓冲区占用的内存空间可以释放,其中包含的数据可以被丢弃
988 | * NF_QUEUE:将分组置于一个等待队列上,以便其数据可以由用户空间代码处理,不会执行其他挂钩函数
989 | * NF_REPEAT:表示再次调用该挂钩
990 |
991 | 最后,除非所有挂钩函数都返回NF_ACCEPT,否则分组不会在网络子系统进一步处理
992 |
993 | 内核提供了一个挂钩函数的集合,它们称为iptables,用于分组的高层处理(可以使用用户工具iptables来配置)
994 |
995 | ### 9.6 传输层
996 |
997 | #### 9.6.1 UDP
998 |
999 | 经过前面[9.5.2节](# 9.5.2 接收分组) 介绍,ip_local_deliver负责分发IP分组传输的数据内容,net/core/udp.c中的udp_rcv用于进一步处理UDP数据报,其代码流程图如下
1000 |
1001 | 
1002 |
1003 | udp_rcv函数是 \_\_udp4_lib_rcv的包装器,而后者的输入参数是一个套接字缓冲区,在确认分组未经篡改之后,调用\_\_udp4_lib_lookup查找与之匹配的监听套接字,连接参数从UDP首部中获取,其结构如下
1004 |
1005 | 
1006 |
1007 | \_\_udp4_lib_rcv用于查找与分组目标匹配的内核内部的套接字,在有某个监听进程对分组感兴趣时,在udphash全局数组中会有与分组目标端口匹配的sock结构实例,\_\_udp_lib_lookup采用散列方法查找并返回该实例;如果找不到,则向源系统发送一个“目标不可达”的消息,并丢弃分组内容
1008 |
1009 | 内核中有两种数据结构表示套接字,sock是到网络访问层的接口,socket是到用户空间的接口
1010 |
1011 | sock结构简化版如下
1012 |
1013 | 
1014 |
1015 | 在udp_rcv找到适当的sock实例后,控制转移到udp_queue_rcv_skb,然后立即调用sock_queue_rcv_skb,它会执行两个重要的操作,完成到应用层的数据交付
1016 |
1017 | * 等待通过套接字交付数据的进程,会再sk_sleep等待队列上睡眠
1018 | * 调用skb_queue_tail将包含分组数据的套接字缓冲区插入到sk_receive_queue链表末端,其表头保存在sock结构中
1019 | * 调用sk_data_ready指向的函数,通知套接字有新数据到达,这会唤醒sk_sleep队列上睡眠、等待数据到达的所有进程
1020 |
1021 | #### 9.6.2 TCP
1022 |
1023 | 下面主要讨论TCP协议的3个主要部分:连接建立、连接终止和数据流的按序传输
1024 |
1025 | TCP的状态转换如下图
1026 |
1027 | 
1028 |
1029 | 1. **TCP首部**
1030 |
1031 | TCP分组的首部包含了状态数据和其他连接信息,如下图所示
1032 |
1033 | 
1034 |
1035 | 2. **接收TCP数据**
1036 |
1037 | 在互联网络层处理过分组之后,tcp_v4_rcv是TCP的入口函数,其代码流程图如下
1038 |
1039 | 
1040 |
1041 | 系统中的每个TCP套接字都归入3个散列表之一,分别对应下列状态:
1042 |
1043 | * 完全连接的套接字
1044 | * 等待连接(监听状态)的套接字
1045 | * 处于建立连接过程中的套接字
1046 |
1047 | 在对分组数据进行各种检查并将首部中的信息复制到套接字缓冲区的控制块之后,内核将查找等待该分组的工作委托给\_\_inet_lookup函数,它会调用两个函数,分别扫描各种散列表,其中\_\_inet_lookup_established函数寻找一个已连接的套接字,如果没有找到合适的结构,则调用inet_lookup_listener函数检查所有的监听套接字
1048 |
1049 | 与UDP相比,在找到对应该连接适当的sock结构之后,工作尚未结束,必须根据连接的状态进行相应的状态迁移,tcp_v4_do_rcv是一个多路分解器,会基于套接字的状态将代码控制流划分到不同的分支
1050 |
1051 | 3. **被动连接建立**
1052 |
1053 | 被动连接是在接收到一个连接请求的SYN分组后出发的,它的起点是tcp_v4_rcv函数,其代码流程图如下
1054 |
1055 | 
1056 |
1057 | 调用tcp_v4_hnd_req执行网络层中建立连接的各种初始化任务,实际的状态迁移是在tcp_rcv_state_process函数,它由一个长的switch/case语句组成,区分各种可能的套接字状态来调用适当的传输函数
1058 |
1059 | 可能的套接字状态定义在一个枚举中
1060 |
1061 | 
1062 | 如果套接字的状态是TCP_LISTEN,则调用tcp_v4_conn_request,该函数结束前发送确认分组,其中包含了设置的ACK标志和接受到的分组序列号,还包含新生成的序列号和SYN标志,此时服务端的套接字状态变为TCP_SYN_RECV;下一步,发送ACK分组给客户端,客户端返回确认分组,此时套接字状态由TCP_SYN_RECV变为TCP_ESTABLISHED
1063 |
1064 | 4. **主动连接建立**
1065 |
1066 | 主动连接发起是,是通过用户空间应用程序调用open库函数,发出socketcall系统调用到达内核函数tcp_v4_connect,其代码流程图如下
1067 |
1068 | 
1069 |
1070 | 该函数开始于查找目标主机的IP路由,在产生TCP首部并将相关的值设置到套接字缓冲区之后,套接字状态从CLOSED变为SYN_SENT,接下来tcp_connect讲一个SYN分组发送到互联网络层,接下来发送到服务端。同时,会在内核创建一个定时器,确保如果在一定时间内没有接收到确认,将重新发送分组
1071 |
1072 | 接下来客户端等待服务端对SYN分组的确认以及确认连接请求的SYN分组,这回通向tcp_rcv_state_process分配器,然后控制流转到tcp_rcv_synsent_state_process函数,然后套接字状态设为ESTABLISHED,同时tcp_send_ack向服务器发送一个ACK分组,完成连接建立
1073 |
1074 | 5. **接收分组**
1075 |
1076 | 如下代码流程图是接收分组时的代码流程图,从tcp_v4_rcv函数开始
1077 |
1078 | 
1079 |
1080 | 在控制传递到tcp_v4_do_rcv后,在确定目标套接字的状态为TCP_ESTABLISHED之后,调用tcp_rcv_established函数,判断分组是否易于处理,如果是,在**快速路径**中处理,否则,在**低速路径**中处理
1081 |
1082 | 分组符合下列条件之一,归入易于分析:
1083 |
1084 | * 分组必须只包含对上一次发送数据的确认
1085 | * 分组必须只包含预期接收的数据
1086 |
1087 | **快速路径**:
1088 |
1089 | * 会进行分组的检查,找到更为复杂的分组返回到低速路径
1090 | * 接下来分析分组的长度,确认分组的内容是数据还是确认
1091 | * 快速路径并不处理ACK部分,会调用tcp_ack函数,该函数最重要的功能是分析有关连接的新信息,同时从重传队列中删除确认数据(该队列包含所有发送的分组,如果在一定时间限制内没有收到ACK确认,则需要重传)
1092 | * 由于进入快速路径的数据是紧接着前一部分的,无须进行进一步检查,最后调用套接字中的sk_data_ready函数指针,通知用户进程数据可用
1093 |
1094 | **低速路径**:要处理更多TCP选项,其中的代码要牵涉更广泛的内容,在低速路径中,数据保护能直接发送到套接字,需要对分组选项进行复杂的检查,然后是TCP子系统的响应,不按序到达的数据放置到一个专门的等待队列上,直至形成一个连续的数据段,才能将完整的数据传递到套接字
1095 |
1096 | 6. **发送分组**
1097 |
1098 | TCP分组的发送,由更高层网络协议实例对tcp_sendmsg函数的调用开始,代码流程图如下
1099 |
1100 | 
1101 |
1102 | * 首先,内核会等待直至连接建立,此时套接字状态是TCP_ESTABLISHED
1103 |
1104 | * 数据从用户空间进程的地址空间复制到内核空间,用于建立一个TCP分组
1105 |
1106 | * 接下来内核到达tcp_push_one,它执行下列3个任务:
1107 |
1108 | * tcp_snd_test检查目前是否可以发送数据,比如接收方是否过载积压
1109 |
1110 | * tcp_transmit使用协议族相关的af_specific->queue_xmit函数(IPv4使用ip_queue_xmit函数),将数据转发到互联网络层
1111 |
1112 | * update_send_head处理对统计量的更新,初始化发送TCP信息段的重传定时器
1113 |
1114 | 发送TCP分组的过程需要满足下列需求:
1115 |
1116 | * 接收方等待队列上必须有足够的空间可用于该数据
1117 | * 必须实现防止连接拥塞的ECN机制
1118 | * 必须检测某一方出现失效的情况
1119 | * TCP慢启动机制
1120 | * 发送但未得到确认的分组,需要超时重传
1121 |
1122 | 7. **连接终止**
1123 |
1124 | 连接终止的状态迁移在分配器函数tcp_rcv_state_process进行,代码路径可能包含tcp_rcv_established和tcp_close函数
1125 |
1126 | 主动关闭的一方,会在用户进程调用close关闭连接,调用tcp_close函数
1127 |
1128 | * 如果套接字的状态为LISTEN,则将套接字的状态改为CLOSED;否则,控制转到tcp_close_state,其中调用tcp_set_state将套接字状态设置为FIN_WAIT_1,tcp_send_fin向另一方发送FIN分组
1129 | * 收到带有ACK标志的分组,触发FIN_WAIT_1到FIN_WAIT_2的迁移(在tcp_set_state中进行)
1130 | * 最后收到另一方发送过来的FIN分组,则将套接字的状态改为TIME_WAIT状态(之后会自动切换到CLOSED状态)
1131 |
1132 | 被动关闭的一方状态迁移过程是类似的
1133 |
1134 | * 在收到第一个FIN分组时状态是TCP_ESTABLISHED,处理由tcp_rcv_established的低速路径进行,向主动关闭一方发送ACK分组,并将套接字状态改为TCP_CLOSING
1135 | * 然后发送FIN分组,状态变为LAST_ACK,是通过调用close库函数(调用内核的rcp_close_state函数)进行的
1136 | * 最后接收到主动关闭方发送的ACK分组,即可终止连接,通过tcp_rcv_state_process函数将套接字状态改为CLOSED(tcp_done函数处理),释放套接字占用的内存空间,并最终终止连接
1137 |
1138 | ### 9.7 应用层
1139 |
1140 | 内核与用户空间套接字之间的接口实现在C标准库中实现,使用了socketcall系统调用,它充当一个多路分解器,将各种任务分配由不同的过程执行
1141 |
1142 | 对程序使用的每个套接字来说,都对应于一个socket结构和sock结构的实例,二者分别充当向下(内核)和向上(用户空间)的接口
1143 |
1144 | #### 9.7.1 socket数据结构
1145 |
1146 | socket结构定义如下
1147 |
1148 | 
1149 |
1150 | * type:指定所用协议类型的数字标识符
1151 |
1152 | * state:表示套接字的连接状态,可使用下列值
1153 |
1154 | 
1155 |
1156 | 这里的枚举值,与传输层协议在建立和关闭连接的状态值没有关系,他们表示与外界(用户程序)相关的一般性状态
1157 |
1158 | * file:指向一个伪文件file实例的指针,用于与套接字通信
1159 |
1160 | * ops:指向proto_ops结构的指针,其中包含处理套接字的特定于协议的函数
1161 |
1162 | 
1163 |
1164 | 其中的需要函数指针与C标准库的函数同名,因为C库函数会通过socketcall系统调用导向上述的函数指针
1165 |
1166 | * sk:指向sock结构的指针,它包含了对内核有意义的附加的套接字管理数据,其中最重要的成员放置到了sock_common结构中,并将该结构嵌入到struct sock中
1167 |
1168 | 
1169 |
1170 | 系统的各个sock结构实例组织在一个协议相关的散列表中,skc_node用作散列表的表元,而skc_hash表示散列值
1171 |
1172 | 在接收和发送数据时,需要将数据放置到包含套接字缓冲区的等待队列上(skb_receive_queue和sk_write_queue)
1173 |
1174 | 每个sock结构都关联了一组回调函数,由内核引起用户程序对特定事件的关注或进行状态改变,例如前面提到的sk_data_ready函数指针,在数据到达时,将调用它指向的函数,通常是指向sock_def_readable函数
1175 |
1176 | socket结构的ops成员类型为struct proto_ops,而sock的prot成员类型为struct proto,二者容易混淆,后者定义如下
1177 |
1178 | 
1179 | 
1180 |
1181 | sock结构中的操作用于套接字层和传输层之间的通信,而socket结构的ops成员包含的函数指针则用于与系统调用通信
1182 |
1183 | #### 9.7.2 套接字和文件
1184 |
1185 | 在连接建立后,用户空间使用普通的文件操作来访问套接字,这个实现是基于VFS结构,每个套接字都分配了一个inode,inode又关联到另一个与普通文件相关的结构,用于操作文件的函数保存在一个单独的指针表i_fop中
1186 |
1187 | 
1188 |
1189 | 对套接字文件描述符的文件操作,可以透明的重定向到网络子系统的代码
1190 |
1191 | 
1192 |
1193 | inode和套接字的关联,是通过下列辅助结构,将对应的两个结构实例分配到内存中的连续位置
1194 |
1195 | 
1196 |
1197 | 内核提供了两个宏来进行运算,SOCKET_I根据inode找到相关的socket实例,SOCK_INODE根据socket找到inode实例
1198 |
1199 | #### 9.7.3 socketcall系统调用
1200 |
1201 | 对于套接字的部分操作,如文件的读写操作,可以通过虚拟文件系统相关系统调用进入内核,然后重定向到socket_file_ops结构的函数指针,除此之外,还需要对套接字执行其他操作,如创建套接字、bind、listen等
1202 |
1203 | 因此内核提供了socketcall系统调用,它充当一个分派器,将系统调用转到其他函数并传递相关参数
1204 |
1205 | 
1206 | 
1207 |
1208 | 下列表格对应socketcall的各个“子调用”
1209 |
1210 | 
1211 |
1212 | #### 9.7.4 创建套接字
1213 |
1214 | sys_socket是创建套接字的起点,代码流程图如下
1215 |
1216 | 
1217 |
1218 | * 首先,使用sock_create创建一个新的套接字数据结构,该函数调用__sock_create,其中sock_alloc为socket实例和inode实例分配内存,这使得两个对象联合起来
1219 |
1220 | * 数组static net_proto_family* net_families[NPROTO]包含所有传输协议,各个数组项都提供特定于协议的初始化函数create,它会创建一个内部的sock实例
1221 |
1222 | 
1223 |
1224 | * map_sock_fd为套接字创建一个伪文件,然后分配一个文件描述符,将其作为系统调用的结果返回
1225 |
1226 | #### 9.7.5 接收数据
1227 |
1228 | 使用recvfrom和recv以及文件相关的readv和read函数来接收数据,这些函数的控制流在内核的特定位置会合并,这里只讨论recv_form对应的sys_recvfrom,期待吗流程图如下
1229 |
1230 | 
1231 |
1232 | * 套接字对应的文件描述符作为参数传递到该系统调用
1233 | * fget_light根据task_struct的描述符表查找对应的file实例
1234 | * sock_from_file确定与之关联的inode,并通过SOCKET_I找到相关的套接字
1235 | * sock_recvmsg调用特定于协议的接收例程sock->ops->recvmsg(例如UDP使用udp_recvmsg,TCP使用tcp_recvmsg),其中UDP的实现过程为:
1236 | * 如果接收队列(sock结构的receive_queue成员实现)上至少有一个分组,则移除并返回该分组
1237 | * 如果接收队列是空的,进程使用wait_for_packet使自身睡眠,直至数据到达
1238 | * 在新数据到达时总是调用sock结构的data_ready函数,此时进程被唤醒
1239 | * 最后,move_addr_to_user将数据从内核空间复制到用户空间
1240 |
1241 | #### 9.7.6 发送数据
1242 |
1243 | 用户空间程序发送数据时,可以使用两个网络相关的库函数(sendto和send)或文件层的write和writev函数,他们也会在内核的某个位置合并,这里只讨论sendto对应的sys_sendto,其代码流程图如下
1244 |
1245 | 
1246 |
1247 | ## 12 内存管理
1248 |
1249 | > 内核不能像用户空间那样奢侈的使用内存,获取内存币用户空间复杂很多
1250 |
1251 | ### 12.1 概述
1252 |
1253 | 内存管理的实现涵盖:
1254 |
1255 | * 内存中物理内存页的管理
1256 | * 分配大块内存的伙伴系统
1257 | * 分配较小内存的slab、slub和slob分配器
1258 | * 分配非连续内存块的vmalloc机制
1259 | * 进程的地址空间
1260 |
1261 | 有两种类型计算机,分别以不同的方法管理物理内存:
1262 |
1263 | * UMA计算机(一致内存访问,uniform memory access)将可用内存已连续方式组织起来(可能有小的缺口)。SMP系统中的每个处理器访问各个内存块都是同样快
1264 | * NUMA计算机(非一致内存访问,non-uniform memory access),总是多处理器计算机。系统的各个CPU都有本地内存,可支持特别快速的访问,而各个处理器之间通过总线连接起来,以支持对其他CPU本地内存的访问,比访问本地内存会慢一些
1265 |
1266 | 
1267 |
1268 | **分配阶**:表示内存中页的数目(取以2为底对应阶数的数量),例如阶1分配21=2个页帧。
1269 |
1270 | ### 12.2 (N)UMA模型中的内存组织
1271 |
1272 | #### 12.2.1 概述
1273 |
1274 | 如下图为内存划分的图示,内存划分为**结点**,每个结点关联到系统的一个处理器,对应数据结构为`pg_data_t`的实例。各个结点划分为**内存域**,分别为:
1275 |
1276 | * ZONE_DMA:标识适合DMA的内存域
1277 | * ZONE_DMA32:标记了使用32位地址可寻址,适合DMA的内存域,在64位系统两种DMA内存域才有区别,而在32位计算机上,本内存域是空的,长度为0M
1278 | * ZONE_NORMAL:标记了可直接映射到内核段的普通内存域,所有体系结构都会保证这一区域的存在,但是不保证该地址对应了实际的物理内存
1279 | * ZONE_HIGHMEM:标记了超出内核段的物理内存
1280 |
1281 | 各个内存域都关联一个数组,组织改内存域的物理内存页(页帧),每个页帧对应结构`page`的实例。各个内存节点保存在单链表中,供内核遍历
1282 |
1283 | **注意**:处于性能考虑,在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA节点上进行,如果当前节点的内存用尽,会使用节点**备用列表**的内存(借助于zone_list),该列表包含了其他结点
1284 |
1285 | 
1286 |
1287 | #### 12.2.2 数据结构
1288 |
1289 | 1. 结点管理
1290 |
1291 | pg_data_t用于节点的基本元素,定义如下
1292 |
1293 | 
1294 |
1295 | * node_zones:是一个数组,包含节点中各内存域的数据结构
1296 |
1297 | * node_zonelists:指定了备用结点及其内存域的列表
1298 |
1299 | * nr_zones:节点中不同内存域的数目
1300 |
1301 | * node_mem_map:指向page实例数组的指针,用于描述结点的所有物理内存页
1302 |
1303 | * bdata:指向**自举内存分配器**数据结构的实例(系统启动期间,内存管理子系统初始化之前,内核也需要内存,会使用到自举内存分配器)
1304 |
1305 | * node_start_pfn:该结点第一个页帧的逻辑编号,系统中所有结点的页帧依次编号,每个页帧的号码全局唯一(在UMA系统中总是为0,因为其中只有一个结点)
1306 |
1307 | * node_id:全局结点id,系统中NUMA结点都从0开始编号
1308 |
1309 | * pgdat_next:连接到下一个内存结点(系统中所有结点使用单链表,结尾通过空指针标记)
1310 |
1311 | * kswapd_wait:交换守护进程的等待队列,在将页帧换出结点时会用到
1312 |
1313 | * kswapd:指向负责该节点的交换守护进程的的task_struct
1314 |
1315 | * kswapd_max_order:用于也交换子系统的实现,代表需要释放的区域的长度
1316 |
1317 | 2. 结点状态管理
1318 |
1319 | 
1320 |
1321 |
1322 |
1323 | ### 12.3 页
1324 |
1325 | 内核把物理页作为内核管理的基本单元,内存管理单元(MMU)是管理内存并将虚拟内存转换为物理内存的硬件,它以页为单位来管理系统中的页表
1326 |
1327 | 结构体struct page表示系统中的每个物理页
1328 |
1329 | ```c
1330 | struct page {
1331 | unsigned long flags,
1332 | atomic_t _count,
1333 | atomic_t _mapcount,
1334 | unsigned long private,
1335 | struct address_space *mapping,
1336 | pgoff_t index,
1337 | struct list_head lru,
1338 | void *virtual
1339 | };
1340 | ```
1341 |
1342 | * `flags`域,用来存放页的状态(包括是不是脏的,是不是锁定在内存),每一位单独表示一种状态,至少可以表示32中不同的状态
1343 | * `_count`域,存放页的引用计数,-1时内核没有引用该页,在新的内存分配中可以使用。内核调用`page_count()`检查该域,返回0表示页空闲,返回正整数表示正在使用
1344 | * 页可以由页缓存使用(此时,mapping域指向页关联的address_space对象),或者作为私有数据(`private`指向),或者作为进程页表中的映射
1345 | * `virtual`域,页的虚拟地址,当页在高端内存(不会永久映射到内核空间)中时,这个域为`NULL`
1346 |
1347 | **注意**:
1348 |
1349 | 1. `page`结构与物理页相关,并非与虚拟页相关,它仅仅描述当前时刻在相关物理中存放的数据(由于交换等原因,关联的数据继续存在,但是和当前物理页不再关联),它对于页的描述是短暂的
1350 | 2. 页的拥有者可能是用户空间进程、动态分配的内核数据,静态内核数据或者页高速缓存等
1351 |
1352 | ### 12.4 页表
1353 |
1354 |
1355 |
1356 | ### 12.5 区
1357 |
1358 | Linux主要使用四种区:
1359 |
1360 | * ZONE_DMA,其中包含的页只能进行DMA操作(直接内存访问)
1361 | * ZONE_DMA32,和ZONE_DMA类似,不同之处是只能被32位设备访问
1362 | * ZONE_NORMAL,包含能够正常映射的页
1363 | * ZONE_HIGHMEM,包含“高端内存”,其中的页不能永久地映射到内核空间
1364 |
1365 | > 高端内存,由于一些体系结构的物理内存比虚拟内存大的多,为了充分利用物理内存,将物理内存中的部分区域划分为高端内存,他们不能永久地映射到内核空间,而是动态的映射
1366 | >
1367 | > 在32位x86体系中,ZONE_HIGHMEM为高于896MB的所有物理内存,其余内存为低端内存,其中ZONE_NORMAL为16MB到896MB的物理内存,ZONE_DMA为小于16MB的物理内存
1368 | >
1369 | > x86-64系统没有高端内存区
1370 |
1371 | | 区 | 描述 | 物理内存 |
1372 | | :----------: | :------------: | :------: |
1373 | | ZONE_DMA | DMA使用的页 | < 16MB |
1374 | | ZONE_NORMAL | 正常可寻址的页 | 16~896MB |
1375 | | ZONE_HIGHMEM | 动态映射的页 | > 896MB |
1376 |
1377 | 每个区使用结构体`zone`表示,具体结构如下图
1378 |
1379 |
1380 |
1381 | 结构体中域的说明:
1382 |
1383 | * `lock`域,是一个自旋锁,防止结构被并发访问,这个域只保护结构,不保护驻留在这个区中的页
1384 | * `watermark`域,水位值,为每个内存区设置合理的内存消耗基准
1385 | * `name`域,表示区的名字,三个区的名字分别为"DMA","Normal","HighMem"
1386 |
1387 | ### 12.6 物理内存的管理
1388 |
1389 | #### 12.6.1 伙伴系统结构
1390 |
1391 | ### 12.7 获得页
1392 |
1393 | * 分配2order(1< Linux内核提供slab层(即slab分配器),作为通用数据结构缓存层
1474 |
1475 | #### 12.10.1 slab的设计
1476 |
1477 | > slab层将不同的对象划分为**高速缓存组**,每个高速缓存组存放不同类型的对象,例如,分别存放进程描述符(task_struct结构的空闲链表),索引节点对象(struct inode)
1478 |
1479 | * `kmalloc()`建立在slab层之上,使用了一组通用高速缓存
1480 | * 一般slab仅仅由一页组成,每个高速缓存由多个slab组成
1481 | * 每个slab包含一些对象成员,对象指的是被缓存的数据结构
1482 | * slab包含三种状态:满、部分满或空
1483 |
1484 | 高速缓存使用结构体`kmem_cache`表示,这个结构包括三个链表:`slabs_full`, `slabs_partial`和`slabs_empty`,这些链表包含高速缓存中的所有slab
1485 |
1486 | slab使用slab描述符表示,详见**P216**
1487 |
1488 | ```c
1489 | struct slab {
1490 | ...
1491 | };
1492 | ```
1493 |
1494 | slab分配器使用`__get_free_pages()`创建新的slab
1495 |
1496 | #### 12.10.2 slab分配器的接口
1497 |
1498 | * 创建一个新的高速缓存
1499 | * `name`:高速缓存的名字
1500 | * `size`:高速缓存中每个元素的大小
1501 | * `align`:slab内第一个对象的偏移量,用来确保在页内进行特定的对齐
1502 | * `flags`:可选的设置项,控制高速缓存的行为,详见**P218**
1503 | * `ctor`:高速缓存的构造函数(Linux的高速缓存不使用构造函数)
1504 | * 返回指向高速缓存的指针
1505 | * 函数调用可能会睡眠,不能再中断上下文使用
1506 |
1507 | ```c
1508 | struct kmem_cache *kmem_cache_create(const char *name,
1509 | size_t size,
1510 | size_t align,
1511 | unsigned long flags,
1512 | void (*ctor)(void *));
1513 | ```
1514 |
1515 | * 撤销一个高速缓存(可能睡眠,不能再中断上下文使用)
1516 |
1517 | ```c
1518 | int kmem_cache_destroy(struct kmem_cache *cachep);
1519 | ```
1520 |
1521 | **注意**:调用`kmem_cache_destroy`之前要确保两个条件:
1522 |
1523 | 1. 高速缓存中的所有slab为空
1524 | 2. 在调用此函数过程中不在访问这个高速缓存
1525 |
1526 | * 从已经创建的缓存中分配释放对象,使用示例详见**P219**
1527 |
1528 | ```c
1529 | void kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
1530 | void kmem_cache_free(struct kmem_cache *cachep, void *objp);
1531 | ```
1532 |
1533 | #### 12.10.3 slab分配的原理
1534 |
1535 |
1536 |
1537 | ### 12.11 栈上的静态分配
1538 |
1539 | > 不同于用户栈,内核栈小而且固定,内核栈一般是两个页大小
1540 |
1541 | #### 12.11.1 单页内核栈
1542 |
1543 | 2.6内核之后,引入选项可以设置**单页内核栈**,激活这个选项,每个进程的内核栈只有一页大小
1544 |
1545 | 引入的原因有两点:
1546 |
1547 | 1. 可以让每个进程减少内存消耗,另外随着机器运行时间增加,寻找两个连续的物理页变得越来越困难
1548 | 2. 当内核栈使用两页时,中断处理程序使用它所中断进程的内核栈,而当进程使用单页的内核栈时,中断处理程序不放在进程内核栈中,而是放在**中段栈**中。
1549 |
1550 | > 中断栈:为每个进程提供运行中断处理程序的栈,一页大小。
1551 |
1552 | 总之,历史上,进程和中断处理程序共享一个栈空间,当1页栈的选项激活之后,中断处理程序获得了自己的栈。
1553 |
1554 | #### 12.11.2 在栈上工作
1555 |
1556 | 在任何函数,都要尽量节省内核栈的使用,让所有局部变量大小不要超过几百字节。栈溢出非常危险,所出的数据会直接覆盖紧邻堆栈末端的数据(例如`thread_info`结构就是紧邻进程堆栈末端)。
1557 |
1558 | 因此,推荐使用动态分配。
1559 |
1560 | ### 12.12 高端内存的映射
1561 |
1562 | #### 12.12.1 永久映射
1563 |
1564 | 映射给定的page结构到内核地址空间,使用如下函数
1565 |
1566 | ```c
1567 | void *kmap(struct page *page);
1568 | ```
1569 |
1570 | 函数在对于高端内存或者低端内存都能使用:
1571 |
1572 | 1. 如果page对应低端内存的一页,函数会单纯返回该物理页对应的虚拟地址
1573 | 2. 如果page对应高端内存页,函数会建立一个永久映射,在返回对应的虚拟地址
1574 | 3. 函数可以睡眠,只能在进程上下文中使用
1575 |
1576 | 当不再需要高端内存中的这一个页时,使用如下函数解除映射
1577 |
1578 | ```c
1579 | void kunmap(struct page *page);
1580 | ```
1581 |
1582 | #### 12.12.2 临时映射
1583 |
1584 | > 当必须创建映射而上下文不能睡眠是,内核提供了临时映射(原子映射)
1585 |
1586 | 临时映射可以用在像中断上下文一样的不能睡眠的地方,使用如下函数建立心是映射:
1587 |
1588 | ```c
1589 | void *kmap_atomic(struct page *page, enum km_type type);
1590 | ```
1591 |
1592 | * 函数禁止了内核抢占(因为映射对每个处理器都是唯一的???)
1593 |
1594 | ### 12.13 每个CPU的分配
1595 |
1596 | > SMP定义:一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。
1597 | >
1598 | > 支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的,每个CPU的数据存放在一个数组中,数组的每一个元素对应一个存在的处理器
1599 |
1600 | ```c
1601 | unsigned long my_percpu[NR_CPUS];
1602 | ```
1603 |
1604 | 访问cpu数据过程:
1605 |
1606 | ```c
1607 | int cpu;
1608 |
1609 | cpu = get_cpu(); //获取当前CPU,并且禁止内核抢占
1610 | data = my_percpu[cpu];
1611 | .... // 使用data的过程
1612 | put_cpu();
1613 | ```
1614 |
1615 | 代码中没有出现锁,因为数据对当前处理器是唯一的,没有其他处理器可以接触这个数据,没有多个处理器并发访问的问题,但是会有内核抢占的问题:
1616 |
1617 | 1. 如果代码被其他处理器抢占并重新调度,这是cpu变量data会变成无效,因为它对应了错误的处理器
1618 | 2. 如果另一个进程抢占了代码,有可能在一个处理器上并发访问data数据的问题
1619 |
1620 | 因此,在获取当前cpu时,就已经禁止了内核抢占。
1621 |
1622 | ### 12.14 新的每个CPU接口
1623 |
1624 | > 描述了一些为每个CPU分配内存的接口,详见**P223**
1625 |
1626 | ### 12.15 使用每个CPU数据的原因
1627 |
1628 | 使用每个CPU的好处有:
1629 |
1630 | 1. 减少数据锁定,每个处理器访问每个CPU数据,不用加锁
1631 | 2. 使用每个CPU数据大大减少了缓存失效,失效发生在处理器试图使他们的缓存保持同步时,如果一个处理器操作数据时,该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须刷新或者清理自己的缓存,频繁的缓存失效会造成**缓存抖动**
1632 |
1633 | 而使用每个CPU数据唯一的要求是需要**禁止内核抢占**
1634 |
1635 |
1636 |
1637 | ## 13 虚拟文件系统
1638 |
1639 | > 虚拟文件系统(VFS): 作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口
1640 |
1641 | ### 13.1 通用文件模型
1642 |
1643 | 在处理文件时,内核空间和用户空间使用的主要对象是不同的。在用户空间,一个文件由一个**文件描述符**标识,在打开文件时由内核分配,只在一个**进程内部有效**;内核空间中处理文件的关键是**inode**,每个文件或目录有且只有一个对应的inode,其中包含元数据(如访问权限、修改日期等)和指向文件数据的指针,但**inode不包含文件名**
1644 |
1645 | #### 13.1.1 inode
1646 |
1647 | > 目录可看做一种特殊的文件
1648 |
1649 | inode的成员分为两类:
1650 |
1651 | * 描述文件状态的元数据
1652 | * 保存实际文件内容的数据段(或指向数据的指针)
1653 |
1654 | 举例说明,内核查找/usr/bin/emacs的过程:
1655 |
1656 | * 查找起始于inode,对应于/目录,它对应一个inode,其数据段包含根目录下的各个目录项(这些目录项可能代表文件或目录),每个项由两个成员组成:
1657 | 1. 该目录项数据所在的inode编号
1658 | 2. 文件或目录的名称(文件名和inode之间的关联通过inode编号建立)
1659 | * 在/目录的inode数据段中查找名为usr的目录项,根据对应的inode编号定位/usr目录的inode;重复相同的步骤查找bin目录、emacs文件的inode
1660 | * 最后,emacs的inode数据段包含实际文件的内容
1661 |
1662 | **注意**:VFS实际的文件查找过程和以上基本一致,但会有些细节差异,如实际的实现使用了缓存加速查找操作,另外VFS需要和提供实际信息的底层文件系统通信
1663 |
1664 |
1665 |
1666 | #### 13.1.2 通用文件系统接口
1667 |
1668 | > 在UNIX系统中,**万物皆文件**
1669 | >
1670 | > VFS使得用户可以直接使用统一的系统调用,无需考虑具体的文件系统和物理介质
1671 |
1672 | 大多数设备都通过VFS定义的文件接口访问:
1673 |
1674 | * 字符和块设备
1675 | * 进程之间的管道
1676 | * 用于所有网络协议的套接字
1677 | * 用于交互式输入和输出的终端
1678 |
1679 | > 内核在底层文件系统接口上建立了一个抽象层,使得Linux能够支持各种文件系统。
1680 | >
1681 | > 实际文件系统的代码在统一的接口和数据结构下隐藏各自具体的实现细节,它们通过编程提供VFS所期望的抽象接口和数据结构
1682 |
1683 | ### 13.2 VFS结构
1684 |
1685 | > VFS由**文件和文件系统**两部分组成
1686 |
1687 | #### 13.2.1 结构概观
1688 |
1689 | 1. **文件的表示**
1690 |
1691 | 对底层文件系统的操作使用**函数指针**来实现,他们保存在两个结构中:
1692 |
1693 | * inode操作:创建链接、文件重命名、在目录创建文件、删除文件
1694 | * 文件操作:文件读写、设置文件位置指针、创建内存映射之类的操作
1695 |
1696 | 每个inode包含一个指向底层文件系统超级块对象的指针,用于执行inode本身的一些操作
1697 |
1698 | 文件和进程的联系:`task_struct`结构中的files数组,包含所有的打开文件(file结构),文件描述符作为数组的索引,同时file对象总包含一个指针指向用于**加速查找操作的目录项缓存dentry对象**
1699 |
1700 |
1701 |
1702 | 2. **文件系统和超级块信息**
1703 |
1704 | 超级块包含了文件系统的关键信息(块长度、最大文件长度等),还有读、写、操作inode的函数指针
1705 |
1706 | 内核建立了一个链表,包含了**所有活动的文件系统超级块实例**
1707 |
1708 | 超级块结构中包含一个列表,包含相关文件系统**所有修改过的inode**(*脏inode*),用于回写到存储介质(磁盘等)
1709 |
1710 | #### 13.2.2 Unix文件系统
1711 |
1712 | > 四个基本要素:文件、目录项、索引节点和安装点(挂载点)
1713 |
1714 | * 目录项:路径中的每一部分都被称为目录条目,统称为目录项
1715 | * 索引节点:Unix系统将文件的相关信息和文件本身这两个概念加以区分(如访问控制权限、大小、创建时间等),文件的相关信息(文件的元数据信息)被存储在一个单独的数据结构,称为索引节点(inode)
1716 | * 超级块:是一种包含文件系统控制信息的数据结构,这些信息称为文件系统数据元
1717 |
1718 | 
1719 |
1720 | ### 13.3 VFS对象及数据结构
1721 |
1722 | VFS的四个对象类型:
1723 |
1724 | * 超级块对象:代表具体的文件系统
1725 | * 索引节点对象:代表具体文件
1726 | * 目录项对象:代表目录项,是路径的一个组成部分
1727 | * 文件对象:代表进程打开的文件
1728 |
1729 | **注意**:VFS将目录作为一个文件来处理,不存在目录对象;目录项不同于目录
1730 |
1731 | > 每个对象中都包含一个操作对象,其中描述了内核针对主要对象可以使用的方法
1732 |
1733 | * `super_operations`对象:内核针对特定文件系统调用的方法,如`write_inode(), sync_fs()`
1734 | * `inode_operations`对象:内核针对特定文件调用的方法,如`create(), link()`
1735 | * `dentry_operations`对象:内核针对特定目录项所能调用的方法,如`d_compare(), d_delete()`
1736 | * `file_operations`对象:进程针对已打开文件所能调用的方法,如`read(), write()`
1737 |
1738 | **注意**:操作对象作为结构体,其中包含操作父对象的函数指针,实际的文件系统可以继承VFS提供的通用函数。
1739 |
1740 | #### 13.3.1 超级块
1741 |
1742 | > 各种文件系统都必须实现超级块对象,该对象存储特定文件系统的信息,对应于存放在磁盘**特定扇区**中文件系统超级块或者文件系统控制块。(非基于磁盘的文件系统,会在使用现场创建超级块并保存在内存中)
1743 |
1744 | 1. **超级块结构super_block**
1745 |
1746 |
1747 |
1748 | 主要成员:
1749 |
1750 | * s_dirty:脏inode的链表,在同步内存数据和底层存储介质时,使用该链表更加高效
1751 | * s_files:包含一系列file结构,列出了该超级块表示的文件系统所有打开的文件,内核在卸载文件系统时会参考该链表
1752 | * s_dev和s_bdev指定了底层文件系统的数据所在的块设备,前者使用了内核内部编号,后者指向block_device结构
1753 | * s_root:用于检查文件系统是否装载,如果为NULL,该文件系统是一个伪文件系统,只在内核可见,否则,在用户空间可见
1754 |
1755 | **注意**:超级块对象通过`alloc_super()`函数创建并初始化,在安装文件系统时,文件系统会调用这个函数从**磁盘**读取文件系统超级块,并将其中的数据**填充到内存中的超级块对象对应的结构体**中。
1756 |
1757 | 2. **超级块操作**
1758 |
1759 | 超级块对象中s_op指针,指向超级块的操作函数表,由`super_operations()`表示,如下图:
1760 |
1761 | 
1762 |
1763 | 
1764 |
1765 | 该结构中的操作会控制**底层文件系统实现获取和返回inode的方法**,不会改变inode的内容
1766 |
1767 | #### 13.3.2 inode对象
1768 |
1769 | > 索引节点对象:包含内核在操作文件系统或者目录时需要的全部信息(对于Unix风格的系统,直接从磁盘的索引节点读入),索引节点对象必须在**内存**中创建
1770 |
1771 | inode的结构如下:
1772 |
1773 | 
1774 |
1775 | 
1776 |
1777 | 上述的inode结构是在**内存**进行管理,其中包含实际介质中存储的inode没有的成员
1778 |
1779 | inode结构主要成员:
1780 |
1781 | * i_ino:唯一标识inode的编号
1782 | * i_count:访问该inode结构的进程数
1783 | * i_nlink:使用该inode的硬链接数目
1784 | * i_mode、i_uid和i_gid:文件访问权限和所有权
1785 | * i_rdev:设备号,用于找到struct block_device的一个实例
1786 | * 联合体
1787 | * i_bdev:指向块设备结构block_device
1788 | * i_pipe:实现管道的inode相关信息pipe_inode_info
1789 | * i_cdev:指向字符设备结构cdev
1790 | * i_devices:这个成员作为链表元素,使得块设备或者字符设备维护一个inode的链表
1791 |
1792 | 1. **索引节点操作**
1793 |
1794 | inode结构包含两个指针:i_op和i_fop,前者指向inode_operation结构,提供了inode相关的操作,后者指向file_operations,提供了文件操作(inode和file结构都包含了指向file_operations结构的指针)
1795 |
1796 |
1797 |
1798 | 2. **inode链表**
1799 |
1800 | 每个inode都有一个ilist成员,将inode存储到一个链表中
1801 |
1802 | 根据状态,inode分为三种:
1803 |
1804 | * inode存在于内存中,未关联到任何文件,不处于活动状态
1805 |
1806 | * inode存在于内存中,正在由一个或多个进程使用,两个计数器i_nlink和i_count都大于0,且文件内容和inode元数据和底层块设备相同
1807 |
1808 | * inode处于使用活动状态,内容已经改变,与存储介质上内容不同,inode是**脏的**
1809 |
1810 | 在fs/inode.c文件中内核定义了两个全局变量表头,inode_unused用于有效但非活动的inode,inode_in_use用于正在使用但是未改变的inode。**脏的inode**保存在一个特定于超级块的链表中
1811 |
1812 | #### 13.3.3 目录项缓存
1813 |
1814 | > Linux使用**目录项缓存**(dentry缓存)来快速访问文件的查找结果
1815 |
1816 | VFS在执行目录项操作时,会现场创建dentry实例,dentry实例没有对应的磁盘数据结构,并非保存在磁盘上,`dentry`结构体中没有是否被修改的标志(是否为脏、是否需要写会磁盘)
1817 |
1818 | 1. **dentry结构**
1819 |
1820 | dentry结构定义如下:
1821 |
1822 |
1823 |
1824 | 每个dentry代表路径中的一个特定部分,比如路径/bin/vi,其中/,bin,vi都是目录项,前两个是**目录**,最后一个是**普通文件**。在路径中,包含普通文件在内,每一项都是目录项对象。给定目录下的所有文件和子目录相关联的dentry实例,都归入到d_subdirs链表中,子目录节点的d_child作为链表元素
1825 |
1826 | dentry结构的**主要作用**:建立**文件名到inode之间映射关系的缓存**,涉及的结构有三个成员:
1827 |
1828 | * d_inode:指向inode实例的指针
1829 | * d_name:指定文件的名称,qstr结构存储了**实际的文件名、文件长度和散列值**
1830 | * d_iname:如果文件名由少量字符组成,保存在d_iname中(不是d_name),以加速访问
1831 |
1832 | 内存中所有活动的dentry实例保存在一个散列表中,使用fs/dcache.c中的全局变量dentry_hashtable实现,d_hash实现溢出链,用于解决哈希冲突,每个元素指向具有相同键值的目录项组成的链表头指针,这个散列表称为**全局dentry散列表**(内核系统提供给文件系统**唯一**的散列函数)
1833 |
1834 | **注意**:dentry对象不是表示文件的主要对象,这一职责分配给inode
1835 |
1836 | 2. **dentry缓存的组织**
1837 |
1838 | 每个由VFS发送到底层实现的请求,都会导致创建一个新的dentry对象,并保存请求结果
1839 |
1840 | dentry的**三种状态**:被使用、未使用和负状态
1841 |
1842 | * 被使用的目录项:对应一个有效的索引节点,`d_node`指向相应的索引节点,`d_count`代表使用者的数量;不能被丢弃
1843 | * 未被使用的目录项:对应有效的索引节点,但是`d_count`为0,仍然指向一个有效对象,被保存在缓存中
1844 | * 负状态的目录项:没有对应的有效索引节点,`d_node`为NULL,索引节点已被删除,或者路径不不再正确
1845 |
1846 | dentry对象**在内存中的组织**,涉及三个部分:
1847 |
1848 | * 一个全局散列表(dentry_hashtable)包含的所有dentry对象
1849 |
1850 | * **“被使用的”** 目录项链表:索引节点中`i_dentry`链接相关的目录项(一个索引节点可能有多个链接,对应多个目录项),因此用一个链表连接他们
1851 |
1852 | * **“最近被使用的”** 双向链表:包含未被使用和负状态的目录项对象(总是在头部插入新的目录项,需要回收内存时,会再尾部删除旧的目录项)
1853 |
1854 | **注意**:目录项释放后也可以保存在**slab缓存**中。
1855 |
1856 | * `dcache`一定意义上提供了对于索引节点的缓存(`icache`),和目录项相关的索引节点对象不会被释放(因为索引节点的使用计数>0),这样确保了索引节点留在内存中
1857 | * 文件访问呈现空间和时间的局部性:时间局部性体现在程序在一段时间内可能会访问相同的文件;空间局部性体现在同一个目录下的文件很可能都被访问。
1858 |
1859 | 3. **dentry操作**
1860 |
1861 | dentry_operations结构中保存了一些对dentry对象执行的函数指针,在不同的文件系统中可以各自实现
1862 |
1863 | 
1864 |
1865 | #### 13.3.4 特定于进程的信息
1866 |
1867 | > 三种结构体:`file_struct`, `fs_struct`, `namespace`
1868 |
1869 | 主要成员包括:
1870 |
1871 | * fs指向的`fs_struct`结构,保存进程的文件系统相关数据
1872 | * files指向的`files_struct`结构,包含当前进程的各个文件描述符
1873 | * mnt_ns指向的`namespace`结构,包含命名空间相关信息
1874 |
1875 |
1876 |
1877 | 1. **结构体 files_struct**
1878 |
1879 | 结构定义如下:
1880 |
1881 |
1882 |
1883 | 主要成员:
1884 |
1885 | * `fd_array`数组指针:指向已打开的文件对象,`NR_OPEN_DEFAULT`默认64,如果进程打开的文件超过64,内核将分配一个新数组,并且将`fdt`指针指向它
1886 | * 当访问的文件对象的数量小于64时,执行比较快,因为是对静态数组的操作;如果大于64,内核需要建立新数组,访问速度相对慢一些
1887 | * 管理员可以增大`NR_OPEN_DEFAULT`选项优化性能
1888 | * fdtable结构定义如下:
1889 |
1890 |
1891 |
1892 | 2. **结构体 fs_struct**
1893 |
1894 | 由进程描述符的`fs`域指向,包含**文件系统和进程相关的信息**,包含进程的当前工作目录(`pwd`)和根目录
1895 |
1896 |
1897 |
1898 | 其中dentry类型的成员指向目录名称,vfsmount类型成员表示已经装载的文件系统,成员如下:
1899 |
1900 | * umask成员:标准掩码,用于设置文件的权限
1901 | * root和rootmnt指定相关进程的根目录和文件系统
1902 | * pwd和pwdmnt指定**当前目录**和文件系统的vfsmount结构,在进程改变当前目录时,二者会动态改变(仅当进入一个新的挂载点的时候,pwdmnt才会变化)
1903 |
1904 | 3. **VFS命名空间**
1905 |
1906 | 单一的系统可以提供多个容器,容器彼此相互独立,从VFS角度来看,需要**针对每个容器分别跟踪装载的文件系统**
1907 |
1908 | **VFS命名空间**是所有已经装载、构成某个容器目录树的文件系统集合。通过fork或clone建立的进程会继承父进程的命名空间,可以通过设置CLONE_NEWS标志,建立新的命名空间
1909 |
1910 |
1911 |
1912 | 进程描述符中的`nsproxy`成员管理命名空间的处理,其主要成员是`mmt_namespace`域,它使得**每个进程在系统中看到唯一的文件系统**(唯一的根目录和文件系统结构层次)
1913 |
1914 |
1915 |
1916 | * count:使用计数器,指定了使用该命名空间的进程数
1917 | * root:指向根目录挂载的vfsmount实例
1918 | * list:一个双向链表的表头,保存了命名空间中所有文件系统的vfsmount实例,链表元素是vfsmount中的mnt_list成员
1919 |
1920 | 对于容器来说,命名空间的操作(如mount和umount)并不作用于内核的全局vfsmount结构,只操作当前容器中进程的命名空间实例。同时,改变**会影响共享同一个命名空间实例的所有进程(容器)**
1921 |
1922 | **注意**:
1923 |
1924 | * 对于多数进程,它们的描述符都指向自己独有的`files_struct`和`fs_struct`,除非使用克隆标志`CLONE_FILES`或者`CLONE_FS`创建的进程会共享这两个结构体
1925 |
1926 | * `namespace`结构体使用方法和前两种结构完全不同,默认情况下,所有进程共享同样的命名空间(都从相同的挂载表中看到同一个文件系统层次结构,除非在`cloen()`操作时使用`CLONE_NEWS`标志,才会给进程一个命名空间结构体的拷贝)
1927 |
1928 | #### 13.3.5 文件
1929 |
1930 | > 文件对象是已打开的文件在内存中的表示
1931 |
1932 | * 结构体`file`表示
1933 |
1934 | * 由`open()`系统调用创建,`close()`撤销
1935 | * 多个进程可以打开同一个文件,所以同一个文件存在多个对应的file实例
1936 | * file对象仅仅在观点上代表已打开文件,它反过来指向dentry对象,而dentry对象反过来指向inode
1937 |
1938 | 类似于目录项对象,文件对象没有对应的磁盘数据,通过`file`结构体中`f_dentry`指针指向相关的目录项对象,而目录项对象指向相关的索引节点,索引节点会记录文件是否为脏
1939 |
1940 | 结构体`file_operation`定义如下:
1941 |
1942 |
1943 |
1944 | 文件相关的操作方法和**系统调用**很类似,具体的文件系统可以为每一种操作方法实现各自的代码,如果存在通用操作,则使用通用操作
1945 |
1946 | #### 13.3.6 文件系统
1947 |
1948 | 所有文件系统都保存在一个**单链表**中,每个文件系统的名字存储为字符串。在文件系统注册到内核时,将逐个元素扫描该链表,直至到达尾部或者找到指定的文件系统
1949 |
1950 | 1. **结构体 file_system_type**
1951 |
1952 |
1953 |
1954 | 主要成员:
1955 |
1956 | * name:文件系统的名称,字符串
1957 |
1958 | * fs_flags:使用的标志,标明只读装载等
1959 |
1960 | * next:指向下一个file_system_type结构
1961 |
1962 | * get_sb函数:从磁盘读取超级块,在文件系统安装时,在内存中组装超级块对象
1963 |
1964 | * kill_sb函数:在不在需要某个文件系统类型时执行清理工作
1965 |
1966 | * fs_supers:同一类型文件系统的所有超级块结构存储在一个链表中,fs_supers是这个链表的表头
1967 |
1968 | **注意**:相同类型的多个文件系统实例,都只有一个对应的`file_system_type`实例
1969 |
1970 | 2. **结构体 vfsmount**
1971 |
1972 | 在文件系统实际被安装时,会有一个`vfsmount`结构体在安装点创建,每个装载的文件系统对应一个vfs_mount实例(代表一个**安装点**),`vfsmount`结构体中维护了各种链表,用于跟踪文件系统和所有安装点之间的关系
1973 |
1974 | 系统使用了散列表mount_hashtable(定义在fs/namespace.c中),链表元素是vfs_mount类型,**vfs_mount实例的地址和相关的dentry实例的地址**用来计算散列和(哈希值)
1975 |
1976 |
1977 |
1978 | ### 13.4 处理VFS对象
1979 |
1980 | #### 13.4.1 文件系统操作
1981 |
1982 | 1. **注册文件系统**
1983 |
1984 | 文件系统注册到内核时,是编译为模块,或者持久编译到内核。fs/super.c中的register_filesystem函数用来向内核注册文件系统,该函数扫描文件系统结构组成的单链表,直至到达链表尾部然后添加新的元素或者找到所需的文件系统
1985 |
1986 | 2. **装载和卸载**
1987 |
1988 | 装载操作开始于**超级块**的读取,file_system_type中保存的read_super函数指针返回一个类型为super_block的对象,用来在内存中表示超级块
1989 |
1990 | **mount系统调用**
1991 |
1992 | 入口点是sys_mount函数,在fs/namespace.c定义,代码流程如下图
1993 |
1994 | 
1995 |
1996 | 首先将装载选项(类型、设备和选项)从用户空间复制到内核空间,内核将控制权转给do_mount函数,该函数调用path_lookup找到装载点所在的dentry项
1997 |
1998 | do_mount充当一个多路分解器,将需要完成的工作委派给装载类型对应的各个函数,其中do_new_mount处理普通装载操作,它分为两部分:do_kern_mount和do_add_mount
1999 |
2000 | 
2001 |
2002 | * do_kern_mount首先使用get_fs_type找到对应的file_system_type实例,并扫描已注册文件系统的链表,返回正确的项,然后调用特定于文件系统类型的get_sb函数读取相关的超级块,返回struct super_block的实例
2003 |
2004 | * do_add_mount首先处理一些必须的锁定操作,确保同一个文件系统不会重复装载到同一位置,然后主要工作是在graft_tree函数,新装载的文件系统通过attach_recursive_mnt函数添加到父文件系统的命名空间
2005 |
2006 | 
2007 |
2008 | * nameidata用于将vfsmount实例和denrty实例聚集起来,该结构保存了装载点的dentry实例和装载之前该目录对应的vfsmount实例
2009 |
2010 | * mnt_set_mount确保新的vfsmount实例中的mnt_parent成员指向父文件系统的vfsmount实例,以及mnt_mountpoint成员指向装载点在父文件系统中的dentry实例,旧的dentry实例d_mounted值加一
2011 |
2012 | 
2013 |
2014 | * 函数commit_tree将新的vfsmount实例添加到**全局散列表**以及**父文件系统vfsmount实例中的子文件系统链表**
2015 |
2016 | 
2017 |
2018 |
2019 |
2020 | **umount**系统调用
2021 |
2022 | umount系统调用的入口点是fs/namespace.c中的sys_umount,如下图
2023 |
2024 | 
2025 |
2026 | * 首先,__user_walk找到装载点的dentry和vfsmount实例,主要的工作委派给do_umount函数
2027 | * 如果定义了特定于超级块的umount_begin函数,则调用它。例如,网络文件系统在卸载前,需要终止与远程文件系统的通信
2028 | * 如果装在的文件系统不再需要(通过使用计数判断),或者指定了MNT_DETACH来强制卸载文件系统,则调用umount_tree和release_mounts函数,前者将d_mounted减一,后者使用保存在mnt_mountpoints和mnt_parent中的数据,将环境恢复到文件系统装在之前的状态。同时被卸载的文件系统对应的数据结构,会从内核链表中移除
2029 |
2030 | #### 13.4.2 文件操作过程
2031 |
2032 | 1. **查找inode**
2033 |
2034 | 主要操作是根据给定的文件名查找inode,nameidata结构用于向查找函数传递参数,并保存查找结果,该结构定义如下
2035 |
2036 | 
2037 |
2038 | 主要成员:
2039 |
2040 | * 查找完成后,dentry和mnt包含了找到的文件系统项的数据
2041 | * last包含了查找的名称,包含字符串和散列值
2042 | * flags保存了相关标志
2043 |
2044 | 内核使用path_lookup函数查找路径和文件名,该函数需要一个nameidata类型的指针,用作临时结果的“暂存器”
2045 |
2046 | 
2047 |
2048 | 内核使用nameidata实例规定查找的起点,如果名称以/开头,使用当前根目录的dentry实例和vfsmount实例;否则,从当前进程的task_struct获得当前工作目录的数据
2049 |
2050 | 主要处理在link_path_walk函数,它调用__link_path_walk函数,该函数代码流程图如下
2051 |
2052 | 
2053 |
2054 | 该函数由一个大的循环组成,逐分量处理文件或路径名(路径根据/被分解为多个分量),每个循环的主要逻辑为:
2055 |
2056 | * 将nameidata实例的mnt和dentry成员设置为根目录或工作目录对应的数据项
2057 | * 对目录进行权限检查,判断进程是否允许进入该目录、
2058 | * 路径名称是逐字母扫描,根据/将路径分为多个路径分量,每个循环处理一个路径分量;路径分量的每个字符传给partial_name_hash函数,用于计算散列和,将他保存到qstr实例中
2059 | * 处理(**.**),直接跳过
2060 | * 处理(**..**),委派给follow_dotdot函数,当前目录为进程的根目录时,没有效果,否则,分两种情况:
2061 | * 如果当前目录不是一个**装载点**的根目录时,将当前dentry对象的d_parent成员作为新的目录
2062 | * 如果是已装载文件系统的根目录,利用保存在mnt_mountpoint和mnt_parent中的信息定义下一个dentry和vfsmount对象。follow_mount和lookup_mnt用于取得所需的信息
2063 | * 如果路径分量是一个普通文件,需要区分两种情况进行处理,数据位于dentry缓存 或者 需要文件系统底层实现进行查找,函数do_lookup负责区分两种情况,返回所需的dentry实例
2064 | * 最后一步:判断该分量是否为符号链接(方法:只有勇于符号链接的inode,其inode_operations中才包含lookup函数,否则为NULL)
2065 |
2066 | 最后一个分量对应的dentry作为函数link_path_walk的返回结果
2067 |
2068 | 2. **打开文件**
2069 |
2070 | 标准库的open函数用于打开文件,该函数使用同名的open系统调用,调用了fs/open.c中的sys_open函数,代码流程图如下
2071 |
2072 | 
2073 |
2074 | * force_o_largefile检查是否应该不考虑用户层传递的标志
2075 | * 接下来的主要处理是在do_sys_open函数
2076 | * 调用get_unused_fd_flags查找未使用的文件描述符
2077 | * 根据open参数中的文件路径名称,查找对应的inode,主要是在do_file_open函数
2078 | * open_namei函数调用path_lookup函数查找inode并执行几个额外检查
2079 | * nameidata_to_filp初始化预读结构,将新创建的file实例放置到超级块的sfiles链表中,并调用底层文件系统随影file_operations中的open函数
2080 | * fd_install函数将file实例放置到进程task_struct结构的files->fd数组中
2081 | * 最后控制权转到用户进程,返回进程描述符
2082 |
2083 | 3. **读取和写入**
2084 |
2085 | 文件打开之后,使用read和write系统调用进行读写,入口函数是sys_read和sys_write(都是在fs/read_write.c实现)
2086 |
2087 | * read
2088 |
2089 | 函数需要三个参数:文件描述符、保存数据的缓冲区和指定读取字符数的长度参数,代码流程如下图
2090 |
2091 | * 根据文件描述符,fget_light函数(fs/file_table.c中)从task_struct结构中找到相关的file实例
2092 | * file_pos_read找到文件当前的读写位置(返回file->f_pos的值)
2093 | * vfs_read函数调用特定于文件的读取函数file->f_op->read,如果该函数没有实现,则调用一般的辅助函数do_sync_read
2094 | * file_pos_write函数记录文件内部新的读写位置
2095 |
2096 | **注意**:读取数据涉及到复杂的缓冲区和缓存系统,详见[文件系统操作]()
2097 |
2098 | 
2099 |
2100 |
2101 | ### 13.5 VFS通用标准函数
2102 |
2103 | 对于文件读写的操作对于所有文件系统来说大同小异:如果数据所在的块是已知的,则首先查询页缓存;如果未保存在其中,则相对应的块设备发出请求。
2104 |
2105 | 为了防止每个文件系统在这部分的代码冗余,大多数文件系统将file_operations中的read和write分别指向do_sync_read和do_sync_write标准例程
2106 |
2107 | #### 13.5.1 通用读取例程
2108 |
2109 | do_sync_read例程同步地读取数据,保证在函数返回时,所需数据已经在内存中,实际的读取操作委托给一个异步例程。简化后函数如下
2110 |
2111 | 
2112 |
2113 | init_sync_kiocb函数初始化一个kiocb实例,用于控制异步输入/输出操作
2114 |
2115 | 实际工作委托给特定于文件系统的异步读取操作,保存在struct file_operations的aio_read成员,它指向generic_file_aio_read,该函数是异步的,无法保证返回时数据已经读取完毕
2116 |
2117 | 返回值-EIOCBQUEUED表示读请求正在排队,尚未处理,此时。wait_on_sync_kiocb将一直等待,直至数据进入内存。该函数根据创建的控制块,来检查请求的完成情况;在等待时,进程将进入睡眠状态,使得其他进程可以利用CPU
2118 |
2119 | 1. **异步读取**
2120 |
2121 | mm/filemap.c中的generic_file_aio_read异步读取数据,代码流程图如下
2122 |
2123 | 
2124 |
2125 | 在generic_segment_checks中确保读请求包含的参数有效之后,需要区分两种不同的读模式:
2126 |
2127 | * 如果设置了O_DIRECT标志,则直接读取数据,不使用页缓存,此时调用generic_file_direct_IO
2128 | * 否则调用do_generic_file_read,然后调用do_generic_mapping_read函数,该函数将**对文件的读操作转换为对映射的读操作**
2129 |
2130 | 2. **对映射读取**
2131 |
2132 | do_generic_mapping_read代码流程图如下
2133 |
2134 | 
2135 |
2136 | 该函数使用映射机制,将文件中需要读取的部分映射到内存中,它由一个大的无限循环组成,持续向内存页读入数据,直至所有文件数据(不在任何缓存中的数据)都传输到内存中
2137 |
2138 | 每个循环执行过程为:
2139 |
2140 | * find_get_page检查页是否在页缓存中
2141 | * 如果没有,调用page_cache_sync_readahead发出同步预读请求,然后再次调用find_get_page保证数据已经进入缓存,如果此次调用还是没有进入缓存,则调用no_cached_page函数
2142 | * 否则,如果设置了页标志PG_readahead,则发出异步预读请求,调用page_cache_async_readahead
2143 | * 虽然页是在页缓存中,但数据不一定是最新的,需要调用Page_Uptodate检查,如果页不是最新的,必须使用mapping->a_ops->readpage再次读取,该函数指针通常指向mpage_readpage,调用之后,可以确保页中填充的数据是最新的
2144 | * 接下来使用mark_page_accessed标记对该页的访问(在需要从物理内存换出数据时,这个标记会用于判断页的活动程度)
2145 | * 最后,actor例程将适当的页映射到用户地址空间
2146 |
2147 | 如果预读机制没有将所需的页读入内存,需要调用do_generic_mapping_read的no_cached_page函数,其代码流程图如下
2148 |
2149 | 
2150 |
2151 | 其中,page_cache_alloc_cold分配了一个缓存冷页,通过add_to_page_cache_lru将该页插入页缓存的LRU链表中,详见 [分配页](# 16.4.1 分配页),映射提供的mapping -> a_ops -> readpage用于读取数据,通常该函数指向mpage_readpage,详见 [对整页的操作](# 16.4.4 对整页的操作)。最后,mark_page_accessed告诉统计系统该页已经访问过
2152 |
2153 | #### 13.5.2 缺页异常处理
2154 |
2155 | 内存映射通常调用VFS提供的filemap_fault标准例程来读取未保存在缓存中的页,如下图是该函数的代码流程图
2156 |
2157 | 
2158 |
2159 |
2160 |
2161 | ## 14 块I/O层
2162 |
2163 |
2164 |
2165 | ## 15 进程地址空间
2166 |
2167 |
2168 |
2169 | ## 16 页缓存和块缓存
2170 |
2171 | 内核为块设备提供了两种通用的缓存方案:
2172 |
2173 | * 页缓存:针对以页为单位的所有操作,例如**内存映射技术**,负责了**块设备的大部分工作**
2174 | * 块缓存:以块为操作单位,存取的单位是设备的各个块,由于块长度取决于特定的文件系统,块缓存能处理不通长度的块
2175 |
2176 | 缓冲区曾经是块设备进行I/O操作的传统方法,目前只用于支持很小的读取操作,对于块传输的标准数据结构变为struct bio,这种方式可以合并统一请求中后续的块,加速处理,但是对于**单个块的操作**,缓冲区仍然是首选,例如经常按块读取元数据的系统。
2177 |
2178 | 很多场合下,页缓存和块缓存结合使用(一个缓存的页在写操作期间划分为不同的缓冲区,在更细的粒度识别出被修改的部分,在数据写回时,只需要回写被修改的部分,不需要整页传输)
2179 |
2180 | ### 16.1 页缓存的结构
2181 |
2182 | #### 16.1.1 管理和查找缓存的页
2183 |
2184 | Linux采用**基数树**的数据结构管理页缓存中的页,如下图
2185 |
2186 | 
2187 |
2188 |
2189 |
2190 | 树的根有一个简单的数据结构表示,包含了树的高度(所包含节点的最大层次数目)和一个指针,指向组成树的第一个节点的数据结构
2191 |
2192 | 树的节点具备两种**搜索标记**,二者用于指定给定页当前是否为脏(即页的内容和后备存储器的数据不同)或该页是否正在向底层块设备回写。标记会一直向上设置到根节点,如果某个层次 *n+1* 的节点设置了某个标记,那么 *n* 层次的父节点也会设置该标记。好处是**内核可以判断在某个范围内是否有一页或多页设置了某个标记位**
2193 |
2194 | #### 16.1.2 回写修改的数据
2195 |
2196 | 由于页缓存的存在,写操作不是直接对块设备进行,而是在内存中进行,修改的数据首先被收集起来,然后被传输到更低的内核层,在那里对写操作进一步优化,具体的优化过程详见 [块I/O层](##14 块I/O层)。这里从页缓存的视角来看,需要确定何时回写?
2197 |
2198 | 内核提供如下几种同步方案:
2199 |
2200 | * 内核守护进程pdflush周期性地同步,他们扫描缓存中的页,将超出一定时间没有与底层块设备同步的页写回
2201 | * 如果缓存中修改的数据项数目在短期内内明显增加,内核会主动激活pdflush进程
2202 | * 提供了系统调用给用户调用来写回未同步的数据,常用的有sync调用
2203 |
2204 | 为管理可以按整页处理和缓存各种不同对象,内核使用了**“地址空间”**抽象,将内存中的页和特定的块设备关联起来,每个地址空间都有一个“宿主”,所谓其数据来源,一般用inode表示
2205 |
2206 | 一般修改文件或者按页缓存的对象时,只会修改页的一部分,为节约时间,在写操作期间,会将缓存中的每一页划分为较小的单位,称为**缓冲区**,回写过程中以缓冲区为单位来操作
2207 |
2208 | ### 16.2 块缓存的结构
2209 |
2210 | Linux早期版本只包含块缓存,用于加速文件操作和系统性能,底层块设备的块缓存存在内存的缓冲区中,可以加速读写(实现部分包含在fs/buffers.c中)
2211 |
2212 | 与内存页相比,块比较小而且长度可变(依赖于使用的块设备或者文件系统)
2213 |
2214 | 文件系统在处理元数据时,一般会使用块缓存;而裸数据的传输则按页进行
2215 |
2216 | 缓冲区的实现基于页缓存,Linux 2.6之前,缓冲区使用缓冲区头buffer head结构实现,在2.6之后,不再使用缓冲区头结构,而是使用bio结构
2217 |
2218 | ### 16.3 地址空间
2219 |
2220 | 地址空间建立了缓存数据与后备存储器之间的关联,实现了两个单元之间的转换机制
2221 |
2222 | * 内存中的页关联到每个地址空间,这些页表示缓存的内容
2223 |
2224 | * 后备存储器指定了填充地址空间中页的数据的来源,它是虚拟内存中区域到后备存储器(块设备)上对应位置的映射
2225 |
2226 | #### 16.3.1 数据结构
2227 |
2228 | 地址空间的基础是address_struct结构,定义如下:
2229 |
2230 | 
2231 | 
2232 |
2233 | 主要成员:
2234 |
2235 | * 与地址空间管理的区域之间的关联,通过两个成员建立:host指向inode实例,指定了后备存储器;一个基数树的根page_tree列出了地址空间中所有的物理内存页
2236 | * i_mmap是一棵树的根节点,包含了与该inode相关的所有普通内存映射,该树的作用在于,支持查找给定区间至少一页的所有内存区域
2237 |
2238 | 
2239 |
2240 | * backing_dev_info是一个指针,指向另一个结构,包含了与地址空间相关的后备存储器的有关信息(后备存储器是指与地址空间相关的外部设备,用作地址空间中信息的来源,通常为**块设备**)
2241 | * aps指针指向address_space_operation结构,其中包含了一组函数指针,指向用于处理地址空间的特定操作
2242 |
2243 | 地址空间与内核其他部分的关联如下图
2244 |
2245 | 
2246 |
2247 | #### 16.3.2 页树
2248 |
2249 |
2250 |
2251 | ### 16.4 页缓存的实现
2252 |
2253 | #### 16.4.1 分配页
2254 |
2255 | page_cache_alloc用于为一个即将加入页缓存的新页分配数据结构,加上后缀_cold的函数是获取一个冷页
2256 |
2257 | 
2258 |
2259 | * 首先page_cache_alloc将工作委托给alloc_pages,它从**伙伴系统**获得一个页帧
2260 |
2261 | * 接下来将新页添加到页缓存,这是在add_to_page_cache函数中实现,如下所示,radix_tree_insert将与页相关的page实例插入到地址空间的基数树,在页缓存中的索引和指向所属地址空间的指针保存在page的成员index和mapping中
2262 |
2263 | 
2264 |
2265 | 内核还提供了另一个可选的参数add_to_page_cache_lru,他首先调用add_to_page_cache向地址空间的页缓存添加一页,然后使用lru_cache_add函数将该页加入到系统的LRU缓存
2266 |
2267 | #### 16.4.2 查找页
2268 |
2269 | 使用基数树判断给定页是否已经缓存,使用find_get_page实现该功能
2270 |
2271 | 
2272 |
2273 | 其中radix_tree_lookup用于查找位于给定偏移量的页,在找到页之后,page_cache_get将页的引用计数加1
2274 |
2275 | #### 16.4.3 在页上等待
2276 |
2277 | 内核经常要在页上等待,直至其状态改变为预期值。例如,数据同步时需要确保对于某页的回写已经结束,此时内存页的内容和底层块设备是相同的。处于回写过程中的页会设置PG_writeback标志位
2278 |
2279 | 内核提供了wait_on_page_writeback函数,用于等待页的该标志位清除
2280 |
2281 | 
2282 | 
2283 |
2284 | wait_on_cahe_bit安装一个等待队列,进程可以在上面睡眠,直至PG_writeback标志位清除
2285 |
2286 | 另外也有等待页解锁的需求,使用函数wait_on_page_locked实现
2287 |
2288 | #### 16.4.4 对整页的操作
2289 |
2290 | 内核在块设备和内存之间传输数据时,相关的算法和数据结构都是以页为基本单位,而逐个缓冲区/块的传输会对性能产生负面影响。因此,在再重新设计块层的过程中,内核版本2.5引入**BIO**,来替代缓冲区,用于处理与块设备的数据传输。内核添加了4个函数,来支持读写一页或多页
2291 |
2292 | 
2293 |
2294 | 其中,writeback_control用于精确控制回写操作的选项
2295 |
2296 | 这4个函数共同之处:都是构建一个BIO实例,用于对块层进行传输
2297 |
2298 | 以mpage_readpages为例,该函数需要nr_pages个page实例,以链表的形式通过参数pages传递进去,mapping是相关的地址空间,get_block用于查找匹配的块地址
2299 |
2300 | 该函数首先遍历所有的page实例,在循环的每一遍,首先将该页添加到地址空间相关的页缓存中,然后创建一个bio请求,从块层读取所需的数据
2301 |
2302 | 
2303 | 
2304 | 
2305 |
2306 | 在do_mpage_readpage建立bio请求时,会包含此前各页的BIO数据,以构造一个合并的请求(将几页的读取合并到一个请求,而不是每页一个请求)
2307 |
2308 | 如果在循环结束时,do_mpage_readpage留下一个未处理的BIO请求,则提交该请求
2309 |
2310 | 
2311 |
2312 | #### 16.4.5 页缓存预读
2313 |
2314 | 页缓存预读不是由页缓存独立完成的,还需要VFS和内存管理层的支持
2315 |
2316 | 预读在内核的几处都有涉及
2317 |
2318 | * do_generic_mapping_read:一个内核通用的读取例程
2319 | * filemap_fault函数:缺页异常处理程序,负责为内存映射读取缺页
2320 |
2321 | 下面以do_generic_mapping_read为例,来考察预读的具体过程
2322 |
2323 | 
2324 |
2325 | 假定进程已经打开了一个文件,准备读取第一页,该页尚未读入页缓存,此时不会只读入一页,而是顺序读取多页,内核调用page_cache_sync_readahead读取一行中的8页(数字8是具体说明),第一页对于do_generic_mapping_read来说是立即可用的,而在实际需要之前就被读入页缓存的页,处于**预读窗口**中
2326 |
2327 | 之后进程继续读取接下来的页,在访问第6页(6是举例说明)时,内核检测到在该页设置了PG_Readahead标志。此时触发了一个异步操作,会在后台读取若干页(由于还有两页可用,所以不需要同步读取,但在后台进行的I/O操作需要确保进一步读取文件时,相关页已经读入页缓存)。page_cache_async_read函数负责发出异步读请求,它又会将窗口中的一页标记为PG_Readahead,在进程遇到该页时,又会触发异步读取,以此类推
2328 |
2329 | 最重要的问题在于**预测预读窗口的最优长度**。因此,内核会记录每个文件上一次的设置,使用file_ra_state关联到每个file实例
2330 |
2331 | 
2332 |
2333 | 主要成员:
2334 |
2335 | * start:表示页缓存开始预读的位置
2336 | * size:表示预读窗口的长度
2337 | * async_size:表示剩余预读页的最小值,如果预读窗口中的页数等于这个值,则会触发异步预读
2338 | * ra_pages:表示预读窗口的最大长度,内核读入的页数可以小于这个值,但是不能超过
2339 | * prev_pos:表示前一次读取时,最后访问的位置
2340 |
2341 | 预读机制的实现涉及如下几个函数
2342 |
2343 | 
2344 |
2345 | 其中,ondemand_readahead例程负责实现预读策略(即判断预读多少当前不需要的页),在确定预读窗口的长度之后,调用ra_submit,将技术性问题委托给__do_page_cache_readhead函数,其中页是在页缓存中分配的,而后由块层填充
2346 |
2347 |
--------------------------------------------------------------------------------
/Linux内核设计与实现.md:
--------------------------------------------------------------------------------
1 | [TOC]
2 |
3 |
4 | # Linux内核设计与实现
5 |
6 | ## 第3章 进程管理
7 |
8 | ### 3.1 进程
9 |
10 | > 进程:处于执行期的程序以及相关资源(打开的文件、挂起的信号、内核内部数据、处理器状态等)的总称
11 | >
12 | > 线程:是在进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,不是进程
13 | >
14 | > Linux不区分进程和线程,对它来说,线程只不过是一种特殊的进程而已
15 |
16 | 现代操作系统的两种**虚拟机制**:
17 |
18 | * 虚拟处理器:给进程一种假象,让它觉得自己在独享处理器
19 | * 虚拟内存:让进程在分配和管理内存时觉得自己拥有整个系统的内存资源
20 |
21 | **注意**:线程之间可以共享虚拟内存,但是都拥有自己的虚拟处理器
22 |
23 | ### 3.2 进程描述
24 |
25 | 内核把进程的列表存放在叫做**任务队列**的双向循环链表中。链表中的每一项类型为`task_struct`,称为**进程描述符**的结构,描述了一个具体进程的所有信息
26 |
27 | #### 3.2.1 分配进程描述符
28 |
29 | Linux通过slab分配器分配`task_struct`结构,这样能够**对象复用**和**缓存着色**。
30 |
31 | 每个任务的`thread_info`结构在它的内核栈尾端分配,其中`task`域存放的是指向该任务实际`task_struct`的指针
32 |
33 | ```c
34 | struct thread_info {
35 | struct task_struct *task;
36 | struct exec_domain *exec_domain;
37 |
38 | ...
39 | };
40 | ```
41 |
42 | #### 3.2.2 进程描述符的存放
43 |
44 | 在内核中,访问任务需要获取指向`task_struct`结构的指针,通过`current`宏查找到当前进程的进程描述符,这个查找的**速度**很重要
45 |
46 | 有的硬件体系结构拿出一个专门的寄存器存放当前进程的`task_struct`指针,而有些像x86的体系结构(寄存器不太富余),就只能在**内核栈的尾部**创建`thread_info`结构,通过计算偏移量间接找到`task_struct`结构
47 |
48 | #### 3.2.3 进程状态
49 |
50 | 进程描述符中的`state`域描述了进程的当前状态。系统中进程的状态包括:
51 |
52 | * **TASK_RUNNING**(运行或就绪):进程是可执行的
53 | * **TASK_INTERRUPTIBLE**(可中断睡眠)
54 | * **TASK_UNINTERRUPTIBLE**(不可中断睡眠)
55 | * **__TASK_TRACED**:被其他进程跟踪的进程,例如ptrace调试的程序
56 | * **__TASK_STOPPED**:被暂停执行的任务,通常在接收到**SIGSTOP**,**SIGTSTP**,**SIGTTIN**,**SIGTTOU**等信号时
57 |
58 | #### 3.2.4 设置当前状态
59 |
60 | 使用`set_task_state(task,state)`函数
61 |
62 | #### 3.2.6 进程家族树
63 |
64 | 所有的进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本(initsctript)并执行其他的相关程序,最终完成系统启动的整个过程
65 |
66 | 每个`task_struct`结构都包含一个指向其父进程`task_struct`结构的`parent`指针,还包含一个`children`的子进程链表
67 |
68 | ### 3.3 进程创建
69 |
70 | Unix将进程的创建分解到两个单独的函数中去执行:fork()和exec()
71 |
72 | 首先,fork()通过拷贝当前进程创建一个子进程,exec()负责读取可执行文件并将其载入地址空间开始运行
73 |
74 | #### 3.3.1 写时拷贝
75 |
76 | Linux的fork()使用**写时拷贝**页实现,是一种推迟甚至免除拷贝数据的技术,在创建子进程时,内核并不复制整个进程地址空间,而是让父子进程共享一个拷贝,只有在写入的时候,数据才会被复制。在页根本不会被写入的情况下,例如fork()之后马上exec(),进程的地址空间就不用复制了
77 |
78 | fork()的实际开销:复制父进程的页表以及给子进程创建唯一的进程描述符
79 |
80 | #### 3.3.2 fork()
81 |
82 | Linux通过clone()实现fork(),clone()通过一系列参数指明父子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone(),然后在clone()中调用do_fork()
83 |
84 | do_fork()调用copy_process()函数,然后让进程运行。copy_process()函数的过程:
85 |
86 | * 调用`dup_task_struct()`为进程创建一个内核栈、thread_info结构和task_struct,与父进程的值相同,此时父子进程的描述符是相同的
87 | * 检查创建子进程后,当前用户拥有的进程数不超过分配资源限制
88 | * 子进程开始将自己与父进程区分开:进程描述符内的很多成员清0或者初始化,大部分的数据未被修改
89 | * 子进程状态设置为UNINTERRUPTIBAL,保证它不会投入运行
90 | * copy_process()调用copy_flags()更新task_struct的flags成员:其中代表进程是否拥有超级用户权限的PF_SUPERPRIV标志清0,代表进程还没有调用exec()函数的PF_FORKNOEXEC的标志被设置
91 | * 调用alloc_pid()为子进程分配PID
92 | * 根据clone()传递进来的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。(一般这些资源会被进程的**所有线程共享**)
93 | * 最后,copy_process()做扫尾工作并返回一个指向子进程的指针
94 |
95 | copy_process()返回到do_fork()函数,如果copy_process()返回成功,新创建的子进程被唤醒并投入运行
96 |
97 | **注意**:内核有意选择子进程首先执行,因为一般子进程会调用exec()函数,这样可以避免写时拷贝的额外开销
98 |
99 | #### 3.3.3 vfork()
100 |
101 | 除了**不拷贝父进程的页表项**,vfork()和fork()的功能相同
102 |
103 | ### 3.4 线程在Linux中的实现
104 |
105 | Linux实现线程的机制非常独特,从内核的角度来说,并没有线程这个概念,Linux把所有的**线程当做进程**来实现。线程仅仅被视为一个与其他进程共享某些资源的进程,拥有唯一隶属于自己的`task_struct`。
106 |
107 | Windows和Sun Solaris等系统都提供了专门支持线程的机制(将线程称为**轻量级进程**),相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于Linux,线程只是**进程间共享资源的一种手段**。
108 |
109 | 举例说明:对于一个包含四个线程的进程,在提供专门线程支持的系统,通常会有一个包含指向四个不同线程的指针的进程描述符,该描述符负责描述像地址空间、打开的文件等共享资源。而Linux只是创建四个进程并分配四个普通的`task_struct`结构,并指定它们共享某些资源。
110 |
111 | #### 3.4.1 创建线程
112 |
113 | 线程的创建和普通进程类似,只是需要在调用clone()时传递一些参数标志来指明共享的资源:
114 |
115 | ```c
116 | clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
117 | ```
118 |
119 | 调用的结果和fork()差不多,只是父子进程**共享地址空间、文件系统资源、打开的文件描述符和信号处理程序**
120 |
121 | 传递给clone()的参数标志决定了**新创建进程的行为方式和父子进程之间共享的资源种类**,详见**P29 表3-1**
122 |
123 | #### 3.4.2 内核线程
124 |
125 | > 内核线程用于内核在后台执行一些任务,他们是独立运行在内核空间的标准进程
126 | >
127 | > 内核线程和普通进程的区别是:**内核线程没有独立的地址空间**(指向地址空间的mm指针为NULL),它们只在内核空间运行,不切换到用户空间。
128 |
129 | 例如软中断ksoftirqd和flush都是内核线程的例子
130 |
131 | 内核是通过从kthreadd内核进程衍生出所有新的内核线程,从现有内核线程创建一个新的内核线程的方法如下:
132 |
133 | ```c
134 | struct task_struct *thread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
135 | ```
136 |
137 | 新的内核线程是由kthreadd进程通过clone()系统调用创建,它们将运行threadfn函数,传递的参数是data,进程命名为namefmt。
138 |
139 | 新创建的进程处于不可运行状态,需要通过wake_up_process()唤醒来运行
140 |
141 | ```c
142 | struct task_struct *thread_run(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
143 | ```
144 |
145 | `thread_run`方法先调用`thread_run`方法,然后调用`wake_up_process()`
146 |
147 | 内核线程启动后一直运行直到调用`do_exit()`退出,或者内核的其他部分调用`kthread_stop()`退出(传递给kthread_stop()的参数是kthread_create()返回的task_struct结构地址)
148 |
149 | ```c
150 | int kthread_stop(struct task_struct *k)
151 | ```
152 |
153 | ### 3.5 进程终结
154 |
155 | 进程终结的几种情况:
156 |
157 | * 显式的调用`exit()`
158 | * 隐式地在某个程序的主函数返回(C语言在main()函数的返回点防止调用exit()的代码)
159 | * 接收到不能处理也不能忽略的信号或者异常时,被动地终结
160 |
161 | 不管进程如何终结,大部分都是靠`do_exit()`(定义在kernel/exit.c)来完成,它的主要工作包括:
162 |
163 | * 将task_struct的标志成员设置为PF_EXITING
164 | * 调用`del_timer_sync()`删除任一内核定时器,确保没有定时器在排队,且没有定时器处理程序在运行
165 | * 如果BSD记账功能开启,调用`acct_update_intergrals()`来输出记账信息
166 | * 调用`exit_mm()`函数释放进程占用的mm_struct,如果没有别的进程在使用它(没有被共享),就彻底释放它们
167 | * 调用`sem__exit()`函数,如果进程排队等候IPC信号,则它离开队列
168 | * 调用`exit_files()`和`exit_fs()`来分别递减文件描述符和文件系统数据的引用计数,如果引用计数降为0,就可以直接释放
169 | * 接着将存放在task_struct的exit_code成员中的任务退出代码置为`exit()`提供的退出代码。**退出代码存放在这里供父进程检索**
170 | * 调用`exit_notify()`向父进程发送信号,给自己的子进程重新找**养父**(为进程组的其他线程或者init进程),并把进程的状态(task_struct中的exit_state)设置为EXIT_ZOMBIE
171 | * 调用`schedule()`切换到新的进程,因为EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程执行的最后一段代码。`do_exit()`永不返回
172 |
173 | 至此,与进程相关联的所有资源都释放掉了(假设该进程是这些资源的唯一使用者),进程不可运行且处于EXIT_ZOMBIE状态,他占用的所有内存包括**内核栈、thread_info结构和task_struct结构**。此时存在的唯一目的就是向它的父进程提供信息用于检索,父进程通知内核都是无关的信息后,进程所持有的剩余内存被释放,归还给系统使用
174 |
175 | #### 3.5.1 删除进程描述符
176 |
177 | 在调用`do_exit()`后,进程处于僵死状态不再运行,但是系统保留了它的进程描述符,这样可以**让系统能在进程中杰后仍能获取它的信息**。可以看到,**进程终结时所做的清理工作和进程描述符的删除是分开执行的**,在父进程获得已终结的子进程信息后,通知内核它不关注这些信息后,子进程的task_struct结构被释放
178 |
179 | 回收子进程状态是通过wait()一族函数实现,他们都是通过唯一的系统调用`wait4()`来实现,它首先会挂起调用它的进程,直到有一个子进程退出,此时函数返回子进程的PID,调用**该函数提供的指针指向子进程的退出代码**
180 |
181 | 当需要释放进程描述符时,会调用`release_task()`函数,它的工作包括:
182 |
183 | * 调用`__exit_signal()`,该函数调用`_unhash_process()`,后者再调用`detach_pid()`从pidhash上删除该进程,同时从任务列表中删除该进程
184 | * `__exit_signal()`函数释放目前僵死进程所使用的所有剩余资源,进行最终统计和记录
185 | * 如果这个进程是进程组的最后一个进程,且领头进程(进程组首进程)已经死掉,那么`release_task()`就通知僵死的领头进程的父进程
186 | * `release_task()`调用`put_task_struct()`释放进程内核栈和thread_info结构所占的内存页,并释放task_struct占用的slab高速缓存
187 |
188 | 至此,进程描述符和所有进程独享的资源全部释放
189 |
190 | #### 3.5.2 孤儿进程造成的进退维谷
191 |
192 | 当父进程在子进程之前推出时,需要保证子进程找到一个新的父进程,否则这些孤儿进程就会在退出时一直处于僵死状态。
193 |
194 | 解决方法是:给子进程在当前进程组找一个进程作为父亲,如果不行,就让init进程作为父进程
195 |
196 | 在`do_exit()`中会调用`exit_nodify()`,该函数调用`forget_original_parent()`,后者再调用`find_new_reaper()`进程寻父过程。
197 |
198 | 代码中会遍历两个链表:**子进程链表和ptrace子进程链表**,给每个子进程设置新的父进程。
199 |
200 | **注意**:当一个进程被跟踪时,它的临时父亲被设置为调试进程,如果他们真正的父进程退出,系统会为它及其兄弟进程找一个父进程。以前的内核版本中需要遍历系统所有的进程来找到这些子进程,现在只需要遍历这个单独ptrace的子进程链表,减轻了遍历的时间消耗
201 |
202 | ## 第4章 进程调度
203 |
204 | > 进程调度程序:在可运行态进程之间分配有限处理器时间资源的**内核子系统**。
205 |
206 | ### 4.1 多任务
207 |
208 | > 多任务操作系统是同时并发地交互执行多个进程的操作系统,能使多个进程处于阻塞或者睡眠状态,这些任务位于内存中,但是并不处于可运行状态,他们利用内核阻塞自己,直到某一时间(键盘输入、网络数据等)发生。
209 |
210 | 多任务系统分为两类:
211 |
212 | * 非抢占式多任务
213 | * 抢占式多任务
214 |
215 | Linux提供了抢占式的多任务模式,由调度程序决定什么时候停止一个进程的运行,以便其他进程得到运行机会,这个强制的挂起动作叫做抢占。
216 |
217 | 时间片:可运行进程在被抢占之前预先设置好的处理器时间段。
218 |
219 | 非抢占任务模式下,除非进程自己主动停止运行,否则他会一直运行。进程主动挂起自己的操作称为**让步**(yielding)
220 |
221 | 非抢占任务模式的缺点:调度程序无法对每个进程该执行多长时间做出统一规定,进程独占的CPU时间可能超出预期,另外,一个绝不做出让步的悬挂进程就能使系统崩溃
222 |
223 | ### 4.2 Linux的进程调度
224 |
225 | 2.6内核系统开发初期,为了提供对交互程序的调度性能,引入新的调度算法,最为著名的是**反转电梯最后期限调度算法**(RSDL),在2.6.3版本替代了**O(1)调度算法**,最后被称为**完全公平调度算法(CFS)**
226 |
227 | ### 4.3 策略
228 |
229 | #### 4.3.1 I/O消耗型和CPU消耗型的进程
230 |
231 | CPU消耗型进程把时间大多用在了执行代码上,不属于I/O驱动类型,从系统响应速度考虑,调度策略往往是降低它们的调度频率,而延长其运行时间
232 |
233 | 调度策略的主要矛盾是:**进程响应迅速和最大系统利用率(高吞吐量)**
234 |
235 | Unix系统的调度程序更倾向于I/O消耗型程序,以提供更好的响应速度。Linux为了保证交互式应用和桌面系统的性能,对进程的响应做了优化(缩短响应时间),更倾向于调度I/O消耗型进程。
236 |
237 | #### 4.3.2 进程优先级
238 |
239 | 调度程序总是选择时间片为用尽而且优先级最高的进程运行
240 |
241 | Linux采用了两种不同的优先级范围:
242 |
243 | * 第一种用nice值,范围-20~+19,默认值0;越大的nice值优先级越低。相比高nice值(低优先级)的进程,低nice值(高优先级)的进程可以获得更多的处理器时间
244 | * 第二种是实时优先级,数值可配置,默认范围是0~99,数值越大优先级越高。任何实时进程的优先级都比普通进程高,实时优先级和nice优先级处于互不相交的范畴
245 |
246 | #### 4.3.3 时间片
247 |
248 | 调度策略选择合适的时间片并不简单,时间片太短会增加进程切换的处理器消耗,太长会导致系统的交互响应变差
249 |
250 | Linux的CFS调度器没有直接分配时间片到进程,它是将**处理器的使用比**划分给进程,所以进程所获得的时间片时间是和**系统负载(系统活跃的进程数)**密切相关的
251 |
252 | Linux中新的CFS调度器,它的进程抢占时机取决于**新的可运行程序消耗了多少处理器使用比**。如果消耗的处理器使用比比当前进程小,则新进程投入运行(当前进程被强占),否则,推迟运行。
253 |
254 | **总而言之,CFS会先根据进程的nice值预期设定每个进程的cpu使用比,而在进程调度时,需要将新的被唤醒进程实际消耗的cpu使用比和当前进程比较,如果更小,则抢占当前进程,投入运行,否则,推迟运行**
255 |
256 | ### 4.4 Linux调度算法
257 |
258 | #### 4.4.1 调度器类
259 |
260 | Linux调度器以模块提供,允许不同类型的进程针对性地选择调度算法,这种模块化结构成为**调度器类**,它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
261 |
262 | 完全公平调度(CFS)是一个针对普通进程的调度类,称为**SCHED_NORMAL**,具体算法实现定义在文件kernel/sched_fair.c中
263 |
264 | #### 4.4.2 Unix系统中的进程调度
265 |
266 | 传统Unix系统调度:进程启动会有默认的时间片,具有高优先级的进程将运行的更频繁,而且被赋予更多的时间片。存在的问题如下:
267 |
268 | * nice映射到时间片,就会将nice单位值对应到处理器的绝对时间,这样将会导致进程切换无法最优化进行,同时会导致进程获得的处理器时间很大程度上取决于其nice初始值。场景实例详见**P40**
269 | * 时间片一般为系统定时器节拍的整数倍,它会随着定时器节拍改变
270 |
271 | CFS采用的方法是:**完全摒弃时间片而是分配给进程一个处理器使用比重**,确保了调度中恒定的公平性,切换频率是在动态变化中
272 |
273 | #### 4.4.3 公平调度
274 |
275 | > 完美的多任务系统:每个进程获得1/*n*的处理器时间(*n*是指可运行进程的数量),同时调度给他们无限小的时间周期(交互性会很好)
276 |
277 | CFS的做法:**允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程**,在所有进程总数基础上计算一个进程应该运行多久,不在依靠nice值计算绝对的时间片,而是作为**进程获得的处理器运行比的权重**,越高的nice值获得更低的处理器使用权重。
278 |
279 | 每个进程按照其权重在全部可运行进程中所占比例的“时间片”来运行,由于越小的调度周期(重新调度所有可运行进程所花的时间)交互性会越好,也就更接近完美的所任务,CFS为调度周期设定一个目标(无限小的调度周期近似值)。
280 |
281 | 当可运行任务数量区域无限大时,他们所获得的处理器使用比和时间片将趋近于0(这会增加CPU的切换消耗)。因此,CFS引入每个进程获得的时间片底线,称为最小粒度。而当进程数非常多时,由于这个最小粒度的存在,调度周期会比较长,因此CFS并非完美的多任务。
282 |
283 | **总之,在CFS中任何进程所获得的的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的,nice值对时间片的作用不再是算数加权,而是几何加权,CFS是近乎完美的多任务**
284 |
285 | ### 4.5 Linux调度的实现
286 |
287 | Linux调度主要关注四个部分:
288 |
289 | * 时间记账
290 | * 进程选择
291 | * 调度器入口
292 | * 睡眠和唤醒
293 |
294 | #### 4.5.1 时间记账
295 |
296 | 1. 调度器实体结构
297 |
298 | CFS不再有时间片的概念,但是它会维护每个进程运行的时间记账,需要确保每个进程在分配给它的处理器时间内运行。CFS使用**调度器实体**(文件中的struct_sched_entity中)来追踪进程运行记账
299 |
300 | ```c
301 | struct sched_entity {
302 | struct load_weight load;
303 | struct rb_node run_node;
304 | struct list_head group_node;
305 | ...
306 | u64 vruntime;
307 | ...
308 | };
309 | ```
310 |
311 | 调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符task_struct内
312 |
313 | 2. 虚拟实时
314 |
315 | vruntime变量存放进程的虚拟运行时间,这个数值的计算是经过所有可运行进程总数的标准化,以ns为单位,与定时器节拍无关
316 |
317 | 定义在kernel/sched_fair.h文件中的update_curr()函数实现记账功能,它是系统定时器周期性调用,无论进程是在可运行态还是阻塞状态
318 |
319 | ```c
320 | static void update_curr(struct cfs_rq *cfs_rq)
321 | {
322 | ...
323 | __update_curr(cfs_rq, curr, delta_exec)
324 | ...
325 | }
326 | ```
327 |
328 | #### 4.5.2 进程选择
329 |
330 | **CFS**算法调度核心:**当CFS需要选择下一个运行进程时,选择具有最小vruntime的进程**
331 |
332 | **CFS**使用红黑树组织可运行进程队列,红黑树的键值为vruntime,检索对应节点的时间复杂度为对数级别
333 |
334 | 1. 挑选下一个任务
335 |
336 | CFS选择进程的算法为:运行rbtree中最左边叶子结点代表的那个进程。实现的函数是`__pick_next_entity()`,定义在kernel/sched_fair.c中
337 |
338 | ```c
339 | static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
340 | {
341 | struct rb_node *left = cfs_rq->rb_leftmost;
342 |
343 | if(!left)
344 | return NULL;
345 |
346 | return rb_entry(left, struct sched_entity, run_node)
347 | }
348 | ```
349 |
350 | **注意**:如果该函数返回值为NULL,说明树中没有任何节点,代表没有可运行进程,CFS调度器选择idle任务运行
351 |
352 | 2. 向树中加入进程
353 |
354 | 当**进程变为可运行状态(被唤醒)或者通过fork()调用第一次创建进程时**,会将进程加入到rbtree。`enqueue_entity()`函数实现了这个过程,代码详见P45
355 |
356 | 3. 从树中删除进程
357 |
358 | 删除动作发生在**进程阻塞(变为不可运行状态)或者终止(结束运行)**,是由函数`dequeue_entity()`函数完成
359 |
360 | #### 4.5.3 调度器入口
361 |
362 | 进程调度的入口函数是`schedule()`,定义在kernel/sched.c文件,**它是内核其他部分调用进程调度器的入口**。
363 |
364 | `schedule()`通常需要和一个调度类相关联,它会先找到一个最高优先级的调度类,后者要有自己的可运行进程队列,然后这个调度类决定下一个可运行的进程。
365 |
366 | 因此,`schedule()`函数的逻辑比较简单,它的主要逻辑就是调用`pick_next_task()`,这个函数会以优先级为序,从高到低一次检查每个调度器类,从最高优先级的调度类中选择下一个运行的进程。详细代码见**P48**
367 |
368 | ```c++
369 | static inline struct task_struct *pick_next_task(struct rq *rq)
370 | {
371 | ...
372 | }
373 | ```
374 |
375 | 每个调度类都实现了`pick_next_task()`函数,它会返回指向下一个可运行进程的指针,在CFS中`pick_next_task()`会调用`pick_next_entity()`,该函数会调用 [4.5.2节](#4.5.2 进程选择) 提到的`__pick_next_entity()`
376 |
377 | **函数优化**:由于CFS是普通进程的调度类,而系统绝大多数进程是普通进程。函数使用了一个小技巧,当所有可运行进程数等于CFS类对应的可运行进程数时,直接返回CFS调度类的下一个运行进程
378 |
379 | #### 4.5.4 睡眠和唤醒
380 |
381 | 睡眠(或阻塞)的进程处于一个特殊的不可运行状态。
382 |
383 | 进程睡眠时,进程把自己标记为休眠状态,从可执行进程对应的红黑树中移出,放入等待队列,然后调用`schedule()`调度下一个进程;唤醒的过程相反:进程被设置成可执行状态,然后从等待队列移到可执行红黑树中
384 |
385 | 1. 等待队列
386 |
387 | 等待队列是由**等待某些事件发生的进程组成的简单链表**,内核用`wake_queue_head_t`代表等待队列
388 |
389 | 进程加入等待队列的详细过程和代码详见**P50**
390 |
391 | 2. 唤醒
392 |
393 | 唤醒操作通过函数`wake_up()`进行,它会唤醒等待队列上的所有进程,它调用函数`tey_to_wake_up()`将进程状态设置为**TASK_RUNNING**,调用`enqueue_task()`将此进程放入红黑树,如果被唤醒的进程比当前正在执行的进程优先级高(这里不是指nice值,而是根据CFS调度的cpu使用比规则得出的结果),还要设置进程的need_resched标志。
394 |
395 | **注意**:通常哪段代码促使等待条件达成,它就要负责调用`wake_up()`函数。例如,当磁盘数据到来时,VFS需要负责对等待队列调用`wake_upe()`。
396 |
397 |
398 | ### 4.6 抢占和上下文切换
399 |
400 | 上下文切换由定义在kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来投入运行的时候,schedule()会调用函数context_switch(),后者完成两项工作:
401 |
402 | * 调用声明在中的switch_mm()函数,它负责将虚拟内存从上一个进程映射切换到新进程中
403 | * 调用声明在的switch_to(),负责从上一个进程的处理器状态切换到新进程的处理器状态,其中包括**保存、恢复栈信息和寄存器信息**
404 |
405 | 内核提供一个`need_resched`标志标明是否需要重新执行一次调度,2.2以前放在全局变量,2.2~2.4在每个进程的进程描述符中(由于current宏速度很快并且进程描述符通常是在高速缓存中,访问`task_struct`内的数值比全局变量更快),而在2.6版本中,它放在thread_info结构体中,用一个特别的标志变量的一位来表示。
406 |
407 | `need_resched`标志被设置的时机:
408 |
409 | * 当某个进程应该被抢占时,scheduler_tick()函数会设置这个标志
410 | * 当一个优先级更高的进程进入可运行状态时,try_to_wake_up()也会设置这个标志
411 |
412 | 然后内核检查该标志,确认被设置后,会调用schedule()切换到一个新进程
413 |
414 | #### 4.6.1 用户抢占
415 |
416 | 内核在中断处理程序或者系统调用返回后,都会检查`need_resched`标志。从中断处理程序或者系统调用返回的返回路径都是跟体系结构相关,在entry.S(包含内核入口和退出的代码)文件通过汇编实现
417 |
418 | 当内核将返回用户空间的时候,如果`need_resched`标志被设置,会导致schedule()调用,会发生用户抢占
419 |
420 | 因此,用户抢占发生在以下情况:
421 |
422 | * 系统调用返回用户空间时
423 | * 中断处理程序返回用户空间时
424 |
425 | #### 4.6.2 内核抢占
426 |
427 | > 在没有内核抢占的系统中,调度程序没有办法在一个内核级的任务正在执行时重新调度,内核中的任务以协作方式调度,不具备抢占性,内核代码一直执行到完成(返回用户空间)或者阻塞为止
428 |
429 | 在2.6版本,Linux内核引入抢占能力,只要重新调度是**安全的**(没有持有锁的情况),内核可以在任何时间抢占正在执行的任务。
430 |
431 | 在每个进程的thread_info结构中加入preempt_count计数,代表进程使用锁的个数。
432 |
433 | * 在中断返回内核空间的时候,会检查need_resched和preempt_count,如果need_resched被设置且preempt_count为0,则可以进行安全的抢占,调度程序schedule()会被调用,否则,中断直接返回当前进程
434 | * 如果进程持有的所有锁被释放,preempt_count会减为0,此时释放锁的代码会检查need_resched标志,如果被设置,则调用schedule()
435 |
436 | 因此,内核抢占发生在:
437 |
438 | * 中断处理程序正在执行,且返回内核空间之前
439 | * 进程在内核空间释放锁的时候
440 | * 内核任务显式的调用schedule()
441 | * 内核中的任务阻塞
442 |
443 | ### 4.7 实时调度策略
444 |
445 | > Linux提供了一种软实时的工作方式
446 | >
447 | > 软实时的定义:内核调度进程尽力使进程在规定时间到来前运行,但是内核不能总是满足这些进程的要求
448 | >
449 | > 硬实时的定义:保证在一定条件下,可以完全满足进程在规定的时间内完成操作
450 |
451 | Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,普通的、非实时的调度策略是SCHED_NORMAL。实时策略不被CFS调度器管理,而是被一个特殊的实时调度器管理
452 |
453 | SCHED_FIFO实现了**简单的、先入先出的调度算法**,它不使用时间片,SCHED_RR和前者大致相同,不同点在于它使用时间片,是一种**实时轮转调度算法**
454 |
455 | ## 第5章 系统调用
456 |
457 |
458 |
459 | ## 第6章 内核数据结构
460 |
461 | ## 第7章 中断和中断处理
462 |
463 | ## 第8章 下半部和推后执行的工作
464 |
465 | ## 第12章 内存管理
466 |
467 | > 内核不能像用户空间那样奢侈的使用内存,获取内存币用户空间复杂很多
468 |
469 | ### 12.1 页
470 |
471 | 内核把物理页作为内核管理的基本单元,内存管理单元(MMU)是管理内存并将虚拟内存转换为物理内存的硬件,它以页为单位来管理系统中的页表
472 |
473 | 结构体struct page表示系统中的每个物理页
474 |
475 | ```c
476 | struct page {
477 | unsigned long flags,
478 | atomic_t _count,
479 | atomic_t _mapcount,
480 | unsigned long private,
481 | struct address_space *mapping,
482 | pgoff_t index,
483 | struct list_head lru,
484 | void *virtual
485 | };
486 | ```
487 |
488 | * `flags`域,用来存放页的状态(包括是不是脏的,是不是锁定在内存),每一位单独表示一种状态,至少可以表示32中不同的状态
489 | * `_count`域,存放页的引用计数,-1时内核没有引用该页,在新的内存分配中可以使用。内核调用`page_count()`检查该域,返回0表示页空闲,返回正整数表示正在使用
490 | * 页可以由页缓存使用(此时,mapping域指向页关联的address_space对象),或者作为私有数据(`private`指向),或者作为进程页表中的映射
491 | * `virtual`域,页的虚拟地址,当页在高端内存(不会永久映射到内核空间)中时,这个域为`NULL`
492 |
493 | **注意**:
494 |
495 | 1. `page`结构与物理页相关,并非与虚拟页相关,它仅仅描述当前时刻在相关物理中存放的数据(由于交换等原因,关联的数据继续存在,但是和当前物理页不再关联),它对于页的描述是短暂的
496 | 2. 页的拥有者可能是用户空间进程、动态分配的内核数据,静态内核数据或者页高速缓存等
497 |
498 | ### 12.2 区
499 |
500 | Linux主要使用四种区:
501 |
502 | * ZONE_DMA,其中包含的页只能进行DMA操作(直接内存访问)
503 | * ZONE_DMA32,和ZONE_DMA类似,不同之处是只能被32位设备访问
504 | * ZONE_NORMAL,包含能够正常映射的页
505 | * ZONE_HIGHMEM,包含“高端内存”,其中的页不能永久地映射到内核空间
506 |
507 | > 高端内存,由于一些体系结构的物理内存比虚拟内存大的多,为了充分利用物理内存,将物理内存中的部分区域划分为高端内存,他们不能永久地映射到内核空间,而是动态的映射
508 | >
509 | > 在32位x86体系中,ZONE_HIGHMEM为高于896MB的所有物理内存,其余内存为低端内存,其中ZONE_NORMAL为16MB到896MB的物理内存,ZONE_DMA为小于16MB的物理内存
510 | >
511 | > x86-64系统没有高端内存区
512 |
513 | | 区 | 描述 | 物理内存 |
514 | | :----------: | :------------: | :------: |
515 | | ZONE_DMA | DMA使用的页 | < 16MB |
516 | | ZONE_NORMAL | 正常可寻址的页 | 16~896MB |
517 | | ZONE_HIGHMEM | 动态映射的页 | > 896MB |
518 |
519 | 每个区使用结构体`zone`表示,具体结构详见**P206**
520 |
521 | 域说明:
522 |
523 | * `lock`域,是一个自旋锁,防止结构被并发访问,这个域只保护结构,不保护驻留在这个区中的页
524 | * `watermark`域,水位值,为每个内存区设置合理的内存消耗基准
525 | * `name`域,表示区的名字,三个区的名字分别为"DMA","Normal","HighMem"
526 |
527 | ### 12.3 获得页
528 |
529 | * 分配2order(1< Linux内核提供slab层(即slab分配器),作为通用数据结构缓存层
610 |
611 | #### 12.6.1 slab的设计
612 |
613 | > slab层将不同的对象划分为**高速缓存组**,每个高速缓存组存放不同类型的对象,例如,分别存放进程描述符(task_struct结构的空闲链表),索引节点对象(struct inode)
614 |
615 | * `kmalloc()`建立在slab层之上,使用了一组通用高速缓存
616 | * 一般slab仅仅由一页组成,每个高速缓存由多个slab组成
617 | * 每个slab包含一些对象成员,对象指的是被缓存的数据结构
618 | * slab包含三种状态:满、部分满或空
619 |
620 | 高速缓存使用结构体`kmem_cache`表示,这个结构包括三个链表:`slabs_full`, `slabs_partial`和`slabs_empty`,这些链表包含高速缓存中的所有slab
621 |
622 | slab使用slab描述符表示,详见**P216**
623 |
624 | ```c
625 | struct slab {
626 | ...
627 | };
628 | ```
629 |
630 | slab分配器使用`__get_free_pages()`创建新的slab
631 |
632 | #### 12.6.2 slab分配器的接口
633 |
634 | * 创建一个新的高速缓存
635 | * `name`:高速缓存的名字
636 | * `size`:高速缓存中每个元素的大小
637 | * `align`:slab内第一个对象的偏移量,用来确保在页内进行特定的对齐
638 | * `flags`:可选的设置项,控制高速缓存的行为,详见**P218**
639 | * `ctor`:高速缓存的构造函数(Linux的高速缓存不使用构造函数)
640 | * 返回指向高速缓存的指针
641 | * 函数调用可能会睡眠,不能再中断上下文使用
642 |
643 | ```c
644 | struct kmem_cache *kmem_cache_create(const char *name,
645 | size_t size,
646 | size_t align,
647 | unsigned long flags,
648 | void (*ctor)(void *));
649 | ```
650 |
651 | * 撤销一个高速缓存(可能睡眠,不能再中断上下文使用)
652 |
653 | ```c
654 | int kmem_cache_destroy(struct kmem_cache *cachep);
655 | ```
656 |
657 | **注意**:调用`kmem_cache_destroy`之前要确保两个条件:
658 |
659 | 1. 高速缓存中的所有slab为空
660 | 2. 在调用此函数过程中不在访问这个高速缓存
661 |
662 | * 从已经创建的缓存中分配释放对象,使用示例详见**P219**
663 |
664 | ```c
665 | void kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
666 | void kmem_cache_free(struct kmem_cache *cachep, void *objp);
667 | ```
668 |
669 | ### 12.7 栈上的静态分配
670 |
671 | > 不同于用户栈,内核栈小而且固定,内核栈一般是两个页大小
672 |
673 | #### 12.7.1 单页内核栈
674 |
675 | 2.6内核之后,引入选项可以设置**单页内核栈**,激活这个选项,每个进程的内核栈只有一页大小
676 |
677 | 引入的原因有两点:
678 |
679 | 1. 可以让每个进程减少内存消耗,另外随着机器运行时间增加,寻找两个连续的物理页变得越来越困难
680 | 2. 当内核栈使用两页时,中断处理程序使用它所中断进程的内核栈,而当进程使用单页的内核栈时,中断处理程序不放在进程内核栈中,而是放在**中段栈**中。
681 |
682 | > 中断栈:为每个进程提供运行中断处理程序的栈,一页大小。
683 |
684 | 总之,历史上,进程和中断处理程序共享一个栈空间,当1页栈的选项激活之后,中断处理程序获得了自己的栈。
685 |
686 | #### 12.7.2 在栈上工作
687 |
688 | 在任何函数,都要尽量节省内核栈的使用,让所有局部变量大小不要超过几百字节。栈溢出非常危险,所出的数据会直接覆盖紧邻堆栈末端的数据(例如`thread_info`结构就是紧邻进程堆栈末端)。
689 |
690 | 因此,推荐使用动态分配。
691 |
692 | ### 12.8 高端内存的映射
693 |
694 | #### 12.8.1 永久映射
695 |
696 | 映射给定的page结构到内核地址空间,使用如下函数
697 |
698 | ```c
699 | void *kmap(struct page *page);
700 | ```
701 |
702 | 函数在对于高端内存或者低端内存都能使用:
703 |
704 | 1. 如果page对应低端内存的一页,函数会单纯返回该物理页对应的虚拟地址
705 | 2. 如果page对应高端内存页,函数会建立一个永久映射,在返回对应的虚拟地址
706 | 3. 函数可以睡眠,只能在进程上下文中使用
707 |
708 | 当不再需要高端内存中的这一个页时,使用如下函数解除映射
709 |
710 | ```c
711 | void kunmap(struct page *page);
712 | ```
713 |
714 | #### 12.8.2 临时映射
715 |
716 | > 当必须创建映射而上下文不能睡眠是,内核提供了临时映射(原子映射)
717 |
718 | 临时映射可以用在像中断上下文一样的不能睡眠的地方,使用如下函数建立心是映射:
719 |
720 | ```c
721 | void *kmap_atomic(struct page *page, enum km_type type);
722 | ```
723 |
724 | * 函数禁止了内核抢占(因为映射对每个处理器都是唯一的???)
725 |
726 | ### 12.9 每个CPU的分配
727 |
728 | > SMP定义:一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。
729 | >
730 | > 支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的,每个CPU的数据存放在一个数组中,数组的每一个元素对应一个存在的处理器
731 |
732 | ```c
733 | unsigned long my_percpu[NR_CPUS];
734 | ```
735 |
736 | 访问cpu数据过程:
737 |
738 | ```c
739 | int cpu;
740 |
741 | cpu = get_cpu(); //获取当前CPU,并且禁止内核抢占
742 | data = my_percpu[cpu];
743 | .... // 使用data的过程
744 | put_cpu();
745 | ```
746 |
747 | 代码中没有出现锁,因为数据对当前处理器是唯一的,没有其他处理器可以接触这个数据,没有多个处理器并发访问的问题,但是会有内核抢占的问题:
748 |
749 | 1. 如果代码被其他处理器抢占并重新调度,这是cpu变量data会变成无效,因为它对应了错误的处理器
750 | 2. 如果另一个进程抢占了代码,有可能在一个处理器上并发访问data数据的问题
751 |
752 | 因此,在获取当前cpu时,就已经禁止了内核抢占。
753 |
754 | ### 12.10 新的每个CPU接口
755 |
756 | > 描述了一些为每个CPU分配内存的接口,详见**P223**
757 |
758 | ### 12.11 使用每个CPU数据的原因
759 |
760 | 使用每个CPU的好处有:
761 |
762 | 1. 减少数据锁定,每个处理器访问每个CPU数据,不用加锁
763 | 2. 使用每个CPU数据大大减少了缓存失效,失效发生在处理器试图使他们的缓存保持同步时,如果一个处理器操作数据时,该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须刷新或者清理自己的缓存,频繁的缓存失效会造成**缓存抖动**
764 |
765 | 而使用每个CPU数据唯一的要求是需要**禁止内核抢占**
766 |
767 | ## 第13章 虚拟文件系统
768 |
769 | > 虚拟文件系统(VFS): 作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口
770 |
771 | ### 13.1 通用文件系统接口
772 |
773 | > VFS使得用户可以直接使用统一的系统调用,无需考虑具体的文件系统和物理介质
774 |
775 | ### 13.2 文件系统抽象层
776 |
777 | > 内核在底层文件系统接口上建立了一个抽象层,使得Linux能够支持各种文件系统。
778 | >
779 | > 实际文件系统的代码在统一的接口和数据结构下隐藏各自具体的实现细节,它们通过编程提供VFS所期望的抽象接口和数据结构
780 |
781 | ### 13.3 Unix文件系统
782 |
783 | > 四个基本要素:文件、目录项、索引节点和安装点(挂载点)
784 |
785 | * 目录项:路径中的每一部分都被称为目录条目,统称为目录项
786 | * 索引节点:Unix系统将文件的相关信息和文件本身这两个概念加以区分(如访问控制权限、大小、创建时间等),文件的相关信息(文件的元数据信息)被存储在一个单独的数据结构,称为索引节点(inode)
787 | * 超级块:是一种包含文件系统控制信息的数据结构,这些信息称为文件系统数据元
788 |
789 | 
790 |
791 | ### 13.4 VFS对象及数据结构
792 |
793 | #### 对象类型
794 |
795 | VFS的四个对象类型:
796 |
797 | * 超级块对象:代表具体的文件系统
798 | * 索引节点对象:代表具体文件
799 | * 目录项对象:代表目录项,是路径的一个组成部分
800 | * 文件对象:代表进程打开的文件
801 |
802 | **注意**:VFS将目录作为一个文件来处理,不存在目录对象;目录项不同于目录
803 |
804 | #### 操作对象
805 |
806 | > 每个对象中都包含一个操作对象,其中描述了内核针对主要对象可以使用的方法
807 |
808 | * `super_operations`对象:内核针对特定文件系统调用的方法,如`write_inode(), sync_fs()`
809 | * `inode_operations`对象:内核针对特定文件调用的方法,如`create(), link()`
810 | * `dentry_operations`对象:内核针对特定目录项所能调用的方法,如`d_compare(), d_delete()`
811 | * `file_operations`对象:进程针对已打开文件所能调用的方法,如`read(), write()`
812 |
813 | **注意**:操作对象作为结构体,其中包含操作父对象的函数指针,实际的文件系统可以继承VFS提供的通用函数。
814 |
815 | ### 13.5 超级块对象
816 |
817 | > 各种文件系统都必须实现超级块对象,该对象存储特定文件系统的信息,对应于存放在磁盘**特定扇区**中文件系统超级块或者文件系统控制块。(非基于磁盘的文件系统,会在使用现场创建超级块并保存在内存中)
818 |
819 | * 超级块对象有`super_block`结构体表示,详见**P231**
820 |
821 | **注意**:超级块对象通过`alloc_super()`函数创建并初始化,在安装文件系统时,文件系统会调用这个函数从**磁盘**读取文件系统超级块,并将其中的数据**填充到内存中的超级块对象对应的结构体**中。
822 |
823 | ### 13.6 超级块操作
824 |
825 | > 超级块对象中s_op指针,指向超级块的操作函数表,由`super_operations()`表示
826 |
827 | **详见P233**
828 |
829 | ### 13.7 索引节点对象
830 |
831 | > 索引节点对象:包含内核在操作文件系统或者目录时需要的全部信息(对于Unix风格的系统,直接从磁盘的索引节点读入),索引节点对象必须在**内存**中创建。
832 |
833 | * 结构体`inode`表示,**详见P235**
834 | * 索引节点代表**普通文件**或者**设备、管道**等特殊文件
835 |
836 | ### 13.8 索引节点操作
837 |
838 | `inode_operation`结构体,**详见P239**
839 |
840 | ### 13.9 目录项对象
841 |
842 | > 每个dentry代表路径中的一个特定部分,比如路径/bin/vi,其中/,bin,vi都是目录项,前两个是**目录**,最后一个是**普通文件**。
843 | >
844 | > **注意**:在路径中,包含普通文件在内,每一项都是目录项对象。
845 |
846 | * 结构体`dentry`表示,详见**P239**
847 |
848 | * VFS在执行目录项操作时,会现场创建目录项对象
849 |
850 | **注意**:目录项对象没有对应的磁盘数据结构,并非保存在磁盘上,`dentry`结构体中没有是否被修改的标志(是否为脏、是否需要写会磁盘)
851 |
852 | #### 13.9.1 目录项状态
853 |
854 | > 三种状态:被使用、未使用和负状态
855 |
856 | * 被使用的目录项:对应一个有效的索引节点,`d_node`指向相应的索引节点,`d_count`代表使用者的数量;不能被丢弃
857 | * 未被使用的目录项:对应有效的索引节点,但是`d_count`为0,仍然指向一个有效对象,被保存在缓存中
858 | * 负状态的目录项:没有对应的有效索引节点,`d_node`为NULL,索引节点已被删除,或者路径不不再正确
859 |
860 | **注意**:目录项释放后也可以保存在**slab缓存**中。
861 |
862 | #### 13.9.2 目录项缓存
863 |
864 | > 内核将目录项对象缓存在目录项缓存dcache中
865 |
866 | 目录项分为三个部分:
867 |
868 | * **“被使用的”** 目录项链表:索引节点中`i_dentry`链接相关的目录项(一个索引节点可能有多个链接,对应多个目录项),因此用一个链表连接他们
869 | * **“最近被使用的”** 双向链表:包含未被使用和负状态的目录项对象(总是在头部插入新的目录项,需要回收内存时,会再尾部删除旧的目录项)
870 | * **散列表**和相应的**散列函数**:快速将给定路径解析(哈希)成相关的目录项对象
871 |
872 | 散列表由数组`dentry_hashtable`表示,每个元素指向具有相同键值的目录项组成的链表头指针;散列值有`d_hash`计算(内核系统提供给文件系统**唯一**的散列函数)
873 |
874 |
875 |
876 | **注意**:
877 |
878 | 1. `dcache`一定意义上提供了对于索引节点的缓存(`icache`),和目录项相关的索引节点对象不会被释放(因为索引节点的使用计数>0),这样确保了索引节点留在内存中
879 | 2. 文件访问呈现空间和时间的局部性:时间局部性体现在程序在一段时间内可能会访问相同的文件;空间局部性体现在同一个目录下的文件很可能都被访问。
880 |
881 | ### 13.10 目录项操作
882 |
883 | `dentry_operation`结构体,**详见P241**
884 |
885 | ### 13.11 文件对象
886 |
887 | > 文件对象是已打开的文件在内存中的表示
888 |
889 | * 结构体`file`表示
890 |
891 | * 由`open()`系统调用创建,`close()撤销`
892 | * 多个进程可以打开同一个文件,所以同一个文件存在多个对应的文件对象
893 | * 文件对象仅仅在观点上代表已打开文件,它反过来指向目录项对象,而目录项对象反过来指向索引节点
894 |
895 | **注意**:类似于目录项对象,文件对象没有对应的磁盘数据,通过`file`结构体中`f_dentry`指针指向相关的目录项对象,而目录项对象指向相关的索引节点,索引节点会记录文件是否为脏
896 |
897 | ### 13.12 文件操作
898 |
899 | 结构体`file_operation`,详见**P242**,文件相关的操作方法和**系统调用**很类似
900 |
901 | * 具体的文件系统可以为每一种操作方法实现各自的代码,如果存在通用操作,则使用通用操作
902 |
903 | ### 13.13 和文件系统相关的数据结构
904 |
905 | #### 结构体 file_system_type
906 |
907 | > 描述特定文件系统类型,详见**P248**
908 |
909 | * 主要方法`get_sb()`:从磁盘读取超级块,在文件系统安装时,在内存中组装超级块对象
910 | * 剩余的函数描述文件系统的属性
911 | * 每种文件系统,不管有多少实例,都只有一个`file_system_type`结构体
912 |
913 | #### 结构体 vfsmount
914 |
915 | > 描述安装的文件系统的实例,详见**P248**
916 |
917 | * 在文件系统实际被安装时,会有一个`vfsmount`结构体在安装点创建,它代表文件系统的**实例**(也代表一个**安装点**)
918 | * `vfsmount`结构体中维护了各种链表,用于跟踪文件系统和所有安装点之间的关系
919 |
920 |
921 |
922 | ### 13.14 和进程相关的数据结构
923 |
924 | > 三种结构体:`file_struct`, `fs_struct`, `namespace`
925 |
926 | #### 结构体 file_struct
927 |
928 | ```c
929 | struct file_struct{
930 | struct file *fd_array[NR_OPEN_DEFAULT]; /*缺省的文件对象数组*/
931 | struct fdtable *fdt /*指向其他fd的指针*/
932 | ...
933 |
934 | }
935 | ```
936 |
937 | * 由进程描述符中的`files`指向
938 | * `fd_array`数组指针:指向已打开的文件对象,`NR_OPEN_DEFAULT`默认64,如果进程打开的文件超过64,内核将分配一个新数组,并且将`fdt`指针指向它
939 | * 当访问的文件对象的数量小于64时,执行比较快,因为是对静态数组的操作;如果大于64,内核需要建立新数组,访问速度相对慢一些
940 | * 管理员可以增大`NR_OPEN_DEFAULT`选项优化性能
941 |
942 | #### 结构体 fs_struct
943 |
944 | * 由进程描述符的`fs`域指向,包含文件系统和进程相关的信息
945 | * 包含进程的当前工作目录(`pwd`)和根目录
946 |
947 |
948 |
949 | #### 结构体 namespace
950 |
951 | * 进程描述符的`mmt_namespace`域指向,它使得每个进程在系统中看到唯一的文件系统(唯一的根目录和文件系统结构层次)
952 |
953 |
954 |
955 | **注意**:
956 |
957 | 1. 对于多数进程,它们的描述符都指向自己独有的`file_struct`和`fs_struct`,除非使用克隆标志`CLONE_FILES`或者`CLONE_FS`创建的进程会共享这两个结构体
958 | 2. `namespace`结构体使用方法和前两种结构完全不同,默认情况下,所有进程共享同样的命名空间(都从相同的挂载表中看到同一个文件系统层次结构,除非在`cloen()`操作时使用`CLONE_NEWS`标志,才会给进程一个命名空间结构体的拷贝)
959 |
960 | ## 第14章 块I/O层
961 |
962 | ## 第15章 进程地址空间
963 |
964 | ## 第16章 页高速缓存和页回写
965 |
966 |
967 |
968 |
969 |
970 |
--------------------------------------------------------------------------------
/Linux性能调优.md:
--------------------------------------------------------------------------------
1 | [toc]
2 |
3 |
4 |
5 | # Linux性能调优
6 |
7 | ## CPU性能篇
8 |
9 | ### 2 怎么理解“平均负载”
10 |
11 |
12 |
13 | ### 3 CPU上下文切换(上)
14 |
15 | > CPU上下文:包括CPU寄存器和程序计数器
16 | >
17 | > CPU寄存器:是 CPU 内置的容量小、但速度极快的内存
18 | >
19 | > 程序计数器:是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置
20 |
21 | 
22 |
23 | * CPU上下文切换:是先把前一个任务的 CPU 上下文(也就是**CPU 寄存器和程序计数器**)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务
24 | * 这些这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来
25 |
26 | 根据任务的不同,CPU的上下文切换分为进程上下文切换、线程上下文切换和中断下文切换
27 |
28 | #### 3.1 进程上下文切换
29 |
30 | Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。
31 |
32 | * 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
33 | * 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
34 |
35 | 
36 |
37 | 从进程用户态到内核态的转变,需要通过**系统调用**来完成,系统调用的过程中会发生**两次CPU上下文切换**。CPU里原来用户态指令的执行位置需要先保存起来,然后更新为内核态执行的指令位置,最后跳转到内核态运行内核任务。在系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
38 |
39 | **注意**:
40 |
41 | 1. 系统调用的过程中,不会涉及到虚拟内存等进程态的资源,不会切换进程,系统调用过程和进程上下文切换不一样,整个过程都是同一个进程
42 |
43 | 2. 系统调用称为特权模式切换,不是上下文切换
44 |
45 | 进程上下文切换和系统调用的区别:
46 |
47 | 进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的**虚拟内存、栈**等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈
48 |
49 | 
50 |
51 | **保存上下文和恢复上下文的过程需要内核在CPU上运行才能完成**(上下文切换过程是CPU密集型),每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。
52 |
53 | 在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在**寄存器、内核栈以及虚拟内存**等资源的保存和恢复上,进而大大缩短了真正运行进程的时间,从而导致系统平均负载升高。
54 |
55 | Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在**多处理器系统**上,缓存是被多个处理器**共享**的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
56 |
57 | Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是**优先级最高和等待 CPU 时间最长**的进程来运行。
58 |
59 | 进程被CPU重新调度的时机:
60 |
61 | 1. 进程执行完终止了,它之前使用的 CPU 会释放出来,这个时候再从就绪队列里,拿一个新的进程过来运行
62 | 2. 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行
63 | 3. 进程在系统**资源**不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行
64 | 4. 进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度
65 | 5. 有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行
66 | 6. 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序
67 |
68 | #### 3.2 线程上下文切换
69 |
70 | 线程和进程的区别:**线程是调度的基本单位,而进程则是资源拥有的基本单位**。
71 |
72 | 所谓内核中的任务调度,实际上的调度对象是**线程**;而进程只是给线程提供了虚拟内存、全局变量等资源。
73 |
74 | * 当进程只有一个线程时,可以认为进程就等于线程
75 | * 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的
76 | * 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的
77 |
78 | 因此,线程的上下文切换分为两种情况:
79 |
80 | 1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样
81 | 2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的**私有数据、寄存器**等不共享的数据
82 |
83 | **注意**:同进程的线程切换要比进程间的切换消耗更少的资源,更加轻量级
84 |
85 | #### 3.3 中断上下文切换
86 |
87 | 为了响应硬件事件,**中断处理会打断进程的正常调度和执行**,转而调用中断处理程序,响应设备事件。
88 |
89 | 中断上下文切换不会涉及到进程的用户态,它其实只包括内核态中断服务程序执行所必需的状态,包括**CPU 寄存器、内核堆栈、硬件中断参数**等
90 |
91 | 对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以**中断上下文切换并不会与进程上下文切换同时发生**
92 |
93 | 大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
94 |
95 | 中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能
96 |
97 |
98 |
99 | ### 4 CPU上下文切换(下)
100 |
101 | #### 4.1 查看上下文切换
102 |
103 | 1. 查看系统总体情况
104 |
105 | ```bash
106 | # 每隔5s输出一组数据
107 | [root@VM_194_74_centos ~]# vmstat 5 5
108 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
109 | r b swpd free buff cache si so bi bo in cs us sy id wa st
110 | 1 0 0 118120 1188128 13137072 0 0 0 10 0 0 3 1 97 0 0
111 | 1 0 0 117040 1188128 13137080 0 0 0 22 1071 1311 1 0 99 0 0
112 | 0 0 0 116824 1188128 13137092 0 0 0 13 1181 1421 1 0 99 0 0
113 | 0 0 0 117328 1188128 13137100 0 0 0 12 1165 1374 1 0 99 0 0
114 | 1 0 0 117168 1188128 13137112 0 0 0 22 1148 1391 1 0 99 0 0
115 | ```
116 |
117 | 参数:
118 |
119 | * `cs`:context switch,每秒上下文切换的次数
120 | * `in`:interrupt ,每秒中断的次数
121 | * `r`:就绪队列的长度(正在运行和等待CPU的进程数)
122 | * `b`:blocked,处于不可中断睡眠状态的进程数
123 |
124 | 2. 查看进程的详细信息
125 |
126 | 命令:pidstat
127 |
128 | ```bash
129 | [root@VM_194_74_centos ~]# pidstat -w 5
130 | Linux 3.10.107-1-tlinux2_kvm_guest-0049 (VM_194_74_centos) 05/07/20 _x86_64_ (8 CPU)
131 |
132 | 08:20:54 UID PID cswch/s nvcswch/s Command
133 | 08:20:59 0 1 0.80 0.00 systemd
134 | 08:20:59 0 7 0.80 0.00 migration/0
135 | 08:20:59 0 9 90.40 0.00 rcu_sched
136 | 08:20:59 0 10 0.20 0.00 watchdog/0
137 | 08:20:59 0 11 0.20 0.00 watchdog/1
138 | 08:20:59 0 12 2.00 0.00 migration/1
139 | 08:20:59 0 16 0.20 0.00 watchdog/2
140 | 08:20:59 0 17 1.00 0.00 migration/2
141 | 08:20:59 0 18 0.20 0.00 ksoftirqd/2
142 | ...
143 | ```
144 |
145 | 参数:
146 |
147 | * cswch:每秒自愿上下文切换的次数(voluntary context switch)
148 | * nvcswch:每秒非自愿上下文切换的次数(non voluntary context switch)
149 |
150 | > 自愿上下文切换:进程无法获取所需资源导致的上下文切换,比如I/O,内存等**系统资源不足**时发生的上下文切换
151 | >
152 | > 非自愿上下文切换:进程因**时间片**已到等原因,被系统强制**调度**发生的上下文切换,比如多个进程**竞争**CPU是发生的上下文切换
153 |
154 | #### 4.2 案例分析
155 |
156 | `sysbench`模拟多线程调度切换
157 |
158 | ##### 4.2.1 准备
159 |
160 | > 一台Linux机器,打开三个终端
161 |
162 | ##### 4.2.2 正式实战
163 |
164 | 1. 第一个终端:运行`sysbench`
165 |
166 | ```
167 | # 以10个线程运行5分钟的基准测试,模拟多线程切换的问题
168 | $ sysbench --threads=10 --max-time=300 threads run
169 | ```
170 |
171 | 2. 第二个终端:运行`vmstat`
172 |
173 | ```bash
174 | # 每隔1秒输出1组数据(需要Ctrl+C才结束)
175 | $ vmstat 1
176 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
177 | r b swpd free buff cache si so bi bo in cs us sy id wa st
178 | 6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
179 | 8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0
180 | ```
181 |
182 | 指标观察:
183 |
184 | * cs列:上升到39万
185 |
186 | * r列:就绪队列长度上升到8
187 | * in列:终端次数上升到1万
188 | * us(user)和sy(system)列:使用率加起来100%,sy为84%,主要被内核占用
189 |
190 | 3. 查看进程情况
191 |
192 | ```bash
193 |
194 | # 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
195 | # -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
196 | $ pidstat -w -u 1
197 | 08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
198 | 08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
199 | 08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2
200 |
201 | 08:06:33 UID PID cswch/s nvcswch/s Command
202 | 08:06:34 0 8 11.00 0.00 rcu_sched
203 | 08:06:34 0 16 1.00 0.00 ksoftirqd/1
204 | 08:06:34 0 471 1.00 0.00 hv_balloon
205 | 08:06:34 0 1230 1.00 0.00 iscsid
206 | 08:06:34 0 4089 1.00 0.00 kworker/1:5
207 | 08:06:34 0 4333 1.00 0.00 kworker/0:3
208 | 08:06:34 0 10499 1.00 224.00 pidstat
209 | 08:06:34 0 26326 236.00 0.00 kworker/u4:2
210 | 08:06:34 1000 26784 223.00 0.00 sshd
211 | ```
212 |
213 | **分析**:CPU 使用率的升高果然是 sysbench 导致的,它的 CPU 使用率已经达到了 100%。但上下文切换则是来自其他进程,包括非自愿上下文切换频率最高的 pidstat ,以及自愿上下文切换频率最高的内核线程 kworker 和 sshd
214 |
215 | **注意**:pidstat 输出的上下文切换次数,加起来也就几百,比 vmstat 的 139 万明显小了太多?
216 |
217 | 4. 查看线程的情况
218 |
219 | 可以看到,sysbench 进程(也就是主线程)的上下文切换次数看起来并不多,但它的子线程的上下文切换次数却有很多。上下文切换罪魁祸首,还是过多的 sysbench 线程
220 |
221 | ```bash
222 | # 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
223 | # -wt 参数表示输出线程的上下文切换指标
224 | $ pidstat -wt 1
225 | 08:14:05 UID TGID TID cswch/s nvcswch/s Command
226 | ...
227 | 08:14:05 0 10551 - 6.00 0.00 sysbench
228 | 08:14:05 0 - 10551 6.00 0.00 |__sysbench
229 | 08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
230 | 08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
231 | 08:14:05 0 - 10554 18827.00 103954.00 |__sysbench
232 | ...
233 | ```
234 |
235 | 5. 查看中断升高的原因
236 |
237 | 根据 [4.1节](# 4.1 查看上下文切换) 的分析,终端次数也升高到了1万左右,从/proc/interrupts只读文件查看中断情况
238 |
239 | ```bash
240 | # -d 参数表示高亮显示变化的区域
241 | $ watch -d cat /proc/interrupts
242 | CPU0 CPU1
243 | ...
244 | RES: 2450431 5279697 Rescheduling interrupts
245 | ...
246 | ```
247 |
248 | 观察发现,变化速度最快的是**重调度中断(RES)**,它代表唤醒空闲状态的 CPU 来调度新的任务运行,这是在多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为**处理器间中断**(Inter-Processor Interrupts,IPI)
249 |
250 | **分析**:过多任务导致了重调度中断的升高,和前面分析结果一致
251 |
252 | #### 4.3 每秒上下文切换多少次正常?
253 |
254 | **上下文切换次数取决于系统本身的CPU性能**。如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,或者切换次数出现数量级的增长时,就很可能已经出现了性能问题,这时根据具体上下文切换的类型具体分析:
255 |
256 | * 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题
257 | * 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈
258 | * 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型
259 |
260 |
261 |
262 | ### 5 系统出现大量不可中断进程和僵尸进程怎么办?
263 |
264 | #### 5.1 进程状态
265 |
266 | * **R**:表示正在就绪队列中的进程,正在运行或者正在等待运行
267 | * **D**:Disk Sleep,不可中断状态睡眠(Uninterruptible Sleep),一般是进程和硬件交互,并且交互过程不允许其他进程或中断打断
268 | * **Z** :Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)
269 | * **S** :Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态
270 | * **I**: Idle 的缩写,也就是空闲状态,用在**不可中断睡眠的内核线程**上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会
271 | * **T**:Stopped或者Traced,表示进程处于暂停或者跟踪状态(SIGSTOP信号会让进程变为暂停状态,再发送SIGCONT信号,进程又会恢复运行)
272 | * **X**:Dead,表示进程已经消亡,top或者ps看不到
273 |
274 | > 不可中断状态,是为了保证进程数据与硬件状态一致,正常情况下,不可中断状态在很短时间内就会结束。短时的不可中断状态进程,我们一般可以忽略。
275 | >
276 | > 但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。需要注意下,系统是不是出现了 I/O 等性能问题。
277 |
278 | **注意**:ps查看进程状态时,会有Ss+,D+等情况,其中s表示进程是会话的领导进程,+表示前台进程组
279 |
280 |
281 |
282 | #### 5.2 案例分析
283 |
284 | ##### 5.2.1 指标分析
285 |
286 | 1. 运行案例的docker
287 |
288 | ```bash
289 | docker run --privileged --name=app -itd feisky/app:iowait
290 | ```
291 |
292 | 2. top查看指标
293 |
294 | ```bash
295 | # 按下数字 1 切换到所有 CPU 的使用情况,观察一会儿按 Ctrl+C 结束
296 | $ top
297 | top - 05:56:23 up 17 days, 16:45, 2 users, load average: 2.00, 1.68, 1.39
298 | Tasks: 247 total, 1 running, 79 sleeping, 0 stopped, 115 zombie
299 | %Cpu0 : 0.0 us, 0.7 sy, 0.0 ni, 38.9 id, 60.5 wa, 0.0 hi, 0.0 si, 0.0 st
300 | %Cpu1 : 0.0 us, 0.7 sy, 0.0 ni, 4.7 id, 94.6 wa, 0.0 hi, 0.0 si, 0.0 st
301 | ...
302 |
303 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
304 | 4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top
305 | 4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app
306 | 4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app
307 | 1 root 20 0 160072 9416 6752 S 0.0 0.1 0:38.59 systemd
308 | ...
309 | ```
310 |
311 | 3. 分析
312 |
313 | * 第一行的平均负载( Load Average),过去 1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。
314 | * 第二行的 Tasks,有 1 个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理。
315 | * CPU 的使用率情况,用户 CPU 和系统 CPU 都不高,但 iowait 分别是 60.5% 和 94.6%,好像有点儿不正常。
316 | * 最后再看每个进程的情况, CPU 使用率最高的进程只有 0.3%,看起来并不高;但有两个进程处于 D 状态,它们可能在等待 I/O,但光凭这里并不能确定是它们导致了 iowait 升高。
317 |
318 | 4. 结论
319 |
320 | * 第一点,iowait 太高了,导致系统的平均负载升高,甚至达到了系统 CPU 的个数
321 | * 第二点,僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。
322 |
323 | ##### 5.2.2 iowait分析
324 |
325 | 1. dstat查看系统I/O情况
326 |
327 | ```bash
328 | # 间隔1秒输出10组数据
329 | $ dstat 1 10
330 | You did not select any stats, using -cdngy by default.
331 | --total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
332 | usr sys idl wai stl| read writ| recv send| in out | int csw
333 | 0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885
334 | 0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138
335 | 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135
336 | 0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177
337 | 0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144
338 | 0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147
339 | 0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134
340 | 0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131
341 | 0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168
342 | 0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134
343 | ```
344 |
345 | 可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的
346 |
347 | 2. pidstat分析D状态的进程
348 |
349 | ```bash
350 | # -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
351 | $ pidstat -d -p 4344 1 3
352 | 06:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
353 | 06:38:51 0 4344 0.00 0.00 0.00 0 app
354 | 06:38:52 0 4344 0.00 0.00 0.00 0 app
355 | 06:38:53 0 4344 0.00 0.00 0.00 0 app
356 | ```
357 |
358 | * kB_rd 表示每秒读的 KB 数
359 | * kB_wr 表示每秒写的 KB 数
360 | * iodelay 表示 I/O 的延迟(单位是时钟周期)。
361 | * 它们都是 0,那就表示此时没有任何的读写,说明问题不是 4344 进程导致的。
362 |
363 | 3. pidstat查看所有进程情况
364 |
365 | ```bash
366 | # 间隔 1 秒输出多组数据 (这里是 20 组)
367 | $ pidstat -d 1 20
368 | ...
369 | 06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
370 | 06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1
371 | 06:48:47 0 6080 32768.00 0.00 0.00 170 app
372 | 06:48:47 0 6081 32768.00 0.00 0.00 184 app
373 |
374 | 06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
375 | 06:48:48 0 6080 0.00 0.00 0.00 110 app
376 |
377 | 06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
378 | 06:48:49 0 6081 0.00 0.00 0.00 191 app
379 |
380 | 06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
381 |
382 | 06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
383 | 06:48:51 0 6082 32768.00 0.00 0.00 0 app
384 | 06:48:51 0 6083 32768.00 0.00 0.00 0 app
385 |
386 | 06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
387 | 06:48:52 0 6082 32768.00 0.00 0.00 184 app
388 | 06:48:52 0 6083 32768.00 0.00 0.00 175 app
389 |
390 | 06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
391 | 06:48:53 0 6083 0.00 0.00 0.00 105 app
392 | ...
393 | ```
394 |
395 | 观察一会儿可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 32 MB,看来就是 app 的问题。不过,app 进程到底在执行啥 I/O 操作呢?**进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出 app 进程的系统调用**
396 |
397 | 4. strace跟踪进程
398 |
399 | ```bash
400 | $ strace -p 6082
401 | strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
402 | ```
403 |
404 | * 检查一下进程的状态,已经变成僵尸进程
405 |
406 | ```bash
407 | $ ps aux | grep 6082
408 | root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app]
409 | ```
410 |
411 | 5. 动态追踪
412 |
413 | ```bash
414 | $ perf record -g
415 | $ perf report
416 | ```
417 |
418 | 如下图,swapper是内核的调度进程,可忽略
419 |
420 | 可以发现, app 的确在通过系统调用 **sys_read()** 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行**直接读**,也就是**绕过了系统缓存**,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了
421 |
422 | 
423 |
424 | 6. 直接查看docker内代码
425 |
426 | 打开app.py文件,可以看到使用了 O_DIRECT 选项打开磁盘
427 |
428 | ```python
429 | open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
430 | ```
431 |
432 | > 直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O
433 |
434 | 7. 修复代码
435 |
436 | 修复后的文件名app-fix1.py,运行docker如下
437 |
438 | ```
439 | # 首先删除原来的应用
440 | $ docker rm -f app
441 | # 运行新的应用
442 | $ docker run --privileged --name=app -itd feisky/app:iowait-fix1
443 | ```
444 |
445 | top检查
446 |
447 | ```bash
448 | $ top
449 | top - 14:59:32 up 19 min, 1 user, load average: 0.15, 0.07, 0.05
450 | Tasks: 137 total, 1 running, 72 sleeping, 0 stopped, 12 zombie
451 | %Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.0 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
452 | %Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
453 | ...
454 |
455 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
456 | 3084 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app
457 | 3085 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app
458 | 1 root 20 0 159848 9120 6724 S 0.0 0.1 0:09.03 systemd
459 | 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
460 | 3 root 20 0 0 0 0 I 0.0 0.0 0:00.40 kworker/0:0
461 | ...
462 | ```
463 |
464 | ##### 5.2.3 僵尸进程分析
465 |
466 | > 僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,就需要找出父进程,然后在父进程里解决。
467 |
468 | 1. pstree
469 |
470 | ```bash
471 | # -a 表示输出每个程序完整的命令(包含路径,参数或是常驻服务的标示)
472 | # p指定PID
473 | # s表示显示指定进程的父进程
474 | $ pstree -aps 3084
475 | systemd,1
476 | └─dockerd,15006 -H fd://
477 | └─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml
478 | └─docker-containe,3991 -namespace moby -workdir...
479 | └─app,4009
480 | └─(app,3084)
481 | ```
482 |
483 | 2. 查看app-fix1.py代码
484 |
485 | ```python
486 | int status = 0;
487 | for (;;) {
488 | for (int i = 0; i < 2; i++) {
489 | if(fork()== 0) {
490 | sub_process();
491 | }
492 | }
493 | sleep(5);
494 | }
495 |
496 | while(wait(&status)>0);
497 | ```
498 |
499 | 可以发现,文件错误地把 wait() 放到了 for 死循环的外面,也就是说,wait() 函数实际上并没被调用到,我们把它挪到 for 循环的里面就可以了。
500 |
501 | 修改后的文件我放到了 app-fix2.c ,运行对应的docker
502 |
503 | ```bash
504 | # 先停止产生僵尸进程的 app
505 | $ docker rm -f app
506 | # 然后启动新的 app
507 | $ docker run --privileged --name=app -itd feisky/app:iowait-fix2
508 | ```
509 |
510 | 3. top查看
511 |
512 | 僵尸进程(Z 状态)没有了, iowait 也是 0,问题解决
513 |
514 | ```bash
515 |
516 | $ top
517 | top - 15:00:44 up 20 min, 1 user, load average: 0.05, 0.05, 0.04
518 | Tasks: 125 total, 1 running, 72 sleeping, 0 stopped, 0 zombie
519 | %Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
520 | %Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
521 | ...
522 |
523 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
524 | 3198 root 20 0 4376 840 780 S 0.3 0.0 0:00.01 app
525 | 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
526 | 3 root 20 0 0 0 0 I 0.0 0.0 0:00.41 kworker/0:0
527 | ...
528 | ```
529 |
530 |
531 |
532 | ### 6 怎么理解CPU软中断
533 |
534 | > 中断是一种异步的事件处理机制,可以提高系统的并发处理能力
535 | >
536 | > 为了减少对正常进程运行调度的影响,中断处理程序应该尽快完成
537 |
538 | #### 6.1 软中断
539 |
540 | 中断过程分为上半部和下半部:
541 |
542 | * 上半部:用来快速处理中断,它在中断禁止模式下运行,主要处理和**硬件紧密相关**或者**时间敏感**的工作
543 | * 下半部:用来延迟处理上半部未完成的工作,通常以**内核线程**的形式运行
544 |
545 | 网卡接收数据包的例子:网卡接收到数据包后,会通过硬件中断的方式,通知内核有新的数据到了。对上半部来说,既然是快速处理,其实就是要把网卡的数据**读到内存**中,然后**更新硬件寄存器的状态**(表示数据已经读好了),最后再发送一个软中断信号,通知下半部做进一步的处理。而下半部被软中断信号唤醒后,需要从内存中找到网络数据,再按照**网络协议栈**,对数据进行**逐层解析和处理**,直到把它送给应用程序。
546 |
547 | 可以理解为:**上半部快速执行,下半部延迟执行**
548 |
549 | #### 6.2 查看软中断和内核线程
550 |
551 | 1. 查看/proc文件系统
552 |
553 | * /proc/softirqs,提供了软中断的运行情况
554 | * /proc/interrupts,提供了硬中断的运行情况
555 |
556 | ```bash
557 | # 可以看到各类软中断在不同CPU上累积的运行次数
558 | $ cat /proc/softirqs
559 | CPU0 CPU1
560 | HI: 0 0
561 | TIMER: 811613 1972736
562 | NET_TX: 49 7
563 | NET_RX: 1136736 1506885
564 | BLOCK: 0 0
565 | IRQ_POLL: 0 0
566 | TASKLET: 304787 3691
567 | SCHED: 689718 1897539
568 | HRTIMER: 0 0
569 | RCU: 1330771 1354737
570 | ```
571 |
572 | **注意**:
573 |
574 | * 软中断的类型:第一列的内容,对应软中断的类型,比如**NET_TX代表网络接收中断,NET_RX代表网络发送中断,SCHE代表调度,TIMER代表定时器**等等
575 | * 每种软中断在不同CPU上的运行情况:同一行的内容,正常情况下,同一种中断在不同CPU上的累计次数应该差不多,比如NET_RX。 而TASKLET只在调用它的函数所在的CPU运行(存在的**问题**:由于只在一个 CPU 上运行导致的调度不均衡,或者因为不能在多个 CPU 上并行运行带来了性能限制)
576 |
577 | 2. 软中断以内核线程方式运行,每个CPU都对应一个软中断内核线程(ksoftirqd/CPU编号)
578 |
579 | ```bash
580 | $ ps aux | grep softirq
581 | root 7 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/0]
582 | root 16 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/1]
583 | ```
584 |
585 |
586 |
587 | ### 7 系统的软中断CPU使用率升高,该怎么办?
588 |
589 | #### 7.1 案例准备
590 |
591 | 工具介绍:
592 |
593 | * sar 是一个系统活动报告工具,既可以实时查看系统的当前活动,又可以配置保存和报告历史统计数据。
594 | * hping3 是一个可以构造 TCP/IP 协议数据包的工具,可以对系统进行安全审计、防火墙测试等。
595 | * tcpdump 是一个常用的网络抓包工具,常用来分析各种网络问题
596 |
597 | 案例图示
598 |
599 |
600 |
601 | 其中一台虚拟机运行 Nginx ,用来模拟待分析的 Web 服务器;而另一台当作 Web 服务器的客户端,用来给 Nginx 增加压力请求
602 |
603 | #### 7.2 操作和分析
604 |
605 | 运行Nginx应用
606 |
607 | ```bash
608 | # 运行Nginx服务并对外开放80端口
609 | $ docker run -itd --name=nginx -p 80:80 nginx
610 | ```
611 |
612 | 在另一个终端运行hping3模拟客户端的请求
613 |
614 | ```bash
615 | # -S参数表示设置TCP协议的SYN(同步序列号),-p表示目的端口为80
616 | # -i u100表示每隔100微秒发送一个网络帧
617 | # 注:如果你在实践过程中现象不明显,可以尝试把100调小,比如调成10甚至1
618 | $ hping3 -S -p 80 -i u100 192.168.0.30
619 | ```
620 |
621 | 会发现简单的shell命令都变慢了,执行top查看系统整体情况
622 |
623 | ```bash
624 | # top运行后按数字1切换到显示所有CPU
625 | $ top
626 | top - 10:50:58 up 1 days, 22:10, 1 user, load average: 0.00, 0.00, 0.00
627 | Tasks: 122 total, 1 running, 71 sleeping, 0 stopped, 0 zombie
628 | %Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 96.7 id, 0.0 wa, 0.0 hi, 3.3 si, 0.0 st
629 | %Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 95.6 id, 0.0 wa, 0.0 hi, 4.4 si, 0.0 st
630 | ...
631 |
632 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
633 | 7 root 20 0 0 0 0 S 0.3 0.0 0:01.64 ksoftirqd/0
634 | 16 root 20 0 0 0 0 S 0.3 0.0 0:01.97 ksoftirqd/1
635 | 2663 root 20 0 923480 28292 13996 S 0.3 0.3 4:58.66 docker-containe
636 | 3699 root 20 0 0 0 0 I 0.3 0.0 0:00.13 kworker/u4:0
637 | 3708 root 20 0 44572 4176 3512 R 0.3 0.1 0:00.07 top
638 | 1 root 20 0 225384 9136 6724 S 0.0 0.1 0:23.25 systemd
639 | 2 root 20 0 0 0 0 S 0.0 0.0 0:00.03 kthreadd
640 | ...
641 | ```
642 |
643 | 可以看到:
644 |
645 | * 平均负载全是 0,就绪队列里面只有一个进程(1 running)。
646 | * 每个 CPU 的使用率都挺低,最高的 CPU1 的使用率也只有 4.4%,并不算高。
647 | * 再看进程列表,CPU 使用率最高的进程也只有 0.3%
648 | * 两个 CPU 的使用率虽然分别只有 3.3% 和 4.4%,但都用在了软中断上;而从进程列表上也可以看到,CPU 使用率最高的也是软中断进程 ksoftirqd
649 |
650 | 查看软中断变化情况
651 |
652 | ```bash
653 | $ watch -d cat /proc/softirqs
654 | CPU0 CPU1
655 | HI: 0 0
656 | TIMER: 1083906 2368646
657 | NET_TX: 53 9
658 | NET_RX: 1550643 1916776
659 | BLOCK: 0 0
660 | IRQ_POLL: 0 0
661 | TASKLET: 333637 3930
662 | SCHED: 963675 2293171
663 | HRTIMER: 0 0
664 | RCU: 1542111 1590625
665 | ```
666 |
667 | 可以发现, TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU 锁)等这几个软中断都在不停变化,这些中断是保证 Linux 调度、时钟和临界区保护这些正常工作所必需,变化是正常的。而其中的NET_RX,也就是**网络数据包接收软中断**的变化速率最快
668 |
669 | 使用sar工具查看网络收发情况(可以观察网络收发吞吐量和PPS)
670 |
671 | ```bash
672 | # -n DEV 表示显示网络收发的报告,间隔1秒输出一组数据
673 | $ sar -n DEV 1
674 | 15:03:46 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
675 | 15:03:47 eth0 12607.00 6304.00 664.86 358.11 0.00 0.00 0.00 0.01
676 | 15:03:47 docker0 6302.00 12604.00 270.79 664.66 0.00 0.00 0.00 0.00
677 | 15:03:47 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
678 | 15:03:47 veth9f6bbcd 6302.00 12604.00 356.95 664.66 0.00 0.00 0.00 0.05
679 | ```
680 |
681 | 可以发现:
682 |
683 | * 对网卡 eth0 来说,每秒接收的网络帧数比较大,达到了 12607,而发送的网络帧数则比较小,只有 6304;每秒接收的千字节数只有 664 KB,而发送的千字节数更小,只有 358 KB。
684 | * docker0 和 veth9f6bbcd 的数据跟 eth0 基本一致,只是发送和接收相反,发送的数据较大而接收的数据较小。这是 Linux 内部网桥转发导致的,暂且不用深究,只要知道这是系统把 eth0 收到的包转发给 Nginx 服务即可
685 | * 重点来看 eth0 :接收的 PPS 比较大,达到 12607,而接收的 BPS 却很小,只有 664 KB。直观来看网络帧应该都是比较小的,664*1024/12607 = 54 字节,说明平均每个网络帧只有 54 字节,这显然是很小的网络帧,也就是所谓的**小包问题**
686 |
687 | tcpdump抓取eth0上的包,指定TCP协议和80端口
688 |
689 | ```bash
690 | # -i eth0 只抓取eth0网卡,-n不解析协议名和主机名
691 | # tcp port 80表示只抓取tcp协议并且端口号为80的网络帧
692 | $ tcpdump -i eth0 -n tcp port 80
693 | 15:11:32.678966 IP 192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq 458303614, win 512, length 0
694 | ...
695 | ```
696 |
697 | 从 tcpdump 的输出中,你可以发现:
698 |
699 | * 192.168.0.2.18238 > 192.168.0.30.80 ,表示网络帧从 192.168.0.2 的 18238 端口发送到 192.168.0.30 的 80 端口,也就是从运行 hping3 机器的 18238 端口发送网络帧,目的为 Nginx 所在机器的 80 端口。
700 | * Flags [S] 则表示这是一个 SYN 包
701 |
702 | **最后,可以确定这是从192.168.0.2.18238来的SYN FLOOF攻击**
703 |
704 | SYN FLOOD 问题最简单的解决方法:从交换机或者硬件防火墙中封掉来源 IP,这样 SYN FLOOD 网络帧就不会发送到服务器中(后面的网络篇再进一步深究)
705 |
706 | ### 8 套路篇:如何迅速分析出CPU的瓶颈在哪里?
707 |
708 | #### 8.1 CPU性能指标
709 |
710 | 性能指标总览
711 |
712 |
713 |
714 | ##### 8.1.1 CPU使用率
715 |
716 | CPU 使用率描述了非空闲时间占总 CPU 时间的百分比,根据 CPU 上运行任务的不同,又被分为用户 CPU、系统 CPU、等待 I/O CPU、软中断和硬中断等。用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态
717 |
718 | * CPU 使用率(nice),表示 CPU 在**用户态**运行的时间百分比。用户 CPU 使用率高,通常说明有**应用程序**比较繁忙。
719 | * 系统 CPU 使用率,表示 CPU 在**内核态**运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明**内核**比较繁忙。
720 | * 等待 I/O 的 CPU 使用率,通常也称为 **iowait**,表示**等待 I/O** 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
721 | * 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
722 | * 除了上面这些,还有在虚拟化环境中会用到的**窃取 CPU 使用率(steal)**和**客户 CPU 使用率(guest)**,分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。
723 |
724 | ##### 8.1.2 平均负载
725 |
726 | > 系统的平均活跃进程数。它反应了系统的整体负载情况,主要包括三个数值,分别指过去 1 分钟、过去 5 分钟和过去 15 分钟的平均负载。
727 | >
728 | > 理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑 CPU 个数,就表示负载比较重了。
729 |
730 | ##### 8.1.3 进程上下文切换
731 |
732 | 进程上下文切换分为:
733 |
734 | 1. 自愿上下文切换
735 | 2. 非自愿上下文切换
736 |
737 | **注意**:过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在**寄存器、内核栈以及虚拟内存等数据的保存和恢复**上,缩短进程真正运行的时间,成为性能瓶颈
738 |
739 | ##### 8.1.4 CPU缓存命中率
740 |
741 | CPU 缓存的速度介于 CPU 和内存之间,缓存的是**热点的内存数据**。
742 |
743 | 如下图,根据不断增长的热点数据,这些缓存按照大小不同分为 L1、L2、L3 等三级缓存,其中 L1 和 L2 常用在单核中, L3 则用在多核中。从 L1 到 L3,三级缓存的大小依次增大,相应的,性能依次降低(当然比内存还是好得多)。而它们的命中率,衡量的是 **CPU 缓存的复用情况**,命中率越高,则表示性能越好。
744 |
745 | 
746 |
747 | #### 8.2 CPU性能工具
748 |
749 | * 平均负载案例:使用**uptime**查看平均负载,在平均负载升高时,使用**mpstat**和**pidstat**分别观察每个CPU和每个进程CPU的使用情况,找到导致平均负载升高的stress进程
750 | * 上下文切换的案例:先使用**vmstat**,查看系统的上下文切换次数和中断次数;然后通过**pidstat**(-w参数)观察进程的自愿上下文切换和非自愿上下文切换;最后通过**vmstat**(-wt参数)查看线程的上下文切换情况,从而找到了线程上下文切换增多的原因是sysbench工具
751 | * 进程CPU使用率升高的案例:先使用top找出系统和进程CPU的使用情况,发现了CPU使用率很高的进程php-fpm,再使用perf top找出热点函数sqrt();如果是Python应用,可以使用profiler工具**pyflame**对指定进程分析(pyflame -p pid --threads -s 检测时间 -r 取样间隔 -o ),再通过flamegraph.pl将输出的txt文件转换为*.svg格式的火焰图(./flamegraph.pl prof.txt > prof.svg)
752 | * 不可中断进程和僵尸进程的案例:
753 | * 不可中断进程分析过程:先使用top查看,发现存在D状态(不可中断休眠进程)和Z状态(僵尸进程),并且iowait较高;使用**dstat**分析磁盘I/O,发现**app**进程有大量的磁盘读请求;使用**pidstat**(-d -p 参数)分析app进程的I/O操作,发现没有大量的I/O操作,再用pidstat -d分析系统的I/O情况,发现还是app进程在进行磁盘读;再使用**strace**跟踪D状态进程对应进程号的系统调用,发现没有权限;ps查看发现对应进程号的进程已经变成僵尸进程;之后,通过perf record -g和perf report生成报告,查看app进程的调用栈,发现CPU使用主要是在sys_read()函数,定位到是在对磁盘进行直接读(direct_IO);查看代码发现open()系统调用使用了O_DIRECT参数
754 | * 僵尸进程分析:使用pstree命令找出僵尸进程的父进程是app进程,然后查看app.c文件,发现wait()使用位置不当导致不能回收子进程
755 | * 软中断的案例:先使用top查看系统指标,发现系统CPU使用率很低,但是主要是在软中断si上,然后查看/proc/softirqs查看系统软中断变化情况,发现NET_RX变化率很快,再使用sar工具查看系统的网络收发情况,发现eth0网卡接收到了大量的小包;在通过抓包工具tcpdump,发现eth0接受到了大量的SYN包,最终确定了是SYN FLOOD攻击
756 |
757 | ##### 8.2.1 性能指标找工具
758 |
759 |
760 |
761 | ##### 8.2.2 工具找指标
762 |
763 |
764 |
765 | #### 8.3 如何分析CPU的性能瓶颈
766 |
767 | **重点**:弄清楚性能指标之间的关联性
768 |
769 |
770 |
771 | ### 9 CPU性能优化的几个思路
772 |
773 | #### 9.1 性能优化方法论
774 |
775 | 确定三个问题:
776 |
777 | * 判断所做的性能优化是否有效?优化后,能提升多少性能,有多少收益?
778 | * 如果有多个性能问题同时存在,应该先优化哪一个?
779 | * 当有多种优化的方法,应该选择哪一种?
780 |
781 | ##### 9.1.1 怎么评估性能优化的效果
782 |
783 | **三步走**的原则:
784 |
785 | 1. 确定性能的量化指标
786 | 2. 测试优化前的性能指标
787 | 3. 测试优化后的性能指标
788 |
789 | **第一步**,性能的量化指标包括CPU使用率、应用的吞吐量、响应时间等等,**不要局限在单一维度的指标上**。例如,以Web应用为例:
790 |
791 | * 应用程序的维度,使用**吞吐量和请求延时**来评估
792 | * 系统资源的维度,使用**CPU使用率**来评估
793 |
794 | 好的应用程序是性能优化的最终结果和目的,要使用应用程序的指标,来评估性能优化的整体效果;而系统资源的使用情况是影响应用程序的根源,需要用资源的指标,来分析应用性能的瓶颈来源
795 |
796 | **第二三步**,对比第一步确定的**量化指标**在优化前后的差距,拿数据说话。例如,使用ab工具测试Web应用的并发请求数和响应延时,同时使用vmstat,pidstat等工具,观察系统和进程的CPU使用率,同时获得了应用和系统两个维度的性能指标
797 |
798 | **进行性能测试需要注意的是**:
799 |
800 | * 要避免性能测试工具干扰应用程序的性能
801 | * 避免外部环境的变化影响性能指标的评估。在优化前、后的应用程序,都运行在相同配置的机器上,并且它们的外部依赖也要完全一致
802 |
803 | ##### 9.1.2 多个性能问题同时存在,怎么选择?
804 |
805 | 遵循**二八原则**,80%的性能问题都是由于20%的代码导致的,**并不是所有的性能问题都值得优化**
806 |
807 | 分析的步骤:
808 |
809 | * 挨个分析出所有的性能瓶颈,排除掉有因果关系的性能问题
810 | * 在剩下的几个性能问题中,选择能明显提升应用性能的问题进行修复,有两种方法:
811 | * 如果系统资源出现瓶颈,首先优化系统资源使用的问题
812 | * 针对不同类型的指标,,首先优化导致**性能指标变化幅度最大**的那些瓶颈问题
813 |
814 | ##### 9.1.3 有多种优化方法时,如何选择?
815 |
816 | **性能优化并非没有成本**。
817 |
818 | 一个很典型的例子网络中的 DPDK(Data Plane Development Kit)。DPDK 是一种优化网络处理速度的方法,它通过绕开内核网络协议栈的方法,提升网络的处理能力。不过它有一个很典型的要求,就是要**独占一个 CPU 以及一定数量的内存大页**,并且总是以 100% 的 CPU 使用率运行。所以,如果你的 CPU 核数很少,就有点得不偿失了。
819 |
820 | 因此,在考虑性能优化方法时,要结合实际情况,考虑多方面的因素,进行权衡在做选择
821 |
822 | #### 9.2 CPU优化
823 |
824 | ##### 9.2.1 应用程序优化
825 |
826 | 常见的几种优化方法:
827 |
828 | * **编译器优化**:很多编译器都会提供优化选项,适当开启它们,在编译阶段你就可以获得编译器的帮助,来提升性能。比如, gcc 就提供了优化选项 -O2,开启后会自动对应用程序的代码进行优化。
829 | * **算法优化**:使用复杂度更低的算法,显著加快处理速度
830 | * **异步处理**:使用异步处理,可以避免程序因为等待某个资源而一直阻塞,从而提升程序的并发处理能力。比如,把轮询替换为事件通知,就可以避免轮询耗费 CPU 的问题。
831 | * **多线程代替多进程**:前面讲过,相对于进程的上下文切换,线程的上下文切换并不切换进程地址空间,因此可以降低上下文切换的成本。
832 | * **善用缓存**:经常访问的数据或者计算过程中的步骤,可以放到内存中缓存起来,这样在下次用时就能直接从内存中获取,加快程序的处理速度。
833 |
834 | ##### 9.2.2 系统优化
835 |
836 | 常见的系统优化方法:
837 |
838 | * **CPU 绑定**:把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题
839 | * **CPU 独占**:跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些 CPU
840 | * **优先级调整**:使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。可以适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理
841 | * **为进程设置资源限制**:使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。
842 | * **NUMA(Non-Uniform Memory Access)优化**:支持 NUMA 的处理器会将内存划分为多个 node,每个 node 关联到系统的一个处理器。NUMA 优化,其实就是让 CPU 尽可能只访问本地内存。
843 | * **中断负载均衡**:无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的 CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把**中断处理过程自动负载均衡到多个 CPU 上**。
844 |
845 | ##### 9.2.3 避免过早优化
846 |
847 | 性能优化最好是**逐步完善,动态进行,不追求一步到位**,而要**首先保证能满足当前的性能要求**。当发现性能不满足要求或者出现性能瓶颈时,再根据性能评估的结果,选择最重要的性能问题进行优化
848 |
849 | #### 9.3 总结
850 |
851 | **要忍住“把 CPU 性能优化到极致”的冲动**,因为 CPU 并不是唯一的性能因素,还会有其他的性能问题,比如内存、网络、I/O 甚至是架构设计的问题。
852 |
853 | 如果不做全方位的分析和测试,只是单纯地把某个指标提升到极致,并不一定能带来整体的收益。
854 |
855 | ## 内存性能篇
856 |
857 | ### 10 Linux内存工作原理
858 |
859 | #### 10.1 内存分配与回收
860 |
861 | malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()
862 |
863 | 对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
864 |
865 | 对大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去
866 |
867 | 各自的**优缺点**:
868 |
869 | * brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率;不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片
870 | * mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大
871 |
872 | 整体来说,Linux 使用**伙伴系统**来管理内存分配。前面我们提到过,这些内存在 MMU 中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如 brk 方式造成的内存碎片)
873 |
874 | 在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。在内核空间,Linux 通过 slab 分配器来管理小内存,可以把 slab 看成构建在**伙伴系统上的一个缓存**,主要作用就是分配并释放内核中的小对象
875 |
876 | 系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存:
877 |
878 | * 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面
879 | * 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中(会用到交换分区)
880 | * 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程
881 |
882 | OOM是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:
883 |
884 | * 进程消耗的内存越大,oom_score 就越大
885 | * 进程运行占用的 CPU 越多,oom_score 就越小
886 |
887 | 可以手动设置进程的oom_adj来调整oom_score。oom_adj的范围是[-17, 15],数值越大,进程越容易被OOM杀死;反之,越不容易被OOM杀死
888 |
889 | #### 10.2 如何查看内存使用情况
890 |
891 | 1. `free`命令
892 |
893 | ```bash
894 | # 注意不同版本的free输出可能会有所不同
895 | $ free
896 | total used free shared buff/cache available
897 | Mem: 8169348 263524 6875352 668 1030472 7611064
898 | Swap: 0 0 0
899 | ```
900 |
901 | * 第一列,total 是总内存大小;
902 | * 第二列,used 是已使用内存的大小,包含了共享内存;
903 | * 第三列,free 是未使用内存的大小;
904 | * 第四列,shared 是共享内存的大小;
905 | * 第五列,buff/cache 是缓存和缓冲区的大小;
906 | * 最后一列,available 是新进程可用内存的大小
907 |
908 | **注意**:available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大。不过,并不是所有缓存都可以回收,因为有些缓存可能正在使用中
909 |
910 | 2. `top`命令
911 |
912 | 可以查看每个进程的内存使用情况
913 |
914 | ```bash
915 | # 按下M切换到内存排序
916 | $ top
917 | ...
918 | KiB Mem : 8169348 total, 6871440 free, 267096 used, 1030812 buff/cache
919 | KiB Swap: 0 total, 0 free, 0 used. 7607492 avail Mem
920 |
921 |
922 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
923 | 430 root 19 -1 122360 35588 23748 S 0.0 0.4 0:32.17 systemd-journal
924 | 1075 root 20 0 771860 22744 11368 S 0.0 0.3 0:38.89 snapd
925 | 1048 root 20 0 170904 17292 9488 S 0.0 0.2 0:00.24 networkd-dispat
926 | 1 root 20 0 78020 9156 6644 S 0.0 0.1 0:22.92 systemd
927 | 12376 azure 20 0 76632 7456 6420 S 0.0 0.1 0:00.01 systemd
928 | 12374 root 20 0 107984 7312 6304 S 0.0 0.1 0:00.00 sshd
929 | ...
930 | ```
931 |
932 | 主要的几个信息:
933 |
934 | * VIRT 是**进程虚拟内存**的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内
935 | * RES 是**常驻内存**的大小,也就是进程**实际使用的物理内存**大小,但**不包括 Swap 和共享内存**
936 | * SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等
937 | * %MEM 是进程使用物理内存占系统总内存的百分比
938 |
939 | **注意**:
940 |
941 | * 虚拟内存通常并不会全部分配物理内存。从上面的输出,你可以发现每个进程的虚拟内存都比常驻内存大得多
942 | * 共享内存 SHR 并不一定是共享的,比方说,**程序的代码段、非共享的动态链接库**,也都算在 SHR 里。SHR 也包括了**进程间真正共享的内存**。所以在计算多个进程的内存使用时,不要把所有进程的 SHR 直接相加得出结果
943 |
944 | ### 11 内存的Buffer和Cache
945 |
946 | #### 11.1 free的数据来源
947 |
948 | man free查看
949 |
950 | 
951 |
952 | 从手册看到:
953 |
954 | * Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值
955 | * Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 Slab之和
956 |
957 | #### 11.2 proc文件系统
958 |
959 | man proc
960 |
961 | 
962 |
963 | 
964 |
965 | 通过文档可以看到:
966 |
967 | * Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
968 | * Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘
969 | * Slab 代表内核数据结构缓存,包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录
970 |
971 | #### 11.3 案例测试
972 |
973 | ##### 11.3.1 场景一:磁盘和文件写
974 |
975 | 运行vmstat命令
976 |
977 | ```bash
978 | [root@VM_194_74_centos ~]# vmstat 1
979 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
980 | r b swpd free buff cache si so bi bo in cs us sy id wa st
981 | 1 0 0 14613256 131120 772856 0 0 0 16 2 1 1 1 98 0 0
982 | 0 0 0 14612504 131120 772848 0 0 0 0 1323 1513 1 1 98 0 0
983 | 0 0 0 14612044 131120 772824 0 0 0 0 1297 1537 1 1 98 0 0
984 | 1 0 0 14612484 131120 772812 0 0 0 0 1229 1562 1 1 99 0 0
985 | 0 0 0 14612252 131120 772784 0 0 0 0 1338 1571 1 1 98 0 0
986 | 0 0 0 14612640 131120 772812 0 0 0 72 1231 1533 1 1 98 0 0
987 | 1 0 0 14612500 131120 772828 0 0 0 0 1258 1612 1 0 98 0 0
988 | 0 0 0 14612568 131120 772840 0 0 0 0 1241 1579 1 1 98 0 0
989 | 0 0 0 14612676 131124 772848 0 0 0 164 1247 1506 1 1 98 0 0
990 | 1 0 0 14612908 131132 772800 0 0 0 28 1284 1517 1 1 98 0 0
991 | 0 0 0 14612396 131132 772804 0 0 0 0 1257 1517 1 1 98 0 0
992 | 0 0 0 14612264 131132 772784 0 0 0 0 1236 1497 1 1 98 0 0
993 | ```
994 |
995 | * buff 和 cache 就是我们前面看到的 Buffers 和 Cache,单位是 KB
996 | * bi 和 bo 则分别表示块设备读取和写入的大小,单位为块 / 秒。因为 Linux 中块的大小是 1KB,所以这个单位也就等价于 KB/s
997 |
998 | 在另一终端执行dd命令通过读取随机设备,生成一个 500MB 大小的文件
999 |
1000 | ```bash
1001 | dd if=/dev/urandom of=/data/file bs=1M count=500
1002 | ```
1003 |
1004 | 继续观察buff和cache的变化如下
1005 |
1006 | ```bash
1007 | [root@VM_194_74_centos /tmp]# vmstat 1
1008 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
1009 | r b swpd free buff cache si so bi bo in cs us sy id wa st
1010 | 3 0 0 14519588 141424 853684 0 0 0 16 2 1 1 1 98 0 0
1011 | 1 0 0 14508040 141428 864424 0 0 0 56 2358 2068 1 14 85 0 0
1012 | 3 1 0 14495872 141440 875812 0 0 0 816 2290 2160 1 13 85 1 0
1013 | 2 0 0 14487364 141564 886348 0 0 0 1652 2837 3160 0 13 85 2 0
1014 | 1 0 0 14477220 141644 896736 0 0 0 2040 3100 3700 1 13 83 2 0
1015 | 1 0 0 14465036 141744 908372 0 0 0 1440 2903 2931 2 13 83 2 0
1016 | 1 0 0 14454148 141936 918752 0 0 0 2088 3608 4388 1 13 83 3 0
1017 | 1 0 0 14442756 142124 930444 0 0 0 1848 3596 4150 1 14 83 3 0
1018 | ...
1019 | 2 1 0 14273756 143756 1097612 0 0 0 1896 3653 4056 1 13 83 3 0
1020 | 2 1 0 14261684 143796 1109212 0 0 0 1932 3229 3276 1 13 83 3 0
1021 | 1 1 0 14250692 143880 1119576 0 0 0 1572 3168 3173 1 13 83 2 0
1022 | 4 1 0 14236556 143928 1131176 0 0 0 1684 3120 3054 0 13 84 2 0
1023 | 1 0 0 14228684 144008 1141704 0 0 0 2336 3421 3446 1 13 83 3 0
1024 | 1 0 0 14218020 144076 1153268 0 0 0 2044 3590 3837 1 13 83 3 0
1025 | 2 0 0 14206820 144244 1164052 0 0 8 105372 3650 3916 1 13 82 3 0
1026 | 0 2 0 14197416 144316 1173492 0 0 4 152984 3821 4611 1 13 69 17 0
1027 | 1 1 0 14187000 144392 1184100 0 0 0 138236 3805 4367 1 12 72 15 0
1028 | 1 0 0 14175248 144468 1194476 0 0 0 2032 3816 4080 1 13 82 4 0
1029 | ```
1030 |
1031 | 可以看到:
1032 |
1033 | * 在 dd 命令运行时, Cache 在不停地增长,而 Buffer 基本保持不变
1034 | * 当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且,多次 I/O 写的结果加起来,才是 dd 要写的 500M 的数据
1035 |
1036 | 同样的,使用dd命令进行磁盘写入(由于需要系统配置多块磁盘,并且磁盘分区 /dev/sdb1 还要处于未使用状态,没有实际操作)
1037 |
1038 | ```bash
1039 | dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
1040 | ```
1041 |
1042 | 可以看到,**写磁盘用到了大量的 Buffer**
1043 |
1044 | ##### 11.3.2 场景二:磁盘和文件读
1045 |
1046 | 运行文件读的命令如下
1047 |
1048 | ```bash
1049 | # 文件读
1050 | dd if=/tmp/file of=/dev/null
1051 | ```
1052 |
1053 | ```bash
1054 | [root@VM_194_74_centos /tmp]# vmstat 1
1055 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
1056 | r b swpd free buff cache si so bi bo in cs us sy id wa st
1057 | 1 0 0 15026816 916 487588 0 0 0 17 3 1 1 1 98 0 0
1058 | 0 0 0 15024456 1304 488564 0 0 1736 0 1469 1738 1 1 97 0 0
1059 | 0 0 0 15023804 1312 489356 0 0 0 88 1240 1515 1 1 98 0 0
1060 | 1 0 0 14913544 1316 600244 0 0 111040 0 2174 3221 1 2 93 4 0
1061 | 0 1 0 14753816 1320 759112 0 0 159364 0 2515 3853 2 2 86 10 0
1062 | 1 0 0 14600624 1320 912268 0 0 152704 0 2494 3843 1 2 86 10 0
1063 | 0 0 0 14511172 1320 1001580 0 0 89008 0 2019 2887 1 2 91 6 0
1064 | 1 0 0 14511452 1324 1001532 0 0 4 0 1215 1414 1 1 98 0 0
1065 | 1 0 0 14512776 1332 1001544 0 0 0 100 978 977 0 0 99 0 0
1066 | 0 0 0 14512776 1332 1001528 0 0 0 0 1248 1501 1 1 98 0 0
1067 | 0 0 0 14512272 1332 1001552 0 0 0 0 1306 1521 1 1 98 0 0
1068 | ```
1069 |
1070 | 运行磁盘读的命令如下
1071 |
1072 | ```bash
1073 | # 磁盘度
1074 | dd if=/dev/sda1 of=/dev/null bs=1M count=1024
1075 | ```
1076 |
1077 | ```bash
1078 | [root@VM_194_74_centos /tmp]# vmstat 1
1079 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
1080 | r b swpd free buff cache si so bi bo in cs us sy id wa st
1081 | 1 1 0 15016116 7920 490748 0 0 0 17 3 1 1 1 98 0 0
1082 | 0 0 0 15014992 8816 490736 0 0 0 1960 2577 4077 1 1 96 3 0
1083 | 0 1 0 15012224 9812 492796 0 0 2060 2224 2745 4192 1 1 94 3 0
1084 | 0 0 0 15010188 10748 493040 0 0 4 1984 2423 3921 1 1 96 3 0
1085 | 0 2 0 14814940 200276 497868 0 0 188992 1912 3979 6307 1 2 87 10 0
1086 | 1 2 0 14656116 355108 501324 0 0 153856 1508 3356 5470 1 2 86 11 0
1087 | 2 2 0 14495260 509548 505376 0 0 153600 1772 3275 5219 1 2 86 11 0
1088 | 1 1 0 14339432 664144 509788 0 0 153856 1564 3228 5322 0 2 87 11 0
1089 | 1 1 0 14171264 828176 513820 0 0 163072 2100 3983 6555 1 2 86 11 0
1090 | 0 1 0 14012056 984016 517804 0 0 154880 2112 4066 6601 1 2 86 11 0
1091 | 0 0 0 13928396 1065744 520012 0 0 80672 2436 3445 5497 1 1 90 7 0
1092 | 0 0 0 13928432 1066576 520244 0 0 0 1876 2521 3985 1 1 96 2 0
1093 | 1 1 0 13924696 1068660 520500 0 0 0 4488 3529 5999 2 1 91 6 0
1094 | ```
1095 |
1096 | 可以看到,读取文件时(也就是 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长;而读取磁盘时,Cache保持不变,Buffer不断增长
1097 |
1098 | #### 11.4 结论
1099 |
1100 | **Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中**
1101 |
1102 |
1103 |
1104 | ### 12 案例篇:如何利用系统缓存优化程序的运行效率
1105 |
1106 |
1107 |
1108 | ### 13 案例篇:内存泄漏该如何定位和处理
1109 |
1110 |
1111 |
1112 | ### 14 系统的Swap机制
1113 |
1114 | **文件页**:代表可回收内存,文件页的大部分可以直接回收,以后有需要时,再从磁盘重新读取;而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放
1115 |
1116 | 脏页一般以两种方式写入磁盘:
1117 |
1118 | * 在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中
1119 | * 由内核线程 pdflush 负责这些脏页的刷新
1120 |
1121 | **匿名页**:应用程序动态分配的**堆内存**,使用Swap机制回收
1122 |
1123 | #### 14.1 Swap原理
1124 |
1125 | Swap 简单来说就是把一块磁盘空间或者一个本地文件,当成内存来使用。它包括**换出和换入**两个过程。
1126 |
1127 | * 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
1128 | * 换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来
1129 |
1130 | 常见的笔记本电脑的休眠和快速开机的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度
1131 |
1132 | 内存回收的时机:
1133 |
1134 | * **直接内存回收**:当有新的大块内存分配请求,但是剩余内存不足,这个时候系统就需要回收一部分内存
1135 | * **内核线程kswapd0**来定期回收内存,它定义了三个内存阈值(watermark,也称为水位),分别是页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示
1136 |
1137 | 
1138 |
1139 | kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作:
1140 |
1141 | * 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存
1142 | * 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止
1143 | * 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求
1144 | * 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力
1145 |
1146 | **页低阈值**是由内核选项 /proc/sys/vm/min_free_kbytes 设置,其他两个阈值,都是根据页最小阈值计算生成的
1147 |
1148 | #### 14.2 NUMA和Swap
1149 |
1150 | 在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等,如下图所示:
1151 |
1152 | 
1153 |
1154 | 使用numactl命令查看Node的分布情况
1155 |
1156 | ```bash
1157 | [root@VM_194_74_centos /tmp]# numactl --hardware
1158 | available: 1 nodes (0)
1159 | node 0 cpus: 0 1 2 3 4 5 6 7
1160 | node 0 size: 16383 MB
1161 | node 0 free: 13452 MB
1162 | node distances:
1163 | node 0
1164 | 0: 10
1165 | ```
1166 |
1167 | 前面提到的三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看
1168 |
1169 | ```bash
1170 | [root@VM_194_74_centos ~/millerxie/taskCreate]# cat /proc/zoneinfo| head -n 20
1171 | Node 0, zone DMA
1172 | pages free 3969
1173 | min 3
1174 | low 3
1175 | high 4
1176 | scanned 0
1177 | spanned 4095
1178 | present 3998
1179 | managed 3977
1180 | nr_free_pages 3969
1181 | nr_inactive_anon 0
1182 | nr_active_anon 0
1183 | nr_inactive_file 0
1184 | nr_active_file 0
1185 | ...
1186 | ```
1187 |
1188 | 主要指标包括:
1189 |
1190 | * pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是**剩余内存页数**,它跟后面的 nr_free_pages 相同。
1191 | * nr_zone_active_anon 和 nr_zone_inactive_anon,分别是**活跃和非活跃的匿名页数**
1192 | * nr_zone_active_file 和 nr_zone_inactive_file,分别是**活跃和非活跃的文件页数**
1193 |
1194 | 某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它支持以下几个选项:
1195 |
1196 | * 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存
1197 | * 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存
1198 |
1199 | #### 14.3 swapness
1200 |
1201 | 内存回收包括文件页和匿名页:
1202 |
1203 | * 对文件页的回收,是直接回收缓存,或者把脏页写回磁盘后再回收
1204 | * 对匿名页的回收,是通过 **Swap 机制**,把它们写入磁盘后再释放内存
1205 |
1206 | Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度:
1207 |
1208 | swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页
1209 |
1210 | ### 15 系统Swap升高的原因
1211 |
1212 | #### 15.1 案例
1213 |
1214 | Linux 本身支持两种类型的 Swap,即 Swap 分区和 Swap 文件,以Swap文件为例,运行如下命令开启Swap文件
1215 |
1216 | ```bash
1217 | # 创建swap文件
1218 | fallocate -l 500M /data/swapfile
1219 | # 修改权限,仅root用户可读写
1220 | chmod 600 /data/swapfile
1221 | # 配置swap文件
1222 | mkswap /data/swapfile
1223 | # 开启swap
1224 | swapon /data/swapfile
1225 | ```
1226 |
1227 | 执行free看到swap添加成功
1228 |
1229 | ```bash
1230 | [root@VM_194_74_centos /data]# free
1231 | total used free shared buff/cache available
1232 | Mem: 16092196 562160 613616 295992 14916420 15135272
1233 | Swap: 511996 0 511996
1234 | ```
1235 |
1236 | 执行dd命令,模拟大文件的读取
1237 |
1238 | ```bash
1239 | dd if=/dev/vdb1 of=/dev/null bs=1G count=400
1240 | ```
1241 |
1242 | 执行sar查看内存和swap指标
1243 |
1244 | ```bash
1245 | [root@VM_194_74_centos ~]# sar -rS 3
1246 | Linux 3.10.107-1-tlinux2_kvm_guest-0051 (VM_194_74_centos) 07/05/20 _x86_64_ (8 CPU)
1247 |
1248 | 20:16:14 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
1249 | 20:16:17 12016228 4075968 25.33 1944876 394804 3089884 18.61 2364636 1364468 88
1250 |
1251 | 20:16:14 kbswpfree kbswpused %swpused kbswpcad %swpcad
1252 | 20:16:17 511996 0 0.00 0 0.00
1253 |
1254 | 20:16:17 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
1255 | 20:16:20 11542068 4550128 28.28 2405684 394868 3089856 18.61 2364984 1825212 120
1256 |
1257 | 20:16:17 kbswpfree kbswpused %swpused kbswpcad %swpcad
1258 | 20:16:20 511996 0 0.00 0 0.00
1259 | ...
1260 |
1261 | 20:16:50 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
1262 | 20:16:53 6331744 9760452 60.65 7474548 395052 3089736 18.61 2364948 6894124 84
1263 |
1264 | 20:16:50 kbswpfree kbswpused %swpused kbswpcad %swpcad
1265 | 20:16:53 511996 0 0.00 0 0.00
1266 | ...
1267 |
1268 | 20:17:44 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
1269 | 20:17:47 90140 16002056 99.44 13573540 392440 3089728 18.61 2180308 13149900 164
1270 |
1271 | 20:17:44 kbswpfree kbswpused %swpused kbswpcad %swpcad
1272 | 20:17:47 486836 25160 4.91 120 0.48
1273 | ...
1274 |
1275 | 20:18:44 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
1276 | 20:18:47 87988 16004208 99.45 13658536 354504 3089760 18.61 2112144 13218932 140
1277 |
1278 | 20:18:44 kbswpfree kbswpused %swpused kbswpcad %swpcad
1279 | 20:18:47 403652 108344 21.16 140 0.13
1280 | ```
1281 |
1282 | 可以看到,总的内存使用率(%memused)在不断增长,从开始的 25% 一直长到了 99%,并且主要内存都被缓冲区(kbbuffers)占用,大致的变化过程为:
1283 |
1284 | * 刚开始,剩余内存(kbmemfree)不断减少,而缓冲区(kbbuffers)则不断增大,由此可知,剩余内存不断分配给了缓冲区;
1285 | * 一段时间后,剩余内存已经很小,而缓冲区占用了大部分内存。此时,Swap 的使用开始逐渐增大,缓冲区和剩余内存则只在小范围内波动
1286 |
1287 | 为什么Swap会升高呢?(按理来说应该先回收缓冲区中的内存,这属于可回收内存),观察/proc/zoneinfo指标如下
1288 |
1289 | ```bash
1290 | $ watch -d grep -A 15 'Normal' /proc/zoneinfo
1291 |
1292 | Every 2.0s: grep -A 15 Normal /proc/zoneinfo Sun Jul 5 20:19:39 2020
1293 |
1294 | Node 0, zone Normal
1295 | pages free 5200
1296 | min 3268
1297 | low 4085
1298 | high 4902
1299 | scanned 24
1300 | spanned 3407872
1301 | present 3407872
1302 | managed 3276302
1303 | nr_free_pages 5200
1304 | nr_inactive_anon 134532
1305 | nr_active_anon 246943
1306 | nr_inactive_file 2466171
1307 | nr_active_file 280987
1308 | nr_unevictable 0
1309 | nr_mlock 0
1310 | ```
1311 |
1312 | 可以发现,剩余内存(pages_free)在一个小范围内不停地波动。当它小于页低阈值(pages_low) 时,又会突然增大到一个大于页高阈值(pages_high)的值
1313 |
1314 | * 当剩余内存小于页低阈值时,系统会回收一些缓存和匿名内存,使剩余内存增大。其中,缓存的回收导致 sar 中的缓冲区减小,而匿名内存的回收导致了 Swap 的使用增大。
1315 | * 同时由于 dd 还在继续,剩余内存又会重新分配给缓存,导致剩余内存减少,缓冲区增大
1316 |
1317 | 利用proc 文件系统,可以查看进程 Swap 换出的虚拟内存大小,它保存在 /proc/pid/status 中的 VmSwap
1318 |
1319 | ```bash
1320 | [root@VM_194_74_centos /]# for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head
1321 | systemd-journal 3048 86160 kB
1322 | writeback 50
1323 | watchdog/7 41
1324 | watchdog/6 36
1325 | watchdog/5 31
1326 | watchdog/4 26
1327 | watchdog/3 21
1328 | watchdog/2 16
1329 | watchdog/1 11
1330 | watchdog/0 10
1331 | ```
1332 |
1333 | 可以看到,使用swap较多的是systemd-journal进程
1334 |
1335 | 结束之后,需要关闭swap
1336 |
1337 | ```bash
1338 | swapoff -a
1339 | ```
1340 |
1341 | 一般关闭swap并重新打开,可以这么执行(是一种常见的swap清理方法)
1342 |
1343 | ```bash
1344 | swapoff -a && swapon /data/swapfile
1345 | ```
1346 |
1347 | #### 15.2 小结
1348 |
1349 | 在内存资源紧张时,Linux 会通过 Swap ,把不常访问的匿名页换出到磁盘中,下次访问的时候再从磁盘换入到内存中来。你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值;也可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向
1350 |
1351 | 当 Swap 变高时,你可以用 sar、/proc/zoneinfo、/proc/pid/status 等方法,查看系统和进程的内存使用情况,进而找出 Swap 升高的根源和受影响的进程
1352 |
1353 | 通常,降低 Swap 的使用,可以提高系统的整体性能。有几种常见的降低方法:
1354 |
1355 | * 禁止 Swap,现在服务器的内存足够大,所以除非有必要,一般会**禁用 Swap**,大部分云平台中的虚拟机都默认禁止 Swap
1356 | * 如果实在需要用到 Swap,可以尝试**降低 swappiness** 的值,减少内存回收时 Swap 的使用倾向
1357 | * 响应延迟敏感的应用,如果它们可能在开启 Swap 的服务器中运行,你还可以用库函数 mlock() 或者 mlockall() **锁定内存**,阻止它们的内存换出
1358 |
1359 | 常见的三种清理缓存的方法:
1360 |
1361 | * 清理pagecache
1362 |
1363 | ```bash
1364 | echo 1 > /proc/sys/vm/drop_caches # 或者 sysctl -w vm.drop_caches = 1
1365 | ```
1366 |
1367 | * 清理dentries和inodes
1368 |
1369 | ```bash
1370 | echo 2 > /proc/sys/vm/drop_caches # 或者 sysctl -w vm.drop_caches = 2
1371 | ```
1372 |
1373 | * 清理pagecache,dentries和inodes
1374 |
1375 | ```bash
1376 | echo 3 > /proc/sys/vm/drop_caches # 或者 sysctl -w vm.drop_caches = 3
1377 | ```
1378 |
1379 | * 使用sync命令来清理文件系统缓存,还会清理僵尸(zombie)对象和它们占用的内存
1380 |
1381 | ```bash
1382 | sync
1383 | ```
1384 |
1385 | ### 16 内存泄漏如何定位和解决
1386 |
1387 |
1388 |
1389 | ### 17 利用系统缓存优化程序运行效率
1390 |
1391 |
1392 |
1393 | ### 18 如何“快准狠”找到系统内存的问题
1394 |
1395 | #### 18.1 内存性能指标
1396 |
1397 | 系统调用内存分配请求后,并不会立刻为其分配物理内存,而是在请求首次访问时,通过缺页异常来分配
1398 |
1399 | 缺页异常又分为下面两种场景:
1400 |
1401 | * 可以直接从物理内存中分配时,被称为**次缺页异常**
1402 | * 需要磁盘 I/O 介入(比如 Swap)时,被称为**主缺页异常**
1403 |
1404 | 
1405 |
1406 | #### 18.2 分析内存性能瓶颈
1407 |
1408 | 分析过程如下图
1409 |
1410 | * 先用 free 和 top,查看系统整体的内存使用情况
1411 | * 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型
1412 | * 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等
1413 |
1414 | 
1415 |
1416 | #### 18.3 总结
1417 |
1418 | 内存常见的优化思路有这么几种
1419 |
1420 | * 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向
1421 | * 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等
1422 | * 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问
1423 | * 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽
1424 | * 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死
1425 |
1426 | ### 19 文件系统和磁盘的区别
1427 |
1428 | 磁盘是一个存储设备(确切地说是块设备),可以被划分为不同的磁盘分区。而在磁盘或者磁盘分区上,还可以再创建文件系统,并挂载到系统的某个目录中。这样,系统就可以通过这个挂载目录,来读写文件
1429 |
1430 | 换句话说,磁盘是存储数据的块设备,也是文件系统的载体。所以,文件系统确实还是要通过磁盘,来保证数据的持久化存储
1431 |
1432 | **Linux 中一切皆文件**。可以通过相同的文件接口,来访问磁盘和文件(比如 open、read、write、close 等)
1433 |
1434 | * 通常说的“文件”,是指普通文件
1435 | * 磁盘和分区,是指块设备文件
1436 |
1437 | 在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交互。而在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的“裸 I/O”。文件系统管理的缓存,是 Cache 的一部分;而裸磁盘的缓存,用的正是Buffer
1438 |
1439 | ## IO性能篇
1440 |
1441 | ### 20 Linux文件系统如何工作
1442 |
1443 | #### 20.1 索引节点和目录项
1444 |
1445 | * 索引节点,简称inode,和文件一一对应,存储在磁盘中,记录文件的元数据
1446 | * 目录项,dentry,记录文件的名字、索引节点以及其他目录项的关联关系
1447 |
1448 | 举例说明,为文件创建的硬链接,会对应不同的目录项,他们都连接到同一个文件,索引节点相同
1449 |
1450 | 磁盘的最小单位是**扇区**,文件系统将连续的扇区组成逻辑块,以逻辑块为最小单位,来读写磁盘数据。常见的逻辑块4KB,由连续的8个扇区组成。
1451 |
1452 | **示意图**
1453 |
1454 | 
1455 |
1456 |
1457 |
1458 | 磁盘在执行文件系统格式化时,分为三个区域:超级块、索引节点和数据块区
1459 |
1460 | * 超级块:整个文件系统的状态
1461 | * 索引节点区:存储索引节点
1462 | * 数据块区:存储文件数据
1463 |
1464 |
1465 |
1466 | #### 20.2 虚拟文件系统
1467 |
1468 | **示意图**
1469 |
1470 | 
1471 |
1472 | 文件系统分类:
1473 |
1474 | * 基于磁盘的文件系统:常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统
1475 | * 基于内存的文件系统:常说的虚拟文件系统,不需要磁盘空间,但是占用内存。比如,/proc和/sys
1476 | * 网络文件系统:用来访问其他计算机的文件系统,比如NFS、SMB、iSCSI 等
1477 |
1478 | **注意**:这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为**挂载点**),然后才能访问其中的文件。
1479 |
1480 |
1481 |
1482 | #### 20.3 文件系统IO
1483 |
1484 | 根据是否利用标准库缓存,分为**缓冲IO和非缓冲IO**
1485 |
1486 | * 缓存IO:利用标准库缓存,加速文件访问,标准库内部利用系统调用访问文件
1487 | * 非缓存IO:直接通过系统调用访问文件,不再经过标准库缓存
1488 |
1489 | **注意**:这里的“缓冲”,是指**标准库内部实现的缓存**,最终还是需要通过系统调用,而系统调用还会通过**页缓存**,来减少磁盘的IO操作
1490 |
1491 | 根据是否利用操作系统的**页缓存**,分为**直接IO和非直接IO**
1492 |
1493 | * 直接IO:跳过操作系统的页缓存,直接和**文件系统**交互来访问文件
1494 | * 非直接IO:先通过页缓存,再通过内核或者额外的系统调用,真正和磁盘交互(`O_DIRECT`标志)
1495 |
1496 | 根据应用程序是否阻塞自身,分为**阻塞IO和非阻塞IO**
1497 |
1498 | * 阻塞 I/O:是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程
1499 | * 非阻塞 I/O:是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过**轮询或者事件通知**的形式,获取调用的结果
1500 |
1501 | 根据是否等待相应结果,分为**同步IO和异步IO**
1502 |
1503 | * 同步IO:应用程序执行IO操作之后,要等到整个IO完成后,才能获得IO响应
1504 | * 异步IO:应用程序不用等待IO完成,会继续执行,等到IO执行完成,会以事件的方式通知应用程序
1505 |
1506 | 设置`O_SYNC`或者`O_DSYNC`,代表同步IO。如果是`O_DSYNC`,要等到文件数据写入磁盘之后,才能返回,如果是`O_SYNC`,是在`O_DSYNC`的基础上,要求文件**元数据**写入磁盘,才返回
1507 |
1508 | 设置`O_ASYNC`,代表异步IO,系统会再通过`SIGIO`或者`SIGPOLL`通知进程
1509 |
1510 | #### 20.4 性能观测
1511 |
1512 | ##### 20.4.1 容量
1513 |
1514 | `df`命令查看磁盘空间
1515 |
1516 | ```bash
1517 | $ df -h /dev/sda1
1518 | Filesystem Size Used Avail Use% Mounted on
1519 | /dev/sda1 29G 3.1G 26G 11% /
1520 |
1521 | # 查看索引节点所占的空间
1522 | $ df -i /dev/sda1
1523 | Filesystem Inodes IUsed IFree IUse% Mounted on
1524 | /dev/sda1 3870720 157460 3713260 5% /
1525 | ```
1526 |
1527 | 当索引节点空间不足,但是磁盘空间充足时,很可能是过多小文件导致的。**解决方法**一般是删除这些小文件,或者移动到索引节点充足的其他磁盘区
1528 |
1529 | ##### 20.4.2 缓存
1530 |
1531 | 可以使用free或者vmstat,观察页缓存的大小
1532 |
1533 | 也可以查看/proc/meminfo
1534 |
1535 | ```bash
1536 | $ cat /proc/meminfo | grep -E "SReclaimable|Cached"
1537 | Cached: 748316 kB
1538 | SwapCached: 0 kB
1539 | SReclaimable: 179508 kB
1540 | ```
1541 |
1542 | 内核使用slab机制,管理目录项和索引节点的缓存,/proc/meminfo给出了整体的slab大小,/proc/slabinfo可以查看每一种slab的缓存
1543 |
1544 | ```bash
1545 |
1546 | $ cat /proc/slabinfo | grep -E '^#|dentry|inode'
1547 | # name : tunables : slabdata
1548 | xfs_inode 0 0 960 17 4 : tunables 0 0 0 : slabdata 0 0 0
1549 | ...
1550 | ext4_inode_cache 32104 34590 1088 15 4 : tunables 0 0 0 : slabdata 2306 2306 0hugetlbfs_inode_cache 13 13 624 13 2 : tunables 0 0 0 : slabdata 1 1 0
1551 | sock_inode_cache 1190 1242 704 23 4 : tunables 0 0 0 : slabdata 54 54 0
1552 | shmem_inode_cache 1622 2139 712 23 4 : tunables 0 0 0 : slabdata 93 93 0
1553 | proc_inode_cache 3560 4080 680 12 2 : tunables 0 0 0 : slabdata 340 340 0
1554 | inode_cache 25172 25818 608 13 2 : tunables 0 0 0 : slabdata 1986 1986 0
1555 | dentry 76050 121296 192 21 1 : tunables 0 0 0 : slabdata 5776 5776 0
1556 | ```
1557 |
1558 | 其中,dentry代表目录项缓存,inode_cache代表VFS索引节点缓存,其他的就是各种文件系统的索引节点缓存
1559 |
1560 |
1561 |
1562 | 实际性能分析中,更常使用slabtop命令,来找出占用内存最多的缓存类型
1563 |
1564 | 示例如下:可以看到,目录项和索引节点占用了最多的 Slab 缓存,总共大约23M
1565 |
1566 | ```bash
1567 |
1568 | # 按下c按照缓存大小排序,按下a按照活跃对象数排序
1569 | $ slabtop
1570 | Active / Total Objects (% used) : 277970 / 358914 (77.4%)
1571 | Active / Total Slabs (% used) : 12414 / 12414 (100.0%)
1572 | Active / Total Caches (% used) : 83 / 135 (61.5%)
1573 | Active / Total Size (% used) : 57816.88K / 73307.70K (78.9%)
1574 | Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K
1575 |
1576 | OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
1577 | 69804 23094 0% 0.19K 3324 21 13296K dentry
1578 | 16380 15854 0% 0.59K 1260 13 10080K inode_cache
1579 | 58260 55397 0% 0.13K 1942 30 7768K kernfs_node_cache
1580 | 485 413 0% 5.69K 97 5 3104K task_struct
1581 | 1472 1397 0% 2.00K 92 16 2944K kmalloc-2048
1582 | ```
1583 |
1584 |
1585 |
1586 | ### 21 Linux磁盘I/O工作原理
1587 |
1588 | #### 21.1 磁盘
1589 |
1590 | 按照存储介质,磁盘分为:
1591 |
1592 | * 机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。机械磁盘主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,才能访问数据。如果 I/O 请求刚好连续,就不需要磁道寻址,可以获得最佳性能。这就是连续 I/O 的工作原理。与之相对应的是随机 I/O,它需要不停地移动磁头,来定位数据位置,读写速度就会比较慢
1593 | * 固态磁盘(Solid State Disk),通常缩写为 SSD,由固态电子元器件组成。固态磁盘不需要磁道寻址,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。
1594 |
1595 | 无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多,原因是:
1596 |
1597 | * 随机 I/O 需要更多的磁头寻道和盘片旋转,它的性能自然要比连续 I/O 慢
1598 | * 对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 I/O 的性能比起连续 I/O 来,也还是差了很多
1599 | * 连续 I/O 还可以通过预读的方式,来减少 I/O 请求的次数,这也是其性能优异的一个原因
1600 |
1601 | 最小读写单位:
1602 |
1603 | * 机械硬盘的最小读写单位是扇区,一般512字节
1604 | * 固态硬盘的最小读写单位是页,一般是4KB或者8KB
1605 |
1606 | 按照接口,磁盘可分为 IDE(Integrated Drive Electronics)、SCSI(Small Computer System Interface) 、SAS(Serial Attached SCSI) 、SATA(Serial ATA) 、FC(Fibre Channel) 等
1607 |
1608 | 磁盘介入服务器时,按照不同的使用方式,会划分为不同的架构:
1609 |
1610 | * 最简单的直接作为独立磁盘设备来使用
1611 | * 将多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列(RAID),提高数据访问的性能,并增强数据存储的可靠性
1612 | * 最后一种,是将磁盘组合成网络存储集群,再通过NFS、SMB、iSCSI等网络存储协议,暴露给服务器使用
1613 |
1614 | 在Linux中,磁盘是作为一个块设备来管理,以块为单位来读写,支持随机读写。每个块设备赋予两个设备号,分别是主、次设备号,主设备号用在驱动程序中,用来区分设备类型;次设备号用来在多个同类设备编号
1615 |
1616 | #### 21.2 通用块层
1617 |
1618 | 和VFS类似,为了减少不同块设备的差异带来的影响,Linux通过统一的通用块(块I/O层),管理不同的块设备
1619 |
1620 | 块设备层是处在文件系统和磁盘驱动中间的一个块设备抽象层,主要功能是:
1621 |
1622 | * 向上为文件系统和应用程序提供访问块设备的标准接口;向下,把各种异构的磁盘块设备抽象为统一的块设备,提供统一框架管理这些设备的驱动程序
1623 | * 通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率
1624 |
1625 | 对 I/O 请求排序的过程就是I/O调度,Linux支持四种I/O调度算法,分别是NONE、NOOP、CFQ以及DeadLine
1626 |
1627 | * NONE,不使用任何调度,对I/O不作任何处理(常用在虚拟机,此时磁盘I/O完全由物理机负责)
1628 | * NOOP,先入先出调度算法(常用在SSD)
1629 | * CFQ(Completely Fair Schedule)完全公平调度器,很多Linux发行版的默认调度器,它为每个进程维护了一个I/O调度队列,按照时间片来均匀分配每个进程的I/O请求;还支持优先级调度,适用于大量进程的系统(如桌面、多媒体应用等)
1630 | * DeadLine调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理,多用在 I/O 压力比较重的场景,比如数据库等
1631 |
1632 | #### 21.3 I/O栈
1633 |
1634 | 
1635 |
1636 | 根据这张 I/O 栈的全景图,可以看到存储系统 I/O 的工作原理
1637 |
1638 | * 文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据
1639 | * 通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层
1640 | * 设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作
1641 |
1642 | 存储系统的 I/O ,通常是整个系统中最慢的一环。
1643 |
1644 | Linux 通过多种缓存机制来优化 I/O 效率。为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。同样,为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据
1645 |
1646 | #### 21.4 磁盘性能指标
1647 |
1648 | 使用率、饱和度、IOPS、吞吐量以及响应时间五个指标,是磁盘性能的基本指标
1649 |
1650 | * 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈
1651 | * 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求
1652 | * IOPS(Input/Output Per Second),是指每秒的 I/O 请求数
1653 | * 吞吐量,是指每秒的 I/O 请求大小
1654 | * 响应时间,是指 I/O 请求从发出到收到响应的间隔时间
1655 |
1656 | **注意**:
1657 |
1658 | 1. 使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受新的 I/O 请求
1659 | 2. 随机读写多(如数据库、大量小文件)的情况下主要关注IOPS,而顺序读写多(如流媒体)的情况下,主要关注吞吐量
1660 |
1661 | 在为应用程序的服务器选型时,要先对磁盘的 I/O 性能进行基准测试,以便可以准确评估,磁盘性能是否可以满足应用程序的需求
1662 |
1663 | #### 21.5 磁盘I/O观测
1664 |
1665 | 使用iostat观测每块磁盘的使用情况,提供了每个磁盘的使用率、IOPS、吞吐量等各种常见的性能指标,这些指标实际上来自 /proc/diskstats
1666 |
1667 | ```bash
1668 | [root@VM_194_74_centos ~]# iostat -dx 1
1669 | Linux 3.10.107-1-tlinux2_kvm_guest-0051 (VM_194_74_centos) 07/10/20 _x86_64_ (8 CPU)
1670 |
1671 | Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
1672 | vda 0.00 8.69 0.20 8.62 1.98 114.42 26.38 0.01 1.14 2.58 1.11 0.68 0.60
1673 | vdb 31.81 0.23 1.16 0.12 132.40 1.68 210.15 0.00 1.48 1.46 1.62 0.71 0.09
1674 | scd0 0.00 0.00 0.00 0.00 0.00 0.00 14.20 0.00 0.29 0.29 0.00 0.29 0.00
1675 |
1676 | Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
1677 | vda 0.00 9.00 0.00 3.00 0.00 48.00 32.00 0.01 2.67 0.00 2.67 1.33 0.40
1678 | vdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
1679 | scd0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
1680 | ```
1681 |
1682 | 各个指标解读如下
1683 |
1684 | 
1685 |
1686 | **注意**:
1687 |
1688 | * %util ,就是我们前面提到的磁盘 I/O 使用率;
1689 | * r/s+ w/s ,就是 IOPS;
1690 | * rkB/s+wkB/s ,就是吞吐量;
1691 | * r_await+w_await ,就是响应时间
1692 |
1693 | 在观测指标时,可以结合请求的大小( rareq-sz 和 wareq-sz)一起分析
1694 |
1695 | #### 21.6 进程I/O观测
1696 |
1697 | pidstat可以实时查看每个进程的I/O情况
1698 |
1699 | ```bash
1700 | [root@VM_194_74_centos ~]# pidstat -d 1
1701 | Linux 3.10.107-1-tlinux2_kvm_guest-0051 (VM_194_74_centos) 07/10/20 _x86_64_ (8 CPU)
1702 |
1703 | 17:53:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
1704 | 17:53:52 0 17517 0.00 3.96 0.00 sap1004
1705 |
1706 | 17:53:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
1707 | 17:53:53 0 17508 0.00 4.00 0.00 sap1002
1708 | 17:53:53 0 17517 0.00 4.00 0.00 sap1004
1709 | ...
1710 | ```
1711 |
1712 | 指标如下:
1713 |
1714 | * 用户 ID(UID)和进程 ID(PID)
1715 | * 每秒读取的数据大小(kB_rd/s),单位是 KB
1716 | * 每秒发出的写请求数据大小(kB_wr/s),单位是 KB
1717 | * 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB
1718 |
1719 | ### 22 狂打日志问题定位
1720 |
1721 |
1722 |
1723 | ### 23 为什么磁盘I/O延迟很高?
1724 |
1725 | ### 24 SQL查询慢的问题定位?
1726 |
1727 | ### 25 Redis响应严重延迟的原因
1728 |
1729 | ### 26 套路篇:如何迅速分析系统I/O的瓶颈?
1730 |
1731 | ### 27 套路篇:磁盘I/O性能优化的几个思路
1732 |
1733 |
1734 |
1735 | ## 网络性能篇
1736 |
1737 | ### 28 Linux网络原理
1738 |
1739 | #### 28.1 Linux网络栈
1740 |
1741 | Linux网络栈示意图如下:
1742 |
1743 | * 最上层的应用程序,需要通过系统调用,来跟套接字接口进行交互;
1744 | * 套接字的下面,就是传输层、网络层和网络接口层;
1745 | * 最底层,则是网卡驱动程序以及物理网卡设备。
1746 |
1747 |
1748 |
1749 | > **网卡**是发送和接收网络包的基本设备。
1750 | >
1751 | > 在系统启动过程中,网卡通过内核中的**网卡驱动程序注册**到系统中。
1752 | >
1753 | > 在网络收发过程中,内核通过中断跟网卡进行交互。由于网络包的处理非常复杂,网卡硬中断只处理最核心的网卡数据读取或发送,而协议栈中的大部分逻辑,都会放到**软中断**中处理。
1754 |
1755 | #### 28.2 Linux网络收发流程
1756 |
1757 | ##### 28.2.1 网络报的接收流程
1758 |
1759 | * 当一个网络帧到达网卡后,网卡会通过 **DMA 方式**,把这个网络包放到**收包队列**中;然后通过**硬中断**,告诉中断处理程序已经收到了网络包,网卡中断处理程序会为网络帧分配**内核数据结构(sk_buff)**,并将其拷贝到 sk_buff 缓冲区中
1760 | * 再通过软中断,通知内核收到了新的网络帧
1761 | * 内核协议栈从缓冲区中取出网络帧,并通过网络协议栈,从下到上逐层处理这个网络帧,如下图左半部分
1762 |
1763 |
1764 |
1765 | ##### 28.2.2 网络包的发送流程
1766 |
1767 | 网络报发送流程如上图右半部分
1768 |
1769 | * 应用程序调用 Socket API(比如 sendmsg)发送网络包。由于这是一个系统调用,会陷入到**内核态的套接字层**中。套接字层会把数据包放到 Socket 发送缓冲区中
1770 | * 网络协议栈从 Socket 发送缓冲区中,取出数据包;再按照 TCP/IP 栈,从上到下逐层处理
1771 | * 网络包再送到网络接口层,进行物理地址寻址,以找到下一跳的 MAC 地址。然后添加帧头和帧尾,放到发包队列中
1772 | * 接下来会有**软中断通知驱动程序**:发包队列中有新的网络帧需要发送
1773 | * 驱动程序通过 DMA ,从发包队列中读出网络帧,并通过物理网卡把它发送出去
1774 |
1775 | ### 29 Linux网络性能指标
1776 |
1777 | #### 29.1 性能指标
1778 |
1779 | * **带宽**,表示链路的**最大传输速率**,单位通常为 b/s (比特 / 秒)。
1780 | * **吞吐量**,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。**吞吐量受带宽限制**,而吞吐量 / 带宽,也就是该网络的使用率。
1781 | * **延时**,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。
1782 | * **PPS**,是 Packet Per Second(包 / 秒)的缩写,表示以**网络包为单位的传输速率**。PPS 通常用来评估**网络的转发能力**,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。
1783 |
1784 | 另外,**网络的可用性**(网络能否正常通信)、**并发连接数**(TCP 连接数量)、**丢包率**(丢包百分比)、**重传率**(重新传输的网络包比例)等也是常用的性能指标。
1785 |
1786 | #### 29.2 网络配置
1787 |
1788 | 使用命令`ifconfig`或者`ip`查看
1789 |
1790 | ```bash
1791 | [root@VM_194_74_centos ~]# ifconfig eth1
1792 | eth1: flags=4163 mtu 1500
1793 | inet 9.134.194.74 netmask 255.255.248.0 broadcast 9.134.199.255
1794 | ether 52:54:00:82:12:e8 txqueuelen 1000 (Ethernet)
1795 | RX packets 70297502 bytes 34143392231 (31.7 GiB)
1796 | RX errors 0 dropped 0 overruns 0 frame 0
1797 | TX packets 78816203 bytes 45528648722 (42.4 GiB)
1798 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
1799 |
1800 | [root@VM_194_74_centos ~]# ip -s addr show dev eth1
1801 | 2: eth1: mtu 1500 qdisc mq state UP qlen 1000
1802 | link/ether 52:54:00:82:12:e8 brd ff:ff:ff:ff:ff:ff
1803 | inet 9.134.194.74/21 brd 9.134.199.255 scope global eth1
1804 | valid_lft forever preferred_lft forever
1805 | RX: bytes packets errors dropped overrun mcast
1806 | 34143407013 70297654 0 0 0 0
1807 | TX: bytes packets errors dropped carrier collsns
1808 | 45528723929 78816328 0 0 0 0
1809 | ```
1810 |
1811 | 第一,网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
1812 |
1813 | 第二,MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。
1814 |
1815 | 第三,网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
1816 |
1817 | 第四,网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。其中:
1818 |
1819 | * errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
1820 | * dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
1821 | * overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
1822 | * carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
1823 | * collisions 表示碰撞数据包数。
1824 |
1825 | #### 29.3 套接字信息
1826 |
1827 | 使用`netstat`或`ss`来查看**套接字、网络栈、网络接口以及路由表的信息**
1828 |
1829 | ```bash
1830 | # head -n 3 表示只显示前面3行
1831 | # -l 表示只显示监听套接字
1832 | # -n 表示显示数字地址和端口(而不是名字)
1833 | # -p 表示显示进程信息
1834 | [root@VM_194_74_centos ~]# netstat -nlp | head -n 3
1835 | Active Internet connections (only servers)
1836 | Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
1837 | tcp 0 0 0.0.0.0:36000 0.0.0.0:* LISTEN 7788/sshd
1838 |
1839 | # -l 表示只显示监听套接字
1840 | # -t 表示只显示 TCP 套接字
1841 | # -n 表示显示数字地址和端口(而不是名字)
1842 | # -p 表示显示进程信息
1843 | [root@VM_194_74_centos ~]# ss -ltnp | head -n 3
1844 | State Recv-Q Send-Q Local Address:Port Peer Address:Port
1845 | LISTEN 0 128 *:36000 *:* users:(("sshd",pid=7788,fd=3))
1846 | LISTEN 0 128 127.0.0.1:5432 *:* users:(("postgres",pid=2534,fd=3))
1847 | ```
1848 |
1849 | 其中,**接收队列(Recv-Q)和发送队列(Send-Q)**需要关注,它们通常应该是 0。当你发现它们不是 0 时,说明有网络包的堆积发生
1850 |
1851 | 当套接字处于连接状态(Established)时,
1852 |
1853 | * Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。而
1854 |
1855 | * Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。
1856 |
1857 | 当套接字处于监听状态(Listening)时,
1858 |
1859 | * Recv-Q 表示当前全连接队列(accept 队列)的长度(backlog含义参考链接 [详解socket中的backlog 参数](https://zhuanlan.zhihu.com/p/104874605))
1860 |
1861 | * Send-Q 表示全连接队列的最大长度
1862 |
1863 |
1864 |
1865 | #### 29.4 协议栈统计信息
1866 |
1867 | 使用`netstat`或`ss`命令
1868 |
1869 | ```bash
1870 | $ netstat -s
1871 | ...
1872 | Tcp:
1873 | 3244906 active connection openings
1874 | 23143 passive connection openings
1875 | 115732 failed connection attempts
1876 | 2964 connection resets received
1877 | 1 connections established
1878 | 13025010 segments received
1879 | 17606946 segments sent out
1880 | 44438 segments retransmitted
1881 | 42 bad segments received
1882 | 5315 resets sent
1883 | InCsumErrors: 42
1884 | ...
1885 |
1886 | $ ss -s
1887 | Total: 186 (kernel 1446)
1888 | TCP: 4 (estab 1, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 0
1889 |
1890 | Transport Total IP IPv6
1891 | * 1446 - -
1892 | RAW 2 1 1
1893 | UDP 2 2 0
1894 | TCP 4 3 1
1895 | ...
1896 | ```
1897 |
1898 | #### 29.5 网络吞吐和PPS
1899 |
1900 | 使用`sar`命令,加上`-n`参数,可以查看网络的统计信息,比如网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等
1901 |
1902 | ```bash
1903 | # 数字1表示每隔1秒输出一组数据
1904 | $ sar -n DEV 1
1905 | Linux 4.15.0-1035-azure (ubuntu) 01/06/19 _x86_64_ (2 CPU)
1906 |
1907 | 13:21:40 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
1908 | 13:21:41 eth0 18.00 20.00 5.79 4.25 0.00 0.00 0.00 0.00
1909 | 13:21:41 docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
1910 | 13:21:41 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
1911 | ```
1912 |
1913 | * rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
1914 | * rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
1915 | * rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
1916 | * %ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。
1917 |
1918 | Bandwidth 可以用 ethtool 来查询,它的单位通常是 Gb/s 或者 Mb/s(千兆网卡或者万兆网卡的单位都是bit)
1919 |
1920 | ```bash
1921 | $ ethtool eth0 | grep Speed
1922 | Speed: 1000Mb/s
1923 | ```
1924 |
1925 | #### 29.6 连通性和延时
1926 |
1927 | 使用命令`ping`,来测试远程主机的连通性和延时(基于 **ICMP 协议**)
1928 |
1929 | 如下,测试本机到 114.114.114.114 这个 IP 地址的连通性和延时
1930 |
1931 | ```bash
1932 | # -c3表示发送三次ICMP包后停止
1933 | $ ping -c3 114.114.114.114
1934 | PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.
1935 | 64 bytes from 114.114.114.114: icmp_seq=1 ttl=54 time=244 ms
1936 | 64 bytes from 114.114.114.114: icmp_seq=2 ttl=47 time=244 ms
1937 | 64 bytes from 114.114.114.114: icmp_seq=3 ttl=67 time=244 ms
1938 |
1939 | --- 114.114.114.114 ping statistics ---
1940 | 3 packets transmitted, 3 received, 0% packet loss, time 2001ms
1941 | rtt min/avg/max/mdev = 244.023/244.070/244.105/0.034 ms
1942 | ```
1943 |
1944 | ping 的输出,可以分为两部分:
1945 |
1946 | * 每个 ICMP 请求的信息,包括 ICMP 序列号(icmp_seq)、TTL(生存时间,或者跳数)以及往返延时。
1947 | * 三次 ICMP 请求的汇总
1948 |
1949 | ### 30 C10K和C100K问题
1950 |
1951 | #### 30.1 C10K
1952 |
1953 | C10K代表同时处理10000个请求
1954 |
1955 | 从资源上来说,对 2GB 内存和千兆网卡的服务器来说,同时处理 10000 个请求,只要每个请求处理占用不到 200KB(2GB/10000)的内存和 100Kbit (1000Mbit/10000)的网络带宽就可以
1956 |
1957 | 从软件上来看,主要是网络I/O模型的问题,在C10K之前,Linux主要是**同步阻塞**的方式,每个请求都分配一个进程或者线程,而10000个进程或者线程的调度、上下文切换和内存,都可能成为瓶颈
1958 |
1959 | 需要解决的问题:
1960 |
1961 | * 怎样在一个线程内处理多个请求,也就是要在一个线程内响应多个网络 I/O?
1962 |
1963 | ##### 30.1.1 I/O模型优化
1964 |
1965 | 异步、非阻塞I/O的思路:I/O多路复用
1966 |
1967 | > 两种 I/O 事件通知的方式:**水平触发和边缘触发**
1968 | >
1969 | > 水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作。
1970 | >
1971 | > 边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。这时候,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了
1972 |
1973 | * 第一种,使用非阻塞 I/O 和水平触发通知,比如使用 select 或者 poll
1974 | * 第二种,使用非阻塞 I/O 和边缘触发通知,比如 epoll(在select和poll基础上进行优化)
1975 | * 第三种,使用异步 I/O(Asynchronous I/O,简称为 AIO
1976 |
1977 | ##### 30.1.2 工作模型优化
1978 |
1979 | I/O多路复用有两种主要的工作模式:
1980 |
1981 | * **第一种**:主进程 + 多个 worker 子进程(比如nginx),主要流程是:
1982 | * 主进程执行 bind() + listen() 后,创建多个子进程;
1983 | * 在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字
1984 |
1985 |
1986 |
1987 | **注意**:accept() 和 epoll_wait() 调用,还存在一个**惊群**的问题(当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠)
1988 |
1989 | 为了避免**惊群问题**, Nginx 在每个 worker 进程中,都增加一个了**全局锁**(accept_mutex)。这些 worker 进程需要首先竞争到锁,只有竞争到锁的进程,才会加入到 epoll 中,这样就确保只有一个 worker 子进程被唤醒
1990 |
1991 | * **第二种**:监听到相同端口的多进程模型,所有的进程都监听相同的端口,有各自的套接字,开启 **SO_REUSEPORT** 选项(Linux 3.9+才支持),由内核负责将请求负载均衡到这些监听进程中去
1992 |
1993 |
1994 |
1995 | #### 30.2 C1000K
1996 |
1997 | C1000K代表同时有100w个请求
1998 |
1999 | * 从物理资源上来说,100 万个请求需要大量的系统资源
2000 | * 内存:假设每个请求需要 16KB 内存的话,那么总共就需要大约 15 GB 内存
2001 | * 带宽:假设只有 20% 活跃连接,即使每个连接只需要 1KB/s 的吞吐量,总共需要200000 * 8 / 1024 /1024 = 1.6 Gb/s 的吞吐量,千兆网卡已经不能满足,需要配置万兆网卡
2002 | * 从软件资源上来说,大量的连接也会占用大量的软件资源,比如**文件描述符的数量、连接状态的跟踪(CONNTRACK)、网络协议栈的缓存大小**(比如套接字读写缓存、TCP 读写缓存)等等,也会带来**大量的中断处理**。
2003 |
2004 | **优化**:在I/O多路复用的基础上,需要多队列网卡、硬中断负载均衡、CPU 绑定、RPS/RFS(软中断负载均衡到多个 CPU 核上),以及将网络包的处理卸载(Offload)到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各种硬件和软件的优化
2005 |
2006 | #### 30.3 C10M
2007 |
2008 | 同时有1000w个请求,解决方法是**跳过内核协议栈**,将网络包直接送到要处理的应用程序
2009 |
2010 | * 第一种机制:DPDK,用户网络的标准,跳过内核协议栈,直接由用户态进程通过轮询的方式来处理网络请求
2011 |
2012 |
2013 |
2014 | * 第二种机制:XDP(eXpress Data Path),Linux 内核提供的一种高性能网络数据路径,它允许网络包,在进入内核协议栈之前,就进行处理
2015 |
2016 |
2017 |
2018 | ### 31 怎么评估系统的网络性能
2019 |
2020 | #### 31.1 各协议层的性能测试
2021 |
2022 | ##### 31.1.1 转发性能
2023 |
2024 | `hping3`工具:测试网络包的处理能力
2025 |
2026 | ##### 31.1.2 TCP/UDP性能
2027 |
2028 | `iperf`命令测试
2029 |
2030 |
2031 |
2032 | ## 问题记录
2033 |
2034 | #### 19 swap使用升高
2035 |
2036 | 1. 文件页和匿名页的回收
2037 |
2038 | #### 21 系统内存套路篇
2039 |
2040 | 1. cgroups等方式限制进程内存使用情况 -> cgroup学习
2041 | 2. 主缺页异常和次缺页异常
2042 |
2043 | #### Linux文件系统怎么工作
2044 |
2045 | 1. 目录项、目录的区别(目录也是文件,可以用inode节点表示,而目录项在系统缓存中,如何构成目录结构?)
2046 |
2047 | > 内存中的目录项指向目录对应的inode节点,目录inode中的数据指针指向磁盘中的数据块,数据块中包含着目录下的文件列表,从而映射到子目录下
2048 |
2049 |
--------------------------------------------------------------------------------
/Linux系统编程.md:
--------------------------------------------------------------------------------
1 | [toc]
2 |
3 | # Linux/Unix系统编程
4 |
5 | ## 第12章 系统和进程信息
6 |
7 | ### 12.1 /proc文件系统
8 |
9 | 许多UNIX系统提供/proc虚拟文件系统,驻留在/proc目录中,包含了各种用于查看内核信息的文件,允许进程通过常规文件I/O调用来查看
10 |
11 | > /proc称为虚拟文件系统:其中包含的文件和子目录并未存储在磁盘,是由内核在进程访问此类信息时动态创建
12 |
13 | #### 12.1.1 进程相关的信息:/proc/PID
14 |
15 | | 文件 | 描述 (进程属性) |
16 | | :-----: | :------------------------------------: |
17 | | cmdline | 以\0分隔的命令行参数 |
18 | | cwd | 指向当前工作目录的符号链接 |
19 | | environ | 键值对形式的环境变量列表 |
20 | | fd | 目录,包含指向进程所打开文件的符号链接 |
21 | | root | 指向根目录的符号链接 |
22 | | status | 文件,包含进程的各种信息(进程ID,内存使用,信号) |
23 | | task | 目录,包含进程内每个线程的相关信息 |
24 | | maps | 进程的内存映射 |
25 | | mounts | 进程的挂载点 |
26 | | exe | 符号链接,指向正在执行的文件 |
27 | | mem | 进程的虚拟内存 |
28 |
29 |
30 |
31 | #### 12.1.2 /proc目录下的系统信息
32 |
33 | |目录|目录中的信息|
34 | |:-:|:-:|
35 | |/proc/net|有关网络和套接字的状态信息|
36 | |/proc/sys/fs|文件系统的相关设置|
37 | |/proc/sys/kernel|内核相关的设置|
38 | |/proc/sys/net|网络和套接字的设置|
39 | |/proc/sys/vm|内存管理设置|
40 | |/proc/sysvipc|有关system V IPC对象的信息|
41 |
42 |
43 |
44 |
45 | ## 第14章 系统编程概念
46 |
47 | ### 14.1 设备(专用)文件
48 |
49 | 设备专用文件与系统的某一设备相对应。在内核中,每种**设备类型**都有与之对应的**驱动程序**,用于处理设备的所有I/O请求。
50 |
51 | 设备分类:
52 |
53 | * 字符型设备,基于每个字符处理数据(如 终端和键盘)
54 | * 块设备,每次处理一块数据,块的大小取决于设备类型,磁盘和磁带都属于块设备
55 |
56 | 设备文件一般位于/dev目录下,可使用`mknod`命令创建设备文件
57 |
58 | #### 设备ID
59 |
60 | 每个设备文件主ID和辅ID各一个,主ID标识一般的设备等级(**同一类型**设备),辅ID在一般等级中唯一标识特定设备(`ls -l`命令可以查看主、辅ID)
61 |
62 | 设备文件的inode节点记录了设备文件的主、辅ID,每个**设备驱动程序**将自己与**特定主设备号**的关联关系向内核注册,从而建立设备文件与设备驱动程序之间的关系。
63 |
64 | ### 14.2 磁盘和分区
65 |
66 | > 普通文件和目录通常放在硬盘设备(磁盘)
67 |
68 | #### 磁盘驱动器
69 |
70 | 磁盘驱动器是一种机械设备,组成部分:
71 |
72 | 1. 多个同心圆组成的**磁道**,同一个磁道分为多个**扇区**,每个扇区包含一系列**物理块**(一般为512字节)
73 | 2. 磁头,在磁盘上快速移动,可**获取/修改**磁盘表面的**磁性编码信息**
74 |
75 | **物理块**代表磁盘驱动器读写的**最小单元**
76 |
77 |
78 |
79 | 磁盘读写的过程:
80 |
81 | * 磁盘移动到相应的磁道(**寻道时间**)
82 | * 在相应扇区旋转到磁头下面之前,驱动器会一直等待(**旋转延迟**)
83 | * 最后,从所请求的块上传输数据(**传输时间**)
84 |
85 | 执行上述**磁盘**操作的时间,可供CPU执行**数百万条**指令
86 |
87 | #### 磁盘分区
88 |
89 | > fdisk -l 可以列出磁盘的所有分区
90 | >
91 | > /proc/partitions 文件记录了每个磁盘分区的主辅设备号、大小和名称
92 |
93 | 每块磁盘分为一个或多个分区,一般包括:
94 |
95 | * 文件系统,用于存放常规文件
96 | * 数据区域,用作裸设备对其进行访问
97 | * 交换区域:供内存管理使用
98 |
99 | ### 14.3 文件系统
100 |
101 | > 文件系统是对常规文件和目录的组织集合,mkfs用于创建文件系统
102 | >
103 | > 最常见的文件系统是ext2
104 | >
105 | > /proc/filesystems中可以查看当前内核所知的所有文件系统类型
106 |
107 | #### 文件系统结构
108 |
109 | > 文件系统中分配的基本单元是**逻辑块**,对应文件系统所在磁盘设备上若干个**连续的物理块**
110 | >
111 | > 在ext2文件系统中,逻辑块的大小为1024、2048或者4096字节
112 |
113 | **文件系统包含在磁盘分区**中,主要组成为:
114 |
115 | * **引导块**,文件系统的**首块**,它不为文件系统所用,其中包含的信息用来**引导操作系统**(操作系统只需要一个引导块,但是所有文件系统都设有引导块)
116 | * **超级块**,**引导块之后**的一个独立块,包含文件系统有关的信息,包括:
117 | * inode节点表的容量
118 | * 文件系统中逻辑块的大小
119 | * 文件系统中**逻辑块的数量**
120 | * **inode**节点表,文件系统的每个文件或者目录都对应inode节点表中的一条记录,其中记录了文件的元数据信息
121 | * **数据块**,用于存放文件中实际的数据
122 |
123 | ### 14.4 inode节点
124 |
125 | > 驻留在文件系统的每个文件,在inode表中都会对应一个inode(`ls -li`可查看inode信息)
126 |
127 | **inode节点**包含的信息包括:
128 |
129 | * 文件类型(常规文件、目录、符号链接或者字符设备等)
130 | * 用户,用户组(UID,GID)
131 | * 三类用户的访问权限(user,group,others)
132 | * 三个时间戳
133 | * 文件最后访问时间(`ls -lu`显示)
134 | * 文件最后修改时间(`ls -l`显示)
135 | * 文件状态的最后改变时间(`ls -lc`显示)
136 | * 大部分文件系统不会记录文件的**创建时间**
137 | * 指向文件的硬链接数量
138 | * 文件大小(字节为单位)
139 | * 实际分配给文件的**块数量**(512字节为一个快),不一定完全等于文件字节大小(由于**文件黑洞**,实际分配给文件的块数可能会低于根据文件正常字节大小所计算出来的块数)
140 | * 执行文件数据块的指针
141 |
142 | #### ext2中的inode和数据块指针
143 |
144 | ext2在存储文件时,数据块不一定连续,也可能不按照顺序存放。
145 |
146 | 内核在inode节点中维护了一组指针,用于定位文件数据块。每个inode包含15个指针,其中前12个指针是直接指向文件的前12个数据块,接着还有一个**间接指针**,一个**双重间接指针**,一个**三重间接指针**。
147 |
148 | 间接指针不直接指向数据块,而是指向**指针块**,其中指针的数量取决于文件系统中逻辑块的大小,由于指针大小为4字节,因此指针的数量在256(块大小1024字节)~1024(块大小4096字节)之间。
149 |
150 | 
151 |
152 | inode指针设计:
153 |
154 | * 在维护inode节点大小固定的同时,支持任意大小的文件
155 | * 文件的数据块可以不连续,可以通过`lseek()`随机访问文件
156 | * 对于大多数小文件,可以通过直接指针快速访问数据块
157 |
158 | * 允许文件可以有**黑洞**(将inode节点和间接指针块的相应指针打上标记0,表名并未指向实际的磁盘块,这部分文件黑洞不会分配数据块空间)
159 |
160 | ### 14.5 虚拟文件系统(VFS)
161 |
162 | VFS设计原理:
163 |
164 | 1. 定义一套通用接口,所有与文件交互的程序按照这些统一接口操作
165 | 2. 每种实际的文件系统根据自己的实际情况,提供不同VFS接口的实现
166 |
167 | 底层文件系统会将错误代码传回VFS层,VFS会将错误代码传回应用程序
168 |
169 | ### 14.6 日志文件系统
170 |
171 | > 文件的一致性检查:系统崩溃时,文件的更新可能只完成了一部分,文件系统元数据(目录项、inode信息、数据块指针)处于不一致的状态,系统重启时,需要遍及整个文件系统进行检查,过程比较耗时
172 |
173 | 日志文件系统避免了漫长的一致性检查
174 |
175 | * 在实际更新元数据之前,会先将更新操作记录与专用的磁盘日志文件中,对元数据的更新以**事务**的方式进行
176 | * 一旦系统崩溃,系统重启后利用日志重做(redo)任何不完整的更新
177 | * 文件状态很快恢复,但是**增加了文件更新的时间**
178 |
179 | 常见的日志文件系统:
180 |
181 | 1. Reiserfs
182 | 2. ext3
183 | 3. JFS
184 | 4. XFS
185 | 5. ext4
186 | 6. Btrfs
187 |
188 | ### 14.7 单根目录层级和挂载点
189 |
190 | > Linux中所有文件系统的文件都位于单根目录树下,树根为根目录"/"。其他文件系统都挂载到根目录之下,视为整个目录层级的子树
191 |
192 | 挂载命令
193 |
194 | ```bash
195 | mount device directory
196 | ```
197 |
198 | ### 14.10 虚拟内存文件系统:tmpfs
199 |
200 | 普通的文件系统都驻留在磁盘上,同时Linux也支持驻留在内存中的虚拟文件系统
201 |
202 | 最复杂的为**tmpfs**,和其他内存文件系统不同在于它属于**虚拟内存文件系统**,它不仅使用RAM,在RAM耗尽的情况下,还会使用**交换空间**。
203 |
204 | 创建tmpfs的命令
205 |
206 | ```bash
207 | mount -t tmpfs source target
208 |
209 | # 示例
210 | mount -t tmpfs newtmp /tmp
211 | ```
212 |
213 | * source:可以为任意名称
214 | * target:该文件系统的挂载点
215 |
216 | 除了用于用户应用程序之外,tmpfs还有两个特殊用途:
217 |
218 | * 内核**内部挂载**的隐形tmpfs,用于**实现System V共享内存和共享匿名内存映射**
219 | * 挂载与**/tmp/shm**的tmpfs,为glibc用于**实现POSIX共享内存和POSIX信号量**
220 |
221 |
222 |
223 | ## 第18章 目录和链接
224 |
225 |
226 |
227 | ## 第34章 进程组、会话和作业控制
228 |
229 |
230 |
231 | ## 第37章 DAEMON守护进程
232 |
233 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linux-kernel-notes
2 |
3 |
--------------------------------------------------------------------------------
/image-20200531102202520.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531102202520.png
--------------------------------------------------------------------------------
/image-20200531102311595.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531102311595.png
--------------------------------------------------------------------------------
/image-20200531110908683.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531110908683.png
--------------------------------------------------------------------------------
/image-20200531123204686.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531123204686.png
--------------------------------------------------------------------------------
/image-20200531123538872.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531123538872.png
--------------------------------------------------------------------------------
/image-20200531162345047.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531162345047.png
--------------------------------------------------------------------------------
/image-20200531162554050.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531162554050.png
--------------------------------------------------------------------------------
/image-20200531163039995.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531163039995.png
--------------------------------------------------------------------------------
/image-20200531163630230.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531163630230.png
--------------------------------------------------------------------------------
/image-20200531164328810.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200531164328810.png
--------------------------------------------------------------------------------
/image-20200604082129145.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200604082129145.png
--------------------------------------------------------------------------------
/image-20200604082802402.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200604082802402.png
--------------------------------------------------------------------------------
/image-20200604084503749.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200604084503749.png
--------------------------------------------------------------------------------
/image-20200604090111563.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200604090111563.png
--------------------------------------------------------------------------------
/image-20200615090347978.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200615090347978.png
--------------------------------------------------------------------------------
/image-20200618083457485.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200618083457485.png
--------------------------------------------------------------------------------
/image-20200618084630339.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200618084630339.png
--------------------------------------------------------------------------------
/image-20200619090453579.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619090453579.png
--------------------------------------------------------------------------------
/image-20200619103702377.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619103702377.png
--------------------------------------------------------------------------------
/image-20200619104102379.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619104102379.png
--------------------------------------------------------------------------------
/image-20200619104457849.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619104457849.png
--------------------------------------------------------------------------------
/image-20200619111240263.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619111240263.png
--------------------------------------------------------------------------------
/image-20200619111413066.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619111413066.png
--------------------------------------------------------------------------------
/image-20200619112545758.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619112545758.png
--------------------------------------------------------------------------------
/image-20200619113142088.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619113142088.png
--------------------------------------------------------------------------------
/image-20200619114904496.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619114904496.png
--------------------------------------------------------------------------------
/image-20200619135038953.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619135038953.png
--------------------------------------------------------------------------------
/image-20200619135129369.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619135129369.png
--------------------------------------------------------------------------------
/image-20200619135204831.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619135204831.png
--------------------------------------------------------------------------------
/image-20200619140433905.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619140433905.png
--------------------------------------------------------------------------------
/image-20200619142443132.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619142443132.png
--------------------------------------------------------------------------------
/image-20200619144135833.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619144135833.png
--------------------------------------------------------------------------------
/image-20200619145306992.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619145306992.png
--------------------------------------------------------------------------------
/image-20200619145720447.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619145720447.png
--------------------------------------------------------------------------------
/image-20200619145815123.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200619145815123.png
--------------------------------------------------------------------------------
/image-20200622083824843.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200622083824843.png
--------------------------------------------------------------------------------
/image-20200622085138977.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200622085138977.png
--------------------------------------------------------------------------------
/image-20200622090154215.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200622090154215.png
--------------------------------------------------------------------------------
/image-20200622090919581.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200622090919581.png
--------------------------------------------------------------------------------
/image-20200622091237024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200622091237024.png
--------------------------------------------------------------------------------
/image-20200623100711987.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623100711987.png
--------------------------------------------------------------------------------
/image-20200623102542286.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623102542286.png
--------------------------------------------------------------------------------
/image-20200623191143202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623191143202.png
--------------------------------------------------------------------------------
/image-20200623191624065.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623191624065.png
--------------------------------------------------------------------------------
/image-20200623193750366.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623193750366.png
--------------------------------------------------------------------------------
/image-20200623194904111.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200623194904111.png
--------------------------------------------------------------------------------
/image-20200703193455046.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200703193455046.png
--------------------------------------------------------------------------------
/image-20200705165658500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200705165658500.png
--------------------------------------------------------------------------------
/image-20200705170712936.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200705170712936.png
--------------------------------------------------------------------------------
/image-20200705170755922.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200705170755922.png
--------------------------------------------------------------------------------
/image-20200705171053845.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200705171053845.png
--------------------------------------------------------------------------------
/image-20200707091040150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707091040150.png
--------------------------------------------------------------------------------
/image-20200707091125308.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707091125308.png
--------------------------------------------------------------------------------
/image-20200707091245724.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707091245724.png
--------------------------------------------------------------------------------
/image-20200707091319282.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707091319282.png
--------------------------------------------------------------------------------
/image-20200707091505999.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707091505999.png
--------------------------------------------------------------------------------
/image-20200707092746157.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707092746157.png
--------------------------------------------------------------------------------
/image-20200707093319597.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707093319597.png
--------------------------------------------------------------------------------
/image-20200707093832701.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707093832701.png
--------------------------------------------------------------------------------
/image-20200707094443166.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707094443166.png
--------------------------------------------------------------------------------
/image-20200707095202477.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707095202477.png
--------------------------------------------------------------------------------
/image-20200707095225496.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200707095225496.png
--------------------------------------------------------------------------------
/image-20200713152306980.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713152306980.png
--------------------------------------------------------------------------------
/image-20200713153253447.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153253447.png
--------------------------------------------------------------------------------
/image-20200713153304300.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153304300.png
--------------------------------------------------------------------------------
/image-20200713153340086.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153340086.png
--------------------------------------------------------------------------------
/image-20200713153412875.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153412875.png
--------------------------------------------------------------------------------
/image-20200713153434116.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153434116.png
--------------------------------------------------------------------------------
/image-20200713153535101.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713153535101.png
--------------------------------------------------------------------------------
/image-20200713154924924.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713154924924.png
--------------------------------------------------------------------------------
/image-20200713160114414.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713160114414.png
--------------------------------------------------------------------------------
/image-20200713160800427.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713160800427.png
--------------------------------------------------------------------------------
/image-20200713164051426.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713164051426.png
--------------------------------------------------------------------------------
/image-20200713165518219.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713165518219.png
--------------------------------------------------------------------------------
/image-20200713174143283.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713174143283.png
--------------------------------------------------------------------------------
/image-20200713185213374.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713185213374.png
--------------------------------------------------------------------------------
/image-20200713192813123.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200713192813123.png
--------------------------------------------------------------------------------
/image-20200720194433868.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200720194433868.png
--------------------------------------------------------------------------------
/image-20200720195417684.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200720195417684.png
--------------------------------------------------------------------------------
/image-20200720195445895.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200720195445895.png
--------------------------------------------------------------------------------
/image-20200720200359909.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200720200359909.png
--------------------------------------------------------------------------------
/image-20200721082112889.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200721082112889.png
--------------------------------------------------------------------------------
/image-20200721083726563.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200721083726563.png
--------------------------------------------------------------------------------
/image-20200721084613884.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200721084613884.png
--------------------------------------------------------------------------------
/image-20200721085534908.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200721085534908.png
--------------------------------------------------------------------------------
/image-20200721090550123.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200721090550123.png
--------------------------------------------------------------------------------
/image-20200727084714720.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200727084714720.png
--------------------------------------------------------------------------------
/image-20200727085803749.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200727085803749.png
--------------------------------------------------------------------------------
/image-20200727090201863.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200727090201863.png
--------------------------------------------------------------------------------
/image-20200729082148633.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200729082148633.png
--------------------------------------------------------------------------------
/image-20200729083424801.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200729083424801.png
--------------------------------------------------------------------------------
/image-20200729083533669.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200729083533669.png
--------------------------------------------------------------------------------
/image-20200729083648597.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200729083648597.png
--------------------------------------------------------------------------------
/image-20200729084944972.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200729084944972.png
--------------------------------------------------------------------------------
/image-20200730083703350.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200730083703350.png
--------------------------------------------------------------------------------
/image-20200730085636637.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200730085636637.png
--------------------------------------------------------------------------------
/image-20200730085816908.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200730085816908.png
--------------------------------------------------------------------------------
/image-20200730090512169.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200730090512169.png
--------------------------------------------------------------------------------
/image-20200730091212221.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200730091212221.png
--------------------------------------------------------------------------------
/image-20200731081843643.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200731081843643.png
--------------------------------------------------------------------------------
/image-20200731082550667.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200731082550667.png
--------------------------------------------------------------------------------
/image-20200731090358996.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200731090358996.png
--------------------------------------------------------------------------------
/image-20200731091121429.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200731091121429.png
--------------------------------------------------------------------------------
/image-20200731091142602.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200731091142602.png
--------------------------------------------------------------------------------
/image-20200802092331057.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802092331057.png
--------------------------------------------------------------------------------
/image-20200802092411500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802092411500.png
--------------------------------------------------------------------------------
/image-20200802092849165.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802092849165.png
--------------------------------------------------------------------------------
/image-20200802093257426.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802093257426.png
--------------------------------------------------------------------------------
/image-20200802093500875.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802093500875.png
--------------------------------------------------------------------------------
/image-20200802095755049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802095755049.png
--------------------------------------------------------------------------------
/image-20200802100106115.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802100106115.png
--------------------------------------------------------------------------------
/image-20200802101629272.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802101629272.png
--------------------------------------------------------------------------------
/image-20200802103414354.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802103414354.png
--------------------------------------------------------------------------------
/image-20200802103603545.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802103603545.png
--------------------------------------------------------------------------------
/image-20200802104015706.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802104015706.png
--------------------------------------------------------------------------------
/image-20200802104445502.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802104445502.png
--------------------------------------------------------------------------------
/image-20200802111837958.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802111837958.png
--------------------------------------------------------------------------------
/image-20200802111902259.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802111902259.png
--------------------------------------------------------------------------------
/image-20200802112524577.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802112524577.png
--------------------------------------------------------------------------------
/image-20200802112647741.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802112647741.png
--------------------------------------------------------------------------------
/image-20200802112815989.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802112815989.png
--------------------------------------------------------------------------------
/image-20200802113302644.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802113302644.png
--------------------------------------------------------------------------------
/image-20200802113325385.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802113325385.png
--------------------------------------------------------------------------------
/image-20200802113445670.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802113445670.png
--------------------------------------------------------------------------------
/image-20200802113750230.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802113750230.png
--------------------------------------------------------------------------------
/image-20200802114225988.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802114225988.png
--------------------------------------------------------------------------------
/image-20200802114732166.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802114732166.png
--------------------------------------------------------------------------------
/image-20200802120202420.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200802120202420.png
--------------------------------------------------------------------------------
/image-20200803084218092.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803084218092.png
--------------------------------------------------------------------------------
/image-20200803084315826.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803084315826.png
--------------------------------------------------------------------------------
/image-20200803084515122.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803084515122.png
--------------------------------------------------------------------------------
/image-20200803085509088.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803085509088.png
--------------------------------------------------------------------------------
/image-20200803085745108.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803085745108.png
--------------------------------------------------------------------------------
/image-20200803085807606.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803085807606.png
--------------------------------------------------------------------------------
/image-20200803090535044.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803090535044.png
--------------------------------------------------------------------------------
/image-20200803101934382.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803101934382.png
--------------------------------------------------------------------------------
/image-20200803125209144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803125209144.png
--------------------------------------------------------------------------------
/image-20200803125215783.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Miller-Xie/Linux-kernel-notes/eeae774c8db77f400cdf9dfb5c5568aa3502a1e8/image-20200803125215783.png
--------------------------------------------------------------------------------