Linux内核调用I2C驱动实现MPU6050的数据读取

0. 导语最近一段时间都在恶补数据结构和C++,加上导师的事情比较多,Linux内核驱动的学习进程总是被阻碍、不过,十一假期终于没有人打扰,有这个奢侈的大块时间,可以一个人安安静静的在教研室看看Linux内核驱动的东西。按照Linux嵌入式学习的进程,SPI驱动搞完了之后就进入到I2C驱动的学习当中,十一还算是比较顺利,I2C的Linux驱动完成了。 为了测试I2C是否好用,选择一个常用的I2C传感器,手头有个MPU6050,刚好作为I2C的从器件,那就以MPU6050为例,进行Linux底层的I2C驱动开发。 同样的使用Linux内核中的GPIO模拟I2C的时序一点难度没有,I2C的硬件标准时序也是非常的简单,闭着眼睛都能画出时序图吧,如果我们使用Linux内核提供了I2C机制,那么问题不单单是要解决时序,而重点在于对于整个I2C的机制的把握,,。刚刚拿到I2C内核机制的时候,我也看的很晕,i2c_client, i2c_master, i2c_driver, i2c_device,这些东西到底有什么关系呢?到底我该如何让Linux系统的I2C为我所用,按照我的意愿对MPU6050进行读取?到底我能挑出对我有用的Linux的I2C机制,其他没用的机制我不启动,以简化代码。 那么,就真需要从I2C最底层说起。 1. 实验平台平台内容ARM板子友善之臂Nano-T3 (CortexA53架构,Samsung S5C6818芯片)ARM板子的Linux系统Ubuntu 16.04.2 LTSLinux开发主机Ubuntu 16.04.3 LTS amd4版本Linux内核版本Linux3.4.y编译器COMPILE_CROSSarm-cortexa9-linux-gnueabihf-从设备MPU6050模块(I2C接口)2. 查看系统I2C的支持按照SPI驱动的思维,使用spi_driver注册,然后和spi_device匹配,使之进入probe函数,完成spi_master的获取,依照这个方法,我的I2C驱动也是按照这个方法,寻求i2c_driver和i2c_device匹配,然而I2C的驱动尤其特殊之处,使得我的i2c_driver怎么注册都不成功,不是内核内存炸了,就是总是返回失败。 后来我才发现,i2c的使用是不需要注册的,或者严格说一点,Linux系统在启动的时候已经帮你注册好了,而你再去i2c_driver_register的时候肯定是失败的。所以到底我们使用I2C驱动的时候到底需不需要注册,则需要在Linux系统里面查看当前I2C的注册状态。那么流程就比较清晰了,如果查看系统注册了I2C那么就在驱动中直接使用;如果系统没有注册I2C那么我们先注册I2C再使用。 2.1 如何查看?目标板终端输入:ls /sys/bus/i2c/devices 可以看到我这个主机是支持4个I2C外设的(方框圈出),如果是这样的情况,我们就可以直接使用上面的i2c。这里的i2c-0,i2c-1....指的是4个i2c_master,而i2c_master可以挂N个i2c_client。 其他的数字设备就是我挂载的i2c_master上的i2c_client,举个例子,画圈的【0-0069】意思是:挂载到i2c-0上的从地址为0x69的设备,那么【2-0048】的意思就是:挂载到i2c-2 adapter上的从地址为0x48的设备。 我们开发的MPU6050驱动依托I2C进行传输,则需要在这个文件夹创建设备节点才能利用Linux内核提供的I2C方法进行数据的交互。 2.2 弄清楚MPU6050的从地址与Linux I2C从地址的合法性随手搜了一下MPU6050的从地址,有的给出了MPU6050的从地址是0x68,有的给出的是0xD0,一开始我也懒查,认定MPU6050的地址在A0引脚为低电平的时候为0x68,加载驱动的时候出现了很尴尬的事情,0-0068这个地址已经被DS1607实时时钟占用,然后网上有人说是把A0引脚打到高电平地址就是0xD0,可是我试0xD0的时候,被Linux警告,说是从地址不合法,我查看了Linux内核的i2c_core.c文件,里面有个地址校验,高于0x7F的7-bit地址,都是不合法的,Linux不可能犯这样的错误,肯定是网友的锅。果然,我阅读了手册,如果A0的电平为高那么地址是0x69。说从地址是0xD0的人,犯了一个错误,他们多半玩的是模拟IO出的I2C波形,他们对I2C协议标准不够了解,的确0x69 << 1 = 0xD0,I2C在读写的时候,预留出7-bit地址前移1位,把最低位作为读写标识,但绝对不能说从地址就是0xD0。 不过可以再一次看见Linux内核的严谨、严肃的态度。也再一次说,不能懒惰,自己查手册,看最标准的说明。 3 I2C 驱动开发我这里给出最简单的模型,其他的字符驱动注册什么的同spi驱动,这里只说明I2C驱动怎么使用。 3.1 I2C的注册static struct i2c_board_info __initdata sp6818_mpu6050_board_info = { I2C_BOARD_INFO("mpu6050-i2c", MPU6050_SLAVE_ADDRESS),, .irq = -1,};int xxx_hw_init(){ struct i2c_client *client; struct i2c_adapter *adapter; adapter = i2c_get_adapter(0); if (!adapter) { ret = -ENXIO; printk(DRV_NAME "\terror: %d : init i2c adapter failed.\n", ret); return ret; } strlcpy(adapter->name, "nxp_i2c",sizeof(adapter->name)); client = i2c_new_device(adapter, &sp6818_mpu6050_board_info); if (!client) { ret = -ENXIO; printk(DRV_NAME "\terror: %d : init i2c client failed.\n", ret); return ret; }}你没有看错,i2c的使用就是这么简单,我有什么办法,我之前开发加上i2c_register和字符驱动的初始化什么的,整init函数整了近100多行,结果不断的尝试,发现就这些。 ...

November 5, 2019 · 2 min · jiezi

Linux内核调用SPI驱动实现OLED显示功能

0. 导语进入Linux的世界,发现真的是无比的有趣,也发现搞Linux驱动从底层嵌入式搞起真的是很有益处。我们在单片机、DSP这些无操作系统的裸机中学习了这些最基本的驱动,然后用过GPIO时序去模拟、然后用那个芯片平台的外设去配置参数,到Linux的世界,对于底层的时序心中有数,做起来就容易很多。学习的过程就是不断的给自己出难题,然后去解决他,在未来工程里面遇到这个问题,就瞬间可以解决了,这就是经验的积累吧。 Linux驱动目录,包含了底层写好的SPI驱动,我们需要想办法调用人家写好的SPI驱动,就不需要写IO口模拟SPI时序了。在网络上,对于SPI应用级的驱动倒是很多,平台级驱动很少,而我们想把平台级驱动二次包装在我们的字符设备驱动中,对于用户,无需考虑SPI通信写协议还是写命令,只需要使用read和write函数写显示的内容就好了。 基于这样的想法,我们找了一个使用SPI协议的从器件来实现,我手里面有OLED设备,是支持SPI协议在OLED显示面板上显示字符的。所以搭建一个实验平台,做一个OLED的demo,未来所有的从SPI设备都遵循这个框架(而且我们在这个驱动中加入 了内核机制的驱动的自旋锁、互斥体的内核操作)。 实验平台如下: ARM板子: 友善之臂Nano-T3 (CortexA53架构, Samsung s5c6818)ARM的Linux系统:Ubuntu 16.04.2 LTS编译调试Linux:Ubuntu 16.04.3 LTS amd64版本编译器:arm-cortexa9-linux-gnueabihf-gcc (64位版本)从设备:OLED (SPI模式) 1. 驱动架构模型 总体驱动架构模型如图所示,对于OLED驱动的表述,主要包含两个方面,一个是OLED这个传感器的抽象;一个是,misc字符驱动的注册,里面有read和write函数,供用户接口调用,(在read和write函数里面使用OLED设备表述里面的master控制oled的行为就好了,比如显示,清除,复位之类的)。 oled设备表述,为OLED设备的抽象,里面包含对硬件的描述和SPI的描述,还有对于写时序的时候使用自旋锁和互斥体对时序进行的保护,master为对oled设备的基本操作,包含复位,写字节等等。 在本博客中最重要的就是SPI平台驱动的使用,问题也非常的清晰,我们如何使用linux内核驱动里面写好的spi,参考Linux SPI API文档里面,那么复杂的结构体,哪些是在驱动中要使用,哪些是在应用级程序中使用的。网络上的资料大部分都是应用级的,没有讲述在字符驱动中二级注册spi驱动的,而我们对于OLED这样的SPI设备,则需要在驱动中调用,让用户无需关心任何SPI的调用。 在驱动模型中,master操作结构体里面,oled_write_byte这样的函数里面则需要调用系统级SPI,问题就非常明确,就在写byte的时候使用SPI。 那么我们就需要在注册完字符设备的时候,向内核注册spi,然后我们使用该SPI对OLED操作。 2. linux SPI驱动的注册Linux Drivers目录具备一定的通用性也具备各个架构区别不同,在包含头文件的时候,要包含 通用性的linux spi文件 #include <linux/spi/spi.h>mach级特性文件#include <mach/slsi-spi.h>同时也要关注: plat级的device.c文件,里面包含了spi_board信息的模板,用这个可以省去了很多麻烦。我们使用的oled_hw_t, 图上的结构(OLED->hw)的具体定义,里面定义了io口的编号和spi的各种机制,注意谁是指针,谁是实体。 struct oled_hw_t { unsigned int res_io_num; unsigned int dc_io_num; struct spi_transfer spi_trans; struct spi_message spi_msg; struct spi_driver *spi_drv; struct spi_device *spi_dev; struct spi_master *spi_master_bus;};我需要定义以下机制: spi_driver spi_driver会向内核申请总线处理的权限,当我们加载驱动的时候,在ARM机器的linux上的/sys/bus/spi/drivers目录下会看到申请SPI驱动内核的名字。 static const struct spi_device_id oled_spi_id[] ={ {“oledspi”, 1}, {},};static struct spi_driver sp6818_spi_driver = { .driver = { .name = "oled_spi", .bus = &spi_bus_type, .owner = THIS_MODULE, }, .probe = oled_bus_spi_probe, .remove = __devexit_p(oled_bus_spi_remove), .suspend = oled_bus_spi_suspend, .id_table = oled_spi_id,};MODULE_DEVICE_TABLE( spi, oled_spi_id );按照spi_driver驱动的格式进行,补充好probe和remove,suspend函数,但是这里存在一个问题,当我们应该spi_register_driver的时候,正常应该执行probe函数里面的内容,但是这个不执行,怀疑是因为二级包装问题,我们的主调还是使用misc驱动的字符设备 __init标示在 misc的初始化函数上,而导致不进入spi_driver的probe函数。 ...

November 5, 2019 · 2 min · jiezi

OMAPL多核异构通信驱动AD9833Notify组件demo

OMAPL多核通信有三个主要机制,Notify,MessageQ,RegionShare;这里主要利用了Notify机制进行通信控制。 要做一个什么实验?简单的说,ARM跑一个界面上面有一些按钮,DSP负责驱动AD9833产生正弦、方波和三角波,写入频率信息。这个实验结构是一个经典的单向的传输结构,由用户触发ARM跑的界面上的按钮,发出消息通知DSP,DSP控制AD9833产生波形,写入频率字等信息。 那么ARM的Linux端首选Qt,DSP端的程序使用SYSLINK/BIOS实施操作系统,IPC通讯组件使用Notify。 视频预览: <iframe height=498 width=510 src='http://player.youku.com/embed...' frameborder=0 'allowfullscreen'></iframe> 多核通信工程目录结构几个文件,arm,dsp,run,shared,还有makefile文件,makefile文件自己要会修改。 DSP端程序 DSP端程序对于用户来讲ad9833_dev.c ad9833_server.c main.c 三个主要的文件, ad9833_dev.c 为AD9833底层驱动,负责写时序,写参数的ad9833_server.c 相当于以太网scoket通信因子,负责进行多核通信和调用dev中的api的main.c 为dspbios启动,初始化操作。环境搭建正确之后,最核心的就是这三个东西,对还有个makefile要配置正确。我在环境调试的时间花的比开发时间多的多,最重要的就是要环境配置正确,库啊,路径啊,这类的。 AD9833底层驱动-ad9833_dev.c我们这里给出接口函数目录,具体实现不给出: enum ad9833_wavetype_t{ SIN,SQU,TRI};struct ad9833_hw_t { uint16 clk; uint16 sdi; uint16 fsy;};// AD9833结构体表述typedef struct ad9833_t { struct ad9833_hw_t hw; struct ad9833_t *self; enum ad9833_wavetype_t wave_type; u16 delay; void (*write_reg)( struct ad9833_t *self, u16 reg_value); void (*init_device)( struct ad9833_t *self ); void (*set_wave_freq)( struct ad9833_t *self , float freqs_data); void (*set_wave_type)( struct ad9833_t *self, enum ad9833_wavetype_t wave_type ); void (*set_wave_phase)( struct ad9833_t *self, u16 phase ); void (*set_wave_para)( struct ad9833_t *self, u32 freqs_data, u16 phase, enum ad9833_wavetype_t wave_type );} AD9833;// 函数列表void ad9833_set_para( struct ad9833_t *self,u32 freqs_data, u16 phase, enum ad9833_wavetype_t wave_type );void ad9833_device_init( struct ad9833_t *self );void ad9833_write_reg( struct ad9833_t *self, uint16_t data );void ad9833_delay( struct ad9833_t *self );void ad9833_gpio_init( void );void ad9833_set_wave_type( struct ad9833_t *self, enum ad9833_wavetype_t wave_type );void ad9833_set_phase( struct ad9833_t *self, uint16_t phase );void ad9833_set_freq( struct ad9833_t *self, float freq );void ad9833_dev_destroy( AD9833 *dev );void ad9833_dev_new();AD9833的驱动,按照手册进行编辑,然后封装成这个样子,这里一定需要有的函数是: ...

November 4, 2019 · 5 min · jiezi

基于OMAPL138的Linux字符驱动GPIO驱动AD9833一之miscdevice和ioctl

0. 导语在嵌入式的道路上寻寻觅觅很久,进入嵌入式这个行业也有几年的时间了,从2011年后半年开始,我清楚的记得当时拿着C51的板子闪烁了LED灯,从那时候开始,就进入到了嵌入式的大门里面。嵌入式的学习从来没有停止过,中间也有无数的插曲和机缘巧合学会C++和Java,做一些好玩的应用。无论是嵌入式DSP也好,还是如今的嵌入式ARM,7年之久从来没有停止过。技术最大的好处就是,无论发展到什么境地,那种第一次点亮LED灯欣喜永远的可以伴随着你,只要你解决了一个卡了你很久的问题,这就是技术的魅力。我也将开始大肆的从嵌入式DSP转入到嵌入式Linux,在研究生阶段,完成这个转型。 这个Demo意义重大,使用Linux也有四五年的时间了,Linux良好的基础和嵌入式基础让我在嵌入式inux道路上算的上是顺风顺水。这个Demo将过去STM32,F28xx的DSP或者那些单片机桥接起来,将过去裸机上的程序全部编到内核里面,通过嵌入式的应用进行互联。 本DEMO依然使用AD9833作为例子,将用linux内核级的gpio对AD9833写时序,完成对于AD9833的驱动程序,在嵌入式Linux上生成/dev/目录节点,使用Linux命令行对AD9833产生波形进行控制。(只要有了/dev节点,使用Qt,C++,Python都可以控制了,这就是物联网最注重的。) 效果视频观看地址: https://v.youku.com/v_show/id... 1. 开发驱动综述本开发驱动基于Linux3.3内核版本,且内核必须编译正确,否则不能运行。这个Demo可以归结为三个部分,一个部分为Linux字符驱动模板,第二部分为AD9833驱动程序,第三部分为通信协议。还附加一个配置文件。 Linux字符驱动模板主要包含init exit 还有ioctl,函数;AD9833驱动程序为AD9833的GPIO时序(AD9833为SPI协议,这里先用GPIO模拟时序,后续升级为SPI外设);通信协议格式方式,用户对于AD9833的控制字,比如发送波形命令,频率命令等;将自己编写的驱动写入内核的代码树,编译成模块或者编译进内核随内核启动;本Demo就围绕这三点进行。 2. Linux字符驱动模板* 函数ioctl主要负责进行数据交互的。当设备生成字符设备驱动节点(/dev目录下),使用shell级命令cat或者编译一段C应用程序用open打开节点的时候,后面将参数就是通过ioctl函数进行传递。(在嵌入式Linux端定义一个ioctl的函数,在C语言的程序也有一个ioctl用来和其进行对应,这样就完成了数据参数传递。) *结构体file_operationsstatic intad9833_ioctl(struct file *file, unsigned int cmd, unsigned long arg ){ printk(DRV_NAME "\tRecv cmd: %u\n", cmd); printk(DRV_NAME "\tRecv arg: %lu\n", arg); switch( cmd ) { case CMD_TYPE_SIN: ad9833->set_wave_freq(ad9833, 1500); ad9833->set_wave_type(ad9833, SIN); printk( DRV_NAME " set wave is sine wave! arg = %lu\n" , arg ); break; case CMD_TYPE_TRI: ad9833->set_wave_freq(ad9833, 1500); ad9833->set_wave_type(ad9833, TRI); printk( DRV_NAME " set wave is tri wave! arg = %lu\n" , arg ); break; case CMD_TYPE_SQE: ad9833->set_wave_freq(ad9833, 1500); ad9833->set_wave_type(ad9833, SQU); printk( DRV_NAME " set wave is sw wave! arg = %lu\n" , arg ); break; } return 0;}ioctl函数不能独立的存在需要file_operations指针进行操作,ioctl为一个执行命令的清单,file_operations就是这个清单的执行者。下面就是file_operations的指针,里面的成员需要接收到ad9833_ioctl的函数地址,在内部运行的时候会调用该地址。 ...

November 4, 2019 · 3 min · jiezi

基于OMAPL138的字符驱动GPIO驱动AD9833三之中断申请IRQ

0. 导语学习进入到了下一个阶段,还是以AD9833为例,这次学习是向设备申请中断,实现触发,在未来很多场景,比如做用户级的SPI传输完毕数据之后,怎么知道从设备要发数据呢,则需要一个IO信号通知主设备来读从设备的数据,那么就需要一个外部的IO中断信号,所以呢,对于中断的处理十分重要,本demo实现这样的一个功能增加一个GPIO口,这个GPIO口接的是一个按键,通过触发信号,进入中断服务函数,在中断服务函数内改变AD9833的波形。以此达到学习实验目的。 之前的代码都是一样的,在这里尽量剥离AD9833驱动和Linux的代码模板,只写中断相关相关程序。 效果演示视频: https://v.youku.com/v_show/id... 1. 前篇导读:基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl基于OMAPL138的Linux字符驱动_GPIO驱动AD9833(二)之cdev与read、writeLinux GPIO键盘驱动开发记录_OMAPL138原理图: 2. 申请中断准备首先需要两个头文件:#include <linux/interrupt.h>#include <linux/irq.h>IO口配置准备在此次使用中断中,主要用的是GPIO口,我们使用电平跳变使之进入到中断处理程序中,所以作为IO口,需要配置IO口的方向为输入方向。我的OMAPL138中给的IO口操作使用GPIO_TO_PIN这个宏函数进行,在IO口操作上每个平台都会给定自己的寻IO口的方法,然后使用linux通用gpio_direction_output进行设定该GPIO口为输入方向,如上面的原理图,本demo的驱动使用的GPIO6[1],所以as follow:gpio_direction_output( GPIO_TO_PIN(6, 1) , 0 ); 硬件中断号IRQ我参考了很多文献,也找了很多书籍,对于硬件中断号码从哪里得到一笔带过,也没有具体说明,不过,经过一下午的努力,我找到了查找中断号码的方法。使用gpio_to_irq这个函数方法可以得到irq。我之前找到手册,看到了手册里面说GPIO6 BANK的IRQ为48号,我尝试加载到内核里面,每次初始化的时候都告诉我中断申请失败,这个号看来不是datasheet给定的48号,在Linux内核里面对于硬件IRQ又进行了重新映射。 中断事件在内核中断申请的时候,需要指定中断事件是什么,边沿信号,高电平触发,低电平触发,在irq.h里面定义了: IRQ_TYPE_NONE = 0x00000000, IRQ_TYPE_EDGE_RISING = 0x00000001, IRQ_TYPE_EDGE_FALLING = 0x00000002, IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING), IRQ_TYPE_LEVEL_HIGH = 0x00000004, IRQ_TYPE_LEVEL_LOW = 0x00000008, IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW | IRQ_TYPE_LEVEL_HIGH), IRQ_TYPE_SENSE_MASK = 0x0000000f 我们需要指定这个事件。 中断的名字这个使用#cat /proc/interrupts 查看当前内核中断资源的时候就可以看到你指定注册的名字。 中断服务程序这个我们自己定一个函数就可以,然后一会儿使用中断申请的时候将数据传输进去就好。我们在中断服务函数里面进行按键进行波形切换: static int key_count = 0;static irqreturn_t ad9833_press_intHandle( int irq, void *dev_id ){ printk( DRV_NAME "\t press trigger!\n" ); if( key_count == 0 ) { ad9833->set_wave_type( ad9833, SIN ); printk( DRV_NAME "\tSet wave is SIN.\n" ); }else if( key_count == 2 ) { ad9833->set_wave_type( ad9833, TRI ); printk( DRV_NAME "\tSet wave is TRI.\n" ); }else if( key_count == 4 ) { ad9833->set_wave_type( ad9833, SQU ); printk( DRV_NAME "\tSet wave is SQU.\n" ); } key_count ++; if( key_count >= 5 ) key_count = 0; return IRQ_RETVAL( IRQ_HANDLED );}3. 申请中断准备使用request_irq函数就好,我们在初始化函数里面,申请irq。在申请irq前,为了更好的管理中断函数,我们定义了一个结构体,专门进行irq配置。 ...

November 4, 2019 · 2 min · jiezi

基于OMAPL138的Linux字符驱动GPIO驱动AD9833二之cdev与readwrite

0. 导语在上一篇博客里面,基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl 中使用#include <linux/miscdevice.h>中的miscdevice机制,在呢篇博客中使用宋宝华的Linux驱动设备中提供的cdev机制完成注册, 根据参考文献[1]中所说: misc设备其实也是字符设备,主不过misc设备驱动在字符设备的基础上又进行了一次封装,使用户可以更方便的使用。在本次实验中确实印证了使用cdev比较复杂,且加载ko模块驱动之后还需要查看设备号,手动mknod节点,而且在卸载驱动的时候也是非常繁琐的,但在这里本着学习的目的也进行了实验,后续的开发会使用miscdevice机制而不使用cdev机制 本次实验主要针对字符设备的: cdev注册设备read函数的使用write函数的使用在上一篇博客基于OMAPL138的字符驱动_GPIO驱动AD9833(一)之ioctl,只能用ioctl函数进行一个字节的幻数进行指令通信,但无法传输类似于设置频率指令。如果传递这样的参数,只需要使用write和read函数完成数据的传递。 1. cdev的使用cdev的定义cdev的定义信息包含在#include <linux/cdev.h>头文件中,需要使用cdev当然要定义cdev的结构体了,我们将cdev的信息定义在了我们的设备定义struct ad9833下。 AD9833 结构体定义: struct ad9833_t { struct ad9833_hw_t hw; struct ad9833_t *self; enum ad9833_wavetype_t wave_type; struct cdev cdev; unsigned char mem[ AD9833_SIZE ]; unsigned int delay; void (*write_reg) ( AD9833 *self, unsigned int reg_value); void (*init_device) ( AD9833 *self ); void (*set_wave_freq)( AD9833 *self , unsigned long freqs_data); void (*set_wave_type)( AD9833 *self, enum ad9833_wavetype_t wave_type ); void (*set_wave_phase)( AD9833 *self, unsigned int phase ); void (*set_wave_para)( AD9833 *self, unsigned long freqs_data, unsigned int phase, enum ad9833_wavetype_t wave_type );};结构体内的struct cdev cdev就为我们使用的cdev目的就是向Linux内核申请自己的位置。 ...

November 4, 2019 · 4 min · jiezi

Linux设备驱动-块设备之通用块层

通用块层简介Linux中的通用块层是一个内核组件,它负责处理来自系统中的所有块设备访问,并将块设备的访问转换为请求下发到IO调度层。这个过程中会涉及到多种数据结构的转换,下面我们来讨论通用块层所涉及的数据结构以及通用块层所做的工作。通用块层数据结构注:本文所涉及的数据结构均为Linux 3.0内核中的数据结构1、bio结构bio描述符是通用块层的核心数据结构,它描述了块设备的IO操作,包含了IO操作所设计的磁盘的存储区标识符,与IO操作相关的内存区的段信息等。bio秒数据结构如下:struct bio { sector_t bi_sector; /* 块IO操作的第一个磁盘扇区 */ struct bio bi_next; / 链接到请求队列的下一个bio */ struct block_device bi_bdev; / 指向块设备描述符的指针 / unsigned long bi_flags; / bio的状态标志 / unsigned long bi_rw; / IO操作标志 / unsigned short bi_vcnt; / bio中bio_vec数组中段的数目,即bio_vec数组长度 / unsigned short bi_idx; / bio中bio_vec数组当前索引值 / / Number of segments in this BIO after * physical address coalescing is performed. / unsigned int bi_phys_segments; / 合并之后bio中的物理段 / unsigned int bi_size; / 需要传送的字节数 / / * To keep track of the max segment size, we account for the * sizes of the first and last mergeable segments in this bio. / unsigned int bi_seg_front_size; / 段合并算法使用 / unsigned int bi_seg_back_size; / 段合并算法使用 / unsigned int bi_max_vecs; / bio中bio_vec数组允许的最大段数 / unsigned int bi_comp_cpu; / completion CPU / atomic_t bi_cnt; / pin count */ struct bio_vec bi_io_vec; / 指向bio的bio_vec数组中的第一个段的指针 */ bio_end_io_t bi_end_io; / bio的IO操作结束时调用的方法 */ void bi_private; / bio私有数据 /#if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload bi_integrity; / data integrity /#endif bio_destructor_t bi_destructor; / 释放bio时调用的析构方法/ / * We can inline a number of vecs at the end of the bio, to avoid * double allocations for a small number of bio_vecs. This member * MUST obviously be kept at the very end of the bio. */ struct bio_vec bi_inline_vecs[0];};bio中的每个段都是由bio_vec结构表示,其中个字段如下所示。bio中的bio_io_vec字段存放bio_vec数组中的第一个元素地址,bi_vcnt字段存放bio_vec数组当前的元素个数struct bio_vec { struct page bv_page; / 指向段的页框中页描述符的指针 / unsigned int bv_len; / 段的长度,以字节为单位 / unsigned int bv_offset; / 页框中段数据的偏移量 /};2、gendisk结构gendisk描述一个磁盘或磁盘分区。磁盘是由通用块层梳理的逻辑设备,通常一个磁盘对应一个硬件设备。gendisk具体字段如下所示:struct gendisk { / major, first_minor and minors are input parameters only, * don’t use directly. Use disk_devt() and disk_max_parts(). / int major; / 磁盘主设备号 / int first_minor; / 与磁盘关联的第一个次设备号 / int minors; / 与磁盘关联的此设备号范围 / char disk_name[DISK_NAME_LEN]; / 磁盘标准名称 */ char *(*devnode)(struct gendisk *gd, mode_t mode); unsigned int events; / supported events / unsigned int async_events; / async events, subset of all / / Array of pointers to partitions indexed by partno. * Protected with matching bdev lock but stat and other * non-critical accesses use RCU. Always access through * helpers. */ struct disk_part_tbl __rcu part_tbl; / 磁盘分区表 / struct hd_struct part0; / 磁盘第一个分区 */ const struct block_device_operations fops; / 磁盘操作方法 */ struct request_queue queue; / 指向磁盘请求队列的指针 */ void private_data; / 块设备驱动的私有数据 / int flags; / 磁盘类型的标志 */ struct device *driverfs_dev; // FIXME: remove struct kobject *slave_dir; struct timer_rand_state random; atomic_t sync_io; / RAID */ struct disk_events *ev;#ifdef CONFIG_BLK_DEV_INTEGRITY struct blk_integrity *integrity;#endif int node_id;};gendisk结构中fops字段指向磁盘操作方法的结构提指针,这些方法包括open、release、ioctl等,类似于字符设备驱动程序中的fops结构;gendisk结构中part_tbl字段指向磁盘的分区表,分区表结构具体的字段如下:struct disk_part_tbl { struct rcu_head rcu_head; int len; struct hd_struct __rcu *last_lookup; struct hd_struct __rcu part[];};struct hd_struct { sector_t start_sect; / 磁盘中分区的起始扇区 / sector_t nr_sects; / 分区的扇区数 / sector_t alignment_offset; unsigned int discard_alignment; struct device __dev; / 实际的块设备 */ struct kobject *holder_dir; int policy, partno; struct partition_meta_info *info;#ifdef CONFIG_FAIL_MAKE_REQUEST int make_it_fail;#endif unsigned long stamp; atomic_t in_flight[2];#ifdef CONFIG_SMP struct disk_stats __percpu *dkstats;#else struct disk_stats dkstats;#endif atomic_t ref; struct rcu_head rcu_head;};通用块层工作流程本节以do_erase函数为例讨论Linux中当向通用块层提交了一个IO操作时通用块层的处理流程。首先附上do_erase函数代码:static int do_erase(struct super_block *sb, u64 ofs, pgoff_t index, size_t nr_pages){ struct logfs_super *super = logfs_super(sb); struct bio *bio; struct request_queue q = bdev_get_queue(sb->s_bdev); unsigned int max_pages = queue_max_hw_sectors(q) >> (PAGE_SHIFT - 9); int i; if (max_pages > BIO_MAX_PAGES) max_pages = BIO_MAX_PAGES; bio = bio_alloc(GFP_NOFS, max_pages); //申请bio结构 BUG_ON(!bio); for (i = 0; i < nr_pages; i++) { if (i >= max_pages) { //当请求的数据大于磁盘一次数据传输大小时,会将请求分成多个bio提交 / Block layer cannot split bios :( */ bio->bi_vcnt = i; bio->bi_idx = 0; bio->bi_size = i * PAGE_SIZE; bio->bi_bdev = super->s_bdev; bio->bi_sector = ofs >> 9; bio->bi_private = sb; bio->bi_end_io = erase_end_io; atomic_inc(&super->s_pending_writes); submit_bio(WRITE, bio); ofs += i * PAGE_SIZE; index += i; nr_pages -= i; i = 0; bio = bio_alloc(GFP_NOFS, max_pages); //申请新的bio结构 BUG_ON(!bio); } bio->bi_io_vec[i].bv_page = super->s_erase_page; bio->bi_io_vec[i].bv_len = PAGE_SIZE; bio->bi_io_vec[i].bv_offset = 0; } bio->bi_vcnt = nr_pages; bio->bi_idx = 0; bio->bi_size = nr_pages * PAGE_SIZE; bio->bi_bdev = super->s_bdev; bio->bi_sector = ofs >> 9; bio->bi_private = sb; bio->bi_end_io = erase_end_io; atomic_inc(&super->s_pending_writes); submit_bio(WRITE, bio); return 0;}上述代码可有如下流程图解释:图1 do_erase流程图 在do_erase函数中,如果像磁盘请求的数据大小大于一次bio操作允许的最大值(i>max_pages),则会将磁盘数据请求分成多个bio进行,先完善并提交当前bio,然后申请新的bio结构并将剩余的数据请求填充到新的bio中。接下来讨论一下提交bio请求函数submit_bio,源代码如下:void submit_bio(int rw, struct bio bio){ int count = bio_sectors(bio); bio->bi_rw |= rw; / * If it’s a regular read/write or a barrier with data attached, * go through the normal accounting stuff before submission. */ if (bio_has_data(bio) && !(rw & REQ_DISCARD)) { if (rw & WRITE) { count_vm_events(PGPGOUT, count); } else { task_io_account_read(bio->bi_size); count_vm_events(PGPGIN, count); } if (unlikely(block_dump)) { char b[BDEVNAME_SIZE]; printk(KERN_DEBUG “%s(%d): %s block %Lu on %s (%u sectors)\n”, current->comm, task_pid_nr(current), (rw & WRITE) ? “WRITE” : “READ”, (unsigned long long)bio->bi_sector, bdevname(bio->bi_bdev, b), count); } } generic_make_request(bio);}submit_bio完善一下bio信息后会调用generic_make_request函数提交bio。generic_make_request函数源代码如下:void generic_make_request(struct bio bio){ struct bio_list bio_list_on_stack; if (current->bio_list) { / make_request is active / bio_list_add(current->bio_list, bio); return; } BUG_ON(bio->bi_next); bio_list_init(&bio_list_on_stack); current->bio_list = &bio_list_on_stack; do { __generic_make_request(bio); bio = bio_list_pop(current->bio_list); } while (bio); current->bio_list = NULL; / deactivate */}generic_make_request函数将bio连接到current->bio_list链表中,并调用__generic_make_request函数提交链表中所有的bio。__generic_make_request函数最终会调用块设备的请求队列中的make_request_fn成员函数将bio请求发送给I/O调度层,至此对磁盘的数据请求离开通用块层,进入下一层——I/O调度层通用块层总结综上,一个磁盘数据请求在通用块层经过的流程为:上层下发磁盘数据请求通用块层申请bio结构,将请求的数据分段记录到bio中如果请求的数据大于一个bio允许的最大数据量,则将请求分成多个bio调用submit_bio提交bio请求submit_bio函数经过层层调用,最终调用块设备请求队列中的make_request_fn成员函数将bio提交给I/O调度层进行处理 ...

March 13, 2019 · 4 min · jiezi