Listen功能简述
编写服务器程序时,在Linux中需要调用Listen系统调用,如下所示,Listen系统调用的主要功能就是根据传入的backlog参数创建连接队列,并将套接字的状态迁移至LISTEN状态,最后将监听sock注册到TCP全局的监听套接字哈希表。
int listen(int sockfd, int backlog);
Listen系统调用-函数执行流程
系统调用调用的函数执行如下所示:
SYSCALL_DEFINE2(listen, int, fd, int, backlog){ struct socket *sock; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; if ((unsigned int)backlog > somaxconn) backlog = somaxconn; err = security_socket_listen(sock, backlog); if (!err) err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed); } return err;}
其中sockfd_lookup_light函数根据fd描述符得到struct socket结构体,并找到当前系统设定的最大可监听连接数somaxconn ,PROC文件系统中somaxconn默认为128,意味着单个套接口队列的长度,可最大监听128个连接 ,如下所示:
# cat /proc/sys/net/core/somaxconn128
net_defaults_init_net函数初始化此值为宏SOMAXCONN(128):
static int __net_init net_defaults_init_net(struct net *net){ net->core.sysctl_somaxconn = SOMAXCONN; return 0;}
somaxconn与Listen系统调用传入的参数backlog进行比较,若当前传入的参数backlog大于somaxconn则使用somaxconn,即backlog最大值不能超过somaxconn。该系统调用核心是执行:sock->ops->listen(sock,backlog) ;也就是说找到服务器的socket后,通过它的协议操作表结构struct proto_ops执行其listen钩子函数,proto_ops协议操作表结构的挂入是在socket创建过程根据协议类型进行设置的,TCP实际挂入的是inet_stream_ops操作表结构,listen在inet_stream_ops表中的赋值如下所示:
const struct proto_ops inet_stream_ops = { ...... .listen = inet_listen, ......};
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
故sock->ops->listen针对于TCP而言,继续调用inet_listen函数:
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; if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM) goto out; old_state = sk->sk_state; //状态检查 if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN))) goto out; tcp_fastopen = 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, backlog); if (err) goto out; } sk->sk_max_ack_backlog = backlog; err = 0;out: release_sock(sk); return err;}
inet_listen函数首先是对套接字类型、状态进行检查,类型必须是流式套接字且状态必须是close或者listen状态:
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM) goto out; old_state = sk->sk_state; if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN))) goto out;
inet_listen函数核心的继续调用inet_csk_start_listen函数:
int inet_csk_listen_start(struct sock *sk, int backlog){ struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet = inet_sk(sk); int err = -EADDRINUSE; //分配以及初始化accept队列 reqsk_queue_alloc(&icsk->icsk_accept_queue); //设置accept队列的最大长度 sk->sk_max_ack_backlog = backlog; //初始化当前的sk_ack_backlog,即当前队列的计数 sk->sk_ack_backlog = 0; inet_csk_delack_init(sk); sk_state_store(sk, TCP_LISTEN); if (!sk->sk_prot->get_port(sk, inet->inet_num)) { inet->inet_sport = htons(inet->inet_num); sk_dst_reset(sk);、 //注册全局监听哈希表中 err = sk->sk_prot->hash(sk); if (likely(!err)) return 0; } sk->sk_state = TCP_CLOSE; return err;}
inet_csk_listen_start函数通过reqsk_queue_alloc创建连接队列,队列结构体如下,队列的最大长度是sk_max_ack_backlog,也就是用户传入的backlog参数值,队列的长度计数是sk_ack_backlog。
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. */};
其中request_sock结构体是请求队列的节点如下所示,*dl_next将所有的accept请求串起来。
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 cookie_ts: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; u32 *saved_syn; u32 secid; u32 peer_secid;};
struct request_sock_queue和struct request_sock的关系如下:
inet_csk_listen_start调用的分配并初始化连接队列的函数reqsk_queue_alloc如下所示,其中可以看到queue->rskq_accept_head初始化为NULL
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; queue->fastopenq.qlen = 0; queue->rskq_accept_head = NULL;}
inet_csk_listen_start函数中另一个核心内容就是调用哈希函数:
sk->sk_prot->hash(sk)将监听sock注册到TCP全局的监听套接字哈希表,对于TCP对应的协议栈,hash函数是inet_hash:
int inet_hash(struct sock *sk){ int err = 0; if (sk->sk_state != TCP_CLOSE) { local_bh_disable(); err = __inet_hash(sk, NULL); local_bh_enable(); } return err;}
继续调用__inet_hash:
int __inet_hash(struct sock *sk, struct sock *osk){ struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; struct inet_listen_hashbucket *ilb; int err = 0; if (sk->sk_state != TCP_LISTEN) { inet_ehash_nolisten(sk, osk); return 0; } WARN_ON(!sk_unhashed(sk)); ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)]; spin_lock(&ilb->lock); if (sk->sk_reuseport) { err = inet_reuseport_add_sock(sk, ilb); if (err) goto unlock; } if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport && sk->sk_family == AF_INET6) hlist_add_tail_rcu(&sk->sk_node, &ilb->head); else hlist_add_head_rcu(&sk->sk_node, &ilb->head); sock_set_flag(sk, SOCK_RCU_FREE); sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);unlock: spin_unlock(&ilb->lock); return err;}
关于:
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
通过图解,如下所示:
最终得到struct inet_hashinfo:
struct inet_hashinfo { struct inet_ehash_bucket *ehash; spinlock_t *ehash_locks; unsigned int ehash_mask; unsigned int ehash_locks_mask; struct inet_bind_hashbucket *bhash; unsigned int bhash_size; struct kmem_cache *bind_bucket_cachep; struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE] ____cacheline_aligned_in_smp;};
内核将监听队列分为32个哈希桶bucket:listening_hash[INET_LHTABLE_SIZE],保存处于监听状态的TCP套接口哈希链表,每个哈希桶由独立的保护锁和链表,此结构通过减小锁的粒度,增加并行处理的可能,每个哈希桶如下所示:
struct inet_listen_hashbucket { spinlock_t lock; struct hlist_head head;};
哈希桶的选择由函数inet_sk_listen_hashfn的返回值决定
struct inet_listen_hashbucket *ilb;ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
inet_sk_listen_hash函数使用数据包的目的端口号(本地监听端口号)计算的hash值为索引得到具体的哈希桶,如下所示:
static inline int inet_sk_listen_hashfn(const struct sock *sk){ return inet_lhashfn(sock_net(sk), inet_sk(sk)->inet_num);}
static inline u32 inet_lhashfn(const struct net *net, const unsigned short num){ return (num + net_hash_mix(net)) & (INET_LHTABLE_SIZE - 1);}
内核为处于LISTEN状态的socket分配了大小为32的哈希桶,监听的端口号经过哈希算法运算打散到这些哈希桶中(如果开启了IPV6,并且启用了端口重用,将此套接口添加在监听套接口桶的链表末尾;否则,添加到链表头部,如下代码所示)
if (sk->sk_reuseport) { err = inet_reuseport_add_sock(sk, ilb); if (err) goto unlock; } if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport && sk->sk_family == AF_INET6) hlist_add_tail_rcu(&sk->sk_node, &ilb->head); else hlist_add_head_rcu(&sk->sk_node, &ilb->head);
如下图所示,哈希链表的组织方式:
当收到客户端的 SYN 握手报文以后,会根据目标端口号的哈希值计算出哈希冲突链表,然后遍历这条哈希链表得到对应的socket。
原文地址:Linux内核角度分析服务器Listen细节 - Linux内核 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛
留言与评论(共有 0 条评论) “” |