注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
上回我们分析了send的几个类似函数的实现,接下来我们来探讨一下write家族的数据发送实现,主要有两个函数,write和writev。
write()函数
函数原型
ssize_t write(int fd, const void *buf, size_t count);
其中,
fd为文件描述符,在socket编程中即为socket或者accept系统调用返回的网络文件描述符;
buf是用户数据存储起始地址;
count为用户数据长度。
内核实现
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
{
//根据文件描述符获取file结构体,和之前listen、connect等函数类似
//同时获取锁,保证数据一致性
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
//获取文件偏移量
loff_t pos = file_pos_read(f.file);
//写数据的主要处理函数
ret = vfs_write(f.file, buf, count, &pos);
//设置文件偏移量,以便下次读写
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
fdget_pos()函数的操作和sockfd_lookup_light()十分类似,都是根据文件描述符fd,获取对应数据结构。
static inline struct fd fdget_pos(int fd)
{
struct fd f = fdget(fd);
struct file *file = f.file;
if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
if (file_count(file) > 1) {
f.flags |= FDPUT_POS_UNLOCK;
mutex_lock(&file->f_pos_lock);
}
}
return f;
}
static inline struct fd fdget(unsigned int fd)
{
int b;
//从文件描述符位图表获取对应的file结构体
struct file *f = fget_light(fd, &b);
return (struct fd){f,b};
}
关于fget_light()的相关操作,可参考以下链接,
Linux sockfd_lookup_light()—-根据文件描述符fd获取socket结构体
获取到file文件结构,就可以进行写数据操作了,也就是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;
//对传入参数的校验,比如写的数据会不会过大溢出等问题
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
count = ret;
file_start_write(file);
//这个判断是比较重要的,是网络IO和磁盘IO的分叉口
//对于网络IO,socket的操作集没有定义write函数,因此会走else分支
if (file->f_op->write)
//磁盘IO路径
ret = file->f_op->write(file, buf, count, pos);
else
//socket IO路径
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
file->f_op的定义是在socket系统调用中,内核sock_alloc_file()里,
//socket_file_ops 没有定义write函数
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
...
file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);
...
}
struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
...
file->f_op = fop;//也就是socket_file_ops
...
}
此时,write流程就会进入do_sync_write()函数,
ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
//构造iovec结构体,存放用户数据存储信息
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
ssize_t ret;
//初始化IO请求信息,在内核中,每个IO请求都对应一个kiocb结构体
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
kiocb.ki_nbytes = len;
//调用IO写接口,sock_aio_write,第三个参数表示数据块数量,这里设置为1,只有一个数据块
//由此可知,肯定有接口可以同时发送多个非连续的数据块,这就是writev(),但这就是后话了
ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
if (-EIOCBQUEUED == ret)
//等待同步IO请求完成
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;//文件写偏移位置
return ret;
}
由上面socket_file_ops 结构体可知,filp->f_op->aio_write最终调用的就是sock_aio_write()函数,
static ssize_t sock_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t pos)
{
struct sock_iocb siocb, *x;
if (pos != 0)
return -ESPIPE;
//分配一个sock_iocb结构,用于表示socket IO,并与kiocb相互关联
x = alloc_sock_iocb(iocb, &siocb);
if (!x)
return -ENOMEM;
return do_sock_write(&x->async_msg, iocb, iocb->ki_filp, iov, nr_segs);
}
static struct sock_iocb *alloc_sock_iocb(struct kiocb *iocb,
struct sock_iocb *siocb)
{
if (!is_sync_kiocb(iocb)) {
//如果是异步IO,为siocb分配内存
siocb = kmalloc(sizeof(*siocb), GFP_KERNEL);
if (!siocb)
return NULL;
iocb->ki_dtor = sock_aio_dtor;
}
//关联kiocb和sock_iocb
siocb->kiocb = iocb;
iocb->private = siocb;
return siocb;
}
在do_sock_write()函数中,我们就能看到write的庐山真面目了,
static ssize_t do_sock_write(struct msghdr *msg, struct kiocb *iocb,
struct file *file, const struct iovec *iov,
unsigned long nr_segs)
{
//根据file结构体获取socket,这个赋值也是在socket系统调用流程中完成的
struct socket *sock = file->private_data;
size_t size = 0;
int i;
for (i = 0; i < nr_segs; i++)
size += iov[i].iov_len;
//构造msghdr消息头结构
msg->msg_name = NULL;
msg->msg_namelen = 0;
msg->msg_control = NULL;
msg->msg_controllen = 0;
msg->msg_iov = (struct iovec *)iov;//数据所在处
msg->msg_iovlen = nr_segs;
msg->msg_flags = (file->f_flags & O_NONBLOCK) ? MSG_DONTWAIT : 0;
if (sock->type == SOCK_SEQPACKET)
msg->msg_flags |= MSG_EOR;
//殊途同归,最后还是走入了__sock_sendmsg的怀抱
return __sock_sendmsg(iocb, sock, msg, size);
}
所以,最后write()也和sendto一样,进入__sock_sendmsg()函数。
最后再来看看writev系统调用。
writev()函数
函数原型
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
其中,
fd为网络文件描述符;
iov是用户数据存储位置,可包含多个数据块,指向struct iovec结构数组;
iovcnt为struct iovec结构数组长度,即数据块个数。
struct iovec结构体信息如下,
struct iovec
{
void __user *iov_base; /* 用户数据 */
__kernel_size_t iov_len; /* 用户数据长度 */
};
writev和sendmmg功能类似,都能通过一次系统调用发送多个数据。
内核实现
SYSCALL_DEFINE3(writev, unsigned long, fd, const struct iovec __user *, vec,
unsigned long, vlen)
{
//此处和write一样,通过文件描述符,获取对应文件结构
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);//获取文件偏移位置
//主要的处理函数
ret = vfs_writev(f.file, vec, vlen, &pos);
file_pos_write(f.file, pos);//设置文件写偏移位置
fdput_pos(f);
}
//更新写IO的统计信息
if (ret > 0)
add_wchar(current, ret);
inc_syscw(current);
return ret;
}
和write不一样的是,这里调用的是vfs_writev(),而write调用的是vfs_write,但是参数是一样的。
ssize_t vfs_writev(struct file *file, const struct iovec __user *vec,
unsigned long vlen, loff_t *pos)
{
//对文件权限做检查
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!file->f_op || (!file->f_op->aio_write && !file->f_op->write))
return -EINVAL;
//做真正的写动作
return do_readv_writev(WRITE, file, vec, vlen, pos);
}
简单检查后,进入do_readv_writev的流程。
static ssize_t do_readv_writev(int type, struct file *file,
const struct iovec __user * uvector,
unsigned long nr_segs, loff_t *pos)
{
size_t tot_len;
struct iovec iovstack[UIO_FASTIOV];
struct iovec *iov = iovstack;
ssize_t ret;
io_fn_t fn;
iov_fn_t fnv;
if (!file->f_op) {
ret = -EINVAL;
goto out;
}
//将用户态多个数据块都拷贝至内核,同样,数据块个数的上限是1024
ret = rw_copy_check_uvector(type, uvector, nr_segs,
ARRAY_SIZE(iovstack), iovstack, &iov);
if (ret <= 0)
goto out;
tot_len = ret;
//参数检查,对一些长度和偏移检验,以防溢出
ret = rw_verify_area(type, file, pos, tot_len);
if (ret < 0)
goto out;
fnv = NULL;
if (type == READ) {
//读操作
fn = file->f_op->read;
fnv = file->f_op->aio_read;
} else {
//写操作
//这里和write的流程一样,对于socket文件不会注册write函数,但是会注册aio_write函数
//因此对于socket流程,fn为空,fnv不为空,指向sock_aio_write()
fn = (io_fn_t)file->f_op->write;
fnv = file->f_op->aio_write;
file_start_write(file);
}
if (fnv)
//socket流程,fnv不为空
ret = do_sync_readv_writev(file, iov, nr_segs, tot_len, pos, fnv);
else
ret = do_loop_readv_writev(file, iov, nr_segs, pos, fn);
if (type != READ)
file_end_write(file);
out:
if (iov != iovstack)
kfree(iov);
if ((ret + (type == READ)) > 0) {
if (type == READ)
fsnotify_access(file);
else
fsnotify_modify(file);
}
return ret;
}
和write类似,这里也出现磁盘IO和socket IO的分叉点,socket IO进入do_sync_readv_writev()流程,
static ssize_t do_sync_readv_writev(struct file *filp, const struct iovec *iov,
unsigned long nr_segs, size_t len, loff_t *ppos, iov_fn_t fn)
{
struct kiocb kiocb;
ssize_t ret;
//构建IO请求
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
kiocb.ki_nbytes = len;
//fn指向的是sock_aio_write
//我们之前提过,sock_aio_write的第三个参数表示数据块数量,在write的流程里传入的是1
//在writev里,传入的则是nr_segs,表示可以有多个数据块
//这就实现了多个数据块通过一个系统调用发送的目的
ret = fn(&kiocb, iov, nr_segs, kiocb.ki_pos);
if (ret == -EIOCBQUEUED)
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;
return ret;
}
由此,write和writev系统调用最终在__sock_sendmsg()汇合。它们俩将在这等待着send,sendto、sendmsg、sendmmsg的到来,一起走向实现数据发送的康庄大道。