Linux socket 数据发送类函数实现(二)

注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4

上回我们分析了send的几个类似函数的实现,接下来我们来探讨一下write家族的数据发送实现,主要有两个函数,writewritev

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

由此,writewritev系统调用最终在__sock_sendmsg()汇合。它们俩将在这等待着send,sendto、sendmsg、sendmmsg的到来,一起走向实现数据发送的康庄大道。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值