文章目录
Linux内核系统调用实现原理
Linux设备驱动相关概念
Linux内核设备驱动分类
字符设备文件特点及属性
字符设备文件创建的方法
主设备号、次设备号、设备号
Linux相关库函数
字符设备相关数据结构
配套相关函数
编写Linux字符设备驱动步骤
定义初始化硬件操作接口对象:
定义初始化字符设备对象:
最终向内核注册字符设备对象
从内核卸载字符设备对象
最后编写之前定义的设备接口具体内容
示例
具体代码:
测试执行:
总结
Linux内核系统调用实现原理
- 当应用程序(进程)调用write系统调用函数,首先会调用C库的write函数
- 接下来C库的write函数会做以下的事情:
- 首先保存write函数的系统调用号到R7寄存器。
- “系统调用号”就是Linux内核给每一个系统调用函数分配的唯一的软件编号(类似函数ID,write为4)定义内核源码的arch/arm/include/asm/unistd.h
- 然后调用swi指令触发一个软中断异常(新版本触发异常指令为svc,老版本触发软中断指令为swi,新的编译器也同样支持swi指令)。
- 一旦触发软中断异常,CPU核立刻处理软中断异常。
- 硬件自动执行:1、备份CPSR到SPSR_SVC,设置CPSR:MODE=SVC_MODE(切换SVC管理模式),T=0:切换到ARM状态,IF=11:禁止FIQ/IRQ异常中断。保存返回地址:LR_SVC=PC-4,设置PC=0x08,至此CPU跑到0x08软中断处理的入口地址开启了软件处理中断异常流畅,软中断的处理入口地址由相关的Linux内核来实现,也就是说当前进程到此由用户空间陷入了内核空间执行。
- Linux内核软中断处理的入口地址相关代码将做以下工作:
- 从R7寄存器中取出之前保存的write函数的系统调用号(write : 4)。
- 以write系统调用号4作为下标在内核的系统调用表(数组)中找到对应的内核函数sys_write,找到对应的内核函数继续调用此函数,调用完毕后原路返回用户空间,至此write函数返回!
- “系统调用表”:本质就是一个数组,数组元素就是函数指针,数组元素的小标就是系统调用号,定义在内核源码的arch/arm/kernel/calls.S
Linux设备驱动相关概念
Linux内核设备驱动分类
- 字符设备驱动:对字符设备的访问按照字节流形式访问。
- 例如:
- LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)
- 触摸屏(XY绝对坐标),LCD显示屏(像素点)
- 声卡,摄像头,各种硬件传感器(三轴,重力,光线,距离,温度等)
- EEPROM存储器(I2C接口)
- 块设备驱动:对块设备的访问按照数据块进行,比如一次操作512字节
- 例如:
- 硬盘,U盘,TF卡,SD卡
- Nandflash,Norflash,EMMC
- 网络设备驱动:对网络设备驱动一般按照网络协议进行
- 例如:
- 有线网卡和无线网卡
注意
- 对应Linux系统来说,“一切皆文件”,即任何硬件外设都是以文件的形式存放,用户访问文件本质就是在访问对应的硬件外设。
- 字符设备对应的文件称之为字符设备文件。
- 块设备对应的文件称之为块设备文件。
- 网络设备无设备文件,通过socket套接字进行访问。
字符设备文件特点及属性
- 字符设备文件本身代表的就是字符设备硬件本身
- 字符设备文件存在于根文件系统必要目录的dev目录下 ,当然块设备文件也存于dev目录下
- 例如:查看板子上UART设备的字符设备文件:
- ls /dev/ttySAC* -lh 得到以下信息:
- crw-rw---- 204, 64 /dev/ttySAC0
- crw-rw---- 204, 65 /dev/ttySAC1
- crw-rw---- 204, 66 /dev/ttySAC2
- crw-rw---- 204, 67 /dev/ttySAC3
- 说明:
- c:表示此设备文件对应的设备为字符设备
- 204:表示串口的主设备号
- 64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
- ttySAC0:表示第一个串口的设备文件名
- ttySAC1:表示第二个串口的设备文件名
- ttySAC2:表示第三个串口的设备文件名
- ttySAC3:表示第四个串口的设备文件名
- 注意:一个硬件外设个体有唯一的一个设备文件名
字符设备文件创建的方法
- 手动创建,只需要mknod命令:
mknod /dev/(字符设备文件名) c 主设备号 次设备号
- 自动创建,该方式后续补上。
主设备号、次设备号、设备号
- 主设备号作用:应用程序根据字符设备文件的主设备号,在茫茫的内核驱动中找到对应的唯一的设备驱动,一个设备驱动仅有唯一的主设备号
- 次设备号作用:设备驱动根据次设备号能够找到应用程序要访问的硬件外设个体
- 总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号应用根据主设备号找驱动,驱动根据次设备号找硬件个体,所以,主,次设备号对于linux内核是一个宝贵的资源,某个 设备驱动必须要想linux内核申请主,次设备号
- 设备号:设备号包含主,次设备号,linux内核用dev_t(unsigned int)数据类型描述设备号信息,
- 设备号的高12位用来描述主设备号,
- 设备号的低20位用来描述次设备号,
- 设备号和主,次设备号之间的转换操作宏:
- 设备号=MKDEV(已知的主设备号,已知的次设备号);
- 主设备号=MAJOR(已知的设备号);
- 次设备号=MINOR(已知的设备号);
Linux相关库函数
- 向内核申请设备号函数
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- 参数:
- dev:保存申请的设备号,包括主设备号和起始的次设备号
- baseminor:希望申请的起始次设备号,一般给0。
- count:申请的次设备号的个数 , 如果baseminor=0,count=2,那么申请的次设备号分别是0和1
- name:申请设备号指定的名称,随便取 ,将来通过执行cat /proc/devices查看。
- 设备驱动一旦不再使用设备号,记得要将设备号资源归还给linux内核:
释放申请的设备号资源函数
void unregister_chrdev_region(dev_t dev, unsigned count);
- 参数:
- dev:申请的设备号
- count:申请的次设备号的个数
字符设备相关数据结构
- Linux内核描述给用户提供操作接口的数据结构
//描述字符设备驱动给用户提供的操作接口 struct file_operations { int (*open) (struct inode *, struct file *); //给用户提供打开设备的接口 int (*release) (struct inode *, struct file *); //给用户提供关闭设备的接口 ... };
//描述字符设备驱动 struct cdev { dev_t dev; //描述申请的设备号 int count; //描述申请的次设备号的个数 struct file_operations *ops;//给用户提供的操作接口 ... };
- 字符设备驱动和应用程序调用关系:
- 应用程序open->软中断->内核的sys_open->驱动的open接口
- 应用程序close->软中断->内核的sys_close->驱动的release接口
配套相关函数
cdev_init(strcut cdev *cdev, struct file_operations *fops)
- 函数功能:初始化字符设备驱动对象,就是给字符设备驱动对象添加一个硬件操作接口
- cdev:要初始化的字符设备对象
- fops:给用户提供的硬件操作接口
cdev_add(struct cdev *cdev, dev_t dev, int count)
- 函数功能:向内核注册添加一个字符设备对象,一旦添加完毕内核就有一个真实的字符设备驱动
- cdev:要注册的字符设备对象
- dev:申请的设备号
- count:申请的次设备号的个数
cdev_del(struct cdev *cdev)
- 函数功能:从内核中卸载字符设备对象,一旦卸载完毕,内核就不会有一个真实的字符设备驱动
编写Linux字符设备驱动步骤
定义初始化硬件操作接口对象:
- 例如:
struct file_operations led_fops = { .open = led_open, //打开设备接口 .release = led_close, //关闭设备接口 };
定义初始化字符设备对象:
struct cdev led_cdev; //定义字符设备对象 //led_cdev.ops = &led_fops cdev_init(&led_cdev, &led_fops);//给字符设备对象添加硬件操作接口
最终向内核注册字符设备对象
- cdev_add(&led_dev, 申请的设备号,次设备号的个数);
- 一旦注册成功,内核就有一个真实的字符设备驱动,并且给用户提供硬件操作接口(open/release)。
从内核卸载字符设备对象
cdev_del(&led_cdev);
最后编写之前定义的设备接口具体内容
- 具体内容就是操作字符设备的操作内容了。
示例
- 编写LED字符设备驱动,实现打开设备开灯,关闭设备关灯
具体代码:
- led_drv.c
#include <linux/init.h> #include <linux/module.h> #include <linux/gpio.h> #include <mach/platform.h> #include <linux/cdev.h> //strcut cdev #include <linux/fs.h> //struct file_operations //声明描述LED硬件信息相关的数据结构 struct led_resource { int gpio; //GPIO软件编号 char *name;//LED的名称 }; //定义初始化四个LED灯的硬件信息对象 static struct led_resource led_info[] = { { .name = "LED1", .gpio = PAD_GPIO_C+12 }, { .name = "LED2", .gpio = PAD_GPIO_C+7 }, { .name = "LED3", .gpio = PAD_GPIO_C+11 }, { .name = "LED4", .gpio = PAD_GPIO_B+26 } }; //定义设备号对象 static dev_t dev; //定义字符设备对象 static struct cdev led_cdev; //打开设备操作接口 //调用关系:应用open->软中断->内核的sys_open->驱动的led_open int led_open(struct inode *inode, struct file *file) { //1.开灯 int i; for(i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 0); } printk("%s\n", __func__); return 0; //执行成功返回0,执行失败返回负值 } //关闭设备操作接口 //调用关系:应用close->软中断->内核的sys_close->驱动的led_close int led_close(struct inode *inode, struct file *file) { //1.关灯 int i; for(i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 1); } printk("%s\n", __func__); return 0; //执行成功返回0,执行失败返回负值 } //定义初始化LED的硬件操作接口对象 static struct file_operations led_fops = { .open = led_open, //打开设备接口 .release = led_close //关闭设备接口 }; static int led_init(void) { int i; //1.申请GPIO资源,配置GPIO为输出,输出1(省电) for (i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_request(led_info[i].gpio, led_info[i].name); gpio_direction_output(led_info[i].gpio, 1); } //2.申请设备号 alloc_chrdev_region(&dev, 0, 1, "tarena"); //3.初始化字符设备对象,给字符设备对象添加操作接口 cdev_init(&led_cdev, &led_fops); //4.最终向内核注册字符设备对象,一旦注册成功, //内核就有一个真实的字符设备对象并且提供了 //硬件操作接口 cdev_add(&led_cdev, dev, 1); return 0; } static void led_exit(void) { int i; //1.输出1,释放GPIO资源 for(i = 0; i < ARRAY_SIZE(led_info); i++) { gpio_set_value(led_info[i].gpio, 1); gpio_free(led_info[i].gpio); } //2.释放设备号 unregister_chrdev_region(dev, 1); //3.卸载字符设备对象 cdev_del(&led_cdev); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
- Makefile
kernel_dir=/home/ww/ARM/kernel obj-m += led_drv.o all: make -C ${kernel_dir} SUBDIRS=$(PWD) modules clean: make -C ${kernel_dir} SUBDIRS=$(PWD) clean
- led_test.c(用户空间测试程序)
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { int fd; //应用open->软中断->内核sys_open->驱动led_open fd = open("/dev/myled", O_RDWR); if (fd < 0) { printf("打开设备失败~!\n"); return -1; } sleep(3); //应用close->软中断->内核sys_close->驱动led_close close(fd); return 0; }
测试执行:
从中能看到设备号244是我们的设备myled,使用mknod命令注册设备文件:
mknode /dev/myled c 244 0
总结
- 字符设备驱动,最终达到的效果就是用户程序调用Linux系统调用函数open、close、read、write等从而最终能够调用到内核驱动空间内部写好的操作接口来达到操作硬件的效果。
- 这样需要注意到设备号、主设备号、次设备号的含义及使用方式。