目态,管态
大多数计算机系统将CPU执行状态分为目态与管态。CPU的状态属于程序状态字PSW的一位。CPU交替执行操作系统程序和用户程序。
管态又叫特权态,系统态或核心态。CPU在管态下可以执行指令系统的全集。通常,操作系统在管态下运行。
目态又叫常态或用户态。机器处于目态时,程序只能执行非特权指令。用户程序只能在目态下运行,如果用户程序在目态下执行特权指令,硬件将发生中断,由操作系统获得控制,特权指令执行被禁止,这样可以防止用户程序有意或无意的破坏系统。
从目态转换为管态的唯一途径是中断。
从管态到目态可以通过修改程序状态字来实现,这将伴随这由操作系统程序到用户程序的转换。
特权级
特权级(Ring)也叫(hierarchical protection domains),有的也称为用户态(user mode)。它是一种机制来保护数据和阻止恶意行为(确保计算机安全)。电脑操作系统提供不同权限访问级别的资源。特权级分为4级,特权级0、1、2、3。
在windows中只使用特权级0和特权级3。特权最高的一般是特权级0,可以直接操作硬件,如CPU和内存。一般操作系统和驱动运行在此级别下。特权级3是给一般的程序使用的,可以调用基本的CPU指令。在特权级三无法调用特权级0的指令,如果调用则显示为非法指令。
引用特权级的概念是为了保护计算机,一些危险指令只有操作系统可以执行,防止普通程序滥用其他程序的资源。如间谍软件要想开启摄像头就必须向特权级0的驱动程序请求开启,否则就不允许。
中断门、调用门之类的。当然,也可以直接写一个驱动,因为驱动一般是ring0级别的,所以,只要驱动被加载,就意味着进入了ring0.
在系统的表象上看,我们打开任务管理器,可以看到一个名称为“System”的进程,这个事实上就是ring0的进程。所有的驱动都显示为在这个进程中,所有的驱动中创建的系统线程都是在这个进程中。表面上看,好像是ring3的进程,事实上,它是工作在ring0的。
用户空间与内核空间
我们知道现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。空间分配如下图所示:
有了用户空间和内核空间,整个linux内部结构可以分为三部分,从最底层到最上层依次是:硬件–>内核空间–>用户空间。如下图所示:
需要注意的细节问题:
(1) 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
(2) Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。
内核态和用户态
内核态与用户态:
(1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
(2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。
内核态与用户态的切换:
当进程运行在用户态,此时无论发生中断、异常、系统调用(本质是中断),都会通过异常向量表进入系统状态(内核态),执行完中断服务程序时又恢复原状,返回到用户态。主要步骤:
[1] 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
[2] 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
[3] 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。
其实写过linux内核驱动程序的同学应该就知道,实现一个字符设备驱动,在write方法和read方法中,内核态和用户态之间的桥梁就是copy_to_user和copy_form_user这两个接口,在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态,而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。其实正好说明了这个问题,程序员或者软件应用工程师在编写应用程序去控制设备驱动的时候,首先肯定是会打开相应的文件描述符,然后对相应的文件描述符进行读写,ioctl,lseek之类的操作。当在应用层编写程序即是属于用户态,在应用程序不能访问任意的硬件物理地址,所以当用户需要读取文件描述符的内的内容时,就需要调用read,当用户需要写数据进文件描述符时,就需要用write,在用户态调用这两个接口,进而就会进行系统调用,产生相应的系统调用号,然后内核会根据系统调用号找到相应的驱动程序,此时系统就处在内核态中,在驱动程序中,首先进行驱动程序初始化,然后注册,产生驱动程序最重要主设备号和次设备号。初始化的过程中由于对相应的read方法和wirte方法进行初始化,故用户态执行read操作,就会进而使CPU产生异常,然后进入内核态找到相应的read,write当然也是一样的。
究竟什么是用户态,什么是内核态,这两个基本概念以前一直理解得不是很清楚,根本原因个人觉得是在于因为大部分时候我们在写程序时关注的重点和着眼的角度放在了实现的功能和代码的逻辑性上,先看一个例子:
1)例子
C代码
void testfork(){
if(0 = = fork()){
printf(“create new process success!\n”);
}
printf(“testfork ok\n”);
}
这段代码很简单,从功能的角度来看,就是实际执行了一个fork(),生成一个新的进程,从逻辑的角度看,就是判断了如果fork()返回的是则打印相关语句,然后函数最后再打印一句表示执行完整个testfork()函数。代码的执行逻辑和功能上看就是如此简单,一共四行代码,从上到下一句一句执行而已,完全看不出来哪里有体现出用户态和进程态的概念。
如果说前面两种是静态观察的角度看的话,我们还可以从动态的角度来看这段代码,即它被转换成CPU执行的指令后加载执行的过程,这时这段程序就是一个动态执行的指令序列。而究竟加载了哪些代码,如何加载就是和操作系统密切相关了。
2)特权级
熟悉Unix/Linux系统的人都知道,fork的工作实际上是以系统调用的方式完成相应功能的,具体的工作是由sys_fork负责实施。其实无论是不是Unix或者Linux,对于任何操作系统来说,创建一个新的进程都是属于核心功能,因为它要做很多底层细致地工作,消耗系统的物理资源,比如分配物理内存,从父进程拷贝相关信息,拷贝设置页目录页表等等,这些显然不能随便让哪个程序就能去做,于是就自然引出特权级别的概念,显然,最关键性的权力必须由高特权级的程序来执行,这样才可以做到集中管理,减少有限资源的访问和使用冲突。
特权级显然是非常有效的管理和控制程序执行的手段,因此在硬件上对特权级做了很多支持,就Intel x86架构的CPU来说一共有0~3四个特权级,0级最高,3级最低,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查,相关的概念有 CPL、DPL和RPL,这里不再过多阐述。硬件已经提供了一套特权级使用的相关机制,软件自然就是好好利用的问题,这属于操作系统要做的事情,对于 Unix/Linux来说,只使用了0级特权级和3级特权级。也就是说在Unix/Linux系统中,一条工作在级特权级的指令具有了CPU能提供的最高权力,而一条工作在3级特权级的指令具有CPU提供的最低或者说最基本权力。
3)用户态和内核态
现在我们从特权级的调度来理解用户态和内核态就比较好理解了,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在级特权级上时,就可以称之为运行在内核态。
虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序,比如上面例子中的testfork()就不能直接调用 sys_fork(),因为前者是工作在用户态,属于用户态程序,而sys_fork()是工作在内核态,属于内核态程序。
当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,比如testfork()最初运行在用户态进程下,当它调用fork()最终触发 sys_fork()的执行时,就切换到了内核态。
- 用户态和内核态的转换
1)用户态切换到内核态的3种方式
a. 系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
2)具体的切换操作
从触发方式上看,可以认为存在前述3种不同的类型,但是从最终实际完成由用户态到内核态的切换操作上来说,涉及的关键步骤是完全一致的,没有任何区别,都相当于执行了一个中断响应的过程,因为系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的,关于它们的具体区别这里不再赘述。关于中断处理机制的细节和步骤这里也不做过多分析,涉及到由用户态切换到内核态的步骤主要包括:
[1] 从当前进程的描述符中提取其内核栈的ss0及esp0信息。
[2] 使用ss0和esp0指向的内核栈将当前进程的cs,eip,eflags,ss,esp信息保存起来,这个
过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一
条指令。
[3] 将先前由中断向量检索得到的中断处理程序的cs,eip信息装入相应的寄存器,开始
执行中断处理程序,这时就转到了内核态的程序执行了。
本文参考:http://blog.sina.com.cn/s/blog_69f141290100ul02.html
http://www.aichengxu.com/view/24604997