优化开机过程中的内核空闲时间
作者: zjujoe 转载请注明出处
Email:zjujoe@yahoo.com
BLOG:http://blog.youkuaiyun.com/zjujoe
Linux 下有一条命令:
~ # cat /proc/uptime
8.01 4.64
其中输出一表示系统启动时间,输出二表示系统空闲时间。
空闲时间表示此时 CPU 无事可做,将这段时间优化掉可以提高系统启动速度。
网络上已经有很多优化系统进入用户空间空闲时间的文档,我们也进行了很多相关实验。效果非常明显。
另外,我们发现,系统在进入用户空间之前,就已经存在一些空闲时间了!这里, 我们跟踪一下这一段空闲时间。
cat /proc/uptime 对应的内核实现
107static int uptime_read_proc(char *page, char **start, off_t off,
108 int count, int *eof, void *data)
109{
110 struct timespec uptime;
111 struct timespec idle;
112 int len;
113 cputime_t idletime = cputime_add(init_task.utime, init_task.stime);
114
115 do_posix_clock_monotonic_gettime(&uptime);
116 monotonic_to_bootbased(&uptime);
117 cputime_to_timespec(idletime, &idle);
118 len = sprintf(page,"%lu.%02lu %lu.%02lu/n",
119 (unsigned long) uptime.tv_sec,
120 (uptime.tv_nsec / (NSEC_PER_SEC / 100)),
121 (unsigned long) idle.tv_sec,
122 (idle.tv_nsec / (NSEC_PER_SEC / 100)));
123
124 return proc_calc_metrics(page, start, off, count, eof, len);
125}
由此可见,所谓 idletime, 其实就是系统的 idle进程(0号进程init_task)的运行时间。 为了能在内核空间显示同样消息,我们模仿该函数,写一个打印时间信息的内核函数:
void print_uptime(const unsigned char * func, unsigned long line) {
struct timespec uptime;
struct timespec idle;
cputime_t idletime;
idletime = cputime_add(init_task.utime, init_task.stime);
do_posix_clock_monotonic_gettime(&uptime);
cputime_to_timespec(idletime, &idle);
printk("%s: %lu@%s oscr0:%lx %lu.%02lu %lu.%02lu/n",/
__func__, line, func, *(unsigned long *)0xf
6a
00010,/
(unsigned long)uptime.tv_sec,/
(uptime.tv_nsec / (NSEC_PER_SEC / 100)),/
(unsigned long)idle.tv_sec,/
(idle.tv_nsec / (NSEC_PER_SEC / 100)));/
}
其中, oscr 为一个free running 的 3.25M timer, 表示系统自 reset 以来的时间。对于我们这个实验,可以忽略它。
我们在需要打印时间信息的地方调用:
print_uptime(__func__, __LINE__);
就可以得到函数名,代码行位置,以及时间信息, 比如:
[ 0.230000] print_uptime: 3233@synchronize_net oscr0:2715e1 0.22 0.17
最后一个 field 为系统的闲等时间。
关于启动过程中的进程
内核启动之初,系统还没有进程调度的概念, start_kernel(内核C语言入口函数) 会调用一堆的初始化函数,最后调用的是rest_init:
460static void noinline __init_refok rest_init(void)
461 __releases(kernel_lock)
462{
463 int pid;
464
465 kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
466 numa_default_policy();
467 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
468 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
469 unlock_kernel();
470
471 /*
472 * The boot idle thread must execute schedule()
473 * at least once to get things moving:
474 */
475 init_idle_bootup_task(current);
476 preempt_enable_no_resched();
477 schedule();
478 preempt_disable();
479
480 /* Call into cpu_idle with preempt disabled */
481 cpu_idle();
482}
我们在kernel_thread 函数前插入打印语句, 得到:
[ 0.140000] print_uptime: 465@rest_init oscr0: 22a 0a 4 0.14 0.14
可见此时的 idle 时间并非真正的 idle, 而是实实在在的 CPU 运行时间。
kernel_thread 函数创建了系统的 1 号进程, 而 rest_init 函数会最终调用 cpu_idle, 变成真正的 idle 进程。
底下的主要任务则由1 号进程 init 来完成, 最终它会运行用户空间的 init 程序, 变成一个用户进程。
跟踪空闲时间发生位置
首先研究 init 函数:
849 static int __init init(void *unused)
850 {
851 lock_kernel();
852 /*
853 * init can run on any cpu.
854 */
855 set_cpus_allowed(current, CPU_MASK_ALL);
856 /*
857 * Tell the world that we're going to be the grim
858 * reaper of innocent orphaned children.
859 *
860 * We don't want people to have to make incorrect
861 * assumptions about where in the task array this
862 * can be found.
863 */
864 init_pid_ns.child_reaper = current;
865
866 cad_pid = task_pid(current);
867
868 smp_prepare_cpus(max_cpus);
869
870 do_pre_smp_initcalls();
871
872 smp_init();
873 sched_init_smp();
874
875 cpuset_init_smp();
876
877 do_basic_setup();
878
879 /*
880 * check if there is an early userspace init. If yes, let it do all
881 * the work
882 */
883
884 if (!ramdisk_execute_command)
885 ramdisk_execute_command = "/init";
886
887 if (sys_access((const char __user *)ramdisk_execute_command, 0) != 0) {
888 ramdisk_execute_command = NULL;
889 prepare_namespace();
890 }
891
892 /*
893 * Ok, we have completed the initial bootup, and
894 * we're essentially up and running. Get rid of the
895 * initmem segments and start the user-mode stuff..
896 */
897 init_post();
898 return 0;
899 }
加入打印语句, 发现 do_basic_setup 花了不少闲等时间。
[ 0.140000] print_uptime: 877@init oscr0: 22a 3d6 0.15 0.14
[ 0.960000] print_uptime: 879@init oscr0:4b92ee 1.27 0.72
再来看 do_basic_setup 函数:
726 static void __init do_basic_setup(void)
727 {
728 /* drivers will send hotplug events */
729 init_workqueues();
730 usermodehelper_init();
731 driver_init();
732 init_irq_proc();
733 do_initcalls();
734 }
同样, 定位出 do_initcalls 函数花了不少闲等时间。
676 static void __init do_initcalls(void)
677 {
678 initcall_t *call;
679 int count = preempt_count();
680
681 for (call = __initcall_start; call < __initcall_end; call++) {
682 char *msg = NULL;
683 char msgbuf[40];
684 int result;
685
686 if (initcall_debug) {
687 printk("Calling initcall 0x%p", *call);
688 print_fn_descriptor_symbol(": %s()",
689 (unsigned long)*call);
690 printk("/n");
691 }
692
693 result = (*call) ();
694
695 if (result && result != -ENODEV && initcall_debug) {
696 sprintf(msgbuf, "error code %d", result);
697 msg = msgbuf;
698 }
699 if (preempt_count() != count) {
700 msg = "preemption imbalance";
701 preempt_count() = count;
702 }
703 if (irqs_disabled()) {
704 msg = "disabled interrupts";
705 local_irq_enable();
706 }
707 if (msg) {
708 printk(KERN_WARNING "initcall at 0x%p", *call);
709 print_fn_descriptor_symbol(": %s()",
710 (unsigned long)*call);
711 printk(": returned with %s/n", msg);
712 }
713 }
714
715 /* Make sure there is no pending stuff from the initcall sequence */
716 flush_scheduled_work();
717 }
该函数正如其名, 调用各种 init 函数。在 693 行前后插入语句, 再结合各驱动初始化函数的打印信息,发现,空闲时间主要发生在 tcp/ip, 驱动 misc_control, usb host, mmc 的初始化函数上。
继续, 我们定位出 mmc 驱动中调用了 msleep(1) , 将其改为 mdelay(1) 后, 发现闲等时间没有了, 忙等时间也缩短了!
总结
内核启动过程中, 使用 msleep 函数会导致进程切换, 而此时只有 0 号 idle 进程与 1 号 init 进程。切换到 idle 进程没有意义, 反而浪费了进程切换时间。
所以,系统启动过程中应该使用mdelay 等忙等函数, 而不能使用 msleep 等闲等函数。