嵌入式Linux 系统的优化策略和方法

2019-07-12 16:33发布

class="markdown_views prism-atom-one-light">

嵌入式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:不同压缩算法的解压速度比较
    • 数据来源:http://free-electrons.com/blog/lzo-kernel-compression/
    • 解压最快:LZO; 压缩比最高:XZ (针对可执行文件优化)
    • 不压缩:拷贝时间?从哪里拷贝?多大?够小无须压缩。
    • 思路:在不同需求之间进行权衡
  • 减少内核大小
    • 内核(未压缩的)越小,拷贝快,功能可能也少,执行快
    • 减少内核大小的详细方法见《减少系统和程序的大小》
    • 思路:启动速度的影响因素是多方面的
  • 减少或者消除动态探测
    • 基本思想:类似传lpj给内核避免计算loops_per_jiffy
      思想扩展
      把“不变”参数作为binary,类似DTB,传给内核
      把“不变”参数定义成宏等数据,重编译内核,例
      如:arch/mips/include/asm/cpu-features.h
    • 可采用的对象
      处理器:大部分属性都是固定的,比如tlbsize,
      cachesize等feature
      PCI:PCI的外设不变的情况下,可把各外设的配置定死
    • 效果:减少Probe过程,如果重编译,还可减少内核大小,
      减少大量分支跳转以及由此相应的分支预测失败等
    • 思路:以静制动
统筹考虑整个启动过程
- 一般启动过程: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和节能模式
    • 设备驱动特定的优化

加快程序运行速度

  • 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
  • 追踪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中
  • 使用更快的文件系统
    • 文件系统影响程序的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大小。

优化系统调用和库函数

  • 优化库函数 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格式方便浏览和分析

减少内核大小和内存使用

  • 内核配置.
    • 默认关闭所有配置: 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, 合并内核和模块空间

降低系统功耗

  • 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子系统

提高系统响应能力

  • 测试系统响应延迟
    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;