本文目標
- 什么是文件描述符?
- 進程打開文件相關信息管理
- Linux設備文件三大結構:inode,file,file_operations
- mknod 做了什么事?
- 進程打開設備文件
- 驅動如何支持同類型設備?
- 如何獲得注冊的設備結構體私有地址?
什么是文件描述符?
Linux 中一切都可以看作文件,包括普通文件、鏈接文件、Socket 以及設備驅動等,對其進行相關操作時,都可能會創建對應的文件描述符。文件描述符(file descriptor)是內核為了高效管理已被打開的文件所創建的索引,用於指代被打開的文件,對文件所有 I/O 操作相關的系統調用都需要通過文件描述符。
Linux啟動后,會默認打開3個文件描述符,分別是:
0:標准輸入 standard input
1:正確輸出 standard output
2:錯誤輸出 error output
這就是為什么我們在程序運行時可以直接打印信息和從命令終端獲取信息的原因。
並且以后打開文件后。新增文件綁定描述符 可以依次增加(從3開始累加)。每一條shell命令執行,都會繼承父進程的文件描述符。因此,所有運行的shell命令,都會有默認3個文件描述符。
- 進程級別的文件描述符表files_struct:內核為每個進程維護一個文件描述符表,該表記錄了文件描述符的相關信息,包括文件描述符、指向打開文件表中記錄的指針。
- 系統級別的打開文件表file:內核對所有打開文件維護的一個進程共享的打開文件描述表,表中存儲了處於打開狀態文件的相關信息,包括文件類型、訪問權限、文件操作函數(file_operations)等。
- 系統級別的 i-node 表:i-node 結構體記錄了文件相關的信息,包括文件長度,文件所在設備,文件物理位置,創建、修改和更新時間等,"ls -i" 命令可以查看文件 i-node 節點。
進程在Linux內核中是由結構體task_struct維護,進程打開的所有文件描述符都在進程維護的結構體task_struct的files變量中維護:
//include\linux\sched.h
struct task_struct {
……
/* open file information */
struct files_struct *files;
……
}
該結構體定義如下:
/*
* Open file table structure
*/
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
該進程所有打開的文件對應的file指針均由fd_array維護,文件描述符和數組下標一一對應。
進程打開文件相關信息管理
文件描述符是一種系統資源,可以通過以下命令來查看文件描述符的上限。
查看所有進程允許打開的最大 fd 數量
查看所有進程已經打開的 fd 數量以及允許的最大數量
查看單個進程允許打開的最大 fd 數量.
查看某個文件被哪些進程打開?
可以借助lsof命令
編寫調試代碼如下:
該代碼功能是打開文件test,然后休眠100秒,我們需要在這100秒內執行lsof操作。
&是程序放在后台運行,為了釋放終端,方便輸入下一個命令;
7284:程序進程ID;
lsof功能:查看某個文件被進程打開的詳細信息。
查看某個進程打開了哪些文件?
接着上述的例子,ls -l /proc/{PID}/fd 可以查看某個進程打開了哪些文件。
可以看到該進程打開了除了test之外,還打開了前面所述的3個默認文件,結構體對應關系如下:
實際開發中,可能會遇到 fd 資源超過上限導致的 "Too many open files" 之類的問題,一般都是因為沒有及時釋放掉 fd,若循環執行超過單個進程允許打開的最大 fd 數量,程序就會出現異常。
Linux設備文件三大結構:inode,file,file_operations
驅動程序就是向下控制硬件,向上提供接口,驅動向上提供的接口最終對應到應用層有三種方式:設備文件,/proc,/sys,其中最常用的就是使用設備文件,而Linux設備中用的最多的就是字符設備,本文就以字符設備為例來分析創建並打開一個字符設備的文件內部機制。
struct inode
Linux中一切皆文件,當我們在Linux中創建一個文件時,就會在相應的文件系統創建一個inode與之對應。
對於不同的文件類型,inode被填充的成員內容也會有所不同,以創建字符設備為例,我們知道,add_chrdev_region其實是把一個驅動對象和一個(一組)設備號聯系到一起。而創建設備文件,其實是把設備文件和設備號聯系到一起。至此,這三者就被綁定在一起了。這樣,內核就有能力創建一個struct inode實例了,下面是Linux 3.14內核中的inode。這個inode是VFS的inode,是最具體文件系統的inode的進一步封裝,也是驅動開發中關心的inode,針對具體的文件系統,還有struct ext2_inode_info 等結構。
//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; //用戶ID
kgid_t i_gid; //用戶組ID
unsigned int i_flags; //文件系統標志
#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; //最后access時間
struct timespec i_mtime; //最后modify時間
struct timespec i_ctime; //最后change時間
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;//引用計數,當引用計數變為0時,會釋放inode實例
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
//創建設備文件的時候i_fops填充的是def_chr_fops,
//blk_blk_fops,def_fifo_fops,bad_sock_fops之一,
//參見創建過程中調用的init_special_inode()
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
//特殊文件類型的union,pipe,cdev,blk.link etc,
//i_cdev表示這個inode屬於一個字符設備文件,
//本文中創建設備文件的時候會把與之相關的設備號的驅動對象cdev拿來填充
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
//inode的私有數據
void *i_private; /* fs or device private pointer */
};
重要的成員已經添加注釋。
struct file
Linux內核會為每一個進程維護一個文件描述符表,這個表其實就是struct file[]的索引。open()的過程其實就是根據傳入的路徑填充好一個file結構並將其賦值到數組中並返回其索引。下面是file的主要內容
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 */
關鍵成員定義如下:
-->f_path里存儲的是open傳入的路徑,VFS就是根據這個路徑逐層找到相應的inode
-->f_inode里存儲的是找到的inode
-->f_op里存儲的就是驅動提供的file_operations對象,這個對象應該在第一次open()的時候被填充,具體地,應用層的open通過層層搜索會調用inode.i_fops->open(),即我們注冊的open接口函數chrdev_open()
-->f_count的作用是記錄對文件對象的引用計數,也即當前有多少個使用CLONE_FILES標志克隆的進程在使用該文件。典型的應用是在POSIX線程中。就像在內核中普通的引用計數模塊一樣,最后一個進程調用put_files_struct()來釋放文件描述符。
-->f_flags當打開文件時指定的標志,對應系統調用open的int flags,比如驅動程序為了支持非阻塞型操作需要檢查這個標志是否有O_NONBLOCK。
-->f_mode;對文件的讀寫模式,對應系統調用open的mod_t mode參數,比如O_RDWR。如果驅動程序需要這個值,可以直接讀取這個字段。
-->private_data表示file結構的私有數據
mknod 做了什么事?
本例假定我們創建兩個串口com0、com1,他們公用同一個主設備號250,次設備號分別為0、1,他們公用同一個字符設備驅動,那么我們的驅動要能夠根據應用進程打開的是設備com0還是com1來操作不同的串口。
首先創建兩個設備節點:
mknod /dev/com0 c 250 0
mknod /dev/com1 c 250 1
執行結果如下:
內核為了維護這兩個文件節點,內核需要創建結構體維護這兩個文件,具體如下圖所示:
當我們通過命令mknod創建一個字符設備文件,那么內核就會創建好一個inode會存在存儲器中,創建和該文件實體一一對應的inode。這個inode和其他的inode一樣,通常用來存儲關於這個文件的靜態信息(不變的信息),包括這個設備文件對應的設備號,文件的路徑以及對應的驅動對象等。
inode作為VFS四大對象之一,在驅動開發中很少需要自己進行填充,更多的是在open()方法中進行查看並根據需要填充我們的file結構。
創建字符設備 /dev/com0、 /dev/com1,只是增加了對應的inode節點,此時VFS層並沒有並沒有創建file結構體,而且inode和驅動也並沒有產生聯系。
進程打開設備文件發生了什么?
當進程試圖打開設備文件的時候,系統做了什么事?
如果應用程序執行以下代碼:
fd0 = open("/dev/com0",O_RDWR);
fd1 = open("/dev/com1",O_RDWR);
各個結構體之間關系入下圖所示:
當應用程序執行open函數,該函數會調用到內核的sys_open(),該函數會根據該設備節點inode保存的信息,i_flags:文件類型, i_rdev:設備號,初始化結構體inode其他信息,比如inode->i_cdev,此時已經指向我們注冊的cdev結構體。
通過設備號,可以很容易找到該設備在設備號全局管理數組chedevs[]的下標,進而找到我們注冊的驅動cdev以及file_operations。
同時內核會在VFS層為創建結構體file,該函數調用成功之后,應用層會返回整型值用來和該file對應,就是上圖的文件描述符fd0、fd1。
其中:
file->f_dentry->d_inode->i_rdev 保存對應的設備節點的設備號,
file-> f_op保存我們注冊的file_operations 字符設備接口函數集合。
由此可得在read和write等其他接口函數中,我們可以通過file來得到次設備號。
【注意】同一個文件如果打開了兩次,那么第二次linux內核仍然會重新分配1個新的file結構體和文件描述符。
驅動如何支持多種同類型設備
對於同種類型設備,比如多個串口、網口等,這些驅動比較類似,僅僅是一些寄存器基地址不一樣,所以我們沒有必須要為每一個設備單獨寫一個驅動,這些設備的驅動完全可以共用同一個驅動,我們只需要在驅動中區分出設備的次設備號,然后根據次設備號的訪問不同的內存地址空間即可。
根據上一屆內容,驅動的read、write可以通過以下方式獲得設備號:
file->f_dentry->d_inode->i_rdev
這樣我們就可以通過宏MINOR來提取此設備號。
實現代碼如下:
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;
}
當驅動可以提取次設備號之后,我們就可以實現一份驅動支持多個同種類型的設備。
如何獲得注冊的設備結構體私有地址?
在大多情況下,我們會創建一個自定義的設備信息維護結構體,同時創建一個指針數組用來管理不同的設備。
#define MAX_COM_NUM 2
struct mydev{
struct cdev cdev;
char *reg;
int test;
};
struct mydev *pmydev[MAX_COM_NUM];
然后通過成員cdev注冊字符設備,
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;
}
}
想一個問題:如果我們為每一個同類型設備分配獨立的設備結構體,分別注冊對應的cdev,假如我打開/dev/com0 進行操作的時候,我怎么知道com0對應我們自己定義的設備管理結構體變量的地址呢?
有問題是好的,我們帶着問題出發,看看大牛們是怎么做的。
//打開設備
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;
}
該函數功能:
字符設備架構調用我們注冊的接口函數open會傳遞參數inode和file,inode->i_cdev指向了我們注冊的pmydev[i]->cdev,在open中通過inode->cdev來識別具體的設備,通過container_of來找到對應的pmycdev結構體變量,並將其私有數據隱藏到file結構的private_data中,進而識別同一個驅動操作一類設備。
而read,write接口函數可以直接通過file的 private_data獲取對應的pmycdev結構體變量。
cd = (struct mydev *)file->private_data;
【補充1】
再來看下contianer_of 接口功能參數如下:
該宏是如何實現的,留給讀者自己思考。
【補充2】
我們也可以在回調cdev.fops->open()階段重新填充file結構的fop,進而實現同一個驅動操作不同的設備,這種思想就是內核驅動中常用的分層!
執行結果如下:
由結果可知,應用程序正確讀取了minor的值。
從內核log來看,MINOR(file->f_dentry->d_inode->i_rdev)可以成功讀取此設備號。而read接口函數也成功通過file->private_data得到了設備結構體變量(初始化的時候為不同設備的test成員附了不同的值)。
驅動程序:
#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);
}
最后 附送一幅我總結的字符設備的框架圖:
獲取更多關於Linux的資料,請關注公眾號「一口Linux
」