從零開始學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結構中的問文件位置值
      
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章