copy from:https://yq.aliyun.com/articles/559628
本文主要阐述,内核态,怎么样启动到用户态的;
代码在start_kernel函数运行的最后到了rest_init()函数中
1:rest_init()函数分析
(1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd
(2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了。
(3)rest_init最终调用cpu_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。
(4)简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。
(5)之前已经启动了内核调度系统,调度系统会负责考评系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止cpu_idle死循环进程(空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。
2.1:什么是内核线程
(1)进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。
独立的意思就是这个程序和别的程序是分开的,这个程序可以被内核单独调用执行或者暂停。
(2)在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是
一样的。
(3)进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行
一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。
(4)所以我们kernel_thead函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,以后人家调度的时候会考虑你。
2.2:进程0、进程1、进程2
(1)操作系统是用一个数字来表示/记录一个进程/线程的,这个数字就被称为这个进程的进程号。这个号码是从0开始分配的。因此这里涉及到的三个进程分别是linux系统的进程0、进程1、进程2.
(2)在linux命令行下,使用ps命令可以查看当前linux系统中运行的进程情况。
(4)我们在ubuntu下ps -aux可以看到当前系统运行的所有进程,可以看出进程号是从1开始的。为什么不从0开始,因为进程0不是一个用户进程,而属于内核进程。
进程0:进程0其实就是刚才讲过的idle进程,叫空闲进程,也就是死循环。
进程1:kernel_init函数就是进程1,这个进程被称为init进程。
进程2:kthreadd函数就是进程2,这个进程是linux内核的守护进程。它的作用是管理调度其他内核进程这个进程是用来保证linux内核自己本身能正常工作的。
3:init进程分析
需要注意的一点是这个进程刚开始运行的时候是内核态,是属于内核进程,然后它自己运行了一个用户太下面的程序后把自己强行转成了用户态,因为init进程自身完成了从内核态到用户态的过渡,所以后续的其他进程都可以工作在用户态下面了
3.1:init进程在内核态下做了什么
重要的点就挂载根文件系统,并试图找到用户态下的那个init程序,原因是init进程要完成从内核态到用户态的转变就必须去运行一个用户态的应用程序,而内核源代码中的程序都是属于内核态的,所以这个应用程序必须不属于内核源代码,这样才能保证自己是用户态,所以这个应用程序就的是由另外一份文件提供,即根文件系统
3.2: init进程在用户态下做了什么
init进程大部分有意义的工作都是在用户态下进行的,原因是用户态下的所有进程都是直接或者间接由init进程生成的。
3.3:如何从内核态跳跃到用户态?还能回来不?
init进程在内核态下面时,通过调用kernel_execve函数来执行一个用户空间编译链接的应用程序就跳跃到了用户态下面了,需要注意的是,这个跳跃的过程进程号并没有改变还是进程1,并且这个跳跃是单向的,以后要从用户态回到内核态只有走API这一条路了
kernel_execve函数被调用的路径start_kernel->rest_init->kernel_thread->kernel_init->init_post->run_init_process->kernel_execve
4:init进程在内核态下的分析(也就是kernel_init函数)
4.1:打开控制台,代码如下:
1
2
3
4
5
|
/* Open the /dev/console on the rootfs, this should never fail */ if (sys_open(( const char __user *) "/dev/console" , O_RDWR, 0) < 0) printk(KERN_WARNING "Warning: unable to open an initial console.
" ); ( void ) sys_dup(0); ( void ) sys_dup(0); |
(1)linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。
(2)linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。打开一个设备的文件就会得到这个设备的文件描述符(或者是文件描述符的编号),这个编号就代表这个设备,以后操作这个设备就用这个文件描述符来操作它
(3)这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。
(4)进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符
4.2:挂载根文件系统,代码如下
1
2
3
4
|
if (sys_access(( const char __user *) ramdisk_execute_command, 0) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); } |
(1)prepare_namespace函数中挂载根文件系统
(2)根文件系统在哪里?根文件系统的文件系统类型是什么? uboot通过传参来告诉内核这些信息。uboot传参中的root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里uboot传参中的rootfstype=ext3这一句就是告诉内核rootfs的类型。
(3)如果内核挂载根文件系统成功,则会打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.如果挂载根文件系统失败,则会打印:No filesystem could mount root, tried: yaffs2
(4)如果内核启动时挂载rootfs失败,则后面肯定没法执行了。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候大家会看到反复重启的情况。
(5)如果挂载rootfs失败,可能的原因有:最常见的错误就是uboot的bootargs设置不对。rootfs烧录失败(fastboot烧录不容易出错,以前是手工烧录很容易出错)rootfs本身制作失败的。(尤其是自己做的rootfs,或者别人给的第一次用)
5:执行用户态下的进程1程序
(1)上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,
这个程序就是用户空间的进程1.找到后用run_init_process(里面的kernel_execve函数)去执行他
(2)我们如果确定init程序是谁?方法是:先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。如果uboot传参cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。
第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh。
如果以上都不成功,则kernel启动失败