950 likes | 1.19k Views
åµŒå…¥å¼ Linux 驱动高级开å‘åŠå†…æ ¸åŽŸç†. 陈应刚 chenyg@miiceic.org.cn. 日程安排. 设备驱动简介 建立和è¿è¡Œæ¨¡å— å—符驱动 调试技术 并å‘和竞争 高级å—符驱动æ“作 时间,延时和延åŽå·¥ä½œ 分é…å†…å˜ ä¸Žç¡¬ä»¶é€šè®¯ ä¸æ–å¤„ç† å—设备驱动. 日程安排. 设备驱动简介. 设备驱动简介. 驱动是什么 Driver is a software layer that lies between the applications and the actual device 驱动程åºçš„角色 æ供机制 , 而ä¸æ˜¯ç–ç•¥
E N D
嵌入式Linux驱动高级开发及内核原理 陈应刚 chenyg@miiceic.org.cn
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动 • 调试技术 • 并发和竞争 • 高级字符驱动操作 • 时间,延时和延后工作 • 分配内存 • 与硬件通讯 • 中断处理 • 块设备驱动
日程安排 • 设备驱动简介
设备驱动简介 • 驱动是什么 • Driver is a software layer that lies between the applications and the actual device • 驱动程序的角色 • 提供机制, 而不是策略 • 隐藏在UNIX中的哲学 • mechanism: What capabilities are provided. • policy: How these capabilities can be used.
Kernel的作用 • Kernel可划分为下列功能单元 • 进程管理: 进程调度, 资源分配, 进程间通信. • 内存管理: 其实也算是资源分配的一部分 • 文件系统: 管理, 组织物理媒介上数据的方法 • 设备控制: 设备驱动(ldd3所关注的) • 网络: 实质上是进程间通信. 但它不局限于一个特定的进程. 它关注收/发packets, 路由, 地址解析...
模块 • 可加载模块(lodable modules) • module: 可实时加载到内核中的代码, 它可动态连接到内核(insmod, rmmod) • 设备驱动就是module的代表, 但module还包括文件系统等等.
设备和模块的分类 • 模块分为这些类型,每种类型的模块驱动对应类型的设备 • character module, • block module, • network interface • other module
字符设备和块设备 • 字符设备: 以字节流的形式被访问的设备。e.g: /dev/console : 文本控制台. /dev/ttyS0 : 串口 • 它通过文件系统节点被访问. e.g: /dev/tty1, /dev/lp0 • 字符设备与一般文件(regular file)的区别 • 可以在一般文件中前后移动(lseek), 但只能顺序访问字符设备. • 当然, 也有特例: frame grabbers. • 块设备: 能支持文件系统的设备 • 传统的UNIX: 只能以block(512B)为单位访问块设备 • Linux: 能以访问字符设备的方式访问块设备, 即以字节文单位访问块设备. • Linux中字符设备与块设备的区别 • 内核内部对数据的组织和管理不同, 对驱动开发者来说透明 • 接口不同: 使用两套不同的interface
网络设备 • 网络接口: 能与其他主机通信的设备 • 它可以是硬件设备, 也可以是软件设备, 比如lo. (参考TCP/IP详解p26) • 网络接口只管收发数据包, 而不管这些数据包被什么协议所使用 • 不同于字符设备和块设备, 网络接口没有对应的文件系统节点. 虽然可以通过类似eth0这样的"文件名"来访问网络接口, 但文件系统节点中却没有针对网络接口的节点 • 内核与网络接口之间的通信也不同于内核与字符/块设备之间的通信(read, write), 它们之间使用特定的传输数据包的函数调用
其他设备 • 也有一些module不能严格地划分类型. • USB module: 它工作在内核的USB子系统之上 • 实际的USB设备可以是字符设备, 块设备, 也可以是网络接口 • 在设备驱动之外, 别的功能, 不论硬件和软件, 在内核中都是模块化的 • 例如文件系统
日程安排 • 设备驱动简介 • 建立和运行模块
建立和运行模块 • 建立开发环境 • ldd3例子开发环境linux2.6.10 • 2.6驱动开发需要预先安装内核源码 • 源码需要从官方下载kernel.org • 或者其他发行版的官方下载 • 直接解压到/usr/src目录下 • 版本影响 • 内核官方版本注意kernel.org • 注意发行版的内部版本 • 最新内核版本linux2.6.20/21工作队列接口变化 • 小版本变动不会对驱动的架构造成太大影响 • 对于不同发行版,不同内核版本要做少量移植和测试
内核模块VS应用程序 • 执行机制不同 • 模块初始化 • 模块退出 • 类似事件编程 • 使用库不一样 • 无法使用标准库 • 只能调用内核提供的函数
用户空间VS内核空间 • 用户空间VS内核空间 • 应用程序运行在用户空间 • 设备模块运行在内核空间 • 运行模式不一样 • 内存地址映射也不一样 • 用户空间和内核空间的转换 • 可能发生在进程中的系统调用时或者硬件中断 • 系统调用虽然在内核中执行,但是依然是在进程的上下文中进行的,所以可以访问到进程中的数据。 • 中断处理和进程是异步的了,而且不和任何进程有关系 • 模块跨越两个空间,有两个触发入口 • 一些函数作为系统调用的一部分执行 • 一些函数负责中断处理
内核中的并发 • 应用程序很多时候是按照顺序来执行的 • 内核处于并发的执行环境当中 • 内核当中有并发的进程 • 中断需要响应和处理 • 内核中的服务也在运行 • 对称多处理器导致并行
模块的加载卸载和查看 • 加载使用insmod • 卸载使用rmmod • 查看使用lsmod
模块代码 • static int __init initialization_function(void) • { • /*initialization code here*/ • } • module_init(initialization_function);
模块代码 • static void __exit cleanup_function(void) • { • /* Cleanup code here*/ • } • module_exit(cleanup_function);
如何处理加载中的失败 • int __init my_init_function(void) • { • int err; • /* registration takes a pointer and a name */ • err = register_this(ptr1, "skull"); • if (err) • goto fail_this; • err = register_that(ptr2, "skull"); • if (err) • goto fail_that; • err = register_those(ptr3, "skull"); • if (err) • goto fail_those; • return 0; /* success */ • fail_those: • unregister_that(ptr2, "skull"); • fail_that: • unregister_this(ptr1, "skull"); • fail_this: • return err; /* propagate the error */ • }
如何编写清理函数 • void __exit my_cleanup_function(void) • { • unregister_those(ptr3, "skull"); • unregister_that(ptr2, "skull"); • unregister_this(ptr1, "skull"); • return; • }
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动
主次设备号 • 字符设备可以通过文件系统来存取 • 字符设备一般位于/dev下 • 有c标志的是字符设备 • 有b标志的是块设备 • 设备号文档Documentation/devices.txt • 主设备号决定驱动的种类 • 次设备号决定使用哪个设备
设备编号的内部表达 • dev_t 类型(在 <linux/types.h>中定义)用来持有设备编号 -- 主次部分都包括 • 获得一个 dev_t 的主或者次编号, 使用 • MAJOR(dev_t dev); • MINOR(dev_t dev); • 转换为一个 dev_t, 使用: • MKDEV(int major, int minor);
分配和释放设备编号 • 分配指定的主设备号 • int register_chrdev_region(dev_t first, unsigned int count, char *name); • 动态分配主设备号 • int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); • 释放 • void unregister_chrdev_region(dev_t first, unsigned int count);
字符驱动中重要的数据结构 • file_operations • file • inode
字符设备的注册 • 在Linux 2.6下使用“struct cdev”记录字符设备的信息。结构定义如下:struct cdev {…struct module *owner;struct file_operations *ops;dev_t dev;…}; • void cdev_init(struct cdev *, struct file_operations *); • struct cdev *cdev_alloc(void); • int cdev_add(struct cdev *, dev_t, unsigned) • void cdev_del(struct cdev *);…
Open方法 • 检查设备的特定错误 • 如果设备是首次打开,则对其进行初始化 • 如有必要,更新f_op指针 • 分配且填写filp->private_data里的数据结构
Release方法 • 释放由open分配的, 保存在filp->private中的所有内容 • 在最后一次关闭操作时关闭设备
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动 • 调试技术
通过打印调试 • 通过宏可以定义日志级别 • 参考P79 • 如果开启Klogd及Syslogd则输出到日志 • 日志文件参考/var/log/message • 在printk当中打印设备编号 • Print_dev_t • Format_dev_t
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动 • 调试技术 • 并发和竞争
并发和管理 • 并发源很多 • 多个进程运行 • SMP多个CPU并行 • 设备中断 • 延迟机制(工作队列,定时器,Tasklet)
并发和竞争 • 两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,就称为竞争条件(Race Conditions)。 • 竞争情况来自对资源的共享存取的结果. • 存取管理的常用技术是加锁或者互斥 • 其次常用的技术是引用计数
临界区 • 把对共享内存进行访问的程序片段称作临界区(critical region),或临界段(critical section)。如果我们能够适当地安排使得两个进程不可能同时处于临界区,则就能够避免竞争条件。 • 临界区四要素 • 任何两个进程不能同时处于临界区 • 临界区外的进程不能阻塞其他进程 • 不能使进程在临界区外无限等待 • 不应对CPU的速度和数目做假设
PV操作解决同步互斥 • PV原语的含义P操作和V操作是不可中断的程序段,称为原语。PV原语及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量sem是一整数,sem大于等于零时代表可供并发进程使用的资源实体数,但sem小于零时则表示正在等待使用临界区的进程数。P原语操作的动作是: (1)sem减1; (2)若sem减1后仍大于或等于零,则进程继续执行; (3)若sem减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。V原语操作的动作是: (1)sem加1; (2)若相加结果大于零,则进程继续执行; (3)若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。
解决互斥 • 用PV原语实现进程的互斥 • 由于用于互斥的信号量sem与所有的并发进程有关,所以称之为公有信号量。公有信号量的值反映了公有资源的数量。 • 只要把临界区置于P(sem)和V(sem)之间,即可实现进程间的互斥。就象火车中的每节车厢只有一个卫生间,该车厢的所有旅客共享这个公有资源:卫生间,所以旅客间必须互斥进入卫生间,只要把卫生间放在P(sem)和V(sem)之间,就可以到达互斥的效果。
解决同步 • 用PV原语实现进程的同步 • 与进程互斥不同,进程同步时的信号量只与制约进程及被制约进程有关而不是与整组并发进程有关,所以称该信号量为私有信号量。 • 利用PV原语实现进程同步的方法是:首先判断进程间的关系为同步的,且为各并发进程设置私有信号量,然后为私有信号量赋初值,最后利用PV原语和私有信号量规定各进程的执行顺序。
Linux 信号量实现 • void sema_init(struct semaphore *sem, int val); • DECLARE_MUTEX(name); • DECLARE_MUTEX_LOCKED(name); • void init_MUTEX(struct semaphore *sem); • void init_MUTEX_LOCKED(struct semaphore *sem); • void down(struct semaphore *sem); • int down_interruptible(struct semaphore *sem); • int down_trylock(struct semaphore *sem); • void up(struct semaphore *sem);
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动 • 调试技术 • 并发和竞争 • 高级字符驱动操作
ioctl 接口 • 大部分驱动需要通过设备驱动进行各种硬件控制的能力. • 大部分设备可进行超出简单的数据传输之外的操作; • 例如, 设备锁上它的门, 弹出它的介质, 报告错误信息, 改变波特率, 或者自我销毁. • 这些操作常常通过 ioctl 方法来支持, 它通过相同名字的系统调用来实现.
阻塞 I/O • 数据操作可能会遇到 • read 的调用时可能没有数据时 • Write的调用时设备没有准备好接受数据 • 当驱动不能立刻满足要求怎么办 • 程序员希望调用 read 或 write 并且使调用返回 • 驱动应当(缺省地)阻塞进程, 使它进入睡眠直到请求可继续.
进程的休眠 • 进程被置为睡眠, 从调度器的运行队列移除 • 睡眠的进程被搁置一边, 等待以后发生事件 • 睡眠注意安全编程 • 在原子上下文时不能睡眠 • 休眠醒来,无法确定休眠时间和时序 • 休眠的进程必须有时机被唤醒
与休眠相关的数据结构和函数 • 等待队列 • 等待-唤醒函数 • wait_event(queue, condition) • wait_event_interruptible(queue, condition) • wait_event_timeout(queue, condition, timeout) • wait_event_interruptible_timeout(queue,condition, timeout) • void wake_up(wait_queue_head_t *queue); • void wake_up_interruptible(wait_queue_head_t *queue);
阻塞操作的推荐用法 • 阻塞操作标准语法: • 如果一个进程调用 read 但是没有数据可用(尚未), 这个进程必须阻塞. 这个进程在有数据达到时被立刻唤醒, 并且那个数据被返回给调用者, 即便小于在给方法的 count 参数中请求的数量. • 如果一个进程调用 write 并且在缓冲中没有空间, 这个进程必须阻塞, 并且它必须在一个与用作 read 的不同的等待队列中. 当一些数据被写入硬件设备, 并且在输出缓冲中的空间变空闲, 这个进程被唤醒并且写调用成功, 尽管数据可能只被部分写入,这时缓冲内没有足够空间给被请求的 count 字节.
非阻塞I/O,poll 和 select • 可以实现非阻塞读写多个文件 • 三者的区别和联系 • select 在 BSD Unix 中引入 • poll 是 System V 的解决方案 • epoll扩展到几千个文件描述符,提高了性能 • 内部实现 • unsigned int (*poll) (struct file *filp, poll_table *wait);
日程安排 • 设备驱动简介 • 建立和运行模块 • 字符驱动 • 调试技术 • 并发和竞争 • 高级字符驱动操作 • 时间,延时和延后工作
测量时间流失 • 内核通过定时器中断来跟踪时间的流动 • 定时器中断由系统定时硬件以规律地间隔产生 • 每次发生一个时钟中断, 一个内核计数器的值递增. • 这个计数器在系统启动时初始化为 0, 因此它代表从最后一次启动以来的时钟嘀哒的数目 • 这个计数器是一个 64-位 变量( 即便在 32-位的体系上)并且称为 jiffies_64
获知当前时间 • void do_gettimeofday(struct timeval *tv);
延后执行 • 长延时技术 • 忙等待 • 让出处理器 • 超时 • 短延时技术 • void ndelay(unsigned long nsecs); • void udelay(unsigned long usecs); • void mdelay(unsigned long msecs);