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


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

基於4.14內核,運行在beagleBone green

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

預備知識

在linux中,一切皆文件,不管用戶是控制某個外設又或者是操作I/O,都是通過文件實現。

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

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

舉個例子:在開發一款溫度傳感器時,內核驅動模塊可以在驅動程序中實現傳感器的初始化,然后在/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目錄下創建一個目錄,作為當前設備的描述信息。

    • 參數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不沾身.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM