# Linux 文件写入原理

本文将详细研究Linux对于文件从用户空间写入到内核,然后到磁盘的整个过程。在混沌学堂中我们说过分为两步:

  1. sys_open 打开一个文件,返回fd
  2. sys_write 写入文件

那么,我们首先来看 文件 打开的过程。

sys_open 函数

asmlinkage long sys_open(const char __user * filename, int flags, int mode)

{

  char * tmp;

  int fd, error;

  tmp = getname(filename); // 将用户空间的filename 复制到内核空间,此时tmp指向的内存为内核内存

  fd = PTR_ERR(tmp); // 检查tmp 是否包含了错误码,因为错误码范围将会在:(0xffff f000,0xffff ffff)之间(注:在内核中,将最高地址的最后一页保留,用作错误码表示)

  if (!IS_ERR(tmp)) { // 返回值为正常指针

    fd = get_unused_fd();

    if (fd >= 0) { // 成功获取fd,那么调用 filp_open 打开 文件 file

      struct file *f = filp_open(tmp, flags, mode);

      error = PTR_ERR(f);

      if (IS_ERR(f))

        goto out_error;

      fd_install(fd, f); // 随后将fd与file文件关联

   }

    out:

    putname(tmp);

 }

  return fd;

  out_error:

  put_unused_fd(fd);

  fd = error;

  goto out;

}
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

filp_open 函数

该函数用于打开文件。源码如下。

/*

 * 在sys_open函数时,flag 值的低两位表示:

 * 00 - read-only 只读

 * 01 - write-only 只写

 * 10 - read-write 读写

 * 11 - special 特殊值

 * 将会被改变为如下含义:

 * 00 - no permissions needed 不需要权限

 * 01 - read-permission 只读权限

 * 10 - write-permission 只写权限

 * 11 - read-write 读写权限

 */

struct file *filp_open(const char * filename, int flags, int mode)

{

 ...

  error = open_namei(filename, namei_flags, mode, &nd); // 尝试直接通过文件名获取file 目录

  if (!error) // 目录获取成功,那么通过目录对象打开文件

    return dentry_open(nd.dentry, nd.mnt, flags);

  return ERR_PTR(error);

}
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

open_namei 函数

该函数用于根据pathname来查找文件,注意:有时候我们传入的filename可能携带路径,也可能不携带路径,该函数主要调用 path_lookup 函数来完成查找,由于Linux允许在文件不存在时,传入 O_CREAT 标志,可以自动创建文件,但是为了保证主流程顺畅,这里读者只需要观察文件存在情况即可。源码如下。

int open_namei(const char * pathname, int flag, int mode, struct nameidata *nd)

{

  int acc_mode, error = 0;

  struct dentry *dentry; // 文件目录

  struct dentry *dir;

  int count = 0;

 ...

    if (!(flag & O_CREAT)) { // 不需要创建文件,我们主要观察文件存在情况

      error = path_lookup(pathname, lookup_flags(flag)|LOOKUP_OPEN, nd); // 根据路径查找

      if (error)

        return error;

      // 查找成功

      dentry = nd->dentry;

      goto ok;

   }

  // 不存在该文件,那么创建(这里不考虑)

  error = path_lookup(pathname, LOOKUP_PARENT|LOOKUP_OPEN|LOOKUP_CREATE, nd);

  if (error)

    return error;

 ...

    ok:

  error = may_open(nd, acc_mode, flag); // 检擦文件是否可以打开

  if (error)

    goto exit;

  return 0;

 ...

}// 根据name查找文件,文件信息放入 nameidata *nd 中

int path_lookup(const char *name, unsigned int flags, struct nameidata *nd)

{

 ...

  if (*name=='/') { // 以路径符传入,说明此时打开的文件以绝对路径查找

   ...

    // 设置查找路径为根目录

    nd->mnt = mntget(current->fs->rootmnt);

    nd->dentry = dget(current->fs->root);

 }

  else{ // 否则设置当前进程所处的路径(pwd)为查找路径

    nd->mnt = mntget(current->fs->pwdmnt);

    nd->dentry = dget(current->fs->pwd);

 }

 ...

  return link_path_walk(name, nd); // 开始查找

}// 根据当前 nd 设置的 mnt 和 dentry 查找 name 文件的目录

int link_path_walk(const char * name, struct nameidata *nd)

{

 struct path next;

 struct inode *inode;

 int err;

 unsigned int lookup_flags = nd->flags;

 

 while (*name=='/') // 移动name指针到最后一个 / 分隔符,此时 name 指针指向文件名

  name++;

 if (!*name) // 不存在文件名,直接退出

  goto return_reval;

​

 inode = nd->dentry->d_inode; // 获取目录的inode

 ...

 for(;;) { // 循环查找

  unsigned long hash;

  struct qstr this; // 保存文件名信息

  unsigned int c;

  ...

  this.name = name; // 保存文件名

    // 根据文件名计算hash值 

  c = *(const unsigned char *)name;

  hash = init_name_hash();

  do {

   name++;

   hash = partial_name_hash(c, hash);

   c = *(const unsigned char *)name;

  } while (c && (c != '/'));

    // 保存文件名长度与hash值

  this.len = name - (const char *) this.name;

  this.hash = end_name_hash(hash);

  ...

  // 修正 "." 和 ".." 分别表示当前目录和上一级目录

  if (this.name[0] == '.') switch (this.len) {

   default:

    break;

   case 2: 

    if (this.name[1] != '.')

     break;

    follow_dotdot(&nd->mnt, &nd->dentry);

    inode = nd->dentry->d_inode;

   case 1:

    continue;

  }

  ...

  err = do_lookup(nd, &this, &next); // 执行实际搜索

  if (err)

   break;

  ...

  inode = next.dentry->d_inode;

  if (!inode) // 找到目录(最后一个目录便是包含了所查找文件的inode)

   goto out_dput;

  err = -ENOTDIR; 

  if (!inode->i_op) // 查找到最后一个inode,但不是目录,所以没有 i_op 操作,直接退出

   goto out_dput;

  ...

    // 继续查找下一个目录

  dput(nd->dentry);

  nd->mnt = next.mnt;

  nd->dentry = next.dentry;

  err = -ENOTDIR; 

  ...

 }

 ...

}// 实际查找过程

static int do_lookup(struct nameidata *nd, struct qstr *name,

    struct path *path)

{

 struct vfsmount *mnt = nd->mnt; // 获取挂载点

 struct dentry *dentry = __d_lookup(nd->dentry, name); // 从设置的目录 dentry 处查找

 if (!dentry)

  goto need_lookup;

 ...

done: // 查找成功

 path->mnt = mnt;

 path->dentry = dentry;

 return 0;

​

need_lookup: // 注意:当内存中的dentry 不存在时,因为初始时 内存中将不会存在dentry,dentry 只是 对磁盘上的数据进行缓存优化,所以此时将会陷入到FS文件系统中读磁盘数据查找,这里太过繁琐,读者了解即可

 dentry = real_lookup(nd->dentry, name, nd);

 if (IS_ERR(dentry))

  goto fail;

 goto done;

...

fail:

 return PTR_ERR(dentry);

}// 从parent目录处,查找 name 所述的 dentry

struct dentry * __d_lookup(struct dentry * parent, struct qstr * name)

{

 unsigned int len = name->len;

 unsigned int hash = name->hash;

 const unsigned char *str = name->name;

 struct hlist_head *head = d_hash(parent,hash); // 根据目录和文件名在hash表找到目录下的文件头部指针(读者考虑下:hash冲突?)

 struct dentry *found = NULL;

 struct hlist_node *node;

 ...

 hlist_for_each (node, head) { // 遍历该链表

  struct dentry *dentry; 

  unsigned long move_count;

  struct qstr * qstr;

  dentry = hlist_entry(node, struct dentry, d_hash); // 获取当前node处的dentry目录信息

  ...

  if (dentry->d_name.hash != hash) // hash 值不同继续查找

   continue;

  if (dentry->d_parent != parent) // 父目录不同继续查找

   continue;

  qstr = dentry->d_qstr;

    // 进行名字比较

  if (parent->d_op && parent->d_op->d_compare) { 

   if (parent->d_op->d_compare(parent, qstr, name))

    continue;

  } else {

   if (qstr->len != len)

    continue;

   if (memcmp(qstr->name, str, len))

    continue;

  }

  ... // 成功查找

  break;

  }

  return found;

}
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345

dentry_open 函数

在open_namei中,我们看到:通过dentry 遍历找到文件所述的目录dentry信息,那么dentry_open 函数将会根据 dentry 找到该目录下的 name 文件。源码如下。

struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags)

{

 struct file * f;

 struct inode *inode;

 int error;

 error = -ENFILE;

 f = get_empty_filp(); // 分配一个新的file结构,注意在该函数中将会检查打开的文件的计数,读者可以自行查看

 ...

 inode = dentry->d_inode; // 获取目录inode信息

 ...

  // 保存文件的元数据信息

 f->f_dentry = dentry;

 f->f_vfsmnt = mnt;

 f->f_pos = 0; // 初始文件操作点为从offset偏移量为0处开始

 f->f_op = fops_get(inode->i_fop); // 从inode中获取操作文件的函数指针

 file_move(f, &inode->i_sb->s_files); // 将打开文件信息绑定到super_block中,此时可以根据超级块看到该文件系统打开的所有文件

 if (f->f_op && f->f_op->open) { // 若inode 操作存在 open函数,那么进行回调(对于ext2来说:这里对文件长度进行了校验)

  error = f->f_op->open(inode,f);

  if (error)

   goto cleanup_all;

 }

 ...

}
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

fd_install 函数

该函数用于将fd与打开文件file进行关联,我们看到这里就是将其放入当前进程 task_struct 的files数组对应 fd下标处。源码描述如下。

void fd_install(unsigned int fd, struct file * file)

{

 struct files_struct *files = current->files;

 spin_lock(&files->file_lock);

 if (unlikely(files->fd[fd] != NULL))

  BUG();

 files->fd[fd] = file; // 在对应fd下标处放置file,将fd返回给用户

 spin_unlock(&files->file_lock);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

sys _write 函数

该系统调用将根据传入的fd下标,找到对应的file结构,然后执行写入操作。源码如下。

asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)

{

  struct file *file;

  ssize_t ret = -EBADF;

  int fput_needed;

  file = fget_light(fd, &fput_needed); // 根据fd 找到 file

  if (file) { // 文件存在,那么 开始调用vfs 接口写入

    ret = vfs_write(file, buf, count, &file->f_pos);

    fput_light(file, fput_needed);

 }return ret;

}struct file *fget_light(unsigned int fd, int *fput_needed)

{

  struct file *file;

  struct files_struct *files = current->files; // 获取进程打开文件结构

  *fput_needed = 0;

  if (likely((atomic_read(&files->count) == 1))) { // 原子性增加file引用计数

    file = fcheck(fd); // 从fd数组下标处获取file结构

 } else { // cas 失败,那么上锁获取

    spin_lock(&files->file_lock);

    file = fcheck(fd);

    if (file) {

      get_file(file);

      *fput_needed = 1;

   }

    spin_unlock(&files->file_lock);

 }

  return file;

}#define fcheck(fd) fcheck_files(current->files, fd)

static inline struct file * fcheck_files(struct files_struct *files, unsigned int fd)

{

 struct file * file = NULL;

 if (fd < files->max_fds) // 直接从下标处获取即可

  file = files->fd[fd];

 return file;

}
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

vfs_write 函数

该函数将会调用vfs接口向file文件中写入数据。我们看到首先根据file获取到inode,然后执行写入。源码如下。

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)

{

  struct inode *inode = file->f_dentry->d_inode; // 通过file结构获取到文件inode节点

  ssize_t ret;

 ...

  if (!ret) {

    ret = security_file_permission (file, MAY_WRITE); // 检测权限

    if (!ret) {

      if (file->f_op->write) // 若存在文件存在write 函数,那么直接调用

        ret = file->f_op->write(file, buf, count, pos);

      else // 否则尝试使用aio_write 并等待完成 ,因为有些设备文件只支持异步写入

        ret = do_sync_write(file, buf, count, pos);

     ...

   }

 }return ret;

}
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

generic_file_write 函数实现

generic_file_write 函数将会作为write函数在ext2文件系统的实现,所以我们关注该函数的实现过程即可。

// ext2 文件系统注册的文件写入操作

struct file_operations ext2_file_operations = {

 ...

 .write  = generic_file_write,

 ...

};ssize_t generic_file_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)

{

 struct inode *inode = file->f_dentry->d_inode->i_mapping->host; // 获取文件inode

 ssize_t  err;

 struct iovec local_iov = { .iov_base = (void __user *)buf, .iov_len = count }; // 构建写入向量信息

 down(&inode->i_sem); // 上锁并开始写入

 err = generic_file_write_nolock(file, &local_iov, 1, ppos);

 up(&inode->i_sem);

 return err;

}ssize_t generic_file_write_nolock(struct file *file, const struct iovec *iov,

    unsigned long nr_segs, loff_t *ppos)

{

 struct kiocb kiocb; // io 控制块,用于表示一次IO操作

 ssize_t ret;

 init_sync_kiocb(&kiocb, file);

 ret = generic_file_aio_write_nolock(&kiocb, iov, nr_segs, ppos); // 调用该函数完成写入

 if (-EIOCBQUEUED == ret) // IOCB控制块已经进入调度队列,那么等待其执行完成

  ret = wait_on_sync_kiocb(&kiocb);

 return ret;

}// 初始化io控制块

#define init_sync_kiocb(x, filp)   \

 do {      \

  struct task_struct *tsk = current; \

  (x)->ki_flags = 0;   \

  (x)->ki_users = 1;   \

  (x)->ki_key = KIOCB_SYNC_KEY;  \

  (x)->ki_filp = (filp);   \

  (x)->ki_ctx = &tsk->active_mm->default_kioctx; \

  (x)->ki_cancel = NULL;   \

  (x)->ki_user_obj = tsk;   \

 } while (0)
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

generic_file_aio_write_nolock 函数

该函数将实现真正文件数据写入流程。我们看到将会根据文件打开的类型来选择写入方式,如果是O_DIRECT 那么直接写入调度层,如果是 O_SYNC ,那么等待数据落盘,否则我们写入page cache,并且由于buffer_head 的数据区存在于page中,所以当函数 filemap_copy_from_user 复制成功时,表明写入完成。源码如下。(对于 O_DIRECT 和 O_SYNC 的原理,我们在后面再详细描述)

ssize_t generic_file_aio_write_nolock(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t *ppos)

{

 ...

  for (seg = 0; seg < nr_segs; seg++) { // 循环计算写入数量,注意:这里由于是单次写入,所以在上述函数中nr_segs为1

    const struct iovec *iv = &iov[seg];

    ocount += iv->iov_len; // 增加计数

    if (unlikely((ssize_t)(ocount|iv->iov_len) < 0))

      return -EINVAL;

    if (access_ok(VERIFY_READ, iv->iov_base, iv->iov_len)) // 检测地址是否可读

      continue;

    if (seg == 0)

      return -EFAULT;

    nr_segs = seg;

    ocount -= iv->iov_len; // 当前写入段有误,那么减少增加的计数

    break;

 }

 ...

  if (unlikely(file->f_flags & O_DIRECT)) { // 文件打开类型为 O_DIRECT,那么不经过PAGE CACHE,直接写入

   ...

    written = generic_file_direct_IO(WRITE, iocb,

                    iov, pos, nr_segs); // 直接写入

   ...

    if (written >= 0 && file->f_flags & O_SYNC) // 文件打开类型为 O_SYNC,那么将写入的文件数据落盘

      status = generic_osync_inode(inode, OSYNC_METADATA);

   ...

 }

  // 否则执行数据写入到page cache中

  buf = iov->iov_base;

  do { // 循环写入

   ...

    page = __grab_cache_page(mapping,index,&cached_page,&lru_pvec); // 获取当前写入文件的page页(过程暂时省略,又是一大堆内容~这里了解流程即可)

   ...

    status = a_ops->prepare_write(file, page, offset, offset+bytes);

   ...

    if (likely(nr_segs == 1)) // 从用户空间buf中,将数据复制到 page 页中,由于buffer_head的数据便存在于page中,当该函数执行完成那么就完成了写入操作

      copied = filemap_copy_from_user(page, offset,

                      buf, bytes);

    else

      copied = filemap_copy_from_user_iovec(page, offset,

                         cur_iov, iov_base, bytes);

   ...

    status = a_ops->commit_write(file, page, offset, offset+bytes); // 提交写入结果

   ....

 } while (count);

 ...

}
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

generic_commit_write 函数

在ext2中,可以看到是该函数完成了提交写操作,所以我们分析即可,可以看到这里将会把映射到page中的buffer_head高速缓冲区设置为dirty。源码如下。

struct address_space_operations ext2_aops = {

 ...

 .commit_write  = generic_commit_write,

 ...

};int generic_commit_write(struct file *file, struct page *page, unsigned from, unsigned to)

{

 ...

 __block_commit_write(inode,page,from,to); // 提交写

 if (pos > inode->i_size) { // 写入成功,那么标记inode为脏,表明内存中的数据尚未落盘

  i_size_write(inode, pos);

  mark_inode_dirty(inode);

 }

 return 0;

}static int __block_commit_write(struct inode *inode, struct page *page,

  unsigned from, unsigned to)

{

 ...

 for(bh = head = page_buffers(page), block_start = 0; // 从页结构中获取 buffer_head 高速缓冲区块,然后遍历写入

   bh != head || !block_start;

   block_start=block_end, bh = bh->b_this_page) {

  block_end = block_start + blocksize;

  if (block_end <= from || block_start >= to) { // 部分写入

   if (!buffer_uptodate(bh))

    partial = 1;

  } else { // 写入整个高速缓冲区块,那么标记为脏

   set_buffer_uptodate(bh); 

   mark_buffer_dirty(bh);

  }

 }

 ...

 return 0;

}
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