Linux学习之路-------内核初始化

本文围绕Linux内核启动展开,介绍了从入口函数start_kernel()开始的初始化过程,包括创建0号、1号进程,初始化中断、内存、调度等模块。阐述了从内核态到用户态的转换过程,说明了ramdisk作为根文件系统的作用,最后提到创建负责内核态线程调度管理的二号进程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内核的启动从入口函数start_kernel()开始。在init/main.c文件中,start_kernel相当于内核的main函数。打开这个函数,你会发现,里面是各种个样初始化函数XXXX_init。
在这里插入图片描述

初始化:

首先是项目管理部门,在操作系统中,有一个创始进程,有一行指令set_task_stack_end_magic(&init_task)。这里面有一个参数init_task,它的定义是struct task_struct init_task=INIT_TASK(init_task)。它是系统创建的第一个进程,我们称为0号进程。这是唯一一个没有通过fork或者kernel_thread产生的进程,是进程列表(Procese List)的第一个。

第二个要初始化的是办事大厅,来响应客户需求。这里面对应的函数是trap_init(),里面设置了很多中断门(Interrupt Gate),用于处理各种中断。其中有一个set_system_intr_gate(IA32_SYSCALL_VECTOR,entry_INT80_32),这是系统调用的中断门。系统调用也是通过发送中断的方式进行的。当然,64位的有另外的系统调用的方法。

接下来要初始化的是咱们的会议室管理系统。对应的,mm_init()就是用来初始化内存管理模块。项目需要项目管理进行调度,需要执行一定的调度策略。sched_init()就是用于初始化调度模块。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),虚拟文件系统。

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

  • reset_init()的第一大工作是,用kernel_thread(kernel)init,NULL,CLONE_FS)创建第二个进程,这个是1号进程。有了多个进程就要开始对进程访问的资源进行一些限制,好在x86提供了分层的权限机制,把区域分成了四个Ring,越往里权限越高。

    操作系统很好的利用了这个机制,将能够访问关键资源的代码放在Ring0,我们称为内核态;将普通的程序代码放在Ring3,我们称为用户态

当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的,那如果用户态的代码想要访问核心资源,怎么办呢?这个过程是:用户态—系统调用—保存寄存器—内核态执行系统调用—恢复寄存器—返回用户态,然后接着执行。
在这里插入图片描述

从内核态到用户态

我们再回到1号进程启动的过程。当前执行kernel_thread这个函数的时候,我们还在内核态,现在我们就来跨越这道屏障,到用户态去运行一个程序。这该怎么办呢?

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

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

先不管ramdisk是啥,我们回到kernel_init里面。这里有这样的代码块:

	if (ramdisk_e
	xecute_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_exccve。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文件的机会,从内核态回到用户态呢?我们从系统调用的过程可以得到启发,“用户态–系统调用–保存寄存器–内核态执行系统调用–恢复寄存器–返回用户态”,然后接着运行。而咱们刚才运行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。于是我们就有了下面的这个定义:

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的作用

ramdisk是根文件系统,在内核启动时,我们会配置一个参数:

initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img

这是一个基于内存的文件系统。为啥会有这个呢?

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

但是存储系统越来越多,如果市面上的存储系统的驱动默认都放进内核,内核就太大了。这怎么办?

我们只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是ramdisk。这个时候,ramdisk是根文件系统。

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

这时,我们仅仅形成了用户态的所有进程的祖先。

创建二号进程

kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES)又一次使用kernel_thread函数创建进程。这里需要指出一点,函数名thread可以翻译为线程,这也是操作系统很重要的一个概念。从内核态来看,无论是进程还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。

这里的函数kthreadd,负责所有内核态的线程的调度与管理,是内核态所有线程运行的祖先。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值