擊左上方藍色“一口Linux”,選擇“設為星標”
第一時間看干貨文章 ?【干貨】嵌入式驅(qū)動工程師學習路線 ?【干貨】Linux嵌入式知識點-思維導圖-免費獲取 ?【就業(yè)】一個可以寫到簡歷的基于Linux物聯(lián)網(wǎng)綜合項目 ?【就業(yè)】簡歷模版
本文目標

文件描述符和設備節(jié)點之間關(guān)系;
三大結(jié)構(gòu)體inode,file,file_operations之間的關(guān)系;
驅(qū)動如何區(qū)分出次設備號。
read/write接口函數(shù)要能夠獲得字符設備結(jié)構(gòu)體變量地址。
什么是文件描述符?
Linux 中一切都可以看作文件,包括普通文件、鏈接文件、Socket 以及設備驅(qū)動等,對其進行相關(guān)操作時,都可能會創(chuàng)建對應的文件描述符。文件描述符(file descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用于指代被打開的文件,對文件所有 I/O 操作相關(guān)的系統(tǒng)調(diào)用都需要通過文件描述符。
Linux啟動后,會默認打開3個文件描述符,分別是:
0:標準輸入standard input
1:正確輸出standard output
2:錯誤輸出:error output
以后打開文件后。新增文件綁定描述符 可以依次增加(從3開始累加)。每一條shell命令執(zhí)行,都會繼承父進程的文件描述符。因此,所有運行的shell命令,都會有默認3個文件描述符。

l 進程級別的文件描述符表:內(nèi)核為每個進程維護一個文件描述符表,該表記錄了文件描述符的相關(guān)信息,包括文件描述符、指向打開文件表中記錄的指針。
l 系統(tǒng)級別的打開文件表:內(nèi)核對所有打開文件維護的一個進程共享的打開文件描述表,表中存儲了處于打開狀態(tài)文件的相關(guān)信息,包括文件類型、訪問權(quán)限、文件操作函數(shù)(file_operations)等。
l 系統(tǒng)級別的 i-node 表:i-node 結(jié)構(gòu)體記錄了文件相關(guān)的信息,包括文件長度,文件所在設備,文件物理位置,創(chuàng)建、修改和更新時間等,"ls -i" 命令可以查看文件 i-node 節(jié)點。
進程在Linux內(nèi)核中是由結(jié)構(gòu)體task_struct維護,進程打開的所有文件描述符都在進程維護的結(jié)構(gòu)體task_struct的files變量中維護:
include\linux\sched.h

該結(jié)構(gòu)體定義如下:

該進程所有打開的文件對應的file指針均由fd_array維護,文件描述符和數(shù)組下標一一對應。
文件描述符是一種系統(tǒng)資源,可以通過以下命令來查看文件描述符的上限。
打開文件相關(guān)操作
查看所有進程允許打開的最大 fd 數(shù)量

查看所有進程已經(jīng)打開的 fd 數(shù)量以及允許的最大數(shù)量

查看單個進程允許打開的最大 fd 數(shù)量.

查看某個文件被哪些進程打開?
可以借助lsof命令
編寫調(diào)試代碼如下:

該代碼只打開文件123.c。

&是程序放在后臺運行。
3042:程序進程。
查看某個進程打開了哪些文件?
接著上述的例子,ls -l /proc/{PID}/fd 可以查看某個進程打開了哪些文件。

可以看到該進程打開了出了123.c之外,還打開了前面所述的3個默認文件。


實際開發(fā)中,可能會遇到 fd 資源超過上限導致的 "Too many open files" 之類的問題,一般都是因為沒有及時釋放掉 fd,若循環(huán)執(zhí)行超過單個進程允許打開的最大 fd 數(shù)量,程序就會出現(xiàn)異常。
Linux設備文件三大結(jié)構(gòu):inode,file,file_operations
驅(qū)動程序就是向下控制硬件,向上提供接口,這里的向上提供的接口最終對應到應用層有三種方式:設備文件,/proc,/sys,其中最常用的就是使用設備文件,而Linux設備中用的最多的就是字符設備,本文就以字符設備為例來分析創(chuàng)建并打開一個字符設備的文件內(nèi)部機制。
struct inode
Linux中一切皆文件,當我們在Linux中創(chuàng)建一個文件時,就會在相應的文件系統(tǒng)創(chuàng)建一個inode與之對應.
對于不同的文件類型,inode被填充的成員內(nèi)容也會有所不同,以創(chuàng)建字符設備為例,我們知道,add_chrdev_region其實是把一個驅(qū)動對象和一個(一組)設備號聯(lián)系到一起。而創(chuàng)建設備文件,其實是把設備文件和設備號聯(lián)系到一起。至此,這三者就被綁定在一起了。這樣,內(nèi)核就有能力創(chuàng)建一個struct inode實例了,下面是Linux 3.14內(nèi)核中的inode。這個inode是VFS的inode,是最具體文件系統(tǒng)的inode的進一步封裝,也是驅(qū)動開發(fā)中關(guān)心的inode,針對具體的文件系統(tǒng),還有struct ext2_inode_info 等結(jié)構(gòu)。
//include/linux/fs.h 596
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
dev_t i_rdev;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct mutex i_mutex;
unsigned long dirtied_when; /* jiffies of first dirtying */
struct hlist_node i_hash;
struct list_head i_wb_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock *i_flock;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
void *i_private; /* fs or device private pointer */
};
這里面與本文相關(guān)的成員主要有:

struct inode
--602-->i_mode表示訪問權(quán)限控制
--604-->UID
--605-->GID
--606-->i_flags文件系統(tǒng)標志
--630-->硬鏈接數(shù)計數(shù)
--635-->i_size以字節(jié)為單位的文件大小
--636-->最后access時間
--637-->最后modify時間
--638-->最后change時間
--669-->i_dentry; //目錄項鏈表
--673-->i_count引用計數(shù),當引用計數(shù)變?yōu)?時,會釋放inode實例
--675-->i_writecount寫者計數(shù)
--679-->創(chuàng)建設備文件的時候i_fops填充的是def_chr_fops,blk_blk_fops,def_fifo_fops,bad_sock_fops之一,參見創(chuàng)建過程中調(diào)用的init_special_inode()
--683-->特殊文件類型的union,pipe,cdev,blk.link etc,i_cdev表示這個inode屬于一個字符設備文件,本文中創(chuàng)建設備文件的時候會把與之相關(guān)的設備號的驅(qū)動對象cdev拿來填充
--702-->inode的私有數(shù)據(jù)
struct file
Linux內(nèi)核會為每一個進程維護一個文件描述符表,這個表其實就是struct file[]的索引。open()的過程其實就是根據(jù)傳入的路徑填充好一個file結(jié)構(gòu)并將其賦值到數(shù)組中并返回其索引。下面是file的主要內(nèi)容
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

struct file
--882-->f_path里存儲的是open傳入的路徑,VFS就是根據(jù)這個路徑逐層找到相應的inode
--883-->f_inode里存儲的是找到的inode
--884-->f_op里存儲的就是驅(qū)動提供的file_operations對象,這個對象應該在第一次open()的時候被填充,具體地,應用層的open通過層層搜索會調(diào)用inode.i_fops->open(),即我們注冊的open接口函數(shù)chrdev_open()
--891-->f_count的作用是記錄對文件對象的引用計數(shù),也即當前有多少個使用CLONE_FILES標志克隆的進程在使用該文件。典型的應用是在POSIX線程中。就像在內(nèi)核中普通的引用計數(shù)模塊一樣,最后一個進程調(diào)用put_files_struct()來釋放文件描述符。
--892-->f_flags當打開文件時指定的標志,對應系統(tǒng)調(diào)用open的int flags,比如驅(qū)動程序為了支持非阻塞型操作需要檢查這個標志是否有O_NONBLOCK。
--893-->f_mode;對文件的讀寫模式,對應系統(tǒng)調(diào)用open的mod_t mode參數(shù),比如O_RDWR。如果驅(qū)動程序需要這個值,可以直接讀取這個字段。
--905-->private_data表示file結(jié)構(gòu)的私有數(shù)據(jù)
mknod 做了什么事?
本例假定我們創(chuàng)建兩個串口com0、com1,他們公用同一個主設備號250,次設備號分別為0、1,他們公用同一個字符設備驅(qū)動,那么我們的驅(qū)動要能夠根據(jù)應用進程打開的是設備com0還是com1來操作不同的串口。
首先創(chuàng)建兩個設備節(jié)點:
mknod /dev/com0 250 0
mknod /dev/com1 250 1
執(zhí)行結(jié)果如下:

內(nèi)核為了維護這兩個文件節(jié)點,內(nèi)核需要創(chuàng)建結(jié)構(gòu)體維護這兩個文件,具體如下:

從以上結(jié)構(gòu)圖可以得知,當我們通過命令mknod 創(chuàng)建一個字符設備文件,那么內(nèi)核就會創(chuàng)建好一個inode會存在存儲器中,創(chuàng)建和該文件實體一一對應的inode。這個inode和其他的inode一樣,用來存儲關(guān)于這個文件的靜態(tài)信息(不變的信息),包括這個設備文件對應的設備號,文件的路徑以及對應的驅(qū)動對象等。
inode作為VFS四大對象之一,在驅(qū)動開發(fā)中很少需要自己進行填充,更多的是在open()方法中進行查看并根據(jù)需要填充我們的file結(jié)構(gòu)。
由上圖所示,創(chuàng)建字符設備 /dev/com0、 /dev/com1,只是增加了對應的inode節(jié)點,此時VFS層并沒有并沒有創(chuàng)建file結(jié)構(gòu)體,而且inode和驅(qū)動也并沒有產(chǎn)生聯(lián)系。
如果獲得次設備號
對于同種類型設備,比如多個串口、網(wǎng)口等,這些驅(qū)動比較類似,僅僅是一些寄存器基地址不一樣,所以我們沒有必須要為每一個設備單獨寫一個驅(qū)動,這些設備的驅(qū)動完全可以共用同一個驅(qū)動,我們只需要在驅(qū)動中區(qū)分出設備的次設備號,然后根據(jù)次設備號的訪問不同的內(nèi)存地址空間即可。
如果應用程序執(zhí)行以下代碼
fd0 = open("/dev/com0",O_RDWR);
fd1 = open("/dev/com1",O_RDWR);

當應用程序執(zhí)行open函數(shù),該函數(shù)會調(diào)用到內(nèi)核的sys_open(),該函數(shù)會根據(jù)該設備節(jié)點inode保存的信息,i_flags:文件類型, i_rdev:設備號,初始化結(jié)構(gòu)體inode其他信息,比如inode->i_cdev,此時已經(jīng)指向我們注冊的cdev結(jié)構(gòu)體,。通過設備號,可以很容易找到該設備在設備號全局管理數(shù)組chedevs[]的下標,進而找到我們注冊的驅(qū)動cdev以及file_operations。
同時內(nèi)核會在VFS層為創(chuàng)建結(jié)構(gòu)體file,該函數(shù)調(diào)用成功之后,應用層會返回整型值用來和該file對應,就是上圖的文件描述符fd0、fd1。
其中:
file->f_dentry->d_inode->i_rdev 保存對應的設備節(jié)點的設備號,
file-> f_op保存我們注冊的file_operations 字符設備接口函數(shù)集合。
由此可得在read和write等其他接口函數(shù)中,我們可以通過file來得到次設備號。
【注意】同一個文件如果打開了兩次,那么第二次linux內(nèi)核仍然會重新分配1個新的file結(jié)構(gòu)體。
實現(xiàn)代碼如下:

這樣我們就可以根據(jù)次設備號來操作不同的硬件。
如何獲得注冊的設備結(jié)構(gòu)體私有地址?
在大多情況下,我們會創(chuàng)建一個自定義的設備信息維護結(jié)構(gòu)體,同時創(chuàng)建一個指針數(shù)組用來管理不同的設備。

然后通過成員cdev注冊字符設備,

想一個問題:如果我們?yōu)槊恳粋€同類型設備分配獨立的設備結(jié)構(gòu)體,分別注冊對應的cdev,假如我打開/dev/com0 進行操作的時候,我怎么知道com0對應我們自己定義的設備管理結(jié)構(gòu)體變量的地址呢?
有問題是好的,我們帶著問題出發(fā),看看大牛們是怎么做的。

該函數(shù)功能:
字符設備架構(gòu)調(diào)用我們注冊的接口函數(shù)open會傳遞參數(shù)inode和file,inode->i_cdev指向了我們注冊的pmydev[i]->cdev,在open中通過inode->cdev來識別具體的設備,通過container_of來找到對應的pmycdev結(jié)構(gòu)體變量,并將其私有數(shù)據(jù)隱藏到file結(jié)構(gòu)的private_data中,進而識別同一個驅(qū)動操作一類設備。
而read,write接口函數(shù)可以直接通過file的 private_data獲取對應的pmycdev結(jié)構(gòu)體變量。

【補充1】
再來看下contianer_of 接口功能參數(shù)如下:

該宏是如何實現(xiàn)的,留給讀者自己思考。
【補充2】
我們也可以在回調(diào)cdev.fops->open()階段重新填充file結(jié)構(gòu)的fop,進而實現(xiàn)同一個驅(qū)動操作不同的設備,這種思想就是內(nèi)核驅(qū)動中常用的分層!
執(zhí)行結(jié)果如下:

由結(jié)果可知,應用程序正確讀取了minor的值。

從內(nèi)核log來看,MINOR(file->f_dentry->d_inode->i_rdev)可以成功讀取此設備號。而read接口函數(shù)也成功通過file->private_data得到了設備結(jié)構(gòu)體變量(初始化的時候為不同設備的test成員附了不同的值)。
驅(qū)動程序:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
static int major = 250;
static int minor = 0;
static dev_t devno;
#define MAX_COM_NUM 2
struct mydev{
struct cdev cdev;
char *reg;
int test;
};
struct mydev *pmydev[MAX_COM_NUM];
ssize_t dev_fifo_read (struct file *file, char __user *buf, size_t size, loff_t *pos)
{
int minor = MINOR(file->f_dentry->d_inode->i_rdev);
struct mydev *cd;
printk("read() MINOR(file->f_dentry->d_inode->i_rdev)=%d\n",minor);
cd = (struct mydev *)file->private_data;
printk("read() file->private_data cd->test=%d\n",cd->test);
if(copy_to_user(buf, &minor, size)){
return -EFAULT;
}
return size;
}
int dev_fifo_close (struct inode *inode, struct file *file)
{
printk("dev_fifo_close()\n");
return 0;
}
//打開設備
static int dev_fifo_open (struct inode *inode, struct file *file)
{
struct mydev *cd;
cd = container_of(inode->i_cdev, struct mydev, cdev);
file->private_data = cd;
return 0;
}
static struct file_operations dev_fifo_ops =
{
.open = dev_fifo_open,
.read = dev_fifo_read,
.release = dev_fifo_close,
};
static int dev_fifo_init(void)
{
int result;
int error;
int i = 0;
printk("dev_fifo_init \n");
devno = MKDEV(major,minor);
result = register_chrdev_region(devno, MAX_COM_NUM, "test");
if(result<0)
{
printk("register_chrdev_region fail \n");
goto ERR1;
}
for(i=0;i<MAX_COM_NUM;i++)
{
pmydev[i] =kmalloc(sizeof(struct mydev), GFP_KERNEL);
}
for(i=0;i<MAX_COM_NUM;i++)
{
pmydev[i]->test = i;
cdev_init(&pmydev[i]->cdev,&dev_fifo_ops);
devno = MKDEV(major,i);
error = cdev_add(&pmydev[i]->cdev,devno,1);
if(error < 0)
{
printk("cdev_add fail \n");
goto ERR2;
}
}
return 0;
ERR2:
devno = MKDEV(major,0);
unregister_chrdev_region(devno,MAX_COM_NUM);
for(i=0;i<MAX_COM_NUM;i++)
{
kfree(pmydev[i]);
}
return error;
ERR1:
return result;
}
static void dev_fifo_exit(void)
{
int i;
printk("dev_fifo_exit \n");
for(i=0;i<MAX_COM_NUM;i++)
{
cdev_del(&pmydev[i]->cdev);
}
for(i=0;i<MAX_COM_NUM;i++)
{
kfree(pmydev[i]);
}
devno = MKDEV(major,0);
unregister_chrdev_region(devno,MAX_COM_NUM);
return;
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng");
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
測試程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
int fd0,fd1;
int minor;
fd0 = open("/dev/com0",O_RDWR);
if(fd0<0)
{
perror("open fail \n");
return;
}
printf("open /dev/com0 OK\n");
read(fd0,&minor,sizeof(minor));
printf("minor of /dev/com0 =%d\n",minor);
close(fd0);
fd1 = open("/dev/com1",O_RDWR);
if(fd1<0)
{
perror("open fail \n");
return;
}
printf("open /dev/com1 OK\n");
read(fd1,&minor,sizeof(minor));
printf("minor of /dev/com1 =%d\n",minor);
close(fd1);
end
一口Linux
關(guān)注,回復【1024】海量Linux資料贈送
精彩文章合集
文章推薦