视频学习
一、用户态、内核态和中断
内核态:处于高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
用户态:处于低的执行级别下,代码只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
Intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0 3级分别表示内核态和用户态。cs寄存器的最低两位表明了当前代码的特权级,00或者11。
内核态cs:eip的值是任意的,即可以访问所有的地址空间。用户态只能访问其中的一部分内存地址(0x00000000-0xbbbbbbbf),0xc0000000以上的地址(逻辑地址而不是物理地址)只能在内核态下访问。
中断处理是从用户态进入内核态的主要方式,系统调用是一种特殊的中断。从用户态切换到内核态时,中断/int指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、当时的cs:eip的值,还有内核态的栈顶地址、内核态的状态字、中断处理程序的入口。中断发生后的第一件事就是保存现场,保存一系列的寄存器的值;中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。特别说明: 保护现场:就是进入中断程序,保存需要用到的寄存器的数据;恢复现场:就是退出中断程序,恢复保存寄存器的数据。
二、系统调用概述
系统调用的意义:操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性。
API和系统调用:API是一个系统调用封装成的一个函数定义;系统调用通过软中断向内核发出一个明确的请求;Libc库定义的一些API引用了封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用;一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API。
应用编程接口(application program interface, API) 和系统调用是不同的
- 不是每个API都对应一个特定的系统调用。API可能直接提供用户态的服务,比如一些数学函数;一个单独的API可能调用几个系统调用;不同的API可能调用了同一个系统调用。
- 系统调用的三层皮:xyz(API)、system_call(中断向量)、sys_xyz(中断服务程序)
- 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数(使用eax寄存器来传递)
- 寄存器传递参数具有如下限制: 1)每个参数的长度不能超过寄存器的长度,即32位 2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp) 3)超过6个怎么办?超过6个的话就把某一个寄存器作为一个指针,指向某一块内存。
API-int 0x80 陷入内核态-systemcall-调用函数
三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用
学习了通过使用13号系统调用time这个库函数来获取当前系统时间
如图所示的,"=m"表示写入到内存变量val3里面,"c"表示ecx接,"d"edx。下面的输入输出分别用0,1,2...编号表示,比如"addl %1,%%eax\n\t"就表示将输入输出的第2个,即"c"(ecx)的值加到eax中去,eax前面为什么有两个%,前一个%是转义符号。
实验
这次我实验所用的系统调用是mkdir,创建一个目录,在c语言中,该库函数如果执行成功则返回0,失败返回-1.代码如下:
#include
int main()
{
int flag;
flag = mkdir("/home/shiyanlou/testdir");
if (flag == -1)
printf("mkdir failed!\n");
else
printf("make dirctory success!\n");
return 0;
}
先使用
vi mkdir.c
命令创建mkdir.c这个文件并进行编辑(要按i才能进入编辑模式),如图:
保存退出后(先按'esc',再输入':wq'),在终端输入ls
命令,先看下当前目录下的文件和目录,如图:
再编译我们的mkdir.c文件
gcc -o makedir mkdir.c
会生成一个makedir的可执行文件,输入./makedir
来执行,会弹出生成目录成功的消息,并且可以看到当前目录下多了一个testdir的目录,如图
这说明我们的代码执行成功,下面我们更改代码,将其换成嵌入汇编的代码,如图
其中我们将要生成的目录换成了testdir2,以便与之前的区分。最后我们将其编译并执行。可以看到成功创建了新的目录testdir2。
遇到的问题
- 就是关于系统调用函数编号的问题,mkdir的编号是39,我在看实验楼给的那些系统调用列表,将前面的编号48当成编程里面的系统调用号了,总是出错,后来仔细看才发现是39,对应十六进制为0x27。
- 编写嵌入汇编代码的时候,将 asm volatile()写成了asm volatitle(),以至于编译的时候也总是报错,由于是新手,也不知道错在哪,很是痛苦,不过最终还是发现问题了。而且,我发现,这里的这个写对了volatile是会变色的,错了不会变色,以后就不会犯错了。
- 关于调用中断服务的时候,例子里面只传入eax的值就开始调用了,而我这里还有一个参数dir(保存了一串字符串)需要传递,当时有点不知道怎么做,于是上网查资料,虽然类型不一样,但是我看他们都是接着用ebx传参数,那么我想我是否也可以用ebx传参数试试?然后又想起视频里讲了寄存器传递参数的限制:在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp)。那么,传参的次序是不是也依次是ebx, ecx,edx,esi,edi,ebp呢,然后试着也用ebx进行参数的传递,果然最后成功了。所以,碰到问题,多上网搜索办法,总是可以解决的。
教材内容学习
- 中断使得硬件得以发出通知给处理器,不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志,这些中断值通常被称为中断请求IRQ线;在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序或中断服务例程;上半部:中断处理程序,下半部:稍后完成的工作;注册中断:注册中断 request_irq()。irq 中断号。handle 一个指针,指向中断处理程序。flags参数可以为0,也可能是下列一个或多个标志的位掩码。第四个参数name 与中断相关的设备的ASCII文本表示。 第五个参数dev用于共享中断线,当中断需要退出时,dev提供唯一的标志信息,从共享中断线的中断处理程序中删除指定程序。
- 中断处理程序 irqreturn_t intr_handle(int irq, void *dev_id),内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序;当执行一个中断处理程序时,内核处于中断上下文中;Linux内核提供了一组接口用于操作机器上的中断状态;中断上下文不可以睡眠,不能从中断上下文中调用睡眠函数。
- 下半部的任务就是执行与中断处理密切相关但中断处理本身不执行的工作;下半部:缩短中断屏蔽时间,执行期间可以响应所有中断。下半部的环境:BH,任务队列,软中断和tasklet,工作队列,内核定时器。
- 软中断的实现,软中断是在编译期间静态分配的,它不像tasklet那样可以动态地注册或注销;一个软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序;软中断的执行时机:(1)从一个硬件中断代码返回时;(2)在ksoftirq内核线程中;(3)显示检查和执行处;理的软中断代码中,如网络子系统中;软中断保留给系统中对时间要求最严格以及最重要的下半部使用。
- tasklet通过软中断实现,本身也是软中断,有两种:HI_SOFTIRQ和TASKLET_SOFTIRQ;state成员只能在 0、TASKLET_STATE_SCHED(已经被调度,正准备投入运行)和TASKLET_STATE_RUN(正在运行)之间取值;使用tasklet:声明tasklet、编写tesklet程序、调度tasklet、ksoftirq 辅助软中断内核线程。
- 工作队列是另外一种将工作推后执行的形式,工作队列可把工作推后,交由一个内核线程去执行。使用工作队列:创建推后的工作,工作队列处理函数,对工作进行调度,刷新操作,创建新的工作队列。
- 两个相同类型的tasklet不允许同时执行即使在不同的处理器上也不行;任何在工作队列中被共享的数据也需要使用锁机制。