作者简介:王月妮,西安邮电大学研二在读,师从陈莉君教授、梁琛教授,Linux内核爱好者,目前在学习操作系统底层原理和内核编程。
当网络中存在延迟或失效时,旧的连接请求包(如 SYN 包)可能会滞留在网络中,延迟到达服务器。这时,客户端可能已经不再需要建立连接,但如果服务器收到滞留的 SYN 包并响应 ACK,就可能错误地创建一个新的连接,导致资源浪费。
为了解决这一问题,TCP 协议通过三次握手机制来建立可靠的连接状态。在三次握手中,客户端与服务器相互验证连接请求的有效性,确保请求是真实且最新的,并确认双方具备收发数据的能力。通过这一机制,TCP 有效避免了滞留的 SYN 包引发的误连接和资源浪费。
linux内核协议栈中三次握手关键源码函数关系图:
进行TCP三次握手的前置准备工作如下:
服务器端:套接字的bind、listen和accept函数,用于监听和处理连接请求。
客户端:通过connect函数发起连接请求,并等待服务器的响应。
本次源码基于Linux6.2版本进行分析socket系统调用部分。
socket系统调用篇
使用 socket() 函数创建一个套接字,传入的参数为 IPv4 地址族、套接字类型为 SOCK_STREAM,用于 TCP连接。相对的,SOCK_DGRAM 用于 UDP通信。
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
调用C库的socket函数,通过系统调用接口(syscall指令)进入内核:socket() 函数最终通过系统调用号,调用内核的 sys_socket,即通过软中断 int 0x80(32 位系统)或 syscall 指令(64 位系统)进入内核态。
内核根据系统调用号从 sys_call_table 中找到对应的函数指针(如 sys_socket)。一系列操作之后进入网络栈专用操作函数集的总入口函数SYSCALL_DEFINE2。
~/net/socket.c
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
......
switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = __sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
__sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
err =
__sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_SOCKETPAIR:
err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
break;
case SYS_SEND:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
NULL, 0);
break;
case SYS_SENDTO:
err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4], a[5]);
break;
case SYS_RECV:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
NULL, NULL);
break;
case SYS_RECVFROM:
err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
(struct sockaddr __user *)a[4],
(int __user *)a[5]);
break;
case SYS_SHUTDOWN:
err = __sys_shutdown(a0, a1);
break;
case SYS_SETSOCKOPT:
err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
a[4]);
break;
case SYS_GETSOCKOPT:
err =
__sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
(int __user *)a[4]);
break;
case SYS_SENDMSG:
err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_SENDMMSG:
err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
a[3], true);
break;
case SYS_RECVMSG:
err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
a[2], true);
break;
case SYS_RECVMMSG:
if (IS_ENABLED(CONFIG_64BIT))
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3],
(struct __kernel_timespec __user *)a[4],
NULL);
else
err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
a[2], a[3], NULL,
(struct old_timespec32 __user *)a[4]);
break;
case SYS_ACCEPT4:
err = __sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], a[3]);
break;
default:
err = -EINVAL;
break;
}
return err;
}
主要功能根据传入的call值进入SYS_SOCKET调用相应的套接字处理函数 __sys_socket 。
int __sys_socket(int family, int type, int protocol)
{
struct socket *sock; // 套接字对象的指针
int flags; // 套接字标志
// 创建套接字
sock = __sys_socket_create(family, type, protocol);
// 如果返回的指针表示错误,则返回错误代码
if (IS_ERR(sock))
return PTR_ERR(sock);
// 提取套接字类型中的 flags(如 O_NONBLOCK, O_CLOEXEC)
flags = type & ~SOCK_TYPE_MASK;
// 如果 SOCK_NONBLOCK 和 O_NONBLOCK 不等且套接字类型中包含 SOCK_NONBLOCK 标志,则进行处理
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
// 为套接字分配文件描述符并返回,传递标志位 O_CLOEXEC 和 O_NONBLOCK
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
调用__sys_socket_create函数根据family、type、protocol创建新的套接字,创建完成后,处理套接字的标志(如 O_NONBLOCK 和 O_CLOEXEC),并将套接字映射为文件描述符fd。
static struct socket *__sys_socket_create(int family, int type, int protocol)
{
struct socket *sock;
int retval; // 返回值,用于存储 `sock_create` 函数的结果
/* 检查 SOCK_* 常量的一致性 */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
// 检查 type 参数是否合法,确保没有设置非法的标志位
if ((type & ~SOCK_TYPE_MASK) & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return ERR_PTR(-EINVAL); // 如果标志无效,返回 -EINVAL 错误
// 去掉 type 中的附加标志位,保留实际的套接字类型
type &= SOCK_TYPE_MASK;
// 调用 sock_create 函数创建套接字
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return ERR_PTR(retval); // 如果创建失败,返回错误指针
return sock; // 返回创建成功的套接字指针
}
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
EXPORT_SYMBOL(sock_create);
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
// 检查协议族范围是否合法
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT; // 不支持的协议族
if (type < 0 || type >= SOCK_MAX)
return -EINVAL; // 无效的套接字类型
// 兼容性检查,处理旧的 PF_INET 和 SOCK_PACKET 组合
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET; // 将 family 修改为 PF_PACKET
}
// 进行安全检查,确保可以创建套接字
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
// 分配套接字
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; // 没有可用的套接字,返回错误
}
sock->type = type; // 设置套接字类型
#ifdef CONFIG_MODULES
// 如果协议族没有找到,尝试加载协议模块
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
#endif
// 获取协议族指针
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
// 尝试增加协议模块的引用计数,防止模块被卸载
if (!try_module_get(pf->owner))
goto out_release;
rcu_read_unlock();
// 调用协议族的创建函数
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
// 增加套接字操作模块的引用计数
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
// 释放协议模块的引用计数
module_put(pf->owner);
// 执行创建后的安全检查
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock; // 返回创建成功的套接字
return 0;
out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;
out_release:
rcu_read_unlock();
goto out_sock_release;
}
EXPORT_SYMBOL(__sock_create);
核心利用sock_alloc创建struct socket结构体,并设置type套接字类型,rcu_dereference获取对应的协议实体对象,net_families[]数组数组中获取fp,pf->create中调用对用协议create方法。
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
struct net_proto_family {
int family;
int (*create)(struct net *net, struct socket *sock,
int protocol, int kern);
struct module *owner;
};
static const struct net_proto_family inet_family_ops = {
.family = PF_INET, //IPv4 协议族
.create = inet_create, // IPv4 套接字的函数,指向 inet_create 函数
.owner = THIS_MODULE, // 负责该协议族的内核模块
};
net_proto_family是网络协议族的操作集合,在Linux内核网络子系统中,每个协议族(如 AF_INET,AF_INET6,AF_UNIX 等)都有一个 net_proto_family 结构体,所以实际的套接字创建是在inet_create函数中完成。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct inet_protosw *answer; // 用于存储匹配的协议族
struct inet_sock *inet; // inet socket 指针
struct proto *answer_prot; // 协议类型的指针
unsigned char answer_flags; // 协议标志
int try_loading_module = 0; // 尝试加载模块的次数
int err;
// 检查协议号是否合法
if (protocol < 0 || protocol >= IPPROTO_MAX)
return -EINVAL;
// 初始化套接字状态为未连接
sock->state = SS_UNCONNECTED;
// 查找与套接字类型和协议匹配的协议族
lookup_protocol:
err = -ESOCKTNOSUPPORT; // 不支持的 socket 类型
rcu_read_lock(); // 加锁,保护协议列表的读取
//根据socket传入的protocal在inetsw[]数组中查找对应的元素,获取对应协议类型的接口操作集信息,sock->type应用层传入的是SOCK_STREAM
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
// 检查协议是否匹配
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break; // 找到完全匹配的协议
} else {
// 如果当前协议是 IPPROTO_IP,值0
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT; // 不支持的协议
}
......
//将查找到的对应协议族的协议函数操作集赋值给创建的socket
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_flags = answer->flags;
rcu_read_unlock(); // 解锁
// 如果未成功调用协议的内存源,发出警告
WARN_ON(!answer_prot->slab);
// 分配套接子底层结构
err = -ENOMEM;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
if (!sk)
goto out;
// 设置套接子重用标志
err = 0;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = SK_CAN_REUSE;
inet = inet_sk(sk); // 获取 inet_sock 结构
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
inet->nodefrag = 0; // 禁止分片标志初始化
// 配置原始套接子的协议号
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1; // 如果协议是 IPPROTO_RAW,包含头部
}
// 设置路径 MTU 发现选项
if (READ_ONCE(net->ipv4.sysctl_ip_no_pmtu_disc))
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->inet_id = 0; // 初始化 ID
// 初始化套接子数据
sock_init_data(sock, sk);
sk->sk_destruct = inet_sock_destruct; // 设置析构函数
sk->sk_protocol = protocol; // 设置协议
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv; // 设置 backlog 接收函数
sk->sk_txrehash = READ_ONCE(net->core.sysctl_txrehash); // 设置重传响应时的响应过程
inet->uc_ttl = -1;
inet->mc_loop = 1;
inet->mc_ttl = 1;
inet->mc_all = 1;
inet->mc_index = 0;
inet->mc_list = NULL;
inet->rcv_tos = 0;
sk_refcnt_debug_inc(sk); // 增加套接子引用计数
// 如果协议允许用户在套接子创建时指定端口号
if (inet->inet_num) {
inet->inet_sport = htons(inet->inet_num); // 设置源端口号
// 添加到协议响应的响应链接
err = sk->sk_prot->hash(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
// 如果协议有初始化函数,根据不同协议类型调用相对应的4层协议init函数
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
// 如果不是内核创建的套接子,执行 BPF 程序
if (!kern) {
err = BPF_CGROUP_RUN_PROG_INET_SOCK(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
out:
return err;
out_rcu_unlock:
rcu_read_unlock(); // 解锁
goto out;
}
核心设置套接字状态SS_UNCONNECTED,遍历 inetsw 协议列表,根据传入的套接字类型和协议号查找合适的协议实现,为套接字分配内存(使用 sk_alloc),并对其进行初始化,设置特定的协议参数(如 TTL、PMTU、重用标志等)。
static struct list_head inetsw[SOCK_MAX];
static int __init inet_init(void)
{
......
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 是一个数组,其中每个元素是一个双向链表,用于存储不同套接字类型(如 SOCK_STREAM、SOCK_DGRAM)的协议族实现。inet_init 函数通过 INIT_LIST_HEAD 初始化这些链表头部。
通过 inet_register_protosw 注册协议族(如 TCP、UDP),使得内核能够根据套接字类型和协议号找到相应的协议实现。
void inet_register_protosw(struct inet_protosw *p)
{
struct list_head *lh;
struct inet_protosw *answer;
int protocol = p->protocol;
struct list_head *last_perm;
......
/* Add the new entry after the last permanent entry if any, so that
* the new entry does not override a permanent entry when matched with
* a wild-card protocol. But it is allowed to override any existing
* non-permanent entry. This means that when we remove this entry, the
* system automatically returns to the old behavior.
*/
list_add_rcu(&p->list, last_perm);
out:
spin_unlock_bh(&inetsw_lock);
return;
out_permanent:
pr_err("Attempt to override permanent protocol %d\n", protocol);
goto out;
out_illegal:
pr_err("Ignoring attempt to register invalid socket type %d\n",
p->type);
goto out;
}
inet_protosw 是 Linux 内核中的结构体,用来描述某种类型的协议实现(如 TCP、UDP、ICMP 等)。
协议族根据套接字类型(SOCK_STREAM, SOCK_DGRAM, SOCK_RAW)存储在 inetsw[] 链表数组中。
static struct inet_protosw inetsw_array[] =
{
//TCP
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
//UDP
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
//ICMP
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
//原始IP
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
数组 inetsw_array,用于存储各种协议族的实现及其相关操作。每个 inet_protosw 结构体表示一个特定协议的实现,并包含该协议的操作函数和标志。
struct socket 提供的是用户空间应用程序可以直接使用的高层接口,用于执行诸如 bind()、listen()、connect() 等系统调用。
struct sock 则包含了实际处理底层协议(如 TCP、UDP 等)所需的数据和逻辑。由于套接字的用户空间接口与内核的协议处理逻辑分离,所以需要将struct socket 和struct sock通过sock_init_data初始化时进行关联,而sock_inti_data是通过sk_alloc调用的。
void sock_init_data(struct socket *sock, struct sock *sk)
{
sk_init_common(sk);
sk->sk_send_head = NULL;
timer_setup(&sk->sk_timer, NULL, 0);
//初始化sock结构体
sk->sk_allocation = GFP_KERNEL;
sk->sk_rcvbuf = READ_ONCE(sysctl_rmem_default);
sk->sk_sndbuf = READ_ONCE(sysctl_wmem_default);
sk->sk_state = TCP_CLOSE;
sk->sk_use_task_frag = true;
//关联struct sock 和struct socket
sk_set_socket(sk, sock);
sock_set_flag(sk, SOCK_ZAPPED);
//如果 socket 存在,将套接字类型设置为 socket->type,并初始化 sk->sk_wq(等待队列)。同时将 sock 结构的 UID 设置为 socket 的用户 ID。如果 socket 不存在,则将 sk->sk_uid 设置为系统默认的 UID。
if (sock) {
sk->sk_type = sock->type;
RCU_INIT_POINTER(sk->sk_wq, &sock->wq);
sock->sk = sk;
sk->sk_uid = SOCK_INODE(sock)->i_uid;
} else {
RCU_INIT_POINTER(sk->sk_wq, NULL);
sk->sk_uid = make_kuid(sock_net(sk)->user_ns, 0);
}
rwlock_init(&sk->sk_callback_lock);
//设置套接字状态回调函数
if (sk->sk_kern_sock)
lockdep_set_class_and_name(
&sk->sk_callback_lock,
af_kern_callback_keys + sk->sk_family,
af_family_kern_clock_key_strings[sk->sk_family]);
else
lockdep_set_class_and_name(
&sk->sk_callback_lock,
af_callback_keys + sk->sk_family,
af_family_clock_key_strings[sk->sk_family]);
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
sk->sk_error_report = sock_def_error_report;
sk->sk_destruct = sock_def_destruct;
sk->sk_frag.page = NULL;
sk->sk_frag.offset = 0;
sk->sk_peek_off = -1;
sk->sk_peer_pid = NULL;
sk->sk_peer_cred = NULL;
spin_lock_init(&sk->sk_peer_lock);
sk->sk_write_pending = 0;
sk->sk_rcvlowat = 1;
sk->sk_rcvtimeo = MAX_SCHEDULE_TIMEOUT;
sk->sk_sndtimeo = MAX_SCHEDULE_TIMEOUT;
sk->sk_stamp = SK_DEFAULT_STAMP;
......
}
EXPORT_SYMBOL(sock_init_data);
static inline void sk_set_socket(struct sock *sk, struct socket *sock)
{
sk->sk_socket = sock;
}
使用内联函数sk_set_socket将 struct sock 和 struct socket 结构体关联起来,确保高层套接字接口和底层协议栈实现之间的协同工作。
struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
// 分配一个伪文件系统的 inode 节点,sock_mnt 是 sockfs 文件系统的挂载点
inode = new_inode_pseudo(sock_mnt->mnt_sb);
if (!inode) // 如果分配 inode 失败,返回 NULL
return NULL;
// 将 inode 转换为 socket 结构体
sock = SOCKET_I(inode);
// 初始化 inode 的属性
inode->i_ino = get_next_ino(); // 分配一个新的 inode 编号
inode->i_mode = S_IFSOCK | S_IRWXUGO; // 设置 inode 模式为 socket,并赋予读写执行权限
inode->i_uid = current_fsuid(); // 设置文件系统用户 ID 为当前用户
inode->i_gid = current_fsgid(); // 设置文件系统组 ID 为当前用户组
inode->i_op = &sockfs_inode_ops; // 设置 inode 的操作函数集为 sockfs 的 inode 操作
// 返回分配的 socket 结构体
return sock;
}
EXPORT_SYMBOL(sock_alloc);
调用 sock_alloc() 后,得到了一个初始化的 socket 对象,但它还没有与 file 对象或文件描述符(fd)关联。在 Linux 内核中,套接字通过 file 结构体与文件系统相关联。
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
struct file *file;
if (!dname)
dname = sock->sk ? sock->sk->sk_prot_creator->name : "";
// 调用 alloc_file_pseudo() 为套接字分配一个伪文件
// SOCK_INODE(sock) 是与该套接字关联的 inode,sock_mnt 是套接字文件系统的挂载点
// dname 是文件名,O_RDWR 代表读写模式,socket_file_ops 是套接字的文件操作集
file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
O_RDWR | (flags & O_NONBLOCK),
&socket_file_ops);
if (IS_ERR(file)) {
sock_release(sock);
return file;
}
//将socket和file关联
sock->file = file;
file->private_data = sock;
stream_open(SOCK_INODE(sock), file);
return file;
}
EXPORT_SYMBOL(sock_alloc_file);
套接字被包装成file,以便通过文件系统接口进行操作。file 结构体的 private_data 字段保存了与之关联的 socket 对象,在进行 I/O 操作时,文件系统可以访问套接字。
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
int fd = get_unused_fd_flags(flags); // 分配文件描述符
if (unlikely(fd < 0)) {
sock_release(sock); // 如果分配失败,释放套接字
return fd; // 返回错误码
}
// 将 socket 转换为 file 对象
newfile = sock_alloc_file(sock, flags, NULL);
if (!IS_ERR(newfile)) {
fd_install(fd, newfile); // 将 file 与 fd 关联
return fd; // 返回文件描述符
}
put_unused_fd(fd); // 如果分配失败,释放未使用的 fd
return PTR_ERR(newfile); // 返回错误码
}
sock_map_fd() 将 socket 转换为 file 对象后,为其分配一个文件描述符 fd,并将文件描述符与 file 对象关联。用户进程最终通过这个文件描述符访问和操作套接字。
总结部分:
1、用户调用 socket():用户通过调用 socket(domain, type, protocol) 请求内核创建一个套接字,参数包括协议族(如 AF_INET)、套接字类型(如 SOCK_STREAM)和协议号(如 IPPROTO_TCP)。
2、陷入内核:系统调用进入内核,传递的参数交给内核处理,调用 __sys_socket() 函数来处理套接字的创建。
3、分配 struct socket 对象:内核为套接字分配内存,创建一个 struct socket 对象,该对象是套接字的高层抽象,负责维护用户与套接字之间的接口。
4、调用协议族的创建函数:根据协议族(如 AF_INET 对应 IPv4),内核调用协议族的创建函数(如 inet_create()),并初始化套接字的底层协议(如 TCP 或 UDP)。
5、分配底层 struct sock 对象:内核为套接字分配底层 struct sock 结构,包含协议栈的具体实现。
6、分配文件描述符 fd:内核为套接字分配一个文件描述符(fd),用于用户进程操作套接字。
7、关联 socket 与 fd:内核将分配的 socket 对象与文件描述符 fd 关联起来,通过 fd,用户进程可以对这个套接字进行操作(如 send、recv、close 等)。
8、返回 fd 给用户进程:最后,内核返回文件描述符 fd,用户进程通过这个 fd 来使用套接字。
bind系统调用篇
成功创建socket后, 使用 bind将该套接字与一个 IP 地址和端口号绑定,使得套接字能够接收和发送通过该 IP 和端口的网络数据。
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
应用层调用bind将socket和服务器ip、port进行绑定,应用程序传入套接字描述符、IP 地址和端口号,调用 C 标准库中的 bind() 函数,通过系统调用机制(在 x86 系统中使用 socketcall),进入内核,socketcall 的参数为操作类型(如 SYS_BIND),进行绑定操作。内核根据call值进入 SYS_BIND 类型分支调用 __sys_bind 函数,处理绑定请求。
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
// 保存从用户空间传来的地址信息。
int err, fput_needed;
//定义返回错误码的变量和标志变量 fput_needed。
sock = sockfd_lookup_light(fd, &err, &fput_needed);
/* 查找与文件描述符 fd 关联的 socket,出错时返回错误码。*/
if (sock) {
/* 如果查找到有效的 socket:*/
err = move_addr_to_kernel(umyaddr, addrlen, &address);
/* 将用户空间的地址复制到内核空间。*/
if (!err) {
err = security_socket_bind(sock, (struct sockaddr *)&address, addrlen);
/* 进行安全检查,确保绑定操作合法。*/
if (!err)
err = sock->ops->bind(sock, (struct sockaddr *)&address, addrlen);
/* 如果安全检查通过,调用 socket 的 bind 操作,绑定地址。*/
}
fput_light(sock->file, fput_needed);
/* 释放文件引用,清理资源。*/
}
return err;
}
在 __sys_bind 函数中,调用 sock->ops->bind 实际上执行的是协议栈中定义的 bind 操作。这个操作取决于具体的协议类型,例如在 inetsw_array[] 中定义的不同协议集。对于 inet_stream_ops,bind 对应的实现函数是 inet_bind。
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
// 获取与套接字 sock 关联的内核层套接字结构体 sock
struct sock *sk = sock->sk;
// 初始化绑定标志位为 BIND_WITH_LOCK,用于控制绑定操作
u32 flags = BIND_WITH_LOCK;
int err;
//如果 sk->sk_prot->bind 存在(例如对于 RAW 套接字),就直接调用自定义的绑定函数。RAW 套接字是用于处理低级别的 IP 报文(比如 ICMP)
if (sk->sk_prot->bind) {
return sk->sk_prot->bind(sk, uaddr, addr_len);
}
// 传入的地址长度小于 sockaddr_in 结构体的大小
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
//BPF
err = BPF_CGROUP_RUN_PROG_INET_BIND_LOCK(sk, uaddr,
CGROUP_INET4_BIND, &flags);
if (err)
return err;
// 如果 BPF 程序成功通过,调用核心的 __inet_bind() 函数执行实际的绑定操作
return __inet_bind(sk, uaddr, addr_len, flags);
}
EXPORT_SYMBOL(inet_bind);
int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len, u32 flags)
{
// 将用户传入的地址转换为 IPv4 地址结构
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk); // 获取内核中的 inet_sock 结构,包含 IP 和端口信息
struct net *net = sock_net(sk); // 获取网络命名空间
unsigned short snum; // 存储端口号
int chk_addr_ret; // 地址类型检查结果
u32 tb_id = RT_TABLE_LOCAL; // 路由表 ID,默认是本地表
int err;
// 检查地址族是否为 IPv4 (AF_INET)
if (addr->sin_family != AF_INET) {
err = -EAFNOSUPPORT;
if (addr->sin_family != AF_UNSPEC || addr->sin_addr.s_addr != htonl(INADDR_ANY))
goto out;
}
// 通过绑定的设备获取路由表 ID(tb_id),如果存在绑定的设备,则查询相应表
tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
// 检查传入地址的类型(单播、多播、广播)
chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);
// 验证是否可以绑定到指定的地址,避免绑定无效或非法地址
err = -EADDRNOTAVAIL;
if (!inet_addr_valid_or_nonlocal(net, inet, addr->sin_addr.s_addr, chk_addr_ret))
goto out; // 地址不可用,返回错误
// 提取传入的端口号
snum = ntohs(addr->sin_port);
// 检查用户是否具有绑定特权,如果需要绑定特权且没有权限则返回 EACCES 错误
err = -EACCES;
if (!(flags & BIND_NO_CAP_NET_BIND_SERVICE) &&
snum && inet_port_requires_bind_service(net, snum) &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;
/* 接收地址用于哈希查找,发送地址用于传输。
* 对于广播/多播情况,接收地址与发送地址可以不同。
*/
if (flags & BIND_WITH_LOCK)
lock_sock(sk); // 如果设置了 BIND_WITH_LOCK 标志,则加锁防止并发操作
// 检查套接字状态,确保当前套接字处于关闭状态且未绑定到端口
err = -EINVAL;
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock; // 如果套接字已经处于打开状态,或已绑定端口,返回错误
// 设置接收地址和发送地址
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
// 对于多播或广播地址,发送地址设置为 0,表示使用设备的地址
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->inet_saddr = 0;
// 如果需要绑定到特定端口,调用协议栈的 get_port() 函数获取端口
if (snum || !(inet->bind_address_no_port || (flags & BIND_FORCE_ADDRESS_NO_PORT))) {
err = sk->sk_prot->get_port(sk, snum);
if (err) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
goto out_release_sock; // 获取端口失败,返回错误
}
// 运行 BPF 程序进行后处理(POST_BIND),检查 BPF 程序是否成功
if (!(flags & BIND_FROM_BPF)) {
err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
if (err) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
if (sk->sk_prot->put_port)
sk->sk_prot->put_port(sk); // 如果 BPF 检查失败,释放端口
goto out_release_sock;
}
}
}
// 如果绑定成功,设置相应的锁定标志
if (inet->inet_rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK; // 锁定地址
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK; // 锁定端口
// 设置本地端口号
inet->inet_sport = htons(inet->inet_num);
inet->inet_daddr = 0; // 清空目的地址
inet->inet_dport = 0; // 清空目的端口
sk_dst_reset(sk); // 重置套接字的路由缓存
err = 0; // 绑定成功
out_release_sock:
if (flags & BIND_WITH_LOCK)
release_sock(sk);
out:
return err;
}
__inet_bind 是用于 IPv4 绑定操作的内核实现,涵盖了从地址和端口的验证到实际绑定的整个过程。它处理了多种特殊情况,例如多播、广播地址的处理以及权限验证,并支持通过 BPF 进行安全策略管理。
绑定过程中,关键的一步是通过 sk->sk_prot->get_port 获取端口号,这个函数用于为套接字分配合适的端口。对于 TCP 套接字,sk->sk_prot->get_port 对应于 inet_csk_get_port。
//~/net/ipv4/tcp_ipv4.c
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,
.bpf_bypass_getsockopt = tcp_bpf_bypass_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,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.put_port = inet_put_port,
#ifdef CONFIG_BPF_SYSCALL
.psock_update_sk_prot = tcp_bpf_update_proto,
#endif
.enter_memory_pressure = tcp_enter_memory_pressure,
.leave_memory_pressure = tcp_leave_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.per_cpu_fw_alloc = &tcp_memory_per_cpu_fw_alloc,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
.sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_TYPESAFE_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = NULL,
.no_autobind = true,
.diag_destroy = tcp_abort,
};
EXPORT_SYMBOL(tcp_prot);
inet_csk_get_port 函数的主要功能是为 TCP 套接字分配或获取一个端口号,确保该端口号在系统中唯一且不会与其他套接字冲突。
通过使用一级和二级哈希表来管理端口绑定条目,支持自动分配可用端口号以及处理端口复用选项。该函数还负责检查端口冲突,并处理多设备绑定场景,确保在不同网络命名空间或设备上下文中正确绑定端口。如果未找到冲突,则创建绑定条目并完成端口分配。
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
//获取TCP/DCCP协议的端口哈希表
struct inet_hashinfo *hinfo = tcp_or_dccp_get_hashinfo(sk);
//标志控制是否允许端口复用,是否找到端口,是否检查端口冲突等
bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
bool found_port = false, check_bind_conflict = true;
bool bhash_created = false, bhash2_created = false;
//错误码端口冲突
int ret = -EADDRINUSE, port = snum, l3mdev;
// 用于查找绑定哈希桶
struct inet_bind_hashbucket *head, *head2;
struct inet_bind2_bucket *tb2 = NULL;
struct inet_bind_bucket *tb = NULL;
bool head2_lock_acquired = false;
struct net *net = sock_net(sk);
//获取绑定设备
l3mdev = inet_sk_bound_l3mdev(sk);
//未分配端口号
if (!port) {
//查找可用端口号
head = inet_csk_find_open_port(sk, &tb, &tb2, &head2, &port);
if (!head)
return ret;
//查找成功
head2_lock_acquired = true;
if (tb && tb2)
goto success;
found_port = true;
} else {
//inet_bhashfn() 是一个哈希函数,用来根据传入的 port 和网络命名空间 net 计算出该端口号在哈希表 hinfo->bhash 中的索引位置。它使用端口号进行哈希运算,并根据 hinfo->bhash_size 作为模数,确保计算出的索引值不会超出哈希表的大小
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
//依次遍历当前哈希桶 head 中的所有绑定条目,存储在链表 head->chain 中。每个绑定条目 tb(类型为 struct inet_bind_bucket)代表一个绑定到该端口号的套接字
inet_bind_bucket_for_each(tb, &head->chain)
//port是否绑定
if (inet_bind_bucket_match(tb, net, port, l3mdev))
break;
}
// 如果没有找到绑定的哈希桶,创建一个新的哈希桶
if (!tb) {
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net,
head, port, l3mdev);
if (!tb)
goto fail_unlock;
bhash_created = true;
}
if (!found_port) {
// 如果绑定桶的 owners 链表非空,表示该端口已有其他套接字绑定
if (!hlist_empty(&tb->owners)) {
// 检查端口复用的条件:
// 1. 如果套接字的复用标志为 SK_FORCE_REUSE,则强制允许端口复用。
// 2. 如果 fastreuse 大于 0 且当前套接字允许复用 (reuse),也允许复用。
// 3. 通过 sk_reuseport_match() 检查复用端口的套接字是否匹配(支持SO_REUSEPORT)。
if (sk->sk_reuse == SK_FORCE_REUSE ||
(tb->fastreuse > 0 && reuse) ||
sk_reuseport_match(tb, sk))
check_bind_conflict = false; // 满足条件时,不检查绑定冲突
}
// 如果需要检查绑定冲突并且开启了二级哈希桶机制(bhash2)
if (check_bind_conflict && inet_use_bhash2_on_bind(sk)) {
// 检查二级哈希表中是否有地址冲突。
if (inet_bhash2_addr_any_conflict(sk, port, l3mdev, true, true))
goto fail_unlock; // 如果存在地址冲突,则退出并解锁
}
// 计算二级哈希表中该端口的哈希桶位置,基于 IP 地址和端口号的组合。
head2 = inet_bhashfn_portaddr(hinfo, sk, net, port);
// 锁定二级哈希桶的锁,以确保接下来的操作是原子的,防止并发访问。
spin_lock(&head2->lock);
head2_lock_acquired = true;
// 在二级哈希桶中查找端口是否已被绑定。
tb2 = inet_bind2_bucket_find(head2, net, port, l3mdev, sk);
}
if (!tb2) {
// 如果没有找到二级绑定哈希桶(tb2 为 NULL),则创建新的二级绑定桶
tb2 = inet_bind2_bucket_create(hinfo->bind2_bucket_cachep,
net, head2, port, l3mdev, sk);
if (!tb2)
goto fail_unlock;
// 创建成功
bhash2_created = true;
}
if (!found_port && check_bind_conflict) {
// 如果没有找到可用端口,并且需要检查绑定冲突
// 调用 `inet_csk_bind_conflict` 函数,检查绑定桶 tb 和二级绑定桶 tb2 是否有冲突
if (inet_csk_bind_conflict(sk, tb, tb2, true, true))
goto fail_unlock;
}
......
EXPORT_SYMBOL_GPL(inet_csk_get_port);
listen系统调用篇
bind成功绑定后,使用 listen 使套接字进入监听模式,等待客户端连接。这时,套接字就从一个普通的套接字转换为一个监听套接字,准备接收连接请求。
listen(sockfd, backlog);
listen 在用户态调用时,实际会触发一次系统调用。用户态函数 listen 底层调用 syscall 指令,将 fd 和 backlog 作为参数传递到内核态。内核通过系统调用号定位到 __sys_listen,并执行由 SYSCALL_DEFINE2 定义的系统调用。负责对传入的文件描述符进行检查,并将套接字的状态设置为监听状态。
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed; //错误码,是否需要关闭文件
int somaxconn; //系统允许的最大等待连接数
//通过fd获取对应的socket结构
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
//获取somaxconn,somaxconn是由sysctl_somaxconn系统参数控制,如果用户指定的backlog超过了sysctl_somaxconn,将backlog限制在sysctl_somaxconn范围内
somaxconn = READ_ONCE(sock_net(sock->sk)->core.sysctl_somaxconn);
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
//安全检查,是否有足够权限执行listen
err = security_socket_listen(sock, backlog);
if (!err)
//调用协议栈注册的listen函数
err = sock->ops->listen(sock, backlog);
//释放资源
fput_light(sock->file, fput_needed);
}
return err;
}
在 Linux 内核中,每个套接字 struct socket)都有一个指向 sock->ops 的指针,ops 是一个 struct proto_ops 类型的结构,定义了与该套接字相关联的所有操作。比如,TCP 套接字使用的 proto_ops 操作表中包含了 TCP 协议相关的所有操作函数,包括 listen、accept、sendmsg、recvmsg 等。
//~/include/linux/net.h
struct proto_ops {
int family;
struct module *owner;
int (*release) (struct socket *sock);
int (*bind) (struct socket *sock,
struct sockaddr *myaddr,
int sockaddr_len);
int (*connect) (struct socket *sock,
struct sockaddr *vaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1,
struct socket *sock2);
int (*accept) (struct socket *sock,
struct socket *newsock, int flags, bool kern);
int (*getname) (struct socket *sock,
struct sockaddr *addr,
int peer);
__poll_t (*poll) (struct file *file, struct socket *sock,
struct poll_table_struct *wait);
......
}
创建一个 TCP 套接字时,套接字的 ops 字段会被初始化为指向 TCP 协议的操作集。在 Linux 内核中,这个操作集是由 inet_stream_ops(对于流式协议,即 TCP)来定义的。inet_stream_ops 是一个 struct proto_ops 类型的结构,它包含了 TCP 协议相关的所有操作函数。
//~/net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.gettstamp = sock_gettstamp,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
#ifdef CONFIG_MMU
.mmap = tcp_mmap,
#endif
.sendpage = inet_sendpage,
.splice_read = tcp_splice_read,
.read_sock = tcp_read_sock,
.read_skb = tcp_read_skb,
.sendmsg_locked = tcp_sendmsg_locked,
.sendpage_locked = tcp_sendpage_locked,
.peek_len = tcp_peek_len,
#ifdef CONFIG_COMPAT
.compat_ioctl = inet_compat_ioctl,
#endif
.set_rcvlowat = tcp_set_rcvlowat,
};
EXPORT_SYMBOL(inet_stream_ops);
sock->ops->listen指针指向inet_listen函数。
//~/net/ipv4/af_inet.c
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err, tcp_fastopen;
lock_sock(sk);
err = -EINVAL;
//检查socket的状态以及类型,SS_UNCONNECTED:未连接状态、SOCK_STREAM:TCP
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
/*
检查socket状态是否允许监听,将socket状态old_state左移操作转换成唯一的二进制掩码
例如:
1、如果 old_state == TCP_CLOSE,1 << TCP_CLOSE 生成一个只有第 TCP_CLOSE 位为 1 的二进制数
2、如果 old_state == TCP_LISTEN,1 << TCP_LISTEN 生成一个只有第 TCP_LISTEN 位为 1 的二进制数
*/
old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
//设置全连接队列长度
WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
/* Really, if the socket is already in listen state
* we can only allow the backlog to be adjusted.
/
//TFO功能
if (old_state != TCP_LISTEN) {
/* Enable TFO w/o requiring TCP_FASTOPEN socket option.
* Note that only TCP sockets (SOCK_STREAM) will reach here.
* Also fastopen backlog may already been set via the option
* because the socket was in TCP_LISTEN state previously but
* was shutdown() rather than close().
*/
tcp_fastopen = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_fastopen);
if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
(tcp_fastopen & TFO_SERVER_ENABLE) &&
!inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
fastopen_queue_tune(sk, backlog);
tcp_fastopen_init_key_once(sock_net(sk));
}
//启动监听
err = inet_csk_listen_start(sk);
if (err)
goto out;
//TCP_LISTEN 状态可以挂载的钩子函数
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
}
err = 0;
out:
release_sock(sk);
return err;
}
套接字类型为SS_UNCONNECTED、SOCK_STREAM,设置 sk->sk_max_ack_backlog = backlog,其中 backlog <= somaxconn,全连接队列长度执行listen函数的时候传入的是backlog和somaxconn之间较小的值。
为避免全连接队列溢出,可以在应用层通过 listen() 调整 backlog 值,或者系统层通过修改 somaxconn 提高系统允许的最大连接数。
当前state不是TCP_LISTEN 状态,默认启用Client设置fastopen 为 0x01,调用inet_csk_listen_start开始监听。
//~net/ipv4/inet_connection_sock.c
int inet_csk_listen_start(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk); //inet_connection_sock TCP特定连接信息的结构体
struct inet_sock *inet = inet_sk(sk); //inet_sock IPV4相关信息结构体
int err;
//检查sock是否可以进TCP_LISTEN状态
err = inet_ulp_can_listen(sk);
if (unlikely(err))
return err;
//接收队列分配内存并初始化
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_ack_backlog = 0;//连接数
inet_csk_delack_init(sk);//初始化延迟ACK机制
/* There is race window here: we announce ourselves listening,
* but this transition is still not validated by get_port().
* It is OK, because this socket enters to hash table only
* after validation is complete.
*/
inet_sk_state_store(sk, TCP_LISTEN);//设置sock状态为TCP_LISTEN
err = sk->sk_prot->get_port(sk, inet->inet_num);//为sock分配一个port
if (!err) {
inet->inet_sport = htons(inet->inet_num);//port转换成网络字节序
//重置sock相关路由缓存
sk_dst_reset(sk);
//sock插入哈希表,便于后续可以通过sock元组信息快速查找
err = sk->sk_prot->hash(sk);
if (likely(!err))
return 0;
}
//错误将sock状态置于TCP_CLOSE
inet_sk_set_state(sk, TCP_CLOSE);
return err;
}
为 icsk->icsk_accept_queue 分配请求队列,初始化 sk_ack_backlog 为 0,表示已经完成三次握手、进入 ESTABLISHED 状态,并等待 accept() 函数处理的连接数量。将套接字状态设置为 TCP_LISTEN,表示服务器正在监听传入的连接请求。
核心将sock转换成TCP_LISTEN状态,其中涉及inet_connection_sock、inet_sock、sock等。
//~include/net/inet_connection_sock.h
struct inet_connection_sock {
struct inet_sock icsk_inet; // 存储与IPv4相关的套接字信息,例如IP地址和端口号
struct request_sock_queue icsk_accept_queue; // 全连接队列,用于存储已完成三次握手的连接
struct inet_bind_bucket *icsk_bind_hash; // 绑定哈希桶,管理端口绑定的哈希表
struct inet_bind2_bucket *icsk_bind2_hash; // 第二个绑定哈希桶,用于多地址绑定时
unsigned long icsk_timeout; // 通用的超时时间
struct timer_list icsk_retransmit_timer;// 重传定时器,用于超时重传机制
struct timer_list icsk_delack_timer; // 延迟确认定时器,用于延迟发送ACK
__u32 icsk_rto; // 重传超时(RTO,Retransmission Timeout)
......
}
//~/include/net/inet_sock.h
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct sock sk; // 基础的通用套接字结构(struct sock),存储通用套接字信息
#if IS_ENABLED(CONFIG_IPV6)
struct ipv6_pinfo *pinet6; // 如果启用了 IPv6,这个指针指向 IPv6 特定的信息(struct ipv6_pinfo)
#endif
/* Socket demultiplex comparisons on incoming packets. */
#define inet_daddr sk.__sk_common.skc_daddr // 目的IP地址(从sk_common中提取)
#define inet_rcv_saddr sk.__sk_common.skc_rcv_saddr // 接收的本地IP地址
#define inet_dport sk.__sk_common.skc_dport // 目的端口号
#define inet_num sk.__sk_common.skc_num // 本地端口号
__be32 inet_saddr; // 源IP地址(big-endian格式)
__s16 uc_ttl; // 单播TTL(Time To Live),用于指定数据包的生存时间
__u16 cmsg_flags; // 控制消息标志
struct ip_options_rcu __rcu *inet_opt; // IP选项,例如源路由或时间戳
__be16 inet_sport; // 源端口号(big-endian格式)
__u16 inet_id; // IP标识符,用于区分不同的IP数据包
__u8 tos; // 服务类型(Type of Service),用于区分IP优先级
__u8 min_ttl; // 最小TTL值,用于限制数据包的生存时间
__u8 mc_ttl; // 多播TTL,用于多播数据包的生存时间
__u8 pmtudisc; // 路径MTU发现的状态,用于动态调整传输单元大小
......
}
//~/nclude/net/request_sock.h
struct request_sock_queue {
spinlock_t rskq_lock;
u8 rskq_defer_accept;
u32 synflood_warned;
atomic_t qlen;
atomic_t young;
//全连接队列
struct request_sock *rskq_accept_head;
struct request_sock *rskq_accept_tail;
struct fastopen_queue fastopenq; /* Check max_qlen != 0 to determine
* if TFO is enabled.
*/
};
//~/nclude/net/request_sock.h
struct request_sock {
struct sock_common __req_common;
#define rsk_refcnt __req_common.skc_refcnt
#define rsk_hash __req_common.skc_hash
#define rsk_listener __req_common.skc_listener
#define rsk_window_clamp __req_common.skc_window_clamp
#define rsk_rcv_wnd __req_common.skc_rcv_wnd
struct request_sock *dl_next;
u16 mss;
u8 num_retrans; /* number of retransmits */
u8 syncookie:1; /* syncookie: encode tcpopts in timestamp */
u8 num_timeout:7; /* number of timeouts */
u32 ts_recent;
struct timer_list rsk_timer;
const struct request_sock_ops *rsk_ops;
struct sock *sk;
struct saved_syn *saved_syn;
u32 secid;
u32 peer_secid;
u32 timeout;
};
全连接队列数据结构示意图:
在早期版本的Linux内核中,全连接队列和半连接队列的存储方式是分离的。具体来说:
半连接队列 以 哈希表 的形式存储,称为 syn_table。这个哈希表用于管理处于 SYN_RECV 状态的连接请求,即正在进行三次握手但尚未完成的连接。
全连接队列 则通过 链表 存储,用于管理那些已经完成三次握手的连接,等待应用程序调用 accept() 处理。全连接队列由 request_sock_queue 的 rskq_accept_head 和 rskq_accept_tail 指针管理。
然而,在高版本(如 Linux 6.x 版本)中,连接管理机制进行了整合。全连接和半连接队列都通过统一的 inet_connection_sock 和 request_sock_queue 结构来管理,虽然存储方式仍然有区别,但操作流程和调用的函数有所不同:
当有新的连接请求到达时,调用 inet_csk_reqsk_queue_hash_add() 函数,将 request_sock 添加到半连接队列中,等待三次握手的完成。
一旦三次握手完成,inet_csk_complete_hashdance() 函数会将该连接从半连接队列移至全连接队列,等待进一步处理。
调用reqsk_queue_alloc函数完成其初始化。
//~/net/core/request_sock.c
void reqsk_queue_alloc(struct request_sock_queue *queue)
{
spin_lock_init(&queue->rskq_lock);
spin_lock_init(&queue->fastopenq.lock);
queue->fastopenq.rskq_rst_head = NULL;
queue->fastopenq.rskq_rst_tail = NULL;
// 初始化 Fast Open 队列的请求计数为 0,表示没有挂起的连接请求
queue->fastopenq.qlen = 0;
// 初始化全连接队列的头指针为 NULL,表示全连接队列为空
queue->rskq_accept_head = NULL;
}
早期版本调用reqsk_queue_alloc进行一系列复杂的半连接队列和全连接队列长度的计算,在高版本中,并未进行复杂计算,由动态操作和原子变量进行管理。内核在处理每个连接请求时,动态调整队列长度,而不再在 reqsk_queue_alloc 这种初始化函数中完成这些任务。
对服务器进行压力测试,验证在 listen() 函数中,全连接队列长度取决于 backlog 和 somaxconn 中较小的值,并观察队列溢出。
somaxconn 是系统允许的全连接队列的最大值,可以通过以下命令查看当前的配置:
sysctl net.core.somaxconn
使用 Python 编写一个简单的 TCP 服务器,通过调用 listen() 函数设置 backlog 值,并模拟客户端连接。确保服务器监听 8080 端口。
import socket
# 创建TCP/IP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定套接字到地址和端口
server_address = ('0.0.0.0', 8080)
server_socket.bind(server_address)
server_socket.listen(1024)
print("Server is listening on port 8080")
# 无限循环,等待连接
while True:
client_socket, addr = server_socket.accept()
print(f"Accepted connection from {addr}")
# 发送简单响应
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 19\r\n"
"Connection: close\r\n"
"\r\n"
"Hello from server!\n"
)
# 发送HTTP响应
client_socket.sendall(response.encode('utf-8'))
client_socket.close()
wrk 工具来生成大量并发请求,高负载下测试服务器的连接处理能力。
wrk -t12 -c400 -d30s http://127.0.0.1:8080/
观察队列溢出情况以及 SYN cookies 的使用情况:
通过调整 somaxconn 的值来限制全连接队列的最大值,并观察队列溢出情况:
connect系统调用篇
connect() 函数通常由客户端发起,标志着 TCP 三次握手的开始。
服务端在收到 SYN 包后,会回复 SYN + ACK 并将该连接放入半连接队列,进入 SYN_RCVD 状态。随后,当服务端接收到客户端的 ACK 后,连接会从半连接队列移至全连接队列,此时该 socket 进入 ESTABLISHED 状态,连接已完成。
客户端调用 connect() 创建 socket 并进入系统调用,相关的 socket 系统调用函数位于 net/socket.c 目录下,调用逻辑和上述一致,根据用户态传入的文件描述符fd查询socket内核对象,实际调用函数__sys_connect。
//~/net/socket.c
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
int ret = -EBADF;
struct fd f;
//获取fd对应的struct file结构
f = fdget(fd);
if (f.file) {
//创建一个临时的 sockaddr_storage 结构来存储地址
struct sockaddr_storage address;
//将用户态的地址移动到内核态
ret = move_addr_to_kernel(uservaddr, addrlen, &address);
if (!ret)//地址改动
//connect连接
ret = __sys_connect_file(f.file, &address, addrlen, 0);
fdput(f);//释放fd
}
return ret;
}
//~/net/socket.c
int __sys_connect_file(struct file *file, struct sockaddr_storage *address,
int addrlen, int file_flags)
{
struct socket *sock;
int err;
// 从 file 结构中获取 socket 对象
sock = sock_from_file(file);
if (!sock) {
// 如果文件不是一个 socket,则返回错误 ENOTSOCK
err = -ENOTSOCK;
goto out;
}
// 检查当前 socket 连接的安全性,通过安全模块执行连接操作
err = security_socket_connect(sock, (struct sockaddr *)address, addrlen);
if (err)
goto out;
// 调用 socket 的具体操作函数 connect 来发起连接
// 连接的地址和文件的标志位作为参数传递
err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,
sock->file->f_flags | file_flags);
out:
return err;
}
对于TCP协议(AF_INET或 AF_INET6 协议族中的流式套接字 SOCK_STREAM),其 sock->ops->connect 字段指向 const struct proto_ops inet_stream_ops中的inet_stream_connect,处理TCP连接请求。
//~/net/ipv4/af_inet.c
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
int err;
lock_sock(sock->sk);
err = __inet_stream_connect(sock, uaddr, addr_len, flags, 0);
release_sock(sock->sk);
return err;
}
EXPORT_SYMBOL(inet_stream_connect);
锁定套接字冰调用底层__inet_stream_connect执行实际的TCP连接操作。
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
struct sock *sk = sock->sk;
int err;
long timeo;
/*
* uaddr can be NULL and addr_len can be 0 if:
sk is a TCP fastopen active socket and
* TCP_FASTOPEN_CONNECT sockopt is set and
we already have a valid cookie for this socket.
* In this case, user can call write() after connect().
write() will invoke tcp_sendmsg_fastopen() which calls
* __inet_stream_connect().
/
//检查传入的地址参数是否有效
if (uaddr) {
//检查传入的地址长度是否小于地址族字段
if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;
//检查地址簇是不是AF_UNSPEC,AF_UNSPEC表示用户请求断开
if (uaddr->sa_family == AF_UNSPEC) {
//执行断开连接操作
err = sk->sk_prot->disconnect(sk, flags);
//设置状态,释放成功:SS_DISCONNECTING,失败:SS_UNCONNECTED
sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
goto out;
}
}
switch (sock->state) {
default:
err = -EINVAL;
goto out;
//已连接
case SS_CONNECTED:
err = -EISCONN;
goto out;
//正在连接
case SS_CONNECTING:
if (inet_sk(sk)->defer_connect)
//EINPROGRESS:连接请求已经发起,但尚未完成,等待数据发送,EISCONN:socket 已经在连接过程中或已经连接
err = is_sendmsg ? -EINPROGRESS : -EISCONN;
else
err = -EALREADY;
/* Fall out of switch with err, set for this state */
break;
//未连接
case SS_UNCONNECTED:
err = -EISCONN;
//socket内部状态不是TCP_CLOSE,说明socket并未完全关闭
if (sk->sk_state != TCP_CLOSE)
goto out;
//是否开启BPF连接的钩子
if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) {
//调用connect
err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
if (err)
goto out;
}
//实际实行TCP连接的操作
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;
//更新socket状态
sock->state = SS_CONNECTING;
//没有错位并且defer_connect被设置,则连接操作被推迟
if (!err && inet_sk(sk)->defer_connect)
goto out;
/* Just entered SS_CONNECTING state; the only
* difference is that return value in non-blocking
e is EINPROGRESS, rather than EALREADY.
*/
err = -EINPROGRESS;
break;
}
//获取发送超时的时间,阻塞模式则返回0,否则返回timeo
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
//检查socket的TCP状态是否是TCPF_SYN_SENT:SYN请求已经发送,等待对方回应/TCPF_SYN_RECV:SYN请求已收到,等待对方确认
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
//TFO(允许客户端在连接建立时同时发送数据,以减少延迟)操作时,如果连接时已经有数据需要发送设置writebias:1,允许连接过程中同时发送数据
int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
tcp_sk(sk)->fastopen_req &&
tcp_sk(sk)->fastopen_req->data ? 1 : 0;
/* Error code is set above */
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
//等待过程中收到信号
err = sock_intr_errno(timeo);
//是否有挂起的信号
if (signal_pending(current))
goto out;
}
/* Connection was closed by RST, timeout, ICMP error
* or another process disconnected us.
/
//等待连接过程中,socket 的状态变成 TCP_CLOSE(连接被关闭,如通过 RST、超时或 ICMP 错误)
if (sk->sk_state == TCP_CLOSE)
goto sock_error;
/* sk->sk_err may be not zero now, if RECVERR was ordered by user
* and error was received after socket entered established state.
Hence, it is handled normally after connect() return successfully.
*/
//更新状态为连接已建立
sock->state = SS_CONNECTED;
err = 0;
out:
return err;
sock_error:
err = sock_error(sk) ? : -ECONNABORTED;
sock->state = SS_UNCONNECTED;
if (sk->sk_prot->disconnect(sk, flags))
sock->state = SS_DISCONNECTING;
goto out;
}
EXPORT_SYMBOL(__inet_stream_connect);
根据socket的状态进入不同的分支,socket创建后尚未与远程主机连接,状态为SS_UNCONNECTED,在此状态下,socket 尚未参与任何数据传输或通信,调用sk->sk_prot->connect发起连接,sk->sk_prot 是指向协议操作的指针,而 connect 是协议特定的连接操作函数。
每个协议栈(如 TCP、UDP)都有自己的 proto_ops 结构,它包含了所有与该协议相关的操作。对于IPV4来说,调用的函数是tcp_v4_connect,建立连接后更新状态SS_CONNECTED。
至此系统调用篇幅工作结束,可以开启三次握手之旅啦~
《深入理解Linux网络》