一、用户态、内核态
开篇,我们先来了解下什么是用户态、内核态。
一般现代CPU都有几种不同的指令执行级别。
在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。
而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。
举例:
intel x86 CPU有四种不同的执行级别0-3,linux只使用了其中的0级和3级分别来表示内核态和用户态。
二、如何区分用户态和内核态?
一般来说在linux中,地址空间是一个显著的标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问。
注意:这里所说的地址空间是逻辑地址而不是物理地址。
孟宁老师在讲解内核知识点已经把这个知识点最精华的部分提取出来了,那么到底内核中有什么样的接口是跟老师说的相关的呢?
其实写过linux内核驱动程序的同学应该就知道,实现一个字符设备驱动,在write方法和read方法中,内核态和用户态之间的桥梁就是copy_to_user和copy_form_user这两个接口,因为在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态,而在相应的低级别执行状态下,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。其实正好说明了这个问题,程序员或者软件应用工程师在编写应用程序去控制设备驱动的时候,首先肯定是会打开相应的文件描述符,然后对相应的文件描述符进行读写,ioctl,lseek之类的操作。当在应用层编写程序即是属于用户态,在应用程序不能访问任意的硬件物理地址,所以当用户需要读取文件描述符的内的内容时,就需要调用read,当用户需要写数据进文件描述符时,就需要用write,在用户态调用这两个接口,进而就会进行系统调用,产生相应的系统调用号,然后内核会根据系统调用号找到相应的驱动程序,此时系统就处在内核态中,在驱动程序中,首先进行驱动程序初始化,然后注册,产生驱动程序最重要主设备号和次设备号。初始化的过程中由于对相应的read方法和wirte方法进行初始化,故用户态执行read操作,就会进而使CPU产生异常,然后进入内核态找到相应的read,write当然也是一样的。
当然,我的分析依然还是处于非常片面的,只能说个大概,因为操作系统在执行系统调用的过程依然是非常复杂的,但是复杂归复杂,对于这样的一个流程我们还是应当要去了解清楚。
还有一个例子就是,假设我需要实现一个led驱动或者其它的驱动,在内核驱动中,我需要将相应的物理地址ioremap成为一个虚拟地址,当驱动调用结束后,还应当取消相应的地址映射,这其实就是在内核态进行的操作,因为在内核中,访问这些地址以虚拟地址的形式进行相应的内存分配。为了使软件访问I/O内存,必须为设备分配虚拟地址.这就是ioremap的工作。
以上是Linux内核中断以及异常需要了解的基础。
三、中断基础
中断,通常被定义为一个事件。打个比方,你烧热水,水沸腾了,这时候你要去关掉烧热水的电磁炉,然后再去办之前手中停不下来的事情。那么热水沸腾就是打断你正常工作的一个信号机制。当然,还有其它的情况,我们以后再做分析。
中断也就是这样产生的,中断分为同步中断还有异步中断。
同步中断在Intel的手册中被称为异常,而异步中断被称作中断。打个比方在ARM处理器的异常种类就有不少,有未定义指令异常,软中断异常,快中断异常等等。异常是由程序错误产生的,或者是内核必须处理的异常条件产生的。如果你曾经学过单片机,那么你一定会清楚,51单片机的P32,P33是外部中断0和1,假设当你在程序中开启了外部中断0,然后在中断中执行了相应的程序,这时你在外部中断0的一脚连接一个按键,这时候你按下去P30这个引脚就会产生一个中断。那么中断服务程序就会响应你的操作,比如点亮一个LED灯,或者说让蜂鸣器叫一下。
那么在linux内核中的中断其实也是和单片机类似的,只不过linux内核的中断定义的比较丰富,但是基本思想还是一样的。linux内核处理中断有一种叫做中断信号的机制。它的作用就是当一个中断信号到来时,CPU必须停止它当然正在做的事情,然后切换到一个新的活动,为了做到这一点,内核态堆栈保存的程序计数器的当前值,其实就是eip和cs寄存器的存储数据,然后把中断相关类型的一个地址放到一个程序计数器当中去。
其实在内核中,中断这样的切换机制很像进程的调度,上下文切换这样的机制,但是依然存在着一个非常明显的差异,那就是中断或者异常在处理的代码并不是一个进程。
中断信号的来临必将会引起中断的处理,那么中断处理必须要满足以下的约束:
1、linux内核在响应中断以后必须要进行的操作分为两部分:我们把非常重要的,非常紧急的处理程序让内核立即去运行。剩下的有延时的部分就让它后面再去执行。这样也就验证了水沸腾,而人停下手中的事去关电磁炉,再回去做他的事一样的道理。
2、中断编写的程序必须编写成可以使内核控制的路径能以嵌套的方式来执行,或者说,当最后一个内核控制路径终止的时候,内核必须能恢复被中断进程的执行,或者说,中断信号已经导致了进程重新调度,内核能切换到另外一个进程。这是我们分析的另外一种情况,水开了,人去关电磁炉,然后人接着做事,这是第一种情况。水开了,人去关电磁炉,接下来门铃响了,客人来了,你必须去迎接客人,然后就打断了你之前在做的事情,也就是客人来了打断了你正在做的这件事进入到与陪客的阶段。
3、在临界区中,中断必须要被禁止。临界区其实就是加锁和去锁的实现。程序员将非常关键的步骤放进临界区,就是为了防止中断或者其它的信号去影响到它,其实在内核中这样的步骤是有必要的。临界区的代码必须在短时间内被执行,而不应该出现延时的操作,且必须尽可能的去限制这样的临界区,因为,内核在处理中断程序的时候,应该是在大部分时间以开中断的方式去运行。
在intel的文档中,中断和异常通常分为几类:中断有可屏蔽的,不可屏蔽的。异常有处理器探测异常,这就包括故障的产生,陷阱,异常的中止,还有编程异常的状况。每个中断和异常都是由0-255之间的一个数来标识。intel管这东西叫向量。
其实在ARM中就有那么一张表叫异常向量表,那就是我刚刚文章里说过的那几个。
在linux中也有这么一张表:
在linux内核中,每一个能够发出中断请求的硬件设备控制器都有一条名为IRQ的输出线。所有现在存在的IRQ线都与一个名为可编程中断控制器的硬件电路的输入引脚相连,上次讲到单片机的时候,我就讲到了单片机中断的一些概念。我们现在来看一幅图,更好说明一个问题:
下面的这幅图是51单片机的一个关于矩阵键盘的学习的一个proteus的仿真电路图。
其中P3.2和P3.3为外部中断引脚,当可编程控制器(51MCU)收到外部中断响应的时候,会执行一些特定的操作,当然这需要开发者去编写一个中断初始化程序和一个中断服务程序。
那么,可编程中断控制器会做以下的操作:
1、监视IRQ线,我们可以理解就是监视单片机外部中断的IO口,检查产生的信号。如果有条或者两条以上的IRQ上产生信号,就选择引脚编号较小的IRQ线。
2、如果一个引发信号出现在IRQ线上:
a.把接收到的引发信号转换成对应的向量。
b.把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读这个向量。
c.把引发信号发送到处理器的INTR引脚,即会产生一个中断。
d.等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这种情况发送时,清INTR线。
3、最后一步返回到第一步继续监视,然后依次执行。
当然,也存在着一些更加高级的可编程中断控制器,其中ARM算一种,Intel也是,等等。。。单片机算是最简单的一种。像多APIC系统的结构,会存在以下的一个图的关系:
中断信号通过IO引脚,然后通过中断控制器I2C总线与相应的CPU进行通信。
一般情况下,有两种分发的方式:
1、静态分发模式:IRQ信号传递给重定向表相应的项中所列出的本地APIC,然后中断立即传递一个特定的CPU,或者是一组CPU,或者是所有的CPU。其实这是广播模式的一种模型,接触过UNIX网络编程应该会知道。
2、动态分发模式:如果处理器正在执行最低优先级的进程,IRQ信号线就会传递给这种处理器的本地APIC。也就是说,在CPU内部有一个控制优先级的寄存器,用来计算当前运行进程的优先级。如果两个或者多个CPU共享最低优先级,那么就利用仲裁的技术在这些CPU之间分配负荷等等的形式。
异常有很多种,在8086处理器可以找到多达20种不同的异常,内核必须为每种异常提供一个专门的异常处理程序。对于某些异常,CPU控制单元会在开始执行异常处理程序前产生一个硬件出错码,并且压入内核态的堆栈中去。
关于这个异常处理信息,我们有必要来了解以下perror这个函数。
perror( ) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno(这里的说法不准确,errno是一个宏,该宏返回左值) 的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。
用法:
void perror(const char *s); perror ("open_port");
我们写段代码来看看就知道了:
#include <stdio.h> #include <stdlib.h> int main(void) { FILE *filp = NULL; filp = fopen("txt","r"); if(NULL == filp) { perror("没有相应的文件"); } fclose(filp); return 0 ; }
运行结果:
在当前目录下,找不到txt这个文件,所以perror会根据相应的出错信息打印No such file or directory。
看了这个函数的应用,相信更会理解上面的异常的相关知识。当然还有更多日常写程序发现的BUG,比如段错误,段错误是最常见的,一些初学者在使用指针的时候,没有分配相应的空间,这时候给指针赋值,虽然没有语法错误,但可能会有警告。当程序运行的时候,就会自动退出并提示段错误(Segment fault),这一般是在linux上会出现这两个英语单词,在window的Devcpp上是这样,:
段错误的产生原因有很多种,程序在进行递归的时候,如果没有相应的条件退出的话,程序一旦进行死循环递归之后就会产生爆栈错误,也就是栈被挤爆了,栈这个概念其实并不陌生。我们在写C语言程序的时候,一旦写了一个子函数,那就相当于建立了一个堆栈,一般情况下函数在执行完退出后堆栈是自动分配,自动销毁的,不用程序员去手动malloc申请内存再free释放内存。因为手动分配的内存是用了堆区的内存,而自动分配是在栈区进行分配的。在32位操作系统上,栈的大小就只有12M,所以写代码的时候,一定要记得防止爆栈错误的产生,特别是递归!在main函数中多写些子函数是有好处的,要养成良好的编程习惯。
四、Linux中断应用
接下来我们结合一个真实的驱动案例来描述linux内核中驱动的中断机制,首先我们先了解一下linux内核中提供的中断接口。
这个接口我们需要包含一个头文件:#include <linux/interrupt.h>,在Linux中断编程中,最重要的是要了解以下的接口函数:
1、这个是请求中断函数
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) irq: 中断号 arch/arm/plat-s3c64xx/include/plat/irqs.h handler: 中断处理函数 irqreturn_t handler(int irq, void *dev_id); irqreturn_t: See include/linux/irqreturn.h irqflags: See line 21-59 in include/linux/interrupt.h 使用IRQF_SHARED共享irq时, irqflags必须相同 如: request_irq(IRQ_EINT(0), handler1, IRQF_TRIGGER_FALLING | IRQF_SHARED, "dev1", &dev1); request_irq(IRQ_EINT(0), handler2, IRQF_TRIGGER_FALLING | IRQF_SHARED, "dev2", &dev2); devname: 设备名, cat /proc/interrupts dev_id: 发生中断时将dev_id传递给handler函数, irqflags含有IRQF_SHARED时dev_id不能为NULL, 并且要保证唯一 dev_id一般采用当前设备的结构体指针
2、释放中断
void free_irq ( unsigned int irq, void * dev_id); 释放匹配irq和dev_id的中断, 如果irq有多个相同的dev_id, 将释放第一个 So, 共享中断的dev_id不是唯一时, 可能会释放到其它设备的中断
3、开启中断
void enable_irq(unsigned int irq); 开启irq号中断
4、关闭中断
void disable_irq(unsigned int irq); 关闭irq号中断
5、关闭当前CPU中断并保存在flag中去
void local_irq_save(unsigned long flags);
6、恢复flag到CPU中去
void local_irq_restore(unsigned long flags); 恢复flags到当前CPU
7、关闭当前的CPU中断
void local_irq_disable(void);
8、开始当前的CPU中断
void local_irq_enable(void);
接下来我们来看一个按键中断的例子,这个例子是基于Tiny4412按键驱动的源码:
使用的Linux3.5的内核,虽然版本有点老,但作为开发者来说,其实并没有什么差别。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/poll.h> #include <linux/sched.h> #include <linux/irq.h> #include <asm/irq.h> #include <asm/io.h> #include <linux/interrupt.h> #include <asm/uaccess.h> #include <mach/hardware.h> #include <linux/platform_device.h> #include <linux/cdev.h> #include <linux/miscdevice.h> #include <linux/gpio.h> #include <mach/map.h> #include <mach/gpio.h> #include <mach/regs-clock.h> #include <mach/regs-gpio.h> //设备名称 #define DEVICE_NAME "buttons" struct button_desc { int gpio; int number; char *name; struct timer_list timer; }; //定义按键相关的寄存器 static struct button_desc buttons[] = { { EXYNOS4_GPX3(2), 0, "KEY0" }, { EXYNOS4_GPX3(3), 1, "KEY1" }, { EXYNOS4_GPX3(4), 2, "KEY2" }, { EXYNOS4_GPX3(5), 3, "KEY3" }, }; //存储按键的键值 static volatile char key_values[] = { '0', '0', '0', '0', '0', '0', '0', '0' }; //创建一个等待队列头并初始化 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); static volatile int ev_press = 0; //按键定时器 static void tiny4412_buttons_timer(unsigned long _data) { struct button_desc *bdata = (struct button_desc *)_data; int down; int number; unsigned tmp; //获取按键的值 tmp = gpio_get_value(bdata->gpio); //判断是否为低电平 down = !tmp; printk(KERN_DEBUG "KEY %d: %08x\n", bdata->number, down); number = bdata->number; //如果此时不为低电平,中断处理进入休眠状态,一般有事件产生就会立即被唤醒 if (down != (key_values[number] & 1)) { key_values[number] = '0' + down; ev_press = 1; //中断休眠 wake_up_interruptible(&button_waitq); } } //按键中断处理函数 //irq:中断号 //dev_id:设备ID号 static irqreturn_t button_interrupt(int irq, void *dev_id) { struct button_desc *bdata = (struct button_desc *)dev_id; //注册一个定时器 mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(40)); //返回一个中断句柄 return IRQ_HANDLED; } //按键打开函数 //inode : 节点 //file : 打开文件的形式 static int tiny4412_buttons_open(struct inode *inode, struct file *file) { int irq; int i; int err = 0; //循环遍历四个IO口,看看有哪个按键被按下了 for (i = 0; i < ARRAY_SIZE(buttons); i++) { if (!buttons[i].gpio) continue; //初始化定时器 setup_timer(&buttons[i].timer, tiny4412_buttons_timer, (unsigned long)&buttons[i]); //设置GPIO为中断引脚,也就是对应那四个按键 irq = gpio_to_irq(buttons[i].gpio); err = request_irq(irq, button_interrupt, IRQ_TYPE_EDGE_BOTH, //请求中断处理函数 buttons[i].name, (void *)&buttons[i]); if (err) break; } if (err) { i--; for (; i >= 0; i--) { if (!buttons[i].gpio) continue; irq = gpio_to_irq(buttons[i].gpio); disable_irq(irq); //关中断 free_irq(irq, (void *)&buttons[i]);//释放中断 del_timer_sync(&buttons[i].timer);//删除一个定时器 } return -EBUSY; } ev_press = 1; return 0; } //按键关闭处理函数 static int tiny4412_buttons_close(struct inode *inode, struct file *file) { int irq, i; for (i = 0; i < ARRAY_SIZE(buttons); i++) { if (!buttons[i].gpio) continue; //同样的,这里也是释放 irq = gpio_to_irq(buttons[i].gpio); free_irq(irq, (void *)&buttons[i]); <span style="white-space:pre"> </span>//删除一个定时器 del_timer_sync(&buttons[i].timer); } return 0; } //读取按键的键值函数 static int tiny4412_buttons_read(struct file *filp, char __user *buff, size_t count, loff_t *offp) { unsigned long err; if (!ev_press) { if (filp->f_flags & O_NONBLOCK) return -EAGAIN; else //等待中断的事件产生 wait_event_interruptible(button_waitq, ev_press); } ev_press = 0; //将获取到的键值返回到用户空间 err = copy_to_user((void *)buff, (const void *)(&key_values), min(sizeof(key_values), count)); return err ? -EFAULT : min(sizeof(key_values), count); } //按键非阻塞型接口设计 static unsigned int tiny4412_buttons_poll( struct file *file, struct poll_table_struct *wait) { unsigned int mask = 0; <span style="white-space:pre"> </span>//非阻塞型等待 poll_wait(file, &button_waitq, wait); if (ev_press) mask |= POLLIN | POLLRDNORM; return mask; } //驱动文件操作结构体成员初始化 static struct file_operations dev_fops = { .owner = THIS_MODULE, .open = tiny4412_buttons_open, .release = tiny4412_buttons_close, .read = tiny4412_buttons_read, .poll = tiny4412_buttons_poll, }; //注册杂类设备的结构体成员初始化 static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, //这里就是把上面那个文件操作结构体的成员注册到杂类操作这里 }; //按键驱动初始化 static int __init button_dev_init(void) { int ret; //先注册一个杂类设备 //这相当于让misc去管理open ,read,write,close这些接口 ret = misc_register(&misc); // printk(DEVICE_NAME"\tinitialized\n"); return ret; } //按键驱动注销 static void __exit button_dev_exit(void) { //注销一个杂类设备驱动 misc_deregister(&misc); } module_init(button_dev_init); module_exit(button_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Yang.yuanxin");
运行结果: