内核参数调整

释放内存

/proc/sys/vm/dropcaches Writing to this file causes the
kernel to drop clean caches, dentries and inodes from memory, causing
that memory to become free. To free pagecache, use echo 1 \>
/proc/sys/vm/dropcaches; To free dentries and inodes, use
echo 2 \> /proc/sys/vm/dropcaches; To free pagecache,
dentries and inodes, use echo 3 \> /proc/sys/vm/dropcaches.

`` example
sync #确保文件系统完整性
echo 3 > /proc/sys/vm/drop_caches


<h1 id="内核配置 <span class="tag" tag-name="module"><span class="smallcaps">module</span></span>">内核配置 <span class="tag" tag-name="module"><span class="smallcaps">module</span></span></h1>

<h2 id="从运行的Linux系统中找到需要的模块">从运行的Linux系统中找到需要的模块</h2>

一般Linux发行版的内核会包含大量的驱动,编译时需要花费大量时间,这些驱动模块中很多不是本机所需要的,因此有必要学会从当前运行的Linux系统中找出所需的模块,编译出自己的内核。这样做可以节省内存,也能得到一个跑得更快的系统。
有多个地方可以找到设备与哪个驱动关联在一起,其中之一就是sysfs。sysfs总会挂载到/sys,通过它可以看到kernel的各个部分是如何粘合在一起的。/sys中包含了到文件系统各个不同位置的链接。
具体查找步骤如下:

  1. 到sysfs的class目录下找到相应的device。网络设备会列在/sys/class/net目录下,tty设备会在/sys/class/tty目录之下。
  1. 通过跟踪sysfs tree找到控制该device的模块的名字。
example

basename readlink /sys/class/class_name/device_name/device/driver/module


其中class<sub>name和devicename要根据具体情况改变</sub>。

  1. 用find和grep到kernel的makefile中找到用来编译该模块的CONFIG<sub>规则</sub>。
example

find -type f -name Makefile | xargs grep module_name


其中moduel<sub>name会第2步骤找出的名称</sub>。

  1. 到kernel的配置系统(make

menuconfig)中找到相应的配置位置启用相应的驱动。

<h2 id="用modalias文件找出系统所用模块">用modalias文件找出系统所用模块</h2>

modalias文件中的mod alias由设备制造商、ID、class type、其它unique
identifier组成。驱动程序(driver)会告诉kernel自己支持的设备的列表,这个设备列表会保存在驱动模块(driver
module)中。modprobe命令会从驱动程序的设备列表中找出与当前设备的mod
alias匹配的驱动并加载对应模块。通过下面的脚本可从获得当前系统所有加载的驱动模块。

example

for i in find /sys/ -name modalias -exec cat {} \;; do
/sbin/modprobe --config /dev/null --show-depends $i;
done | rev | cut -f 1 -d '/' | rev | sort -u


<h2 id="手动找出设备所需的模块">手动找出设备所需的模块</h2>

方法一:把设备所属类别的所有驱动程序都编译成模块,让udev在启动时自动进行匹配,然后用前面所列的方法找出对应的模块。
方法二:根据每种设备的具体情况,手动找出所需模块。

<h3 id="找出PCI设备的驱动模块">找出PCI设备的驱动模块</h3>

PCI设备由vendor ID和device ID组成。查找驱动步骤如下:

  1. 用lspci找到需要找到驱动的设备的PCI bus ID。
  1. 切换到/sys/bus/pci/devices/0000:bus<sub>id目录</sub>。
  1. 在该目录下找到vendor和device两个文件,里面的内容即是vendor

id和device id。

  1. 到内核源码目录下打开include/linux/pci<sub>ids</sub>.h文件,找到vendor<sub>id和deviceid</sub>。
example

grep -i 0x10ec include/linux/pci_ids.h
#define PCI_VENDOR_ID_REALTEK 0x10ec


  1. 在源码目录中搜索,vendor<sub>id和deviceid都应该在struct</sub>

pci<sub>deviceid结构体中</sub>。对应的源码文件的名称即是DRIVER<sub>NAME</sub>。

应该 : grep -Rl PCI<sub>VENDORIDREALTEK</sub> \*

  1. 在内核源的Makefile中搜索对应的编译对应驱动的CONFIG<sub>规则</sub>。
example

find -type f -name Makefile | xargs grep DRIVER_NAME


  1. 在内核配置系统中搜索对应的配置,并根据需要做出修改。

<h3 id="找出USB设备的驱动模块">找出USB设备的驱动模块</h3>

  1. 通过添加、删除USB设备,并观察lsusb输出,找出对应的USB设备的vendor

id和product id。每次的USB总线编号可能会改变。

  1. 与pci不同之外在于没有定义了所有vendor

id的单独的pci<sub>ids</sub>.h文件。需要到源文件中搜索USB设备的vendor
id。vendor<sub>id和productid应该出现在名为struct</sub>
usb<sub>deviceid的结构体的定义之中</sub>。

example

grep -i -R -l 157e drivers/*


  1. 在内核源的Makefile中搜索对应的编译对应驱动的CONFIG<sub>规则</sub>。
example

find -type f -name Makefile | xargs grep DRIVER_NAME


  1. 在内核配置系统中搜索对应的配置,并根据需要做出修改。

<h3 id="找出磁盘控制器的驱动">找出磁盘控制器的驱动</h3>

  1. 找到控制设备的logical

device,在/sys/block/sda下的device链接会指向对应的逻辑设备。

example

ls -l /sys/block/sda/device


  1. 对逻辑设备对应的目录下可以找到driver文件,它指向的链接即是驱动模块的名称,一般这时会找到类似于sd的驱动模块。
  1. 从该目录往上一级一级查找,直到有driver文件的目录为止,此时driver文件指向的即是磁盘控制器所需驱动名称。

<h2 id="内核驱动数据库">内核驱动数据库</h2>

<http://cateee.net/lkddb/> LKDDb: Linux Kernel Driver DataBase

<h1 id="内核编译 <span class="tag" tag-name="compile"><span class="smallcaps">compile</span></span>">内核编译 <span class="tag" tag-name="compile"><span class="smallcaps">compile</span></span></h1>

<h2 id="交叉编译工具 <span class="tag" tag-name="cross"><span class="smallcaps">cross</span></span> <span class="tag" tag-name="compile"><span class="smallcaps">compile</span></span>">交叉编译工具 <span class="tag" tag-name="cross"><span class="smallcaps">cross</span></span> <span class="tag" tag-name="compile"><span class="smallcaps">compile</span></span></h2>

<h3 id="host,build,target">host,build,target</h3>

<http://gcc.gnu.org/onlinedocs/gccint/Configure-Terms.html> There are
three system names that the build knows about: the machine you are
building on (build), the machine that you are building for (host), and
the machine that GCC will produce code for (target). When you configure
GCC, you specify these with –build=, –host=, and –target=.

<h3 id="coreutils版本选择">coreutils版本选择</h3>

coreutils从8.20开始会用目标编译器生成可执行程序,并用这些程序来测试,这些程序在交叉编译的host主机是无法执行,因此就无法正常构建。因此构建coreutils最好是使用8.20之前的版本。
<http://comments.gmane.org/gmane.linux.lfs.clfs.devel/1932>

<h2 id="编译参数文档">编译参数文档</h2>

/Documents/kbuild/kbuild.txt

<h2 id="initrd">initrd</h2>

<h3 id="linux内核initrd文件自定义方法">linux内核initrd文件自定义方法</h3>

重新编译内核后,可能加入了自定义的模块,就有可能需要修改init文件,而init文件就在initrd中,这里记录下操作步骤,以防遗忘。

  1. cp /boot/initrd-3.2.img /tmp/mylinux/initrd-3.2.img.gz

这里之所以进行改名,是因为initrd-3.2.img是经过gzip压缩过的,所以需要对其解压,但是gzip对解压的文件的文件后缀名又有要求,所以就先进行改名。

  1. gunzip initrd-3.2.9.img.gz
  2. cpio -id \< initrd-3.2.9.img

经过以上三步,就在当前目录下解压了initrd文件,从而得到了init文件。
根据自己的需求修改init文件后,通过下面命令重新生成initrd文件。

  1. find . \| cpio -H newc -o \| gzip -9 \> /boot/initrd-3.2.9.img

注意一下内容摘自网上资料,留作参考:

en<sub>initcpio</sub> 获取 gen<sub>initcpio</sub>,工具
,gen<sub>initcpio是编译内核时得到的</sub>, 在内核源代码的 usr
目录下,我们可以通过 以下步骤获取它,进入内核源代码 执行 :

这样即编译好gen<sub>initcpio</sub>, gen<sub>initramfslist</sub>.sh
在内核源代码的 script 目录下, 将这两个 文件 copy 到 /tmp
目录下,/tmp/initrd 为 解压好的 initrd 目录,执行以下命令 制作initrd :
\#制作initrd :

只有用这个方式压缩的initrd ,在Linux系统重启的时候才能 一正确的文件格式
boot 起来,也可以用 这种方式修改安装光盘的initrd文件 然后 进行系统安装。

  1. 如何在 initrd 中添加新的驱动,以 ahci.ko 为例

3.1 gen<sub>initcpio</sub>

\#cpio –ivdum \< initrd-‘uname –r’.img;

\#cd /tmp/initrd \#vim init加上一行 insmod *lib/ahci.ko \#cp ahci.ko
lib* \#cd /tmp

至此,新的initrd文件initrd-‘uname –r’.img中就包含了ahci的驱动程序了
,这种方式是最简单有效的。 3.2 mkinitrd (1) Add “alias
scsi<sub>hostadapter</sub> ahci” at /etc/modprobe.conf (2) copy ahci.ko
to “/lib/module/\$(kernel-version)”/kernel/drivers/scsi” (3) mkinitrd
initrd.img ‘uname -r’ 至此,新的initrd文件initrd-‘uname
–r’.img中就包含了ahci的驱动程序了 . \#释放cpio格式的initrd: mv
initrd.img imitrd.img.gz gunzip initrd.img.gz cpio -i –make-directories
\< initrd.img \#释放centos6.2系统的initramfs.img 1."gunzip
initrd.img-2.6.27-7-generic.gz",得到一个未压缩的initrd.img-2.6.27-7-generic

  1. ”cpio -iv \< initrd.img-2.6.27-7-generic",提取成功

\#制作cpio格式的initrd(新2012年使用过的) \#cd
/root/busybox-1.15.3/rootfs9260 \#find . \| cpio -H newc -o \>
../initrd<sub>cpio</sub>.img
\#制作cpio格式的initrd(2009年制作的LFS的方式): dd if=/dev/zero
of=/tmp/rootfs bs=1k count=35000 losetup /dev/loop0 /tmp/rootfs
mkfs.ext2 –F –i 2000 /tmp/rootfs mkdir /tmp/loop mount –o loop
/tmp/rootfs /tmp/loop \#然后将刚才建立的基本系统拷贝到/tmp/loop cp
/lfs/\* /tmp/loop –arfp find . \| cpio –o –H newc \| gzip –c \>
/tmp/initrd.img

<h3 id="生成initrd时No support for locale: zh<sub>CN</sub>.utf8错误">生成initrd时No support for locale: zh<sub>CN</sub>.utf8错误</h3>

the problem was that
/usr/share/initramfs-tools/hooks/root<sub>locale</sub> is expecting to
see individual locale directories in /usr/lib/locale but locale-gen is
configured to generate an archive file by default.

example

sudo locale-gen --purge --no-archive


下面的命令并不通解决问题:

example

sudo dpkg-reconfigure locales


<h2 id="模块尺寸变大的解决办法">模块尺寸变大的解决办法</h2>

The default kernel configuration is configured to support as many
hardware as possible. A non-stripped kernel with default configuration
resulted in a size of 1897996 kB (including kernel + modules). When
stripping many unnecessary drivers and options, it resulted in a size of
892892 kB which is a size reduction of 53% compared to the stock kernel.

When installing the kernel modules, append the
INSTALL<sub>MODSTRIP</sub>=1 option. This will strip all debugging
symbols and reduced the size by 92% for me (from 892892 kB to 69356 kB).
Note this will only affects modules to be installed and not the kernel
(vmlinuz) itself.

Use the INSTALL<sub>MODSTRIP</sub> option for removing debugging
symbols:

Similarly, for building the deb packages:

具体参数参看/Documents/kbuild/kbuild.txt

<h2 id="iwlwifi不能直接编译进内核">iwlwifi不能直接编译进内核</h2>

iwlwifi由于需要用到firmware,直接编译进内核后会导致无法加载firmware文件。Intel无线网卡芯片firmware文件可从网站直接下载。
<http://crunchbang.org/forums/viewtopic.php?id=33070>

<h1 id="软件配置">软件配置</h1>

<h2 id="usbutils-0.0.8">usbutils-0.0.8</h2>

依赖libusb.

<h2 id="udev-174">udev-174</h2>

<h3 id="configure">configure</h3>

example

./configure --prefix=/usr --sysconfdir=/etc --sbindir=/sbin --libexecdir=/lib/udev --disable-introspection --disable-keymap --disable-gudev --disable-hwdb --host=$TARGET


<h1 id="存储管理 <span class="tag" tag-name="mem"><span class="smallcaps">mem</span></span>">存储管理 <span class="tag" tag-name="mem"><span class="smallcaps">mem</span></span></h1>

<h2 id="Paging vs. Swapping">Paging vs. Swapping</h2>

Swapping与Swap分区是完全没有关系的。Swapping在内存分页机制出现之前就已经出现,它是指在内存资源紧张的情况下,操作系统将整个进程拷贝到磁盘上。Swapping虽然解决了内存资源紧张的情况,然而内存的利用率却很底。反复的拷贝还会造成大量的碎片,另外,进程中最为忙碌的部分可能无法有效地保存在内存中,导致运行效率的降低。
Swap分区用于分页机制,当进程需要的页不在内存中时,就会产生page
fault。系统就要从Swap分区中加载页面。

<h2 id="内存地址分配">内存地址分配</h2>

Kernel地址:C0000000H-FFFFFFFFH,3G-4G User地址:0-BFFFFFFFH,0-3G

<h2 id="Kernel Pages">Kernel Pages</h2>

内核的页面不会交换至磁盘,Linux内核会一直在内存中。只有通过加载或卸载模块才能改变内核所用内存。

<h2 id="地址映射">地址映射</h2>

从虚拟地址到物理地址的映射是由处理器的硬件提供支持的,x86的页表是双层结构。Page
Table Table包含1024个Page Table,每个Page
Table包含1024个Page。每个Table结构包含1024条entry,每条entry占4个字节。每个table本身也是page。
进程中32位虚拟地址会分成三个部分: 32 bits = 10 bits + 10 bits + 12 bits
2<sup>10</sup> =
1024,10位正好可以表示1024个位置,因此32位地址正好拆分成以下三部分: 32
bits = (Index into Page Table Table) + (Index into Page Table) + (Index
into Page) 从虚拟地址映射到物理地址按下面的步骤来做:

  1. 取出前10位,在Page Table Table中找到相应Page Table Table的地址。
  2. 根据Page Table Table的地址,再加上中间10位代表的索引,找到Page

Table的真实地址。

  1. 根据Page Table的真实地址,再加上最后12位的索引,找到真实的物理地址。

通过前20位可查到Page Table中的entry,Page
Table中的entry保存了真实的物理地址,这个物理地址的后12位必为0。操作系统并没有浪费这12位,最低位被称为Present
Bit,用来表示表示该页是否已经在RAM中。Present
Bit为0的情况有两种:一是该页还在磁盘上,结果就是产生Page
Fault。二是该页地址禁止访问,结果产生Segmentation Fault。
每个任务都会有自己的Page Table Table。Page Table
Table的地址会被加载到CR3寄存器。每次任务的切换都会自动加载CR3,这就是为什么每个任务都会有自己独立的虚拟地址的原因。Linux中最高1G内存是内核使用,在创建进程时,Page
Table
Table中最后这1G内存会被映射到相同的物理地址,因此,所有Linux会共享相同的内核。
内核的地址虽然是共享的,但进程并不能访问所有内核代码。Page
Table的entry的第2位,称为US(第0位为Present
Bit),该位为1时,表示地址可以访问,该位为0,则表示必须进行权限检查。权限的级别在CPL寄存中,通常用户进程的CPL值为3,CPL为3时,相应的内存页并不能在用户进程中访问。这是Linux内存保护的基本机制。
Page Table Table entry中的Present Bit与Page Table
entry中的含义是相同的。Page Table Entry中Present
Bit为0表示4K大小的页在磁盘上,而Page Table Table entry中Present
Bit为0,则表示4M的内存页在磁盘上。 Linux用Page Table
entry中的第1位来防止内存修改,第1位称为WR位。设置WR位后,执行该页内存的写入操作会导致Segmentation
Fault。
使用了写保护位后,数据和代码就必须分离,因为代码所在页设置了写保护,因此代码只能在没有写保护的页是执行写入。机器在加载进程时,就要实现代码与数据的分离。事实上ELF之类的可执行文件本身就是分段存储的。代码段与数据段是所有段中最基本的两个。
可执行文件有分segment的,object code
file则分为多个section。链接器要把多个object code file中的text
section拼装成一个text segment,再将多个object code file中的data
section拼装成一个data segment。 object code
file中的section,则是由assembler来完成的。当然,这些事情并不是自动完成的,编程者必须在代码中指明相应的section。如:

example

.section .data
.section . text


<h2 id="ELF文件的格式">ELF文件的格式</h2>

由于程序中的地址需要在加载时才能确定,可执行文件必须列出这些需要重新定位的地址在文件中的位置,这就是relocation
table。
由于代码段需要写保护,ELF中必须保存不同段的位置信息,这样加载时就能根据不同的段设置写保护位。
ELF Header中包含了Program Header Table及Section Header
Table的位置及大小。Program Header
Table中记录了segment的信息,如:Type,File Offset,Virtual Address等。
连接器会把section拼装成segment,但是也存在没属于任何segment的section。
需要其它代码才能运行的文件形式称为object code file。连接器负责将object
code file连接在一起,拼装出可执行文件。 创建object code
file是assembler的主要任务。

<h2 id="中断">中断</h2>

反复地fetch-execute,这是计算的program-driven模型,然而计算机并不纯粹是program-driven的,有时会是hardware-driven。CPU的中断针脚允许fetch-execute的过程被打断。最初设计这些针脚的目的是为了提高处理器的利用效率,减少与I/O设备交互的时间。
如果没有中断,I/O交互只能依赖于CPU和IN和OUT指令。由于外设速度远远低于CPU的速度,CPU必须耗费大量的时间去检测设备是否已经准备好了。把系统中所有pending设备排队,依次检查是否已经准备好了,这个过程称为Polling。相比于逐个检查,效率是好多了,但依旧会浪费大量的CPU时间。
为了解决Polling的问题,CPU就提供了允许I/O设备向自己发送信号的针脚,这个针脚就称为interrupt
pin。有了它,I/O设备就可主动向CPU发起通信。CPU就可以在接收到中断信号后再去处理相应的I/O,处理好后,又可完全忽视这些I/O设备的存在。
需要向CPU发送中断信号的I/O设备很多,而CPU只有一个中断针脚。事实上,I/O设备会选通过中断总线连接到interrupt
controller
circuit上,每个I/O设备会分配到中断总线中的一条,这些中断线会以IRQ0,IRQ1,…的形式编号。IRQ就是interrupt
request,数字是中断号。
由于CPU只有一根中断针脚,当它收到中断信号时,并不能确定是由哪个设备发出的中断请求。interrupt
controller
circuit也是连接在数据总线上的,CPU就可向它查出中断号。CPU的三个针脚:D/C#,M/IO#,W/R#称为Bus
Cycle Definition Pins,用于控制不同的总线访问周期。

| | D/C# | M/IO# | W/R# |
|-----------------------|------|-------|------|
| Interrupt Acknowledge | 0 | 0 | 0 |
| Halt/Special Cycle | 0 | 0 | 1 |
| Code Read | 0 | 1 | 0 |
| reserved | 0 | 1 | 1 |
| I/O Read | 1 | 0 | 0 |
| I/O Write | 1 | 0 | 1 |
| Memory Read | 1 | 1 | 0 |
| Memory Write | 1 | 1 | 1 |

CPU通过控制针脚进入Interrupt Acknowlege bus cycle,此时,只有interrupt
controller可以响应这个时钟周期,interrupt
controller会把中断号放入数据总线的最低8位上。CPU从数据总线上取出中断号,以中断号为索引,从Interrupt
Descriptor Table中取出Interrupt Service
Routine的地址。中断号只有一个字节,因此,它的值只能是0~255。
创建interrupt descriptor table,把Interrupt Service
Routine加载到内存上,这些都要由操作系统在启动过程中完成。中断向量表的位置会在IDT寄存器上,有两条指令可以操作IDT寄存器:

example

LIDT mem
SIDT mem


中断向量表中每条记录长度为8字节,这些8字节记录也称为gate。
中断例程执行的过程与CALL指令相似,eip的值会被推入栈中,这一点与普通CALL是一样的,不过,跳转到中断例程之前flags寄存器的值也会被推入栈中。由此,中断例程在返回时不能用RET指令,而是要使用IRET指令。
额外保存flags寄存器是为了中断仲裁(interrupt
arbitration)。中断例程正在执行时,再有中断过来,该怎么办?如果允许在中断例程不断嵌套,那么有可能会导致栈溢出,从而系统崩溃。
通常interrupt enable
flag会被设置,此时,CPU能够正常响应中断请求。当CPU进入中断例程后,会自动清除该位标记,CPU随后就会忽略任务中断请求信号。中断例程中将flags寄存保存到栈上,就是为了能够在中断例程执行结束后恢复interrupt
enable
flag的原来状态。一次一中断的策略在现在的CPU中很少使用,因为一些紧急的中断并不能等待,需要立即处理。
现代计算机为不同的中断分配了优先级,允许中断嵌套。x86中用于控制interrupt
enable flag的指令有两条:

example

STI
CLI


在ISR的开始位置包含STI指令,可打开中断处理,此时有可能产生中断嵌套。在中断可嵌套的情况下,维护中断优先级的次序主要由中断控制器完成。当优先级高的中断在执行时,中断控制器不能发送低优先级的中断请求。不过,问题随之产生:中断控制器何时知道高优先级的中断例程已经执行好了?x86系统中,中断例程在执行的最后都会向中断控制器发送"All
Done"消息,这个消息也被称为EOI(end of
interrupt)。EOI其实只是OUT指令,这是因为中断控制器本身就接在数据总线上的缘故。中断控制器与一般的I/O设备很相似,只除了一点:中断控制器可直接向CPU发送中断请求,而其它I/O设备只能通过中断控制器向CPU发送中断请求。

<h3 id="NMI和RESET">NMI和RESET</h3>

除了普通中断针脚外,CPU还有NMI和RESET中断针脚。NMI(notmaskable
interrupt)不受interrupt enable
flag的影响。CPU接收到NMI中断,并不需要切换到interrupt acknowledge bus
cycle来读取中断号,因为,NMI的中断号为2,这是预先规定好的。一般NMI中断用于报告内存校验错误或电源供应等严重问题。
RESET中断针脚一般在内存中还没有中断向量表时使用。RESET中断针脚对应的中断例程一般会指向CMOS中的代码。用于开机自检。RESET针脚与机箱的电源开关会连接在一起。

<h3 id="内部中断和软中断">内部中断和软中断</h3>

除0,内存访问越界等异常情况,也需要指定中断号,由中断例程来处理。异常不需要interrupt
acknowledge bus
cycle,不过也像外部中断一样要分配中断号。区分外部中断还是内部中断,要由中断处理例程自己来判断。
软件中使用中断例程的指令为:

example

INT imm


| | Ordinary Subroutine | Interrupt Service Routine |
|-----------|---------------------|---------------------------|
| Invoke | CALL | INT |
| Terminate | RET | IRET |

中断调用与CALL函数调用的行为在细节上并不完全一样。函数调用完成后,程序会回到CALL指令的下一条指令继续执行,而由硬件导致的异常,在中断例程执行完成后,会重试产生硬件异常的指令,而不是跳到下一条。最典型的就是Page
Fault,Page
Fault表示该内存页还在磁盘上,操作系统加载后,就要重试产生Page
Fault的指令。这种硬件异常称为Fault。
还有些硬件异常与CALL一样,中断例程执行完成后,会执行产生异常指令的下一条指令。这种硬件异常称为Trap,比如,除0异常。

<h2 id="系统调用">系统调用</h2>

用系统调用输出Hello World:

c

int main(){
char s[] = "Hello world!\n";
write(1,s,13);
}


write()系统调用其实是通过:

example

INT 80H


指令来完成的,所有的系统调用号都可在unistd.h头文件中找到。DOS中的系统调用使用INT
21H。
参数调用约定也由unistd.h中的宏规定好。一般来说,参数从左到右,依次使用寄存器EBX,ECX,EDX,EDI,ESI。write(1,2,13)用下面的命令可以看到参数所在的寄存器。

<h2 id="权限级别">权限级别</h2>

Linux只用了0级(内核)和3级(用户进程)。当前执行的代码的运行级别放在CPL寄存中。Current
Privilege Level。
级别为0的权限远比root帐号来得强大。root帐号也只是一个帐号,而CPL为0意味着可以做任何事情,包括修改Page
Table或Interrupt Descriptor Table。
LIDT指令可以修改中断向量表的位置,通过这条指令,可让CPU用自定义的中断处理例程来处理中断。这种指令被称为privileged
instruction,只有在CPL=0的情况下才允许执行。CPL\>0情况下执行privileged
instruction,会产生general protection
error。mov指令可修改寄存器值,如果允许用mov指令去修改CR3的值,那么整个进程的Page
Table Table都可能被替换成其它Table,这条指令也是特权指令,需要CPL=0。
中断处理例程用到的栈,也要防止被用户进程修改。read系统调用会把文件内容放到参数指定的内存位置,如果read使用与用户进程相同的栈,那么调用者就有可能把栈的地址传递给read调用,于是read调用执行时就把自己的栈给覆盖掉了,造成不可预料的后果。x86系统中为每个Privilege
Level准备了相应的栈,这四个栈的指针被放在Task State
Segment中。系统调用发生时,栈也会切换到相应Privilege Level对应的栈上。

<h2 id="进入中断例程的过程">进入中断例程的过程</h2>

  1. 确定是否清除Interrupt Enable Flag

中断向量表中每条记录为8字节,其中最高2字节与最低2字节组成32位中断处理例程的地址,中间有4们(40-43)表示Type。Type中与Interrupt
Enable Flag相关的含义如下:

| Type Value | Gate Type | Clear Interrupt Enable Flag |
|------------|----------------|-----------------------------|
| 14 | Interrupt Gate | Yes |
| 15 | Trap Gate | No |

  1. CPL置为0 Confidence in the safety of this step must be based on

confidence in the code that is being jumped to.
IRET指令返回后,CPL会被切换回原来的值。

  1. 切换栈 内核所用ESP保存在task state

segment上。相应于CPL=0的栈会被加载到ESP。原来ESP的值会被推入当前栈中。当CPL变为0时,task
state segment并不会发生切换,我们可以说这是在任务的内部执行Kernel。

  1. 跳转到中断处理例程(ISR)

<h2 id="调度">调度</h2>

System
Timer中断号为0,是优先级最高的中断。这个中断的处理例程会跟踪所有进程的用时,如果若进程out
of
time,它就会设置need<sub>resched标记</sub>。Linux从中断处理例程(包括时间中断)中返回后,会检查need<sub>resched标记</sub>。要是发现need<sub>resched被设置了</sub>,那就会调用schedle()系统调用。决定下一个要运行的进程。

<h1 id="启动 <span class="tag" tag-name="boot"><span class="smallcaps">boot</span></span>">启动 <span class="tag" tag-name="boot"><span class="smallcaps">boot</span></span></h1>

<h2 id="DOS程序">DOS程序</h2>

8086CPU共有四个寄存器可用于地址:BX,BP,SI,DI。如:

example

MOV BX, 1234H
MOV AL, [BX]


可以把1234H位置的内容放进AL寄存器。然而BX,BP,SI,DI都只有16位,而8086的地址是20位。也就是说,地址寄存器只能放得下64K的地址空间。8086的设计者于是提出了segment
register,用于指定base address,每个base
address指向64K内存。8086的段寄存器有:CS,DS,ES,SS。CS寄存器指向的60K被称为CS段(即code
segment)。所有x86处理器都从CS段中取代码。Stack则由SS
Segment指定。80386及以后的CPU还有FS和GS。
可用mov指令修改段寄存器的值,唯一的例外是CS寄存器不能是mov指令的目的地址。不同段间的跳转称为far
jump,段内跳转称为near jump。段寄存器不能直接赋值,下面的代码是错误的:

example

MOV DS, 1234H


正确的做法是:

example

MOV AX, 1234H
MOV DS, AX


在不指定段寄存器的情况下,会有默认段。立即地址和BX寄存器中的地址的默认段寄存器为DS。32位环境下也有默认段,也正是因为有了默认段,Linux才能忽略段的存在。
Linux系统中与外设打交道,必定要通过系统调用,而在实模式下的DOS环境中,程序可直接与外设打交道,系统调用是可选的。

<h3 id="Video Buffer">Video Buffer</h3>

实模式下,程序可以访问所有物理内存,B8000H被称为video
buffer。在大多数文本模式下,video
buffer的内容就是在屏幕上显示的ASCII字符。可以通过修改这段内存的内容来改变文本模式下显示器上显示的文本。B8000H相当于屏幕的左上角的字符,B8001H则是attribute信息。B8002H则是第二个字符,接下来是相应的attribute。顶行共有80个ASCII字符。25行x80列x2字节=4000字节。实际上video
buffer共有4096字节=1000H。

<h3 id="Keyboard Buffer">Keyboard Buffer</h3>

按键按下会触发IRQ#1中断,中断控制器分配给它的中断号是9。DOS的9号中断的处理例程与键盘的I/O端口64H交互后,会把字符放在41EH位置。这个位置上直接来自键盘的字符称为scan
code。中断处理例程会把scan code转换成ASCII字符,再将scan
code和ASCII字符保存在缓存中。 Keyboard
Buffer是循环使用的,所以又称为circular buffer。

<h2 id="从实模式到保护模式">从实模式到保护模式</h2>

CR0的第0位(PE)表示Protection Mode
Enable,PE位置1示CPU进入保护模式。PE位可说是x86中最重要的位了。下面的指令可将PE位置1:

example

MOV EAX, CR0
OR EAX, 1
MOV CR0, EAX


然而,切换到保护模式远非置PE为0这么简单。没有下面的准备工作而直接将PE置1,会导致系统崩溃。

  1. 准备好segment descriptor table 保护模式下,所有的内存引用都与segment

descriptor table有关,尤其是global descriptor table。

  1. 代码要放到新的CS segment

保护模式使用完全不一样的地址系统,这也就意味着指令要从不同的CS
segment中取出。

  1. 准备好新的中断向量表

DOS下的中断向量是4字节的地址,而保护模式下的中断向量表要用8个字节,因此切换到保护模式下就要求有新的中断向量表。

<h3 id="Segment Descriptor Table">Segment Descriptor Table</h3>

从80286开始,段的长度不再是64K,而可以是OS程序员设定的任意长度。Segment由8字节长的segment
descriptor来定义。这些descriptor被放在descriptor table中。descriptor
table与中断表一样是在内存中的。段寄存器中的值不再是8086中的基址,而是指向descriptor
table的指针,于是段寄存器的值也被称为选择子(selector)。
段的长度由descriptor指定,由此可以限定程序在该上段能够访问的地址空间。
保护模式的内存中有以下四块重要的内容: TSS(Task State
Segment),LDTS(Local Descriptor Table),GDT(Global Descriptor
Table),IDT(Interrupt Descriptor Table)。
其中,GDT和IDT不是通过selector来访问的,它们并不称为memory
segment。而TSS和LDT被称为special memory segment。
TSS用于任务切换,任务退出前会把寄存器值保存在TSS中。TSS与普通memory
segment没什么区别,要由segment descriptor规定好base address和limit。
LDTS用来存放任务专有的段描述符(segment descriptor)。
GDT用来存放可被所有任务共享的memory segment的descriptor。
任意时刻只能有唯一的GDT和唯一LDT生效。任务切换时,GDT不变,LDT会和page
table一样切换。 GDT和IDT也有base
address和limit,但这些地址并不会放在某个descriptor上,因此无法通过selector来访问。Intel认为这两个表并不是segment。
Selector即为段寄存器中的值。实模式下,段寄存器的值即为base
address。保护模式下,Selector中的值为descriptor
table中的offset。由于每个descriptor占8字节,因此,offset值的最低三位为0。x86把这三位用来存放额外的信息。最低两位放了privilege
level,第2位则用来表示指向的是global table还是local
table。1=local,0=global。 selector
offset为0的selector在global table中称为null
selector。在段寄存器中放置null selector会禁止该段的使用。由此,global
table的第0项是无法被任意段寄存器指向的。

example

LGDT mem
LIDT mem


其中的mem指向6字节内存,2字节limit和4字节base address。
LDT本身是segment,GDT中有记录指向LDT,LDT则包含了进程所需的段的信息。

<h1 id="模块配置问题">模块配置问题</h1>

<h2 id="ACPI警告问题">ACPI警告问题</h2>

: \[ 5.280422\] ACPI Warning: 0x0000000000000428-0x000000000000042f
SystemIO conflicts with Region (20130117/utaddress-251) : \[ 5.280428\]
ACPI: If an ACPI driver is available for this device, you should use it
instead of the native driver 可能是由 SuperIO sensors or SMBus
controller设备的驱动造成的: blacklist pcspkr blacklist
lpc<sub>ich</sub> blacklist gpio-ich

<h1 id="命名空间(namespace)">命名空间(namespace)</h1>

<h2 id="六大类型">六大类型</h2>

No.1 MNT Namespace 提供磁盘挂载点和文件系统的隔离能力 No.2 IPC Namespace
提供进程间通信的隔离能力 No.3 Net Namespace 提供网络隔离能力 No.4 UTS
Namespace 提供主机名隔离能力 No.5 PID Namespace 提供进程隔离能力 No.6
User Namespace 提供用户隔离能力

<h2 id="MNT Namespace">MNT Namespace</h2>

使用unshare隔离mnt
namespace:(<http://mojijs.com/2016/09/218989/index.html>)
linux已经提供了非常贴心的unshare命令,只需要下面一行语句我们就能重新启动一个bash的进程,而在其中的mnt
namespace是被隔离的。

example

root@ubuntu:~# echo $$
32968
root@ubuntu:~# unshare --mount /bin/bash


好像没有任何变化,其实这个已经不是刚才我们的32968进程了,而是一个新的进程,通过确认$$就能确认
: root@ubuntu:~# echo $$

example

33447


再来确认一下,33447和32968两个进程的关系,我们能清楚地看到这是父子关系的两个进程,虽然都是bash

example

admin01@ubuntu:~$ ps -ef |grep 32968 |grep -v grep
root 32968 32967 0 10:16 pts/0 00:00:00 -su
root 33447 32968 0 11:09 pts/0 00:00:00 /bin/bash
admin01@ubuntu:~$ pstree 32968

``