前面內(nèi)容:
1 Linux驅(qū)動—內(nèi)核模塊基本使用
2 Linux驅(qū)動—內(nèi)核模塊參數(shù),依賴(進(jìn)一步討論)
linux根據(jù)驅(qū)動程序?qū)崿F(xiàn)的模型框架將設(shè)備的驅(qū)動分為了三類:
- 字符設(shè)備驅(qū)動:以字節(jié)流為單位順序讀寫,不能隨機(jī)訪問。如,幀緩沖
( framebuffer)驅(qū)動,聲卡,串口等。 - 塊設(shè)備驅(qū)動:以固定大小的塊(block) 為單位讀寫,可以隨機(jī)訪問。
- 網(wǎng)絡(luò)設(shè)備驅(qū)動:網(wǎng)絡(luò)接口是一個(gè)能夠和其他主機(jī)交換數(shù)據(jù)的設(shè)備。接口通常是一個(gè)硬件設(shè)備,但也可能是個(gè)純軟件設(shè)備,比如回環(huán)(loopback)接口。網(wǎng)絡(luò)接口由內(nèi)核中的網(wǎng)絡(luò)子系統(tǒng)驅(qū)動,負(fù)責(zé)發(fā)送和接收數(shù)據(jù)包。
以上驅(qū)動程序的分類是按照驅(qū)動的模型框架進(jìn)行的,在現(xiàn)實(shí)生活中,有的設(shè)備很難被嚴(yán)格界定是字符設(shè)備還是塊設(shè)備。甚至有的設(shè)備同時(shí)具有兩類驅(qū)動,如MTD (存儲技術(shù)設(shè)備,如閃存)。一個(gè)設(shè)備的驅(qū)動屬于上述三類中的哪一類, 還要看具體的使用場合和最終的用途。
字符設(shè)備驅(qū)動基礎(chǔ)
在正式學(xué)習(xí)字符設(shè)備驅(qū)動之前,我們來看看相關(guān)的基礎(chǔ)知識。
在類UNIX系統(tǒng)中,有一個(gè)眾所周知的說法,即“一切皆文件”,當(dāng)然網(wǎng)絡(luò)設(shè)備是一個(gè)例外。這就意味著設(shè)備最終也會體現(xiàn)為一個(gè)文件,應(yīng)用程序要對設(shè)備進(jìn)行訪問,最終就會轉(zhuǎn)化為對文件的訪問,這樣做的好處是統(tǒng)一了 對上層的接口。
設(shè)備文件通常位于/dev目錄
下,使用下面的命令可以看到很多設(shè)備文件及其相關(guān)的信息。
ls -l dev
在上面列出的信息中,前面的字母“b”表示是塊設(shè)備,“c” 表示是字符設(shè)備。
比如sda、sda1, sda2、 sda5就是塊設(shè)備,實(shí)際上這些設(shè)備是筆者的Ubuntu主機(jī)上的一個(gè)硬盤和這個(gè)硬盤上的三個(gè)分區(qū),其中sda
表示的是整個(gè)硬盤,而sdal1、sda2、 sda5分別是三個(gè)分區(qū)。tty0、 tty1 就是終端設(shè)備
,shell 程序使用這些設(shè)備來同用戶進(jìn)行交互。
從上面的打印信息來看,設(shè)備文件和普通文件有很多相似之處,都有相應(yīng)的權(quán)限、所屬的用戶和組、修改時(shí)間和名字。但是設(shè)備文件會比普通文件多出兩個(gè)數(shù)字,這兩個(gè)數(shù)字分別是主設(shè)備號和次設(shè)備號
。這兩個(gè)號是設(shè)備在內(nèi)核中的身份或標(biāo)志,是內(nèi)核區(qū)分不同設(shè)備的唯一信息。通常內(nèi)核用主設(shè)備號區(qū)別一類設(shè)備,次設(shè)備號用于區(qū)分同一類設(shè)備的不同個(gè)體或不同分區(qū)。而路徑名則是用戶層用于區(qū)別設(shè)備信息的。
通過mknod命令來創(chuàng)建一個(gè)設(shè)備文件
mknod /dev/vser0 c 256 0
ls -li /dev/vser0
mknod是make node的縮寫,就是創(chuàng)建一個(gè)節(jié)點(diǎn)(設(shè)備文件)。
在linux系統(tǒng)中,一個(gè)節(jié)點(diǎn)代表一個(gè)文件,創(chuàng)建一個(gè)文件最主要的工作就是分配一個(gè)新的節(jié)點(diǎn)。
包含節(jié)點(diǎn)號的分配(節(jié)點(diǎn)號在系統(tǒng)中是唯一的,可以區(qū)分不同的文件)。
如上面的命令的結(jié)果會出現(xiàn)126695 crw-r--r-- 1 root root 256, 0 Jul 13 10:03 /dev/vser0
這里的126695就是節(jié)點(diǎn)號
然后初始化這個(gè)節(jié)點(diǎn)的(文件模式 crw-r--r-- 、訪問時(shí)間 Jul 13 10:03 、用戶ID 1 、組ID 13等信息
如果是設(shè)備文件需要初始化好設(shè)備號
再將這個(gè)初始化好的節(jié)點(diǎn)放入磁盤,還需要在文件所在目錄下添加一個(gè)目錄項(xiàng),目錄項(xiàng)中包含了前面分配的節(jié)點(diǎn)號和文件的名字,然后寫入磁盤。存在磁盤上的這個(gè)節(jié)點(diǎn)用一個(gè)結(jié)構(gòu)封裝。
下面用extr2文件系統(tǒng)為例:
在linux3.14內(nèi)核文件中
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
.....
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
......
};
可以從這里清楚的看出一個(gè)node節(jié)點(diǎn)的結(jié)構(gòu)體包含的數(shù)值,前面說的節(jié)點(diǎn)的(文件模式 crw-r–r-- 、訪問時(shí)間 Jul 13 10:03 、用戶ID 1 、組ID 13等信息)都有
另外,對于i_blocks
,
- 如果是普通文件,則這個(gè)數(shù)組存放的是真正的文件數(shù)據(jù)所在的
塊號
(看成對文件的索引,所以ext2文件是按照索引的方式找的); - 如果是設(shè)備文件,這個(gè)數(shù)組主要存放設(shè)備的
主次設(shè)備號
可以看下面代碼:
static int __ext2_write_inode(struct inode *inode, int do_sync)
{
struct ext2_inode * raw_inode = ext2_get_inode(sb, ino, &bh);
.......
raw_inode->i_mode = cpu_to_le16(inode->i_mode);
.......
if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) {
if (old_valid_dev(inode->i_rdev)) {
raw_inode->i_block[0] =
cpu_to_le32(old_encode_dev(inode->i_rdev));
raw_inode->i_block[1] = 0;
} else {
raw_inode->i_block[0] = 0;
raw_inode->i_block[1] =
cpu_to_le32(new_encode_dev(inode->i_rdev));
raw_inode->i_block[2] = 0;
}
}
......
}
這個(gè)struct ext2_inode * raw_inode = ext2_get_inode(sb, ino, &bh);
獲得了一個(gè)要寫入磁盤的ext2_inode結(jié)構(gòu),并初始化了部分成員。
這個(gè)
if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) {
if (old_valid_dev(inode->i_rdev)) {
raw_inode->i_block[0] =
cpu_to_le32(old_encode_dev(inode->i_rdev));
raw_inode->i_block[1] = 0;
} else {
raw_inode->i_block[0] = 0;
raw_inode->i_block[1] =
cpu_to_le32(new_encode_dev(inode->i_rdev));
raw_inode->i_block[2] = 0;
}
}
判斷了設(shè)備的類型,如果是字符設(shè)備或塊設(shè)備,那么將設(shè)備號寫入i_ block 的前2個(gè)或前3個(gè)元素
,其中ionde的i_dev 成員就是設(shè)備號
。而這里的inode是存在于內(nèi)存中的節(jié)點(diǎn)
,是涉及文件操作的一個(gè)非常關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)
關(guān)于該結(jié)構(gòu)我們之后還要討論,這里只需要知道寫入磁盤中的ext2_ inode
結(jié)構(gòu)內(nèi)的成員基本上都是靠存在于內(nèi)存中的inode中對應(yīng)的成員初始化
的即可,其中就包含了這里講的設(shè)備號。之前我們說過,設(shè)備號有主、次設(shè)備號之分,而這里的設(shè)備號只有一個(gè)。原因是主、次設(shè)備號的位寬有限制,可以將兩個(gè)設(shè)備號合并
,之后我們會看到相應(yīng)的代碼。
在代碼raw_inode->i_mode = cpu_to_le16(inode->i_mode);
我們可以看到,文件的類型也被保存在了ext2_ inode 結(jié)構(gòu)中,并且寫在了磁盤上。
剛才還談到需要在文件所在目錄下添加目錄項(xiàng),這又是怎樣完成的呢?
文件目錄下添加目錄項(xiàng)
linux系統(tǒng)中,目錄本身也是一個(gè)文件,其中保存的數(shù)據(jù)是若干個(gè)目錄項(xiàng),目錄項(xiàng)的主要內(nèi)容就是剛才分配的節(jié)點(diǎn)號和文件或子目錄的名字。
在ext2中,寫入磁盤的目錄項(xiàng)數(shù)據(jù)結(jié)構(gòu)如下:
上面的inode就是節(jié)點(diǎn)號,name成員就是文件或者子目錄的名字。
具體代碼實(shí)現(xiàn)可以參考fs/ext2/namei.c”的ext2_mknod函數(shù).
下圖說明了mknod命令在ext2文件系統(tǒng)上完成的工作
上面的整個(gè)過程,就是mknod命令將文件名、文件類型、和主次設(shè)備號等信息保存在磁盤上
接下來我們來討論如何打開一個(gè)文件,這是理解上層應(yīng)用程序和底層驅(qū)動程序如何建立聯(lián)系的關(guān)鍵,也是理解字符設(shè)備驅(qū)動編寫方式的關(guān)鍵。整個(gè)過程非常煩瑣,涉及的數(shù)據(jù)結(jié)構(gòu)和相關(guān)的內(nèi)核知識非常多。為了便于大家理解,下面將該過程進(jìn)行大量簡化,并以圖3.2和調(diào)用流程來進(jìn)行說明。
理解以下流程:
在內(nèi)核中,一個(gè)進(jìn)程用一個(gè)task_ struct
結(jié)構(gòu)對象來表示,其中的files成員指向了一個(gè)files_ struct 結(jié)構(gòu)變量,該結(jié)構(gòu)中有一個(gè)fd_ array
的指針數(shù)組(用于維護(hù)打開文件的信息), 數(shù)組的每一個(gè)元素是指向file 結(jié)構(gòu)的一個(gè)指針。 open 系統(tǒng)調(diào)用函數(shù)在內(nèi)核中對應(yīng)的函數(shù)是sys_ _open
, sys_ open調(diào)用了do_ sys_ open
,在do_ sys_ open中首先調(diào)用了getname
函數(shù)將文件名從用戶空間復(fù)制到了內(nèi)核空間。接著調(diào)用get__unused fd_ flags
來獲取一個(gè)未使用的文件描述符,要獲得該描述符,其實(shí)就是搜索files_ _struct
中的fd_ arrary
數(shù)組,查看哪一個(gè)元素沒有被使用,然后返回其下標(biāo)即可
。接下來調(diào)用do_ filp. _open
函數(shù)來構(gòu)造一-個(gè) file結(jié)構(gòu)
,并初始化里面的成員。其中最重要的是將它的f _op
成員指向和設(shè)備對應(yīng)的驅(qū)動程序的操作方法集合的結(jié)構(gòu)file_ operations
, 這個(gè)結(jié)構(gòu)中的絕大多數(shù)成員都是函數(shù)指針,通過file_operations
中的open函數(shù)指針可以調(diào)用驅(qū)動中實(shí)現(xiàn)的特定于設(shè)備的打開函數(shù),從而完成打開的操作。do_ filp_open
函數(shù)執(zhí)行成功后,調(diào)用fd_ install
函數(shù),該函數(shù)將剛才得到的文件描述符作為訪問fd_ array
數(shù)組的下標(biāo),讓下標(biāo)對應(yīng)的元素指向新構(gòu)造的file 結(jié)構(gòu)。
最后系統(tǒng)調(diào)用返回到應(yīng)用層,將剛才的數(shù)組下標(biāo)作為打開文件的文件描述符返回。
do_ filp_ open
函數(shù)包含的內(nèi)容很多,是這個(gè)過程中最復(fù)雜的一部分,下面進(jìn)行一下非
常簡化的介紹。do_ filp_ open 函數(shù)調(diào)用path _openat
來進(jìn)行實(shí)際的打開操作,path_ openat
調(diào)用get_empty_ filp
快速得到一一個(gè)file結(jié)構(gòu),再調(diào)用link. path walk來處理文件路徑中除最后一個(gè)分量的前面部分。
舉個(gè)例子來說,如果要打開/dev/vser0這個(gè)文件,那么link_path_ walk
需要處理/dev這部分,包含根目錄和dev目錄。
接下來path _openat 調(diào)用do_ last
來處理最后一個(gè)分量,do_ last
首先調(diào)用lookup_fast
在RCU模式下來嘗試快速查找,如果第一次這么做會失敗,所以繼續(xù)調(diào)用lookup_ open
, 而lookup_ open
首先調(diào)用lookup_dcache
在目錄項(xiàng)高速緩存中進(jìn)行查找,第一次這么做也會失敗,所以轉(zhuǎn)而調(diào)用lookup_ real, lookup_ real
則在磁盤上真正開始查找最后一個(gè)分量所對應(yīng)的節(jié)點(diǎn),如果是ext2文件系統(tǒng),則會調(diào)用ext2_ lookup, 得到inode 的編號后
,ext2_ lookup 又會調(diào)用ext2_ iget
從磁盤上獲取之前使用mknod保存的節(jié)點(diǎn)信息
。對字符設(shè)備驅(qū)動來說,這里最重要的就是將文件類型和設(shè)備號取出并填充到了內(nèi)存中的inode 結(jié)構(gòu)的相關(guān)成員中。
另外,通過判斷文件的類型,還將inode中的f_op
指針指向了def _chr_ fops,
這個(gè)結(jié)構(gòu)中的open函數(shù)指針指向了chrdev_ open
, 那么自然chrdev_ open 緊接著會被調(diào)用。chrdev_ open
完成的主要工作是:首先根據(jù)設(shè)備號找到添加在內(nèi)核中代表字符設(shè)備的cdev (cdev 是放在cdev_ map 散列表中的,驅(qū)動加載時(shí)會構(gòu)造相應(yīng)的cdev并添加到這個(gè)散列表中,并且在構(gòu)造這個(gè)cdev時(shí)還實(shí)現(xiàn)了一個(gè)操作方法集合,由cdev的ops成員指向它),找到對應(yīng)的cdev對象后,用cdev關(guān)聯(lián)的操作方法集合替代之前構(gòu)造的file結(jié)構(gòu)中的操作方法集合,然后調(diào)用edev所關(guān)聯(lián)的操作方法集合中的打開函數(shù),完成設(shè)備真正的打開操作,這也標(biāo)志著do_ filp_ open 函數(shù)基本結(jié)束。
為了下一次能夠快速打開文件,內(nèi)核在第一次打開一個(gè)文件或 目錄時(shí)都會創(chuàng)建一個(gè)dentry
的目錄項(xiàng),它保存了文件名和所對應(yīng)的inode 信息,所有的dentry使用散列
的方式存儲在目錄項(xiàng)高速緩存中,內(nèi)核在打開文件時(shí)會先在這個(gè)高速緩存中查找相應(yīng)的dentry,如果找到,則可以立即獲取文件所對應(yīng)的inode,否則就會在磁盤上獲取。對于字符設(shè)備驅(qū)動來說,設(shè)備號、cdev 和操作方法集合至關(guān)重要,內(nèi)核找到路徑名所對應(yīng)的inode后,要和驅(qū)動建立連接,首先要做的就是根據(jù)inode中的設(shè)備號找到cdev,然后根據(jù)cdev找到關(guān)聯(lián)的操作方法集合,從而調(diào)用驅(qū)動所提供的操作方法來完成對設(shè)備的具體操作。可以說,字符設(shè)備驅(qū)動的框架就是圍繞著設(shè)備號、cdev 和操作方法集合來實(shí)現(xiàn)的。
雖然設(shè)備的打開操作很煩瑣,但是其他系統(tǒng)的調(diào)用過程就要簡單很多。因?yàn)榇蜷_操
作返回了一個(gè)文件描述符,其他系統(tǒng)調(diào)用時(shí)都會以這個(gè)文件描述符作為參數(shù)傳遞給內(nèi)核,
內(nèi)核得到這個(gè)文件描述符后可以直接索引fd_ array,找到對應(yīng)的file結(jié)構(gòu),然后調(diào)用相應(yīng)
的方法。
字符設(shè)備驅(qū)動框架
cdev結(jié)構(gòu)
視頻:
描述字符設(shè)備使用cdev結(jié)構(gòu)
struct cdev {
struct kobject kobj; //父類。
struct module *owner; //當(dāng)前結(jié)構(gòu)所屬模塊,THIS_ MODULE (當(dāng)前模塊)。
const struct file_ operations *ops; //設(shè)備對應(yīng)操作。
struct list_head list; //內(nèi)核鏈表,內(nèi)核用來管理字符設(shè)備。
dev_ t dev;//設(shè)備編號(dev_ t) ,高12主設(shè)備號,低20位次設(shè)備號。
unsigned int count;//次設(shè)備號個(gè)數(shù)。
};
內(nèi)核鏈表就是一個(gè)struct
設(shè)備編號 高12主設(shè)備號,低20位次設(shè)備號。如果是-crw 以-開頭是普通文件
l開頭是鏈接文件
d開頭是目錄文件
b開頭是塊設(shè)備
還得用次設(shè)備號區(qū)分不同的設(shè)備
用主設(shè)備號區(qū)分不同的程序(文件不同)
struct file_ operations *ops; //設(shè)備對應(yīng)操作
struct file_ operations {
struct module *owner; //THIS MODULE.
ssize_ t (*read) (struct file *, char__ user*, size_t, loff_t *); //對應(yīng)系統(tǒng)調(diào)用read.
ssize_ t (*write) (struct file *, const char_user*, size_ t, loff_t *); //對應(yīng)系統(tǒng)調(diào)用write.
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //對應(yīng)系統(tǒng)調(diào)用ioctl.
int (*open) (struct inode *, struct file *); //對應(yīng)系統(tǒng)調(diào)用open.
int (*release) (struct inode *, struct file *); //對應(yīng)系統(tǒng)調(diào)用close.
[..]
};
下面介紹一下 字符設(shè)備函數(shù)
字符設(shè)備函數(shù)
分配字符設(shè)備: struct cdev *cdev_ alloc(void); //已經(jīng)初始化,內(nèi)存自動釋放(不用free)
初始化字符設(shè)備: void cdev_ init(struct cdev *cdev, const struct file_ operations *fops); 。
注意:除使用cdev_ alloc
函數(shù)分配的字符設(shè)備以外,都可以調(diào)用該函數(shù)初始化。如初始化靜態(tài)分配的字符設(shè)備。
也就是說 init初始化與調(diào)用cdev_ alloc
不能同時(shí)使用
添加設(shè)備:int cdev_ aldd(struct cdev *p, dev_ t dev, unsigned count)。
參數(shù):
p,字符設(shè)備指針。
dev,設(shè)備編號,
count,次設(shè)備號數(shù)。 子設(shè)備
返回值:成功返回0,失敗返回錯(cuò)誤碼。
刪除設(shè)備: void cdev_ del(struct cdev *p)。
設(shè)備編號: 設(shè)備編號是一個(gè)32無符號整數(shù)(dev _t), 高12主設(shè)備號,低20位次設(shè)備號。
主設(shè)備號用來識別驅(qū)動,次設(shè)備號用來區(qū)分不同的設(shè)備。
MKDEV(major, minor); //構(gòu)造設(shè)備編號,轉(zhuǎn)換為設(shè)備編號類型dev_t
//MKDEV是將主設(shè)備號和次設(shè)備號轉(zhuǎn)換成dev_t類型的一個(gè)內(nèi)核函數(shù)。
MAJOR(dev_ t); //取主設(shè)備號.
MINOR(dev_ t); //取次設(shè)備號.
下面我們來試試如何實(shí)現(xiàn)驅(qū)動
實(shí)現(xiàn)字符設(shè)備驅(qū)動
通過上面我們知道了cdev結(jié)構(gòu),但是我們要將cdev構(gòu)造就要將cdev架構(gòu)對象添加到內(nèi)核的cdev_map散列表中
注冊設(shè)備號
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1 //數(shù)量1 設(shè)備號1個(gè)
#define VSER_DEV_NAME "vser"
static int __init vser_init(void)
{
int ret;
dev_t dev;
dev = MKDEV(VSER_MAJOR, VSER_MINOR);
ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
if (ret)
goto reg_err;
return 0;
reg_err:
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev;
dev = MKDEV(VSER_MAJOR, VSER_MINOR);
unregister_chrdev_region(dev,VSER_DEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");
在模塊的初始化函數(shù)中:
#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1 //數(shù)量1 設(shè)備號1個(gè)
#define VSER_DEV_NAME "vser"
dev = MKDEV(VSER_MAJOR, VSER_MINOR);
這里用MKDEV宏將主設(shè)備號和次設(shè)備號合并成一個(gè)一個(gè)設(shè)備號dev
在內(nèi)核源碼中相關(guān)宏定義如下:
#define MINORBITS 20
#define MINORMASK((1U << MINORBITS) -1)
#define MAJOR (dev) ((unsigned int)((dev)>> MINORBITS))
#define MINOR (dev)((unsigned int)((dev)&MINORMASK) )
#define MKDEV (ma,mi) (((ma) << MINORBITS)|(mi))
不難發(fā)現(xiàn),該宏的作用是將主設(shè)備號左移20位和次設(shè)備號相或(12高主設(shè)備號+20低次設(shè)備號=32位設(shè)備號)
構(gòu)造好設(shè)備號后
代碼ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
它調(diào)用了register_chrdev_region
將構(gòu)造的設(shè)備號注冊到內(nèi)核中,表明該設(shè)備號已經(jīng)被占用,如果有其他驅(qū)動隨后要注冊該設(shè)備號不行。
其函數(shù)原型:int register chrdev_region(dev_t from, unsigned count, const char *name);
該函數(shù)一次可以注冊多個(gè)連續(xù)的號,由count形參指定個(gè)數(shù),由from指定起始的設(shè)備號,name用于標(biāo)記主設(shè)備號的名稱。該函數(shù)成功則返回0,不成功則返回負(fù)數(shù)
返回負(fù)數(shù)通常是因?yàn)橐缘脑O(shè)備號已經(jīng)被其他的驅(qū)動搶先注冊了。如果注冊出錯(cuò),則使用goto
語句跳轉(zhuǎn)到錯(cuò)誤處理代碼處執(zhí)行,否則初始化函數(shù)返回0。
在卸載模塊時(shí)候,已注冊的號應(yīng)該從內(nèi)核中注銷,否則再次加載該驅(qū)動時(shí)候,注冊設(shè)備號操作會失敗
。代碼中調(diào)用unregister_chrdev_region(dev,VSER_DEV_CNT);
上面的代碼再一次印證了前面所說的內(nèi)容,即在模塊初始化的函數(shù)中負(fù)責(zé)注冊、分配內(nèi)存等操作,而在模塊清除函數(shù)中負(fù)責(zé)相反的操作,即注銷、釋放內(nèi)存等操作。
以上的代碼可以編譯并進(jìn)行測試,在Ubuntu主機(jī)上測試的步驟如下( 在ARM目標(biāo)板上的測試和前面所講的模塊在ARM目標(biāo)板上測試的過程類似)。
下面試試編譯
make
make modules_install
sudo insmod vser.ko
或者
modprobe vser
然后要查看,先建立個(gè)文件proc,再在里面建立devices目錄
主要是為了使用cat /proc/devices查看設(shè)備號
character devices如下
256號是vser,這樣就相當(dāng)于注冊好了
使用register _chrdev_ region
注冊設(shè)備號的方式稱為靜態(tài)注冊設(shè)備號
,但是該方式有一個(gè)明顯的缺點(diǎn),就是如果兩個(gè)驅(qū)動都使用了同樣的設(shè)備號,那么后加載的驅(qū)動將會失敗,因?yàn)樵O(shè)備號沖突了。為了解決這個(gè)問題,可以使用動態(tài)分配設(shè)備號的函數(shù),其原型如下:
int alloc_chrdev_region(dev_t*dev, unsigned baseminor, unsigned count, const char*name) ;
其中,count和name形參同register_ chrdev_ region函數(shù)中相應(yīng)的形參一致 。baseminor
是動態(tài)分配的設(shè)備號的起始次設(shè)備號,而dev
則是分配得到的第一個(gè)設(shè)備號。 該函數(shù)成功則返回0,失敗則返回負(fù)數(shù)。
這樣就避免了各個(gè)驅(qū)動使用相同的設(shè)備號而帶來的沖突,
但是會存在另外-一個(gè)問題,那就是不能事先知道主次設(shè)備號,在使用mknod命令創(chuàng)建設(shè)備節(jié)點(diǎn)時(shí),必須先查看/proc/devices文件才能確定主設(shè)備號(次設(shè)備號在代碼中確定),也就是要求mknod命令要后于驅(qū)動加載執(zhí)行,不過這個(gè)問題在新的Linux設(shè)備模型中已經(jīng)得到了比較好的解決,設(shè)備節(jié)點(diǎn)會自動地創(chuàng)建和銷毀,這在后面的章節(jié)會詳細(xì)描述。
成功注冊了設(shè)備號后,接下來應(yīng)該構(gòu)造并添加cdev結(jié)構(gòu)對象
構(gòu)造并添加cdev結(jié)構(gòu)對象
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define VSER_MAJOR 256
#define VSER_MINOR 0
#define VSER_DEV_CNT 1
#define VSER_DEV_NAME "vser"
static struct cdev vsdev;
static struct file_operations vser_ops = {
.owner = THIS_MODULE,
};
static int __init vser_init(void)
{
int ret;
dev_t dev;
dev = MKDEV(VSER_MAJOR, VSER_MINOR);
ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
if (ret)
goto reg_err;
cdev_init(&vsdev, &vser_ops);
vsdev.owner = THIS_MODULE;
ret = cdev_add(&vsdev, dev, VSER_DEV_CNT);
if (ret)
goto add_err;
return 0;
add_err:
unregister_chrdev_region(dev, VSER_DEV_CNT);
reg_err:
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev;
dev = MKDEV(VSER_MAJOR, VSER_MINOR);
cdev_del(&vsdev);
unregister_chrdev_region(dev, VSER_DEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");
代碼static struct cdev vsdev;
定義了一個(gè)struct cdev類型的全局變量vsdev。
下面
static struct file_operations vser_ops = {
.owner = THIS_MODULE,
};
定義了一個(gè)struct file_operations類型的全局變量vser_ops
字符設(shè)備操作結(jié)構(gòu)很關(guān)鍵
其中,vsdev表示一個(gè)具體的字符設(shè)備,而vser_ops是操作該設(shè)備的一些方法。
代碼cdev_init(&vsdev, &vser_ops);
調(diào)用cdev_init 初始化了vsdev中的部分成員。
另外一個(gè)最重要的操作就是將vsdev中的ops指針指向了vser_ ops,這樣通過
設(shè)備號找到vsdev對象后,就能找到相關(guān)的操作方法集合,并調(diào)用其中的方法。
cdev_ init
函數(shù)的原型如下,第一個(gè)參數(shù)是要初始化的cdev地址,第二個(gè)參數(shù)是設(shè)備操作方法集合的結(jié)構(gòu)地址。
void cdev_init(struct cdev*cdev,const struct file_operations*fops) ;
繼續(xù)代碼
static struct file_operations vser_ops = {
.owner = THIS_MODULE,
};
static int __init vser_init(void)
{
··············
vsdev.owner = THIS_MODULE;
將owner成員賦值為THIS_MODULE,owner是一個(gè)指向struct module類型變量的指針。
THIS_MODULE
是包含驅(qū)動的模塊中的struct module類型對象的地址
,類似于c++中的this指針。
這樣就能通過vsdev或者vser_fops找到對應(yīng)的模塊
在對前面兩個(gè)對象進(jìn)行訪問時(shí)都要調(diào)用類似于try_module_get
的函數(shù)增加模塊的引用計(jì)數(shù),因?yàn)樵谶@兩個(gè)對象使用的過程中,模塊是不能被卸載的,模塊被卸載的前提條件是引用計(jì)數(shù)為0。
cdev對象初始化以后,就應(yīng)該添加到內(nèi)核中的cdev_ map散列表中,調(diào)用的函數(shù)是cdev_ add,其函數(shù)原型如下int cdev add(struct cdev *p, dev_ t dev, unsigned count) ;
cdev_ add 函數(shù)的主要工作是將主設(shè)備號通過對255取余,將余數(shù)作為cdev_ map數(shù)組的下標(biāo)索引,然后構(gòu)造一個(gè)probe對象,并讓data指向要添加的cdev結(jié)構(gòu)地址,然后加入到鏈表當(dāng)中
該函數(shù)的最后一個(gè)參數(shù)count指定了被添加的cdev可以管理多少個(gè)設(shè)備。這里需要特別注意的是,參數(shù)p只指向一個(gè)cdev對象,但該對象可以同時(shí)管理多個(gè)設(shè)備
,由count 的值來決定具體有多少個(gè)設(shè)備,那么cdev和設(shè)備就不是一 一對應(yīng)的關(guān)系。
這樣,對于一個(gè)驅(qū)動支持多個(gè)設(shè)備的情況,我們可以采用兩種方法來實(shí)現(xiàn),
- 第一種方法是為每一個(gè)設(shè)備分配一個(gè)cdev對象,每次調(diào)用cdev_add 添加一個(gè)cdev對象,直到多個(gè)cdev對象全部被添加到內(nèi)核中
- 第二種方法是只構(gòu)造一個(gè)cdev對象,但在調(diào)用cdev. add 時(shí),指定添加的
cdev可以管理多個(gè)設(shè)備。
這兩種方法我們在后面的例子中都會看到。以上是簡化的討論,
實(shí)際的實(shí)現(xiàn)要復(fù)雜一些,如果要詳細(xì)了解,請參考cdev_ add 的內(nèi)核源碼。
在初始化函數(shù)中添加了cdev對象,那么在清除函數(shù)中自然就應(yīng)該刪除該cdev對象,代碼 cdev_del(&vsdev);
演示了這一操作, 實(shí)現(xiàn)的函數(shù)是cdev_del
, 其函數(shù)原型如下。
void edev_ del (struct cdev *p);
該函數(shù)的作用就是根據(jù)cdev找到散列表中的probe,并進(jìn)行刪除。
在上面的例子中,cdev是靜態(tài)的,我們也可以動態(tài)分配,對應(yīng)的函數(shù)是cdev_alloc
。
struct cdev *cdev_ alloc (void);
該函數(shù)成功則返回動態(tài)分配的cdev 對象地址,失敗則返回NULL。
下面試試編譯加載
從上面的操作可以看到,在未加載驅(qū)動之前,使用cat命令讀取/dev/vser0設(shè)備,錯(cuò)誤信息是設(shè)備找不到,這是因?yàn)檎也坏胶驮O(shè)備號對應(yīng)的cdev對象。文章來源:http://www.zghlxwxcb.cn/news/detail-404567.html
在加載驅(qū)動后,cat命令的錯(cuò)誤信息變成了參數(shù)無效,說明驅(qū)動工作了,只是還未實(shí)現(xiàn)具體的設(shè)備操作的方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-404567.html
到了這里,關(guān)于字符設(shè)備驅(qū)動的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!