linux驱动
This is my study note of linux.
设备驱动程序简介
作用
- 设备驱动程序的作用在于提供机制,而不在于提供策略。
- 机制代表“提供什么功能”
- 策略代表“如何使用这些功能”
1
软驱的驱动程序不带策略,它的作用是将磁盘表示为一个连续的数据块阵列,系统高层负责提供策略。编写访问硬件的内核代码时,不考虑策略,只考虑机制,支持同步和异步。
考虑因素
- 提供给用户尽量多的选项
- 编写驱动占的时间
- 保持程序简单
1
用户程序主要用于帮助配置和访问目标设备。
可装载模块
Linux有一个很好的特性:内核提供的特性可在运行时进行扩展。这意味着当系统启动并运行时,我们可以向内核添加功能,当然也可以移除功能。
- 模块——运行时添加到内核中的代码(insmod/rmmod)
设备和模块的分类
- 字符设备
1
像字节流一样被访问的设备,由字符设备驱动程序来实现这种特性。实现open、close、write、read,大多数字符设备只能顺序访问数据通道。
- 块设备
1
进行I/O操作时,块设备每次只能传输一个或多个完整的块,每个块包含512个字节,和字符设备的区别在于内核内部管理数据的方式。
- 网络接口
1
通常,接口是个硬件设备,但也可能是个纯软件设备,比如回环( loopback)接口,Unix访问网络接口的方法仍然是给他们分配一个唯一的名字,这个名字在文件系统中不存在对应的节点。
版本问题
1 | 对内核来讲,偶数编号的内核版本(如2.6.x)是用于正式发行的稳定版本,而奇数编号的版本(如2.7.x)则是开发过程中的一个快照,它将很快被下一开发版本更新。 |
构造和运行模块
1 | 模块被装入内核时就连接到了内核,因此可以访问内核的公有符号(包括函数和变量) |
- MODULE_LICENSE(“GPL”)
1
告诉系统采用自由许可证。
- current
1
2
3
4
5内核代码通过访问全局项获得当前进程,其在<asm/current.h>中定义。是一个指向struct task_struct的指针,在<linux/sched.h>
当前进程ID:
current->comm
当前命令名:
current->pid
编译模块
- 构造模块
1
obj-m := module.o
- 构造模块依赖多个文件
1
2module-objs := file1.o file2.o
生成module.ko需要file1.c file2.c - make
1
2make -C $(shell uname -r)/build M=$(shell pwd) modules
上述命令首先改变目录到-c选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile文件。M=选项让该makefile在构造modules目标之前返回到模块源代码目录。然后,modueles目标指向obj-m变量中设定的模块;在上面的例子中,我们将该变量设置成了module.o。
模块装载和卸载
- insmod 装载
1
使用公共内核符号表解析模块中未定义的符号。
- rmmod 卸载
- modprobe 装载
1
与insmod不同,会考虑要装载的模块是否引用一些当前内核不存在的模块,如果modprobe找到这些模块,则会带着一起装载,这种情况下使用insmod会直接报错。
- lsmod 列出装载的所有模块
版本依赖
宏定义在linux/versions.h中
- UTS_RELEASE 拓展为一个描述内核版本的字符串
- LINUX_VERSION_CODE 拓展为内核版本的二进制表示,例如2.6.10对应132618(0x2006a)
- KERNEL_VERSION(major, minor, release) 以组成版本号的三部分为参数,创建整数的版本号。
内核符号表
通常情况下,模块只需实现自己的功能,无需导出任何符号,如果一个模块需要向其他模块到处符号,则使用如下宏:
- EXPORT_SYMBOL(name);
- EXPORT_SYMBOL_GPL(name);
注意,GPL版本下导出的模块只能GPL许可证下的模块使用。
模块初始化和关闭
- 初始化初始化模块应该被声明为static。在内核代码中还会遇到__devinit和__devinitdata两个,只有在内核未被配置成热拔插时,这两个标记才会被翻译成__init和__initdata。
1
2
3
4
5static int __init init(void)
{
// 初始化代码
}
module_init(init); - 关闭如果一个模块没有定义清楚函数,则不允许卸载此模块。
1
2
3
4
5static void __exit exit(void)
{
// 初始化代码
}
module_exit(exit);
初始化过程中的错误处理
- 通过goto进行清除
- 通过记录任何已经完成注册的设施,然后出错时只回滚已经完成的步骤
字符型设备驱动程序
编写驱动程序的第一步就是定义驱动程序为用户程序提供的能力。
主设备号和次设备号
设备驱动程序可在/dev下面输入命令ls -l识别,用”c”标识,块设备用”b”标识。
如上图,主设备号有10、5、1、230、229,次设备有235、40、100、234、60等。
1 | 主设备用于标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的设备。 |
- 在内核中,采用dev_t类型保存设备编号
1
32位数,其中12位用来表示主设备号,其余20位用来表示次设备号。
- MAJOR(dev_t dev)获取主设备号
- MINOR(dev_t dev)获取次设备号
- MKDEV(int major, int minor)将主设备号和次设备号合并为dev_t类型数据
分配和释放设备编号
- 模块的建立
建立一个字符设备以前,最主要的操作是获得一个或者多个设备编号,使用register_chrdev_region函数。1
first是要分配的设备编号范围的起始值。first的次设备号经常被置为0,但对该函数来讲并不是必需的,count是所请求的连续设备编号的个数,name是和该编号范围关联的设备名称,将出现在/proc/devices和sysfs中。
1
常用于动态分配设备编号,在上面这个函数中,dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor应该是要使用的被请求的第一个次设备号,它通常是0,count和name参数与register_chrdev_region函数是一样的。
- 模块的清除
文件操作
分配到设备号后,还需要将驱动程序操作连接到这个编号,file_operations结构就是用来建立这个连接的。我们可以认为文件是一个”对象”,操作它的函数是方。file_operations结构如下:
struct module *owner
1
指向拥有该结构的模块的指针,一般初始化为THIS_MODULE
loff t (*llseek)(struct file * , lofft, int);
1
修改文件的当前读写位置,并将新位置做为返回值返回。如果这个函数指针是NULL,对seek的调用将会以某种不可预期的方式修改file结构(在“file结构”一节中有描述)中的位置计数器。
ssize_t (*read)(struct file *, char __user * , size_t, loff_t *);
1
用来从设备中读取数据。
ssize t (*aio_read) (struct kiocb *, char _ _user *, size_t, loff_t);
1
初始化一个异步的读取操作----即在函数返回之前可能不会完成的读取操作。
ssize_t (*write)(struct file *, const char _._user *, size_t, loff_t *);
1
向设备发送数据。
ssize_t (*aio_write)(struct kiocb *,const char .._user *, size_t, loff_t *) ;
1
初始化设备上的异步写入操作。
…
file_operations结构被初始化如下形式:
file、inode结构
1
由于内容太多,自行查看《LINUX设备驱动程序第三版》57面。
字符设备的注册
内核内部使用struct cdev结构来表示字符设备。
- 创建字符设备
1
2struct cdev *my_cdev = cdev_alloc();
my_cdev_ops = &my_fops; - 初始化cdev结构
1
void cdev_init(struct cdev *cdev, struct file_operations *fops);
- 指明所有者字段
1
my_cdev.owner = THIS_MODULE;
- 告诉内核该结构的信息
1
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
- 移除设备
1
void cdev_del(struct cdev *dev);
open方法
- 检查设备特定的错误
- 如果设备是首次打开,则对其进行初始化
- 更新f_op指针
- 分配并填写置于filp->private_data的数据结构其中,inode参数在i_cdev字段包含了我们所需要的信息。但是我们通常不需要cdev结构本身,而是需要包含cdev结构的scull_dev结构。
1
int (*open)(struct inode *inode, struct file *flip);
因此,我们需要使用<linux/kernel.h>的container_of宏实现:
1 | container_of(pointer, container_type, container_filed); |
release方法
- 释放由open分配的,保存在filp->private_data中的所有内容
- 在最后一次关闭操作时关闭设备
1
每个file结构维护其被使用多少次的计数器,只有计数器为0时,才会执行release方法。
内存使用
scull驱动程序引入了Linux内核中用于内存管理的两个核心函数,定义在<linux/slab.h>中。
- void *kmalloc(size_t size, int flags);
1
size表示分配大小,flags表示描述内存的分配方法。
- void kfree(void *ptr);修改量子和量子集大小的策略:
1
在scull中,每个设备都是一个指针链表,其中每个指针都指向一个scull_qset结构。默认情况下,每一个这样的结构通过一个中间指针数组最多可引用4000000个字节。我们发布的源代码使用了一个有1000个指针的数组,每个指针指向一个4000字节的区域。我们把每一个内存区称为一个量子,而这个指针数组(或它的长度)称为量子集。
- 编译时,修改scull.h中的宏SCULL_QUANTUM和SCULL_QSET
- 模块加载时,设置scull_quantum和scull_qset的整数值
- 运行时,使用ioctl修改当前值以及默认值
- quantum表示量子
- qset表示量子集大小
read和write
实现read和write的两个核心函数:
- copy_to_user
- copy_from_user
1
正确传输后,我们的返回值必须为成功传输的字节数,发生错误时,返回负值表示错误,返回值表示了错误的类型。运行在用户态的程序能看到的始终只有返回值-1,为了找出出错的原因,用户空间程序必须访问errno。
调试技术
通过打印调试
最普通的调试技术,通过在适当地点调用printk显示监视信息,我们通常使用宏来表示消息日志的级别。
- 为什么优先级和格式字串间没有逗号呢?
1
因为编译时,预处理器将它和消息文本拼接在一起。
- 优先级别
当优先级小于console_loglevel这个整数的值,才会被输出出来。
重定向控制台消息
内核可以将消息发送到一个指定的虚拟控制台,默认情况下为当前的虚拟终端。可以使用misc-progs目录下的setconsole程序,调用时需要附件一个参数指定要接受消息的控制台编号。
1 | 可以在任何一个控制台设备上调用ioctl来指定接收消息的其他虚拟终端,调用ioctl(TIOCLINUX)。 |
消息记录
- printk函数将消息写到一个长度为__LOG_BUF_LEN字节的循环缓冲区中,然后该函数会唤醒所有等待消息的进程(注意:要是对/proc/kmsg进行读操作时,日志缓冲区被读取的数据就不再保留,用syslog系统调用能够通过选项返回日志数据并保留)
- 如果缓冲数据填满了,则printk就绕回缓冲区的开始处填写新的数据
- klogd运行时会读取内核消息并将他们分发到syslogd,syslogd随后查看/etc/syslog.conf找出处理这些数据的方法。
- 如果想要避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为klogd指定 -f选项,将消息保存在特定的文件。
开启及关闭消息
当删除已认为不再需要的提示消息后,又需要实现新的功能,这时又希望开启一部分消息,我们可以通过定义宏来开始是否开启打印消息。
速度限制
若是慢速的控制台设备过高的消息输出会导致系统变慢。
int printk_ratelimit(void);
1 | 在打印一条可能被重复的消息之前,应该调用上面这个函数,如果返回值是非零值,则可以继续打印我们的消息,否则就应该跳过。 |
使用/proc文件系统
/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息。/proc下面的每个文件都绑定一个内核函数,用户读取其中的文件时,该函数动态地生成文件的“内容”。
1 | 进程读取/proc文件时,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间,该缓冲区会传入到我们定义的函数,该函数如上。其中start参数指示要返回给用户的数据保存在内存页的什么位置。 |
- printk_ratelimit通过跟踪发送到控制台的消息数量工作。
- 输出速度超过一个阈值时,返回零。
打印设备编码
- int print_dev_t(char *buffer, dev_t dev);
- char *format_dev_t(char *buffer, dev_t dev);
1
print_dev_t返回的是打印的是字符数,format_dev_t返回缓冲区。
使用/proc文件系统
/proc是一种特殊的、由软件创建的文件系统,内核使用它向外界导出信息。
- /pro下面的每个文件都绑定于一个内核函数
- 用户读取其中的文件时,动态生成文件“内容”
- 不建议使用/proc导出信息,建议使用sysfs向外界导出信息
1
在某个进程读取我们的/proc文件时,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间,该缓冲区会传入我们定义的函数。
- page指针指向用来写入数据的缓冲区
- start返回数据写道内存页的哪个位置
- eof参数指向一个整型数,当没有数据返回时,驱动程序必须设置这个参数
- 由于/proc的接口越来越“声名狼藉”,因此增加了seq_file接口
1
使用该接口必须建立四个迭代器对象,start、next、stop、show。
通过监视调试
通过监视用户空间中应用程序的运行情况可以捕获到一些信息。
- 通过调试器一步步跟踪
- 插入打印语句
- strace状态下运行程序
1
可以显示由用户空间程序发出的所有系统调用。它不仅可以显示调用,而且还能显示调用参数以及用符号形式表示的返回值。
oops消息
- 大部分错误都是对NULL指针取值或者使用了其他不正确的指针值
- OOPS显示发生错误的处理器状态
- 用户空间的栈地址默认自0xc00000000向下,如果获得的内核oops中包含小于0xc00000000的地址,则可以肯定时在某处忘记初始化动态分配到的内存。
- 只有在构造内核时打开了CONFIG_KALLSYMS选项,我们才能看到调用栈
系统挂起
尽管内核代码中大多数错误只会导致一个oops消息,但是有时他们会将系统完全挂起,任何消息都无法打印(例如代码进入死循环)。
处理系统挂起有两个选择:
- 防范挂起
- 亡羊补牢,挂起后调试代码
通过在一些关键点上插入schedule可以防止死循环。
1 | shcedule会调用调度器,并允许其他进程偷取当前进程的CPU时间。为了防止调用schedule导致的代码重入问题,我们需要选择合适的锁定(不可在持有自旋锁的代码中调用shedule) |
魔术键调试
- 通过(ALT+SysRq键组合激活)
- 通过 echo 1>/proc/sys/kernel/sysrq 命令使能魔术键
- 通过 echo 0>/proc/sys/kernel/sysrq 命令关闭魔术键
命令 | 说明 |
---|---|
r | 关闭键盘的raw模式 |
k | 激活“留意安全键”功能 |
s | 对所有磁盘进行紧急同步 |
u | 尝试以只读模式重新挂载所有磁盘 |
b | 立即重启系统 |
p | 打印当前的处理器寄存器信息 |
t | 打印当前的任务列表 |
m | 打印内存信息 |
gdb调试
使用gdb进行内部行为的探索。调用例子如下:
1 | gdb /usr/src/linux/vmlinux /proc/kcore |
- 第一个参数是未经压缩的内核ELF可执行文件的名字
- 第二个参数是core文件的名字
1
对内核进行调试时,gdb的许多常用功能都不可用。Linux的可装载模块是ELF格式的可执行映像,模块会被划分为许多代码段。一个典型的模块可能包含十多个或者更多的代码段,但对调试会话来讲,相关的代码段只有下面三个:.text、.bss、.data。
kdb调试
当kdb运行时,内核所做的每一件事情都会停下来,当激活kdb时,系统不应该运行任何东西(尤其是网络功能,除非是网络的驱动程序),最好在单用户下启动。
并发和竞态
竞态会导致对共享数据的非控制访问。当两个执行进程需要访问相同的数据结构时,混合的可能性就永远存在,因此我们应该避免资源的共享。
信号和互斥体
建立临界区:在在任意给定的时刻,代码只能被一个线程执行。
1 | 在进入临界区前执行P,如果信号量此时大于0,则该值会减小1,进程可以继续执行,如果此时信号量小于0,则该进程必须等待其他人释放信号量,释放由V操作完成(增加信号量的值,并唤醒等待进程)。 |
- 申明互斥体
1
2
3DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
第一个函数初始化一个值为1的信号量name,第二个函数,互斥体的初始状态是锁定的。 - 初始化
1
2void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem); - P操作
1
2
3
4void down(struct semaphore *sem);
int down_interruptiable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
可中断的版本几乎是我们始终要使用的版本,它允许等待在某个信号量上的用户空间进程可被用户中断,使用此函数时要注意检查返回值,如果发生错误我们返回-ERESTARTSYS则必须首先撤销已经做出的任何用户可见的修改,系统调用可正确重试,如果无法撤销这些操作,则返回-EINTR。 - V操作
1
2void up(struct semaphore *sem);
调用up后,调用者不再拥有信号量,如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放该信号量。
读取者/写入者信号量
允许多个并发的读取者是可能的,因为只读任务可并行完成他们的工作,而不需要等待其他读取者退出临界区。
1 | 读取者/写入者信号量“rwsem” |
一个rwsem允许一个写入者或多个读取者拥有信号量,写入者拥有更高的优先级,如果存在大量写入者会存在竞争“饿死”现象。
completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。
- 创建completion
1
2
3
4DECLARE_COMPLETION(my_completion);
如果需要动态创建和初始化completion,则使用下面方法:
struct completion my_completion;
init_completion(&my_completion); - 等待completion
1
2void wait_for_completion(struct completion *c);
如果代码调用了wait_for_completion且没有人会完成该任务,则将产生一个不可杀的进程。 - completion触发completion机制的典型使用时模块退出时的内核线程终止。
1
2
3void complete(struct completion *c);
void complete_all(struct completion *c);
complete只会唤醒一个等待线程,complete_all允许唤醒所有等待线程。
自旋锁
自旋锁不能在休眠的代码中使用。
1 | 自旋锁是一个互斥设备,它只能由两个值:"锁定"和"解锁",如果锁可用,则锁定位被设置,代码进入临界区。如果锁不可用,则代码进入忙状态并重复检查这个锁,直到锁可用为止,这个循环就是自旋锁的自旋部分。 |
- 即使多个线程在给定时间自旋,也只有一个线程可以获得锁。
- 存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。
- 自旋锁API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 静态分配
void spin_lock_init(spinlock_t *lock); 动态分配
上锁
void spin_lock(spinlock_t *lock); 进入临界区之前,获得锁
void spin_lock_bh(spinlock_t *lock); 获得锁之前禁止软件中断,让硬件中断打开
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); 获得锁之前禁止中断,而先前的中断状态保存在flags。
释放获取的锁
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqsave(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
非阻塞自旋锁
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock); - 任何拥有自旋锁的代码都必须是原子的,他不能休眠,不能因为任何原因放弃处理器,除了服务中断以外。
- 只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止,甚至在单处理器系统上,必须以同样的方式禁止抢占以避免竞态。
锁陷阱
- 不明确的规则
1
如果某个锁的函数要调用其他同样试图获得这个锁的函数,我们的代码就会死锁,无论是信号量还是自旋锁,都不允许锁的拥有者第二次获得这个锁。
- 锁的顺序规则
1
如果我们有两个锁,LOCK1和LOCK2,某个线程锁定了LOCK1,而其他线程同时锁定了LOCK2,这是每个线程都试图获得另外那个锁,于是两个线程都将死锁。
- 在必须获得多个锁时,应该始终以相同的顺序获得
- 如果我们必须获得一个局部锁,以及一个属于内核更核心的锁,首先应该获取自己的局部锁
1
假如我们拥有信号量和自旋锁的组合,则必须首先获得信号量,在拥有多个自旋锁时调用down是很严重的。
- 细粒度锁和粗粒度锁的区别
1
以前的内核有且具有一个巨大的内核锁,其不具备良好的伸缩性,等待大的内核锁时花费大量时间。现在内核可包含数千个锁,每个锁保护一个小的资源,具有良好的伸缩性。
除了锁以外的办法
- 免锁算法
常用于免锁的数据结构之一是循环缓冲区,在这个算法中,一个生产者将数据放到数组的结尾,消费者从数组的另一端移走数据。1
当达到数组尾部的时候,生产者回到数组的头部,因此,一个循环缓冲区需要一个数组以及两个索引。
- 原子变量、位操作
具体查看书本127页~139页。
seqlock
seqlock从本质上将,会允许读取者对资源的自由访问,但需要读取者检查是否和写入者发生冲突。当冲突发生时,需要重试对资源的访问。
- 初始化
1
2
3
4
5
6有两种方式,第一种:
seqlock_t lock1 = SEQLOCK_UNLOCKED;
第二种:
seqlock_t lock2;
seqlock_init(&lock2);
读取访问通过获得一个整数顺序值而进入临界区,退出时该顺序值和当前值比较,如果不相等则必须重试读取访问。 - 中断处理历程中,使用IRQ安全版本
1
2unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags); - 写入锁必须在进入seqlock保护的临界区获得一个互斥锁
1
void write_seqlock(seqlock_t *lock);
- 写入锁使用自旋锁实现
1
void write_sequnlock(seqlock_t *lock);
- 写入锁变种
读取-复制-更新
简称RCU,RCU对它可以保护的数据结构做了一些限定,他针对经常发生读取而很少写入的情景做了优化。
1 | 在读取段,代码使用收到RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间。rcu_read_lock调用非常快,会禁止内核抢占。 |
高级字符驱动——程序操作
ioctl
除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。
- 用户空间ioctl
1
2int ioctl(int fd, unsigned long cmd, ...);
通常后面的...代表可变数目的参数表,在实际使用中,系统调用不会真正使用可变数目的参数,而必须是精确定义的原型。使用指针可以向ioctl调用传递任意数据,这样设备可以与用户空间交换任意数量的数据。 - 驱动程序ioctl
1
2int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
如果调用程序没有传递第三个参数,那么驱动程序所接收的arg参数就处在未定义状态。由于对这个附加参数的类型检查被关闭了,所以如果为ioctl传递一个非法参数,编译器是无法报警的,这样,相关联的程序错误就很难被发现。
选择ioctl命令
为了方便程序员创建唯一的ioctl命令号,每一个命令号被分为多个位字段,类型、序号、方向、大小。
- type 幻数
1
选择一个号码,并在整个驱动程序中使用这个号码,8位宽。幻数范围为 0~255 。通常,用英文字符 "A" ~ "Z" 或者 "a" ~ "z" 来表示。设备驱动程序从传递进来的命令获取幻数,然后与自身处理的幻数想比较,如果相同则处理,不同则不处理。幻数是拒绝误使用的初步辅助状态。设备驱动程序可以通过 _IOC_TYPE (cmd) 来获取幻数。不同的设备驱动程序最好设置不同的幻数,但并不是要求绝对,也是可以使用其他设备驱动程序已用过的幻数。
- number 序数
1
顺序编号,也是8位宽。序数用于区别各种命令。通常,从0开始递增,相同设备驱动程序上可以重复使用该值。例如,读取和写入命令中使用了相同的序数,设备驱动程序也能分辨出来,原因在于设备驱动程序区分命令时 使用switch且直接使用命令变量cmd值。创建命令的宏生成的值由多个域组合而成,所以即使是相同的序数,也会判断为不同的命令。
- direction
1
数据传输方向,包括_IOC_NONE(没有数据流通)、_IOC_READ、_IOC_WRITE、_IOC_READ| _IOC_WRITE(双向数据传输)。
- size
1
涉及用户数据大小。
- 解开字段的宏
1
_IOC_DIR(nr)、_IOC_TYPE(nr)、_IOC_NR(nr)、_IOC_SIZE(nr),分别对应:方向、类型(幻数)、序数、大小
返回值
ioctl的实现通常就是一个基于命令号的switch语句。对非法的命令返回-EINVAL。
预定义命令
分为三种:
- 可用于任何文件的命令
1
2
3
4
5FIOCLEX 设置执行时关闭标志
FIONCLEX 清除执行时关闭标志
FIOASYNC 设置或复位文件异步通知
FIOQSIZE 返回文件或目录大小
FIONBIO 该调用修改了filp->f_flags中的O_NONBLOCK标志 - 只用于普通文件的命令
- 特定于文件系统类型的命令
使用ioctl参数
我们要通过函数access_ok验证地址,而不传输数据。
头文件介绍
- linux/module.h
1
包含可装载模块需要的大量符号和函数定义。
- linux/init.h
1
制定初始化和清除函数。
- linux/errno.h
1
定义错误编码(负整数)。
- linux/fs.h
1
用于获得一个或多个设备编号、文件操作等操作。
- linux/cdev.h
1
定义了字符设备的结构与其相关的辅助函数。
- linux/proc_fs.h
1
/proc操作的头文件
- linux/seq_file.h
1
包含四个迭代对象
- asm/semaphore.h
1
信号量的实现
- linux/completion.h
1
包含completion接口
- linux/spinlock.h
1
包含自旋锁原语
- asm/atomic.h
1
提供原子的数据类型
- linux/rcupdate.h
1
包含RCU的代码
- asm/uaccess.h
1
包含access_ok验证操作
细节注意
- (__)
1
经常会在内核API中看到具有两个下划线前缀(_-)的函数名称,这种函数一般是接口的底层组件,双下划线是为了告诉程序员谨慎使用。
- 内核代码不能执行浮点数运算
- MODULE_LICENSE(‘GPL’); 设置许可证为公共许可证
- __user字符串表明指针是一个用户空间地址,不能被直接引用。