动静结合学内核:linux idle进程和init进程浅析

时间:2021-08-11 06:27:57

刘柳 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 + titer1@qq.com


退休的贵族进程 0号进程


所有进程的祖先叫做进程0
在系统初始化阶段由start_kernel()函数从无到有手工创建的一个内核线程
进程0最后的初始化工作创建init内核线程,此后运行cpu_idle,成为idle进程
控制权的接力棒从bios-->bootloader-->idle,某种程度上说,就是完成子系统初始化使命后,就退居二线了。
0号进程一直处于皇宫“内核态”,没有出过宫“到用户态”,所谓贵族终身。

0号进程的代码概要图

   字画的臭(逃),主要意思是0号进程是这样串行产生的:   start_kernel  -->rest_init --> cpu_idle_loop 
动静结合学内核:linux idle进程和init进程浅析

进入idle loop的堆栈样本如下(堆栈调用图也可以从中画出来的)
(gdb) bt
#0 cpu_idle_loop () at kernel/sched/idle.c:201
#1 cpu_startup_entry (state=<optimized out>) at kernel/sched/idle.c:274
#2 0xc175d22d in rest_init () at init/main.c:418
#3 0xc1a4bb59 in start_kernel () at init/main.c:680
#4 0xc1a4b360 in i386_start_kernel () at arch/x86/kernel/head32.c:49
#5 0x00000000 in ?? ()

idle最核心的代码位置(前方高能,含有chinglish,蹩脚翻译,请大拿指点。只翻译部分主要的)
static void cpu_idle_loop(void)
{
while (1) {
/*如果本架构下面有标示轮询poll的bit位,我们会保持不变??(理解:始终在这个循环里)
如果idle没有被调度,那么poll bit是被清空的
反过来说, 如果 设置了poll bit,那么need_resched将会保证cpu进行重新调度。
*/

__current_set_polling();
tick_nohz_idle_enter();

while (!need_resched()) {
check_pgt_cache();
rmb();

if (cpu_is_offline(smp_processor_id()))
arch_cpu_idle_dead();

local_irq_disable();
arch_cpu_idle_enter();

/*
在poll mode 中,我们会使能中断 和 自旋锁
同时 如果检测到唤醒(来自一些设备广播的),
我们将努力避免进入深度睡眠,因为我们知道 IPI (???)即将马上来到
*/
if (cpu_idle_force_poll || tick_check_broadcast_expired())
cpu_idle_poll();
else
cpuidle_idle_call();

arch_cpu_idle_exit();
}

/*
* Since we fell out of the loop above, we know
* TIF_NEED_RESCHED must be set, propagate it into
* PREEMPT_NEED_RESCHED.
*
* This is required because for polling idle loops we will
* not have had an IPI to fold the state for us.
*/
preempt_set_need_resched();
tick_nohz_idle_exit();
__current_clr_polling();

/*
* We promise to call sched_ttwu_pending and reschedule
* if need_resched is set while polling is set. That
* means that clearing polling needs to be visible
* before doing these things.
*/
smp_mb__after_atomic();

sched_ttwu_pending();
schedule_preempt_disabled();
}
}




放 大杀器(o(^▽^)o,自大了吧),进入一个进入idle loop的现场追踪过程 (初步制作gif,没有提示大家什么时候开始/结束,下次功力深入后将重制)。
动静结合学内核:linux idle进程和init进程浅析

用户1号进程的前世今生


进程1又称为init进程,是所有用户进程的祖先 由进程0在start_kernel调用rest_init创建 init进程PID为1,当调度程序选择到init进程时,init进程开始执行kernel_init ()函数 init是个普通的用户态进程,它是Unix系统内核初始化与用户态初始化的接合点,它是所有用户进程的祖宗。在运行init以前是内核态初始化,该过程(内核初始化)的最后一个动作就是运行/sbin/init可执行文件

这段话是孟老师课件摘取,字字珠玑啊。     所谓祖先,就是所有用户态进程都从这个进程fork出来。     而init进程(pid=1)的产生也是第一个使用fork调用的函数。     其他注意区分的是用户控件的/sbin/init在我们实验中指的是menuos编译出来的init.(pid!=1)
首先来看微缩的start_kernel函数代码(情景分析):
asmlinkage __visible void __init start_kernel(void)
{
...
//初始化0号进程pcb
set_task_stack_end_magic(&init_task);
...
/* 当只有一个CPU的时候这个函数就什么都不做,
但是如果有多个CPU的时候那么它就
* 返回在启动的时候的那个CPU的号
*/
smp_setup_processor_id();

...

/* 关闭当前CPU的中断 */
local_irq_disable();
early_boot_irqs_disabled = true;

...

/* 初始化页地址,使用链表将其链接起来 */
page_address_init();
/* 显示内核的版本信息 */
pr_notice("%s", linux_banner);
/*
* 每种体系结构都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个
* 体系结构的setup_arch()函数,由源码树顶层目录下的Makefile中的ARCH变量
* 决定
*/
setup_arch(&command_line);

...
/* 打印Linux启动命令行参数 */
pr_notice("Kernel command line: %s\n", boot_command_line);

/* 对内核选项的两次解析 */
parse_early_param();
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
set_init_arg);

jump_label_init();

...
/* 初始化hash表,便于从进程的PID获得对应的进程描述符指针 */
pidhash_init();
/* 虚拟文件系统的初始化 */
vfs_caches_init_early();
sort_main_extable();


/*
* trap_init函数完成对系统保留中断向量(异常、非屏蔽中断以及系统调用)
* 的初始化,init_IRQ函数则完成其余中断向量的初始化
*/
trap_init();
mm_init();

/* 进程调度器初始化 */
sched_init();

preempt_disable();
/* 检查中断是否已经打开,如果已经打开,则关闭中断 */
if (WARN(!irqs_disabled(),
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
...

/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
/* 对高精度时钟进行初始化 */
hrtimers_init();
/* 初始化tasklet_softirq和hi_softirq */
softirq_init();
timekeeping_init();
/* 初始化系统时钟源 */
time_init();
sched_clock_postinit();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), "Interrupts were enabled early\n");
early_boot_irqs_disabled = false;
local_irq_enable();

/* slab初始化 */
kmem_cache_init_late();

/*
* 初始化控制台以显示printk的内容,在此之前调用的printk
* 只是把数据存到缓冲区里
*/

console_init();
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);

lockdep_info();

...

/*
* CPU性能测试函数,可以计
算出CPU在1s内执行了多少次一个
* 极短的循环,计算出来的值经过处理后得
到BogoMIPS值(Bogo是Bogus的意思),
*/
calibrate_delay();
pidmap_init();
...

/* 创建init进程 */
rest_init();//66 analysis 0 #, never return ...
}


     下面这个演示过程,给出了在start_kernel到rest_init的跟踪过程,(其中每次qume打印出新的东西时候,我都是鼠标移动提示),这样所见即所得的方式希望读者喜欢。


动静结合学内核:linux idle进程和init进程浅析


继续我们的内核之旅,以上是追踪到 rest_init,下面将从rest_init 到 kthread_init, 图中,直接在init进程的函数段(kernel_init)中开始, (一些小插曲,本演示为了说明init进程最后变成了用户台进程,去查证了cs寄存器,不过source insight没有找到对应的bit,下次将会更新
用户常见的是从用户态(博文下一轮更新将会说明)从核心态,
这里init(pid=1)是从核心态变为用户态,一个比较核心的变化就是会把cs寄存器从核心段cs变为用户段cs
从数字上来说,cs值从96(0x60)变为115(0x73)

先看宏的解释:
#define __USER_CS(GDT_ENTRY_DEFAULT_USER_CS*8+3) //用户段cs 计算出来14*8+3 =0x73
#define GDT_ENTRY_DEFAULT_USER_CS14

#define __KERNEL_CS(GDT_ENTRY_KERNEL_CS*8)//核心段cs :0x60
#define GDT_ENTRY_KERNEL_CS(GDT_ENTRY_KERNEL_BASE+0)
#define GDT_ENTRY_KERNEL_BASE(12)


再看切换的代码:为什么启动Init进程会涉及到start_thread, 仅从调用图来看,丑图再现(逃。。) 动静结合学内核:linux idle进程和init进程浅析
一言难尽,请看精彩的解释:原文再现
这里直接定位到用户态切换的代码:
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs= 0;
regs->ds= __USER_DS;
regs->es= __USER_DS;
regs->ss= __USER_DS;
regs->cs= __USER_CS;
regs->ip= new_ip;
regs->sp= new_sp;
regs->flags= X86_EFLAGS_IF;
/*
* force it to the iret return path by making it look as if there was
* some work pending.
*/
set_thread_flag(TIF_NOTIFY_RESUME);
}

上面的内容,一句话,说明Init进程如何从核心态变成用户态的。
最后我们在动态图里面把相关过程串起来吧。

动静结合学内核:linux idle进程和init进程浅析

如果图看到,请点击这里: 点我,我是图

总结:



总体来说,这里是几乎各种子系统的诞生之地。这里牵一发,动全身。
如果你在不同版本内核比较start_kernel,就会发现很大差异。

idle进程,如标题所说,完成重要子系统初始化,就退居二线。
1号进程从0号进程fork出来,然后又切换到用户态,完成控制权从核心态到用户态的转换,
因此用户交互才能开始。

使命,决定了一生。
Linux进程如此,咋们的人生使命是?
码农陷入了思索。。。     

附录:

题目自拟,内容围绕Linux内核的启动过程,即从start_kernel到init进程启动;
博客中需要使用实验截图
博客内容中需要仔细分析start_kernel函数的执行过程
总结部分需要阐明自己对“Linux系统启动过程”的理解,尤其是idle进程、1号进程是怎么来的。

参考:http://book.51cto.com/art/201007/213598.htm