先来描述一下,write系统调用的大体流程,首先内核会取得对应的文件偏移,然后调用vfs的write操作,而在vfs层的write操作的时候会调用对应文件系统的write方法,而在对应文件系统的write方法中aio_write方法,最终会调用底层驱动。这里有一个需要注意的就是内核在写文件的时候会加一把锁(有些设备不会加锁,比如块设备以及裸设备).这样也就是说一个文件只可能同时只有一个进程在写。而且快设备是不支持append写的。

而这里append的原子操作的实现很简单,由于每次写文件只可能是一个进程操作(计算文件偏移并不包含在锁里面),而append操作是每次写到末尾(其他类型的写是先取得pos才进入临界区,而这个时间内有可能pos已经被其他进程改变,而append的pos的计算是包含在锁里面的),因此自然append就是原子的了.

ok,接下来来看代码。

首先来看write的系统调用,函数很简单,就是取得当前文件的偏移,然后调用vfs的写方法。最后更改文件的偏移。这里要注意,取得文件偏移的方法并没有加锁,也就是说这里存在竞争。

这里有个要注意的就是POS,如果是append写的话,后面的代码会修改这个值,这里先跳过,后面遇到我们会说明。

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
  
SYSCALL_DEFINE3(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);

if (file) {

//取得文件句柄的偏移

loff_t pos = file_pos_read(file);

//写文件。传递偏移量。

ret = vfs_write(file, buf, count, &pos);

//更新偏移

file_pos_write(file, pos);

fput_light(file, fput_needed);

}

return ret;

}

接下来就是vfs_write,这个函数主要就是进行一些合法性判断,然后调用具体文件系统的write方法,这里要注意,write方法不一定会调用到文件系统的write方法,比如块设备以及裸设备都会调用到blkdev_aio_write。

而file op的初始化在ext3_iget中的,也就是获取超级块的方法,可以看到如果是一般文件,则会被初始化为ext3_file_inode_operations。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  
struct inode \*ext3_iget(struct super_block \*sb, unsigned long ino)

……………………………………………………………………..

if (S_ISREG(inode->i_mode)) {

//初始化

inode->i_op = &ext3_file_inode_operations;

inode->i_fop = &ext3_file_operations;

ext3_set_aops(inode);

………………………………………….
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
  
ssize_t vfs_write(struct file \*file, const char __user \*buf, size_t count, loff_t *pos)

{

……………………………………………………………….

ret = rw_verify_area(WRITE, file, pos, count);

if (ret >= 0) {

count = ret;

//调用具体的文件系统的方法。

if (file->f_op->write)

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

else

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

……………………………………………………………………….

}

return ret;

}

我们主要来看ext3的实现,其他的文件系统基本差不多。下面就是ext3文件系统对应回调操作函数。

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
  
const struct file_operations ext3_file_operations = {

.llseek = generic_file_llseek,

//主要是下面4个

.read = do_sync_read,

.write = do_sync_write,

.aio_read = generic_file_aio_read,

.aio_write = generic_file_aio_write,

.unlocked_ioctl = ext3_ioctl,

#ifdef CONFIG_COMPAT

.compat_ioctl = ext3_compat_ioctl,

#endif

.mmap = generic_file_mmap,

.open = dquot_file_open,

.release = ext3_release_file,

.fsync = ext3_sync_file,

.splice_read = generic_file_splice_read,

.splice_write = generic_file_splice_write,

};

可以看到它的write方法就是do_sync_write,在do_sync_write中会调用它自己的aio_write方法:

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
  
ssize_t do_sync_write(struct file \*filp, const char __user \*buf, size_t len, loff_t *ppos)

{

………………………………………..

//如果不是块设备才进入下面的处理

if (!isblk) {

/\* FIXME: this is for backwards compatibility with 2.4 \*/

//调用i_size_read得到文件大小,从而定位append的位置。

if (file->f_flags & O_APPEND)

*pos = i_size_read(inode);

if (limit != RLIM_INFINITY) {

if (*pos >= limit) {

send_sig(SIGXFSZ, current, 0);

return -EFBIG;

}

if (\*count > limit – (typeof(limit))\*pos) {

\*count = limit – (typeof(limit))\*pos;

}

}

}

……………………………………………

return ret;

}

因此可以看到最关键的操作都是放在aio_write中,也就是generic_file_aio_write,这个函数我们可以看到在调用具体的实现__generic_file_aio_write之前会加一把锁(i_mutex),这样就保证了一个文件同时只会有一个进程来写。

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
  
ssize_t generic_file_aio_write(struct kiocb \*iocb, const struct iovec \*iov,

unsigned long nr_segs, loff_t pos)

{

struct file *file = iocb->ki_filp;

struct inode *inode = file->f_mapping->host;

ssize_t ret;

BUG_ON(iocb->ki_pos != pos);

//加锁

mutex_lock(&inode->i_mutex);

//调用具体的实现

ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);

//释放锁

mutex_unlock(&inode->i_mutex);

if (ret > 0 || ret == -EIOCBQUEUED) {

ssize_t err;

err = generic_write_sync(file, pos, ret);

if (err < 0 && ret > 0)

ret = err;

}

return ret;

}

上面可以看到先加锁然后调用generic_file_aio_write,而对应的blkdev_aio_write则是直接调用\generic_file_aio_write,也就是不用加锁,下面就是内核里面的注释:

1
2
3
4
   
* It expects i_mutex to be grabbed unless we work on a block device or similar

* object which does not need locking at all.

然后来看__generic_file_aio_write的实现,这里它调用了generic_write_checks,这个函数主要用来执行写之前的一些检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  
ssize_t __generic_file_aio_write(struct kiocb \*iocb, const struct iovec \*iov,

unsigned long nr_segs, loff_t *ppos)

{

&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;.

err = generic_write_checks(file, &pos, &count, S_ISBLK(inode->i_mode));

&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;.

}

然后是generic_write_checks,这个函数就是做一些检测,并且APPEND写的原子性也是由这个函数进行控制的。

这里会修改对应的pos,调用i_size_read来得到文件的大小,从而进行append操作。

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
  
inline int generic_write_checks(struct file \*file, loff_t \*pos, size_t *count, int isblk)

{

struct inode *inode = file->f_mapping->host;

unsigned long limit = rlimit(RLIMIT_FSIZE);

if (unlikely(*pos < 0))

return -EINVAL;

if (!isblk) {

/\* FIXME: this is for backwards compatibility with 2.4 \*/

//如果是append操作,则调用i_size_read得到文件大小,然后得到文件该写的位置,这里更改了pos的值.

if (file->f_flags & O_APPEND)

//更改pos

*pos = i_size_read(inode);

if (limit != RLIM_INFINITY) {

if (*pos >= limit) {

send_sig(SIGXFSZ, current, 0);

return -EFBIG;

}

if (\*count > limit &#8211; (typeof(limit))\*pos) {

\*count = limit &#8211; (typeof(limit))\*pos;

}

}

}

&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;&#8230;.

}