linux設備驅動程序--在用戶空間註冊文件接口

linux字符設備驅動程序--建立設備節點

基於4.14內核,運行在beagleBone greenhtml

在上一講中,咱們寫了第一個linux設備驅動程序——hello_world,在驅動程序中,咱們什麼也沒有作,僅僅是打印了兩條日誌消息,今天,咱們就要豐富這個設備驅動程序,在/dev目錄下建立一個設備節點,用戶經過讀寫文件來與內核進行交互。node

預備知識

在linux中,一切皆文件,無論用戶是控制某個外設又或者是操做I/O,都是經過文件實現。react

設備驅動程序被裝載在內核中運行,當用戶程序須要使用對應設備時,天然不可能直接訪問內核空間,那麼用戶程序應該怎麼作呢?linux

答案是內核將設備驅動程序操做接口以文件接口的形式導出到用戶空間,通常爲相應的設備在/dev目錄下創建相應的操做接口文件,自linux2.6內核版本以來,內核還會在系統啓動時建立sysfs文件系統,內核一樣能夠將設備操做接口導出到/sys目錄下。shell

舉個例子:在開發一款溫度傳感器時,內核驅動模塊能夠在驅動程序中實現傳感器的初始化,而後在/dev目錄下建立對應文件,關聯/dev的讀寫回調函數,當用戶訪問/dev下相應文件時,就會調用相應的回調函數,執行設備的操做。數組

下面咱們就演示如何在/dev目錄下建立一個設備節點。安全

程序實現

#include <linux/init.h>  
#include <linux/module.h>
#include <linux/device.h>  
#include <linux/kernel.h>  
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_AUTHOR("Downey");
MODULE_LICENSE("GPL");

static int majorNumber = 0;
/*Class 名稱,對應/sys/class/下的目錄名稱*/
static const char *CLASS_NAME = "basic_class";
/*Device 名稱,對應/dev下的目錄名稱*/
static const char *DEVICE_NAME = "basic_demo";

static int basic_open(struct inode *node, struct file *file);
static ssize_t basic_read(struct file *file,char *buf, size_t len,loff_t *offset);
static ssize_t basic_write(struct file *file,const char *buf,size_t len,loff_t* offset);
static int basic_release(struct inode *node,struct file *file);


static char msg[] = "Downey!";
static char recv_msg[20];

static struct class *basic_class = NULL;
static struct device *basic_device = NULL;

/*File opertion 結構體,咱們經過這個結構體創建應用程序到內核之間操做的映射*/
static struct file_operations file_oprts = 
{
    .open = basic_open,
    .read = basic_read,
    .write = basic_write,
    .release = basic_release,
};


static int __init basic_init(void)
{
    printk(KERN_ALERT "Driver init\r\n");
    /*註冊一個新的字符設備,返回主設備號*/
    majorNumber = register_chrdev(0,DEVICE_NAME,&file_oprts);
    if(majorNumber < 0 ){
        printk(KERN_ALERT "Register failed!!\r\n");
        return majorNumber;
    }
    printk(KERN_ALERT "Registe success,major number is %d\r\n",majorNumber);

    /*以CLASS_NAME建立一個class結構,這個動做將會在/sys/class目錄建立一個名爲CLASS_NAME的目錄*/
    basic_class = class_create(THIS_MODULE,CLASS_NAME);
    if(IS_ERR(basic_class))
    {
        unregister_chrdev(majorNumber,DEVICE_NAME);
        return PTR_ERR(basic_class);
    }

    /*以DEVICE_NAME爲名,參考/sys/class/CLASS_NAME在/dev目錄下建立一個設備:/dev/DEVICE_NAME*/
    basic_device = device_create(basic_class,NULL,MKDEV(majorNumber,0),NULL,DEVICE_NAME);
    if(IS_ERR(basic_device))
    {
        class_destroy(basic_class);
        unregister_chrdev(majorNumber,DEVICE_NAME);
        return PTR_ERR(basic_device);
    }
    printk(KERN_ALERT "Basic device init success!!\r\n");

    return 0;
}

/*當用戶打開這個設備文件時,調用這個函數*/
static int basic_open(struct inode *node, struct file *file)
{
    printk(KERN_ALERT "Open file\r\n");
    return 0;
}

/*當用戶試圖從設備空間讀取數據時,調用這個函數*/
static ssize_t basic_read(struct file *file,char *buf, size_t len,loff_t *offset)
{
    int cnt = 0;
    /*將內核空間的數據copy到用戶空間*/
    cnt = copy_to_user(buf,msg,sizeof(msg));
    if(0 == cnt){
        printk(KERN_INFO "Send file!!");
        return 0;
    }
    else{
        printk(KERN_ALERT "ERROR occur when reading!!");
        return -EFAULT;
    }
    return sizeof(msg);
}

/*當用戶往設備文件寫數據時,調用這個函數*/
static ssize_t basic_write(struct file *file,const char *buf,size_t len,loff_t *offset)
{
    /*將用戶空間的數據copy到內核空間*/
    int cnt = copy_from_user(recv_msg,buf,len);
    if(0 == cnt){
        printk(KERN_INFO "Recieve file!!");
    }
    else{
        printk(KERN_ALERT "ERROR occur when writing!!");
        return -EFAULT;
    }
    printk(KERN_INFO "Recive data ,len = %s",recv_msg);
    return len;
}

/*當用戶打開設備文件時,調用這個函數*/
static int basic_release(struct inode *node,struct file *file)
{
    printk(KERN_INFO "Release!!");
    return 0;
}

/*銷燬註冊的全部資源,卸載模塊,這是保持linux內核穩定的重要一步*/
static void __exit basic_exit(void)
{
    device_destroy(basic_class,MKDEV(majorNumber,0));
    class_unregister(basic_class);
    class_destroy(basic_class);
    unregister_chrdev(majorNumber,DEVICE_NAME);
}

module_init(basic_init);
module_exit(basic_exit);

程序註解

看程序固然是要從入口函數開始,咱們將目光投入到basic_init函數:函數

  1. majorNumber = register_chrdev(0,DEVICE_NAME,&file_oprts);調用這個函數建立了一個字符設備。
    • 參數1爲次設備號,在linux內核中,一個設備由主次設備號標記。
    • 參數2爲設備名稱,這個設備名稱將會做爲/dev下創建的設備
    • 參數3爲綁定的file operation結構體,當用戶空間讀寫操做設備時,產生系統調用,即調用這個結構體中相應的讀寫函數
    • 返回主設備號
  2. basic_class = class_create(THIS_MODULE,CLASS_NAME);調用這個函數建立一個class,同時在/sys/class目錄下建立一個目錄,做爲當前設備的描述信息。ui

    • 參數1指定當前module,主要是用來標識當這個模塊正被操做時阻止模塊被卸載。
    • 參數2指定class名,這個名稱將做爲/sys/class下的目錄名
  3. basic_device = device_create(basic_class,NULL,MKDEV(majorNumber,0),NULL,DEVICE_NAME);調用這個函數在/dev下注冊一個用戶空間設備:/dev/DEVICE_NAME
    • 參數1爲傳入的class信息
    • 參數2爲父目錄,這裏爲NULL,表示默認直接掛在/dev下
    • 參數3爲設備號,由主次設備構成的設備號
    • 參數4爲drvdata指針,指向設備數據
    • 參數5爲*fmt,與printf函數類型,支持可變參數,表示設備節點的名稱

執行原理

驅動使用register_chrdev()函數在內核中註冊一個設備節點,同時將初始化的file_operation結構體註冊進去,內核會維護一個file_operation結構體集合,註冊一個file_operation結構體而且返回設備號,在這裏將設備號和結構體相關聯。操作系統

在用戶空間使用device_create()在/dev目錄下建立新的設備節點,可是在這個目錄建立時並無關聯相應的file_operation結構體,那咱們在對設備節點進行read,write操做時,是怎麼調用相應的file_operation結構體中的接口的呢?

答案是經過設備號,內核維護file_operation結構體數組,而且將其與設備號進行關聯,另外一方面,在/dev下建立設備節點時,將設備節點與設備號進行關聯。

因此在操做設備節點時,能夠獲得設備節點關聯的設備號,再經過設備節點找到相應的file_operation結構體,再調用結構體中相應的函數,執行完畢返回到用戶空間。

因此當模塊加載完成後,整個過程是這樣的:

用戶打開/dev/DEVICE_NAME設備產生系統調用 
    ->系統找到設備節點對應的設備號 
        ->經過設備號找到內核維護的file_operation結構體集合中對應的結構體  
            ->因爲初始化時指定了.open = basic_open,調用basic_open函數 
返回到用戶空間
用戶的下一步操做...

須要注意的是,用戶的read會最終會觸發調用file_operation結構體中的相應read函數,前提是咱們進行了相應的賦值:.read = basic_read,可是用戶的read函數的返回值並不是就是file_operation結構體中basic_read的返回值,從用戶read /dev下的文件到觸發調用file_operation結構體中.read還有一些中間過程。

copy_to_user()和copy_from_user()

linux kernel是操做系統的核心,掌握着整個系統的運行和硬件資源的分配。

因此爲了安全考慮,linux的內存空間被劃分爲用戶空間和內核空間,而若是用戶空間須要使用到內某些由核掌控的資源,就必須提出申請,這個申請就是產生系統調用,遵循一些內核指定的接口來訪問內核資源。

將用戶空間和內核空間進行隔離可以保障內核的安全,由於用戶進程的任何行爲都由內核最終把控,出現問題就直接結束進程。而內核一旦出現問題,很大可能直接致使死機或者產生一些不可預期的行爲,這是咱們不肯意看到的。

既然進行了隔離,那麼用戶進程和內核之間的數據交換就得經過專門的接口而非隨意的指針相互訪問,這兩個接口就是copy_to_user()和copy_from_user()。

顧名思義copy_to_user()就是將內核數據copy到用戶空間,copy_from_user()就是將用戶數據copy到內核中,這兩種行爲都由內核管理。

這兩個接口主要作了兩件事:

  1. 檢查數據指針是否越界,這對內核安全來講是很是重要的,用戶程序永遠不可能直接訪問內核空間
  2. copy數據

編譯加載

編譯依舊延續上一章節的操做,修改Makefile:

obj-m+=create_device_node.o
all:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules
clean:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean

編譯完成以後加載模塊:

sudo insmod create_dev_node.ko

而後使用lsmod命令進行檢查,若是一切正常,咱們就能夠在/dev目錄下看到咱們新建立的設備節點:/dev/$DEVICE_NAME。

用戶空間的操做代碼

加載內核模塊成功以後,接下來的事情就是在用戶空間操做設備節點了,咱們嘗試着打開設備節點,而後對其進行讀寫:

create_device_node_user.c:

#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

static char buf[256] = {1};
int main(int argc,char *argv[])
{
    int fd = open("/dev/basic_demo",O_RDWR);
    if(fd < 0)
    {
        perror("Open file failed!!!\r\n");
    }
    int ret = write(fd,"huangdao!",strlen("huangdao!"));
    if(ret < 0){
        perror("Failed to write!!");
    }
    ret = read(fd,buf,7);
    if(ret < 0){
        perror("Read failed!!");
    }
    else
    {
        printf("recv data = %s \n",buf);
    }
    close(fd);
    return 0;
}

在上面的代碼示例中,建立了/dev/basic_demo這個設備節點,在user程序中,先打開/dev/basic_demo設備,而後寫字符串"huangdao",寫完以後而後讀取7個字節。

在上面的設備程序中實現:read將調用basic_read()函數,而write將調用basic_write()函數,在basic_write()函數中,接收用戶空間的數據並打印出來,而在basic_read()中,將msg中的信息返回到用戶空間。

對用戶程序編譯運行

編譯很簡單:

gcc create_device_node_user.c -o user

運行:

sudo ./user

內核端的log輸出:
Dec 20 14:12:55 beaglebone kernel: [ 433.070666] Open file
Dec 20 14:12:55 beaglebone kernel: [ 433.073308] Recieve file!!
Dec 20 14:12:55 beaglebone kernel: [ 433.073318] Recive data ,len = huangdao!
Dec 20 14:12:55 beaglebone kernel: [ 433.073335] Send file!!

用戶端的log輸出:

recv data = Downey!

linux設備節點訪問權限

在加載完上述設備驅動程序以後,咱們能夠看到生成了一個新設備/dev/basic_demo,查看這個設備節點的權限:

ls -l /dev/basic_demo

輸出:

crw------- 1 root root 241, 0 Dec 20 14:10 /dev/basic_demo

發現權限是隻有root用戶才能讀寫,因此在上面執行用戶程序訪問設備節點時,咱們必須加上sudo以超級用戶執行程序,既然設備驅動程序的節點服務於普通用戶,那麼普通用戶若是沒有權限訪問,那豈不是白瞎。因此咱們要修改設備節點的屬性。

第一個辦法,固然是最簡單粗暴的,使用root權限直接修改:

sudo chmod 666 /dev/basic_demo

這種辦法是能夠完成修改的,並且也達到了用戶可訪問的目的,優勢就是簡單。

可是可別忘了,內核模塊具備可動態加載卸載的屬性,若是咱們每一次加載模塊以後都要以root權限從新去設置一次設備節點權限,在linux嚴格的權限管理系統下,在某些場景這並不合適。

第二個辦法:使用系統提供的方式:

  • 首先,咱們可使用udevadm info -a -p /sys/class/$CLASS_NAME/$DEVICE_NAME來查看模塊信息:

    udevadm info -a -p /sys/class/basic_class/basic_demo
    輸出:
    looking at device '/devices/virtual/basic_class/basic_demo':
    KERNEL=="basic_demo"
    SUBSYSTEM=="basic_class"
    DRIVER==""
    在這裏能夠看到模塊的設備名(KERNEL),class名稱(SUBSYSTEM),這裏只是一個查詢做用,獲取相應信息,若是能記住就能夠省略這一步驟。

    tips:
    在上述的驅動程序中,咱們使用class_create()建立了一個設備節點,在/sys/class目錄下生成了相應的以$CLASS_NAME爲名稱的目錄,這個目錄下存放着模塊信息。

  • 而後,在/etc/udev目錄下建立一個.rules爲後綴的文件,這個文件就是udev的規則文件,可對權限進行管理:
    • 文件名以數字開頭,由於這個目錄下的文件都以數字開頭,在不瞭解所有原理以前咱們得遵循系統的規則,事實上不以數字開頭也沒有問題,文件必須以.rules結尾,這裏建立的文件名爲:99-basic_demo.rules.
    • 填充文件名:

      KERNEL=="basic_demo", SUBSYSTEM=="basic_class", MODE="0666"
      完成以上操做,再加載模塊時,咱們就能夠查看設備節點信息了:

    ls -l /dev/basic_demo
    結果顯示:

    crw-rw-rw- 1 root root 241, 0 Dec 20 14:37 /dev/basic_demo
    表示權限修改完成,通過此次修改,每次加載完模塊,生成的設備節點文件都是.rules文件中指定的訪問權限了。

關於/etc/udev/下.rules文件規則請參考

好了,關於linux設備驅動中建立設備節點的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索