【Linux】网络专题(一)——应用层与套接字层数据通路

本篇是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_protops域就有意思了,它是接口中的接口成员,包含了大量关于协议的信息,我们打开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_sendmsginet_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数据通路中继续讲解。

参考资料

What is a retpoline and how does it work? – Stack Overflow

发表评论

您的电子邮箱地址不会被公开。