上面的文章已经分析了tcp建立的整个过程,下面我们来看下write是如何实现tcp写的。
// fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
...
if (f.file) {
...
ret = vfs_write(f.file, buf, count, &pos);
...
}
return ret;
}
该方法先通过fd找到struct file,再调用vfs_write继续执行write逻辑。
// fs/read_write.c
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
...
if (!ret) {
...
ret = __vfs_write(file, buf, count, pos);
...
}
return ret;
}
EXPORT_SYMBOL_GPL(vfs_write);
该方法又调用了__vfs_write方法。
// fs/read_write.c
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
由第一篇文章我们可以知道,file->f_op的值为&socket_file_ops。
// net/socket.c
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.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,
};
由上可见,socket_file_ops里并没有write方法,只有write_iter方法,所以上面的__vfs_write方法最终会调用new_sync_write方法。
// fs/read_write.c
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
...
iov_iter_init(&iter, WRITE, &iov, 1, len);
ret = call_write_iter(filp, &kiocb, &iter);
...
return ret;
}
该方法的各种初始化最终使得,kiocb持有filp,即我们要写入的文件,iter持有iov,iov又持有buf和len,即我们要写入的数据。
之后,该方法又调用了call_write_iter方法,传入上面初始化好的新参数。
// include/linux/fs.h
static inline ssize_t call_write_iter(struct file *file, struct kiocb *kio,
struct iov_iter *iter)
{
return file->f_op->write_iter(kio, iter);
}
该方法又调用了file->f_op->write_iter指向的方法,由上面的socket_file_ops变量我们可以知道,这个方法就是sock_write_iter。
// net/socket.c
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {.msg_iter = *from,
.msg_iocb = iocb};
...
if (file->f_flags & O_NONBLOCK)
msg.msg_flags = MSG_DONTWAIT;
...
res = sock_sendmsg(sock, &msg);
...
return res;
}
该方法又把参数iocb和from包装成了一个类型为struct msghdr的变量msg,之后又调用sock_sendmsg方法,传入这个新变量。
// net/socket.c
int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{
...
return err ?: sock_sendmsg_nosec(sock, msg);
}
EXPORT_SYMBOL(sock_sendmsg);
该方法又调用了sock_sendmsg_nosec方法。
// net/socket.c
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
...
return ret;
}
该方法又调用了sock_sendmsg_nosec方法。
该方法又调用了sock->ops->sendmsg指向的方法,由第一篇文章我们可以知道,这个方法是inet_sendmsg。
调用这个方法的第三个参数为方法msg_data_left的返回值,该值为我们最开始调用write时,传入的要写的数据长度。
// net/ipv4/af_inet.c
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
...
return sk->sk_prot->sendmsg(sk, msg, size);
}
EXPORT_SYMBOL(inet_sendmsg);
该方法又调用了sk->sk_prot->sendmsg指向的方法,由第一篇文章我们可以知道,这个方法是tcp_sendmsg。
// net/ipv4/tcp.c
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
...
ret = tcp_sendmsg_locked(sk, msg, size);
...
return ret;
}
EXPORT_SYMBOL(tcp_sendmsg);
该方法又调用了tcp_sendmsg_locked方法。
// net/ipv4/tcp.c
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);
...
struct sk_buff *skb;
...
int flags, err, copied = 0;
int mss_now = 0, size_goal, copied_syn = 0;
...
/* Ok commence sending. */
copied = 0;
restart:
mss_now = tcp_send_mss(sk, &size_goal, flags);
err = -EPIPE;
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto do_error;
...
while (msg_data_left(msg)) {
int copy = 0;
int max = size_goal;
skb = tcp_write_queue_tail(sk);
if (skb) {
...
copy = max - skb->len;
}
if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
...
skb = sk_stream_alloc_skb(sk,
select_size(sk, sg, first_skb),
sk->sk_allocation,
first_skb);
...
copy = size_goal;
max = size_goal;
...
}
/* Try to append data to the end of skb. */
if (copy > msg_data_left(msg))
copy = msg_data_left(msg);
/* Where to copy to? */
if (skb_availroom(skb) > 0) {
/* We have some space in skb head. Superb! */
copy = min_t(int, copy, skb_availroom(skb));
err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
...
} else if (!uarg || !uarg->zerocopy) {
...
} else {
...
}
...
copied += copy;
if (!msg_data_left(msg)) {
...
goto out;
}
...
continue;
...
}
out:
if (copied) {
...
tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
}
...
return copied + copied_syn;
do_fault:
...
do_error:
if (copied + copied_syn)
goto out;
out_err:
...
err = sk_stream_error(sk, flags, err);
...
return err;
}
EXPORT_SYMBOL_GPL(tcp_sendmsg_locked);
终于到了实现真正写逻辑的方法,我们来看下这个方法。
方法描述
1. 设置copied为0,该变量用于记录我们写成功的字节数。
2. 查看当前的mss值,即tcp包最大可带多少数据,并赋值给mss_now。
3. 检查当前socket是否有错误,或当前socket是否已SEND_SHUTDOWN,如果是则跳转到do_error逻辑。
do_error的逻辑大体上为,如果当前写成功的字节数大于0,则正常返回当前写成功的字节数,如果等于0,则调用sk_stream_error方法,获取当前应该返回给用户的错误码并赋值给err,最后返回err。
4. 进入while循环,循环继续的条件为当前剩余要写的字节数大于0。
5. 设置copy变量的值为0,该变量用于表示这次while循环可拷贝的字节数。
6. 设置max值为size_goal,size_goal变量的值是由上面tcp_send_mss方法中获取的,用于表示一个struct sk_buff最多可放多少数据,以字节表示。
7. 调用tcp_write_queue_tail方法,从sk->sk_write_queue队列尾部拿出一个struct sk_buff实例,并赋值给skb变量。
8. 如果skb不为null,则看该skb还剩余多大的空间可写,把该值赋值给copy变量。
9. 如果skb为null,或者skb没有可写空间了,此时copy为0,则调用sk_stream_alloc_skb方法,创建一个新的struct sk_buff实并赋值给skb,同时将copy和max的值都设置为size_goal。
10. 判断copy是否大于msg中剩余要写字节数,如果是,则修正copy的值。
11. 调用skb_availroom方法,查看skb是否有可写空间,如果有的话,先根据可写空间大小修正copy的值,再调用skb_add_data_nocache方法,将msg中的数据拷贝到skb中。
12. 如果skb没有可写空间,则将数据拷贝到skb间接指向的空间内,具体介绍略。
13. 将这次while循环成功拷贝的字节数累加到copied变量中。
14. 判断msg中是否还有要写的数据,如果有,则继续while循环,如果没有,则跳出while循环,进入到out标签指向的逻辑。
15. 如果while循环拷贝的字节数大于0,则调用tcp_push,将数据发送出去。
16. 最后返回整个方法成功写的字节数。
完。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!