Linux内核管理和编译初步

现在让我们一起来看看这只企鹅到底是个什么吧

内核管理初步

之前的Linux系统基本原理我们说过, Linux是单内核体系设计, 但它充分借鉴了微内核体系的优点, 引入了模块化的机制.

内核的组成部分有这些:

  • kernel: 内核核心, 一般为bzImage的映像文件, 放在/boot下, 名称为vmlinuz;
  • kernel object: 内核对象, 一般放置与/lib/mudule/VERSION-RELEASE/下

对于内核对象我们有三种选择:

  • [ ]: 不选择此功能

  • [M]: 编译成模块

  • [*]: 编译进内核核心

要注意的是, 不是所有的内核代码都可以抽成模块的, 正常情况下, 内核核心自己有自己的框架结构, 压缩之后大小应该为1M左右, 而CentOS默认的内核已经有4M了, 这就说明已经将一些功能编译进了内核. 同样有些东西是不支持的模块化的, 对于这些功能来说就只有两种结果: 编译进内核和不编译进去.

为了内核能够在启动时加载真正的根文件系统我们 还需要一些辅助的文件: ramdisk. (分成两种格式: initrd, initramfs) 这些是我们站在静态的角度看到的情况.

而在系统运行起来的时候, 我们正在运行的内核是已经被加载到内存中的, 相当于是才能够磁盘上的文件复制的一个副本. 此时如果查看版本,就使用那个uname或者直接cat /proc/version就行.这个地方了解的仅仅是内核核心, 而我们更关注的应该是内核的模块:

之前说过一个命令可以列出已经装载使用哪些模块, 这个就是lsmod指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@WWW ~]$ lsmod
Module Size Used by
autofs4 26888 3
sunrpc 243758 1
bnx2fc 120526 0
cnic 53443 1 bnx2fc
uio 10974 1 cnic
fcoe 20748 0
libfcoe 39661 2 bnx2fc,fcoe
libfc 105924 3 bnx2fc,fcoe,libfcoe
scsi_transport_fc 52241 3 bnx2fc,fcoe,libfc
scsi_tgt 12173 1 scsi_transport_fc
8021q 23575 0
garp 7344 1 8021q
stp 2173 1 garp
llc 5642 2 garp,stp
ipt_REJECT 2383 2
nf_conntrack_ipv4 9506 2
nf_defrag_ipv4 1483 1 nf_conntrack_ipv4
iptable_filter 2793 1
ip_tables 17831 1 iptable_filter
...(omitted)

这个命令其实读取的是/proc/modules这个文件的内容. 这不过可读性太差了所以对结果进行了封装.

单独查看某一个模块的详细信息使用modinfo:

1
2
3
4
5
6
7
8
[root@WWW ~]$ modinfo ext4
filename: /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/ext4/ext4.ko
license: GPL
description: Fourth Extended Filesystem
author: Remy Card, Stephen Tweedie, Andrew Morton, Andreas Dilger, Theodore Ts'o and others
srcversion: A80608676B83D55514B450E
depends: mbcache,jbd2
vermagic: 2.6.32-220.el6.x86_64 SMP mod_unload modversions

modinfo除了查看已经加载使用的内核外, 也支持查看还没有使用的内核(当然前提是你已经安装了).

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@WWW ~]$ lsmod | grep xfs
[root@WWW ~]$ modinfo xfs
filename: /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/xfs/xfs.ko
license: GPL
description: SGI XFS with ACLs, security attributes, large block/inode numbers, no debug enabled
author: Silicon Graphics, Inc.
srcversion: 004A543AFBA52F9C2C6AF51
depends: exportfs
vermagic: 2.6.32-220.el6.x86_64 SMP mod_unload modversions
# 一般我们不需要这么多信息, 更多其实是路径比较重要
# 所以可以加上 -n 参数, 让modinfo仅输出路径
[root@WWW ~]$ modinfo -n xfs
/lib/modules/2.6.32-220.el6.x86_64/kernel/fs/xfs/xfs.ko

之所以能查到, 并且速度这么快显然不是访问的磁盘, 其实modinfo是访问的数据库.

我们说过Linux是支持模块的动态改变的, 所以我们卸载和加载.Warning:: 一定不要把正在使用的模块或者一些基础模块给卸载了否则会出大麻烦的.

支持的命令是: modprobe

1
2
3
4
5
6
[root@WWW ~]$ modprobe [ modulename ] [ module parameters... ] # 加载模块, 还可以带上参数
[root@WWW ~]$ modprobe [ -r ] [ -n ] [ modulename... ]
-r remove 移除一个模块
-n --dry-run 测试运行
-q quiet
-v verbose

默认不加参数的时候, 会自动从/etc/modprobe.conf读取获得.先来做个实验:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]$ lsmod | grep xfs
[root@localhost ~]$ modprobe -v xfs
insmod /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/exportfs/exportfs.ko
insmod /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/xfs/xfs.ko
[root@localhost ~]$ lsmod | grep xfs
xfs 1042093 0
exportfs 4236 1 xfs
[root@localhost ~]$ modprobe -rv xfs
rmmod /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/xfs/xfs.ko
rmmod /lib/modules/2.6.32-220.el6.x86_64/kernel/fs/exportfs/exportfs.ko
[root@localhost ~]$ lsmod | grep xfs

很容易吧.

我们说过在模块之间是存在依赖关系的, 这些都被存在modules.dep里面, 但是我们几乎不会使用到他, 更多的是使用的已经编译之后的二进制文件 – modules.dep.bin. 这个文件损坏了也没关系, 会有工具来帮我们生成回来.

1
2
3
4
5
[root@localhost 2.6.32-220.el6.x86_64]$ ls -l
...(omitted)
-rw-r--r--. 1 root root 191785 Jul 11 17:08 modules.dep
-rw-r--r--. 1 root root 280509 Jul 11 17:08 modules.dep.bin
...(omitted)

depmod 这个命令不仅可以帮我们生成module.dep还可以生成我们在说grub中见到的System.map-2.6.32-220.el6.x86_64这样的文件. 说的标准点, depmod是内核模块依赖关系文件及系统信息映射文件的生成工具.在大多数情况下, 我们都无需手动调用.

不知道你有没有注意到, 在上面进行modprobe的试验演示的时候, 分别出现了insmodrmmod.

这个其实才是真正的加载模块和卸载模块的命令, 不同的地方在于, 这两个命令仅仅在乎加载和卸载, 而不在意依赖关系, 这些命令的关系就有点类似是yum/apt和rpm/dpkg的关系. 不仅如此, 你也注意到了吧, 在执行insmod和rmmod的时候 后面跟上的参数是完整的绝对路径文件名. 其实也只有加载的时候麻烦点. 卸载时可以直接写模块名, 但是依然的, 不能解决依赖问题.

/proc目录是内核的状态映射输出, 内核有很多状态信息和可设置的参数, 这些就通过这么一个伪文件系统.有关/proc的信息在Linux系统-服务管理学习笔记中提到了一些.

内核的参数分成两种: 只读的和可写的. 只读的也就是那些输出的状态信息, 而可写的就是提供了设置参数, 来实现对内核某功能或特性的配置.

一个控制的重要命令就是sysctl. 可用来查看和设定/proc下的诸多参数(内核参数) 我们不能通过直接修改文件的方式来进行修改, 但是还可以通过echo重定向来修改大部分参数的值.

当我们想要进行某个属性修改的时候, 就: sysctl -w path.to.parameter 或者 echo "VALUE" > path/to/parameter 在使用sysctl -w 路径的开头就直接是/proc/sys/了.

还是举个例子吧.

1
2
3
4
5
6
7
8
9
10
[root@WWW ~]$ uname -n
WWW
[root@WWW ~]$ echo "TestHost" > /proc/sys/kernel/hostname
[root@WWW ~]$ hostname
TestHost
[root@WWW ~]$ sysctl -w kernel.hostname=Test
kernel.hostname = Test
[root@WWW ~]$ hostname
Test
[root@WWW ~]$ sysctl kernel.hostname # 直接加上Key就可以查看对应的Value了

那么现在我想知道一共有哪些可以配置的选项怎么办?直接使用sysctl -a就行 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
[root@WWW ~]$ sysctl -a
kernel.printk_ratelimit_burst = 10
...
kernel.hung_task_timeout_secs = 120
kernel.keys.maxbytes = 20000
...
kernel.shmmni = 4096
kernel.msgmax = 65536
kernel.msgmni = 1985
kernel.msgmnb = 65536
kernel.sem = 250 32000 32 128
(....)
vm.hugepages_treat_as_movable = 0
vm.nr_overcommit_hugepages = 0
vm.lowmem_reserve_ratio = 256 256 32
vm.drop_caches = 0
(....)
fs.inode-nr = 27355 31
fs.inode-state = 27355 31 0 0 0 0 0
fs.file-nr = 928 0 98253
fs.file-max = 98253
(....)
fs.suid_dumpable = 0
fs.binfmt_misc.status = enabled
fs.quota.reads = 0
fs.quota.writes = 0
(...)
dev.raid.speed_limit_max = 200000
dev.hpet.max-user-freq = 64
dev.mac_hid.mouse_button_emulation = 0
(...)
dev.cdrom.autoeject = 0
dev.cdrom.debug = 0
dev.cdrom.lock = 1
dev.cdrom.check_media = 0
net.netfilter.nf_log.0 = NONE
(...)
net.netfilter.nf_conntrack_max = 31900
net.netfilter.nf_conntrack_log_invalid = 0
net.netfilter.nf_conntrack_expect_max = 128
(....)
net.core.rps_sock_flow_entries = 0
net.core.netdev_budget = 300
net.core.warnings = 1
net.ipv4.route.gc_thresh = 32768
net.ipv4.route.max_size = 524288
(....)
net.ipv4.ip_forward = 0
net.ipv4.xfrm4_gc_thresh = 262144
net.ipv4.ipfrag_high_thresh = 262144
(....)
net.ipv4.icmp_ratelimit = 1000
net.ipv4.icmp_ratemask = 6168
net.ipv6.neigh.default.mcast_solicit = 3
net.ipv6.neigh.default.ucast_solicit = 3
(....)
net.ipv6.neigh.lo.mcast_solicit = 3
net.ipv6.neigh.lo.ucast_solicit = 3
net.ipv6.neigh.lo.app_solicit = 0
(....)
net.ipv6.bindv6only = 0
net.ipv6.ip6frag_secret_interval = 600
net.ipv6.mld_max_msf = 64
net.nf_conntrack_max = 31900
net.unix.max_dgram_qlen = 10
abi.vsyscall32 = 1
crypto.fips_enabled = 0
(....)
sunrpc.min_resvport = 665
sunrpc.max_resvport = 1023
sunrpc.tcp_fin_timeout = 15

好多, 我们直接wc来看一下吧:

1
2
[root@WWW ~]$ sysctl -a | wc -l
696

sysctl有个配置文件, 就是/etc/sysctl.conf 通过这个配置文件也可来进行内核参数的修改. 说道这里就要提一句了, 用之前的方式(sysctl -w和echo都是进行的当前修改, 一旦重新启动就会sysctl重新读取配置文件, 之前所作的修改就没有了), 那么使得修改持久化的方式显然易见: 修改配置文件.

直接进入配置文件修改值之后要主带调用一次sysctl -p, 才可以读取 .(这个地方可以联想bash的配置, 修改后不会立即向生效而是要调用一次source/.)

e.g: 配置内核中的路由转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 路由转发的功能就是net.ipv4.ip_forward这个Key, 现在编辑sysctl.conf来把它开启吧
[root@WWW ~]$ head -3 /etc/sysctl.conf
# Kernel sysctl configuration file for Red Hat Linux
# Controls IP packet forwarding
net.ipv4.ip_forward = 0
# vim编辑保存之后
[root@WWW ~]$ head -3 /etc/sysctl.conf
# Kernel sysctl configuration file for Red Hat Linux
# Controls IP packet forwarding
net.ipv4.ip_forward = 1
[root@WWW ~]$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
# 没有变化?重新读取一次配置文件!
[root@WWW ~]$ sysctl -p
net.ipv4.ip_forward = 1 # 值得到了更新!
[root@WWW ~]$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

再举一个例子咯, 我们都知道Linux使用内存来吃充当缓存和缓冲, 这个也是可以关掉的.

位置就在vm.drop_cache这个地方, 将他的值设置成1就行了. 接着在使用free就会发现buffer的值已经是0了.

先前介绍过了/proc/*, 现在再让我们来看看/sys这个虚拟目录.

/sys

不同于/proc, /sys用来输出硬件相关参数, 包括生产厂商信息, ROM大小, 序列号等等… 也有内核对硬件特性的设定信息, 有些参数可以进行修改, 用于调整硬件的工作特性.

现在我们先说点东西: 我们都知道/dev是用来输出设备文件的对吗, 那么我们当前主机是怎么知道怎么创建那些设备文件呢? 内核启动时, 从各个硬件的IO端口 以及这些硬件自己的电压输出的信息就能知道. 这是内核, 但是内核是不可能创建设备文件, 他根本就不需要这些东西哇, 内核读取硬件可以直接访问, 只有用户空间需要借助这些设备文件, 仅此而已. 这意味着内核根本不会创建这些设备文件.在原先的2.4版的内核上, 这个/dev/内的设备文件是实现存在的, 无论你是否使用. 这个版本的/dev下有近两万个文件. 但实际上我们使用到的, 也不过区区几十个. 所以后来内核就使用一种更精巧的方式: 使用/sys目录.

在2.6版本的内核上 , 我们的设备文件是这样创建的: 内核启动时探测硬件设备, 探测完之后该启动的启动, 当用户空间需要调用某个硬件的时候, 就会触发一个内建机制通知内核探测一遍,并且将探测完的结果输出到sys目录(这个时候用户空间 已经是存在的, 在内核第一次探测硬件的时候用户空间并没有启动输出也不知道往哪里输出) 这样当我们想要创建设备文件的时候, 就会通过udev这样的命令去读取sys内的内容来按需创建/dev/*.

通过sys下的输出信息来动态的对各设备创建所需要的设备文件, udev是运行在用户空间的程序, 所以也不会直接和内核打交道.他也不能自己产生硬件, 内核扫描一遍之后就停止了, 所以只能再次探测一遍.

udev在进行设备文件的时候, 创建的设备文件实际上是这个设备的驱动, 而驱动的名字是根据创建规则来新建的.这些规则文件一般在: /etc/udev/rules.d && /usr/lib/udev/rules.d (但是我找了半天试了Ubuntu/CentOS/VMWare&VM Box 都没有发现类似重命名网卡规则的规则文件, 以后再研究.)

那么现在就说一下ramdisk的制作, 在说grub的时候我们就说过了这个东西. 使用的指令依然是二次封装过后的.但他很好用:

1
[root@WWW ~]$ mkinitrd /boot/initramfs-$(uname -r).img $(uname -r)

如果你的/boot下已经有同名的文件了, 那么就会收到一个提示, 和一个广播:

1
2
3
4
Will not override existing initramfs (/boot/initramfs-3.10.0-514.el7.x86_64.img) without --force

Broadcast message from systemd-journald@localhost.localdomain (Thu 2017-07-13 13:41:11 EDT):
dracut[2420]: Will not override existing initramfs (/boot/initramfs-3.10.0-514.el7.x86_64.img) without --force

这个mkinitrd 封装的操作就是使用了dracut命令.

这个命令有很多参数但是同样, 我们并不会用到他们 可以直接将mklinitrd换成dracut 基本上效果是一样的.必要的时候也就是使用了with-module这样的功能, 不过真的不多.

现在我们来仔细看一下initramfs这个img的真实内容吧!

1
2
[root@localhost ~]$ file initramfs-3.10.0-514.el7.x86_64.img 
initramfs-3.10.0-514.el7.x86_64.img: ASCII cpio archive (SVR4 with no CRC)

是一个cpio的文档, 那么现在就把它解开来看一下:

CPIO的使用:

CPIO是一个近乎全能的备份工具., 因为他可以备份所有的文件, 包括这个/dev下的任何设备文件.使用起来有三种模式, 分别是压缩(备份), 还原, 查看. 下面简单的介绍一下:

**压缩:cpio -ovcB > [file|device] **

还原:cpio -ivcdu < [file|device]

查看:cpio -ivct < [file|device]

参数太多记不住吗? 其实这些组合都已经是套装了. 可以直接使用…不过还是要了解下各个参数是什么意思吧. 首先要明确的是使用cpio必须出现这四个参数中的其中一个: o i p t .

1
2
3
4
5
6
7
8
9
-o 将数据 cOpy 到文件或者硬盘上
-i 将数据自文件或设备复制到系统当中
-p copy-pass模式
-t 配合-i参数, 查看新建的文件或设备内容
-c 使用较新的portable format方式存储
-B 可以使得大文件的速度加快, 因为这个选项将block的大小512 -> 5120
-d 自动新建目录
-u 使用较新的文件覆盖旧的
-v verbose, 不用说了

使用CPIO来进行还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@WWW ~]$ cpio -ivcd < initramfs-2.6.32-220.el6.x86_64.img
...(omitted)
[root@WWW ~]$ ls -l
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 bin
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 cmdline
drwxr-xr-x. 3 root root 4096 Jul 14 00:00 dev
-rw-r--r--. 1 root root 19 Jul 14 00:00 dracut-004-256.el6
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 emergency
drwxr-xr-x. 7 root root 4096 Jul 14 00:00 etc
-rwxr-xr-x. 1 root root 8874 Jul 14 00:00 init
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 initqueue
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 initqueue-finished
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 initqueue-settled
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 initqueue-timeout
-rw-r--r--. 1 root root 45508096 Jul 13 23:57 initramfs-2.6.32-220.el6.x86_64.img
drwxr-xr-x. 7 root root 4096 Jul 14 00:00 lib
drwxr-xr-x. 4 root root 4096 Jul 14 00:00 lib64
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 mount
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 pre-pivot
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 pre-trigger
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 pre-udev
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 proc
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 sbin
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 sys
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 sysroot
drwxr-xr-x. 2 root root 4096 Jul 14 00:00 tmp
drwxr-xr-x. 7 root root 4096 Jul 14 00:00 usr
drwxr-xr-x. 4 root root 4096 Jul 14 00:00 var

目录文件是否很熟悉呢?这些就是熟悉的根目录fs.在sbin里也都是最基本需要的程序.像lvm和raid的支持也在这里呀~这里面还有一个叫switch_root 这个就是在帮助我们做根切换的!

编译内核

编译内核的步骤分这么几步:

  • 准备开发环境
  • 获取主机的硬件设备相关信息
  • 获取到主机上系统功能的相关信息(要启用的文件系统等等)
  • 获取内核源代码包

一步一步的来说吧:

准备开发环境

在说yum的时候, 我们提到过包组的概念, 由一些专门负责系统开发的库, 就CentOS6而言, 有:

  • Server Platform Development
  • Development tools

获取硬件信息

1
2
3
4
5
6
7
8
9
10
11
# 1.获取CPU信息
[root@WWW ~]$ cat /proc/cpuinfo
[root@WWW ~]$ lscpu
# 2. 获取PCI设备
[root@WWW ~]$ lspci # 查看PCI桥
-v verbose
-vv more verbose
[root@WWW ~]$ lsusb # 查看USB
-v verbose
-vv more verbose
[root@WWW ~]$ lsblk # 查看块设备

有一个可以用来查看所有的硬件, 这个命令就是hal-device .

获取系统信息

/boot/下有叫做config-VERSION的文件, 这个就是一个系统信息的模板, 在这里也推荐使用模板, 当然模板也是有兼容性的. 也即是后面的版本号.怎么进行修改, 等到我们获取到内核源代码之后再说.

获取内核源代码

内核的源码包的官方下载地址就是kernel.org , 在这上面直接Down想要的版本内核就行了, 注意如果使用wget下载, 需要跳过证书验证.

好了, 现在就可以开始编译内核了!

我们把内核的源码包进行解压, 解压到./usr/src下, 接着就可以进去看一下了, 首先我们说过要加入配置对嘛, 所以:

1
[root@WWW ~]$ cp -a /boot/config-2.6.32-220.el6.x86_64  .config

接下来就是执行第一步, 开始进行配置(config), 刚刚说过了我们不能直接就拿来用还是要调用内核源码提供的接口进行配置:

1
[root@WWW ~]$ make menuconfig

在执行这一步的时候, 会先尝试从当前目录寻找.config文件, 这个就是通过这个命令配置的产物, 如果没有找到, 会尝试从/boot/下找config-VERSION, 如果找到了就会加载它作为默认的配置. 如果还是没有, 就会根据当前系统的架构尝试从自己的arch下寻找相配的默认配置. 但现在我们把这个配置文件已经加载进来了.

Kernel_config

根据具体需求来进行配置之后就可以开始进行安装啦! 但是, 先别急着安装! 想一个问题先, 在我们安装时, 网络会断开 那么我们session断掉了, 整个安装进程也就直接结束了. 那岂不是就陷入了死循环了吗. 所以我们可以考虑使用screen这么一个工具. 他可以创建一个虚拟的屏幕, 使得任务只要虚拟机不关机, 就不会终止.

好了! 开始编译吧! 编译的时候可以根据自己机器的情况加上-j参数来加快速度.

编译了两个多小时…大家注意啊..(当然也许和我的电脑配置有关…) 如果不是必要的话就不要加进去这么多功能啊…10G的硬盘, 光编译个内核就6个G了,我都没怎么选就进行make了, 所以说在进行上面的配置的时候要多花点时间来筛选啊.

(编译到大半夜开始继续码字的我) 编译结束之后就可以开始安装了, 在执行常规的make install之前 要先将内核进行模块的安装.

模块安装以及系统安装(会自动写入GRUB引导文件)

1
2
[root@WWW ~]$ make modules_install
[root@WWW ~]$ make install