500 likes | 711 Views
Linux 操作系统分析与实践 第七讲: Linux 驱动程序编写基础. 《 Linux 操作系统分析与实践 》 课程建设小组 北京大学 二零零八年春季 *致谢:感谢 Intel 对本课程项目的资助. 本讲主要内容. Linux 内核模块 中断和中断处理 下半部. Linux 内核模块. Linux 操作系统的内核是单一体系结构( monolithic kernel ) 有了模块机制后, 提高 Linux 操作系统的可扩充性 ,内核编程不再是一个恶梦 什么是模块呢?
E N D
Linux操作系统分析与实践第七讲: Linux驱动程序编写基础 《Linux操作系统分析与实践》课程建设小组 北京大学 二零零八年春季 *致谢:感谢Intel对本课程项目的资助
本讲主要内容 • Linux内核模块 • 中断和中断处理 • 下半部
Linux内核模块 • Linux操作系统的内核是单一体系结构(monolithic kernel) • 有了模块机制后,提高Linux操作系统的可扩充性,内核编程不再是一个恶梦 • 什么是模块呢? • 模块的全称是“动态可加载内核模块”(Loadable Kernel Module,LKM) • 模块在内核空间运行 • 模块实际上是一种目标对象文件 • 没有链接,不能独立运行,但是其代码可以在运行时链接到系统中作为内核的一部分运行或从内核中取下,从而可以动态扩充内核的功能 • 这种目标代码通常由一组函数和数据结构组成
Linux内核模块的优点与缺点 • 优点 • 使得内核更加紧凑和灵活 • 修改内核时,不必全部重新编译整个内核。系统如果需要使用新模块,只要编译相应的模块,然后使用insmod将模块装载即可 • 模块的目标代码一旦被链接到内核,它的作用域和静态链接的内核目标代码完全等价 • 缺点 • 由于内核所占用的内存是不会被换出的,所以链接进内核的模块会给整个系统带来一定的性能和内存利用方面的损失; • 装入内核的模块就成为内核的一部分,可以修改内核中的其他部分,因此,模块的使用不当会导致系统崩溃; • 为了让内核模块能访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改符号表; • 模块会要求利用其它模块的功能,所以,内核要维护模块之间的依赖性.
Linux内核模块与应用程序的区别 C语言程序 Linux内核模块 运行 用户空间 内核空间 入口 main() module_init()指定; 出口 无 module_exit()指定; 编译 gcc –c Makefile 连接 ld insmod 运行 直接运行 insmod 调试 gdb kdbug, kdb, kgdb等
模块相关命令 • insmod <module.ko> [module parameters] • Load the module • 注意,只有超级用户才能使用这个命令 • Rmmod • Unload the module • lsmod • List all modules loaded into the kernel • 这个命令和cat /proc/modules等价 • modprobe [-r] <module name> • – Load the module specified and modules it depends
模块依赖 • 一个模块A引用另一个模块B所导出的符号,我们就说模块B被模块A引用。 • 如果要装载模块A,必须先要装载模块B。否则,模块B所导出的那些符号的引用就不可能被链接到模块A中。这种模块间的相互关系就叫做模块依赖。
最简单的内核模块例子 #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> static int __init hello_init(void) { printk(KERN_INFO "Hello world\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye world\n"); } module_init(hello_init); module_exit(hello_exit);
static int __init hello_init(void) • static void __exit hello_exit(void) • Static声明,因为这种函数在特定文件之外没有其它意义 • __init标记, 该函数只在初始化期间使用。模块装载后,将该函数占用的内存空间释放 • __exit标记 该代码仅用于模块卸载。 • Init/exit • 宏:module_init/module_exit • 声明模块初始化及清除函数所在的位置 • 装载和卸载模块时,内核可以自动找到相应的函数 module_init(hello_init); module_exit(hello_exit);
编译内核模块 • Makefile文件 obj-m := hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean • Module includes more files obj-m:=hello.o hello-objs := a.o b.o
装载和卸载模块 • 相关命令 • lsmod • insmod hello.ko • rmmod hello.ko
模块参数传递 • 有些模块需要传递一些参数 • 参数在模块加载时传递 #insmod hello.ko test=2 • 参数需要使用module_param宏来声明 module_param的参数:变量名称,类型以及访问许可掩码 • 支持的参数类型 Byte, short, ushort, int, uint, long, ulong, bool, charp Array (module_param_array(name, type, nump, perm))
#include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/moduleparam.h> static int test; module_param(test, int, 0644); static int __init hello_init(void) { printk(KERN_INFO “Hello world test=%d \n” , test); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye world\n"); } MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Test"); MODULE_AUTHOR("xxx"); module_init(hello_init); module_exit(hello_exit);
导出符号表 • 如果一个模块需要向其他模块导出符号(方法或全局变量),需要使用: EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name); *注意:符号必须在模块文件的全局部分导出,不能在函数部分导出。更多信息可参考 <linux/module.h>文件 • Modules仅可以使用由Kernel或者其他Modules导出的符号不能使用Libc • /proc/kallsyms 可以显示所有导出的符号
内核模块操作/proc文件 • /proc文件系统,这是内核模块和系统交互的两种主要方式之一。 • /proc文件系统也是Linux操作系统的特色之一。 • /proc文件系统不是普通意义上的文件系统,它是一个伪文件系统。 • 通过/proc,可以用标准Unix系统调用(比如open()、read()、write()、 ioctl()等等)访问进程地址空间 • 可以用cat、more等命令查看/proc文件中的信息。 • 用户和应用程序可以通过/proc得到系统的信息,并可以改变内核的某些参数。 • 当调试程序或者试图获取指定进程状态的时候,/proc文件系统将是你强有力的支持者。通过它可以创建更强大的工具,获取更多信息。
/proc相关函数 • create_proc_entry()创建一个文件 • proc_symlink()创建符号链接 • proc_mknod()创建设备文件 • proc_mkdir()创建目录 • remove_proc_entry()删除文件或目录
Linux2.6内核中有关模块部分的改变 • 模块引用计数器 Linux2.4中在linux/module.h中定义了三个宏来维护实用计数: __MOD_INC_USE_COUNT 当前模块计数加一__MOD_DEC_USE_COUNT当前模块计数减一__MOD_IN_USE 计数非0时返回真 在Linux2.6中,模块引用计数器由系统自动维护,所以程序中有关这些宏都可以注释掉。 • 关于符号导出列表(list of exported symbols)Linux2.4中会用EXPORT_NO_SYMBOLS宏,来表示不想导出任何变量或函数。 在Linux2.6中这个宏也已经消失。系统默认为不导出任何变量或函数。
模块程序编译方法的改变Linux2.4中命令为: gcc –Wall –DMODULE –D__KERNEL__ -DLINUX –c 源文件名.c其中:__KERNEL__: 即告诉头文件这些代码将在内核模式下运行MODULE: 即告诉头文件要给出适当的内核模块的定义LINUX: 并非必要-Wall: 显示所有warning信息。 • Linux2.6中必须写makefile。通过make命令编译程序。Linux2.6中makefile的写法:(以helloworld为例)//Makefile obj-m += hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean * 注意:all下一行需要有一个“Tab”键,不要写成空格或略去,不然系统无法识别,报错:nothing to be done for “all”。
用以加载的目标文件类型已经改变 • Linux2.4中用以加载为模块的目标文件扩展名为.o • Linux2.6中中用以加载为模块的目标文件扩展名为.ko • 在Linux2.4中,函数init_module()和函数cleanup_module()是必不可少的;而在Linux2.6 中,同样功能的函数并非一定要起这两个函数名。
Lab 模块编程 Write Two Module: • Module1: Hello World Module. Load/unload the module can output some info. • Module2: Module accepts a parameter. Load the module, output the parameter's value. • Modify Module1 and Module2, let Module1 exports symbols, Module2 use the symbols . *注:详见实验指导
二、中断和中断处理程序 • 中断处理的基本过程 • When receiving an interrupt, CPU program counter jumps to a predefined address (interrupt vectors) • The state of interrupted program is saved • The corresponding service routine is executed • The interrupting component is served, and interrupt signal is removed • The state of interrupted program is restored • Resume the interrupted program at the interrupted address
中断描述符表IDT • 中断描述符表是一个系统表,它与每一个中断或者异常向量相联系 • 每个向量在表中有相应的中断或者异常处理程序的入口地址。 • 每个描述符8个字节,共256项,占用空间2KB • 内核在允许中断发生前,必须适当的初始化IDT • CPU的idtr寄存器指向IDT表的物理基地址 • Interrupt vectors on x86
初始化IDT • Linux内核在系统的初始化阶段要初始化可编程控制器8259A; • 将中断描述符表的起始地址装入IDTR寄存器, • 并初始化表中的每一项 • 当计算机运行在实模式时 • IDT被初始化,并由BIOS使用 。 • 真正进入了Linux内核 • IDT就被移到内存的另一个区域,并为进入保护模式进行预初始化
中断处理程序 • 注册中断处理程序 int request_irq( unsigned int irq, irq_handler_t *handler, long irqflags,const char* devname, void *dev_id) • 释放中断处理程序 int free_irq( unsigned int irq, void *dev_id) • 编写中断处理程序 int irqreturn_t handler(int irq, void *dev_id, struct pt_regs *regs); • 共享的中断处理程序 • register_irq() with SA_SHIRQ flag • The registration fails if other handler already register the same IRQ without SA_SHIRQ flag • The dev_id argument must be unique to each handler • The interrupt handler must be able to find out whether its device actually generate an interrupt • Hardware must provide a status register for inquiry
中断上下文 • 当执行中断处理程序或下半部时,内核处于中断上下文 • 中断上下文不同于进程上下文 • 中断或异常处理程序执行的代码不是一个进程 • 它是一个内核控制路径,代表了中断发生时正在运行的进程执行,作为一个进程的内核控制路径,中断处理程序比一个进程要“轻”(中断上下文只包含了很有限的几个寄存器,建立和终止这个上下文所需要的时间很少) • 中断上下文不可以睡眠,也不能调用某些函数,具有较为严格的时间限制
下半部及推后执行的工作 • 中断处理程序的局限 • 中断处理程序必须非常快速结束来避免打断其他重要代码的执行 • 中断实时任务或其他中断处理程序 • 当前IRQ被屏蔽或者CPU上所有的IRQ被屏蔽(如果设置了SA_INTERRUPT ) • 运行在中断上下文(不是运行在进程上下文),不能被阻塞 • 解决办法:中断处理划分为上半部分和下半部分 • 上半部: (中断处理程序)内核立即执行 • Simple and fast, dealing with time-critical hardware tasks • E.g. packets transmission and receiving • 下半部:留着稍后处理 • Deferring work to a later point where interrupts can be enabled • Processing time-consuming and maybe software-only tasks • E.g. network protocols processing
下半部 外部设备 TIMER_BH 定时器 TQUEUE_BH 周期性任务队列 SERIAL_BH 串行端口 IMMEDIATE_BH 立即任务队列 下半部的环境 • 下半部可以通过多种机制实现,分别由不同的接口和子系统组成 • BH接口 静态创建 由32 个 Bottom half组成的链表 • Taskqueue 任务队列 机制 • 软中断 • Tasklet • 工作队列 • BH 2.6中去除 • task queue(任务队列) 2.6中去除 • 软中断(softirq) 2.4引入 • Tasklet 2.4引入 • 工作队列(work queue) 2.4引入
SoftIRQ(软中断) • 在编译期间静态分配的 • Only 32 softIRQs can exist • only 6 currently used. • 由softirq_action结构表示 <linux/interrupt.h>: struct softirq_action { void (*action)(struct softirq_action *); //待执行的函数 void *data; //传给函数的参数 }; • <kernel/softirq.c>中 定义了一个包含32个该结构体的数组 static struct softirq_action softirq_vec[32];
6个当前使用的SoftIRQs • include/linux/interrupt.h 109 enum 110 { 111 HI_SOFTIRQ=0, 112 TIMER_SOFTIRQ, 113 NET_TX_SOFTIRQ, 114 NET_RX_SOFTIRQ, 115 BLOCK_SOFTIRQ, 116 TASKLET_SOFTIRQ 117 };
软中断处理程序 • 注册 软中断处理程序(kernel/softirq.c) 205 void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) 206 { 207 softirq_vec[nr].data = data; 208 softirq_vec[nr].action = action; 209 } • 当软中断处理程序运行时,当前处理器上的软中断被禁止。其它处理器仍可以执行别的软中断。 • 引入软中断的原因就是其可扩展性。如果不需要扩展到多个处理器,那么就使用tasklet. • 软中断不能睡眠。
raise_softirq • 调用open_softirq()进行注册后,新的软中断就可以运行了。 • 调用raise_softirq()可以将一个软中断设置为挂起状态,使它在下一次调用do_softirq()函数投入运行。
do_softirq • 在下列地方,待处理的软中断会被检查和执行 • 从一个硬件中断代码处返回时 • 在ksoftirqd内核线程中 • 在那些显式检查和执行待处理的软中断的代码中。 • 无论什么方式,软中断都要在do_softirq()中执行。 • 遍历每一个软中断,调用他们的处理程序。
Tasklets • Tasklets 是利用软中断实现的一种下半部机制。 • Tasklet结构体 struct tasklet_struct{ struct tasklet_struct *next; /* 队列指针 */ unsigned long state; /* tasklet的状态*/ atomic_t count; /* 引用计数,通常用1表示disabled */ void (*func)(unsigned long); /* 函数指针 */ unsigned long data; /* func(data) */};
Tasklet实现的软中断向量表 • 在软中断中相关的向量: • softirq_vec[HI_SOFTIRQ] • softirq_vec[TASKLET_SOFTIRQ] • 触发(激活、调度)tasklet: • HI: tasklet_hi_schedule() • TASKLET: tasklet_schedule() • 通过do_softirq()调度tasklet的运行 • HI action: tasklet_hi_action() • TASKLET action: tasklet_action()
使用tasklet • 大多数情况下,tasklet机制是实现下半部的最佳选择 • 编写tasklet处理程序 • 声明tasklet • 调度tasklet • 编写tasklet处理程序 • 定义一个小任务的处理函数 • 并把用户的代码写到其中。 void my_tasklet_fun(unsigned long) { 用户代码; }
声明takslet • 使用DECLARE_TASKLET()宏 DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); • 调用tasklet_schedule()函数 • 系统会在适当的时候调度并运行这个tasklet. tasklet_schedule(&my_tasklet);
工作队列(work queue) • 工作队列使用内核线程来执行驱动中需延迟执行的工作(Bottom Half), 这些内核线程被称为工作线程。 • 在Linux2.6 内核中, 系统除了提供默工作认线程来帮助驱动方便的执行延迟操作,还允许驱动自己产生自定义的工作线程来执行某些特殊的延迟工作。 • 默认工作线程被称作events/n, 其中n 为CPU 个数, 也就是说, 每个CPU 都有一个默认工作线程。 • 一般情况下, 大多数驱动都使用默认工作线程来执行自己的Bottom- Half 工作。 • 某些情况下, 驱动产生自己的自定义工作线程可以满足更高的性能要求, 并可以减轻默认工作线程的负担。
工作线程 • 每种类型的工作线程都有一个这样的结构与其关联。 • 它有一个重要的成员CPU_wq, 即元素类型为CPUworkqueue_struct 的数组, 表示系统中的每个CPU 都有自己的工作线程 • 假设需要在有2 个CPU 的计算机上创建类型为myworker 的工作线程, 则系统除了有2 个类型为events 的默认工作线程外, 还有2 个类型为myworker 的工作线程, 每一个events 或myworker 都有一个CPU_workqueue_struct 结构与之关联。 struct workqueue_struct { struct CPU_workqueue_struct CPU_wq[NR_CPUS]; const char *name; struct list_head list; }
struct CPU_workqueue_struct { spinlock_t lock; /* lock protecting this structure */ long remove_sequence; /* least- recently added (next to run) */ long insert_sequence; /* next to add */ struct list_head worklist; /*该CPU 上的所需处理的工作队列*/ wait_queue_head_t more_work; wait_queue_head_t work_done; struct workqueue_struct *wq; /*所属的workqueue_struct,*/ task_t *thread; /*所关联的工作线程,*/ int run_depth; /* run_workqueue() recursion depth */ }
工作单元 • 每个CPU 的同一类型工作单元被连接成一个工作队列 • work_struct用来表示每一个需要被延迟处理的工作单元。 struct work_struct { unsigned long pending; /* is this work pending? */ struct list_head entry; /* link list of all work */ void (*func)(void *); /* handler function */ void *data; /* argument to handler */ void *wq_data; /* used internally */ struct timer_list timer; /* timer used by delayed work queues */ }
worker_thread( )线程函数 • 所有的工作线程都是普通内核线程,使用worker_thread( )作为线程函数 • 该线程函数在进行一段初始化操作后便进入无限循环, 并睡眠等待。 • 当有需处理的延迟工作被加入到工作队列中时, 该线程函数被唤醒并循环处理对应工作队列中的每一个工作单元; • 当工作队列中没有工作单元需要被处理时, 它又重新进入睡眠状态, 等待下一次的唤醒。 • 主要任务就是遍历工作队列上所有需要被处理的工作单元, 如果队列非空, 则调用run_workqueue( )处理具体的延迟工作。 • 通过list_entry 取得每一个工作单元的work_struct 结构, 并以work- >data 为参数调用其具体处理函数work- >func( )。 • 阅读代码 /kernel/workqueue.c?v=2.6.17.13#L188
使用工作队列 • 驱动为需要延迟处理的工作建立一work_struct 结构, 该结构即为工作单元, 它还包含一函数指针用来处理具体的延迟工作; • 该工作单元被添加到当前CPU 的默认工作线程或自定义工作线程的工作队列中等待处理 • 在某一时刻, 工作线程被唤醒, 它将循环处理工作队列中的每一个工作单元。
使用系统中的默认工作队列 • 首先, 要为需要延迟处理的工作单元建立一个work_struct 结构 • 内核提供了下面2 个宏来方便地建立该结构: • DECLARE_WORK(name, void (*func)(void *), void *data); //静态创建 • INIT_WORK (struct work_struct *work, void (*func)(void *),void *data) //动态初始化 • 将该结构放入到默认工作线程的工作队列中去, 内核提供以下2 个宏操作: • schedule_work(work); • schedule_delayed_work(work, delay)。 • 这2 个宏操作的主要区别就在于一个是立即被调度, 一个是延迟delay 个时钟周期后被调度。
使用自定义工作队列 • 创建工作线程使用 struct workqueue_struct *create_workqueue (const char *name)。 • 其中, name 为该类型工作工作线程的名字 • 创建类型为myworker的工作线程的代码如下: struct workqueue_struct *myworker; myworker = create_workqueue( “myworker”)。 • 将work_struct 加入到自定义工作线程的工作队列中可以采用以下接口: • int queue_work(struct workqueue_struct *wq, struct work_struct *work); • int queue_delayed_work (struct workqueue_struct *wq,struct work_struct *work,unsigned long delay)。
Q&A 本讲结束 !