Go to file
He Chen b48a63ef2c sched: add offline scheduler class
The offline scheduler named BT sched is based on the CFS scheduler. We
also use the rb-tree as the run queue to save the runnable tasks. And the
vruntime concept is also used in the offline scheduler. And the priority
of offline scheduler is from 140 to 179. So now the schedulers in the
kernel are as follows: stop, RT, CFS, BT and idle.

Signed-off-by: Xiaoming Gao <newtongao@tencent.com>
Signed-off-by: Hua Liu <shookliu@tencent.com>
Signed-off-by: Xiaogguang Chen <xiaoggchen@tencent.com>
Signed-off-by: Zhiguang Peng <zgpeng@tencent.com>
Signed-off-by: Bin Fan <tombinfan@tencent.com>
Signed-off-by: He Chen <heddchen@tencent.com>
2020-03-02 22:03:09 +08:00
Documentation ceph: quota: report root dir quota usage in statfs 2020-01-02 12:23:38 +08:00
arch arm64/init: Fix crashkernel parse build error 2020-01-02 12:24:04 +08:00
block blkcg: add per blkcg diskstats 2020-01-02 12:19:49 +08:00
certs Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
crypto Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
drivers virtio-net: lower min ring num_free for efficiency 2020-01-02 14:55:55 +08:00
firmware Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
fs sched: add offline scheduler class 2020-03-02 22:03:09 +08:00
images images: upload README.md images 2020-01-06 10:28:49 +08:00
include sched: add offline scheduler class 2020-03-02 22:03:09 +08:00
init sched: add offline scheduler class 2020-03-02 22:03:09 +08:00
ipc Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
kernel sched: add offline scheduler class 2020-03-02 22:03:09 +08:00
lib xfs: add kmem_alloc_by_vmalloc and kmem_alloc_large_dump_stack sysctl 2020-01-02 11:54:24 +08:00
mm arm64: implement ftrace with regs by using patchable-function-entry 2020-01-02 12:14:57 +08:00
net nsfs: fix netns proc warning 2020-01-02 14:55:31 +08:00
package sched: add offline scheduler class 2020-03-02 22:03:09 +08:00
samples Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
scripts Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
security Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
sound Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
tools perf: disable --children by default 2020-01-02 12:20:39 +08:00
usr Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
virt Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
.gitignore Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
COPYING Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
CREDITS Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
Kbuild Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
Kconfig Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
MAINTAINERS Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
Makefile Makefile: modify EXTRA_VERSION to 19 2020-01-02 12:17:51 +08:00
README Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00
README.md Update README.md 2020-01-06 21:21:35 +08:00
backport_remove_lists.txt Init Repo base on 4.14.105 long term, and add base tlinux kernel interfaces 2020-01-02 10:51:04 +08:00

README.md

img

TencetOS Server kernel

  • TencentOS Server( 又名Tencent Linux 简称Tlinux) 是腾讯针对云的场景研发的 Linux 操作系统提供了专门的功能特性和性能优化为云服务器实例中的应用程序提供高性能且更加安全可靠的运行环境。Tencent Linux 使用免费,在 CentOS及发行版上开发的应用程序可直接在 Tencent Linux 上运行,用户还可持续获得腾讯云的更新维护和技术支持。

支持平台

  • X86: 支持intel, AMD(包括ROME平台)。

  • ARM64: 支持热补丁,虚拟化。

主要特性

特性类型 特性内容
内核定制 基于内核社区长期支持的4.14.105版本定制而成,增加适用于云场景的新特性、改进内核性能并修复重大缺陷
容器支持 针对容器场景进行优化提供了隔离增强和性能优化特性meminfo、vmstat、cpuinfo、stat、loadavg, uptime, diskstats
Sysctl 隔离,如 tcp_no_delay_ack、tcp_max_orphans大量文件系统和网络的 BUGFIX
NVME IO按比例隔离
性能优化 计算、存储和网络子系统均经过优化,包括:优化 xfs 内存分配,解决 xfs kmem_alloc 分配失败告警优化网络收包大内存分配问题,解决 UDP 包量大时,占据过多内存问题限制系统 page cache 占用内存比例,从而避免内存不足影响业务的性能或者 OOM
其他特性 离线调度算法(BT)
进程防gdb
ARM64热补丁
pagecache limit
缺陷支持 提供操作系统崩溃后的 kdump 内核转储能力提供内核的热补丁升级能力
安全更新 Tencent Linux 会定期进行更新,增强安全性及功能

通过源代码编译内核rpm包

  • ARM64环境

    安装gcc 8.2或者以上, 可自行编译gcc或者使用腾讯软件源的rpm包腾讯软件源使用方式参见下面章节。

  • 默认配置文件 package/default/config.default

  • 通过以下步骤编译内核rpm

    git tag 4.14.105-19-0008.beta10
    cd package/defeault
    ./generate-rpms.sh -j  jobs_num
    
  • debuginfo会包含在kernel-debuginfo包里内核vmlinux默认释放到/boot目录模块debuginfo释放到/usr/lib/debug/目录

通过腾讯云获取 TencentOs Server(Tlinux)

腾讯云在 云服务器控制台 提供了 Tencent Linux 公共镜像,您可通过下列方法获取并使用 Tencent Linux。

  • 创建云服务器实例时,选择公共镜像,并选择 Tencent Linux 的相应版本。 操作详情请参见 创建实例
  • 已创建的云服务器实例,可通过重装系统将现有操作系统更换为 Tencent Linux。 操作详情请参见 重装系统

通过腾讯软件源获取TencentOS Server软件安装包

  • 软件源使用简介 TencentOS server使用腾讯软件源作为其yum服务操作系统中已默认配置好软件源如需安装软件只需要使用yum即可在线安装。比如安装httpd执行下面的命令
# yum install httpd

TencentOS server的内核和用户态包的更新也会持续同步至腾讯软件源如需升级系统可执行下面的命令

# yum update

通过SCL源的方式安装使用高版本gcc的命令如下

# yum -y install tlinux-release-scloCentOS 7系统请使用yum -y install centos-release-scl-rh
# yum -y install devtoolset-8-gcc 
# scl enable devtoolset-8 bash

容器隔离增强

由于在docker容器中/proc下数据是通过mount bind host中proc得到的而内核中proc文件系统大部分没有实现namespace功能仅有pid和net实现了namespace因此container中看到的proc诸多统计数据例如/proc/meminfo ,/proc/stat等都是host的全局数据而非container对应的统计数据这会导致用户在container中使用freetop或者资源采集时等得到的是错误的数据。为了修正这个问题使得业务在使用docker容器时可以有途径获得docker自身的统计状态tlinux内核实现了对一些常用数据的隔离。

  • 方案设计

常用的命令free、top、vmstat以及网管agent所涉及到的目前已实现的隔离数据包括/proc/meminfo、/proc/stat、/proc/vmstat、/proc/loadavg、/proc/cpuinfo。

隔离方案如图所示

img

tlinux内核在cgroup的memorycpuset等子系统中分别添加对应的文件输出然后由用户通过mount bind操作将同名文件绑定到container的proc中。Mount bind操作可以在docker启动container的流程中添加。

例如在memeory子系统对应的container目录中添加meminfo和vmstat文件。

img

在cpu子系统对应的container下实现cpuinfostat文件。

img

3. 文件接口说明

目前已实现的隔离文件列表如下:

/sys/fs/cgroup/cpuset/system.slice/docker-[id].scope目录下实现

  • cpuset.stat, 对应/proc/stat

  • cpuset.cpuinfo, 对应/proc/cpuinfo

  • cpuset.loadavg, 对应/proc/loadavg

/sys/fs/cgroup/memory/system.slice/docker-[id].scope目录下实现

  • memory.vmstat, 对应/proc/vmstat
  • memory.meminfo, 对应/proc/meminfo

/sys/fs/cgroup/cpuacct/system.slice/docker-[id].scope目录下实现

  • cpuacct.uptime, 对应/proc/uptime, 包含两个字段,第一个字段为容器启动时间,第二个字段为系统空闲时间在容器启动之后的增量。

/sys/fs/cgroup/blkio/system.slice/docker-[id].scope目录下实现

  • blkio.diskstats, 对应/proc/diskstats

Uptime隔离的实现

  • /proc/uptime只要用于获取系统启动时间以及系统空闲时间目前这一接口内核层尚未做隔离。业务一般通过lxcfs模拟实现lxcfs是通过获取pidns中的init进程的启动时间来确定容器启动时间。为了减低系统的复杂性我们在此对/proc/uptime做了隔离相应的入口为cpuacc.uptime。与/proc/uptime类似的cpuacct.uptime也包含两个字段第一个字段为容器启动时间第二个字段为系统空闲时间在容器启动之后的增量。

  • 容器启动时间统计逻辑比较简单在cpuacct子系统创建的时候记录一下系统启动时间并保存到cpuacct私有数据里面用户cat cpuacct.uptime的时候也记录一下系统启动时间取两者差值为定义为容器启动时间。值得注意的是这里统计到的启动时间是不包含系统休眠时间的。

Diskstats隔离的实现

  • /proc/diskstats用于统计各个块设备的io信息目前内核尚未对这个接口进行隔离。与/proc/uptime类似目前业务大多通过lxcfs进行模拟。Lxcfs实际返回的是blkio子系统的一些统计这里面有个问题blkio子系统的Io统计大多是在cfq调度器里面执行的如果设备未依赖于cfq调度器那么相关的统计将失效。其次blkio部分点的统计时机与disktstats的统计时机不全一致比如io_serviced cfq是在dispatch阶段统计而diskstats则是在驱动完成之后统计的。为了降低系统负载性以及提高兼容性我们对/proc/diskstats接口进行了隔离相应的入口为blkio.diskstats。

  • blkio.diskstats的通过blkcg_diskstats对象统计当前blkcg对特定设备的io量由于单个blkcg可以访问多个设备因此blkcg会维护一个blkcg_diskstats队列。由于实际blkcg_diskstats队列长度较短同时为了提高blkcg_diskstats搜索效率我们设置了一个cache点用于缓存最近命中的blkcg_diskstats对象的地址。Io统计的基本流程是io提交阶段我们会将bio与blkcg进行绑定因为end_of_io函数的运行上下文非提交io进程的上下文因此我们需要通过bio确定相应的blkcg。如果当前bio可以与plug队列设备dispatch队列或者io调度器内部队列的request合并此时进行io_merged的统计Io完成的时候我们对io_sectorsio_serviced, io_wait_time的统计。in_flightio_ticks, time_in_queue这三个字段与物理设备的处理能力相关因此我们不单独进行统计全部填0然后追加了两个字段将母机侧的io_ticks, time_in_queue的值透传到容器里面。值得注意的是blkio.diskstats入口默认是关闭的用户需要通过echo 1 > blkio.diskstats打开方可获取当前cgroup的io统计。基本框架如下所示

img

4. Docker修改

业务如需在docker环境下使用该特性需要自行修改docker添加相关的mount bind操作。

bind方案需要将host中/sys/fs/cgroup下对应container中的统计信息bind到container命名空间的/proc目录下。要做到这一点需要两个条件一是必须在container运行起来之后再挂载因为只有container运行时才会有统计信息输出二需要在host中能够索引到container的根。满足上述两个条件就需要在docker的容器启动流程中修改添加mount bind操作。

Docker容器启动容器根切换的大致流程如下

  1. Docker为container分配一个工作目录即container的rootfs并将container的根设备挂载到这个目录下。

  2. Docker将container的根设备host中的一些全局配置如/etc/host等直接mount bind到container根下对应的设备上其中也包括将主机的/proc挂载到container中这样container中看到的proc信息是host的。

  3. Docker启动container子进程在子进程中执行change root操作如此container中就可以看到独立的根了。

需要在第三步中加入对/proc下指定文件的mount bind操作。加入的时机是子进程启动之后change root之前change root之后子进程将无法访问host的命名空间了。

5. mount点隐藏特性

  • /proc/tkernel/shield_mounts, 通过配置该接口可以隐藏一些不想对容器可见的mount信息, 详细 用法如下:
echo "set 192.168.1.1:/nfs /nfstest " > /proc/tkernel/shield_mounts 
echo "clear 192.168.1.1:/nfs /nfstest " > /proc/tkernel/shield_mounts 

6. iotop支持

  • 因为taskstats不支持net namespace所以iotop不能在容器里正常运行tlinux通过修改taskstats后使得iotop能在容器里正常运行。

内核新增启动参数

  • irq_force_manage 4.14内核默认是不允许用户态修改中断亲和性的完全交给内核来管理tlinux默认修改了该行为使得用户态默认可以修改中断亲和性当irq_force_manage添加时回到4.14内核默认行为,即不想允许用户态修改中断亲和性。

sysctl/proc新增&隔离

proc新增

  • /proc/tkernel/nonpriv_netbind, 通过设置可以允许非特权用户bind 1024以下的端口,用法如下

    echo +80 > /proc/tkernel/nonpriv_netbind
    echo -80 > /proc/tkernel/nonpriv_netbind
    
  • /proc/tkernel/shield_mounts, 通过配置该接口可以隐藏一些不想对容器可见的mount信息, 详细用法参考“ 容器隔离增强”。

  • /proc/tkernel/release_suffix, 如果为1在容器内uname 命令输出会增加"_docker_guest", 方便需要区别容器内核内核名称。

sysctl新增

  1. kernel.watch_host_pid, 当开启时, 在容器里可以通过/proc/$PID/hostinfo来查看容器内进程的容器外pid, 参考 “容器pid映射信息获取”。

  2. kernel.cpuset_cpuinfo_show_realinfo, 当开启时,在容器内的/proc/cpuinfo显示真实的cpu序号, 默认值为0

  3. kernel.print-fatal-signals-src-dst , 当开启时在fatal信号发送处dump出信号和栈方便定位为什么一些异常信号会被发送该值默认关闭。

  4. kernel.min_epoll_wait_time, 避免用户态错误的使用epoll超时时间浪费cpu, 默认值为1 HZ, 设为0可以禁掉该功能。

  5. net.ipv4.tcp_loss_init_cwnd, 控制进入tcp_enter_loss时的初始拥塞窗口默认值为1。

  6. net.ipv4.tcp_no_delay_ack, 开启后可以立即进入quick ack模式默认值为0。 并且已按namespace隔离。

  7. net.ipv4.tcp_init_cwnd, 可以显示设置tcp初始拥塞窗口。

  8. net.ipv4.tcp_rto_min, net.ipv4.tcp_rto_max, 用户态可以显示控制rto最小最大值。

namespace隔离列表

  1. net.ipv4.tcp_max_orphans

  2. net.ipv4.tcp_workaround_signed_windows

  3. net.ipv4.tcp_rmem

  4. net.ipv4.tcp_wmem

  5. vm.max_map_count, 默认值继承父namespace

容器pid映射信息获取

  • /proc/$PID/hostinfo 在kernel.watch_host_pid = 1时 容器内可以通过读取该文件来获取容器内进程在容器外的真实pid。
[root@653a04f37e36 /]# cat /proc/self/hostinfo
Name:   cat
NStgid: 31      11194
NSpid:  31      11194
NSsid:  1       11126

page cache limit特性

需求分析

  • page cache是linux内核的一种文件缓存机制它主要是用来缓存一些磁盘文件。通过把磁盘文件缓存到内存中减少系统对磁盘IO的操作从而提高系统的读写性能。但是过多的文件缓存会占用大量的系统内存可能会导致系统剩余内存不足反而会影响业务的性能或使用。因此在某些场景下我们需要限制page cache的使用率尽量使系统剩余的内存能够满足业务的需求。

  • 现有的linux内核中已有回收page cache的机制进程分配内存时如果系统的剩余内存低于一定值(与/proc/sys/vm/min_free_kbytes相关)时就会进入slow path唤醒kswapd进程回收一些page cache如果kswapd没有回收成功或者回收较慢则进入到direct_reclaim进行深度的回收直到系统剩余内存满足要求如果还是不行那么只能OOM了。这样的机制是否足够了呢

  • 在一些原子操作的场景比如中断上下文中是不能睡眠的因此在这种场景下的内存分配时是不会进入到direct_reclaim环节甚至也不会去唤醒kswapd进程的。比如在网卡收包的软中断处理函数中可能存在因为page cache占用过多系统剩余内存不足无法为收到的数据包分配内存而直接丢包的现象。这也就是page cache limit要解决的问题。

page cache介绍

  • page cache是linux内核实现的一种磁盘文件缓存它主要用来减少对磁盘的IO操作具体地讲是通过把磁盘中的数据缓存到物理内存中把对磁盘的访问变为对物理内存的访问。目的是提高文件的访问性能。那么page cache是什么时候生成的呢保存到哪里呢

  • page cache在系统中的大致位置如下图所示

img

  • 比如当应用程序读取磁盘数据时首先会调用find_get_page()函数来判断系统中是否有对应的page cache如果有而且是有效的最新数据则就会直接返回读取到的数据无需访问磁盘。

  • 如果系统中没有对应的page cache则会调用对应的文件系统触发磁盘读操作。当数据读取到并返回时就会调用add_to_page_cache_lru()将刚刚读取到的数据pages缓存到lru列表中。lru列表会对page cache进行管理根据其访问的活跃程度分为active list和inactive list两个列表管理关于lru的详细管理机制可以参考网络文章。

page cache limit实现分析

  • page cache limit主要就是在应用程序添加page cache时即调用add_to_page_cache_lru函数时来检测page cache是否超过了我们设置的上限(/proc/sys/vm/pagecache_limit_ratio)。

  • 如果检测到page cache超过限额就会调用shrink_page_cache()来回收page cache可见这个函数是page cache limit回收page cache的核心函数。

  • shrink_page_cache()回收page cache直到低于page cache的阈值回收顺序如下

    1. 回收lru inactive_list中的page caches。

    2. 回收lru active_list中的page caches但不回收映射页、匿名页和脏页。

    3. 回收lru active_list中的page caches但不回收映射页、匿名页如果pagecache_limit_ignore_dirty为0会收回脏页。

  • 上面的步骤不一定全部走完只要低于page cache的阈值就会退出回收过程。这里之所以不回收匿名页和映射页是因为匿名页需要swap比较耗时而映射页是只映射到进程的页表里的pages也是不能回收的。

page cache limit功能介绍及使用

  • 我们提供了3个/proc接口分别为

    /proc/sys/vm/pagecache_limit_ratio
    

    默认值为0表示不开启page cache limit功能。通过echo x > /proc/sys/vm/pagecache_limit_ratio0 < x < 100开启page cache limit功能。非0时比如30表示限制page cache占系统总内存的30%

  •  /proc/sys/vm/pagecache_limit_reclaim_ratio
    

    page cache真实回收的比例为了防止频繁的page cache回收抖动我们会多回收一些page cache默认比pagecache_limit_ratio多2%

  •  /proc/sys/vm/pagecache_limit_ignore_dirty
    

    计算page cache占用内存时是否忽略dirty pages。默认值是1表示忽略。因为dirty pages的回收是比较耗时的。

  •  /proc/sys/vm/pagecache_limit_async
    

    page cache的回收方式

    • 为1时表示异步回收page cache这时会创建一个 [kpclimitd]内核线程由这个线程负责page cache的回收。

    • 为0时不会创建任何专用回收线程直接在page cache limit触发的进程上下文回收。

    • 默认值为0表示同步回收。

    • 优缺点对比:

      异步回收的好处就是回收的开销不会发生在业务进程的上下文但不足就是无法准确控制page cache的使用比例仍然可能发生OOM。

      同步回收的好处是可以较为准确的控制page cache的使用比例但可能给业务进程本身带来一定的开销发生时间毛刺。所以还要看具体的业务场景需求而合理的设置。

结论

经过测试已经能够按照用户的设置限制page cache的比例。而且在1Gbit/s速度的IO读写流中page cache limit带来的额外cpu开销低于3%。但减少系统的page cache可能会增加一定的cache miss但一般情况下我们也不会把page cache限制过低这个可以根据业务场景需求进行适当的调整。

热补丁

  • x86, x86热补丁tlinux内核默认合入了kpatch内核模块可以自行选择是用内核自带的livepatch或者kpatch。
  • ARM64, arm64默认不支持热补丁tlinux内核在社区内核的基础上支持了热补丁特性参加下面章节。

ARM64 热补丁

热补丁简介

内核热补丁技术是一种无需重启服务器即可实现修改内核运行时代码的技术。基于该技术可以在不影响业务正常运行的情况下修复内核bug或者安全漏洞以提高运营效率、底层平台的稳定性和可用性并使得业务运营体验有效提升。

img

工作原理

目前不同厂商都推出了自己的热补丁技术包括Ksplice、Kgraft、Kpatch、Livepatch。这几种方案从实现原理上讲大同小异对x64架构都做了充分的验证但一直缺乏对arm64架构的支持。业界也没有相应的解决方案。

随着tlinux内核对外版发布需要我们支持的架构也包含了arm64随着arm64机型的大量部署。arm64热补丁技术需求变得尤为迫切。

由此,我们基于Kpatch框架开发了arm64热补丁特性。

arm64热补丁功能实现包括内核、编译器、用户态工具几部分。

内核态工作原理

kpatch在内核中是基于ftrace实现内核函数的替换类似于ftrace的动态探测点不过不是统计某些运行数据而是修改函数的运行序列在函数运行某些额外的代码之后略过旧函数代码并跳转至新函数。框架如下图所示

img

针对arm64架构整个流程可以细化为下图所示

img

用户态工作原理

img

  1. 通过kernel 源码编译内核
  2. 打上补丁后再次编译内核
  3. 分析两次目标文件的变动情况生成diff.o
  4. diff.o通过包装生成最终的patch.ko
  5. ko文件里面调用内核的接口通过插入该模块完成热补丁部署

内核态改造

使用patchable-function-entry实现类似于fmentry功能使用了GCC 8.2.1版本)

x86机器上

img

x86机器上如果使用-mfentryelf文件中ftrace跳转指令位于prologue前面在由旧函数跳转到新函数后执行指令流程不会出错。如果使用mcount则在新函数前需要添加stub函数用于处理栈信息等。arm64机器上

img

Arm64只支持mcount功能但是arm64 prologue会对寄存器做修改所以无法使用stub函数来适配。所以采用gcc patchable-function-entry来实现类似于mfentry的功能。使用了GCC 8.2.1版本来编译内核rpm包链接地址https://tlinux-mirror.tencent-cloud.com/tlinux/2.4/arm64/tlinux-sclo/aarch64/tl/devtoolset-8/devtoolset-8-gcc-8.2.1-3.tl2.aarch64.rpm

img

ftrace regs 实现

热补丁中涉及到修改regs参数所以在ftrace跳转时需要将寄存器入栈所以针对arm64实现了ftrace with regs功能为热补丁功能做准备。x0 ~ x30入栈操作如下

img

kpatch arm64支持

包括ftrace_ops注册删除、模块载入时数据重定位等功能。 重定位简要代码如下:

img

用户态工具改造

通过前面的用户态工作原理的介绍我们可以看出我们生成diff.o时没法借助编译器的力量需要手动解析

1ELF 文件 ELF 文件分为三类

  • 目标文件obj文件 目标文件仅仅是编译生成的,在目标文件中,有专门的信息来标识没有识别到的符号。 我们在C语言中我们通常用函数来作为代码的组成的最小单位。但是构成obj文件的基本单位是section。不同的section作用也不同。通常我们可以看到的有text sectiondata section等等
  • 可执行文件 可执行文件由多个目标文件链接而成,通过链接,可以确定每个符号在内存中的具体地址。
  • 动态可链接文件so库 内核的实现仅用倒了目标文件和可执行文件。

2、链接过程 目标文件中有一个很重要的section——relocation section。 基本上链接上的重点都是围绕这个section展开的。 relocation section可以看作是一个数组数组中的每一项可以看作一个三元组 addrsymbolfilling_function 这个三元组表达的意思可以用C伪代码来表示 *addr = filling_function(symbol, addr)

通过对各个目标文件进行链接,可以生成最终的可执行文件。链接过程主要解决以下问题:

  1. 整合各个section比如所有obj文件中的text段即可以合并在一起。
  2. 符号决议。整合之后便可以确定每个符号的运行时地址即确定每个obj文件中引用的不在本目标文件中的符号存在。
  3. 确定每个符号的地址并根据relocation section中的每一项完成重定位。

filling_function 在不同的架构下规则不同在arm64架构中主要有以下几种重定位类型

1.    R_AARCH64_ABS64
符号运行时的绝对运行地址,即

*addr = symbol.addr

2.    R_AARCH64_PREL32

*addr = symbol.addr - addr

3.    R_AARCH64_CALL26
该指令为arm br指令重定位
*addr = symbol.addr - addr

4.    R_AARCH64_ADR_PREL_PG_HI21

*addr = Page(symbol.addr) - Page(addr)

5.    R_AARCH64_ADD_ABS_LO12_NC

*addr = symbol.addr[11:0]
//symbol.addr 的 bit 11 ~ bit 0

3、kernel module 明白了链接过程的原理其实大体就可以明白kernel module的工作方式了。 可以简单的说内核把链接器的工作搬到了内核中这样内核就可以加载目标文件了。ko文件的本质就是目标文件只不过加了一些别的section用来描述你的kernel module。当你insmod的时候内核就会解析ko文件中的relocation section帮你决议符号成功后你的代码就可以被运行了。 有一点需要注意的是kernel module虽然把连接工作放到了内核去做但是由于各种开源协议的原因他只会帮你决议通过EXPORT_SYMBOL_XXX导出的符号。

  • 1、找到变动的函数 上面说到一个目标文件是通过不同的section来组成的那么我们可以通过比对对应的段来找到不同。可麻烦的是所有的函数都被编译到text段中我们通过逐个字节比对可以找到text段是否变动但是很难办法找到哪个函数变动。 好在编译器提供了特定的编译选项将每个函数和全局变量单独成立一个section其section name和函数名或者变量名一一对应这样我们就可以轻易找到变动的函数。
  • 2、 生成 arm64 架构的diff.o 通过比对我们可以找到变动的section那么也就能找到对应的函数。剩下的问题是如何生成diff.o。我们需要考虑以下几个问题
    1. 由于生成diff.o不是通过C代码生成的所以我们需要借助一些工具手动构造一个obj文件。 这个问题可以通过libelf来解决利用这个库可以手动生成一个新的obj文件。
    2. 变动的函数可能引入新的全局符号。 新的全局符号可能是新的全局变量或者新的函数那么肯定会产生新的section我们将这些section也一并放到diff.o里面。
    3. 变动的函数会调用系统中已经存在的符号。 我们可以利用relocation section让内核帮我们处理掉他。原理和kernel module加载过程一致。 但是我们需要让他帮我们决议所有的内核符号。

由于上述的分析都是基于分析arm64 的elf文件来实现的所以我们需要对照arm64的elf规范进行实现。

验证

首先需要载入kpatch模块然后载入用户态工具生成的新函数模块。通过lsmod查看模块是否载入成功。同时kpatch提供了sysfs接口可以查看载入新函数模块的信息包括新旧函数地址等。可以通过/sys/kernel/kpatch/xxx/enabled来卸载模块,恢复执行原函数。 简要操作流程如下:

img

进程防gdb功能

进程防gdb功能可以让进程在设置保护后即使是root也不能gdb该进程阻止跨进程获取内存加载动态库等。gdb是通过ptrace系统调用来实现的tlinux内核通过在ptrace调用里增加判断条件实现到阻止进程被gdb的功能ptrace改动如下

 --- a/kernel/ptrace.c
 +++ b/kernel/ptrace.c
 @@ -1118,6 +1118,9 @@ static struct task_struct *ptrace_get_task_struct(pid_t pid)
  #define arch_ptrace_attach(child)      do { } while (0)
  #endif
 
 +int (*ptrace_pre_hook)(long request, long pid, struct task_struct *task, long addr, long data);
 +EXPORT_SYMBOL(ptrace_pre_hook);
 +
  SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
                 unsigned long, data)
  {
 @@ -1136,6 +1139,12 @@ SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
                 ret = PTR_ERR(child);
                 goto out;
         }
 
 +       
 +       if (ptrace_pre_hook) {
 +       ret = ptrace_pre_hook(request, pid, child, addr, data);
 +       if (ret)
 +       goto out_put_task_struct;
 +       }

其中ptrace_pre_hook回调指针在ttools内核模块里赋值实现。ttools模块初始化会创建字符控制设备/dev/ttools, 并且提供两个ioctl来让用户态进程把自身 加入/移除 gdb防护列表。

#define TTOOLS_PTRACE_PROTECT       _IO(TTOOLS_IO, 0x00)
#define TTOOLS_PTRACE_UNPROTECT     _IO(TTOOLS_IO, 0x01)

用户态需要先modprobe ttools加载ttools内核模块然后使用类似以下代码逻辑来将进程加入/移除gdb防护列表, 内核模块ttools实现参见kernel/tkernel/ttools。

//ttoos.h
#ifndef __TTOOLS_H__
#define __TTOOLS_H__

#include <linux/types.h>
#include <linux/ioctl.h>

#define TTOOLS_IO 0xEE

struct ttools_fd_ref {
        int fd;
        long ref_cnt;
};

#define TTOOLS_PTRACE_PROTECT           _IO(TTOOLS_IO, 0x00)
#define TTOOLS_PTRACE_UNPROTECT         _IO(TTOOLS_IO, 0x01)
#define TTOOLS_GET_FD_REFS_CNT          _IOWR(TTOOLS_IO, 0x02, struct ttools_fd_ref)

#endif
//ttools_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "ttools.h"

#define handle_error(msg) \
     do { perror(msg); exit(1); } while (0)

int main()
{
        int fd;
        int ret;
    
        fd = open("/dev/ttools", O_RDONLY); 
        if (fd == -1)
                handle_error("open ttools dev failed");
    
        if (ioctl(fd, TTOOLS_PTRACE_PROTECT, 0))
                handle_error("ioctl protect failed");
    
        sleep(60);
    
        if (ioctl(fd, TTOOLS_PTRACE_UNPROTECT, 0))
                handle_error("ioctl unprotect failed");
        close(fd);
    
        return 0;

}

离线调度算法(BT)

  • 待push, 敬请期待

NVME IO隔离

  • 待push, 敬请期待

联系我们