高级IO操作—ioctl设备操作

x33g5p2x  于2022-05-13 转载在 其他  
字(9.3k)|赞(0)|评价(0)|浏览(512)

为了处理设备非数据的操作(这些可以通过read、write接口来实现),内核将对设备的控制操作委派给了ioctl接口,ioctl 也是一个系统调用,其函数原型如下。

  1. int ioctl(int d,int request,...);

d是要操作文件的文件描述符,request 是代表不同操作的数字值,比如驱动可以规定0x12345678表示点灯,而0x12345679表示灭灯等。但是这个操作码,更确切地说是命令,应该具有一定的编码规则,这个我们在后面会介绍。...表示C语言中实参个数可变的.但在这里表示的是第三个参数可有可无

比如对于刚才的LED例子,第三个参数可以用于指定将哪个LED点亮或熄灭,0表示LEDO,1表示LED1等。

因为第三个形参是unsigned long 类型的,所以除了可以传递数字值,还可以传递一个指针,这样就可以和内核空间交互任意多个字节的数据。

查看前面的file_operations结构的定义。和ioctl系统调用对应的驱动接口函数是unlocked_ ioctl 和compat_ioctl, compat_ ioctl 是为了处理32位程序和64位内核兼容的一个函数接口,和体系结构相关。unlocked_ioctl 的函数原型如下。

  1. long (*unlocked ioct1) (struct file*,unsigned int, unsigned long) ;

第一个参数表示打开的文件的file 结构指针,第二个参数和系统调用的第二个参数request对应,第三个参数对应系统调用函数的第三个参数。

还要说明的是,在之前的内核版本中,同ioctl系统调用的驱动接口也是ioctl,但是最近的内核废除了该接口。
因为之前的ioctl接口在调用之前要获得大内核锁(BLK, 一种全局的粗粒度锁),如果ioctl 的执行时间过长,则会导致内核其他也需要大内核锁的代码需
要延迟很长时间,严重降低了效率(关于锁的机制,本书后面的章节会详细讨论)。

之前说到用于ioctl 的命令需要遵从一种编码规则(0x12345678表示点灯,而0x12345679表示灭灯等), 那么这个编码规则是怎样的呢?在当前的内核源码版本中,命令按照以下方式组成。

上述内容可以看内核文档ioctl-decoding.txt

  1. To decode a hex IOCTL code:
  2. Most architectures use this generic format, but check
  3. include/ARCH/ioctl.h for specifics, e.g. powerpc
  4. uses 3 bits to encode read/write and 13 bits for size.
  5. bits meaning
  6. 31-30 00 - no parameters: uses _IO macro
  7. 10 - read: _IOR
  8. 01 - write: _IOW
  9. 11 - read/write: _IOWR
  10. 29-16 size of arguments
  11. 15-8 ascii character supposedly
  12. unique to each driver
  13. 7-0 function #
  14. So for example 0x82187201 is a read with arg length of 0x218,
  15. character 'r' function 1. Grepping the source reveals this is:
  16. #define VFAT_IOCTL_READDIR_BOTH _IOR('r', 1, struct dirent [2])

一个命令由四部分组成,每部分有规定的意义和位宽限制。

之所以这样定义命令,而不是简单地用0,1,2, 来定义命令,是为了避免命令定义的重复,从而导致应用程序误操作,把一个命令发送给本不应该执行它的驱动程序,而驱动程序又错误地执行了这个命令。

采用这种机制,使得驱动有机会来检查这个命令是否属于驱动,从一定程度上避免了这种问题的发生。理想的要求是比特15位到比特8位所定义的幻数在一种体系结构下是全局唯一的,但很显然,这很难做到。尽管如此,我们还是应该遵从内核所规定的这种命令定义形式。

内核提供了一组宏来定义、提取命令中的字段信息。

  1. #define _IOC(dir,type,nr,size)\
  2. ((dir) << _IOC_DIRSHIFT) | \
  3. ((type) << _IOC_TYPESHIFT | \
  4. ((nr) << _IOC_NRSHIFT | \
  5. ((size) << _IOC_SIZESHIFT))
  6. #ifndef __KERNRL__
  7. #define _IOC_TYPECHECK(t) (SIZEOF(T))
  8. #endif
  9. #define _IO(type,nr) IOC(_IOC_NONE,(type),(nr),0)
  10. #define _IOR(type,nr,size) IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
  11. #define _IOW(type,nr,size) IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
  12. #define _IOWR(type,nr,size) IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
  13. #define _IOC_DIR(nr) ((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
  14. #define _IOC_TYPE(nr) ((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
  15. #define _IOC_NR(nr) ((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
  16. #define _IOC_SIZE(nr) ((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

定义命令使用的是最底层的宏_IOC,它将4个部分通过移位合并在一起。

假如要定义一个设置串口帧格式的命令,那么按照前面的规则,这个命令要带参数,并且是将数据写入到驱动,则最高两个比特是01。如果要写入的参数是一个struct option的结构,而结构占12个字节,那么比特29到比特16的10进制值应该是12。如果定义幻数为字母s,命令码为2,最终就应使用_10C(1,‘s’,0,12)来定义该命令。

不过内核还提供了更方便的宏,刚才那个命令可以通过_IOW('s' 2,struct option)来定义。另外还有4个宏IOC_DIR、_ IOC_ TYPE、_IOC_ NR和IOC_ SIZE来分别提取命令中的4个部分。

在实现unlocked_ ioctl 接口函数之前,我们还要来看看ioctl系统调用的过程。相关代码如下。

部分代码示例:

  1. static long vfs_ioctl(struct file *filp, unsigned int cmd,
  2. unsigned long arg)
  3. {
  4. int error = -ENOTTY;
  5. if (!filp->f_op->unlocked_ioctl)
  6. goto out;
  7. error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
  8. if (error == -ENOIOCTLCMD)
  9. error = -ENOTTY;
  10. out:
  11. return error;
  12. }
  13. ...........
  14. int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
  15. unsigned long arg)
  16. {
  17. int error = 0;
  18. int __user *argp = (int __user *)arg;
  19. struct inode *inode = file_inode(filp);
  20. switch (cmd) {
  21. case FIOCLEX:
  22. set_close_on_exec(fd, 1);
  23. break;
  24. case FIONCLEX:
  25. set_close_on_exec(fd, 0);
  26. break;
  27. case FIONBIO:
  28. error = ioctl_fionbio(filp, argp);
  29. break;
  30. case FIOASYNC:
  31. error = ioctl_fioasync(fd, filp, argp);
  32. break;
  33. case FIOQSIZE:
  34. if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) ||
  35. S_ISLNK(inode->i_mode)) {
  36. loff_t res = inode_get_bytes(inode);
  37. error = copy_to_user(argp, &res, sizeof(res)) ?
  38. -EFAULT : 0;
  39. } else
  40. error = -ENOTTY;
  41. break;
  42. case FIFREEZE:
  43. error = ioctl_fsfreeze(filp);
  44. break;
  45. case FITHAW:
  46. error = ioctl_fsthaw(filp);
  47. break;
  48. case FS_IOC_FIEMAP:
  49. return ioctl_fiemap(filp, arg);
  50. case FIGETBSZ:
  51. return put_user(inode->i_sb->s_blocksize, argp);
  52. default:
  53. if (S_ISREG(inode->i_mode))
  54. error = file_ioctl(filp, cmd, arg);
  55. else
  56. error = vfs_ioctl(filp, cmd, arg);
  57. break;
  58. }
  59. return error;
  60. }
  61. SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
  62. {
  63. int error;
  64. struct fd f = fdget(fd);
  65. if (!f.file)
  66. return -EBADF;
  67. error = security_file_ioctl(f.file, cmd, arg);
  68. if (!error)
  69. error = do_vfs_ioctl(f.file, fd, cmd, arg);
  70. fdput(f);
  71. return error;
  72. }

sys_ ioctl 函数首先调用了security_ file_ ioctl, 然后调用了do_ vfs_ ioctl, 在do_vfs_ ioctl中先对一些特殊的命令进行了处理,再调用vfs_ ioctl, 在vfs_ ioctl 中最后调用了驱动的unlocked_ ioctl。

之所以要来看这个系统调用的过程,是为了让读者明白,在我们的驱动解析这些命令之前已经有内核的代码来处理这些命令了,如果我们的命令定义和这些命令一样,那么我们驱动中的unlocked_ ioctl 就永远不会得到调用了。这些命令(如FIOCLEX等)的定义,请读者参阅内核源码,在此不详细列出了。

经过前面的介绍,读者可能已经知道unlocked_ ioctl 接口函数的实现形式就
是一个大的switch语句,如同do_vfs_ioctl一样。

下面就是将前面的虚拟串口驱动添加unlocked_ioctl接口后的完整代码
vser.h

  1. #ifndef _VSER_H
  2. #define _VSER_H
  3. struct option {
  4. unsigned int datab;
  5. unsigned int parity;
  6. unsigned int stopb;
  7. };
  8. #define VS_MAGIC 's'
  9. #define VS_SET_BAUD _IOW(VS_MAGIC, 0, unsigned int)
  10. #define VS_GET_BAUD _IOW(VS_MAGIC, 1, unsigned int)
  11. #define VS_SET_FFMT _IOW(VS_MAGIC, 2, struct option)
  12. #define VS_GET_FFMT _IOW(VS_MAGIC, 3, struct option)
  13. #endif

先定义了一个结构类型struct option,包括波特率、奇偶效验、停止位,然后定义了设置波特率、获得波特率、设置帧格式、获取帧格式,共4个函数命令

vser.c

  1. #include <linux/init.h>
  2. #include <linux/kernel.h>
  3. #include <linux/module.h>
  4. #include <linux/fs.h>
  5. #include <linux/cdev.h>
  6. #include <linux/kfifo.h>
  7. #include <linux/ioctl.h>
  8. #include <linux/uaccess.h>
  9. #include "vser.h"
  10. #define VSER_MAJOR 256
  11. #define VSER_MINOR 0
  12. #define VSER_DEV_CNT 1
  13. #define VSER_DEV_NAME "vser"
  14. struct vser_dev {
  15. unsigned int baud;
  16. struct option opt;
  17. struct cdev cdev;
  18. };
  19. DEFINE_KFIFO(vsfifo, char, 32);
  20. static struct vser_dev vsdev;
  21. static int vser_open(struct inode *inode, struct file *filp)
  22. {
  23. return 0;
  24. }
  25. static int vser_release(struct inode *inode, struct file *filp)
  26. {
  27. return 0;
  28. }
  29. static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
  30. {
  31. int ret;
  32. unsigned int copied = 0;
  33. ret = kfifo_to_user(&vsfifo, buf, count, &copied);
  34. return ret == 0 ? copied : ret;
  35. }
  36. static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
  37. {
  38. int ret;
  39. unsigned int copied = 0;
  40. ret = kfifo_from_user(&vsfifo, buf, count, &copied);
  41. return ret == 0 ? copied : ret;
  42. }
  43. static long vser_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
  44. {
  45. if (_IOC_TYPE(cmd) != VS_MAGIC)
  46. return -ENOTTY;
  47. switch (cmd) {
  48. case VS_SET_BAUD:
  49. vsdev.baud = arg;
  50. break;
  51. case VS_GET_BAUD:
  52. arg = vsdev.baud;
  53. break;
  54. case VS_SET_FFMT:
  55. if (copy_from_user(&vsdev.opt, (struct option __user *)arg, sizeof(struct option)))
  56. return -EFAULT;
  57. break;
  58. case VS_GET_FFMT:
  59. if (copy_to_user((struct option __user *)arg, &vsdev.opt, sizeof(struct option)))
  60. return -EFAULT;
  61. break;
  62. default:
  63. return -ENOTTY;
  64. }
  65. return 0;
  66. }
  67. static struct file_operations vser_ops = {
  68. .owner = THIS_MODULE,
  69. .open = vser_open,
  70. .release = vser_release,
  71. .read = vser_read,
  72. .write = vser_write,
  73. .unlocked_ioctl = vser_ioctl,
  74. };
  75. static int __init vser_init(void)
  76. {
  77. int ret;
  78. dev_t dev;
  79. dev = MKDEV(VSER_MAJOR, VSER_MINOR);
  80. ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
  81. if (ret)
  82. goto reg_err;
  83. cdev_init(&vsdev.cdev, &vser_ops);
  84. vsdev.cdev.owner = THIS_MODULE;
  85. vsdev.baud = 115200;
  86. vsdev.opt.datab = 8;
  87. vsdev.opt.parity = 0;
  88. vsdev.opt.stopb = 1;
  89. ret = cdev_add(&vsdev.cdev, dev, VSER_DEV_CNT);
  90. if (ret)
  91. goto add_err;
  92. return 0;
  93. add_err:
  94. unregister_chrdev_region(dev, VSER_DEV_CNT);
  95. reg_err:
  96. return ret;
  97. }
  98. static void __exit vser_exit(void)
  99. {
  100. dev_t dev;
  101. dev = MKDEV(VSER_MAJOR, VSER_MINOR);
  102. cdev_del(&vsdev.cdev);
  103. unregister_chrdev_region(dev, VSER_DEV_CNT);
  104. }
  105. module_init(vser_init);
  106. module_exit(vser_exit);
  107. MODULE_LICENSE("GPL");
  108. MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
  109. MODULE_DESCRIPTION("A simple character device driver");
  110. MODULE_ALIAS("virtual-serial");

好长,分析一下咱们没学过的:
在vser.c文件中,代码

  1. struct vser_dev {
  2. unsigned int baud;
  3. struct option opt;
  4. struct cdev cdev;
  5. };

定义了一个vser_dev结构,将波特率、帧格式信息同cdev包含在了一起。

相应的,在init函数中,初始化这些信息

  1. vsdev.baud = 115200;
  2. vsdev.opt.datab = 8;
  3. vsdev.opt.parity = 0;
  4. vsdev.opt.stopb = 1;

代码

  1. .unlocked_ioctl = vser_ioctl,

这里在file_operations结构vser_ops中添加vser_ioctl接口,实现的函数是vser_ioctl

vser_ ioctl 和我们预期的是一致的,
首先通过IOC_ TYPE宏提取出命令中的幻数字段,然后和预定义的幻数进行比较,如果··不匹··配则返回-ENOTTY,表示参数不对(用-ENOTTY表示这一错误,是历史原因造成的);如果··匹配··则根据命令进行相应的操作。

在这里特意演示了第三个参数的两种使用方法,第一种方法是直接传数据,如波特率。第二种方法是传指针,如帧格式。

在帧格式的设置和获取上使用了copy_ to_usercopy_from_user 两个函数,它们的函数原型如下。

  1. unsigned long __must_check copy_to_user(void __user *to,const void *from,unsigned long n);
  2. unsigned long __must_check copy_from_user(void *to,const void __user *from,unsigned long n);

__must_check要求必须检查函数返回值,to是目的内存地址,from是原内存地址,n是期望复制的字节数

这两个函数都返回没有复制成功的字节数,全部成功返回0.

没有用memcpy函数是因为这两个函数用了access_ok来验证用户空间是否真实可读写,避免了在内核中的缺页故障了带来的问题。

这两个函数还可能使进程休眠。如果只是复制简单的数据类型(char、int、short等),可以使用get_user和put_user宏

  1. get_user(x,p)
  2. put_user(x,p)
  3. int ret = 0x123456;
  4. int val;
  5. put_user(ret,(int __user *)arg);
  6. get_user(val,(int __user *)arg);

原型是这样的,与前面的函数性质类似,复制字节

测试:
test.c

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <sys/ioctl.h>
  6. #include <fcntl.h> // for open
  7. #include <unistd.h> // for close
  8. #include <errno.h>
  9. #include "vser.h"
  10. int main(int argc, char *argv[])
  11. {
  12. int fd;
  13. int ret;
  14. unsigned int baud;
  15. struct option opt = {8,1,1};
  16. fd = open("/dev/vser0", O_RDWR);
  17. if (fd == -1)
  18. goto fail;
  19. baud = 9600;
  20. ret = ioctl(fd, VS_SET_BAUD, baud);
  21. if (ret == -1)
  22. goto fail;
  23. ret = ioctl(fd, VS_GET_BAUD, baud);
  24. if (ret == -1)
  25. goto fail;
  26. ret = ioctl(fd, VS_SET_FFMT, &opt);
  27. if (ret == -1)
  28. goto fail;
  29. ret = ioctl(fd, VS_GET_FFMT, &opt);
  30. if (ret == -1)
  31. goto fail;
  32. printf("baud rate: %d\n", baud);
  33. printf("frame format: %d%c%d\n", opt.datab, opt.parity == 0 ? 'N' : \
  34. opt.parity == 1 ? 'O' : 'E', \
  35. opt.stopb);
  36. close(fd);
  37. exit(EXIT_SUCCESS);
  38. fail:
  39. perror("ioctl test");
  40. exit(EXIT_FAILURE);
  41. }

是求baud和frame format的

相关文章