iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
自我挑戰組

Linux Kernel 網路巡禮系列 第 11

番外篇 (3) mknod 與 device driver

  • 分享至 

  • xImage
  •  

昨天,我們介紹了 character device 驅動的撰寫與使用方式。今天,我們要深入探討,當我們使用 mknodopen 指令時,設備檔案如何與驅動連結。

mknod 指令的流程

首先,我們來追蹤 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

這裡有幾個重要的參數:

  1. 建立的目標檔案路徑/dev/hello
  2. 目標檔案類型S_IFCHR 表示為 character device
  3. 設備編號:主設備號 240,次設備號 0

接下來,我們來看看 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 與 dentry 的建立

回憶一下 VFS(Virtual File System)的檔案系統概念,建立 dentry 的流程如下:

  1. 建立一個空的 dentry。
  2. 呼叫父目錄的 inode_operations.lookup,來填充 dentry 的資料並分配 inode。
  3. 將 dentry 綁定到 VFS 檔案系統樹上。

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 的運作原理

這邊簡單說明一下,為什麼會需要 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 的檔案系統驅動

這邊我們追蹤 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 的執行邏輯,這將:

  1. 建立一個 /dev/hello 的空 dentry
  2. 呼叫 /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 函數接收一個 dentryinode,會將 dentryd_inode 設置為輸入的 inode,然後把 dentry 掛到 VFS 檔案樹上面。這邊很重要的點是 inode 參數是 NULL,所以這個 dentry 沒有 inode

https://ithelp.ithome.com.tw/upload/images/20240925/20152703RAE8K8OxX7.png

所以當filename_create函數執行完成後,VFS檔案系統樹是長這樣的,/dev/hello的dentry已經被建立,但是沒有指向到任何inode。

vfs_mknod 與驅動的連結

dentry 建立完成後,根據檔案類型不同,會呼叫不同的函數。對 S_IFCHR 也就是 character device file 會呼叫 vfs_mknod

一樣 dir/devinodedentry/dev/hellodentry

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 inodemknod 函數,根據前面對 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 中。

open 設備檔案

在介紹 VFS 時,我們提到,當我們使用 open system call打開 /dev/hello時,file 提供 file_operations 會從 inode 繼承過來,同時呼叫 file_operations.open 函數。

根據 /dev/hello 的 inode 定義,會呼叫 chrdev_openchrdev_open 會透過 kobj_lookup 函數,從cdev_map 找到設備號對應的驅動資訊 cdev,同時 file 的 f_op (file_operations) 會被覆蓋為驅動定義的 file_operations (inode->i_cdev->ops)。

總結

透過這一系列過程,打開的設備檔案的 file_operations 就被設置為驅動提供的的 file_operations 了!

https://ithelp.ithome.com.tw/upload/images/20240925/20152703rSR55A9Hye.png

最後讓我們一張圖,回顧這一切。


上一篇
番外篇 (2) Character Device
下一篇
初探 PROC 檔案系統
系列文
Linux Kernel 網路巡禮30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言