- Today
- Total
Byeo
listen system call 3 (inet_hash) 본문
이전 포스트: https://byeo.tistory.com/entry/listen-system-call-2-inetlisten
sk->sk_prot->hash(sk)는 tcp_prot 구조체에 선언되어 있고, inet_hash 함수포인터를 값으로 갖고 있습니다.
4. inet_hash
// net/ipv4/inet_hashtables.c/inet_hash() :668
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;
}
EXPORT_SYMBOL_GPL(inet_hash);
inet_hash는 sk_state가 TCP_CLOSE가 아닐 때만 유효합니다. TCP_CLOSE가 아니라면 local bottom half irq를 disable하고 __inet_hash를 호출합니다.
현재 상태는 TCP_LISTEN이므로 __inet_hash를 호출하게됩니다.
5. __inet_hash
// net/ipv4/inet_hashtables.c/__inet_hash() :633
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, NULL);
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)
__sk_nulls_add_node_tail_rcu(sk, &ilb->nulls_head);
else
__sk_nulls_add_node_rcu(sk, &ilb->nulls_head);
inet_hash2(hashinfo, sk);
ilb->count++;
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;
}
EXPORT_SYMBOL(__inet_hash);
osk는 NULL입니다.
5-1) inet_listen_hashbucket 얻어오기
// net/ipv4/inet_hashtables.c/__inet_hash() :633
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, NULL);
return 0;
}
WARN_ON(!sk_unhashed(sk));
ilb = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)];
먼저, sk->sk_state != TCP_LISTEN if문은 현재 상태에서는 false이므로 건너뛰겠습니다.
bind과정에서 봤던 로직과 유사합니다. 그 때에는 tcp_prot의 inet_hashinfo안에 있는 inet_bind_hashbucket *bhash의 hlist_head chain을 통해서 tb를 얻어왔다면, 이번에는 inet_listen_hashbucket *listening_hash를 사용합니다.
// include/net/inet_hashtables.h:124
struct inet_hashinfo {
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE]
____cacheline_aligned_in_smp;
INET_LTABLE_SIZE는 상수 32입니다.
inet_listen_hashbucket 구조체는 다음과 같습니다.
// include/net/inet_hashtables.h :106
/* Sockets can be hashed in established or listening table.
* We must use different 'nulls' end-of-chain value for all hash buckets :
* A socket might transition from ESTABLISH to LISTEN state without
* RCU grace period. A lookup in ehash table needs to handle this case.
*/
#define LISTENING_NULLS_BASE (1U << 29)
struct inet_listen_hashbucket {
spinlock_t lock;
unsigned int count;
union {
struct hlist_head head;
struct hlist_nulls_head nulls_head;
};
};
ilb는 어느 hash bucket을 사용할지 그 포인터를 보관합니다. 이를 얻기 위해서 inet_sk_listen_hashfn(sk)함수를 이용해서 index 값을 얻어옵니다. 당연컨대, index는 INET_LTABLE_SIZE (32)를 넘을 수 없습니다.
5-2) SO_REUSEPORT 처리
// net/ipv4/inet_hashtables.c/__inet_hash() :646
spin_lock(&ilb->lock);
if (sk->sk_reuseport) {
err = inet_reuseport_add_sock(sk, ilb);
if (err)
goto unlock;
}
spin_lock을 획득합니다. sk->sk_reuseport는 현재 가정에서 SO_REUSEPORT setsockopt을 하지 않았으므로 걸리지 않습니다. inet_reuseport_add_sock은 주소가 일치하는 두 개의 socket이 존재한다면, 현재 sk을 기존에 존재하던 sk의 reuseport group에 추가하는 함수로 보입니다. (reuseport logic도 생각보다 복잡해서 나중에 기회가 된다면 다루는 것으로...)
5-5) _sk_nulls_add_node_rcu
// net/ipv4/inet_hashtables.c/__inet_hash() :652
if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport &&
sk->sk_family == AF_INET6)
__sk_nulls_add_node_tail_rcu(sk, &ilb->nulls_head);
else
__sk_nulls_add_node_rcu(sk, &ilb->nulls_head);
inet_hash2(hashinfo, sk);
ilb->count++;
sock_set_flag(sk, SOCK_RCU_FREE);
sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
unlock:
spin_unlock(&ilb->lock);
sk->sk_family가 AF_INET이므로 else branch에 있는 __sk_nulls_add_node_rcu를 호출하게 됩니다.
// include/net/sock.h/__sk_nulls_add_node_rcu() :777
static inline void __sk_nulls_add_node_rcu(struct sock *sk, struct hlist_nulls_head *list)
{
hlist_nulls_add_head_rcu(&sk->sk_nulls_node, list);
}
해당 함수는 inet_listen_bucket중에 존재하는 hlist_nulls_head type의 nulls_head에 sk를 집어넣는 작업을 수행합니다.
5-6) inet_hash2
지금까지는 inet_hashinfo 구조체에서 listening_hash에 작업을 해왔다면, 이제는 inet_hashinfo 구조체에 있는 lhash2에 작업을 합니다. (같은 listen 과정에서 이렇게 2단계 (listening_hash, lhash2)로 나눠놓은 이유는 아직 모르겠습니다.)
// net/ipv4/inet_hashtables.c/inet_hash2() :196
static void inet_hash2(struct inet_hashinfo *h, struct sock *sk)
{
struct inet_listen_hashbucket *ilb2;
if (!h->lhash2)
return;
ilb2 = inet_lhash2_bucket_sk(h, sk);
spin_lock(&ilb2->lock);
if (sk->sk_reuseport && sk->sk_family == AF_INET6)
hlist_add_tail_rcu(&inet_csk(sk)->icsk_listen_portaddr_node,
&ilb2->head);
else
hlist_add_head_rcu(&inet_csk(sk)->icsk_listen_portaddr_node,
&ilb2->head);
ilb2->count++;
spin_unlock(&ilb2->lock);
}
inet_lhash2_bucket_sk는 lhash2 bucket을 얻어오기 위한 함수입니다.
// net/ipv4/inet_hashtables.c/inet_lhash2_bucket_sk() :178
static struct inet_listen_hashbucket *
inet_lhash2_bucket_sk(struct inet_hashinfo *h, struct sock *sk)
{
u32 hash;
#if IS_ENABLED(CONFIG_IPV6)
if (sk->sk_family == AF_INET6)
hash = ipv6_portaddr_hash(sock_net(sk),
&sk->sk_v6_rcv_saddr,
inet_sk(sk)->inet_num);
else
#endif
hash = ipv4_portaddr_hash(sock_net(sk),
inet_sk(sk)->inet_rcv_saddr,
inet_sk(sk)->inet_num);
return inet_lhash2_bucket(h, hash);
}
ipv4_portaddr_hash는 address와 port를 기반으로 hash 값을 연산합니다. 그리고 inet_lhash2_bucket은 그 연산된 hash값을 사용하여 bucket에 대한 pointer를 반환하죠.
// include/net/inet_hashtables.h/inet_lhash2_bucket() :172
static inline struct inet_listen_hashbucket *
inet_lhash2_bucket(struct inet_hashinfo *h, u32 hash)
{
return &h->lhash2[hash & h->lhash2_mask];
}
그 다음은 이어서 마찬가지로 hlist_add_head_rcu 함수를 통해서 ilb2->head에 icsk_listen_portaddr_node를 추가합니다.
hlist_add_head_rcu(&inet_csk(sk)->icsk_listen_portaddr_node,
&ilb2->head);
ilb2->count++;
그리고 ilb2 bucket의 count를 추가합니다.
아직 Kernel API에 모르는 것이 많아서 이 행위의 목적을 파악하지 못했습니다. hlist_node가 struct sock* sk내에 여러 종류로 존재하고, 이를 hlist_head에 붙이는 작업을 bind의 bhash, listen의 listening_socket, lhash에서 수행하고 있는데, 어떤 영향을 주는걸까요?
inet_hash2는 이렇게 종료됩니다.
5-7) __inet_hash로 돌아와서..
ilb->count++;
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;
}
이제 남은 코드는 위와 같습니다. listening_hashbucket의 count 개수를 1 증가시켜줍니다.
그리고 sk의 flag에 SOCK_RCU_FREE flag를 추가합니다. 의미는 다음과 같습니다.
// include/net/sock.h:873
SOCK_RCU_FREE, /* wait rcu grace period in sk_destruct() */
sock_rot_inuse_add 함수는 다음과 같습니다.
// net/core/sock.c/sock_prot_inuse_add() :3460
void sock_prot_inuse_add(struct net *net, struct proto *prot, int val)
{
__this_cpu_add(net->core.prot_inuse->val[prot->inuse_idx], val);
}
EXPORT_SYMBOL_GPL(sock_prot_inuse_add);
namespace에서 해당 protocol에 대해 얼마나 많은 socket이 사용중인지 기록하는 것으로 보이는데요, val은 1로 1만큼 증가하게 됩니다. inuse 값은 다음 proc fs에서 찾아볼 수 있습니다. (참고)
이렇게 __inet_hash가 끝나게 됩니다. 차례로 inet_hash도 끝나게 되고, inet_csk_listen_start도 곧바로 return 0을 하게 됩니다.
그러면 inet_listen으로 돌아오게 되고, tcp_call_bpf가 그 다음으로 실행되는 함수가 되겠습니다.
// net/ipv4/af_inet.c/inet_listen() :231
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
tcp_call_bpf는 BPF와 관련된 hook으로서 bpf program을 실행합니다. 단, 현재는 bpf는 out of scope로 두고 넘어가도록 하겠습니다! 나중에 언젠가 다루게 될 것 같습니다.
이러면 inet_listen도 종료됩니다.
6. __sys_listen
sys_listen에서는 sys_bind에서 봤던 것과 마찬가지로, sockfd에 대한 refcnt를 감소시키는 fput_light를 마지막으로 실행합니다.
최종 흐름도
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
accept system call 1 (__sys_accept4) (0) | 2024.05.14 |
---|---|
accept system call 0 - Intro (0) | 2024.05.14 |
listen system call 2 (inet_listen) (0) | 2024.05.11 |
listen system call 1 (__sys_listen) (0) | 2024.05.11 |
bind system call 3 (__inet_bind - 2) (0) | 2024.05.01 |