优化开机过程中的内核空闲时间

来源:互联网 发布:win10命令改mac地址 编辑:程序博客网 时间:2024/04/28 12:55
 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 *)0xf6a00010,\
                                       (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:22a0a4 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:22a3d6 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等闲等函数。