linux学习0:内核初始化

内核初始化

内核的启动从入口函数 start_kernel() 开始。在 init/main.c 文件中,start_kernel 相当于内核的 main 函数。

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

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

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

初始化模块:

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。

最后,start_kernel() 调用的是 rest_init(),用来做其他方面的初始化。

初始化 1 号进程

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

 

当一个用户态的程序运行到一半,要访问一个核心资源,例如访问网卡发一个网络包,就需要暂停当前的运行,调用系统调用,接下来就轮到内核中的代码运行了。

首先,内核将从系统调用传过来的包,在网卡上排队,轮到的时候就发送。发送完了,系统调用就结束了,返回用户态,让暂停运行的程序接着运行。

这个暂停怎么实现呢?其实就是把程序运行到一半的情况保存下来。例如,我们知道,内存是用来保存程序运行时候的中间结果的,现在要暂时停下来,这些中间结果不能丢,因为再次运行的时候,还要基于这些中间结果接着来。另外就是,当前运行到代码的哪一行了,当前的栈在哪里,这些都是在寄存器里面的。(就是所谓的上下文切换,环境保护)

暂停的那一刻,要把当时 CPU 的寄存器的值全部暂存到一个地方,这个地方可以放在进程管理系统很容易获取的地方。当系统调用完毕,返回的时候,再从这个地方将寄存器的值恢复回去,就能接着运行了。

这个过程就是这样的:用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态,然后接着运行

 

从内核态到用户态

1 号进程启动的过程:

当前执行 kernel_thread 这个函数的时候,我们还在内核态。ernel_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。execve 是一个系统调用,它的作用是运行一个执行文件。加一个 do_ 的往往是内核系统调用的实现。它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init”“/etc/init”“/bin/init”“/bin/sh”。


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);
}

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 也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。

创建 2 号进程

kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 又一次使用 kernel_thread 函数创建进程。这里函数名 thread 可以翻译成“线程”,这也是操作系统很重要的一个概念。为什么这里创建的是进程,函数名却是线程呢?

从内核态来看,无论是进程,还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值