2023-06-11  阅读(2)
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10946851.html

我们知道,对于内核提供的进程管理子系统,将来肯定是要运行各种各样的进程,对于我们做Linux内核开发的同学来说,大家熟悉Linux下有3个特殊的进程,其主要内容如下:

  • Idle进程(PID = 0),本章主要讲解进程0是什么?
  • Init进程(PID = 1),本章主要讲解进程1是什么?
  • kthread(PID = 2),本章主要讲解进程2是什么?

1 进程初始化(0号进程)

内核的启动从入口函数 start_kernel() 开始;在 init/main.c 文件中,start_kernel 相当于内核的main 函数;
这个里面是各种各样初始化函数,用来初始化各个子系统。对于操作系统,开机的时候首先会创建第一个进程,也就是唯一一个没有通过fork产生的进程。首先内核就需要为init_task进程的task_struct数据结构进行分配。

1.1 进程描述符分配

第0号进程描述符变量是Init_task,在init/init_task.c文件中静态初始化,其代码实现如下:

    struct task_struct init_task = INIT_TASK(init_task);
    EXPORT_SYMBOL(init_task);

宏INIT_TASK的定义在文件include/linux/init_task.h中:

    #define INIT_TASK(tsk)	\
    {									\
    	INIT_TASK_TI(tsk)						\
    	.state		= 0,						\
    	.stack		= init_stack,					\
    	.usage		= ATOMIC_INIT(2),				\
    	.flags		= PF_KTHREAD,					\
    	.prio		= MAX_PRIO-20,					\
    	.static_prio	= MAX_PRIO-20,					\
    	.normal_prio	= MAX_PRIO-20,					\
    	.policy		= SCHED_NORMAL,					\
    	.cpus_allowed	= CPU_MASK_ALL,					\
    	.nr_cpus_allowed= NR_CPUS,					\
    	.mm		= NULL,						\
    	.active_mm	= &init_mm,					\
    	.restart_block = {						\
    		.fn = do_no_restart_syscall,				\
    	},								\
    	.se		= {						\
    		.group_node 	= LIST_HEAD_INIT(tsk.se.group_node),	\
    	},								\
    	.rt		= {						\
    		.run_list	= LIST_HEAD_INIT(tsk.rt.run_list),	\
    		.time_slice	= RR_TIMESLICE,				\
    	},								\
    	.tasks		= LIST_HEAD_INIT(tsk.tasks),			\
    	...
    	.comm		= INIT_TASK_COMM,				\
    	.thread		= INIT_THREAD,					\
    	.fs		= &init_fs,					\
    	.files		= &init_files,					\
    	.signal		= &init_signals,				\
    	.sighand	= &init_sighand,				\
    	.nsproxy	= &init_nsproxy,				\
    }

从comm字段看出,进程0叫swapper,此外,系统中的所有进程的task_struct数据结构都通过list_head类型的双向链表链接在一起,因此每个进程的task_struct数据结构都包含一个list_head的tasts成员。这个进程链表的头是init_task进程,也就是所谓的进程0。

1.2 进程堆栈

init_task进程使用init_thread_union数据结构描述的内存区域作为该进程的堆栈空间,并且和自身的thread_info参数公用这一内存空间空间

    #define INIT_TASK(tsk)	\
    {
    	...
        .stack		= init_stack,					\
        ...
    }

而init_thread_info则是一段体系结构相关的定义,被定义在/arch/arm64/include/asm/thread_info.h

    #define init_thread_info	(init_thread_union.thread_info)
    #define init_stack		(init_thread_union.stack)

而init_thread_union则定义在init/init_task.c中

    union thread_union init_thread_union __init_task_data = {
    #ifndef CONFIG_THREAD_INFO_IN_TASK
    	INIT_THREAD_INFO(init_task)
    #endif
    };
    #define INIT_THREAD_INFO(tsk)						\
    {									\
    	.task		= &tsk,						\
    	.flags		= 0,						\
    	.preempt_count	= INIT_PREEMPT_COUNT,				\
    	.addr_limit	= KERNEL_DS,					\
    }

1.3 进程空间

由于init_task是一个运行在内核空间的内核线程, 因此其虚地址段mm为NULL, 但是必要时他还是需要使用虚拟地址的,因此avtive_mm被设置为init_mm

    #define INIT_TASK(tsk)	\
    {
    	.mm		= NULL,						\  
    	.active_mm	= &init_mm,		\
    }
    
    struct mm_struct init_mm = {
    	.mm_rb		= RB_ROOT,
    	.pgd		= swapper_pg_dir,
    	.mm_users	= ATOMIC_INIT(2),
    	.mm_count	= ATOMIC_INIT(1),
    	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
    	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
    	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
    	.user_ns	= &init_user_ns,
    	INIT_MM_CONTEXT(init_mm)
    };

对于普通进程而言,这两个指针变量的值相同。但是,内核线程不拥有任何内存描述符,所以它们的mm成员总是为NULL。当init_task内核线程得以运行时,它的active_mm成员被初始化为init_mm的值。

2 其他初始化

内核启动阶段的最后函数reset_init()函数在内部再次调用多个函数,其最终会创建1号进程2号进程

    static noinline void __ref rest_init(void)
    {
    	int pid;
    
    	rcu_scheduler_starting();
    	/*
    	 * We need to spawn init first so that it obtains pid 1, however
    	 * the init task will end up wanting to create kthreads, which, if
    	 * we schedule it before we create kthreadd, will OOPS.
    	 */
    	kernel_thread(kernel_init, NULL, CLONE_FS);                            -----------(1)
    	numa_default_policy();
    	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);           -----------(2)
    	rcu_read_lock();
    	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    	rcu_read_unlock();
    	complete(&kthreadd_done);
    
    	/*
    	 * The boot idle thread must execute schedule()
    	 * at least once to get things moving:
    	 */
    	init_idle_bootup_task(current);                                        ------------(3)
    	schedule_preempt_disabled();                                           ------------(4)
    	/* Call into cpu_idle with preempt disabled */
    	cpu_startup_entry(CPUHP_ONLINE);                                       ------------(5)
    }
    1. 调用kernel_thread()创建1号内核线程, 该线程随后转向用户空间, 演变为init进程
    1. 调用kernel_thread()创建kthreadd内核线程
    1. init_idle_bootup_task():当前0号进程init_task最终会退化成idle进程,所以这里调用init_idle_bootup_task()函数,让init_task进程隶属到idle调度类中。即选择idle的调度相关函数。
    1. 调用schedule()函数切换当前进程,在调用该函数之前,Linux系统中只有两个进程,即0号进程init_task和1号进程kernel_init,其中kernel_init进程也是刚刚被创建的。调用该函数后,1号进程kernel_init将会运行
    1. 调用cpu_idle(),0号线程进入idle函数的循环,在该循环中会周期性地检查。

2.1 初始化1号进程

init进程是启动过程中内核生成的进程,其PID为1,它生成所有用户进程并监视其运行,在系统结束前始终保持执行状态。kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是1 号进程。详细的介绍见根文件系统初见

    static int __ref kernel_init(void *unused)
    {
    	int ret;
    
    	kernel_init_freeable();
    	/* need to finish all async __init code before freeing the memory */
    	async_synchronize_full();                                         -----------------(1)
    	free_initmem();
    	mark_readonly();
    	system_state = SYSTEM_RUNNING;
    	numa_default_policy();
    
    	rcu_end_inkernel_boot();
    
    	if (ramdisk_execute_command) {                                    -----------------(2)
    		ret = run_init_process(ramdisk_execute_command);
    		if (!ret)
    			return 0;
    		pr_err("Failed to execute %s (error %d)\n",
    		       ramdisk_execute_command, ret);
    	}
    
    	/*
    	 * 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) {                                            -----------------(3)
    		ret = run_init_process(execute_command);
    		if (!ret)
    			return 0;
    		panic("Requested init %s failed (error %d).",
    		      execute_command, ret);
    	}
    	if (!try_to_run_init_process("/sbin/init") ||                     -----------------(4)
    	    !try_to_run_init_process("/etc/init") ||
    	    !try_to_run_init_process("/bin/init") ||
    	    !try_to_run_init_process("/bin/sh"))
    		return 0;
    
    	panic("No working init found.  Try passing init= option to kernel. "
    	      "See Linux Documentation/init.txt for guidance.");
    }
  • async_synchronize_full中结束所有非同步操作,准备释放内存,在free_initmem中释放所有初始化函数和函数使用的.init.data内存区域
  • 内核态的1号kernel_init进程将会转换为用户空间内的1号进程init。户进程init将根据/etc/inittab中提供的信息完成应用程序的初始化调用。然后init进程会执行/bin/sh产生shell界面提供给用户来与Linux系统进行交互,调用run_init_process()创建用户模式1号进程。

2.2 初始化2号进程

内核线程守护进程是执行内核线程的守护进程,在内核启动时生成注册到kthread_create_list的所有内核线程,下面是kthreadd函数:

    int kthreadd(void *unused)
    {
    	struct task_struct *tsk = current;                     //获取当前恩物
    
    	/* Setup a clean context for our children to inherit. */
    	set_task_comm(tsk, "kthreadd");                        //配置2号进程的名字kthreadd
    	ignore_signals(tsk);                                   //将任务信号处理设置为忽略所有信号
    	set_cpus_allowed_ptr(tsk, cpu_all_mask);            //允许kthreadd在任意CPU上运行,设置亲和性
    	set_mems_allowed(node_states[N_MEMORY]);
    
    	current->flags |= PF_NOFREEZE;
    	cgroup_init_kthreadd();
    
    	for (;;) {
    //首先将线程状态设置为 TASK_INTERRUPTIBLE,没有要创建的线程则主动放弃 CPU 完成调度.此进程变为阻塞态
    		set_current_state(TASK_INTERRUPTIBLE);
    		if (list_empty(&kthread_create_list))//没有需要创建的内核线程
    			schedule();						 //执行一次调度, 让出CPU
    		__set_current_state(TASK_RUNNING);  //运行到此表示 kthreadd 线程被唤醒
    		//设置进程运行状态为 TASK_RUNNING
    		spin_lock(&kthread_create_lock);
    		while (!list_empty(&kthread_create_list)) {                    --------------(1)
    			struct kthread_create_info *create;
    
    			create = list_entry(kthread_create_list.next,
    					    struct kthread_create_info, list);
    			list_del_init(&create->list);
    			spin_unlock(&kthread_create_lock);
    
    			create_kthread(create);
    
    			spin_lock(&kthread_create_lock);
    		}
    		spin_unlock(&kthread_create_lock);
    	}
    
    	return 0;
    }

代码1是kthread函数的核心部分,如果内核线程列表kthread_create_list不为空,就调用list_entry函数使kthread_create_info结构体的create成员执行kthread_create_list的第一个成员。生成的内核线程的信息,其定义如下:

    kthread_create_list成员struct kthread_create_info
    {
    	/* Information passed to kthread() from kthreadd. */
    	int (*threadfn)(void *data);                                 //要执行的函数
    	void *data;                                                  //传递给函数的数据
    	int node;                                                    
    
    	/* Result passed back to kthread_create() from kthreadd. */
    	struct task_struct *result;                                  //生成内核线程后的任务
    	struct completion *done;                                     //通知已结束
     
     	struct list_head list;                                       //连接到kthread_create_list成员
    };

所以该代码大致做了以下几件事情

  • 设置当前进程的名称为kthreadd,也就是task_struct的comm字段
  • 然后就是for循环,设置当前的进程状态为TASK_INTERRUPTIBLE是可以中断的
  • 判断kthread_create_list链表是否为空,如果是空就调度出去,让出CPU;如果不是空,则从链表中取出,然后调用create_kthread去创建内核线程
  • 所以所有的内核线程的父进程都是2号进程,也就是kthread

3. idle进程

Linux Kernel 会在系统启动完成后,在 Idle 进程中,处理 CPUIdle 相关的事情。在多核系统中,CPU 启动的过程是,先启动主 CPU,启动过程和传统的单核系统类似。其函数调用关系如下:

stext –> start_kernel –> rest_init –> cpu_startup_entry

而启动其它 CPU,可以有多种方式,例如 CPU hotplug 等,启动过程:

secondary_startup –> __secondary_switched –> secondary_start_kernel –> cpu_startup_entry

202306111303597651.png

在这个函数中,最终程序会掉进无限循环里 cpu_idle_loop。到此,Idle 进程创建完成,以下是 Idle 进程的代码实现

    static void cpu_idle_loop(void)
    {
    	int cpu = smp_processor_id();
    
    	while (1) {
    		__current_set_polling();
    		quiet_vmstat();
    		tick_nohz_idle_enter();                        //关闭周期tick,CONFIG_NO_HZ_IDLE必须打开
    
    		while (!need_resched()) {                      //如果系统当前不需要调度,执行后续动作
    			check_pgt_cache();
    			rmb();
    
    			if (cpu_is_offline(cpu)) {
    				cpuhp_report_idle_dead();
    				arch_cpu_idle_dead();
    			}
    
    			local_irq_disable();                      //关闭irq中断
    			arch_cpu_idle_enter();                   //arch相关的cpuidle enter
    													// 主要执行注册到 idle 的 notify callback
    			if (cpu_idle_force_poll || tick_check_broadcast_expired())
    				cpu_idle_poll();					//idle pill
    			else
    				cpuidle_idle_call();               //进入cpu的idle模式,进行省电
    
    			arch_cpu_idle_exit();                  //idle退出,主要执行注册idle的notify callback
    		}
            //如果系统当前需要调度,就退出idle进程
    		preempt_set_need_resched();
    		tick_nohz_idle_exit();                    //打开周期tick
    		__current_clr_polling();
    
    		smp_mb__after_atomic();
    
    		sched_ttwu_pending();
    		schedule_preempt_disabled();             //让出cpud,是调度器调度其他优先级更高的进程
    	}
    }

系统的周期tick可动态地关闭和打开,这个功能可以通过内核配置项CONFIG_NO_HZ打开,而IDLE正是使用这项技术,使系统尽量长时间处于空闲状态,从而尽可能节省功耗。这个内容比较多,后续再单独学习。

4. 总结

Linux启动的第一个进程是0号进程,是静态创建的,然后0号进程启动后会创建两个进程,分别是1号和2号进程

  • 0号进程是系统创建的第一个进程,也是唯一一个没有通过fork或kernel_thread产生的进程,完成加载系统后,演变为idle进程
  • 1号(init)进程由idle通过kernel_thread创建,在内核空间完成初始化后,最终会调用init可执行文件,init进程最终会去创建所有的应用进程,是其他用户进程的祖先。
  • 2号(kthread)进程由idle进程通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理

202306111304015002.png

从上面的图示可以看出,PID=1的进程是init,PID=2的进程是kthreadd,而他们的父进程PPID=0,也就是0号进程。在往后面看,所有的内核线程的PPID=2,页就是说内核线程的父进程都是kthreadd进程。

202306111304034113.png

所有用户态的进程的父进程PPID=1,也就是1号进程都是他们的父进程。其中用户态的不带中括号,内核态的带中括号。其关系图如下图所示

202306111304043384.png


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文