在Ubuntu上为Android系统编写Linux内核驱动程序(老罗学习笔记1)

时间:2023-11-20 12:49:50

这里,我们不会为真实的硬件设备编写内核驱动程序。为了方便描述为Android系统编写内核驱动程序的过程,我们使用一个虚拟的硬件设备,这个设备只有一个4字节的寄存器,它可读可写。想起我们第一次学习程序语言时,都喜欢用“Hello, World”作为例子,这里,我们就把这个虚拟的设备命名为“hello”,而这个内核驱动程序也命名为hello驱动程序。其实,Android内核驱动程序和一般Linux内核驱动程序的编写方法是一样的,都是以Linux模块的形式实现的,具体可参考前面Android学习启动篇一文中提到的Linux Device Drivers一书。不过,这里我们还是从Android系统的角度来描述Android内核驱动程序的编写和编译过程。

一. 参照前面两篇文章在Ubuntu上下载、编译和安装Android最新源代码在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)准备好Android内核驱动程序开发环境。

二. 进入到kernel/common/drivers目录,新建hello目录:

USER-NAME@MACHINE-NAME:~/Android$ cd kernel/common/drivers

       USER-NAME@MACHINE-NAME:~/Android/kernel/common/drivers$ mkdir hello

三. 在hello目录中增加hello.h文件:

#ifndef _HELLO_ANDROID_H_
#define _HELLO_ANDROID_H_ #include <linux/cdev.h>             //对字符设备结构cdev以及一系列的操作函数定义
#include <linux/semaphore.h>          //信号量使能的头文件 #define HELLO_DEVICE_NODE_NAME "hello"    //字节名字
#define HELLO_DEVICE_FILE_NAME "hello"    //文件名
#define HELLO_DEVICE_PROC_NAME "hello"    //任务名
#define HELLO_DEVICE_CLASS_NAME "hello"    //类名 struct hello_android_dev {            //驱动结构体定义(虚拟的硬件设备)
int val;                    //设备里面的寄存器地址
struct semaphore sem;            //信号量,用于同步访问寄存器val
struct cdev dev;               //dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法   
}; #endif

这个头文件定义了一些字符串常量宏,在后面我们要用到。此外,还定义了一个字符设备结构体hello_android_dev,这个就是我们虚拟的硬件设备了,val成员变量就代表设备里面的寄存器,它的类型为int,sem成员变量是一个信号量,是用同步访问寄存器val的,dev成员变量是一个内嵌的字符设备,这个Linux驱动程序自定义字符设备结构体的标准方法。

四.在hello目录中增加hello.c文件,这是驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。这里,提供了三种访问设备寄存器的方法,一是通过proc文件系统来访问,二是通过传统的设备文件的方法来访问,三是通过devfs文件系统来访问。下面分段描述该驱动程序的实现。

size_t:在32位系统上 定义为 unsigned int 也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long 也就是说在64位系统上是64位无符号整形。size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。而ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是signed size_t类型的。)

首先是包含必要的头文件和定义三种访问设备的方法:

/*---------------------------------------

(三)该文件包

 ---------------------------------------*/

#include <linux/init.h>        //包含了模块的初始化的宏定义 以及一些其他函数的初始化函数
/*MODULE.H:写内核驱动的时候 必须加载这个头文件,作用是动态的将模块加载到内核中去,常用的宏定义如 MODULE_LICESENCE(),MODULE_AUTHOR(),等在此文件中
而且 kobject,kset结构体题及其操作函数也在这个结构体中
*/ #include <linux/module.h>     
#include <linux/types.h>       //原系统数据类型 
#include <linux/fs.h>       //file_operations、inode_operations、super_operations结构体(文件系统函数定义头文件)
#include <linux/proc_fs.h>     /*proc 文件系统是由软件创建,被内核用来向外界报告信息的一个文件系统,,http://www.cnblogs.com/Ph-one/p/4399326.html */
#include <linux/device.h>   //在这个头文件中包含了 bus的一些函数 和 drifver的一些函数,以及class_create()等函数
#include <asm/uaccess.h>     /* copy_to_user和copy_from_user */
#include "hello.h"         //自定义头文件 /*主设备和从设备号变量;设备号有什么意义,作用是什么,大小有无关系*/
static int hello_major = ;   //主设备
static int hello_minor = /*设备类别和设备变量*/
static struct class* hello_class = NULL;
static struct hello_android_dev* hello_dev = NULL;

/*---------------------------------------

(二)

/*------------------------------------------------------------------------------------
-*/
/*传统的设备文件操作方法*/
static int hello_open(struct inode* inode, struct file* filp);     //打开
static int hello_release(struct inode* inode, struct file* filp);      //释放
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos); /*设备文件操作方法表 */
/* 第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中,
它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏. */
static struct file_operations hello_fops = { /*对应用层出口*/
.owner = THIS_MODULE,
.open = hello_open, //尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
.release = hello_release, //在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL
.read = hello_read,
.write = hello_write,
};
 ---------------------------------------*/
/*------------------------------------------------------------------------------------
sysfs访问设置设备属性-*/
/*访问设置属性方法 */
static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf);
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count); /*定义设备属性*/
static DEVICE_ATTR(val, S_IRUGO | S_IWUSR, hello_val_show, hello_val_store);
/*----------------------------------------------------------------------------------*/ 
 

DEVICE_ATTR(_name, _mode, _show, _store)
_name:名称,也就是将在sys fs中生成的文件名称。
_mode:上述文件的访问权限,与普通文件相同,UGO的格式。
_show:显示函数,cat该文件时,此函数被调用。
_store:写函数,echo内容到该文件时,此函数被调用。

/*---------------------------------------

(一)

定义传统的设备文件访问方法,主要是定义hello_open、hello_release、hello_read和hello_write这四个打开、释放、读和写设备文件的方法:

/*打开设备方法*/
static int hello_open(struct inode* inode, struct file* filp) {
struct hello_android_dev* dev; /*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/
dev = container_of(inode->i_cdev, struct hello_android_dev, dev); //container_of()的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址
                         //struct cdev *
    filp->private_data = dev;          //void * private_data: 该成员是系统调用时保存状态信息非常有用的资源

    return ;
} /*设备文件释放时调用,空实现*/
static int hello_release(struct inode* inode, struct file* filp) {
return ; //函数返回零,就是释放,整个退出,好笑
} /*读取设备的寄存器val的值*/
static ssize_t hello_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) {
ssize_t err = ;
struct hello_android_dev* dev = filp->private_data; //将系统保存状态信息(地址)传给Hello结构体首地址,,(hello_android_dev* dev:这样定义是不是有问题??没问题)
/*同步访问*/
if(down_interruptible(&(dev->sem))) {         
  /* 这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步 */
     return -ERESTARTSYS;
} if(count < sizeof(dev->val)) {         //这一步的作用是做什么的呢?
goto out;
} /*将寄存器val的值拷贝到用户提供的缓冲区*/
if(copy_to_user(buf, &(dev->val), sizeof(dev->val))) { //拷贝成功返回‘1’;
    /*copy_to_user :从内核区中读取数据到用户区
     #include <linux/uaccess.h>
     unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
     如果数据拷贝成功,则返回零;否则,返回没有拷贝成功的数据字节数。
     *to是用户空间的指针,
     *from是内核空间指针,
     n表示从内核空间向用户空间拷贝数据的字节数
   */
        err = -EFAULT;
goto out;
} err = sizeof(dev->val); out:
   up(&(dev->sem));  
    return err;
} /*写设备的寄存器值val*/
static ssize_t hello_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) {
struct hello_android_dev* dev = filp->private_data;
ssize_t err = ; /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS;
} if(count != sizeof(dev->val)) {
goto out;
} /*将用户提供的缓冲区的值写到设备寄存器去,读成功返回‘0’,失败返回数据字节数*/
if(copy_from_user(&(dev->val), buf, count)) { //这个buf是个地址
err = -EFAULT;
goto out;
} err = sizeof(dev->val); out:
up(&(dev->sem));
return err;
}

 ---------------------------------------*/

定义通过sysfs文件系统访问方法,这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现hello_val_show和hello_val_store两个方法,同时定义了两个内部使用的访问val值的方法__hello_get_val和__hello_set_val:

/*读取寄存器val的值到缓冲区buf中,内部使用*/
static ssize_t __hello_get_val(struct hello_android_dev* dev, char* buf) {
int val = ; /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS; //函数直接跳出,没有执行读取任务
} val = dev->val;
up(&(dev->sem)); return snprintf(buf, PAGE_SIZE, "%d\n", val); //snprintf:将可变个参数(...)按照format格式化成字符串,然后将其复制到str中,不是太懂?答:snprintf有数据格式转换的功能
}  

/*把缓冲区buf的值写到设备寄存器val中去,内部使用*/
static ssize_t __hello_set_val(struct hello_android_dev* dev, const char* buf, size_t count) {
int val = ; /*将字符串转换成数字*/
  /*

    就是解析字符串cp 中 8,10,16 进制数字   ,返回值是解析的数字,endp 指向第一个不是数值的字符串起始处,base :进制 

    unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)

  */
val
= simple_strtol(buf, NULL, ); //将一个字符串转换成 sigend long型数据,十进制 /*同步访问*/
if(down_interruptible(&(dev->sem))) {
return -ERESTARTSYS;
} dev->val = val;
up(&(dev->sem)); return count;
} /*读取设备属性val*/
static ssize_t hello_val_show(struct device* dev, struct device_attribute* attr, char* buf) {
struct hello_android_dev* hdev = (struct hello_android_dev*)dev_get_drvdata(dev);
//返回驱动数据的指针, 参数设备指针
return __hello_get_val(hdev, buf);
} /*写设备属性val*/
static ssize_t hello_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count) {
struct hello_android_dev* hdev = (struct hello_android_dev*
return __hello_set_val(hdev, buf, count);
}

/*----------------------------------------------

device_attribute:

struct driver_attribute {
struct attribute attr;
ssize_t (*show)(struct device_driver *driver, char *buf);
ssize_t (*store)(struct device_driver *, const char * buf, size_t count);
};

Device drivers can export attributes via their sysfs directories.Drivers can declare attributes using a DRIVER_ATTR macro that works identically to the DEVICE_ATTR macro.实例:在GSENSOR 8452驱动中申明的属性,可以在ADB中查看到属性。

属性部分出口确实不太清楚

---------------------------------------------------*/

定义通过proc文件系统访问方法,主要实现了hello_proc_read和hello_proc_write两个方法,同时定义了在proc文件系统创建和删除文件的方法hello_create_proc和hello_remove_proc:

proc:在用户态检查内核状态的机制

/*读取设备寄存器val的值,保存在page缓冲区中*/
static ssize_t hello_proc_read(char* page, char** start, off_t off, int count, int* eof, void* data) {
/*off_t类型默认是32位的long int */
if(off > ) {
*eof = ;
return ;
} return __hello_get_val(hello_dev, page);
} /*把缓冲区的值buff保存到设备寄存器val中去*/
static ssize_t hello_proc_write(struct file* filp, const char __user *buff, unsigned long len, void* data) {
int err = ;
char* page = NULL; if(len > PAGE_SIZE) { //PAGE_SIZE这个如何定义,量多少?
printk(KERN_ALERT"The buff is too large: %lu.\n", len); //保存量超标
return -EFAULT;
} page = (char*)__get_free_page(GFP_KERNEL);
if(!page) {
      /*没有足够的内存,你必须处理这种错误! */
printk(KERN_ALERT"Failed to alloc page.\n");
return -ENOMEM;
} /*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/
if(copy_from_user(page, buff, len)) {
printk(KERN_ALERT"Failed to copy buff from user.\n");
err = -EFAULT;
goto out;
} err = __hello_set_val(hello_dev, page, len); out:
free_page((unsigned long)page);
return err; //为‘0’拷贝失败(与上面相反)
} /*创建/proc/hello文件*/
static void hello_create_proc(void) {
// proc_dir_entry: http://www.cnblogs.com/Ph-one/p/4411557.html struct proc_dir_entry* entry;
entry = create_proc_entry(HELLO_DEVICE_PROC_NAME, , NULL);
if(entry) {
entry->owner = THIS_MODULE;
entry->read_proc = hello_proc_read;
entry->write_proc = hello_proc_write;
}
} /*删除/proc/hello文件*/
static void hello_remove_proc(void) {
remove_proc_entry(HELLO_DEVICE_PROC_NAME, NULL);
}

/*--

struct file ──字符设备驱动相关重要结构

文件结构 代表一个打开的文件描述符,它不是专门给驱动程序使用的,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,知道最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。

--*/

/*--

printk是在内核中运行的向控制台输出显示的函数,printk相当于printf的孪生姐妹.

--*/

最后,定义模块加载和卸载方法,这里只要是执行设备注册和初始化操作:

/*初始化设备*/
static int __hello_setup_dev(struct hello_android_dev* dev) {
int err;
dev_t devno = MKDEV(hello_major, hello_minor); //dev_t:unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号
          //MKDEV:获取设备在设备表中的位置
memset(dev, , sizeof(struct hello_android_dev)); //清零 cdev_init(&(dev->dev), &hello_fops); //空间的申请,并且清零
dev->dev.owner = THIS_MODULE;
dev->dev.ops = &hello_fops; //动态内存定义初始化 /*注册字符设备,初始化后将它添加到系统中*/
err = cdev_add(&(dev->dev),devno, );
if(err) {
return err;
} /*初始化信号量和寄存器val的值*/
init_MUTEX(&(dev->sem));
dev->val = ; return ;
} /*模块加载方法*/
static int __init hello_init(void){
int err = -;
dev_t dev = ;
struct device* temp = NULL; printk(KERN_ALERT"Initializing hello device.\n"); /*动态分配主设备和从设备号*/
err = alloc_chrdev_region(&dev, , , HELLO_DEVICE_NODE_NAME);
if(err < ) {
printk(KERN_ALERT"Failed to alloc char dev region.\n");
goto fail;
} hello_major = MAJOR(dev);
hello_minor = MINOR(dev); /*分配hello设备结构体变量*/
hello_dev = kmalloc(sizeof(struct hello_android_dev), GFP_KERNEL);
if(!hello_dev) {
err = -ENOMEM;
printk(KERN_ALERT"Failed to alloc hello_dev.\n");
goto unregister;
} /*初始化设备*/
err = __hello_setup_dev(hello_dev);
if(err) {
printk(KERN_ALERT"Failed to setup dev: %d.\n", err);
goto cleanup;
} /*在/sys/class/目录下创建设备类别目录hello*/
hello_class = class_create(THIS_MODULE, HELLO_DEVICE_CLASS_NAME);
if(IS_ERR(hello_class)) {   //IS_ERR检验函数,1:错,0:对
err = PTR_ERR(hello_class);
printk(KERN_ALERT"Failed to create hello class.\n");
goto destroy_cdev;
} /*在/dev/目录和/sys/class/hello目录下分别创建设备文件hello*/
temp = device_create(hello_class, NULL, dev, "%s", HELLO_DEVICE_FILE_NAME); //创建一个设备节点,节点名为:HELLO_DEVICE_FILE_NAME
if(IS_ERR(temp)) {     //  http://www.cnblogs.com/Ph-one/p/4414540.html
err = PTR_ERR(temp);
//将指针转换成错误号
printk(KERN_ALERT"Failed to create hello device.");
goto destroy_class;
} /*在/sys/class/hello/hello目录下创建属性文件val*/
err = device_create_file(temp, &dev_attr_val);
if(err < ) {
printk(KERN_ALERT"Failed to create attribute val.");
goto destroy_device;
} dev_set_drvdata(temp, hello_dev);
/*创建/proc/hello文件*/
hello_create_proc(); printk(KERN_ALERT"Succedded to initialize hello device.\n");
return ; destroy_device:
device_destroy(hello_class, dev);


destroy_class:
class_destroy(hello_class); //删class destroy_cdev:
cdev_del(&(hello_dev->dev)); //删 cleanup:
kfree(hello_dev); //kfree函数很简单,将Slab对象释放到阵列缓存中,如果缓存满了,则批量释放到Slab缓存中 unregister:
unregister_chrdev_region(MKDEV(hello_major, hello_minor), ); //释放hello_major在设备号,1个设备 fail:
return err;
} /*模块卸载方法*/
static void __exit hello_exit(void) { //__exit:标记退出代码,对于非模块无效
dev_t devno = MKDEV(hello_major, hello_minor);
   /*dev_t:unsigned int 类型,32位,用于在驱动程序中定义设备编号,高12位为主设备号,低20位为次设备号*/ printk(KERN_ALERT"Destroy hello device.\n"); //驱动破坏 /*删除/proc/hello文件*/
hello_remove_proc(); /*销毁设备类别和设备*/
if(hello_class) {
device_destroy(hello_class, MKDEV(hello_major, hello_minor));
class_destroy(hello_class);
} /*删除字符设备和释放设备内存*/
if(hello_dev) {
cdev_del(&(hello_dev->dev)); //释放 cdev占用的内存
kfree(hello_dev);
} /*释放设备号*/
unregister_chrdev_region(devno, ); //当用到这个函数的时候,就不能用杂项设备misc,中的 misc_deregister(&tiny4412_led_dev);杂项设备相当于设备号为10的字符设备
} MODULE_LICENSE("GPL"); //模块许可证
MODULE_DESCRIPTION("First Android Driver"); //模块描述 module_init(hello_init); // module_init除了初始化加载之外,还有后期释放内存的作用
module_exit(hello_exit);
/*module_exit的参数卸载时同__init类似,如果驱动被编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,显然__init和__exit对动态加载的模块是无效的,只支持完全编译进内核*/

/*--

/* 判断返回的指针是错误信息还是实际地址,即指针是否落在最后一页 
是实际地址:落在最后一页,返回‘0’
不是实际地址:没有落在最后一页,返回‘1’
*/
static inline long IS_ERR(const void *ptr) //☆☆
{
return IS_ERR_VALUE((unsigned long)ptr);
}

--*/

/*--

unregister_chrdev_region | 释放设备号
在调用 cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,其函数原型为:
引用 void unregister_chrdev_region(dev_t first, unsigned int count);
first为第一个设备号,count为申请的设备数量

--*/

 以上是通过devfs和proc两种方式访问设备属性;

五.在hello目录中新增Kconfig和Makefile两个文件,其中Kconfig是在编译前执行配置命令 make menuconfig 时用到的,而Makefile是执行编译命令make是用到的:

Kconfig文件的内容

       config HELLO
           tristate "First Android Driver"
           default n
           help
           This is the first android driver.
      Makefile文件的内容
      obj-$(CONFIG_HELLO) += hello.o
      在Kconfig文件中,tristate表示编译选项HELLO支持在编译内核时,hello模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行make menuconfig命令来配置编译选项,使得hello可以以模块或者内建的方法进行编译。
      在Makefile文件中,根据选项HELLO的值,执行不同的编译方法。
      六. 修改arch/arm/Kconfig和drivers/kconfig两个文件,在menu "Device Drivers"和endmenu之间添加一行:
      source "drivers/hello/Kconfig"
        这样,执行make menuconfig时,就可以配置hello模块的编译选项了。. 
        七. 修改drivers/Makefile文件,添加一行:
        obj-$(CONFIG_HELLO) += hello/
        八. 配置编译选项:
        USER-NAME@MACHINE-NAME:~/Android/kernel/common$ make menuconfig
        找到"Device Drivers" => "First Android Drivers"选项,设置为y。
        注意,如果内核不支持动态加载模块,这里不能选择m,虽然我们在Kconfig文件中配置了HELLO选项为tristate。要支持动态加载模块选项,必须要在配置菜单中选择Enable loadable module support选项;在支持动态卸载模块选项,必须要在Enable loadable module support菜单项中,选择Module unloading选项。
        九. 编译:
        USER-NAME@MACHINE-NAME:~/Android/kernel/common$ make
        编译成功后,就可以在hello目录下看到hello.o文件了,这时候编译出来的zImage已经包含了hello驱动。
        十. 参照在Ubuntu上下载、编译和安装Android最新内核源代码(Linux Kernel)一文所示,运行新编译的内核文件,验证hello驱动程序是否已经正常安装:
        USER-NAME@MACHINE-NAME:~/Android$ emulator -kernel ./kernel/common/arch/arm/boot/zImage &
        USER-NAME@MACHINE-NAME:~/Android$ adb shell
        进入到dev目录,可以看到hello设备文件:
        root@android:/ # cd dev
        root@android:/dev # ls
        进入到proc目录,可以看到hello文件:
        root@android:/ # cd proc
        root@android:/proc # ls
        访问hello文件的值:
        root@android:/proc # cat hello
        root@android:/proc # echo '5' > hello
        root@android:/proc # cat hello
        5
        进入到sys/class目录,可以看到hello目录:
        root@android:/ # cd sys/class
        root@android:/sys/class # ls
        进入到hello目录,可以看到hello目录:
        root@android:/sys/class # cd hello
        root@android:/sys/class/hello # ls
        进入到下一层hello目录,可以看到val文件:
        root@android:/sys/class/hello # cd hello
        root@android:/sys/class/hello/hello # ls
        访问属性文件val的值:
        root@android:/sys/class/hello/hello # cat val
        5
        root@android:/sys/class/hello/hello # echo '0'  > val
        root@android:/sys/class/hello/hello # cat val
        至此,我们的hello内核驱动程序就完成了,并且验证一切正常。这里我们采用的是系统提供的方法和驱动程序进行交互,也就是通过proc文件系统和devfs文件系统的方法,下一篇文章中,我们将通过自己编译的C语言程序来访问/dev/hello文件来和hello驱动程序交互,敬请期待。
原创:http://blog.csdn.net/luoshengyang/article/details/6580267
出自:http://www.cnblogs.com/Ph-one/p/4399233.html
瘋耔注解添加修改