[TOC]

drivers/base/devtmpfs.c

handle_create 创建一个设备节点(device node)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// 在内核空间创建一个目录。
// name: 要创建的目录的路径字符串。
// mode: 目录的访问权限模式 (例如 0755)。
static int dev_mkdir(const char *name, umode_t mode)
{
// dentry: 目录项(directory entry)的缩写,是VFS中用于将文件名链接到inode的对象。
struct dentry *dentry;
// path: VFS中用于表示一个完整路径的对象,包含挂载点和dentry。
struct path path;

// 调用VFS核心函数,尝试创建一个路径。
// AT_FDCWD: 表示路径`name`是相对于当前工作目录。在内核线程中,这通常是根目录 "/"。
// &path: 用于返回查找到的路径信息。
// LOOKUP_DIRECTORY: 标志,指示VFS我们期望创建一个目录。
// 此函数会解析路径,找到父目录,并为新目录准备好dentry。
dentry = kern_path_create(AT_FDCWD, name, &path, LOOKUP_DIRECTORY);
// IS_ERR是一个宏,用于检查返回的指针是否编码了一个错误。
if (IS_ERR(dentry))
// PTR_ERR宏从错误指针中提取出标准的负数错误码(如 -ENOENT)。
return PTR_ERR(dentry);

// 调用VFS层核心的mkdir函数,在指定的父目录(path.dentry)下创建新的目录(dentry)。
// &nop_mnt_idmap: 一个默认的ID映射,在没有复杂用户命名空间的嵌入式系统上,这表示直接使用传入的UID/GID。
// d_inode(path.dentry): 获取父目录的inode。
dentry = vfs_mkdir(&nop_mnt_idmap, d_inode(path.dentry), dentry, mode);
if (!IS_ERR(dentry))
/*
* 将此inode的i_private字段指向一个静态的内核变量(&thread)。
* 这是一个标记机制,用于标识这个inode是由内核的这个特定机制创建的,
* 以便将来可以识别和管理它。
*/
d_inode(dentry)->i_private = &thread;

// 释放由kern_path_create获取的路径引用,完成VFS操作。
done_path_create(&path, dentry);
// 如果dentry是指向有效的dentry,则返回0(成功);如果是错误指针,则返回错误码。
return PTR_ERR_OR_ZERO(dentry);
}
// 确保一个完整路径中的所有目录都存在,如果不存在则创建它们。
// nodepath: 目标节点的完整路径,例如 "/dev/bus/usb"。
static int create_path(const char *nodepath)
{
char *path;
char *s;
int err = 0;

// 使用kmalloc复制一份路径字符串,因为后续需要对其进行修改。
// GFP_KERNEL表示在内存分配时,如果需要可以睡眠等待。
path = kstrdup(nodepath, GFP_KERNEL);
if (!path)
// 如果内存分配失败,返回-ENOMEM。
return -ENOMEM;

// s 指向复制的路径字符串的开头。
s = path;
// 一个无限循环,用于逐级解析和创建目录。
for (;;) {
// 寻找下一个'/'字符。
s = strchr(s, '/');
if (!s)
// 如果找不到更多的'/',说明已经处理完所有子目录,退出循环。
break;
// 暂时将'/'替换为字符串结束符'\0'。
// 例如,对于"/dev/bus/usb",第一次循环时,path会变成"dev"。
s[0] = '\0';
// 调用dev_mkdir尝试创建当前级别的目录(例如"dev")。
err = dev_mkdir(path, 0755); // 使用0755权限。
// 如果创建失败,并且错误不是“文件已存在”(-EEXIST),
// 那么这是一个真正的错误,终止操作。
if (err && err != -EEXIST)
break;
// 将'\0'恢复为'/',以便进行下一轮的查找。
s[0] = '/';
// 指针s移动到'/'之后,准备处理下一个路径组件。
s++;
}
// 释放之前分配的路径字符串副本。
kfree(path);
// 返回最终的错误码,如果一切顺利,err将是0或-EEXIST,最终被视为成功。
return err;
}
// 定义处理设备节点创建的主函数。
// `nodename`: 要创建的设备节点的完整路径,例如 "/dev/mydevice"。
// `mode`: 节点的权限和类型(例如 S_IFCHR 表示字符设备)。
// `uid`, `gid`: 节点的用户ID和组ID。
// `dev`: 指向该设备节点的内核`device`结构体。
static int handle_create(const char *nodename, umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
int err;

// 第一次尝试创建路径。这里的0标志表示查找父目录并为最终组件创建dentry。
dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
// 如果返回的错误是-ENOENT (No such file or directory),
// 这通常意味着父目录不存在。
if (dentry == ERR_PTR(-ENOENT)) {
// 调用 create_path 来递归创建所有不存在的父目录。
create_path(nodename);
// 再次尝试创建路径,此时父目录应该已经存在了。
dentry = kern_path_create(AT_FDCWD, nodename, &path, 0);
}
// 如果在创建父目录后仍然出错,则返回错误。
if (IS_ERR(dentry))
return PTR_ERR(dentry);

// 调用核心的VFS mknod函数,以创建设备节点。
// `d_inode(path.dentry)`: 父目录的inode。
// `dentry`: 新设备节点的dentry。
// `mode`: 节点类型(字符/块设备)和权限。
// `dev->devt`: 最关键的参数,包含了设备的主/次设备号(major/minor)。
err = vfs_mknod(&nop_mnt_idmap, d_inode(path.dentry), dentry, mode,
dev->devt);
// 如果节点创建成功...
if (!err) {
// 定义一个iattr结构体,用于之后修改inode的属性。
struct iattr newattrs;

newattrs.ia_mode = mode; // 设置权限
newattrs.ia_uid = uid; // 设置用户ID
newattrs.ia_gid = gid; // 设置组ID
// `ia_valid` 是一个掩码,指明了哪些属性需要被更改。
newattrs.ia_valid = ATTR_MODE|ATTR_UID|ATTR_GID;

// 在修改inode之前获取其锁,以防止竞态条件。
// 在单核系统上,这主要用于防止中断上下文的干扰。
inode_lock(d_inode(dentry));
// 调用notify_change,它会应用新的属性并通知其他子系统(如inotify)。
notify_change(&nop_mnt_idmap, dentry, &newattrs, NULL);
// 释放inode锁。
inode_unlock(d_inode(dentry));

// 再次“标记”这个inode是由内核创建的。
d_inode(dentry)->i_private = &thread;
}
// 释放路径查找资源。
done_path_create(&path, dentry);
// 返回操作的最终错误码。
return err;
}

handle_remove 删除一个设备节点(device node)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// 定义一个用于移除目录的函数。
static int dev_rmdir(const char *name)
{
// `parent`: 用于存储找到的目录的父目录路径信息。
struct path parent;
// `dentry`: 指向要删除的目录的目录项(directory entry)。
struct dentry *dentry;
int err;

// 在VFS中查找名为`name`的路径,并锁定其父目录的inode以防止竞争。
// 返回`name`对应的dentry。
dentry = kern_path_locked(name, &parent);
// 检查查找操作是否出错。
if (IS_ERR(dentry))
return PTR_ERR(dentry);

// 这是一个核心安全检查:检查此目录的inode私有指针是否指向`thread`。
// 这确保我们只删除由本模块的`dev_mkdir`函数创建的目录。
if (d_inode(dentry)->i_private == &thread)
// 如果检查通过,则调用VFS核心函数来移除目录。
err = vfs_rmdir(&nop_mnt_idmap, d_inode(parent.dentry),
dentry);
else
// 如果该目录不是由我们创建的,则返回“操作不允许”错误,防止误删。
err = -EPERM;

// 递减`dentry`的引用计数,如果降为0,则释放该dentry。
dput(dentry);
// 解锁父目录的inode。
inode_unlock(d_inode(parent.dentry));
// 递减父目录路径的引用计数。
path_put(&parent);
// 返回操作结果。
return err;
}
// 定义一个用于删除路径中父目录的函数。
static int delete_path(const char *nodepath)
{
char *path;
int err = 0;

// 为路径字符串创建一个可修改的副本。
path = kstrdup(nodepath, GFP_KERNEL);
if (!path)
return -ENOMEM;

// 循环尝试删除父目录。
for (;;) {
char *base;

// `strrchr`查找路径中最后一个斜杠'/'。这是从深到浅处理的关键。
base = strrchr(path, '/');
// 如果找不到斜杠,意味着已经到达路径顶部(或路径无效),循环终止。
if (!base)
break;
// 临时将斜杠替换为字符串结束符,从而截断路径。
// 例如,"/dev/bus/usb"会变成"/dev/bus"。
base[0] = '\0';
// 尝试删除被截断出来的路径。
err = dev_rmdir(path);
// 如果删除失败(例如目录非空,或权限不足),则停止向上删除。
if (err)
break;
}

// 释放字符串副本的内存。
kfree(path);
// 返回最后一次删除操作的结果。
return err;
}
// 判断一个inode是否是本模块为特定设备所创建的节点。
static int dev_mynode(struct device *dev, struct inode *inode)
{
/* 首先,检查inode的私有指针,判断它是否由我们创建。 */
if (inode->i_private != &thread)
return 0; // 不是我们创建的,返回false。

/* 其次,检查设备类型是否匹配。 */
if (is_blockdev(dev)) { // 如果内核设备对象是块设备...
if (!S_ISBLK(inode->i_mode)) // ...但inode不是块设备文件...
return 0; // ...则不匹配,返回false。
} else { // 如果内核设备对象是字符设备...
if (!S_ISCHR(inode->i_mode)) // ...但inode不是字符设备文件...
return 0; // ...则不匹配,返回false。
}
/* 再次,检查设备号是否完全匹配。 */
// `inode->i_rdev` 存储了设备文件的设备号(主/次)。
// `dev->devt` 是内核设备对象的设备号。
if (inode->i_rdev != dev->devt)
return 0; // 设备号不匹配,返回false。

/* 所有检查都通过,确认这是我们要找的节点。 */
return 1; // 返回true。
}
// 处理设备节点移除的主函数。
static int handle_remove(const char *nodename, struct device *dev)
{
struct path parent;
struct dentry *dentry;
struct inode *inode;
int deleted = 0; // 标记节点是否已成功删除。
int err = 0;

// 查找要移除的节点,并锁定其父目录。
dentry = kern_path_locked(nodename, &parent);
if (IS_ERR(dentry))
return PTR_ERR(dentry);

inode = d_inode(dentry);
// 调用验证函数,确认这确实是我们要移除的设备节点。
if (dev_mynode(dev, inode)) {
struct iattr newattrs;
/*
* 在取消链接此节点之前,重置可能的引用(如硬链接)的权限。
* 这是一项安全措施。
*/
newattrs.ia_uid = GLOBAL_ROOT_UID; // 设置所有者为root
newattrs.ia_gid = GLOBAL_ROOT_GID; // 设置所属组为root
newattrs.ia_mode = inode->i_mode & ~0777; // 清除所有用户、组和其他人的权限位。
newattrs.ia_valid = ATTR_UID|ATTR_GID|ATTR_MODE; // 标记要修改的属性。

inode_lock(d_inode(dentry)); // 锁定inode。
// 应用属性变更。
notify_change(&nop_mnt_idmap, dentry, &newattrs, NULL);
inode_unlock(d_inode(dentry)); // 解锁inode。

// 调用VFS核心函数来取消链接(即删除)文件节点。
err = vfs_unlink(&nop_mnt_idmap, d_inode(parent.dentry),
dentry, NULL);
// 如果删除成功(err==0)或文件本就不存在(err==-ENOENT),则标记为已删除。
if (!err || err == -ENOENT)
deleted = 1;
}

dput(dentry); // 释放dentry引用。
inode_unlock(d_inode(parent.dentry)); // 释放父目录锁。
path_put(&parent); // 释放父路径引用。

// 如果节点被成功删除,并且其路径中包含'/'(意味着它在子目录中)...
if (deleted && strchr(nodename, '/'))
// ...则调用delete_path尝试清理空的父目录。
delete_path(nodename);
return err;
}

devtmpfs_work_loop devtmpfs 的事件处理主循环

  • devtmpfs_work_loop 是 devtmpfsd 内核线程的无限工作循环,也是 devtmpfs 机制的引擎。一旦 devtmpfs 初始化完成,线程就会进入这个函数并且永不返回(由 __noreturn 属性指明)。其核心原理是作为一个高效的、事件驱动的消费者:在没有设备事件时,它会深度睡眠,不消耗任何 CPU 资源;当内核的其他部分(如驱动核心)需要创建或删除设备节点时,它会被唤醒,批量处理所有待办请求,然后再次进入睡眠。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
static struct req {
struct req *next;
struct completion done;
int err;
const char *name;
umode_t mode; /* 0 => delete */
kuid_t uid;
kgid_t gid;
struct device *dev;
} *requests;

static int handle(const char *name, umode_t mode, kuid_t uid, kgid_t gid,
struct device *dev)
{
if (mode)
return handle_create(name, mode, uid, gid, dev);
else
return handle_remove(name, dev);
}

/*
* __noreturn: 这是一个函数属性,用于告知编译器此函数永远不会返回。
* 这有助于编译器进行某些优化,并抑制关于缺少返回值的警告。
* devtmpfs_work_loop: 函数名。它作为 devtmpfsd 守护线程的主体,无限期运行。
*/
static void __noreturn devtmpfs_work_loop(void)
{
/*
* 启动一个无限循环,这是守护线程的标准模式。线程将永远停留在此循环中,
* 等待并处理工作。
*/
while (1) {
/*
* 获取 req_lock 锁。这是一个自旋锁,用于保护对共享数据“requests”链表的访问。
* 在单核可抢占的系统中(如运行在 STM32H750 上的可抢占内核),
* spin_lock 会禁止内核抢占,确保在访问 requests 链表时,当前线程不会被其他任务中断,
* 从而防止数据竞争。如果内核是不可抢占的,它可能主要用来防止来自中断上下文的访问。
*/
spin_lock(&req_lock);

/*
* 检查全局的“requests”链表是否为空。requests 是一个指向请求链表头部的指针。
* 如果它不为 NULL,说明有待处理的设备节点创建/删除请求。
*/
while (requests) {
/*
* 将全局的 requests 链表头指针赋值给一个局部的 req 指针。
* 这是为了“接管”整个待处理请求链表。
*/
struct req *req = requests;
/*
* 将全局的 requests 链表头指针设置为 NULL。
* 这个操作与上面的赋值一起,构成了一个原子性的“抓取并清空”操作。
* 这样做的好处是,可以快速释放锁,让其他代码能够继续提交新的请求,
* 而处理请求的耗时操作则在锁之外进行,极大地减小了锁的粒度和持有时间。
*/
requests = NULL;

/*
* 释放 req_lock 锁。从此刻起,其他内核线程或中断就可以安全地向
* 全局的 requests 链表中添加新的请求了。
*/
spin_unlock(&req_lock);

/*
* 现在,在没有持有锁的情况下,开始遍历并处理刚刚抓取到的本地请求链表 (req)。
*/
while (req) {
/*
* 在处理当前请求之前,先将其下一个请求的地址保存在 next 指针中。
* 这是遍历链表的标准做法。
*/
struct req *next = req->next;

/*
* 调用 handle() 函数,这是实际执行工作的函数。
* 它根据请求结构体 req 中携带的信息(设备名、模式、用户ID、组ID、设备号),
* 在 devtmpfs 文件系统中执行创建或删除设备节点的操作。
* 操作的结果(错误码)被存储回请求结构体的 err 成员中。
*/
req->err = handle(req->name, req->mode,
req->uid, req->gid, req->dev);
/*
* 调用 complete() 函数,并传入请求结构体中的 done 成员。
* req->done 是一个 completion 结构体。提交请求的内核代码
* 可能会调用 wait_for_completion(&req->done) 来同步等待此操作完成。
* complete() 会唤醒那个等待的线程。
*/
complete(&req->done);

/*
* 将 req 指针移动到下一个请求,继续循环。
*/
req = next;
}
/*
* 当处理完一批请求后,重新获取锁,准备再次检查是否有新的请求或进入睡眠。
*/
spin_lock(&req_lock);
}

/*
* 如果执行到这里,说明 requests 链表为空。
* 调用 __set_current_state() 将当前线程的状态设置为 TASK_INTERRUPTIBLE。
* 这告诉内核调度器,该线程现在没有工作可做,可以被安全地换出,进入睡眠状态。
* 它只会被明确的唤醒信号(wakeup)所唤醒。
* 这一步必须在持有 req_lock 的情况下完成,以防止“丢失的唤醒”竞争条件。
*/
__set_current_state(TASK_INTERRUPTIBLE);

/*
* 释放锁。此时线程状态已设置为睡眠,可以安全地让出 CPU。
*/
spin_unlock(&req_lock);

/*
* 调用 schedule() 函数,主动放弃 CPU。内核调度器将会运行,
* 看到当前线程状态为 TASK_INTERRUPTIBLE,就会选择另一个可运行的线程来执行,
* 并将当前线程放入等待队列,直到它被唤醒。
*/
schedule();
}
}

devtmpfs_setup devtmpfs 的隔离环境设置

  • devtmpfs_setup 是一个在 devtmpfsd 内核线程上下文中执行的初始化函数。其核心目标是在一个受控的、隔离的环境中,将 devtmpfs 文件系统实例精确地挂载到系统初始命名空间的 /dev 目录上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
* noinline: 告知编译器不要将此函数内联。这有助于在调试和分析时保持函数调用栈的清晰。
* __init: 标记此函数为内核初始化代码。在内核启动过程结束后,该函数所占用的内存会被释放。
* devtmpfs_setup: 函数名。
* void *p: 接收一个通用指针参数。根据 devtmpfsd 的调用,我们知道它实际是一个指向整型变量的指针(int *p),
* 用于将函数内部的错误码传回给调用者 devtmpfsd。
*/
static noinline int __init devtmpfs_setup(void *p)
{
/*
* 声明一个整型变量 err,用于存储后续函数调用的返回值(通常是错误码)。
*/
int err;

/*
* 调用 ksys_unshare(),并传入 CLONE_NEWNS 标志,为当前线程创建一个新的、独立的挂载命名空间。
* 这是本函数最关键的一步。它将 devtmpfsd 线程的挂载视图与系统其他部分隔离开来。
* 这样做允许该线程在自己的环境中自由地进行挂载操作,而不会立即影响到全局的初始挂载命名空间。
* 即使在没有 MMU 的 STM32H750 系统上,内核的虚拟文件系统(VFS)层依然支持挂载命名空间这一逻辑隔离机制。
*/
err = ksys_unshare(CLONE_NEWNS);
if (err)
/*
* 如果创建新的挂载命名空间失败,则跳转到 out 标签处进行统一的错误处理和返回。
*/
goto out;

/*
* 在刚刚创建的、属于自己的新挂载命名空间内,执行挂载操作。
* err = init_mount("devtmpfs", "/", "devtmpfs", DEVTMPFS_MFLAGS, NULL);
* "devtmpfs": 要挂载的文件系统的名称(源)。
* "/": 挂载点。这里指的是在新命名空间内的根目录。此操作将 devtmpfs 挂载到了该线程私有的根目录上。
* "devtmpfs": 文件系统类型。
* DEVTMPFS_MFLAGS: 挂载标志,定义了挂载的行为。
* NULL: 传递给文件系统的额外数据,此处为无。
*/
err = init_mount("devtmpfs", "/", "devtmpfs", DEVTMPFS_MFLAGS, NULL);
if (err)
/*
* 如果挂载失败,则跳转到 out 标签处。
*/
goto out;

/*
* 这是一个精巧的技巧,用于“逃离”刚刚完成的挂载。
* init_chdir("/..");
* 在执行完上一步挂载后,当前线程的根目录 "/" 已经是指向新挂载的 devtmpfs。
* 执行 chdir("..") 会切换到当前目录的父目录。对于根目录来说,它的父目录就是它自身。
* 但是,由于这个 devtmpfs 是“覆盖”在原始根文件系统之上的,这里的 ".." 操作会穿越挂载点,
* 回到被覆盖的那个目录,也就是系统初始命名空间的根目录。
*/
init_chdir("/.."); /* 将会穿越被覆盖的根目录 */

/*
* 将当前线程的根目录(root)切换到当前所在目录(".")。
* init_chroot(".");
* 经过上一步的 chdir,当前目录已经变成了系统初始命名空间的根目录。
* 执行 chroot(".") 后,devtmpfsd 线程的根目录就从它自己的私有根目录,变回了系统初始的根目录。
* 这一系列操作的最终效果是:devtmpfs 实例被成功挂载到了初始命名空间的 /dev 目录,
* 同时 devtmpfsd 线程也恢复了对初始命名空间的视图,确保其后续操作(在 devtmpfs_work_loop 中)
* 是在正确的全局上下文中进行的。
*/
init_chroot(".");
out:
/*
* out 标签:一个统一的出口点。
* 将 void 指针 p 强制类型转换为 int 指针,并解引用,将最终的错误码 err 赋值给它。
* 这样,调用者 devtmpfsd 函数就可以通过检查传递进来的指针所指向的值,来获知 setup 是否成功。
*/
*(int *)p = err;
/*
* 将错误码作为函数返回值返回。
*/
return err;
}

devtmpfsd devtmpfs 守护线程

  • devtmpfsd 函数是 devtmpfs 文件系统的核心,它作为一个内核线程运行,负责处理所有与设备节点创建和删除相关的异步事件。这个线程由 devtmpfs_init 函数在初始化过程中创建并启动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* 此处的 __ref 是一个特殊的宏,用于告诉编译器,虽然 devtmpfsd 函数本身
* 没有被标记为 __init(因为它需要持续运行),但它会引用一些 __init
* 代码(即 devtmpfs_setup 函数)。devtmpfs_setup 之所以需要是 __init,
* 是因为它调用了只能在初始化阶段使用的函数。
* 这个调用是在 devtmpfs_init(一个 __init 函数)同步等待 devtmpfsd
* 完成其初始设置时发生的。这种机制确保了初始化代码在完成其使命后可以被安全地释放,
* 同时允许 devtmpfsd 线程本身继续存在于内核内存中。
*/
static int __ref devtmpfsd(void *p)
{
/*
* 声明一个整型变量 err,用于接收 devtmpfs_setup 函数的返回值。
* 调用 devtmpfs_setup 函数,执行 devtmpfs 的核心设置。
* 这包括在根文件系统的 /dev 目录挂载 devtmpfs。
* p 是一个指向 err 变量的指针,在 devtmpfs_init 中传递进来,
* 允许 devtmpfs_setup 在内部报告错误。
* 在 STM32H750 系统上,此步骤将建立起实际的 /dev 目录,使其成为
* 一个基于 RAM 的动态设备文件系统。
*/
int err = devtmpfs_setup(p);

/*
* 调用 complete() 函数,唤醒正在等待的进程。
* &setup_done 是一个全局的 completion 结构体。在 devtmpfs_init 函数中,
* 内核线程启动后会调用 wait_for_completion(&setup_done) 进行等待。
* 此处的 complete() 调用通知 devtmpfs_init,初始设置(主要是挂载到 /dev)
* 已经完成,devtmpfs_init 可以继续执行后续的初始化步骤了。
* 这是一个典型的内核同步机制。
*/
complete(&setup_done);

/*
* 检查 devtmpfs_setup 函数的执行结果。
*/
if (err)
/*
* 如果 devtmpfs_setup 返回了错误码,则该线程直接返回错误码并退出,
* 不会进入主循环。
*/
return err;

/*
* 调用 devtmpfs_work_loop() 函数,进入一个无限循环。
* 这个循环是 devtmpfsd 线程的主要工作区,它会持续等待并处理来自
* 内核(主要是驱动核心)的 uevent 事件,这些事件通知线程需要
* 创建或删除设备节点。
*/
devtmpfs_work_loop();

/*
* devtmpfs_work_loop() 是一个无限循环,所以理论上不会执行到这里。
* 返回 0 仅作为函数声明的完整性要求。
*/
return 0;
}

devtmpfs_init devtmpfs 文件系统的初始化

  • devtmpfs 是一个基于 tmpfs 的文件系统,在 Linux 内核启动期间创建,用于存放设备节点。这确保了设备节点在设备被发现时即可用,而无需等待 udev 等用户空间工具来创建它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/* Ops are filled in during init depending on underlying shmem or ramfs type */
struct fs_context_operations devtmpfs_context_ops = {};

/* Call the underlying initialization and set to our ops */
static int devtmpfs_init_fs_context(struct fs_context *fc)
{
int ret;
// #ifdef CONFIG_TMPFS
// ret = shmem_init_fs_context(fc);
// #else
ret = ramfs_init_fs_context(fc);
// #endif
if (ret < 0)
return ret;

fc->ops = &devtmpfs_context_ops;

return 0;
}

static struct file_system_type dev_fs_type = {
.name = "devtmpfs",
.init_fs_context = devtmpfs_init_fs_context,
};

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,
};

/*
* 创建 devtmpfs 实例,驱动核心设备将在此处添加其设备节点。
* 此函数负责初始化一个临时的、基于内存的文件系统来管理设备文件,这对于
* 动态设备发现至关重要。
*/
int __init devtmpfs_init(void)
{
/*
* 定义一个字符串数组,用于存储传递给 devtmpfs 文件系统的挂载选项。
* "mode=0755" 设置了 devtmpfs 根目录的默认权限为 0755 (rwxr-xr-x)。
*/
char opts[] = "mode=0755";

/*
* 声明一个整型变量 err,用于存储函数调用的返回值,以便进行错误检查。
*/
int err;

/*
* 调用 vfs_kern_mount 函数挂载一个新的文件系统实例。
* &internal_fs_type: 指向一个静态定义的 file_system_type 结构体,
* 对于 devtmpfs 来说,这通常是 ramfs 或 shmem_fs。
* 0: 挂载标志,此处为 0,表示使用默认标志。
* "devtmpfs": 文件系统的名称,将显示在 /proc/mounts 中。
* opts: 传递给文件系统的挂载选项,即 "mode=0755"。
* mnt: 是一个全局的 vfsmount 结构体指针,用于保存新挂载的文件系统实例的引用。
*/
mnt = vfs_kern_mount(&internal_fs_type, 0, "devtmpfs", opts);

/*
* 检查 vfs_kern_mount 的返回值,判断挂载是否成功。
* IS_ERR(mnt) 是一个宏,用于检查指针是否包含一个错误码。
* 在单核 STM32H750 系统上,如果内存分配失败,此步骤可能会失败。
*/
if (IS_ERR(mnt)) {
/*
* 如果挂载失败,使用 pr_err 打印错误信息。
* PTR_ERR(mnt) 从指针中提取错误码。
*/
pr_err("unable to create devtmpfs %ld\n", PTR_ERR(mnt));

/*
* 返回从 mnt 指针中提取的错误码。
*/
return PTR_ERR(mnt);
}

/*
* 调用 devtmpfs_configure_context 函数配置 SELinux 上下文。
* 在不使用 SELinux 的系统中,此函数可能为空操作。
*/
err = devtmpfs_configure_context();

/*
* 检查 devtmpfs_configure_context 的返回值。
*/
if (err) {
/*
* 如果配置失败,打印错误信息。
*/
pr_err("unable to configure devtmpfs type %d\n", err);

/*
* 返回错误码。
*/
return err;
}

/*
* 调用 register_filesystem 函数向内核注册一个新的文件系统类型。
* &dev_fs_type: 指向 devtmpfs 的 file_system_type 结构体。
* 这使得用户空间可以通过 "mount -t devtmpfs" 命令来挂载 devtmpfs。
*/
err = register_filesystem(&dev_fs_type);

/*
* 检查 register_filesystem 的返回值。
*/
if (err) {
/*
* 如果注册失败,打印错误信息。
*/
pr_err("unable to register devtmpfs type %d\n", err);

/*
* 返回错误码。
*/
return err;
}

/*
* 调用 kthread_run 创建并启动一个新的内核线程。
* devtmpfsd: 是新线程要执行的函数,即 devtmpfs 的守护进程。
* &err: 传递给 devtmpfsd 函数的参数,用于在线程内部报告错误。
* "kdevtmpfs": 内核线程的名称。
* thread: 是一个全局的 task_struct 指针,用于保存新创建线程的引用。
* 在单核 STM32H750 系统上,此线程将与其他内核任务一起被调度器分时执行。
*/
thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");

/*
* 检查 kthread_run 的返回值,判断线程是否创建成功。
*/
if (!IS_ERR(thread)) {
/*
* 如果线程创建成功,调用 wait_for_completion 等待 devtmpfsd 守护进程
* 完成其初始化设置。
* &setup_done: 是一个 completion 结构体,devtmpfsd 函数在完成
* 初始挂载后会调用 complete(&setup_done) 来唤醒此处的等待。
*/
wait_for_completion(&setup_done);
} else {
/*
* 如果线程创建失败,从 thread 指针中提取错误码。
*/
err = PTR_ERR(thread);

/*
* 将全局的 thread 指针设置为 NULL,表示没有正在运行的 devtmpfsd 线程。
*/
thread = NULL;
}

/*
* 检查线程创建或其初始化的过程中是否发生错误。
*/
if (err) {
/*
* 如果发生错误,打印错误信息。
*/
pr_err("unable to create devtmpfs %d\n", err);

/*
* 调用 unregister_filesystem 注销之前注册的 devtmpfs 文件系统类型。
*/
unregister_filesystem(&dev_fs_type);

/*
* 将全局的 thread 指针设置为 NULL。
*/
thread = NULL;

/*
* 返回错误码。
*/
return err;
}

/*
* 打印初始化成功的信息。
*/
pr_info("initialized\n");

/*
* 返回 0,表示 devtmpfs 初始化成功。
*/
return 0;
}