目录

什么是驱动:

设备分类:

驱动认知:

字符设备驱动工作原理

字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

驱动程序开发步骤

基于驱动框架的代码开发

驱动模块代码编译和测试

加载内核驱动


什么是驱动:

驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。

设备分类:

linux系统将设备分为3类:字符设备、块设备、网络设备。

  1. 字符设备:指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
  2. 块设备: 指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  3. 网络设备: 网络设备可以是一个硬件设备,如网卡; 但也可以是一个纯粹的软件设备, 比如回环接口(lo).一个网络接口负责发送和接收数据报文。

驱动认知:

先看一张图,图中描述了流程,有助了解驱动。

用户态

  • 是指用户编写程序、运行程序的层面,用户态在开发时需要C的基础和C库,C库讲到文件,进程,进程间通信,线程,网络,界面(GTk)。C库(是linux标准库一定有):就是Clibary,提供了程序支配内核干活的接口,调用的open,read,write,fork,pthread,socket由此处封装实现,由写的应用程序调用,C库中的各种API调用的是内核态,支配内核干活。

内核态:

  • 用户要使用某个硬件设备时,需要内核态的设备驱动程序,进而驱动硬件干活,就比如之前文章里面所提到的wiringPi库,就是提供了用户操控硬件设备的接口,在没有wiringPi库时就需要自己实现wiringPi库的功能,就是自己写设备驱动程序。这样当我们拿到另一种类型的板子时,同样也可以完成开发。
  • 在linux中一切皆文件,各种的文件和设备(比如:鼠标、键盘、屏幕、flash、内存、网卡、如下图所示:)都是文件,那既然是文件了,就可以使用文件操作函数来操作这些设备。有一个问题,open、read等这些文件操作函数是如何知道打开的文件是哪一种硬件设备呢?①在open函数里面输入对应的文件名,进而操控对应的设备。②通过设备号(主设备号和次设备号)。除此之外我们还要了解这些驱动程序的位置,和如何实现这些驱动程序,每一种硬件设备对应不同的驱动(这些驱动有我们自己来实现)。
  • Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号(如下图所示:)。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。 一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

  • 驱动链表:管理所有设备的驱动,添加或查找, 添加是发生在我们编写完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。
  • 综上所述:如果想要打开dev下面的pin4引脚,过程是:用户态调用open(“/de/pin4”,O_RDWR),对于内核来说,上层调用open函数会触发一个软中断(系统调用专用,中断号是0x80,0x80代表发生了一个系统调用),系统进入内核态,并走到system_call,可以认为这个就是此软中断的中断服务程序入口,然后通过传递过来的系统调用号来决定调用相应的系统调用服务程序(在这里是调用VFS中的sys_open)。sys_open会在内核的驱动链表里面根据设备名和设备号查找到相关的驱动函数(每一个驱动函数是一个节点),驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。
  • system_call函数是怎么找到详细的系统调用服务例程的呢? 通过系统调用号查找系统调用表sys_call_table!软中断指令INT 0x80运行时,系统调用号会被放入 eax 寄存器中,system_call函数能够读取eax寄存器获取,然后将其乘以4,生成偏移地址,然后以sys_call_table为基址。基址加上偏移地址,就能够得到详细的系统调用服务例程的地址了!然后就到了系统调用服务例程了。

补充:

  1. 每个系统调用都对应一个系统调用号,而系统调用号就对应内核中的相应处理函数。

  2. 所有系统调用都是通过中断0x80来触发的。

  3. 使用系统调用时,通过eax 寄存器将系统调用号传递到内核,系统调用的入参通过ebx、ecx……依次传递到内核

  4. 和函数一样,系统调用的返回值保存在eax中,所有要从eax中取出

字符设备驱动工作原理

字符设备驱动工作原理在linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?

补充:

  • 在Linux文件系统中,每个文件都用一个 struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。

  • 在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。

  • 在linux操作系统中, 每个驱动程序都有一个设备号。

  • 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。

(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。

(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。

(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。

(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。

其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。

字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operations之间的连接,通过cdev_add( )向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。

用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。

驱动程序开发步骤

Linux 内核就是由各种驱动组成的,内核源码中有大约 85%是各种驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合具体单板。

编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。

一般来说,编写一个 linux 设备驱动程序的大致流程如下:

  • 查看原理图、数据手册,了解设备的操作方法;
  • 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
  • 实现驱动程序的初始化:比如向内核注册这个驱动程序,这样应用程序传入文件名时,内核才能找到相应的驱动程序;
  • 设计所要实现的操作,比如 open、close、read、write 等函数;
  • 实现中断服务(中断并不是每个设备驱动所必须的);
  • 编译该驱动程序到内核中,或者用 insmod 命令加载;
  • 测试驱动程序;

下面就以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。

基于驱动框架的代码开发

上层调用代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void main()
{int fd,data;fd = open("/dev/pin4",O_RDWR);if(fd<0){printf("open fail\n");perror("reson:");}else{printf("open success\n");}fd=write(fd,'1',1);
}

驱动框架代码

#include <linux/fs.h>       //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>  //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件static struct class *pin4_class;
static struct device *pin4_class_dev;static dev_t devno;                //设备号
static int major = 231;           //主设备号
static int minor = 0;             //次设备号
static char *module_name = "pin4";   //模块名//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数和printf类似return 0;
}//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{printk("pin4_write\n");return 0;
}static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,
};int __init pin4_drv_init(void)   //真实驱动入口
{int ret;devno = MKDEV(major, minor);  //创建设备号ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中pin4_class=class_create(THIS_MODULE, "myfirstdemo");       //用代码在dev自动生成设备pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);  //创建设备文件return 0;
}void __exit pin4_drv_exit(void)
{device_destroy(pin4_class, devno);class_destroy(pin4_class);unregister_chrdev(major, module_name);  //卸载驱动}module_init(pin4_drv_init);  //入口,内核加载该驱动(insmod)的时候,这个宏被使用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

上面说过驱动开发的重点难点在于读懂框架代码,在里面进行设备的添加和修改,下面就来了解一下这个框架逻辑。

驱动框架设计流程

1. 确定主设备号
 2. 定义结构体   类型 file_operations
 3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
 4. 实现驱动入口:安装驱动程序时,就会去调用这个入口函数,执行工作:                       ① 把 file_operations 结构体告诉内核:注册驱动程序register_chrdev.
 ② 创建类class_create.
 ③ 创建设备device_create.
 5. 实现出口:卸载驱动程序时,就会去调用这个出口函数,执行工作:                           ① 把 file_operations 结构体从内核注销:unregister_chrdev.
 ② 销毁类class_create.
 ③ 销毁设备结点device_destroy.
 6. 其他完善:GPL协议、入口加载

1、确定主设备、变量定义

static struct class *pin4_class;
static struct device *pin4_class_dev;static dev_t devno;                //设备号
static int major = 231;           //主设备号
static int minor = 0;             //次设备号
static char *module_name = "pin4";   //模块名

2、定义file_operations结构体,加载到内核驱动链表中

这是Linux内核中的file_operations  结构体

根据上层调用函数定义结构体成员

static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,.read  = pin4_read,
};

3、实现结构体成员pin4_read等函数

static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{printk("pin4_read\n");return 0;
}//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数和printf类似return 0;
}//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{printk("pin4_write\n");return 0;
}

4、驱动入口

int __init pin4_drv_init(void)   //真实驱动入口
{int ret;devno = MKDEV(major, minor);  //创建设备号ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动  告诉内核,把这个驱动加入到内核驱动的链表中pin4_class=class_create(THIS_MODULE, "myfirstdemo");//由代码在dev自动生成设备pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);  //创建设备文件return 0;
}

其中pin4_class=class_create(THIS_MODULE, "myfirstdemo");//由代码在dev自动生成设备,除此之外还可以手动生成设备,在dev目录下 sudo mknod +设备名字 +设备类型(c表示字符设备驱动) +主设备号+次设备号。

5、出口

void __exit pin4_drv_exit(void)
{device_destroy(pin4_class, devno);class_destroy(pin4_class);unregister_chrdev(major, module_name);  //卸载驱动}

6、GPI协议,入口加载,出口加载

module_init(pin4_drv_init);  //入口,内核加载该驱动(insmod)的时候,这个宏被使用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

驱动模块代码编译和测试

编译阶段

驱动模块代码编译(模块的编译需要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)

#include <linux/fs.h>            //file_operations声明
#include <linux/module.h>    //module_init  module_exit声明
#include <linux/init.h>      //__init  __exit 宏定义声明
#include <linux/device.h>        //class  devise声明
#include <linux/uaccess.h>   //copy_from_user 的头文件
#include <linux/types.h>     //设备号  dev_t 类型声明
#include <asm/io.h>          //ioremap iounmap的头文件static struct class *pin4_class;
static struct device *pin4_class_dev;static dev_t devno;                //设备号
static int major = 231;                    //主设备号
static int minor = 0;                      //次设备号
static char *module_name = "pin4";   //模块名static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{printk("pin4_read\n");return 0;
}
//led_open函数
static int pin4_open(struct inode *inode,struct file *file)
{printk("pin4_open\n");  //内核的打印函数和printf类似return 0;
}//led_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{printk("pin4_write\n");return 0;
}static struct file_operations pin4_fops = {.owner = THIS_MODULE,.open  = pin4_open,.write = pin4_write,.read  = pin4_read,
};int __init pin4_drv_init(void)   //真实驱动入口
{int ret;devno = MKDEV(major, minor);  //创建设备号ret   = register_chrdev(major, module_name, &pin4_fops);  //注册驱动  告诉内
核,把这个驱动加入到内核驱动的链表中pin4_class=class_create(THIS_MODULE, "myfirstdemo");                //用代码
在dev自动生成设备return 0;
}void __exit pin4_drv_exit(void)
{device_destroy(pin4_class, devno);class_destroy(pin4_class);unregister_chrdev(major, module_name);  //卸载驱动
}module_init(pin4_drv_init);  //入口,内核加载该驱动(insmod)的时候,这个宏被使>用
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");

将该驱动代码拷贝到   linux-rpi-4.14.y/drivers/char   目录下 文件中(也可选择设备目录下其它文件)

修改该文件夹下Makefile(驱动代码放到哪个目录,就修改该目录下的Makefile),将上面的代码编译生成模块,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),所以只需要将obj-m += pin4drive.o添加到Makefile中即可。

回到linux-rpi-4.14.y/编译驱动文件

使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。

编译生成驱动模块会生成以下几个文件:

.o的文件是object文件,.ko是kernel object,与.o的区别在于其多了一些sections,比如.modinfo。.modinfo section是由kernel source里的modpost工具生成的,包括MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_LICENSE, device ID table以及模块依赖关系等等。 depmod 工具根据.modinfo section生成modules.dep, modules.*map等文件,以便modprobe更方便的加载模块。
编译过程中,经历了这样的步骤:先进入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件,而后根据此文件编译出pin4drive.mod.o,之后连接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。

将生成的.ko文件发送给树莓派:scp pin4drive.ko pi@192.168.1.106:/home/pi

pin4test.c (上层调用代码) 进行 交叉编译后发送给树莓派,就可以看到pi目录下存在发送过来的.ko文件pin4test这两个文件,

加载内核驱动

sudo insmod pin4drive.ko

加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中) 加载完成后就可以在dev下面看到名字为pin4的设备驱动(这个和驱动代码里面static char *module_name="pin4"; //模块名这行代码有关),设备号也和代码里面相关。

lsmod查看系统的驱动模块,执行上层代码,赋予权限

查看内核打印的信息,

dmesg |grep pin4

如下图所示:表示驱动调用成功

在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写ko)将驱动卸载。

调用流程:

我们上层空间的open去查找dev下的驱动(文件名),文件名背后包含了驱动的主设备号和次设备号,此时用户open触发一个系统调用,系统调用经过vfs(虚拟文件系统),vfs根据文件名背后的设备号去调用sys_open去判断,找到内核中驱动链表的驱动位置,再去调用驱动里面自己的dev_open函数

为什么生成驱动模块需要在虚拟机上生成?树莓派不行吗?

生成驱动模块需要编译环境(linux源码并且编译,需要下载和系统版本相同的Linux内核源代码),也可以在树莓派上面编译,但在树莓派里编译,效率会很低,要非常久。这篇文章有讲树莓派驱动的本地编译。

Linux驱动(驱动程序开发、驱动框架代码编译和测试)相关推荐

  1. 《精通Linux设备驱动程序开发》——1.7 编译内核

    本节书摘来自异步社区<精通Linux设备驱动程序开发>一书中的第1章,第1.7节,作者:[印]Sreekrishnan Venkateswaran(斯里克里斯汉 温卡特斯瓦兰)著,更多章节 ...

  2. 《精通Linux设备驱动程序开发》——1.5 Linux发行版

    本节书摘来自异步社区<精通Linux设备驱动程序开发>一书中的第1章,第1.5节,作者:[印]Sreekrishnan Venkateswaran(斯里克里斯汉 温卡特斯瓦兰)著,更多章节 ...

  3. 嵌入式Linux设备驱动程序开发指南9(平台设备驱动)——读书笔记

    平台设备驱动 九.平台设备驱动 9.1 平台设备驱动概述 9.2 GPIO驱动 9.2.1 简介 9.2.2 硬件名称 9.2.3 引脚控制器 9.2.4 引脚控制子系统 9.2.5 GPIO控制器驱 ...

  4. linux usbnet网络驱动,[技术资料]基于usb设备的linux网络驱动程序开发

    1 引言 做为开放源代码 (open source) 运动重要组成部分,linux操作系统吸引了数以万计的程序员共同开发.由于linux比较完整的继承了各种unix版本的稳定和高效,并且克服和改进了传 ...

  5. 《精通linux设备驱动程序开发》——视频驱动程序

    一.显示架构 1.PC系统的显示连接.(P247  图12-1) 2.VGA(视频图形阵列)是IBM提出的早期显示标准.VGA的分辨率是640X480,而更新的标准比如SVGA(高级视频图形阵列)和X ...

  6. 《精通linux设备驱动程序开发》——PCMCIA和CF

    一.PCMCIA和CF是什么 1.PCMCIA是16位数据传输接口规范.CF卡比PCMCIA要小,但和PCMCIA相兼容. 1).CF卡有50个引脚,但使用无源的CF_PCMCIA转换的适配器,可插入 ...

  7. 嵌入式Linux设备驱动程序开发指南14(Linux设备驱动使用DMA)——读书笔记

    Linux设备驱动使用DMA 十四.Linux设备驱动使用DMA 14.1 简介 14.2 缓存一致性 14.3 DMA控制器接口 14.4 流式DMA模块 14.4.1 sdma_sam_m2m.c ...

  8. 嵌入式Linux设备驱动程序开发指南20(Linux USB设备驱动)——读书笔记

    Linux USB设备驱动 二十.Linux USB设备驱动 20.1 USB简介 20.1.1 USB2.0总线拓扑 20.1.2 USB总线枚举和设备布局 20.1.3 USB数据传输 20.1. ...

  9. linux驱动设备信息失败怎么办,Linux设备驱动程序(LDD)中snull的编译问题 《转》

    对LDD中snull程序,编译的时候会有许多问题,鉴于网上还没有合适的解决办法,做此总结,整理知识.本文在debian6.0上运行通过,内核版本为2.6.32. 学习LDD中网络驱动程序部分,理解sn ...

最新文章

  1. 单点登录与权限管理本质:session和cookie介绍
  2. NLP:自然语言处理技术近十年发展技术更迭的简介、案例之详细攻略(持续更新)
  3. 2021年Kaggle所有赛事TOP方案汇总
  4. linux用户操作的日志,linux 用户操作记录并录入日志
  5. java删除csv一行_如何删除两个CSV之间的不常见行?
  6. C++算法五:二分查找(折半)查找
  7. linux操作系统基础与实训教程,清华大学出版社-图书详情-《Linux操作系统基础与实训教程》...
  8. [转]Java jdbc数据库连接池总结!
  9. matlab 方差_统计学原理与matlab——(3)几何分布与超几何分布
  10. LINUX上安装Caffe
  11. 网维无盘服务器错误代码,网维大师无盘环境INTER傲腾方案常见问题解答?
  12. C#学习(二十八)——ManualResetEvent的理解和使用
  13. Texlive安装宏包
  14. 计算机图形学入门(十四)-光线追踪(加速包围盒体)
  15. 编程,初来乍到,多多关照。
  16. 《英语语法新思维初级教程》学习笔记(四)数量限定词和个体限定词
  17. Codeforces 1359 C. Mixing Water (二分 /数学)
  18. Android练手小项目--仿72g赚吧App
  19. python乘法符号手写_利用Python自动生成小学生加减乘除口算考试题卷,不再为手写算术题烦恼!...
  20. 资深大牛带你了解源码!带你轻松理解Android-Hook机制,已开源

热门文章

  1. vscode报错No such file or directory:
  2. 【CSDN的2022与2023】普普通通的三年,从懵懂、焦虑到坚定、奋进,破除焦虑努力成为更好的自己
  3. 开源数据库成为发展自主可控产品的主要途径 ▏开源数据库系列谈之三
  4. 常用linux、命令单词缩写对照
  5. 制作系统U盘,重装系统
  6. 微信登录出现的问题及解决方案
  7. 大家好,给大家介绍一下,这是我要买的新手机
  8. 高通骁龙670是为压制联发科P60而生
  9. Day52_Flume
  10. mysql复合索引单列索引