关于usb_skeleton的理解

题记:

临近2012年的尾声,南京又下起了大雪,起床一看钟,又快到中午了,我最近真是太能睡了。。。好了,闲话少说,花了2天的时间,大致看了下usb_skeleton,越发觉得骨架程序的并发处理要比Gadget_Printer要经典的多。分析方法则采用我们之前的《Printer Gadget Driver 分析》思路来展开。

正文:

首先看一下struct usb_skel结构体,同其他驱动程序一样,这个结构体负责将skeleton所代表的设备资源打包管理。先来重点分析一下这个结构体。

struct usb_skel {

struct usb_device *udev; /* the usb device for this device */

//这是内核用来代表一个插入的USB设备,该结构体含有了很有设备必备的信息(比如设备描述符结构体)

struct usb_interface *interface; /* the interface for this device */

//代表该设备其中的一个接口

struct semaphore limit_sem; /* limiting the number of writes in progress */

//由于urb可以连续提交,所以这里使用信号量,在wirte设备方法中,使得多个(有限定的)进程可以同时访问wirte设备方法,申请urb,并且连续发送。这里的信号量最大的作用是:限定连续发送的urb个数。

struct usb_anchor submitted; /* in case we need to retract our submissions */

//这个结构体很特殊,一般文档中也见不到关于它的解释:主要是由于urb可以连续提交.那么必须有一个结构体用于记录和维护已经提交的urb(假设我们想取消其中几个urb,那么anchor就派上用场了)详细解释见:usb_anchor.c

unsigned char           *bulk_in_buffer; /* the buffer to receive data */

//接收来自USB设备的缓存区指针,主要用于urb

size_t bulk_in_size; /* the size of the receive buffer */

//缓存区的大小

__u8 bulk_in_endpointAddr; /* the address of the bulk in endpoint */

//批量IN端点地址

__u8 bulk_out_endpointAddr; /* the address of the bulk out endpoint */

//批量OUT端点地址

int errors; /* the last request tanked */

int open_count; /* count the number of openers */

spinlock_t err_lock; /* lock for errors */

struct kref kref;

//内核引用计数,主要是用于内核计数。后面会详细分析其作用

struct mutex io_mutex; /* synchronize I/O with disconnect */

//互斥信号量

};

几个重要结构体关系:

在具体分析设备方法之前,首先应该分析的是USB Host Driver的几个重要结构体关系,它们分别是:

Usb_device:   是最顶层的结构,代表一个USB设备,一个设备可有多个配置

Usb_host_config:  是设备的一个配置,一个配置可有多个接口

Usb_interface:    是配置的一个接口,一个接口可有多个设置

详解:代表该设备其中的一个接口(注:USB 端点被绑为接口,USB接口只处理一种USB逻辑连接。一个USB接口代表一个基本功能,每个USB驱动控制一个接口。)USB 接口可以有其他的设置,它是对接口参数的不同选择. 接口的初始化的状态是第一个设置,编号为0。 其他的设置可以以不同方式控制独立的端点。 而代表接口的设置时用struct usb_host_interface 来代表的。

在上面的分析我们已经说到,每一个USB驱动程序对应USB的一个接口,因此这里也就说明了:为什么USB内核传递struct usb_interface给驱动程序probe函数。

在这里我应该提醒读者,在阅读本篇文档之前,应该首先理清USB Host驱动端几个核心的结构体关系(详见我提供的《USB 设备驱动开发之几个重要结构体分析》)。

Usb_host_interface:   是接口的一个设置,每个设置可绑定多个端点

Usb_host_endpoint:   是接口设置的端点,而端点是USB体系结构最下端,代表了USB实际传输的媒介,因此我们可以认为,上述的几个结构都是对端点的逻辑组装。

图1 备相关结构体关系

USB Host端的驱动体系结构:

其次,我们在介绍一个USB Host端的驱动体系结构,不了解这个结构,也就没法进行后续分析。

上述图表,形象的表现出HOST端的驱动结构框架,正是由于USB Core的出现,我们设计驱动的时候,才不用去考虑Host Controller是哪种型号的,这种结构易于提高我们驱动的可移植性。

我们所有对Host Controller的操作都转换了对USB Core的操作。而上述的几种重要的结构体都是USB Core提供给USB Host Driver。是USB Core从Host Controller获取到的信息,封装提供给USB Host Driver来使用的。

我们对USB Host Controller的访问,都是通过提交URB或者其他手段给USB Core,USB Core再将我们的请求具体转换对Host Controller的操作。而Host Controller获取到的信息也是通过USB Core交给USB Host Driver的。

如果读者对其中的细节实现非常感兴趣的话,推荐:华清远见《Linux那些事儿之我是USB》。

竞争保护机制的一些基本原理:

介绍完驱动的体系结构以及几个重要的结构体之后,读者在分析驱动时,除了了解驱动的设计思路,还需要去了解驱动的竞争保护机制,个人觉得竞争保护禁止在某些考虑周全的驱动程序中,代码量会占到相当一部分。网上大篇幅的介绍驱动的设计思路时,往往会忽略掉这一部分的介绍,恰不知很多同学往往遇到这一部分甚是头疼。

那么接下来,我会简要的分析一下驱动程序的竞争保护机制的一些基本原理,在接下来的代码分析中,我也会重复这方面的工作,希望使得大家能够很快的入门,当然这个部分和本驱动分析没有太多的耦合,如果大家已经了解或者是不想了解,完全可以忽略该章节,直接看:Skeleton驱动的生命周期.

在骨架程序中,遇到了信号量、互斥信号量、自旋锁。

自旋锁最多只能被一个可执行线程持有(读写自旋锁除外)。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去(一直占用 CPU),在那里看是否该自旋锁的保持者已经释放了锁, " 自旋 " 一词就是因此而得名。

信号量、互斥信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(因为中断的上下文不允许休眠)

信号量(包括互斥信号量)与自旋锁两者优缺点和使用注意点:

自旋锁 适合于保持时间非常短的情况,因为一个被争用的自旋锁使得请求它的线程在等待重新可用时自旋,特别浪费处理时间,这是自旋锁的要害之处,所以自旋锁不应该被长时间持有。

在实际应用中自旋锁代码只有几行,而持有自旋锁的时间也一般不会超过两次上下方切换,因线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就可以让线程睡眠,这就失去了设计自旋锁的意义。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁

而使用信号量会引起睡眠,因此会导致线程进行切换,因此在短时间内的使用(几行代码所耗费的时间)的效率远不如自旋锁那样高效,但是对于长时间的锁定保护,则必须使用信号量,因为长时间持有自旋锁的话,则会导致当前进程一直占有CPU进行轮询,导致其他进程没有办法进行处理,从系统的角度考虑,这样会导致整个系统的效率下降。因此大家要分清楚情况,在我们所说的效率是在不同环境下相对而言的。

自旋锁有一个非常重要的使用规则(主要是为了防止”死锁“的情况出现)

旋锁不允许任务睡眠,也就是持有锁期间,不能调用会导致睡眠的函数 ( 持有自旋锁的任务睡眠会造成自死锁 —— 因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁 ) 。

而信号量也有一个非常重要的使用规则:

信号量只能再进程上下文中出现使用,不能够在中断上下文中使用。

这里涉及到了一个内核基本原则中断或者说原子上下文中,内核不能访问用户空间而且内核是不能睡眠的。也就是说在这种情况下,内核是不能调用有可能引起睡眠的任何函数一般来讲原子上下文指的是在中断或软中断中,以及在持有自旋锁的时候

那么为什么在中断上下文中不能睡眠的原因是这样的:

首先睡眠的含义是将进程置于“睡眠”状态,在这个状态的进程不能被调度执行。然后,在一定的时机,这个进程可能会被重新置为“运行”状态,从而可能被调度 执行。 

可见,“睡眠”与“运行”是针对进程而言的,代表进程的task_struct结构记录着进程的状态。内核中的“调度器”通过task_struct对进 程进行调度。
     但是,中断上下文却不是一个进程,它并不存在task_struct,所以它是不可调度的。所以,在中断上下文就不能睡眠。

那么,中断上下文为什么不存在对应的task_struct结构呢? 
    中断的产生是很频繁的(至少每毫秒(看配置,可能10毫秒或其他值)会产生一个时钟中断),并且中断处理过程会很快。如果为中断上下文维护一个对应的task_struct结构,那么这个结构频繁地分配、回收、并且影响调度器的管理,这样会对整个系统的吞吐量有所影响。

因此在中断上下文对竞争资源的保护只能使用自旋锁。使用时还需要注意是否会导致死锁情况的发生。

下面以表格的形式详细的介绍在哪种情况下需要使用那种锁定机制,清晰易懂:

------------------------自旋锁VS信号量-------------------------------

需求                                     建议的加锁方法

低开销加锁                               优先使用自旋锁

短期锁定                                 优先使用自旋锁

长期加锁                                 优先使用信号量

中断上下文中加锁                         使用自旋锁

持有锁是需要睡眠、调度                   使用信号量

对于竞争保护机制,个人觉得是一个博大精深的领域,在这里也只能略说一二,希望大家补充,纠错!

Skeleton驱动的生命周期:

了解上述的基础知识后,直接切入正题:了解一下Skeleton驱动的生命周期:

1. 注册Skeleton驱动到USB子系统(调用usb_skel_init函数)

2. 设备被安装,USB核心认为驱动程序应该处理(检测信息等)(调用probe函数)

3. 驱动程序使用USB主设备号,在用户空间使用传统字符驱动程序注册到USB Core(usb_register_dev函数)

4. 设备驱动被打开,开始数据传输等工作(open、read、write、release等)

5. 设备被拔除,USB核心调用disconnect函数,清除关于该设备的所有资源(disconnect函数)

Skel_probe函数分析

1.给设备相关的结构体分配内核内存:dev = kzalloc(sizeof(*dev), GFP_KERNEL)。

2.填充dev结构体,并且初始化dev结构体中相继使用的一些资源:如自旋锁、信号量、引用计数等。

3.通过接口的当前设置,获取关于接口的端点类型、端点信息、端点地址以及大小等,保存在局部结构体dev中(参见LDD3:page347)。

4.通过usb_set_intfdata函数,将初始化好的设备相关结构体保存在interface结构体中,便于后续的取出使用,这样的好处在LDD3中也提到了:USB驱动程序不需要维护一个静态的指针数组来存储系统中所有当前设备的结构体。

Skel_disconnect函数分析:

1.通过usb_get_intfdata,取出dev结构体

2.将之前使用的次设备号还给USB核心

3.将struct usb_anchor记录的正在工作的urb取消掉。

4.减少对dev的引用计数,如果当前的引用计数为0,那么就释放dev结构体

Skel_open设备方法:

1.获取interface结构体,记住之前说过,USB的一个interface就对应一个驱动程序

2.从interface中取出之前probe函数中初始化好的代表设备的dev结构体

3.每一个进程调用该设备方法时,实际上就是对dev做具体的操作。因此,我们需要增加dev的引用计数:kref_get(&dev->kref)。这样做的目的:如果多进程操作该设备结构dev,每多一个进程操作的时候,我们就增加引用计数,那么在任意调用一次release设备方法时,我们就能清楚的知道当前的dev是否还有进程在操作,也就可以清楚的知道,什么时候才可以释放dev资源。这里顺便说一下引用计数,内核正是靠着引用计数,才知道当前的资源是否有进程在操作,我们内核的很多函数会对引用计数有影响,目的就是使得内核可以清楚的知道,当前的一个资源有多少个进程在操作。

4.open_count是提供给驱动程序的计数,当前第一个线程去操作open设备方法时,open_count为0,那么随即调用了usb_autopm_get_interface函数,该函数是usb core提供的给驱动端的,是一个电源管理的函数,这个函数所做的事情就是让这个usb interface的电源引用计数加一,也就是说,只要这个引用计数大于0,这个设备就不允许autosuspend。在open和release设备方法中,采用open_count这种计数处理机制,目的是这样的:只要当前有进程或者线程在操作驱动,那么就一直使得设备不允许autosuspend。而如何判断当前有多少个进程在操作驱动,就是通过open_count计数来达到的。所以说:open_count是给本驱动程序用的,dev->kref引用计数是给内核用的。但是大体上的设计思想是一致的。

5.在open设备方法中,使用了互斥信号量,目的就是为了对局部共享变量dev进行访问,当前可能会有多个线程对该变量访问,如果没有锁定机制,那么访问的结果将是不可预料的。

Skel_release设备方法:

1.由于open设备方法已经将代表dev结构体存储在file结构体中,所以我们在release方法中直接从file结构体中取出dev即可。

2.对open_count减1,并判断当前的open_count是否为0,open_count为0,则代表对当前没有线程对dev操作,那么我们可以允许设备autosuspend。

3.同样对dev的引用计数减1,看kref是否为0,是否需要释放dev该结构体所占用的资源。

4.同open设备方法一样,都使用了互斥信号量,我们可以在代码中看到大量的这种锁定机制,第一是对dev结构体访问的保护,第二,对于usb_autopm_put_interface函数的访问是否会导致睡眠,有待验证。如果不导致睡眠的话,个人认为几行代码的保护机制,优先使用自旋锁。

Skel_read设备方法:

相对于linux2.3.36来说,skel_read的设备方法实现相对简单许多,虽然简单,但是在这里还是要把简明的道理顺带说一下:

1. 从struct file结构体中取出dev结构体,使用信号量保护下面一段代码对临界资源的访问。

2. 使用usb_bulk_msg(简单的USB数据传输函数,没有使用urb,参见LDD3)最多从设备取出bulk端点大小的数据。注意该函数会睡眠,因此才使用互斥信号量来锁定保护。

3. Copy_to_user,将数据返给用户进程。注意该函数也会导致阻塞睡眠。

Skel_write设备方法:

1. 从struct file结构体中取出dev,利用down_interruptible函数,获得信号量(当前信号量值不为0,才可被获得,否则睡眠阻塞)。可能有的同学不太理解这里使用信号量的意义,在这里我粗略的解释一下:wirte设备方法可以简单地认为是可以并行化操作的。只是当前限定了访问进程的个数(通过信号量数值来限定)。在构建urb、并为urb分配相应的user_buffer等这些操作都是并行的。只是在填充、提交urb的时候,才使用的互斥锁来串行化提交urb的操作。

2. usb_alloc_urb:为当前进程分配一个urb

3. usb_buffer_alloc(dev->udev, writesize, GFP_KERNEL, &urb->transfer_dma);给当前的urb分配相应的buf(该buf的作用是拷贝用户空间数据,通过urb提交给USB Core,再发送给USB device端),注意,第一:usb_buffer_alloc在内核中已经变更为usb_alloc_coherent。第二:该函数具体的作用是申请了一块DMA内存。那么DMA内存的大致原理是什么呢?我们经常使用的kmalloc申请的内存在ram中,但是这些数据要通过CPU才能到设备中。现在申请的DMA内存也在ram中,但不再需要送到CPU中,直接通过DMA方式,也就是通过DMA控制器,送到设备中。这样的好处是:数据处理快,并且减轻了CPU的数据处理负担。

4. copy_from_user 将用户的数据拷贝到之前分配的DMA内存中,等待发送。

5. 接下来就要访问临界资源,dev结构体。在之前,我们使用互斥信号量,对竞争进行保护。

6. usb_fill_bulk_urb:对urb进行填充,初始化。注意一下回调函数即可。urb的传输的异步操作的,那么我们提交urb之后,CPU无需等待urb的完成,继续其他的工作。Urb在完成工作之后,会调用这个回调函数,进行后续的一些处理

7. usb_anchor_urb 该函数的意义是:将我们将要提交的urb添加到submitted中(dev结构体中),用于记录和维护当前将要提交的urb。由于urb的异步操作,而且可以连续提交,所以使得当前正在处理的urb可能会大于1个,因此我们必须提供一个函数,用于记录和维护这些将要提交的urb,使得我们随时可以控制他们:比如取消正在处理(已经被提交出去的)的urb等。

8. usb_submit_urb 提交urb给USB Core。我们可以认为USB core 维护了一个等待队列,当前提交的多个urb,处在这个等待队列中,按顺序依次工作。

9. 当一个urb完成工作时,立即调用skel_write_bulk_callback函数。该函数完成的工作是:先获取的dev结构体,其次检测urb的state,以便了解当前的urb是否发送成功,如果成功,那么调用usb_buffer_free(内核中已经更换为:usb_free_coherent)释放之前申请的DMA内存,并且将信号量加1。表示当前进程对write方法的访问已经结束。

结:

这次的文档写的不太易懂,主要是文字叙述太多,没有许多形象的程序流程图等。由于时间仓促,而且限于自己的能力有限。

文档中难免出现一些低级或者是逻辑错误,个人的能力还需加强,希望读者能不吝赐教!同样,文档的形成也是与DK,Pure的讨论,在此感谢他!顺便附上联系方式:

Leo.Chou(ZSJ):   leo.chou.sj.foxmail.com

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值