用mmap写Linux用户空间驱动
2011-03-31 20:13
495 查看
一、 mmap函数解析:
Ø 函数原型:
#include <sys/mman.h>
void *mmap(void *start, size_t length,int prot, int flags,int fd,off_t offset);
int munmap(void *start, size_t length);
Ø 函数功能:
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。
Ø 参数说明:
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。
Ø 返回值说明:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
二、 Linux驱动程序的两种形式:
下面任然以AT91Sam9260系列ARM9开发板中对GPIO的控制为例进行讲解,Linux内核为2.6.19版本。
Ø 内核层驱动:
遵循Linux内核关于各种硬件架构驱动的游戏规则编写,把驱动程序编译进内核或者编译为模块,这中方法符合驱动的底层含义,但需要程序员对Linux底层实现机制有一定了解,难度相对要大一些。下面的驱动实现对GPIO PB17的输出控制。
Ø 用户空间驱动:
这种方式使用mmap函数将硬件地址映射到进程空间,从而可以在用户层通过指针操作映射后的地址,从而控制硬件,绕过了内核实现,避开了read,write等函数调用。
三、 用户空间驱动的优缺点:
从上面的例子可以看到,用户空间驱动避开了内核复杂的实现机制,似乎更简单,那为什么硬件驱动不都在用户层面完成呢?有时编写一个所谓的用户空间设备驱动对比钻研内核是一个明智的选择。但为什么事实上多数情况下使用的是内核层面的驱动。
Ø 用户空间驱动的优点:
² 完整的 C 库可以连接. 驱动可以进行许多奇怪的任务, 不用依靠外面的程序(实现使用策略的工具程序, 常常随着驱动自身发布);
² 程序员可以在驱动代码上运行常用的调试器, 而不必走调试一个运行中的内核的弯路;
² 如果一个用户空间驱动挂起了, 你可简单地杀掉它. 驱动的问题不可能挂起整个系统, 除非被控制的硬件真的疯掉了;
² 用户内存是可交换的, 不象内核内存. 一个不常使用的却有很大一个驱动的设备不会占据别的程序可以用到的 RAM, 除了在它实际在用时;
² 一个精心设计的驱动程序仍然可以, 如同内核空间驱动, 允许对设备的并行存取;
² 如果你必须编写一个封闭源码的驱动, 用户空间的选项使你容易避免不明朗的许可的情况和改变的内核接口带来的问题;
² 一种在用户空间工作的情况可能是有意义的, 当你开始处理新的没有用过的硬件时. 这样你可以学习去管理你的硬件, 不必担心挂起整个系统. 一旦你完成了, 在一个内核模块中封装软件就会是一个简单操作了。
Ø 用户空间驱动的缺点:
² 中断在用户空间无法用. 在某些平台上有对这个限制的解决方法, 例如在 IA32体系上的 vm86 系统调用;
² 只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样做;
² 存取 I/O 端口只能在调用 ioperm 或者 iopl 之后. 此外,不是所有的平台支持这些系统调用, 而存取/dev/port可能太慢而无效率. 这些系统调用和设备文件都要求特权用户;
² 响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作;
² 更不好的是,如果驱动已被交换到硬盘, 响应时间会长到不可接受。 使用 mlock 系统调用可能会有帮助,但是常常的你将需要锁住许多内存页, 因为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上。最重要的设备不能在用户空间处理, 包括但不限于, 网络接口和块设备。
下面来看在前面的基础上加上中断的情况,由于用户空间不能操作中断,所以只能使用内核驱动,下面贴出代码,具体解析我在《GPIO中断程序》一文中有讲述,此处不再赘述。
最后给出自己写的一个在用户层读写AT91sam9260SDRAM某一个位置的小程序结束本文。
Ø 函数原型:
#include <sys/mman.h>
void *mmap(void *start, size_t length,int prot, int flags,int fd,off_t offset);
int munmap(void *start, size_t length);
Ø 函数功能:
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。
Ø 参数说明:
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。
Ø 返回值说明:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
二、 Linux驱动程序的两种形式:
下面任然以AT91Sam9260系列ARM9开发板中对GPIO的控制为例进行讲解,Linux内核为2.6.19版本。
Ø 内核层驱动:
遵循Linux内核关于各种硬件架构驱动的游戏规则编写,把驱动程序编译进内核或者编译为模块,这中方法符合驱动的底层含义,但需要程序员对Linux底层实现机制有一定了解,难度相对要大一些。下面的驱动实现对GPIO PB17的输出控制。
#include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/platform_device.h> #include <linux/cdev.h> #include <linux/ioctl.h> #include <linux/gpio.h> #include <linux/errno.h> #include <asm/arch/hardware.h> #include <asm/arch/gpio.h> #include <asm/uaccess.h> #include <asm/io.h> #include <asm/hardware.h> #include <asm/arch/at91_pio.h> /*******************************************/ void led_on(unsigned int led_num) { at91_set_gpio_output(AT91_PIN_PB17 + led_num,1); } void led_off(unsigned int led_num) { at91_set_gpio_output(AT91_PIN_PB17 + led_num,0); } struct light_dev { struct cdev cdev; unsigned char value; }; struct light_dev *light_devp; int light_major = 249; MODULE_AUTHOR("Cun Tian Rui"); MODULE_LICENSE("Dual BSD/GPL"); int light_open(struct inode *inode,struct file *filp) { struct light_dev *dev; dev = container_of(inode->i_cdev,struct light_dev,cdev); filp->private_data = dev; return 0; } int light_release(struct inode *inode,struct file *filp) { return 0; } // ioctl int light_ioctl(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg) { struct light_dev *dev = filp->private_data; switch(cmd) { case 0: dev->value = 0; led_on(arg); break; case 1: dev->value = 1; led_off(arg); break; default: return -ENOTTY; // break; } return 0; } struct file_operations light_fops = { .owner = THIS_MODULE, .ioctl = light_ioctl, .open = light_open, .release = light_release, }; static void light_setup_cdev(struct light_dev *dev,int index) { int err,devno = MKDEV(light_major,index); cdev_init(&dev->cdev,&light_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &light_fops; err = cdev_add(&dev->cdev,devno,1); if(err) { printk(KERN_NOTICE "Error %d adding LED%d",err,index); } } int light_init(void) { int result; dev_t dev = MKDEV(light_major,0); if(light_major) { result = register_chrdev_region(dev,1,"CTRLED"); } if(result < 0) { return result; } light_devp = kmalloc(sizeof(struct light_dev),GFP_KERNEL); if(!light_devp) { result = - ENOMEM; goto fail_malloc; } memset(light_devp,0,sizeof(struct light_dev)); light_setup_cdev(light_devp,0); return 0; fail_malloc:unregister_chrdev_region(dev,light_devp); return result; } void light_cleanup(void) { cdev_del(&light_devp->cdev); kfree(light_devp); unregister_chrdev_region(MKDEV(light_major,0),1); } module_init(light_init); module_exit(light_cleanup);
Ø 用户空间驱动:
这种方式使用mmap函数将硬件地址映射到进程空间,从而可以在用户层通过指针操作映射后的地址,从而控制硬件,绕过了内核实现,避开了read,write等函数调用。
#include <sys/types.h> #include <stdio.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <fcntl.h> #include <memory.h> #define PIOB_Base 0xFFFFF600 #define PIOB_PER 0x0 #define PIOB_OER 0x10 #define PIOB_CODR 0x34 #define PIOB_SODR 0x30 volatile unsigned int *map_base=NULL; volatile unsigned int *SODR =NULL; volatile unsigned int *CODR =NULL; int fd; int main(int argc ,char *argv[]) { int state; fd = open("/dev/mem", O_RDWR|O_SYNC) ; if(fd == -1) return -1; map_base=(volatile unsigned int*)mmap(NULL,0x200,PROT_READ|PROT_WRITE,MAP_SHARED,fd,PIOB_Base); if(map_base==NULL || map_base ==MAP_FAILED) printf("Can't mmap/n"); else printf(" mmap success/n"); SODR = (volatile unsigned int*)(map_base+PIOB_SODR); CODR = (volatile unsigned int*)(map_base+PIOB_CODR); *(volatile unsigned int*)(map_base+PIOB_PER)|= 1<<17; *(volatile unsigned int*)(map_base+PIOB_OER)|= 1<<17; while(1) { printf("please input 1 or 0/n"); scanf("%d",&state); if(state) *(volatile unsigned int*)SODR= (1<<17); else *(volatile unsigned int*)CODR = (1<<17); } close(fd); munmap((void*)map_base,0xff); return 0; }
三、 用户空间驱动的优缺点:
从上面的例子可以看到,用户空间驱动避开了内核复杂的实现机制,似乎更简单,那为什么硬件驱动不都在用户层面完成呢?有时编写一个所谓的用户空间设备驱动对比钻研内核是一个明智的选择。但为什么事实上多数情况下使用的是内核层面的驱动。
Ø 用户空间驱动的优点:
² 完整的 C 库可以连接. 驱动可以进行许多奇怪的任务, 不用依靠外面的程序(实现使用策略的工具程序, 常常随着驱动自身发布);
² 程序员可以在驱动代码上运行常用的调试器, 而不必走调试一个运行中的内核的弯路;
² 如果一个用户空间驱动挂起了, 你可简单地杀掉它. 驱动的问题不可能挂起整个系统, 除非被控制的硬件真的疯掉了;
² 用户内存是可交换的, 不象内核内存. 一个不常使用的却有很大一个驱动的设备不会占据别的程序可以用到的 RAM, 除了在它实际在用时;
² 一个精心设计的驱动程序仍然可以, 如同内核空间驱动, 允许对设备的并行存取;
² 如果你必须编写一个封闭源码的驱动, 用户空间的选项使你容易避免不明朗的许可的情况和改变的内核接口带来的问题;
² 一种在用户空间工作的情况可能是有意义的, 当你开始处理新的没有用过的硬件时. 这样你可以学习去管理你的硬件, 不必担心挂起整个系统. 一旦你完成了, 在一个内核模块中封装软件就会是一个简单操作了。
Ø 用户空间驱动的缺点:
² 中断在用户空间无法用. 在某些平台上有对这个限制的解决方法, 例如在 IA32体系上的 vm86 系统调用;
² 只可能通过内存映射 /dev/mem 来使用 DMA, 而且只有特权用户可以这样做;
² 存取 I/O 端口只能在调用 ioperm 或者 iopl 之后. 此外,不是所有的平台支持这些系统调用, 而存取/dev/port可能太慢而无效率. 这些系统调用和设备文件都要求特权用户;
² 响应时间慢, 因为需要上下文切换在客户和硬件之间传递信息或动作;
² 更不好的是,如果驱动已被交换到硬盘, 响应时间会长到不可接受。 使用 mlock 系统调用可能会有帮助,但是常常的你将需要锁住许多内存页, 因为一个用户空间程序依赖大量的库代码. mlock, 也, 限制在授权用户上。最重要的设备不能在用户空间处理, 包括但不限于, 网络接口和块设备。
下面来看在前面的基础上加上中断的情况,由于用户空间不能操作中断,所以只能使用内核驱动,下面贴出代码,具体解析我在《GPIO中断程序》一文中有讲述,此处不再赘述。
/* * PB18_IRQTest.c * This is a test program for sam9260, using PB19(J5_18 pin) input a signal to PB18(J5_16 pin), * PB18 receive this signal as IRQ and make the LED linking on PB17((J5_14 pin)) turn on or turn off * * @Author: Cun Tian Rui * @Date :March.18.2011 */ #include <linux/types.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/platform_device.h> #include <linux/cdev.h> #include <linux/ioctl.h> #include <linux/fs.h> #include <linux/gpio.h> #include <asm/arch/hardware.h> #include <asm/arch/gpio.h> #include <linux/interrupt.h> #include <asm/io.h> #include <asm/arch/board.h> #include <linux/cdev.h> #include <asm/arch/gpio.h> #include <asm/uaccess.h> #include <asm/io.h> #include <asm/arch/at91_pio.h> #include <asm/arch/at91_aic.h> #include <asm/arch/at91_pmc.h> void led_on() { at91_set_gpio_output(AT91_PIN_PB17,1); } void led_off() { at91_set_gpio_output(AT91_PIN_PB17 ,0); } struct light_dev *light_devp; int light_major = 200; struct light_dev { struct cdev cdev; unsigned char value; }; MODULE_AUTHOR("Cun Tian Rui"); MODULE_LICENSE("Dual BSD/GPL"); static void io_init(void) { at91_set_gpio_input(AT91_PIN_PB18, 1); at91_set_deglitch(AT91_PIN_PB18, 1); at91_sys_write(1 + PIO_IDR, 1<<18); at91_sys_write(1 + PIO_IER, (~(1<<18))); at91_sys_write(AT91_PMC_PCER, 1 << 3); } struct gpio_irq_desc { int irq; unsigned long flags; char *name; }; static struct gpio_irq_desc PB18_IRQ={AT91_PIN_PB18,AT91_AIC_SRCTYPE_LOW,"PB18"}; static irqreturn_t PB18_intHandle(int irq, void *dev_id) { led_on(); return IRQ_RETVAL(IRQ_HANDLED); } int light_open(struct inode *inode,struct file *filp) { int err; struct light_dev *dev; dev = container_of(inode->i_cdev,struct light_dev,cdev); filp->private_data = dev; io_init(); err = request_irq(PB18_IRQ.irq,PB18_intHandle,PB18_IRQ.flags,PB18_IRQ.name,(void*)0); if(err) { free_irq(PB18_IRQ.irq,(void*)0); return -EBUSY; } return 0; } int light_release(struct inode *inode,struct file *filp) { free_irq(PB18_IRQ.irq,(void*)0); return 0; } // ioctl int light_ioctl(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg) { struct light_dev *dev = filp->private_data; switch(cmd) { case 0: at91_set_gpio_output(AT91_PIN_PB19,0); break; case 1: at91_set_gpio_output(AT91_PIN_PB19,1); led_off(); break; default: return -ENOTTY; // break; } return 0; } struct file_operations light_fops = { .owner = THIS_MODULE, .ioctl = light_ioctl, .open = light_open, .release = light_release, }; static void light_setup_cdev(struct light_dev *dev,int index) { int err,devno = MKDEV(light_major,index); cdev_init(&dev->cdev,&light_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &light_fops; err = cdev_add(&dev->cdev,devno,1); if(err) { printk(KERN_NOTICE "Error %d adding LED%d",err,index); } } int light_init(void) { int result; dev_t dev = MKDEV(light_major,0); if(light_major) { result = register_chrdev_region(dev,1,"PB18_IRQTest"); } if(result < 0) { return result; } light_devp = kmalloc(sizeof(struct light_dev),GFP_KERNEL); if(!light_devp) { result = - ENOMEM; goto fail_malloc; } memset(light_devp,0,sizeof(struct light_dev)); light_setup_cdev(light_devp,0); return 0; fail_malloc:unregister_chrdev_region(dev,light_devp); return result; } void light_cleanup(void) { cdev_del(&light_devp->cdev); kfree(light_devp); unregister_chrdev_region(MKDEV(light_major,0),1); } module_init(light_init); module_exit(light_cleanup);
最后给出自己写的一个在用户层读写AT91sam9260SDRAM某一个位置的小程序结束本文。
#include <stdio.h> #include <unistd.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define SDRAM_PHY_Position 0x20800000 int main (int args, char* arg[]) { int i; int fd; char* mem; char *buff = "HELLO"; //open /dev/mem with read and write mode if ((fd = open ("/dev/mem", O_RDWR)) < 0) { perror ("open error"); return -1; } //map physical memory 0-10 bytes mem = mmap (0, 10, PROT_READ | PROT_WRITE, MAP_SHARED, fd, SDRAM_PHY_Position); if (mem == MAP_FAILED) { perror ("mmap error:"); return 1; } //Read old value for (i = 0; i < 5; i++) { printf("/nold mem[%d]:%d", i, mem[i]); } //write memory memcpy(mem, buff, 5); //Read new value for (i = 0; i<5 ; i++) { printf("/nnew mem[%d]:%c", i, mem[i]); } printf("/n"); munmap (mem, 10); //destroy map memory close (fd); //close file return 0; }
相关文章推荐
- [arm驱动]linux设备地址映射到用户空间 推荐
- Linux音频设备驱动_OSS驱动框架(四)————OSS 用户空间编程
- Linux tty驱动学习 - 在用户空间设置串口参数操作流程
- linux驱动开发之字符设备--内核和用户空间数据的交换(ioctl)
- linux驱动之用户空间驱动
- linux驱动开发--copy_to_user 、copy_from_user函数实现内核空间数据与用户空间数据的相互访问
- linux驱动开发:用户空间操作LCD显示简单的图片【转】
- 虚拟字符驱动,申请n页内存,使用mmap映射到应用程序空间,用户就可以直接访问不需要任何同步机制
- Linux驱动子系统之I2C用户空间调用
- linux驱动开发:用户空间操作LCD显示简单的图片
- Linux 用户空间spi读写外围驱动
- [android] 调试linux input子系统驱动的用户空间命令 getevent/sendevent
- linux驱动开发之字符设备--内核和用户空间数据的交换(sysfs)
- linux驱动开发之字符设备--内核和用户空间数据的交换(read write)
- Linux驱动:用户空间,内核空间内存交互测试
- [arm驱动]linux设备地址映射到用户空间
- 转载_linux驱动中的poll 和 用户空间的select
- 【从零开始,从内核驱动驱动到用户空间调用】编写第一个linux驱动,通过端口访问I/O寄存器。
- Linux 用户空间i2c 字符驱动
- 【从零开始,从内核驱动驱动到用户空间调用】编写第一个linux驱动,通过端口访问I/O寄存器。