目錄
- 字元裝置
- Linux字元裝置驅動結構
- cdev結構體
- 配置設定、釋放裝置号
- 配置設定裝置号
- 釋放裝置号
- file_operations結構體
- 字元裝置驅動的組成
- 字元裝置驅動子產品的加載、解除安裝函數
- 字元裝置驅動的file_operations結構體的成員函數
- copy_to_user和copy_from_user
- put_user和get_user
- I/O控制函數unlocked_ioctl
- 字元裝置驅動檔案操作file_operations
- 參考
這部分主要講Linux字元裝置驅動程式的結構,解釋主要組成部分的程式設計方法。
字元裝置
字元裝置:指隻能一個byte一個byte讀寫的裝置,不能随機讀寫資料,要按先後順序。字元裝置是面向流的裝置,常見字元裝置有滑鼠、鍵盤、序列槽、終端、LED燈。
塊裝置:指可以從裝置的任意位置讀取一定長度資料的裝置。常見塊裝置有磁盤、硬碟、U盤、SD卡等。
每個字元裝置或塊裝置,都在/dev目錄下有一個對應的裝置檔案。Linux APP可以通過這些裝置檔案(又稱裝置節點),來使用驅動程式操作字元裝置和塊裝置。
字元裝置、字元裝置驅動與使用者空間通路該裝置的程式三者之間的關系:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZwpmLzATO1YTN0EzMtQzN5MjM2MDNxEjM2AjMyAjMtEDM0EDN38CX2AjMyAjMvwVMwQTM0czLcd2bsJ2Lc12bj5ycn9Gbi52YuIjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
from Linux 字元裝置驅動結構(一)—— cdev 結構體、裝置号相關知識解析 | CSDN
Linux核心中,
- 使用cdev結構體描述字元裝置;
- 通過成員dev_t定義裝置号(主、次裝置号),确定字元裝置的唯一性;
- 通過成員file_operations定義字元裝置驅動,為VFS(虛拟檔案系統)提供接口函數,如open/close/read/write等。
Linux字元裝置驅動中,
- 子產品加載函數通過register_chrdev_region()或alloc_chrdev_region(),來靜态或動态擷取裝置号;
- 通過cdev_init()建立cdev與file_opreations之間的連接配接,通過cdev_add()向系統添加一個cdev以完成注冊;
- 子產品解除安裝函數通過cdev_del()來登出cdev,通過unregister_chrdev_region()來釋放裝置号。
TIPS: register_chrdev 與 register_chrdev_region, alloc_chrdev_region有何差別?
register_chrdev 裝置注冊 + 裝置号申請。register_chrdev_region和alloc_chrdev_region 裝置号申請,裝置注冊由cdev_init + cdev_add完成。
register_chrdev() 支援一次注冊一個裝置,而且需要傳入參數file_operations。預設寫死注冊的裝置号範圍0~255。釋放字元裝置時,使用unregister_chrdev()。但不必使用cdev_xxx系列操作。
register_chrdev_region() 支援一次注冊多個裝置号,不需要傳入參數file_operations,在cdev_init()中綁定cdev與file_operations。釋放裝置号時,使用unregister_chrdev_region。register_chrdev_region需要搭配cdev_xxx系列操作使用。
alloc_chrdev_region() 與register_chrdev_region()的差別在于前者申請的裝置号由系統決定,後者由調用者指定。
APP中通路裝置驅動程式,
- 通過Linux系統調用,如open/close/read/write,調用file_operations中定義的接口函數。
Linux字元裝置驅動結構
cdev結構體
Linux核心中,使用cdev結構體描述一個字元裝置。cdev定義:
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; /* 内嵌的kobject對象 */
struct module *owner; /* 所屬子產品 */
struct file_operations *ops; /* 檔案操作結構體 */
struct list_head list;
dev_t dev; /* 裝置号 */
unsigned int count; /* 該裝置關聯的裝置編号的數量 */
};
cdev結構體的dev_t成員定義裝置号(32bit),其中高12bit為主裝置号,低20bit為次裝置号。
如何擷取主次裝置号,或dev_t?
- 從dev_t獲得主裝置号和次裝置号
MAJOR(dev_t dev); // 主裝置号
MINOR(dev_t dev); // 次裝置号
- 通過主裝置号、次裝置号生成dev_t
MKDEV(int major, int minor); // 生成dev_t, 包含主次裝置号資訊
這幾個宏定義如下:
#include <linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 高12bit為主裝置号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 低20bit為次裝置号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
核心提供一組函數用于操作cdev結構體:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
1)cdev_init 初始化cdev成員,最重要的是建立cdev和file_operations之間的連接配接
源碼:
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev); /* 将整個結構體清零 */
INIT_LIST_HEAD(&cdev->list); /* 初始化list成員, 指向自身 */
kobject_init(&cdev->kobj, &ktype_cdev_default); /* 初始化kobj成員 */
cdev->ops = fops; /* 建立cdev和file_operations之間的連接配接 */
}
2)cdev_alloc 動态申請一個cdev記憶體
源碼:
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); /* 動态申請一個cdev記憶體, GFP_KERNEL: 無記憶體可用時可休眠 */
if (p) {
INIT_LIST_HEAD(&p->list); /* 初始化list成員, 指向自身 */
kobject_init(&p->kobj, &ktype_cdev_dynamic); /* 初始化kobj成員 */
}
return p;
}
上面兩個初始化函數,為何都沒看到owner、dev、count 這3個成員的初始化?
對于owner成員,struct module類型對象,是核心對于一個子產品的抽象。該成員在字元裝置中可以展現該裝置隸屬于哪個子產品,在驅動程式的編寫中一般由使用者顯式初始化.owner = THIS_MODULE
對于dev和count成員,在cdev_add中才會指派。
3)cdev_add 向核心添加一個cdev,完成字元裝置的注冊
這裡需要提供參數dev(裝置号)和count(該裝置關聯的裝置編号的數量),直接指派給cdev結構的dev和count成員。
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p); /* 将cdev放入cdev_map中 */
if (error)
return error;
kobject_get(p->kobj.parent); /* 增加引用計數 */
return 0;
}
4)cdev_del 從核心删除一個cdev
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count); /* 将dev從cdev_map中擦除 */
kobject_put(&p->kobj); /* 減少引用計數 */
}
static void cdev_unmap(dev_t dev, unsigned count)
{
kobj_unmap(cdev_map, dev, count); /* 将dev從cdev_map中擦除 */
}
配置設定、釋放裝置号
配置設定裝置号
調用cdev_add()向系統注冊字元裝置前,應先申請裝置号。配置設定裝置号有2種方法:
1)靜态申請:register_chrdev_region
register_chrdev_region() 用于已知起始裝置号的情況,向系統靜态申請裝置号(範圍)。
要申請的裝置号範圍:[from, from + count)。
有些裝置号已被Linux核心開發者配置設定掉了,具體配置設定内容可檢視Documentation/devices.txt。
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail: /* 出錯復原 */
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
2)動态申請:alloc_chrdev_region
alloc_chrdev_region() 用于裝置号未知,向系統動态申請未被占用的裝置号的情況。
得到的裝置号會放入第一個參數dev中。alloc_chrdev_region相比register_chrdev_region,優點:alloc_chrdev_region會自動避開裝置号重複的沖突。
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd; /* 字元裝置結構指針 */
cd = __register_chrdev_region(0, baseminor, count, name); /* 注冊單個指定主裝置号、次裝置号 */
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
檢查:注冊裝置成功後,會在/proc/devices 添加字元裝置名稱。
是以,可以利用insmod指令加載裝置驅動後,觀察/proc/devices值,判斷是否注冊了裝置。
# cat /proc/devices
釋放裝置号
在調用cdev_del()從系統登出字元裝置後,unregister_chrdev_region()應該被調用以釋放原先申請的裝置号。
從系統反注冊裝置号,範圍:[from, from + count)
/**
* unregister_chrdev_region() - unregister a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0); /* 下一個裝置号dev_t */
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); /* 反注冊單個裝置号, 并釋放空間 */
}
}
file_operations結構體
file_operations 是裝置驅動程式與APP互動的接口,其成員函數是字元裝置驅動程式設計的主體,實際會在APP調用open/write/read/close等系統調用時被核心調用。
file_operations結構體定義:
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_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_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);
};
主要成員:
- llseek() 用來修改一個檔案的目前讀寫位置,并将新位置傳回,出錯時,函數傳回一個負值。
- read() 用來從裝置中讀取資料,成功時函數傳回讀取的位元組數,出錯時傳回一個負值。對應使用者空間read(2)。
- write() 向裝置發送資料,成功時函數傳回寫入的位元組數。如果未被實作,當使用者進行write()系統調用時,将得到-EINVAL傳回值。對應使用者空間write(2)。
read和write傳回0,暗示end-of-line(EOF)。
- unlocked_ioctl() 提供裝置相關控制指令的實作(不是讀,也不是寫),成功時傳回一個非負值。對應使用者空間fcntl(2)應。
- mmap() 将裝置記憶體映射到程序的虛拟位址空間,如果裝置驅動未實作該函數,使用者調用mmap()系統調用時傳回-ENODEV。對應使用者空間mmap(2)。與mmap對應的是unmap。
- open() 打開裝置,用于初始化裝置狀态。使用者空間調用open(2)時,裝置驅動的open()被調用。驅動程式可以不實作該函數,裝置打開操作永遠成功。與open對應的是release。
- release() 釋放裝置資源。如果open()中有申請系統資源,則可以在release()中釋放。對應使用者空間close(2)。
- poll() 用于詢問裝置是否可以被非阻塞地立即讀寫。當詢問的條件未觸發時,使用者空間進行select()和poll()系統調用将引起程序阻塞。
- aio_read()/aio_write() 分别對與檔案描述符對應的裝置進行異步讀、寫操作。裝置實作這2個函數後,使用者空間可以對該裝置檔案描述符執行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系統調用進行讀寫。
字元裝置驅動的組成
Linux中,字元裝置驅動組成:字元裝置驅動子產品加載、解除安裝函數,字元裝置驅動的file_operations結構體的成員函數。
字元裝置驅動子產品的加載、解除安裝函數
加載函數應該實作:1)裝置号的申請;2)cdev的注冊。
解除安裝函數應該實作:1)裝置号的釋放;2)cdev的登出。
典型的裝置結構體、子產品加載函數、解除安裝函數代碼形式:
/* 裝置結構體
struct xxx_dev_t = {
struct cdev cdev;
...
} xxx_dev;
*/
/* 裝置驅動子產品加載函數 */
static init __init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */
xxx_dev.cdev.owner = THIS_MODULE;
/* 獲得字元裝置号 */
if (xxx_major) {
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注冊裝置 */
...
}
/* 裝置驅動子產品解除安裝函數 */
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 釋放占用的裝置号 */
cdev_del(&xxx_dev.cdev); /* 登出裝置 */
}
字元裝置驅動的file_operations結構體的成員函數
file_operations的成員函數是字元裝置驅動跟核心虛拟檔案系統的接口,是使用者空間對Linux進行系統調用最終的落實者。大多數字元裝置驅動會實作read()/write/ioctl()。
典型字元裝置驅動代碼形式:
/* 讀裝置
* filp: 檔案結構指針
* buf: 使用者空間記憶體位址, 在核心空間不能直接讀寫
* count: 要讀的位元組數
* f_pos: 讀的位置相對于檔案開頭的偏移
*/
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...); /* 将資料從核心空間拷貝到使用者空間 */
...
}
/* 寫裝置 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_from_user(.... buf, ...); /* 将資料從使用者空間拷貝到核心空間 */
...
}
/* ioctl函數 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch(cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
/* 不支援的指令 */
return -ENOTTY;
}
return 0;
}
copy_to_user和copy_from_user
注:使用者空間不能直接通路核心空間的記憶體,是以要借助copy_to_user()将資料從核心空間拷貝到使用者空間;
同樣地,核心空間不能直接通路使用者空間的記憶體,是以借助copy_from_user()将資料從使用者空間拷貝到核心空間。
#include <linux/uaccess.h>
/* 使用者 -> 核心 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
/* 核心 -> 使用者 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
注:函數傳回不能被複制的位元組數。如果完全複制成功,傳回0;如果失敗,傳回負值。
其源碼如下:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(access_ok(VERIFY_READ, from, n))) /* 檢查位址的合法性, from起始位址, 長度n */
n = __copy_from_user(to, from, n); /* 資料拷貝, 但不做位址合法性檢查 */
else
memset(to, 0, n);
return n;
}
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(access_ok(VERIFY_WRITE, to, n))) /* 檢查位址的合法性, to起始位址, 長度n */
n = __copy_to_user(to, from, n); /* 資料拷貝, 但不做位址合法性檢查 */
return n;
}
likely:是宏定義,常用于編譯器優化,告訴編譯器分支大機率會發生。
access_ok(type, addr, size):核心空間可以通路使用者空間的緩沖區,但通路之前需要用access_ok檢查其合法性,以确定傳入的緩沖區位址的确術語使用者空間。
如果要複制的記憶體是簡單類型,如char、int、long等,則可以使用簡單的put_user()和get_user()。
int val; /* 核心空間變量 */
...
get_user(val, (int* ) arg); /* 使用者 -> 核心, arg 是使用者空間位址 */
...
put_user(val, (int* ) arg); /* 核心 -> 使用者, arg 是使用者空間位址 */
copy_from_user函數中的__user宏是什麼?
該宏表明背後的指針指向使用者空間,實際上更多地充當了代碼注釋的功能。
#ifdef __CHECKER__
# define __user __attribute__((noderef, address_space(1)))
#else
# define __user
#endif
put_user和get_user
put_user(), get_user() 也有另外一個版本:__put_user(), __get_user()。差別在于__put_user()不用access_ok()檢查位址的合法性,而put_user()會。通常,在調用__put_user()之前,會手動檢查使用者空間緩沖區。
get_user()和__get_user() 關系類似。
I/O控制函數unlocked_ioctl
I/O控制函數的cmd參數為事先定義的I/O控制指令,arg為對應于指令的參數。例如,對于串行裝置,如果SET_BAUDRATE是設定波特率的指令,那arg就應該是波特率值。
字元裝置驅動檔案操作file_operations
字元裝置驅動檔案操作,通過定義file_operations執行個體,并将具體裝置驅動函數指派給file_operations成員來完成。
struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
.unlocked_ioctl = xxx_ioctl,
};
通過子產品加載函數中調用cdev_init(&xxx_dev.cdev, &xxx_fops) 為cdev和fops建立連接配接。
參考
[1]宋寶華. Linux裝置驅動開發詳解[M]. 人民郵電出版社, 2010.
[2] https://blog.csdn.net/zqixiao_09/article/details/50839042