Linux设备驱动第九天(非阻塞、内存管理)

来源:互联网 发布:阿里推荐算法典型特征 编辑:程序博客网 时间:2024/06/04 19:28

回顾:
linux内核等待队列机制:
目的:让进程在内核空间进行休眠
为什么要休眠?进程不能直接在用户空间访问硬件设备!只有在内核空间进行访问!
进程进入内核空间发现访问的设备不可用,就需要休眠等待访问的设备可用为止!
休眠 = 阻塞
编程方法:
两种方法!


linux内核非阻塞:
阻塞就是休眠
非阻塞:不进行休眠;在某些场合,当进程陷入内核空间访问设备,如果设备不可用,不想让进程进行休眠操作,而是立即返回到用户空间!此时可以采用非阻塞方式访问硬件设备!
用户对设备的访问默认采用阻塞方式!!
问:如保实现对设备的访问采用非阻塞?
答:在用户空间,应用程序中:
int fd;
fd = open(“a.txt”,O_RDWR);//采用阻塞方式
非阻塞方式:
int fd;
fd = open(“a.txt”,O_RDWR|O_NONBLOCK);//采用非阻塞方式

struct inode;
struct file;
注意:如果一个文件被成功打开以后,内核会为这个文件打开以后的状态属性分配一个struct file对象来描述这个文件打开以后的新的属性内含,其中struct file对象中有一个f_flags成员保存着O_NONBLOCK的信息(前提是如果采用非阻塞方式访问)!

内核空间设备驱动如何判断用户采用非阻塞方式呢?
struct file_operations各个接口函数
(open,release,read,write,ioctl)它们都有一个形参(struct file *file),其中这个file指针就是指向内核分配的file对象,所以驱动判断用户是否采用非阻塞只需执行以下代码即可:

if(file->f_flags & O_NONBLOCK){    printk("采用非阻塞方式");    if(设备不可用){        printk("进程返回用户你空间,不做休眠");        return -EAGAIN    }}...后面代码采用阻塞方式

案例:给按键驱动添加非阻塞方式
btn_drv.c

#include <linux/init.h>#include <linux/moudule.h>#include <linux/miscdevice.h>#include <linux/irq.h>#include <linux/fs.h>#include <linux/interrupt.h>#include <asm/gpio.h>#include <plat/gpio-cfg.h>#include <linux/input.h>//标准的键值KEY_UP,KEY_DOWN....#include <linux/sched.h>//TASK_*#include <linux/wait.h>//add_wait_queue....//上报按键信息的数据结构struct btn_event{   int code;//上报的按钮的值   int state;//上报的按键状态}//声明按键硬件相关的数据结构struct btn_resource{    int irq;//中断号    int gpio;//gpio    int code;//键值    char *name;//按键名称};//分配初始化按键信息static struct btn_resource btn_info[] = {    [0] = {       .irq = IRQ_EINT(0),       .gpio = S5PV210_GPH0(0),       .code = KEY_UP,       .name = "KEY_UP"    },    [1] = {       .irq = IRQ_EINT(1),       .gpio = S5PV210_GPH0(),       .code = KEY_DOWN,       .name = "KEY_DOWN"    }};//分配上报的按键信息static struct btn_event g_data;//1,分配读进程的等待队列头对象static wait_queue_head_t rwq;//里面保存休眠的读进程sataic int ispressed;//如果按键有操作 1,无操作 0;//当应用程序调用read的时候,通过软中断到这里//让读进程进入休眠状态(这里用schedule函数休眠)static ssize_t btn_read(struct file *file,char __user *buf,size_t count,lofft_t *ppos){    //首先判断对设备访问是否采用非阻塞    if(file->f_flag & O_NONBLOCK){        printk("采用非阻塞方式!\n");        //判断按键是否有操作        if(!ispressed){//ispressed 有操作 1,无操作 0;        }    }    //如果读进程发现按键无操作,该进程进入休眠状态,并且添加到req    //所对应的休眠队列中,静静等待唤醒!    //如果发现按键有操作,该进程不休眠    wait_event_interruptible(rwq,ispressed);    ispressed =0;    //10,上报按键信息    copy_to_user(buf,&g_data,sizeof(g_data));    return count;}static irqreturn_r button_isr(int irq,void *dev_id){    //1,获取对应按键的结构体的首地址    struct btn_resource *pdata = (struct btn_resource *)dev_id    g_data.code = pdata->code;    g_data.state = !gpio_get_value(pdata->gpio);//返回:按下是1,松开是0    //唤醒读等待队列中休眠的读进程    wake_up_interruptible(&rwq);    return IRQ_HANDLE;}//分配硬件操作集合static struct file_opeartons  btn_fops={    .owner = THIS_MODULE,    .read = btn_read,    .write = btn_write}//分配初始化混杂设备对象static struct miscdevice btn_misc = {     .minor = MISC_DYNAMDIC_MINOR,//动态分配次设备号     .name = "mybtn",//   /dev/mybtn     .fops = &btn_fops}static int btn_init(void){     int i;     //注册混杂设备     misc_register(&btn_misc);     //2,初始化读进程的等待队列头     init_waitqueue_head(&rwq);     //注册中断处理函数和申请GPIO资源     for(i=0;i<ARRAY_SIZE(btn_info);i++){          gpio_request(btn_info[i].gpio,btn_info[i].name);             request_irq(btn_info[i].irq,                         button_isr,                         IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,                        &btn_info[i]);     }     return 0;}static void  btn_exit(void){     //释放资源     int i;     for(i=0;i<ARRAY_SIZE(btn_info);i++){          gpio_free(btn_info[i].gpio);          free_irq(btn_info[i].irq,&btn_info[i]);          }     misc_deregister(&led_misc);//卸载混杂设备}module_init(btn_init);module_exit(btn_exit);MODULE_LICENSE("GPL");

总结:如果以后进行设备驱动开发,驱动中有阻塞的代码,必须添加非阻塞的代码!

应用程序:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>struct btn_event{    int code;    int state;};int main(int argc,char *argv[]){    int fd;    struct btn_event data;    if(argc<2){        printf("Usage \n %s <r|w> \n",argv[0]);        return -1;    }    //采用非阻塞方式访问    fd = open("/dev/mybtn",O_RDWR|O_NONBLOCK);    if(fd<0){       printf("open fail \n");       return -1;    }    while(1){       read(fd,&data,sizeof(data));//读的时候会休眠,       printf("code = %d ,state = %d",data.code,data.state);    }    close(fd);    return 0;}

linux内核内存分配相关内容:
内存空间、IO空间
对于X86平台:有两类总线,一类总线的位宽为16位,地址空间为64K,如果外设连接到这类总线上,CPU访问这个外设通过in,out指令完成,这个地址空间也称为IO空间;还有一类总线的位宽为32位,物理地址空间为4G,如果外设连接到这个类总线,CPU访问这个外设通过地址,指针的形式访问即可,这个地址空间也称为内存空间:
*add =0x55;
对于ARM平台:只有内存空间,无IO空间

虚拟内存相关:
1,对于32位平台,软件实际 可以使用4G的虚拟地址,比实际 的物理内存要大
2,如果要采用虚拟内存技术,CPU必须具有MMU这个硬件部件!如果CPU没有MMU,无法采用虚拟内存技术,软件访问的地址都是物理地址!
3,软件不管在用户空间还是内核空间,只能访问虚拟地址,不能直接访问物理地址!
4,整个4G的虚拟内存对于用户空间和内核空间进行地址空间的划分:
用户空间:0x0000000~0xBFFFFFF
内核空间:0xC0000000~0xFFFFFFF
5,用户空间都是有自己独立的3G虚拟地址空间,但是内核1G是所有的进程共享
6,采用虚拟内存技术可以实现操作系统的自我保护,这占关键体现在MMU机制上。
7,虽然软件表面看到的地址都是虚拟地址,CPU最终访问的都是物理地址

MMU硬件:
特点:集成在CPU内部,如果CPU集成MMU,可以采用虚拟内存技术
功能:就是将虚拟地址转换成实际的物理地址
注意:在转换的时候,要保护正常转换,前提是物理地址和这个虚拟地址必须建立好它们之间的映射关系(页表)!!除了以上功能,MMU还会检查 地址是否合法有效,是否有权限 访问,如果不合法,无权限,MMU会给CPU抛出一个异常!

uclinux:支持不带MMU的处理器!访问的地址都是物理地址!!
FPGA+arm核:
DSP处理器:
PWERPC:
VHDL:

明确:不管是在内核空间还是在用户空间,只能访问虚拟地址,不能访问物理地址!

用户空间最大能够访问3G的物理内存!
内核空间能够访问所有的物理内存!

内核空间1G虚拟内存和物理内存之间的映射是一个静态映射过程:就是在内核启动的时候,就已经建立1G虚拟内存和物理地址一一映射关系(页表),一旦建立好这种关系,以后直接访问这个虚拟内存,可以不通过复杂的地址转换,就可以直接访问物理地址!一一映射关系

假设物理内存的起始地址为:0x00000000
内核虚拟地址 物理地址
0xC0000000 0x0
0xC0000001 0x1
0xC000000c 0x2
…… ….
0xFFFFFFF 1G物理内存

线性关系:物理地址 = 内核虚拟地址 - 0xC0000000

问题:如果物理内存大于1G,并且采用这种一一映射,内核空间最多只能访问1G的物理内存,剩余的物理内存,内核空间无法访问到!

问:如何通过1G内核虚拟内存访问所有的物理内存呢?
答:内核为了访问所有物理内存,内核将1G的内核虚拟内存进行分区划分;
x86平台
内核将1G的虚拟内存划分为4个区域:
直接内存映射区
特点:如果系统的物理内存大于1G,在内核初始化启动时,将1G的内核,虚拟内存的前896M跟物理内存的前896M进行一一映射。这种映射导致内存的访问速度加快!
如果系统的物理内存比如为512M,直接内存映射区的大小就为512M!
直接内存映射区大小最大小为896M
动态内存映射区:
动态内存映射区大小默认为120M,如果要想访问其余的某个物理内存或者某个物理地址,可以动态的建立这个物理地址和动态内存映射区中内核虚拟地址的映射关系(页表),如果不再使用,一定要将这个映射关系要解除,否则造成动态内存映射区的内核虚拟内存泄漏!这种访问效率相对比较低!
永久内存映射区:
固定内存映射区:
“永久” = “固定”,这两块内核虚拟内存的大小分别为4M,如果要经常、频繁的访问某个物理地址,可以将这个物理地址跟永远或固定内存映射区中的内核虚拟地址建立映射(页表),一经映射,即使目前不使用,也可以不用解除映射,便于下一次访问!加快地址的访问速度!
永久和固定的区别在于如果把物理地址和永久内存映射区的内核虚拟地址进行映射,这个过程有可能会休眠,不能在中断上下文中使用!
如果把物理地址和固定内存映射区的内核虚拟地址进行映射,这个过程有可能不会休眠,并且禁止进程之间的抢占,所以可以用在中断上下文中!

总结
直接内存映射区又叫“低端内存”
直接内存映射区以外的所有区域又叫“高端内存”

对于S5PV210处理器,内核1G的虚拟内存如何进行划分?
通过查看内核的启动信息即可:

这里写图片描述

分别表示:映射区的名称 起始地址 结束地址 大小
lowmem:表示直接内存映射区,610MB
vmalloc:动态内存映射区,344MB
DMA:内存映射区
fixmap:固定内存映射区
vector:异常向量表,4KB(一页)
modules:用于处理驱动模块

linux内核空间分配内存的方法

1,linux用户空间分配民内存的方法:
malloc/free
2,linux内核空间分配分配内存的方法:
kmalloc/kfree
函数原型:
void *kmalloc(size_t size, int flags);
功能:用于在内核空间分配内存,从低端内存进行分配。
物理上连续,虚拟上也连续!
参数说明:
size:指定分配内存的大小,最小为32字节,最大128KB(老版本内核)或者4M(新版本内核);
flags: 分配内存的行为
GFP_KERNEL:如果是用此函数分配内存,并且指定了这个标志,表明告诉内核请努力的帮我把这次内存分配搞定!这个标志导致分配内存的结果成功的概率比较大!如果内存不中,导致分配内存将进入休眠状态,不能用于中断上下文中!
GFP_ATOMIC:如果内存不足,不会进行休眠操作,而是立即返回!可以用在中断上下文中!
返回值:返回值为分配内存的起始地址(内核虚拟地址);如果返回NULL,表示分配失败!

例如:分配2M的内核虚拟内存
unsigned char *kerneladdr;
kerneladdr = kmalloc(2*1024*1024,GFP_KERNEL);

kmalloc 衍生版本:kzmalloc - kmalloc + memset(,0,)//分配内存,并且把对应的内存清0

释放内存:kfree(void *addr);

__get_free_pages/free_pages:
明确:linux系统管理内存都是以页为单位:1页为4K
函数原型:
unsingned long __get_free_pages(gfp_t gfp_mask, unsingned int order);
功能:用于在内核空间分配内存,此函数的实现是基于kmalloc实现的,所以分配的内存也是在低端内存分配,物理上连续,虚拟上也连续;
分配内存的大小限定:最小为1页,最大为4M
参数说明:
gfp_mask:分配内存的行为(GFP_KERNEL,GFP_ATOMIC)
order:如果order =0 ,分配的大小为1页
如果order =1,分配的大小为2页
如果order =2,分配的大小为4页
如果order =3,分配的大小为8页
…..
返回值:返回值为分配内存的起始地址

释放内存:free_page(unsigned long add ,unsigned int order);

vmalloc/vfree:
函数原型:
void *vmalloc(int size);
功能:用于在内核空间分配内存,在动态内存映射区分配,虚拟上连续,但是物理上不一定连续! 也会导致休眠!分配内存大小:最小没有限定,默认最大120M ! 实际上分配不了这么大!
返回值:内存的起始地址

释放内存:
voif vfree();

说明:
如果要在永久内存区映射分配:kmap函数
如果要在固定内存映射区分配:kmap_atomic函数

其他分配内存的方法:
1,在设备驱动程序中定义一个静态的数组:

static char buf[5*1024*1024];//5M  位于BSS段 ,不会影响目标文件本身的大小static char buf1[5*1024*1024] = {0x55,0xaa,0x55};//位于数据段,如果在代码//中没有进行对数组进行访问操作,编译器会直接进行优化,最终不会导致目标文件的体积变大,//但是如果代码中有对数组进行访问操作,数组不会被优化,最终会影响目标文件的体积,导致//加载模块的速度变慢!static char buf2[5*1024*1024] __atribute__((used)) = {0x55};//即使代码//不进行访问操作,编译器敢不会进行优化!

在内核启动的时候,在内核的启动参数中指定:
vmalloc = 256M,内核会在启动的时候,将动态内存映射区的大小进行扩展,由原先的120M变为256M:
setenv bootargs root=/dev/nfs nfsroot=…. vmalloc=256M

在内核的启动参数中指定mem = 8M,表明在内核启动是,将物理内存的最后8M单独保留,内核不做处理!以后驱动只需要调用ioremap动态映射这8M的内存即可!

mem_dev.c

#include <linux/init.h>#include <linux/module.h>#include <linux/slab.h>//static char buf[5*1024*1024]  __attribute__((used)) = {0x55,0xaa,0x55};static char buf[5*1024*1024];static int mymem_init(void){    //buf[3] = 0x66;    return 0;}static void mymem_exit(void){}module_init(mymem_init);module_exit(mymem_exit);

问:如何在内核驱动程序中直接访问寄存器来来操作灯呢?
答:
明确:
1,不管是在用户空间还是内核空间一律不允许访问物理地址,只能访问虚拟地址(用户虚拟地址和内核虚拟地址);
2,用户不能直接在用户空间访问硬件设备,必须在内核空间访问硬件设备;

问:如何将硬件设备的物理地址映射到内核的虚拟地址呢?
如果一旦完成这种映射,以后在内核空间访问内核的虚拟地址就是访问实际的物理地址(地转的转换靠MMU)!
答:将硬件设备的物理地址映射到内核的虚拟地址上只需要利用大名鼎鼎的ioremap函数即可完成这种映射!

ioremap函数:
函数原型:
void *ioremap(unsigned long phys_addr, unsigned long size)
功能:将一个IO地址空间映射到内核的虚拟地址空间上去
参数说明:
phys_addr:要访问的设备起始物理地址
size:要映射的物理地址空间的大小
返回值:为映射好的内核虚拟地址,以后访问这个虚拟地址就是在访问这个实际的物理地址

以操作LED为例:
硬件LED1,LED2对应的GPC1_3,PGC1_4
对就的寄存器的物理地址:
配置寄存器:0xE0200080
数据寄存器:0xE0200084
特点:物理上连续,连续的空间大小为8字节

方法1:
void *gpiobase;//寄存器的起始虚拟地址
gpiobase - ioremap(0xE0200080,8);
配置寄存器的内核虚拟地址 - gpiobase;
配置寄存器的内核虚拟地址 - gpiobase | 4;

方法2:
void *gpiocon, *gpiodata;//寄存器的内核虚拟地址
gpiocon - ioremap(0xE0200080,4);
gpiodata - ioremap(0xE0200080,4);

解除地址映射:
iounmap();
案例:通过直接访问寄存器来实现开关灯!
混杂设备+ioctl

#include <linux/init.h>#include <linux/moudule.h>#include <linux/miscdevice.h>#include <linux/io.h>//ioremap#include <linux/fs.h>#include <linux/uaccess.h>#define LED_ON  0x100001;//on#define LED_OFF 0x100002;//ofstatic unsigned long *gpiobase;//定义寄存器的内核虚拟地址static unsigned long *gpiocon,*gpiodata;static long led_ioctl(struct file *file,unsigned int cmd,unsigned long arg){   //获取用户要访问的哪个灯   int index;   copy_from_user(&index,(int *)arg,4);   //解析开头命令   switch(cmd){       case LED_ON:          if(index == 1)             *gpio |= (1<<3);          else if(index ==2)              *gpiodata |= (1<<4);          break;       case LED_OFF:          if(index == 1)              *gpiodata &= ~(1<<3);          else if(index ==2)              *gpiodata &= ~(1<<4);          break;       default:          return -1;   }      printk("配置寄存的内容=%#x , 数据寄存器的内容 = %#x",*gpiocon,*gpiodata);   return 0;}//分配硬件操作接口static struct file_operations led_fops ={     .owner = THIS_MODULE,     .unlocked_ioctl = led_ioctl;};//分配混杂设备对象static struct miscdevice led_misc = {    .minor = MISC_DYNAMIC_MINOR,    .name = "myled",    .fops = &led_fops};static int led_init(void){   //1,注册混杂设备   misc_register(&led_misc);   //2,将寄存器地址映射到内核虚拟地址上   gpiobase = ioremap(0xE0200080,8);   gpiocon = (unsigned long*)gpiobase;   gpiodata = (unsigned long*)(gpiobase + 4);   //3,配置GPIO为输出口,输出0   *gpiocon &= ~((0xf<<12)|(0xf<<16));//   *gpiocon |= (1<<12)|(1<<16);   *gpiodata &= ~((1<<3)|(1<<4));   return 0;}static void led_exit(void){    *gpiodata &= ~((1<<3)|(1<<4));    iounmap(gpiobase);//解除地址映射    misc_deregister(&led_misc);}module_init(led_init);module_exit(led_exit);MODULE_LICENSE("GPL");

应用程序测试代码:

#include <stdio.h>#include <sys/types.h>#incldue <sys/sta.h>#incldue <fcnt.h>#define LED_ON  0x100001;//on#define LED_OFF 0x100002;//ofint main(int argc,char *argv[]){   int fd;   int index;   if(argc<3){       return -1;   }   fd = open("/dev/myled",O_RDWR);   if(fd<0){       return  -1;   }   index = strtoul(argv[2],NULL,0);//将第二个字符参数转换为intif(strcmp(argv[1],"on")){      ioctl(fd,LED_ON,&index)   }else if(!strcmp(argv[1],"off")){      ioctl(fd,LED_OFF,&index);   }   close(fd);   return 0;}
0 0