Linux Kernel
Kernel相关
Basics
说明
最近计划开始看下linux内核相关的内容,是一个相当大的挑战,但是人总要push自己一把,万一自己又行了呢,加油吧,少年!
参考查询文档
- https://makelinux.github.io/kernel/map/
- https://www.kernel.org/
- https://linuxhint.com/browse_linux_kernel_source/
- http://www.treelib.com/book-detail-id-33-aid-2102.html
- M-x ag-files搜索选择cpp
- https://kernelnewbies.org/OPWIntro-RCU 的搜索框
- https://docs.huihoo.com/doxygen/linux/kernel/3.7/ 搜索代码比较方便
- https://elixir.bootlin.com/linux/latest/C/ident/hash_rnd 另一个搜索网站
- 非常推荐 youtube的一个linux相关的视频系列
下载内核源码
- 直接去kernel.html下载即可
- 这里需要注意,other resources里面的资源也可以看看,挺好的
- 右下角的feed建议订阅一下,罗马不是一天建成的
- 2.6版本下载
|
|
我建议可以通过git下载
|
|
编译内核
配置内核选项,我的debian的编译config文件位于/boot/config-xxx
|
|
copy config and make oldconfig
|
|
make
|
|
目录结构
打开了压缩包,发现如下目录结构
|
|
可用看到我们的内核已经非常庞大,但是没事,我们只是了解,并不需要开发,先放松心态
目录结构对应哪些内容(这里参考)
arch: arch(architecture)架构的意思,这里放的是适配x86,arm等平台的代码
|
|
block: block(块设备),用于处理io请求和管理块设备
从/linux-5.12.4/Documentation/block/文档的index.rst里面可用看出来, 我们还看到了deadline bfq等调度算法 .. SPDX-License-Identifier: GPL-2.0
===
Block
===
.. toctree:: :maxdepth: 1
bfq-iosched biodoc biovecs blk-mq capability cmdline-partition data-integrity deadline-iosched inline-encryption ioprio kyber-iosched null_blk pr queue-sysfs request stat switching-sched writeback_cache_control
certs: 定义证书等信息确保模块被正确加载的
crypto: 加密, 里面定义了很多加密算法的实现
Documentation: 文档,里面定义了很多模块的说明文档
|
|
drivers: 驱动, 里面写了很多硬件驱动的实现
|
|
fs: filesystem,文件系统,里面定义了各种fs的实现
include: kernel headers, 要编译的kernel header供其他函数使用
|
|
init: initialization,启动,包含启动内核相关的代码
|
|
ipc: inter-process-communication, 进程间通信,定义进程之间如何通信,比如信号和管道等
kernel: 内核子系统,比如cgroup, debug, irq, coredump等
|
|
libs: 内核经常用到的一些库,比如字符处理,加密,字体,计算等信息
|
|
LICENSES: 内核各个部分代码的LICENSE,GPL and so on.
|
|
mm: memory management and virtual memory, 用于内存和虚拟内存的管理
通过目录结构我们可用发现里面有swap, page等的管理和使用
|
|
net: network stack, 网络协议栈
从目录结构我们可用看出来是网络协议的实现,包含类似802, ipv4, dns等对数据包的管理
|
|
samples: 样例,里面写了一些工具的使用demo
|
|
scripts: 用于构建内核的脚本
|
|
security: Linux Secure Module(LSM), 放置linux安全模块, 里面还有selinux
|
|
sound: 音频系统
tools: 放了一些工具方便做小内核测试
usr: initramfs相关
|
|
virt: 虚拟化相关
|
|
整个内核内部工作流
操作系统,shell,内核的关系
- 对于计算机来讲,硬件是基础,而能让硬件运转起来的带有这些驱动的平台就是操作系统
- 为了更方便管理好所有的硬件软件资源,操作系统需要做常见的内存管理、进程管理、块设备管理等,由于内部写的代码
并不是很好理解,所以提供库函数入口给其他软件层应用使用,而对于系统管理员和其他用户来说,shell就是很好的软件入口
- 对于操作系统,其实包含了包含但不限于用户接口(比如shell)、运行程序(比如C),硬件设备管理,文件系统,各种资源分配,错误检查,各个
模块的信息交流(比如中断),安全等功能
- 而内核是操作系统的核心,即使我们有各种发行版本,但是它们都有一个大脑,内核,而且随着时间推移,一直在更新里面的特性
操作系统启动流程
可以简要概述一下, 分为如下几个步骤
- 按下启动按钮,电源接通
- BIOS自检(检查硬件等情况)
- 读取硬盘第一扇区MBR(512bytes并执行)
- 这一段程序我们通常称为bootstrap program,常见的grub boot loader
- 熟悉grub的配置的知道,里面对应了内核的位置和菜单等选项
- 内核加载并运行第一个进程init
- 内核运行自己的其他程序直至login
内核内部功能实现逻辑(个人理解,待修改)
从目录结构看,include,lib等地方放了常用的功能函数,其他地方比如是开发网络相关模块,就在net下面开发,开发完在 sample写一些例子,开发过程中include上面的功能函数,和自己的功能合并,最后通过内核perf等测试,最后合到分支
从了解网络开始
个人认为网络理解起来没那么抽象,作为第一个部分入门学习比较合适,主要了解内核的工作流和如何看内核,梳理用到的库等信息, 并且这里面会涉及到用哪些工具更快找到所需要的函数定义等信息
目录结构
net目录结构概览
|
|
目录结构看内核
先找网络的入口文件,我觉得是Makefile(linux-5.12.4/net/Makefile), 这里大家自己去看吧 首先发现有 obj-y devres.o socket.o core obj-$(CONFIG_COMPAT) compat.o 其他都是编译选项对应的具体模块目录比如ipv4, netfilter, ethtool等 所以当内核build的时候,其实通过调用各个模块的Makefile就可以把需要的模块包含进kernel-headers和kernel-common 这里我计划先看下ipv4的模块
ipv4的协议实现
|
|
IP协议是网络层协议,所以核心是实现网络层通信的功能
开篇, TCP/IP层makefile,想起了两本大书
obj-y又来了,现在看来y是yes的意思,可以发现obj-y本质上是每个模块的最重要的输出文件,如果要看一个文件的实现,那么看这些C编译好的.o文件就好了 其他仍然是一些编译选项,这里可以看到很多之前内核优化的一些参数, 可以看到如果要了解net里面有啥,从选项里面一目了然,
|
|
route.c肯定是route.o的前身, 所以进入route.c 注释部分:(意思这个文件是实现路由的,啥也没说相当于)
|
|
开头部分大量引入linux和net的.h(header)文件, 这些.h都是包含性的模块列表,相当于入口,因为它里面又包含了一堆include 所以实现net的ipv4 route功能需要用到net自身的库,linux自身的库,这些include都包含在了include文件夹
#define pr_fmt(fmt) "IPv4: " fmt //这里是为了优化日志输出(C语言define http://c.biancheng.net/view/187.html) 看看pr_fmt的实现,grep下发现在include/printk.h里面, C语言已经扔后脑勺了,所以赶紧复习一下, 但是首先看一下注释 大概意思如下:
- pr_fmt可以用来pr_*()这种宏,来打印printk形式的字符串消息
这个fmt就是传到pr_*()里面的字符串参数
- 示例用法就是route.c里面的用法
|
|
这样的定义的好处就是每个pr_*()打印出来的结果会多一个prefix, 为什么呢
|
|
那么先来了解C的语法吧
这个肯定是如果没定义,则定义
C里面define是标识常量的意思 所以这一句#define pr_fmt(fmt) fmt的意思是标识pr_fmt(fmt)用到的地方都变成我们写的一个常量+字符串 看一个我们写的笨拙的例子
|
|
运行结果为(记得gcc ./xxx.o编译一下) liuliancao@liuliancao-dev:~/study/C/kernel$ ./a.out TEST: start
- 这样每次打印我们只需要打印自己的内容,就会自动有模块的开头,减少代码冗余,类似pr_emerg这种也是同理,
减少自定义等级带来的问题
- 如果定义函数,那调用就要分配资源,通过定义宏,并不占内存预编译后就不存在
- #开头的命令是预编译命令
看了route.c前几行定义,发现dst_entry和sk_buff是路由的关键,而对于整个路由操作,对应了这个核心结构体
|
|
在231行还发现一个ops(这个看起来是路由表存放和遍历的地方),同样的udp.c等地方也使用了类似定义
|
|
而我们知道linux一切皆是文件,所以下面还有一段seq对应文件的函数操作集合rt_cache_proc_ops, 从这里可以看出这里是对应route程序的调用部分
|
|
然后又看到写了个rt_cpu_seq_ops
|
|
可以看到通过这三个代码段,实现了程序对于cpu seq部分的处理, 这个部分路由信息应该是最底层和准确的 总结下: 对于rt结构,通过实现rt_cache_seq_ops、rt_proc_seq_ops、rt_cpu_seq_ops实现对于从文件到路由的抽象映射, 这个在其他的类似udp等的数据结构实现中如出一辙
这里取出上一次的bytes然后和现在的计数相加,最后写入seq文件,对应的是256种数据字段
|
|
|
|
可以看出来,net初始化(这里对应路由cache)对应一个pde相当于一个进程在/proc下面,这个column分别是rt_cache的proc_net和proc_net_stat,和rc_acct 其实对应的都是linux下的文
|
|
可以看出来这里面对应命中和源和目的地址等情况 还对应了一个exit(就是删除对应的proc下面的文件)
|
|
可以找一下pde(proc_dir_entry)对应的数据结构(直接在include/linux下面grep下就可以找到)
|
|
- 简单看下,可以看出来这是一个系统进程对于文件的处理入口,里面有很多内核的公共函数,比如proc_create,通过这个,
可以创建指定mode的文件, 同时通过proc_dir_entry把proc_ops也传了过去
- 这里其实一直有个疑问,cpu_proc_ops和cache_proc_ops到底是啥区别,目前知道cache_proc_ops在/proc/net/rt_cache这个一般没有内容,
而cpu_proc_ops在/porc/net/status/rt_cache,初步猜想是cpu里面的是真实的数据,而cache这是做一层缓存,默认都去cache找, cache没有再去cpu这个文件找,带着这个疑问继续看下去吧
路由最重要的就是找邻居,那怎么找,数据存在哪,找的逻辑又是什么呢,看下内核是如何实现的
|
|
看入参: 需要传入一个dst_entry,传入一个sk_buff(目测是一个buffer缓冲段存放),传了一个目的地址
第一行是初始化一个路由表,根据dst_entry, 我们看下rtable的定义(猜一下也在顶部定义的route.h里面哈)
|
|
先来想一个路由表对应了什么,
|
|
可以对照如下, dst对应的是目的地址段 rt_genid是路由的id rt_flags对应flag 这个其实对应路由标志 标识不同的路由形式,比如直连,到一个主机,是否可以使用等 neighbour就是下一跳的地址 此外还放了其他额外信息 路由表的数据结构看完了,这个container_of也需要看下, 我现在还没调试好查询,用grep找下哈 liuliancao@liuliancao-dev:~/projects/linux-5.12.4/include$ cat ../net/ipv4/route.c |grep '.h'|grep include|awk -F '[<>]' '{print $2}'|grep -v '^$'|xargs -i grep -H container_of {} 可以看到在linux/kernel.h, ok,打开看看
|
|
这里看出来如果member或者ptr是空指针则返回msg mismatch 而container_of函数本质上是为了初始化struct,并且顺便判断是否是空指针等信息, 其他暂时不深究
ipv4_neigh_lookup还看到有一个rcu_read锁,看看这个锁对应了什么 在首行注释可以看到,RCU(Read-Copy Update)是读写更新控制,注释还有一些文档对于想了解的可以看看
|
|
请仔细读一遍注释
|
|
- rcu_read_lock首行注释
- 介绍了synchronize_rcu函数 当synchronize_rcu在某个cpu上调用时候,如果其他cpu有在RCU-read状态的,那么使用这个函数就保证
函数会等待到其他rcu-read状态都退出的时候才会继续进行,在此之前会block
- call_cpu类似,注册并等到所有cpu的rcu-read状态都退出时候会收到一个callback,来调用cpu
第二行注释
- rcu callback 存在这种可能
a)cpu0进入rcu read状态; b)cpu1触发call_rcu, 申请callback; c)cpu0退出rcu read状态; d)cpu2进入rcu read状态; e)rcu callback被触发; 这里的解释是因为read端程序一般是并发的,所以cpu2可能会早于cpu1进入rcu read状态 也可能有这种情况,在rcu callback被调用之前,可能有其他的rcu callback已经被释放 问题: 在别的callback触发(cpu2),这个callback(cpu1)也可能会触发,触发就会进入触发函数,我猜测 是在这里面进行额外的控制,否则和第一行注释冲突
- 第三行注释
rcu read可以嵌套,这个比较好理解,嵌套的底层解锁后上层才会解锁
- 第四行
可以不看第五行,但是要知道如下规则: 不要在rcu_read_lock()函数后放置任何导致block !PREEMTION kernel的代码段, 我觉得这里 是因为一旦放了阻塞函数,rcu_read_unlock将无法获得预知的效果,而且认为非抢占式系统中这个是多此一举
- 第五行
preempt(抢占) 这在内核是一个编译选项CONFIG_PREEMPTION 在非抢占rcu声明情况(pure TREE_RCU and TINY_RCU), 在RCU read端代码做block是不合法的 在抢占式rcu声明情况下,rcu端代码有可能会被抢占,但是显式的block式非法的 在抢占且实时的系统内核中,rcu read只有当需要获取受优先级约束的自旋锁时候会被抢占并block
- 第六行
所以没有rcu_write_block(), 因为没有办法去对rcu reader进行解锁,这是一个特性,不是一个bug 这个特性式rcu实现带来的好处 当然写方必须要注意互相协调,spinlock原语可以很好的解决这种情况,但是任何其他的技术也可以被使用 rcu并不关心有多少writer在外面
- rcu_read_lock_lockbh相当于互斥增加软中断的部分,而cu_read_lock_sched则增加禁止抢占的部分
自旋锁,锁如其名,当申请锁后,在那自己旋转,等待锁的释放 在使用自旋锁的时候,会禁止内核抢占,并且禁止同cpu上的中断调用,具体可以看看文章
rcu的原理是啥,如何做到lock的行为, 从rcu_read_lock函数内容入手
|
|
所以在非抢占模式下,__rcu_read_lock()啥也不干,而抢占模式下,__rcu_read_lock()让程序禁止抢占 而__acquire让RCU置于某一个值方便后面进行控制, 这里看非sparse check的时候是返回0 可以看到__acquire本质上是通过linux库文件lockdep.h的lock_acquire实现对map的控制, lock_release是实现释放 RCU_LOCKDEP_WARN是如果配置了CONFIG_PROVE_RCU参数,则会有warnning相关内容, 否则啥也不干
- rcu并不是锁,而是一种同步机制,也没有锁竞争
- rcu update分为removal和reclaimation
removal相当于copy副本,并把读的指针指向它,这样读端可以安全并发 reclaimation相当于回收,当所有的读都读完释放没有引用时候,就释放这些空间 引用博客: 所以典型的RCU update顺序如下三步:
- 移去一个数据结构的指针(s),以使后到的readers不能得到它的引用。
- 等待所有已经进行的readers完成RCU读侧(端)临界区。
- 在某一时刻,没有任何readers保留数据结构的(旧)引用,此时就可以安全地reclaim。
在route.c里面其实是使用了很多库文件rcuupdate.h, compiler_types.h, lockdep.h实现基础的资源控制
|
|
可以看到ip_neigh_gw4入参是设备加一个32位的ip地址网关,通过arp获取设备和地址信息 结果类似于ip r get xxxx的输出,通过5个参数的hash映射和表对比,最终得到目的地址,需要网关,arp信息等等 2个参数到__ipv4_neigh_lookup_noref里面变成5个参数,这个5个参数的意思首先要弄明白
参数名 | 意义 | 定义 |
---|---|---|
&arp_tb1 | 一个arp表,用于hash查询 | e x t e rn struct neigh_t able arp_tb1 (具体见代码段最后) |
neigh_key_eq32 | 每个nb存的时候有pk,这个在比对的时候很重要 | 见代码段最后,是一个bool值,标记neighbour的pk和传入的pk是否相等 |
hash_rnd | 一个随机32位hash种子,用于防止数据泄露和提供冗余能力 | Random seed to fold into hash |
arp_hashfn | 通过关键字, 设备网关,hash种子计算的唯一匹配标识 | 见上代码段 |
&key | 搜索关键字 | 见代码段 |
dev | 设备网关号 | 见代码段 |
当次查询生成对应的hashfn放到路由表里面,也就是下一次查询的时候,会遍历路由表,查到返回,查不到就写入 create的逻辑也需要了解
Process Management
websites
- kernel mod ul e programming