嵌入式Linux 系统启动优化的那些事儿
嵌入式Linux 系统优化的那些儿事之系统启动时间的优化方法。。
嵌入式Linux 系统时间测量工具以及用法
- Printk Times – 用于显示每个 printk 的执行时间
-
- 配置
CONFIG_PRINTK_TIME
Kernel hacking –> Show timing information on printks 结果
dmesg > boot.log
[
3.038027] Memory: 3154176k/3325940k available …
[
3.042366] SLUB: Genslabs=13, HWalign=32, Order=0-3, MinObjects=0, CPUs=32, Nodes=1
[
3.050169] Hierarchical RCU implementation.
[
3.050558] RCU-based detection of stalled CPUs is enabled.
[
3.066428] console [ttyS0] enabled, bootconsole disabled
[
3.066965] Calibrating delay loop… 166.40 BogoMIPS (lpj=332800)分析
scripts/show_delta boot.log | cut -d’ ’ -f2- | sort -k3 -gr
< 3.179683 >] 0000:01:00.0: eth0: (PCI Express:2.5GB/s:Width x1) …
< 1.834118 >] Initializing cgroup subsys cpuset
< 1.555749 >] IP-Config: Complete:
< 0.463878 >] Memory: 3154176k/3325940k available …
< 0.119374 >] Initrd not found or empty - disabling initrd
< 0.111845 >] Serial: 8250/16550 driver, 2 ports, IRQ sharing disabled避免丢失内核日志: CONFIG_LOG_BUF_SHIFT=21
-
在printk中打印时间戳:kernel/printk.c: vprintk()
if (printk_time) {
...
t = cpu_clock(printk_cpu);
nanosec_rem = do_div(t, 1000000000);
tlen = sprintf(tbuf, "[%5lu.%06lu] ",
(unsigned long) t,
nanosec_rem / 1000);
...
} cpu_clock()调用sched_clock()
- 时间单位us,但有些平台精度只有10ms
-
默认实现:直接通过jiffies转换
如果HZ=100,jiffies单位为1/HZ, 即10ms
kernel/sched_clock.c: __weak sched_clock()
return (unsigned long long)(jiffies - INITIAL_JIFFIES) * (NSEC_PER_SEC / HZ); -
高精度sched_clock():读硬件时钟计数器,如果时钟频率为400M,精度达2.5ns
arch/x86/kernel/tsc.c: native_sched_clock()
/* read the Time Stamp Counter: */
rdtscll(this_offset);
/* return the value in ns */
return __cycles_2_ns(this_offset); 潜在问题:计数器溢出?include/linux/cnt32_to_63.h
- 相关信息:http://elinux.org/Printk_Times
- 配置
内核函数跟踪(Ftrace) – 用于报告内核中每个函数的调用时间。详细用法见Ftrace 简介
- Linux 跟踪工具箱(LTT) – 用于报告确切的内核和进程事件的时间数据。
- Oprofile(译注:最新替代品是 perf) – 通用的 Linux 分析器(Profile) - 详细用法见间Oprofile
- Bootchart – 用于 Linux 启动过程的性能分析和数据展示。收集启动过程中的用户空间部分的资源使用情况和进程信息,然后渲染成 PNG、SVG 或者 EPS 格式的图表。
- Bootprobe – 一组用于分析系统启动过程的 System Tap 脚本
- 当然,别忘了 cat /proc/uptime (译注:统计系统已经运行的时间)
- grabserial – Tim Bird (译注:CE Linux Forum 主席)写的一个非常赞的工具用于记录控制台输出并打上时间戳
- 进程跟踪 –- 同样是 Tim Bird 写的一个简单补丁,用于记录 exec、fork 和 exit 系统调用。
- ptx_ts – Pengutronix 的时间戳记录器(TimeStamper):一个简单的过滤器,可前置时间戳到标准输出(STDOUT)上,有点像 grabserial 但是不限于串口。
- Initcall(内核初始化函数)调试 – 一个用于显示 initcalls 所花时间的内核命令行选项
- 也可以看下: Kernel 检测工具,里头列举了一些已知的内核检测工具,这些对于测量内核启动时间来说可能会有帮助。
基础常规优化方法
-
禁用内核的IP地址自动配置
net/ipv4/ipconfig.c: ip_auto_config() 1.58s
/* Wait for devices to appear */
err = wait_for_devices();
if (err)
return err;
/* Setup all network devices */
err = ic_open_devs();
if (err)
return err;
/* Give drivers a chance to settle */
ssleep(CONF_POST_OPEN); /* 1s */- 禁用办法
不传递ip参数给内核
禁用内核配置:CONFIG_IP_PNP* - 延迟到用户态配置IP地址:/etc/init.d/rcS
将IP配置相关放在启动后交于业务去做这些事情。 - 思路:分开两个相互依赖的操作,消除不必要的IO等待时间
- 禁用办法
-
减少或禁用pseudo终端设备
drivers/tty/pty.c: pty_init() 0.65 s- pseudo终端:用于远程连接(ssh)或者X下的虚拟终端(xterm)
- 减少设备个数:LEGACY_PTY_COUNT=2
- 直接禁用:UNIX98_PTYS, LEGACY_PTYS
- 思路:根据实际需要减少或停用某些功能
-
关闭控制台输出
- 打印日志信息到控制台很耗时
控制台设备:VGA、Framebuffer、串口 - 关闭控制台输出
关闭部分输出: quiet参数, console_loglevel=4
关闭所有输出:loglevel=0,副作用:错误也不显示 - 禁用控制台设备: e.g. 如果产品基于X11
CONFIG_{SERIAL*,VGA*, FB}=n - 完全禁用printk:副作用:不能收集错误日志
CONFIG_{EARLY_PRINTK, PRINTK}=n
e.g. #define panic(…) do { } while (0) or loop or reboot - 效果:提速30 ˜ 60%
- 思路:考虑研发阶段和产品阶段的不同需求
- 打印日志信息到控制台很耗时
-
计算loops_per_jiffy
{u,n}delay(): 根据loops_per_jiffy执行相应的nops
loops_per_jiffy: 每个jiffy需要执行的nop次数
1个jiffy = 1/HZ = 10ms (HZ = 100)
tick_periodic() -> do_timer(1) -> jiffies_64 += 1;
loops_per_jiffy计算:init/calibrate.c: calibrate_delay()
最慢:calibrate_delay_converge(): 250ms
最快:启动一次记录下来,下次直接传lpj参数给内核
$ dmesg | grep lpj
… calculated using timer frequency.. (lpj=7181976)- 问题:下次启动时处理器主频变了怎么办?
- 解决办法:实现不基于loops_per_jiffy的{u,n}delay()
新办法:读取硬件计数器直到delay的时长:e.g. delay_tsc()
在delay_tsc()里头设置lpj_fine - 思路:1、以静制动;2、转变思维方式寻求突破
-
采用更快的内核解压算法
-
为减少内核大小,一般都会对内核进行压缩
算法 内核大小 解压时间 不压缩 3.24M - LZO 1.76M 0.552s Gzip 1.62M 0.775s Bzip2 ? ? LZMA ? ? XZ ? ? 表1:不同压缩算法的解压速度比较
- 解压最快:LZO; 压缩比最高:XZ (针对可执行文件优化)
- 不压缩:拷贝时间?从哪里拷贝?多大?够小无须压缩。
- 思路:在不同需求之间进行权衡
-
-
减少内核大小
- 内核(未压缩的)越小,拷贝快,功能可能也少,执行快
- 减少内核大小的详细方法见《减少系统和程序的大小》
- 思路:启动速度的影响因素是多方面的
-
减少或者消除动态探测
- 基本思想:类似传lpj给内核避免计算loops_per_jiffy
思想扩展
把“不变”参数作为binary,类似DTB,传给内核
把“不变”参数定义成宏等数据,重编译内核,例
如:arch/mips/include/asm/cpu-features.h - 可采用的对象
处理器:大部分属性都是固定的,比如tlbsize,
cachesize等feature
PCI:PCI的外设不变的情况下,可把各外设的配置定死 - 效果:减少Probe过程,如果重编译,还可减少内核大小,
减少大量分支跳转以及由此相应的分支预测失败等 - 思路:以静制动
- 基本思想:类似传lpj给内核避免计算loops_per_jiffy
统筹考虑整个启动过程
- 一般启动过程:man boot
硬件开机、重启、软件重启(reboot)
BIOS(EFI)、Boot Loader(U-boot)
OS Loader in MBR: Lilo, Grub
装载Linux: cp.b, tftp
启动Linux: boot, bootm
运行Linux: 初始化、内核线程
进入用户态
- 优化后
硬件开机、重启、软件重启(reboot)
完成必要的硬件初始化: X-loader
Kexecboot: 装载、启动、运行Linux并进入用户态
- 切换其他Linux
- 效果:减少若干秒
- 思路:从整体上考虑问题
-
更多内核启动加速方法
- 快速重启: 直接装载、启动、运行Linux并进入用户态
Kexec: Documentation/kdump/kdump.txt
reboot=soft (?) - 快速分配内存:内存预留(mem, reserve_bootmem)映射(ioremap)后直接使用
- 优化内存拷贝:DMA方式从Flash拷贝内核到RAM
- initcalls优化
串行转并行:异步API: http://lwn.net/Articles/314808/
延迟initcalls到用户态: http://elinux.org/Deferred_Initcalls - 全速(或超速)启动、重启
确保处理器在启动过程中全速运行,刚初始化时就设置处理器主频为全速
在处理器启动过程中禁用变频、idle和节能模式 - 设备驱动特定的优化
- 快速重启: 直接装载、启动、运行Linux并进入用户态
加快程序运行速度
-
Init 进程
- SysV init
串行地启动预先配置好的服务
启动下一个时需要等前一个完成 - upstart
基于事件驱动,基于系统状态的改变启用和停用相应的任务
e.g. /etc/init/cron.conf
start on runlevel [2345]
stop on runlevel [!2345]
…
exec cron - systemd
基于socket和D-Bus激活来启动服务
按需启动daemons,类似xinetd
- SysV init
-
追踪Init启动服务过程
- 在内核态跟踪进程执行
在系统调用kernel/exec.c: sys_exec()入口打印时间戳
可以用scripts/show_delta统计分析 - bootchart
统计资源利用率和各个进程的执行情况,导出SVG结果
启动阶段资源利用率越高越好 - timechart
统计更多信息,结果更详细,导出SVG结果
tools/perf/builtin-timechart.c
- 在内核态跟踪进程执行
-
从休眠的映像文件启动内核
- 休眠接口:/sys/power/state
- 开发:启动内核和必须的应用,把系统休眠到磁盘或者Flash设备中,产生一个休眠映像文件
echo disk > /sys/power/state - 产品:启动内核,直接从休眠映像文件恢复系统
- 效果:无需重新一个一个地启动程序,只需要恢复到一个早期休眠到内存的系统状态
-
预读:readahead与tmpfs
- readahead
预先读取文件到内存中
减少iowait
用法:sys_readahead(), readahead-list - tmpfs
Documentation/filesystems/tmpfs.txt
tmpfs:内存中的文件系统+no swap
如果内存足够可以考虑把程序预先复制到tmpfs中
- readahead
-
使用更快的文件系统
- 文件系统影响程序的IO操作
- Squashfs v.s. CramFS
- UBIFS v.s. JFFS2
- Reiser4 v.s. Ext3
- XFS(mount) v.s. JFS(cpu utilization)
- 文件系统操作属性:async,noatime,nodirtime,relatime
- 文件系统性能评测:Dbench, Bonnie++, IOzone, Flexible,IO Tester
-
使用更小的执行文件
- 可执行文件更小,启动更快,占用内存更少
- ash v.s. bash
- busybox
- buildroot
-
编译器优化
- 常规:-O2, -O3
- gcc 4.5新特性:-flto
- 处理器特定优化:-march=, -mtune=
- 使用处理器优化指令: liboil
- 更多参数:http://en.wikipedia.org/wiki/Compiler_optimization
- 装载大量共享连接库很耗时
- 一般情况下可执行文件和共享连接库不变
- prelink通过修改可执行文件和共享连接库预先链接
- 减少动态链接的开销
- 更多信息:http://elinux.org/Pre_Linking
-
优化程序本身
- 记录程序执行时间: time
$ time find /var/log/ -name “test” > /dev/null
real 0m0.006s
user 0m0.004s
sys 0m0.000s - 跟踪程序函数执行开销: gprof
- 跟踪程序代码执行覆盖情况:gcov
-: 35:int main(int argc, char **argv)
function main called 3 returned 100% blocks executed 60%
3: 36:{
3: 37: if (argc < 2) {
#####: 38: a();
-: 39: } else {
3: 40: b();
-: 41: }
-: 42:} - 跟踪Cache miss, branch miss, page fault, tlb miss等:oprofile, perf, valgrind
- Cache miss: cacheline对齐。
- branch miss: 消除不必要分支,通过gcov把执行多的branch调到前面或者用likely。
- page fault: mlock/munlock防止swap出去。
- tlb miss: 增加page大小。
- 记录程序执行时间: time
优化系统调用和库函数
-
优化库函数 Ltrace.
- 用法:e.g. ltrace -T -f -o ltrace.log ls -l
- ltrace跟踪可执行文件调用的系统库文件
-
pre_load(LD_PRELOAD)优化过后的函数,如memcpy
$ ltrace -c -f ls -l
% time seconds usecs/call calls function
24.88 0.020998 42 491 strlen
10.64 0.008981 41 216 __ctype_get_mb_cur_max
9.02 0.007613 45 166 __overflow
8.75 0.007388 47 156 __errno_location
7.67 0.006472 41 156 memcpy
5.33 0.004497 55 81 strcoll
…
100.00 0.084397 1733 total
-
优化系统调用: strace .
- 用法:e.g. strace -T -f -o strace.log ls -l
- 减少fork/exec,合并程序成applet
- 优化shell程序:去掉不必要的pipe, pipe也使用fork/exec
- 减少不必要的系统调用
- fast system call: e.g. MIPS syscall指令有预留的指令域,
可以用于实现快速系统调用,比如模拟rdtsc,在用户态读
取硬件时钟计数器
-
优化内核函数: Ftrace/Kgcov.
- Ftrace: Documentation/trace/ftrace.txt
Ftrace可以跟踪内核的函数执行情况
执行程序前后开关Ftrace可追踪程序运行时的内核执行路径
优化跟程序相关的内核路径 - Kgcov: Documentation/gcov.txt
内核代码覆盖率测试
lgcov:把测试结果转换成HTML格式方便浏览和分析
- Ftrace: Documentation/trace/ftrace.txt
减少内核大小和内存使用
-
内核配置.
- 默认关闭所有配置: make allnoconfig
- 开启一些必须的配置选项: lspci, lsusb…
- 通过CONFIG_EMBEDDED去掉某些功能: futex?
- 开启内核和initramfs压缩支持: lzo, lzma, gzip, bzip2, xz
- 采用支持压缩的文件系统: squashfs, ubifs
- 去掉内核调试支持: 调试功能、调试符号
- 去掉模块支持?
- -Os: CONFIG_CC_OPTIMIZE_FOR_SIZE
- strip -X: CONFIG_STRIP_ASM_SYMS
-
Linux-Tiny.
- 目标:致力于降低内核大小和内存开销
- 下载:http://elinux.org/Linux_Tiny
- 策略
让更多选项可配置
删除内核消息(printk, BUG, panic, die)
不内联inline函数:性能跟大小折中
内存分配: Slob v.s. slab
减少内存数据结果大小:性能与大小折中
相同功能的简单实现:BFS v.s. CFS
-
降低内存消耗.
- initramfs v.s initrd
no block
no filesystem
no duplication - strip -x: 删除non-global符号,模块的non-global符号可删除
- strip -s: 删除所有符号; strip -S: 删除跟调试相关符号
- sstrip(来自buildroot): 删除可执行文件的section table
- objcopy -O binary: 仅保留可直接执行的二进制映像
- section garbage collection patchset
-ffunction-sections -fdata-sections and -gc-sections - 动态probing转静态definition
- 去掉更多的内核特性
系统调用: ptrace
内核和模块参数支持
让某些宏可配置:NR_IRQS, COMMAND_LINE_SIZE…
多选一:多个重复功能选其中一个,比如emulated
FPU和hardware FPU - 减少长调用:-mno-long-calls, 合并内核和模块空间
- initramfs v.s initrd
降低系统功耗
-
Tickless Kernel(Dynamic Ticks)
- 配置:CONFIG_NO_HZ
- HZ: 周期性的发出中断以便进行任务调度而支持多任务
- NO_HZ: 时钟中断按需发出, Idle时无时钟中断
- include/linux/clockchips.h: clock_event_device()
CLOCK_EVT_FEAT_ONESHOT
set_next_event - 2.6.24: 支持X86, ARM, MIPS和PowerPC架构
-
Powertop
- 监测频繁唤醒系统的内核和应用
- 提供一些减少功耗的建议
-
其他节能措施
- CPUFreq
处理器支持多级频率支持且可软件调节
自动调节策略:governor用户态可配置
实现驱动:配置可调节频率范围和操作底层寄存器 - 挂起隐藏的GUI
Suspend: kill -SIGTSTP
Resume: kill -SIGCONT - 软件休眠与挂起
休眠到Disk: echo disk > /sys/power/state
挂起到内存:echo mem > /sys/power/state
设备驱动支持:dev_pm_ops: suspend/resume - 视频输出控制:video_output子系统
- 背光控制: backlight子系统
- 无线射频:rfkill子系统
- CPUFreq
提高系统响应能力
-
测试系统响应延迟
yclictest: git://git.kernel.org/pub/scm/linux/kernel/git/clrkwllms/rt-tests.git- 更换调度策略
BFS v.s. CFS
低延迟桌面:PREEMPT - 中断线程化
- 降低某些长中断处理的优先级
request_threaded_irq() - 调整任务优先级:nice, chrt
- 绑定任务到处理器:taskset
- 资源分配:Session cgroup, ulimit
- 更换调度策略
文章引用自吴老师的ppt;