Byeo

listen system call 3 (inet_hash) 본문

프로그래밍 (Programming)/네트워크 스택

listen system call 3 (inet_hash)

BKlee 2024. 5. 12. 18:30
반응형

이전 포스트: https://byeo.tistory.com/entry/listen-system-call-2-inetlisten

 

listen system call 2 (inet_listen)

이전 포스트: https://byeo.tistory.com/entry/listen-system-call-1-syslisten listen system call 1 (__sys_listen)BSD socket API에서 server쪽은 bind를 실행한 뒤, listen을 시작합니다.   Application이 listen을 시작하면  이제 받

byeo.tistory.com

 

 

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를 마지막으로 실행합니다.

 

최종 흐름도

 

 

반응형
Comments