读完LKD之后开始看这本,用了十天,关于内核的章节读得比较仔细,关于驱动的章节就草草一读(实在太枯燥了,以后动手的时候再参考,否则看过也是忘),最后两章直接忽略。
书里介绍了大量开发经验,对一些基础概念却讲解的不够细致,所以对初学者并不是特别友好,很多地方需要来回翻阅,并且对照内核源码仔细分析才能弄懂。
翻译得非常糟糕,有些章节像是机器或者完全不理解内容的外行翻的,许多复合句的逻辑都搞错了,需要自己揣摩纠错。
比如10.2节,p268,“读者不应该使用...除非有足够必要的理由想要在其它中断被禁用的时候运行自己的中断处理例程”应该是 “...想要在运行自己的中断处理例程的时候禁用其它中断”。
像上面这样的错误有很多,这还都是好的,不用查原文也能大致理解。有些就莫名其妙了,比如9.2节,p242,“这种暂停可通过对端口0x80的一条out b指令实现(通常这样做,但很少使用)” 括号里的标注绝对不是正常中国人能理解的。查英文原文是“normally but not always unused”,其实是针对“端口0x80”的(英文里"port 0x80"在句末),说这个端口通常用不到所以可以这么干,不是针对整句的。
上面这两类错误都是比较容易发现的,但也有一些内容错误既神奇又隐蔽,比如7.4节,p197,“内核定时器常常是作为‘软件中断’的结果而运行的”,英文是“kernel timers are run as the result of a 'software interrrupt'”,翻译里多出来的“常常”简直误人子弟。
再比如6.2节,p159,“休眠将会把进程状态置为TASK_RUNNING...”,英文是“The wakeup resets the process state to TASK_RUNNING...”,太神奇了。
有些小错误不神奇,但也很有误导性,像IN变成INT,PCI变成PC什么的,比比皆是。
感觉人与书之间最基本的信任没有了。
===================================
笔记,基本就是一组内容索引,不要在意格式
LDD 1: 设备驱动程序简介
驱动程序应处理如何使硬件可用的问题,而将怎样使用硬件的问题留给上层应用程序(提供机制而非策略)
LDD 2: 构造和运行模块
不同于应用程序,模块退出时必须仔细完成资源释放和其它清理工作
模块在内核空间运行,其中一部分作为系统调用运行在进程上下文中,另一部分负责中断处理
所有内核代码都要考虑并发,都必须是可重入的
内核栈空间小(4K),不要声明大的自动变量;谨慎使用__开头的函数;No浮点运算
装载/卸载/列出模块:insmod/modprobe/rmmod/lsmod;导出符号EXPORT_SYMBOL();支持参数module_param(name, type, perm)
LDD 3: 字符设备驱动程序
设备编号: <types.h>dev_t,宏MAJOR和MINOR从dev_t取主次设备号,MKDEV由主次设备号构造dev_t
分配设备号: <fs.h>register_chrdev_region静态;alloc_chrdev_region动态,Documentation/devices.txt常见静态主设备号
文件操作<fs.h>file_operations;file打开的文件描述符;inode文件(包含cdev指针)
字符设备注册<cdev.h>cdev_alloc申请cdev结构,将其嵌入自己定义的设备结构中,cdev_init将cdev关联一组文件操作file_operations,cdev_add激活设备,cdev_del移除设备
示例scull设备实现文件操作open、release、read和write的方法
在用户空间和内核空间之间拷贝数据: <uaccess.h>copy_from/to_user
LDD 4: 调试技术
内核中的调试选项 CONFIG_DEBUG_KERNEL等
通过打印调试:printk; if(printk_retelimit()); print_dev_t, format_dev_t;查看消息:klogd: /etc/syslog.conf;cat /proc/kmsg
通过查询调试:/proc方法;ioctl方法
strace监视系统调用;SysRq;readprofile/oprofile(启用CONFIG_PROFILING);gdb(启用CONFIG_DEBUG_INFO)
LDD 5: 并发和竞态
信号量<semaphore.h>semaphore结构; sema_init; DECLARE_MUTEX(_LOCKED); init_MUTEX(_LOCKED); down(_interruptible/_trylock); up
RW信号量<rwsem.h>rw_semaphore结构; init_rwsem; down_read(_trylock); up_read; down_write(_trylock); up_write; downgrade_write
Completion<completion.h>struct completion; complete(_all)
自旋锁<spinlock.h>struct spinlock_t; spin_(un)lock(_init/_irqsave/_irq/_bh)拥有自旋锁的代码不能休眠,不能被抢占;若自旋锁可能被中断代码获取,则获取该锁时应禁止中断(_irq/_irqsave),若仅可能被软中断(如tasklet)获取,则应只禁止软中断(_bh)
RW自旋锁<spinlock.h>struct rwlock_t;
规则:提供给外部调用的函数需要锁时均要申请,内部函数可假定调用者已获取锁(避免重复申请导致死锁),但必须显式说明;申请多个锁时,先局部再核心,先信号量再自旋锁
原子变量<atomic.h>atomic_t; <bitops.h>; <seqlock.h>seqlock中写比读具有更高优先级; RCU
LDD 6: 高级字符驱动程序
file_operations中的ioctl方法; 命令编号参考ioctl.h和Documentation/ioctl-number.txt
休眠wait_event(_interruptible+/_timeout)<linux/wait.h>和wake_up(_interruptible),详见笔记
poll, select, epoll三个用于轮询的系统调用均由file_operations中的poll方法支持
file_operations中的fsync实现异步通知:用户程序首先用fcntl系统调用发送F_SETOWN将自己的进程号设置为文件的owner,继而发送F_SETFL在设备中设置FASYNC标志(该步调用fsync方法,fsync利用<fs.h>fsync_helper函数设置fasync_struct)。当设备通过write获得数据时,write检查fasync_struct,若不为空则调用<fs.h>kill_fasync函数向设备的owner进程发送SIGIO信号
file_operations中的llseek方法:若设备不支持llseek,需要在open方法中调用nonseekable_open以通知内核,并可以将llseek方法设置为<fs.h>no_llseek函数
LDD 7: 时间、延迟及延缓操作
时钟中断周期HZ<param.h>内核中HZ可能是50-1200,而用户空间中HZ总是100(相关函数自动换算)
开机以来的时钟中断数jiffies(_64)<jiffies.h>,比较两个时间用time_after/before(_eq)宏
x86时钟周期数rdtsc(l(l))<asm/msr.h>; 体系结构无关版本get_cycles<timex.h>
墙钟时间较少用mktime, do_gettimeofday, current_kernel_time <time.h>
长延迟:set_current_state(TASK_(UN)INTERRUPTIBLE); schedule_timeout(delay_jiffies)<sched.h>schedule_timeout函数仅设置超时提醒并让出处理器,进程状态需要手动改变;超时返回0(无论超多少),否则(被中断)返回剩余延迟数
短延迟: <delay.h>ndelay/udelay/mdelay忙等待; msleep(_interruptible)/ssleep休眠
<hardirq.h>in_interrupt()判断当前是否在中断上下文(软/硬); in_atomic()判断是否在中断上下文或持有自旋锁,后者中虽current可用但也禁止访问用户空间,因为会导致调度
定时器: <timer.h>init_timer/TIMER_INITIALIZER; add_timer; mod_timer; del_timer(_sync)timer_list结构中function的参数data可以是多个参数组成的struct的指针(强制转换为ulong)定时器用于在非阻塞情况下延迟执行任务,任务不在进程上下文中,而是软中断,故必须是原子的,且总在注册它的代码所在的同一CPU上运行(为了获得缓存的局部性),可注册自身
tasklet: <interrupt.h>task_struct结构; tasklet_init, _disable(_nosync), _enable, (_hi)_schedule, _killtasklet除了不指定延迟时间与timer非常类似,但可多次enable/disable(内核对此有个计数),同一tasklet总在同一CPU运行
工作队列: <workqueue.h>workqueue_struct; create(_singlethread)_workqueue; DECLARE/INIT/PREPARE_WORK提交队列的工作work_struct; queue(_delayed)_work; cancel_delayed_work; flush_workqueue; destroy_workqueue工作队列中的任务在内核进程上下文,可休眠(会耽搁同一队列中的其它任务)但不能访问用户空间
共享队列(系统公用的工作队列): <workqueue.h>schedule(_delayed)_work; flush_scheduled_work
LDD 8: 分配内存
kmalloc/kfree: <slab.h>flags可用GFP_KERNEL/_USER或GFP_ATOMIC选择是否接受休眠,size最好小于若干KB,不能超过128KB
Lookaside cache: <slab.h>kmem_cache_t类型; kmem_cache_create/_destroy; 注意destroy前需要释放其中的所有对象,否则会失败cache中包含任意个大小都为size的备用内存对象(防止碎片),可以用kmem_cache_alloc_/_free从中申请/释放对象,使用情况见/proc/slabinfo
按页分配(对内存利用率高且能够完全控制): get_zeroed_page; __get_free_page(s); free_page(s); alloc_page(s); __free_page(s); free_hot/cold_page其中__get_free_pages的第二个参数是申请内存页面数的log2,当需要特定字节数的内存时,可用<asm/page.h>get_order函数计算它
vmalloc/vfree: <vmalloc.h>分配物理不连续的虚拟内存(不推荐);<asm/io.h>ioremap/iounmap将IO设备的物理地址映射到虚拟地址空间
per-CPU变量(每个CPU使用同一变量的不同副本): <percpu.h>DEFINE_PER_CPU; get_cpu_var; put_cpu_var; per_cpu; alloc_percpu; per_cpu_ptr
系统引导时分配内存alloc_bootmem(_low+/_pages): <bootmem.h>获取大块连续内存的唯一方法,不适用于模块,获得的内存无法返还系统
LDD 9: 与硬件通信
内存屏障-保证读写顺序: <kernel.h>barrier(); <asm/system.h>(smp_)rmb/wmb/mb(), set_(r/w)mb(var,value)
声明对IO端口的访问权<ioport.h>request/release/check_region,端口分配见/proc/ioports
读写端口<asm/io.h>8位inb/outb, 16位inw/outw, 32位inl/outl; 串操作insb/outsb(w,l)注意字节序x86中端口数据类型是unsigned short,I/O空间64KB,独立于内存空间
I/O内存: <ioport.h>request/release/check_mem_region, <asm/io.h>ioremap/iounmapioremap返回的地址不能直接引用,而要用<asm/io.h>ioread/write8/16/32(_rep), memset_io, memcpy_fromio/_toio
LDD 10: 中断处理
请求中断通道<sched.h>request/free_irq(可能睡眠); 中断统计见/proc/interrupts和/proc/stat
IRQ可从设备的IO端口或配置空间读取(若设备支持)或用<interrupt.h>probe_irq_on/off函数探测(仅针对非共享IRQ)
request_irq中的dev_id参数在共享中断信号线中用来区分设备id,一般设置为指向设备数据结构的指针,中断产生时该参数会被传递给中断处理程序
禁单个中断<asm/irq.h>disable_irq(_nosync), enable_irq;禁当前CPU所有中断<asm/system.h>local_irq_save/disable/restore/enable
底半部:tasklet或工作队列
共享IRQ:不能用probe_irq_on,不能disable_irq,中断到来时内核会调用注册在该IRQ上的每个中断处理程序并传递各自的dev_id,处理程序必须首先判断是否是自己的设备产生的中断,如不是则立即返回IRQ_NONE
LDD 11: 内核的数据类型
内存地址虽然是指针,但几乎从不对其解引用,故一般用unsigned long类型以防止解引用,该类型在所有Linux支持的平台上都与指针大小相同
接口特定类型xx_t的问题在于打印时不易选择合适的printk/f输出格式,从而在部分平台上引起警告,方法是强制转换为最大可能长度
用__BIG/LITTLE_ENDIAN宏+条件编译处理字节序,或使用cpu_to_le32/le32_to_cpu等宏处理 <asm/byteorder.h>
用<asm/unaligned.h>get/put_unaligned宏访问未对齐数据;定义struct时用__attribute__ ((packed))防止编译器为对齐而自动填充
为了用指针表示除NULL之外更丰富的错误代码,用<err.h>ERR_PTR将错误代码转换为指针,IS_ERR/PTR_ERR判断/取出错误代码
用<list.h>中的list_head结构和相关操作实现任何不需要锁的链表
LDD 12: PCI驱动程序
驱动程序通过pci_dev结构访问PCI设备,不必关心其总线编号、设备编号和功能编号(分别为8/5/3位共16位,在往上还可以分域)
所有PCI设备都包含至少256B地址空间,其中前64B是标准化的,包含vendorID、device、class等
PCI_DEVICE(_CLASS)宏用来创建pci_device_id结构,包括vendor、device、class和driver_data等
PCI驱动的主要结构是pci_driver,其中包含pci_device_id指针,指示驱动支持的设备列表,还有name字段和probe、remove等回调函数
PCI驱动程序在初始化阶段用pci_register_driver完成注册,之后当PCI核心认为有适用于该驱动的pci_dev时会调用probe函数,pci_unregister_driver函数则对每个绑定设备调用remove函数并注销pci_driver结构
(目前少用)pci_get_device/subsys/slot用来查找特定的PCI设备(仅用于进程上下文),之后用pci_enable_device将其激活后方可访问其资源
访问设备配置空间: <pci.h>pci_read/write_config_byte/word/dword; 没有pci_dev结构只有编号时用pci_bus_read/write_config_byte/word/dword
访问设备I/O和内存空间: 驱动程序无需关心其映射方式,只要使用内核提供的pci_resource_start/end/flags函数
PCI中断: 只需读配置空间中的PCI_INTERRUPT_LINE位置即得到中断号
LDD 13: USB驱动程序
USB协议规范定义了存储、键盘、鼠标、手柄、网络和modem等几类设备的标准,若设备遵从该标准则无需专门的驱动,而视频、USB2Serial等设备没有标准,需要厂家自己的驱动
USB驱动分为host驱动和gadget驱动(为了避免混淆所以不叫USB device driver)
usb_host_endpoint: 基本传输单元(单向),分CONTROL INTERRUPT BULK ISOCHRONOUS
usb_interface: 每个USB驱动对应一个interface,包含0~n个endpoint
usb_host_config: 每个usb_device包含若干config(通常为1),每个config包含若干interface
usb hub、设备和interface都被系统作为device,usb设备格式为”root_hub-hub_port-…-hub_port”,interface格式为”设备:config.interface”,如1-1:1.1
sysfs(/sys/devices)中包含上述device,其中bConfigurationValue可写;usbfs(/proc/bus/usb)额外包含endpoint层级和可选的config
通过urb异步传输数据: driver构造urb结构,指定endpoint,递交给USB core,后者将其递交给USB controller,controller完成传输并通知driver
用usb_sndctrlpipe等宏指定endpoint编号和类型构造pipe,pipe包含于urb结构中
urb结构中还包含usb_complete_t结构(函数指针, void (*foo)(struct urb *)),urb传输完成后USB core调用该函数(中断上下文)
建立urb要像申请内存一样:usb_alloc/free_urb, 用usb_fill_int/bulk/control_urb填充, iso手动填充
usb_submit_urb提交urb, usb_kill_urb终止urb, usb_unlink_urb非阻塞终止urb
usb_driver结构中包含driver支持的设备列表(usb_device_id结构数组)、探测函数和卸载函数的指针等
不使用urb的传输:usb_bulk/control_msg,阻塞等待传输完成,不能取消,不能用于中断或持有自旋锁代码
LDD 14: Linux设备模型
kobject初始化:kobject_init引用计数置1; kobject_set_name; 另需设置ktype, kset, parent
kobject_get/put增加/减少引用计数,计数为0时调用release方法,该方法包含在ktype(struct kobj_type)中
kobject所属kset的ktype优先级高于kobj自身的ktype,因此后者经常设为NULL
kset包含子kobj的链表,子kobj包含指向kset的指针;kset本身也是kobj,子kobj的parent指针经常指向所属kset的kobj
kobject/kset_init/add/register/unregister初始化/注册至上一层级并增引用/以上两者/删除并减引用
kobject未必在sysfs中,kset和其子kobject总是在sysfs中
kobject是sysfs中的目录,其属性为文件,属性和操作属性的方法包含在ktype中,属性用sysfs_create/remove_(bin_)file创建/删除,操作方法包含show和store函数
sysfs_create/remove_link在sysfs中创建符号链接,如表示/sys/devices中设备和/sys/bus中驱动的对应关系
device结构需要设置parent, bus_id设备在总线上的id, bus所属总线指针和release函数指针,后者在device内嵌的kobj的release方法中被调用
class; 热插拔; 固件
LDD 15: 内存映射和DMA
内核逻辑地址vs内核虚拟地址: 逻辑地址包含于虚拟地址;逻辑地址一般等价于物理地址(有偏移量);高端内存没有逻辑地址,可以映射到虚拟地址
__pa()将逻辑地址转换为物理地址; PAGE_SIZE页大小; PAGE_SHIFT页偏移量(页大小的位数)
virt/pfn_to_page<page.h>内核逻辑地址/页号转换为page结构指针; page_address<mm.h>page指针转换为内核虚拟地址
kmap/kunmap(_atomic)<highmem.h>低端内存直接给逻辑地址,高端先映射再给虚拟地址
/proc/pid/maps查看VMA,每行对应一个vm_area_struct结构
mm_struct结构<sched.h>包含进程的内存管理信息,用current->mm访问,多个进程可共享(用以实现线程)
mmap是file_operations中需要定义的方法,用于实现mmap系统调用(若未定义返回-ENODEV),mmap方法参数为file指针和VMA指针,VMA由内核创建,包含mmap系统调用中指定的地址信息,mmap方法的任务是进行内存映射、指定VMA的vm_operations_struct(包含open, close, nopage等方法)指针并调用VMA的open方法
remap_pfn_range指定要映射的物理地址页号、映射到的虚拟地址范围和VMA指针进行内存映射(建立页表)
当指定了VMA的nopage方法时,mmap方法可不必显式进行内存映射;nopage为NULL时内核会映射零内存页;用mremap系统调用扩展VMA时也可能调用nopage
直接I/O访问:get_user_pages<mm.h>
在底层分配DMA缓冲区非常困难,建议使用内核提供的体系架构无关的通用DMA层<dma-mapping.h>
若设备不支持32位DMA操作,应先调用dma_set_mask通知内核并检查返回值
一致性DMA映射(可同时被CPU和设备访问):pci_alloc/free_coherent
DMA池用于小型多次的一致性DMA映射:dma_pool_create/destroy, dma_pool_alloc/free
流式DMA映射(须指定方向):dma_(un)map_single/page
分散/聚集DMA映射(将多个流式DMA缓冲区组合成一个操作)<scatterlist.h>:dma_(un)map_sg; sg_dma_address/len
流式DMA映射不能同时被CPU和设备访问,需要用dma_sync_(sg_)for_cpu/device
LDD 16: 块设备驱动程序
块block大小与体系结构和文件系统相关,通常是4096B;扇区sector在内核中总是512B,但设备中可能有其它定义
注册块设备驱动<fs.h>: (un)register_blkdev
块设备操作block_device_operations,包括open,release,ioctl,media_changed,revalidate_disk
<genhd.h>gendisk结构表示磁盘,add_disk激活磁盘(在驱动就绪后),del_gendisk卸载磁盘
请求队列<blkdev.h>, drivers/block/ll_rw_block.c, elevator.c: request结构,bio结构