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
2
3
模块被装入内核时就连接到了内核,因此可以访问内核的公有符号(包括函数和变量)
大多数小规模及中规模应用程序是从头到尾执行单个任务,而模块却只是预先注册自己以便服务于将来的某个请求,然后它的初始化函数就立即结束。
应用程序通过解析外部引用从而使用适当的函数库,模块只能使用内核中的函数。
  • 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
    2
    module-objs := file1.o file2.o
    生成module.ko需要file1.c file2.c
  • make
    1
    2
    make -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许可证下的模块使用。

模块初始化和关闭

  • 初始化
    1
    2
    3
    4
    5
    static int __init init(void)
    {
    // 初始化代码
    }
    module_init(init);
    初始化模块应该被声明为static。在内核代码中还会遇到__devinit和__devinitdata两个,只有在内核未被配置成热拔插时,这两个标记才会被翻译成__init和__initdata。
  • 关闭
    1
    2
    3
    4
    5
    static void __exit exit(void)
    {
    // 初始化代码
    }
    module_exit(exit);
    如果一个模块没有定义清楚函数,则不允许卸载此模块。

初始化过程中的错误处理

  • 通过goto进行清除
  • 通过记录任何已经完成注册的设施,然后出错时只回滚已经完成的步骤

字符型设备驱动程序

编写驱动程序的第一步就是定义驱动程序为用户程序提供的能力。

主设备号和次设备号

设备驱动程序可在/dev下面输入命令ls -l识别,用”c”标识,块设备用”b”标识。
Alt text

如上图,主设备号有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函数。
    Alt text
    1
    first是要分配的设备编号范围的起始值。first的次设备号经常被置为0,但对该函数来讲并不是必需的,count是所请求的连续设备编号的个数,name是和该编号范围关联的设备名称,将出现在/proc/devices和sysfs中。
    Alt text
    1
    常用于动态分配设备编号,在上面这个函数中,dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。firstminor应该是要使用的被请求的第一个次设备号,它通常是0,count和name参数与register_chrdev_region函数是一样的。
  • 模块的清除
    Alt text

文件操作

分配到设备号后,还需要将驱动程序操作连接到这个编号,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结构被初始化如下形式:
    Alt text

  • file、inode结构

    1
    由于内容太多,自行查看《LINUX设备驱动程序第三版》57面。

字符设备的注册

内核内部使用struct cdev结构来表示字符设备。

  • 创建字符设备
    1
    2
    struct 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的数据结构
    1
    int (*open)(struct inode *inode, struct file *flip);
    其中,inode参数在i_cdev字段包含了我们所需要的信息。但是我们通常不需要cdev结构本身,而是需要包含cdev结构的scull_dev结构。
    Alt text

因此,我们需要使用<linux/kernel.h>的container_of宏实现:

1
2
container_of(pointer, container_type, container_filed);
pointer表示i_cdev结构,container_type表示struct scull_dev结构,container_filed表示cdev

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
    因为编译时,预处理器将它和消息文本拼接在一起。
  • 优先级别
    Alt text

当优先级小于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下面的每个文件都绑定一个内核函数,用户读取其中的文件时,该函数动态地生成文件的“内容”。
Alt text

1
2
3
进程读取/proc文件时,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间,该缓冲区会传入到我们定义的函数,该函数如上。其中start参数指示要返回给用户的数据保存在内存页的什么位置。
if(printk_ratelimit())
printk(KERN_NOTICE "The printer is still on fire\n");
  • 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文件时,内核会分配一个内存页,驱动程序可以将数据通过这个内存页返回到用户空间,该缓冲区会传入我们定义的函数。
    Alt text
  • 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
    3
    DECLARE_MUTEX(name);
    DECLARE_MUTEX_LOCKED(name);
    第一个函数初始化一个值为1的信号量name,第二个函数,互斥体的初始状态是锁定的。
  • 初始化
    1
    2
    void init_MUTEX(struct semaphore *sem);
    void init_MUTEX_LOCKED(struct semaphore *sem);
  • P操作
    1
    2
    3
    4
    void down(struct semaphore *sem);
    int down_interruptiable(struct semaphore *sem);
    int down_trylock(struct semaphore *sem);
    可中断的版本几乎是我们始终要使用的版本,它允许等待在某个信号量上的用户空间进程可被用户中断,使用此函数时要注意检查返回值,如果发生错误我们返回-ERESTARTSYS则必须首先撤销已经做出的任何用户可见的修改,系统调用可正确重试,如果无法撤销这些操作,则返回-EINTR。
  • V操作
    1
    2
    void up(struct semaphore *sem);
    调用up后,调用者不再拥有信号量,如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放该信号量。

读取者/写入者信号量

允许多个并发的读取者是可能的,因为只读任务可并行完成他们的工作,而不需要等待其他读取者退出临界区。

1
2
3
4
5
6
7
读取者/写入者信号量“rwsem”
使用void init_rwsem(struct rw_semaphore *sem)初始化
休眠函数:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
down_read_trylock不会在读取访问不可获得时等待,它授予访问时返回非零,其它情况都是返回零。

一个rwsem允许一个写入者或多个读取者拥有信号量,写入者拥有更高的优先级,如果存在大量写入者会存在竞争“饿死”现象。

completion

completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。

  • 创建completion
    1
    2
    3
    4
    DECLARE_COMPLETION(my_completion);
    如果需要动态创建和初始化completion,则使用下面方法:
    struct completion my_completion;
    init_completion(&my_completion);
  • 等待completion
    1
    2
    void wait_for_completion(struct completion *c);
    如果代码调用了wait_for_completion且没有人会完成该任务,则将产生一个不可杀的进程。
  • completion触发
    1
    2
    3
    void complete(struct completion *c);
    void complete_all(struct completion *c);
    complete只会唤醒一个等待线程,complete_all允许唤醒所有等待线程。
    completion机制的典型使用时模块退出时的内核线程终止。

自旋锁

自旋锁不能在休眠的代码中使用。

1
自旋锁是一个互斥设备,它只能由两个值:"锁定"和"解锁",如果锁可用,则锁定位被设置,代码进入临界区。如果锁不可用,则代码进入忙状态并重复检查这个锁,直到锁可用为止,这个循环就是自旋锁的自旋部分。
  • 即使多个线程在给定时间自旋,也只有一个线程可以获得锁。
  • 存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。
  • 自旋锁API
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    spinlock_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. 在必须获得多个锁时,应该始终以相同的顺序获得
  2. 如果我们必须获得一个局部锁,以及一个属于内核更核心的锁,首先应该获取自己的局部锁
    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
    2
    unsigned 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);
  • 写入锁变种
    Alt text

读取-复制-更新

简称RCU,RCU对它可以保护的数据结构做了一些限定,他针对经常发生读取而很少写入的情景做了优化。

1
在读取段,代码使用收到RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间。rcu_read_lock调用非常快,会禁止内核抢占。

高级字符驱动——程序操作

ioctl

除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。

  • 用户空间ioctl
    1
    2
    int ioctl(int fd, unsigned long cmd, ...);
    通常后面的...代表可变数目的参数表,在实际使用中,系统调用不会真正使用可变数目的参数,而必须是精确定义的原型。使用指针可以向ioctl调用传递任意数据,这样设备可以与用户空间交换任意数量的数据。
  • 驱动程序ioctl
    1
    2
    int (*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
    5
    FIOCLEX 设置执行时关闭标志
    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字符串表明指针是一个用户空间地址,不能被直接引用。