Linux系統(tǒng)將設備分成字符設備澎语、塊設備、網(wǎng)絡設備三類验懊。
用戶程序調用硬件的過程如下擅羞。
一、用戶級义图、內核級和系統(tǒng)調用
Linux/Unix系統(tǒng)下的進程運行分為用戶態(tài)和進程態(tài)兩種狀態(tài)减俏。我們的應用程序通常僅在用戶態(tài)下運行,出于保護內核資源的需要碱工,用戶態(tài)下運行的程序在只能訪問有限的資源娃承,例如不能訪問內核的數(shù)據(jù)結構和程序。
內核的一個重要功能就是協(xié)調和管理硬件資源怕篷,包括CPU草慧、內存、I/O設備等匙头,從而為上層運行的諸多應用程序提供更好的運行環(huán)境。因此仔雷,驅動程序通常都是在內核態(tài)下運行蹂析。我們的進程大多數(shù)時間都是運行在用戶態(tài)下的,一旦它需要操作系統(tǒng)幫助它完成一些自己沒有權限和能力完成的工作碟婆,就會切換到內核態(tài)电抚。而系統(tǒng)調用就是進程主動申請從用戶態(tài)切換到內核態(tài)的方式。
除了系統(tǒng)調用以外竖共,發(fā)生異瞅眩或外圍設備的中斷也會讓進程從用戶態(tài)進入內核態(tài)。但是公给,系統(tǒng)調用是應用程序主動轉入內核態(tài)借帘,而異常和中斷是被動轉入內核態(tài)。
異常又稱內中斷淌铐,來源于操作系統(tǒng)的內部肺然,通常是進程執(zhí)行時引發(fā)了故障,如缺頁腿准、地址越界际起、算術溢出、除數(shù)為零等。異常產生后街望,操作系統(tǒng)會奪回內核的使用權校翔,對異常進行處理。
外圍設備的中斷是指在外圍設備在處理完用戶要求的操作后灾前,會向操作系統(tǒng)發(fā)出中斷信號防症,此時操作系統(tǒng)會暫停執(zhí)行當前正在執(zhí)行的指令,轉而處理中斷信號豫柬。這個過程需要在內核態(tài)下進行告希,因此會由用戶態(tài)進入內核態(tài)。
二烧给、虛擬文件系統(tǒng)(VFS)
1. 什么是虛擬文件系統(tǒng)
我們在Unix/Linux系統(tǒng)下用戶程序操作普通文件燕偶,即txt、pdf础嫡、mp4等指么,使用open函數(shù)打開文件,使用read榴鼎、write函數(shù)對文件進行讀寫伯诬,使用close函數(shù)關閉文件。但除了普通文件外巫财,Unix/Linux的創(chuàng)作者提出“一切皆文件”的思想盗似,希望將目錄、字符設備平项、塊設備赫舒、套接字等也被等同于文件對待,使用普通文件使用相同的文件操作接口闽瓢,如open接癌、read、write扣讼,對這些設備進行操作缺猛。
虛擬文件系統(tǒng)(Virtual File System,VFS)是Linux系統(tǒng)中的一個軟件抽象層椭符,它就是實現(xiàn)上述功能的關鍵荔燎。它為用戶空間的程序提供了文件系統(tǒng)接口,同時定義了所有文件系統(tǒng)都支持的基本的销钝、概念上的接口和數(shù)據(jù)結構湖雹。例如,讀寫普通的文本文件和讀寫I/O設備的具體實現(xiàn)方法必然是不同的曙搬,但VFS提供了統(tǒng)一的接口read和write摔吏,開發(fā)人員需要編寫這些接口的具體的不同的實現(xiàn)鸽嫂。因此,在VFS層和內核的其他部分看來征讲,所有文件都只需調用read和write函數(shù)就可以完成讀寫功能据某,具體的實現(xiàn)過程它們并不關心。
2. 文件接口結構體file_operation
Linux內核定義了結構體file_operation诗箍,作為提供給VFS的文件接口癣籽。該結構體的定義如下。
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 (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,struct file *file_out, loff_t pos_out,loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
}滤祖;
file_operations的成員變量基本上都是函數(shù)指針筷狼,驅動開發(fā)者需要根據(jù)不同設備的要求,為這些函數(shù)指針編寫具體的實現(xiàn)匠童。以open函數(shù)為例埂材,不同設備的打開方式不同,因此open函數(shù)的實現(xiàn)顯然應該是不同的汤求,但從VFS層面上看俏险,都是只需要調用open函數(shù)就可以滿足打開設備的需求。
三扬绪、設備號
1. 什么是設備號
Linux系統(tǒng)使用設備號來區(qū)分各個設備竖独。設備號是一個32位的無符號整型數(shù),它的高12位是主設備號挤牛,低20位是次設備號莹痢。
為什么要區(qū)分主設備號和次設備號呢?
舉個例子墓赴,在一個硬件上可能有多個串口竞膳,系統(tǒng)會將每個串口都作為一個設備處理。但很多時候竣蹦,這些串口發(fā)揮的作用是相同的,只有地址不同沧奴。我們希望這些串口都由同一個驅動管理痘括,而不是為每個串口都寫一個驅動程序,因此Linux將設備號分為主設備號和次設備號滔吠。我們將這些串口分配相同的主設備號和不同的次設備號纲菌,這樣只要使用同一個驅動程序即可。
/proc/devices文件下記錄著每個設備和它對應的設備號疮绷,注意翰舌,字符設備和塊設備是分開獨立編號的。(/proc目錄用來存放系統(tǒng)進程運行時使用的文件)
2. 與設備號有關的宏
Linux內核定義了一些與設備號有關的宏冬骚,常見的宏如下椅贱。
#define MKDEV(major, minor) //將主設備號和次設備號拼接成設備號
#define MAJOR(devno) //從設備號中提取主設備號
#define MINOR(devno) //從設備號中提取次設備號
3. 分配和釋放設備號的函數(shù)
register_chrdev_region函數(shù)用來將一系列字符設備號分配給字符設備懂算。這一系列字符設備有相同的主設備號,次設備號是連續(xù)的庇麦。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
各個參數(shù)的含義计技。
參數(shù) | 含義 |
---|---|
from | 要分配的這一系列設備號的第一個設備號 |
count | 分配的設備號的個數(shù) |
name | 設備的名字 |
返回值。設備分配成功時返回0山橄,分配失敗時垮媒,例如要分配的設備號已經被占用了,則返回一個負值的錯誤碼航棱。
unregister_chrdev_region函數(shù)用來將設備號釋放掉睡雇。由于設備號是臨界資源,同一個設備號不能被多個設備共有饮醇,因此設備使用結束后一定要及時釋放它抱,以免不必要的資源浪費。
void unregister_chrdev_region(dev_t from, unsigned count)
該函數(shù)參數(shù)的含義與register_chrdev_region參數(shù)的含義相同驳阎。
4. 設備號分配的原理
在Linux內核中定義了一個全局指針數(shù)組chrdevs抗愁,用來管理分配出去的設備號。chrdevs的每一個元素都是一個指向char_device_struct結構體的指針呵晚。它的具體定義如下蜘腌。
fs/char_dev.c
-----------------------------------------------------------------------------------------------
#define CHRDEV_MAJOR_HASH_SIZE 255
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev;
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
每個成員的含義如下。
成員 | 含義 |
---|---|
next | 鏈表中指向下一個節(jié)點的指針 |
major | 主設備號 |
baseminor | 次設備號的第一個 |
minorct | 申請的次設備號的數(shù)量 |
name | 設備的名字 |
cdev | 下文中會具體介紹cdev結構體 |
chrdevs實際上是一個哈希表饵隙,它的關鍵字為主設備號major撮珠,散列函數(shù)為index = major % 255。初始狀態(tài)下金矛,沒有設備號被分配出去芯急,chrdevs數(shù)組為空。一旦有設備號被分配驶俊,則首先為該設備創(chuàng)建一個char_device_struct結構體對象娶耍,然后根據(jù)主設備號計算得到散列結果,將結構體對象掛載在chrdevs中的對應位置上饼酿。如果該位置已經有節(jié)點榕酒,則根據(jù)次設備號由小到大的尋找到合適的位置,掛載在對應節(jié)點的next指針上故俐,構成有序列表想鹰。
下面舉個例子說明這個過程。
初始狀態(tài)下的的chrdevs表药版,實際上是255個char_device_struct *類型指針辑舷。
使用register_chrdev_region函數(shù),向系統(tǒng)注冊一個主設備號major = 257槽片、次設備號為0到3的設備何缓、名稱為“dev1”的設備肢础,則需要首先創(chuàng)建一個char_device_struct結構體,并對結構體中的成員進行初始化歌殃,然后計算散列值i = 257 % 255 = 2乔妈,并讓chrdevs[2]這個指針指向這個結構體。
如果我們再向系統(tǒng)注冊一個主設備號為2氓皱、次設備號為0到1路召、名稱為“dev2”的設備,則同樣創(chuàng)建一個char_device_struct結構體并初始化波材。這次我們計算得到的散列值i = 2 % 255依舊為2股淡,與上一個設備在哈希表上產生了沖突,這時結構體中的指針發(fā)揮了作用廷区,我們將新結構體掛在老結構體的前面或后面唯灵,形成一個鏈表即可。掛載的位置由主設備號的大小決定隙轻,我們要求形成的鏈表是一個major由小到大排序的有序鏈表埠帕。
四、字符設備的內核抽象
1. 字符設備結構體cdev
內核為字符設備抽象出一個結構體cdev玖绿,定義如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operation *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
每個成員的含義如下敛瓷。
成員 | 含義 |
---|---|
kobj | 內嵌的內核對象,此處不展開討論 |
owner | 字符設備驅動程序所在的內核模塊指針斑匪,此處不展開討論 |
ops | 提供給VFS的文件接口結構體 |
list | 將系統(tǒng)中的字符設備形成鏈表 |
dev | 字符設備的設備號 |
count | 隸屬于同一主設備號的次設備號的數(shù)量呐籽,表示當前設備驅動程序控制的設備的數(shù)量 |
2. 字符設備的初始化
Linux內核為初始化cdev對象提供了cdev_init函數(shù),定義如下蚀瘸。
fs/char_dev.c
-----------------------------------------------------------------------------------------------
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
這個函數(shù)的作用就是針對一個cdev結構體里面的成員進行初始化狡蝶,其中最重要的一步就是把cdev和fops連接在一起,因為每一個設備都會有一個自己的文件操作邏輯贮勃,即需要實現(xiàn)一個自己的file_operations結構體贪惹。
3. 向系統(tǒng)添加或刪除字符設備
將字符設備加入到系統(tǒng)中,簡單地說寂嘉,就是將上文中我們初始化的cdev結構體添加到Linux系統(tǒng)維護的一個全局哈希鏈表cdev_map中奏瞬,這樣其他模塊才能使用通過cdev_map找到它。Linux為了完成這個操作垫释,提供了cdev_add函數(shù)丝格,實現(xiàn)如下撑瞧。
fs/char_dev.c
-----------------------------------------------------------------------------------------------
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
函數(shù)參數(shù)的含義如下棵譬。
參數(shù) | 含義 |
---|---|
p | 要加入系統(tǒng)的字符設備對象的指針 |
dev | 該設備的設備號 |
count | 被注冊的設備數(shù)量 |
該函數(shù)的核心功能在kobj_map函數(shù)中實現(xiàn),該函數(shù)的實現(xiàn)略微有些復雜预伺,此處不再贅述订咸,有興趣的可以閱讀附錄曼尊。
相應的,刪除設備需要使用cdev_del函數(shù)脏嚷。該函數(shù)的作用就是將對應的設備從系統(tǒng)中移除骆撇,即從cdev_map鏈表中刪除節(jié)點并釋放內存空間。其定義如下父叙。
fs/char_dev.c
-----------------------------------------------------------------------------------------------
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p-> kobj);
}
五神郊、設備節(jié)點
1. 什么是設備節(jié)點
設備節(jié)點也可以稱為設備文件,是一種特殊類型的文件趾唱,其作用是溝通用戶空間的應用程序和內核空間的驅動程序的節(jié)點涌乳。應用程序如果想使用驅動程序提供的服務,則必須經過設備節(jié)點實現(xiàn)甜癞。
2. 設備節(jié)點的創(chuàng)建
設備節(jié)點有靜態(tài)創(chuàng)建和動態(tài)創(chuàng)建兩種方式夕晓,我們這里只介紹靜態(tài)創(chuàng)建方法。Linux系統(tǒng)使用mknod命令靜態(tài)創(chuàng)建設備節(jié)點悠咱,該命令需要列出設備節(jié)點的設備節(jié)點名蒸辆、主設備號和次設備號。通常情況下析既,Linux系統(tǒng)將所有設備節(jié)點都放在/dev目錄下躬贡,因此我們也將設備節(jié)點創(chuàng)建在/dev目錄下。該命令具體格式如下渡贾。
mknod /dev/<設備節(jié)點名> c <主設備號> <次設備號>
參數(shù)c表示節(jié)點的類型為字符設備逗宜。
mknod命令是通過調用mknod函數(shù)實現(xiàn),它會通過系統(tǒng)調用sys_mknod進入內核空間空骚,并生成一個inode纺讲。
inode是Linux文件管理系統(tǒng)維護的一個結構體,每一個文件(不限于設備節(jié)點)都會有一個自己inode囤屹,用來存儲文件的靜態(tài)信息熬甚,如文件訪問權限、屬主肋坚、組乡括、大小、生成時間智厌、訪問時間诲泌、最后修改時間等。
由于inode結構體中的成員非常多铣鹏,我們此處不再一一列舉敷扫,只列舉幾個常用的。
struct inode {
kuid_t i_uid; //屬主的ID(UID)
kgid_t i_gid; //屬主的組ID(GID)
loff_t i_size; //文件大小
dev_t i_rdev;
struct cdev *i_cdev;
......
};
這里我們主要用到i_rdev和i_cdev兩個成員诚卸,i_rdev是該設備的設備號葵第,i_cdev是該設備的cdev結構體绘迁。
3. 設備節(jié)點的打開
在創(chuàng)建了設備節(jié)點和它的inode后,應用程序就可以像打開普通文件一樣使用open函數(shù)打開設備節(jié)點卒密。用戶空間下的應用程序使用open系統(tǒng)調用缀台,其函數(shù)原型如下。
int open(const char *filename, int flags, mode_t mode);
參數(shù)的含義
參數(shù) | 含義 |
---|---|
filename | 要打開的文件的文件名 |
flags | 該文件的打開模式或創(chuàng)建模式 |
mode | 僅在創(chuàng)建一個新文件時使用哮奇,用于指定新建文件的訪問權限 |
返回值 | 成功時返回該文件的文件描述符膛腐,失敗時返回-1 |
文件描述符(File Discriptor, fd)本質是一個int型變量(非負整數(shù)),可以看作Linux系統(tǒng)為每個打開的文件分配的一個索引值鼎俘。在后續(xù)對該文件的read依疼、write、close等操作時而芥,都要通過這個索引值律罢,來找到這個打開的文件。
系統(tǒng)調用open通過層層復雜的機制棍丐,最終會調用到我們?yōu)樵撛O備編寫的file_operations結構體中的.open函數(shù)中误辑,最終實現(xiàn)該設備的打開操作。
Linux會為每一個打開的文件維護一個file結構體歌逢,它在文件打開時被創(chuàng)建巾钉,直到該文件關閉時被釋放。與inode結構體不同秘案,一個文件只能有一個inode結構體砰苍,但如果它被同時打開很多次,那么Linux會為它創(chuàng)建多個file結構體阱高,且file結構體最終指向同一個inode赚导。
file結構體中的成員較多,下面列舉一些常用的成員赤惊。
struct file {
struct inode *f_inode;
const struct file_operations *f_op;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
void *private_data;
......
};
幾個常用的成員及其含義如下吼旧。
成員 | 含義 |
---|---|
f_op | 與文件關聯(lián)的各種操作file_operations |
f_count | 記錄該文件對象被引用的次數(shù),也就是有多少個進程正在使用該文件 |
f_flags | 文件打開時指定的標志未舟,驅動程序中常用O_NONBLOCK來檢查是否是非阻塞請求 |
f_mode | 文件的讀寫模式圈暗,通過置位FMODE_READ和FMODE_WRITE,來確定文件是可讀裕膀、可寫或者既可讀又可寫的 |
f_pos | 文件當前的讀寫位置员串,本質是一個long long類型的值 |
private_data | 用來保存自定義設備結構體的地址 |
以上就是字符設備中常用的數(shù)據(jù)結構和算法愈腾,在下一篇博客中伤柄,我會舉一個實例膘掰,來使用上述內容完成一個應用程序與驅動程序交互的實例肄扎。