当前位置:首页 > 芯闻号 > 充电吧
[导读]linux设备驱动读书笔记 设备驱动简介 机制:提供什么能力 策略:如何使用这些能力 在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求

linux设备驱动读书笔记
设备驱动简介 机制:提供什么能力
策略:如何使用这些能力
在编写驱动时, 程序员应当编写内核代码来存取硬件, 但是不能强加特别的策略给用户, 因为不同的用户有不同的需求. 驱动应当做到使硬件可用, 将所有关于如何使用硬件的事情留给应用程序
编写驱动需要注意的地方:
必须注意并发/重入的问题
内核空间和用户空间不能直接操作,必须通过特别的函数(copy_from_user/copy_to_user)来操作
内核线程只有一个非常小的堆栈; 它可能小到一个4096 字节的页. 驱动模块的函数必须与内核函数共享这个堆栈. 因此, 声明一个巨大的自动变量不是一个好主意; 如果需要大的结构, 应当在调用时动态分配.
以双下划线(__)开始的函数通常是一个低层的接口组件, 应当小心使用. 本质上讲, 双下划线告诉程序员:" 如果你调用这个函数, 确信你知道你在做什么."
内核代码不能做浮点算术
每个进程的系统栈空间分配的大小为2个连续的物理页面(通常来讲是8K),而task_struct占了大约1K(在栈的底部), 所以系统空间非常有限,在中断/软中断/驱动程序中不允许嵌套太深或使用大量局部变量
编写缺少进程上下文的函数需要注意:
不允许存取用户空间. 因为没有进程上下文, 没有和任何特定进程相关联的到用户空间的途径.
current指针在原子态没有意义, 并且不能使用因为相关的代码没有和已被中断的进程的联系.
不能进行睡眠或者调度. 原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数. 例如, 调用 kmalloc(..., GFP_KERNEL) 是违犯规则的. 旗标也必须不能使用因为它们可能睡眠.


重要的数据结构的重要成员
struct task_struct {(得到当前进程task_struct结构指针的宏为: current) volatile long state: 进程状态. -1 unrunnable; 0 runnable; >0 stopped
mm_segment_t addr_limit: 线程地址空间: 0-0xBFFFFFFF for user-thead; 0-0xFFFFFFFF for kernel-thread
struct mm_struct *mm: 虚存管理与映射相关信息,是整个用户空间的抽象
unsigned long sleep_time:
pid_t pid: 进程pid
uid_t uid,euid,suid,fsuid:
gid_t gid,egid,sgid,fsgid:
gid_t groups[NGROUPS]:
kernel_cap_t cap_effective, cap_inheritable, cap_permitted:
int keep_capabilities:1:
struct user_struct *user:
char comm[16]: 命令名称. 由当前进程执行的程序文件的基本名称( 截短到 15 个字符, 如果需要 )
struct tty_struct *tty:
unsigned int locks:
struct rlimit rlim[RLIM_NLIMITS]: 当前进程各种资源分配的限制, 如current->rlim[RLIMIT_STACK]是对用户空间堆栈大小的限制
struct files_struct *files: 打开的文件
};
struct file_operations{
struct module *owner;是一个指向拥有这个结构的模块的指针. 这个成员用来当模块在被使用时阻止其被卸载. 一般初始化为: THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int);用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char *, size_t, loff_t *);从设备中获取数据. 空指针导致read系统调用返回-EINVAL("Invalid argument") . 非负返回值代表了成功读取的字节数
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);发送数据给设备. 空指针导致write 系统调用返回-EINVAL. 非负返回值代表成功写的字节数.
unsigned int (*poll) (struct file *, struct poll_table_struct *);3 个系统调用的后端: poll, epoll, 和 select. 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);提供了发出设备特定命令的方法. 注意:有几个 ioctl 命令被内核识别而不会调用此方法.
int (*mmap) (struct file *, struct vm_area_struct *);请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL,系统调用返回 -ENODEV.
int (*open) (struct inode *, struct file *);open一个设备文件. 如果这个项是 NULL, 设备打开一直成功
int (*release) (struct inode *, struct file *);在文件结构被释放时引用这个操作. 即在最后一个打开设备文件的文件描述符关闭时调用(而不是每次close时都调用)
int (*fsync) (struct file *, struct dentry *, int datasync);fsync系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.
int (*fasync) (int, struct file *, int);通知设备它的 FASYNC 标志(异步通知)的改变. 这个成员可以是NULL 如果驱动不支持异步通知.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);包含多个内存区的单个读操作; 如果为 NULL, read方法被调用( 可能多于一次 ).
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);包含多个内存区的单个写操作; 如果为 NULL, write方法被调用( 可能多于一次 ).
}
struct file{
struct dentry *f_dentry;关联到文件的目录入口( dentry )结构. 设备驱动不需要关心, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.
struct file_operations *f_op;和文件关联的操作. 可改变之, 并在返回后新方法会起作用. 例如, 关联到主编号1 (/dev/null, /dev/zero...)的open根据打开的次编号来更新filp->f_op
unsigned int f_flags;文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作
mode_t f_mode;文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 检查是有内核做的,所以驱动里不需要再次检查
loff_t f_pos;当前读写位置. 驱动可以读这个值, 但是正常地不应该改变它; 读和写应当使用它们的最后一个参数来更新一个位置. 一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
void *private_data; 可自由使用或者忽略它.
}
struct inode{
kdev_t i_rdev;对于代表设备文件的节点, 这个成员包含实际的设备编号. 需要这两个函数来操作: unsigned int iminor(struct inode *inode)/unsigned int imajor(struct inode *inode)
struct char_device *i_cdev; 内核的内部结构, 代表字符设备. 当节点是一个字符设备文件时, 这个成员包含一个指针, 指向这个结构 }

建立和运行模块
下图展示了函数调用和函数指针在模块中如何使用来增加新功能到一个运行中的内核.
编译和加载
内核版本的问题
linux/version.h中有下面的宏定义:

UTS_RELEASE

这个宏定义扩展成字符串, 描述了这个内核树的版本. 例如, "2.6.10".

LINUX_VERSION_CODE

这个宏定义扩展成内核版本的二进制形式, 版本号发行号的每个部分用一个字节表示. 例如, 2.6.10 的编码是 132618 ( 就是, 0x02060a ). [4]有了这个信息, 你可以(几乎是)容易地决定你在处理的内核版本.

KERNEL_VERSION(major,minor,release)

这个宏定义用来建立一个整型版本编码, 从组成一个版本号的单个数字. 例如, KERNEL_VERSION(2.6.10) 扩展成 132618. 这个宏定义非常有用, 当你需要比较当前版本和一个已知的检查点.

特殊GNU make变量名
obj-m := module.o #最终模块名
module-objs := file1.o file2.o #最终模块用到的obj列表
make命令中的"M="选项使 makefile 在试图建立模块目标(obj-m 变量中指定的)前, 回到你的模块源码目录
Makefile示例:
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
加载卸载:
insmod:仅加载指定的模块)
modprobe:加载指定的模块及其相关模块. 它查看要加载的模块, 看是否它引用了当前内核没有定义的符号. 如果未定义的symbols, modprobe 在模块搜索路径(/etc/modprobe.conf)中寻找并加载其他定义了symbols的模块
rmmod: 从内核中去除指定模块
lsmod: 打印内核中当前加载的模块的列表. 通过读取/proc/modules或/sys/module 的 sysfs 虚拟文件系统工作
错误信息:
unresolved symbols: 加载是找不到symbols. 可能是使用了未定义的symbols; 也可能是需要使用modprobe试一下
-1 Invalid module format:编译模块用的内核源代码版本与当前运行的内核的版本不匹配

调试技术
内核的config配置
kernel hacking菜单
CONFIG_DEBUG_KERNEL:
这个选项只是使其他调试选项可用; 它应当打开, 但是它自己不激活任何的特性.
CONFIG_DEBUG_SLAB:
这个重要的选项打开了内核内存分配函数的几类检查. 激活这些检查, 就可能探测到一些内存覆盖和遗漏初始化的错误.被分配的每一个字节在递交给调用者之前都设成 0xa5, 随后在释放时被设成 0x6b.内核还会在每个分配的内存对象的前后放置特别的守护值; 如果这些值曾被改动, 内核知道有人已覆盖了一个内存分配区.
CONFIG_DEBUG_PAGEALLOC:
满的页在释放时被从内核地址空间去除(full pages are removed from the kernel address space when freed)(?). 这个选项会显著拖慢系统, 但是它也能快速指出某些类型的内存损坏错误.
CONFIG_DEBUG_SPINLOCK
激活这个选项, 内核捕捉对未初始化的自旋锁的操作, 以及各种其他的错误( 例如 2 次解锁同一个锁 ).
CONFIG_DEBUG_SPINLOCK_SLEEP
这个选项激活对持有自旋锁时进入睡眠的检查. 实际上, 如果你调用一个可能会睡眠的函数, 它就发出警告, 即便这个有疑问的调用没有睡眠.
CONFIG_INIT_DEBUG
用__init (或者 __initdata) 标志的项在系统初始化或者模块加载后都被丢弃. 这个选项激活了对代码的检查, 这些代码试图在初始化完成后存取初始化时内存.
CONFIG_DEBUG_INFO
这个选项使得内核在建立时包含完整的调试信息. 如果你想使用 gdb 调试内核, 你将需要这些信息. 如果你打算使用 gdb, 你还要激活 CONFIG_FRAME_POINTER.
CONFIG_MAGIC_SYSRQ
激活"魔术 SysRq"键.
CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE
这些选项能帮助跟踪内核堆栈溢出. 堆栈溢出的确证是一个 oops 输出, 但是没有任何形式的合理的回溯. 第1个选项给内核增加了明确的溢出检查; 第 2 个使得内核监测堆栈使用并作一些统计, 这些统计可以用魔术 SysRq 键得到.
CONFIG_KALLSYMS
这个选项(在"Generl setup/Standard features"下)使得内核符号信息建在内核中; 缺省是激活的. 符号选项用在调试上下文中; 没有它, 一个 oops 列表只能以 16 进制格式给你一个内核回溯, 这不是很有用.
CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC
这些选项(在"Generl setup"菜单)使得完整的内核配置状态被建立到内核中, 可以通过 /proc 来使其可用. 大部分内核开发者知道他们使用的哪个配置, 并不需要这些选项(会使得内核更大). 但是如果你试着调试由其他人建立的内核中的问题, 它们可能有用.
Power management/ACPI菜单
CONFIG_ACPI_DEBUG
这个选项打开详细的 ACPI (Advanced Configuration and Power Interface) 调试信息, 如果你怀疑一个问题和 ACPI 相关可能会用到 Device drivers菜单
CONFIG_DEBUG_DRIVER
打开了驱动核心的调试信息, 可用以追踪低层支持代码的问题. Device drivers/SCSI device support菜单
CONFIG_SCSI_CONSTANTS
建立详细的 SCSI 错误消息的信息. 如果你在使用 SCSI 驱动, 你可能需要这个选项. Device drivers/Input device support菜单
CONFIG_INPUT_EVBUG
如果你使用一个输入设备的驱动, 这个选项可能会有用. 然而要小心这个选项的安全性的隐含意义: 它记录了你键入的任何东西, 包括你的密码.
Profiling support菜单
CONFIG_PROFILING
剖析通常用在系统性能调整, 但是在追踪一些内核挂起和相关问题上也有用.
strace 命令
显示所有的用户空间程序发出的系统调用. 并以符号形式显示调用的参数和返回值.当一个系统调用失败, 错误的符号值(例如, ENOMEM)和对应的字串(Out of memory) 都会显示.
-t: 来显示每个系统调用执行的时间
-T: 来显示调用中花费的时间
-e: 来限制被跟踪调用的类型
-o: 来重定向输出到一个文件. 缺省地, strace 打印调用信息到 stderr.

GDB调试
调试内核:
gdb /usr/src/linux/vmlinux /proc/kcore

第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage

第二个参数是核心文件的名子.

注意事项:
无法检查module的相关内容
不能修改内核数据, 不能单步,不能设置断点
读到的是内核即时映象,内核仍在运行,所以有些数据可能会与即时值不匹配--刷新映象:core-file /proc/kcore
调试模块(内核版本2.6.7 以上)

Linux 可加载模块是 ELF 格式的可执行映象;ELF被分成几个sections. 其中有 3 个典型的sections与调试会话相关:

.text

这个节包含有模块的可执行代码. 调试器必须知道在哪里以便能够给出回溯或者设置断点.

.bss

在编译时不初始化的任何变量在 .bss 中

.data

在编译时需要初始化的任何变量在 .data 里.

为了gdb能够调试可加载模块需要通知调试器一个给定模块的各个sections加载在哪里. 这个信息在 /sys/module/module_name/sections下. 包含名子为 .text , .bss, .data等文件; 每个文件的内容是那个section的基地址.

gdb的add-symbol-file命令用来加载模块相关信息

add-symbol-file 模块名 text所在的基地址 -s .bss bss所在基地址 -s .data data所在基地址

add-symbol-file ../scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836be0


KDB调试
KDB是来自oss.sgi.com的一个非官方补丁. 应用KDB时不应该运行任何程序, 特别的, 不能打开网络. 一般地以单用户模式启动系统
进入KDB:
Pause(或者 Break) 键启动调试器
一个内核 oops(异常?) 发生时
命中一个断点时
命令:
bp function_name
在下一次内核进入function_name时停止
bt
打印出调用回溯中每个函数的参数
mds variable_name
mds address
查看变量/内存数据
mm address value
将value赋给address所指向的内存
......



内核中的数据类型
不同体系结构下各个类型的大小
arch Size: char short int long ptr long-long u8 u16 u32 u64
i386 1 2 4 4 4 8 1 2 4 8
alpha 1 2 4 8 8 8 1 2 4 8
armv4l 1 2 4 4 4 8 1 2 4 8
ia64 1 2 4 8 8 8 1 2 4 8
m68k 1 2 4 4 4 8 1 2 4 8
mips 1 2 4 4 4 8 1 2 4 8
ppc 1 2 4 4 4 8 1 2 4 8
sparc 1 2 4 4 4 8 1 2 4 8
sparc64 1 2 4 4 4 8 1 2 4 8
x86_64 1 2 4 8 8 8 1 2 4 8
应当安排有明确类型大小的数据类型, 如u8, u16, ... uint8_t, uint16_t, ...
接口特定的类型, 请参考原文
其他移植性问题:
Tick: HZ
页大小: PAGE_SIZE
页偏移: PAGE_SHIFT
字节序:
条件编译
#include
#ifdef __BIG_ENDIAN
......
#endif
#ifdef __LITTLE_ENDIAN
......
#endif
转换
#include
#include u32 cpu_to_le32 (u32);
u32 le32_to_cpu (u32);
cpu_to_le16/le16_to_cpu/cpu_to_le64/....
cpus_to_le16/le16_to_cpus/cpus_to_le64/....
带's'后缀的是有符号版
数据对齐:
存取不对齐的数据应当使用下列宏
#include
get_unaligned(ptr);
put_unaligned(val, ptr);
指针返回值:
有时候内核函数会返回已编码的指针值来指示错误, 这类返回值是否有效的测试应当使用下列宏
void *ERR_PTR(long error); 将错误码转换成指针形式
long IS_ERR(const void *ptr); 判断一个返回值是否有效
long PTR_ERR(const void *ptr); 提取返回的错误码, 在提取前需要判断返回值是否有效
链表:
鼓励使用内核自带的struct list_head结构来构造双向链表
#include
struct list_head { struct list_head *next, *prev; };
LIST_HEAD(struct list_head);
编译时初始化
INIT_LIST_HEAD(struct list_head*)
运行时初始化
list_add(struct list_head *new, struct list_head *head);
在head后链入new. 常用来构造FILO
list_add_tail(struct list_head *new, struct list_head *head);
在head前链入new, 常用来构造FIFO
list_del(struct list_head *entry);
把entry从链表中脱链
list_del_init(struct list_head *entry);
把entry从链表中脱链并重新初始化entry
list_move(struct list_head *entry, struct list_head *head);
将entry移动到head后
list_move_tail(struct list_head *entry, struct list_head *head);
将entry移动到head前
list_empty(struct list_head *head);
判断链表是否为空
list_splice(struct list_head *list, struct list_head *head);
从链表head后断开, 将list链表链接进去

list_entry(struct list_head *ptr, type_of_struct, field_name);
从list_head地址得到包含list_head的结构体的开始地址.
类似的宏为container_of(pointer, container_type, container_field); 从结构体成员地址得到结构体指针
list_for_each(struct list_head *cursor, struct list_head *list)
这个宏创建一个for循环, 执行一次, cursor 指向链表中的下个入口项
list_for_each_prev(struct list_head *cursor, struct list_head *list)
这个版本反向遍历链表.
list_for_each_safe(struct list_head *cursor, struct list_head *next, struct list_head *list)
如果循环可能删除链表中的项, 使用这个版本.
list_for_each_entry(type *cursor, struct list_head *list, member)
直接得到包含list_head的结构体的地址
list_for_each_entry_safe(type *cursor, type *next, struct list_head *list, member)
如果循环可能删除链表中的项, 使用这个版本
举例:
struct list_head todo_list;
...
void todo_add_entry(struct todo_struct *new)
{
struct list_head *ptr;
struct todo_struct *entry;

list_for_each(ptr, &todo_list)
{
entry = list_entry(ptr, struct todo_struct, list);
if (entry->priority < new->priority) {
list_add_tail(&new->list, ptr);
return;
}
}
list_add_tail(&new->list, &todo_struct)
}


模块特殊宏/函数(注意大小写)

module_init(initialization_function): 声明模块初始化函数
module_exit(cleanup_function): 声明模块注销函数

EXPORT_SYMBOL(name): 声明符号在模块外可用

EXPORT_SYMBOL_GPL(name): 声明符号仅对使用 GPL 许可的模块可用.

MODULE_LICENSE("GPL"): 声明模块许可
MODULE_AUTHOR: 声明谁编写了模块
MODULE_DESCRIPION: 一个人可读的关于模块做什么的声明
MODULE_VERSION: 一个代码修订版本号; 看 的注释以便知道创建版本字串使用的惯例
MODULE_ALIAS: 模块为人所知的另一个名子
MODULE_DEVICE_TABLE: 来告知用户空间, 模块支持那些设备

module_param(name, type, perm): 声明模块加载时允许设置的参数(2.6.11之前版本中为MODULE_PARM)
module_param_array(name,type,num,perm): 声明模块加载时允许设置的数组参数
name: 是你的参数(数组)的名子
type: 是数组元素的类型
bool/invbool: 一个布尔型( true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.
charp: 一个字符指针值. 需要为其分配内存(charp, NOT char)
int/long/short/uint/ulong/ushort: 基本的变长整型值. 以 u 开头的是无符号值.
num: 一个整型变量
perm: 通常的权限值, 在 中定义. S_IRUGO: 可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR: 允许 root 改变参数. 注意, 如果一个参数被 sysfs 修改, 模块看到的参数值也改变了, 但模块不会有任何通知
示例:
在模块中声明如下:
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
调用时如下:
insmod moduel_name howmany=10 whom="Mom"
模块初始化函数原型为
static int __init function(void);
大部分注册函数以 register_ 做前缀
__init/__initdata: 给定的函数/数据只是在初始化使用. 模块加载后丢掉这个初始化函数, 使它的内存可做其他用途.
__devinit/__devinitdata: 内核没有配置支持 hotplug 设备时等同于__init/_initdata.
模块注销函数原型为
static void __exit function(void);
__exit/__exitdata: 如果模块直接建立在内核里, 或者如果内核配置成不允许模块卸载, 标识为__exit 的函数被简单地丢弃

container_of(pointer, container_type, container_field);通过一个结构体成员的地址得到结构体的地址
比如:
struct test{int a; int b; int c; inte}test_t;
&test_t == container_of(&(test_t.c), struct test, c)
__setup("test=", test_setup);
这个宏将test_setup这个函数放在特定的section中.
在执行init/main.c::checksetup()时会去kernel boot commandline中寻找字符串"test=xxx": 如果有找到,就用"xxx"作为参数调用test_setup; 否则不运行
在insmod中如果参数里带有"test=xxx"也会运行
void *kmalloc(size_t size, int flags); 试图分配 size 字节的内存; 返回值是指向那个内存的指针或者如果分配失败为NULL. flags 参数用来描述内存应当如何分配
申请的空间大小限制: 大概为128K
void kfree(void *ptr);分配的内存应当用 kfree 来释放. 传递一个 NULL 指针给 kfree 是合法的.




get_free_page申请的page数限制: 2^MAX_ORDER, 2的MAX_ORDER次方个page. 通常MAX_ORDER=10, 也就是最多2^10=1024个page, 4Mbyte


int access_ok(int type, const void *addr, unsigned long size): 验证用户空间有效性
type: VERIFY_READ/VERIFY_WRITE. 如果需要验证读写许可, 则只要 VERIFY_WRITE
addr: 一个用户空间地址,
size: 需要验证的大小.
返回值: 1 是成功(存取没问题); 0 是失败(存取有问题). 如果它返回0, 驱动应当返回 -EFAULT
put_user(datum, ptr) 
__put_user(datum, ptr) 
写 datum 到用户空间. 它们相对(copy_to_user)快. 传送的数据大小依赖 prt 参数的类型. 比如: prt 是一个char指针就传送一个字节
put_user 检查用户空间确保能写. 在成功时返回 0, 并且在错误时返回 -EFAULT.
__put_user 进行更少的检查(它不调用 access_ok),
驱动应当调用put_user来节省几个周期; 或者拷贝几个项时, 在第一次数据传送之前调用access_ok一次, 之后使用__put_user
get_user(local, ptr) 
__get_user(local, ptr) 
从用户空间读单个数据, 获取的值存储于本地变量 local;
如果使用上述四个函数时, 发现一个来自编译器的奇怪消息, 例如"coversion to non-scalar type requested". 必须使用 copy_to_user 或者 copy_from_user.

unsigned long copy_to_user(void __user *to,const void *from,unsigned long count); 拷贝一整段数据到用户地址空间. 任何存取用户空间的函数必须是可重入的. 此函数可能导致睡眠
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count); 从用户地址空间拷贝一整段数据. 任何存取用户空间的函数必须是可重入的. 此函数可能导致睡眠
这两个函数的作用不仅限于拷贝数据到和从用户空间: 它们还检查用户空间指针是否有效. 如果指针无效, 不进行拷贝; 如果在拷贝中遇到一个无效地址, 只拷贝有效部分的数据. 在第二种情况下, 返回值是未拷贝的数据数. 驱动应当查看返回值, 并且如果它不是0, 就返回 -EFAULT 给用户.
int capable(int capability); 
在进行一个特权操作之前, 一个设备驱动应当检查调用进程有合适的能力
capability 取值有以下这些:
CAP_DAC_OVERRIDE
这个能力来推翻在文件和目录上的存取的限制(数据存取控制, 或者 DAC).
CAP_NET_ADMIN
进行网络管理任务的能力, 包括那些能够影响网络接口的.
CAP_SYS_MODULE
加载或去除内核模块的能力.
CAP_SYS_RAWIO
进行 "raw" I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯.
CAP_SYS_ADMIN
一个捕获-全部的能力, 提供对许多系统管理操作的存取.
CAP_SYS_TTY_CONFIG
进行 tty 配置任务的能力.
CAP_SYS_ADMIN
在任务缺乏一个更加特定的能力时, 可以选这个来测试
int printk(const char * fmt, ...);向console(而不是虚拟终端)打印一条消息, 并通过附加不同的记录级别或者优先级在消息上对消息的严重程度分类.没有指定优先级的printk语句缺省是DEFAULT_MESSAGE_LOGLEVEL, 在 kernel/printk.c里指定作为一个整数. 在2.6.10内核中, DEFAULT_MESSAGE_LOGLEVEL是KERN_WARNING, 但这个值在不同的内核中可能不一样.
按消息的严重程度从高到低为:
KERN_EMERG

用于紧急消息, 常常是那些崩溃前的消息.

KERN_ALERT

需要立刻动作的情形.

KERN_CRIT

严重情况, 常常与严重的硬件或者软件失效有关.

KERN_ERR

用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障.

KERN_WARNING

有问题的情况的警告, 这些情况自己不会引起系统的严重问题.

KERN_NOTICE

正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告.

KERN_INFO

信息型消息. 在这个级别, 很多驱动在启动时打印它们发现的硬件的信息.

KERN_DEBUG

用作调试消息.

使用举例:
printk(KERN_INFO "hello, worldn");//注意:消息优先级与正文内容之间没有逗号
int printk_ratelimit(void); 在你认为打印一个可能会常常重复的消息之前调用来避免重复输出很多相同的调试信息. 如果这个函数返回非零值, 继续打印你的消息, 否则跳过打印.
使用举例
if (printk_ratelimit())
printk(KERN_NOTICE "The printer is still on firen");
int print_dev_t(char *buffer, dev_t dev); 
char *format_dev_t(char *buffer, dev_t dev);
从一个驱动打印消息, 你会想打印与感兴趣的硬件相关联的设备号.两个宏定义都将设备号编码进给定的缓冲区; 唯一的区别是 print_dev_t 返回打印的字符数, 而 format_dev_t 返回缓存区

void set_current_state(int new_state); 设置当前进程的运行状态
new_state: TASK_INTERRUPTIBLE/TASK_RUNNING/TASK_INTERRUPTIBLE/TASK_UNTINTERRUPTIBLE/...
在新代码中不鼓励使用下面这种方式
current->state = TASK_INTERRUPTIBLE
int in_interrupt(void)
如果处理器当前在中断上下文(包括软中断和硬中断)运行就返回非零
int in_atomic(void)
若调度被禁止(即当前状态是原子态, 包括硬中断,软件中断以及持有自旋锁时), 返回值是非零. 在持有自旋锁这种情况, current 可能是有效的, 但是禁止存取用户空间, 因为它能导致调度发生.
无论何时使用 in_interrupt(), 应当真正考虑是否 in_atomic 是你实际想要的



主次设备号:
主编号标识设备驱动; 次编号被内核用来决定引用哪个设备
dev_t 类型(在 中定义)用来标识设备编号 -- 同时包括主次部分
MAJOR(dev_t dev): 从dev_t中取得主设备号
MINOR(dev_t dev): 从dev_t中取得次设备号

MKDEV(int major, int minor): 讲主次设备号转换成dev_t

int register_chrdev_region(dev_t first, unsigned int count, char *name): 获取一个或多个设备编号来使用
first 是你要分配的起始设备编号. first 的次编号部分常常是 0
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);动态分配一个主编号
dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数.
fisetminor 应当是请求的第一个要用的次编号; 它常常是 0.
count 是你请求的连续设备编号的总数
name 是应当连接到这个编号范围的设备的名子; 它会出现在 /proc/devices 和 sysfs 中
void unregister_chrdev_region(dev_t first, unsigned int count); 释放设备编号
first 是你要分配的起始设备编号. first 的次编号部分常常是 0
count 是你请求的连续设备编号的总数
设备注册:
struct cdev *cdev_alloc(void);为struct cdev申请内存空间
void cdev_init(struct cdev *cdev, struct file_operations *fops);初始化struct cdev结构. 其成员owner应当设置为 THIS_MODULE
cdev 是需要初始化的struct cdev结构
fops: 是关联到这个驱动的方法集合(read/write等)
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);将设备注册到内核

dev 是struct cdev结构

num 是这个设备响应的第一个设备号

count 是应当关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个特定的设备的情形.

void cdev_del(struct cdev *dev);将设备注销
设备注册的老方法:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major 是感兴趣的主编号
name 是驱动的名子(出现在 /proc/devices)
fops 是缺省的 file_operations 结构.
int unregister_chrdev(unsigned int major, const char *name);

major和name 必须和传递给register_chrdev的相同, 否则调用会失败.


设备节点:
devfs_handle_t devfs_register (devfs_handle_t dir,
            const char *name,
            unsigned int flags,
            unsigned int major, unsigned int minor,
            umode_t mode,
            void *ops, void *info);创建设备节点
dir:需要创建的设备文件所在目录,默认为/dev
name: 需要创建的设备文件名
flags: 通常取DEVFS_FL_DEFAULT
major: 主设备号
minor: 次设备号
mode: 此设备文件的读写权限
ops: 此设备的file_operations结构
info: 
#define DEV_ID ((void*)123456)
#define DEV_NAME "XXXXXXXXXXXXXX"
#define DEV_MAJOR 200
#define DEV_IRQ IRQ_XXXX
#define DEV_IRQ_MODE SA_SHIRQ

...

//regist char device
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
ret = register_chrdev(DEV_MAJOR, DEV_NAME, &fops);
#else
cdev_init(&dev_char, &fops);
dev_char.owner = THIS_MODULE;
dev_char.ops = &fops;
ret = cdev_add(&dev_char, MKDEV(DEV_MAJOR, 0), 1);
#endif
if (ret < 0)
goto __mod_init_err1;
//make devfs
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
devfs_handle = devfs_register(NULL, DEV_NAME, DEVFS_FL_DEFAULT,
DEV_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &fops, NULL);
if (NULL == devfs_handle)
{
ret = -1;
goto __mod_init_err2;
}
#else
dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(dev_class))
{
ret = PTR_ERR(dev_class);
goto __mod_init_err2;
}
class_device_create(dev_class, MKDEV(DEV_MAJOR, 0), NULL, DEV_NAME);
ret = devfs_mk_cdev(MKDEV(DEV_MAJOR, 0), S_IFCHR | S_IRUGO | S_IWUSR, DEV_NAME);
if(ret)
goto __mod_init_err3;
#endif

......

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
__mod_init_err3:
class_device_destroy(dev_class, MKDEV(DEV_MAJOR, 0));
class_destroy(dev_class);
#endif
__mod_init_err2:
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
unregister_chrdev(DEV_MAJOR, DEV_NAME);
#else
cdev_del(&dev_char);
#endif
__mod_init_err1:
free_irq(DEV_IRQ, DEV_ID);

#endif//end of "ifndef INPUT_DEVICE"

__mod_init_err0:
return ret;


file_operations函数:
ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) 1. 通常应当更新 *offp 中的文件位置来表示在系统调用成功完成后当前的文件位置.
2. 如果"没有数据, 但是可能后来到达", 在这种情况下, read 系统调用应当阻塞.
3. 返回值
如果等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送
如果是正数, 但是小于 count, 只有部分数据被传送.
如果值为 0, 到达了文件末尾(没有读取数据).

如果值为负值表示有一个错误. 这个值指出了什么错误, 根据 . 出错的典型返回值包括 -EINTR( 被打断的系统调用) 或者 -EFAULT( 坏地址 ).

如果一些数据成功传送接着发生错误, 返回值必须是成功传送的字节数. 在函数下一次调用前错误不会报告. 这要求驱动记住错误已经发生, 以便可以在以后返回错误状态.



ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)

1. 通常应当更新 *offp 中的文件位置来表示在系统调用成功完成后当前的文件位置.

2. 返回值:

如果值等于 count, 要求的字节数已被传送

如果正值, 但是小于 count, 只有部分数据被传送

如果值为 0, 什么没有写. 这个结果不是一个错误

一个负值表示发生一个错误

如果一些数据成功传送接着发生错误, 返回值必须是成功传送的字节数. 在函数下一次调用前错误不会报告. 这要求驱动记住错误已经发生, 以便可以在以后返回错误状态.

ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
};
每个 iovec 描述了一块要传送的数据; 它开始于 iov_base (在用户空间)并且有 iov_len 字节长. count 参数告诉有多少 iove结构.
若未定义此二函数. 内核使用 read 和 write 来模拟它们,

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.
cmd 参数从用户那里不改变地传下来,
arg 是可选的参数, 无论是一个整数还是指针(按照惯例应该用指针), 均以unsigned long的形式传递进来
返回值:
-ENIVAL("Invalid argument"): 命令参数不是一个有效的POSIX 标准(通常会返回这个)
-ENOTTY: 一个不合适的 ioctl 命令. 这个错误码被 C 库解释为"设备的不适当的ioctl"(inappropriate ioctl for device)

ioctl的cmd参数应当是系统唯一的, 这是出于阻止向错误的设备发出其可识别但具体内容无法解析的命令的考虑
cmd参数由这几部分组成: type, number, direction, size
type
魔数. 为整个驱动选择一个数(参考ioctl-number.txt). 这个成员是 8 位宽(_IOC_TYPEBITS).
number
序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.
direction
数据传送的方向(如果这个特殊的命令涉及数据传送).
数据传送方向是以应用程序的观点来看待的方向
_IOC_NONE: 没有数据传输
_IOC_READ: 从系统到用户空间
_IOC_WRITE: 从用户空间到系统
_IOC_READ|_IOC_WRITE: 数据在2个方向被传送
size
用户数据的大小. 这个成员的宽度(_IOC_SIZEBITS)是依赖体系的. 通常是13或者14位.
命令号相关操作:
_IO(type,nr): 创建没有参数的命令
_IOR(type, nre, datatype): 创建从驱动中读数据的命令
_IOW(type,nr,datatype): 创建写数据的命令
_IOWR(type,nr,datatype): 创建双向传送的命令
_IOC_TYPE(cmd):得到magic number
_IOC_NR(cmd):得到顺序号
_IOC_DIR(cmd): 得到传送方向
_IOC_SIZE(cmd): 得到参数大小
预定义命令(会被内核自动识别而不会调用驱动中定义的ioctl)分为 3 类:
1. 可对任何文件发出的(常规, 设备, FIFO, 或者 socket). 这类的magic number以'T'开头
2. 只对常规文件发出的那些.
3. 对文件系统类型特殊的那些.
驱动开发只需注意第一类命令,以及以下这些
FIOCLEX
设置 close-on-exec 标志(File IOctl Close on EXec).
FIONCLEX
清除 close-no-exec 标志(File IOctl Not CLose on EXec).
FIOASYNC
设置或者复位异步通知. 注意直到 Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因为两个动作都可通过 fcntl 来完成, 没有人真正使用 FIOASYNC 命令, 它在这里出现只是为了完整性.
FIOQSIZE
返回一个文件或者目录的大小; 当用作一个设备文件, 返回一个 ENOTTY 错误.
FIONBIO(File IOctl Non-Blocking I/O)
修改在 filp->f_flags 中的 O_NONBLOCK 标志. 给这个系统调用的第 3 个参数用作指示是否这个标志被置位或者清除. 注意常用的改变这个标志的方法是使用 fcntl 系统调用, 使用 F_SETFL 命令.

unsigned int (*poll) (struct file *filp, poll_table *wait);
filp: 文件描述符指针
wait: 用于poll_wait函数
返回值: 可能不必阻塞就立刻进行的操作
void poll_wait(struct file *, wait_queue_head_t *, poll_table *);
驱动通过调用函数poll_wait增加一个等待队列到poll_table结构. 这个等待队列是驱动定义并处理的, 进程唤醒方式(只唤醒其中一个还是唤醒所有等待的进程)也由驱动(read/write)来决定

返回的位掩码:
POLLIN
设备可不阻塞地读
POLLRDNORM
可以读"正常"数据. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLRDBAND
可从设备中读取带外数据. 当前只用在 Linux 内核的一个地方(DECnet代码), 并且通常对设备驱动不可用.
POLLPRI
可不阻塞地读取高优先级数据(带外). 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.
POLLHUP
当读这个设备的进程见到文件尾, 驱动必须设置POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同selcet功能所规定的.
POLLERR
一个错误情况已在设备上发生. 当调用 poll, 设备被报告为可读可写, 因为读写都返回一个错误码而不阻塞.
POLLOUT
设备可被写入而不阻塞.
POLLWRNORM
这个位和POLLOUT有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).
POLLWRBAND
同POLLRDBAND, 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.
例子:
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
struct scull_pipe *dev = filp->private_data;
unsigned int mask = 0;
/*
* The buffer is circular; it is considered full
* if "wp" is right behind "rp" and empty if the
* two are equal.
*/
down(&dev->sem);
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM; /* readable */
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM; /* writable */
up(&dev->sem);
return mask;
}
这个代码简单地增加了 2 个 scullpipe 等待队列到 poll_table, 接着设置正确的掩码位, 根据数据是否可以读或写.
如果在进程A得到poll/select/epoll通知后, 另外一个进程B将数据读走/将缓冲区填满, 这时候进程A进来进行读写操作,会如何?

int (*fasync) (int fd, struct file *filp, int mode);
异步通知. (常常假定异步能力只对 socket 和 tty 可用)
从用户的角度看异步通知的设置过程:
1. 指定一个进程作为文件的拥有者: fcntl 系统调用发出 F_SETOWN 命令, 进程ID被保存在 filp->f_owner 给以后使用
2. 在设备中设置 FASYNC 标志: fcntl 系统调用发出 F_SETFL 命令
这样设置后设备有 新数据到达/缓冲有空间 的时候就会发送一个SIGIO信号到filp->f_owner 中的进程(如果值为负值则发给整个进程组).
举例:
signal(SIGIO, &input_handler); /* dummy sample; sigaction() is better */
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL); //get original setting
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
从内核的角度看用户设置过程
1. 当发出 F_SETOWN, 一个值被赋值给 filp->f_owner.
2. 当发出 F_SETFL 来打开 FASYNC, 驱动的fasync方法被调用. 无论何时filp->f_flags中的FASYNC的值有改变, 都会调用驱动中的fasync方法(这个标志在文件打开时缺省地未设置).
3. 当数据到达, 向所有的注册异步通知的进程发出一个 SIGIO 信号.
从驱动的角度看内核响应过程:
1. 内核的第一步与驱动无关
2. 内核的第二步驱动应当用下列函数响应:
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
当 FASYNC 标志因一个打开文件而改变, 这个函数用来从相关的进程列表中添加或去除入口项. 它的所有参数除了最后一个, 都直接来自fasync方法.
mode: 0: 去除入口项; 其他: 添加入口项
fa: 是由驱动提供的一个struct fasync_struct结构. (*fa)在第一次使用之前应该初始化成NULL, 不然可能会出错. 从函数返回的时候会被分配一块内存, 在mode=0时free掉. 所以添加与去除入口项必须配对使用
void kill_fasync(struct fasync_struct **fa, int sig, int band);
数据到达时通知相关的进程.
fa: 与fasync_help里的fa同
sig: 被传递的信号(常常是 SIGIO)
band: 异步状况. 在网络代码里可用来发送"紧急"或者带外数据
POLL_IN: 有新数据到达. 等同于 POLLIN|POLLRDNORM.
POLL_OUT: 有空间可供写入
举例: (注意: 若设备允许多次打开, 每个打开的filp需要有独立的async_queue)
struct fasync_struct *async_queue = NULL;
static int fasync(int fd, struct file *filp, int mode)
{
return fasync_helper(fd, filp, mode, &async_queue);
}
当数据到达, 用下面的语句来通知异步读者.
if (async_queue)
kill_fasync(&async_queue, SIGIO, POLL_IN);
在release方法中应该调用
/* remove this filp from the asynchronously notified filp's */
fasync(-1, filp, 0);
loff_t (*llseek) (struct file *, loff_t, int); 如果未定义llseek方法, 内核缺省通过修改 filp->f_pos来实现移位
如果需要禁止lseek操作, 需要在open中调用
int nonseekable_open(struct inode *inode; struct file *filp);
并把file_operations::llseek设为no_llseek(loff_t no_llseek(struct file *file, loff_t offset, int whence))
举例:
loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
struct scull_dev *dev = filp->private_data;
loff_t newpos;
switch(whence)
{
case 0: /* SEEK_SET */
newpos = off;
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + off;
break;
case 2: /* SEEK_END */
newpos = dev->size + off;
break;
default: /* can't happen */
return -EINVAL;
}
if (newpos < 0)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}

                
        
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭