已经有段时间没好好地写博客了,最近在研究安卓底层,所以想写写我对安卓底层的认识和总结。本篇是安卓底层学习总结系列的第一篇,驱动开发基础。
安卓系统,想必我也不用作太多介绍,这里我要提及的是安卓系统和嵌入式系统十分接近,所编写的驱动程序实际上大多也可以认为是嵌入式驱动程序。并且安卓的内核是Linux,所以写安卓驱动程序实际上和写Linux内核模块差不多,我门这篇主要认识PC中的Linux驱动。
所谓驱动,就是内核与外部设备的媒介,下面介绍有关驱动需要知道的知识。
在shell中查看这个目录
ls -l /dev
可以看到所有的设备文件节点,通常为以下格式
crw-r--r-- 1 root root 10, 235 3月 29 08:12 autofs
文件类型
– 上面格式的第一个字符c代表了这个设备文件的文件类型为字符设备,b就是块设备,网络设备没有设备文件。
主设备号
– 设备类型和主设备号唯一确定设备文件的驱动程序和界面。在上述格式中10, 235的10就是代表了主设备号。
次设备号
– 说明目标设备是同类设备的第几个,在上述格式中10, 235的235就是代表了次设备号。
例:
crw------- 1 root root 10, 59 3月 29 08:12 cpu_dma_latency
crw------- 1 root root 10, 203 3月 29 08:12 cuse
上面两个字符设备同属于一种设备,但不是一个设备。
统一管理查看内核功能参数和设备模型
/sys/block # 所有块设备
/sys/bus # 按总线类型分层放置的目录结构
/sys/class # 按设备功能放置
/sys/class/mem # mem目录包含各个设备的链接,指向devices各个具体设备
/sys/devices # 分层次放置
/sys/dev # 字符设备和块设备的主次号
/sys/fs # 描述所有文件系统
/sys/kernel # 内核所有可调整参数位置
/sys/module # 所有模块信息
/sys/power # 系统电源选项
驱动程序通常是以内核模块的方式编写,并且插入到系统内核进行执行,所以我们得先了解什么是内核模块。
Linux是一个单体内核系统,分成5个子系统,整个内核在一个地址空间。Linux提供了模块机制,来为其增加设备;只需编译模块,再插入内核就可以完成设备增加。而内核模块就是可以在系统运行期间动态安装和拆卸的内核功能单元。
要编写一个内核模块就要先了解一下基本函数。
首先,内核与用户之间数据是不互通的,要互相使用数据得经过系统调用,系统调用中有着一些基本函数,用来完成基本任务。
比如:
- copy_to_user
主要用于将内核段中的数据拷贝到用户段的内存中去
- copy_from_user
主要用于将用户段内存中的数据拷贝到内核中
这些函数在用户态是无法使用的,也就是说,在外部写的.c程序库中是不包含这两个函数的。所以编写内核程序是与编写普通c程序是有所区别的。
下面贴出一个简单的helloworld内核程序,我们在具体程序中进行解释。
#include // 定义了module_init等函数
#include // 最基本的头文件,其中定义了MODULE_LICENSE等宏
// 当插入内核模块时,系统将调用下面的module_init宏,然后通过module_init调用此函数
static int hello_init(void){
/**
*printk在函数内部,有代码申请了一块静态缓冲区,当与控制台建立连接时,将缓冲区打印到终端
*注意:它不支持浮点数,记得打印时+\n,不然的话不会立即打印,打印级别数字越小级别越高
*KERN_CRIT表示 critical conditions级别的调试级别,级别数字为2
**/
printk(KERN_CRIT "HELLO WORLD!!!\n"); // \n用处很大,最好不要省
return 0;
}
// 与hello_init对应,在移除该内核模块时调用module_exit宏,然后调用此函数
static void hello_exit(void){
// KERN_WARNING级别数字为4
printk(KERN_WARNING "bye bye!!\n");
return;
}
// 下面都是宏,在加载卸载模块时调用
module_init(hello_init);
module_exit(hello_exit);
// 下面的内容是必须的,用于表明该模块的信息,用modinfo *.ko即可查看
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("一个简单的内核模块测试");
接下来编写Makefile文件,具体请自行查看资料
obj-m:=hello_module.o
PWD:=$(shell pwd)
default:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
下面,我们对其进行测试,在shell中输入以下命令
make
sudo insmod hello_module.ko
dmesg
即可查看信息,其中:
insmod
用于插入内核模块。
dmesg
用于打印内核日志信息。
sudo dmesg -C
可以清空日志信息。
用 modinfo *.ko
查看模块信息。
用 sudo rmmod hello_module
卸载模块。
最后我的内核日志打印信息为:
[ 9806.210068] HELLO WORLD!!! [10004.819841] perf: interrupt took too long (3137 > 3130), lowering kernel.perf_event_max_sample_rate to 63750 [10097.027480] bye bye!!
至此我们完成了一个简单的内核模块编程模板。
上面我们已经简单介绍了内核模块编写,下面我们来正式写一个有基本输入输出和基本测试程序的字符驱动程序模板。
#include // 定义了module_init
#include // 最基本的头文件,其中定义了MODULE_LICENSE等宏
#include // file_operations结构体所在
static const char *dev_name = "first_cdev"; // 设置设备名,之后可以在/proc/devices中查看该设备
static unsigned int major = 55; // 设置主设备号
/* open函数,用于打开设备文件
* 注:在linux中,一切皆文件,驱动设备文件也不例外,只不过设备文件是一种
* 特殊的文件,而对驱动程序的操作其实也是基于文件操作的。
*/
static int first_cdev_open(struct inode *inode, struct file *file){
printk("open\n");
return 0;
}
// 必须关闭设备文件
static int first_cdev_close(struct inode *inode, struct file *file)
{
printk(KERN_DEBUG "close\n");
return 0;
}
// 读取设备文件
static ssize_t first_cdev_read(struct file *file, char *buf,
size_t count, loff_t *offset)
{
printk(KERN_DEBUG "read :%ld", count);
if(count >= sizeof(unsigned int)){ // 如果读到了来自内核的数据
// 复制数据到用户程序进行输出
if(copy_to_user((void __user *)buf,
(void *)(&file->private_data), sizeof(unsigned int)))
return -EFAULT;
}
return count;
}
// ioctl操作,主要用于对驱动设备进行命令控制
// 被注释的这种方法已经被废弃static int first_cdev_ioctl(struct inode *inode, struct file *file,
/*
注意:在2.6.36以后ioctl函数已经不存在了,用unlocked_ioctl和compat_ioctl两个函数代替。参数去除了原来ioctl中的struct inode参数,返回值也发生了改变。
1、compat_ioctl:支持64bit的driver必须要实现的ioctl,当有32bit的用户程序调用64bit内核的ioctl的时候,这个callback会被调用到。如果没有实现compat_ioctl,那么32位的用户程序在64位的kernel上执行ioctl时会返回错误:Not a typewriter
2、如果是64位的用户程序运行在64位的kernel上,调用的是unlocked_ioctl,如果是32位的APP运行在32位的kernel上,调用的也是unlocked_ioctl
*/
static long first_cdev_ioctl(struct file *file,
unsigned int cmd, unsigned long arg)
{
char argk[4]; // 定义一个字符数组,存放一些字符
argk[0] = 0;
argk[1] = 1;
argk[2] = 2;
argk[3] = 3;
printk(KERN_DEBUG "ioctl:%x\n", cmd);
switch(cmd){ // 根据传来的命令指示进行操作
case 0: // 指令 0
printk(KERN_DEBUG "ctl NO.0\n");
// 将用户态程序的数据覆盖本地定义的字符数组,并打印从用户态程序获取的数据
if(copy_from_user(argk, (void __user *)arg, 4))
return -EFAULT;
printk("arg:%x,%x,%x,%x\n", argk[0], argk[1], argk[2], argk[3]);
break;
case 1: // 指令 1
printk(KERN_DEBUG "ctl NO.1\n");
// 将数据传入用户态应用程序
if(copy_to_user((void __user *)arg, argk, 4))
return -EFAULT;
break;
default:break;
}
return 0;
}
// write函数,当向内核程序写数据时调用
static ssize_t first_cdev_write(struct file *file,
const char __user *buf, size_t size, loff_t *ppos){
printk("write\n");
return 0;
}
// 在file_operations中注册open和write等函数
static struct file_operations first_cdev_fo = {
.owner = THIS_MODULE,
.open = first_cdev_open,
.release = first_cdev_close,
.read = first_cdev_read,
// .ioctl= first_cdev_ioctl,
.unlocked_ioctl = first_cdev_ioctl,
.write = first_cdev_write,
};
// 插入模块时调用
static int first_cdev_init(void){
// 注册设备,将file_operations结构体放到内核的特定数组中
// major作为主设备号
int res;
// 注册设备
res = register_chrdev(major, dev_name, &first_cdev_fo);
if(res < 0){
printk(KERN_DEBUG "register fail\n");
return res;
}
//if(dev_id < 0){
// printk("error\n");
//}
printk(KERN_CRIT "hello character devices!!\n");
return 0;
}
// 卸载模块时调用
static void first_cdev_exit(void){
// 注销设备
unregister_chrdev(major, dev_name);
printk(KERN_INFO "bye,character devices\n");
return;
}
module_init(first_cdev_init);
module_exit(first_cdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alexander");
MODULE_DESCRIPTION("第一个字符驱动模块编写");
下面是测试程序
#include
#include
#include
#include
#include
// 要调用的设备文件名
#define DEF_FILE_NAME "/dev/xxx"
int main(int argc, char **argv)
{
int fd, size;
char readbuf[8];
char writebuf[8] = "writebuf";
char ioarg[4];
char *dev_file;
if(1 == argc){ // 从命令行获取还是使用本地定义的设备文件名
dev_file = DEF_FILE_NAME;
} else {
dev_file = argv[1];
}
printf("<<<<<>>>>\n" , dev_file);
printf("test write:\n");
fd = open("/dev/xxx", O_RDWR); // 以读写方式打开设备文件
if(fd < 0){
printf("can't open device\n");
}
size = write(fd, writebuf, sizeof(writebuf)); // 向设备文件写入数据
close(fd); // 关闭设备文件
printf("test read:\n");
fd = open(dev_file, O_RDONLY); // 以只读方式打开设备文件
size = read(fd, readbuf, sizeof(readbuf)); // 从设备文件读取数据
// close(fd)
printf("read size:%d\n",size);
for(int i=0; i<size; i++){
printf("readbuf[%d]:%x\n", i, (unsigned char)readbuf[i]);
}
close(fd);
printf("test ioctl:\n");
fd = open(dev_file, O_RDWR); // 以读写方式打开设备文件
// 设置初始数组数据
ioarg[0] = 0xf0;
ioarg[1] = 0xf1;
ioarg[2] = 0xf2;
ioarg[3] = 0xf3;
printf("ioctl test 0\n");
ioctl(fd, 0, ioarg); // 执行0号命令,将数组写入设备文件
printf("ioctl test 1\n");
ioctl(fd, 1, ioarg); // 执行1号命令,从设备文件读
printf("arg:%x, %x, %x, %x\n", ioarg[0], ioarg[1], ioarg[2], ioarg[3]);
close(fd);
return 0;
}
Makefile文件
obj-m:=first_cdev.o
PWD:=$(shell pwd)
default:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
下面进行测试
make
gcc test.c -o test
sudo mknod /dev/xxx c(设备类型) 55(主设备号) 0(次设备号)
sudo insmod *.ko
./test
注:mknod
用来创建设备文件,指定设备文件主设备号为55,在制定前最好用 ls -l /dev
查看是否主设备号重复.
执行完insmod
命令就可以用cat /proc/devices
查看设备是否加载成功了。
接下来我们运行测试程序,以下是输出内容
<<<<<>>>> test write: can't open devive test read: read size:8 readbuf[0]:0 readbuf[1]:0 readbuf[2]:0 readbuf[3]:0 readbuf[4]:9d readbuf[5]:55 readbuf[6]:0 readbuf[7]:0 test ioctl: ioctl test 0 ioctl test 1 arg:fffffff0, fffffff1, fffffff2, fffffff3
用dmesg
查看内核日志
[11955.133491] hello character devices!! [11960.960142] open [11960.960148] read :8 [11960.960205] close
最后,卸载驱动程序
sudo rmmod first_cdev
sudo rm /dev/xxx
至此,我们完成了一个有输入输出功能的字符设备驱动程序模板。
总的来说,编写驱动程序并不难,但驱动程序主要与硬件相关,编写具体的驱动会需要特定硬件的芯片手册,所以以上只是Linux的驱动程序基础,编写驱动程序还需要进一步学习,比如学习系统的启动、设备树、硬件引脚等概念,学完后希望能在安卓开发板子上动手实践,下篇文章,我将对系统启动流程进行总结介绍。
本系列链接传送:
【Android底层学习总结】2. 安卓系统内核的启动
【Android底层学习总结】3. 内核中driver_init函数源码解析