第三章介绍了一个简单的字符设备驱动程序,它是一块内存当做设备的。当时的scull还很简单(只是介绍了open release read write四个函数),这一章将进入讲述字符设备驱动程序的一些高级操作。
ioctl函数,从函数名来看的话,io control,看上去是用来操作IO的。更通俗的讲,对于硬件控制的代码应该置于此处。对于当前的scull来说,最常用的对硬件的操作也就是修改当前的量子大小(quantun)和当前的数组大小(qset)了。
在用户空间调用的ioctl函数原型如下:
1 int ioctl(int fd, unsigned long cmd, ...);
三个点的参数看上去有点新奇,它代表着可变数量的参数。然而事实上,这个参数的数目又并非可变,它完全依赖于第二个参数cmd,这个cmd参数代表着传输过来的控制命令,根据控制命令的不同,第三个参数的数目也有不同。多说无益,其实看下这个就明白了。
1 int printf(const char *format,...);
驱动程序的ioctl的函数原型稍有变化:
1 int (*ioctl) (struct inode *inode, struct file *filp,unsigned int cmd, unsigned long arg);
仔细看了之后,其实跟之前的是一样的,文件描述符fb变成了两个重要的文件结构指针,cmd参数毫无变化,arg同样是依赖于cmd的,作为一个ULONG变量,它就可以单纯的作为一个数字,也可以作为一个指针。
Ioclt函数会根据传入的cmd参数来采取不同的处理方式,后面的arg只是在处理这个命令时的一个附加参数。这种模式很容易让人想到使用switch语句。然后,cmd参数以UINT的方式存在,把命令跟数字组合起来,是首先需要完成的工作。换句话说,需要给所有的命令编一个号。
为了让整个系统中的驱动的不同命令不使用一个编号,内核使用了如下的方法来定义命令的号码(有点类似于之前的主设备号和从设备号)。
对于32位的机器,每一个ioctl编号就是一个32位的值,这个值由四个部分组成。
Type:8位,对应一个驱动程序;
Number:8位,对应一个命令;
Dirction:2位,数据传输的方向,00为为没有数据传输,01为写,10为读,11为读写兼顾;
Size:14位,涉及数据的大小。
强烈推荐去看一下内核的ioctl.h源码(它位于include/asm-generic/ioctl.h),这里有这个编号的具体定义以及定义了一堆操作这个编号的宏定义。只要稍微有一些位操作的基础技能把源码看懂的,看了之后收获很大的。
基于这个原则和宏定义,scull定义了一些自己的硬件操作命令,在scull.h中。
这里很多东西都说不太清楚,还是要看源代码。关键在于在ioctl函数中的swit语句中捕捉到特定的cmd之后,要做什么操作就看开发人员自己了,取决于当初定义这个cmd的初衷。宁外,进行这些操作时的一些关键点也值得提出。
首先,如果参数是作为指针传入的,那么对于这个地址的合法性就一定要进行判断。
使用
1 int access_ok(int type, const void *addr, unsigned long size);
函数,如果地址合法就返回1,不合法返回0。
其次,虽然传来了命令,在执行命令的时候要怕段命令的发出者是否具有执行这个命令的权限,系统的一些全下定义在<linux/capability.h>,即便作为开发人员,也是无权修改或者增加新的权限的。具体权限对应的内容需参照源码。于是,在执行一些特殊命令时,需要调用
1 int capable(int capability);
函数来判断调用者得权限是否符合条件,参数为你设定的条件,为系统设定的权限中的一个。
最后,再补充一点的是,在新的内核中,这个ioctl函数已经消失了,
新增加了这样一个函数:
1 long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
参数跟之前的ioctl及其的类似,而之前inode参数也可以通过filp->f_dentry->d_inode 得到。关于这个改动的一些细节我暂时也不是很清楚,只知道什么不使用大内核锁什么什么的。至于其他,就跟之前ioctl一样了,在新的内核下就把函数名改一下就好,其他的不用改。