Linux SCSI 子系统 III

本文以LSI公司基于PCIe接口的MegaSAS RAID卡驱动为例,分析SCSI总线在内核注册对象的过程。SCSI设备在内核初始化可以分为两部分,LLDD的部分和HLDD的部分,但无论哪部分,和其他内核对象的构造流程一样,都遵循”分配->初始化->注册“的流程。

LLDD INIT

LLDD的init主要是在内核中构造与SCSI设备直接相关的对象。在主板上已经插入了RAID卡的前提下,内核在启动过程中基于相关协议标准获取PCIe设备信息,并根据这些信息在内核中构造相应的PCIe设备对象,此外,内核在启动过程还会加载该RAID卡的驱动,本质上这是一个PCIe驱动,无论PCIe设备对象和PCIe驱动谁先谁后,只要二者都被加载进内核,就会match并回调PCI驱动的probe()。对于该款RAID卡的驱动来说,probe()被注册为megasas_probe_one(),由该函数根据板卡需求在内核SCSI子系统中构造相关对象。
梳理mega_probe_one()的流程,可以发现,该函数依然遵循内核驱动框架的”分配->初始化->注册“的流程, 其中,又分为针对SCSI适配器对象的三步走( scsi_host_alloc()scsi_add_host())以及针对SCSI设备的三步走(scsi_scan_host())。

megasas_probe_one()
	scsi_host_alloc()               //alloc scsi host
		init Scsi_Host shost        //init scsi host
		scsi_proc_hostdir_add()     
	megasas_io_attach() 
		scsi_add_host()             //register scsi host
			scsi_add_host_with_dma()
	scsi_scan_host()                //alloc, init and register scsi device
		scsi_prep_async_scan()
		do_scsi_scan_host()
		async_schedule(do_scan_async)

scsi_host_alloc()
主要负责Scsi_Host对象的分配和部分域的缺省值的初始化,相关逻辑整理如下:

struct Scsi_Host * scsi_host_alloc(struct scsi_host_template *sht, int privsize)               //alloc scsi host
{ 
	Scsi_Host shost = kzalloc();
	shost->shost_state = SHOST_CREATED;
	init_waitqueue_head(&shost->host_wait);
	//init list_head
	INIT_LIST_HEAD(list_heads in shost); 
	//set default values which can be overwritten
	shost->max_channel = 0;
	shost->max_id = 8;
	shost->max_lun = 8;
	shost->transportt = &blank_transport_template;
    
	//init Scsi_Host by scsi_host_template
	shost->hostt = sht;
	shost->this_id = sht->this_id;
	
	//init DD fields
	device_initialize(&shost->shost_gendev);
	shost->shost_gendev.bus = &scsi_bus_type;
	shost->shost_gendev.type = &scsi_host_type;
	device_initialize(&shost->shost_dev);
	shost->ehandler = kthread_run(scsi_error_handler);
	shost->tmf_work_q = alloc_workqueue();
	scsi_proc_hostdir_add() 
}

scsi_add_host()
负责将已经分配好并部分初始化的Scsi_Host对象继续初始化并注册到内核SCSI子系统中。该函数的实质是调用scsi_add_host_with_dma函数。

Continue reading

Linux SCSI 子系统 II

SubSySScSi – PreFAce一文中已经讨论了硬件原理,并给出了下图的Scsi与内核关系的关系,本文主要讨论SCSI子系统的关键角色-关键的类与关键的对象。
内核支持的总线众多,每一种总线上挂载的设备又多种多样,涉及的核心类与对象千差万别,但总的来说,根据bus-device-driver模型,在一个总线子系统中,其核心类与对象大体可以分为以下6类:

  1. 总线的抽象->struct bus_type scsi_bus_type
  2. 总线控制器的抽象->struct scsi_host_template, struct Scsi_Host
  3. 总线控制器驱动的抽象->None
  4. 总线设备的抽象->struct scsi_target, struct scsi_device, struct scsi_disk
  5. 总线设备驱动的抽象->struct scsi_driver
  6. 命令的抽象->struct scsi_cmnd

他们之间的关系可以用下图简要表示:

核心的对象有:scsi_bus_type, sdev_class, shost_class, sd_disk_class。,这些对象在内核初始化SCSI子系统时候即创建好,包括总线级的实例scsi_bus_type以及为各种SCSI对象准备的class,它们负责将后续注册到内核的SCSI相关对象通过sysfs融入内核设备驱动框架中,进而被用户使用,个中关系可以用下图简要表示。

scsi_bus_type

scsi_bus_type,封装了SCSI总线级的共性资源,比如总线名、匹配函数等等。一如所有的bus_type实例,通过其上的设备链表与驱动链表,管理着SCSI总线中的”设备”及其”驱动”,有意思的是,在SCSI子系统中,不论是Scsi_Host实例、还是scsi_target实例抑或scsi_device实例,都被看作”设备”被挂接到scsi_bus_type的”设备链表”中,虽然只有scsi_device实例在”驱动链表”有其对应的驱动。

474 struct bus_type scsi_bus_type = {
475         .name           = "scsi",
476         .match          = scsi_bus_match,
477         .uevent         = scsi_bus_uevent,
478 #ifdef CONFIG_PM                               
479         .pm             = &scsi_bus_pm_ops,
480 #endif
481 };
Continue reading

Linux SCSI 子系统 I

在内核中,不仅要考虑SCSI协议栈本身,还必须和内核的DD框架融合到一起。
在内核中SCSI子系统的主要功能是:

  1. 探测SCSI设备,在内存建立可供设备驱动使用的数据结构
  2. 在sysfs文件系统中构造SCSI子系统的目录树
  3. SCSI高层驱动绑定SCSI设备,在内存中建立对应的数据结构
  4. 提供错误修复API,在SCSI命令错误和超时后被调用

如上图所示, 在内核中,SCSI 子系统被分为三层,从上到下分别是提供request<->CDB转换、设备级驱动能力的HLDD(High Level Device Driver),提供注册回调等公共功能的CommonService层,以及提供某种具体的SCSI协议栈实现的LLDD(Low Level Device Driver)

由此可见,SCSI子系统同IDE等接口驱动一样,位于内核IO栈中的最底层。例如,将一张使用一张PCI接口的SCSI适配器插入PCI插槽后,相应的PCI driver 就会在合适的时候match到相应的PCI device,并通过SCSI Common Service中提供的接口向SCSI LLDD 注册一个SCSI适配器对象及其操作方法,之后,
当上层的Block层将准备好的request提交到SCSI子系统中HLDD中的相应驱动,HLDD将request转换为CDB(__make_request()->__generic_unplug_device()->scsi_request_fn()->blk_peek_request()->sd_prep_fn()->scsi_setup_fs_cmnd()->scsi_init_io() ),并回调当初注册的接口((__make_request()->__generic_unplug_device()->scsi_request_fn()->scsi_dispatch_cmd()->scsi_host_template.queuecommand()),将CDB通过相应的SCSI协议栈发出。

Linux sysfs IV

本文主要呈现一个使用sysfs编写代码的样例

准备属性和回调接口

我们知道,呈现在sysfs中的文件名其实都是内核中ktype的属性值,而从用户空间对这些属性值进行读写其实就是回调了我们在ktype结构中注册读写函数,所以,这里我们准备了两个函数,值得注意的是,内核会将用户空间的buf转换到内核空间并当作参数传入回调函数,所以我们就不用再进行这个转换。这里由于没有实际的属性,我就只是打印一下信息,实际使用的时候这两个函数要对内核中的真实属性进行读写。

static char kbuf[1024] = {0};
static ssize_t my_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
    char info[]="my_show is called\n";
    return scnprintf(buf,sizeof(info),info);
}

static ssize_t my_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
    printk("%s is called\n",__func__);
    strncpy(kbuf,buf,count);
    printk("user_buf:%s,count:%ld|after copy,kbuf:%s\n",buf,count,kbuf);
    return count;
}

构造kobj_attribute

准备好了原材料,第一道工序就是将属性和回调接口封装到一个kobj_attribute结构对象中,当然对这个属性的读写权限等信息也应该进行封装,我们来回顾一下这个结构

//linux/kobject.h
139 struct kobj_attribute {  
140         struct attribute attr;
141         ssize_t (*show)(struct kobject *kobj, struct kobj_attribute *attr,
142                         char *buf);
143         ssize_t (*store)(struct kobject *kobj, struct kobj_attribute *attr,
144                          const char *buf, size_t count);
145 };

//linux/sysfs.h
 29 struct attribute { 
 30         const char              *name;
 31         umode_t                 mode;   //权限
 32         ...
 37 };

当然,内核也给我们提供了相应的宏来快速的构造这个结构

//linux/sysfs.h
100 #define __ATTR(_name, _mode, _show, _store) {                           \
101         .attr = {.name = __stringify(_name),                            \
102                  .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },             \
103         .show   = _show,                                                \
104         .store  = _store,                                               \
105 }   

使用了这个宏,我们就可以快速的构造我们的kobj_attribute结构

//show是name,就是sys中的文件名
static struct kobj_attribute my_sysfs_read =__ATTR(show, S_IRUSR, my_show, NULL);

static struct kobj_attribute my_sysfs_write =__ATTR(write, S_IWUSR, NULL,my_store);

构造attribute数组

一个kobject网完对应多个attribute,此时就需要将这些attribute封装成一个结构体数组,注意这个数组最后一个元素一定要是NULL

static struct attribute *my_sysfs_attr[] = {
    &my_sysfs_read.attr,
    &my_sysfs_write.attr,
    NULL,
};

如果这些属性直接放到kobject的目录中,我们可以直接使用sysfs_create_file(),但通常情况下,我们更多的将上述的struct attribute进行进一步的封装,并使用sysfs_create_group()来创建一个名为attribute_group.name的、包含struct attribute中的属性目录,这种方式更加的灵活,因为如果我们不指定目录的名字,那么效果个sysfs_create_file()是一样的。

static struct attribute_group my_sysfs_attr_group = {
    .name = "sub_my_attr",      //不写这个成员就不会创建子文件夹
    .attrs = my_sysfs_attr,
};

struct kobject *my_kobj = NULL;
int mysys_init(void)
{
    ...
    my_kobj = kobject_create_and_add("my_sysfs", NULL);
    sysfs_create_group(my_kobj, &my_sysfs_attr_group);
    ...
}

void mysys_exit(void)
{
    ...
    sysfs_remove_group(my_kobj, &my_sysfs_attr_group);
    kobject_put(my_kobj);
}

输出

将上述的程序编译成模块,我们就可以观察到下面的输出结果。

Linux sysfs III

sysfs是一个基于ramfs的文件系统,在2.6内核开始引入,用来导出内核对象(kernel object)的数据、属性到用户空间。与同样用于查看内核数据的proc不同,sysfs只关心具有层次结构的设备信息,比如系统中的总线,驱动以及已经加载的模块等,而诸如PID等信息还是使用proc来管理。本质上,sysfs文件的层次结构就是基于内核中kset与kobject逻辑结构来组织的。从驱动开发的角度,/sysfs为我们提供了除了设备文件/dev/proc之外的另外一种通过用户空间访问内核数据的方式。想要使用sysfs,编译内核的时候需要定义CONFIG_SYSFS,可以通过mount -t sysfs sysfs /sys命令来挂载sysfs到“/sys”目录。本文以ubuntu15.04(3.19)为例分析。

/sys

sysfs的布局体现内核的数据结构,顶层的目录有

$ls /sys/
block/  bus/  class/  dev/  devices/  firmware/  fs/  hypervisor/  kernel/  module/  power/

其中 “sys/class/”,”sys/bus/”,”sys/devices”是设备开发中最重要的几个目录。他们之间的关系可以用下图表示。

每一个目录都对应内核中的一个kset,每一个kset还会包含一些kobject或其他kset。下面针对常用目录做一个简单的介绍

Continue reading

Linux sysfs II

本文聚焦于sysfs Inside Tree的另一个层次的问题:对总线设备的描述和管理。对设备的管理,主要集中于sysfs Inside Tree中的5个概念:device,bus,driver,class和interface,本文也围绕这几个概念展开。

在硬件系统中,计算机通过形形色色的总线将设备组织成一个树状结构,这棵树的根就是CPU核。在软件的世界,同样有一棵树用于描述硬件信息。很容易想到,软件拓扑要严格的对应硬件结构才会清晰易懂,但是易懂并不是设备管理的目标(至少不是唯一的目标),所以实际的Device Driver子系统在此基础上充分发挥了代码重用,面向对象,合理的数据结构等设计方式,使整个Device Driver子系统既保持了与硬件变化相适配的软件数据结构,又提供了高效的管理与驱动方式。

当我们说一个总线的时候,其硬件实质就是一个”控制器+线”,而这个控制器又是挂载到另外一个总线(Parent)上的,所以如果站在另外一条总线(Parent)上看,这条总线又是一个Device,既然是Device,那么是要有对应的Driver来存储诸如硬件寄存器读写等驱动方法… ,所以,在Device Driver 子系统中,Bus,Device Driver并不是完全独立的,而是你中有我,我中有你的关系。无论是什么关系,显然,他们在内核中都是作为对kobject的再次封装,服从sysfs的组织和管理。 

bus_type

Continue reading

Linux sysfs I

kernel管理的设备千差万别,但最终都归于统一设备模型的管理, 即sysfs,理解了sysfs,就可以管窥蠡豹,对整个DeviceDriver子系统有一个清晰的认识。sysfs的实质是以文件系统的形式展示内核中的Device Driver信息,因此,理解sysfs可以分为2个维度的问题:

  • sysfs的Inside Tree的组织思想
  • sysfs的Outside Tree的目录结构

本文主要讨论内部树的基础,kobject,kset和ktype的定义联系及其常用API

kobject

list_head是内核所有链式存储数据结构的基础,可以看作这些struct的父类,基于同样的思想,在Device Driver Subsystem中, 这个全局父类的角色由kobject担任,当然,kobject本身也是一个list_head的子类。kobject为Device Driver管理的对象提供了最高层次的抽象,无论是Device对象还是Driver对象,都可以看作kobject的父类,sysfs通过kobject,就可以管理系统中所有的Device对象和Driver对象。

//include/linux/kobject.h
 63 struct kobject { 
 64         const char              *name;
 65         struct list_head        entry;
 66         struct kobject          *parent;
 67         struct kset             *kset;
 68         struct kobj_type        *ktype;
 69         struct kernfs_node      *sd;
 70         struct kref             kref;
 71 #ifdef CONFIG_DEBUG_KOBJECT_RELEASE
 72         struct delayed_work     release;
 73 #endif
 74         unsigned int state_initialized:1;
 75         unsigned int state_in_sysfs:1;
 76         unsigned int state_add_uevent_sent:1;
 77         unsigned int state_remove_uevent_sent:1;
 78         unsigned int uevent_suppress:1;
 79 };

–64–>kobject对象的名字
–65–>kobject对象之间的连接件
–66–>该kobject对象的父kobject对象
–67–>该kobject所属的kset
–68–>该kobject附属的ktype
–69–>该kobject在sysfs中的形式,比如符号链接,目录以及属性等信息
–70–>引用计数成员,本质是一个原子变量,用于决定何时释放对象
–74–>1bit,如果该kobject对象已经被初始化,则为1
–75–>1bit,如果该kobject对象已经被添加到sysfs中,则为1
–76–>1bit,如果该kobject对象已经发送过uevent add事件到用户空间,则为1
–77–>1bit,如果该kobject对象已经发送过uevent remove事件到用户空间,则为1
–78–>1bit,如果该kobject对象”抑制”发送事件到用户空间,则为1

kobj_init()

Continue reading

Linux 设备号管理 II

使用设备号的一个基本需求就是根据设备号索引能够找到设备,而在Linux设备模型中,所有的设备最后都归结到kobject,所以问题也就转变成 dev_t到kobject实例的mapping问题,内核中使用kobj_map来描述这种映射,具体地,内核创建了两个实例cdev_map和bdev_map来分别管理字符设备和块设备及其设备号的映射关系, 二者都是kobj_map类型, 可以用下图简要表示

本文主要讨论如下问题:

  1. kobj_map是如何实现这种映射的。
  2. kobj_map实现的这种映射是如何被使用的。
Continue reading

Linux 设备号管理 I

Linux使用设备号——dev_t类型的一个数值来标识一个字符设备或块设备对象,虽然实际开发中通常使用对cdev或gendisk的更高层的封装接口,诸如输入子系统,MTD,MutiDisk等设备模型,但万丈高楼平地起,管理这些复杂设备的根基都是对cdev或gendisk对象的管理,其中,设备号的获取以及管理就是重要的一个方面,这就是本文主要讨论问题。

Continue reading

Linux i2c 子系统 IV

“./drivers/i2c/busses/i2c-s3c2410.c”是3.14.0内核中三星SoC的i2c控制器驱动程序, 本文试图通过对这个程序的分析, 剥离繁复的细节, 总结一套编写i2c主机控制器驱动的框架以及一个分析内核驱动的流程.

匹配之前

1287 static int __init i2c_adap_s3c_init(void)
1288 {
1289         return platform_driver_register(&s3c24xx_i2c_driver);
1290 }
1291 subsys_initcall(i2c_adap_s3c_init);

–1291–>将主机控制器驱动在系统启动的时候就注册好
–1289–>这个驱动是基于platform总线的, 设备信息的部分在板级文件i2c_board_info中描述并作为platform_device随内核启动被注册, 所以控制器驱动在系统启动的时候就可以工作了

1275 static struct platform_driver s3c24xx_i2c_driver = {
1276         .probe          = s3c24xx_i2c_probe,
1277         .remove         = s3c24xx_i2c_remove,
1278         .id_table       = s3c24xx_driver_ids,
1279         .driver         = {
1280                 .owner  = THIS_MODULE,
1281                 .name   = "s3c-i2c",
1282                 .pm     = S3C24XX_DEV_PM_OPS,
1283                 .of_match_table = of_match_ptr(s3c24xx_i2c_match),
1284         },
1285 };

既然是遵循的platform编写, 那么所有的信息都要在一个platform_driver中描述, 分析也是围绕这个对象展开

–1276–>probe函数, 最重要的函数
–1278–>用于匹配的id表, 由于是平台文件编写的设备信息, 所以会使用这个域作为匹配的依据, 如下

 132 static struct platform_device_id s3c24xx_driver_ids[] = {
 133         {
 134                 .name           = "s3c2410-i2c",
 135                 .driver_data    = 0,
 136         }, {
 137                 .name           = "s3c2440-i2c",
 138                 .driver_data    = QUIRK_S3C2440,
 139         }, {
 140                 .name           = "s3c2440-hdmiphy-i2c",
 141                 .driver_data    = QUIRK_S3C2440 | QUIRK_HDMIPHY | QUIRK_NO_GPIO,
 142         }, { }, 
 143 }; 

我们可以在”arch/arm/plat-samsung”中找到相应的设备信息

 485 struct platform_device s3c_device_i2c0 = {
 486         .name           = "s3c2410-i2c",
 487         .id             = 0,
 488         .num_resources  = ARRAY_SIZE(s3c_i2c0_resource),
 489         .resource       = s3c_i2c0_resource,
 490 };

二者一匹配, probe就执行

匹配之后

Continue reading

Linux i2c 子系统 III

如果你也遇到了填充了id_match_table,compitible怎么看都一样,但probe就是不执行(让我哭一会),你可以回头看一下上一篇的模板,我们这里虽然使用的是设备树匹配,但和platform的设备树匹配只填充i2c_match_table不同,i2c_driver的设备树匹配需要同时填充i2c_match_table和id_table两个域,虽然后者是个空。如果你没有填充后面的成员,不妨试一下我的这种写法,我敢打赌你的probe也没有执行^-^。
问题是明确的,探索是漫长的,但是至少答案一定在源码中,也一定出在匹配的源码中,带着这样的思路,我从“i2c_add_driver”开始一路狂追,结论是使用设备树的话,只要id_match_table,不需要id_table!, 下面的i2c_device_match即可看出。

i2c_add_driver()
  i2c_register_driver()
    driver_register()
      driver_find()
        kset_find_obj()
        kobject_put()
        to_driver()
      bus_add_driver()
        driver_attach()
          bus_for_each_dev()
            next_device()
            __driver_attach()
              driver_match_device()
                i2c_device_match()
                  acpi_driver_match_device()
                  i2c_match_id()
                  of_driver_match_device()
                  of_match_device()
                    of_match_node()
                      __of_match_node()
                        __of_device_is_compatible()
//3.14.0/drivers/i2c/i2c-core.c
  78 static int i2c_device_match(struct device *dev, struct device_driver *drv)
  79 {
  80         struct i2c_client       *client = i2c_verify_client(dev);
  81         struct i2c_driver       *driver;
  82 
  83         if (!client)
  84                 return 0;
  85 
  86         /* Attempt an OF style match */
  87         if (of_driver_match_device(dev, drv))
  88                 return 1;
  89 
  90         /* Then ACPI style match */
  91         if (acpi_driver_match_device(dev, drv))
  92                 return 1;
  93 
  94         driver = to_i2c_driver(drv);
  95         /* match on an id table if there is one */
  96         if (driver->id_table)              
  97                 return i2c_match_id(driver->id_table, client) != NULL;
  98 
  99         return 0;
 100 }

从i2c_device_match的定义可以看出, 和platform总线一样, i2c的match函数也是优先选择设备树, 如果设备树已经匹配了, 函数就会返回, 不会再最平台文件的设备信息进行判断, 即不会理会id_table的值, 确保匹配是一定不需要id_table了,而事实上probe确实没有执行,那么问题就可能出现在probe的回调过程了,和所有的总线设备一样,回调probe的过程始于driver_match_id,于是又是一路狂追,终于在i2c_device_probe()中找到了我臆想中的对id_table的检测

下面是我追的源码树,大家可以验证一下

i2c_add_driver()
  i2c_register_driver()
    driver_register()
      driver_find()
        kset_find_obj()
        kobject_put()
        to_driver()
      bus_add_driver()
        driver_attach()
          bus_for_each_dev()
            next_device()
            __driver_attach()
              driver_match_device()
              driver_probe_device()
                really_probe()
                  i2c_device_probe()
                    i2c_match_id()

所以,结论是:即使使用设备树来匹配,也要对id_table进行有效的赋值,否则probe不会被回调!!!如果你也遇到了这个问题, 可以试试在驱动中定义一个空数组, 将其赋值给id_table

Linux i2c子系统 II

应用层除了使用上述的使用i2c_driver接口来访问i2c设备,Linux内核还提供了一种简单粗暴的方式——直接通过虚拟i2c设备驱动的方式,即上一篇中的i2c-dev提供的方式,这种方式使用的i2c_client是随着open的操作临时创建的虚拟的client,即不是挂接在i2c_bus_type中的链表中的,对于用户程序来说,这种方式的驱动只是提供了相应的操作方法并创建设备文件,可以看作是一种“i2c_driver成员函数+字符设备驱动”的虚拟驱动,需要让用户空间程序通过芯片手册配置时序来访问总线上的设备,看起来就像是在用户空间直接操作i2c控制器,但其实它更多的用法是当我们的i2c_driver工作不正常的时候,我们可以通过这种方式来排查具体是设备驱动工作的问题or主机驱动工作的问题。
如若需要使用这个功能,需要对内核进行下述配置,重新编译加载之后我们就可以在内核中看到设备号为89的设备文件,这个就是主机驱动提供给应用层的访问接口

>device drivers--->
    I2C support --->
        I2C device interface

以mpu6050为例,下面是一个简单的应用层直接通过主机驱动访问的demo

#define MPU6050_MAGIC 'K'

union mpu6050_data
{
    struct {
        short x;
        short y;
        short z;
    }accel;
    struct {
        short x;
        short y;
        short z;
    }gyro;
    unsigned short temp;
};

#define GET_ACCEL _IOR(MPU6050_MAGIC, 0, union mpu6050_data)
#define GET_GYRO  _IOR(MPU6050_MAGIC, 1, union mpu6050_data) 
#define GET_TEMP  _IOR(MPU6050_MAGIC, 2, union mpu6050_data)

int main(int argc, char * const argv[])
{
    int fd = open(argv[1],O_RDWR);
    union mpu6050_data data = {{0}};
    while(1){
        ioctl(fd,GET_ACCEL,&data);
        printf("acc:x %d, y:%d, z:%d\n",data.accel.x,data.accel.y,data.accel.z);
        ioctl(fd,GET_GYRO,&data);
        printf("gyro:x %d, y:%d, z:%d\n",data.gyro.x,data.gyro.y,data.gyro.z);
        ioctl(fd,GET_TEMP,&data);
        printf("temp: %d\n",data.temp);
        sleep(1);
    }
    return 0;
}

Linux i2c 子系统 I

i2c总线是一种十分常见的板级总线,本文以linux3.14.0为参考, 讨论Linux中的i2c驱动模型并利用这个模型写一个mpu6050的驱动, 最后在应用层将mpu6050中的原始数据读取出来

除了经典的分层与分离模型,我们也可以看到一个有意思的现象——Linux 的应用程序不但可以通过设备驱动来访问i2c从设备,还可以通过一个并没有直接挂接到i2c_bus_type的i2c_cleint来找到主机控制器进而访问任意一个i2c设备. 这部分我在下一篇分析.

Continue reading

Linux input 子系统

输入设备都有共性:中断驱动+字符IO,基于分层的思想,Linux内核将这些设备的公有的部分提取出来,基于cdev提供接口,设计了输入子系统,所有使用输入子系统构建的设备都使用主设备号13,同时输入子系统也支持自动创建设备文件,这些文件采用阻塞的IO读写方式,被创建在“/dev/input/”下。如下图所示。内核中的输入子系统自底向上分为设备驱动层,输入核心层,事件处理层。由于每种输入的设备上报的事件都各有不同,所以为了应用层能够很好识别上报的事件,内核中也为应用层封装了标准的接口来描述一个事件,这些接口在“/include/upai/linux/input”中。

  • 设备驱动层是具体硬件相关的实现,也是驱动开发中主要完成的部分,
  • 输入核心层主要提供一些API供设备驱动层调用,通过这些API设备驱动层上报的数据就可以传递到事件处理层,
  • 事件处理层负责创建设备文件以及将上报的事件传递到用户空间,

具体的,这三个层次每一个层次都由一条结构体链表组成,在设备驱动层,核心结构体是input_dev;在input核心层,是input_handle;在事件处理层,是input_handler。内核通过链表和指针将三者结合到一起,最终实现了input_dev和input_handler的多对多的映射关系,这种关系可用下图简单描述:

input对象描述了一个输入设备,包括它可能上报的事件,这些事件使用位图来描述,内核提供的相应的工具帮助我们构建一个input对象,大家可以参考内核文档“Documentation/input/input-programming.txt”,里面对于input子系统的使用有详细的描述。

Continue reading

Linux 内核定时器- timer_list 与delayed_work

软件上的定时器最终要依靠硬件时钟来实现,简单的说,内核会在时钟中断发生后检测各个注册到内核的定时器是否到期,如果到期,就回调相应的注册函数,将其作为中断底半部来执行。实际上,时钟中断处理程序会触发TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
设备驱动程序如要获得时间信息以及需要定时服务,都可以使用内核定时器。

要说内核定时器,首先就得说说内核中关于时间的一个重要的概念:jiffies变量,作为内核时钟的基础,jiffies每隔一个固定的时间就会增加1,称为增加一个节拍,这个固定间隔由定时器中断来实现,每秒中产生多少个定时器中断,由在<linux/param.h>中定义的HZ宏来确定,如此,可以通过jiffies获取一段时间,比如jiffies/HZ表示自系统启动的秒数。下两秒就是(jiffies/HZ+2),内核中用jiffies来计时,秒转换成的jiffies:seconds*HZ,所以以jiffiy为单位,以当前时刻为基准计时2秒:(jiffies/HZ+2)*HZ=jiffies+2*HZ如果要获取当前时间,可以使用do_gettimeofday(),该函数填充一个struct timeval结构,有着接近微妙的分辨率。

//kernel/time/timekeeping.c
 473 /**
 474  * do_gettimeofday - Returns the time of day in a timeval
 475  * @tv:         pointer to the timeval to be set
 476  *
 477  * NOTE: Users should be converted to using getnstimeofday()
 478  */
 479 void do_gettimeofday(struct timeval *tv)   

驱动程序为了让硬件有足够的时间完成一些任务,常常需要将特定的代码延后一段时间来执行,根据延时的长短,内核开发中使用长延时短延时两个概念。长延时的定义为:延时时间>多个jiffies,实现长延时可以用查询jiffies的方法:

time_before(jiffies, new_jiffies);
time_after(new_jiffiesmjiffies);

**短延时的定义为:延迟事件接近或短于一个jiffy,实现短延时可以调用

udelay();
mdelay();

这两个函数都是忙等待函数,大量消耗CPU时间,前者使用软件循环来延迟指定数目的微妙数,后者使用前者的嵌套来实现毫秒级的延时。

timer_list

Continue reading

Linux 设备文件的阻塞/非阻塞IO

等待队列是内核中实现进程调度的一个十分重要的数据结构,其任务是维护一个链表,链表中每一个节点都是一个PCB(进程控制块),内核会将PCB挂在等待队列中的所有进程都调度为睡眠状态,直到某个唤醒的条件发生,本文主要讨论驱动中怎么实现对设备IO的阻塞与非阻塞读写。显然,实现这种与阻塞相关的机制要用到等待队列机制。本文的内核源码使用的是3.14.0版本

设备阻塞IO的实现

当我们读写设备文件的IO时,最终会回调驱动中相应的接口,而这些接口也会出现在读写设备进程的进程(内核)空间中,如果条件不满足,接口函数使进程进入睡眠状态,即使读写设备的用户进程进入了睡眠,也就是我们常说的发生了阻塞。In a word,读写设备文件阻塞的本质是驱动在驱动中实现对设备文件的阻塞,其读写的流程可概括如下:

1. 定义-初始化等待队列头

//定义等待队列头
wait_queue_head_t waitq_h;
//初始化,等待队列头
init_waitqueue_head(wait_queue_head_t *q);
 //或
//定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(waitq_name);

上面的几条选择中,最后一种会直接定义并初始化一个等待头,但是如果在模块内使用全局变量传参,用着并不方便,具体用哪种看需求。
我们可以追一下源码,看一下上面这几行都干了什么:

//include/linux/wait.h 
 35 struct __wait_queue_head { 
 36         spinlock_t              lock;
 37         struct list_head        task_list;
 38 };
 39 typedef struct __wait_queue_head wait_queue_head_t;

–36–>这个队列用的自旋锁
–27–>将整个队列”串”在一起的纽带

然后我们看一下初始化的宏:

 55 #define __WAIT_QUEUE_HEAD_INITIALIZER(name) {                           \
 56         .lock           = __SPIN_LOCK_UNLOCKED(name.lock),              \
 57         .task_list      = { &(name).task_list, &(name).task_list } }
 58 
 59 #define DECLARE_WAIT_QUEUE_HEAD(name) \
 60         wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

–60–>根据传入的字符串name,创建一个名为name的等待队列头
–57–>初始化上述task_list域,竟然没有用内核标准的初始化宏,无语。。。

2. 将本进程添加到等待队列

为等待队列添加事件,即进程进入睡眠状态直到condition为真才返回。_interruptible的版本版本表示睡眠可中断,_timeout版本表示超时版本,超时就会返回,这种命名规范在内核API中随处可见。

Continue reading

Linux 异步通知技术简介

异步通知的全称是”信号驱动的异步IO”,通过”信号”的方式,放期望获取的资源可用时,驱动会主动通知指定的应用程序,和应用层的”信号”相对应,这里使用的是信号”SIGIO“。操作步骤是

  1. 应用层程序将自己注册为接收来自设备文件的SIGIO信号的进程
  2. 驱动实现相应的接口,以期具有向所有注册接收这个设备驱动SIGIO信号的应用程序发SIGIO信号的能力。
  3. 驱动在适当的位置调用发送函数,应用程序即可接收到SIGIO信号。

整个机制的框架:

应用层接收SIGIO

和其他信号一样,应用层需要注册一个信号处理函数,
注册的方式还是使用signal()sigaction()

此外,应用层还需要把自己加入到驱动的通知链表中,加入的代码如下

fcntl(dev_fd,F_SETOWN,getpid());
int oflags = fcntl(dev_fd,F_GETFL);
fcntl(dev_fd,F_SETFL,oflags|FASYNC);
...
while(1);

完成了上面的工作,应用层的程序就可以静待SIGIO的到来了。

驱动发送SIGIO

Continue reading

Linux 内存模型与内存申请

下图是Linux的内存映射模型

  1. 每一个进程都有自己的进程空间,进程空间的0-3G是用户空间,3G-4G是内核空间
  2. 每个进程的用户空间不在同一个物理内存页,但是所有的进程的内核空间对应同样的物理地址
  3. vmalloc分配的地址可以高端内存,也可以是低端内存
  4. 0-896MB的物理地址是线性映射到物理映射区的。
  5. 内核参数和系统页表都在TEXT_OFFSET保存,除了进程除了访问自身的用户空间对应的DRAM内存页外,都要经过内核空间,也就是都要切换到内核态

内存动态申请

和应用层一样,内核程序也需要动态的分配内存,不同的是,内核进程可以控制分配的内存是在用户空间还是内核空间,前者可以用于给用户空间的堆区分配内存,eg,用户进程的用户空间的malloc最终就会通过系统调用回调内核空间的内存分配函数,此时该内存分配函数就属于该用户进程,可以给在该用户进程的堆区分配空间并返回,最终使得一个用会进程在自己的用户空间获得内存分配;后者只在内核空间分配,所以用户进程不能直接访问该空间,所以多用在满足内核程序自身的内存需求,下面是Linux内核空间申请内存常用API:

kmalloc – kfree

kmalloc申请的内存在物理内存上是连续的,他们与真实的物理地址只有一个固定的偏移,因此存在简单的转换关系。这个API 多用来申请不到一个page大小的内存。kmalloc的底层需要调用__get_free_pages,参数中表示内存类型的gtp_t flags正是这个函数的缩写,常用的内存类型有GFP_USER,GFP_KERNEL,GFP_ATOMIC几种。

  • GFP_USER表示为用户空间页分配内存,可以阻塞;
  • GFP_KERNEL是最常用的flag,注意,使用这个flag来申请内存时,如果暂时不能满足,会引起进程阻塞,So,一定不要在中断处理函数,tasklet和内核定时器等非进程上下文中使用GFP_KERNEL!!!
  • GFP_ATOMIC就可以用于上述三种情境,这个flag表示如果申请的内存不能用,则立即返回。
Continue reading

Linux 字符设备驱动 II

前文中已经简单的介绍了字符设备驱动的基本的编程框架,这里我们来探讨一下Linux内核(以4.8.5内核为例)是怎么管理字符设备的,即当我们获得了设备号,分配了cdev结构,注册了驱动的操作方法集,最后进行cdev_add()的时候,究竟是将哪些内容告诉了内核,内核又是怎么管理我的cdev结构的,这就是本文要讨论的内容。我们知道,Linux内核对设备的管理是基于kobject的,这点从我们的cdev结构中就可以看出,所以,接下来,你将看到”fs/char_dev.c”中实现的操作字符设备的函数都是基于“lib/kobject.c”以及“drivers/base/map.c”中对kobject操作的函数。好,现在从cdev_add()开始一层层的扒。

cdev_map对象

//fs/char_dev.c
 27 static struct kobj_map *cdev_map;

内核中关于字符设备的操作函数的实现放在“fs/char_dev.c”中,打开这个文件,首先注意到就是这个在内核中不常见的静态全局变量cdev_map(27),我们知道,为了提高软件的内聚性,Linux内核在设计的时候尽量避免使用全局变量作为函数间数据传递的方式,而建议多使用形参列表,而这个结构体变量在这个文件中到处被使用,所以它应该是描述了系统中所有字符设备的某种信息,带着这样的想法,我们可以在“drivers/base/map.c”中找到kobj_map结构的定义:

//drivers/base/map.c
 19 struct kobj_map {     
 20         struct probe {
 21                 struct probe *next;
 22                 dev_t dev;
 23                 unsigned long range;
 24                 struct module *owner;
 25                 kobj_probe_t *get;
 26                 int (*lock)(dev_t, void *);
 27                 void *data;
 28         } *probes[255];  
 29         struct mutex *lock;
 30 };

从中可以看出,kobj_map的核心就是一个struct probe类型、大小为255的数组,而在这个probe结构中,第一个成员next(21)显然是将这些probe结构通过链表的形式连接起来,dev_t类型的成员dev显然是设备号,get(25)和lock(26)分别是两个函数接口,最后的重点来了,void作为C语言中的万金油类型,在这里就是我们cdev结构(通过后面的分析可以看出),所以,这个cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(这里就是cdev),下图中体现两种常见的对设备号和cdev管理的方式,其一是一个cdev对象对应这一个/多个设备号的情况, 在cdev_map中, 一个probes对象就对应一个主设备号,多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象;其二是当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200],可以表示设备号200,455…3895等所有对255取余是200的数字, 参见下文的kobj_map–58–。

cdev_add()

了解了cdev_map的功能,我们就可以一探cdev_add()。从中可以看出,其工作显然是交给了kobj_map()


–460–>就是将我们之前获得设备号和设备号长度填充到cdev结构中,
–468–>kobject_get()将kobject的计数减一,并返回struct kobject*

Continue reading

Linux 字符设备驱动 I

字符设备就是字节流形式通讯的I/O设备, 是Linux三大设备之一, 日常所见的绝大部分设备都是字符设备,包括鼠标、键盘、显示器、串口等等,当我们执行ls -l /dev的时候,就能看到大量的设备文件,c就是字符设备,b就是块设备,网络设备没有对应的设备文件。编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码。

Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在struct file_operations,当我们写一个驱动的时候,一定要实现相应的接口,这样才能使这个驱动可用,Linux的内核中大量使用”注册+回调”机制进行驱动程序的编写,所谓注册回调,简单的理解,就是当我们open一个设备文件的时候,其实是通过VFS找到相应的inode,并执行此前创建这个设备文件时注册在inode中的open函数,其他函数也是如此,所以,为了让我们写的驱动能够正常的被应用程序操作,首先要做的就是实现相应的方法,然后再创建相应的设备文件。

#include <linux/cdev.h> //for struct cdev
#include <linux/fs.h>   //for struct file
#include <asm-generic/uaccess.h>    //for copy_to_user
#include <linux/errno.h>            //for error number

static int ma = 0;
static int mi = 0;
const int count = 3;

/* 准备操作方法集 */
/* 
struct file_operations {
    struct module *owner;   //THIS_MODULE
    
    //读设备
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    //写设备
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    //映射内核空间到用户空间
    int (*mmap) (struct file *, struct vm_area_struct *);

    //读写设备参数、读设备状态、控制设备
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

    //打开设备
    int (*open) (struct inode *, struct file *);
    //关闭设备
    int (*release) (struct inode *, struct file *);

    //刷新设备
    int (*flush) (struct file *, fl_owner_t id);

    //文件定位
    loff_t (*llseek) (struct file *, loff_t, int);

    //异步通知
    int (*fasync) (int, struct file *, int);
    //POLL机制
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    。。。
};
*/

ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset)
{
    return 0;
}

struct file fops = {
    .owner = THIS_MODULE,
    .read = myread,
    ...
};

/* 字符设备对象类型 
struct cdev {
    struct kobject kobj;     
    struct module *owner;        //模块所有者(THIS_MODULE),用于模块计数
    const struct file_operations *ops;    //操作方法集(分工:打开、关闭、读/写、...)
    struct list_head list;
    dev_t dev;                            //设备号(第一个)
    unsigned int count;            //设备数量
};
*/

static int __init chrdev_init(void)
{
    ...
    /* 构造cdev设备对象 */
    struct cdev *cdev_alloc(void);

    /* 初始化cdev设备对象 */
    void cdev_init(struct cdev*, const struct file_opeartions*);

    /* 申请设备号,静态or动态*/
    /* 为字符设备静态申请第一个设备号 */
    int register_chrdev_region(dev_t from, unsigned count, const char* name);

    /* 为字符设备动态申请第一个设备号 */
    int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);

    ma = MAJOR(dev)     //从dev_t数据中得到主设备号
    mi = MINOR(dev)     //从dev_t数据中得到次设备号
    MKDEV(ma,1) //将主设备号和次设备号组合成设备号,多用于批量创建/删除设备文件

    /* 注册字符设备对象cdev到内核 */
    int cdev_add(struct cdev* , dev_t, unsigned);
    ...
}

static void __exit chrdev_exit(void)
{
    ...
    /* cdev_del()、cdev_put()二选一 */
    /* 从内核注销cdev设备对象 */
    void cdev_del(struct cdev* );

    /* 从内核注销cdev设备对象 */
    void cdev_put(stuct cdev *);

    /* 回收设备号 */
    void unregister_chrdev_region(dev_t from, unsigned count);
    ...
}
Continue reading