昨天,我們介紹了 character device 驅動的撰寫與使用方式。今天,我們要深入探討,當我們使用 mknod
與 open
指令時,設備檔案如何與驅動連結。
首先,我們來追蹤 mknod
指令。當下達 mknod
指令時,實際上會呼叫到 mknod
system call。
> strace -f -e mknod mknod /dev/hello c 240 0
mknod("/dev/hello", S_IFCHR|0666, makedev(240, 0)) = 0
這裡有幾個重要的參數:
/dev/hello
S_IFCHR
表示為 character device接下來,我們來看看 mknod
system call 的實作。
// fs/namei.c
SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
{
return do_mknodat(AT_FDCWD, getname(filename), mode, dev);
}
static int do_mknodat(int dfd, struct filename *name, umode_t mode,
unsigned int dev)
{
...
struct dentry *dentry = filename_create(dfd, name, &path, lookup_flags); // 建立 dentry
...
switch (mode & S_IFMT) {
case 0: case S_IFREG:
...
break;
case S_IFCHR: case S_IFBLK: // 處理 character/block device
error = vfs_mknod(idmap, path.dentry->d_inode,
dentry, mode, new_decode_dev(dev)); // 執行 mknod
break;
case S_IFIFO: case S_IFSOCK:
...
break;
}
...
}
從這裡可以看到,mknod
做了兩件事。首先,使用 filename_create
建立 /dev/hello
的 dentry,接著使用 vfs_mknod
處理 character device 檔案的建立。
mknod 的目的是在檔案系統中建立一個節點,這個節點可以是一般檔案、設備檔案,甚至可能是 named pipe 這樣的特殊檔案,所以這邊透過一個 switch 判定檔案的類型,執行不同的建立流程。
回憶一下 VFS(Virtual File System)的檔案系統概念,建立 dentry 的流程如下:
inode_operations.lookup
,來填充 dentry 的資料並分配 inode。inode_operations
的來源是父目錄的檔案系統驅動,我們今天建立的檔案是 /dev/hello
,所以需要了解 /dev
的檔案系統類型。
> mount | grep dev
udev on /dev type devtmpfs (rw,nosuid,relatime,size=3989348k,nr_inodes=129970,mode=755)
...
可以看到 /dev
使用的是 devtmpfs
,這是一個專門用來放置設備檔案的檔案系統。
這邊簡單說明一下,為什麼會需要 devtmpfs
這個特殊的檔案系統。
當系統開機時,有一個重要的執行路徑:start_kernel -> arch_call_rest_init -> rest_init -> user_mode_thread
,這條路徑會建立 PID 1 的 init process,並執行 kernel_init
函數。kernel_init
會進一步執行 kernel_init_freeable -> do_basic_setup -> driver_init -> devtmpfs_init -> kthread_run
,最終建立 kdevtmpfs process,這個 process 會執行 devtmpfsd
函數。
devtmpfs
提供了 device_add
這個 utility function,允許 driver 在 /dev
下建立設備檔案。device_add
會提交一個請求給 devtmpfsd
,而 devtmpfsd
會在迴圈中接收請求並處理,最終完成設備檔案的建立。
這邊我們追蹤 devtmpfs 這個檔案系統驅動的定義。
// drivers/base/devtmpfs.c
// devtmpfs 檔案系統驅動定義
static struct file_system_type internal_fs_type = {
.name = "devtmpfs",
#ifdef CONFIG_TMPFS
.init_fs_context = shmem_init_fs_context, // 檔案系統上下文初始化函數
#else
.init_fs_context = ramfs_init_fs_context,
#endif
.kill_sb = kill_litter_super,
};
// mm/shmem.c
int shmem_init_fs_context(struct fs_context *fc)
{
...
fc->ops = &shmem_fs_context_ops;
...
}
static const struct fs_context_operations shmem_fs_context_ops = {
.free = shmem_free_fc,
.get_tree = shmem_get_tree, // 使用 mount 掛載 devtmpfs 時,會呼叫到該函數
#ifdef CONFIG_TMPFS
.parse_monolithic = shmem_parse_options,
.parse_param = shmem_parse_one,
.reconfigure = shmem_reconfigure,
#endif
};
static int shmem_get_tree(struct fs_context *fc)
{
return get_tree_nodev(fc, shmem_fill_super);
}
// 填充該檔案系統的 super_block 資料結構
static int shmem_fill_super(struct super_block *sb, struct fs_context *fc)
{
...
sb->s_op = &shmem_ops; // 設置 superblock_operations
...
}
當 devtmpfs
檔案系統被掛載時,kernel 會呼叫 shmem_get_tree
函數,建立一個空的 superblock 結構,然後透過 devtmpfs
提供的 shmem_fill_super
函數對 superblock 結構初始化,包含將 superblock 的superblock_operations設置為 shmem_ops
。
static const struct super_operations shmem_ops = {
.alloc_inode = shmem_alloc_inode,
...
}
static struct inode *shmem_get_inode(struct mnt_idmap *idmap,
struct super_block *sb, struct inode *dir,
umode_t mode, dev_t dev, unsigned long flags)
{
...
inode = __shmem_get_inode(idmap, sb, dir, mode, dev, flags);
...
}
static struct inode *__shmem_get_inode(struct mnt_idmap *idmap,
struct super_block *sb,
struct inode *dir, umode_t mode,
dev_t dev, unsigned long flags)
{
...
switch (mode & S_IFMT) {
default:
...
break;
case S_IFREG:
...
break;
case S_IFDIR: // 針對目錄
...
inode->i_op = &shmem_dir_inode_operations; // devtmpfs 下,目錄的 inode 使用的 inode_operations
...
break;
case S_IFLNK:
...
break;
}
...
}
前面,我們說到 do_mknod
會呼叫 filename_create
來建立 /dev/hello
的 dentry,根據 VFS 的執行邏輯,這將:
/dev/hello
的空 dentry/dev
inode 的 lookup 函數,對/dev/hello
的 dentry 的內容填充。/dev
是一個目錄,所以從對 devtmpfs
的追蹤來看,/dev
的 inode_operations (i_op) 會被設置為 shmem_dir_inode_operations。
static const struct inode_operations shmem_dir_inode_operations = {
...
.lookup = simple_lookup,
.mknod = shmem_mknod,
...
};
所以實際上呼叫的 lookup 函數是 simple_lookup
// fs/libfs.c
struct dentry *simple_lookup(struct inode *dir, struct dentry *dentry, unsigned int flags)
{
...
d_add(dentry, NULL); // 這邊的 dentry 是 /dev/hello 的 dentry
return NULL;
}
// linux/dcache.h
extern void d_add(struct dentry *, struct inode *);
這邊輸入的參數是 /dev
的 inode 跟 /dev/hello
的 dentry。
simple_lookup
會呼叫 VFS 提供的 d_add
函數,d_add
函數接收一個 dentry
和 inode
,會將 dentry
的d_inode
設置為輸入的 inode
,然後把 dentry
掛到 VFS 檔案樹上面。這邊很重要的點是 inode
參數是 NULL
,所以這個 dentry
沒有 inode
。
所以當filename_create
函數執行完成後,VFS檔案系統樹是長這樣的,/dev/hello的dentry已經被建立,但是沒有指向到任何inode。
dentry 建立完成後,根據檔案類型不同,會呼叫不同的函數。對 S_IFCHR
也就是 character device file 會呼叫 vfs_mknod
。
一樣 dir
是 /dev
的 inode
,dentry
是 /dev/hello
的 dentry
。
int vfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t dev)
{
...
error = dir->i_op->mknod(idmap, dir, dentry, mode, dev);
...
}
熟悉了VFS系統邏輯的各位可能會猜到,他會呼叫 /dev
inode
的 mknod
函數,根據前面對 devtmpfs 的追蹤, 實際上會呼叫到 shmem_mknod
函數。
// mm/shmem.c
shmem_mknod(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t dev)
{
struct inode *inode = shmem_get_inode(idmap, dir->i_sb, dir, mode, dev, VM_NORESERVE);
...
d_instantiate(dentry, inode);
...
}
// fs/dcache.c
void d_instantiate(struct dentry *entry, struct inode * inode)
{
...
__d_set_inode_and_type(dentry, inode, add_flags);
...
}
static inline void __d_set_inode_and_type(struct dentry *dentry,
struct inode *inode,
unsigned type_flags)
{
...
dentry->d_inode = inode;
...
}
根據 shmem_mknod
的定義,他要做的事情,就是使用 shmem_get_inode
去生成 /dev/hello
的 inode。
// mm/shmem.c
static struct inode *shmem_get_inode(struct mnt_idmap *idmap,
struct super_block *sb, struct inode *dir,
umode_t mode, dev_t dev, unsigned long flags)
{
struct inode *inode;
ino_t ino;
err = shmem_reserve_inode(sb, &ino);
inode = new_inode(sb);
..
inode->i_ino = ino;
switch (mode & S_IFMT) {
default:
inode->i_op = &shmem_special_inode_operations;
init_special_inode(inode, mode, dev);
break;
case S_IFREG:
...
case S_IFDIR:
...
case S_IFLNK:
...
}
...
return inode;
}
首先會呼叫 shmem_reserve_inode
申請 inode
,接著根據不同的檔案類型處理,/dev/hello
的檔案類型是 character device file (S_IFCHR),所以會執行 default 行為,i_op
被設置為 shmem_special_inode_operations
,重點是會呼叫 init_special_inode
函數。
// fs/inode.c
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
}
...
}
// fs/char_dev.c
const struct file_operations def_chr_fops = {
.open = chrdev_open, // /dev/hello 的 file open 函數
.llseek = noop_llseek,
};
static int chrdev_open(struct inode *inode, struct file *filp)
{
...
struct kobject *kob = kobj_lookup(cdev_map, inode->i_rdev, &idx);
inode->i_cdev = container_of(kobj, struct cdev, kobj);
replace_fops(filp, inode->i_cdev->ops);
ret = filp->f_op->open(inode, filp);
...
}
在 init_special_inode
可以看到 inode->i_fop
被設置為 def_chr_fops
,同時設備號會被保存在 inode
中。
在介紹 VFS 時,我們提到,當我們使用 open system call打開 /dev/hello
時,file 提供 file_operations
會從 inode
繼承過來,同時呼叫 file_operations.open
函數。
根據 /dev/hello
的 inode 定義,會呼叫 chrdev_open
,chrdev_open
會透過 kobj_lookup
函數,從cdev_map
找到設備號對應的驅動資訊 cdev
,同時 file 的 f_op
(file_operations) 會被覆蓋為驅動定義的 file_operations
(inode->i_cdev->ops)。
透過這一系列過程,打開的設備檔案的 file_operations 就被設置為驅動提供的的 file_operations
了!
最後讓我們一張圖,回顧這一切。