从零开始学Linux设备驱动--(7)高级Io操作

高级I/o操作

一、ioctl设备操作

  • 除了之前提到的对设备的读写操作(read、write接口来实现),内核将对设备的控制操作委派给ioctl接口,ioctl也是一个系统调用

    int iooctl(int d,int request,...);
    @ d :要操作的文件描述符
    @ request :代表不同操作的数字值,遵循一定的规则
    @ ... :C语言中可变参数函数的声明,表示第三个参数可有可无。
    

    这个是在应用层的系统调用函数,和read、write等系统调用一样,其对应的驱动接口函数式unlocked_ioctl和compat_ioctl。compat_ioctl是为了处理32位程序和64位内核兼容的一个函数接口,和体系结构相关。重点来看一下比较常见的unlocked_ioctl。

    long (*unlocked_ioctl)(struct file*,unsigned int,unsigned long);
    @ 参数一 :打开的FILE结构指针
    @ 参数二 :和request对应
    @ 参数三 :对应系统调用ioctl的第三个参数
    
  • 前面提到ioctl的request参数遵循一定的规则,现在来看一下它的规则是什么。内核源码中,其遵循如下的格式:

    比特位	 含义
    31-30  00 - 命令不带参数
           10 - 命令需要从驱动中获取数据,读方向
           01 - 命令需要把数据写入驱动,写方向
           11 - 命令既要写入数据又要获取数据,读写双向
           
    29-16  若命令待参数,则指定参数所占用的内存空间大小
    
    15-8   每个驱动全局唯一的幻数
        
    7-0    命令码
    

    以上内容来自内核文档"document/ioctl/ioctl-decoding.txt",可以看到这个request的组成还是相当复杂的,要让我们从新去组成一个命令,肯定要费不少的功夫。但是呢,Linux 内核的开发人员早就为我们想到了,他们定义了一系列的宏用于定义、提取ioctl系统调用命令中的字段信息。
    在这里插入图片描述

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

    在这里需要注意的时候,我们不能随便指定的,我们最终设计的命令应该是Linux系统中没有的命令才符合规范。那怎么知道Linux系统中已经设计了哪些命令呢?可以通过查阅Linux 源码中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已经被使用过了

    举个例子:如果我们构造一个自己的命令,根据前面的命令构造格式。

    1.该命令是写入驱动的,故31-30bit应该是01
    2.我们要写入的参数为int型,其所占据的内存大小为4字节,则29-16bit应写4
    3.我们指定幻数为字母'c'
    4.指定命令码为1
    --------------------------------------------------
    我们用__IOC来实现,得到如下:
    __IOC(1,'c',0,8)
    用更简单的__IOW来实现,我们一般也是用这个宏
    __IOW('c',2int)
    

二、proc文件操作

  • proc文件系统是一种伪文件系统,这种文件系统不存在于磁盘上,只存在于内存中,只有在内核运行时才会动态生成里面的内容。
  • 这个文件系统通常是挂载在/proc目录下,是内核开发者向用户导出信息的常用方式。
  • 之前驱动开发者会经常使用这种方式对驱动进行调试,但是现在而言,随着该文件系统越来越复杂。现在更能推荐使用sysfs文件系统,或者编写应用程序的方式。
  • 一般我们会直接通过访问该目录下文件来读取当前驱动的一些参数或者想里面写入以及改动一些参数。这一般在驱动中来实现,因为我们可以在驱动所在目录下新建一个文件,驱动中可以对该文件进行解析以及打印驱动信息到该文件中。

三、IO操作

  1. 非阻塞IO
  • 设备不一定随时都能够给用户提供服务,这就有了资源的可用与不可用两种状态。比如,我们想用电脑上的COM8这个串口来打印我们调试的log信息,那么另一进程此时又正想调用它来打印其他的一些东西,此时就会产生冲突。

  • 应用程序和驱动程序一起就组成了多种IO模型,假设应用程序配置的的是非阻塞的方来打开设备文件,此时当资源不可用时,驱动就应该立即返回,并用一个错误码来通知应用程序,资源不可用,应用程序可稍后再做尝试。

    fd = open("/dev/hello",O_RDWR|O_NONBLOCK);
    //O_NONBLOCK就表示以非阻塞的方式打开设备文件
    
  1. 阻塞型IO
  • 看了非阻塞型IO,再来看阻塞型IO就很好来理解了。简而言之,当我们一阻塞的方式打开设备文件的时候(一般我们默认打开方式为阻塞方式),如果资源不可用那么进程会进入休眠。
  • 那么这中方式具体是怎么做的呢?简单来说,当进程发现资源不可用时,就会主动将自己的状态设置为TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE,然后就将自己假如一个驱动所维护的等待队列中,最后调用schdule主动放弃CPU,操作系统将之从运行队列上移除,并调度其他进程执行。当然了,这是简化之后的步骤。这种方式更利用内核来处理,所以一般这中方式呗设置为默认方式的原因。
  • 相比于非阻塞型IO来说这种方式来说这种方式不会占用CPU的时间,而非阻塞方式会定期去看下资源是否可用。当然了,阻塞方式用完后is有缺点的,那就是当进程进入休眠状态后,进程无法做其他事情。
  • 那么既然有休眠,自然就有唤醒了。当资源可用时,驱动会负责去唤醒该进程。另外,我们也可以指定进程的最长休眠时间,超时后进程自动苏醒。
  1. IO多路复用
  • 在阻塞型IO中,我们了解了阻塞型IO的缺点:当进程进入休眠状态后,进程无法做其他事情。那么如果我们的进程有被设定为需要去访问多个设备的时候,假设其中一个处于被占用状态,那么此进程进入休眠,自然也就无法访问其他的设备资源了。在应用层中我们一般有select、poll以及Linux所特有的epoll三种方式。下面以poll为例,我们来看下:

    int poll(struct pollfd *fds,nfds_t nfds,int 
    timeout);
    @ 参数1:要监听的文件描述符的集合
    @ 参数2:要监听的文件描述符的个数
    @ 参数3:超时值,以毫秒为单位
    
    struct pollfd{
        int fd;	//要监听的文件描述符
        short events;	//要监听的事件
        short revents;	//返回的事件
    };
    
    //event在内核中有定义相关的宏,常见event宏
    POLLIN		//可进行读操作
    POLLOUT		//可进行写
    POLLRDNORM	//等价于POLLIN
    POLLWRNORM	//等价于POLLOUT
    
  1. 异步IO
  • 异步IO是POSIX定义的一组标准接口,Linux也支持。相对于前面的几种IO模型,异步IO在提交IO操作请求后就立即返回,程序不需要等到IO曹组完成后再去做别的事情,具有非阻塞的特性。当底层IO操作完成后,可以提交者发信号,或者调用注册调用的回调函数,告知请求者IO操作已完成。

  • 调用者只发起IO操作的请求然后立即返回,程序向可以去做别的事情。具体的IO操作在驱动中完成,驱动中可能会被阻塞,也可能不会被阻塞。当驱动中IO操作完成后,调用者会得到通知,通常是内核向调动向调用者发送信号。或者自动套用调用者注册的回调函数,通知操作是由内核完成的,而不是驱动本身。

  • 对前面提到的几种IO模式做一个总结:

    阻塞 非阻塞
    同步 阻塞IO 非阻塞IO
    异步 IO多路复用 异步IO
  1. 异步通知
  • 异步通知类似于前面提到的异步IO,只是当设备资源可用时,它才想向应用层发信号。而不能直接调用应用层注册的回调函数,并且发信号的操作也是驱动程序自身来完成的。

  • 和前面的应用程序主动发起IO请求不同,异步通知是驱动主动通知应用程序,再有应用程序来发起访问。这个过程和中断是非常像的,信号其实相当于应用层的中断。

  • 应用层异步通知实现的步骤:

    1. 注册信号处理函数(相当于注册中断处理函数)
    2. 打开设备文件,设置文件属主。(目的是驱动打开file结构,找到对应的进程,从而向该进程发送信号)
    3. 设置设备资源可用时驱动向进程发送的信号(非必须步骤)
    4. 设置文件的FASYNC标志,使能异步通知机制,这相当于打开中断使能位。
  • 驱动层异步通知操作

    1. 构造struct faync_struct链表的头。
    2. 实现fasync接口函数,调用fasync_helper函数来构造struct fasync_struct节点,并加入链表。
    3. 在资源可用时,调用kill_fasync发送信号,并设置资源可用类型是可读还是可写。
    4. 在文件最后一次关闭时,即在release接口中,需要显示调用驱动实现fasync接口函数,将节点从链表中删除,这样进程就不会在接收到信号。
  1. mmap设备文件操作

    • 有时候我们需要从用户空间复制大量的数据到内核空间,或者相反。比如我们的显卡,需要显示图像,此时我们需要从用户空间拷贝大量的数据给到内核空间,最后才能显示到屏幕上。试想,若是这样操作是否会给显卡的性能带来很大的损耗呢?有没有更好的方法?

    • 字符设备驱动提供了一个mmap接口,**可以把内核空间中的那片内存所对应的物理地址空间再次映射到用户空间,这样一个物理内存就有了两份映射。一个在内核空间,一个在用户空间。**这样就可以通过直接操作用户空间的这片映射之后的内存来直接访问物理内存,从而提高效率。

    • mmap接口的实现(驱动中的接口):

      int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn,unsigned long size,pgprot_t port);
      @vma: 描述一片映射区域的结构指针
      @addr: 用户指定的映射之后的虚拟起始地址,若用户未指定则会有内核来指定。
      @pfn: 物理内存所对应的页框号,就是将物理 地址除以页大小得到的值
      @size: 想要映射的空间的大小。
      

      在这里插入图片描述

  2. 定位操作

    • 对于支持随机访问的设备文件,访问的问阿金位置可以由用户来指定,并且对于读写这类操作,下一次访问的文件位置将会紧接在上一次访问结束的位置之后,上面模拟的虚拟显卡设备并不支持这一操作。

    • 文件对用户的抽象是一段线性存储的的数据,那么可以把文件看成一个数组,每个数组元素占一个字节。

    • 我们知道在应用程序中我们一般使用llseek来定位文件,其在file_operation结构中对应接口如下:

      loff_t (*llseek)(struct file *,loff_t ,int);
      @参数一: 指向打开的file结构
      @参数二: 参数偏移量
      @参数三: 位置
      功能:根据传入的参数来调整保存在file结构中的问文件位置值
      
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章