Linux中写入文件操作,最常用的就是write函数,其原型如下:
ssize_t write(int fd, const void *buf, size_t count);
write尝试从buf指向的地址,写入count个字节到文件描述符fd中,并返回成功写入的字节数,同时将文件偏移向前移动相同的字节数。
write有可能写入比指定count少的字节数。
1.7.1 write源码跟踪
write的源码与read的很相似,位于read_write.c中,代码如下:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
/* 得到
file管理结构指针 */
file = fget_light(fd, &fput_needed);
if (file) {
/* 得到当前的文件偏移 */
loff_t pos = file_pos_read(file);
/* 利用
VFS写入 */
ret = vfs_write(file, buf, count, &pos);
/* 更新文件偏移量
*/
file_pos_write(file, pos);
/* 释放文件管理指针
file */
fput_light(file, fput_needed);
}
return ret;
}
进入vfs_write,代码如下:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
/* 检查文件是否为写入打开
*/
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
/* 检查文件是否支持打开操作
*/
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
/* 检查用户给定的地址范围是否可读取*/
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
/* 验证文件从pos起始是否可以写入count个字节数并返回可以写入的字节数 */
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
/* 更新写入字节数
*/
count = ret;
/*
如果定义write操作,则执行定义的write操作如果没有定义write操作,则调用do_sync_write—其利用异步aio_write来完成同步的write操作*/
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
/* 写入了一定的字节数,进行通知操作
*/
fsnotify_modify(file);
/* 增加进程读取字节的统计计数
*/
add_wchar(current, ret);
}
/* 增加进程系统调用的统计计数
*/
inc_syscw(current);
}
return ret;
}
write同样有部分写入的情况,这个与read类似,都是由具体实现来决定的。在此就不再深入探讨
write的部分写入的情况了。
1.7.2 追加写的实现
前面说过,文件的读写操作都是从当前文件的偏移处开始的。这个文件偏移量保存在文件表中,而每个进程都有一个文件表。那么当多个进程同时写一个文件时,即使对write进行了锁保护,在进行串行写操作时,文件依然不可避免地会被写乱。根本原因就在于文件偏移量是进程级别的。
当使用O_APPEND以追加的形式来打开文件时,每次写操作都会先定位到文件末尾,然后再执行写操作。
Linux下大多数文件系统都是调用generic_file_aio_write来实现写操作的。在generic_file_aio_write中,有如下代码:
mutex_lock(&inode->i_mutex);
blk_start_plug(&plug);
ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);
mutex_unlock(&inode->i_mutex);
这里有一个关键的语句,就是使用mutex_lock对该文件对应的inode进行保护,然后调用__generic_file_aio_write->generic_write_check。其部分代码如下:
if (file->f_flags & O_APPEND)
*pos = i_size_read(inode);
上面的代码中,如果发现文件是以追加方式打开的,则将从inode中读取到的最新文件大小作为偏移量,然后通过**__generic_file_aio_write**再进行写操作,这样就能保证写操作是在文件末尾追加的。
1.8 文件的原子读写
使用O_APPEND可以实现在文件的末尾原子追加新数据,Linux还提供pread和pwrite从指定偏移位置读取或写入数据。
它们的实现很简单,代码如下:
SYSCALL_DEFINE(pread64)(unsigned int fd, char __user *buf, size_t count, loff_t pos)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
if (pos < 0)
return -EINVAL;
file = fget_light(fd, &fput_needed);
if (file) {
ret = -ESPIPE;
if (file->f_mode & FMODE_PREAD)
ret = vfs_read(file, buf, count, &pos);
fput_light(file, fput_needed);
}
return ret;
}
看到这段代码,是不是有一种似曾相识的感觉?让我们再来回顾一下read的实现,代码如下所示。
/* 得到文件的当前偏移量 */
loff_t pos = file_pos_read(file);
/* 利用vfs进行真正的read */
ret = vfs_read(file, buf, count, &pos);
/* 更新文件偏移量 */
file_pos_write(file, pos);