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

退休的贵族进程 0号进程

所有进程的祖先叫做进程0
 在系统初始化阶段由start_kernel()函数从无到有手工创建的一个内核线程
 进程0最后的初始化工作创建init内核线程,此后运行cpu_idle,成为idle进程

控制权的接力棒从bios-->bootloader-->idle,某种程度上说,就是完成子系统初始化使命后,就退居二线了。

0号进程一直处于皇宫“内核态”,没有出过宫“到用户态”,所谓贵族终身。

0号进程的代码概要图

字画的臭(逃),主要意思是0号进程是这样串行产生的:

start_kernel -->rest_init --> cpu_idle_loop

进入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();
	}
}
【文章福利】小编推荐自己的Linux内核技术交流群:【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

放 大杀器(o(^▽^)o,自大了吧) ,进入一个进入idle loop的现场追踪过程

(初步制作gif,没有提示大家什么时候开始/结束,下次功力深入后将重制)。

用户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打印出新的东西时候,我都是鼠标移动提示),这样所见即所得的方式希望读者喜欢。

继续我们 的内核之旅, 以上是追踪 到 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_CS	14
 
#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,

仅从调用图来看,丑图再现(逃。。)

这里直接定位到用户态切换的代码:

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进程如何从核心态变成用户态的。

最后我们在动态图里面把相关过程串起来吧。

总结:

总体来说,这里是几乎各种子系统的诞生之地。这里牵一发,动全身。

如果你在不同版本内核 比较 start_kernel,就会发现很大差异。

idle进程,如标题所说,完成重要子系统初始化,就退居二线。

1号进程从0号进程fork出来,然后又切换到用户态,完成控制权从核心态到用户态的转换,

因此用户交互才能开始。

使命,决定了一生。

Linux进程如此,咋们的人生使命是?

码农陷入了思索。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核进程调度是一个非常复杂的系统,主要由可调度进程队列、进程调度策略、调度器负责。 在Linux中,进程调度器是一个单独的内核线程,其主要工作是在可调度进程队列中选择下一个要运行的进程,并将CPU分配给该进程进程调度器根据进程的优先级和调度策略来选择下一个要运行的进程Linux进程调度的核心代码位于sched目录下,主要包括以下文件: 1. sched.h:定义了调度器的数据结构和函数原型。 2. sched.c:实现了进程调度器的主要功能,包括进程的加入、删除、更新等操作。 3. rt.c:实时调度策略相关代码。 4. fair.c:CFS调度策略相关代码。 5. idle.c:空闲进程相关代码。 6. deadline.c:DEADLINE调度策略相关代码。 下面我们以CFS调度策略为例,简单介绍一下进程调度的实现过程: CFS调度策略是一种完全公平的调度策略,它通过动态优先级来保证进程的公平性。在CFS调度策略中,每个进程都有一个虚拟运行时间(virtual runtime),该时间是进程已经运行的时间和优先级的函数。 CFS调度策略的核心代码位于fair.c文件中,主要包括以下函数: 1. enqueue_task_fair():将一个进程添加到可调度进程队列中。 2. dequeue_task_fair():将一个进程从可调度进程队列中删除。 3. update_curr_fair():更新当前进程的虚拟运行时间。 4. pick_next_task_fair():选择下一个要运行的进程。 以上函数的实现过程中,都涉及到了对进程调度器的数据结构的操作,如可调度进程队列、进程控制块等。具体实现过程需要结合代码进行分析。 总体来说,Linux内核进程调度的实现非常复杂,需要涉及到很多的数据结构和算法,同时还需要考虑到性能、公平性等因素。因此,对于想要深入了解Linux内核的人来说,进程调度是必须要掌握的一个重要部分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值