探究TCP连接的奥秘—系统调用篇

文摘   2024-10-13 11:22   陕西  

作者简介:王月妮,西安邮电大学研二在读,师从陈莉君教授、梁琛教授,Linux内核爱好者,目前在学习操作系统底层原理和内核编程。

当网络中存在延迟或失效时,旧的连接请求包(如 SYN 包)可能会滞留在网络中,延迟到达服务器。这时,客户端可能已经不再需要建立连接,但如果服务器收到滞留的 SYN 包并响应 ACK,就可能错误地创建一个新的连接,导致资源浪费。

为了解决这一问题,TCP 协议通过三次握手机制来建立可靠的连接状态。在三次握手中,客户端与服务器相互验证连接请求的有效性,确保请求是真实且最新的,并确认双方具备收发数据的能力。通过这一机制,TCP 有效避免了滞留的 SYN 包引发的误连接和资源浪费。

linux内核协议栈中三次握手关键源码函数关系图:

进行TCP三次握手的前置准备工作如下:

服务器端:套接字的bindlistenaccept函数,用于监听和处理连接请求。

客户端:通过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.cstruct 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.hstruct 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.cint 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_UNCONNECTEDSOCK_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.cint 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.hstruct 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.hstruct 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.hstruct 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.hstruct 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.cvoid 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.cint __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.cint __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.cint 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

至此系统调用篇幅工作结束,可以开启三次握手之旅啦~

参考资料
[1]

《深入理解Linux网络》



Linux内核之旅
Linux内核之旅
 最新文章