Linux启动流程

Posted Jocelin47

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux启动流程相关的知识,希望对你有一定的参考价值。

Linux启动过程详解

Uboot启动阶段参考我的博客:
Uboot启动过程详解

1、内核引导阶段

(1) 处理Uboot传入到内核的参数。

内核中排布的第一个文件是: arch/arm/kernel/head.S

(2) 检查编译的内核是否支持处理器ID



__lookup_machine_type机器 ID 。
一个编译好的内核能支持哪些单板,都是定下来的。内核上电后会检测下看是否支持当前的单板。若可以支持则 继续往下跑,不支持则__error_a跳到死循环

(3) 创建页表

内核的链接地址从虚拟地址

. = (0xc0000000) + 0x00008000;

开始。这个地址并不代表真实存在的内存。我们的是从 0x3000 0000 开始的。故这里面要建立一个页表,启动 MMU 。

(4) 使能MMU


@ mmu has been enabled 当 MMU 使能后,会跳到 __switch_data 中去。如何跳到 __switch_data ,则看: __enable_mmu

在 head_common.S 文件中,__switch_data 后是:__mmap_switched

(5) 跳转到start_kernel(内核的第一个C函数)

UBOOT传进来的启动参数:

参 2:机器 ID 在 head.Skh 中会比较。
参 3:传进来的参数,就是在这个第一个 C函数 start_kernel 中处理。在 init/main.c 文件中:

进入入口函数start_kernel函数后里面就是各种初始化函数:

2. 内核启动第二阶段

(0) start_kernel启动概述

整个过程的函数包含:内核启动流( arch/arm/kernel/head.S )

start_kernel -->
	setup_arch() // 解析 UBOOT 传进来的启动参数
	mm_init_owner(&init_mm, &init_task); -->
		INIT_TASK(init_task); // 0号进程
	setup_command_line() //解析 UBOOT 传进来的启动参数
	trap_init();
	mm_init();
	sched_init();
	vfs_caches_init()
	rest_init(); -->
		kernel_thread(kernel_init, NULL, CLONE_FS); --> // 一号进程:用户态总管
			kernel_init_freeable(); 
				prepare_namespace() -->
					mount_root() // 挂接根文件系统 (识别根文件系统)
				console_on_rootfs(); -- > //打开终端 标准输入输出以及erro
					ksys_open((const char __user *) "/dev/console", O_RDWR, 0) 
					/* create stdout/stderr */
					ksys_dup(0);
					ksys_dup(0);
				//假设挂接好了根文件系统
				run_init_process()
				//再执行应用程序 
				try_to_run_init_process("/sbin/init");
				try_to_run_init_process("/etc/init");
				try_to_run_init_process("/bin/init");
				try_to_run_init_process("/bin/sh");
		kernel_thread(kthreaddd, NULL, CLONE_FS | CLONE_FILES); //二号进程:内核态总管

(1) 打印内核信息


linux_banner:在 Version.c 中

(2) Uboot传入的启动参数


UBOOT传来的启动参数有:
1、内存有多大
2、内存的起始地址


命令行参数(来源于 UBOOT 设置的“ bootargsbootargs”环境变量: getenv(“bootargx”) ))

这些 UBOOT 传进来的启动参数,就是在上面这两个函数来处理的。

static int __init customize_machine(void) 

在 arch \\arm \\kernelkernel\\Setup.c 中。

static void __init setup_command_line(char *command_line)

(3) 0号进程

在操作系统里面,先要有个创始进程,有一行指令 set_task_stack_end_magic(&init_task)

这里面有一个参数 init_task,它的定义是 struct task_struct init_task = INIT_TASK(init_task)

它是系统创建的第一个进程,我们称为0号进程。这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。

(4) 响应中断

这里面对应的函数是 trap_init(),里面设置了很多中断门(Interrupt Gate),用于处理各种中断。其中有一个 set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32),这是系统调用的中断门。系统调用也是通过发送中断的方式进行的。当然,64 位的有另外的系统调用方法。

(5) 初始化内存管理模块

mm_init() 用来初始化内存管理模块

(6) 初始化调度模块

sched_init() 用于初始化调度模块。

(7) 初始化文件系统

vfs_caches_init() 会用来初始化基于内存的文件系统 rootfs。在这个函数里面,会调用 mnt_init()->init_rootfs()。这里面有一行代码,register_filesystem(&rootfs_fs_type)。在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type。

为了兼容各种各样的文件系统,我们需要将文件的相关数据结构和操作抽象出来,形成一个抽象层对上提供统一的接口,这个抽象层就是 VFS(Virtual File System),虚拟文件系统。

(8) 最后调用rest_init()

最后,start_kernel() 调用的是 rest_init(),用来做其他方面的初始化,这里面做了好多的工作。

①初始化 1 号进程

rest_init 的第一大工作是,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是1号进程。

1 号进程对于操作系统来讲,它将运行一个用户进程。有了1号进程,后面就可以形成很多的进程。

从内核态到用户态

当前执行 kernel_thread 这个函数的时候,我们还在内核态。那如何从内核态转换到用户态去执行?

kernel_thread 的参数是一个函数 kernel_init,也就是这个进程会运行这个函数。在 kernel_init 里面,会调用 kernel_init_freeable(),里面有这样的代码:

if (!ramdisk_execute_command)
		ramdisk_execute_command = "/init";

在kernel_init里面还有

if (ramdisk_execute_command) 
		ret = run_init_process(ramdisk_execute_command);

	

	if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

这就说明,1号进程运行的是一个文件。如果我们打开run_init_process函数,会发现它调用的是 do_execve。

② run_init_process 启动第一个应用程序

系统调用的时候,execve 是一个系统调用,它的作用是运行一个执行文件。加一个 do_ 的往往是内核系统调用的实现。它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init” “/etc/init” “/bin/init” “/bin/sh”。不同版本的 Linux 会选择不同的文件启动,但是只要有一个起来了就可以。

static int run_init_process(const char *init_filename)

	argv_init[0] = init_filename;
	return do_execve(getname_kernel(init_filename),
		(const char __user *const __user *)argv_init,
		(const char __user *const __user *)envp_init);

系统调用的过程是 “用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态”,然后接着运行。而咱们刚才运行 init,是调用 do_execve,正是上面的过程的后半部分,从内核态执行系统调用开始。

do_execve->do_execveat_common->exec_binprm->search_binary_handler,这里面会调用这段内容:

int search_binary_handler(struct linux_binprm *bprm)

  ......
  struct linux_binfmt *fmt;
  ......
  retval = fmt->load_binary(bprm);
  ......

也就是说,我要运行一个程序,需要加载这个二进制文件。Linux 常用的格式是ELF(Executable and Linkable Format,可执行与可链接格式)。于是我们就有了下面这个定义:

static struct linux_binfmt elf_format = 
.module	= THIS_MODULE,
.load_binary	= load_elf_binary,
.load_shlib	= load_elf_library,
.core_dump	= elf_core_dump,
.min_coredump	= ELF_EXEC_PAGESIZE,
;

这其实就是先调用 load_elf_binary,最后调用 start_thread。

void
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_iret();

EXPORT_SYMBOL_GPL(start_thread);

这里就是设置用户态的寄存器。struct pt_regs,看名字里的 register,就是寄存器。这个结构就是在系统调用的时候,内核中保存用户态运行上下文的,里面将用户态的代码段 CS 设置为 __USER_CS,将用户态的数据段 DS 设置为 __USER_DS,以及指令指针寄存器 IP、栈指针寄存器 SP。这里相当于补上了原来系统调用里,保存寄存器的一个步骤。

最后的 iret 是用于从系统调用中返回。这个时候会恢复寄存器。从哪里恢复呢?按说是从进入系统调用的时候,保存的寄存器里面拿出。好在上面的函数补上了寄存器。CS 和指令指针寄存器 IP 恢复了,指向用户态下一个要执行的语句。DS 和函数栈指针 SP 也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。

ramdisk 的作用

init 终于从内核到用户态了。一开始到用户态的是 ramdisk 的 init,后来会启动真正根文件系统上的 init,成为所有用户态进程的祖先。

ramdisk 其实是一个基于内存的文件系统。 init 程序是在文件系统上的,文件系统一定是在一个存储设备上的,例如硬盘。Linux 访问存储设备,要有驱动才能访问。如果存储系统数目很有限,那驱动可以直接放到内核里面,反正前面我们加载过内核到内存里了,现在可以直接对存储系统进行访问。

如果所有的存储系统的驱动都默认放进内核,内核就太大了。只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是 ramdisk。这个时候,ramdisk 是根文件系统。

然后,我们开始运行 ramdisk 上的 /init。等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk 上的 /init 会启动文件系统上的 init。

接下来就是各种系统的初始化。启动系统的服务,启动控制台,用户就可以登录进来了。

③创建 2 号进程

用户态由init进程管理后,rest_init 第二大事情就是第三个进程,管理内核进程的 2 号进程。

kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 又一次使用 kernel_thread 函数创建进程。这里的函数 kthreadd,负责所有内核态的线程的调度和管理,是内核态所有线程运行的祖先。

以上是关于Linux启动流程的主要内容,如果未能解决你的问题,请参考以下文章

Linux启动流程

Linux嵌入式驱动学习之路⑦Linux内核启动流程

分享在MPSoC ZCU106单板上运行Docker

Linux日志文件总管——logrotate

Linux日志文件总管——logrotate

Linux日志文件总管——logrotate