字符設備驅動程序
- 1 主設備和次設備的概念
- 設備號的注冊和釋放
- 靜態方法
- 動態方法
- 區別
- 2 設備文件操作
- struct file_operations與struct file、struct inode關系
- 3 分配和注冊字符設備
- class_create
- cdev_add
- device_create
- 4 字符設備驅動程序
字符設備通過字符(一個接一個的字符)以流方式向用戶程序傳遞數據,就像串行端口那樣。字符設備驅動通過/dev目錄下的特殊文件公開設備的屬性和功能,通過這個文件可以在設備和用戶應用程序之間交換數據,也可以通過它來控制實際的物理設備。這也是Linux的基本概念,一切皆文件。字符設備驅動程序是內核源碼中最基本的設備驅動程序。字符設備在內核中表示為struct cdev
的實例,struct cdev
定義在include/linux/cdev.h中:
struct cdev {struct kobject kobj; struct module *owner; /* 指向提供驅動程序的模塊 */const struct file_operations *ops; /* 是一組文件操作,實現了與設備通信的具體操作 */struct list_head list; /* 用來將已經向內核注冊的所有字符設備形成鏈表.*/dev_t dev; /* 設備號 */unsigned int count; /* 隸屬于同一主設備號的次設備號的個數.*/
} __randomize_layout;
1 主設備和次設備的概念
字符設備在/dev目錄下,使用 ls -l命令查看
開頭為c的代表字符設備文件,開頭為b的代表塊設備文件,日期左邊的第五列、第六列用<X,Y>格式表示,X代表的是主設備號,Y代表的次設備號,這是典型的從用戶空間標識字符設備,及其主次設備號的方法。
內核用dev_t類型變量維持設備號,該變量是u32。主設備號僅占12位,次設備號占20位。
typedef __kernel_dev_t dev_t;typedef __u32 __kernel_dev_t;typedef unsigned int __u32;
dev_t類型定義在include/linux/kdev_t.h中,可以通過如下兩個宏定義來獲取主、次設備號:
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果有主設備和次設備號,也可以通過宏MKDEV(int major,int minor)
來構建dev_t。
設備注冊時,必須使用主設備號和次設備號,前者標識一個特定的驅動程序,后者用作標識使用該驅動程序的各設備(設備列表中的數組索引),因為同一個驅動可處理多個設備,而不同的驅動程序可以處理相同類型的不同設備。
設備號的注冊和釋放
設備號在系統范圍內標識設備文件,有兩種不同的方法分配設備號。
下面兩個函數都在fs/char_dev.c實現
靜態方法
靜態方法是調用register_chardev_region()函數,該方法必須事先知道所需的設備號
int register_chrdev_region(dev_t from, unsigned count, const char *name)
這個函數成功返回0,失敗返回錯誤碼。from是由我們所需的主設備號和合理范圍內的次設備號組成,可由MKDEV構建。count是所需的連續設備號數目,name是相關設備或者驅動程序的名字。
動態方法
使用alloc_chardev_region()函數,使內核自動分配設備號,建議采用這種方法獲得有效的設備號。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
這個函數成功返回0,失敗返回錯誤碼。dev獲取分配的設備號,baseminor代表申請的次設備號范圍內的第一個數字,count代表次設備的數目,name代表相關設備或者驅動程序的名字。
區別
這兩種分配方法的區別在于,第一種方法必須事先知道所需的設備號,這就是注冊制:把所需的設備號告訴內核。這可能在教學中使用,只有自己使用該驅動程序時,才會這樣選擇,如果在其他機器上加載該驅動程序,就無法保證所選擇的設備號在這臺機器未被占用,這會引起設備號的沖突和麻煩。第二種更安全,因為內核幫助獲取一個合適的設備號,所以我們甚至不需要關心在其他機器上加載該模塊所出現的問題,內核將根據具體情況來自動分配。
2 設備文件操作
可以在文件上執行的操作取決于管理文件的設備驅動程序。這樣的操作在內核中定義為struct file_operations
的實例。struct file_operations
定義了一組回調函數,用于處理文件上的所有用戶空間的系統調用。舉個例子,如果想讓用戶在設備文件上執行write操作,必須在驅動中實現write函數對于的回調函數,并把它添加到綁定在設備上的struct file_operations
中,struct file_operations
定義在include/linux/fs.h中。
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);int (*iterate_shared) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endifssize_t (*copy_file_range)(struct file *, loff_t, struct file *,loff_t, size_t, unsigned int);int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,u64);
};
其中的每一個函數都和系統調用鏈接在一起,它們都不是必需的。當用戶代碼在指定文件上調用與文件相關的系統調用時,內核會查找負責這個文件的驅動程序,定位它的struct file_operations結構,并檢查和該系統調用匹配的方法是否已經定義。如果已經定義了,就運行它。如果未定義,則根據系統調用不同返回不同的錯誤碼。
struct file_operations與struct file、struct inode關系
struct inode表示一個具體文件。一個設備或者驅動會由struct inode的實例表示。在該結構體中,我們需要注意以下幾個域。
struct inode {...const struct file_operations *i_fop; /* former ->i_op->default_file_ops */union {struct pipe_inode_info *i_pipe; /* 如果是Linux管道,則設置并使用 */struct block_device *i_bdev; /* 如果是塊設備,則設置并使用 */struct cdev *i_cdev; /* 如果是字符設備,則設置并使用 */char *i_link;unsigned i_dir_seq;};
....
};
struct inode
里面也有struct file_operations
,但是i_fop
指向的是默認的索引節點操作,如果struct inode
代表的是字符設備,則i_cdev會指向一個struct cdev
結構,對文件進行操作時,使用的是cdev中file_operations中定義的文件操作方法。
struct file代表的是一個進程打開的文件,其里面也有struct file_operations
struct file {...const struct file_operations *f_op;...
};
當我們在應用層使用open函數打開一個文件時,會創建struct file
對象,初始化struct file
對象時,struct file
對象中的file_operations將指向struct inode
的file_operations(準備的來說,struct inode
如果沒有定義文件的具體操作,將指向默認的file_operations,如果定義了,比如字符設備,將指向字符設備的file_operations)
比如我們使用open打開兩個字符設備
fd0 = open("/dev/com0",O_RDWR);fd1 = open("/dev/com1",O_RDWR);
如下圖
struct inode
使用struct cdev
中的file_operations,struct file
也指向struct cdev
中的file_operations,當對com0或者com1操作時,直接調用struct file
的file_operations。
如何將file_operations里面定義的操作和struct cdev
結構綁定到一起呢?我們可以使用cdev_init函數,將struct cdev
中的ops指向第二個參數指向的內容
void cdev_init(struct cdev *, const struct file_operations *)
3 分配和注冊字符設備
字符設備在內核中表示為struct cdev的實例。在編寫字符設備驅動程序時,目標是最終創建并注冊與struct file_operations關聯的結構實例,為用戶空間提供一組可以在該設備上執行的操作函數,為了實現這個目標,必須執行以下幾個步驟:
- 使用alloc_chardev_region()保留一個主設備號和一定范圍的次設備號。
- 使用class_create()創建自己的設備類。
- 創建一個struct file_operations(傳遞給cdev_init),每一個設備都需要創建,并調用call_init和cdev_add注冊這個設備。
- 調用device_create()創建每個設備,并給它們一個合適的名字,這樣,就可以在/dev目錄下創建出設備。
class_create
宏class_create()用于動態創建設備的邏輯類,并完成部分字段的初始化,然后將其添加進Linux內核系統中。此函數的執行效果就是在目錄/sys/class下創建一個新的文件夾,此文件夾的名字為此函數的第二個輸入參數,但此文件夾是空的。宏class_create()在實現時,調用了函數__class_create(),作用和函數__class_create()基本相同。
class_create在include/linux/device.h中被定義
#define class_create(owner, name) \
({ \static struct lock_class_key __key; \__class_create(owner, name, &__key); \
})
- 參數owner是一個struct module結構體類型的指針,指向函數__class_create()即將創建的struct class類型對象的擁有者,一般賦值為THIS_MODULE,此結構體的詳細定義見文件include/linux/module.h。
- 參數name是char類型的指針,代表即將創建的struct class變量的名字,用于給struct class的name字段賦值。
返回值為創建的邏輯類。
此宏需要與函數class_destroy()配對使用,不能單獨使用,當單獨使用時,第一次不會出現錯誤,但當第二次插入模塊時就會出現錯誤。
cdev_add
函數cdev_add()用于向Linux內核系統中添加一個新的cdev結構體變量所描述的字符設備,并且使這個設備立即可用。
在文件linux/cdev.h中定義:
int cdev_add(struct cdev *, dev_t, unsigned)
函數 cdev_add()有三個輸入參數,第一個輸入參數代表即將被添加入Linux內核系統的字符設備;第二個輸入參數是dev_t類型的變量,此變量代表設備的設備號,其中包括主設備號和次設備號;第三個輸入參數是無符號的整型變量,代表想注冊設備的設備號的范圍,用于給struct cdev中的字段count賦值
device_create
函數device_create()用于動態地創建邏輯設備,并對新的邏輯設備類進行相應的初始化,將其與此函數的第一個參數所代表的邏輯類關聯起來,然后將此邏輯設備加到Linux內核系統的設備驅動程序模型中。函數能夠自動地在/sys/devices/virtual目錄下創建新的邏輯設備目錄,在/dev目錄下創建與邏輯類對應的設備文件。
該函數定義在linux/device.h中
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
-
函數device_create()的第一個輸入參數代表與即將創建的邏輯設備相關的邏輯類,也就是class_create
-
第二個輸入參數代表即將創建的邏輯設備的父設備的指針,子設備與父設備的關系是:當父設備不可用時,子設備不可用,子設備依賴父設備,父設備不依賴子設備。
-
第三個輸入參數是邏輯設備的設備號
-
第四個輸入參數是void類型的指針,代表回調函數的輸入參數。
-
第五個輸入參數是邏輯設備的設備名,即在目錄/sys/devices/virtual創建的邏輯設備目錄的目錄名。
返回值是struct device結構體類型的指針,指向新創建的邏輯設備,
device_create創建了設備文件,我們就可以根據該設備文件來和驅動或設備交互了。
注意:函數device_create()必須和函數device_destroy()配對使用,這樣才不會出現錯誤
4 字符設備驅動程序
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/version.h>
#include <linux/device.h>
#include <linux/cdev.h>static unsigned int major; /* major number for device */
static struct class *dummy_class;
static struct cdev dummy_cdev;int dummy_open(struct inode * inode, struct file * filp)
{pr_info("Someone tried to open me\n");return 0;
}int dummy_release(struct inode * inode, struct file * filp)
{pr_info("Someone closed me\n");return 0;
}ssize_t dummy_read (struct file *filp, char __user * buf, size_t count,loff_t * offset)
{pr_info("Nothing to read guy\n");return 0;
}ssize_t dummy_write(struct file * filp, const char __user * buf, size_t count,loff_t * offset)
{pr_info("Can't accept any data guy\n");return count;
}struct file_operations dummy_fops = {open: dummy_open,release: dummy_release,read: dummy_read,write: dummy_write,
};static int __init dummy_char_init_module(void)
{struct device *dummy_device;int error;dev_t devt = 0;/* Get a range of minor numbers (starting with 0) to work with */error = alloc_chrdev_region(&devt, 0, 1, "dummy_char");if (error < 0) {pr_err("Can't get major number\n");return error;}major = MAJOR(devt);pr_info("dummy_char major number = %d\n",major);/* Create device class, visible in /sys/class */dummy_class = class_create(THIS_MODULE, "dummy_char_class");if (IS_ERR(dummy_class)) {pr_err("Error creating dummy char class.\n");unregister_chrdev_region(MKDEV(major, 0), 1);return PTR_ERR(dummy_class);}/* Initialize the char device and tie a file_operations to it */cdev_init(&dummy_cdev, &dummy_fops);dummy_cdev.owner = THIS_MODULE;/* Now make the device live for the users to access */cdev_add(&dummy_cdev, devt, 1);dummy_device = device_create(dummy_class,NULL, /* no parent device */devt, /* associated dev_t */NULL, /* no additional data */"dummy_char"); /* device name */if (IS_ERR(dummy_device)) {pr_err("Error creating dummy char device.\n");class_destroy(dummy_class);unregister_chrdev_region(devt, 1);return -1;}pr_info("dummy char module loaded\n");return 0;
}static void __exit dummy_char_cleanup_module(void)
{unregister_chrdev_region(MKDEV(major, 0), 1);device_destroy(dummy_class, MKDEV(major, 0));cdev_del(&dummy_cdev);class_destroy(dummy_class);pr_info("dummy char module Unloaded\n");
}module_init(dummy_char_init_module);
module_exit(dummy_char_cleanup_module);MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Dummy character driver");
MODULE_LICENSE("GPL");
在/dev目錄下創建了字符設備
在/sys/class下創建了目錄
設備的詳細信息