Linux字符设备架构是如何实现的?

文章正文
发布时间:2024-09-11 21:56

3. unlocked_ioctl接口实现

(1)为什么要实现xxx_ioctl ?

前面我们在驱动中已经实现了读写接口,通过这些接口我们可以完成对设备的读写。但是很多时候我们的应用层工程师除了要对设备进行读写数据之外,还希望可以对设备进行控制。例如:针对串口设备,驱动层除了需要提供对串口的读写之外,还需提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息需要从应用层传递一些基本数据,仅仅是数据类型不同。

通过xxx_ioctl函数接口,可以提供对设备的控制能力,增加驱动程序的灵活性。

(2)如何实现xxx_ioctl函数接口?

增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。

kernel 2.6.35 及之前的版本中struct file_operations 一共有3个ioctl :ioctl,unlocked_ioctl和compat_ioctl 现在只有unlocked_ioctl和compat_ioctl 了

在kernel 2.6.36 中已经完全删除了struct file_operations 中的ioctl 函数指针,取而代之的是unlocked_ioctl 。

·         2.6.36 之前的内核

long (ioctl) (struct inode node ,struct file* filp, unsigned int cmd,unsigned long arg)

·         2.6.36之后的内核

long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)

参数cmd: 通过应用函数ioctl传递下来的命令

先来看看应用层的ioctl和驱动层的xxx_ioctl对应关系:

<1>应用层ioctl参数分析

int ioctl(int fd, int cmd, ...);
参数:
@fd:打开设备文件的时候获得文件描述符
@ cmd:第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一
@第三个参数: "..."在C语言中,很多时候都被理解成可变参数。
返回值
      成功:0
      失败:-1,同时设置errno

小贴士:

当我们通过ioctl调用驱动层xxx_ioctl的时候,有三种情况可供选择:
1: 不传递数据给xxx_ioctl
2: 传递数据给xxx_ioctl,希望它最终能把数据写入设备(例如:设置串口的波特率)
3: 调用xxxx_ioctl希望获取设备的硬件参数(例如:获取当前串口设备的波特率)
这三种情况中,有些时候需要传递数据,有些时候不需要传递数据。在C语言中,是
无法实现函数重载的。那怎么办?用"..."来欺骗编译器了,"..."本来的意思是传
递多参数。在这里的意思是带一个参数还是不带参数。
参数可以传递整型值,也可以传递某块内存的地址,内核接口函数必须根据实际情况
提取对应的信息。

<2>驱动层xxx_ioctl参数分析

long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
参数:
@file:   vfs层为打开字符设备文件的进程创建的结构体,用于存放文件的动态信息
@ cmd: 用户空间传递的命令,可以根据不同的命令做不同的事情
@第三个参数: 用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),也可能是一个数值,也可能没值
返回值
      成功:0
      失败:带错误码的负值

<3>如何确定cmd 的值。

该值主要用于区分命令的类型,虽然我只需要传递任意一个整型值即可,但是我们尽量按照内核规范要求,充分利用这32bite的空间,如果大家都没有规矩,又如何能成方圆?

现在我就来看看,在Linux 内核中这个cmd是如何设计的吧!

具体含义如下:

设备类型类型或叫幻数,代表一类设备,一般用一个字母或者1个8bit的数字序列号代表这个设备的第几个命令方 向表示是由内核空间到用户空间,或是用户空间到内核空间,入:只读,只写,读写,其他数据尺寸表示需要读写的参数大小

由上可以一个命令由4个部分组成,每个部分需要的bite都不完全一样,制作一个命令需要在不同的位域写不同的数字,Linux 系统已经给我们封装好了宏,我们只需要直接调用宏来设计命令即可。

在这里插入图片描述

通过Linux 系统给我们提供的宏,我们在设计命令的时候,只需要指定设备类型、命令序号,数据类型三个字段就可以了。

Linux 系统中已经设计了一场用的命令,可以通过查阅Linux 源码中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已经被使用过了。

<4> 如何检查命令?

可以通过宏_IOC_TYPE(nr)来判断应用程序传下来的命令type是否正确;

可以通过宏_IOC_DIR(nr)来得到命令是读还是写,然后再通过宏access_ok(type,addr,size)来判断用户层传递的内存地址是否合法。

使用方法如下:

if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
   pr_err("cmd   %u,bad magic 0x%x/0x%x.",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
   return-ENOTTY;
 }
 if(_IOC_DIR(cmd)&_IOC_READ)
   ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
 else if( _IOC_DIR(cmd)&_IOC_WRITE )
   ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
 if(ret){
   pr_err("bad   access %ld.",ret);
   return-EFAULT;
 }

5 注册cdev

定义好file_operations结构体,就可以通过函数cdev_init()、cdev_add()注册字符设备驱动了。

实例如下:

static struct cdev cdev;
cdev_init(&cdev,&hello_ops);
error = cdev_add(&cdev,devno,1);

注意如果使用了函数register_chrdev(),就不用了执行上述操作,因为该函数已经实现了对cdev的封装。

五、实例

千言万语,全部汇总在这一个图里,大家可以对照相应的层次来学习。

六、实例

好了,现在我们可以来实现一个完整的字符设备框架的实例,包括打开、关闭、读写、ioctrl、自动创建设备节点等功能。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include "dev_fifo_head.h"
//指定的主设备号
#define   MAJOR_NUM 250
//自己的字符设备
struct mycdev

   int len;
   unsigned   char buffer[50];
   struct   cdev cdev;
};
MODULE_LICENSE("GPL");
//设备号
static dev_t   dev_num = {0};
//全局gcd
struct mycdev *gcd;
//设备类
struct class *cls;
//获得用户传递的数据,根据它来决定注册的设备个数
static int ndevices = 1;
module_param(ndevices, int, 0644);
MODULE_PARM_DESC(ndevices, "The number of devices for register.");
//打开设备
static int dev_fifo_open(struct   inode *inode,   struct file *file)

   struct   mycdev *cd;  
   printk("dev_fifo_open   success!");  
   //用struct file的文件私有数据指针保存struct mycdev结构体指针
   cd   = container_of(inode->i_cdev,struct   mycdev,cdev);
   file->private_data =   cd;  
   return   0;

//读设备
static ssize_t   dev_fifo_read(struct file *file, char   __user *ubuf,   size_t
size, loff_t *ppos)

   int n;
   int ret;
   char   *kbuf;
   struct   mycdev *mycd =   file->private_data;
   printk("read *ppos :   %lld",*ppos);
   if(*ppos == mycd->len)
       return   0;
   //请求大大小 > buffer剩余的字节数   :读取实际记得字节数
   if(size > mycd->len - *ppos)
       n = mycd->len - *ppos;
   else
       n = size;
   printk("n =   %d",n);
   //从上一次文件位置指针的位置开始读取数据
   kbuf   = mycd->buffer   + *ppos;
   //拷贝数据到用户空间
   ret   = copy_to_user(ubuf,kbuf, n);
   if(ret != 0)
       return   -EFAULT;
   //更新文件位置指针的值
   *ppos += n;
   printk("dev_fifo_read   success!");
   return   n;

//写设备
static ssize_t   dev_fifo_write(struct file *file, const char __user *ubuf,size_t size, loff_t *ppos)

   int n;
   int ret;
   char   *kbuf;
   struct   mycdev *mycd =   file->private_data;
   printk("write *ppos :   %lld",*ppos);
   //已经到达buffer尾部了
   if(*ppos == sizeof(mycd->buffer))
      return   -1;
   //请求大大小 > buffer剩余的字节数(有多少空间就写多少数据)
   if(size > sizeof(mycd->buffer) - *ppos)
       n = sizeof(mycd->buffer) - *ppos;
   else
       n = size;