- Today
- Total
Byeo
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을 시작하면 이제 받을 수 있게 됩니다. 이 때, listen은 client가 요청한 연결들을 보관하는 함수라고 볼 수 있을
byeo.tistory.com
sock->ops->listen 함수는 inet_listen입니다.
현재까지 흐름도
2. inet_listen
// net/ipv4/af_inet.c/inet_listen() :193
/*
* Move a socket into listening state.
*/
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;
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.
*/
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 = 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;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
}
err = 0;
out:
release_sock(sk);
return err;
}
EXPORT_SYMBOL(inet_listen);
err = -EINVAL;
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
inet_listen은 socket의 상태가 SS_UNCONNECTED고 socket의 type이 SOCK_STREAM (TCP)인 경우만 지원합니다. 그렇지 않으면 -EINVAL을 return 합니다.
old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
sk->sk_state는 sock_init_data에서 TCP_CLOSE로 초기화된 뒤 건드려진 적이 없습니다. 이를 TCP_CLOSE의 값은 7인데요, (1 << old_state)는 결국 1<<7 이 됩니다. 따라서 if에 걸리지 않게 됩니다.
TCPF_CLOSE의 값은 1<<7입니다.
// include/net/tcp_stats.h :12
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING, /* Now a valid state */
TCP_NEW_SYN_RECV,
TCP_MAX_STATES /* Leave at the end! */
};
#define TCP_STATE_MASK 0xF
#define TCP_ACTION_FIN (1 << TCP_CLOSE)
enum {
TCPF_ESTABLISHED = (1 << TCP_ESTABLISHED),
TCPF_SYN_SENT = (1 << TCP_SYN_SENT),
TCPF_SYN_RECV = (1 << TCP_SYN_RECV),
TCPF_FIN_WAIT1 = (1 << TCP_FIN_WAIT1),
TCPF_FIN_WAIT2 = (1 << TCP_FIN_WAIT2),
TCPF_TIME_WAIT = (1 << TCP_TIME_WAIT),
TCPF_CLOSE = (1 << TCP_CLOSE),
TCPF_CLOSE_WAIT = (1 << TCP_CLOSE_WAIT),
TCPF_LAST_ACK = (1 << TCP_LAST_ACK),
TCPF_LISTEN = (1 << TCP_LISTEN),
TCPF_CLOSING = (1 << TCP_CLOSING),
TCPF_NEW_SYN_RECV = (1 << TCP_NEW_SYN_RECV),
};
즉, sk->sk_state가 TCP_CLOSE 혹은 TCP_LISTEN이 맞는지 검사하는 코드입니다.
그런데 TCP_LISTEN은 왜 허용할까요?
Listen 중인 socket에 Listen을 다시 호출할 일이 있을까요? 바로 밑 줄의 주석에 있었습니다.
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.
*/
backlog를 변경할 때, listen을 다시 호출하면 적용 가능토록 의도한 것 같네요.
TCP STATE에 문제가 없다면 위에 적힌 WRITE_ONCE(sk->sk_max_ack_backlog, backlog) 매크로를 이용해서 struct sock* sk에 backlog를 기록합니다.
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 = 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;
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
}
TCP_CLOSE상태인 socket에 대해 실제로 listen을 시작하는 코드 영역입니다.
가장 먼저 tcp_fastopen이 활성화되어있는지 확인합니다.
tcp_fastopen은 클라이언트와 서버가 연결을 수립했던 적이 있고, 연결을 재수립할 때 쿠키를 교환해서 SYN packet에 data를 실어 보내는 기법입니다. SYN에 data를 실어 보냄으로써 1-RTT를 아낄 수 있다는 장점이 있습니다.
// include/net/tcp.h :231
/* Bit Flags for sysctl_tcp_fastopen */
#define TFO_CLIENT_ENABLE 1
#define TFO_SERVER_ENABLE 2
#define TFO_CLIENT_NO_COOKIE 4 /* Data in SYN w/o cookie option */
하지만 tcp fastopen에 bitmask가 맞지 않아서 if문 안으로 들어가지는 않습니다.
따라서 다음으로 inet_csk_listen_start를 호출하게 됩니다.
3. inet_csk_listen_start(sk, backlog)
// net/ipv4/inet_connection_sock.c:inet_csk_listen_start() :1038
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;
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
/* 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);
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;
}
inet_sk_set_state(sk, TCP_CLOSE);
return err;
}
EXPORT_SYMBOL_GPL(inet_csk_listen_start);
3-1) req queue alloc
// net/ipv4/inet_connection_sock.c/inet_csk_listen_starnt() :1044
reqsk_queue_alloc(&icsk->icsk_accept_queue);
먼저 queue를 할당합니다.
// net/core/request_sock.c/reqsk_queue_alloc() :19
/*
* Maximum number of SYN_RECV sockets in queue per LISTEN socket.
* One SYN_RECV socket costs about 80bytes on a 32bit machine.
* It would be better to replace it with a global counter for all sockets
* but then some measure against one socket starving all other sockets
* would be needed.
*
* The minimum value of it is 128. Experiments with real servers show that
* it is absolutely not enough even at 100conn/sec. 256 cures most
* of problems.
* This value is adjusted to 128 for low memory machines,
* and it will increase in proportion to the memory of machine.
* Note : Dont forget somaxconn that may limit backlog too.
*/
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;
}
이 함수는 icsk->icsk_accept_queue를 초기화합니다. request_sock_queue 구조체는 다음과 같이 생겼습니다.
// include/net/request_sock.h:166
/** struct request_sock_queue - queue of request_socks
*
* @rskq_accept_head - FIFO head of established children
* @rskq_accept_tail - FIFO tail of established children
* @rskq_defer_accept - User waits for some data after accept()
*
*/
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.
*/
};
3-2) Value initialization
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
reqsk_queue_alloc함수 이후에는 값을 몇 가지 초기화합니다. sk->sk_ack_backlog는 0으로, inet_csk_delack_init은 icsk->icsk_ack을 0으로 초기화 합니다.
// include/net/inet_connection_sock.h/inet_csk_delack_init() :185
static inline void inet_csk_delack_init(struct sock *sk)
{
memset(&inet_csk(sk)->icsk_ack, 0, sizeof(inet_csk(sk)->icsk_ack));
}
icsk_ack는 구조체로서, 다음과 같이 생겼습니다.
// include/net/inet_cionnection_sock.h:111
struct {
__u8 pending; /* ACK is pending */
__u8 quick; /* Scheduled number of quick acks */
__u8 pingpong; /* The session is interactive */
__u8 retry; /* Number of attempts */
__u32 ato; /* Predicted tick of soft clock */
unsigned long timeout; /* Currently scheduled timeout */
__u32 lrcvtime; /* timestamp of last received data packet */
__u16 last_seg_size; /* Size of last incoming segment */
__u16 rcv_mss; /* MSS used for delayed ACK decisions */
} icsk_ack;
3-3) set_state to TCP_LISTEN
// net/ipv4/inet_connection_sock.c/inet_csk_listen_start() :1049
/* 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);
그다음으로는 inet_sk_state_store함수를 통해서 sk의 TCP status를 TCP_LISTEN으로 변경합니다.
// net/ipv4/af_inet.c/inet_sk_state_store() :1325
void inet_sk_state_store(struct sock *sk, int newstate)
{
trace_inet_sock_set_state(sk, sk->sk_state, newstate);
smp_store_release(&sk->sk_state, newstate);
}
해당 함수는 TCP state를 tracing 할 수 있도록 기록을 남기고, 실제로 sk_state에 값을 변경합니다.
TCP state의 종류는 검색해 보면 많이 나오는 자료여서 이미 유명할 테지만, 다음과 같은 value들이 있습니다.
// include/net/tcp_states.h :12
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING, /* Now a valid state */
TCP_NEW_SYN_RECV,
TCP_MAX_STATES /* Leave at the end! */
};
3-4) get_port (inet_csk_get_port)
sk->sk_prot->get_port(sk, inet->inet_num)
다시 inet_csk_listen_start로 돌아와서, 상태까지 잘 저장했다면 다음은 get_port를 실행합니다. get_port는 bind 할 때 한 번 확인해 봤던 함수인데요, 이번에는 아마도 흐름이 바뀔 겁니다.
get_port는 inet_csk_get_port 함수 그대로입니다.
// net/ipv4/inet_connection_sock.c/inet_csk_get_port() :358
/* Obtain a reference to a local port for the given sock,
* if snum is zero it means select any available local port.
* We try to allocate an odd port (and leave even ports for connect())
*/
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN;
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
int ret = 1, port = snum;
struct inet_bind_hashbucket *head;
struct net *net = sock_net(sk);
struct inet_bind_bucket *tb = NULL;
int l3mdev;
l3mdev = inet_sk_bound_l3mdev(sk);
if (!port) {
head = inet_csk_find_open_port(sk, &tb, &port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&head->lock);
inet_bind_bucket_for_each(tb, &head->chain)
if (net_eq(ib_net(tb), net) && tb->l3mdev == l3mdev &&
tb->port == port)
goto tb_found;
tb_not_found:
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev); printk("[byeo:bind] bind tb_notfound %d\n", port);
if (!tb)
goto fail_unlock;
tb_found:
if (!hlist_empty(&tb->owners)) { printk("[byeo:bind] bind tb_found %d\n", port);
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
if ((tb->fastreuse > 0 && reuse) ||
sk_reuseport_match(tb, sk))
goto success;
if (inet_csk_bind_conflict(sk, tb, true, true))
goto fail_unlock;
}
success:
inet_csk_update_fastreuse(tb, sk);
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, port);
WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
ret = 0;
fail_unlock:
spin_unlock_bh(&head->lock);
return ret;
}
EXPORT_SYMBOL_GPL(inet_csk_get_port);
이번에는 inet_bind_bucket_for_each(tb, &head->chain)을 돌면서 namespace도 일치하고, l3mdev도 같으며, port도 일치하는 tb가 존재할 것입니다. 그 이유는... bind system call과정에서 inet_bind_bucket_create를 통해서 tb를 hlist_add_head(&tb->node, &head->chain)을 통해 넣어놓았기 때문이죠!
실제로 이런 식으로 tb_not_found를 탈 지, tb_found를 탈 지 debug printk 코드를 넣어보았습니다.
14.499150초에 호출된 내용이 bind함수에서, 14.499177초에 호출된 내용이 listen과정에서 불린 inet_csk_get_port 결과입니다.
- inet_csk_get_port: bind과정에서는 tb_not_found를
- listen과정에서는 tb_found를 타는 것이
일반적이라고 볼 수 있겠습니다!
3-5) hash
// net/ipv4/inet_connection_sock.c/inet_csk_listen_start() :1055
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;
}
get_port를 성공했다면 0을 return 할 것이고, inet->inet_sport의 값이 설정됩니다. (사실 bind에서 이미 설정했는데 왜 또 하는지 는 잘 모르겠습니다.)
sk_dst_reset은 bind에서도 봤던 함수인데요, dst_entry 자료구조를 NULL로 초기화합니다. (이 구조체도 아직 역할을 모르겠습니다.)
sk->sk_prot->hash(sk)는 tcp_prot의 .hash함수를 호출합니다.
이 함수는 inet_hash인데요, 다음 포스트에서 알아보도록 하겠습니다.
// net/ipv4/tcp_ipv4.c:3051
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
...
.hash = inet_hash,
현재까지 흐름도
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
accept system call 0 - Intro (0) | 2024.05.14 |
---|---|
listen system call 3 (inet_hash) (0) | 2024.05.12 |
listen system call 1 (__sys_listen) (0) | 2024.05.11 |
bind system call 3 (__inet_bind - 2) (0) | 2024.05.01 |
bind system call 2 (__inet_bind) (0) | 2024.04.28 |