abbshr / abbshr.github.io

人们往往接受流行,不是因为想要与众不同,而是因为害怕与众不同

Home Page:http://digitalpie.cf

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Linux中鲜为人知的幕后工作者——Idle, Init & Scheduler

abbshr opened this issue · comments

Idle进程和Init进程, 谁是老大?

感谢这学期开了一门嵌入式操作系统, 纠正了我一些认识上的误区.

当涂老师提到idle进程时, 我突然想起了init,

Init是所有进程的祖先, 它是内核创建的第一个进程...

记得我们学操作系统时水笔老范说了好几遍, 已经牢牢刻在脑海里了, 所以潜意识里仍是不假思索认为init是头子. 可是仔细看idle的作用以及地位, 貌似比init进程还有高, 这就让有点怀疑当初所学的东西是不是漏掉了什么...

确实当年学操作系统时漏掉了一些重要的东西, 今天和实验室的师兄们讨论了之后终于补回来了.

先从Linux启动流程说起...

当boot loader选定并加载一个内核后, 将计算机控制权交给加载的内核, 并创建一些系统函数. 当准备工作完成, 内核逻辑开始调用定义的start_kernal()函数.

start_kernal()函数的任务就是建立中断处理机制, 初始化内存管理的剩余部分, 初始化调度器, 初始化设备以及驱动等等. 最后调用rest_init()函数创建init进程(pid 1), 并将(内核)自己做为idle进程(pid 0).

init进程由内核创建, 并在用户空间执行. 它在用户空间执行upstart服务(启动脚本), 创建非系统服务并调用login程序进行用户登录控制. 下面是init的代码:

static int init(void * unused)
{
        lock_kernel();
        do_basic_setup();

        prepare_namespace();

        /*
         * Ok, we have completed the initial bootup, and
         * we're essentially up and running. Get rid of the
         * initmem segments and start the user-mode stuff..
         */
        free_initmem();
        unlock_kernel();

        if (open("/dev/console", O_RDWR, 0) < 0)        // stdin
                printk("Warning: unable to open an initial console.\n");

        (void) dup(0);                                  // stdout
        (void) dup(0);                                  // stderr

        /*
         * We try each of these until one succeeds.
         *
         * The Bourne shell can be used instead of init if we are
         * trying to recover a really broken machine.
         */

        if (execute_command)
                execve(execute_command,argv_init,envp_init);
        execve("/sbin/init",argv_init,envp_init);
        execve("/etc/init",argv_init,envp_init);
        execve("/bin/init",argv_init,envp_init);
        execve("/bin/sh",argv_init,envp_init);
        panic("No init found.  Try passing init= option to kernel.");
}

init函数最后执行了系统调用exec, 将可作为init程序的二进制镜像加载到内存.

idle做什么?

现在来看看kernal space在系统启动过程中都干点什么.

the kernel looks for an init process to run, which (separately) sets up a user space and the processes needed for a user environment and ultimate login. The kernel itself is then allowed to go idle, subject to calls from other processes.

调用start_kernal()函数这一阶段称作Kernel startup stage. 阶段最后才创建init进程. rest_init代码如下:

rest_init() {
    // init process, pid = 1
    kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
    unlock_kernel();
    current->need_resched = 1;
    // idle process, pid = 0
    cpu_idle();     // never return
}

可见idle"进程"就是start_kernal演变过来.

The startup function for the kernel (also called the swapper or process 0)

而idle的任务就是空转! 当系统中没有其他任何进程使用CPU时, 因为CPU也不能闲着呀, 所以这时调度器就把CPU控制权交给idle进程, idle进程的诞生是通过cpu_idle()函数完成的, 而这个函数永远不会返回.:

/*
 * The idle thread. There's no useful work to be
 * done, so just try to conserve power and have a
 * low exit latency (ie sit in a loop waiting for
 * somebody to say that they'd like to reschedule)
 */
void cpu_idle (void)
{
        /* endless idle loop with no priority at all */
        init_idle();
        current->nice = 20;
        current->counter = -100;

        while (1) {
                void (*idle)(void) = pm_idle;
                if (!idle)
                        idle = default_idle;
                while (!current->need_resched)
                        idle();
                schedule();
                check_pgt_cache();
        }
}

///////////////////////////////////////////////////////////////////////////////
void __init init_idle(void)
{
        struct schedule_data * sched_data;
        sched_data = &aligned_data[smp_processor_id()].schedule_data;

        if (current != &init_task && task_on_runqueue(current)) {
                printk("UGH! (%d:%d) was on the runqueue, removing.\n",
                        smp_processor_id(), current->pid);
                del_from_runqueue(current);
        }
        sched_data->curr = current;
        sched_data->last_schedule = get_cycles();
        clear_bit(current->processor, &wait_init_idle);
}

///////////////////////////////////////////////////////////////////////////////
void default_idle(void)
{
        if (current_cpu_data.hlt_works_ok && !hlt_counter) {
                __cli();
                if (!current->need_resched)
                        safe_halt();
                else
                        __sti();
        }
}

到这里我想你应该明白idle进程的由来了以及与init的关系了.

现在在linux下执行命令ps -eaf, 查看一下:

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 17:48 ?        00:00:01 /sbin/init
root         2     0  0 17:48 ?        00:00:00 [kthreadd]
root         3     2  0 17:48 ?        00:00:00 [ksoftirqd/0]
root         5     2  0 17:48 ?        00:00:00 [kworker/0:0H]
root         7     2  0 17:48 ?        00:00:09 [rcu_sched]

pid为1的init进程的父进程pid为0 (第二个进程是内核进程的守护进程, 也是由内核创建), 也就是说忽略内核进程的情况下:

Init是所有进程的祖先, 它是内核创建的第一个进程

这句话是对的, init确实是所有用户级进程的祖先.

Scheduler由谁来调用?

其实最初讨论话题是由调度工作谁来做展开的. 现在如果你不知道上面的内容, 你怎么想?

我最开始头脑中有这么几个策略:

  1. 由中断处理程序(也就是内核)调用schedule()函数进行进程调度.
  2. 由init进程负责调度管理.
  3. 由idle进程负责.

之所以会有后两个想法, 是因为我觉得每个进程都可以主动让出CPU控制权并调用schedule函数挑选就绪队列中的进程.

如果我清除的意识到init是user-space进程的话就直接排除掉了. 假设init负责了进程的调度, 那么首先由init → process_a, 如果process_a的时间片到了呢? 由于调度权在init进程, 所以这时没有任何用户空间进程可以执行, 于是idle进程篡位, 系统暂时进入空转, cpu_idle函数通过调用schedule函数才可以从就绪队列选择一个进程继续执行. 如果process_a正在执行时来了一个高优先级的进程呢? 中断之后由于没有调度器执行, 于是又进入idle的天下.

也就是说, 把调度权交给init完全就是废了, 一点用没有.

idle进程其实就是内核的一部分, 读一读它的源码你会发现它还负责调度工作:

while (1) {
        void (*idle)(void) = pm_idle;
        if (!idle)
                idle = default_idle;
        while (!current->need_resched)
                idle();
        schedule();
        check_pgt_cache();
}

脑补一下这样一个场景: 某个进程由于某些原因放弃了CPU使用权. 由于idle进程(与其说是一个进程, 到不如说是部分内核代码, 本身没有什么就绪可言)是一个死循环, 检测到需要调度, 则调用schedule()函数完成进程调度工作.

所以不把idle进程看做"进程"的一个原因可能是它是Scheduler吧.

但是呢, 这仅仅是现有操作系统的一种调度手段, 可并不代表这是唯一的调度手段. 可以脑洞大开, 让任一进程都可以作为调度器! 只要修改linux现有的代码, 当然, 那种情况就另当别论了.