<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; kernel</title>
	<atom:link href="https://blog.gmem.cc/tag/kernel/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 03 Apr 2026 04:13:36 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Linux内核编程知识集锦</title>
		<link>https://blog.gmem.cc/linux-kernel-programming-faq</link>
		<comments>https://blog.gmem.cc/linux-kernel-programming-faq#comments</comments>
		<pubDate>Sat, 07 Jan 2012 07:26:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35145</guid>
		<description><![CDATA[<p>编译内核 下载源码 [crayon-69d313db68bc5694937274/]  根据需要，切换分支。 安装工具 [crayon-69d313db68bcd100375982/] 配置内核 [crayon-69d313db68bd0711723541/] 你也可以手工直接编辑.config文件。每个选项都可以设置以下值之一： 取值 说明 y 将相应特性构建到内核中 n 不包含此特性 m 构建为模块，这样可以按需加载 注意： 除非你使用initrd，否则绝不要把挂载根文件系统必需的驱动程序(硬件驱动以及文件系统驱动)编译成模块 如果系统中有网卡，将它们的驱动编译成模块。这样就能够在 /etc/modules.conf 中用别名定义哪一块网卡第1，哪一块第2，等等。如果您将驱动程序编译进了内核，它们加载的顺序将取决于当初它们链接进内核的顺序，而这不一定是您想要的 <a class="read-more" href="https://blog.gmem.cc/linux-kernel-programming-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-programming-faq">Linux内核编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">编译内核</span></div>
<div class="blog_h2"><span class="graybg">下载源码</span></div>
<pre class="crayon-plain-tag">git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git</pre>
<p> 根据需要，切换分支。</p>
<div class="blog_h2"><span class="graybg">安装工具</span></div>
<pre class="crayon-plain-tag">apt install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison</pre>
<div class="blog_h2"><span class="graybg">配置内核</span></div>
<pre class="crayon-plain-tag">cd linux-stable

# 使用当前系统内核配置
cp -v /boot/config-$(uname -r) .config

# 使用图形化界面进行配置
make menuconfig

# 使用字符界面配置
make config

# 在现有的内核设置文件基础上建立一个新的设置文件，只会向用户提供有关新内核特性的问题，
# 在新内核升级的过程 中，make oldconfig非常有用
make oldconfig

# 自动的尽量选择 y 选项
make allyesconfig

# 自动的尽量选择 m 选项
make allmodconfig</pre>
<p>你也可以手工直接编辑.config文件。每个选项都可以设置以下值之一：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>y</td>
<td>将相应特性构建到内核中</td>
</tr>
<tr>
<td>n</td>
<td>不包含此特性</td>
</tr>
<tr>
<td>m</td>
<td>
<p>构建为模块，这样可以按需加载</p>
<p>注意：</p>
<ol>
<li>除非你使用initrd，否则绝不要把挂载根文件系统必需的驱动程序(硬件驱动以及文件系统驱动)编译成模块</li>
<li>如果系统中有网卡，将它们的驱动编译成模块。这样就能够在 /etc/modules.conf 中用别名定义哪一块网卡第1，哪一块第2，等等。如果您将驱动程序编译进了内核，它们加载的顺序将取决于当初它们链接进内核的顺序，而这不一定是您想要的</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">编译内核</span></div>
<pre class="crayon-plain-tag"># 编译
make -j 8

# 编译bzImage
make bzImage</pre>
<div class="blog_h3"><span class="graybg">内核文件格式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">格式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>vmlinux</td>
<td>编译出来的最原始的内核文件，未压缩</td>
</tr>
<tr>
<td>zImage</td>
<td>zImage是ARM Linux常用的一种压缩映像文件，是vmlinux经过gzip压缩产生的</td>
</tr>
<tr>
<td>bzImage</td>
<td>bz表示big zImage，和zImage的区别是，zImage解压缩内核到低端内存（第一个640K），bzImage解压缩内核到高端内存（1M以上）。如果内核比较小，那么采用zImage或bzImage都行，如果比较大应该用bzImage</td>
</tr>
<tr>
<td>uImage</td>
<td>uImage是U-boot专用的映像文件，它是在zImage之前加上一个长度为0x40的头，说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说，如果直接从uImage的0x40位置开始执行，zImage和uImage没有任何区别</td>
</tr>
<tr>
<td>vmlinuz</td>
<td>是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接</td>
</tr>
<tr>
<td>initrd</td>
<td>initramfs</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装内核</span></div>
<pre class="crayon-plain-tag"># 安装内核模块到系统
sudo make modules_install
# 安装内核到系统
sudo make install  </pre>
<p>更新<a href="/linux-kernel-study-note-vol5#initramfs">initramfs</a>，指向最新的内核： </p>
<pre class="crayon-plain-tag">sudo update-initramfs -c -k 4.15.0</pre>
<p>更新GRUB BootLoader：</p>
<pre class="crayon-plain-tag">sudo update-grub</pre>
<p>重启后就可以使用新内核了。 </p>
<div class="blog_h1"><span class="graybg">调试内核</span></div>
<div class="blog_h2"><span class="graybg">编译内核</span></div>
<p>设置内核选项：</p>
<pre class="crayon-plain-tag">make O=build/ defconfig

make O=build/ menuconfig

# Processor type and features ----&gt;
#     [ ]   Randomize the address of the kernel image (KASLR) 

# Kernel hacking  ---&gt;
#     [*] Kernel debugging
#     Compile-time checks and compiler options  ---&gt;
#         [*] Compile the kernel with debug info
#         [*]   Provide GDB scripts for kernel debugging</pre>
<p>然后按正常流程构建。</p>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">阅读源码</span></div>
<div class="blog_h2"><span class="graybg">使用Clion</span></div>
<p>内核项目本身是基于make的，我们需要将其转换为CMake项目。</p>
<p>首先，安装scan-build</p>
<pre class="crayon-plain-tag">sudo -H pip install scan-build
# 或者
sudo apt install bear</pre>
<p>这是一个构建拦截工具（build interceptor） ，它能够拦截make的构建过程，并生成一个<pre class="crayon-plain-tag">compile_commands.json</pre>文件。这个文件叫<a href="https://clang.llvm.org/docs/JSONCompilationDatabase.html">JSON Compilation Database</a>，这种文件格式描述了如何独立于某种构建系统，来Replay编译过程。</p>
<p>拦截构建过程：</p>
<pre class="crayon-plain-tag">cd linux-stable
intercept-build make -j8
# 或者
bear make -j8

# 拦截其它目标
bear make tools/all
bear make bzImage</pre>
<p>从compile_commands.json生成CMakeList.txt文件：</p>
<pre class="crayon-plain-tag">cd ..
git clone https://github.com/habemus-papadum/kernel-grok.git
cd linux-stable
../kernel-grok/generate_cmake</pre>
<p>如果报头文件找不到的错误，在CMakeList.txt中增加：</p>
<pre class="crayon-plain-tag">set(CMAKE_C_COMPILER "gcc")
include_directories(".")
include_directories("./include")</pre>
<div class="blog_h2"><span class="graybg">使用VSCode</span></div>
<p>首先配置并编译好内核，然后在源码目录下执行：</p>
<pre class="crayon-plain-tag">git clone https://github.com/amezin/vscode-linux-kernel.git .vscode

# 生成compile_commands.json
python .vscode/generate_compdb.py
# 如果不是编译的x64内核，则修改c_cpp_properties.json的intelliSenseMode。可选值：
# gcc-x86    gcc-x64    gcc-arm    gcc-arm64</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-programming-faq">Linux内核编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-programming-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核学习笔记（五）</title>
		<link>https://blog.gmem.cc/linux-kernel-study-note-vol5</link>
		<comments>https://blog.gmem.cc/linux-kernel-study-note-vol5#comments</comments>
		<pubDate>Thu, 10 Mar 2011 09:14:58 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=9175</guid>
		<description><![CDATA[<p>调试 内核的调试比用户级程序困难的多，并且风险高——内核中的一个错误可能导致系统立即崩溃。驾驭内核调试的能力很大程度上取决于经验和对内核的深刻理解。 通过打印来调试 内核提供的打印函数[crayon-69d313db6979a350323040-i/] 与对应的C库函数功能几乎相同，但是它有一些特殊的特性： 健壮性 printk()函数是个弹性极佳的函数，可以在中断上下文、进程上下文、持有任何锁时、多处理器环境下使用。 尽管如此，还是有极少部分的地方不能使用该函数，比如系统启动最开始的时候，终端尚未初始化。这种情况下可以使用已经工作的设备，例如串口，与外界通信，或者使用printk()的变体[crayon-69d313db697a0322985765-i/] ，这个函数仅仅是能够更早的在终端上打印数据而已。 日志等级 printk()支持指定日志等级，以便在不同调试级别下打印不同的信息： [crayon-69d313db697a4403512562/] 内核会根据调用时指定的记录等级，和当前终端配置的console_loglevel来决定是否向终端上打印。所有可用的记录等级定义在[crayon-69d313db697a7669502665-i/]  ，如下表：  记录等级 说明  KERN_EMERG 值为0，紧急情况，系统可能崩溃 KERN_ALERT 值为1，必须立即注意的问题 KERN_CRIT 值为2，关键状况 KERN_ERR 值为3，错误 <a class="read-more" href="https://blog.gmem.cc/linux-kernel-study-note-vol5">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol5">Linux内核学习笔记（五）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">调试</span></div>
<p>内核的调试比用户级程序困难的多，并且风险高——内核中的一个错误可能导致系统立即崩溃。驾驭内核调试的能力很大程度上取决于经验和对内核的深刻理解。</p>
<div class="blog_h2"><span class="graybg">通过打印来调试</span></div>
<p>内核提供的打印函数<pre class="crayon-plain-tag">printk()</pre> 与对应的C库函数功能几乎相同，但是它有一些特殊的特性：</p>
<div class="blog_h3"><span class="graybg">健壮性</span></div>
<p>printk()函数是个弹性极佳的函数，可以在中断上下文、进程上下文、持有任何锁时、多处理器环境下使用。</p>
<p>尽管如此，还是有极少部分的地方不能使用该函数，比如系统启动最开始的时候，终端尚未初始化。这种情况下可以使用已经工作的设备，例如串口，与外界通信，或者使用printk()的变体<pre class="crayon-plain-tag">early_printk()</pre> ，这个函数仅仅是能够更早的在终端上打印数据而已。</p>
<div class="blog_h3"><span class="graybg">日志等级</span></div>
<p>printk()支持指定日志等级，以便在不同调试级别下打印不同的信息：</p>
<pre class="crayon-plain-tag">printk(KERN_WARNING "This is a warning!\n");
printk(KERN_DEBUG "This is a debug notice!\n");
printk("I did not specify a loglevel!\n");</pre>
<p>内核会根据调用时指定的记录等级，和当前终端配置的console_loglevel来决定是否向终端上打印。所有可用的记录等级定义在<pre class="crayon-plain-tag">linux/kernel.h</pre>  ，如下表：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 记录等级</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>KERN_EMERG</td>
<td>值为0，紧急情况，系统可能崩溃</td>
</tr>
<tr>
<td>KERN_ALERT</td>
<td>值为1，必须立即注意的问题</td>
</tr>
<tr>
<td>KERN_CRIT</td>
<td>值为2，关键状况</td>
</tr>
<tr>
<td>KERN_ERR</td>
<td>值为3，错误</td>
</tr>
<tr>
<td>KERN_WARNING</td>
<td>值为4，警告</td>
</tr>
<tr>
<td>KERN_NOTICE</td>
<td>值为5，一般的，可能值得注意的情况</td>
</tr>
<tr>
<td>KERN_INFO</td>
<td>值为6，一般性信息</td>
</tr>
<tr>
<td>KERN_DEBUG</td>
<td>值为7，调试信息</td>
</tr>
</tbody>
</table>
<p>如果调用printk()时不指定记录等级，默认使用<pre class="crayon-plain-tag">DEFAULT_MESSAGE_LOGLEVEL</pre> ，当前该宏的值等于KERN_WARNING。</p>
<div class="blog_h3"><span class="graybg">记录缓冲区</span></div>
<p>内核消息全部被保存在一个大小为<pre class="crayon-plain-tag">LOG_BUF_LEN</pre> 的环形缓冲区内，该长度在编译时可以通过配置项<pre class="crayon-plain-tag">CONFIG_LOG_BUF_SHIFT</pre> 定制。对于单处理器机器，默认值是16KB，也就是说，内核同时能存储16KB的内核消息。如果缓冲区满了时发起一个新的printk()调用，那么最老的消息被冲掉。环形缓冲区有多个好处，例如可以同时读写，便于维护，甚至可以在中断上下文中使用；环形缓冲区的缺点是可能丢失消息。</p>
<div class="blog_h3"><span class="graybg">syslogd和klogd</span></div>
<p>在标准的Linux系统上，用户空间程序klogd负责读取环形缓冲，并通过syslogd守护程序写入到系统日志文件。klogd可以读取<pre class="crayon-plain-tag">/proc/kmsg</pre> 或者调用<pre class="crayon-plain-tag">syslog()</pre> 系统调用来获取环形缓冲中的内核消息，其中前者是默认值，klogd会一直阻塞直到一个新的内核消息可读。</p>
<div class="blog_h2"><span class="graybg">oops</span></div>
<p>oops是内核通知用户坏事情发生的常用手段，由于内核掌管整个系统，它不能自我修复或杀死自己——向对付用户空间程序那样，内核的做法是发起oops：在控制台打印错误信息 、dump出寄存器的内容、保留栈信息。通常发生oops后内核处于不一致状态（例如正在处理重要数据的时候、持有锁的时候以外oops），内核会尽力恢复对系统的控制，但是很多情况无法做到：</p>
<ol>
<li>如果oops发生在中断上下文，内核无法继续运行，导致<span style="background-color: #c0c0c0;">Panics（死机）</span></li>
<li>如果oops发生在idle进程（PID:0）或者init进程（PID:1），也会导致系统陷入混乱，因为内核缺少这两个重要进程无法工作</li>
<li>如果oops发生在其它进程中，内核会杀该进程并尝试继续执行</li>
</ol>
<p>oops发生的原因很多，例如内存访问越界、非法指令等。</p>
<p>下面是一个PPC平台上oops的示例（已经解码）：</p>
<pre class="crayon-plain-tag">Oops: Exception in kernel mode, sig: 4
#这里显示了Oops的原因，无法处理空指针的解引用
Unable to handle kernel NULL pointer dereference at virtual address 00000001
NIP: C013A7F0 LR: C013A7F0 SP: C0685E00 REGS: c0905d10 TRAP: 0700
Not tainted
MSR: 00089037 EE: 1 PR: 0 FP: 0 ME: 1 IR/DR: 11
TASK = c0712530[0] 'swapper' Last syscall: 120
#下面是32个寄存器，配合函数的汇编代码，寄存器中的值可以帮助解决问题
GPR00: C013A7C0 C0295E00 C0231530 0000002F 00000001 C0380CB8 C0291B80 C02D0000
GPR08: 000012A0 00000000 00000000 C0292AA0 4020A088 00000000 00000000 00000000
GPR16: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
GPR24: 00000000 00000005 00000000 00001032 C3F7C000 00000032 FFFFFFFF C3F7C1C0
#下面的调用树显示了导致问题的完整的函数调用链，下面的行调用上面的行
Call trace:
[c013ab30] tulip_timer+0x128/0x1c4   #根据后面的偏移量可以找到精确的代码行
[c0020744] run_timer_softirq+0x10c/0x164
[c001b864] do_softirq+0x88/0x104
[c0007e80] timer_interrupt+0x284/0x298
[c00033c4] ret_from_except+0x0/0x34
[c0007b84] default_idle+0x20/0x60
[c0007bf8] cpu_idle+0x34/0x38
[c0003ae8] rest_init+0x24/0x34</pre>
<p>在2.5以前，oops打印的是未解码的信息，调用跟踪仅仅显示函数地址，可以通过下面的命令解码：</p>
<pre class="crayon-plain-tag">#该命令配置编译时生成的system.map一起使用，如果使用模块，则还需要模块的一些信息
ksymoops saved_oops.txt</pre>
<p>在2.5以后，内核引入kallsyms特性，ksymoops命令不再需要了，该特性通过配置选项<pre class="crayon-plain-tag">CONFIG_KALLSYMS</pre> 激活。该特性在内核镜像中存储<span style="background-color: #c0c0c0;">内核函数地址和符号名称的映射关系</span>，因此内核可以打印解码后的调用跟踪。启用该选项的缺点是内核的尺寸变大。配置选项<pre class="crayon-plain-tag">CONFIG_KALLSYMS_ALL</pre> 存储所有符号名称，不仅仅是函数的，一般供特殊debugger使用。</p>
<div class="blog_h2"><span class="graybg">内核调试配置选项</span></div>
<p>有若干配置选项可以辅助内核调试，在内核配置编辑器的“Kernel Hacking”菜单中可以看到，这些配置项都依赖于<pre class="crayon-plain-tag">CONFIG_DEBUG_KERNEL</pre> ，不防打开全部这些选项。这些选项中常用的包括： </p>
<ol>
<li>enabling slab layer debugging 启用slab调试</li>
<li>high-memory debugging 高端内存调试</li>
<li>I/O mapping debugging I/O映射调试</li>
<li>spin-lock debugging 自旋锁调试</li>
<li>stack-overflow checking 栈溢出检查</li>
<li>sleep-inside-spinlock checking 在自旋锁内睡眠检查</li>
</ol>
<div class="blog_h2"><span class="graybg">引发bug并打印信息</span></div>
<p>一些内核调用可用来方便的标记bug，提供断言并输出信息，其中最常用的是：<pre class="crayon-plain-tag">BUG()</pre>  和<pre class="crayon-plain-tag">BUG_ON()</pre>  。当它们被调用的时候会引起oops，下面是这两个调用的示例：</p>
<pre class="crayon-plain-tag">if (bad_thing)
    BUG();
BUG_ON(bad_thing); //一般认为该调用更加清晰可读</pre>
<p>可以使用panic()引起严重错误，导致内核挂起：</p>
<pre class="crayon-plain-tag">if (terrible_thing)
    panic("terrible_thing is %ld!\n", terrible_thing);</pre>
<p>如果仅仅向查看调用栈以辅助调试，可以调用：</p>
<pre class="crayon-plain-tag">if (!debug_check) {
printk(KERN_DEBUG "provide some information...\n");
    dump_stack(); //打印寄存器和调用栈信息
}</pre>
<div class="blog_h2"><span class="graybg">魔法系统请求键</span></div>
<p>使用配置选项<pre class="crayon-plain-tag">CONFIG_MAGIC_SYSRQ</pre> 可以激活魔法系统请求键，无论内核处于什么状态，它都可以和请求键进行通信。在运行时，还需要使用下面的命令启用它：</p>
<pre class="crayon-plain-tag">echo 1 &gt; /proc/sys/kernel/sysrq</pre>
<p>系统请求键（SysRq）存在于大部分标准键盘，在x86上，可以按Alt + Print Screen启用。内核支持的请求键如下表： </p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">组合键</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>SysRq-b</td>
<td>重启机器</td>
</tr>
<tr>
<td>SysRq-e</td>
<td>发送信号SIGTERM给所有进程（init除外） </td>
</tr>
<tr>
<td>SysRq-h</td>
<td>显示帮助</td>
</tr>
<tr>
<td>SysRq-i</td>
<td>发送信号SIGKILL给所有进程（init除外） </td>
</tr>
<tr>
<td>SysRq-k</td>
<td>安全访问：杀死该控制台上所有程序</td>
</tr>
<tr>
<td>SysRq-l</td>
<td>发送信号SIGKILL给所有进程</td>
</tr>
<tr>
<td>SysRq-m</td>
<td>将内存信息Dump到控制台</td>
</tr>
<tr>
<td>SysRq-o</td>
<td>关机</td>
</tr>
<tr>
<td>SysRq-p</td>
<td>Dump寄存器信息到控制台</td>
</tr>
<tr>
<td>SysRq-r</td>
<td>关闭键盘原始模式</td>
</tr>
<tr>
<td>SysRq-s</td>
<td>把所有已安装的文件系统刷出到磁盘</td>
</tr>
<tr>
<td>SysRq-t</td>
<td>把任务信息打印到控制台</td>
</tr>
<tr>
<td>SysRq-u</td>
<td>卸载所有文件系统</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">内核调试器</span></div>
<p>很多第三方补丁为Linux内核提供了调试器的支持。 </p>
<div class="blog_h3"><span class="graybg">gdb</span></div>
<p>使用标准的GNU调试器gdb，可以对正在运行的内核进行查看：</p>
<pre class="crayon-plain-tag"># vmlinux是未压缩的内核镜像
# 可选参数/proc/kcore作为一个core文件，允许gdb查看正在运行内核的内存
gdb vmlinux /proc/kcore

#下面可以使用标准的gdb命令来读取信息，例如
p global_variable_name  #读取全局变量
disassemble function    #反汇编一个函数</pre>
<p>如果使用-g选项（添加在CFLAGS后）编译内核，gdb可以提供更多的信息，例如可以dump出结构体的内容、跟踪指针，但是这样编译会让内核变大很多。</p>
<p>gdb有很多局限性，它不能修改任何内核数据，特别是不支持单步跟踪。 </p>
<div class="blog_h3"><span class="graybg">kgdb</span></div>
<p>这是一个补丁，利用它可以在远端主机上通过串口，并利用gdb的全部功能对内核进行调试。要使用kgdb需要两台机器，一台是待遇kgdb补丁的被调试机器；另外一条是执行调试的远程机器，通过串口线直连到被调试器。通过kgdb，包括修改变量值、设置断点、单步执行在内的全部gdb功能均可用，某些gdb版本甚至支持执行函数。</p>
<div class="blog_h1"><span class="graybg">启动</span></div>
<div class="blog_h2"><span class="graybg">内核启动过程</span></div>
<p>一旦 Linux 内核控制了系统（<span style="background-color: rgb(192, 192, 192);">内核在由启动加载引导程序加载后获得控制权</span>），它就会<span style="background-color: rgb(192, 192, 192);">准备好内存结构和驱动程序</span>。然后它将控制交给应用程序<span style="background-color: rgb(192, 192, 192);">（通常是 init），其任务是进一步准备系统并确保在引导过程结束时，所有必要的服务正在运行且用户能够登录</span>。该<span style="background-color: rgb(192, 192, 192);"> init 应用程序通过启动 udev 守护程序来执行此操作</span>，该守护程序将<span style="background-color: rgb(192, 192, 192);">根据检测到的设备进一步加载和准备系统</span>。<span style="background-color: rgb(192, 192, 192);">启动 udev 时，将挂载尚未挂载的所有剩余文件系统，并启动其余服务</span>。</p>
<p>对于那些所有必需的文件和工具驻留在同一个文件系统中的系统， init 应用程序可以完全控制进一步的引导过程。但当有多个文件系统被定义（或拥有更多的外来设备）时，情况可能变得更棘手些︰</p>
<ol>
<li>当 /usr 分区位于单独的文件系统上时，除非 /usr 可用，否则无法使用存储在 /usr 中的文件的工具和驱动程序。如果需要这些工具来使 /usr 可用，那么我们就无法启动系统</li>
<li>如果根文件系统被加密，那么 Linux 内核将无法找到 init 程序，导致系统无法启动。</li>
</ol>
<p>这个问题的解决方案长期以来一直使用 initrd（初始根磁盘）。</p>
<div class="blog_h2"><span class="graybg">初始根磁盘</span></div>
<p><span style="background-color: rgb(192, 192, 192);">initrd 是一个内存中的磁盘结构</span>（ramdisk），其中包含必要的工具和脚本，用于在<span style="background-color: #c0c0c0;">将控制权交给根文件系统上的 init 应用程序<strong>之前</strong>挂载所需的文件系统</span>。 Linux 内核在此根磁盘上触发安装脚本（通常称为 linuxrc，但该名称不是必需的），<span style="background-color: rgb(192, 192, 192);">linuxrc的工作是准备系统、切换到真正的根文件系统，然后调用 init</span>。</p>
<p>虽然使用initrd是必要的，但是它有一些缺点：</p>
<ol>
<li>如果initrd太小了，所需要的脚本不适用。让它过大的话，就会浪费内存</li>
<li>因为它是一个真实的、 <span style="background-color: rgb(192, 192, 192);">静态的</span>设备，它消耗 Linux 内核中的缓存内存，这使得 initrd 有更大的内存消耗</li>
</ol>
<p>initramfs 的诞生解决了这些的问题。</p>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg"><a id="initramfs"></a>Initramfs</span></div>
<p>Initramfs基于tmpfs（大小灵活、 内存中的轻量级文件系统），和initrd一样，它包含的工具和脚本在被称为真正的根文件系统上的二进制文件 init启动之前被挂载 。这些工具包括<span style="background-color: rgb(192, 192, 192);">解密抽象层 （用于加密的文件系统），逻辑卷管理器，软件 raid等</span>。 </p>
<p>Initramfs的全部内容，由一个.cpio归档文件提供，这是一种容易实现的归档格式。所有文件、工具、库、配置设置，都被归档到.cpio文件中，此文件随后<span style="background-color: rgb(192, 192, 192);">被gzip压缩，和Linux内核存放在一起</span>。</p>
<p>BootLoader在启动期间，会将Initramfs提供给内核，内核因而知晓需要使用哪个Initramfs。</p>
<p>内核会创建一个tmpfs，将Initramfs的内容解压到此tmpfs上，然后执行此文件系统上的init脚本。该脚本负责真实的根文件系统的加载，以及其它文件系统的加载。</p>
<p>文件系统加载完毕后，init脚本将<span style="background-color: rgb(192, 192, 192);">自动将根文件系统切换到真实的，并调用/sbin/init继续启动流程</span>。</p>
<div class="blog_h1"><span class="graybg">可移植性</span></div>
<p>对于OS来说，可移植性是指代码从一种体系结构移植到另外一种体系结构的容易程度。Linux的可移植性非常好，它广泛支持多种不同的体系结构。Linux综合考虑可移植性和性能的权衡，并把体系结构特殊的代码存放在arch目录中。2.6版本的内核支持多达21种体系结构</p>
<div class="blog_h2"><span class="graybg">字长和数据类型</span></div>
<p>能够由机器一次完成处理的数据称为<span style="background-color: #c0c0c0;">字</span>，该计量单位类似于字节（byte）和页。字指明了整数的位数，通常说某个机器是64位的时候，其实就是说该机器是的<span style="background-color: #c0c0c0;">字长</span>是64位，即8字节。</p>
<p>处理器的通用寄存器（General-purpose registers）的大小和<span style="background-color: #c0c0c0;">处理器的字长一致</span>。对于一个体系结构，它各部件的宽度——例如内存总线，至少要<span style="background-color: #c0c0c0;">和字长一样大</span>。物理地址空间有时会比字长小，但是<span style="background-color: #c0c0c0;">虚拟地址空间一般等于字长</span>。</p>
<p>此外，C语言定义的<span style="background-color: #c0c0c0;">long的长度总是等于字长</span>，而int有时比字长小。例如对于64位x86，long为8字节，而int为4字节。</p>
<p>某些操作系统（例如Windows）和处理器<span style="background-color: #c0c0c0;">不把机器的标准字称为字</span>，相反，处于历史原因，它们<span style="background-color: #c0c0c0;">用字表示固定长度的数据类型</span>，例如字节（byte）为8位；字（word）为16位；双字（dword）为32位；四字为（qword）为64位，而真实的字长为32位。但是对于Linux，一般提到“字”，就是指CPU的字长。</p>
<p>对于每一个支持的体系结构，Linux都要将<pre class="crayon-plain-tag">asm/types.h</pre> 中的<pre class="crayon-plain-tag">BITS_PER_LONG</pre> 设置的符合C语言规定。</p>
<p>虽然C语言<span style="background-color: #c0c0c0;">规定了变量的最小长度</span>，但是标准长度却可以根据实现来变化，这导致编程时不能对数据类型的长度进行假设。但是以下规则目前是适用的：</p>
<ol>
<li>ANSI C规定，char总是1字节长</li>
<li>尽管没有规定int的长度，但是目前Linux支持的所有体系结构中，它都是32位</li>
<li>short类似， 目前都是16位的</li>
<li>绝不应该假设<span style="background-color: #c0c0c0;">long和指针</span>的长度，随着体系结构的不同，它们可以在32-64位之间变化</li>
<li>类似的，不要假设指针长度和int相同</li>
</ol>
<div class="blog_h3"><span class="graybg">不透明数据类型</span></div>
<p>这类数据类型隐藏其内部格式或结构，在C语言中可以使用typedef声明不透明类型，在<span style="background-color: #c0c0c0;">定义一套接口</span>的时候经常会用到这种技巧，开发者不希望其他人将重新转换为对应的标准C类型。不透明数据类型的例子包括：pid_t，在老的Unix它是short类型的；atomic_t，用于存放可以支持原子操作的整数值，尽管它就是int，但是变量类型提示该类型仅仅在原子操作相关的函数中才使用。</p>
<p>使用不透明数据类型时应该注意：</p>
<ol>
<li>不要假设其长度，称为一个“长度不可知论者”</li>
<li>不要尝试将其转换为标准C类型</li>
</ol>
<div class="blog_h3"><span class="graybg">长度明确的类型</span></div>
<p>很多地方必须使用明确长度的类型，例如一个网络包有一个16位字段；一块声卡可能具有32位寄存器；一个可执行文件有8位cookie。这时候，可以使用内核定义的长度明确的类型，这些类型声明在<pre class="crayon-plain-tag">linux/types.h</pre> 中，如下表：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">类型 </td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>s8</td>
<td>带符号字节</td>
</tr>
<tr>
<td>u8</td>
<td>无符号字节</td>
</tr>
<tr>
<td>s16</td>
<td>带符号16位整数</td>
</tr>
<tr>
<td>u16</td>
<td>无符号16位整数</td>
</tr>
<tr>
<td>s32</td>
<td>带符号32位整数</td>
</tr>
<tr>
<td>u32</td>
<td>无符号32位整数</td>
</tr>
<tr>
<td>s64</td>
<td>带符号64位整数</td>
</tr>
<tr>
<td>u64</td>
<td> 无符号64位整数</td>
</tr>
</tbody>
</table>
<p>上述类型只能在内核中使用，这是为了避免污染用户空间的名字空间。Linux提供了用户空间可用的对应版本，只需要在这些类型前面加两个下划线即可，例如<pre class="crayon-plain-tag">__u64</pre> 。</p>
<div class="blog_h3"><span class="graybg">char型的符号问题 </span></div>
<p>C标准表示的char类型可以带符号，也可以不带，这取决于具体编译器和CPU，或者两者共同决定。在大部分体系结构上char是带符号的其范围是-128~127，但是在ARM上它是不带符号的，范围0~255。如果要明确使用不但符号的字符类型，可以声明<pre class="crayon-plain-tag">unsigned char</pre> 。</p>
<div class="blog_h2"><span class="graybg">数据对齐</span></div>
<p>对齐是跟数据块在内存中位置相关的话题，如果一个<span style="background-color: #c0c0c0;">变量的地址刚好是它长度的整数倍</span>，则称为<span style="background-color: #c0c0c0;">自然对齐</span>。例如，对于32位整数，如果其地址可以被4整除（最低2位为0）则它是自然对齐的。<span style="background-color: #c0c0c0;">对于2^n字节的数据，它地址最低有效n位都是0，则自然对齐</span>。</p>
<p>某些体系结构的对齐要求非常严格，例如对于sparc，载入未对齐的数据会导致错误；另外一些体系结构支持访问未对齐的数据，只不过<span style="background-color: #c0c0c0;">性能会下降</span>。性能下降和CPU访问内存的特性有关（比如某些体系结构要求必须从偶数地址访问内存，虽然理论上可以从任何地址访问），不对齐的数据可能导致不<span style="background-color: #c0c0c0;">必要的多次访问内存</span>。编写可移植性代码要避免对齐问题，保证所有类型都能够自然对齐。</p>
<div class="blog_h3"><span class="graybg">避免对齐引发的问题</span></div>
<p>编译器<span style="background-color: #c0c0c0;">通常会让所有数据自然对齐</span>。但是，编程时如果使用特殊的方式访问指针，就会引发对齐问题：</p>
<pre class="crayon-plain-tag">char str[] = "Hello, World!";
char *p = &amp;str[0];
int i  = *(int *)p;</pre>
<p>上面的例子中，把指向char的指针强制当做int*指针使用，如果变量的内存地址不能被4整除，那么就存在对齐问题。 </p>
<div class="blog_h3"><span class="graybg">非基本类型的对齐</span></div>
<p>复合数据类型按照以下规则对齐：</p>
<ol>
<li>数组：只要按照基本数据类型进行对齐即可</li>
<li>联合体：只要长度最大的数据类型能够对齐即可</li>
<li>结构体：需要每个元素都能正确的对齐</li>
</ol>
<p>结构体的对齐需要引入<span style="background-color: #c0c0c0;">填补机制</span>，才能满足每个元素都对齐的要求，利于对于下面的结构定义：</p>
<pre class="crayon-plain-tag">struct animal_struct
{
    char dog; /* 1 byte */
    unsigned long cat; /* 4 bytes */
    unsigned short pig; /* 2 bytes */
    char fox; /* 1 byte */
};</pre>
<p>编译器会在内存中创建这样的结构体：</p>
<pre class="crayon-plain-tag">struct animal_struct
{
    char dog; /* 1 byte */
    u8 __pad0[3]; /* 填充三字节，确保cat字段按照四字节对齐 */
    unsigned long cat; /* 4 bytes */
    unsigned short pig; /* 2 bytes */
    char fox; /* 1 byte */
    u8 __pad1; /* 额外的填充，使结构体本身的长度能够被4整除 */
};
//使用GCC选项 __attribute__ ((aligned (n)));可以强制结构体按n字节对齐，默认是4字节，n=1字节对齐就不会填充
//使用伪指令可以改变缺省的对齐规则
#pragma pack (n)
//下面的伪指令则取消自定义对齐规则
#pragma pack ()</pre>
<p><pre class="crayon-plain-tag">sizeof</pre> 操作符返回填充后的结构体大小，可以通过重新排列结构体的成员，来避免填充，这样可以<span style="background-color: #c0c0c0;">得到较小的结构体（从而节约空间）</span>。 注意：ANSI C明确规定，<span style="background-color: #c0c0c0;">编译器不得改变结构体成员对象的次序</span>，重新排序必须手工完成。</p>
<p>内核开发者需要注意结构体填充带来的问题，由于不同体系结构使用的填补方式不同，对结构体进行逐字接比较可能没有意义，这也是为何C语言没有提供结构体的比较操作符的原因。</p>
<div class="blog_h2"><span class="graybg">字节顺序</span></div>
<p>字节顺序是指<span style="background-color: #c0c0c0;">一个字中各个字节的排列顺序</span>。处理器可能将：</p>
<ol>
<li>低位字节排放在内存的低地址端，高位字节排放在内存的高地址端，此字节顺序称为<span style="background-color: #c0c0c0;">低位优先（小端，little-endian）</span></li>
<li>高位字节排放在内存的低地址端，低位字节排放在内存的高地址端，此字节顺序称为<span style="background-color: #c0c0c0;">高位优先（大端，little-endian）</span></li>
</ol>
<p>一个32位数<pre class="crayon-plain-tag">0x12345678</pre> 在两种字节序中，内存布局如下：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 内存地址</td>
<td style="text-align: center;">小端 </td>
<td style="text-align: center;">大端</td>
</tr>
</thead>
<tbody>
<tr>
<td>0x0000 </td>
<td>0x78</td>
<td>0x12</td>
</tr>
<tr>
<td>0x0001 </td>
<td>0x56 </td>
<td>0x34</td>
</tr>
<tr>
<td>0x0002</td>
<td>0x34</td>
<td>0x56</td>
</tr>
<tr>
<td>0x0003</td>
<td>0x12</td>
<td>0x78</td>
</tr>
</tbody>
</table>
<p><span style="background-color: #c0c0c0;">x86体系结构都是使用小端，其它大部分体系结构则使用大端</span>。</p>
<p>下面的代码可能很方便的判断当前机器的字节序：</p>
<pre class="crayon-plain-tag">int x = 1;
if (*(char *)&amp;x == 1)
    /* 小端，作为char看待后，会取最低内存的一字节，如果它是1，说明是地位优先 */
else
    /* 大端 */</pre>
<p>对于Linux支持的每一种体系结构，<pre class="crayon-plain-tag">asm/byteorder.h</pre> 中要么定义了宏<pre class="crayon-plain-tag">__BIG_ENDIAN</pre> 要么定义了<pre class="crayon-plain-tag">__LITTLE_ENDIAN</pre> ，以反应机器的字节序。同时，该头文件中还提供了字节序转换的宏：</p>
<pre class="crayon-plain-tag">u23 __cpu_to_be32( u32 );  /* 将CPU字节序转换为大端 */
u32 __cpu_to_le32( u32 );  /* 将CPU字节序转换为小端 */
u32 __be32_to_cpu( u32 );  /* 将大端转换为CPU字节序 */
u32 __le32_to_cpus( u32 ); /* 将小端转换为CPU字节序 */</pre>
<div class="blog_h2"><span class="graybg">时间</span></div>
<p>绝不要假设时钟中断的发生频率<pre class="crayon-plain-tag">HZ</pre> ——也就是每秒中<pre class="crayon-plain-tag">jiffies</pre> 的增量——为某个固定的数值，因为不同体系结构、不同内核版本的HZ值不同。计量时间的正确方法是对HZ进行算术运算，将结果和jiffies增量比较：</p>
<pre class="crayon-plain-tag">HZ /* one second */
(2*HZ) /* two seconds */
(HZ/2) /* half a second */
(HZ/100) /* 10 ms */
(2*HZ/100) /* 20 ms */</pre>
<div class="blog_h2"><span class="graybg">页长度</span></div>
<p>当处理用页管理的内存时，绝对不要假设页的长度。x86-32程序员习惯性的将其假设为4KB，但是其它体系结构上可能不是这个值，有些体系结构还支持多种不同长度的页。应当使用<pre class="crayon-plain-tag">PAGE_SIZE</pre> 表示页长度。</p>
<div class="blog_h2"><span class="graybg">处理器的指令排序</span></div>
<p>有些CPU严格限制指令排序，代码指定的load、store指令的顺序都不能重新排列；而另外一些CPU则会自行排序指令序列。在排序要求最弱的的CPU上，必须使用内存屏障来保证CPU以正确的顺序提交load、store指令。</p>
<div class="blog_h2"><span class="graybg">SMP、内核抢占、高端内存</span></div>
<p>记住下面几条：</p>
<ol>
<li>假设你的代码在SMP上运行， 要正确的选择和使用锁</li>
<li>假设你的代码会在支持内核抢占的情况下运行，正确的选择和使用锁，启停内核抢占</li>
<li>假设你的代码会运行在高端内存上，必要时使用kmap()</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol5">Linux内核学习笔记（五）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-study-note-vol5/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核学习笔记（四）</title>
		<link>https://blog.gmem.cc/linux-kernel-study-note-vol4</link>
		<comments>https://blog.gmem.cc/linux-kernel-study-note-vol4#comments</comments>
		<pubDate>Sun, 06 Feb 2011 02:01:40 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=9084</guid>
		<description><![CDATA[<p>I/O系统架构 Linux的I/O系统的整体架构如下： 当Linux内核组件要读写一些数据时，并不是请求一发出，内核便立即执行该请求，而是将其推迟执行。当传输一个新数据块时，内核需要检查它能否通过。Linux IO调度程序是介于通用块层和块设备驱动程序之间，所以它接收来自通用块层的请求，试图合并请求，并找到最合适的请求下发到块设备驱动程序中。之后块设备驱动程序会调用一个函数来响应这个请求。  Linux整体I/O体系可以分为七层，它们分别是： VFS虚拟文件系统：内核要跟多种文件系统打交道，内核抽象了这VFS，专门用来适配各种文件系统，并对外提供统一操作接口 磁盘缓存：磁盘缓存是一种将磁盘上的一些数据保留在RAM中的软件机制，这使得对这部分数据的访问可以得到更快的响应。磁盘缓存在Linux中有三种类型：Dentry cache ，Page cache ， Buffer cache 映射层：内核从块设备上读取数据，这样内核就必须确定数据在物理设备上的位置，这由映射层（Mapping Layer）来完成 通用块层：由于绝大多数情况的I/O操作是跟块设备打交道，所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备，对上提供统一的Block IO请求标准 I/O调度层：大多数的块设备都是磁盘设备，所以有必要根据这类设备的特点以及应用特点来设置一些不同的调度器 块设备驱动：块设备驱动对外提供高级的设备操作接口 物理硬盘：这层就是具体的物理设备 虚拟文件系统（VFS） [crayon-69d313db6afb5759765690-i/] 、[crayon-69d313db6afbd954812885-i/]  <a class="read-more" href="https://blog.gmem.cc/linux-kernel-study-note-vol4">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol4">Linux内核学习笔记（四）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">I/O系统架构</span></div>
<p>Linux的I/O系统的整体架构如下：<a href="https://blog.gmem.cc/wp-content/uploads/2011/02/linux-io-arch.png"><img class="aligncenter size-full wp-image-22529" src="https://blog.gmem.cc/wp-content/uploads/2011/02/linux-io-arch.png" alt="linux-io-arch" width="359" height="729" /></a></p>
<p>当Linux内核组件要读写一些数据时，<span style="background-color: #c0c0c0;">并不是请求一发出，内核便立即执行该请求</span>，而是将其推迟执行。当传输一个新数据块时，内核需要检查它能否通过。Linux IO调度程序是介于通用块层和块设备驱动程序之间，所以它接收来自通用块层的请求，<span style="background-color: #c0c0c0;">试图合并请求，并找到最合适的请求下发到块设备驱动程序中</span>。之后块设备驱动程序会调用一个函数来响应这个请求。 </p>
<p>Linux整体I/O体系可以分为七层，它们分别是：</p>
<ol>
<li>VFS虚拟文件系统：<span style="background-color: #c0c0c0;">内核要跟多种文件系统打交道，内核抽象了这VFS</span>，专门用来<span style="background-color: #c0c0c0;">适配</span>各种文件系统，并对外提供统一操作接口</li>
<li>磁盘缓存：磁盘缓存是一种将磁盘上的一些<span style="background-color: #c0c0c0;">数据保留在RAM中的软件机制</span>，这使得对这部分数据的访问可以得到<span style="background-color: #c0c0c0;">更快的响应</span>。磁盘缓存在Linux中有三种类型：<span style="background-color: #c0c0c0;">Dentry cache ，Page cache ， Buffer cache</span></li>
<li>映射层：内核从块设备上读取数据，这样内<span style="background-color: #c0c0c0;">核就必须确定数据在物理设备上的位置</span>，这由映射层（Mapping Layer）来完成</li>
<li>通用块层：由于绝大多数情况的I/O操作是跟块设备打交道，所以Linux在此提供了一个类似vfs层的<span style="background-color: #c0c0c0;">块设备操作抽象层</span>。下层对接各种不同属性的块设备，对上<span style="background-color: #c0c0c0;">提供统一的Block IO请求标准</span></li>
<li>I/O调度层：大多数的块设备都是磁盘设备，所以有必要<span style="background-color: #c0c0c0;">根据这类设备的特点以及应用特点来设置一些不同的调度器</span></li>
<li>块设备驱动：块设备驱动对外提供高级的设备操作接口</li>
<li>物理硬盘：这层就是具体的物理设备</li>
</ol>
<div class="blog_h1"><span class="graybg">虚拟文件系统（VFS）</span></div>
<p><pre class="crayon-plain-tag">open()</pre> 、<pre class="crayon-plain-tag">read()</pre>  、<pre class="crayon-plain-tag">write()</pre> 等系统调用可用来访问各种文件系统和媒体介质，这得益于现代操作系统引入的<span style="background-color: #c0c0c0;">抽象层（对于Linux就是VFS）</span>。作为内核子系统，VFS为<span style="background-color: #c0c0c0;">内核其它部分、用户空间程序</span>提供文件、文件系统相关的统一接口。所有底层文件系统依赖于VFS共存、协作，程序可以使用标准的Unix系统调用访问不同的文件系统，甚至是不同的媒体介质。 </p>
<p>为了支持多文件系统，VFS提供了一个通用文件系统模型，囊括了常见文件系统的常用功能集和行为，该模型偏向于Unix风格的文件系统，但是支持FAT、NTFS等差异很大的文件系统。</p>
<p>在内核，除了文件系统本身以外，不需要了解文件系统的任何细节。例如下面这个简单的操作：</p>
<pre class="crayon-plain-tag">ret = write (fd, buf, len);</pre>
<p>将buf指向的缓冲区中len字节写入到文件描述符fd所代表的文件的当前位置，其处理过程如下图： <br /> <img class="aligncenter size-full wp-image-9017" src="https://blog.gmem.cc/wp-content/uploads/2011/01/write-call-procedure.png" alt="write-call-procedure" width="85%" /></p>
<p>和具体文件系统相关的部分，被屏蔽在VFS层的下面，用户程序不知道页不需要知道文件系统是如何把数据写入到物理介质中的。</p>
<div class="blog_h2"><span class="graybg">Unix文件系统</span></div>
<p>Unix使用了四个和文件系统相关的重要概念：<span style="background-color: #c0c0c0;">文件、目录项、索引节点、挂载点</span>（Mount point）。</p>
<p>文件系统本质上是一种特殊的<span style="background-color: #c0c0c0;">数据分层存储结构</span>，它包含文件、目录和相关的控制信息。文件系统的通用操作包括<span style="background-color: #c0c0c0;">创建、删除、安装</span>等。在Unix中<span style="background-color: #c0c0c0;">文件系统被安装在特定的挂载点</span>上，挂载点在全局层次结构（近来Linux已经允许进程具有自己的层次结构）中被称为<span style="background-color: #c0c0c0;">命名空间</span>。所有安装的文件系统<span style="background-color: #c0c0c0;">都作为根文件系统的枝叶</span>节点出现，这和DOS/Windows基于盘符的命名空间划分截然不同。</p>
<p>文件可以看做<span style="background-color: #c0c0c0;">是字节的有序串</span>，为了系统和用户便于识别，每个文件被分配一个可读的名字。文件支持的典型操作包括<span style="background-color: #c0c0c0;">读、写、创建、删除</span>。Unix文件和面向记录的文件系统（例如OpenVMS的File-11）很不一样，后者提供更丰富的、结构化的表示。</p>
<p>文件通过目录进行组织，目录之间可以嵌套，形成<span style="background-color: #c0c0c0;">文件路径</span>，路径中每一部分称为<span style="background-color: #c0c0c0;">目录条目（directory entry）</span>。对于Unix来说，目录就是普通的文件，只是它列出其中包含的所有文件而已。由于<span style="background-color: #c0c0c0;">VFS把目录作为文件看待</span>，因此可以对目录和文件执行相同的操作。</p>
<p>Unix把描述<span style="background-color: #c0c0c0;">文件的信息（文件元数据）</span>和文件本身加以区分，元数据（权限、大小、所有者、修改日期…）被存储在<span style="background-color: #c0c0c0;">单独的数据结构中，称为索引节点（inode，index node）</span>。</p>
<p>上面这些概念都和<span style="background-color: #c0c0c0;">文件系统本身的控制信息</span>紧密相关，这些控制信息存放在<span style="background-color: #c0c0c0;">超级块（superblock）</span>中，超级块是包含<span style="background-color: #c0c0c0;">整个文件系统相关信息</span>的数据结构。</p>
<p>所有单个文件、文件系统本身的元数据，合称为<span style="background-color: #c0c0c0;">文件系统元数据（filesystem metadata</span>）。</p>
<p>Unix文件系统一直以来都是遵循上述描述设计和实现的，比如在磁盘上，文件（目录）信息按照inode形式存放在单独的块上，控制信息被集中存放在磁盘的超级块上。Linux的VFS就是要保证支持和实现了上述概念的文件系统能够协同工作。对于FAT或者NTFS之类的非Unix文件系统，VFS也提供支持，但是必须进行适配。</p>
<div class="blog_h2"><span class="graybg">VFS对象及其数据结构</span></div>
<p>VFS子系统使用了OO思维设计，定义了一组结构体表示通用文件对象，这些结构在包含数据的同时包含操作（函数指针），这些操作由具体文件系统负责实现。VFS主要包含4个对象类型，每个类型包含了一个“操作对象”： </p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 对象类型</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>超级块对象</td>
<td>
<p>代表一个已安装的文件系统。操作对象：<pre class="crayon-plain-tag">super_operations</pre> ，包含了内核可以针对文件系统进行的操作</p>
</td>
</tr>
<tr>
<td>索引节点对象</td>
<td>代表一个具体文件，注意VFS把目录作为文件看待，因此该对象也用于表示文件。操作对象：<pre class="crayon-plain-tag">inode_operations</pre> ，包含了内核可以针对一个具体文件进行的操作</td>
</tr>
<tr>
<td>目录项对象</td>
<td>代表一个目录项，即路径的一部分。操作对象：<pre class="crayon-plain-tag">dentry_operations</pre> ，包含了内核可以针对具体目录项进行的操作</td>
</tr>
<tr>
<td>文件对象</td>
<td>代表由进程打开的文件。操作对象：<pre class="crayon-plain-tag">file_operations</pre> ，包含了进程可以针对已打开文件进行的操作</td>
</tr>
</tbody>
</table>
<p>操作对象是包含若干函数指针的结构体，这些函数指针可以操控父对象。其中很多操作可以继承使用VFS提供的通用函数，如果通用函数不能满足需求，必须使用实际文件系统的独有方法填充这些函数指针。</p>
<p>除了上述4种基本的类型外，VFS还大量使用了其它结构体，例如：</p>
<ol>
<li>file_system_type：每个注册的文件系统类型使用该结构表示，描述文件系统及其性能参数</li>
<li>vfsmount：表示一个挂载点</li>
</ol>
<div class="blog_h2"><span class="graybg">超级块对象</span></div>
<p>所有文件系统都必须实现超级块对象，以存放特定文件系统的信息：</p>
<ol>
<li>对于基于磁盘的文件系统：超级块通常存放在<span style="background-color: #c0c0c0;">磁盘特定扇区</span>中的<span style="background-color: #c0c0c0;">文件系统超级块或者文件系统控制块（control block）</span>中</li>
<li>对于非磁盘文件系统：例如基于内存的sysfs，会即时（on-the-fly）在内存中创建超级块对象</li>
</ol>
<p>超级块结构定义如下：</p>
<pre class="crayon-plain-tag">struct super_block
{
    struct list_head s_list; /* 指向所有超级块的链表 */
    dev_t s_dev; /* 设备标识符 */
    unsigned char s_dirt; /*修改（脏）标记*/
    unsigned char s_blocksize_bits;/* 以位为单位的块大小 */
    unsigned long s_blocksize; /*以字节为单位的块大小*/
    loff_t s_maxbytes; /* 文件最大尺寸 */
    struct file_system_type *s_type;/* 文件系统类型 */
    const struct super_operations *s_op;/* 支持的操作的集合 */
    const struct dquot_operations *dq_op;/*磁盘配额相关操作的集合 */
    const struct quotactl_ops *s_qcop;/* 磁盘限额控制操作的集合 */
    const struct export_operations *s_export_op;/* 导出相关操作的集合 */
    unsigned long s_flags;/* 挂载标志 */
    unsigned long s_magic; /* 文件系统的魔数 */
    struct dentry *s_root; /* 目录挂载点 */
    struct rw_semaphore s_umount; /* 卸载信号量 */
    struct mutex s_lock;/* 超级块信号量 */
    int s_count;/* 超级块引用计数*/
    int s_need_sync; /* 尚未同步标志 */
    atomic_t s_active;/* 活动引用计数*/
#ifdef CONFIG_SECURITY
    void *s_security; /* 和安全模块相关 */
#endif
    struct xattr_handler **s_xattr;/* 扩展属性操作的集合 */

    struct list_head s_inodes; /*所有inode的链表 */
    struct hlist_head s_anon; /* 用于nfs导出的匿名目录项 */
    struct list_head s_files;/* 已经分配的文件的链表 */
    /* s_dentry_lru和s_nr_dentry_unused由dcache_lock保护 */
    struct list_head s_dentry_lru; /* 未使用目录项的LRU链表 */
    int s_nr_dentry_unused; /* # of dentry on lru */

    struct block_device *s_bdev;/* 相关联的块设备 */
    struct backing_dev_info *s_bdi;/*  */
    struct mtd_info *s_mtd;/* 内存盘信息 */
    struct list_head s_instances;/* 当前文件系统的实例 */
    struct quota_info s_dquot; /* 配额相关的选项 */

    int s_frozen;/* 冻结标志 */
    wait_queue_head_t s_wait_unfrozen;/* 在freeze上的等待队列 */

    char s_id[32]; /* 文本的名称 */
    void *s_fs_info; /* 文件系统私有的信息 */
    fmode_t s_mode; /*挂载的模式（权限）*/

    /* Granularity of c/m/atime in ns.
     Cannot be worse than a second */
    u32 s_time_gran; /* 时间戳的粒度 */

    /*
     * 该字段仅和VFS有关，具体文件系统不需要访问
     */
    struct mutex s_vfs_rename_mutex; /* Kludge */

    char *s_subtype; //文件系统子类型，/proc/mounts 会显示为 "type.subtype"

    char *s_options; //已保存挂载选项
};</pre>
<p>创建、管理、撤销超级块对象的代码位于<pre class="crayon-plain-tag">fs/super.c</pre> 。超级块通过<pre class="crayon-plain-tag">alloc_super()</pre> 函数创建并初始化，在文件系统被安装时，文件系统会调用此函数从磁盘读取超级块，并将信息填充到内存的超级块对象中。</p>
<div class="blog_h2"><span class="graybg">超级块操作</span></div>
<p>该结构对应了索引块对象的<pre class="crayon-plain-tag">s_op</pre> 字段，它是一系列函数指针的集合：</p>
<pre class="crayon-plain-tag">/**
 * 这些函数执行文件系统和索引节点的底层操作
 * 这些函数都由VFS在进程上下文中调用，大部分函数必要时会阻塞
 */
struct super_operations
{
    //在给定的超级块下面创建和初始化一个inode
    struct inode *(*alloc_inode)( struct super_block *sb );
    //释放指定的inode
    void (*destroy_inode)( struct inode * );
    //标记inode为脏的，日志文件系统（ext3/4）会调用该函数进行日志更新
    void (*dirty_inode)( struct inode * );
    //将指定索引节点写入磁盘
    int (*write_inode)( struct inode *, struct writeback_control *wbc );
    //在inode最后一个引用释放后，VFS调用该函数
    void (*drop_inode)( struct inode * );
    //从磁盘上删除指定的索引节点
    void (*delete_inode)( struct inode * );
    //卸载文件系统时由VFS调用，用来释放超级块，调用者必须持有s_lock
    void (*put_super)( struct super_block * );
    //用给定的超级块对象更新磁盘上的超级块，用于超级块同步，调用者必须持有s_lock
    void (*write_super)( struct super_block * );
    //使文件系统的元数据同步到文件系统
    int (*sync_fs)( struct super_block *sb, int wait );
    int (*freeze_fs)( struct super_block * );
    int (*unfreeze_fs)( struct super_block * );
    //由VFS调用以获得文件系统的统计信息
    int (*statfs)( struct dentry *, struct kstatfs * );
    //使用新的选项挂载文件系统，调用者必须持有s_lock
    int (*remount_fs)( struct super_block *, int * flags, char * );
    //VFS调用该函数释放inode，清除包含相关数据的页面
    void (*clear_inode)( struct inode * );
    //VFS调用该函数中断挂载操作，由网络文件系统例如NFS使用
    void (*umount_begin)( struct super_block * );

    int (*show_options)( struct seq_file *, struct vfsmount * );
    int (*show_stats)( struct seq_file *, struct vfsmount * );
#ifdef CONFIG_QUOTA
    ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
    int (*bdev_try_to_free_page)( struct super_block*, struct page*, gfp_t );
};</pre>
<p>这些函数指针中，一部分是可选的，如果设置为NULL，VFS会<span style="background-color: #c0c0c0;">调用通用函数，或者什么也不作（取决于操作类型）</span>。 </p>
<p>在调用这些函数的时候，可能需要把超级块、inode对象传入，例如：</p>
<pre class="crayon-plain-tag">sb-&gt;s_op-&gt;write_super(sb);</pre>
<p>这是由于C语言没有面向对象的支持，不能像C++那样：</p>
<pre class="crayon-plain-tag">sb.write_super();</pre>
<div class="blog_h2"><span class="graybg">索引节点对象</span></div>
<p>inode包含了内核<span style="background-color: #c0c0c0;">操作文件或者目录</span>时需要的<span style="background-color: #c0c0c0;">全部信息</span>。对于Unix来说，inode信息可以直接从磁盘读取。某些不支持inode的文件系统，通常将文件的描述信息（元数据）<span style="background-color: #c0c0c0;">同文件一起存放</span>。某些现代文件系统使用<span style="background-color: #c0c0c0;">数据库来存放文件元数据</span>。不管是那种情况，VFS必须在内存中创建inode对象。</p>
<p>inode使用下面的结构表示：</p>
<pre class="crayon-plain-tag">struct inode
{
    struct hlist_node i_hash; //散列表
    struct list_head i_list; /* inode链表 backing dev IO list */
    struct list_head i_sb_list; //超级块链表
    struct list_head i_dentry; //目录项链表
    unsigned long i_ino; //索引节点号
    atomic_t i_count; //引用计数
    unsigned int i_nlink; //硬链接计数
    uid_t i_uid; //所有者uid
    gid_t i_gid; //所有者gid
    dev_t i_rdev; //实际设备节点
    u64 i_version; //版本号
    loff_t i_size; //文件尺寸（字节数）
#ifdef __NEED_I_SIZE_ORDERED
    seqcount_t i_size_seqcount; //对i_size进行串行计数
#endif
    struct timespec i_atime; //最后访问时间
    struct timespec i_mtime; //最后修改时间
    struct timespec i_ctime; //最后改变实际
    unsigned int i_blkbits;  //以位为单位的块大小
    blkcnt_t i_blocks;  //文件的块数
    unsigned short i_bytes;//消耗的字节数
    umode_t i_mode; //文件访问权限
    spinlock_t i_lock; /* 自旋锁 */
    struct mutex i_mutex;  //互斥锁
    struct rw_semaphore i_alloc_sem; //信号量
    const struct inode_operations *i_op; //inode操作表
    const struct file_operations *i_fop; //默认inode操作
    struct super_block *i_sb; //关联的超级块
    struct file_lock *i_flock; //文件锁链表
    struct address_space *i_mapping;//关联的地址映射
    struct address_space i_data; //关联的设备映射
#ifdef CONFIG_QUOTA
    struct dquot *i_dquot[MAXQUOTAS]; //索引节点的磁盘配额
#endif
    struct list_head i_devices; //块设备链表
    union
    {
        struct pipe_inode_info *i_pipe; //管道信息
        struct block_device *i_bdev; //块设备驱动
        struct cdev *i_cdev;//字符设备驱动
    };

    __u32 i_generation;

#ifdef CONFIG_FSNOTIFY
    __u32 i_fsnotify_mask; /* 该inode关心的所有事件 */
    struct hlist_head i_fsnotify_mark_entries; /* fsnotify标记条目 */
#endif

#ifdef CONFIG_INOTIFY
    struct list_head inotify_watches; //索引节点通知监听链表
    struct mutex inotify_mutex; //保护上一个字段的互斥锁
#endif

    unsigned long i_state; //状态标识
    unsigned long dirtied_when; //第一次变脏的时间

    unsigned int i_flags; //文件系统标志

    atomic_t i_writecount; //写者计数
#ifdef CONFIG_SECURITY
    void *i_security; //安全模块
#endif
#ifdef CONFIG_FS_POSIX_ACL
    struct posix_acl *i_acl;
    struct posix_acl *i_default_acl;
#endif
    void *i_private; //文件系统或者设备私有指针
};</pre>
<p>inode<span style="background-color: #c0c0c0;">代表文件系统中的一个文件</span>，只有文件<span style="background-color: #c0c0c0;">被访问时，inode才会在内存中创建</span>。目标文件可以普通文件，也可以是设备、管道之类的特殊文件（此时i_bdev/i_pipe/icdev指向相关对象）。</p>
<p><span style="background-color: #c0c0c0;">某些文件系统不支持inode结构体中的某些字段</span>，例如i_atime（文件访问时间），此时该字段如何存储取决于具体实现。</p>
<div class="blog_h2"><span class="graybg">索引节点操作</span></div>
<p>该结构对应了inode对象的<pre class="crayon-plain-tag">i_op</pre> 字段，它是一系列函数指针的集合： </p>
<pre class="crayon-plain-tag">/**
 * 索引节点操作，这些函数可能由VFS调用，也可能由具体文件系统调用
 */
struct inode_operations
{
    //由系统调用create()/open()调用，从而为dentry对象创建一个新的inode
    int (*create)( struct inode *dir, struct dentry *dentry, int mode );
    //在特定目录下寻找索引节点，索引节点必须匹配dentry给出的文件名
    struct dentry * lookup( struct inode *dir, struct dentry *dentry );
    //由系统调用link()调用，用来创建dir目录下old_dentry目录项所代表的文件的硬链接，此硬链接的名称由dentry指定
    int (*link)( struct dentry *old_dentry, struct inode *dir, struct dentry *dentry );
    //由系统调用unlink()调用，移除目录dir中dentry代表的inode
    int (*unlink)( struct inode *dir, struct dentry *dentry );
    //由系统调用symlink()调用，创建符号链接
    int (*symlink)( struct inode *dir, struct dentry *dentry, const char * symname );
    //由系统调用mkdir()调用，创建新的目录
    int (*mkdir)( struct inode * dir, struct dentry *, int mode );
    //由系统调用rmdir()调用，从dir中移除dentry引用的目录
    int (*rmdir)( struct inode *dir, struct dentry * dentry );
    /**
     * 由系统调用mknod()调用，以创建一个特殊（设备、套接字、命名管道）文件
     * 该文件关联的设备为rdev，该文件作为dir中的dentry文件
     */
    int (*mknod)( struct inode *dir, struct dentry *dentry, int mode, dev_t rdev );
    //由VFS调用，负责移动文件
    int (*rename)( struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry );
    //由系统调用readlink()调用，拷贝dentry关联符号链接的最多buflen字节到buffer
    int (*readlink)( struct dentry *dentry, char *buffer, int buflen );
    //由VFS调用，转换符号链接为其指向的inode，结果存放在nd中
    void * (*follow_link)( struct dentry *dentry, struct nameidata *nd );
    //由VFS调用，在follow_link()调用之后，进行清理
    void (*put_link)( struct dentry *, struct nameidata *, void * );
    //由VFS调用，截断文件的大小，调用前必须把inode.i_size设置为期望的大小
    void (*truncate)( struct inode * );
    //检查指定的权限模式是否被允许，如果是返回0
    int (*permission)( struct inode *inode, int mask );
    //读、写、列出、移除文件属性
    int (*setattr)( struct dentry *, struct iattr * );
    int (*getattr)( struct vfsmount *mnt, struct dentry *, struct kstat * );
    int (*setxattr)( struct dentry *, const char *, const void *, size_t, int );
    ssize_t (*getxattr)( struct dentry *, const char *, void *, size_t );
    ssize_t (*listxattr)( struct dentry *, char *, size_t );
    int (*removexattr)( struct dentry *, const char * );
    void (*truncate_range)( struct inode *, loff_t, loff_t );
    long (*fallocate)( struct inode *inode, int mode, loff_t offset, loff_t len );
    int (*fiemap)( struct inode *, struct fiemap_extent_info *, u64 start, u64 len );
};</pre>
<div class="blog_h2"><span class="graybg">目录项对象</span></div>
<p>为了方便查找操作，VFS引入目录项（dentry）的概念，每个目录项代表路径中的一部分。以/usr/bin/vi为例，<span style="background-color: #c0c0c0;">第一个/</span>、usr、bin、vi都是目录项，前三个是目录，最后一个是普通文件。</p>
<p>一个有效的目录项必然对应一个inode；反之，一个inode则可能对应多个目录项，因为一个inode可以具有多个路径名（链接）。</p>
<p>目录项由下面的结构表示：</p>
<pre class="crayon-plain-tag">struct dentry
{
    atomic_t d_count; /* 目录项引用计数器 */
    unsigned int d_flags; /* 目录项标志位 */
    spinlock_t d_lock; /* 当前目录项的自旋锁 */
    int d_mounted; /* 该目录项是否代表一个挂载点 */
    struct inode *d_inode; /* 与目录项关联的inode */
    struct hlist_node d_hash; /* 通过该字段，当前目录项挂接到dentry_hashtable的某个链表中 */
    struct dentry *d_parent; /* 父目录的目录项对象 */
    struct qstr d_name; /* 目录项的名称 */
    struct list_head d_lru; /* 如果当前目录项未被使用，则通过此字段挂接到dentry_unused队列中 */
    union
    {
        struct list_head d_child; /* 父目录的子目录项所形成的链表 */
        struct rcu_head d_rcu; /* RCU locking */
    } d_u;
    struct list_head d_subdirs; /* 子目录链表 */
    struct list_head d_alias; /* inode别名（目录项）的链表 */
    unsigned long d_time; /* 重验证时间，由d_revalidate()调用使用 */
    struct dentry_operations *d_op; /* 目录项操作集合 */
    struct super_block *d_sb; /* 文件所属的超级块 */
    void *d_fsdata; /* 文件系统私有数据 */
    unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 短文件名 */
};</pre>
<p>可以看到目录项结构牵涉到散列、树、链表等数据结构，这是VFS高效文件搜索、定位的基础。</p>
<p>与超级块、索引节点不同，目录项<span style="background-color: #c0c0c0;">没有对应的磁盘数据结构</span>（因而也没有脏、回写标志）， VFS会根据字符串形式的路径现场创建目录项。</p>
<div class="blog_h3"><span class="graybg">目录项状态</span></div>
<p>目录项对象具有三种有效的状态：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 状态</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>被使用</td>
<td>如果目录项对应了一个有效的索引节点（d_inode不为空）并且存在一个以上使用者（d_count正数），则目录项处于被使用状态。处于该状态的目录项被VFS使用并且指向有效数据，不得丢弃</td>
</tr>
<tr>
<td>未被使用 </td>
<td>如果目录项对应了一个有效的索引节点（d_inode不为空）但d_count为0，则目录项处于未被使用状态。处于该状态的目录项可以缓存以备后用，但是回收内存时可以安全的丢弃</td>
</tr>
<tr>
<td>负状态（negative）</td>
<td>如果目录项没有有效的索引节点，则处于该状态。处于该状态的目录项仍然可能被使用（比如某个进程读取一个不存在的文件时），但是可以被安全的丢弃</td>
</tr>
</tbody>
</table>
<p>目录项对象释放后，可以被存放到slab缓存中去。</p>
<div class="blog_h3"><span class="graybg">目录项缓存</span></div>
<p>完整的路径解析的开销很大，要得到子目录项，必须读取父目录的内容，则会形成一个递归的处理过程。为避免反复解析路径中的所有元素，VFS将目录项对象缓存起来，该缓存称为<span style="background-color: #c0c0c0;">目录项缓存（dcache）</span>，该缓存包含三项内容：</p>
<ol>
<li>“被使用的”目录项链表：此链表的头由目录项关联索引节点的inode.i_dentry指定（注意该链表的元素的不是inode而是entry，后者通过dentry.d_alias挂接到此链表），一个inode可能存在多个链接，因此就对应多个目录项</li>
<li>“最近被使用的”双向链表：该链表包含未被使用、负状态的目录项对象，由于数据总是在链表头部插入，因此头部的数据总是更新。当内核必须回收内存时，将从尾部删除此链表</li>
<li>用以快速解析路径为目录项的散列表和对应散列函数。散列值由<pre class="crayon-plain-tag">d_hash()</pre> 函数计算，它是内核提供给文件系统的唯一散列函数；散列表的查找则通过<pre class="crayon-plain-tag">d_lookup()</pre> 函数进行，如果dcache中存在匹配的目录项则返回之，否则返回NULL</li>
</ol>
<p>举例来说，如果现在你要打开文件/usr/local/jdk/bin/java，VFS为了避免解析这一路径，会先在dcache中查找此路径对应的目录项，如果找不到，才去查找文件系统，为每个路径分量解析目录项，解析完毕后，会把结果存入dcache中。</p>
<p>dcache实际上也缓存了inode，因为目录项会导致目标inode的使用计数为正，那么目录项驻留内存期间，inode也必然驻留。</p>
<p>因为文件访问呈现<span style="background-color: #c0c0c0;">时间、空间局部性</span>，因此目录项缓存常常具有较高的命中率：</p>
<ol>
<li>时间局部性：程序往往反复的访问同一个文件</li>
<li>空间局部性：程序往往会访问同一目录下所有文件</li>
</ol>
<div class="blog_h2"><span class="graybg">目录项操作</span></div>
<p>目录项支持操作如下表：</p>
<pre class="crayon-plain-tag">struct dentry_operations
{
    //判断目录项是否有效，当VFS从dcache中使用一个目录项时会调用。大部分实现设置为NULL，认为dcache中的目录项总是有效的
    int (*d_revalidate)( struct dentry *, struct nameidata * );
    //为目录项生成散列值，在加入散列表前由VFS调用
    int (*d_hash)( struct dentry *, struct qstr * );
    //VFS调用该函数比较两个文件名，大部分实现设置为NULL，除了像FAT这样不区分大小写的文件系统。调用该函数需要持有dcache_lock锁
    int (*d_compare)( struct dentry *dentry, struct qstr *name1, struct qstr *name2 );
    //当目录项对象的d_count为0时，VFS调用该函数。调用该函数需要持有dcache_lock、d_lock
    int (*d_delete)( struct dentry * );
    //当目录项将被释放时，VFS调用该函数，默认实现不做任何事情
    void (*d_release)( struct dentry * );
    //当目录项对应的inode被删除时，VFS调用该函数，默认实现是调用iput()释放索引节点
    void (*d_iput)( struct dentry *, struct inode * );
    char *(*d_dname)( struct dentry *, char *, int );
};</pre>
<div class="blog_h2"><span class="graybg">文件对象</span></div>
<p>该对象代表一个<span style="background-color: #c0c0c0;">已打开的文件</span>的内存表示，多个进程可能打开一个文件，因此<span style="background-color: #c0c0c0;">同一个物理文件可能对应多个文件对象</span>。从用户角度看待VFS，该对象将首先进入视野，因为用户程序直接处理的是文件而不是超级块、索引节点等。</p>
<p>文件对象使用下面的结构表示：</p>
<pre class="crayon-plain-tag">struct file
{
    union
    {
        struct list_head fu_list; /* 文件对象的链表 */
        struct rcu_head fu_rcuhead; /* 释放之后的RCU链表 */
    } f_u;
    struct path f_path; /* 此结构包含对应的目录项 */
    struct file_operations *f_op; /* 文件操作集合 */
    spinlock_t f_lock; /* 每个文件的自旋锁 */
    atomic_t f_count; /* 文件对象的使用计数 */
    unsigned int f_flags; /* 打开文件时指定的标志位 */
    mode_t f_mode; /* 文件的访问模式 */
    loff_t f_pos; /* 文件偏移量（文件指针） */
    struct fown_struct f_owner; /* owner data for signals */
    const struct cred *f_cred; /* file credentials */
    struct file_ra_state f_ra; /* 预读（read-ahead）状态 */
    u64 f_version; /* 版本号 */
    void *f_security; /* 安全模块 */
    void *private_data; /* tty 设备驱动的钩子 */
    struct list_head f_ep_links; /* list of epoll links */
    spinlock_t f_ep_lock; /* epoll lock */
    struct address_space *f_mapping; /* 页缓存映射 */
    unsigned long f_mnt_write_state; /* 调试状态 */
};</pre>
<p>和目录项类似，文件对象也没有对应的磁盘实体。</p>
<div class="blog_h2"><span class="graybg">文件操作</span></div>
<p>文件操作是标准Unix系统调用的基础，定义如下：</p>
<pre class="crayon-plain-tag">struct file_operations
{
    struct module *owner;
    //系统调用llseek()会调用该函数，将文件指针设置到指定的偏移量
    loff_t (*llseek)( struct file *file, loff_t offset, int origin );
    //系统调用read()会调用该函数，从偏移量offset处开始读取count字节到buf中，并更新文件指针
    ssize_t (*read)( struct file *file, char *buf, size_t count, loff_t *offset );
    //系统调用aio_read()会调用该函数，针对icob描述的文件启动异步读操作
    ssize_t (*aio_read)( struct kiocb *iocb, char *buf, size_t count, loff_t offset );
    //系统调用write()会调用该函数，把buf写入到file的offset偏移处，并更新文件指针
    ssize_t (*write)( struct file *file, const char *buf, size_t count, loff_t *offset );
    //系统调用aio_write()会调用该函数，针对icob描述的文件启动异步写操作
    ssize_t (*aio_write)( struct kiocb *iocb, const char *buf, size_t count, loff_t offset );
    //系统调用readdir()会调用该函数，返回目录列表中的下一个目录
    int (*readdir)( struct file *file, void *dirent, filldir_t filldir );
    //系统调用poll()会调用该函数，睡眠以等待目标文件上的活动（activity）
    unsigned int (*poll)( struct file *file, struct poll_table_struct *poll_table );
    //系统调用ioctl()会调用该函数，用来给设备发送命令和参数对，文件必须是打开的设备节点，调用者必须持有大内核锁（BKL）
    int (*ioctl)( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg );
    //类似上面，但是不持有BKL锁。这两个函数只需实现一个
    long (*unlocked_ioctl)( struct file *file, unsigned int cmd, unsigned long arg );
    //ioctl()的可移植变体，被设计为在64bits架构上对32位也是安全的，该函数能够进行必要的字（word）大小转换
    //该函数不需要持有BKL锁
    long (*compat_ioctl)( struct file *file, unsigned int cmd, unsigned long arg );
    //系统调用mmap()会调用该函数，用于把文件映射到指定的地址空间上
    int (*mmap)( struct file *, struct vm_area_struct * );
    //系统调用open()会调用该函数，创建一个文件对象并关联到特定的inode
    int (*open)( struct inode *, struct file * );
    //当已打开文件的引用计数减少时，VFS会调用该函数，其作用依赖于具体实现
    int (*flush)( struct file *, fl_owner_t id );
    //当已打开文件的最后一个引用注销时（例如最后一个共享文件描述符的进程调用close()或者退出时）该函数被VFS调用，其作用依赖于具体实现
    int (*release)( struct inode *, struct file * );
    //系统调用fsync()会调用该函数，把文件的所有缓存数据回写到磁盘
    int (*fsync)( struct file *, struct dentry *, int datasync );
    //系统调用aio_fsync()会调用该函数，把iocb描述的文件的所有被缓存数据回写到磁盘
    int (*aio_fsync)( struct kiocb *, int datasync );
    //启用或者禁用异步I/O的信号通知（signal notification）
    int (*fasync)( int, struct file *, int );
    //操控指定文件的文件锁
    int (*lock)( struct file *, int, struct file_lock * );
    //系统调用readv()会调用该函数，从文件读取数据，并把结果存放到vector中，完毕后文件的偏移量增加
    ssize_t (*readv)( struct file *file, const struct iovec *vector, unsigned long count, loff_t *offset );
    //系统调用writev()会调用该函数，把vector中的数据写入到文件，完毕后文件的偏移量增加
    ssize_t (*writev)( struct file *file, const struct iovec *vector, unsigned long count, loff_t *offset );
    //系统调用sendfile()会调用该函数，用于在文件之间复制内容，整个拷贝在内核空间完成
    ssize_t (*sendfile)( struct file *file, loff_t *offset, size_t size, read_actor_t actor, void *target );
    //用于在文件之间拷贝数据
    ssize_t (*sendpage)( struct file *, struct page *, int, size_t, loff_t *, int );
    //获得未使用的地址空间（address space），用于映射指定的文件
    unsigned long (*get_unmapped_area)( struct file *, unsigned long, unsigned long, unsigned long, unsigned long );
    //系统调用flock()会调用该函数，用来实现advisory locking
    int (*flock)( struct file *, int, struct file_lock * );
    ssize_t (*splice_write)( struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int );
    ssize_t (*splice_read)( struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int );
    int (*setlease)( struct file *, long, struct file_lock ** );
};</pre>
<div class="blog_h2"><span class="graybg">和文件系统相关的数据结构</span></div>
<p>除了上述几种基础的数据结构外，内核还使用另外一些标准结构来管理文件系统的其它信息：</p>
<div class="blog_h3"><span class="graybg">file_system_type</span></div>
<p>该结构用来描述特定的文件系统类型，例如ext3、ext4或者UDF。</p>
<pre class="crayon-plain-tag">struct file_system_type
{
    const char *name; /* 文件系统类型的名称 */
    int fs_flags; /* 文件系统类型标志 */
    /* 用于从磁盘读取超级块 */
    struct super_block *(*get_sb)( struct file_system_type *, int, char *, void * );
    /* 用于终止超级块的访问 */
    void (*kill_sb)( struct super_block * );
    struct module *owner; /* 文件系统所属的模块 */
    struct file_system_type *next; /* 链表中的下一个文件系统 */
    struct list_head fs_supers; /* 超级块对象列表 */
    /* 下面的字段用于运行时锁定验证 */
    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
    struct lock_class_key i_lock_key;
    struct lock_class_key i_mutex_key;
    struct lock_class_key i_mutex_dir_key;
    struct lock_class_key i_alloc_sem_key;
};</pre>
<p>不管某种文件系统安装了多少个实例，都只有一个file_system_type对象；相应的每个文件系统实例对应各自的vfsmount对象。</p>
<div class="blog_h3"><span class="graybg">vfsmount</span></div>
<p>该结构用来描述一个已挂载的文件系统实例：</p>
<pre class="crayon-plain-tag">struct vfsmount
{
    struct list_head mnt_hash; /* 散列表的链表 */
    struct vfsmount *mnt_parent; /* 父文件系统 */
    struct dentry *mnt_mountpoint; /* 挂载点的目录项 */
    struct dentry *mnt_root; /* 该文件系统的根目录项 */
    struct super_block *mnt_sb; /* 该文件系统的超级块 */
    struct list_head mnt_mounts; /* 子文件系统链表 */
    struct list_head mnt_child; /* 子文件系统链表 */
    /**
     * 挂载标志
     * MNT_NOSUID：禁止该文件系统的可执行文件设置setuid、setgid
     * MNT_MODEV：禁止访问该文件系统上的设备文件
     * MNT_NOEXEC：禁止执行可执行文件
     * 
     * 这些标志在应对不被信任的移动设备时有意义
     */
    int mnt_flags;
    char *mnt_devname; /* 设备文件名称 */
    struct list_head mnt_list; /* 描述符链表 */
    struct list_head mnt_expire; /* entry in expiry list */
    struct list_head mnt_share; /* entry in shared mounts list */
    struct list_head mnt_slave_list; /* list of slave mounts */
    struct list_head mnt_slave; /* entry in slave list */
    struct vfsmount *mnt_master; /* slave's master */
    struct mnt_namespace *mnt_namespace; /* 管理的命名空间 */
    int mnt_id; /* 挂载标识符 */
    int mnt_group_id; /* peer group identifier */
    atomic_t mnt_count; /* 使用计数t */
    int mnt_expiry_mark; /* is marked for expiration */
    int mnt_pinned; /* pinned count */
    int mnt_ghosts; /* ghosts count */
    atomic_t __mnt_writers; /* 写者引用计数 */
};</pre>
<div class="blog_h2"><span class="graybg">和进程相关的数据结构</span></div>
<p>系统中的每个进程都有自己的打开文件列表、根文件系统、当前工作目录、挂载点…等等。有三个数据结构把VFS层和系统中的进程联系在一起：files_struct、fs_struct和mnt_namespace。</p>
<p>一般的，每个进程具有自己独特的files_struct、fs_struct，但是使用<pre class="crayon-plain-tag">CLONE_FILES</pre>  或者<pre class="crayon-plain-tag">CLONE_FS</pre> 创建进程的话，将和父进程共享这两个结构。这种情况下结构的count字段用来做引用计数，防止仍然有进程使用这些结构时却销毁了它们。</p>
<p>默认的，所有进程共享同一个mnt_namespace，除非<pre class="crayon-plain-tag">clone()</pre> 进程时使用<pre class="crayon-plain-tag">CLONE_NEWNS</pre> 标记。</p>
<div class="blog_h3"><span class="graybg">files_struct</span></div>
<p>进程描述符<pre class="crayon-plain-tag">task_struct</pre> 的<pre class="crayon-plain-tag">struct files_struct *files</pre> 字段指向了该结构，进程所有关于打开文件、文件描述符的信息存放在其中：</p>
<pre class="crayon-plain-tag">struct files_struct
    {
        atomic_t count; /* 使用计数 */
        struct fdtable *fdt; /* 指向其他文件描述符表 */
        struct fdtable fdtab; /* base fd table */
        spinlock_t file_lock; /* per-file lock */
        int next_fd; /* cache of next available fd */
        struct embedded_fd_set close_on_exec_init; /* list of close-on-exec fds */
        struct embedded_fd_set open_fds_init;
        /**
         * 文件描述符数组，NR_OPEN_DEFAULT等于BITS_PER_LONG，在64位系统中是64，这意味着该数组可以存放64个对象
         * 如果进程打开超过64个文件，内核会创建新的数组存放之，并让fdt指向该数组
         */
        struct file *fd_array[NR_OPEN_DEFAULT];
    };</pre>
<div class="blog_h3"><span class="graybg">fs_struct</span></div>
<p>进程描述符的<pre class="crayon-plain-tag">struct fs_struct *fs</pre> 字段指向该结构，包含了进程相关的文件系统的信息：</p>
<pre class="crayon-plain-tag">struct fs_struct
{
    int users; /* 用户计数 */
    rwlock_t lock; /* 读写锁 */
    int umask; /* umask */
    int in_exec; /* currently executing a file */
    struct path root; /* 进程的根目录 */
    struct path pwd; /* 进程的当前工作目录，即$PWD */
};</pre>
<div class="blog_h3"><span class="graybg">mnt_namespace</span></div>
<p>进程描述符的<pre class="crayon-plain-tag">mnt_namespace</pre> 字段指向该结构，Per-process的挂载名字空间在2.4版本加入到内核，它允许<span style="background-color: #c0c0c0;">每个进程看到已挂载到系统的文件系统的独特视图</span>，而不仅仅是独特的根目录。该结构的定义如下：</p>
<pre class="crayon-plain-tag">struct mnt_namespace
{
    atomic_t count; /* usage count */
    struct vfsmount *root; /* 根目录 */
    struct list_head list; /* 构成此名字空间的已挂载文件系统的链表 */
    wait_queue_head_t poll; /* polling waitqueue */
    int event; /* event count */
};</pre>
<div class="blog_h1"><span class="graybg">块I/O层</span></div>
<p>根据是否支持<span style="background-color: #c0c0c0;">随机访问</span>（<span style="background-color: #c0c0c0;">不需要按照特定的顺</span>序，或者说<span style="background-color: #c0c0c0;">支持seek操作</span>），设备可以分为两类：</p>
<ol>
<li>块设备（Block device）：可以<span style="background-color: #c0c0c0;">随机</span>的存取<span style="background-color: #c0c0c0;">固定大小（称为chunk）</span>的数据，这些chunk也称为<span style="background-color: #c0c0c0;">块（block）</span>。这类设备包括硬盘、软驱、光驱、U盘等。块设备一般都通过在其上<span style="background-color: #c0c0c0;">建立文件系统</span>，并<span style="background-color: #c0c0c0;">挂载</span>到系统中使用</li>
<li>字符设备（Character device）：必须以<span style="background-color: #c0c0c0;">流（Stream）的形式顺序</span>的、<span style="background-color: #c0c0c0;">一个字节一个字节</span>的访问，当不被使用时，字符设备中的流是空的。这类设备包括串口、键盘等</li>
</ol>
<p>对于内核来说，块设备的管理要比字符设备复杂的多：</p>
<ol>
<li>内核设备只需要控制字符设备的一个位置——当前位置；而块设备的访问需要支持在介质中前后移动</li>
<li>块设备对执行性能的要求很高，块设备的复杂性也为性能优化提供了空间</li>
</ol>
<p>因此内核包含专门管理块设备的子系统，这就是块I/O层</p>
<div class="blog_h2"><span class="graybg">剖析一个块设备</span></div>
<p>块设备中<span style="background-color: #c0c0c0;">最小的可寻址单元</span>是<span style="background-color: #c0c0c0;">扇区（sector）</span>，又称为<span style="background-color: #c0c0c0;">硬扇区、设备块</span>，其的大小是<span style="background-color: #c0c0c0;">2的N次方（常见512B</span>）。扇区是所有<span style="background-color: #c0c0c0;">块设备的基本单元</span>，属于<span style="background-color: #c0c0c0;">物理属性</span>，<span style="background-color: #c0c0c0;">块设备无法对比扇区更小的单元进行寻址和操作</span>。</p>
<p>文件系统依据自己的需要，会设置自己的<span style="background-color: #c0c0c0;">最小逻辑可寻址单元——块（block）</span>，又称为<span style="background-color: #c0c0c0;">文件块、I/O块</span>，（内核）基于文件系统访问，只能以块为最小单位。内核还对块大小提出更严格的要求：</p>
<ol>
<li>必须是扇区大小的整数倍</li>
<li>必须是2的N次方</li>
<li>必须不大于页</li>
</ol>
<p>因此块的大小常常是512B、1KB、4KB。</p>
<p>对于最常见的块设备——硬盘，还有一些常见的概念：</p>
<ol>
<li>盘片（Platter）：典型的硬盘由多个盘片堆叠而成</li>
<li>磁头（Head）：每个盘片通常分配一个磁头，由于读取数据</li>
<li>簇（Cluster）：文件系统能分配给文件的最小数据单元，由连续的扇区构成。这个概念主要是DOS使用，类似于块</li>
<li>磁道（Track）：盘片上的一个圆周</li>
<li>柱面（Cylinder）：所有盘片上等半径的磁盘形成的圆柱</li>
<li>碎片化（fragmentation）：文件分散在物理上不连续的簇中的情况</li>
</ol>
<p>硬盘结构如下图所示：</p>
<p><img class="aligncenter size-full wp-image-9048" src="https://blog.gmem.cc/wp-content/uploads/2011/01/harddisk.png" alt="harddisk" width="336" height="249" /></p>
<div class="blog_h2"><span class="graybg">缓冲区和缓冲区头</span></div>
<p>当一个块被<span style="background-color: #c0c0c0;">调入内存时（读入后，写出前）</span>，它存储在一个缓冲区中，每个<span style="background-color: #c0c0c0;">缓冲区对应一个块</span>，缓冲区相当于块的内存表示，<span style="background-color: #c0c0c0;">一个内存页可以容纳1-N个块</span>。内核在处理缓冲区时，需要额外的控制信息（块属于哪个设备、块对应哪个缓冲区），因此每个缓冲区都关联了一个<span style="background-color: #c0c0c0;">缓冲区头</span>对象，用来<span style="background-color: #c0c0c0;">描述磁盘块与物理内存缓冲区（特定页上的字节序列）的关系</span>，缓冲区头用下面的结构表示：</p>
<pre class="crayon-plain-tag">struct buffer_head
{
    unsigned long b_state; /* 缓冲区状态标志 */
    struct buffer_head *b_this_page; /* 当前页的缓冲区的列表 */
    struct page *b_page; /* 与缓冲区对应的内存物理页 */
    sector_t b_blocknr; /* 与缓冲区对应的磁盘物理块索引号号 */
    size_t b_size; /* 块的大小 */
    char *b_data; /* 指向页中数据块其实位置的指针，此缓冲区的结束位置即此其实位置+b_size */
    struct block_device *b_bdev; /* 关联的块设备 */
    bh_end_io_t *b_end_io; /* I/O completion */
    void *b_private; /* reserved for b_end_io */
    struct list_head b_assoc_buffers; /* 关联的映射链表 */
    struct address_space *b_assoc_map; /* 关联的地址空间 */
    atomic_t b_count; /* 缓冲区使用计数，在操控缓冲区头前，应当使用get_bh()来增加计数，以防止缓冲区头不被再次分配，操控完毕后欧则调用put_bh()减少计数 */
};</pre>
<p>其中<pre class="crayon-plain-tag">b_state</pre> 表示缓冲区的状态，可以是一系列值的组合，这些值枚举定义在：</p>
<pre class="crayon-plain-tag">enum bh_state_bits
{
    BH_Uptodate, /* 包含有效的数据 */
    BH_Dirty, /* 缓冲区是脏的，其内容比磁盘中的块新，必须回写 */
    BH_Lock, /* 缓冲区正在被I/O操作使用，并被锁定以防止并发访问 */
    BH_Req, /* 已经提交I/O操作请求 */
    BH_Uptodate_Lock,/* Used by the first bh in a page, to serialise IO completion of other buffers in the page */
    BH_Mapped, /* 缓冲区映射到了一个磁盘块 */
    BH_New, /* 缓冲区刚刚由get_block()映射到磁盘块，且尚未访问 */
    BH_Async_Read, /* 正在通过end_buffer_async_read()进行异步读 */
    BH_Async_Write, /* 正在通过end_buffer_async_write()进行异步写  */
    BH_Delay, /* 缓冲区尚未在磁盘上分配（延迟分配） */
    BH_Boundary, /* 此缓冲区是一系列连续块的边界，下一个块不再连续 */
    BH_Write_EIO, /*此缓冲区在写操作上遭遇错误 */
    BH_Ordered, /* 顺序写 */
    BH_Eopnotsupp, /* 缓冲区遭遇不支持（not supported）错误 */
    BH_Unwritten, /* 缓冲区对应的磁盘空间已经分配，但是数据尚未写出 */
    BH_Quiet, /* 忽略此缓冲区上的错误 */

    BH_PrivateStart, /*这不是一个可用的状态位，块I/O子系统不会使用比该标志更高的位，因此其它实体（例如驱动）可以安全的使用高位 */
};</pre>
<p>在2.6以前的内核，缓冲区头的作用更加重要，缓冲区头作为<span style="background-color: #c0c0c0;">内核中I/O操作单元</span>——缓冲区头<span style="background-color: #c0c0c0;">不仅仅描述映射，还是I/O操作的容器</span>。将缓冲区头作为I/O操作单元有两个弊端：</p>
<ol>
<li>缓冲区头是个很大、不易控制的结构体。对于内核来说它更倾向于操控页面结构，简便而高效，使用一个巨大的缓冲区头表示每一个独立的缓冲区效率低下。因此，在2.6中，许多I/O操作都是通过内核<span style="background-color: #c0c0c0;">直接操作页面或者地址空间</span>来完成，不再使用缓冲区头</li>
<li>缓冲区头只能描述单个缓冲区，当作为所有I/O操作的容器使用时，缓冲区头迫使内核将（潜在的）<span style="background-color: #c0c0c0;">大块的I/O操作分解为针对多个缓冲区头</span>的操作，这导致不必要的负担和空间浪费。为解决此问题，2.5版本引入新型、轻量级的容器——bio结构</li>
</ol>
<div class="blog_h2"><span class="graybg">bio结构体</span></div>
<p>当前版本内核中，使用bio作为块I/O操作的基本容器，该结构将<span style="background-color: #c0c0c0;">正在进行的（活动的）I/O操作</span>表示为<span style="background-color: #c0c0c0;">片段（Segment）的数组</span>。每个片段是内存中<span style="background-color: #c0c0c0;">连续的</span>一小块（chunk）。bio允许内核从<span style="background-color: #c0c0c0;">多个内存位置</span>针对<span style="background-color: #c0c0c0;">单个缓冲区</span>进行块I/O操作——这样的向量I/O（Vector I/O）称为分散-聚集I/O（Scatter-Gather I/O）。bio的结构如下：</p>
<pre class="crayon-plain-tag">struct bio
{
    sector_t bi_sector; /* 关联的磁盘扇区 */
    struct bio *bi_next; /* 请求的链表 */
    struct block_device *bi_bdev; /* 管理的块设备 */
    unsigned long bi_flags; /* 状态和命令标志 */
    unsigned long bi_rw; /* 区分读还是写 */
    unsigned short bi_vcnt; /* 片段总数，即bi_io_vec数组的长度*/
    /* bi_io_vec的当前索引，当块I/O层开始指向请求时，此字段会不断更新，总是指向当前的片段。
     * 该字段用于跟踪I/O操作的完成进度  */
    unsigned short bi_idx;
    unsigned short bi_phys_segments; /* 物理片段的数目 */
    unsigned int bi_size; /* I/O 计数 */
    unsigned int bi_seg_front_size; /* 第一个片段的大小 */
    unsigned int bi_seg_back_size; /* 最后一个片段的大小 */
    unsigned int bi_max_vecs; /* bio_vecs数目上限 */
    unsigned int bi_comp_cpu; /* completion CPU */
    /* 使用计数，如果为0则该bio应该被撤销并释放内存，
     * 通过bio_get/bio_put函数可以管理计数 */
    atomic_t bi_cnt;
    /* 片段数组，该字段指向第一个片段，后续片段依次存放，共计bi_vcnt个片段 */
    struct bio_vec *bi_io_vec;
    bio_end_io_t *bi_end_io; /* I/O completion method */
    void *bi_private; /* bio结构创建者的私有域，只有创建者才能使用该字段 */
    bio_destructor_t *bi_destructor; /* destructor method */
    struct bio_vec bi_inline_vecs[0]; /* inline bio vectors */
};</pre>
<p>下图反应了bio、bi_io_vec、page的关系：</p>
<p><img class="aligncenter size-full wp-image-9056" src="https://blog.gmem.cc/wp-content/uploads/2011/01/struct_bio.png" alt="struct_bio" width="568" height="474" /></p>
<p><pre class="crayon-plain-tag">bi_io_vec</pre> 字段指向<pre class="crayon-plain-tag">bio_vec</pre> 的数组，该数组包含了特定I/O操作所<span style="background-color: #c0c0c0;">需要的全部片段，构成了完整的缓冲区</span>。<pre class="crayon-plain-tag">bio_vec</pre> 的结构如下：</p>
<pre class="crayon-plain-tag">//该结构描述一个特定的片段
struct bio_vec
{
    /* 指向当前片段所驻留的内存页 */
    struct page *bv_page;
    /* 当前片段的长度 */
    unsigned int bv_len;
    /* 当前片段在页内的偏移量 */
    unsigned int bv_offset;
};</pre>
<div class="blog_h3"><span class="graybg">与buffer_head的对比</span></div>
<p>新的bio结构和缓冲区头存在显著差别：</p>
<ol>
<li><span style="background-color: #c0c0c0;">bio代表的是I/O操作</span>，bio是轻量级的，它描述的（可能多个）块不需要连续的存储区</li>
<li><span style="background-color: #c0c0c0;">buffer_head代表的是一个缓冲区</span>，它仅仅描述磁盘中的一个块，当需要对多个块进行I/O操作时，会导致不必要的分割</li>
</ol>
<p>用bio代替buffer_head可以获得以下好处：</p>
<ol>
<li>bio可以很容易的处理高端内存，因为它处理的是物理页而不是直接指针</li>
<li>bio既可代表<span style="background-color: #c0c0c0;">普通页I/O</span>，页可以代表<span style="background-color: #c0c0c0;">直接I/O（不通过页高速缓存的I/O操作）</span></li>
<li>bio结构便于执行分散-聚集的块I/O操作，操作的数据可以来自多个物理页面</li>
</ol>
<p>尽管如此，buffer_head这个概念还是需要的，毕竟它还负责<span style="background-color: #c0c0c0;">磁盘块到页的映射</span>。</p>
<div class="blog_h2"><span class="graybg">请求队列</span></div>
<p>块设备维护一个请求队列，以存储挂起（Pending）的块I/O请求：</p>
<pre class="crayon-plain-tag">struct request_queue
{
    struct list_head queue_head; //请求的链表头
};</pre>
<p>通过内核中文件系统之类的高层代码，I/O请求被加入到队列中，只要队列不为空，对应块设备的驱动程序就会从队列头获取请求，然后将其送入对应的块设备中。请求队列中的每一项表示一个请求，使用<pre class="crayon-plain-tag">request</pre> 结构表示，由于一个请求可能需要操控<span style="background-color: #c0c0c0;">多个连续的磁盘块</span>，因此每个请求可以由多个bio结构体组成：</p>
<pre class="crayon-plain-tag">struct request
{
    struct request_queue *q; //指向请求队列
    struct bio *bio; //每个队列请求包含一个或者多个bio结构，这里指向第一个
    struct bio *biotail;//最后一个bio
};</pre>
<div class="blog_h2"><span class="graybg">I/O调度程序</span></div>
<p>如果简单的按照内核产生请求的次序直接将请求发送给块设备，性能会很差，这是因为<span style="background-color: #c0c0c0;">磁盘寻址</span>是计算机中<span style="background-color: #c0c0c0;">最慢</span>的操作之一，每一次寻址（即将磁头定位到某个特定的扇区）都会消耗不少时间，减少寻道次数是提供I/O性能的关键。</p>
<p>为优化寻址操作（降低寻址总消耗时间），内核既<span style="background-color: #c0c0c0;">不会简单的依据请求发生顺序</span>发送、也<span style="background-color: #c0c0c0;">不会立即</span>发送请求给磁盘。相反：</p>
<ol>
<li>在正式提交给磁盘前，内核会进行称为<span style="background-color: #c0c0c0;">合并与排序</span>的预操作，此操作可以极大的提升I/O性能
<ol>
<li>合并：将两个或者多个请求合并为一个新请求。例如如果请求B和请求A访问的磁盘扇区相邻，那么I/O调度器就可以将其合并为一个请求，这样只需要一条寻址指令、并把两次请求处理的开销压缩为一次</li>
<li>排序：让请求按照扇区增长的方向顺序排列，以尽量保持磁头单向移动，减少总和寻址时间。这种算法类似于电梯，电梯不会在楼层之间上下移动，它总是抵达同一方向的最后一次后，再折返，因此I/O调度又称<span style="background-color: #c0c0c0;">电梯调度</span></li>
</ol>
</li>
<li>内核会决定何时向磁盘提交请求</li>
</ol>
<p>负责执行上面两个规则的子系统叫做I/O调度程序，其核心目的就是优化寻址以<span style="background-color: #c0c0c0;">提高全局吞吐量</span>（注意这可能导致对某些请求不公），它会服务<span style="background-color: #c0c0c0;">所有挂起</span>的请求，而不是向进程调度程序那样把资源分配给单个请求者。</p>
<div class="blog_h3"><span class="graybg">Linus电梯</span></div>
<p>这是2.4内核的默认调度程序，在2.6被其它两个算法代替。该算法相对简单，便于理解。</p>
<p>Linus电梯能够执行合并排序预处理：</p>
<ol>
<li>当新请求加入队列时，它会判断新请求是否能和任一个挂起的请求合并。Linus电梯同时进行<span style="background-color: #c0c0c0;">向前合并</span>（新请求直接位于既有请求的前面）、<span style="background-color: #c0c0c0;">向后合并</span>（新请求直接位于一个既有请求后面）两种合并类型。由于文件的布局方式（通常是增长扇区号）和I/O操作的典型特征（一般都是从头向尾读），向前合并要少见的多，尽管如此Linus电梯同时检查这两种合并类型。对于不能合并的请求，可能执行下面三类操作：</li>
<li>如果队列中存在<span style="background-color: #c0c0c0;">驻留时间过长</span>的请求，那么新请求将被插入队列尾部，防止其它旧的<span style="background-color: #c0c0c0;">请求饥饿</span>：如果访问某个相近磁盘位置的请求过多，将导致访问其它磁盘位置的请求得不到执行机会</li>
<li>如果队列中存在合适的插入位置，新请求被插入，尽量保证顺序I/O</li>
<li>如果队列中不存在合适的插入位置，那么新请求被插入到队列尾部</li>
</ol>
<div class="blog_h3"><span class="graybg">最终期限（deadline）的I/O调度程序</span></div>
<p>Linus电梯防止饥饿的策略不是很有效，虽然改善了等待时间，但是还会导致请求饥饿现象的发生。特别的，一个对同一磁盘位置的持续请求流可能导致对较远磁盘位置的请求永远得不到执行机会。</p>
<p>一个更糟糕的情况是，普通请求饥饿还会带来一个特殊问题：<span style="background-color: #c0c0c0;">写使读挨饿（writes starving reads）</span>问题。写操作通常是在内核空闲时才提交给磁盘的，写操作常常和提交它的应用程序异步执行；而读操作则相反，通常一个应用程序提交读请求时，都需要阻塞直到读请求被满足，也就是说读操作常常和提交者同步执行。读请求的响应时间对应用程序非常重要，因此WSR问题比较严重。此问题可能进一步复杂化。读请求往往倾向于依赖于其它读请求，考虑一个读取一大批文件并逐行处理的场景，每个读请求都处理一小块数据，前一个读请求处理完毕前，程序可能不会读下一块数据（或者下一个文件）。</p>
<p>此外，不管是读还是写请求，都需要读取一系列的文件元数据（例如inode），读取这些块进一步的串行化了I/O操作。</p>
<p>饥饿问题是2.4内核I/O调度程序必须修改的缺陷，最终期限的调度器因而引入。该调度器致力于<span style="background-color: #c0c0c0;">减少请求饥饿现象，特别是读请求饥饿</span>。需要注意的是，减少请求饥饿必然是 <span style="background-color: #c0c0c0;">以降低全局吞吐量为代价</span>的。</p>
<p>在dealine调度程序中，每个请求具有一个超时时间：默认读请求500ms，写请求5s，这可以防止写使读挨饿，有限照顾了读请求。类似于Linus电梯，该调度器也按磁盘物理位置为次序排列维护请求队列，并称其为<span style="background-color: #c0c0c0;">排序队列</span>。该调度器的合并排序行为类似于Linus电梯，但是它会根据请求类型，将其插入到<span style="background-color: #c0c0c0;">额外队列中</span>：<span style="background-color: #c0c0c0;">读请求按次序插入</span>读FIFO队列；写请求插入写FIFO队列。</p>
<p>Deadline调度器以类似Linus电梯的方式操控排序队列，取出请求分发给设备。但是，<span style="background-color: #c0c0c0;">当FIFO队列头请求超时时</span>，它会立即从FIFO队列取出请求进行服务。这样，就避免了请求明显超期仍得不到服务的饥饿现象（但不能严格保证请求的响应时间）。此工作方式如下图所示：</p>
<p><img class="aligncenter size-full wp-image-9071" src="https://blog.gmem.cc/wp-content/uploads/2011/01/deadline-io-sche.png" alt="deadline-io-sche" width="476" height="140" /></p>
<p>&nbsp;</p>
<p>最后期限调度程序的实现代码位于<pre class="crayon-plain-tag">block/deadline-iosched.c</pre> 中。</p>
<div class="blog_h3"><span class="graybg">预测I/O调度程序</span></div>
<p>最后期限调度程序为了降低读操作的响应时间做了很多工作，但是它降低了系统的吞吐量。预测调度器的目标是，在<span style="background-color: #c0c0c0;">保持良好读响应时间的同时，提供良好的全局吞吐量</span>。</p>
<p>预测调度器在最后期限调度器的基础上改进，同样由三个队列+分发队列，同样设置请求的超时时间，不同的是它具有<span style="background-color: #c0c0c0;">预测启发</span>能力：</p>
<ol>
<li>当调度器提交超时读请求后，不会立即返回排序队列，而是会等待一小段时间（默认6ms，可配置），这段时间应用程序如果提交新的、相邻位置的请求，会得到立即处理。等待时间结束后，调度程序返回原来位置继续执行。如果等待的时间可以减少back-and-forth寻址操作，那么这6ms是值得的，特别是连续访问同样区域的读请求到来，将避免大量的寻址操作</li>
<li>上面的等待，是否有意义，取决于能否正确预测应用程序和文件系统的行为。这种预测依赖于一系列的启发和统计工作。预测调度器会跟踪每一个应用程序块I/O的习惯行为，以便正确预测其未来行为。如果预测正确，则既降低响应时间，也提供吞吐量</li>
</ol>
<p>预测调度器的代码位于<pre class="crayon-plain-tag">block/as-iosched.c</pre> ，它是缺省的I/O调度程序，对于大部分的工作负载来说，效果良好。</p>
<div class="blog_h3"><span class="graybg">完全公正的排队I/O调度程序 </span></div>
<p>CFQ调度器是为了专用工作负载而设计，但是实际应用中为其它工作负载也提供了良好的性能。它与前面的调度器有着根本的不同。该调度器把I/O请求放入特定队列中，这种队列按照引起I/O请求的进程来组织（例如每个进程一个队列），当新请求进入队列时，会发生合并排序。</p>
<p>CFQ调度器以时间片轮转的方式调度队列，从每个队列选取一定个数的请求（默认4），然后进行下一轮调度。这在进程级提供了公平。</p>
<p>CFQ调度器默认的工作负载是<span style="background-color: #c0c0c0;">多媒体环境</span>，该环境下公平性需要保证，例如音频播放器总是能够及时的填满自己的音频缓冲区。尽管主要<span style="background-color: #c0c0c0;">推荐给桌面工作负载</span>使用，CFQ在很多其它场景下亦工作良好。该调度器的代码位于<pre class="crayon-plain-tag">block/cfq-iosched.c</pre> 。</p>
<div class="blog_h3"><span class="graybg">空操作的I/O调度程序 </span></div>
<p>该调度器不做多少事情，它只会进行请求合并，然后维护近乎FIFO的顺序来处理请求。该调度器<span style="background-color: #c0c0c0;">用于支持真正随机访问的块设备，例如SSD</span>，这类设备没有或者<span style="background-color: #c0c0c0;">仅有一点“寻道”的负担</span>，因而没有必要进行插入排序。空操作调度器的代码位于<pre class="crayon-plain-tag">block/noop-iosched.c</pre> </p>
<div class="blog_h3"><span class="graybg">I/O调度程序的选择 </span></div>
<p>可以使用内核命令行选项<pre class="crayon-plain-tag">elevator=name</pre> 来覆盖缺省的I/O调度程序。四种调度程序的名字分别为：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">名称</td>
<td style="text-align: center;">调度器 </td>
</tr>
</thead>
<tbody>
<tr>
<td>as</td>
<td>预测I/O调度程序</td>
</tr>
<tr>
<td>cfq</td>
<td>完全公平I/O调度器程序</td>
</tr>
<tr>
<td>deadline</td>
<td>最后期限I/O调度程序</td>
</tr>
<tr>
<td>noop</td>
<td>空操作调度程序</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">设备与模块</span></div>
<div class="blog_h2"><span class="graybg">设备类型</span></div>
<p>在Linux和所有Unix系统中，设备分为三类：</p>
<ol>
<li>块设备：通常缩写为blkdev，支持以块为单位寻址，块的大小取决于设备。块设备通常支持重定位（seeking）操作，即随机访问。块设备通过特殊文件“块设备节点（block device node）”访问，通常挂在为文件系统</li>
<li>字符设备：通常缩写为cdevs，一般不支持寻址，仅支持以流的方式、以字符为单位（byte）访问数据。字符设备通过特殊文件“字符设备节点（character device node）”访问，与块设备通过文件系统访问不同，应用程序直接<span style="background-color: #c0c0c0;">通过设备节点与字符设备交互</span></li>
<li>网络设备： 网络设备打破了万物皆文件的设计原则，通过专门的Socket API访问。它允许通过网卡、利用某种协议来访问网络</li>
</ol>
<p>Linux还提供一系列不常用的其它设备类型。一个特例是所谓“杂项设备（miscellaneous device，miscdev）”，杂项设备实际上是一个简化的字符设备，允许驱动程序很简单的表示一种简单设备。</p>
<p><span style="background-color: #c0c0c0;">并非所有设备驱动代表了物理设备</span>，有些时候设备是“虚拟”的，我们称其为<span style="background-color: #c0c0c0;">伪设备（pseudo devices）</span>，伪设备用于访问内核功能，常见的包括：</p>
<ol>
<li>内核随机数发生器，可以通过/dev/random、/dev/urandom访问</li>
<li>空设备：/dev/null</li>
<li>零设备：/dev/zero</li>
<li>内存设备：/dev/mem</li>
</ol>
<div class="blog_h2"><span class="graybg">模块</span></div>
<p>尽管Linux是<span style="background-color: #c0c0c0;">单内核（monolithic）</span>操作系统——整个内核运行在<span style="background-color: #c0c0c0;">一个单独的地址空间中</span>、子系统仅仅是逻辑上的划分。它同时却是模块化的，支持在运行时<span style="background-color: #c0c0c0;">动态的插入、移除</span>自身的代码。需要动态加载的模块的典型例子就是驱动程序，很多传统Unix做不到模块化。</p>
<p>这些代码——相关的子例程、数据、入口点和退出点被合并到单独的二进制镜像中，称为“模块”。支持模块机制可以让内核尽可能的小，而<span style="background-color: #c0c0c0;">可选功能、驱动程序</span>可以利用模块提供。</p>
<p>模块化使调试变得方便，而且支持在热拔插设备时通过命令加载新的驱动程序。</p>
<div class="blog_h3"><span class="graybg">Hello World</span></div>
<p>下面的代码示例了最简的内核模块：</p>
<pre class="crayon-plain-tag">#include &lt;linux/init.h&gt;
#include &lt;linux/module.h&gt;
#include &lt;linux/kernel.h&gt;
/*
 * 入口点函数，模块加载时调用
 * 
 * 真实环境中的模块可能需要在这个函数中完成：
 * 1、注册资源
 * 2、初始化硬件
 * 3、分配数据结构
 * 
 * 如果当前源文件被编译入内核映像，那么入口点函数将在内核启动时运行
 */
static int hello_init( void )
{
    printk( KERN_ALERT "Hello Kernel!\n" );
    /**

     */
    return 0;
}
/*
 * 退出点函数，模块卸载时调用
 * 在真实环境中，可能需要进行资源清理，确保硬件处于一致性状态
 * 
 * 如果当前源文件被编译入内核映像，那么退出函数将不被包含，也永远不会被调用
 */
static void hello_exit( void )
{
    printk( KERN_ALERT "Bye!\n" );
}
module_init( hello_init ); //该宏负责注册模块初始化函数，其唯一参数是模块的初始化函数
module_exit( hello_exit );//该宏负责注册模块退出函数，其唯一参数是模块的退出函数
/**
 * 如果非GPL模块被载入内存，那么内核中会设置一个被污染标识
 * 非GPL模块不能调用GPL_only符号
 */
MODULE_LICENSE( "GPL" ); 
MODULE_AUTHOR( "Alex" );
MODULE_DESCRIPTION( "A Hello, Kernel Module" );</pre>
<div class="blog_h3"><span class="graybg">构建模块</span></div>
<p>基于2.6的kbuild构建系统，模块的构建更加简单，第一步需要决定在何处管理模块源码，你可以：</p>
<ol>
<li>把模块源码加入内核源代码树中</li>
<li>作为一个补丁，并最终合并到内核源代码树</li>
<li>在源代码树之外独立维护模块源码</li>
</ol>
<p><strong><span style="text-decoration: underline;">在源代码树树中维护模块</span></strong></p>
<p>这种方式最理想，虽然开始时候需要更多的维护。新开发的设备驱动一般放在<pre class="crayon-plain-tag">drivers/</pre> 目录下，并根据具体设备的类型进一步组织。例如USB设备驱动可以放在usb子目录，但是USB设备也可以是字符设备，因此存放在char目录也无可厚非。如果你的<span style="background-color: #c0c0c0;">模块有很多文件，最好建立目录</span>进行管理。</p>
<p>假设需要开发一个USB网卡的驱动，可以建立目录drivers/usb/mywifi，并在上级目录drivers/usb的Makefile中添加：</p>
<pre class="crayon-plain-tag">#该指令通知构建系统，在编译模块时需要进入mywifi子目录
obj-m += mywifi/</pre>
<p>或者更好的，使用特殊配置选项控制驱动程序的编译：</p>
<pre class="crayon-plain-tag">obj-$(CONFIG_USB_MYWIFI) += mywifi/</pre>
<p>然后，修改mywifi目录的Makefile：</p>
<pre class="crayon-plain-tag">obj-m += mywifi.o
#如果使用编译选项，则使用：
obj-$(CONFIG_USB_MYWIFI) +=  mywifi.o</pre>
<p>这样，构建系统就会编译mywifi/mywifi.c，将其编译为mywifi.ko模块（注意模块编译后的扩展名自动为ko）。 </p>
<p>如果模块包含多个源文件，则可以添加： </p>
<pre class="crayon-plain-tag">mywifi-objs := mywifi-main.o mywifi-sec.o</pre>
<p>如果需要额外的编译标记，可以在Makefile中添加：</p>
<pre class="crayon-plain-tag">EXTRA_CFLAGS += -DMYWIFI_DEBUG</pre>
<p>如果不建立独立目录，那么只需要把上面mywifi/Makefile中的内容存放在上级目录的Makefile中即可。 </p>
<p><span style="text-decoration: underline;"><strong>在内核代码外独立维护模块</strong></span></p>
<p>Makefile和上一种方式是类似的：</p>
<pre class="crayon-plain-tag">obj-m += mywifi.o
mywifi-objs := mywifi-main.o mywifi-sec.o</pre>
<p>区别主要在于如何构建，必须告知make如何找到内核源代码目录：</p>
<pre class="crayon-plain-tag">make -C /kernel/source/location SUBDIRS=$PWD modules
#location是已经配置好的内核源代码树所在目录，注意不要使用/usr/src/linux中的源代码，最好复制一份放在别的地方</pre>
<div class="blog_h3"><span class="graybg">安装模块</span></div>
<p>编译后的模块被装入<pre class="crayon-plain-tag">/lib/modules/内核版本/kernel </pre> 目录，该目录的每一个子目录都<span style="background-color: #c0c0c0;">对应了内核源码树中的模块位置</span>。</p>
<p>使用下面的命令可以安装编译的模块到合适的目录中：</p>
<pre class="crayon-plain-tag">make modules_install</pre>
<div class="blog_h3"><span class="graybg">生成模块依赖关系</span></div>
<p>Linux模块之间存在依赖性，载入模块时，被依赖模块会被自动载入。</p>
<p>模块之间的依赖信息必须实现生成，大多数发行版支持自动生成依赖关系，并在每次启动时更新。可以通过下面的命令手工生成内核依赖关系信息：</p>
<pre class="crayon-plain-tag">depmod
depmod -A #只为新模块生成依赖信息</pre>
<p>生成的模块依赖信息会存放在 <pre class="crayon-plain-tag">/lib/modules/内核版本/modules.dep</pre> 中。</p>
<div class="blog_h3"><span class="graybg">载入模块</span></div>
<p>使用下面的命令可以载入模块，该命令不会进行依赖分析或进一步错误检查：</p>
<pre class="crayon-plain-tag">insmod mywifi.ko
#类似的，可以卸载模块：
rmmod mywifi</pre>
<p>更智能的工具是modprobe，它提供模块依赖分析、错误智能检查、错误报告等功能：</p>
<pre class="crayon-plain-tag">#依赖的模块会被一并加载
modprobe module [ module parameters ]
#下面的命令用于移除模块：
modprobe –r modules #依赖的模块如果没有被使用，会一并卸载</pre>
<div class="blog_h3"><span class="graybg">管理配置选项</span></div>
<p>前面小节我们使用了配置选项CONFIG_USB_MYWIFI，只要该选项被配置，USB网卡模块就会被自动编译。2.6引入的kbuild系统让添加新配置选项很容易，只需要向kconfig中添加一项即可，该文件通常和源代码处于同一目录。如果你新建了子目录，并且使用独立的kconfig文件，那么必须在一个已经存在的kconfig文件中引用它：</p>
<pre class="crayon-plain-tag">source "drivers/usb/mywifi/Kconfig"</pre>
<p>配置选项可以参考下面的格式声明： </p>
<pre class="crayon-plain-tag">#第一行定义了配置选项，注意前缀CONFIG_不需要
config USB_MYWIFI
#tristate表示该模块可以编译入内核映像(Y)、作为模块编译(M)，或者根本不编译(N)
#如果模块作为内核特性而非驱动程序，使用bool代替tristate
#tristate/bool可以跟随 if NAME，如果CONFIG_NAME配置未指定，则当前配置不但被禁用，而且在配置工具中不可见
    tristate "此选项在内核配置工具中显示的名称"
    default n  #选项的默认值，可以是y m n，对于驱动一般默认n。后面也可接if
    help 帮助文本
#说明依赖的配置项，如果依赖配置项没有设置，当前选项自动禁用
#支持同时声明多个依赖，或者冲突排除，例如depends on MOD_DEP &amp;&amp; !CONFLICT_MOD，如果CONFIG_CONFLICT_MOD被设置，当前配置被禁用
    depends on MOD_DEP  
#自动开启的依赖配置，如果当前选项开启，依赖被强制开启，支持同时声明多个
    select BAIT</pre>
<p>配置系统导出了一些元配置，以简化配置文件，例如：</p>
<ol>
<li>CONFIG_EMBEDDED 用于关闭用户想要禁止的关键功能，用于资源非常紧缺的嵌入式环境</li>
<li>CONFIG_BROKEN_ON_SMP表示驱动程序不是多处理器安全的</li>
<li>CONFIG_EXPERIMENTAL表示某些功能尚处于试验阶段</li>
</ol>
<div class="blog_h3"><span class="graybg">模块参数</span></div>
<p>Linux允许模块声明参数，对于驱动程序来说，这些参数属于全局变量。模块参数会出现在sysfs文件系统中，便于灵活管理。在模块代码中可以通过下面的宏声明参数：</p>
<pre class="crayon-plain-tag">//name同时是模块变量名和暴露给用户的参数
//type是参数类型，支持byte, short, ushort, int, uint, long, ulong, charp, bool,invbool
//perm设置该参数对应sysfs文件系统中对应文件的访问权限
module_param(name, type, perm);
//下面的宏用于为参数设置文档
MODULE_PARM_DESC(name, "description");

//下面是一个例子
//变量必须在前面手工声明
static int allow_11g_mode = 1; //默认开启
module_param(allow_11g_mode , bool, 0644); //声明bool型参数

//如果要使参数名与内部变量名不同，可以使用下面的宏
module_param_named(name, variable, type, perm);
//示例
static unsigned int max_test = DEFAULT_MAX_LINE_TEST;
module_param_named(maximum_line_test, max_test, int, 0);

//指定字符串类型的参数
static char *name;
module_param(name, charp, 0);
//或者
static char species[BUF_LEN];
module_param_string(specifies, species, BUF_LEN, 0);</pre>
<p>所有相关的宏位于<pre class="crayon-plain-tag">linux/module.h</pre>  </p>
<div class="blog_h3"><span class="graybg">导出符号表</span></div>
<p>模块被加载后，被<span style="background-color: #c0c0c0;">动态的链接</span>到内核中，和用户空间的动态链接库类似，只有被显式导出的函数才能被模块调用。在内核中，可以使用特殊指令完成导出，导出的函数可以供模块使用。相比起内核镜像中的代码而言，模块代码的链接和调用规则更加严格，核心代码在内核中可以任意调用非static接口，因为所有核心代码被链接成同一个镜像。</p>
<p>导出的内核符号可以称为<span style="background-color: #c0c0c0;">“内核API”</span>，只需要在内核函数后添加宏声明即可：</p>
<pre class="crayon-plain-tag">unsigned long sport_curr_offset_rx(struct sport_device *sport)
{
	unsigned long curr = get_dma_curr_addr(sport-&gt;dma_rx_chan);
	return (unsigned char *)curr - sport-&gt;rx_buf;
}
EXPORT_SYMBOL(sport_curr_offset_rx);</pre>
<p>如果想让函数只对GPL协议模块可用，可以使用<pre class="crayon-plain-tag">EXPORT_SYMBOL_GPL()</pre>  </p>
<div class="blog_h2"><span class="graybg">设备模型</span></div>
<p>统一设备模型是2.6增加的一个新特性，设备模型提供了独立的机制专门用来管理设备，并描述其在系统中的拓扑结构，从而使系统：</p>
<ol>
<li>重复代码最小化</li>
<li>提供诸如引用计数这样的统一机制</li>
<li>可以列举系统中的所有设备，观察它们的状态，查看它们连接的总线</li>
<li>可以把全部设备的以树状展示，包括所有总线和内部连接</li>
<li>可以将设备和对应的驱动关联起来</li>
<li>可以把设备按类型分类</li>
<li>可以从设备树的叶子向根遍历，确保能以正确的顺序关闭各设备的电源</li>
</ol>
<p>上面最后一点正是引入设备模型的最初动机。</p>
<div class="blog_h3"><span class="graybg">kobject</span></div>
<p>该结构是设备模型的核心，很容易创建树形结构：</p>
<pre class="crayon-plain-tag">struct kobject
{
    const char *name; //内核对象的名称
    struct list_head entry; //当前对象在链表中的元素
    struct kobject *parent; //父对象的指针
    struct kset *kset;
    struct kobj_type *ktype; //内核对象类型
    struct sysfs_dirent *sd; //指向sysfs中代表当前对象的目录项
    struct kref kref; //引用计数
    unsigned int state_initialized :1;
    unsigned int state_in_sysfs :1;
    unsigned int state_add_uevent_sent :1;
    unsigned int state_remove_uevent_sent :1;
    unsigned int uevent_suppress :1;
};</pre>
<p> kobject通常嵌入到其它结构中，例如字符设备的定义：</p>
<pre class="crayon-plain-tag">struct cdev {
	struct kobject kobj; //对应的内核对象
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};</pre>
<p>当kobject被嵌入其它结构中后，后者就有了kobject提供的标准功能，更重要的时，后者可以称为对象层次结构中的一部分。 </p>
<div class="blog_h3"><span class="graybg">ktype</span></div>
<p>该结构用于表示kobject的类型，包含一类kobject公共的属性，避免逐个指定：</p>
<pre class="crayon-plain-tag">struct kobj_type
{
    //当此类kobject引用计数为0时需要调用的析构函数
    void (*release)( struct kobject *kobj );
    //描述sysfs读写时的特性
    const struct sysfs_ops *sysfs_ops;
    //定义此类kobject相关的默认属性，最后一项必须为NULL
    struct attribute **default_attrs;
};</pre>
<div class="blog_h3"><span class="graybg">kset</span></div>
<p>该结构定义了kobject的集合，可以把相关的kobject对象置于同一位置，具有相同ktype的koject可以被存放在不同的kset中：</p>
<pre class="crayon-plain-tag">struct kset
{
    struct list_head list;//集合中所有kobject的集合
    spinlock_t list_lock; //保护集合的自旋锁
    struct kobject kobj; //代表了该集合的基类
    /**
     * 指向一个结构体，用于处理集合中kobject对象的热拔插操作
     * uevent是用户事件的意思，提供了与用户空间热拔插信息进行通信的机制
     */
    struct kset_uevent_ops *uevent_ops; 
};</pre>
<div class="blog_h3"><span class="graybg">管理和操作kobject</span></div>
<p>尽管多数时候驱动开发人员不需要直接处理kobject，设备驱动子系统还是会使用到它。使用kobject的第一步是声明和初始化：</p>
<pre class="crayon-plain-tag">// @param kobj 需要初始化的内核对象，调用前kobj必须被清零，例如：memset(kobj, 0, sizeof (*kobj));
void kobject_init( struct kobject *kobj, struct kobj_type *ktype );

//示例
struct kobject *kobj;
kobj = kmalloc( sizeof ( *kobj ), GFP_KERNEL );
if (!kobj)
return -ENOMEM;
memset(kobj, 0, sizeof (*kobj));
kobj-&gt;kset = my_kset;
kobject_init( kobj, my_ktype );

//上面这些步骤可以直接用下面的函数完成
struct kobject * kobject_create(void);
//示例
struct kobject *kobj;
kobj = kobject_create();
if (!kobj)
return -ENOMEM;</pre>
<div class="blog_h3"><span class="graybg">引用计数</span></div>
<p>kobject引入的一个主要特性是统一的引用计数系统，在初始化后kobject的引用计数被设置为1，只要计数不为0则对象持续钉在（pinned）在内存中。任何引用kobject的代码都会应该手工增加计数，在不使用后则减少计数：</p>
<pre class="crayon-plain-tag">//增加引用计数并获得引用本身
struct kobject * kobject_get(struct kobject *kobj);
//减少引用计数
void kobject_put(struct kobject *kobj);</pre>
<p>一旦引用计数为0，定义在ktype上的析构函数就被调用，任何关联的内存被释放，kobject不复存在。</p>
<p>在引用计数系统内部，使用kref结构体，在内核中任何需要使用引用计数机制的地方，都可以使用该结构体：</p>
<pre class="crayon-plain-tag">struct kref
{
    atomic_t refcount;
};
//初始化引用计数为1
void kref_init( struct kref *kref )
{
    atomic_set( &amp;kref-&gt;refcount, 1 );
}
//增加引用计数
void kref_get( struct kref *kref )
{
    WARN_ON( !atomic_read( &amp;kref-&gt;refcount ) );
    atomic_inc( &amp;kref-&gt;refcount );
}
//减少引用计数，如果为零，则执行指定的回调
int kref_put( struct kref *kref, void (*release)( struct kref *kref ) )
{
    WARN_ON( release == NULL );
    WARN_ON( release == ( void (*)( struct kref * ) ) kfree );
    if ( atomic_dec_and_test( &amp;kref-&gt;refcount ) )
    {
        release( kref );
        return 1;
    }
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">sysfs</span></div>
<p>sysfs是一个内存中的虚拟文件系统，它提供了kobject的层次视图，允许用户使用类似文件系统的方式来观察设备的拓扑结构。利用attributes，kobject可以暴<span style="background-color: #c0c0c0;">露内核变量供读取或者（可选的）写入</span>。</p>
<p>尽管设备模型最初目的是支持电源管理，其衍生品sysfs被很快导出为文件系统，以支持调试。该文件系统替换了原先位于/proc的设备相关文件，并提供了系统对象的层次视图。大部分系统中，sysfs挂载在<pre class="crayon-plain-tag">/sys</pre> 下。</p>
<p>/sys至少包含10个子目录：block, bus, class, dev, devices,firmware, fs, kernel, module, power，其中最重要的是devices，它体现了系统真实的设备拓扑结构，很多其它目录中的文件都是指向该目录的符号链接，例如：</p>
<pre class="crayon-plain-tag">ll /sys/class/net
#输出
eth0 -&gt; ../../devices/pci0000:00/0000:00:11.0/0000:02:01.0/net/eth0/
eth1 -&gt; ../../devices/pci0000:00/0000:00:11.0/0000:02:06.0/net/eth1/
lo -&gt; ../../devices/virtual/net/lo/</pre>
<div class="blog_h3"><span class="graybg">从sysfs中添加/移除kobject</span></div>
<p>仅仅初始化kobject不会自动将其导出到sysfs中，必须调用：</p>
<pre class="crayon-plain-tag">int kobject_add( struct kobject *kobj, struct kobject *parent, const char *fmt, ... );</pre>
<p>kobject在sysfs中的位置取决于其在对象层次中的位置。如果父指针被设置，那么kobject将映射为其父目录的子目录；否则， 将被映射为<pre class="crayon-plain-tag">kset-kobj</pre> 中的子目录，如果kobject的parent、kset都没有设置，则映射为sysfs的直接子目录。fmt参数用于创建目录的名字，使用printf()函数的格式化字符串。</p>
<p>辅助函数把kobject的创建、添加到sysfs合并为一个步骤：</p>
<pre class="crayon-plain-tag">struct kobject * kobject_create_and_add( const char *name, struct kobject *parent );</pre>
<p>下面的函数用于从sysfs中移除kobject：</p>
<pre class="crayon-plain-tag">void kobject_del( struct kobject *kobj );</pre>
<div class="blog_h3"><span class="graybg">添加文件到sysfs</span></div>
<p>注意kobject映射到的是目录，因此仅仅能构成目录结构，不提供任何数据。</p>
<p><span style="text-decoration: underline;"><strong>默认属性</strong></span></p>
<p>kobject目录中包含的默认文件集合由kobject和kset的ktype字段提供，所有相同类型的kobject具有<span style="background-color: #c0c0c0;">相同的文件集合</span>。此集合由<pre class="crayon-plain-tag">kobj_type.default_attrs </pre> 提供，它是attribute结构的数组。这些属性负责把内核数据映射为sysfs中的文件：</p>
<pre class="crayon-plain-tag">struct attribute
{
    const char *name; /* 属性名，亦即文件名 */
    struct module *owner; /* 所属的模块 */
    mode_t mode; /* 文件的访问权限 */
};</pre>
<p>同时<pre class="crayon-plain-tag">kobj_type.sysfs_ops</pre> 定义了如何读写这些属性：</p>
<pre class="crayon-plain-tag">struct sysfs_ops
{
    /* 读取一个sysfs文件时调用的函数 */
    ssize_t (*show)( struct kobject *kobj, struct attribute *attr, char *buffer );
    /* 写入一个sysfs文件时调用的函数 */
    ssize_t (*store)( struct kobject *kobj, struct attribute *attr, const char *buffer, size_t size );
};</pre>
<p><span style="text-decoration: underline;"><strong>创建新属性</strong></span></p>
<p>如果某个特定的kobject实例需要特殊属性，可以调用：</p>
<pre class="crayon-plain-tag">int sysfs_create_file( struct kobject *kobj, const struct attribute *attr );</pre>
<p>注意，默认的sysfs_ops必须能够处理新添加的属性。</p>
<p>除了添加属性外，可能还需要在sysfs中建立一个符号链接 ：</p>
<pre class="crayon-plain-tag">int sysfs_create_link( struct kobject *kobj, struct kobject *target, char *name );</pre>
<p><span style="text-decoration: underline;"><strong>销毁属性</strong></span></p>
<p>通过下面的函数可以销毁属性：</p>
<pre class="crayon-plain-tag">void sysfs_remove_file( struct kobject *kobj, const struct attribute *attr );</pre>
<p>类似的，移除符号链接：</p>
<pre class="crayon-plain-tag">void sysfs_remove_link(struct kobject *kobj, char *name);</pre>
<p><span style="text-decoration: underline;"><strong>sysfs约定 </strong></span></p>
<p>当前sysfs文件系统代替了以前需要由<pre class="crayon-plain-tag">ioctl() </pre> （作用于设备节点）和procfs文件系统完成的功能。例如在设备映射的sysfs子目录添加一个属性，可以代替实现一个新的ioctl()。</p>
<p>为保持sysfs干净和直观，开发者必须遵守：</p>
<ol>
<li>sysfs属性应该保证<span style="background-color: #c0c0c0;">每个文件只导出一个值（往往对应一个独立的内核变量）</span>，该值应该是文本形式而且映射为简单C类型。该约定的目的是避免数据过度结构化或凌乱，这正是/proc面临的问题</li>
<li>在sysfs中要以一个清晰的层次组织数据。父子关系、属性都要准确</li>
<li>由于sysfs提供内核到用户空间的服务，多少有点ABI的作用，因此任何时候都不应该改变既有文件</li>
</ol>
<div class="blog_h3"><span class="graybg">内核事件层</span></div>
<p>内核事件层实现了<span style="background-color: #c0c0c0;">内核到用户的消息通知系统</span>，该系统就建立在kobject之上。对于特别是桌面系统来说，将内核中事件传递给用户空间的需求一直存在，用户需要知道硬盘满了、处理器过热，等等…</p>
<p>早起的事件层没有使用kobject和sysfs，它们是“瞬时”的。现在的事件层把事件模拟为信号，<span style="background-color: #c0c0c0;">信号从一个明确的kobject对象发出</span>，每一个事件源都是一个sysfs中的路径。每个事件都被赋予一个动词或者动作字符串以表示发生的事情。最后，每个事件都有一个可选的payload，内核使用sysfs属性表示负载。</p>
<p>从内部实现来说，内核事件从内核传递到用户空间需要经过netlink，netlink是用于传递网络信息的多点套接字，使用netlink就意味着从用户空间获取内核事件就像使用套接字一样简单——从用户空间实现一个服务用于监听套接字，处理任何读到的信息。</p>
<p>在内核代码中，可以使用下面的函数向用户空间发送信号：</p>
<pre class="crayon-plain-tag">/**
 * @param kobj 事件源
 * @param action 动词，用于描述信号，由一个枚举表示，这些枚举映射到一个字符串
 */
int kobject_uevent( struct kobject *kobj, enum kobject_action action );
enum kobject_action
{
    KOBJ_ADD, //add
    KOBJ_REMOVE, //remove
    KOBJ_CHANGE, //change
    KOBJ_MOVE, //move
    KOBJ_ONLINE, //online
    KOBJ_OFFLINE, //offline
    KOBJ_MAX //max
};</pre>
<div class="blog_h1"><span class="graybg">I/O调度器</span></div>
<p>Linux 从2.4内核开始支持I/O调度器，到目前为止有5种类型：Linux 2.4内核的 Linus Elevator、Linux 2.6内核的 Deadline、 Anticipatory、 CFQ、 Noop，其中Anticipatory从Linux 2.6.33版本后被删除了。目前主流的Linux发行版本使用Deadline、 CFQ、 Noop三种I/O调度器。</p>
<div class="blog_h2"><span class="graybg">调度器简介</span></div>
<div class="blog_h3"><span class="graybg">Linus Elevator</span></div>
<p>在2.4 内核中它是第一种I/O调度器。它的主要作用是为每个设备维护一个查询请求，当内核收到一个新请求时，如果能合并就<span style="background-color: #c0c0c0;">合并。</span>如果不能合并，就会尝试<span style="background-color: #c0c0c0;">排序</span>。如果既不能合并，也没有合适的位置插入，就<span style="background-color: #c0c0c0;">放到请求队列的最后</span>。</p>
<div class="blog_h3"><span class="graybg">Anticipatory</span></div>
<p>Anticipatory的中文含义是"预料的，预想的"，顾名思义<span style="background-color: #c0c0c0;">有个I/O发生的时候，如果又有进程请求I/O操作，则将产生一个默认的6毫秒猜测时间，猜测下一个进程请求I/O是要干什么的</span>。这个I/O调度器<span style="background-color: #c0c0c0;">对读操作优化服务时间，在提供一个I/O的时候进行短时间等待，使进程能够提交另外的I/O</span>。Anticipatory算法从Linux 2.6.33版本后被删除了，因为使用CFQ通过配置也能达到Anticipatory的效果。</p>
<div class="blog_h3"><span class="graybg">DeadLine</span></div>
<p>对Linus Elevator的一种改进，它<span style="background-color: #c0c0c0;">避免有些请求太长时间不能被处理</span>。另外可以区分对待读操作和写操作。DEADLINE额外分别为读I/O和写I/O提供了FIFO队列。</p>
<div class="blog_h3"><span class="graybg">CFQ</span></div>
<p>CFQ全称Completely Fair Queuing ，中文名称完全公平排队调度器，它是现在许多 Linux 发行版的<span style="background-color: #c0c0c0;">默认调度器</span>，CFQ是内核默认选择的I/O调度器。它将由进程提交的<span style="background-color: #c0c0c0;">同步请求放到多个进程队列中，然后为每个队列分配时间片以访问磁盘</span>。<span style="background-color: #c0c0c0;">对于通用的服务器是最好的选择</span>，CFQ均匀地分布对I/O带宽的访问。CFQ为每个进程和线程，单独创建一个队列来管理该进程所产生的请求,以此来保证每个进程都能被很好的分配到I/O带宽，I/O调度器<span style="background-color: #c0c0c0;">每次执行一个进程的4次请求</span>。该算法的特点是<span style="background-color: #c0c0c0;">按照I/O请求的地址进行排序，而不是按照先来后到的顺序来进行响应</span>。简单来说就是给所有同步进程分配时间片，然后才排队访问磁盘。</p>
<div class="blog_h3"><span class="graybg">NOOP</span></div>
<p>NOOP全称No Operation，中文名称电梯式调度器，该算法实现了最简单的FIFO队列，所有I/O请求大致按照先来后到的顺序进行操作。NOOP实现了一个简单的FIFO队列，它像电梯的工作主法一样对I/O请求进行组织。它是基于先入先出（FIFO）队列概念的 Linux 内核里最简单的I/O 调度器。此调度程序最适合于固态硬盘。</p>
<div class="blog_h2"><span class="graybg">调度器选型</span></div>
<ol>
<li>Deadline适用于大多数环境，<span style="background-color: #c0c0c0;">特别是写入较多的文件服务器</span>，从原理上看，DeadLine是一种<span style="background-color: #c0c0c0;">以提高机械硬盘吞吐量为思考出发点的调度算法</span>，尽量保证在有I/O请求达到最终期限的时候进行调度，非常适合业务比较单一并且I/O压力比较重的业务，比如Web服务器，数据库应用等。CFQ 为所有进程分配等量的带宽，<span style="background-color: #c0c0c0;">适用于有大量进程的多用户系统</span></li>
<li>CFQ是一种比较通用的调度算法，<span style="background-color: #c0c0c0;">保证对进程尽量公平</span>，<span style="background-color: #c0c0c0;">为所有进程分配等量的带宽</span>，<span style="background-color: #c0c0c0;">适合于桌面多任务及多媒体应用</span></li>
<li>NOOP 对于<span style="background-color: #c0c0c0;">闪存设备和嵌入式系统是最好的选择</span>。对于固态硬盘来说使用NOOP是最好的，DeadLine次之，而CFQ效率最低</li>
</ol>
<div class="blog_h2"><span class="graybg">设置调度器</span></div>
<div class="blog_h3"><span class="graybg">查看支持的</span></div>
<pre class="crayon-plain-tag">dmesg | grep -i scheduler</pre>
<div class="blog_h3"><span class="graybg">查看使用的</span></div>
<pre class="crayon-plain-tag">cat /sys/block/sda/queue/scheduler
# noop deadline [cfq]</pre>
<div class="blog_h3"><span class="graybg">临时修改</span></div>
<pre class="crayon-plain-tag">echo noop &gt; /sys/block/sdb/queue/scheduler</pre>
<div class="blog_h3"><span class="graybg">永久修改 </span></div>
<pre class="crayon-plain-tag">grubby --grub --update-kernel=ALL --args="elevator=deadline"</pre>
<p>或者直接编辑grub文件：</p>
<pre class="crayon-plain-tag">elevator= cfq</pre>
<div class="blog_h1"><span class="graybg">I/O缓冲机制</span></div>
<p>当Linux中的用户程序执行一次磁盘写入操作时，对应的流程如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/11/summary-of-io-buffering.png"><img class="wp-image-34613 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/11/summary-of-io-buffering.png" alt="summary-of-io-buffering" width="939" height="810" /></a></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>从发起系统调用write()到数据落盘，中间要经过Kernel Buffer Cache这一层。Kernel Buffer Cache由两个部分组成：Page Cache、Buffer Cache。</p>
<div class="blog_h2"><span class="graybg">Page Cache</span></div>
<p>在读取磁盘时，内核会先检查 Page Cache 里是不是已经缓存了这个数据。如果数据存在于Page Cache中则直接返回，否则从磁盘加载页面并放入Page Cache中。</p>
<p>在写入磁盘时，内核会把数据写入到Page Cache，并把对应的页标记为Dirty，添加到脏页列表。内核会定期将脏页列表刷出到磁盘以保持数据一致性。</p>
<div class="blog_h2"><span class="graybg">Buffer Cache</span></div>
<p>在Linux还没有引入虚拟内存技术之前，没有页的概念，那时候只有Buffer Cache。<span style="background-color: #c0c0c0;">Buffer Cache以块（磁盘读写的最小单位）为单位进行缓存</span>。</p>
<p>现在，基于文件的操作（例如write/read）、mmap()之后的块设备，都会经过Page Cache。而Buffer Cache 用来在系统对块设备进行读写的时候，对块进行数据缓存的系统来使用，实际上<span style="background-color: #c0c0c0;">负责所有对磁盘的 I/O 访问</span>。</p>
<p>从2.4开始，Buffer Cache融合到Page Cahce中，不再独立存在，这避免了两个缓存之间数据同步的开销。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol4">Linux内核学习笔记（四）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-study-note-vol4/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核学习笔记（三）</title>
		<link>https://blog.gmem.cc/linux-kernel-study-note-vol3</link>
		<comments>https://blog.gmem.cc/linux-kernel-study-note-vol3#comments</comments>
		<pubDate>Sun, 30 Jan 2011 03:41:22 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=8982</guid>
		<description><![CDATA[<p>Linux使用虚拟内存技术。它是一种位于应用程序内存请求与内存管理单元（MMU，一般是集成于CPU的硬件）硬件之间的抽象层。虚拟内存计数有以下优势： 多个进程可以同时并发的运行，使用重复的虚拟内存地址 应用程序所需内存大于物理内存时也可以运行 程序代码中，只有部分装入内存时，进程也可以执行程序 进程可以共享库函数或者程序的一份单一的内存映像 程序在物理内存中的位置可以重新定位 可以编写机器无关的代码，不用关心物理内存的组织结构 虚拟内存子系统的主要由虚拟地址空间（Virtual address space）组成，进程使用的虚拟内存地址不同于其物理内存地址，内核（提供页表）和MMU负责协调并定位物理地址。 机器的物理内存，除了开辟出一小部分专门用于存放内核映像（内核代码、内核静态数据结构）以外，其它部分通常都由虚拟内存子系统管理，并作以下三个主要用途： 满足内核对缓冲区、描述符和其它动态内核数据结构的请求 满足进程对一般内存区域的请求、对文件内存映射的请求 作为高速缓存的载体，让磁盘等I/O获得更好性能 虚拟内存子系统要解决的一个主要问题是内存碎片，由于内核常常需要物理上连续的内存空间，当碎片化严重时，即使物理内存富余，也可能导致失败。内核内存分配器（KMA）为解决内存碎片问题提供了很好的帮助，当前较好的KMA算法是Solaris发明的Slab。 本文分为以下章节，讲述虚拟内存子系统和相关的内核模块： 内存管理 进程地址空间 页缓存和页回写 内存管理 在内核中分配内存比在用户空间困难，原因包括： 内核不能像用户空间那样奢侈的使用内存，这是根本原因所在。内核不支持简单的内存分配方式 <a class="read-more" href="https://blog.gmem.cc/linux-kernel-study-note-vol3">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol3">Linux内核学习笔记（三）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>Linux使用虚拟内存技术。它是一种位于应用程序内存请求与内存管理单元（MMU，一般是集成于CPU的硬件）硬件之间的抽象层。虚拟内存计数有以下优势：</p>
<ol>
<li>多个进程可以同时并发的运行，使用重复的虚拟内存地址</li>
<li>应用程序所需内存大于物理内存时也可以运行</li>
<li>程序代码中，只有部分装入内存时，进程也可以执行程序</li>
<li>进程可以共享库函数或者程序的一份单一的内存映像</li>
<li>程序在物理内存中的位置可以重新定位</li>
<li>可以编写机器无关的代码，不用关心物理内存的组织结构</li>
</ol>
<p>虚拟内存子系统的主要由<span style="background-color: #c0c0c0;">虚拟地址空间（Virtual address space）</span>组成，进程使用的虚拟内存地址不同于其物理内存地址，内核（提供页表）和MMU负责协调并定位物理地址。</p>
<p>机器的物理内存，除了开辟出一小部分专门用于存放内核映像（内核代码、内核静态数据结构）以外，其它部分通常都由虚拟内存子系统管理，并作以下三个主要用途：</p>
<ol>
<li>满足内核对缓冲区、描述符和其它动态内核数据结构的请求</li>
<li>满足进程对一般内存区域的请求、对文件内存映射的请求</li>
<li>作为高速缓存的载体，让磁盘等I/O获得更好性能</li>
</ol>
<p>虚拟内存子系统要解决的一个主要问题是<span style="background-color: #c0c0c0;">内存碎片</span>，由于内核常常需要物理上连续的内存空间，当碎片化严重时，即使物理内存富余，也可能导致失败。内核内存分配器（KMA）为解决内存碎片问题提供了很好的帮助，当前较好的KMA算法是Solaris发明的Slab。</p>
<p>本文分为以下章节，讲述虚拟内存子系统和相关的内核模块：</p>
<ol>
<li><a href="#memory-management">内存管理</a></li>
<li><a href="#process-address-space">进程地址空间</a></li>
<li><a href="#page-cache-and-writeback">页缓存和页回写</a></li>
</ol>
<div id="memory-management" class="blog_h1"><span class="graybg">内存管理</span></div>
<p>在内核中分配内存比在用户空间困难，原因包括：</p>
<ol>
<li>内核不能像用户空间那样奢侈的使用内存，这是根本原因所在。内核不支持简单的内存分配方式</li>
<li>内核一般不能睡眠，这导致涉及到换页（潜在的睡眠）的内存分配受限</li>
<li>内核处理内存分配错误困难，参考第2条</li>
</ol>
<div class="blog_h2"><span class="graybg">页表</span></div>
<p>每个进程都有一个页表，用于存储虚拟地址到物理地址，准确的说是页，的映射关系。</p>
<p>进程顶级页面包含一个项，此项的内容是全局共享的，描述内核空间中的虚拟-物理页映射关系。每个进程随时都可能访问内核空间，例如系统调用，这要求随时能够进行内核空间的地址映射。</p>
<div class="blog_h2"><span class="graybg">MMU</span></div>
<p>内存管理单元，能够通过查找进程的页面，完成从虚拟地址到物理地址的转换。</p>
<p>MMU是由体系结构决定的，因此，页表的结构也和体系结构相关。</p>
<div class="blog_h2"><span class="graybg">页</span></div>
<p>尽管CPU<span style="background-color: #c0c0c0;">最小寻址单位通常为字</span>（甚至字节），内存管理单元（MMU，管理内存并把<span style="background-color: #c0c0c0;">虚拟地址转换为物理地址</span>的硬件）却把<span style="background-color: #c0c0c0;">物理页（也称页帧，Page frame）</span>作为管理内存的基本单位——从虚拟内存角度看，页是最小单位，即页表（Page table）的最小条目是一个页。</p>
<p>体系结构不同，页的大小也不同（甚至某些体系结构支持多种页大小）。<span style="background-color: #c0c0c0;">大部分32位体系结构的页大小为4KB</span>，64位一般支持8KB。大部分Linux系统使用4KB页。</p>
<p>内核使用下面的结构来表示物理页：</p>
<pre class="crayon-plain-tag">/*
 * 页描述符结构体
 *
 * 每个物理页都对应这样的一个结构，以便内核能够跟踪当前时刻页被用来存放什么东西
 * 注意：无法跟踪哪个任务在使用页
 *
 * 该结构本质上和物理页有关，而不是虚拟页，因此该结构对页的描述是临时的
 */
struct page
{
    //位域标识，该标识存放多种状态，例如是否脏页、是否锁在内存中
    unsigned long flags;
    //存放页的引用计数，如果为-1表示内核没有引用该页，在新的分配中可以使用它，内核代码调用page_count()检查此计数，返回0表示空闲
    atomic_t _count;
    union
    {
        //这个页被映射到了几个进程的地址空间
        atomic_t _mapcount;
        struct
        {
            u16 inuse;
            u16 objects;
        };
    };
    union
    {
        struct
        {
            //一个页可以作为私有数据使用
            unsigned long private;
            /**
             * 该字段目前不用于内核空间
             * 如果当前页用于页缓存，该字段用于访问缓存对应的文件。页缓存用于保存文件的逻辑内容，Linux用它加速磁盘访问
             * 如果当前页是一个匿名页（anonymous page，依赖于swap的用户空间内存）则该字段指向anon_vma结构允许内核快速的找到包含该页的页表
             */
            struct address_space *mapping;
        };
#if USE_SPLIT_PTLOCKS
        spinlock_t ptl;
#endif
        struct kmem_cache *slab;
        struct page *first_page;//指向slab中第一个空闲对象
    };
    union
    {
        pgoff_t index; //对于页缓存中的页，该字段指定了缓存映射的文件的偏移量
        void *freelist;//如果页由slub或者slob分配器管理，则该字段指向空闲对象的列表
    };
    struct list_head lru;

#if defined(WANT_PAGE_VIRTUAL)
/**
 * 如果不为空，则指向页对应的内核空间虚拟地址，该字段不是很有用，因为地址可以很容易被计算出来
 * 某些内存（比如高端内存，high memory，32位系统一般1GB以上）在内核地址空间中，不固定的映射到某个虚拟地址，此时该字段为NULL
 */
void *virtual;
#endif
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags;
#endif

#ifdef CONFIG_KMEMCHECK
void *shadow;
#endif
};</pre>
<p>可以看到上述结构的嵌套很复杂，这是出于节约空间的考虑，此结构<span style="background-color: #c0c0c0;">每增加1B，内核占用内存就会增加若干MB</span>。因为每个物理页都需要这样的一个结构，内存越大，这类结构占用的内存就越多——假定页大小为8KB，内存为4GB，那么page结构占用内核内存就是20MB。</p>
<div class="blog_h2"><span class="graybg">区（Zones）</span></div>
<p>由于硬件的限制，内核不能按照同样的方式处理所有的内存，例如某些硬件在内存寻址方面存在缺陷：</p>
<ol>
<li>某些硬件设备只能对特定内存地址进行DMA（直接内存访问）</li>
<li>某些体系结构能够寻址的物理地址范围比虚拟地址大的多，结果是，某些内存不能<span style="background-color: #c0c0c0;">永久性的映射</span>到内核地址空间。例如32位Linux内核把4G虚拟地址中0-3G分配给用户空间，3-4G分配给内核空间——1GB，而x86_32架构支持的<span style="background-color: #c0c0c0;">物理地址扩展（PAE）</span>可以让物理寻址范围扩大到64G</li>
</ol>
<p>为应对这些缺陷，内核使用<span style="background-color: #c0c0c0;">区把相同性质的内存进行分组</span>：</p>
<ol>
<li>ZONE_DMA：该区的页支持DMA操作，<span style="background-color: #c0c0c0;">DMA允许硬件绕过CPU直接读写主存。</span>x86_32该区域为物理内存0-16MB</li>
<li>ZONE_DMA32：和上一区类似，但是这些页只能被32位设备访问，某些体系结构中该区比ZONE_DMA更大</li>
<li>ZONE_NORMAL：包含能够正常映射的页</li>
<li>ZONE_HIGHMEM：包含所谓<span style="background-color: #c0c0c0;">高端内存（High memory）</span>，这些内存不能永久的映射到内核地址空间，需要动态映射。x86_32该区域为物理内存896M以上</li>
</ol>
<p>区的分配和使用依赖于体系结构：</p>
<ol>
<li>某些体系结构支持<span style="background-color: #c0c0c0;">对任何地址进行DMA操作</span>，这些体系结构中<span style="background-color: #c0c0c0;">ZONE_DMA为空</span>。而x86_32上ISA（Industry Standard Architecture，工业标准体系结构，只支持16位设备）设备只能在物理内存的前16MB进行DMA操作</li>
<li>某些体系结构支持<span style="background-color: #c0c0c0;">所有内存的直接映射</span>，这些体系结构中<span style="background-color: #c0c0c0;">ZONE_HIGHMEM为空，例如x86_64</span>。而x86_32中高于896M的都是高端内存</li>
</ol>
<p>注意这些内存分区没有物理意义，只是逻辑分组。内核依照分区进行内存分配：</p>
<ol>
<li>内存不能跨区分配</li>
<li>某些分配可以使用多个区，例如一般用途的内存既可以使用ZONE_NORMAL，也可以使用ZONE_DMA</li>
</ol>
<p>区使用下面的结构表示：</p>
<pre class="crayon-plain-tag">struct zone
{
    // 水位，通过*_wmark_pages(zone)宏访问，该数组持有当前区最小、低、高水位值
    // 内核使用水位为每个区域设置合适的内存消耗基准，水位随着空闲内存的多少而变化
    unsigned long watermark[NR_WMARK];

    /*
     * 各区保留的内存的大小
     *
     * 我们不确定分配出去的内存最终是否会被是否，因此，为了防止完全的浪费数GB的内存，我们必须预留低区域中的一些内存
     * 防止地区与内存出现OOM而高区域还有大量的内存可用
     * 该数组在运行时可能被重新计算，如果内核参数sysctl_lowmem_reserve_ratio被调整
     */
    unsigned long lowmem_reserve[MAX_NR_ZONES];

    struct per_cpu_pageset __percpu *pageset;
    //该自旋锁防止此结构被并发访问
    spinlock_t lock;
    int all_unreclaimable;
    struct free_area free_area[MAX_ORDER];

    ZONE_PADDING (_pad1_)

    spinlock_t lru_lock;
    struct zone_lru
    {
        struct list_head list;
    } lru[NR_LRU_LISTS];

    struct zone_reclaim_stat reclaim_stat;

    unsigned long pages_scanned;
    //区的标志位
    unsigned long flags;

    //该区的统计信息
    atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];

    int prev_priority;

    unsigned int inactive_ratio;

    ZONE_PADDING (_pad2_)
    wait_queue_head_t * wait_table;
    unsigned long wait_table_hash_nr_entries;
    unsigned long wait_table_bits;
    struct pglist_data *zone_pgdat;
    unsigned long zone_start_pfn;
    unsigned long spanned_pages;
    unsigned long present_pages;

    /*
     * 区域的名字，内核启动时初始化，三个区的名字分别为：DMA、Normal、HighMem
     */
    const char *name;
} ____cacheline_internodealigned_in_smp;</pre>
<p>这个结构较大，但是系统中只有三个区，因此该结构的实例只有三个。 </p>
<div class="blog_h2"><span class="graybg">按页分配</span></div>
<p>通过内核提供的接口，我们可以在内核空间进行内存分配和释放。 内核提供了一种请求内存的<span style="background-color: #c0c0c0;">底层机制</span>，可以用来以页为单位分配内存：</p>
<pre class="crayon-plain-tag">//分配2^order个连续的物理页，并且返回执行第一个page结构的指针，如果出错返回NULL
struct page * alloc_pages( gfp_t gfp_mask, unsigned int order );
//把页转换为它映射的逻辑地址
void * page_address( struct page *page );
//类似于alloc_pages，但是直接返回第一页的逻辑地址
unsigned long __get_free_pages( gfp_t gfp_mask, unsigned int order );

//下面两个函数分配单个页
struct page * alloc_page( gfp_t gfp_mask );
unsigned long __get_free_page( gfp_t gfp_mask );</pre>
<p>分配内存后，必须进行错误检查，因为<span style="background-color: #c0c0c0;">内存分配可能失败</span>。</p>
<p>如果想让返回的页全部填充为0，可以调用： </p>
<pre class="crayon-plain-tag">unsigned long get_zeroed_page(unsigned int gfp_mask);</pre>
<p>该函数在为用户空间分配页时很有用，可以防止物理内存中的敏感数据被泄漏。</p>
<p>不再需要页时，应当释放之：</p>
<pre class="crayon-plain-tag">void __free_pages( struct page *page, unsigned int order );
void free_pages( unsigned long addr, unsigned int order );
void free_page( unsigned long addr );</pre>
<p>需要注意的是，只能释放属于自己的页，这要求传递正确的struct page或者地址，传递错误的参数可能导致系统崩溃。 </p>
<div class="blog_h2"><span class="graybg">gfp_mask标志</span></div>
<p>不管是<span style="background-color: #c0c0c0;">按页分配</span>，还是下面的<span style="background-color: #c0c0c0;">按字节分配</span>函数，都有一个标志参数可以设置。该标志参数可以包含多个位域，这些位域都声明在<pre class="crayon-plain-tag">linux/gfp.h</pre> 中声明，可以分为三类：</p>
<ol>
<li>行为修饰符（大部分内存分配不需要直接指定）：<br />
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>__GFP_WAIT</td>
<td>内存分配器（allocator）可以睡眠</td>
</tr>
<tr>
<td>__GFP_HIGH</td>
<td>内存分配器可以访问紧急池（emergency pools）</td>
</tr>
<tr>
<td>__GFP_IO</td>
<td>内存分配器可以启动磁盘I/O</td>
</tr>
<tr>
<td>__GFP_FS</td>
<td>内存分配器可以启动文件系统I/O</td>
</tr>
<tr>
<td>__GFP_COLD</td>
<td>内存分配器应该使用缓存中即将淘汰的页（cache cold pages）</td>
</tr>
<tr>
<td>__GFP_NOWARN</td>
<td>内存分配器不打印失败警告</td>
</tr>
<tr>
<td>__GFP_REPEAT</td>
<td>如果分配失败，内存分配器重复尝试分配。注意这次重复尝试也可能失败</td>
</tr>
<tr>
<td>__GFP_NOFAIL</td>
<td>无限制重复尝试，分配不得失败</td>
</tr>
<tr>
<td>__GFP_NORETRY</td>
<td>如果分配失败，绝不重试</td>
</tr>
<tr>
<td>__GFP_NOMEMALLOC</td>
<td>不使用紧急预留区域</td>
</tr>
<tr>
<td>__GFP_HARDWALL</td>
<td>强制hardwall处理器集合范围，即只在允许访问的CPU上分配内存</td>
</tr>
<tr>
<td>__GFP_RECLAIMABLE</td>
<td>指定页是可回收的</td>
</tr>
<tr>
<td>__GFP_COMP</td>
<td>添加混合页元数据，在hugetlb代码内部使用</td>
</tr>
</tbody>
</table>
</li>
<li>区修饰符（指定内存从何处分配，内核默认从ZONE_NORMAL开始）：<br />
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>__GFP_DMA</td>
<td>仅从ZONE_DMA分配</td>
</tr>
<tr>
<td>__GFP_DMA32</td>
<td>仅从ZONE_DMA32分配 </td>
</tr>
<tr>
<td>__GFP_HIGHMEM</td>
<td>从ZONE_HIGHMEM或者ZONE_NORMAL分配。注意该标志不能和__get_free_pages()、kmalloc()使用，原因是这些函数返回逻辑地址，而不是page结构体，而高端内存分配后是没有自动映射到内核地址空间的。只有alloc_pages()才可以使用该标志，它返回page结构体而不是逻辑地址</td>
</tr>
</tbody>
</table>
</li>
<li>类型标志（实际上是结合上面两种标志，更加简单、不容易出错）：<br />
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志</td>
<td style="text-align: center;">描述 </td>
</tr>
</thead>
<tbody>
<tr>
<td>GFP_ATOMIC</td>
<td>__GFP_HIGH。用于中断处理程序、下半部、持有自旋锁以及其它不能睡眠的地方，例如中断处理程序、软中断、Tasklet</td>
</tr>
<tr>
<td>GFP_NOWAIT</td>
<td>0。类似于上面，但是不会调用紧急内存池，因此增加了内存分配失败的可能性</td>
</tr>
<tr>
<td>GFP_NOIO</td>
<td>__GFP_WAIT。可以阻塞，但是不会启动磁盘I/O，该标志用于不能引擎更多磁盘I/O的阻塞性I/O代码中</td>
</tr>
<tr>
<td>GFP_NOFS</td>
<td>(__GFP_WAIT | __GFP_IO)。可以阻塞，也可能启动磁盘I/O，但是不会启动文件系统操作。在不能启动另外一个文件系统操作时使用，例如文件系统部分的某些代码中，防止再次调用自身导致死锁</td>
</tr>
<tr>
<td>GFP_KERNEL</td>
<td>(__GFP_WAIT | __GFP_IO | __GFP_FS)。常规分配方式，可能会阻塞，用于内核空间睡眠安全的进程上下文中，为了获得足够内存，内核会尽力而为，例如让调用者睡眠、交换页到硬盘</td>
</tr>
<tr>
<td>GFP_USER</td>
<td>(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)。常规分配方式，可能会阻塞，用于用户空间</td>
</tr>
<tr>
<td>GFP_HIGHUSER</td>
<td>(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HIGHMEM | __GFP_HIGHMEM)。使用高端内存，用于为用户空间进程分配内存</td>
</tr>
<tr>
<td>GFP_DMA</td>
<td>__GFP_DMA。获取支持DMA的内存，一般驱动程序可能使用该标志</td>
</tr>
</tbody>
</table>
</li>
</ol>
<div class="blog_h2"><span class="graybg">kmalloc()</span></div>
<p>类似于用户空间的内存分配函数<pre class="crayon-plain-tag">malloc()</pre> ，它分配<span style="background-color: #c0c0c0;">逻辑、物理上都连续</span>的以字节为单位的内核内存：</p>
<pre class="crayon-plain-tag">//该函数返回一个内存区域的指针，该区域至少有size大小，并且在物理上是连续的，如果出错则返回null
void * kmalloc(size_t size, gfp_t flags);

//用法示例
struct person *p;
p = kmalloc( sizeof(struct person), GFP_KERNEL );
if ( !p )
;</pre>
<div class="blog_h3"><span class="graybg">kfree()</span></div>
<p>该函数用于释放kmalloc()分配的内存：</p>
<pre class="crayon-plain-tag">void kfree(const void *ptr);
//下面的调用是安全的：
kfree(NULL);</pre>
<p> 不得释放：</p>
<ol>
<li>已经释放的内存</li>
<li>不是kmalloc()分配的内存</li>
</ol>
<div class="blog_h2"><span class="graybg">vmalloc()</span></div>
<p>该函数与kmalloc()相似，但是只保证分配内存的<span style="background-color: #c0c0c0;">虚拟地址是连续</span>的，<span style="background-color: #c0c0c0;">物理地址不必连续</span> 。用户空间malloc()的工作方式也是这样的。该函数可以分配非连续的物理内存块，然后再<span style="background-color: #c0c0c0;">修正页表</span>，把这些分散的内存映射到逻辑地址空间的连续区域内。</p>
<p>大多数情况下，只有硬件设备需要连续的物理地址，这是因为<span style="background-color: #c0c0c0;">硬件设备运作于MMC之外</span>，不知道虚拟地址为何物。尽管如此，很多内核代码使用kmalloc()，这是出于性能的考虑——不连续的物理地址需要建立额外的页表项，导致大得多的<span style="background-color: #c0c0c0;">TLB</span>（Translation lookaside buffer，转译后备缓冲，一种硬件缓冲区，用来<span style="background-color: #c0c0c0;">缓存虚拟地址到物理地址的映射关系</span>，可以<span style="background-color: #c0c0c0;">极大提升系统性能</span>，因为大部分内存需要虚拟寻址）抖动。</p>
<p>vmalloc()只在不得已时使用，<span style="background-color: #c0c0c0;">典型的是获得大块内存</span>，例如模块被动态加载到内核时，使用该函数分配的空间装载内核。</p>
<p>该函数以及相应的释放函数如下：</p>
<pre class="crayon-plain-tag">//返回至少size的虚拟连续空闲内存，如果失败返回NULL
//该函数可能睡眠，不得用于中断上下文或者任何不支持阻塞的地方
void * vmalloc(unsigned long size);
//释放由vmalloc()分配的内存
void vfree(const void *addr);</pre>
<div class="blog_h2"><span class="graybg">slab层</span></div>
<p>内核中内存的分配和回收非常频繁。为了提高性能，程序员常常使用<span style="background-color: #c0c0c0;">空闲链表（free lists）</span>， 其中包含特定结构的空闲实例，需要使用时，从中获取一个，用完则放回去，空闲链表相当于<span style="background-color: #c0c0c0;">对象高速缓冲（对象池）</span>，避免不必要的内存分配/回收动作。</p>
<p>这种分散的空闲链表机制难以全局控制，例如当内存紧缺的时候，无法通知这些链表收缩以腾出内存，因为内核根本不知道空闲链表的存在。为解决此问题Linux引入了slab层（即所谓<span style="background-color: #c0c0c0;">slab分配器</span>），充当<span style="background-color: #c0c0c0;">通用数据结构缓存层</span>。slab在以下原则之间维持平衡：</p>
<ol>
<li>频繁使用的数据结构会导致频繁的内存分配/释放，因此应该<span style="background-color: #c0c0c0;">缓存</span>之</li>
<li>频繁的内存分配/释放导致内存碎片，为避免碎片，空闲链表的缓存应当<span style="background-color: #c0c0c0;">连续存放</span></li>
<li>如果分配器知晓对象大小、页大小、总的高速缓存的大小，将有利于决定最佳算法</li>
<li>如果部分缓存为CPU独占，那么分配/释放可以避免SMP锁</li>
<li>如果分配器与NUMA（非统一内存存取，被共享的存储器物理上是分布式的）相关，那么它应该从相同的内存节点为请求者分配内存</li>
<li>可以对存放的对象进行着色，防止多个对象映射到相同的<span style="background-color: #c0c0c0;">缓存行（cache line，CPU缓存被划分为多个大小固定的行）</span></li>
</ol>
<div class="blog_h3"><span class="graybg">slab层的设计</span></div>
<p>依据对象类型的不同，slab层划分出多个<span style="background-color: #c0c0c0;">高速缓存组</span>：</p>
<ol>
<li>存放进程描述符（struct task_struct）的组</li>
<li>存放索引节点对象（struct inode）的组</li>
<li>通用高速缓存组：kmalloc()接口基于该组</li>
</ol>
<p>上述<span style="background-color: #c0c0c0;">每个组，会划分为多个slab</span>，每个slab由<span style="background-color: #c0c0c0;">1-N个物理连续页（一般1个页）</span>构成。每个slab在一个时刻可以是<span style="background-color: #c0c0c0;">满、空、部分满</span>三种状态，在分配时，优先使用部分满的slab，如果没有部分满slab，则使用空slab，如果空的也没有，则创建新的slab。这种使用策略有利于减少碎片。</p>
<p>高速缓存组使用结构<pre class="crayon-plain-tag">kmem_cache</pre> 表示：</p>
<pre class="crayon-plain-tag">struct kmem_cache {
/* 1) Per-CPU数据，每次分配/释放时访问 */
    struct array_cache *array[NR_CPUS];
/* 2) 可调整参数，由cache_chain_mutex保护  */
    unsigned int batchcount;
    unsigned int limit;
    unsigned int shared;

    unsigned int buffer_size;
    u32 reciprocal_buffer_size;
/* 3) 每次分配/释放时从后端访问 */

    unsigned int flags;     /* constant flags */
    unsigned int num;       /* # of objs per slab */

/* 4) 缓存增长/收缩 */
    /* 每个slab包含2^gfporder个页 */
    unsigned int gfporder;

    /* 控制的GFP标志位 */
    gfp_t gfpflags;

    size_t colour;          /* 缓存着色范围 */
    unsigned int colour_off;    /* 着色偏移量 */
    struct kmem_cache *slabp_cache;
    unsigned int slab_size;
    unsigned int dflags;        /* 动态标志位 */

    /* 构造函数 */
    void (*ctor)(void *obj);

/* 5) 缓存创建/移除 */
    const char *name;//缓存组的名称
    struct list_head next;//下一个缓存组

    /*
     * 节点列表（长度一般就是1），该字段必须是最后一个字段
     *
     */
    struct kmem_list3 *nodelists[MAX_NUMNODES];

};</pre>
<p>注意最后一个字段节点列表，它是<pre class="crayon-plain-tag">kmem_list3</pre> 结构的数组，该结构包含三个链表：slabs_full、slabs_partial、slab_empty，分别表示当前节点满、部分满、空的slab：</p>
<pre class="crayon-plain-tag">struct kmem_list3 {
	struct list_head slabs_partial;	/* 部分满的slab */
	struct list_head slabs_full;  /* 满的slab */
	struct list_head slabs_free; /* 空闲的slab */
	unsigned long free_objects;
	unsigned int free_limit;
	unsigned int colour_next;	/* Per-node cache coloring */
	spinlock_t list_lock;
	struct array_cache *shared;	/* shared per node */
	struct array_cache **alien;	/* on other nodes */
	unsigned long next_reap;	/* updated without locking */
	int free_touched;		/* updated without locking */
};</pre>
<p>这些链表包含所在高速缓存组所有的slab，后者使用slab描述符表示： </p>
<pre class="crayon-plain-tag">struct slab
{
    struct list_head list; /* 数据结构链表，该链表可能是满的、空的或者部分满的 */
    unsigned long colouroff; /* 着色偏移量 */
    void *s_mem; /* 该slab中的第一个对象的指针 */
    unsigned int inuse; /* 该slab已分配的对象数 */
    kmem_bufctl_t free; /* 第一个空闲对象（如果有的话） */
};</pre>
<p>slab描述符要么在slab外面另外分配内存存储，要么直接存放在slab的首部。</p>
<p>当缓存空间不足时，内核会调用<span style="background-color: #c0c0c0;">低级内核页分配</span>函数<pre class="crayon-plain-tag">kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)</pre> 为缓存组创建新的slab，后者会则转调<pre class="crayon-plain-tag">__get_free_pages()</pre> 进行内存分配。通过<pre class="crayon-plain-tag">kmem_freepages()</pre> 则可以释放掉slab，它会调用<pre class="crayon-plain-tag">free_pages()</pre> 。</p>
<p><span style="background-color: #c0c0c0;">slab层存在的意义就是避免频繁的分配/释放内存</span>，因此只有slab中没有可用空间时，才会分配新的slab；类似的，只有当内存紧缺、高速缓存被显式撤销时，才会释放slab。</p>
<div class="blog_h3"><span class="graybg">slab分配器的接口</span></div>
<p>下面的函数用于创建新的高速缓存组：</p>
<pre class="crayon-plain-tag">/**
 * 成功时返回指向高速缓存组的指针，否则返回NULL
 * 注意该函数不能在中断上下文调用，因为它可能睡眠
 */
struct kmem_cache * kmem_cache_create(
    const char *name, //高速缓存（组）的名称
    size_t size, //缓存中每个元素的大小
    size_t align,//第一个对象的偏移，用来确保在页内进行特定的对齐，默认0（标准对齐）
    unsigned long flags,//标志位集合
    void (*ctor)( void * ) );//高速缓存的构造函数，只有新的页追加到缓存中时，才会调用该函数，内核高速缓存不使用构造函数</pre>
<p>要撤销高速缓存组，则可以调用：</p>
<pre class="crayon-plain-tag">int kmem_cache_destroy(struct kmem_cache *cachep);</pre>
<p>该函数常常在模块注销代码中使用，调用前必须保证：组中所有slab都为空；调用过程中、完毕后，不得再使用该缓存。</p>
<p>创建了高速缓存组后，可以调用下面的函数获取或者释放对象：</p>
<pre class="crayon-plain-tag">/**
 * 返回指向组中某个对象的指针，如果缓存组中任何slab都没有足够空间，就会触发新的slab的创建
 */
void * kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
/**
 * 标注缓存池中的objp对象为空闲
 */
void kmem_cache_free( struct kmem_cache *cachep, void *objp );</pre>
<div class="blog_h2"><span class="graybg">在栈上的静态分配</span></div>
<p>在用户空间，<span style="background-color: #c0c0c0;">用户栈可以非常大、动态增长</span>。 在内核空间则相反，<span style="background-color: #c0c0c0;">内核栈固定且很小</span>。内核栈的大小依赖于体系结构和编译时选项。在以前的内核版本中，每个进程都对应一个2页的内核栈，由于32/64位体系结构的页大小分别4/8KB，因此内核栈分别为8/16KB。在2.6版本，引入了一个设置单页内核栈的选项，激活该选项则内核栈只有1页大小。</p>
<p>由于内核栈很小，任何时候在其上进行大量的静态分配（比如大型数组、结构体）都很危险。栈溢出时会导致宕机甚至无声息的数据破坏。使用动态内存分配通常是明智的选择。</p>
<div class="blog_h2"><span class="graybg">高端内存的映射</span></div>
<p>高端内存<span style="background-color: #c0c0c0;">不能永久的映射</span>到内核地址空间，因此通过<pre class="crayon-plain-tag">alloc_pages(__GFP_HIGHMEM, *)</pre>  分配的页，可能没有对应的逻辑地址。</p>
<p>在x86架构上，尽管处理器<span style="background-color: #c0c0c0;">物理寻址范围达4G（启用PAE则64G）</span>，然而<span style="background-color: #c0c0c0;">896M+的内存都是高端内存</span>，高端内存的页<span style="background-color: #c0c0c0;">一旦被分配，就必须映射</span>到内核的逻辑地址空间上，在x86上内核用于<span style="background-color: #c0c0c0;">映射高端内存的逻辑地址范围是3-4G</span>。</p>
<div class="blog_h3"><span class="graybg">永久映射</span></div>
<p>要把一个页映射到内核地址空间，可以调用：</p>
<pre class="crayon-plain-tag">//映射页到逻辑地址，该函数可能会睡眠
void *kmap(struct page *page);</pre>
<p>不管是不是高端内存，上述函数都可用：</p>
<ol>
<li>如果page属于低端内存，其（已经）映射到的虚拟地址直接作为返回值</li>
<li>如果page属于高端内存，则会立即永久的映射到一个内核逻辑地址，并返回该地址</li>
</ol>
<p>由于可供映射的逻辑地址空间有限，因此高端内存不再需要的时候，必须解除映射：</p>
<pre class="crayon-plain-tag">void kunmap(struct page *page);</pre>
<div class="blog_h3"><span class="graybg">临时映射</span></div>
<p>必须映射高端内存，而当前上下文又<span style="background-color: #c0c0c0;">不能睡眠时</span>，可以使用内核提供的临时映射（temporary mappings）机制（亦称原子映射，atomic mappings）。内核预留了一些mappings，专供临时映射使用。调用下面的函数可以进行/解除临时映射：</p>
<pre class="crayon-plain-tag">/**
 * 执行临时映射，该函数不会阻塞。
 * 该函数会禁止内核抢占，这是因为mappings对每个CPU来说是唯一的，而内核抢占（调度程序）可能改变任务在哪个CPU上执行
 */
void *kmap_atomic( struct page *page, enum km_type type );
//第二个参数为枚举，说明临时映射的目的
enum km_type
{
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_SKB_DATA_SOFTIRQ,
    KM_USER0,
    KM_USER1,
    KM_BIO_SRC_IRQ,
    KM_BIO_DST_IRQ,
    KM_PTE0,
    KM_PTE1,
    KM_PTE2,
    KM_IRQ0, KM_IRQ1,
    KM_SOFTIRQ0,
    KM_SOFTIRQ1,
    KM_SYNC_ICACHE,
    KM_SYNC_DCACHE,
    KM_UML_USERCOPY,
    KM_IRQ_PTE,
    KM_NMI,
    KM_NMI_PTE,
    KM_TYPE_NR
};
/**
 * 解除临时映射，该函数不会阻塞
 */
void kunmap_atomic( void *kvaddr, enum km_type type );</pre>
<div class="blog_h2"><span class="graybg">Per-CPU的分配</span></div>
<p>可以在SMP机器上使用Per-CPU数据，对于每个CPU，数据具有独特的副本。在2.4中，声明Per-CPU数据的方式是声明长度等于CPU数量的数组，例如：</p>
<pre class="crayon-plain-tag">unsigned long my_percpu[NR_CPUS];</pre>
<p>然后就可以用下面的代码访问之：</p>
<pre class="crayon-plain-tag">int cpu;
cpu = get_cpu(); /* 获得当前CPU并禁止内核抢占 */
/*操控变量*/
my_percpu[cpu]++;
put_cpu(); /* 启用内核抢占 */</pre>
<p>注意上述代码中没有锁，这是因为操控的数据对于CPU是专用的，不存在多CPU并发问题。但是需要禁止内核抢占，因为：</p>
<ol>
<li>如果当前代码被重新调度到其它CPU，则Per-CPU变量无效，因为它指向的不是当前CPU</li>
<li>如果另外一个任务抢占当前代码，则可能在同一CPU上访问Per-CPU变量，导致竞态条件 </li>
</ol>
<div class="blog_h2"><span class="graybg">新的Per-CPU接口</span></div>
<p>2.6引入了新的接口percpu，可以简化Per-CPU数据的创建、操控：</p>
<pre class="crayon-plain-tag">//编译时定义Per-CPU变量
DEFINE_PER_CPU( type, name );
//类似上面，某些情况下防止编译器警告
DECLARE_PER_CPU( type, name );

//禁止内核抢占，并得到Per-CPU变量的左值
#define get_cpu_var(var) (*({               \
    preempt_disable();              \
    &amp;__get_cpu_var(var); }))
//恢复内核抢占
#define put_cpu_var(var) do {               \
    (void)&amp;(var);                   \
    preempt_enable();               \
} while (0)

//获取其它CPU上的Per-CPU数据，注意该函数既不提供锁保护，也不禁止内核抢占
//该宏定义是非SMP的版本，就是简单的获得变量var
#define per_cpu(var, cpu)           (*((void)(cpu), &amp;(var)))</pre>
<p>注意，上述静态编译时声明的Per-CPU数据不能在模块内使用，要在模块中访问Per-CPU数据，需要动态创建：</p>
<pre class="crayon-plain-tag">//给每个CPU分配一个指定类型的对象实例，封装了__alloc_percpu宏，按单字节对齐（给定类型的自然边界）
void *alloc_percpu( type );
//分配对象，size为尺寸，align表示按几个字节进行对齐
void *__alloc_percpu( size_t size, size_t align );
void free_percpu( const void * ); //释放Per-CPU数据</pre>
<div class="blog_h2"><span class="graybg">使用Per-CPU数据的原因</span></div>
<p>使用Per-CPU数据的好处如下：</p>
<ol>
<li>减少锁定造成的开销：因为数据不存在并发问题，因此自然不需要加锁</li>
<li>大大减少缓存失效：失效发生在CPU试图使它们的缓存保持同步时，如果<span style="background-color: #c0c0c0;">一个CPU需要操作某个数据，而该数据又存放在其它处理器的缓存中，则后者必须清理或者刷出自己的缓存</span>。持续不断的缓存失效称为缓存抖动（thrashing the cache），会对系统性能产生很大影响</li>
</ol>
<div id="process-address-space" class="blog_h1"><span class="graybg"><a id="process-address-space"></a>进程地址空间</span></div>
<p>内核除了需要管理自己的内存外，还需要管理<span style="background-color: #c0c0c0;">用户空间中进程的内存</span>，该内存称为<span style="background-color: #c0c0c0;">进程地址空间</span>。Linux使用虚拟内存技术管理内存，因此系统中的每一个进程觉得自己可以使用全部物理内存——即使一个进程，其拥有的地址空间也远远<span style="background-color: #c0c0c0;">大于系统物理内存</span>。</p>
<p>每个进程都在其私有的进程地址空间上运行，在用户态下，进程可以访问<span style="background-color: #c0c0c0;">进程地址空间的私有栈、数据区、代码区</span>等信息；在内核态下，进程访问内<span style="background-color: #c0c0c0;">核的数据区、代码区，并使用另外的私有栈（内核栈）</span>。尽管每个进程都有自己的私有地址空间，实际上它们会共享一部分内存内容，这种共享可以由进程显式提出，也可以由内核自动完成以节约内存，比如对于程序、库的副本，尽管有多个进程访问它，只会加载一份在内存</p>
<div class="blog_h2"><span class="graybg">地址空间</span></div>
<p><span style="background-color: #c0c0c0;">进程地址空间由可寻址的虚拟内存</span>组成， 每个进程有多达32或64位平坦的（<span style="background-color: #c0c0c0;">flat，意味着连续、全部可用</span>）空间。一些OS不提供平坦空间，而是<span style="background-color: rgb(192, 192, 192);">分段式、不连续的，称为段地址空间，现在使用虚拟内存的OS很少使用</span>这种模式了。两个进程即使使用<span style="background-color: #c0c0c0;">相同的内存地址，也毫不相干</span>。</p>
<p>内存地址是一个数值，其必须在地址空间的范围之内。</p>
<p>进程不一定有权访问其全部虚拟地址空间，地址空间中可以被进程合法访问的部分，称为<span style="background-color: #c0c0c0;">内存区域（Memory areas）</span>，进程可以有<span style="background-color: #c0c0c0;">多个内存区域</span>，每个区域都是<span style="background-color: #c0c0c0;">连续的一段虚拟地址</span>区间。进程可以动态的给自己的地址空间添加/减少内存区域。内存区域具有关联的<span style="background-color: #c0c0c0;">权限</span>，例如<span style="background-color: #c0c0c0;">可读、可写、可执行</span>，进程必须遵守权限规则。如果<span style="background-color: #c0c0c0;">进程访问不是内存区域的内存、或者以错误的方式访问，内核将终结进程，提示段错误（Segmentation Fault）</span>。内存区域可以包含以下类型的对象：</p>
<ol>
<li>可执行文件代码的内存映射，称为<span style="background-color: #c0c0c0;">代码段（Text Section）</span></li>
<li>可执行文件已初始化的<span style="background-color: #c0c0c0;">全局变量</span>的内存映射，称为<span style="background-color: #c0c0c0;">数据段（Data Section）</span></li>
<li>包含<span style="background-color: #c0c0c0;">未初始化全局变量</span>的零页（Zero page，全部存放零的页）的内存映射， 称为<span style="background-color: #c0c0c0;">Bss Section</span></li>
<li>每个共享库（C库、动态库）的代码、数据、Bss段，也被载入进程的地址空间</li>
<li>任何内存映射文件</li>
<li>任何共享内存段</li>
<li>任何<span style="background-color: #c0c0c0;">匿名（没有映射到实际文件，MAP_ANONYMOUS）的内存映射</span>，比如<pre class="crayon-plain-tag">malloc()</pre> 分配的内存</li>
</ol>
<p><span style="background-color: rgb(192, 192, 192);">内存区域不会重叠</span>。<span style="background-color: #c0c0c0;">可执行代码、已初始化全局变量、未初始化全局变量、共享库的代码和数据、内存映射文件、堆（匿名映射）、栈（用户态栈）</span>都具有独立的区域，这些区域有些在程序通过<pre class="crayon-plain-tag">exec()</pre> 系统调用载入进程时，就会初始化。</p>
<div class="blog_h3"><span class="graybg">内核地址空间</span></div>
<p>进程拥有完整的虚拟地址空间 —— 不管是32/64位系统。地址空间分为用户、内核两部分。</p>
<p>出于性能的考虑，<span style="background-color: #c0c0c0;">内核内存映射到任何进程的地址空间</span>。但是，内核地址空间仅仅能由内核代码访问。</p>
<p>对于32位系统来说，<span style="background-color: #c0c0c0;">Linux将最上面的1G内存用作内核虚拟地址，范围0xc0000000 - 0xffffffff</span>。物理内存完全对应的映射到内核空间，这简化了内存管理。任何0-896M范围的内核虚拟地址，减去0xc0000000的偏移即得到物理地址。</p>
<p>对于64位系统来说，<span style="background-color: #c0c0c0;">整个地址空间的高半部分，全部留给内核虚拟地址</span>。</p>
<div class="blog_h2"><span class="graybg">内存描述符</span></div>
<p>内核使用内存描述符来表示进程的地址空间：</p>
<pre class="crayon-plain-tag">struct mm_struct
{
    /**
     * 下面两个字段都在描述该地址空间中全部内存区域：一个链表形式，一个红黑树形式
     * 这种冗余结构是为了快速遍历的同时，能够快速的搜索
     */
    struct vm_area_struct *mmap; /* 虚拟内存区域的链表 */
    struct rb_root mm_rb; /* 虚拟内存区域（VMA）的红黑树 */
    struct vm_area_struct *mmap_cache; /* 最后使用的虚拟内存区域 */
    unsigned long free_area_cache; /* 地址空间的第一个空洞 */
    pgd_t *pgd; /* 页全局目录 */
    atomic_t mm_users; /* 正在使用该地址空间的进程数，多个线程可能共享一个地址空间 */
    /**
     * 主（线程）引用计数，为0则该结构体可以被撤销
     * 多线程程序中只有主线程会导致该计数增加；进程的线程全部退出后，该计数会变为0
     */
    atomic_t mm_count;
    int map_count; /* 内存区域数量 */
    struct rw_semaphore mmap_sem; /* 内存区域信号量 */
    spinlock_t page_table_lock; /* 页表自旋锁 */
    /**
     * 所有内存描述符（mm_struct）形成的链表，该链表的首元素是init_mm描述符，它代表init进程的地址空间
     * 操作该链表时需要持有mmlist_lock锁
     */
    struct list_head mmlist;
    unsigned long start_code; /* 代码段起始地址 */
    unsigned long end_code; /* 代码段结束地址 */
    unsigned long start_data; /* 数据段起始地址 */
    unsigned long end_data; /* 数据段结束地址 */
    unsigned long start_brk; /* 堆的起始地址 */
    unsigned long brk; /* 堆的结束地址 */
    unsigned long start_stack; /* 栈的起始地址 */
    unsigned long arg_start; /* 命令行参数起始地址 */
    unsigned long arg_end; /* 命令行参数结束地址 */
    unsigned long env_start; /* 环境变量起始地址 */
    unsigned long env_end; /* 环境变量结束地址 */
    unsigned long rss; /* 分配的物理页 */
    unsigned long total_vm; /* VMA总数 */
    unsigned long locked_vm; /* 锁定VMA数量 */
    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 保存的auxv */
    cpumask_t cpu_vm_mask; /* lazy TLB switch mask */
    mm_context_t context; /* 体系结构特有数据 */
    unsigned long flags; /* 状态标志 */
    int core_waiters; /* thread core dump waiters */
    struct core_state *core_state; /* core dump support */
    spinlock_t ioctx_lock; /* AIO I/O 链表自旋锁*/
    struct hlist_head ioctx_list; /* AIO I/O 链表 */
};</pre>
<div class="blog_h3"><span class="graybg">分配内存描述符</span></div>
<p>内存描述符的指针存放在进程描述符的<pre class="crayon-plain-tag">task_struct.mm</pre> 字段，当前进程的内存描述符可以通过<pre class="crayon-plain-tag">current -&gt; mm</pre> 访问。</p>
<p><pre class="crayon-plain-tag">fork()</pre> 函数利用<pre class="crayon-plain-tag">copy_mm()</pre> 将父进程的内存描述符拷贝给子进程。</p>
<p>内存描述符从slab缓存组中分配：</p>
<pre class="crayon-plain-tag">//从slab缓存组mm_cachep中分配内存描述符
#define allocate_mm()	(kmem_cache_alloc(mm_cachep, GFP_KERNEL))</pre>
<p>一般的每个进程都有自己独特的进程描述符，也就是独立的地址空间。如果父进程希望<span style="background-color: #c0c0c0;">子进程与自己共享地址空间</span>，可以在调用<pre class="crayon-plain-tag">clone()</pre> 时设置<pre class="crayon-plain-tag">CLONE_VM</pre> 标记，这样的子进程称作<span style="background-color: #c0c0c0;">线程</span>，指定该标记后，就不需要调用allocate_mm()宏来分配描述符了，只需要将子进程的mm指向父进程的内存描述符：</p>
<pre class="crayon-plain-tag">if (clone_flags &amp; CLONE_VM)
{
    atomic_inc(&amp;current-&gt;mm-&gt;mm_users);
    tsk-&gt;mm = current-&gt;mm;
}</pre>
<div class="blog_h3"><span class="graybg">撤销内存描述符</span></div>
<p>进程退出时，内核调用定义在<pre class="crayon-plain-tag">/kernel/exit.c</pre> 中的<pre class="crayon-plain-tag">exit_mm()</pre> 来撤销内存描述符，该函数会：</p>
<ol>
<li>执行一些清理工作，更新统计量</li>
<li>调用<pre class="crayon-plain-tag">mmput()</pre> 减少mm_users计数</li>
<li>如果mm_users为0则调用<pre class="crayon-plain-tag">mmdrop()</pre> 减少mm_count计数</li>
<li>如果mm_count为零，说明该内存描述符没人使用了，调用<pre class="crayon-plain-tag">free_mm()</pre> 宏，通过<pre class="crayon-plain-tag">kmem_cache_free()</pre> 把结构体释放，归还slab缓存组</li>
</ol>
<div class="blog_h3"><span class="graybg">内核线程与内存描述符</span></div>
<p>内核线程没有进程地址空间，因此其进程描述符的mm字段为空，这是合理的——因为内核线程<span style="background-color: #c0c0c0;">没有用户上下文</span>。内核线程<span style="background-color: #c0c0c0;">没有自己的内存描述符、页表</span>。</p>
<p>尽管内核线程没有自己的页表，但是为了访问内核空间，它必须要使用页表。Linux的做法是，让<span style="background-color: #c0c0c0;">内核线程使用前一个进程的页表</span>：</p>
<ol>
<li>当一个进程被调度，获得CPU时，其进程描述符mm字段所指向的<span style="background-color: #c0c0c0;">地址空间被装载到内存</span>。进程描述符的<pre class="crayon-plain-tag">active_mm</pre> 被更新，指向新的地址空间</li>
<li>内核线程没有自己的地址空间，因此它被调度时，内核会发现mm为NULL</li>
<li>这时，内核就会保留刚刚失去CPU的进程的内存描述符，并更新内核线程的<pre class="crayon-plain-tag">active_mm</pre> 使之指向此描述符</li>
<li>内核线程使用前一个进程的页表，从中查询和内核内存相关的信息。每个进程的页表都有描述内核空间的顶级Entry，此Entry的内容是全局共享的，不存在数据冗余</li>
</ol>
<div class="blog_h2"><span class="graybg">虚拟内存区域</span></div>
<p><span style="background-color: #c0c0c0;">进程的内存区域</span>在内核中常被称为<span style="background-color: #c0c0c0;">“虚拟内存区域（VMA）”</span>。 虚拟内存区域是地址空间的<span style="background-color: #c0c0c0;">连续区间上一个独立内存范围</span>，内核把每个内存区域作为独立对象进行管理，虚拟内存区域使用下面的结构表示：</p>
<pre class="crayon-plain-tag">struct vm_area_struct
{
    struct mm_struct *vm_mm; /* 关联的内存描述符 */
    //每个虚拟内存区域都对应地址空间内的连续区间，不同虚拟内存区域不会重叠
    unsigned long vm_start; /* 区域首地址（包含） */
    unsigned long vm_end; /* 区域尾地址（排除） */
    struct vm_area_struct *vm_next; /* VMA的链表 */
    pgprot_t vm_page_prot; /* 访问权限 */
    unsigned long vm_flags; /* 标志位 */
    struct rb_node vm_rb; /* 此区域在红黑树中的节点 */
    union
    { /* 关联于 address_space-&gt;i_mmap 或者 address_space-&gt;i_mmap_nonlinear */
        struct
        {
            struct list_head list;
            void *parent;
            struct vm_area_struct *head;
        } vm_set;
        struct prio_tree_node prio_tree_node;
    } shared;
    struct list_head anon_vma_node; /* 匿名VMA项 */
    struct anon_vma *anon_vma; /* 匿名VMA对象 */
    struct vm_operations_struct *vm_ops; /* VMA操作表 */
    unsigned long vm_pgoff; /* 文件中的偏移量 */
    struct file *vm_file; /* 映射的文件（如果有） */
    void *vm_private_data; /* 私有数据 */
};</pre>
<div class="blog_h3"><span class="graybg">VMA标志</span></div>
<p>flags字段包含若干位标志，其含义如下： </p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志</td>
<td style="text-align: center;">对VMA及其页面的影响 </td>
</tr>
</thead>
<tbody>
<tr>
<td>VM_READ</td>
<td>区域中的内存页是可读的</td>
</tr>
<tr>
<td>VM_WRITE</td>
<td>区域中的内存页是可写的</td>
</tr>
<tr>
<td>VM_EXEC</td>
<td>区域中的内存页是可执行的</td>
</tr>
<tr>
<td>VM_SHARED</td>
<td>区域中的内存页是被共享的，用于指示此区域包含的映射是否可以在多进程间共享。如果该标志被设置，称为“<span style="background-color: #c0c0c0;">共享映射</span>”；反之称为“<span style="background-color: #c0c0c0;">私有映射</span>”</td>
</tr>
<tr>
<td>VM_MAYREAD</td>
<td>VM_READ标志可以被设置</td>
</tr>
<tr>
<td>VM_MAYWRITE</td>
<td>VM_WRITE标志可以被设置</td>
</tr>
<tr>
<td>VM_MAYEXEC</td>
<td>VM_EXEC标志可以被设置</td>
</tr>
<tr>
<td>VM_MAYSHARE</td>
<td>VM_SHARE标志可以被设置</td>
</tr>
<tr>
<td>VM_GROWSDOWN</td>
<td>区域可以向下增长</td>
</tr>
<tr>
<td>VM_GROWSUP</td>
<td>区域可以向上增长</td>
</tr>
<tr>
<td>VM_SHM</td>
<td>区域被用于共享内存</td>
</tr>
<tr>
<td>VM_DENYWRITE</td>
<td>区域映射了不可写文件</td>
</tr>
<tr>
<td>VM_EXECUTABLE</td>
<td>区域映射了可执行文件</td>
</tr>
<tr>
<td>VM_LOCKED</td>
<td>区域中的页面被锁定</td>
</tr>
<tr>
<td>VM_IO</td>
<td>区域映射了一个设备的I/O空间。通常在设备驱动程序执行nmap()函数进行I/O空间映射时才被设置，该标志也表示该区域不得包含在进程的core dump中</td>
</tr>
<tr>
<td>VM_SEQ_READ</td>
<td>区域可能被顺序读，提示内核进行有选择的预读（read-ahead），该标志可以通过系统调用<pre class="crayon-plain-tag">madvise()</pre> 设置</td>
</tr>
<tr>
<td>VM_RAND_READ</td>
<td>区域可能被随机读，类似上面，作用相反</td>
</tr>
<tr>
<td>VM_DONTCOPY</td>
<td>在fork()时，该区域不得拷贝</td>
</tr>
<tr>
<td>VM_DONTEXPAND</td>
<td>区域不能通过mremap()增长</td>
</tr>
<tr>
<td>VM_RESERVED</td>
<td>区域不得被交换出内存（swapped out），也是由设备驱动在进行映射时设置</td>
</tr>
<tr>
<td>VM_ACCOUNT</td>
<td>该区域是一个记账VM对象</td>
</tr>
<tr>
<td>VM_HUGETLB</td>
<td>区域使用了hugetlb页面</td>
</tr>
<tr>
<td>VM_NONLINEAR</td>
<td>区域是非线性映射的</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">VMA操作</span></div>
<p> <pre class="crayon-plain-tag">vm_area_struct.vm_ops</pre> 字段定义了用于操作内存区域函数集合：</p>
<pre class="crayon-plain-tag">struct vm_operations_struct
{
    //当内存区域被加入到一个地址空间时，该函数被调用
    void (*open)( struct vm_area_struct *area );
    //当内存区域从地址空间中移除时，该函数被调用
    void (*close)( struct vm_area_struct * );
    //当访问该内存区域的页，而页不在物理内存中时，页面错误处理器（page fault handler）调用该函数
    int (*fault)( struct vm_area_struct *, struct vm_fault * );
    //当只读页被设置为可修改时，页面错误处理器调用该函数
    int (*page_mkwrite)( struct vm_area_struct *vma, struct vm_fault *vmf );
    //当get_user_pages()失败时，access_process_vm()调用该函数
    int (*access)( struct vm_area_struct *, unsigned long, void *, int, int );
};</pre>
<div class="blog_h3"><span class="graybg">实际使用中的内存区域</span></div>
<p>使用<pre class="crayon-plain-tag">/proc</pre> 文件系统和<pre class="crayon-plain-tag">pmap</pre> 工具可以查看给定进程的内存空间及其包含的内存区域：</p>
<pre class="crayon-plain-tag">pmap $PID
#输出内容：
#开始地址（大小）     权限 （主:次设备号 inode）    文件
myprog[1426]
00e80000 (1212 KB) r-xp (03:01 208530)       /lib/tls/libc-2.5.1.so #C库代码段
00faf000 (12 KB)   rw-p (03:01 208530)       /lib/tls/libc-2.5.1.so #C库数据段
00fb2000 (8 KB)    rw-p (00:00 0)                                   #C库bss段
08048000 (4 KB)    r-xp (03:03 439029)       /root/src/myprog       #程序代码段
08049000 (4 KB)    rw-p (03:03 439029)       /root/src/myprog       #程序数据段
40000000 (84 KB)   r-xp (03:01 80276)        /lib/ld-2.5.1.so       #ld.so的代码段
40015000 (4 KB)    rw-p (03:01 80276)        /lib/ld-2.5.1.so       #ld.so的代码段
4001e000 (4 KB)    rw-p (00:00 0)                                   #ld.so的bss段
bfffe000 (8 KB)    rwxp (00:00 0)            [ stack ]              #栈
mapped: 1340 KB writable/private: 40 KB shared: 0 KB</pre>
<p>可以看到，该进程地址空间中被映射的总计1340KB，大约40KB是可写和私有的。如果一个<span style="background-color: #c0c0c0;">内存范围是共享或不可写</span>的， 那么内核只需要在内存中为文件（backing file）保留一份映射——这是安全的，也是合理的（避免内存浪费）。上面的C库就是不可写的例子。</p>
<p>没有映射文件的内存区域的设备标志为00:00，inode也设置为0，这样的区域属于零页——映射的内容全部是0。</p>
<div class="blog_h2"><span class="graybg">操作内存区域</span></div>
<p>内核常常需要在VMA上执行操作，这类操作非常频繁。内核在<pre class="crayon-plain-tag">linux/mm.h</pre> 中声明了若干VMA操作辅助函数：</p>
<div class="blog_h3"><span class="graybg">find_vma()</span></div>
<p>检查某个地址是否包含在某个VMA中：</p>
<pre class="crayon-plain-tag">/**
 * 该函数搜索包含此地址的VMA，如果找不到返回NULL
 *
 * @param mm 内存描述符，指定了进程地址空间
 * @param addr 需要寻找的地址
 * @return 包含此地址的内存区域
 */
struct vm_area_struct * find_vma( struct mm_struct *mm, unsigned long addr )
{
    struct vm_area_struct *vma = NULL;
    if ( mm )
    {
        /**
         * 由于预期后续还会有更多的调用者查找目标VMA，因此在查找到VMA后，缓存在
         * 内存描述符的mmap_cache字段中
         */
        vma = mm-&gt;mmap_cache;
        if ( ! ( vma &amp;&amp; vma-&gt;vm_end &gt; addr &amp;&amp; vma-&gt;vm_start &lt;= addr ) ) //如果没有命中缓存
        {
            struct rb_node *rb_node;
            rb_node = mm-&gt;mm_rb.rb_node;
            vma = NULL;
            while ( rb_node ) //红黑树遍历
            {
                struct vm_area_struct * vma_tmp;
                vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
                if ( vma_tmp-&gt;vm_end &gt; addr ) //判断结束地址大于addr
                {
                    vma = vma_tmp;
                    if ( vma_tmp-&gt;vm_start &lt;= addr ) //如果其实地址小于等于addr，则找到，返回
                    break;
                    rb_node = rb_node-&gt;rb_left; //找不到，沿着左子节点
                }
                else
                rb_node = rb_node-&gt;rb_right; //遍历红黑树，沿着右子节点
            }
            if ( vma ) mm-&gt;mmap_cache = vma;
        }
    }
    return vma;
}</pre>
<div class="blog_h3"><span class="graybg">find_vma_prev()</span></div>
<p>工作方式与上面的函数类似， 但是同时返回前一个VMA的指针</p>
<pre class="crayon-plain-tag">struct vm_area_struct * find_vma_prev(
    struct mm_struct *mm,
    unsigned long addr,
    struct vm_area_struct **pprev //在此指针中存放前一个VMA
);</pre>
<div class="blog_h3"><span class="graybg">find_vma_intersection</span></div>
<p>该宏用来判断VMA是否和指定的区间交叉，甚至包含该区间：</p>
<pre class="crayon-plain-tag">/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
   NULL if none.  Assume start_addr &lt; end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
    struct vm_area_struct * vma = find_vma(mm,start_addr);

    if (vma &amp;&amp; end_addr &lt;= vma-&gt;vm_start)
        vma = NULL;
    return vma;
}</pre>
<div class="blog_h3"><span class="graybg">创建地址区间：mmap()/do_mmap()</span></div>
<p>内核使用<pre class="crayon-plain-tag">do_mmap()</pre> 函数创建一个新的线性地址区间，但是该函数不一定会创建一个新的VMA——如果指定的地址空间与既有VMA相邻，那么将<span style="background-color: #c0c0c0;">合并为一个VMA</span>，否则创建新的VMA：</p>
<pre class="crayon-plain-tag">//如果有无效参数，返回负数
//如果需要创建新VMA，那么将从slab缓存组中获得一个vm_area_struct实例，并调用vma_link()将其新分配的内存区域加入链表和红黑树
//并更新内存描述符的total_vm字段
unsigned long do_mmap(
    /**
     * 被映射的文件
     * 如果该参数为NULL且offset为0，表示这次映射没有和文件关联，称为“匿名映射（anonymous mapping.）”
     * 如果该参数不为零，则称为“文件映射（file-backed mapping）”
     */
    struct file *file,
    unsigned long addr, //搜索空闲地址的起始点，可选
    unsigned long offset, //文件起始偏移量
    unsigned long len, //映射多长文件内容
    unsigned long prot, //页保护标志：指定映射页的访问权限
    unsigned long flag //映射类型标志：指定类型、改变映射行为
);</pre>
<p>页保护标志的取值依赖于体系结构，定义在<pre class="crayon-plain-tag">asm/mman.h</pre> ，通用的取值如下表：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 标志</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>PROT_READ</td>
<td>对应权限VM_READ</td>
</tr>
<tr>
<td>PROT_WRITE</td>
<td>对应权限VM_WRITE</td>
</tr>
<tr>
<td>PROT_EXEC</td>
<td>对应权限VM_EXEC</td>
</tr>
<tr>
<td>PROT_NONE</td>
<td>不得访问</td>
</tr>
</tbody>
</table>
<p>映射类型标志定义在asm/mman.h ，取值如下：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>MAP_SHARED</td>
<td>该映射可以共享</td>
</tr>
<tr>
<td>MAP_PRIVATE</td>
<td>该映射不得被共享</td>
</tr>
<tr>
<td>MAP_FIXED</td>
<td>新的区间必须开始于addr参数指定的位置</td>
</tr>
<tr>
<td>MAP_ANONYMOUS</td>
<td>该映射是匿名映射，不和文件关联</td>
</tr>
<tr>
<td>MAP_GROWSDOWN</td>
<td>对应VM_GROWSDOWN</td>
</tr>
<tr>
<td>MAP_DENYWRITE</td>
<td>对应VM_DENYWRITE</td>
</tr>
<tr>
<td>MAP_EXECUTABLE</td>
<td>对应VM_EXECUTABLE</td>
</tr>
<tr>
<td>MAP_LOCKED</td>
<td>对应VM_LOCKED</td>
</tr>
<tr>
<td>MAP_NORESERVE</td>
<td>不需要为映射保留空间</td>
</tr>
<tr>
<td>MAP_POPULATE</td>
<td>填充页表</td>
</tr>
<tr>
<td>MAP_NONBLOCK</td>
<td>在I/O操作上不阻塞 </td>
</tr>
</tbody>
</table>
<p>在用户空间可以调用mmap系统调用，间接使用do_mmap()函数，该系统调用如下：</p>
<pre class="crayon-plain-tag">/**
 * mmap的第二个版本，原始版本的mmap()调用由POSIX定义，仍然在C库中作为mmap()方法使用，
 * 但在内核已经没有对应实现
 */
void * mmap2(void *start,
    size_t length,
    int prot,
    int flags,
    int fd,
    off_t pgoff
);</pre>
<div class="blog_h3"><span class="graybg">删除地址区间：munmap()/do_munmap()</span></div>
<p><pre class="crayon-plain-tag">do_munmap()</pre> 函数用于从特定进程地址空间中删除指定的地址区间：</p>
<pre class="crayon-plain-tag">//如果成功返回0，否则返回负数作为错误码
int do_munmap(
    //内存描述符，指明地址空间
    struct mm_struct *mm,
    //被删除区间的起始地址
    unsigned long start,
    //被删除区间的长度
    size_t len
);</pre>
<p>相应的，系统调用 <pre class="crayon-plain-tag">munmap()</pre> 允许进程从自身地址空间删除指定区间：</p>
<pre class="crayon-plain-tag">//声明
int munmap(void *start, size_t length);
//对应实现，对do_munmap()的简单包装
asmlinkage long sys_munmap( unsigned long addr, size_t len )
{
    int ret;
    struct mm_struct *mm;
    mm = current-&gt;mm;
    down_write( &amp;mm-&gt;mmap_sem );
    ret = do_munmap( mm, addr, len );
    up_write( &amp;mm-&gt;mmap_sem );
    return ret;
}</pre>
<div class="blog_h2"><span class="graybg"><a id="page-table"></a>页表</span></div>
<p>应用程序操作的是虚拟地址，而CPU直接操作的是物理地址，虚拟地址到物理地址的转换通过页表机制完成。Linux使用<span style="background-color: #c0c0c0;">三级页表</span>完成地址转换（包括不支持三级页表的体系结构，例如仅支持<span style="background-color: #c0c0c0;">两级页表</span>或者<span style="background-color: #c0c0c0;">散列表</span>的体系结构），<span style="background-color: #c0c0c0;">多级页表可以节约内存空间</span>。在大部分体系结构上，页表的查找和处理是由硬件完成，但是作为前提，<span style="background-color: #c0c0c0;">内核必须正确的设置页表</span>。页表的基本原理是：将虚拟地址<span style="background-color: #c0c0c0;">分段（chunk）</span>，每段虚拟地址作为索引指向页表（table），而页表项指向<span style="background-color: #c0c0c0;">下一级页表或者物理页</span>：</p>
<ol>
<li>顶级页表称为<span style="background-color: #c0c0c0;">“页全局目录”（PGD）</span>，它是一个pgd_t类型（大部分体系结构上是unsigned long）的数组，该数组的条目指向二级页表的条目</li>
<li>二级页表称为<span style="background-color: #c0c0c0;">“页中间目录（PMD）”</span>，它是pmd_t类型的数组，该数组的条目又指向三级页表中的条目</li>
<li>三级页表就叫做<span style="background-color: #c0c0c0;">页表</span>，它是页表条目pte_t的数组，页表条目指向物理页</li>
</ol>
<p>从虚拟地址转换为物理地址的过程如下图所示：</p>
<p><img class="aligncenter size-full wp-image-9122" src="https://blog.gmem.cc/wp-content/uploads/2011/02/page-table.png" alt="page-table" width="90%" /></p>
<p><span style="background-color: #c0c0c0;">每个进程都有自己的页表</span>（线程会共享页表），内存描述符的<pre class="crayon-plain-tag">pgd</pre> 字段就指向进程的页全局目录，操作页表时必须持有<pre class="crayon-plain-tag">page_table_lock</pre> 锁。页表对应的结构体依赖于体系结构，定义在相应的<pre class="crayon-plain-tag">asm/page.h</pre> </p>
<div class="blog_h3"><span class="graybg">TLB</span></div>
<p>由于每次对虚拟内存中页面的访问都用到页表，因此其搜索性能非常关键。为<span style="background-color: #c0c0c0;">提升性能</span>，很多体系结构都实现了<span style="background-color: #c0c0c0;">翻译后备缓冲（Translation lookaside buffer，TLB，也叫快表）</span>，TLB是将<span style="background-color: #c0c0c0;">虚拟地址映射到物理地址的硬件缓存</span>。有了TLB后，处理器都会优先检查TLB，如果缓存命中，则不去搜索页表。</p>
<p>TLB保存了最高频被访问的页表项。</p>
<div id="page-cache-and-writeback" class="blog_h1"><span class="graybg">页面缓存和页回写</span></div>
<p>物理内存的一大优势就是可以作为磁盘或者其它块设备的高速缓存，这是因为磁盘非常慢，常常成为系统的性能瓶颈。使用内存作为缓存后，可以延迟写磁盘的时间，或者避免不必要的读磁盘操作。</p>
<p>Linux内核实现了一种磁盘数据的缓存：<span style="background-color: #c0c0c0;">页缓存（Page cache）</span>，该缓存把磁盘数据存放在物理内存中，以最小化磁盘I/O。为保证数据一致性，内核必须把页缓存中发生变更的数据同步到磁盘上，此过程称为<span style="background-color: #c0c0c0;">页回写（Writeback）</span>。</p>
<p>页缓存是现代OS不可或缺的组件，因为：</p>
<ol>
<li>磁盘访问速度比内存差几个数量级，缓存可以显著提高性能</li>
<li>某个数据被访问后，<span style="background-color: #c0c0c0;">该数据或者临近的数据</span>在一定时间内很可能被<span style="background-color: #c0c0c0;">密集的重复访问</span>，这就是所谓的<span style="background-color: #c0c0c0;">时间局部性（Temporal locality）</span>。时间局部性意味着页缓存常常具有很高的命中率</li>
</ol>
<div class="blog_h2"><span class="graybg">缓存手段</span></div>
<p>页缓存由内存中的<span style="background-color: #c0c0c0;">物理页</span>组成，其中存放着对应的<span style="background-color: #c0c0c0;">磁盘物理块</span>，其数据被缓存的磁盘称为后备存储（Backing store）。页缓存的大小是动态变化的，它可能<span style="background-color: #c0c0c0;">增长以消耗所有空闲内存</span>，或者收缩以减轻内存压力。</p>
<p>当内核开始一个读操作（例如read系统调用）时，会首先检查数据是否存在于页缓存中，如果存在，则直接返回，否则内核需要调度I/O操作，从磁盘读取数据。</p>
<p>相应的，当内核执行写操作时，可能有三种方式和页缓存交互：</p>
<ol>
<li>不缓存：即页缓存不去缓存任何写操作，写操作跳过缓存直接写入磁盘，同时让缓存中对应的数据失效</li>
<li>自动更新缓存：写操作在把数据写入磁盘的同时，更新内存缓存，这种方式称为“Write-through cache”</li>
<li>回写：这是Linux使用的方式。程序的写操作仅仅写入到缓存中，不更新磁盘。更新操作由<span style="background-color: #c0c0c0;">回写进程</span>在合适的时候刷出脏页到磁盘</li>
</ol>
<div class="blog_h3"><span class="graybg">缓存清除</span></div>
<p>当需要：</p>
<ol>
<li>收缩缓存提供内存给<span style="background-color: #c0c0c0;">其它程序</span>使用时</li>
<li>将缓存中不重要的数据移除，<span style="background-color: #c0c0c0;">腾出空间给重要数据</span>缓存时</li>
</ol>
<p>需要进行缓存回收。Linux的缓存回收策略是：<span style="background-color: #c0c0c0;">替换非脏页，如果没有足够非脏页，则强制发起回写操作</span>。决定哪些页被替换是最困难的部分，以下算法较为常见：</p>
<ol>
<li>最近最少使用（LRU）算法：该算法跟踪每个页面的访问踪迹，以便回收时间戳最老的页面。该策略工作良好是基于这样的假设：如果缓存的数据越久没有被访问，则不太可能在近期被访问。LRU算法对那些仅仅被访问一次（最近访问一次，以后永远不会访问）的文件来说尤其失败</li>
<li>双链策略（Two-List Strategy）：为解决LRU算法的缺陷，Linux对其进行改进，它使用两个列表（而不是LRU那样的单列表）：<span style="background-color: #c0c0c0;">活动列表、非活动列表</span>。活动列表中的页是“热”的，不会被清除；非活动列表中的页则可以被清除。仅当<span style="background-color: #c0c0c0;">处于非活动列表中的页被访问后，该页才会进入活动列表</span>。两个列表都使用伪LRU（pseudo-LRU）方式管理：条目从尾部加入、头部移除（类似队列）。两个列表的大小保持<span style="background-color: #c0c0c0;">动态平衡</span>——如果活动列表过大，那么其列表头被移回非活动列表的尾部。双联策略解决了LRU中“仅一次”问题</li>
<li>双链策略可以泛化为多链策略</li>
</ol>
<div class="blog_h2"><span class="graybg">Linux页缓存</span></div>
<p>尽管System V引入的页缓存机制是专用来缓存文件系统数据的，但是Linux的目标是支持任何<span style="background-color: #c0c0c0;">基于页的数据</span>，Linux的页缓存内容可以来自<span style="background-color: #c0c0c0;">普通文件系统文件、块设备文件、内存映射文件</span>，等等。注意页缓存不一定缓存整个文件，可能缓存文件中的几个页。</p>
<p>缓存中的一页可以包含<span style="background-color: #c0c0c0;">多个不连续的物理磁盘块</span>（以x86为例，页大小4KB，而磁盘块一般512B，一个页可以存放8个块，此外文件本身很可能是分散存放在磁盘上的，因此这些块可能不连续），因此，检查某个页中是否包含特定的数据比较困难（否则的话，每个页只需要使用设备名称+块序号即可索引）。</p>
<div class="blog_h3"><span class="graybg">address_space对象</span></div>
<p>为了避免和文件系统耦合，实现更加通用的页缓存，Linux专门设计了一个新结构<span style="background-color: #c0c0c0;">管理页缓存中的条目</span>：<pre class="crayon-plain-tag">address_space</pre> ，可以认为该对象是虚拟内存区域<pre class="crayon-plain-tag">vm_area_struct</pre> 的<span style="background-color: #c0c0c0;">物理对应物</span>。打个比方，假设一个文件有10个虚拟内存区域（5个进程分别映射其2次），但是该文件<span style="background-color: #c0c0c0;">只会对应一个</span>address_space对象。</p>
<p>address_space这个命名具有误导性，可能叫page_cache_entity/physical_pages_of_a_file更加合理。该结构的定义如下：</p>
<pre class="crayon-plain-tag">struct address_space
{
    //此映射（当前页缓存条目）关联某种内核对象，一般就是inode，如果不是关联到inode（例如关联到swapper）则该字段为空
    struct inode *host; /* 拥有此映射的inode */
    struct radix_tree_root page_tree; /* 所有页面组成的基数树 */
    spinlock_t tree_lock; /* page_tree的锁 */
    unsigned int i_mmap_writable; /* VM_SHARED计数 */
    /**
     * 优先搜索树，包含此映射中全部共享、私有的映射页面
     * 该树很好的结合了堆和基数树，可以允许内核快速的找到与目标文件关联的映射
     */
    struct prio_tree_root i_mmap;
    struct list_head i_mmap_nonlinear; /* VM_NONLINEAR 链表 */
    spinlock_t i_mmap_lock; /* i_mmap的锁 */
    atomic_t truncate_count; /* 截断计数 */
    unsigned long nrpages; /* 此条目包含的页总数 */
    pgoff_t writeback_index; /* 回写的起始偏移量 */
    struct address_space_operations *a_ops; /* 操作列表 */
    unsigned long flags; /* gfp_mask和错误标记 */
    struct backing_dev_info *backing_dev_info; /* 预读信息 */
    spinlock_t private_lock; /* 私有锁 */
    struct list_head private_list; /* 私有链表 */
    struct address_space *assoc_mapping; /* 相关的缓冲 */
};</pre>
<div class="blog_h3"><span class="graybg">address_space操作</span></div>
<p>address_space对象的<pre class="crayon-plain-tag">a_ops</pre> 字段定义操控页缓存映射的操作：</p>
<pre class="crayon-plain-tag">/**
 * 定义了管理页缓存的各种行为，例如读取、更新缓存数据
 */
struct address_space_operations
{
    int (*writepage)( struct page *, struct writeback_control * );
    int (*readpage)( struct file *, struct page * );
    int (*sync_page)( struct page * );
    int (*writepages)( struct address_space *, struct writeback_control * );
    int (*set_page_dirty)( struct page * );
    int (*readpages)( struct file *, struct address_space *, struct list_head *, unsigned );
    int (*write_begin)( struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned flags,
        struct page **pagep, void **fsdata );
    int (*write_end)( struct file *, struct address_space *mapping, loff_t pos, unsigned len, unsigned copied,
        struct page *page, void *fsdata );
    sector_t (*bmap)( struct address_space *, sector_t );
    int (*invalidatepage)( struct page *, unsigned long );
    int (*releasepage)( struct page *, int );
    int (*direct_IO)( int, struct kiocb *, const struct iovec *, loff_t, unsigned long );
    int (*get_xip_mem)( struct address_space *, pgoff_t, int, void **, unsigned long * );
    int (*migratepage)( struct address_space *, struct page *, struct page * );
    int (*launder_page)( struct page * );
    int (*is_partially_uptodate)( struct page *, read_descriptor_t *, unsigned long );
    int (*error_remove_page)( struct address_space *, struct page * );
};</pre>
<p>每个后备存储都实现了自己的<pre class="crayon-plain-tag">address_space_operations</pre> ，例如ext3文件系统的实现如下：</p>
<pre class="crayon-plain-tag">static const struct address_space_operations ext3_ordered_aops = {
	.readpage		= ext3_readpage,
	.readpages		= ext3_readpages,
	//...
};</pre>
<p>这些操作中，<pre class="crayon-plain-tag">readpage() </pre> 和<pre class="crayon-plain-tag">writepage()</pre> 最重要，分别对应了页的读写操作。</p>
<p>页读操作的步骤如下：</p>
<ol>
<li>内核尝试在页缓存中找到需要的数据：<br />
<pre class="crayon-plain-tag">struct page *find_get_page(struct address_space *mapping, pgoff_t offset);</pre></p>
<p>参数mapping指定address_space，参数offset则指页偏移（即被缓存的文件偏移）</p>
</li>
<li>如果搜索的页没有存在页缓存中，上面的函数返回NULL，内核将分配一个新页面，并将之前搜索的页加入页缓存：<br />
<pre class="crayon-plain-tag">struct page *page;
int error;
/* 分配页 */
page = page_cache_alloc_cold( mapping );
if (!page){/* 内存分配失败 */}
/* ... 把新页加到页缓存 */
error = add_to_page_cache_lru(page, mapping, index, GFP_KERNEL);
if (error) {/*加入页缓存失败 */}</pre>
</li>
<li>然后，所需的数据从磁盘读入，加入页缓存，并返回给用户：<br />
<pre class="crayon-plain-tag">error = mapping-&gt;a_ops-&gt;readpage(file, page);</pre>
</li>
</ol>
<p>页写操作不太一样，对于文件映射，当页被修改时，仅仅需要设置脏标记：<pre class="crayon-plain-tag">SetPageDirty(page);</pre> 。内核会在稍后通过<pre class="crayon-plain-tag">writepage()</pre> 刷出页面修改到磁盘，具体步骤较为复杂，概括起来包括以下步骤：</p>
<ol>
<li>搜索页缓存，找到需要的页。如果目标页不在缓存中，则分配一个新的空闲页<br />
<pre class="crayon-plain-tag">page = __grab_cache_page(mapping, index, &amp;cached_page, &amp;lru_pvec);</pre>
</li>
<li>创建一个写请求<br />
<pre class="crayon-plain-tag">status = a_ops-&gt;prepare_write(file, page, offset, offset+bytes);</pre>
</li>
<li>将数据从用户空间拷贝到内核空间<br />
<pre class="crayon-plain-tag">page_fault = filemap_copy_from_user(page, offset, buf, bytes);</pre>
</li>
<li>将数据写出到磁盘<br />
<pre class="crayon-plain-tag">status = a_ops-&gt;commit_write(file, page, offset, offset+bytes); </pre>
</li>
</ol>
<p>所有的页I/O操作都需要执行上面的步骤，因此<span style="background-color: #c0c0c0;">所有页I/O必然通过页缓存进行</span>。内核总是尝试先通过页缓存来满足读请求；对于写操作，页缓存更像是一个存储平台，所有要写出的页都加入到页缓存。</p>
<div class="blog_h3"><span class="graybg">基数（Radix）树</span></div>
<p>由于内核每次进行页I/O操作都需要检查目标页是否在缓存中存在，因此缓存检索必须足够快。如前面所见，搜索是通过一个address_space和offset进行的。每个address_space都包含一个唯一的radix树，对应字段page_tree。Radix树是二叉树的变体，通过它可以<span style="background-color: #c0c0c0;">快速的查找需要的页</span>（只需要提供文件偏移量），<pre class="crayon-plain-tag">find_get_page()</pre> 、<pre class="crayon-plain-tag">radix_tree_lookup()</pre> 等函数都是通过检索Radix树来工作的。</p>
<p>Radix树的代码位于<pre class="crayon-plain-tag">lib/radix-tree.c</pre> ，使用该树需要包含头文件<pre class="crayon-plain-tag">linux/radix-tree.h</pre> </p>
<div class="blog_h3"><span class="graybg">旧的页散列表机制</span></div>
<p>在2.6-的内核中，页缓存通过<span style="background-color: #c0c0c0;">一个全局散列表</span>而不是Radix树进行检索，该散列表维护系统中的所有页，此散列表的值是散列计算结果相同的缓存条目构成的双向链表。全局散列表有以下缺陷：</p>
<ol>
<li>一个全局锁保护此散列表，锁争用导致性能问题</li>
<li>散列表过大，只有映射当前正在操作的文件的页才相关</li>
<li>散列查找失败后的性能较差</li>
</ol>
<div class="blog_h2"><span class="graybg">缓冲区缓存（Buffer Cache）</span></div>
<p>通过块I/O缓冲，单个的磁盘块也被存入到页缓存中，块I/O缓冲是单个磁盘块的内存映射，是内存页-磁盘块映射的描述符。我们把这一映射称为<span style="background-color: #c0c0c0;">缓冲缓存（Buffer cache）</span>，它作为页缓存的一部分实现。</p>
<p>历史上缓存缓存、页缓存是两个完全不同的缓存，一个磁盘块可能同时出现在这两个缓存中，在2.4它们被合并了，从而避免了两个缓存之间的同步开销和内存浪费。尽管如此，作为块内存映射描述符的buffer_head仍然被内核使用。</p>
<div class="blog_h2"><span class="graybg">Flusher线程</span></div>
<p>由于页缓存的存在，写操作实际上会被延迟，当页缓存中的数据比后备存储中新时，我们称其为脏数据。内存中积累的脏页必须最终被写回到磁盘。在以下三种情况下，回写发生：</p>
<ol>
<li>当<span style="background-color: #c0c0c0;">空闲内存低于指定阈值</span>时，内核必须回写脏页以释放内存——因为只有干净页才能被回收</li>
<li>当脏页驻留内存<span style="background-color: #c0c0c0;">时间超过指定的阈值</span>时，内核将超时的脏页写回磁盘</li>
<li>当用户进程调用<pre class="crayon-plain-tag">sync()</pre> 、<pre class="crayon-plain-tag">fsync()</pre> 系统调用时，内核按要求执行回写</li>
</ol>
<p>这三项操作的目的完全不同，在旧内核中它们是由两个独立内核线程分别完成的。在2.6则是由一组内核线程——Flusher线程执行所有这三项操作。</p>
<p>第一项操作的目的是物理内存不足时，释放脏页以获得内存，何时启动该操作由内核参数（sysctl）：<span style="background-color: #c0c0c0;">dirty_background_ratio</span>指定。当空闲内存占比小于此值时，内核调用<pre class="crayon-plain-tag">wakeup_flusher_threads()</pre> 唤醒一个或者多个Flusher，Flusher会调用<pre class="crayon-plain-tag">bdi_writeback_all ()</pre> 并开始回写脏页。该函数接受一个参数，用于指定需要回写的页数量，该函数会一直运行，直到满足条件（或者没有脏页）：</p>
<ol>
<li>指定的最小回写页数量到达</li>
<li>空闲内存占比大于dirty_background_ratio</li>
</ol>
<p>对于第二项操作，Flusher会定期唤醒，检查过期的脏页并回写。在系统启动后，定时器被设置，定期唤醒Flusher并执行<pre class="crayon-plain-tag">wb_writeback()</pre> 函数，该函数会把所有变脏超过<span style="background-color: #c0c0c0;">dirty_expire_interval</span>毫秒的页写出到磁盘。</p>
<p>Flusher线程的代码存放在<pre class="crayon-plain-tag">mm/page-writeback.c</pre> 和<pre class="crayon-plain-tag">mm/backing-dev.c</pre> ，回写相关逻辑位于<pre class="crayon-plain-tag">fs/fs-writeback.c</pre> </p>
<div class="blog_h3"><span class="graybg">笔记本模式</span></div>
<p>笔记本模式（Laptop mode）是一种特殊的页回写策略，其目的是最小化硬盘转动的机械行为，允许尽可能长的磁盘停滞，也延迟电池续航。该模式可以通过<pre class="crayon-plain-tag">/proc/sys/vm/laptop_mode</pre> 配置。和传统页回写行为相比，笔记本模式会增加额外判断，以<span style="background-color: #c0c0c0;">避免主动激活磁盘</span>运行。</p>
<p>多数Linux发行版在计算机接上/拔掉电池时，自动开启/关闭笔记本模式。</p>
<div class="blog_h3"><span class="graybg">Flusher为什么要多线程</span></div>
<p>如果仅仅使用一个Flusher线程，那么很可能在<span style="background-color: #c0c0c0;">回写任务繁重时出现阻塞</span>。线程可能阻塞在单个繁忙的设备队列上（队列由等待提交到磁盘的I/O请求构成），导致其它设备的请求队列不能得到即时处理。为避免单个设备队列的拥塞影响整体性能，Flusher使用多线程模式，并且让<span style="background-color: #c0c0c0;">每个设备对应一个Flusher线程</span>。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol3">Linux内核学习笔记（三）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-study-note-vol3/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核学习笔记（二）</title>
		<link>https://blog.gmem.cc/linux-kernel-study-note-vol2</link>
		<comments>https://blog.gmem.cc/linux-kernel-study-note-vol2#comments</comments>
		<pubDate>Wed, 05 Jan 2011 10:02:26 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=9078</guid>
		<description><![CDATA[<p>x86平台中，一个进程调用C库函数读取文件内容，其经历的处理步骤可能如下： 在用户空间，进程调用fread()库函数 库函数触发read()系统调用 0x80软中断被触发，CR0寄存器的PE位被置0，进入内核态 read()系统调用对VFS层发起调用file_operations.read()，VFS则调用实际的文件系统的函数实现 文件系统向块I/O层发起请求，当前进程在内核中睡眠 I/O调度程序向硬盘发送悬挂的请求 硬盘寻道，读取数据完毕，触发硬件中断，内核在中断上下文中处理 中断处理程序拷贝数据到指定的缓冲区，并唤醒等待队列上的进程 时钟中断发生，进程被调度，从内核态醒来 系统调用返回，CR0寄存器PE位被置3，进入用户态 fread()函数返回 特权和内核态 Intel 的 x86 架构的 CPU 提供了 0 到 3 <a class="read-more" href="https://blog.gmem.cc/linux-kernel-study-note-vol2">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol2">Linux内核学习笔记（二）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><p>x86平台中，一个进程调用C库函数读取文件内容，其经历的处理步骤可能如下：</p>
<ol>
<li>在用户空间，进程调用fread()库函数</li>
<li>库函数触发read()系统调用</li>
<li>0x80软中断被触发，CR0寄存器的PE位被置0，进入内核态</li>
<li>read()系统调用对VFS层发起调用file_operations.read()，VFS则调用实际的文件系统的函数实现</li>
<li>文件系统向块I/O层发起请求，当前进程在内核中睡眠</li>
<li>I/O调度程序向硬盘发送悬挂的请求</li>
<li>硬盘寻道，读取数据完毕，触发硬件中断，内核在中断上下文中处理</li>
<li>中断处理程序拷贝数据到指定的缓冲区，并唤醒等待队列上的进程</li>
<li>时钟中断发生，进程被调度，从内核态醒来</li>
<li>系统调用返回，CR0寄存器PE位被置3，进入用户态</li>
<li>fread()函数返回</li>
</ol>
<div class="blog_h1"><span class="graybg">特权和内核态</span></div>
<p>Intel 的 x86 架构的 CPU <span style="background-color: #c0c0c0;">提供了 0 到 3 四个特权级，数字越小，特权越高</span>，Linux 操作系统中主要<span style="background-color: #c0c0c0;">采用了 0 和 3 两个特权级，分别对应的就是内核态和用户态</span>。</p>
<p>运行于<span style="background-color: #c0c0c0;">用户态的进程可以执行的操作和访问的资源都会受到极大的限制</span>，而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态，但在执行的过程中，一些操作需要在内核权限下才能执行，这就涉及到一个<span style="background-color: #c0c0c0;">从用户态切换到内核态</span>的过程。比如 C 函数库中的内存分配函数 malloc()，它使用 sbrk() 系统调用来分配内存，当 malloc 调用 sbrk() 的时候就涉及一次从用户态到内核态的切换，类似的函数还有 printf()，调用的是 wirte() 系统调用来输出字符串，非常普遍。</p>
<p>用户进程切换到内核态的方式主要有三种：</p>
<ol>
<li>系统调用：用户进程主动发起的操作。用户态进程发起系统调用主动要求切换到内核态，陷入内核之后，由操作系统来操作系统资源，完成之后再返回到进程</li>
<li>异常：被动的操作，且用户进程无法无法预测其发生的时机。当用户进程在运行期间发生了异常（比如某条指令出了问题），这时会触发由当前运行进程切换到处理此异常的内核相关进程中，也即是切换到了内核态。异常包括程序运算引起的各种错误如除 0、缓冲区溢出、缺页等</li>
<li>中断：当外围设备完成用户请求的操作后，会向 CPU 发出相应的中断信号，这时 CPU 会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序去执行，如果前面执行的指令是用户态下的程序，那么转换的过程自然就会是从用户态到内核态的切换。中断包括 I/O 中断、外部信号中断、各种定时器引起的时钟中断等。中断和异常类似，都是通过中断向量表来找到相应的处理程序进行处理。区别在于，<span style="background-color: #c0c0c0;">中断来自处理器外部，不是由任何一条专门的指令造成，而异常是执行当前指令的结果</span></li>
</ol>
<div class="blog_h1"><span class="graybg">系统调用</span></div>
<p>为了便于用户进程和内核交互，现代OS都提供了一组接口，让应用能够<span style="background-color: #c0c0c0;">受限</span>的访问硬件、创建新进程、和既有进程通信，以及申请其它OS资源。这组接口一般称为系统调用，它的主要出发点是保证系统的稳定性，防止应用破坏系统。</p>
<div class="blog_h2"><span class="graybg">与内核通信</span></div>
<p>系统调用在用户进程和硬件设备之间设立中间层，目的有三：</p>
<ol>
<li>为用户空间<span style="background-color: #c0c0c0;">提供硬件的抽象接口</span>，而不顾硬件的规格（例如磁盘介质类型）</li>
<li><span style="background-color: #c0c0c0;">保证系统的稳定性和安全性</span>，内核可以基于用户类型、权限等规则对访问进行控制</li>
<li>让每个进程能运行在<span style="background-color: #c0c0c0;">独立的虚拟系统</span>中，以实现虚拟内存和多任务</li>
</ol>
<p><span style="background-color: #c0c0c0;">系统调用是用户空间访问内核的唯一手段</span>，<span style="background-color: #c0c0c0;">除了异常（exceptions）和陷入（traps）</span>，它是进入内核的唯一<span style="background-color: #c0c0c0;">合法入口</span>。</p>
<div class="blog_h2"><span class="graybg">系统调用</span></div>
<p>要执行系统调用（Linux称为syscall），一般通过C库中定义的函数来进行，这些系统调用一般具有0-N个入参，一个long型的返回值，并且对系统具有副作用（side effects）。返回非零值往往意味着错误，C库会把错误码写入<pre class="crayon-plain-tag">errno</pre> 全局变量，应用程序可以调用<pre class="crayon-plain-tag">perror()</pre> 将errno解析为有意义的字符串。</p>
<p>系统调用通过SYSCALL_DEFINE系列宏来定义，例如getpid系统调用：</p>
<pre class="crayon-plain-tag">//asmlinkage是一个编译指令，提示编译器仅从栈中提取函数入参
//long型返回值保证32/64位系统兼容
#define SYSCALL_DEFINE0(name)	   asmlinkage long sys_##name(void)
//SYSCALL_DEFINE0表示无入参系统调用
SYSCALL_DEFINE0( getpid )
{
    return task_tgid_vnr( current );
}

//展开后的形式为：
asmlinkage long sys_getpid(void) { //sys_是所有系统调用命名的统一前缀
    return task_tgid_vnr( current )
}</pre>
<div class="blog_h3"><span class="graybg">系统调用号</span></div>
<p>Linux中的每个系统调用被分配一个<span style="background-color: #c0c0c0;">唯一的系统调用号</span>，即使在后续的内核版本中，某个系统调用被删除（尽管很罕见），该调用号也不能重用，而是关联到<pre class="crayon-plain-tag">sys_ni_call()</pre> ，该函数仅仅返回一个<pre class="crayon-plain-tag">-ENOSYS</pre> </p>
<p>进程不会提及系统调用的名称，而<span style="background-color: #c0c0c0;">只是使用系统调用号来执行系统调用</span>。</p>
<p>内核记录所有已注册的系统调用的列表，存放在<pre class="crayon-plain-tag">sys_call_table</pre> ，该表和体系结构相关，对于x86_64，它定义在/arch/x86/kernel/syscall_64.c中，该表为每个有效的系统调用分配调用号：</p>
<pre class="crayon-plain-tag">// /arch/x86/kernel/syscall_64.c
#define __SYSCALL(nr, sym) [nr] = sym,
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/*
	*Smells like a like a compiler bug -- it doesn't work
	*when the &amp; below is removed.
	*/
	[0 ... __NR_syscall_max] = &amp;sys_ni_syscall,
#include &lt;asm/unistd_64.h&gt;
};

// /arch/x86/include/asm/unistd_64.h
#define __NR_read				0
__SYSCALL(__NR_read, sys_read)
#define __NR_write				1
__SYSCALL(__NR_write, sys_write)
#define __NR_open				2
__SYSCALL(__NR_open, sys_open)
……</pre>
<div class="blog_h3"><span class="graybg">系统调用的性能</span></div>
<p>相比起其它OS，Linux的系统调用执行速度更快，原因是：</p>
<ol>
<li>Linux上下文切换消耗的时间很短，进出内核都被优化的很高效</li>
<li>系统调用和系统调用处理程序本身被设计的很简洁</li>
</ol>
<div class="blog_h2"><span class="graybg">系统调用处理程序</span></div>
<p>内核运行在<span style="background-color: #c0c0c0;">受保护的地址空间</span>上，因此用户空间的程序不能直接调用系统调用（内核代码），否则系统安全性和稳定性被破坏。因此，需要一种合理的方式通知内核需要执行一个系统调用，并由内核自己去执行。</p>
<p>通知内核的机制依靠<span style="background-color: #c0c0c0;">软中断</span>来实现：通过触发一个异常，促使系统<span style="background-color: #c0c0c0;">切换到内核态去执行异常处理程序</span>。在x86体系结构中，预定义的<span style="background-color: #c0c0c0;">软中断是中断号128</span>，通过<pre class="crayon-plain-tag">int $0x80</pre> 指令可以触发该中断，该指令会触发一个异常，导致切换到内核态并执行<span style="background-color: #c0c0c0;">第128号异常处理程序——该程序恰恰就是系统调用处理程序</span>。系统调用出来程序的名称为<pre class="crayon-plain-tag">system_call()</pre> ，其实现和体系结构密切相关。</p>
<p>新型的x86处理器添加了一条<pre class="crayon-plain-tag">sysenter</pre> 指令，可以更快的陷入内核执行系统调用，Linux已经支持该指令。</p>
<p>由于所有系统调用陷入内核的方式都一样，因此需要通过某种方式把系统调用号传递给内核。在<span style="background-color: #c0c0c0;">x86上，系统调用号通过eax寄存器传递</span>。在陷入内核前，用户空间已经负责把系统调用号存入eax。内核获取调用号后，在sys_call_table上查找，并找到相应的系统调用实现。</p>
<p>除了调用号，大部分系统调用还需要一些入参，这些入参也需要传递给内核。最简单的方式是同调用号一样，通过寄存器传递，在x86系统中，<span style="background-color: #c0c0c0;">ebx、ecx、edx、esi、edi可以存放前五个参数</span>。超过五个参数的系统调用比较少见，此时，使用<span style="background-color: #c0c0c0;">单个寄存器来存放所有入参在用户空间地址的指针</span>。</p>
<p>系统调用的返回值，需要传递给用户空间，在x86上<span style="background-color: #c0c0c0;">返回值存放在eax寄存器</span>中。</p>
<p>下面是系统调用执行过程的示意图：</p>
<p><img class="aligncenter size-full wp-image-8763" src="https://blog.gmem.cc/wp-content/uploads/2010/12/system-call-flow.png" alt="system-call-flow" width="80%" /></p>
<div class="blog_h2"><span class="graybg">系统调用的实现</span></div>
<p>如果要实现一个新的系统调用，那么首先要明确其功能，Linux提倡系统调用遵循单一职责的原则。然后，需要明确参数、返回值和错误码，参数应该尽可能少，尽量着眼未来，避免不必要的限制（通用化），另外不要做错误的假设（机器字长、字节序…）</p>
<p>应当避免实现新的系统调用，因为系统调用号由官方分配，而且内核稳定后即固化。</p>
<div class="blog_h3"><span class="graybg">参数验证</span></div>
<p>必须对系统调用的参数进行严格的验证：</p>
<ol>
<li>参数有效、合法性：例如I/O操作应当检测文件描述符有效性，进程操作应当检测PID有效性</li>
<li>指针检查：必须严格检查进程传入的指针：
<ol>
<li>指针指向的内存区域必须属于用户空间，防止进程诱骗内核读取内核空间的数据</li>
<li>指针指向的内存区域必须在进程的地址空间中，防止进程窃取其它进程的数据</li>
<li>如果是读操作，指针目标内存必须可读；写操作、执行操作类似。防止进程绕过内存访问限制</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">用户-内核数据拷贝</span></div>
<p>内核提供了两个函数，用于交换用户空间和内核空间的数据，同时完成必要的检查：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>copy_to_user() </td>
<td>
<p>将内核中的数据复制到用户空间，包含三个参数：dst进程地址空间中的目标地址；src内核空间中的源地址；size需要拷贝的字节数。如果执行失败，返回没能完成拷贝的字节数，如果成功返回0</p>
<p>注意：该函数可能引起阻塞，因为包含用户数据的页可能被交换在磁盘上，此时当前进程将会休眠，直到缺页处理程序将页换入内存</p>
</td>
</tr>
<tr>
<td>copy_from_user()</td>
<td>将用户空间的数据拷贝到内核。类似上面</td>
</tr>
</tbody>
</table>
<p>下面这个简单的系统调用说明了这两个函数的用法：</p>
<pre class="crayon-plain-tag">//使用内核中的一个long作为中转，将src拷贝到dst
SYSCALL_DEFINE3( silly_copy,
    unsigned long *, src,
    unsigned long *, dst,
    unsigned long len )
{
    unsigned long buf; //内核中的缓冲区
    if ( copy_from_user( &amp;buf, src, len ) ) //复制到内核
    return -EFAULT;
    if ( copy_to_user( dst, &amp;buf, len ) ) //复制到用户
    return -EFAULT;
    return len;
}</pre>
<div class="blog_h3"><span class="graybg">权限检查</span></div>
<p>必须检查进程的用户是否有足够的权限来执行系统调用。老版本的内核提供一个粗粒度的函数<pre class="crayon-plain-tag">suser()</pre> 来判断是否具有超级用户权限。现在则可以进行<span style="background-color: #c0c0c0;">细粒度的权限控制</span>，<pre class="crayon-plain-tag">capable()</pre> 函数可以用来检查用户是否有权对特定的资源进行操作，例如<pre class="crayon-plain-tag">capable(CAP_SYS_NICE)</pre> 用来检查当前用户是否有资格调整其它进程的nice值。</p>
<p>默认情况下，超级用户具有所有权限，其它用户没有任何权限。下面的例子展示了reboot系统调用如何进行权限检查：</p>
<pre class="crayon-plain-tag">SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
		void __user *, arg)
{
	char buffer[256];
	int ret = 0;

	/* We only trust the superuser with rebooting the system. */
	if (!capable(CAP_SYS_BOOT))
		return -EPERM;</pre>
<p>/include/linux/capability.h包含了所有权限定义的列表。 </p>
<div class="blog_h3"><span class="graybg">新系统调用的注册</span></div>
<p>一个系统调用编写完后，需要注册：</p>
<ol>
<li>在系统调用表中，将其注册为最后一个表项。对于每个需要支持的体系结构的系统调用表，都需要做这样的工作。对于大部分体系结构，系统定义表位于<pre class="crayon-plain-tag">entry.s</pre> 中</li>
<li>对于每个需要支持的体系结构，必须把系统调用号定义在<pre class="crayon-plain-tag">asm/unistd.h</pre> </li>
<li><span style="background-color: #c0c0c0;">系统调用必须编译进内核映像</span>，而不是内核模块，这意味着系统调用代码必须存放在<pre class="crayon-plain-tag">/kernel</pre> 目录下的某个源文件中，例如<pre class="crayon-plain-tag">sys.c</pre> ，该文件包含大量系统调用</li>
</ol>
<div class="blog_h3"><span class="graybg">从用户空间访问系统调用</span></div>
<p>一般系统调用靠C库来支持，用户程序通过包含头文件，并与C库链接，即可使用系统调用。对于自定义的系统调用，可以通过Linux提供的一组宏来直接调用，而不需要C库的支持。这些宏命名为<pre class="crayon-plain-tag">_syscalln()</pre> ，下面以open系统调用为例说明：</p>
<pre class="crayon-plain-tag">//open系统调用的签名：
long open( const char *filename, int flags, int mode );

//不依赖库的系统调用直接使用形式
/**
 * 下面的这个宏定义在asm/unistd.h中，对应了系统调用号
 */
#define __NR_open 5
/**
 * 对于每个宏，具有2 + 2 * n个参数
 * 第一个参数为系统调用返回值类型
 * 第二个参数为系统调用的名称
 * 后面的2 * n个参数为系统调用入参类型、名称对
 * 
 * 这些宏会被扩展为内联汇编的C函数，由汇编语言负责把系统调用号、入参压入寄存器，并触发软中断导致陷入内核
 */
_syscall3(long, open, const char *, filename, int, flags, int, mode)</pre>
<div class="blog_h2"><span class="graybg">系统调用上下文</span></div>
<p>内核<span style="background-color: #c0c0c0;">执行系统调用时，处于进程上下文</span>——current指针指向引发系统调用的那个进程。<span style="background-color: #c0c0c0;">在进程上下文中内核可以休眠或者被抢占</span>：休眠能力为内核编程带来很大便利；可抢占性意味着和用户空间内的进程一样，<span style="background-color: #c0c0c0;">处于内核中的当前进程同样可以被抢占</span>。由于获得CPU的新进程可以执行同一系统调用，这要求系统调用必须<span style="background-color: #c0c0c0;">是可重入的</span>。</p>
<p>与之相反的，当内核在执行<span style="background-color: #c0c0c0;">中断处理程序时，则不能休眠</span>，因此中断处理程序相对于系统调用，能够进行的操作受到很大限制。</p>
<p>当系统调用返回时，控制权仍然在<pre class="crayon-plain-tag">system_call()</pre> 手中，它负责切换到用户空间，并让用户进程继续执行下去。</p>
<div class="blog_h1"><span class="graybg">中断和中断处理</span></div>
<p>对连接到计算的设备（硬盘、键盘、显卡、网卡……）进行有效管理是任何操作系统的核心任务之一。</p>
<p>相比起CPU，外围硬件的速度非常慢，因此，如果让CPU向硬件发送请求并等待响应，显然是性能低劣的做法。既然如此，就应该在硬件处理请求期间，让内核去处理其它事务，等硬件真正完成处理后，再去处理响应。</p>
<p>那么，CPU怎么知道硬件完成处理了呢？轮询（Polling）是一种可能的方法，但是会让内核做很多无用功，更好的办法是允许硬件完成处理后，<span style="background-color: #c0c0c0;">主动向内核推送一个信号</span>，这就是中断机制。</p>
<div class="blog_h2"><span class="graybg">中断</span></div>
<p>从硬件角度来说，中断使得硬件能够发送通知给处理器，例如，当键盘击键时，键盘控制器会发送一个中断，通知处理器有按键被按下。处理器接收到信号后，马上向操作系统反馈，操作系统就会立即处理此中断。<span style="background-color: #c0c0c0;">硬件设备产生中断时，不会考虑与处理器时钟的同步</span>，也就是说，硬件中断随时都可能发生——内核随时可能被新到来的中断打断。</p>
<p>从物理学角度来说，中断的本质是一种特殊的电信号，由硬件设备生成，并<span style="background-color: #c0c0c0;">直接送入中断控制器的输入引</span>脚中。中断控制器是一个简单的芯片，其作用是汇集多路中断管线，采用复用技术，将它们连接到单个处理器管线。</p>
<p>不同硬件设备的<span style="background-color: #c0c0c0;">中断</span>不同，它们具有<span style="background-color: #c0c0c0;">唯一的数字标识</span>，因此操作系统能够识别中断来自硬盘还是键盘，进而调用不同的<span style="background-color: #c0c0c0;">中断处理程序</span>。这些中断值通常被称为<span style="background-color: #c0c0c0;">中断请求（IRQ）线</span>。在经典的PC上，IRQ 0是时钟中断，IRQ1是键盘中断，但是某些<span style="background-color: #c0c0c0;">中断号是动态分配</span>的，例如连接在PCI总线上的设备。不过中断号的数值本身并不重要，重要的是内核知道中断号和哪个设备关联。</p>
<div class="blog_h2"><span class="graybg">异常</span></div>
<p>讨论中断时，往往提及一个相关的概念——异常。与中断不同，<span style="background-color: #c0c0c0;">异常产生时必须与处理器时钟同步</span>，异常经常被称为<span style="background-color: #c0c0c0;">同步中断</span>。当处理器执行到<span style="background-color: #c0c0c0;">错误指令</span>（比如除零）或者<span style="background-color: #c0c0c0;">特殊情况</span>（如缺页），必须<span style="background-color: #c0c0c0;">依靠内核处理</span>时，<span style="background-color: #c0c0c0;">处理器</span>就会<span style="background-color: #c0c0c0;">产生一个异常</span>。异常发生时<span style="background-color: #c0c0c0;">，内核代表产生异常的进程执行</span>。</p>
<p>由于很多体系结构下，<span style="background-color: #c0c0c0;">异常（处理器本身产生的同步中断）和中断（外围硬件产生的异步中断）</span>的处理方式相同，因而很多知识是通用的。</p>
<p>x86的<span style="background-color: #c0c0c0;">系统调用就是一种异常</span>，通过软中断指令，导致陷入内核，进而引起一种特殊的异常——系统调用处理程序异常。</p>
<div class="blog_h2"><span class="graybg">中断处理程序</span></div>
<p>中断一旦发生，<span style="background-color: #c0c0c0;">硬件一般会一直等待，直到CPU应答它为止</span>，因此CPU必须尽快的处理中断并先响应硬件。响应一个特定中断的时候，内核会执行一个称为<span style="background-color: #c0c0c0;">“中断处理程序（Interrupt service routine）”</span>的函数，不同中断（例如系统时钟中断和键盘中断）的函数亦不同。设备的中断处理程序是它的<span style="background-color: #c0c0c0;">设备驱动程序（Driver，用于对设备进行管理的内核代码）</span>的一部分，某个设备发起多种中断也是可能的。</p>
<p>在Linux中，中断处理程序是遵循特定参数格式的C函数。这些函数与内核其它函数的本质区别是，中断处理程序运行于特殊的上下文——<span style="background-color: #c0c0c0;">中断上下文</span>中。中断上下文有时也被称为“<span style="background-color: #c0c0c0;">原子上下文</span>”，因为它不可阻塞、不可被抢占（但是可能被其它中断处理程序中断）。这一原子特性也要求中断处理程序必须在尽可能短的时间内完成。</p>
<p>中断处理程序至少需要通知设备，中断已经收到。实际场景中，它往往需要完成复杂的工作，以网卡的中断处理程序为例，它需要：</p>
<ol>
<li>给网卡以中断应答</li>
<li>将网卡中的网络数据包拷贝到内存</li>
<li>将数据包转交给协议栈或者应用程序处理</li>
</ol>
<div class="blog_h2"><span class="graybg">上半部与下半部</span></div>
<p>中断处理程序的执行速度与其工作量是一对此消彼长的矛盾需求，因此，一般把中断处理切分为两部分：</p>
<ol>
<li>上半部（Top half）：<span style="background-color: #c0c0c0;">中断处理程序属于上半部</span>，接收到中断就立刻执行，但只执行具有严格时限的工作，例如应答或者复位硬件，这些工作是在<span style="background-color: #c0c0c0;">中断被禁止的情况下</span>完成的</li>
<li>下半部（Bottom half）：其它能够允许稍后执行的工作，推迟到下半部中，在适当的时机，下半部会被执行，其时所有中断被启用</li>
</ol>
<p>还是以网卡中断处理为例，当网卡接收到来自网络的数据包时，需要通过硬件中断通知内核，为了优化网络吞吐量和传输周期，网卡需要立即发出通知。内核也立即开始执行中断处理：</p>
<ol>
<li>上半部处理：通知网卡中断已收到，拷贝网络数据包到内存，并读取更多网络数据包，这些都属于紧迫而硬件相关的工作，因为网卡的缓冲区很小而又大小固定，拷贝数据的动作如果延迟到下半部，很可能导致网卡缓冲区溢出，而导致后续到达的数据被丢弃</li>
<li>结束中断处理程序，控制器回归先前被中断的程序</li>
<li>下半部处理：处理数据包，由协议栈和应用程序后续完成</li>
</ol>
<div class="blog_h2"><span class="graybg">注册中断处理程序</span></div>
<p>中断处理程序是设备驱动的组成部分，因此，驱动负责将其注册到内核。使用下面的函数可以注册中断处理程序：</p>
<pre class="crayon-plain-tag">/**
 * 注册中断处理程序，成功返回0，返回-EBUSY表示给定的中断线已经被使用
 * 注意：该函数可能会休眠（调用了kmalloc），因此不能在中断上下文或者其它不允许阻塞的代码中调用该函数
 */
extern int request_irq(
    /**
     * 需要分配的中断号（中断线的号码）
     * 某些设备是固定的，例如PC系统时钟和键盘；大部分其它设备可以通过探测获取或者编程动态确定
     */
    unsigned int irq,
    irq_handler_t handler, //指向实际中断处理函数的指针
    unsigned long flags, //中断处理程序标志
    const char *name, //与中断相关的设备的ASCII文本表示，例如PC键盘是“keyboard”
    /**
     * 用于共享中断线，当中断处理程序需要释放时dev提供一个唯一标识（cookie），
     * 以便从共享中断线的诸多处理程序中定位并删除当前程序
     * 
     * 内核每次调用中断处理程序时，都会传递该参数，实践中往往使用该参数传递驱动程序的设备结构
     */
    void *dev
    );
//下面是一个例子：
if (request_irq(irqn, my_interrupt, IRQF_SHARED, "my_device", my_dev))
{
    printk(KERN_ERR "my_device: cannot register IRQ %d\n", irqn);
    return -EIO;
}</pre>
<div class="blog_h3"><span class="graybg">中断处理程序标志</span></div>
<p><pre class="crayon-plain-tag">request_irq()</pre>  函数的第三个参数flags可以是0，或者是包含（但不限，仅列出重要的）下表中标志的掩码：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">标志 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>IRQF_DISABLED</td>
<td>指示内核在处理该中断处理程序时，要禁用所有其它中断。一般不用设置，除非是轻量级、需要快速执行的中断</td>
</tr>
<tr>
<td>IRQF_SAMPLE_RANDOM</td>
<td>指示该设备产生的中断可以对内核entropy pool有贡献，从而利于产生真正的随机数，如果设备产生中断的频率不可预知，则可以设置该位</td>
</tr>
<tr>
<td>IRQF_TIMER</td>
<td>特地为系统定时器的中断处理准备</td>
</tr>
<tr>
<td>IRQF_SHARED</td>
<td>指示可以在多个中断处理程序之间共享中断线，在同一个中断线上注册的每个中断处理程序必须设置该标志，否则每条线上只能有一个中断处理程序</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">释放中断处理程序</span></div>
<p>当卸载驱动程序时，需要注销相关的中断处理程序，并释放中断线：</p>
<pre class="crayon-plain-tag">/**
 * 释放中断处理程序
 * 如果中断线不是共享的，那么删除该程序的同时会禁用中断线
 * 否则，只删除dev指向的中断处理程序。仅当中断线上所有程序被删除，中断线才会被禁用
 * @param irq 中断号
 * @param dev 当前中断处理程序的标识
 */
void free_irq(unsigned int irq, void *dev);</pre>
<div class="blog_h2"><span class="graybg">编写中断处理程序</span></div>
<p>中断处理程序的签名如下：</p>
<pre class="crayon-plain-tag">enum irqreturn
{
    IRQ_NONE, //中断处理程序检测到一个中断，但是产生该中断的设备并不是注册该程序时指定的设备
    IRQ_HANDLED, //中断处理程序被正确调用，中断来自指定的源
    IRQ_WAKE_THREAD,
};
typedef enum irqreturn irqreturn_t;
/**
 * 中断处理函数
 * @params int irq 中断号
 * @params  void *dev 设备标识（通常为包含设备特有信息的结构体），2.0-没有该参数，导致使用同一驱动的不同设备必须依赖irq去区分
 * @return irqreturn_t 处理结构枚举值，其实就是int，之所以用typedef是为了和老版本内核兼容，老版本返回值是void
 */
typedef irqreturn_t (*irq_handler_t)( int irq, void *dev );</pre>
<p>中断处理程序包含的逻辑，取决于产生中断的设备，以及产生中断的原因。最简单的程序可能仅仅告知设备中断已经收到，复杂一些的程序可能需要在中断处理程序中收发数据或者其它一些扩充性的工作。扩充性的工作应当尽量推迟到下半部而不是在中断处理程序中。</p>
<p>在Linux中，中断处理程序是<span style="background-color: #c0c0c0;">无需支持重入的</span>： 因为当一个中断处理程序正在执行时，<span style="background-color: #c0c0c0;">相应中断线（其它中断线默认不受影响）</span>在所有处理器上都被<span style="background-color: #c0c0c0;">屏蔽</span>，这就防止了在此期间出现新的中断。不可重入性简化了中断处理程序的编程复杂性。</p>
<div class="blog_h3"><span class="graybg">共享的中断处理程序</span></div>
<p>比起非共享的中断处理程序，共享版本具有以下三个特点：</p>
<ol>
<li>注册时必须设置<span style="color: #000000;">IRQF_SHARED标志</span></li>
<li>注册时dev参数必须具有唯一性</li>
<li>中断处理程序必须能够识别它的设备<span style="background-color: #c0c0c0;">是否真的产生</span>了中断，这需要硬件的配合。内核接收到中断后，<span style="background-color: #c0c0c0;">会依次调用目标中断线上注册的所有中断处理程序</span>，因此，中断处理程序必须知道自己是否应该为某个中断负责，如果它相关的设备没有产生这个中断，那么中断处理程序应该立即退出（返回IRQ_NONE）。硬件必须提供必要的支持，例如包含一个状态寄存器，大部分硬件都有类似机制</li>
</ol>
<p>所有共享中断线的驱动程序必须满足上面三点，否则中断线就无法共享。</p>
<div class="blog_h3"><span class="graybg">实例</span></div>
<p>下面以一个实际的中断处理程序为例，realtime Clock（RTC）驱动程序，该驱动被PC等体系结构支持。该程序用于设置系统时钟、提供报警器（Alarm）或周期性定时器的支持，第一个功能通过直接写寄存器或者内存地址即可实现，但是后两个必须依赖于中断机制。</p>
<p>RTC驱动加载时，它会注册中断处理程序：</p>
<pre class="crayon-plain-tag">static int __init rtc_init(void)
{
	//rtc_irq是中断号，在PC上为8
	if (request_irq(rtc_irq, rtc_interrupt, IRQF_SHARED, "rtc",
			(void *)&amp;rtc_port)) {
		rtc_has_irq = 0;
		printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);
		return -EIO;
	}</pre>
<p>处理程序的内容如下：</p>
<pre class="crayon-plain-tag">/**
 * 只要内核接收到一次RTC中断，就会调用此函数
 */
static irqreturn_t rtc_interrupt(int irq, void *dev_id)
{
    //该自旋锁定防止rtc_irq_data被SMP上其它CPU并发访问
    spin_lock(&amp;rtc_lock);
    //rtc_irq_data是unsigned long，存放RTC相关信息，每次中断都会更新
    rtc_irq_data += 0x100;
    rtc_irq_data &amp;= ~0xff;
    if (is_hpet_enabled()) {
        rtc_irq_data |= (unsigned long)irq &amp; 0xF0;
    } else {
        rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) &amp; 0xF0);
    }
    //如果设置了RTC周期性定时器
    if (rtc_status &amp; RTC_TIMER_ON)
        mod_timer(&amp;rtc_irq_timer, jiffies + HZ/rtc_freq + 2*HZ/100);

    spin_unlock(&amp;rtc_lock);

    //该自旋锁定防止rtc_callback被SMP上其它CPU并发访问
    spin_lock(&amp;rtc_task_lock);
    if (rtc_callback) //执行预先设置好的回调函数
        rtc_callback-&gt;func(rtc_callback-&gt;private_data);
    spin_unlock(&amp;rtc_task_lock);
    wake_up_interruptible(&amp;rtc_wait);

    kill_fasync(&amp;rtc_async_queue, SIGIO, POLL_IN);
    //示意已经完成处理，因为该中断非共享，因此不需要测试虚假中断
    return IRQ_HANDLED;
}</pre>
<div class="blog_h2"><span class="graybg">中断上下文</span></div>
<p>当执行<span style="background-color: #c0c0c0;">中断处理程序时，内核处于中断上下文（Interrupt context）</span>。与之对应的是进程上下文，它是内核所处的另外一种操作模式，此时<span style="background-color: #c0c0c0;">内核代表进程执行</span>——例如执行<span style="background-color: #c0c0c0;">系统调用</span>或者<span style="background-color: #c0c0c0;">运行内核线程</span>，在进程上下文中，<pre class="crayon-plain-tag">current</pre> 宏关联当前进程。在进程上下文中内核<span style="background-color: #c0c0c0;">可以休眠</span>，也可以<span style="background-color: #c0c0c0;">执行调度</span>。</p>
<p>和进程上下文不同，<span style="background-color: #c0c0c0;">中断上下文和任何进程无关</span>（尽管current指向被中断的进程），由于不存在对应的进程，因而也就<span style="background-color: #c0c0c0;">不能睡眠</span>（睡眠是进程的概念）、不能被调度。在中断上下文中，不能调用任何可能导致休眠的函数。</p>
<p>中断上下文具有严格的时间限制，因为其打断了其它代码（甚至是不同中断线上的中断处理程序）的执行。中断上下文中的代码应当<span style="background-color: #c0c0c0;">迅速、简洁</span>，避免循环处理。</p>
<p>中断处理程序的栈是一个配置选项，以前中断处理程序没有自己的栈，而是借用被中断进程的<span style="background-color: #c0c0c0;">内核栈</span>。内核栈的大小为2页（32位8KB，64位16KB）。在2.6+版本，增加了一个选项，可以将内核栈减小为1页，为了应对栈的减小，中断处理程序开始拥有自己的栈了，每个处理器一个，大小1页，称为<span style="background-color: #c0c0c0;">中断栈</span>。</p>
<div class="blog_h2"><span class="graybg">中断处理机制的实现</span></div>
<p>中断处理机制的实现非常依赖于具体的体系结构。下图是中断从硬件到内核的处理路由：</p>
<p><img class="size-full wp-image-8809 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2010/12/interrupt-route.png" alt="interrupt-route" width="75%" /></p>
<ol>
<li>设备产生中断，并通过总线把电信号传递给中断控制器</li>
<li>如果中断线是激活的，则中断控制器将中断发往处理器。在大部分体系结构上，通过电信号向CPU特定管脚发送一个信号实现</li>
<li>如果处理器上没有禁用该中断，则处理器立即停止当前工作，关闭中断系统，并跳转到内存中预定义的位置开始执行那里的代码，此预定义位置由内核设置，是<span style="background-color: #c0c0c0;">中断处理程序的入口点（类似于系统调用通过预定义的Exception handler进入内核）</span></li>
<li>对于每一条中断线，处理器都会跳转到一个唯一的内存位置，内核因此知晓中断号。初始的入口点代码仅仅是简单的保存中断号和寄存器值（属于被中断的进程）到栈上，然后调用<pre class="crayon-plain-tag">do_IRQ()</pre> 。尽管还是和体系结构相关，从这里开始，代码基本上使用C编写</li>
<li><pre class="crayon-plain-tag">do_IRQ()</pre> 的签名为<pre class="crayon-plain-tag">unsigned int do_IRQ(struct pt_regs regs)</pre> ，因为C语言的调用惯例是把函数入参存放在栈顶，因此<pre class="crayon-plain-tag">pt_regs</pre> 包含了原始寄存器的值和中断号，do_IRQ将中断号提取出来，并禁止这条中断线，在PC上，这一逻辑由<pre class="crayon-plain-tag">mask_and_ack_8259()</pre> 完成</li>
<li>然后，<pre class="crayon-plain-tag">do_IRQ()</pre> 判断是否该中断线上具有有效的处理程序，并且当前没有被执行，如果为真，则调用<pre class="crayon-plain-tag">handle_IRQ_event()</pre> 来运行模板中断处理程序：<br />
<pre class="crayon-plain-tag">irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
    irqreturn_t ret, retval = IRQ_NONE;
    unsigned int status = 0;
    //如果没有指定“禁止其它所有中断”，则恢复当前CPU的硬件中断
    if (!(action-&gt;flags &amp; IRQF_DISABLED))
        local_irq_enable_in_hardirq();

    do {
        //遍历所有潜在的处理程序
        trace_irq_handler_entry(irq, action);
        ret = action-&gt;handler(irq, action-&gt;dev_id); //调用一个处理程序
        trace_irq_handler_exit(irq, action, ret);

        switch (ret) {
        case IRQ_WAKE_THREAD: //唤醒该处理程序的执行线程（handler thread）
            ret = IRQ_HANDLED;
            if (unlikely(!action-&gt;thread_fn)) {
                warn_no_thread(irq, action);
                break;
            }
            if (likely(!test_bit(IRQTF_DIED,
                         &amp;action-&gt;thread_flags))) {
                set_bit(IRQTF_RUNTHREAD, &amp;action-&gt;thread_flags);
                wake_up_process(action-&gt;thread);
            }

        case IRQ_HANDLED: //如果该处理程序完成了中断的处理，则退出
            status |= action-&gt;flags;
            break;

        default:
            break;
        }

        retval |= ret;
        action = action-&gt;next;
    } while (action);

    if (status &amp; IRQF_SAMPLE_RANDOM)
        add_interrupt_randomness(irq); //处理entropy pool
    local_irq_disable(); //再次禁止中断

    return retval;
}</pre>
</li>
<li>返回到<pre class="crayon-plain-tag">do_IRQ()</pre> ，执行一些清理工作，并返回到初始入口点，并跳转到<pre class="crayon-plain-tag">ret_from_intr()</pre> </li>
<li><pre class="crayon-plain-tag">ret_from_intr()</pre> 类似于初始入口点，是用汇编编写的，它检查是否存在挂起的重新调度（need_resched），如果是，并且：
<ol>
<li>内核正在返回用户空间（即当初被中断的是用户进程），则调用<pre class="crayon-plain-tag">schedule()</pre> </li>
<li>内核正在返回内核空间（即当初被中断的内核代码），则判断<pre class="crayon-plain-tag">preempt_count</pre> 是否为0，是则<pre class="crayon-plain-tag">schedule()</pre> ，否则说明抢占内核是不安全的，不得进行重新调度</li>
</ol>
</li>
</ol>
<p>对于x86_64，初始的汇编例程位于<pre class="crayon-plain-tag">arch/x86/kernel/entry_64.S</pre> ，相关的C方法位于<pre class="crayon-plain-tag">arch/x86/kernel/irq.c</pre> 。其它体系结构类似。</p>
<div class="blog_h2"><span class="graybg">/proc/interrupts</span></div>
<p>Procfs是一个仅存在于内核内存的虚拟文件系统，通常挂载于<pre class="crayon-plain-tag">/proc</pre> ，读取/写入Procfs文件系统会导致<span style="background-color: #c0c0c0;">相关内核函数的调用</span>。</p>
<p>读取/proc/interrupts文件，可以获得系统关于中断的统计性信息：</p>
<pre class="crayon-plain-tag">root@gmem:~# cat /proc/interrupts
           CPU0       CPU1
 16:   18473588          0  xen-percpu-virq      timer0
 17:        158          0  xen-percpu-ipi       spinlock0
 18:    3935398          0  xen-percpu-ipi       resched0
 19:          0          0  xen-percpu-ipi       callfunc0
 20:          0          0  xen-percpu-virq      debug0
 21:       8117          0  xen-percpu-ipi       callfuncsingle0
 22:          1          0  xen-percpu-ipi       irqwork0
 23:          0   16477170  xen-percpu-virq      timer1

#第一列：中断线（中断号）
#第2-N列：各CPU上，该中断发生的次数
#倒数第2列：处理此中断线的中断控制器
#最后一列：与该中断相关的设备的名称，该名就是request_irq()的参数*name</pre>
<div class="blog_h2"><span class="graybg">中断控制</span></div>
<p>内核提供了一组接口（定义在asm/system.h、&lt;asm/irq.h），用于操控计算机的中断状态。通过这些接口可以：</p>
<ol>
<li>禁止当前CPU的中断系统</li>
<li>屏蔽整台计算机的某条中断线</li>
</ol>
<p>之所有要对中断功能进行控制，归根到底是需要提供<span style="background-color: #c0c0c0;">同步</span>。通过禁止中断，可以确保某个中断处理程序<span style="background-color: #c0c0c0;">不会抢占当前的代码</span>，禁止中断还可以<span style="background-color: #c0c0c0;">禁止内核抢占</span>。</p>
<p>禁止中断并没有提供保护机制来防止其它CPU的并发访问（内核数据结构），因此内核代码常常<span style="background-color: #c0c0c0;">需要获得某种锁</span>。</p>
<p>总结一下：</p>
<ol>
<li>禁止中断提供保护机制，防止来自其它中断处理程序的并发访问（共享数据）</li>
<li>锁提供保护机制，防止来自其它CPU的并发访问（共享数据）</li>
</ol>
<div class="blog_h3"><span class="graybg">禁止和激活（某个处理器的）中断</span></div>
<p>下面两个函数用于禁止和激活当前处理器上的中断：</p>
<pre class="crayon-plain-tag">//禁用当前处理器的中断
local_irq_disable();
//启用当前处理器的中断
local_irq_enable();</pre>
<p>这两个函数往往使用单条汇编指令实现，例如在x86上，分别对应了<pre class="crayon-plain-tag">cli</pre> 和<pre class="crayon-plain-tag">sti</pre>  指令（即CLearInterrupt、SeTInterrupt），这些指令将禁用/启用中的关的传递。</p>
<p>这连个函数强制性的设置中断为禁止或启用状态，可能招致危险——代码执行路径的复杂性导致判断先前是禁用还是启用变得困难。因此，保存/恢复中断状态是更好的选择：</p>
<pre class="crayon-plain-tag">unsigned long flags;
local_irq_save( flags ); /* 导致中断被禁用 */
local_irq_restore( flags ); /* 导致中断被恢复到原先的状态 */
//注意：flags不得传递给另外一个函数，即，必须在当前栈帧上使用flags，上面的函数对只能在一个函数内部使用</pre>
<p>上面四个函数既可以在中断上下文中调用，也可以在进程上下文中调用。</p>
<p>从2.5+开始，Linux禁止了全局的cli()、sti()调用，这两个调用可以在<span style="background-color: #c0c0c0;">全局（所有处理器）返回禁止或者启用中断</span>。 为了确保对共享数据的互斥访问，以前可以通过全局<span style="background-color: #c0c0c0;">禁止中断达到互斥</span>，限制必须通过<span style="background-color: #c0c0c0;">本地中断控制+自旋锁</span>。</p>
<div class="blog_h3"><span class="graybg">禁止指定中断线</span></div>
<p>某些时候，只需要屏蔽（masking out）系统中的某条中断线就足够：</p>
<pre class="crayon-plain-tag">/**
 * 禁用指定中断线上的中断，该函数只有当前所有中断处理程序完成后才返回
 */
void disable_irq( unsigned int irq );
/**
 * 类似上面，立即返回
 */
void disable_irq_nosync( unsigned int irq );
/**
 * 启用中断线上的中断，对于前两个函数的每一次调用，必须有相应的该函数调用，才能最终启用中断
 */
void enable_irq( unsigned int irq );
/**
 * 等待中断线上正在执行的处理程序完毕
 */
void synchronize_irq( unsigned int irq );</pre>
<p>禁止共享中断线是不合适的，因为其它设备的中断也无法传递，因此新的设备驱动程序应避免使用这些接口。</p>
<div class="blog_h3"><span class="graybg">中断系统的状态</span></div>
<p>下表中的函数用于查询中断系统的状态：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">函数/宏 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>irqs_disabled()</td>
<td>定义在asm/system.h，如果当前处理器的中断系统被禁用，返回非0</td>
</tr>
<tr>
<td>in_interrupt()</td>
<td>定义在linux/hardirq.h，如果内核处理任何类型的中断处理（内核要么在执行中断处理程序，要么在执行下半部处理）中，返回非0。<span style="background-color: #c0c0c0;">如果返回0，则内核处于进程上下文</span></td>
</tr>
<tr>
<td>in_irq()</td>
<td>定义在linux/hardirq.h，如果内核正在执行中断处理程序，返回非0</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">下半部和推后执行的工作</span></div>
<p>由于下列原因，中断处理程序只能完成中断处理的上半部分：</p>
<ol>
<li>中断处理程序以异步方式执行，并且可能打断其它重要代码（包括其它中断处理程序），因此，为了避免被打断代码停止时间过长，中断处理程序应该尽快的执行</li>
<li>如果一个中断处理程序正在运行：当IRQF_DISABLED被没有被设置，这是最好的情况，相同中断线上其它中断会被屏蔽；当IRQF_DISABLED被设置则是最坏的情况，当前处理器上所有其它中断被屏蔽。由于禁用中断后，硬件无法与OS通信，这也要求中断处理程序执行的越快越好</li>
<li>中断处理程序往往需要对硬件进行操作，因此时效性要求很高</li>
<li>中断处理程序不在进程上下文中运行，因此不能阻塞，这限制了它们可以做的事情</li>
</ol>
<p>综上，必须<span style="background-color: #c0c0c0;">快速、异步、简单</span>的对硬件中断做出响应并完成对时间要求很严格的操作，对于其它的、<span style="background-color: #c0c0c0;">时间要求相对宽松的操作</span>，应该<span style="background-color: #c0c0c0;">推后到中断被激活之后</span>执行。</p>
<p>上一段内容已经很好的讨论了上半部，本段主要介绍下半部的工作。</p>
<div class="blog_h2"><span class="graybg">下半部</span></div>
<p>下半部的任务就是处理<span style="background-color: #c0c0c0;">和中断处理密切相关</span>，但<span style="background-color: #c0c0c0;">中断处理程序本身不执行</span>的工作。为了让中断处理程序最快，理想情况下是所有工作都由下半部执行，到底哪些工作由下半部处理并无一致性的规定，下面几条建议供借鉴：</p>
<ol>
<li>如果一项任务对时间非常敏感，则放在中断处理程序中</li>
<li>如果一项任务和硬件相关，则放在中断处理程序中</li>
<li>如果一项任务要保证不被其它中断（特别是同一中断）打断，则放在中断处理程序中</li>
<li>其它任务，考虑放在下半部</li>
</ol>
<p>那么下半部到底要推迟到什么时候呢？精确的时间并不需要，通常下半部在中断处理程序返回后就会立马执行，关键是，<span style="background-color: #c0c0c0;">下半部运行的时候，中断机制已经恢复，不会因为下半部导致时效性问题</span>。</p>
<p>与上半部（必须通过中断处理程序）不同，下半部可以有多种<span style="background-color: #c0c0c0;">实现机制</span>，这些机制在Linux发展过程中相继出现（有的已经废弃不用），由<span style="background-color: #c0c0c0;">不同的接口和子系统组成</span>：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">下半部机制</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>BH</td>
<td>
<p>最初的下半部机制就称为“下半部”，因为它是推迟中断处理工作的唯一方式。这里称其为BH以便和术语下半部区分。</p>
<p>BH提供一个静态创建的、由32个bottom halves组成的链表，上半部通过32位整数中的一位来标明哪个bottom half可以执行。每个BH都是全局范围同步（即使中断分属不同CPU也不允许任何两个BH同时执行），该机制简单但是有性能瓶颈</p>
<p>在2.5+，BH被完全废弃</p>
</td>
</tr>
<tr>
<td>任务队列</td>
<td>任务队列（Task Queue）是不久出现的BH代替。内核定义一组队列，每个队列包含一个由等待被调用函数组成的链表。驱动程序会把自己的下半部注册到适当的队列上去，该机制离不开BH。对性能要求高的子系统，例如网络，该机制依然难以胜任</td>
</tr>
<tr>
<td>软中断和Tasklet</td>
<td>
<p>从2.3+，内核引入了软中断（softirqs，和系统调用的软中断，确切的说软件中断，不是一个概念）和Tasklet，如果不考虑兼容性，可以完全代替BH</p>
<p>软中断是一组<span style="background-color: #c0c0c0;">静态定义（编译期）</span>的下半部接口，共32个，支持在<span style="background-color: #c0c0c0;">所有CPU上同时执行</span>——即使<span style="background-color: #c0c0c0;">两个类型相同</span>。软中断在执行期间<span style="background-color: #c0c0c0;">不能睡眠</span>，也<span style="background-color: #c0c0c0;">不会被其它软中断打断</span>，它只能被硬件中断打断（因为执行期间不禁止中断）</p>
<p>Tasklet是基于软中断实现的灵活性强、<span style="background-color: #c0c0c0;">动态创建（注册）</span>的下半部实现机制（和进程没有任何关系，尽管叫task），软中断的限制性特征同样适用于Tasklet</p>
<p>Tasklet是性能和易用性的折中，<span style="background-color: #c0c0c0;">不同类型的Tasklet可以在不同处理器上同时执行</span>，相同类型的，则不可</p>
<p>除非是性能要求非常高的情况，例如网络，才需要使用软中断，否则Tasklet就足够了</p>
</td>
</tr>
<tr>
<td>工作队列</td>
<td>
<p>在2.5+，任务队列被工作队列代替。工作队列简单有效：它们先对推后执行的工作排队，稍后在<span style="background-color: #c0c0c0;">进程上下文</span>中执行它们</p>
</td>
</tr>
<tr>
<td>内核定时器</td>
<td>
<p>用于把某个操作推迟到确定的时间段之后执行</p>
</td>
</tr>
</tbody>
</table>
<p> 综上，在2.6+，软中断、Tasklet、工作队列等是可用的几种不同形式的下半部机制。</p>
<div class="blog_h2"><span class="graybg">软中断（Softirqs）</span></div>
<p>软中断在实践中运用较少，但是它是Tasklet的基础，其代码位于kernel/softirq.c。软中断在编译期静态分配，由下面的结构表示：</p>
<pre class="crayon-plain-tag">struct softirq_action
{
	void	(*action)(struct softirq_action *); //把整个结构体传递给软中断处理程序，是为了方便未来扩展新字段
};</pre>
<p>32个该结构体的数组定义如下：</p>
<pre class="crayon-plain-tag">static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;</pre>
<p>每个注册的软中断，都会占据上述数组的一个元素，因此，最多可能有32个软中断。在2.6版本的内核，有9个元素已经被占用。 </p>
<p><span style="background-color: #c0c0c0;">软中断处理程序（软中断action字段）</span>的原型为：<pre class="crayon-plain-tag">void softirq_handler(struct softirq_action *)</pre>  ，当内核运行一个软中断处理程序时，就会执行该action函数，唯一的入参指向softirq_vec数组的元素，对于元素<pre class="crayon-plain-tag">my_softirq</pre> ，内核调用<pre class="crayon-plain-tag">my_softirq -&gt;action(my_softirq)</pre> 。</p>
<p>一个软中断不会抢占另外一个，实际上，<span style="background-color: #c0c0c0;">只有中断处理程序才能抢占软中断</span>。但是<span style="background-color: #c0c0c0;">其它软中断（包括同类型的）可以在别的处理器上运行</span>。</p>
<div class="blog_h3"><span class="graybg">执行软中断</span></div>
<p>一个注册的软中断必须在被标记后才会执行，这一标记动作称为<span style="background-color: #c0c0c0;">软中断触发</span>（raising the softirq）。通常，<span style="background-color: #c0c0c0;">中断处理程序会在返回前标记它的软中断</span>，使其在稍后被执行。在以下地方待处理的软中断会被检查和执行：</p>
<ol>
<li>硬件中断代码路径的返回点</li>
<li>内核线程ksoftirqd</li>
<li>显式调用检查、执行待处理软中断的任何代码，例如网络子系统</li>
</ol>
<p>不管使用什么方式触发，软中断都在下面的函数中执行：</p>
<pre class="crayon-plain-tag">asmlinkage void do_softirq( void )
{
    __u32 pending;
    unsigned long flags;

    if ( in_interrupt() )
    return;

    local_irq_save( flags ); //保存当前CPU中断状态，并禁用中断
    //32位图，如果第n位设置为1，则意味着第n个软中断等待处理
    pending = local_softirq_pending();

    if ( pending )
    __do_softirq();

    local_irq_restore( flags ); //恢复当前CPU的中断
}</pre>
<p>如果当前有等待处理的软中断，就会调用__do_softirq()，其核心代码如下：</p>
<pre class="crayon-plain-tag">asmlinkage void __do_softirq( void )
{
    struct softirq_action *h;
    pending = local_softirq_pending();
restart :
    //这里，本地中断是被禁止的，而下面一句必须在禁止的情况下执行
    //否则，可能导致新出现的、未处理的软中断被置零
    set_softirq_pending( 0 ); //重置位图，因为当前待处理软中断已经保存
    local_irq_enable(); //软中断程序执行期间允许响应中断
    h = softirq_vec;
    do
    {
        //循环遍历所有软中断
        if ( pending &amp; 1 )
        {
            h-&gt;action( h ); //调用软中断处理程序
        }
        h++;
        pending &gt;&gt;= 1;
    }
    while ( pending );
    local_irq_disable();
    //检查是否在循环过程中又有新的待处理软中断出现，如果是
    pending = local_softirq_pending();
    if ( pending &amp;&amp; --max_restart ) //并且重复最大次数未到达，则再次循环执行
    goto restart;

    if ( pending )
    wakeup_softirqd(); //否则标记软中断处理，等待下一轮执行
}</pre>
<div class="blog_h3"><span class="graybg">使用软中断</span></div>
<p>软中断保留给系统中<span style="background-color: #c0c0c0;">时效性要求最高、最重要的下半部</span>使用。 在2.6中只有网络、SCSI两个子系统直接使用软中断。此外，内核定时器和Tasklet都是基于软中断的，因此应当优先考虑Tasklet。</p>
<p><strong><span style="text-decoration: underline;">分配索引</span></strong></p>
<p>在编译期间，必须在下面的枚举中静态的声明软中断，根据优先级需要，插入到相应位置：</p>
<pre class="crayon-plain-tag">//高优先级的软中断应当声明在前面，默认的9项，优先级分别0-8
enum
{
	HI_SOFTIRQ=0, //高优先级Tasklet，一般作为第一项
	TIMER_SOFTIRQ, //定时器的下半部
	NET_TX_SOFTIRQ, //发送网络数据报
	NET_RX_SOFTIRQ, //接收网络数据报
	BLOCK_SOFTIRQ, //块设备
	Tasklet_SOFTIRQ, //正常优先级的Tasklet
	SCHED_SOFTIRQ, //进程调度器
	HRTIMER_SOFTIRQ, //高分辨率定时器
	RCU_SOFTIRQ	 //RCU锁
};</pre>
<p><span style="text-decoration: underline;"><strong>注册软中断处理程序</strong></span></p>
<p>分配完索引后，需要调用<pre class="crayon-plain-tag">open_softirq()</pre>  注册软中断处理程序，该函数的两个参数分别是软中断索引、处理函数指针，例如网络子系统这样注册软中断处理程序：</p>
<pre class="crayon-plain-tag">static int __init net_dev_init(void)
{
    //...
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);</pre>
<p>软中断处理程序执行时：</p>
<ol>
<li>允许当前CPU响应中断，但是<span style="background-color: #c0c0c0;">软中断处理程序本身不能休眠</span></li>
<li>禁止当前CPU上的软中断，但是其它CPU可以执行软中断，包括同一类型的软中断。这意味着任何共享变量（哪怕仅仅是某个软中断处理程序内部使用的全局变量）都需要严格的锁保护。如果使用互斥锁，则软中断就失去价值了，因此大部分软中断处理程序使用<span style="background-color: #c0c0c0;">单处理器数据（属于单个CPU的私有数据</span>），免去了加锁的开销，提高了性能</li>
</ol>
<p><span style="text-decoration: underline;"><strong>触发软中断</strong></span></p>
<p>调用<pre class="crayon-plain-tag">raise_softirq()</pre>  可以标记一个软中断为挂起中断，这样下一次<pre class="crayon-plain-tag">do_softirq()</pre> 时它就会被执行。</p>
<p>在中断处理程序中触发软中断，是最常见的形式。内核执行完中断处理程序后，马上就会调用<pre class="crayon-plain-tag">do_softirq()</pre> ，这让上半部/下半部的分界一目了然。</p>
<div class="blog_h3"><span class="graybg">ksoftirqd</span></div>
<p>每个处理器包括辅助处理软中断（包括Tasklet）的内核线程，当内核中出现大量软中断的时候，这些线程就会辅助处理之。</p>
<p>在中断处理程序返回时，处理软中断是最常见的情况。但是，软中断可能被高频的触发（例如网络子系统），甚至软中断处理函数还会自行重复触发（网络子系统会这样做）——当软中断正在处理时，触发自己以便再次执行。这种高频触发和自我重新触发的能力会导致用户空间进程无法获得足够的CPU时间而处于饥饿状态。</p>
<p>为了防止用户进程饥饿（同时也避免软中断饥饿），软中断设计者做了一些折中：当大量软中断出现时，一组最低优先级（nice=19）的内核线程被唤醒，以处理软中断。低优先级是防止其与其它重要任务抢夺资源，同时能保证软中断最终会被执行而不致饥饿。</p>
<p>处理软中断的内核线程被命名为ksoftirqd/n，其中n为CPU的编号，<span style="background-color: #c0c0c0;">每核CPU对应一个</span>这样的线程。一旦ksoftirqd线程被初始化，就会执行类似下面的死循环：</p>
<pre class="crayon-plain-tag">for ( ;; )
{
    if ( !softirq_pending( cpu ) ) //如果没有未决的软中断，则执行调度，放弃CPU
    schedule();
    set_current_state( TASK_RUNNING ); //否则，加入运行红黑树
    while ( softirq_pending( cpu ) ) //循环处理未决软中断，每次执行后都判断是否需要重新调度
    {
        do_softirq();
        if ( need_resched() ) //如需则重新调度
        schedule();
    }
    set_current_state( TASK_INTERRUPTIBLE ); //此设置会在下一次循环时生效，决定线程是否被加入睡眠队列
}</pre>
<div class="blog_h2"><span class="graybg">Tasklet</span></div>
<p>Tasklet是一种软中断（HI_SOFTIRQ、Tasklet_SOFTIRQ），但是<span style="background-color: #c0c0c0;">接口更简单、锁要求更低</span>（同一Tasklet不会在多个CPU上并发执行），应当优先使用Tasklet而不是软中断，它们非常易用而且大部分情况下性能不错。</p>
<p>Tasklet使用下面的结构体表示：</p>
<pre class="crayon-plain-tag">struct Tasklet_struct
{
    struct Tasklet_struct *next; //链表中下一个Tasklet
    /**
     * 状态：
     * 0
     * Tasklet_STATE_SCHED 已经调度待执行（等价于软中断触发）
     * Tasklet_STATE_RUN  正在执行，只对多处理器有意义，单处理器总是知道当前是不是在运行Tasklet
     */
    unsigned long state;
    atomic_t count; //引用计数器，只有为0的时候，Tasklet才有资格运行
    void (*func)( unsigned long ); //Tasklet处理函数，类似于软中断的action
    unsigned long data; //给Tasklet处理函数的唯一参数
};</pre>
<div class="blog_h3"><span class="graybg">调度Tasklet</span></div>
<p>已调度（即被触发的软中断）的Tasklet，存放在两个单处理器结构中：<pre class="crayon-plain-tag">Tasklet_vec</pre> 存放普通Tasklet；<pre class="crayon-plain-tag">Tasklet_hi_vec</pre> 存放高优先级Tasklet，它们都是<pre class="crayon-plain-tag">Tasklet_struct</pre> 的链表。</p>
<p>函数<pre class="crayon-plain-tag">Tasklet_schedule()</pre> 和<pre class="crayon-plain-tag">Tasklet_hi_schedule()</pre> 用来执行Tasklet的调度，它们都接受<pre class="crayon-plain-tag">Tasklet_struct*</pre> 作为入参，调度过程如下： </p>
<pre class="crayon-plain-tag">/* /include/linux/interrupt.h */
static inline void Tasklet_schedule( struct Tasklet_struct *t )
{
    //判断Tasklet状态是否为Tasklet_STATE_SCHED，如果是，说明其已经被调度，立即返回
    if ( !test_and_set_bit( Tasklet_STATE_SCHED, &amp;t-&gt;state ) )
    __Tasklet_schedule( t );
}

/* /kernel/softirq.c */
void __Tasklet_schedule( struct Tasklet_struct *t )
{
    unsigned long flags;
    //保存当前CPU中断状态并禁止中断，防止新到达的中断破坏数据一致性
    local_irq_save( flags );
    t-&gt;next = NULL;
    //把需要调度的Tasklet存放到变量Tasklet_vec或Tasklet_hi_vec的头部，这两个变量每个CPU都具有独立的副本
    *__get_cpu_var( Tasklet_vec ).tail = t;
    __get_cpu_var( Tasklet_vec ).tail = &amp; ( t-&gt;next );
    //触发软中断Tasklet_SOFTIRQ/HI_SOFTIRQ，这导致do_softirq()在下一轮执行被调度的Tasklet
    raise_softirq_irqoff( Tasklet_SOFTIRQ );
    local_irq_restore( flags ); //恢复中断状态
}</pre>
<p>当 <pre class="crayon-plain-tag">do_softirq()</pre> 执行时，它会调用<pre class="crayon-plain-tag">Tasklet_action()</pre> 和<pre class="crayon-plain-tag">Tasklet_hi_action()</pre> ，后者会依次执行被调度的Tasklet：</p>
<pre class="crayon-plain-tag">static void Tasklet_action( struct softirq_action *a )
{
    struct Tasklet_struct *list;
    //禁止中断，没必要保存中断状态，因为该例程作为软中断一部分调用，此时中断必然是启用的
    local_irq_disable();
    list = __get_cpu_var( Tasklet_vec ).head; //拷贝链表数据
    //在当前处理器上清空链表
    __get_cpu_var( Tasklet_vec ).head = NULL;
    __get_cpu_var( Tasklet_vec ).tail = &amp;__get_cpu_var( Tasklet_vec ).head;
    //启用中断
    local_irq_enable();

    while ( list ) //循环遍历链表上每一个待处理的Tasklet
    {
        struct Tasklet_struct *t = list;

        list = list-&gt;next;
        if ( Tasklet_trylock( t ) ) //尝试锁定
        {
            if ( !atomic_read( &amp;t-&gt;count ) ) //确认Tasklet没有被禁止
            {
                if ( !test_and_clear_bit( Tasklet_STATE_SCHED, &amp;t-&gt;state ) )
                BUG();
                t-&gt;func( t-&gt;data ); //执行Tasklet处理函数
                Tasklet_unlock( t ); //解锁Tasklet
                continue;
            }
            Tasklet_unlock( t );
        }

        local_irq_disable();
        t-&gt;next = NULL;
        *__get_cpu_var( Tasklet_vec ).tail = t;
        __get_cpu_var( Tasklet_vec ).tail = &amp; ( t-&gt;next );
        __raise_softirq_irqoff( Tasklet_SOFTIRQ );
        local_irq_enable();
    }
}
/**
 * 对于多处理器系统，这里需要检查Tasklet是否正在其它CPU上执行
 * 如果正在其它CPU上执行，则跳过，因为同一Tasklet不能并发执行
 */
#ifdef CONFIG_SMP
static inline int Tasklet_trylock(struct Tasklet_struct *t)
{
    return !test_and_set_bit(Tasklet_STATE_RUN, &amp;(t)-&gt;state);
}
#endif</pre>
<div class="blog_h3"><span class="graybg">使用Tasklet</span></div>
<p>可以静态或者动态的创建Tasklet：</p>
<pre class="crayon-plain-tag">//静态创建一个名为name的task，处理函数为func，传递的参数为data
DECLARE_TASKLET( name, func, data );
//类似，但是设置count=1，导致Tasklet被禁止
DECLARE_TASKLET_DISABLED( name, func, data );

//例如：
DECLARE_TASKLET( my_tasklet, my_tasklet_handler, dev );
//展开为：
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT( 0 ),
    my_tasklet_handler, dev };


//动态创建，结构体由指针指定
void tasklet_init(struct tasklet_struct *t,  void (*func)(unsigned long), unsigned long data);</pre>
<p>创建完Tasklet后，需要编写Tasklet处理函数：</p>
<pre class="crayon-plain-tag">/**
 * 由于Tasklet基于软中断实现，因此不得睡眠，这意味着不能调用信号量之类的阻塞性函数
 * Tasklet运行期间运行CPU响应中断，这意味着如果如果Tasklet和中断处理程序共享数据，需要进行防护（例如屏蔽中断，然后回去锁）
 * Tasklet实例不会在不同CPU上并发执行，这意味着仅仅是Tasklet使用的全局变量不需要保护
 */
void tasklet_handler(unsigned long data);</pre>
<p>在合适的时候（例如在中断处理程序返回前），需要调度Tasklet：</p>
<pre class="crayon-plain-tag">static inline void tasklet_schedule(struct tasklet_struct *t)</pre>
<p>Tasklet被调度后，会尽可能早的执行，如果在执行前，<span style="background-color: #c0c0c0;">同一个Tasklet再一次被调度，它仍然只会执行一次</span>。 如果当前Tasklet已经在运行，则会被重新调度并再次执行。</p>
<p>作为一种优化措施，Tasklet只会在调度它的CPU上执行，以更好的利用CPU高速缓存。</p>
<p>调用下面的函数可以禁用或者激活Tasklet：</p>
<pre class="crayon-plain-tag">//禁用Tasklet，如果Tasklet正在运行，该函数直到它运行完毕才返回
static inline void tasklet_disable(struct tasklet_struct *t);
//类似上面，但是立即返回不同步
static inline void tasklet_disable_nosync(struct tasklet_struct *t);
//激活
static inline void tasklet_disable_nosync(struct tasklet_struct *t);</pre>
<p> 下面的函数用于从调度队列中移除Tasklet：</p>
<pre class="crayon-plain-tag">/**
 * 从挂起（等待执行）队列中去除一个Tasklet
 * 该函数会等待目标Tasklet执行完毕，然后移除
 *
 * 该函数可能引起休眠，因此不能在中断上下文中使用
 */
void tasklet_kill(struct tasklet_struct *t);</pre>
<div class="blog_h2"><span class="graybg">工作队列</span></div>
<p>与其它机制不同，工作队列（Work queue）<span style="background-color: #c0c0c0;">总是在进程上下文中执行</span>，因而它支持重新调度甚至睡眠（信号量、阻塞式I/O）。工作队列将工作推后，由一个内核线程负责去执行，它是唯一能在进程上下文中运行的下半部机制。</p>
<div class="blog_h3"><span class="graybg">工作队列的实现</span></div>
<p>工作队列是一个用于<span style="background-color: #c0c0c0;">创建内核线程</span>的接口，其创建的线程负责执行由内核其它部分排到队里的任务，这些内核线程称为<span style="background-color: #c0c0c0;">工作者线程（Worker thread）</span>。</p>
<p>尽管可以自己创建新的内核线程，但是工作队列提供了一<span style="background-color: #c0c0c0;">个缺省的工作者线程</span>来处理任务队列，其命名为events/n，其中n为处理器编号（每个处理器一个线程）。一般情况下，应当尽量使用缺省工作者线程，除非你需要处理CPU密集型、性能要求严格的任务，并需要减轻缺省工作者线程的负担、防止其它任务饥饿。</p>
<p>工作者线程使用下面的结构表示：</p>
<pre class="crayon-plain-tag">/**
 * 外部可见的工作队列抽象，是由每CPU一个的内部工作队列组串的数组
 */
struct workqueue_struct
{
    //cpu_workqueue_struct表示每一个CPU的工作者线程
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
    struct list_head list;
    const char *name; //
    int singlethread;
    int freezeable;
    int rt;
};
struct cpu_workqueue_struct
{
    spinlock_t lock; //保护此结构的自旋锁
    struct list_head worklist; //工作任务链表
    wait_queue_head_t more_work;
    struct work_struct *current_struct; //当前工作
    struct workqueue_struct *wq; //关联工作队列结构
    task_t *thread; //关联线程
};</pre>
<p> 而工作，则使用下面的结构来抽象：</p>
<pre class="crayon-plain-tag">struct work_struct
{
    atomic_long_t data; //入参
    struct list_head entry; //链表结构
    work_func_t func; //执行的工作处理函数
};</pre>
<p> 所有工作者线程都是普通的内核线程，它们都会执行<pre class="crayon-plain-tag">worker_thread()</pre> 函数，并陷入死循环睡眠，当有任务被插入队列时，它们则被唤醒并执行，然后继续休眠：</p>
<pre class="crayon-plain-tag">int main( int argc, char **argv )
{
    for ( ;; )
    {
        //设置自己为休眠状态，在队列more_work上等待
        prepare_to_wait( &amp;cwq-&gt;more_work, &amp;wait, TASK_INTERRUPTIBLE );
        if ( list_empty( &amp;cwq-&gt;worklist ) ) //如果队列为空，则放弃CPU
        schedule();
        finish_wait( &amp;cwq-&gt;more_work, &amp;wait ); //移出休眠队列，可运行
        run_workqueue( cwq );//执行工作队列中的任务
    }
}</pre>
<p><pre class="crayon-plain-tag">run_workqueue()</pre> 函数是处理任务的核心逻辑所在：</p>
<pre class="crayon-plain-tag">int main( int argc, char **argv )
{
    //循环遍历工作列表
    while ( !list_empty( &amp;cwq-&gt;worklist ) )
    {
        struct work_struct *work;
        //获取函数和参数
        work_func_t f;
        void *data;
        work = list_entry(cwq-&gt;worklist.next, struct work_struct, entry);
        f = work-&gt;func;
        list_del_init( cwq-&gt;worklist.next );//从链表中解除当前项目
        work_clear_pending( work ); //清零当前任务的待处理标记
        f( work ); //执行调用
    }
}</pre>
<div class="blog_h3"><span class="graybg">使用工作队列</span></div>
<p>首先，使用下面的宏创建需要延迟执行的任务：</p>
<pre class="crayon-plain-tag">//静态声明
DECLARE_WORK( name, void (*func)( void * ), void *data );
//动态创建，结构由指针提供
INIT_WORK( struct work_struct *work, void (*func)( void * ), void *data );</pre>
<p>其中func参数就是工作处理函数，其原型为：<pre class="crayon-plain-tag">void work_handler(void *data)</pre>  ，该函数总是在进程上下文中执行，默认的，该函数执行时<span style="background-color: #c0c0c0;">中断处于启用状态、不进行任何锁定</span>。注意，由于内核线程没有关联任何用户空间内存映射，因此func不能访问用户空间（<span style="background-color: #c0c0c0;">只有在内核代表进程执行，例如系统调用时，内核才能访问用户空间</span>）。</p>
<p>创建好任务后，需要对其进行调度：</p>
<pre class="crayon-plain-tag">//调度任务，工作者线程一旦被唤醒，即立刻执行该任务
schedule_work(&amp;work);
//调度任务，延迟至少若干时钟节拍（timer ticks）后执行
schedule_delayed_work(&amp;work, delay);</pre>
<p>有些时候（例如模块卸载），在进行下一步工作前，需要确保一批次的任务已经完成：</p>
<pre class="crayon-plain-tag">//休眠，直到队列中所有任务都执行完毕
void flush_scheduled_work( void );</pre>
<p>注意上述函数不会取消任何延迟执行的任务，下面的函数可以取消之：</p>
<pre class="crayon-plain-tag">int cancel_delayed_work( struct work_struct *work );</pre>
<p>如果默认的工作队列不满足需求，可以创建新的工作队列（包括关联的内核线程）：</p>
<pre class="crayon-plain-tag">//该函数会为每个CPU创建一个工作线程
struct workqueue_struct *create_workqueue( const char *name ); //name表示工作线程的名称</pre>
<p>对于自己创建的工作队列，可以使用下面的函数进行调度、刷空：</p>
<pre class="crayon-plain-tag">int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_delayed_work( struct workqueue_struct *wq, struct work_struct *work, unsigned long delay );
flush_workqueue( struct workqueue_struct *wq );</pre>
<div class="blog_h2"><span class="graybg">下半部机制的选型</span></div>
<p>下表简单的比较了几种下半部机制：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 下半部</td>
<td style="text-align: center;">上下文</td>
<td style="text-align: center;">固有串行化特征</td>
<td style="text-align: center;">优势</td>
</tr>
</thead>
<tbody>
<tr>
<td>软中断 </td>
<td>中断</td>
<td>无 </td>
<td>性能最好，因为序列化限制最少</td>
</tr>
<tr>
<td>Tasklet </td>
<td>中断</td>
<td>相同Tasklet不得同时执行 </td>
<td>接口简单，不必考虑同一Tasklet重入并发</td>
</tr>
<tr>
<td>工作队列</td>
<td>进程</td>
<td>无（作为进程上下文调度）</td>
<td>进程上下文，可以阻塞、休眠，可以推迟到指定的时间之后执行下半部</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">在下半部之间加锁</span></div>
<p>由于Tasklet不可重入，因此不需要考虑Tasklet私有全局变量的同步。</p>
<p>如果某个进程上下文和下半部共享数据，那么，进程上下文应当禁用中断、得到锁，然后再访问共享数据。</p>
<p>任何工作队列使用到的共享数据，和一般的内核代码的锁考虑没有区别。</p>
<div class="blog_h2"><span class="graybg">禁止下半部</span></div>
<p>为了保证共享数据的安全，可能需要<span style="background-color: #c0c0c0;">得到锁，然后禁止下半部的处理</span>，驱动程序中经常使用到这种技巧。</p>
<p>下面两个函数可以禁止所有的下半部（软中断和Tasklet），注意，调用disable多少次，就要<span style="background-color: #c0c0c0;">相应调用</span>enable多少次，才能实现启用：</p>
<pre class="crayon-plain-tag">//禁止当前处理器的软中断、Tasklet处理
void local_bh_disable();
//恢复当前处理器的软中断、Tasklet处理
void local_bh_enable();</pre>
<p>这两个函数使用了每任务一个的<pre class="crayon-plain-tag">preempt_count</pre> 计数器（与内核抢占计数器同名），禁用时递增，启用时递减，为0时下半部机制可用。</p>
<div class="blog_h1"><span class="graybg">内核同步</span></div>
<p>在以前的Linux内核版本中，只有<span style="background-color: #c0c0c0;">发生中断或者内核明确的请求重新调度时</span>，才会存在数据并发访问的问题。从2.0+开始，内核支持对称多处理器（SMP，一种硬件架构，每个CPU地位平等，对资源具有相同、共享的使用权限，并且<span style="background-color: #c0c0c0;">由单个OS内核控制</span>，现代多数多处理器系统<span style="background-color: #c0c0c0;">都使用该架构</span>），支持SMP意味着<span style="background-color: #c0c0c0;">内核代码可以同时运行在多个CPU上</span>，不加保护的内核代码完全可能并发访问共享数据。</p>
<p>从2.6+，Linux发展成<span style="background-color: #c0c0c0;">抢占式内核，这意味着调度程序可以随时抢占运行中的内核代码</span>。内核中的并发来源现在包括三类：</p>
<ol>
<li>SMP：相同的内核代码片段可能在多个CPU上并发运行</li>
<li>内核抢占（Kernel Preemption）：即使对于单CPU系统，只要允许内核抢占，内核仍然是并发性的。例如运行中的系统调用可能被其它高优先级的进程抢占，后者可能访问共享数据</li>
<li>中断处理程序（Interrupt Handler）：即使单CPU系统上只有一个进程（不存在SMP、内核抢占问题），内核仍然是并发性的。除非中断被禁止，任何硬件触发的中断会传递给中断处理程序，后者会立即抢占正在运行的进程，中断处理程序可能和此进程共享某些数据</li>
</ol>
<div class="blog_h2"><span class="graybg">可重入性</span></div>
<p>Linux内核是可重入的，这意味着<span style="background-color: #c0c0c0;">多个控制路径可以同时在内核下运行</span>。当然，在单处理器下，同一时刻只能有一个进程在运行，在SMP上则会出现真正的同时运行。</p>
<p>内核函数可以是可重入的，即同一时刻有多个进程同时访问函数；也可以是<span style="background-color: #c0c0c0;">非可重入的，这时需要使用锁机制来保护</span>，确保同一时刻只有一个进程执行函数（中的非可重入代码，以及临界区）。任意一个函数，如果它只能修改局部变量，不能修改任何全局变量，那么它必然是可重入的。</p>
<p>内核的可重入性对内核的运作有重要影响，为便于表述，我们称内核处理系统调用、异常、中断时所执行的指令序列为：内核控制路径（Kernel control path）。在最简单的情况下，CPU从第一条指令开始，顺序、不间断的执行内核控制路径，一直到最后一条指令。但是下列事件，会导致CPU交错执行多条内核控制路径：</p>
<ol>
<li>用户态下的进程发起系统调用，陷入内核，内核控制路径发现请求无法立刻得到满足。这是内核控制路径调用Scheduler，选择一个新进程运行。这样，一个内核控制路径尚未完成，另外一个又开始执行。这种情况下，两个路径代表不同的进程执行</li>
<li>一个内核路径正在执行时，CPU检测到一个异常（例如访问一个不在RAM中的页）。这样，第一个路径被挂起，CPU转而执行合适的例程。在这个例子中，该例程会负责分配一个新页，并从磁盘读取它的内容，完毕后，第一个控制路径可以恢复执行。在这种情况下，两个控制路径代表同一个进程执行</li>
<li>当一个启用了中断的内核路径正在执行时，硬件中断发生，CPU转而执行另外一个路径，处理中断，完毕后，恢复第一个路径的执行。这种情况下，第二个路径不代表任何进程，但是其<span style="background-color: #c0c0c0;">所消耗的CPU时间，将强行的算在被中断的进程头上</span></li>
<li>在支持抢占式调度的内核中，一个内核路径正在执行，这时一个高优先级的进程加入就绪队列，时钟中断发生后，CPU将代表高优先级进程执行新的路径</li>
</ol>
<p>所有这些出现交错执行的情况，只要存在共享的数据结构，都牵涉到内核同步。在多CPU的SMP机器上，并发执行的情况更加复杂。</p>
<div class="blog_h2"><span class="graybg">临界区和竞态条件</span></div>
<p>所谓临界区（Critical Regions）是指<span style="background-color: #c0c0c0;">访问和操控共享数据</span>的代码路径，并发修改共享数据一般是不安全的，可能破坏数据结构。要防止临界区中的并发修改，必须保证代码以<span style="background-color: #c0c0c0;">原子性</span>的方式来操作——操作一旦开始就会不被打断的执行完毕，就好像是单条CPU指令一样。注意这里的“不被打断”不是指不能被调度程序切换出去，而是指<span style="background-color: #c0c0c0;">临界区执行完毕之前，任何其它执行序列均不得访问</span>此临界区。</p>
<p>如果多个线程同时访问临界区，一般是一个BUG，这种情况称为<span style="background-color: #c0c0c0;">竞态条件（Race condition）</span>，确保竞态条件不发生称为<span style="background-color: #c0c0c0;">同步（synchronization）</span>。</p>
<p>即使<pre class="crayon-plain-tag">i++</pre> 这样简单的操作也会对共享变量i引入竞态条件，完成该操作需要三个指令序列：</p>
<ol>
<li>得到当前（内存）变量i的值，并拷贝到寄存器</li>
<li>在寄存器中加法运算</li>
<li>将运算结果写回到变量</li>
</ol>
<p>很多CPU提供了原子的方式来<span style="background-color: #c0c0c0;">读取、增加并修改变量</span>，可以解决这种简单的并发问题。更复杂的并发场景，必须使用加锁来管理。</p>
<div class="blog_h3"><span class="graybg">造成并发的原因</span></div>
<p><span style="background-color: #c0c0c0;">伪并发</span>，这种并发不是真正的“同时发生”，而是交叉的执行：</p>
<ol>
<li>抢占和重新调度：用户程序很可能在<span style="background-color: #c0c0c0;">正处于临界区时</span>非自愿的被抢占，后续获得CPU的进程可能随后进入临界区</li>
<li>信号处理：信号处理是异步发生的，其可能和应用程序本身形成竞争</li>
</ol>
<p><span style="background-color: #c0c0c0;">真并发</span>，在启用SMP的情况下，两个进程可能真正的在临界区内同时执行（使用不同的CPU）。尽管和伪并发的原因和含义不同的，但是需要同样的保护。</p>
<p><span style="background-color: #c0c0c0;">内核中</span>可能造成<span style="background-color: #c0c0c0;">并发</span>的原因有：</p>
<ol>
<li>中断：中断几乎可以在任何时刻异步的发生，也就可能随时打断当前正在执行的代码</li>
<li>软中断和Tasklet：内核可能在任何时刻唤醒或者调度软中断，从而打断当前正在执行的代码</li>
<li>内核抢占：由于内核具有抢占性，所以内核中任何一个任务都可能被另外一个任务抢占</li>
<li>睡眠：在内核中运行的进程可能会睡眠，导致唤醒调度程序，使另外一个进程运行</li>
<li>对称多处理：两个或者更多CPU同时执行内核代码</li>
</ol>
<div class="blog_h3"><span class="graybg">并发安全代码</span></div>
<ol>
<li>中断安全（interrupt-safe）代码：在中断处理程序中能够避免并发访问的安全代码</li>
<li>SMP安全（smp-safe）代码：在对称对处理机器中能够避免并发访问的安全代码</li>
<li>内核抢占安全（preempt-safe）代码：在内核抢占时能避免并发访问的安全代码</li>
</ol>
<div class="blog_h2"><span class="graybg">加锁</span></div>
<p>锁可以用来防止两个线程并发访问临界区，当一个线程对临界区进行标记时，后续到达的线程就无法访问。锁有多种不同的形式，并且加锁的粒度和范围也不相同。Linux实现了几种不同的锁机制，它们的区别主要是当锁不可用时的行为，某些锁简单的进行<span style="background-color: #c0c0c0;">忙等待</span>（循环轮询锁可用性），其它锁则<span style="background-color: #c0c0c0;">睡眠直到锁可用</span>为止。</p>
<p>加锁/解锁操作是使用原子操作来实现的，几乎所有的处理器都实现了原子的<span style="background-color: #c0c0c0;">测试和设置指令</span>，该指令测试一个整数值，如果它为0（代表锁打开）则设置为新值。</p>
<p>锁的使用并不困难，<span style="background-color: #c0c0c0;">真正的挑战在于发现需要共享的数据和相应的临界区</span>，应当在设计初期就考虑加入锁，而不是发现问题后才想到。</p>
<div class="blog_h3"><span class="graybg">哪些数据需要保护</span></div>
<p>任何可能并发访问的变量都需要保护，下面的代码不会并并发执行：</p>
<ol>
<li>局部自动变量：仅存在于栈中的变量不需要任何保护，因为它只能被当前函数访问</li>
<li>只被一个线程访问的全局变量</li>
</ol>
<p>编写内核代码时，应当思考以下问题：</p>
<ol>
<li>这个数据是否全局的？除了当前线程外，其它线程能不能访问它？</li>
<li>这个数据会不会在进程上下文和中断上下文中共享？是不是要在两个中断处理程序中共享？</li>
<li>进程在访问数据时可不可能被抢占？被调度的新程序会不会访问同一数据？</li>
<li>进程会不会睡眠（阻塞）在某些资源上，如果是，它会让共享数据处于何种状态？</li>
<li>如果函数又在另外一个CPU上被调度执行，会发生什么？</li>
</ol>
<div class="blog_h2"><span class="graybg">死锁</span></div>
<p>死锁的产生需要一定的条件：一个或者更多的执行线程+一个或者多个资源。如果<span style="background-color: #c0c0c0;">每个线程都在等待其中一个资源，但是所有资源都被占用了</span>，就会导致无限期的等待，从而导致死锁。死锁最简单的例子是<span style="background-color: #c0c0c0;">自死锁</span>：一个进程尝试获取已经被自己持有的锁。</p>
<p>要避免死锁，应当遵守一些简单的规则：</p>
<ol>
<li>按顺序加锁：使用嵌套锁时，不同程序必须保证以相同的顺序获取锁，这一点非常重要</li>
<li>防止发生饥饿：代码的执行是否一定会结束，从而是否锁</li>
<li>不要重复请求同一个锁</li>
<li>设计尽可能简单的加锁方案</li>
</ol>
<p>尽管锁的释放顺序和死锁无关，最好是按照获取的反序来释放锁。</p>
<div class="blog_h2"><span class="graybg">争用（Contention）和可扩容性（Scalability）</span></div>
<p>术语锁争用（lock contention），用来描述<span style="background-color: #c0c0c0;">当前正被持有而另外一个线程又尝试获取</span>的锁，如果很多线程在尝试获取锁，则称该锁高度争用，其原因可能是：</p>
<ol>
<li>频繁获取锁</li>
<li>长时间持有锁</li>
</ol>
<p>由于锁的作用是使程序以<span style="background-color: #c0c0c0;">串行方式</span>访问共享资源，因此其必然会降低系统性能，高度争用的锁会成为系统的性能瓶颈。</p>
<p>可扩容性是对系统可通过硬件扩容的能力的一种度量，对于OS，扩容意味着增加CPU、内存以实现更大的进程并发度，理想情况下，CPU数量翻倍可以使性能翻倍，实际上则不可能达到，扩容后增加的锁开销是一个原因。</p>
<p>2.0+以后内核引入SMP支持，Linux对集群处理器的扩展性大大提高，在2.x早期版本中，一个时刻<span style="background-color: #c0c0c0;">只能有一个任务在内核中运行</span>，2.2开始取消了这一限制，锁的粒度也渐渐变细，到了2.6+内核加的锁是非常细粒度的，可扩容性很好。</p>
<p>粗粒度的锁可以锁住大块数据，编程简单，锁争用严重时性能差；反之，细粒度的锁锁住小块数据，编程难度大，<span style="background-color: #c0c0c0;">锁争用严重时性能好（锁争用少见时则浪费资源，加大系统开销）</span>。很多锁在设计初期是粗粒度的，后续因为性能原因而细化——初期的加锁方案应当<span style="background-color: #c0c0c0;">力求简单</span>。</p>
<div class="blog_h2"><span class="graybg">原子操作</span></div>
<p>原子曾被认为是不可再分的粒子，计算机科学借用它表示<span style="background-color: #c0c0c0;">不可再分的操作</span>。这里的不可再分，是指操作<span style="background-color: #c0c0c0;">不能被中途打断</span>（调度程序、中断系统导致的打断和这里的打断不是一个含义）：两个原子操作不可能同时访问共享资源，原子操作的<span style="background-color: #c0c0c0;">所有中间结果都符合预期</span>。</p>
<p>内核提供了两组原子操作接口，分别用于对<span style="background-color: #c0c0c0;">整数、位</span>进行操作。大部分体系结构提供了支持原子操作的简单算术指令，其它体系结构则提供了锁内存总线的指令来支持原子操作（确保其它改变内存的操作不能同时发生）。</p>
<p>能使用原子操作的时候，就不应该使用更复杂的锁机制，因为原子操作的系统开销较小、对cache-line的冲击也更小。</p>
<p>原子性（Atomicity）和顺序性（Ordering）是两个概念，原子性仅仅保证操作不可再分，顺序性要求操作依据预期的顺序发生。顺序性可以通过<span style="background-color: #c0c0c0;">屏障</span>来保证。</p>
<div class="blog_h3"><span class="graybg">原子整数操作</span></div>
<p>Linux支持原子操作的整数使用类型：</p>
<pre class="crayon-plain-tag">typedef struct {
	volatile int counter;
} atomic_t;
//支持32位整数，由于可移植性的考虑，即使是64bit体系结构，该类型仍然只能是32位</pre>
<p>原子整数最常见的用途是<span style="background-color: #c0c0c0;">实现计数器</span>，其相关的操作定义在对应体系结构的<pre class="crayon-plain-tag">/arch/x86/include/asm/atomic.h</pre> 头文件中。这些函数基本都是内联函数，如果函数本身就是原子的（例如大部分体系结构上，读取一个字本身就是原子操作，其执行期间不可能对该字进行写入）则会定义为宏。下面是操作的列表：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 45%; text-align: center;">函数/宏</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>ATOMIC_INIT(int i)</td>
<td>
<p>在声明时初始化原子整数为i，举例：</p>
<pre class="crayon-plain-tag">atomic_t v;
atomic_t u = ATOMIC_INIT(0);</pre>
</td>
</tr>
<tr>
<td>int atomic_read(atomic_t *v)</td>
<td>
<p>原子的读取整数值，其代码实现：
<pre class="crayon-plain-tag">/include/asm-generic/atomic.h
//默认实现：直接read，因为大部分体系结构上读取一个字是原子操作
#define atomic_read(v)	((v)-&gt;counter)</pre>
</td>
</tr>
<tr>
<td>void atomic_set(atomic_t *v, int i)</td>
<td>原子的设置整数值</td>
</tr>
<tr>
<td>void atomic_add(int i, atomic_t *v)</td>
<td>原子的进行加法运算</td>
</tr>
<tr>
<td>void atomic_sub(int i, atomic_t *v)</td>
<td>原子的进行减法运算</td>
</tr>
<tr>
<td>void atomic_inc(atomic_t *v)</td>
<td>原子的进行递增运算</td>
</tr>
<tr>
<td>void atomic_dec(atomic_t *v)</td>
<td>原子的进行递减运算 </td>
</tr>
<tr>
<td>int atomic_sub_and_test(int i, atomic_t *v)</td>
<td>原子的进行减法运算(v - i)并测试结果是否为0，如果为0则返回true</td>
</tr>
<tr>
<td>int atomic_add_negative(int i, atomic_t *v)</td>
<td>原子的进行加法运算并测试结果是否负数，如果是负数则返回true</td>
</tr>
<tr>
<td>int atomic_add_return(int i, atomic_t *v)</td>
<td rowspan="4">原子操作，并返回结果</td>
</tr>
<tr>
<td>int atomic_sub_return(int i, atomic_t *v)</td>
</tr>
<tr>
<td>int atomic_inc_return(int i, atomic_t *v)</td>
</tr>
<tr>
<td>int atomic_dec_return(int i, atomic_t *v)</td>
</tr>
<tr>
<td>int atomic_dec_and_test(atomic_t *v)</td>
<td>原子的递减并测试结果是否为0，如果是返回true</td>
</tr>
<tr>
<td>int atomic_inc_and_test(atomic_t *v)</td>
<td>原子的递增并测试结果是否为0，如果是返回true</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">64位原子操作</span></div>
<p>由于64位体系结构的越发普及，内核引入了64位整数原子操作的支持：
<pre class="crayon-plain-tag">#ifdef CONFIG_64BIT
typedef struct {
	volatile long counter;
} atomic64_t;
#endif</pre>
<p>该类型的用法和32位原子整数完全一样。<span style="background-color: #c0c0c0;">大部分32位体系结构不支持该类型</span>，x86_32除外。</p>
<div class="blog_h3"><span class="graybg">原子位操作</span></div>
<p>可以按位进行原子操作，相关函数定义在体系结构的<pre class="crayon-plain-tag">/arch/x86/include/asm/bitops.h</pre> 文件中，下面是简单用例：</p>
<pre class="crayon-plain-tag">unsigned long word = 0;
set_bit( 0, &amp;word ); /* 最低位被原子的设置为0 */
set_bit( 1, &amp;word ); /* 第二位被原子的设置为1 */
printk( "%ul\n", word ); /* 打印3 */
clear_bit( 1, &amp;word ); /* 原子的清零第二位 */
change_bit( 0, &amp;word ); /*原子的翻转第一位*/ </pre>
<div class="blog_h2"><span class="graybg">自旋锁</span></div>
<p>自旋锁（Spin lock）是内核中最常见的锁，它是一个快速简单的锁实现。自旋锁同时只能被一个执行线程持有，如果出现争用，后续线程会一直忙循环，就好像不停旋转的电机一样；如果没有争用，则会立即获得自旋锁。</p>
<p>自旋锁特别浪费CPU时间，因此<span style="background-color: #c0c0c0;">不适合长时间持有</span>。自旋锁设计的初衷是用于预期会很快被释放的锁，<span style="background-color: #c0c0c0;">空转的开销相比起上下文切换更小</span>。自旋锁适用于<span style="background-color: #c0c0c0;">加锁时间不长且不会睡眠</span>的场景（例如中断处理程序、软中断）中。</p>
<p>对于Per-cpu数据，不需要使用自旋锁来保护，因为其它CPU上运行的内核代码无法访问之。但这类数据的安全性仍然收到内核抢占、中断的影响。</p>
<div class="blog_h3"><span class="graybg">自旋锁操作</span></div>
<p>在多处理器机器中，自旋锁在同一时刻只能被一个执行线程持有，因此只有一个线程可能处于临界区内，这为防止并发提供了有效的保护机制。在单处理器机器上，编译的时候并不会加入自旋锁（没有意义），如果<span style="background-color: #c0c0c0;">禁止内核抢占，那么编译的时候会完全剔除自旋锁</span>。</p>
<p>需要注意：自旋锁是<span style="background-color: #c0c0c0;">不可重入</span>的，如果尝试获得已经被自己持有的锁，会导致死锁。</p>
<p>自旋锁的实现和体系结构密切相关，头文件为<pre class="crayon-plain-tag">/include/linux/spinlock.h</pre> ，函数定义位于体系结构的<pre class="crayon-plain-tag">asm/spinlock.c</pre> ，下面的代码示例了自旋锁基本的使用方法：</p>
<pre class="crayon-plain-tag">DEFINE_SPINLOCK( mr_lock );
spin_lock( &amp;mr_lock );
/* critical region ... */
spin_unlock( &amp;mr_lock );</pre>
<p>自旋锁可以用于中断处理程序中（此处不能使用可能导致休眠的信号量）。如果某个自旋锁会被中断处理程序使用，则任何使用该锁的程序都应该：</p>
<ol>
<li>首先禁用本地（当前CPU）的中断处理</li>
<li>然后获取自旋锁</li>
</ol>
<p>如果不禁用本地中断，当前持有自旋锁的内核代码（例如下半部）可能被中断打断，中断处理程序尝试获取自旋锁时必然自旋（<span style="background-color: #c0c0c0;">中断处理程序不可睡眠性导致无限期占用CPU自旋是问题的本质所在</span>），但是<span style="background-color: #c0c0c0;">锁的持有者在中断处理程序执行完毕前不可能运行</span>，这就导致锁永远无法释放，导致死锁。其它处理器上的中断不会妨碍当前处理器上的内核代码释放锁，因此不必禁用。</p>
<p>内核提供了同时操控自旋锁和中断控制的函数：</p>
<pre class="crayon-plain-tag">DEFINE_SPINLOCK( mr_lock );
unsigned long flags; //貌似传值，实质是宏方式使用
spin_lock_irqsave( &amp;mr_lock, flags ); //保存当前中断状态，禁用中断，并获得自旋锁
/* critical region ... */
spin_unlock_irqrestore( &amp;mr_lock, flags ); //是否自旋锁，并恢复中断状态</pre>
<p>配置选项<pre class="crayon-plain-tag">CONFIG_DEBUG_SPINLOCK</pre> 用于方便自旋锁的调试，激活该选项后内核会检查未初始化的锁、未加锁而尝试解锁。<pre class="crayon-plain-tag">CONFIG_DEBUG_LOCK_ALLOC</pre> 可以进一步全称调试锁。</p>
<p>自旋锁相关的操作如下表：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">函数/宏</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>spin_lock()</td>
<td>获取自旋锁</td>
</tr>
<tr>
<td>spin_lock_irq()</td>
<td>禁用本地中断并获取自旋锁</td>
</tr>
<tr>
<td>spin_lock_irqsave()</td>
<td>保存并禁用本地中断并获取自旋锁</td>
</tr>
<tr>
<td>spin_unlock()</td>
<td>释放自旋锁</td>
</tr>
<tr>
<td>spin_unlock_irq()</td>
<td>释放自旋锁并启用本地中断处理</td>
</tr>
<tr>
<td>spin_unlock_irqrestore()</td>
<td>释放自旋锁并恢复本地中断处理状态</td>
</tr>
<tr>
<td>spin_lock_init()</td>
<td>动态初始化自旋锁</td>
</tr>
<tr>
<td>spin_trylock()</td>
<td>尝试获取自旋锁，如果失败会立即返回非0而不自旋</td>
</tr>
<tr>
<td>spin_is_locked()</td>
<td>如果自旋锁被锁定，则返回非0</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">自旋锁和下半部</span></div>
<p>和下半部配合使用时，必须小心的使用锁机制。函数<pre class="crayon-plain-tag">spin_lock_bh()</pre> 用于获得自旋锁并禁用下半部，<pre class="crayon-plain-tag">spin_unlock_bh()</pre> 则进行相反的操作。</p>
<p>由于下半部可以抢占进程上下文（因为下半部可能立即随着中断处理程序执行，而后者必然意味着中断抢占），因此当下半部和进程上下文共享数据时，必须对进程上下文中的共享数据进行保护——加自旋锁的同时需要禁止下半部执行。</p>
<p>由于中断处理程序可以抢占下半部，因此当下半部和中断处理程序共享数据时，必须在加自旋锁的同时禁止中断。</p>
<div class="blog_h3"><span class="graybg">读/写自旋锁</span></div>
<p>有时候锁的用途可以明确的分为读取、写入两个场景，只要没有写操作，并发的读操作是安全的，写操作则需要完全的互斥。内核专门提供了读写自旋锁，它允许多个执行线程共享的获取读锁而不需自旋。读写自旋锁的用法类似于自旋锁：</p>
<pre class="crayon-plain-tag">//静态声明
DEFINE_RWLOCK( mr_rwlock );
//动态创建
struct rwlock_t mr_rwlock;
rwlock_init( &amp;mr_rwlock );
//读线程
read_lock( &amp;mr_rwlock );
read_unlock( &amp;mr_rwlock );

//写线程
write_lock( &amp;mr_rwlock );
write_unlock( &amp;mr_lock );

//下面的代码会导致死锁，读写自旋锁不支持读锁向写锁“升级”
read_lock( &amp;mr_rwlock );
write_lock( &amp;mr_rwlock ); //写锁不断自旋，等待所有锁持有者（包括自己持有的读锁）释放锁</pre>
<p>尽管不支持向写锁的升级，但是<span style="background-color: #c0c0c0;">一个执行线程递归的获取一个读锁是安全的</span>， 该特性经常被用作一种优化手段：如果在中断处理程序中只有读操作，可以使用<pre class="crayon-plain-tag">read_lock()</pre> 而不是<pre class="crayon-plain-tag">read_lock_irqsave()</pre> 进行读保护。但是如果有任何以写的方式使用共享数据的中断，就必须禁止中断，否则可能出现死锁。</p>
<p>需要注意，读写锁对读操作更加照顾，因此大量读操作可能<span style="background-color: #c0c0c0;">导致写线程处于饥饿状态</span>。</p>
<div class="blog_h2"><span class="graybg">信号量</span></div>
<p>注意这里的信号量和用户空间的Unix System V信号量类似，但是并不等同，根本区别是本节的信号量专用于内核编程。</p>
<p>信号量是一种<span style="background-color: #c0c0c0;">睡眠锁</span>，如果一个任务尝试获取信号量而不可用，则进入等待队列睡眠而放弃CPU。比起自旋锁，<span style="background-color: #c0c0c0;">信号量具有更大的开销和更好的CPU利用率</span>。信号量的特点如下：</p>
<ol>
<li>适用于锁被长时间持有的情况：当出现争用时，长时间的自旋不合适，让等待线程睡眠更节约CPU；相反，如果锁持有时间短，那么睡眠、维护等待队列即唤醒的开销可能比锁被占用的全部时间更长</li>
<li>只能在进程上下文中使用：中断上下文不允许睡眠</li>
<li>持有信号量的同时，可以继续睡眠：其它线程请求信号量时会自动睡眠，不会死锁</li>
<li>当请求信号量时，不能持有自旋锁：请求信号量可能会导致睡眠，而持有自旋锁时不能睡眠</li>
</ol>
<div class="blog_h3"><span class="graybg">计数信号量和二值信号量</span></div>
<p>与自旋锁不同，信号量允许多个任务持有，支持的使用者数量（usage count）在声明信号量时指定：如果使用者数量为1，称为<span style="background-color: #c0c0c0;">二值信号量或者互斥信号量</span>；否则称为<span style="background-color: #c0c0c0;">计数信号量</span>。</p>
<div class="blog_h3"><span class="graybg">创建和初始化</span></div>
<p>类型<pre class="crayon-plain-tag">struct semaphore</pre> 代表信号量，定义在头文件<pre class="crayon-plain-tag">asm/semaphore.h</pre> 中，信号量的实现和体系结构相关。通过下面的方式声明和初始化信号量：</p>
<pre class="crayon-plain-tag">struct semaphore name;
sema_init(&amp;name, count); //count为使用者数量</pre>
<p>使用<pre class="crayon-plain-tag">static DECLARE_MUTEX(name);</pre>  或者<pre class="crayon-plain-tag">init_MUTEX(sem);</pre> 可以快捷的创建/初始化一个互斥信号量。</p>
<div class="blog_h3"><span class="graybg">使用信号量</span></div>
<p>调用<pre class="crayon-plain-tag">down_interruptible()</pre> 可以尝试请求信号量，如果不可用，当前线程进入<pre class="crayon-plain-tag">TASK_INTERRUPTIBLE</pre> 睡眠状态，如果睡眠期间进程被信号唤醒，该函数会返回<pre class="crayon-plain-tag">-EINTR</pre> 。类似的函数<pre class="crayon-plain-tag">down()</pre> 则导致进程进入<pre class="crayon-plain-tag">TASK_UNINTERRUPTIBLE</pre> 状态。一般前者更合适。如果信号量可用，则可用计数递减。</p>
<p>调用<pre class="crayon-plain-tag">down_trylock()</pre> 可以非阻塞的获取信号量，如果不可用，立即返回非0，否则返回0并获取信号量。</p>
<p>调用<pre class="crayon-plain-tag">up()</pre> 则可以释放一个信号量。</p>
<p>下面的代码示例了如何使用信号量：</p>
<pre class="crayon-plain-tag">/* 声明使用者数量为1的信号量（互斥信号量） */
static DECLARE_MUTEX( mr_sem );
/* 尝试获取信号量 */
if ( down_interruptible( &amp;mr_sem ) )
{
    /* 接收到信号，没有获取到信号量 */
}
/* 获取到信号量，这里是临界区 */
/* 释放信号量 */
up( &amp;mr_sem );</pre>
<div class="blog_h3"><span class="graybg">读/写信号量</span></div>
<p>读写信号量的功能类似于读写自旋锁，它声明在<pre class="crayon-plain-tag">linux/rwsem.h</pre> 中，使用<pre class="crayon-plain-tag">struct rw_semaphore</pre> 表示。可以通过下面的方式来创建：</p>
<pre class="crayon-plain-tag">//静态声明
static DECLARE_RWSEM(name);
//动态创建
init_rwsem(struct rw_semaphore *sem);</pre>
<p><span style="background-color: #c0c0c0;">读写信号量都是互斥的</span>，也就是说使用者数量必须是1。任何数量的读请求者可以同时持有信号量，而一个写请求者则必须独占信号量：</p>
<pre class="crayon-plain-tag">static DECLARE_RWSEM( mr_rwsem );
/* 尝试以读方式获取信号量 */
down_read( &amp;mr_rwsem );
/* 临界区（只读操作）*/
/* 释放信号量 */
up_read( &amp;mr_rwsem );


/* 尝试以写方式获取信号量 */
down_write( &amp;mr_rwsem );
/* 临界区（读写操作）.. */
/* 释放信号量 */
up_write( &amp;mr_rwsem );</pre>
<p>读写信号量<span style="background-color: #c0c0c0;">支持降级</span>：<pre class="crayon-plain-tag">downgrade_write()</pre>  可以把写锁变为读锁。</p>
<div class="blog_h2"><span class="graybg">互斥体</span></div>
<p>为了使用更加简单的睡眠锁，互斥体（Mutex）被引入内核，其行为与计数为1的信号量类似，但是更简单、高效，也具有更多的限制：</p>
<ol>
<li>任何时候只有一个任务能够持有互斥体</li>
<li>互斥体的上锁者必须负责解锁。不能在一个上下文中加锁，而在另外一个上下文中解锁，因此互斥体不适合内核同用户空间复杂的同步场景</li>
<li>互斥体不可重入，递归的获取和释放锁是不允许的</li>
<li>当持有互斥体时，进程不可以退出</li>
<li>互斥体不能被中断处理程序或者下半部获取，即使<pre class="crayon-plain-tag">mutex_trylock()</pre> 也不行</li>
</ol>
<p><pre class="crayon-plain-tag">CONFIG_DEBUG_MUTEXES</pre> 配置性可以开启互斥体调试，以监控非法使用互斥体的代码。</p>
<p>互斥体相关函数包括：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;">函数 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>mutex_lock(struct mutex *)</td>
<td>锁定互斥体，如果不可用则睡眠</td>
</tr>
<tr>
<td>mutex_unlock(struct mutex *)</td>
<td>解锁互斥体</td>
</tr>
<tr>
<td>mutex_trylock(struct mutex *)</td>
<td>尝试锁定，如果不可用返回0，否则返回1</td>
</tr>
<tr>
<td>mutex_is_locked (struct mutex *)</td>
<td>判断是否被锁定，如果是返回1</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">完成变量</span></div>
<p>如果内核中一个任务需要<span style="background-color: #c0c0c0;">通知另外一个任务</span>发生了某个特定事件，完成变量（completion variables）是一个简单的方法。一个任务可以在完成变量上等待并睡眠，另外一个任务则在其睡眠期间执行一些工作，这些工作完成后，第二个睡眠唤醒等待中的任务。</p>
<p>完成变量和信号量很类似，它提供后者的简单替代。例如vfork()系统调用在子进程执行或者退出时，使用完成变量来唤醒父进程。</p>
<p>完成变量使用声明在<pre class="crayon-plain-tag">linux/completion.h</pre> 的类型<pre class="crayon-plain-tag">struct completion</pre> 表示，可以用<pre class="crayon-plain-tag">DECLARE_COMPLETION(mr_comp);</pre> 静态的声明。与之相关的函数包括：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>init_completion(struct completion *)</td>
<td>初始化指定的动态创建的完成变量</td>
</tr>
<tr>
<td>wait_for_completion(struct completion *)</td>
<td>等待完成信号的出现</td>
</tr>
<tr>
<td>complete(struct completion *)</td>
<td>唤醒任何等待的任务</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">大内核锁（BKL）</span></div>
<p>BKL是Linux引入SMP支持后，向全面细粒度加锁机制过渡时期的产物。BKL是一种<span style="background-color: #c0c0c0;">全局自旋锁</span>。它具有以下特性：</p>
<ol>
<li>持有BKL的任务可以睡眠，但是任务不可被调度时，其持有的锁自动释放；当它恢复调度时，又自动获得</li>
<li>BKL支持重入，进程可以多次请求一个锁，但是需要相应次数的释放</li>
<li>BKL只能用于进程上下文中</li>
<li>BKL会导致整个内核<span style="background-color: #c0c0c0;">串行化</span>执行</li>
</ol>
<p>BKL现在已经过时，但是内核代码中还有其踪迹。与之相关的函数包括：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;">函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>lock_kernel ()</td>
<td>获取大内核锁</td>
</tr>
<tr>
<td>unlock_kernel()</td>
<td>释放大内核锁</td>
</tr>
<tr>
<td>kernel_locked()</td>
<td>如果大内核锁当前被持有，返回非0</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">顺序锁（Sequential Locks）</span></div>
<p>顺序锁是2.6+引入的一种锁机制，它提供一种简单的读写共享数据的机制。顺序锁维护一个序列计数器，当执行写操作时计数器会增加并得到一个锁，在读取数据前、后，分别得到计数器值，如果：</p>
<ol>
<li>计数器值没有变化：说明在读取数据期间，没有启动过写操作</li>
<li>如果计数器值为偶数：说明在此刻没有进行中的写操作（因为初值0，获取写锁导致值为奇数，释放则变为偶数）</li>
<li>如果计数器值为奇数：说明当前正在进行写操作</li>
</ol>
<p>可以使用<pre class="crayon-plain-tag">seqlock_t mr_seq_lock = DEFINE_SEQLOCK( mr_seq_lock )</pre> 定义一个顺序锁，执行写的代码很简单：</p>
<pre class="crayon-plain-tag">//写代码路径
write_seqlock( &amp;mr_seq_lock ); //获取写锁，增加计数，行为类似于自旋锁
/* 临界区 */
write_sequnlock( &amp;mr_seq_lock ); //释放写锁，增加计数</pre>
<p>执行读的代码则很不一样：</p>
<pre class="crayon-plain-tag">//读代码路径
unsigned long seq;
do
{
    seq = read_seqbegin( &amp;mr_seq_lock ); //循环检测，直到seq不是奇数（即当前没有写操作）
    /* 在这里读取数据 */
}
//每次读取数据完毕，需要进行乐观并发控制：检查释放读取了无效数据，即在读取数据期间，数据释放被修改
while ( read_seqretry( &amp;mr_seq_lock, seq ) ); //判断当前计数是否和读操作开始前的seq一致，即没发生修改</pre>
<p>多个读线程和少数写线程共享一个顺序锁时，它更倾向与写线程，因为只要没有其它任务正在写，写锁总是能成功获得。未决的写操作会导致读线程循环，直到写锁释放。</p>
<p>顺序锁适合这样的场景：</p>
<ol>
<li>共享数据具有多个读请求者，很少的写请求者</li>
<li>虽然写请求者很少，还是倾向与写请求，从不让读请求致使写请求饥饿</li>
<li>共享数据结构简单，例如是一个简单的结构体甚至整数</li>
</ol>
<p>内核中使用顺序锁典型的例子是jiffies，该变量存储Linux机器启动到当前时间依赖，流逝的时钟节拍累加数。读写jiffies的代码如下：</p>
<pre class="crayon-plain-tag">//读取当前jiffies
u64 get_jiffies_64( void )
{
    unsigned long seq;
    u64 ret;
    do
    {
        seq = read_seqbegin( &amp;xtime_lock );
        ret = jiffies_64;
    }
    while ( read_seqretry( &amp;xtime_lock, seq ) );
    return ret;
}

//时钟中断负责写jiffies
write_seqlock(&amp;xtime_lock);
jiffies_64 += 1;
write_sequnlock(&amp;xtime_lock);</pre>
<div class="blog_h2"><span class="graybg">禁止抢占</span></div>
<p>由于内核是抢占性的，内核中的进程在任何时候都可能被其它进程抢占。内核抢占代码使用<span style="background-color: #c0c0c0;">自旋锁作为禁止抢占区域的标记</span>——如果当前<span style="background-color: #c0c0c0;">内核代码持有自旋锁</span>，那么<span style="background-color: #c0c0c0;">对应CPU上的抢占是关闭的</span>：</p>
<pre class="crayon-plain-tag">static inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&amp;lock-&gt;rlock);
}
#define raw_spin_lock(lock)	_raw_spin_lock(lock)
#define _raw_spin_lock(lock)			__LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
  //禁用抢占，然后获得锁

static inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&amp;lock-&gt;rlock);
}
#define raw_spin_unlock(lock)		_raw_spin_unlock(lock)
#define _raw_spin_unlock(lock)			__UNLOCK(lock)
#define __UNLOCK(lock) \
  do { preempt_enable(); __release(lock); (void)(lock); } while (0)
  //启用抢占，然后再释放锁</pre>
<p>如果自旋锁不具有禁止抢占的特性，会发生什么呢？考虑下面的事件序列（A/B执行路径一致）：</p>
<ol>
<li>线程A获得自旋锁，正在CPU0上操控共享数据</li>
<li>发生内核抢占，A丢失CPU0</li>
<li>线程B获得CPU0，尝试获得自旋锁，它开始自旋，它获得自旋锁之前的时间一直是在浪费CPU</li>
</ol>
<p>这样，A因为被抢占，其持有自旋锁的总计时间必然延长，导致性能变差。因此个人觉得自旋锁附带禁止抢占，是出于性能优化的考虑。此外，对于单CPU系统，只需要优化掉自旋部分，保留禁用/启用抢占的代码，恰恰可以满足“锁定”需求——禁止抢占导致自旋锁总是可用的，因为不同线程必须串行化执行，没必要自旋。</p>
<p>某些情况下并不需要自旋锁，但仍然需要关闭内核抢占。例如对于Per-cpu变量，如果没有自旋锁保护、同时内核是抢占式的，那么新调度的任务就可能访问同一个变量，这是一种伪并发。同样的，对于单CPU系统，不管是不是Per-cpu变量，前述伪并发问题都存在，区别就是Per-cpu变量不需要自旋锁。此时可以直接调用下面的API来禁止抢占：</p>
<pre class="crayon-plain-tag">preempt_disable(); //增加抢占计数器，从而禁止抢占
/* 抢占被禁止 */
preempt_enable(); //减少抢占计数器，如果为0，则启用抢占，并执行被挂起需要调度的任务
//注意：对于单CPU的自旋锁，加锁、解锁代码已经优化为上面两条语句，因此该代码可专用于保护Per-cpu数据而不必使用更重的自旋锁

//其它函数
preempt_enable_no_resched(); //与上面类似，但是不检查任何挂起的调度
preempt_count(); //返回抢占计数器</pre>
<p>需要注意的是，<pre class="crayon-plain-tag">preempt_disable()</pre> 和<pre class="crayon-plain-tag">preempt_enable()</pre> <span style="background-color: #c0c0c0;">调用次数必须一致</span>，才能最终启用抢占。 </p>
<p>更简洁的保护Per-cpu数据的方式是使用：</p>
<pre class="crayon-plain-tag">int cpu;
cpu = get_cpu(); //禁用内核抢占，并获得当前CPU
/* 操控Per-cpu数据 */
put_cpu(); //恢复内核抢占</pre>
<div class="blog_h2"><span class="graybg">屏障</span></div>
<p>有时候指令执行顺序问题很重要：</p>
<ol>
<li>当在多处理器之间、硬件设备之间进行同步时，需要明确<span style="background-color: #c0c0c0;">按代码中指定的顺序进行读内存、写内存操作</span></li>
<li>和硬件交互时，常需要<span style="background-color: #c0c0c0;">确保一个读操作发生在其它读写之前</span></li>
<li>在多处理器上，可能需要按照写顺序来读取数据</li>
</ol>
<p>但是，编译器和处理器为了提高效率，可能对读写指令进行<span style="background-color: #c0c0c0;">重新排序</span>，这是问题复杂化了。幸好，执行指令重排的处理器都提供了<span style="background-color: #c0c0c0;">机器指令</span>来保障顺序要求，我们也可以指示编译器不在给定点周围的指令序列进行重新排序。这些确保顺序的指令称为<span style="background-color: #c0c0c0;">屏障（Barriers）</span>。</p>
<p>考虑代码：<pre class="crayon-plain-tag">a = 1; b = 2;</pre> ，在某些处理器上，对b的赋值可能发生在对a赋值之前（<span style="background-color: #c0c0c0;">Happens-bafore</span>）。这是由于编译器、CPU都以为a和b没有任何关系，因此编译器可能在编译期间静态的重新排序，生成和编码时不一样的目标码；CPU也可能在执行期间动态的重新排序，抓取和分发貌似无关的指令以达到最高执行效率。对于<pre class="crayon-plain-tag">a = 1; b = a;</pre> 这样的代码， 重新排序就不会发生，因为这两条指令显然是<span style="background-color: #c0c0c0;">存在依赖关系</span>的，改变顺序语义就不同。</p>
<p>不管是编译器还是CPU，都不知道其它上下文（进程）中的相关代码，特定条件下指令重排可能导致严重的逻辑错误（例如<a href="/singleton-pattern#double-checked-locking">双重检查锁定</a>问题，其本质就是写地址和写对象操作的指令重排）。</p>
<p>Linux提供了若干内存和编译器屏障函数：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;">屏障</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>rmb()</td>
<td>读内存屏障，阻止载入（load，即读）操作跨越此屏障重排序，也就是说，此屏障之前的任何load操作都不会重排到屏障后面</td>
</tr>
<tr>
<td>wmb()</td>
<td>写内存屏障，阻止存储（store，即写）操作跨越此屏障重排序，也就是说，此屏障之前的任何store操作都不会重排到屏障后面</td>
</tr>
<tr>
<td>mb()</td>
<td>同时提供读写内存屏障</td>
</tr>
<tr>
<td>read_barrier_depends()</td>
<td>与读内存屏障类似，但是仅仅针对此屏障<span style="background-color: #c0c0c0;">后面读操作依赖的那些</span>载入，不依赖的load操作仍然可能重排序</td>
</tr>
<tr>
<td>smp_rmb()</td>
<td rowspan="4">这几个宏对屏障进行了优化：对于SMP内核，等价于上面的屏障；对于单处理器内核，优化为编译器屏障</td>
</tr>
<tr>
<td>smp_read_barrier_depends()</td>
</tr>
<tr>
<td>smp_wmb()</td>
</tr>
<tr>
<td>smp_mb()</td>
</tr>
<tr>
<td>barrier()</td>
<td>防止编译器跨越此屏障进行load、store操作的重排序。前面讨论的内存屏障可以完成barrier()的功能，但是barrier()<span style="background-color: #c0c0c0;">更加轻量</span></td>
</tr>
</tbody>
</table>
<p>对于不同的体系结构，屏障的实际效果差别很大，例如对于不会打乱存储顺序的体系结构（例如X86），<pre class="crayon-plain-tag">wmb()</pre> 不做任何事情。 </p>
<p>下面是<pre class="crayon-plain-tag">mb()</pre>和<pre class="crayon-plain-tag">wmb()</pre>的一个示例，假设共享变量初始值<pre class="crayon-plain-tag">a = 1; b = 2;</pre> ，则下面的指令序列：</p>
<pre class="crayon-plain-tag">/* 线程1          线程2*/
 a = 3;             
 mb();
 b = 4;          c = b;
                 rmb(); 
                 d = a;
//wmb()、rmb()相当于提示CPU，在继续执行之前，立即提交尚未处理的存储、载入指令</pre>
<p>如果线程1不使用屏障，那么c可能为4而d却为1（c接受b的新值，而d却接受初值），其原因是，没有内存屏障的情况下，线程1对a和b的赋值顺序可能颠倒。而使用写屏障后，如果b为4则势必a为3。 </p>
<p>同样，如果线程2不使用读屏障，那么可能在读取b之前读a，这就无法在c为4的情况下，断定a必然为3。因为重排后a先读取，线程1可能刚执行到第1行；而b被读取时，线程1已经执行完第3行，导致c的值为4。</p>
<p>有些时候可以使用<pre class="crayon-plain-tag">read_barrier_depends()</pre> 代替读屏障：</p>
<pre class="crayon-plain-tag">pp = p;
read_barrier_depends();
b = *pp; //由于载入*pp依赖于载入p，因此该屏障保证顺序性</pre>
<div class="blog_h1"><span class="graybg">定时器和时间管理</span></div>
<p>对于内核来说时间管理非常重要：</p>
<ol>
<li>内核中大量函数是时间驱动的——这些函数要么延迟一段时间执行（推迟的磁盘I/O），要么周期性执行（红黑树平衡性调整）</li>
<li>内核需要管理系统的运行时间</li>
<li>内核需要管理当前日期时间</li>
</ol>
<p>内核关心两种时间概念：绝对时间、相对时间，所谓相对时间是指相对于当前时间的延迟。不同场景下可能分别用到这两个时间概念。</p>
<p><span style="background-color: #c0c0c0;">系统定时器</span>是一种可编程硬件芯片，它以固定频率产生中断（例如10ms），该中断称为<span style="background-color: #c0c0c0;">定时器中断</span>。该中断对应的<span style="background-color: #c0c0c0;">时钟中断程序</span>负责更新系统时间、驱动周期性任务的运行。系统定时器和时钟中断程序是Linux内核管理机制中的<span style="background-color: #c0c0c0;">中枢</span>。</p>
<div class="blog_h2"><span class="graybg">内核中的时间概念</span></div>
<p>内核需要硬件配合才能计算和管理时间，这个硬件就是系统定时器。系统定时器以<span style="background-color: #c0c0c0;">特定频率定期自行触发时钟中断</span>，此频率称为<span style="background-color: #c0c0c0;">节拍率（Tick rate）</span>，可以通过<span style="background-color: #c0c0c0;">编程</span>指定。</p>
<p>由于节拍率对于内核可知，因此内核就可以根据时钟中断发生的次数来推断<span style="background-color: #c0c0c0;">系统已运行的时间</span>，并推断<span style="background-color: #c0c0c0;">墙上时间（Wall time）</span>。</p>
<p>利用时间中断周期性执行的工作包括：</p>
<ol>
<li>更新系统运行时间</li>
<li>更新墙上时间</li>
<li>在SMP系统中，均衡调度程序中<span style="background-color: #c0c0c0;">各处理器上的运行队列</span>，维持负载均衡</li>
<li>检查当前进程是否用完<span style="background-color: #c0c0c0;">时间片</span>，如果用完，则<span style="background-color: #c0c0c0;">重新调度</span></li>
<li>运行timout的动态定时器</li>
<li>更新资源消耗和处理器时间的统计值</li>
</ol>
<p>这些工作中，有的是在<span style="background-color: #c0c0c0;">每次</span>时钟中断时都执行，有些则是<span style="background-color: #c0c0c0;">每隔n次</span>时钟中断执行一次。</p>
<div class="blog_h2"><span class="graybg">节拍率：HZ</span></div>
<p>节拍率通常是静态预处理定义的，对应宏为HZ，内核启动时会根据这一值对硬件进行设置。不同体系结构HZ默认值不一样，x86为100，即每秒100此时钟中断。</p>
<p>调整HZ对系统又重大的影响，高HZ的好处是：</p>
<ol>
<li>更高的时钟中断频率（解析度）可以提高时间驱动事件的<span style="background-color: #c0c0c0;">解析度</span>。对于100HZ，系统中事件周期最快只能有10ms，不能更短，而调整为1KHZ时则能够短至1ms</li>
<li>提高事件驱动事件的<span style="background-color: #c0c0c0;">准确度</span>。定时任务可能在任何一个时刻超时，而只有在时钟中断发生时，才能执行任务，这意味着最大可能导致1/HZ秒的误差，平均误差则是2/HZ——对于100HZ为±5ms，对于1KHZ则小于1ms</li>
</ol>
<p>从更细节的角度来说，高HZ的优势包括：</p>
<ol>
<li>内核定时器以更高频率和准确度运行，这将带来大量好处</li>
<li>依赖定时值执行的系统调用，例如poll()和select()，可以更精确的运行。这将大幅提高系统性能，因为频繁调用这两个系统调用的应用程序将在等待时钟等待上浪费大量时间</li>
<li>对资源消耗、系统运行时间的度量更精确</li>
<li>提高进程抢占的精确度，时钟中断程序负责减少当前进程的时间片计数，如果跌至0的同时need_resched被设置，内核就会立刻重新运行调度程序，假设当前进程只剩1ms的时间，而时钟中断在5ms后才发生，那么进程可能多运行5ms。这个问题一般情况下不严重，因为这种误差是针对所有进程的。但是，对于时间非常敏感的“实时”应用，则不能容忍</li>
</ol>
<p>高HZ获得益处的同时，也要付出相应的代价——HZ越高，中断处理程序占用的CPU时间越多，这导致应用程序<span style="background-color: #c0c0c0;">可用CPU时间减少</span>，并且频繁的<span style="background-color: #c0c0c0;">打乱CPU高速缓存</span>。在现代硬件中，<span style="background-color: #c0c0c0;">高达1000HZ的节拍率不会造成难以接收的负担</span>。</p>
<div class="blog_h3"><span class="graybg">无（动态）节拍系统</span></div>
<p>Linux支持动态节拍，当编译内核时指定<span style="background-color: #c0c0c0;">CONFIG_HZ</span>选项， 那么系统会根据需要动态调整HZ值，这可以减少空闲系统的电量消耗。</p>
<div class="blog_h2"><span class="graybg">jiffies</span></div>
<p>全局变量jiffies用来记录自系统启动以来，产生的节拍的总数。该变量用来计算已经启动的时间：</p>
<pre class="crayon-plain-tag">//volatile提示不适用CPU缓存，每次都到主存获取此变量的值	 	 
extern unsigned long volatile jiffies; //类型必须明确为unsigned long，使用任何其它类型存储均是错误的	 	 
//将秒数转换为jffies	 	 
(seconds * HZ)	 	 
//将jiffies转换为秒树	 	 
(jiffies / HZ)</pre>
<p>将jiffies转换为秒，往往在与用户空间通信时才需要，内核很少关心绝对时间。</p>
<p>在内核jiffies被表示为无符号长整型，这意味着在32位体系结构上它是32位，64位系统上则是64位。在100HZ设置下，32位系统497天后此变量就溢出；在1000HZ下仅49天后就会溢出。如果使用64位jiffes，则不会溢出。</p>
<p>出于兼容性的考虑，内核使用<pre class="crayon-plain-tag">extern u64 jiffies_64</pre> 存储64位jiffies，而变量jiffies和它存放到一个区域，占用它的低32位。这样，老的32位代码就可以和以前一样访问jiffies（统计流逝的时间），而时间管理代码则使用jiffies_64，避免溢出。访问jiffies的代码只能获取低32位，函数<pre class="crayon-plain-tag">get_jiffies_64()</pre> 可以用来读取整个64位的值。在64位体系结构上jiffies_64和jiffies完全等同。</p>
<div class="blog_h3"><span class="graybg">jiffies回绕问题</span></div>
<p>对于32位系统，存在回绕问题，即jiffies（jiffies_64低32位）达到2^32-1后，其值会回复到0 的问题，这会导致计算时间差的代码出现错误：</p>
<pre class="crayon-plain-tag">unsigned long timeout = jiffies + HZ / 2; /* 半秒后超时 */	 	 
/* 完成一些工作 */	 	 
/* 判断工作消耗时间是否过大 */	 	 
if (timeout &gt; jiffies)	 	 
{	 	 
 /* 如果jiffies回绕，可能导致判断错误，基本上timeout必然大于jiffies */	 	 
}</pre>
<p>因此，直接使用jiffies进行算术运算是不安全的，应当使用下面的宏：</p>
<pre class="crayon-plain-tag">//当a大于b时返回真	 	 
#define time_after(a,b) \	 	 
 (typec heck(unsigned long, a) &amp;&amp; \	 	 
 typecheck(unsigned long, b) &amp;&amp; \	 	 
 ((long)(b) - (long)(a) &lt; 0))	 	 
//这里把无符号数作为有符号数看待，回绕前的大数最高位必然是1，认为是负数，回绕后最高为0，认为是正数	 	 
//因此回绕后的值总被认为是大的数，也就是after	 	 
#define time_before(a,b) time_after(b,a)	 	 
#define time_after_eq(a,b) \	 	 
 (typecheck(unsigned long, a) &amp;&amp; \	 	 
 typecheck(unsigned long, b) &amp;&amp; \	 	 
 ((long)(a) - (long)(b) &gt;= 0))	 	 
#define time_before_eq(a,b) time_after_eq(b,a)</pre>
<div class="blog_h3"><span class="graybg">用户空间和jiffies</span></div>
<p>很多应用程序依赖于将HZ看做是100，则就导致当内核改变HZ时，用户空间的应用程序出现问题。为此，内核定义了<pre class="crayon-plain-tag">USER_HZ</pre> 来表示用户空间“看到”的节拍率，在x86上它就是100。内核使用函数在HZ和USER_HZ表示的<span style="background-color: #c0c0c0;">节拍计数</span>之间进行转换：</p>
<pre class="crayon-plain-tag">//把基于HZ计数的jiffies转换为基于USER_HZ技术的用户空间节拍数	 	 
clock_t jiffies_to_clock_t(long x);	 	 
//类似上面，转换64位jiffies	 	 
u64 jiffies_64_to_clock_t(u64 x);</pre>
<div class="blog_h2"><span class="graybg">硬件时钟和定时器</span></div>
<p><span style="background-color: #c0c0c0;">系统定时器</span>是内核定时机制中最重要的角色，其硬件实现有数种，x86主要使用<span style="background-color: #c0c0c0;">可编程中断时钟（PIT）</span>，可以在内核启动时对其进行编程初始化。</p>
<p>除了前面提到的系统定时器以外，系统结构还提供了另外一种方式进行即时——实时时钟（RTC）。RTC可以用来持久化的存放系统时间，即时关机后仍然可以依靠主板上的电池持续计时。RTC最重要的作用是用来初始化<pre class="crayon-plain-tag">xtime</pre> 变量，尽管x86等体系会定期把当前时间回写到RTC</p>
<div class="blog_h2"><span class="graybg">时钟中断处理程序</span></div>
<p>时钟中断的功能可以分为体系结构相关、无关两部分。前者作为系统定时器的中断处理程序注册到内核，它至少需要完成下面的工作：</p>
<ol>
<li>获得<pre class="crayon-plain-tag">xtime_lock</pre> 锁，以便访问<pre class="crayon-plain-tag">jiffies_64</pre> ，并对墙上时间xtime进行保护</li>
<li>必要时，应答或者设置系统时间</li>
<li>周期性的使用墙上时间更新实时时钟</li>
<li>调用体系结构无关的时钟例程：<pre class="crayon-plain-tag">tick_periodic()</pre></li>
</ol>
<p><pre class="crayon-plain-tag">tick_periodic()</pre> 是体系结构无关的部分，它完成：</p>
<ol>
<li>递增<pre class="crayon-plain-tag">jiffies_64</pre> 变量</li>
<li>更新资源消耗的统计值，必然当前进程的<span style="background-color: #c0c0c0;">系统时间</span>和<span style="background-color: #c0c0c0;">用户时间</span></li>
<li>执行已经到期的<span style="background-color: #c0c0c0;">动态定时器</span>：<pre class="crayon-plain-tag">run_local_timers</pre></li>
<li>调用<pre class="crayon-plain-tag">scheduler_tick()</pre> ，该函数负责更新剩余时间片等</li>
<li>更新存储在<pre class="crayon-plain-tag">xtime</pre> 变量的墙上时钟</li>
<li>计算平均负载值</li>
</ol>
<p>该函数的代码分析如下：</p>
<pre class="crayon-plain-tag">static void tick_periodic( int cpu )	 	 
{	 	 
 if ( tick_do_timer_cpu == cpu )	 	 
 {	 	 
 write_seqlock( &amp;xtime_lock ); //使用序列锁定	 	 
 /* 跟踪下一个节拍事件 */	 	 
 tick_next_period = ktime_add( tick_next_period, tick_period );	 	 
 do_timer( 1 );	 	 
 write_sequnlock( &amp;xtime_lock );	 	 
 }	 	 
 /**	 	 
 * user_mode通过读取当前寄存器来判断处于用户空间还是内核空间	 	 
 * 这并不准确，因为：	 	 
 * 1、在一个节拍内，进程可以多次出入内核	 	 
 * 2、在一个节拍内，该进程不一定真正完全占有CPU	 	 
 * 不幸的是，内核只能支持到这一程度	 	 
 */	 	 
 update_process_times( user_mode( get_irq_regs() ) );	 	 
 profile_tick( CPU_PROFILING );	 	 
}	 	 
void do_timer( unsigned long ticks )	 	 
{	 	 
 jiffies_64 += ticks; //更新jiffies	 	 
 update_wall_time(); //更新墙上时间	 	 
 calc_global_load(); //计算平均负载统计值	 	 
}	 	 
//更新各种处理时间	 	 
void update_process_times( int user_tick )	 	 
{	 	 
 struct task_struct *p = current;	 	 
 int cpu = smp_processor_id();	 	 
 account_process_tick( p, user_tick );	 	 
 run_local_timers(); //运行本地的动态定时器	 	 
 rcu_check_callbacks( cpu, user_tick );	 	 
 printk_tick();	 	 
 perf_event_do_pending();	 	 
 scheduler_tick();	 	 
 run_posix_cpu_timers( p );	 	 
}	 	 
/*	 	 
 * 计算一个节拍的CPU时间	 	 
 * @p: CPU时间统计到哪个进程头上	 	 
 * @user_tick: 只是这是用户节拍还是系统节拍	 	 
 */	 	 
void account_process_tick(struct task_struct *p, int user_tick)	 	 
{	 	 
 cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);	 	 
 struct rq *rq = this_rq();	 	 
 if (user_tick)	 	 
 //计算为用户空间的时间	 	 
 account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);	 	 
 else if ((p != rq-&gt;idle) || (irq_count() != HARDIRQ_OFFSET))	 	 
 //计算为内核空间的时间	 	 
 account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,	 	 
 one_jiffy_scaled);	 	 
 else	 	 
 //位于内核空间，运行idle线程，计算为系统空闲时间	 	 
 account_idle_time(cputime_one_jiffy);	 	 
}	 	 
/**	 	 
 * 减少当前进程的时间片计数值，必要时设置need_resched	 	 
 */	 	 
void scheduler_tick(void)	 	 
{	 	 
 int cpu = smp_processor_id();	 	 
 struct rq *rq = cpu_rq(cpu);	 	 
 struct task_struct *curr = rq-&gt;curr;	 	 
 sched_clock_tick();	 	 
 raw_spin_lock(&amp;rq-&gt;lock);	 	 
 update_rq_clock(rq);	 	 
 update_cpu_load(rq); //更新CPU负载统计信息	 	 
 curr-&gt;sched_class-&gt;task_tick(rq, curr, 0);	 	 
 raw_spin_unlock(&amp;rq-&gt;lock);	 	 
 perf_event_task_tick(curr);	 	 
#ifdef CONFIG_SMP	 	 
 rq-&gt;idle_at_tick = idle_cpu(cpu);	 	 
 trigger_load_balance(rq, cpu); //在SMP上负责平衡运行队列	 	 
#endif	 	 
}</pre>
<div class="blog_h2"><span class="graybg">墙上时间</span></div>
<p>当天的真实时间定义在<pre class="crayon-plain-tag">kernel/time/timekeeping.c</pre> 中：<pre class="crayon-plain-tag">struct timespec xtime;</pre> ，其类型定义在：</p>
<pre class="crayon-plain-tag">struct timespec {	 	 
 __kernel_time_t tv_sec; /* 从19700101到现在的秒数 */	 	 
 long tv_nsec; /* 从上一秒开始流逝的纳秒数 */	 	 
};</pre>
<p>读写此变量需要获得序列锁<pre class="crayon-plain-tag">xtime_lock</pre> 进行写锁定：</p>
<pre class="crayon-plain-tag">write_seqlock(&amp;xtime_lock);	 	 
/* 在这里更新时间 */	 	 
write_sequnlock(&amp;xtime_lock);</pre>
<p>读取此变量时则需要获得读锁：</p>
<pre class="crayon-plain-tag">do	 	 
{	 	 
 unsigned long lost;	 	 
 seq = read_seqbegin(&amp;xtime_lock); //获得读锁	 	 
 usec = timer-&gt;get_offset();	 	 
 lost = jiffies - wall_jiffies;	 	 
 if (lost) usec += lost * (1000000 / HZ);	 	 
 sec = xtime.tv_sec;	 	 
 usec += (xtime.tv_nsec / 1000);	 	 
}while (read_seqretry(&amp;xtime_lock, seq)); //如果乐观并发失败，重试</pre>
<p>从用户空间获得墙上时间，需要调用<pre class="crayon-plain-tag">gettimeofday()</pre> ，它对应系统调用<pre class="crayon-plain-tag">sys_gettimeofday()</pre> 。</p>
<p>除了更新墙上时间以外，内核本身很少使用xtime变量，文件系统代码是一个例外，因为它需要存储多种时间戳在inode中。</p>
<div class="blog_h2"><span class="graybg">动态定时器</span></div>
<p>又称内核定时器（kernel timers），或者简称定时器，是内核能够管理时间流逝的关键。内核常常需要在一定时间后执行特定函数，内核定时器恰恰满足需要。</p>
<p>定时器由下面的结构表示：</p>
<pre class="crayon-plain-tag">struct timer_list	 	 
{	 	 
 struct list_head entry; /* 定时器链表的入口 */	 	 
 unsigned long expires; /* jiffies到达多少时超时 */	 	 
 void (*function)( unsigned long ); /* 定时器处理函数 */	 	 
 unsigned long data; /* 处理函数入参 */	 	 
 struct tvec_t_base_s *base; /* 内部字段，不需要使用 */	 	 
};</pre>
<p>下面的代码例示了如何使用定时器：</p>
<pre class="crayon-plain-tag">//定义一个定时器	 	 
struct timer_list my_timer;	 	 
//初始化	 	 
init_timer(&amp;my_timer);	 	 
my_timer.expires = jiffies + delay;	 	 
my_timer.data = 0;	 	 
my_timer.function = my_function;	 	 
//激活定时器	 	 
add_timer(&amp;my_timer);	 	 
//修改超时时间	 	 
mod_timer(&amp;my_timer, jiffies + new_delay);	 	 
//删除定时器（必须在超时前）	 	 
del_timer(&amp;my_timer);	 	 
/**	 	 
 * 注意删除定时器存在潜在的竞态条件：该函数返回后，只能保证定时器不会在将来执行	 	 
 * 但是在SMP上，其它CPU可能正在运行定时器中断，因此删除定时器时可能需要等待	 	 
 * 其它处理器上的定时器处理程序都退出。	 	 
 * 在几乎全部场景下，都应该使用下面的函数代替del_timer	 	 
 */	 	 
del_timer_sync(&amp;my_timer); //注意该函数可能休眠，不能在中断上下文使用	 	 
//任何使用不要尝试用下面的代码代替mod_timer，在SMP上不安全	 	 
del_timer(my_timer);//此时其它CPU可能正在执行定时器…	 	 
my_timer-&gt;expires = jiffies + new_delay;	 	 
add_timer(my_timer);</pre>
<p>需要注意的是，定时器不能用来执行硬实时的任务，因为它可能在超时后的<span style="background-color: #c0c0c0;">下一个节拍</span>时才执行。</p>
<p>由于定时器代码和当前代码是异步的，因此存在潜在的竞争条件，需要注意同步。</p>
<div class="blog_h3"><span class="graybg">定时器的实现</span></div>
<p>内核在时钟中断发生后执行定时器，定时器<span style="background-color: #c0c0c0;">作为软中断在下半部上下文</span>（bottom-half context）中执行。其核心逻辑位于：</p>
<pre class="crayon-plain-tag">/*	 	 
 * Called by the local, per-CPU timer interrupt on SMP.	 	 
 */	 	 
void run_local_timers( void )	 	 
{	 	 
 hrtimer_run_queues();	 	 
 //执行定时器软中断	 	 
 //所有超时的定时器将在本处理器上执行	 	 
 raise_softirq( TIMER_SOFTIRQ );	 	 
 softlockup_tick();	 	 
}</pre>
<p>为避免遍历定时器列表导致的耗时，内核将定时器划分为5组，定时器超时时间接近时，定时器将随组一起下移。</p>
<div class="blog_h2"><span class="graybg">延迟执行</span></div>
<p>除了使用定时器和下半部机制以外，内核代码（特别是驱动）还需要更特殊的手段延迟执行，这种延迟时间往往<span style="background-color: #c0c0c0;">极短</span>。例如，设置网卡的以太网模式可能需要2ms，因此设置网卡速度后，驱动程序需要等待至少2ms才能继续运行。</p>
<p>内核提供了若干延迟执行的方法，其中某些会挂起CPU，防止它执行任何其它工作；另外一些<span style="background-color: #c0c0c0;">则不会挂起CPU，也就不能确保延迟代码能够在特定延迟后执行</span>。</p>
<div class="blog_h3"><span class="graybg">忙等待</span></div>
<p>最简单（也最不理想）的延迟方式，忙等待就是空转CPU，此方法只能在需要延迟的时间是<span style="background-color: #c0c0c0;">节拍的整数倍或者精确度要求不高时使用</span>：</p>
<pre class="crayon-plain-tag">unsigned long timeout = jiffies + 10; /* 10个节拍 */	 	 
//由于jiffies是volatile变量，因此循环中每次都会去主存加载新值	 	 
while (time_before(jiffies, timeout))	 	 
 cond_resched();	 	 
//单纯的忙等待很蹩脚，因此可以调用cond_resched()	 	 
//该函数可以调度一个新程序运行，但是仅仅在need_resched被设置后才会调度	 	 
//也就是说，只有存在一个“更重要的”任务时，才会进行调度	 	 
//由于该方法需要调用调度程序，因而不能在中断上下文中执行</pre>
<div class="blog_h3"><span class="graybg">超短延迟</span></div>
<p>有时内核代码（通常是驱动）需要更短的、精确的延迟，这个延迟往往小于节拍率，这是任何基于jiffies的方法都不可用了。幸好内核提供了可以处理毫秒、微秒（1/1000ms）、纳秒（1/1000us）的函数：</p>
<pre class="crayon-plain-tag">void udelay(unsigned long usecs);//延迟指定微秒	 	 
void ndelay(unsigned long nsecs);//延迟指定纳秒	 	 
void mdelay(unsigned long msecs);//延迟指定毫秒</pre>
<p>这些方法的原理是：内核知道CPU的运行速度——指定时间内能够执行的忙循环的次数 。这一速度称为BogoMIPS（伪的每秒百万指令数），存放在变量<pre class="crayon-plain-tag">loops_per_jiffy</pre> 中，通过文件<pre class="crayon-plain-tag">/proc/cpuinfo</pre> 可以读到bogomips。</p>
<p>内核就是利用<pre class="crayon-plain-tag">loops_per_jiffy</pre> 来精确的判断需要多少次循环，进而实现超短延迟执行。</p>
<p>这些函数不应当在长延迟中使用。</p>
<div class="blog_h3"><span class="graybg">schedule_timeout()</span></div>
<p>该函数可以让任务睡眠若干节拍，然后在投入运行， 注意它不能保证时间精度，用法如下：</p>
<pre class="crayon-plain-tag">/* 设置任务为不可中断睡眠 */	 	 
set_current_state( TASK_INTERRUPTIBLE );	 	 
/* 导致当前任何小睡s秒，然后唤醒 */	 	 
schedule_timeout( s * HZ );</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol2">Linux内核学习笔记（二）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-study-note-vol2/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内核学习笔记（一）</title>
		<link>https://blog.gmem.cc/linux-kernel-study-note-vol1</link>
		<comments>https://blog.gmem.cc/linux-kernel-study-note-vol1#comments</comments>
		<pubDate>Thu, 09 Dec 2010 03:25:17 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=7034</guid>
		<description><![CDATA[<p>Linux内核代码结构 代码目录 说明  arch 特定体系结构的代码 block 块I/O层 crypto 加密API Documentation 内核源代码文档 drivers 设备驱动 firmware 特定驱动需要的设备固件 fs VFS和各文件系统 include 内核头文件 init 内核启动和初始化 ipc <a class="read-more" href="https://blog.gmem.cc/linux-kernel-study-note-vol1">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol1">Linux内核学习笔记（一）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">Linux内核代码结构</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">代码目录</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>arch</td>
<td>特定体系结构的代码</td>
</tr>
<tr>
<td>block</td>
<td>块I/O层</td>
</tr>
<tr>
<td>crypto</td>
<td>加密API</td>
</tr>
<tr>
<td>Documentation</td>
<td>内核源代码文档</td>
</tr>
<tr>
<td>drivers</td>
<td>设备驱动</td>
</tr>
<tr>
<td>firmware</td>
<td>特定驱动需要的设备固件</td>
</tr>
<tr>
<td>fs</td>
<td>VFS和各文件系统</td>
</tr>
<tr>
<td>include</td>
<td>内核头文件</td>
</tr>
<tr>
<td>init</td>
<td>内核启动和初始化</td>
</tr>
<tr>
<td>ipc</td>
<td>进程间通信的代码</td>
</tr>
<tr>
<td>kernel</td>
<td>核心子系统，例如进程调度器</td>
</tr>
<tr>
<td>lib</td>
<td>助手类例程</td>
</tr>
<tr>
<td>mm</td>
<td>内存管理子系统和虚拟内存</td>
</tr>
<tr>
<td>net</td>
<td>网络子系统</td>
</tr>
<tr>
<td>samples</td>
<td>样例代码</td>
</tr>
<tr>
<td>scripts</td>
<td>用于构建内核的脚本</td>
</tr>
<tr>
<td>security</td>
<td>Linux安全模块</td>
</tr>
<tr>
<td>sound</td>
<td>声音子系统</td>
</tr>
<tr>
<td>usr</td>
<td>早期的用户空间代码</td>
</tr>
<tr>
<td>tools</td>
<td>开发Linux时有用的工具</td>
</tr>
<tr>
<td>virt</td>
<td>虚拟化基础设施</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Linux内核编程的特点</span></div>
<p>Linux内核使用C语言和汇编编写，其涵盖了ISO C99标准和GNU C的扩展特性。内核编程具有以下特点：</p>
<ol>
<li>内联函数：C99和GNU C都支持内联函数，内联函数会在调用位置展开，从而避免函数调用的开销（寄存器存储与恢复），但是会导致代码变长，一般适用于<span style="background-color: #c0c0c0;">小型、反复使用的函数</span>。定义内联函数时需要使用<pre class="crayon-plain-tag">static inline</pre> 修饰之，内联函数一般在头文件中定义。在内核中为了安全和易读性，优先使用内联函数而不是复杂的宏</li>
<li>编译器可以根据gcc内建指令对分支语句进行优化：<pre class="crayon-plain-tag">if(likely(error)){...}</pre> ，likely和unlikely分别用于提示编译器，该分支经常或者很少运行</li>
<li>没有内存保护机制：与用户程序不同，内核代码不具有内存保护机制，因此访问非法的内存地址可能导致oops，导致内核崩溃。此外，内核中的内存也不分页，因此每用掉一个字节，可用物理内存就少一个字节</li>
<li>浮点运算困难：应当尽量避免在内核中进行浮点运算</li>
<li>容积小而<span style="background-color: #c0c0c0;">固定的栈</span>：用户空间的程序可以在栈上分配大量空间来存放变量，甚至巨大的结构体、长度以千计的数组，自所以允许这样做是因为<span style="background-color: #c0c0c0;">用户空间的栈比较大而且支持动态增长</span>。内核栈的大小随着体系结构而变化，在x86上栈的大小在编译时配置，可以是4kb或者8kb，一般配置为2个页。每个处理器都有自己的栈</li>
<li>同步和并发问题较多</li>
<li>可移植性很重要：由于Linux是可以在多个平台上移植的OS，因此需要注重可移植性</li>
</ol>
<div class="blog_h1"><span class="graybg">进程管理</span></div>
<p>Unix类系统将<span style="background-color: #c0c0c0;">进程</span>和它<span style="background-color: #c0c0c0;">正在执行的程序</span>做了清晰的划分。<pre class="crayon-plain-tag">fork()</pre> 和<pre class="crayon-plain-tag">_exit()</pre> 系统调用分别用于创建和终结进程。而<pre class="crayon-plain-tag">exec()</pre> 类的系统调用则用来加载一个程序，一旦该系统调用执行，进程将<span style="background-color: #c0c0c0;">加载全新的地址空间</span>并继续执行。</p>
<p>大多数现代操作系统支持多线程应用程序。多线程应用程序由若干个相对独立的执行流（execution flow）构成。这些执行流被称为“线程”（Thread），或者“轻量级进程（LWP）”，线程们共享地址空间、物理内存页、打开文件。</p>
<p>很多商业Unix变体的LWP是基于内核线程（kernel threads）实现的，而Linux则不同，它把LWP作为基本的执行上下文（execution context）看待。这样，对于Linux来说，线程和进程没有本质区别。它们都可以通过非标准系统调用<pre class="crayon-plain-tag">clone()</pre> 生成。</p>
<div class="blog_h2"><span class="graybg">进程</span></div>
<p>进程就是处于执行状态的程序，进程不仅仅局限于一段<span style="background-color: #c0c0c0;">可执行代码</span>（Unix称其为代码段，Text Section），还包括其它重要的资源，例如<span style="background-color: #c0c0c0;">打开的文件、挂起的信号、内核内部数据、处理器状态</span>、一个或者多个具有内存映射的<span style="background-color: #c0c0c0;">地址空间</span>、一个或者多个<span style="background-color: #c0c0c0;">执行线程</span>、存放全局变量的<span style="background-color: #c0c0c0;">数据段</span>。</p>
<p>执行线程，简称线程，是在进程中活动的对象。<span style="background-color: #c0c0c0;">每个线程具有一个独立的程序计数器、进程栈、一组进程寄存器</span>。<span style="background-color: #c0c0c0;">内核调度的对象是线程</span>而不是进程，传统Unix系统中一个进程只有一个线程，现在多线程的程序司空见惯。Linux并不特别区分线程和进程。</p>
<p>现代OS提供了两种进程虚拟机制：</p>
<ol>
<li>虚拟处理器：虽然可能很多进程在分享处理器，但是虚拟处理器给进程一个假象，好像是它自己在<span style="background-color: #c0c0c0;">独享CPU</span></li>
<li>虚拟内存：让进程在分配和管理内存的时候，觉得自己<span style="background-color: #c0c0c0;">拥有整个系统的所有内存资源</span></li>
</ol>
<p>线程之间可能共享虚拟内存，但是必然有各自的虚拟处理器。</p>
<p>进程的生命周期从创建时开始，在Linux中，这是通过<pre class="crayon-plain-tag">fork()</pre> 调用实现的，fork()以当前进程为蓝本复制一个子进程。fork()调用从<span style="background-color: #c0c0c0;">内核返回两次</span>，一次回到父进程，一次回到子进程。通常fork()之后会立即调用<pre class="crayon-plain-tag">exec()</pre> 以替换当前进程，以创建新的地址空间并载入新的程序。现代Linux系统中，fork()由系统调用<pre class="crayon-plain-tag">clone()</pre> 实现。</p>
<p><span style="background-color: #c0c0c0;">Linux内核通常把进程称为任务（Task）</span>。</p>
<div class="blog_h2"><span class="graybg">进程组和登录会话</span></div>
<p>Linux支持<span style="background-color: #c0c0c0;">进程组</span>（Process group）的概念，用以表示一种<span style="background-color: #c0c0c0;">作业（Job）的抽象</span>。例如，在Shell里面执行：<pre class="crayon-plain-tag">ls | sort | more</pre> 后，Shell会自动为三个进程创建一个组，就好像它们是一个单独实体。进程描述符包含一个字段，用来指明它属于的进程组。每一个进程组可以有一个<span style="background-color: #c0c0c0;">领头进程，它的PID和进程组的ID相同</span>。新创建的进程最初加入到其父进程所在组中。</p>
<p>Linux同时支持<span style="background-color: #c0c0c0;">登录会话</span>（Login session）的概念，一个会话包括了在<span style="background-color: #c0c0c0;">特定终端上启动工作会话的进程的全部子进程</span>。一个登录会话可以有多个同时运作的进程组，其中只有一个进程组一直处于前台，该组可以访问终端。很多发行版使用bg/fg命令，来把一个进程组（作业）放到后台/前台。</p>
<div class="blog_h2"><span class="graybg">进程描述符以及任务结构</span></div>
<p>内核将进程的列表存放在称为“Task list”的双向循环链表中，其每一项均是定义在include/linux/sched.h中的<pre class="crayon-plain-tag">task_struct</pre> 结构，该结构被称为<span style="background-color: #c0c0c0;">进程描述符（Process descriptor）</span>。该描述符能完整的描述一个正在执行的程序。<pre class="crayon-plain-tag">current</pre> 宏代表了当前正在执行的进程的描述符。</p>
<p>Linux通过slab分配器来分配task_struct结构，该分配器会预先分配、重复使用task_struct，以避免动态分配和释放所带来的资源消耗。在2.6以前的内核中，各进程的task_struct存放在它们内核栈的尾端。由于现在使用slab动态分配器生成task_struct，只需要在栈底（对于向下增长的栈）创建一个<pre class="crayon-plain-tag">struct thread_info</pre> 结构即可，该结构依据体系结构的不同，定义有所差异，x86的定义如下：</p>
<pre class="crayon-plain-tag">struct thread_info
{
    struct task_struct *task; /* main task structure */
    struct exec_domain *exec_domain; /* execution domain */
    __u32 flags; /* low level flags */
    __u32 status; /* thread synchronous flags */
    __u32 cpu; /* current CPU */
    int preempt_count; /* 0 =&gt; preemptable,  BUG */
    mm_segment_t addr_limit;
    struct restart_block restart_block;
    void __user *sysenter_return;
#ifdef CONFIG_X86_32
    unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */
    __u8 supervisor_stack[0];
#endif
    int uaccess_err;
};</pre>
<p>注意该结构的第一个成员变量<pre class="crayon-plain-tag">struct task_struct</pre>包含了进程状态的主要信息，这些状态会被进程调度器使用：</p>
<pre class="crayon-plain-tag">struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, &gt;0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

	int lock_depth;		/* BKL lock depth */
	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	const struct sched_class *sched_class;
	struct sched_entity se;  /* 进程调度实体 */
	struct sched_rt_entity rt;
	unsigned char fpu_counter;
	unsigned int policy;
	cpumask_t cpus_allowed;
	struct list_head tasks;
	struct plist_node pushable_tasks;

	struct mm_struct *mm, *active_mm;
/* task state */
	int exit_state;
	int exit_code, exit_signal;
	int pdeath_signal;  /*  The signal sent when the parent dies  */
	/* ??? */
	unsigned int personality;
	unsigned did_exec:1;
	unsigned in_execve:1;	/* Tell the LSMs that the process is doing an
				 * execve */
	unsigned in_iowait:1;

	/* Revert to default priority/policy when forking */
	unsigned sched_reset_on_fork:1;

	pid_t pid;
	pid_t tgid;
	/* 
	 * pointers to (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p-&gt;father can be replaced with 
	 * p-&gt;real_parent-&gt;pid)
	 */
	struct task_struct *real_parent; /* real parent process */
	struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
	/*
	 * children/sibling forms the list of my natural children
	 */
	struct list_head children;	/* list of my children */
	struct list_head sibling;	/* linkage in my parent's children list */
	struct task_struct *group_leader;	/* threadgroup leader */

	/*
	 * ptraced is the list of tasks this task is using ptrace on.
	 * This includes both natural children and PTRACE_ATTACH targets.
	 * p-&gt;ptrace_entry is p's link on the p-&gt;parent-&gt;ptraced list.
	 */
	struct list_head ptraced;
	struct list_head ptrace_entry;

	/*
	 * This is the tracer handle for the ptrace BTS extension.
	 * This field actually belongs to the ptracer task.
	 */
	struct bts_context *bts;

	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	struct list_head thread_group;

	struct completion *vfork_done;		/* for vfork() */
	int __user *set_child_tid;		/* CLONE_CHILD_SETTID */
	int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */
	cputime_t utime, stime, utimescaled, stimescaled;
	cputime_t gtime;
	cputime_t prev_utime, prev_stime;
	unsigned long nvcsw, nivcsw; /* context switch counts */
	struct timespec start_time; 		/* monotonic time */
	struct timespec real_start_time;	/* boot based time */
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
	unsigned long min_flt, maj_flt;

	struct task_cputime cputime_expires;
	struct list_head cpu_timers[3];

/* process credentials */
	const struct cred *real_cred;	/* objective and real subjective task
					 * credentials (COW) */
	const struct cred *cred;	/* effective (overridable) subjective task
					 * credentials (COW) */
	struct mutex cred_guard_mutex;	/* guard against foreign influences on
					 * credential calculations
					 * (notably. ptrace) */
	struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */

	char comm[TASK_COMM_LEN]; /* executable name excluding path
				     - access with [gs]et_task_comm (which lock
				       it with task_lock())
				     - initialized normally by setup_new_exec */
/* file system info */
	int link_count, total_link_count;
/* CPU-specific state of this task */
	struct thread_struct thread;
/* filesystem information */
	struct fs_struct *fs;
/* open file information */
	struct files_struct *files;
/* namespaces */
	struct nsproxy *nsproxy;
/* signal handlers */
	struct signal_struct *signal;
	struct sighand_struct *sighand;

	sigset_t blocked, real_blocked;
	sigset_t saved_sigmask;	/* restored if set_restore_sigmask() was used */
	struct sigpending pending;

	unsigned long sas_ss_sp;
	size_t sas_ss_size;
	int (*notifier)(void *priv);
	void *notifier_data;
	sigset_t *notifier_mask;
	struct audit_context *audit_context;
	seccomp_t seccomp;

/* Thread group tracking */
   	u32 parent_exec_id;
   	u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed,
 * mempolicy */
	spinlock_t alloc_lock;
	/* Protection of the PI data structures: */
	raw_spinlock_t pi_lock;
/* journalling filesystem info */
	void *journal_info;

/* stacked block device info */
	struct bio_list *bio_list;

/* VM state */
	struct reclaim_state *reclaim_state;

	struct backing_dev_info *backing_dev_info;

	struct io_context *io_context;

	unsigned long ptrace_message;
	siginfo_t *last_siginfo; /* For ptrace use.  */
	struct task_io_accounting ioac;
	atomic_t fs_excl;	/* holding fs exclusive resources */
	struct rcu_head rcu;

	/*
	 * cache last used pipe for splice
	 */
	struct pipe_inode_info *splice_pipe;
	struct prop_local_single dirties;
	/*
	 * time slack values; these are used to round up poll() and
	 * select() etc timeout values. These are in nanoseconds.
	 */
	unsigned long timer_slack_ns;
	unsigned long default_timer_slack_ns;

	struct list_head	*scm_work_list;
}</pre>
<p>&nbsp;</p>
<p>进程描述符在进程的内核栈中的位置示意如下：</p>
<p><img class=" wp-image-7023 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2010/05/struct_info.png" alt="struct_info" width="555" height="430" /></p>
<p>可以看到，每个进程的thread_info结构存放在内核栈的尾部，该结构中task字段为指向task_struct的指针。</p>
<p>内核通过唯一的进程标识符——PID来识别每个进程，PID表示为<pre class="crayon-plain-tag">pid_t</pre> 类型，其本质上就是一个int类型，该类型的默认最大值设置为32768以便与老版本的Unix兼容，该最大值受到linux/threads.h中定义的PID最大值限制。内核把某个进程的PID存放到它们各自的进程描述符中。</p>
<p>在内核中，需要频繁的访问进程的描述符，因此查找正在运行的进程的描述符的速度非常重要，内核通过current宏查找描述符，根据体系结构的不同，该宏的实现方式也不同。某些体系结构可以拿出一个专门的寄存器来存放指向当前task_struct的指针，在x86这样寄存器不是很富余的体系结构上就只能通过内核栈尾创建thread_info结构，通过计算偏移量间接查找task_struct结构：</p>
<pre class="crayon-plain-tag">//内核栈大小，在此配置下为8192
unsigned long THREAD_SIZE = ( ( ( 1UL ) &lt;&lt; 12 ) &lt;&lt; 1 );
/* 得到当前栈指针 */
register unsigned long current_stack_pointer asm("esp") __used;

/* 得到当前进程信息的指针 */
static inline struct thread_info *current_thread_info( void )
{
    return ( struct thread_info * )
    ( current_stack_pointer &amp; ~ ( THREAD_SIZE - 1 ) ); 
    //THREAD_SIZE - 1为13位：1111111111111，取反后与栈指针按位与，导致其低13位被置0
    //即定位到该内存栈的最低地址，thread_struct所在。
}
//获得task_struct
current_thread_info()-&gt;task;</pre>
<div class="blog_h3"><span class="graybg">进程状态</span></div>
<p>进程描述符中的state字段描述进程的当前状态，每个进程必然处于以下五种状态之一：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">状态</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>TASK_RUNNING</td>
<td>运行。进程是<strong><span style="background-color: #c0c0c0;">可</span></strong>执行的，它或者正在CPU上执行，或者在运行队列里等待执行。这是<span style="background-color: #c0c0c0;">进程在用户空间中执行的唯一可能的状态</span>，这种状态也可以应用到在<span style="background-color: #c0c0c0;">内核空间中正在执行的进程</span></td>
</tr>
<tr>
<td>TASK_INTERRUPTIBLE</td>
<td>可中断。进程<span style="background-color: #c0c0c0;">正在睡眠（或者说被阻塞）以等待某个条件的达成</span>，一但这些条件达成，内核就会将进程的状态设置为运行。处于此状态的进程也会因为<span style="background-color: #c0c0c0;">接收到信号而提前被唤醒</span>并随时准备投入运行</td>
</tr>
<tr>
<td>TASK_UNINTERRUPTIBLE</td>
<td>不可中断。除了<span style="background-color: #c0c0c0;">即使接收到信号也不会被唤醒</span>之外，与可中断一致。这个状态通常在进程必须<span style="background-color: #c0c0c0;">在等待时不受干扰或者等待事件很快就会发生</span>时出现。由于处于该状态的进程对信号不作响应，因此相比较可中断，该状态使用的少</td>
</tr>
<tr>
<td>__TASK_TRACED</td>
<td>被其它进程跟踪的进程，例如通过ptrace对调试程序进行跟踪</td>
</tr>
<tr>
<td>__TASK_STOPPED</td>
<td>进程停止运行。进程没有投入也不能投入运行。该状态通常发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号时出现。此外，在调试期间接收到任何信号，进程都会进入该状态</td>
</tr>
</tbody>
</table>
<p>下图描述了进程状态的转化：<img class="aligncenter size-full wp-image-7039" src="https://blog.gmem.cc/wp-content/uploads/2010/12/TASK_STATE.png" alt="TASK_STATE" width="95%" /></p>
<p>内核经常需要调整某个进程的状态，这时它会调用该函数：</p>
<pre class="crayon-plain-tag">//设置指定任务的状态，必要时，它会设置内存屏障来强制其它处理器作重新排序（一般只在对称多处理SMP系统中有此必要）
set_task_state(task, state);
//下面的函数与set_task_state(current, state)相同
set_current_state(state)</pre>
<div class="blog_h3"><span class="graybg">进程上下文</span></div>
<p>可执行程序代码是进程的重要组成部分，这些代码从一个可执行文件载入到进程的地址空间执行。<span style="background-color: #c0c0c0;">一般程序在用户空间执行</span>。当一个程序<span style="background-color: #c0c0c0;">调用了系统调用，或者触发了某个异常，它就会陷入内核空间</span>，此时，我们称<span style="background-color: #c0c0c0;">“内核代表进程执行”并处于进程上下文中</span>。在此上下文中，current宏是有效的。除非在此间隙更高优先级的进程需要执行并由进程调度器做出响应调整，否则<span style="background-color: #c0c0c0;">在内核退出的时候，程序恢复在用户空间的继续执行</span>。</p>
<p><span style="background-color: #c0c0c0;">系统调用和异常处理程序是对内核明确定义的接口，进程只有通过这些接口才能陷入内核执行</span>。</p>
<div class="blog_h3"><span class="graybg">进程家族树</span></div>
<p>Unix系统进程之间具有明显的继承关系，所有进程都是PID=1的init的后代。<span style="background-color: #c0c0c0;">内核在系统启动的最后阶段会启动init进程，该进程读取系统初始化脚本并执行其它相关程序</span>，最终完成系统的启动。</p>
<p>进程之间的关系被存放在task_struct中，每个进程具有<pre class="crayon-plain-tag">task_struct *parent</pre> 以及名为children的链表，该链表包含所有子进程task_struct的指针。通过下面的代码可以访问父子进程：</p>
<pre class="crayon-plain-tag">//访问父进程
struct task_struct *my_parent = current-&gt;parent;
//迭代子进程
struct task_struct *task;
struct list_head *list;
list_for_each(list, &amp;current-&gt;children)
{
    task = list_entry(list, struct task_struct, sibling);
}
//注意init进程的描述符是作为init_task变量静态分配的，因此下面的代码可以向上递归寻找父进程
struct task_struct *task;
for (task = current; task != &amp;init_task; task = task-&gt;parent)
;
//for_each_process宏用于遍历整个进程链表
struct task_struct *task;
for_each_process( task )
{
    printk( "%s[%d]\n", task-&gt;comm, task-&gt;pid );
}</pre>
<div class="blog_h2"><span class="graybg">进程的创建</span></div>
<p>大部分操作系统提供了一种<span style="background-color: #c0c0c0;">产生（spawn）进程</span>的机制：首先在新的地址空间里创建进程，读入可执行文件，最后开始执行。</p>
<p>Unix系列系统则采用与众不同的<span style="background-color: #c0c0c0;">分支（fork)</span>方式，同时<span style="background-color: #c0c0c0;">配合exec**()</span>调用完成新进程的创建。该方式的特点如下：</p>
<ol>
<li>fork创建的子进程，相当于对父进程的克隆，区别仅仅在PID、PPID，以及某些资源、统计信息——比如挂起的信号——没有必要继承</li>
<li>exec**读取可执行文件并将其载入地址空间开始执行</li>
</ol>
<div class="blog_h3"><span class="graybg">Copy-on-Write</span></div>
<p>传统的fork()调用直接<span style="background-color: #c0c0c0;">把所有的资源复制给子进程</span>，这种做法效率较为低下，因为拷贝的数据可能并不共享给子进程，或者新进程可能一上来就exec**()，导致所有拷贝都是无用功。Linux的fork()使用<span style="background-color: #c0c0c0;">写时拷贝页</span>实现，可以延迟甚至免除数据拷贝，内核并不会复制进程的整个地址空间，而是让父子进程共享同一个拷贝。只有在<span style="background-color: #c0c0c0;">需要写入的时候，数据才会被复制</span>，从而是父子进程拥有各自的拷贝。</p>
<p>因此，Linux下fork()的实际必须的开销就是<span style="background-color: #c0c0c0;">复制父进程的页表</span>、以及给子进程<span style="background-color: #c0c0c0;">分配唯一的进程描述符</span>。这对Linux快速创建进程的能力非常重要。</p>
<div class="blog_h3"><span class="graybg">fork()</span></div>
<p>Linux在底层通过clone()系统调用来实现fork()，该调用通过一系列参数指明父子进程需要共享的资源。fork()、vfork()、__clone()都是通过不同的参数调用clone()，后者则调用do_fork()。do_fork()完成大部分实质性的进程创建工作，其主要工作是在<pre class="crayon-plain-tag">copy_process</pre> 函数中完成的：</p>
<ol>
<li>为新进程创建一个内核栈、thread_info结构、task_struct，其值与父进程相同，因此此时子进程的描述符与父进程完全一致<br />
<pre class="crayon-plain-tag">p = dup_task_struct(current);</pre>
</li>
<li>检查并确保新创建这个子进程后，当前用户拥有的进程数没有超过资源限制<br />
<pre class="crayon-plain-tag">if ( atomic_read(&amp;p-&gt;real_cred-&gt;user-&gt;processes) &gt;=
        task_rlimit( p, RLIMIT_NPROC ) )
{
    if ( !capable( CAP_SYS_ADMIN ) &amp;&amp; !capable( CAP_SYS_RESOURCE ) &amp;&amp;
            p-&gt;real_cred-&gt;user != INIT_USER )
    goto bad_fork_free;
}</pre>
</li>
<li>将子进程和父进程区分开来。进程描述符中很多字段被清零或者设为初值</li>
<li>子进程的状态被设置为TASK_UNINTERRUPTIBLE，确保其不会投入运行</li>
<li>copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV被置零，表明进程还没有调用exec**()函数的PF_FORKNOEXEC被设置<br />
<pre class="crayon-plain-tag">static void copy_flags(unsigned long clone_flags, struct task_struct *p)
{
	unsigned long new_flags = p-&gt;flags;

	new_flags &amp;= ~PF_SUPERPRIV;
	new_flags |= PF_FORKNOEXEC;
	new_flags |= PF_STARTING;
	p-&gt;flags = new_flags;
	clear_freeze_flag(p);
}</pre>
</li>
<li>调用alloc_pid()给进程分配有效的PID<br />
<pre class="crayon-plain-tag">if (pid != &amp;init_struct_pid)
{
    pid = alloc_pid(p-&gt;nsproxy-&gt;pid_ns);</pre>
</li>
<li>根据传递给clone()的参数标志，copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间、命名空间等</li>
<li>进行一些清理工作，返回指向子进程的指针</li>
</ol>
<p>copy_process()返回后，do_fork()会让新创建的子进程立即投入运行，内核会有意让子进程首先执行，因为子进程通常会<span style="background-color: #c0c0c0;">马上</span>调用exec()函数，这样可以避免写时拷贝的开销：</p>
<pre class="crayon-plain-tag">/*
 * 第一次唤醒新创建的任务，该函数将处理每个新上下文必须进行的最初的调度信息统计，
 * 然后将新的任务存放到运行队列并唤醒之
 */
void wake_up_new_task( struct task_struct *p, unsigned long clone_flags )
{
    unsigned long flags;
    struct rq *rq;
    int cpu __maybe_unused = get_cpu(); //获取一个CPU

    rq = cpu_rq( cpu ); //获取CPU运行队列
    raw_spin_lock_irqsave( &amp;rq-&gt;lock, flags );

    BUG_ON( p-&gt;state != TASK_WAKING ); //断言
    p-&gt;state = TASK_RUNNING; //设置新任务的状态为运行
    update_rq_clock( rq );
    activate_task( rq, p, 0 ); //激活任务，入队
    trace_sched_wakeup_new( rq, p, 1 );
    check_preempt_curr( rq, p, WF_FORK );
    task_rq_unlock( rq, &amp;flags );
    put_cpu();
}</pre>
<div class="blog_h3"><span class="graybg">vfork()</span></div>
<p>该函数与fork()类似，只是<span style="background-color: #c0c0c0;">不拷贝父进程的页表项</span>。vfork()这样调用clone()：</p>
<pre class="crayon-plain-tag">clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);</pre>
<p>而最普通的fork()则是：</p>
<pre class="crayon-plain-tag">clone(SIGCHLD, 0);</pre>
<div class="blog_h3"><span class="graybg">clone()标记参数</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">标记</td>
<td style="text-align: center;">含义</td>
</tr>
</thead>
<tbody>
<tr>
<td>CLONE_FILES</td>
<td>父子进程共享打开的文件</td>
</tr>
<tr>
<td>CLONE_FS</td>
<td>父子进程共享文件系统信息</td>
</tr>
<tr>
<td>CLONE_IDLETASK</td>
<td>将PID设置为0（只供idle进程使用）</td>
</tr>
<tr>
<td>CLONE_NEWNS</td>
<td>为子进程创建新的命名空间，命名空间是Linux内核级别的环境隔离机制，用于把进程分为相对独立的组，进行IPC、用户、网络、PID、挂载等方面的隔离</td>
</tr>
<tr>
<td>CLONE_PARENT</td>
<td>指定子进程与父进程具有同一个父进程</td>
</tr>
<tr>
<td>CLONE_PTRACE</td>
<td>继续调试子进程 </td>
</tr>
<tr>
<td>CLONE_SETTID</td>
<td>将TID（线程标识符）回写至用户空间 </td>
</tr>
<tr>
<td>CLONE_SETTLS</td>
<td>为子进程创建新的TLS（线程本地存储）</td>
</tr>
<tr>
<td>CLONE_SIGHAND</td>
<td>父子进程共享信号处理函数以及被阻塞的信号槽设置 </td>
</tr>
<tr>
<td>CLONE_SYSVSEM</td>
<td>父子进程共享System V的SEM_UNDO语义 </td>
</tr>
<tr>
<td>CLONE_THREAD</td>
<td>父子进程放入相同的线程组 </td>
</tr>
<tr>
<td>CLONE_VFORK</td>
<td>调用vfork()，父进程将睡眠等待子进程将其唤醒 </td>
</tr>
<tr>
<td>CLONE_UNTRACED</td>
<td>防止跟踪进程在子进程上强制指向CLONE_PTRACE </td>
</tr>
<tr>
<td>CLONE_STOP</td>
<td>以TASK_STOPPED状态开始进程 </td>
</tr>
<tr>
<td>CLONE_CHILD_CLEARTID</td>
<td>清除子进程的TID</td>
</tr>
<tr>
<td>CLONE_CHILD_SETTID</td>
<td>设置子进程的TID </td>
</tr>
<tr>
<td>CLONE_PARENT_SETTID</td>
<td>设置父进程的TID </td>
</tr>
<tr>
<td>CLONE_VM</td>
<td>父子进程共享地址空间 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">线程在Linux中的实现</span></div>
<p>线程提供了在同一程序内<span style="background-color: #c0c0c0;">共享内存地址空间</span>运行<span style="background-color: #c0c0c0;">多个执行序列</span>的机制。这些执行序列就是线程。线程可以共享打开的文件和其它资源，在多处理器系统中，线程能保证真正的并行处理（Parallelism）。</p>
<p>Linux实现线程的机制很独特，从内核的角度来看，并没有线程的概念。Linux<span style="background-color: #c0c0c0;">把所有线程作为进程来实现</span>。内核没有特殊的调度算法或者数据结构来表征线程，相反，线程仅仅是和其它进程共享了某些资源的进程。每个线程都有隶属于自己的task_struct。</p>
<p>Windows的线程实现机制与Linux的差异非常大，前者专门提供了支持线程的机制，称为“轻量级进程（lightweight processes）”。其实这个命名本身就对比出了Linux进程设计的优雅性，Linux的进程设计已经足够轻量。</p>
<div class="blog_h3"><span class="graybg">创建线程</span></div>
<p>与创建普通进程非常相似，只是在clone()时传递一些参数，来指明需要共享的资源：</p>
<pre class="crayon-plain-tag">clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);</pre>
<div class="blog_h3"><span class="graybg">内核线程</span></div>
<p>内核经常需要在后台执行一些操作，这些操作可以通过<span style="background-color: #c0c0c0;">内核线程（Kernel Thread）</span> ——独立<span style="background-color: #c0c0c0;">运行在内核空间的标准线程</span>。内核线程与普通线程区别是：它<span style="background-color: #c0c0c0;">没有独立的地址空间（执行地址空间的指针mm设置为NULL）</span>。内核线程只在内核空间运行，不会切换到用户空间去，内核线程同样可以被调度和抢占。内核线程<span style="background-color: #c0c0c0;">一般是在系统启动时创建</span>，然后一直存在直到系统关闭。</p>
<p>内核线程自然只能有内核线程去创建，而它们的起源是一个叫<span style="background-color: #c0c0c0;">kthreadd</span>的内核进程，下面的函数声明了创建内核线程的方法：</p>
<pre class="crayon-plain-tag">struct task_struct *kthread_create(
        int (*threadfn)(void *data), // 新的内核线程执行的函数
        void *data,                  // 传递给函数的参数
        const char namefmt[],        // printf格式的字符串，新线程的名称
        ...
)</pre>
<p>上述创建的新内核线程不会自动运行，除非调用<pre class="crayon-plain-tag">wake_up_process()</pre> 函数明确的唤醒。内核线程启动后会一直运行直到调用<pre class="crayon-plain-tag">do_exit()</pre> 退出。内核的其它部分可以调用<pre class="crayon-plain-tag">kthread_stop(struct task_struct *k)</pre> 使内核线程退出。</p>
<div class="blog_h2"><span class="graybg">进程的终结</span></div>
<p>进程终结时，<span style="background-color: #c0c0c0;">内核将释放其占用的所有内存</span>，并<span style="background-color: #c0c0c0;">通过信号通知其父进程</span>。</p>
<p>通常进程的终结是自发的，进程调用<pre class="crayon-plain-tag">exit()</pre> 系统调用时将终结自身。该系统调用可以被明确的调用，也可以由编译器在main()函数的return后面隐式的添加。进程接收到既不能忽略也不能处理的<span style="background-color: #c0c0c0;">信号</span>、或者<span style="background-color: #c0c0c0;">异常</span>时，也可能被自动的终结。</p>
<p>进程终结时需要做的大部分工作在下列函数中完成：</p>
<pre class="crayon-plain-tag">NORET_TYPE void do_exit(long code);</pre>
<p>该函数完成以下逻辑：</p>
<ol>
<li>设置task_struct标记PF_EXITING</li>
<li>调用del_timer_sync()删除任何内核定时器，确保没有任何定时器在排队，没有任何定时器处理程序在运行</li>
<li>如果开启BSD记账功能，调用<pre class="crayon-plain-tag">acct_update_integrals()</pre>  输出记账信息</li>
<li>调用<pre class="crayon-plain-tag">exit_mm()</pre> 尝试释放<pre class="crayon-plain-tag">mm_struct</pre> ，如果没有其它进程使用，则彻底释放对应的地址空间</li>
<li>调用<pre class="crayon-plain-tag">sem_exit()</pre> 函数。如果进程排队等候IPC信号，则使进程离开队列</li>
<li>调用用<pre class="crayon-plain-tag">exit_files()</pre> 和<pre class="crayon-plain-tag">exit_fs()</pre> 分别递减文件描述符、文件系统的引用计数，如果计数为零，立即释放资源</li>
<li>设置<pre class="crayon-plain-tag">task_struct.exit_code</pre> 为exit()函数指定的值，供父进程随时检索</li>
<li>调用<pre class="crayon-plain-tag">exit_notify()</pre> 向父进程发送信号，并给子进程重新找养父（父进程可能先死），养父为线程组中其它线程或者init进程，设置进程的状态为<pre class="crayon-plain-tag">EXIT_ZOMBIE</pre> </li>
<li>调用<pre class="crayon-plain-tag">schedule()</pre> 切换到新的进程</li>
</ol>
<p>由于处于<pre class="crayon-plain-tag">EXIT_ZOMBIE</pre> 状态的进程永远不会被调度，因此do_exit()函数永远不会返回，上述把就是僵尸进程最后执行的代码。</p>
<p>至此，进程相关的绝大部分资源都被释放了，进程不可运行（也没有地址空间供它运行）并处于僵尸态。它目前只占用几个内存区域：<span style="background-color: #c0c0c0;">内核栈、thread_info结构、task_struct结构</span>。 僵尸进程存在的唯一目的就是向父进程提供信息（退出码等），父进程<span style="background-color: #c0c0c0;">检索</span>到这些信息、或者通知内核它<span style="background-color: #c0c0c0;">不关心</span>子进程的信息后，进程占有的剩余内存全部归还系统。</p>
<div class="blog_h3"><span class="graybg">进程描述符的删除</span></div>
<p>上一小节提到进程的终结清理与其描述符的删除被分开处理，这是为了在进程终结后，其父进程尚能访问其退出码等信息。</p>
<p>父进程可以通过<pre class="crayon-plain-tag">wait4()</pre> 系统调用（通过wait**函数转调）来等待一个子进程的退出，并得到子进程的PID，并进而获取其进程描述符中的信息。当最终需要释放进程描述符时，会调用<pre class="crayon-plain-tag">release_task()</pre> 函数，该函数完成以下逻辑：</p>
<ol>
<li>调用<pre class="crayon-plain-tag">_exit_signal()</pre> ，该函数调用<pre class="crayon-plain-tag">_unhash_process()</pre> ，后者则调用<pre class="crayon-plain-tag">detach_pid()</pre> 从pidhash、任务列表中删除该进程</li>
<li><pre class="crayon-plain-tag">_exit_signal</pre> 释放僵尸进程的占用的所有残余资源，最终进行统计和记录</li>
<li>如果该僵尸进程是线程组的最后一个进程，并且领头进程已经是一个僵尸，那么通知领头进程的父进程</li>
<li>调用<pre class="crayon-plain-tag">put_task_struct</pre> 释放内核栈和thread_info结构所占据的页，释放task_struct占据的slab高速缓存</li>
</ol>
<div class="blog_h3"><span class="graybg">孤儿进程问题</span></div>
<p>如果父进程先于子进程退出，必须有机制保证子进程能找到一个新父亲。否则，子进程将永远处于僵尸状态，浪费内存。Linux的做法是，当父进程do_exit()时，会调用<pre class="crayon-plain-tag">forget_original_parent()</pre> 而后者会调用<pre class="crayon-plain-tag">find_new_reaper()</pre> 来定位父进程：</p>
<pre class="crayon-plain-tag">/*
 * 进程终结时，将为所有子进程需找新的养父
 * 默认是寻找线程组中的其它线程，如果找不到，则指定养父为
 * 当前PID空间中的child reaper process（即init进程）
 */
static struct task_struct *find_new_reaper( struct task_struct *father )
{
    struct pid_namespace *pid_ns = task_active_pid_ns( father );
    struct task_struct *thread;

    thread = father;
    //遍历当前线程组
    while_each_thread( father, thread )
    {
        if ( thread-&gt;flags &amp; PF_EXITING )
        continue;
        if ( unlikely( pid_ns-&gt;child_reaper == father ) )
        pid_ns-&gt;child_reaper = thread;
        return thread;
    }
    //如果当前进程是PID空间的init进程
    if ( unlikely( pid_ns-&gt;child_reaper == father ) )
    {
        write_unlock_irq( &amp;tasklist_lock );
        if ( unlikely( pid_ns == &amp;init_pid_ns ) )
        panic( "Attempted to kill init!" );

        zap_pid_ns_processes( pid_ns );
        write_lock_irq( &amp;tasklist_lock );
        pid_ns-&gt;child_reaper = init_pid_ns.child_reaper;
    }
    //否则，返回init进程
    return pid_ns-&gt;child_reaper;
}</pre>
<p>找到养父后，只需要遍历子进程列表，设置其父进程即可。</p>
<div class="blog_h1"><span class="graybg">进程调度</span></div>
<p>进程调度程序是确保进程能有效工作的一个内核子系统，它决定<span style="background-color: #c0c0c0;">哪个TASK_RUNNING的进程能够投入运行</span>，可以<span style="background-color: #c0c0c0;">运行多长时间</span>。</p>
<p>2.6引入了一个重要的特性，就是内核抢占。这意味着多个处于特权模式（privileged mode）的执行流（即处于内核态的进程）可以任意的交叉执行，因此进程调度适用于很多内核态中的代码。</p>
<div class="blog_h2"><span class="graybg">多任务</span></div>
<p>多任务操作系统能够同时执行多个进程，在单处理器系统中，同时运行只是一种幻觉，在多处理器系统中则真实的发生着。多任务操作系统可以让多个进程处于<span style="background-color: #c0c0c0;">阻塞或者睡眠</span>状态，这些进程不会真正的运行，直到等待的条件（例如键盘输入、网络数据到达、定时器）就绪。</p>
<p>多任务操作系统可以分为两类：</p>
<ol>
<li>非抢占式多任务（Cooperative Multitasking）：除非进程自己主动停止执行，否则它会一直占据CPU。进程主动挂起自己的操作称为让步（Yielding）。现在很少OS使用此模式</li>
<li>抢占式多任务（Preemptive Multitasking）：Unix变体和许多现代OS采取的方式，在此模式下，调度程序确定何时停止一个进程的运行，以便其它进程获得执行机会，<span style="background-color: #c0c0c0;">这个强制挂起正在执行进程的动作叫做抢占</span>。进程在被抢占之前能够运行的时间是预先设定的，称为<span style="background-color: #c0c0c0;">时间片（timeslice）</span>。</li>
</ol>
<div class="blog_h2"><span class="graybg">Linux进程调度机制的历史</span></div>
<p>从Linux1.0到2.4的内核，进程调度程序都比较简陋。2.5版本对调度程序做了大的改进，被称为<span style="background-color: #c0c0c0;">O(1)调度器</span>。该调度器能够在数以10计的多处理器环境下表现良好，但是对响应时间敏感（例如用户交互）的程序则表现的差强人意。</p>
<p>2.6版本开发初期，为了提高交互程序的调度性能，Linux开发团队引入了新的进程调度算法——<span style="background-color: #c0c0c0;">完全公平调度算法（CFS）</span>，并在2.6.23内核版本中代替O(1)算法，</p>
<div class="blog_h2"><span class="graybg">进程调度策略</span></div>
<div class="blog_h3"><span class="graybg">I/O消耗型和处理器消耗型进程</span></div>
<p>这是一种进程的分类方式：</p>
<ol>
<li>I/O消耗型：<span style="background-color: #c0c0c0;">大部分时间用来提交I/O请求或者等待I/O请求</span>，这类进程通常都是<span style="background-color: #c0c0c0;">运行很短的时间，即被阻塞</span>。引起阻塞的I/O资源可以是鼠标键盘输入、网络I/O等等。大部分GUI程序都是I/O消耗型的，因为其大部分时间需要等待用户交互</li>
<li>处理器消耗型：<span style="background-color: #c0c0c0;">大部分时间用来执行代码</span>（指令）。除非被强占，这类进程经常不停的执行。由于这类进程的I/O需求较小，因此调度系统会降低其调度频率，以提供系统的响应速度（允许其它I/O型程序能够尽快获得执行机会）。调度系统倾向于在降低调度频率的同时，延长这类进程的执行时间。处理器消耗型进程的典型例子是科学计算应用，极端例子是无限空循环</li>
</ol>
<p>现实场景中，很多程序不能简单的划分到这两类之一，例如X Window服务既是I/O消耗型，也是处理器消耗型。</p>
<p>调度策略需要在<span style="background-color: #c0c0c0;">响应时间</span>（响应高速性，进程能否尽快的获取CPU）和<span style="background-color: #c0c0c0;">吞吐量</span>（最大系统利用率，单位时间执行的有效指令数量）两个目标之间寻求平衡，Linux<span style="background-color: #c0c0c0;">更加倾向于优先调度I/O消耗型进程</span>，以缩短响应时间。</p>
<div class="blog_h3"><span class="graybg">进程优先级</span></div>
<p>基于优先级的调度是一种最基本的一类调度算法。通常（但为被Linux使用）的做法是，高优先级的进程先运行，低优先级的后运行，相同优先级的则以轮转的方式进行调度。某些系统中高优先级的进程使用的时间片也更长。</p>
<p>Linux使用了两种优先级度量：</p>
<ol>
<li>nice值：从-20到19之间，默认值0，该值越大，意味着优先级越低。nice值在不同的Unix系统中运用方式有所不同，例如Mac OS X中代表了分配给进程的时间片绝对值，而在Linux中则代表了<span style="background-color: #c0c0c0;">时间片的比例</span></li>
<li>实时优先级：从0到99之间，该值越大，意味着优先级越高。所有实时进程的优先级都高于普通进程。使用命令<pre class="crayon-plain-tag">ps -eo state,uid,pid,ppid,rtprio,time,comm</pre> 可以查看进程的实时优先级，显示为“-”的是<span style="background-color: #c0c0c0;">非实时进程</span></li>
</ol>
<div class="blog_h3"><span class="graybg">时间片</span></div>
<p>时间片是一个数值，用来表示进程被<span style="background-color: #c0c0c0;">抢占前，能够持续运行的时间</span>。时间片过长会导致人机交互的响应速度欠佳；时间片过短则明显增大进程切换的开销。I/O消耗型进程不需要太长时间片；CPU消耗型进程则希望时间片越长越好。</p>
<p>为了增加交互响应速度，很多OS把时间片设置为很短的绝对值，例如10ms。Linux的CFS调度器则是以<span style="background-color: #c0c0c0;">占用CPU时间的比例</span>来定义时间片，这就意味着进程获取的CPU时间和系统负载密切相关，这一比例进一步收到nice值的影响，低nice值的进程获得更多的CPU时间比例。</p>
<p>大部分抢占式系统中，一旦一个进程进入可运行状态，它是否立即投入运行（即抢占CPU上的当前进程），<span style="background-color: #c0c0c0;">完全由进程的优先级和是否拥有时间片来决定</span>，Linux的CFS调度使用更加复杂的算法：如果新的可运行进程<span style="background-color: #c0c0c0;">已消耗的CPU时间比例</span>比较小，则立即抢占，否则推迟执行</p>
<div class="blog_h3"><span class="graybg">调度场景举例</span></div>
<p>考虑一个系统中只有两个进程：</p>
<ol>
<li>文本编辑器，交互式程序，需要立即得到响应，但是需要占用的CPU时间很少（人输入的速度相对于CPU来说非常慢）</li>
<li>视频编码器，非交互式程序，需要占用大量的CPU时间</li>
</ol>
<p>如果上述两者的nice值相同，那么CFS分配给它们的CPU时间比例都是50%，则会发生如下的事件序列：</p>
<ol>
<li>视频编码器开始工作，占用超过99%的CPU时间</li>
<li>用户击键，文本编辑器被唤醒，CFS发现该进程占用的CPU时间比例接近0%，而分配给它的比例是50%</li>
<li>CFS立即抢占视频编码器，让文本编辑器立即运行</li>
<li>上面两步骤会不停的发生，因为文本编辑器实际消耗CPU时间比例总是非常小，远远小于分配给它的比例</li>
</ol>
<div class="blog_h2"><span class="graybg">Linux调度算法</span></div>
<div class="blog_h3"><span class="graybg">调度器类</span></div>
<p>Linux以模块的方式提供调度器，以便不同类型的进程可以选择不同的调度算法。这一模块化的结构被称为调度器类（Scheduler Classes），它允许多种可以动态添加的调度算法共存，并遵守以下规则：</p>
<ol>
<li>每一个调度器负责调度自己管理的进程</li>
<li>每一个调度器具有一个优先级</li>
<li>调度器会按优先级高低被遍历，用于一个可运行进程的、最高优先级的调度器胜出</li>
<li>胜出的调度器决定下一个运行的进程</li>
</ol>
<p>CFS是针对普通进程的调度器类，在Linux中被称为SCHED_NORMAL，定义在kernel/sched_fair.c中。</p>
<div class="blog_h3"><span class="graybg">传统Unix进程调度的问题</span></div>
<p>若将nice值映射为处理器绝对时间，必然会导致进程切换无法优化进行。例如将默认nice=0对应的时间片设置为100ms，那么+20则对应5ms，假定限制有0、+20两个进程都处于可运行状态，则0进程将获得20/21即100ms的CPU时间；如果有两个+20优先级的可运行进程，则它们每次只能获得5ms的CPU时间。问题是，高nice的进程往往是后台任务，计算密集型为多，它期望的是更长的运行时间，反之，普通进程往往是用户交互任务，它们期望快速响应。很明显，nice值映射到CPU绝对时间的调度机制，背离进程调度的初衷。</p>
<p>nice值映射为处理器绝对时间的另外一个问题，是CPU时间片比例的非线性变化，0和1分别是100ms、95ms，它们之间的差距很小，然后+19和+20也是差距一个步进，CPU时间片比例差距达1倍。这就导致了把nice值减一，以期望影响进程调度，其结果极大的取决于nice初值。</p>
<div class="blog_h3"><span class="graybg">Linux的公平调度算法</span></div>
<p>尽管某些变通的方法能解决上述Unix进程调度的问题，但是都回避了问题的实质：分配绝对的时间片引发的固定进程切换频率，给公平性造成了很大的变数。</p>
<p>Linux 2.6.23版本引入的CFS（completely fair schedule，完全公平调度）算法，很好的解决了Unix进程调度存在的问题，它对基于时间片的分配方式进行了彻底的重新设计，完全抛弃了时间片，改为分配给进程一个CPU使用的权重（nice越低权重越大）。通过这种方式，CPU确保了进程调度的公平性和动态变化的切换频率。</p>
<p>CFS算法是基于一个简单的理念：进程调度的效果应该如同系统具有一个完美调度器，该调度器能够让每个进程获得1/n的处理器时间，即使策略周期无穷小。传统Unix调度模型可能在10ms内让进程1运行5ms，然后让进程2运行5ms，在运行时每个进程都占用100%的CPU；但是对于完美调度器，从效果上说，它相当于在10ms内让进程1、2同时运行，而各自占用1/2的处理器能力。CFS算法的核心工作，就是在现有硬件的基础上，尽可能的接近完美调度器。</p>
<p>上述完美调度器是不现实的，因为调度引起的进程抢占本身就有代价：包括进程切换的上下文切换、缓存效率问题。因此，虽然我们期望进程只运行非常短的周期，但是CFS充分考虑到短周期带来的额外消耗，它允许<span style="background-color: #c0c0c0;">每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行的进程</span>，CFS将在<span style="background-color: #c0c0c0;">所有可运行进程的总数</span>的基础上、<span style="background-color: #c0c0c0;">结合nice值</span>计算出每个一个进程应该运行多久，而不是简单的依靠nice值计算绝对时间片。</p>
<p>同时，CFS为完美调度器的无限小调度周期设定一个<span style="background-color: #c0c0c0;">近似目标</span>，称为<span style="background-color: #c0c0c0;">目标延迟</span>。例如，如果目标延迟是20ms，两个同样nice的进程将可以在被抢占前运行10ms，如果有4个这样的进程则只能分别运行5ms……当任务数量趋向于无限时，抢占前运行时间接近于0，这意味着无限大的切换损耗。因此CFS引入每个进程能获得的<span style="background-color: #c0c0c0;">时间片底线</span>，称为<span style="background-color: #c0c0c0;">最小粒度</span>，默认<span style="background-color: #c0c0c0;">该底线为1ms</span>。这样一来，不管任务总数变得多大，每个进程最少能获得1ms的运行时间，确保切换损耗被控制在一定范围内。</p>
<div class="blog_h2"><span class="graybg">Linux调度器的实现</span></div>
<p>CFS算法的实现代码位于kernel/sched_fair.c中，主要有四个重要的逻辑部分。</p>
<div class="blog_h3"><span class="graybg">时间记账</span></div>
<p>即对进程运行的时间进行记账，这是所有调度器都必须做的工作。传统Unix使用时间片机制，因此每次系统节拍发生时，时间片都会减去一个节拍周期，当时间片减少到0时，当前进程就会被时间片尚不为0的其它进程抢占。</p>
<p>虽然CFS不使用时间片，但是它仍然需要记录每个进程的运行时间，以确保每个进程只能在公平分配给它的处理器时间内运行，结构<pre class="crayon-plain-tag">sched_entity</pre> 被用来追踪进程运行记账：</p>
<pre class="crayon-plain-tag">//该结构作为成员变量se，存放在进程描述符task_struct中
struct sched_entity {
	struct load_weight	load;		/* for load-balancing */
	struct rb_node		run_node;
	struct list_head	group_node;
	unsigned int		on_rq;

	u64			exec_start;
	u64			sum_exec_runtime;
	u64			vruntime;
	u64			prev_sum_exec_runtime;

	u64			last_wakeup;
	u64			avg_overlap;

	u64			nr_migrations;

	u64			start_runtime;
	u64			avg_wakeup;
};</pre>
<p>变量<pre class="crayon-plain-tag">vruntime</pre>  存放进程的虚拟运行时间（virtual runtime），虚拟运行时间是真实运行时间通过加权运算后得到的值。加权运算的权重，和进程的nice值有关，nice越大，则权重越小：</p>
<pre class="crayon-plain-tag">static const int prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};</pre>
<p>系统定时器会定期调用 <pre class="crayon-plain-tag">update_curr()</pre> 函数，来更新 <pre class="crayon-plain-tag">vruntime</pre> ，不管进程是处于可运行还是阻塞状态，这样， <pre class="crayon-plain-tag">vruntime</pre> 可用来准确的测量给定进程的运行时间，并推断下一个运行的进程是谁：</p>
<pre class="crayon-plain-tag">static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq-&gt;curr;
	u64 now = rq_of(cfs_rq)-&gt;clock;
	//当前任务运行时间的增量
	delta_exec = (unsigned long)(now - curr-&gt;exec_start);
	__update_curr(cfs_rq, curr, delta_exec);
	curr-&gt;exec_start = now; 
}
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
	      unsigned long delta_exec)
{
	unsigned long delta_exec_weighted;
	curr-&gt;sum_exec_runtime += delta_exec; //增加总计运行时间
	//计算权重后的运行时间（虚拟运行时间）
	delta_exec_weighted = calc_delta_fair(delta_exec, curr);
	curr-&gt;vruntime += delta_exec_weighted; //虚拟运行时间增量
	update_min_vruntime(cfs_rq); //修改进程队列红黑树的min_vruntime
}
static inline unsigned long
calc_delta_fair(unsigned long delta, struct sched_entity *se)
{
	//如果不是默认优先级才需要计算，否则虚拟运行时间和实际运行时间一致
	if (unlikely(se-&gt;load.weight != NICE_0_LOAD))
		delta = calc_delta_mine(delta, NICE_0_LOAD, &amp;se-&gt;load);

	return delta;
}
#if BITS_PER_LONG == 32
# define WMULT_CONST    (~0UL)
#else
# define WMULT_CONST    (1UL &lt;&lt; 32)
#endif
#define WMULT_SHIFT     32
//SRR (Shift right and round) 
#define SRR(x, y) (((x) + (1UL &lt;&lt; ((y) - 1))) &gt;&gt; (y))
static unsigned long
calc_delta_mine(unsigned long delta_exec, unsigned long weight,
		struct load_weight *lw)
{
	u64 tmp;

	if (!lw-&gt;inv_weight) {
		if (BITS_PER_LONG &gt; 32 &amp;&amp; unlikely(lw-&gt;weight &gt;= WMULT_CONST))
			lw-&gt;inv_weight = 1;
		else
			lw-&gt;inv_weight = 1 + (WMULT_CONST-lw-&gt;weight/2)
				/ (lw-&gt;weight+1);
	}
	//先乘以NICE_0_LOAD的权重
	tmp = (u64)delta_exec * weight;
	/*
	 * Check whether we'd overflow the 64-bit multiplication:
	 */
	if (unlikely(tmp &gt; WMULT_CONST))
		tmp = SRR(SRR(tmp, WMULT_SHIFT/2) * lw-&gt;inv_weight,
			WMULT_SHIFT/2);
	else
		tmp = SRR(tmp * lw-&gt;inv_weight, WMULT_SHIFT);

	return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);
}</pre>
<p>从上面的代码可以看到，对于nice值为0的进程，其虚拟运行时间和真实运行时间一致，nice越低（优先级越高），则虚拟运行时间相对于真实运行时间的值就越小。虚拟运行时时间和真实运行时间的关系，接近以下公式：</p>
<p style="padding-left: 30px;"><span style="text-decoration: underline;">进程虚拟运行时间  ≈ 进程真实运行时间 × 默认进程权重 / 当前进程权重</span></p>
<p>其中默认进程权重即NICE_0_LOAD，为常量值1024。</p>
<div class="blog_h3"><span class="graybg">进程选择</span></div>
<p>上一节我们介绍了虚拟运行时间（vruntime）的计算，CFS就是根据此vruntime决定下一个运行的进程的，<span style="background-color: #c0c0c0;">CFS会选择vruntime最小的进程</span>，这就是CFS的核心。</p>
<p>为了快速找到vruntime最小的进程，CFS使用红黑树（在Linux中称为rbtree，一种自平衡二叉搜索树）来组织可运行进程的队列，红黑树可以存储任意类型数据的节点，这些节点通过键（key）来识别，红黑树能够快速的定位到一个给定的键，其时间复杂度为O(logn)。</p>
<p>对于已经生成好的、存储所有可运行进程的红黑树，CFS只需要获取其最左侧节点，即可得到vruntime最小的进程：</p>
<pre class="crayon-plain-tag">static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
	//红黑色的最左节点是被缓存起来的，没必要搜索之
	struct rb_node *left = cfs_rq-&gt;rb_leftmost;

	if (!left)
		return NULL; //这意味着树中没有任何节点，也就是说没有可运行进程，调度器将选择idle任务运行

	return rb_entry(left, struct sched_entity, run_node);
}</pre>
<p>进程是什么时候被放入队列的呢？当通过fork()创建进程时，或者进程进入可运行状态时，下面的函数被调用，将其存入红黑树：</p>
<pre class="crayon-plain-tag">static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	if (!(flags &amp; ENQUEUE_WAKEUP) || (flags &amp; ENQUEUE_MIGRATE))
		se-&gt;vruntime += cfs_rq-&gt;min_vruntime;

	//更新当前任务的统计信息
	update_curr(cfs_rq);
	account_entity_enqueue(cfs_rq, se);

	if (flags &amp; ENQUEUE_WAKEUP) {
		place_entity(cfs_rq, se, 0);
		enqueue_sleeper(cfs_rq, se);
	}

	update_stats_enqueue(cfs_rq, se);
	check_spread(cfs_rq, se);
	if (se != cfs_rq-&gt;curr)
		__enqueue_entity(cfs_rq, se); //执行插入操作
}
/*
 * Enqueue an entity into the rb-tree:
 */
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	struct rb_node **link = &amp;cfs_rq-&gt;tasks_timeline.rb_node;
	struct rb_node *parent = NULL;
	struct sched_entity *entry;
	s64 key = entity_key(cfs_rq, se);
	int leftmost = 1;

	//遍历节点，在红黑树中找到合适的位置
	while (*link) {
		parent = *link; //既有的，作为被插入进程父节点的节点
		entry = rb_entry(parent, struct sched_entity, run_node);
		if (key &lt; entity_key(cfs_rq, entry)) { //平衡二叉树：如果键值小于当前节点，则需要转向树的左分支
			link = &amp;parent-&gt;rb_left;
		} else {
			link = &amp;parent-&gt;rb_right; //平衡二叉树：如果键值大于当前节点，则需要转向树的右分支
            //一旦向右（即使只有一次），就不可能是最左节点
			leftmost = 0;
		}
	}

	//缓存最左节点
	if (leftmost)
		cfs_rq-&gt;rb_leftmost = &amp;se-&gt;run_node;
	//在最终确定的父节点下面插入当前进程
	rb_link_node(&amp;se-&gt;run_node, parent, link);
    //更新树的自平衡相关属性
	rb_insert_color(&amp;se-&gt;run_node, &amp;cfs_rq-&gt;tasks_timeline);
}</pre>
<p>当进程被阻塞，或者进程终止时，则需要调用下面的函数将其移除出红黑树：</p>
<pre class="crayon-plain-tag">dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int sleep)
{

	update_curr(cfs_rq);

	update_stats_dequeue(cfs_rq, se);

	clear_buddies(cfs_rq, se);

	if (se != cfs_rq-&gt;curr) //当前正在运行的进程不能被移除
		__dequeue_entity(cfs_rq, se);
	account_entity_dequeue(cfs_rq, se);
	update_min_vruntime(cfs_rq);

	if (!sleep)
		se-&gt;vruntime -= cfs_rq-&gt;min_vruntime;
}
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    //如果被删除节点是最左边的，需要重新计算rb_leftmost值
	if (cfs_rq-&gt;rb_leftmost == &amp;se-&gt;run_node) {
		struct rb_node *next_node;

		next_node = rb_next(&amp;se-&gt;run_node);
		cfs_rq-&gt;rb_leftmost = next_node;
	}
    //rbtree既有的函数可以用来方便的删除红黑树节点
	rb_erase(&amp;se-&gt;run_node, &amp;cfs_rq-&gt;tasks_timeline);
}</pre>
<div class="blog_h3"><span class="graybg">调度器入口</span></div>
<p>进程调度的入口函数定义在/kernel/sched.c的<pre class="crayon-plain-tag">void schedule(void)</pre> 中，该函数是内核其它部分调用进程调度器的入口。该函数会将工作委托给一个具体的调度类，后者有自己的可运行队列，并负责决定下一个运行的进程。该函数中最重要的逻辑是调用<pre class="crayon-plain-tag">pick_next_task()</pre> ，该函数以优先级为序，从高到低，依次检查每一个调度类，并且从<span style="background-color: #c0c0c0;">最高优先级的调度类</span>中，选择<span style="background-color: #c0c0c0;">最高优先级的进程</span>：</p>
<pre class="crayon-plain-tag">static inline struct task_struct *
pick_next_task(struct rq *rq)
{
	const struct sched_class *class;
	struct task_struct *p;

	//如果所有任务都在公平调度器中（所有可运行进程数量等于CFS可运行进程数量），则可以直接调度，由于CFS是普通进程的调度类，因此这里做了优化
	if (likely(rq-&gt;nr_running == rq-&gt;cfs.nr_running)) {
		p = fair_sched_class.pick_next_task(rq); //每个调度器类都实现了该函数
		if (likely(p))
			return p;
	}

	class = sched_class_highest;
	for ( ; ; ) {
		p = class-&gt;pick_next_task(rq);
		if (p)
			return p; //不会返回NULL，因为idle调度器会返回一个p
		class = class-&gt;next;
	}
}</pre>
<div class="blog_h3"><span class="graybg">睡眠与唤醒</span></div>
<p>进程可能因为等待某个事件（磁盘I/O、键盘输入、尝试获取被占用的内核信号量）而进入休眠（被阻塞）状态，处于此状态的进程会被移出可运行红黑树。</p>
<p>在进程进入睡眠状态时：进程将自己标注为休眠状态，从可运行红黑树中移除，存放到等待队列；在进程被唤醒时：进程被设置为可运行状态，并被存放到可运行红黑树。</p>
<p>与睡眠相关的两种进程状态：TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE，唯一的区别是前者能够响应信号，当接收到信号后，会提前被唤醒并处理信号。<span style="background-color: #c0c0c0;">睡眠通过等待队列处理，这两种状态的进程存放在同一个等待队列上</span>。</p>
<p>内核使用wait_queue_head_t类型来表示等待队列：</p>
<pre class="crayon-plain-tag">//等待队列头
struct __wait_queue_head {
	spinlock_t lock; //自旋锁，实现对task_list的互斥访问
	struct list_head task_list; //双向循环链表，存放等待的进程
};
typedef struct __wait_queue_head wait_queue_head_t;

//等待队列项，作为等待队列头的成员
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, void *key)
{
	return try_to_wake_up(curr-&gt;private, mode, wake_flags);
}

struct __wait_queue {
	unsigned int flags;  //可用于指示等待进程是否互斥进程，0表示非互斥进程，1表示互斥进程
#define WQ_FLAG_EXCLUSIVE	0x01
	void *private;
	wait_queue_func_t func;
	struct list_head task_list;
};</pre>
<p>可以通过宏DECLARE_WAITQUEUE()静态创建或者init_waitqueue_head()动态创建等待队列：</p>
<pre class="crayon-plain-tag">#define __WAITQUEUE_INITIALIZER(name, tsk) {				\
	.private	= tsk,						\
	.func		= default_wake_function,			\
	.task_list	= { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)


extern void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *);
#define init_waitqueue_head(q)				\
	do {						\
		static struct lock_class_key __key;	\
							\
		__init_waitqueue_head((q), &amp;__key);	\
	} while (0)</pre>
<p>函数__init_waitqueue_head的定义位于：</p>
<pre class="crayon-plain-tag">void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
{
	spin_lock_init(&amp;q-&gt;lock);
	lockdep_set_class(&amp;q-&gt;lock, key);
	INIT_LIST_HEAD(&amp;q-&gt;task_list);
}</pre>
<p>在睡眠时，进程负责将自身放入等待队列，并标记为不可运行。当<span style="background-color: #c0c0c0;">和等待队列关联的事件发生时，该队列上的进程被唤醒</span>。 睡眠和唤醒必须合理的实现以避免竞态条件，以前某些被广泛使用的简单接口函数可能会导致竞态条件——条件（事件）已经为真（等待的事件已经发生）了，而进程还是陷入睡眠，这会导致进程无限的睡眠下去，为避免此问题，可以在内核中使用类似如下代码：</p>
<pre class="crayon-plain-tag">DEFINE_WAIT( wait );    //创建等待队列项
add_wait_queue(q, &amp;wait); //q是期望在其上睡眠的等待队列（头），添加wait到其中
while (!condition) /*condition表示等待的事件*/
{
    prepare_to_wait(&amp;q, &amp;wait, TASK_INTERRUPTIBLE); //准备休眠，设置为可中断睡眠状态
    if (signal_pending(current)) //伪唤醒（不是因为事件而唤醒）并处理信号
    /* 在这里处理信号 */
    schedule(); //执行调度，当前进程让出CPU
}
finish_wait(&amp;q, &amp;wait); //移出休眠队列，并恢复为可运行状态</pre>
<p>与上述代码相关的宏和函数如下：</p>
<pre class="crayon-plain-tag">// /include/linux/wait.h
#define DEFINE_WAIT_FUNC(name, function)				\
	wait_queue_t name = {						\
		.private	= current,				\
		.func		= function,				\
		.task_list	= LIST_HEAD_INIT((name).task_list),	\  
	}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
	list_add(&amp;new-&gt;task_list, &amp;head-&gt;task_list);
}
// /arch/x86/include/asm/current.h
// /include/linux/list.h
#define LIST_HEAD_INIT(name) { &amp;(name), &amp;(name) }
#define current get_current()
// /kernel/wait.c
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
	int ret = default_wake_function(wait, mode, sync, key);

	if (ret)
		list_del_init(&amp;wait-&gt;task_list); //从链表中删除条目
	return ret;
}
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;
	wait-&gt;flags &amp;= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&amp;q-&gt;lock, flags); //禁用当前处理器的中断并得到q关联的自旋锁，中断状态被存放在flags中
	__add_wait_queue(q, wait); //将wait的task_list插入到q的task_list后面
	spin_unlock_irqrestore(&amp;q-&gt;lock, flags); //恢复中断状态并释放自旋锁
}
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
	//这里添加的是task_list字段
	//后续可以通过container_of，来获取task_list的容器wait_queue_t 
	list_add(&amp;new-&gt;task_list, &amp;head-&gt;task_list);
}
//准备睡眠，与上面的函数类似，额外把进程设置为指定的状态
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
	unsigned long flags;
	wait-&gt;flags &amp;= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&amp;q-&gt;lock, flags);
	if (list_empty(&amp;wait-&gt;task_list))
		__add_wait_queue(q, wait);
	set_current_state(state);
	spin_unlock_irqrestore(&amp;q-&gt;lock, flags);
}
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;
	__set_current_state(TASK_RUNNING);
	if (!list_empty_careful(&amp;wait-&gt;task_list)) {
		spin_lock_irqsave(&amp;q-&gt;lock, flags);
		list_del_init(&amp;wait-&gt;task_list);
		spin_unlock_irqrestore(&amp;q-&gt;lock, flags);
	}
}</pre>
<p>当需要唤醒处于睡眠中的进程时，需要调用wake_up()函数完成：</p>
<pre class="crayon-plain-tag">//得到链表条目对应的结构，ptr为list_head指针，type为容器结构类型，member为list_head在容器结构中的字段名
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member) 
//迭代链表
#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_entry((head)-&gt;next, typeof(*pos), member),	\
		n = list_entry(pos-&gt;member.next, typeof(*pos), member);	\
	     &amp;pos-&gt;member != (head); 					\
	     pos = n, n = list_entry(n-&gt;member.next, typeof(*n), member))

#define TASK_NORMAL		(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
// /include/linux/wait.h
#define wake_up(x)			__wake_up(x, TASK_NORMAL, 1, NULL)

// /kernel/sched.c
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &amp;q-&gt;task_list, task_list) {
		unsigned flags = curr-&gt;flags;
		//调用唤醒函数（默认唤醒函数会调用try_to_wake_up）
		if (curr-&gt;func(curr, mode, wake_flags, key) &amp;&amp;
				(flags &amp; WQ_FLAG_EXCLUSIVE) &amp;&amp; !--nr_exclusive)
			break;
	}
}

/**
 * 唤醒阻塞在等待队列上的进程
 * @q: 等待队列
 * @mode: 哪些状态的进程被唤醒
 * @nr_exclusive: 如果为0，唤醒所有等待进程；否则唤醒所有非互斥进程，以及一定数量的互斥进程
 * @key: 直接作为入参传递给唤醒函数
 *
 */
void __wake_up(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, void *key)
{
	unsigned long flags;

	spin_lock_irqsave(&amp;q-&gt;lock, flags);
	__wake_up_common(q, mode, nr_exclusive, 0, key);
	spin_unlock_irqrestore(&amp;q-&gt;lock, flags);
}</pre>
<p>可以看到，核心的唤醒逻辑存放在队列项的wait_queue_t的func字段中，其默认行为是调用定义在sched.中的<pre class="crayon-plain-tag">try_to_wake_up()</pre> ，该函数执行以下逻辑：</p>
<ol>
<li>设置进程为TASK_RUNNING状态 </li>
<li>调用enqueue_task()将进程存入红黑树</li>
<li>如果被唤醒进程比当前进程优先级高，需要设置need_resched标记</li>
</ol>
<p>通常，<span style="background-color: #c0c0c0;">哪段代码负责促使等待条件达成，则同时需要调用wake_up()函数</span>，比如当磁盘数据到来时，VFS就负责对等待队列调用wake_up()。</p>
<p>注意存在虚假唤醒（spurious wake-ups）问题：进程被唤醒，不代表它等待的条件已经达成，因此总是要在循环结构中处理等待逻辑。</p>
<div class="blog_h2"><span class="graybg">抢占和上下文切换</span></div>
<p>所谓<span style="background-color: #c0c0c0;">上下文切换（Context Switching）</span>，就是<span style="background-color: #c0c0c0;">从一个可执行进程切换到另一个可执行进程</span>。定义在/kernel/sched.c的<pre class="crayon-plain-tag">context_switch()</pre> 函数负责上下文切换，它主要完成：</p>
<ol>
<li>调用声明在asm/mmu_context.h的<pre class="crayon-plain-tag">switch_mm()</pre> 将<span style="background-color: #c0c0c0;">虚拟内存映射</span>（virtual memory mapping）切换到下一个进程，这可能涉及到页表与MMC相关的操作</li>
<li>调用声明在asm/system.h的<pre class="crayon-plain-tag">switch_to()</pre> 将处理器状态切换到下一个进程，这牵涉到存储和恢复<span style="background-color: #c0c0c0;">栈信息</span>、CPU<span style="background-color: #c0c0c0;">寄存器信息</span>（例如程序计数器PC、栈指针寄存器SP、通用寄存器、浮点寄存器、包括CPU状态信息的处理器控制寄存器、跟踪进程对RAM访问的内存管理寄存器……）以及其它和体系结构相关的信息</li>
</ol>
<p>上下午切换时，存储和恢复的信息均通过进程描述符访问。</p>
<p>内核必须知道何时调用<pre class="crayon-plain-tag">schedule()</pre> ，用户代码可能永远不调用该函数，从而导致无限期执行。内核提供一个<pre class="crayon-plain-tag">need_reched</pre> 标记位用来指示是否应当进行一次新的调度。当某个进程被抢占（Preemption）时<pre class="crayon-plain-tag">scheduler_tick()</pre> 会设置该标记位；当一个高优先级进程进入可执行状态时<pre class="crayon-plain-tag">try_to_wake_up()</pre> 也会设置该标记位。</p>
<p>当<span style="background-color: #c0c0c0;">返回用户空间、或者从中断返回时</span>，内核会检查<pre class="crayon-plain-tag">need_reched</pre> 标记位，如果被设置，内核会在继续之前调用调度程序。</p>
<p>每个进程都包含一个need_reched 标记位，这是因为访问进程描述符内的数值比全局变量快（current宏很快且描述符通常位于高速缓存）。2.6+之后，该标记位存放为thread_info中一个位域字段的一个bit。</p>
<div class="blog_h3"><span class="graybg">用户抢占</span></div>
<p>内核即将返回用户空间时，如果need_reched被设置，会导致调度器被调用，以选择一个更加合适的进程来运行。</p>
<p>从中断处理程序或者系统调用的返回路径依赖于体系结构，这些代码包含在entry.S文件中，该文件包含内核入口、退出的代码。</p>
<p>用户抢占的发生时机包括：</p>
<ol>
<li>中断处理程序返回用户空间时</li>
<li>系统调用返回用户空间时</li>
</ol>
<div class="blog_h3"><span class="graybg">内核抢占</span></div>
<p>与大部分Unix变体和其它OS不同，Linux完整的支持内核抢占。</p>
<p>对于不支持内核抢占的OS，<span style="background-color: #c0c0c0;">内核代码会一直运行</span>直到它完成（返回用户空间或者明确阻塞）为止，内核没有办法在这种代码运行期间执行调度——内核中的任务是以协作方式调度的，不具有抢占性。</p>
<p>从2.6+开始，只要重新调度是安全的，内核就可以被抢占。<span style="background-color: #c0c0c0;">当正运行在内核中的进程不持有锁（这里的锁被作为非抢占区域的标记），重新调度就是安全</span>的。由于内核是SMP安全的，因此在不持有锁的情况下，当前代码是可重入的（reentrant，即多个进程可以同时执行，对于单处理器系统不存在重入问题）并且可以被抢占。</p>
<p>为支持内核抢占，进程的<pre class="crayon-plain-tag">thread_info</pre> 引入了<pre class="crayon-plain-tag">preempt_count</pre> 计数器，初值为0，每当使用锁的时候，计数增加，释放锁的时候则计数减小，因此preempt_count 为0时内核就可以被抢占。</p>
<p>当从中断返回内核空间时，内核会检查need_resched和preempt_count，如果need_resched被设置并且preempt_count为0，意味着有一个更加重要的任务可以安全的运行，因此会发生调度。</p>
<p>当进程所有的锁都被释放时，解锁代码负责检查need_resched是否被设置，如果是，调度也会发生。</p>
<p>某些内核代码需要允许或者禁止内核抢占。</p>
<p>如果内核代码被阻塞，或者显式的调用了<pre class="crayon-plain-tag">schedule()</pre> 则内核抢占也会发生，这种形式的内核抢占一直都支持。</p>
<p>内核抢占的发生时机包括：</p>
<ol>
<li>中断处理程序退出，在返回内核空间之前</li>
<li>当内核代码从不可抢占变为可抢占时</li>
<li>内核中的任务显式调用schedule()前</li>
<li>内核中的任务被阻塞时</li>
</ol>
<div class="blog_h2"><span class="graybg">实时调度策略</span></div>
<p>Linux提供了SCHED_FIFO、SCHED_RR这两种实时调度策略，相应的，普通的非实时调度策略是SCHED_NORMAL。基于调度器类框架，实时调度策略由定义在kernel/sched_rt.c中的特殊实时调度器管理（而非CFS）。</p>
<p>实时优先级的范围从0到MAX_RT_PRIO-1，MAX_RT_PRIO一般为99，因此实时优先级的范围通常是0-99，默认99。此优先级空间和SCHED_NORMAL的NICE值共享，默认的[–20,19]映射到[100, 139]</p>
<p>SCHED_FIFO策略实现了一个先入先出的、没有时间片的调度算法，一个可运行的SCHED_FIFO任务总是优先于SCHED_NORMAL任务运行，直到前者阻塞或者显式的让出CPU。由于没有时间片，SCHED_FIFO任务会一直执行，只有更高优先级的SCHED_FIFO/SCHED_RR任务才能抢占它。多个相同优先级的SCHED_FIFO任务会轮流执行，但是只有当前运行的SCHED_FIFO任务明确的让出CPU，其它任务同优先级的SCHED_FIFO任务才有机会运行。只有存在可运行的SCHED_FIFO任务，那么其它低优先级的任务只能等待它们都变为不可运行。</p>
<p>SCHED_RR基本和SCHED_FIFO一样，除了SCHED_RR提供时间片，当时间片耗尽后，同一优先级的其它实时进程获得执行机会。注意时间片只由于重新调度同一优先级的进程，低优先级的实时进程绝不能抢占高优先级的实时进程。</p>
<p>Linux的实时调度算法提供了一种<span style="background-color: #c0c0c0;">软实时工作方式</span>，即，内核尽力保证任务在限定时间到达前运行，但并不总是满足时间性要求。尽管如此，Linux实时调度算法的性能很不错，2.6+可以满足严格的时间要求。</p>
<p>某些Linux补丁，例如<a href="https://rt.wiki.kernel.org/index.php/Main_Page">CONFIG_PREEMPT_RT </a>提供硬实时调度，硬实时调度经常用于工业控制领域（例如激光焊接），时效性非常重要。</p>
<div class="blog_h2"><span class="graybg">与调度相关的系统调用</span></div>
<p>Linux提供了一系列和进程调度有关的系统调用，用于执行进程优先级、调度策略、处理器绑定、让出CPU等操作：</p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 30%; text-align: center;">系统调用 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>nice()</td>
<td>设置进程的nice值。可以将进程的静态优先级增加一个量，只有超级用户才能提供负值以提升优先级。操控进程的task_struct.static_prio、prio</td>
</tr>
<tr>
<td>sched_setscheduler()</td>
<td rowspan="2">设置/获取进程使用的调度策略 。操控进程的task_struct.policy</td>
</tr>
<tr>
<td>sched_getscheduler()</td>
</tr>
<tr>
<td>sched_setparam()</td>
<td rowspan="2">设置/获取进程的实时优先级。操控进程的task_struct.rt_priority</td>
</tr>
<tr>
<td>sched_getparam()</td>
</tr>
<tr>
<td>sched_get_priority_max()</td>
<td rowspan="2">获取实时进程优先级的最大/最小值  </td>
</tr>
<tr>
<td>sched_get_priority_min()</td>
</tr>
<tr>
<td>sched_rr_get_interval()</td>
<td>获取进程的时间片值</td>
</tr>
<tr>
<td>sched_setaffinity()</td>
<td rowspan="2">设置/获取进程和CPU的绑定关系。该绑定是强制绑定，操控task_struct.cpus_allowed位掩码</td>
</tr>
<tr>
<td>sched_getaffinity()</td>
</tr>
<tr>
<td>sched_yield()</td>
<td>暂时让出处理器，显式的把CPU让给等待执行的RUNNABLE进程。当前进程不仅被抢占，而且会被放到过期队列中，这会让它在一段时间内不被执行（实时进程不会过期，这是个例外）</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">内核数据结构</span></div>
<div class="blog_h2"><span class="graybg">链表</span></div>
<p>链表（Linked Lists）是用于存放和操作可变数量元素（称为节点node）的数据结构，其特点是元素的动态插入和删除，无需连续内存区域。只能沿链表线性移动，而不支持直接的随机访问。</p>
<p>链表分类<span style="background-color: #c0c0c0;">单向链表、双向链表</span>两类，如果尾元素的*next指向首元素，则称为<span style="background-color: #c0c0c0;">环形链表</span>。</p>
<pre class="crayon-plain-tag">//单向链表的元素
struct list_element
{
    void *data; /* the payload */
    struct list_element *next; /* pointer to the next element */
};
//双向链表的元素
struct list_element
{
    void *data;
    struct list_element *next;
    struct list_element *prev;
};</pre>
<p>由于双向环形链表的灵活性，Linux内核使用的链表就是这种类型 。Linux使用了一种独到的方式实现链表——不是将<span style="background-color: #c0c0c0;">数据载荷塞进链表节点，而把链表节点塞入数据结构</span>：</p>
<pre class="crayon-plain-tag">//常规的链表实现
struct fox
{
    unsigned long tail_length;
    unsigned long weight;
    bool is_fantastic;
    struct fox *next; //数据载荷就处于链表节点中
    struct fox *prev;
};

//Linux的链表实现
//linux/list.h，通用链表节点结构体
struct list_head
{
    struct list_head *next;
    struct list_head *prev;
};
struct fox
{
    unsigned long tail_length;
    unsigned long weight;
    bool is_fantastic;
    struct list_head list; //链表节点嵌入在数据载荷中，fox.list.prev指向上一个元素；fox.list.next指向下一个元素
    //除非嵌入到结构中，list_head类型本身没有任何意义
};</pre>
<p>这种链表设计很优雅，它<span style="background-color: #c0c0c0;">避免了为每种数据类型单独设计链表类型</span>。</p>
<p>注意，内核版本的链表，其节点的next/prev指向的上/下一个节点是<pre class="crayon-plain-tag">list_head</pre> 类型，而我们需要得到的是具有数据载荷的容器结构（上面代码中的struct fox），怎么办呢？内核提供了依据任意<span style="background-color: #c0c0c0;">元素指针来获得其容器结构体</span>的宏：</p>
<pre class="crayon-plain-tag">//依据元素member的指针ptr，获取其父结构体type的指针
#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)-&gt;member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

//计算指针偏移量的宏，与具体编译器有关
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
/include/linux/compiler-gcc4.h
#define __compiler_offsetof(a,b) __builtin_offsetof(a,b)</pre>
<p>基于上述宏，内核链表可以很方便的得到链表元素：</p>
<pre class="crayon-plain-tag">//根据list_head指针获得载荷元素
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)</pre>
<p>有两种方式来初始化链表：</p>
<pre class="crayon-plain-tag">// /include/linux/list.h 初始化链表元素，将prev/next都指向自己
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list-&gt;next = list;
	list-&gt;prev = list;
}

//运行时动态创建
struct fox *red_fox;
red_fox = kmalloc( sizeof ( *red_fox ), GFP_KERNEL );
red_fox-&gt;tail_length = 40;
red_fox-&gt;weight = 6;
red_fox-&gt;is_fantastic = false;
INIT_LIST_HEAD( &amp;red_fox-&gt;list );

//编译期间静态创建的最简方式
struct fox red_fox = {
    .list = LIST_HEAD_INIT(red_fox.list),  //{ &amp;(red_fox.list), &amp;(red_fox.list) }
    .tail_length = 40,
    .weight = 6,
};</pre>
<p>通过下面的宏可以初始化一个变量名为name的链表头：</p>
<pre class="crayon-plain-tag">#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)</pre>
<div class="blog_h3"><span class="graybg">链表操作</span></div>
<p>下表列出的函数，对链表进行增删元素、连接链表的操作，都提供O(1)的复杂性，亦即需要恒定时间来完成；对列表进行遍历的操作则提供O(n)的复杂性，亦即需要的时间和元素个数成正比： </p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 27%; text-align: center;"> 函数/宏</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>list_add</td>
<td><pre class="crayon-plain-tag">list_add(struct list_head *new, struct list_head *head)</pre> 添加一个新节点new到节点head的后面。该函数可以用于实现Stack结构</td>
</tr>
<tr>
<td>list_add_tail </td>
<td><pre class="crayon-plain-tag">list_add_tail(struct list_head *new, struct list_head *head)</pre> 添加一个新节点new到head的前面。该函数可以用于实现Queue结构</td>
</tr>
<tr>
<td>list_del</td>
<td><pre class="crayon-plain-tag">list_del(struct list_head *entry)</pre> 从链表中删除entry节点，该操作不会释放entry持有的内存，或者其被嵌入的struct</td>
</tr>
<tr>
<td>list_del_init</td>
<td><pre class="crayon-plain-tag">list_del_init(struct list_head *entry)</pre> 删除entry并将其重新初始化</td>
</tr>
<tr>
<td>list_move</td>
<td><pre class="crayon-plain-tag">list_move(struct list_head *list, struct list_head *head)</pre> 将list从它的链表位置中移除，并添加到head元素的后面，注意head可以位于同一个链表或者不同链表</td>
</tr>
<tr>
<td>list_move_tail</td>
<td><pre class="crayon-plain-tag">list_move_tail(struct list_head *list, struct list_head *head)</pre> 与上面类似，但是添加到head的前面</td>
</tr>
<tr>
<td>list_empty</td>
<td><pre class="crayon-plain-tag">list_empty(struct list_head *head)</pre> 检查链表是否为空，如果非空返回0，否则返回非0（true）</td>
</tr>
<tr>
<td>list_splice</td>
<td><pre class="crayon-plain-tag">list_splice(struct list_head *list, struct list_head *head)</pre> 把两个链表相连，前一个链表的list元素接在后一个链表head元素的后面</td>
</tr>
<tr>
<td>list_splice_init</td>
<td><pre class="crayon-plain-tag">list_splice_init(struct list_head *list, struct list_head *head)</pre> 与上面类似，但是list指向的链表会被重新初始化</td>
</tr>
<tr>
<td>list_for_each</td>
<td>
<p>迭代整个链表：</p>
<pre class="crayon-plain-tag">struct list_head *p;
struct fox *f;
list_for_each(p, fox_list) //fox_list是作为链表首元素的指针
{
    /* 局部变量p作为指向当前被迭代元素的指针 */
    f = list_entry(p, struct fox, list); //取得容器结构
}</pre>
</td>
</tr>
<tr>
<td>list_for_each_entry</td>
<td>
<p>迭代整个链表，比上面的更易用：
<pre class="crayon-plain-tag">struct list_head *fox_list;
struct fox *f;
list_for_each_entry( f, fox_list, list ){
    f-&gt;tail_length;
};</pre>
<p>类似的还有反向迭代器<pre class="crayon-plain-tag">list_for_each_entry_reverse()</pre> </p>
</td>
</tr>
<tr>
<td>list_for_each_entry_safe</td>
<td>迭代整个列表，迭代期间支持节点的删除，类似的还有反向迭代器<pre class="crayon-plain-tag">list_for_each_entry_safe_reverse()</pre> </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">队列</span></div>
<p>队列是一种先入先出（FIFO）的数据结构，通常和<span style="background-color: #c0c0c0;">生产者-消费者编程模型</span>密切相关。</p>
<p>Linux内核队列<pre class="crayon-plain-tag">kfifo</pre> 和Java NIO的java.nio.Buffer比较类似，它使用两个指针分别指示<span style="background-color: #c0c0c0;">读、写的偏移量</span>，前者指向下一次出队（dequeue）位置（字节偏移），后者则指向下一次入队（enqueue）位置，读指针总是小于等于写指针。</p>
<p>当执行一次入队操作时，写指针将加上入队的元素个数，出队操作时类似。当写偏移量到达队列长度限制后，即无法再行入队数据，直到队列被重置。</p>
<p>可以通过下面的方式创建队列：</p>
<pre class="crayon-plain-tag">//创建一个队列，存放到fifo指针，尺寸size个字节
int kfifo_alloc( struct kfifo *fifo, unsigned int size, gfp_t gfp_mask );
//自己分配内存，buffer用于指定存放元素的缓冲区
void kfifo_init( struct kfifo *fifo, void *buffer, unsigned int size );
//静态方式定义队列，使用的较少
DECLARE_KFIFO( name, size );
INIT_KFIFO( name );

struct kfifo fifo;
int ret = kfifo_alloc( &amp;kifo, PAGE_SIZE, GFP_KERNEL ); //创建一个和页大小一致的队列</pre>
<p>kfifo提供了以下重要函数： </p>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 25%; text-align: center;"> 函数/宏</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>kfifo_in</td>
<td><pre class="crayon-plain-tag">unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len)</pre> 入队操作。如果成功，返回入队字节数；如果队列可用空间小于len，则返回的值可能小于len甚至为0</td>
</tr>
<tr>
<td>kfifo_out</td>
<td><pre class="crayon-plain-tag">unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len)</pre> 出队操作。从队列取出len字节的数据存放到to，返回实际获得的字节数</td>
</tr>
<tr>
<td>kfifo_out_peek</td>
<td><pre class="crayon-plain-tag">unsigned int kfifo_out_peek(struct kfifo *fifo, void *to, unsigned int len, unsigned offset)</pre> 窥看队列，不修改读指针。offset指定偏移量，如果为0则与kfifo_out类似，从队列头读取</td>
</tr>
<tr>
<td>kfifo_size</td>
<td><pre class="crayon-plain-tag">static inline unsigned int kfifo_size(struct kfifo *fifo)</pre> 获取队列总计尺寸</td>
</tr>
<tr>
<td>kfifo_len</td>
<td><pre class="crayon-plain-tag">static inline unsigned int kfifo_len(struct kfifo *fifo)</pre> 获取已入队的字节数</td>
</tr>
<tr>
<td>kfifo_avail</td>
<td><pre class="crayon-plain-tag">static inline unsigned int kfifo_avail(struct kfifo *fifo)</pre> 获取可用的字节数</td>
</tr>
<tr>
<td>kfifo_is_empty</td>
<td><pre class="crayon-plain-tag">static inline int kfifo_is_empty(struct kfifo *fifo)</pre> 判断队列是否为空，类似还有<pre class="crayon-plain-tag">kfifo_is_full</pre> </td>
</tr>
<tr>
<td>kfifo_reset</td>
<td><pre class="crayon-plain-tag">static inline void kfifo_reset(struct kfifo *fifo)</pre> 重置队列，抛弃其中全部内容</td>
</tr>
<tr>
<td>kfifo_free</td>
<td><pre class="crayon-plain-tag">void kfifo_free(struct kfifo *fifo)</pre> 销毁一个由kfifo_alloc分配的队列。对于使用kfifo_init创建的队列，缓冲区由开发者自己销毁</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">映射</span></div>
<p>映射也称为关联数组。尽管映射常常使用散列表的方式实现，但是<span style="background-color: #c0c0c0;">基于自平衡二叉搜索树实现的话，可以在最坏情况下获得更好性能</span>（对数复杂性vs线性复杂性），同时二叉树提供散列表没有的<span style="background-color: #c0c0c0;">顺序保证</span>，因此像<pre class="crayon-plain-tag">std::map</pre> 等映射是基于二叉树实现的。</p>
<p>Linux内核包含一个非通用目的的映射数据结构，用于将用户空间的<span style="background-color: #c0c0c0;">唯一标识</span>（UID，例如POSIX定时器ID）映射到内核中与之关联的数据结构上（例如k_itimer结构体）。该映射被命名为idr，函数<pre class="crayon-plain-tag">void idr_init(struct idr *idp)</pre> 用于定义一个idr。</p>
<p>idr还可以用来生成UID。</p>
<div class="blog_h2"><span class="graybg">二叉树</span></div>
<p>所谓树，是一种分层（多级）的数据结构。从数学角度来说，树是一个<span style="background-color: #c0c0c0;">无环连接的有向图</span>，每个图顶点（vertex，用树的术语称节点，node）具有0-N个出边（outgoing edges）以及0-1个入边（incoming edges）。二叉树就是<span style="background-color: #c0c0c0;">每个节点最多有2个出边</span>的树。</p>
<div class="blog_h3"><span class="graybg">二叉搜索树</span></div>
<p>简称BST（Binary Search Trees），是一个<span style="background-color: #c0c0c0;">节点有序</span>的二叉树。其节点顺序一般遵循以下法则：</p>
<ol>
<li>根的左侧分支中，节点的值都小于根节点</li>
<li>根的右侧分支中，节点的值都大于根节点</li>
<li>递归的，所有子树也都是二叉搜索树</li>
</ol>
<p>由于这种有序性，在树中搜索一个给定值（对数复杂度）、按序遍历树（线性复杂度）都相当高效。</p>
<div class="blog_h3"><span class="graybg">自平衡二叉搜索树</span></div>
<p>节点的深度是指：从根节点到达当前节点，需要经过的父节点数目。</p>
<p>自平衡（Self-Balancing）二叉搜索树是指树中所有<span style="background-color: #c0c0c0;">叶子节点的深差不超过1</span>的二叉搜索树。</p>
<div class="blog_h3"><span class="graybg">红黑树</span></div>
<p>红黑树是一种“半平衡”的自平衡二叉搜索树，所谓<span style="background-color: #c0c0c0;">半平衡是指：深度最大的叶子节点，其深度不会大于最浅叶子节点的2倍</span>。红黑树节点具有特殊的着色属性，并且具有下面6条特征，正是这些特征保证了它的“半平衡”：</p>
<ol>
<li>任意节点，要么着红色，要么着黑色</li>
<li>根、叶子节点都是黑色</li>
<li>叶子节点不包含数据</li>
<li>所有非叶子节点都有两个子节点</li>
<li>如果一个节点是红色，那么它的子节点都是黑色</li>
<li>节点到其每个叶子节点的简单路径（路径中无重复节点），均包含相同数量的黑色节点</li>
</ol>
<p>要使不同叶子节点的深差拉大，只能通过增加红色中间结点，由于第5条的限制，导致这种深度差距无法达到2倍。下面是一个合法红黑树的示意图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/12/rbtree.png"><img class="size-full wp-image-8796 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2010/12/rbtree.png" alt="rbtree" width="450" height="217" /></a></p>
<p>红黑树较为复杂，但是可以保证良好的<span style="background-color: #c0c0c0;">最坏情况性能</span>。</p>
<p>当从结构上改变了红黑树后，为了仍然满足上述6条性质，可能需要：</p>
<ol>
<li>对部分节点进行重新着色</li>
<li>调整部分指针的指向（左旋，右旋）</li>
</ol>
<div class="blog_h2"><span class="graybg">数据结构的选型</span></div>
<table class="fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 15%; text-align: center;"> 数据结构</td>
<td style="text-align: center;">适用场景 </td>
</tr>
</thead>
<tbody>
<tr>
<td>链表</td>
<td>
<p>如果对集合的操作主要是遍历，可以选用链表，没有任何数据结构能提供<span style="background-color: #c0c0c0;">比线性复杂度更优的的遍历性能</span></p>
<p>当数据量小、性能不是优先考虑因素时，优先使用链表</p>
</td>
</tr>
<tr>
<td>队列</td>
<td>当逻辑符合生产者-消费者模式时，使用队列。特别是需要一个<span style="background-color: #c0c0c0;">定长</span>缓冲区时</td>
</tr>
<tr>
<td>映射</td>
<td>如果需要映射一个UID到内核对象</td>
</tr>
<tr>
<td>红黑树 </td>
<td>如果需要存储<span style="background-color: #c0c0c0;">大量数据</span>，且要求<span style="background-color: #c0c0c0;">检索迅速</span></td>
</tr>
</tbody>
</table>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-study-note-vol1">Linux内核学习笔记（一）</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-study-note-vol1/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
