本篇是Linux网络专题的开篇,这一系列将会自上而下梳理清楚,当我们调用应用层的接口发送报文时,操作系统里到底发生了什么事情。其参考文献放在前面:[译] Linux 网络栈监控和调优:发送数据(2017),有时间和精力的读者可以参考阅读。
本文将介绍应用层、BSD socket层和INET socket层的数据通路。
在阅读本文前,您应当对计算机网络的整体架构有一个基本的了解。可以先阅读Linux内核学习笔记的第八篇网络栈简介。
1. 万恶之源——套接字
1.1. 协议族初始化——以INET为例
内核启动的时候调用众多的初始化函数,其中包括inet_init函数,它定义于net/ipv4/af_inet.c,用于初始化INET协议族。该函数分为四大步:
第一步,注册TCP、UDP、RAW和PING协议。每种协议都是proto结构体;其实proto可以看作是一种接口,其中包括诸多数据和函数成员,各协议对象是对它的实现。在注册协议时,内核会申请一个高速缓存,例如TCP协议结构体会申请tcp_sock大小的高速缓存。之后,会申请一个index,用作标识,存放在结构体的inuse_idx域中。下面可以看到proto结构体定义的一部分。
/* Networking protocol blocks we attach to sockets.
* socket layer -> transport layer interface
*/
struct proto {
void (*close)(struct sock *sk,
long timeout);
int (*pre_connect)(struct sock *sk,
struct sockaddr *uaddr,
int addr_len);
int (*connect)(struct sock *sk,
struct sockaddr *uaddr,
int addr_len);
int (*disconnect)(struct sock *sk, int flags);
struct sock * (*accept)(struct sock *sk, int flags, int *err,
bool kern);
看一个更实际点的,这是TCP协议实例:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
第二步,在sock_register中注册INET协议族。我们看看这个函数的原型:
int sock_register(const struct net_proto_family *fam);
函数的主要工作是把fam代表的结构体加入到全局变量数组net_families中。读者应该能猜出来,net_proto_family也是一个接口。不错,INET协议族对它的实现是:
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
inet_create函数用来创建套接字,例如socket系统调用传入AF_INET作为参数的话,最终就会执行到这个函数。
第三步,向协议族注册协议。
/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
每个协议自然也是接口实例,定义在数组inetsw_array中。我们看看INET协议族的各协议:
/* The inetsw table contains everything that inet_create needs to
* build a new socket.
*/
static struct list_head inetsw[SOCK_MAX];
/* Upon startup we insert all the elements in inetsw_array[] into
* the linked list inetsw.
*/
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
/* and more... */
不错吧?我们看到了熟悉的SOCK_DGRAM。注册协议的时候,这个静态数组中的协议实例prot会被加到一个链表中去,例如我们前面提到的tcp_prot。ops域就有意思了,它是接口中的接口成员,包含了大量关于协议的信息,我们打开TCP的ops看看:
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
/* ... */
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
/* ... */
};
包含了收发数据的时候要用到的回调函数。
第四步,初始化各个协议。各个协议要初始化的东西都不一样,这里就不再赘述了。
1.2. 套接字的产生
一切故事的起源还是socket系统调用,它可以创建一个BSD套接字。
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
进入内核之后:
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
/* ... */
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
大概两件事情:创建套接字,然后映射到一个文件描述符上。
进入sock_create、__sock_create后,内核找出协议族对应实例,调用对应的套接字创建函数。
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
/* ... */
rcu_read_unlock();
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
对于AF_INET,相应的函数就是inet_create!从这里,我们进入INET socket层。该函数中,
对于传入的sock参数(它是一个通用BSD套接字socket),inet_create会找到合适的协议,然后把对应协议的回调函数赋给套接字:
struct inet_protosw *answer;
/* ... */
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
/* break after found */
}
/* ... */
sock->ops = answer->ops;
如果是TCP套接字,那么刚才提到的inet_stream_ops就会被赋上去。
接下来我们以这个将SOCK_STREAM为例,仔细分析一次发包的全流程。TCP协议本身我们不深究,会在另外一篇文章中做简要的探讨。
2. 在进入协议之前
2.1. 应用层
现在用这行代码发送报文:
send(sockfd, cp, remaining, 0);
但是没有send系统调用呀?我们来扒一扒glibc就知道了。glibc要处理的历史遗留问题实在太多,除去那些没有用到的宏,本机上其实就执行了这么一条语句:
ssize_t
__libc_send (int fd, const void *buf, size_t len, int flags)
{
return SYSCALL_CANCEL (sendto, fd, buf, len, flags, NULL, 0);
}
weak_alias (__libc_send, send)
原来调用的是sendto!这下不就明白了,sendto不指定发送地址,就是send。UDP的套接字还没connect,我们就需要指定向哪里发送;TCP当然不用了。
UDP套接字能不能connect?
如果强行调用connect会怎么样?不会怎么样,甚至还有一点好处。如果用connect绑定到了一个地址上,那么套接字就能收到异步错误,从而反馈给上层。不过缺点是,这个UDP套接字没法给多个进程同时发包了,所以类似DNS服务器的UDP套接字就肯定不能使用connect。
2.2. BSD socket层
在网络栈之上一文中,我们提到BSD socket层和INET socket层共同实现了表示层和会话层的内容。sendto系统调用如何进入内核和对应的函数这里就不说了。在__sys_sendto中,内核会找到对应套接字(即sock),将数据整理成一个方便底层处理的形式(即msg),然后调用:
err = sock_sendmsg(sock, &msg);
msg里放了很多东西:
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
该代码会将用户程序传入内核的地址转化为内核地址空间的地址,然后存入msg_name字段。
通过sock_sendmsg往下,数据去往更深处。在一系列安全检查后,数据来到sock_sendmsg_nosec函数:
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
inet_sendmsg, sock, msg,
msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}
INDIRECT_CALL_INET是一个非常神必的宏。它是宏INDIRECT_CALL_2的别名,作用很简单:调用sock->ops->sendmsg,但如果它正好指向inet6_sendmsg或inet_sendmsg,那么直接调用这两个函数,这样可以减少retpoline带来的开销。retpoline本身是有一点讲究的,它用于防止现代CPU预测执行导致的访问权限漏洞,之后如果有空的话我会写写计算机系统的东西,感觉也很有意思。
2.3. INET socket层
inet_sendmsg是INET(IPv4)中通用的报文发送函数,内容很简单,就是调用协议类型对应的发送方法。
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct sock *sk = sock->sk;
if (unlikely(inet_send_prepare(sk)))
return -EAGAIN;
return INDIRECT_CALL_2(sk->sk_prot->sendmsg, tcp_sendmsg, udp_sendmsg,
sk, msg, size);
}
从这里往下,就进入传输层了,我们在下篇L4/L3数据通路中继续讲解。