- Today
- Total
Byeo
accept system call 3 (inet_accept) 본문
이전 포스트: https://byeo.tistory.com/entry/accept-system-call-2-inetaccept
7. inet_accept
inet_accept에서 호출했던 sk1->sk_prot->accept 함수는 inet_csk_accept 함수였었고, 정상적으로 실행이 됐다면 struct request_sock* 을 반환한다는 것 까지 확인했었습니다.
// net/ipv4/af_inet.c/inet_accept() :734
/*
* Accept a pending connection. The TCP layer now gives BSD semantics.
*/
int inet_accept(struct socket *sock, struct socket *newsock, int flags,
bool kern)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern);
if (!sk2)
goto do_err;
lock_sock(sk2);
sock_rps_record_flow(sk2);
WARN_ON(!((1 << sk2->sk_state) &
(TCPF_ESTABLISHED | TCPF_SYN_RECV |
TCPF_CLOSE_WAIT | TCPF_CLOSE)));
sock_graft(sk2, newsock);
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2);
do_err:
return err;
}
EXPORT_SYMBOL(inet_accept);
다음으로 실행하는 함수는 sock_rps_record_flow입니다.
sock_rps_record_flow는 이름에서 알 수 있듯 RPS (Receive Packet Steering)과 관련된 작업을 수행합니다.
7-1) rps
// include/net/sock.h/sock_rps_record_flow() :1050
static inline void sock_rps_record_flow(const struct sock *sk)
{
#ifdef CONFIG_RPS
if (static_branch_unlikely(&rfs_needed)) {
/* Reading sk->sk_rxhash might incur an expensive cache line
* miss.
*
* TCP_ESTABLISHED does cover almost all states where RFS
* might be useful, and is cheaper [1] than testing :
* IPv4: inet_sk(sk)->inet_daddr
* IPv6: ipv6_addr_any(&sk->sk_v6_daddr)
* OR an additional socket flag
* [1] : sk_state and sk_prot are in the same cache line.
*/
if (sk->sk_state == TCP_ESTABLISHED)
sock_rps_record_flow_hash(sk->sk_rxhash);
}
#endif
}
static_branch_unlikely는 rfs_needed내에 있는 key의 boolean 값을 사용하여 if문을 실행할지 넘길지 결정합니다. 일반적인 if와는 다르게 코드 자체를 수정해서 branch prediction을 제거한다고 합니다. 1
sk->sk_state가 TCP_ESTABLISHED인 경우, sock_rps_record_flow_hash를 호출합니다.
static inline void sock_rps_record_flow_hash(__u32 hash)
{
#ifdef CONFIG_RPS
struct rps_sock_flow_table *sock_flow_table;
rcu_read_lock();
sock_flow_table = rcu_dereference(rps_sock_flow_table);
rps_record_sock_flow(sock_flow_table, hash);
rcu_read_unlock();
#endif
}
rps_sock_flow_table 변수는 전역으로 선언되어 있습니다.
// net/core/dev.c:4342
/* One global table that all flow-based protocols share. */
struct rps_sock_flow_table __rcu *rps_sock_flow_table __read_mostly;
구조체는 다음과 같이 생겼습니다.
// include/linux/netdevice.h :685
/*
* The rps_sock_flow_table contains mappings of flows to the last CPU
* on which they were processed by the application (set in recvmsg).
* Each entry is a 32bit value. Upper part is the high-order bits
* of flow hash, lower part is CPU number.
* rps_cpu_mask is used to partition the space, depending on number of
* possible CPUs : rps_cpu_mask = roundup_pow_of_two(nr_cpu_ids) - 1
* For example, if 64 CPUs are possible, rps_cpu_mask = 0x3f,
* meaning we use 32-6=26 bits for the hash.
*/
struct rps_sock_flow_table {
u32 mask;
u32 ents[] ____cacheline_aligned_in_smp;
};
32 bit내에서 상위 bit 일부는 flow hash로, 하위 bit 일부는 CPU 번호로서 사용된다고 합니다. bit를 얼마씩 나눠줄지는 rps_cpu_mask 값을 따르며, 이 값은 $log_2 (number\_of\_cpus)$ 입니다.
그래서 최종적으로 다음에서 hash를 추가합니다.
// include/linux/netdevice.h/rps_record_sock_flow() :707
static inline void rps_record_sock_flow(struct rps_sock_flow_table *table,
u32 hash)
{
if (table && hash) {
unsigned int index = hash & table->mask;
u32 val = hash & ~rps_cpu_mask;
/* We only give a hint, preemption can change CPU under us */
val |= raw_smp_processor_id();
if (table->ents[index] != val)
table->ents[index] = val;
}
}
table과 hash가 모두 존재하면, flow hash를 하위 bit 일부에 대해 masking 해서 특정 CPU 번호를 index에 저장합니다. 그리고 val은 flow hash에서 상위 bit을 저장합니다. 그리고 table에 index와 val을 저장합니다.
7-2) sock_graft
// include/net/sock.h/sock_graft() :1943
static inline void sock_graft(struct sock *sk, struct socket *parent)
{
WARN_ON(parent->sk);
write_lock_bh(&sk->sk_callback_lock);
rcu_assign_pointer(sk->sk_wq, &parent->wq);
parent->sk = sk;
sk_set_socket(sk, parent);
sk->sk_uid = SOCK_INODE(parent)->i_uid;
security_sock_graft(sk, parent);
write_unlock_bh(&sk->sk_callback_lock);
}
graft는 접목하다라는 뜻인데요, accept을 위해서 새로이 추가한 struct sock *sk 과 struct socket *parent를 연결시켜주는 작업을 수행합니다.
해당 함수는 struct socket의 wq를 struct sock에게 전달 및 struct sock의 sk_socket에 struct socket* 을 연결하고, 반대로 struct socket의 sk에 struct sock*을 서로 연결해줍니다. 그리고 inode 번호도 가져오는 역할을 수행합니다.
7-3) state 변경
마지막으로 struct socket의 상태를 변경합니다.
newsock->state = SS_CONNECTED;
8. do_accept
// net/socket.c/do_accept() :1778
if (upeer_sockaddr) {
len = newsock->ops->getname(newsock,
(struct sockaddr *)&address, 2);
if (len < 0) {
err = -ECONNABORTED;
goto out_fd;
}
err = move_addr_to_user(&address,
len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_fd;
}
/* File flags are not inherited via accept() unlike another OSes. */
return newfile;
do_accept에서 남은 부분을 실행합니다. 이 부분은 application의 parameter였던 upeer_sockaddr에 값을 넣어주는 것으로 보입니다.
8-1) inet_getname
newsock->ops->getname에서 newsock->ops는 inet_stream_ops이고, 따라서 getname은 inet_getname 함수포인터입니다.
// net/ipv4/af_inet.c/inet_getname() :765
/*
* This does both peername and sockname.
*/
int inet_getname(struct socket *sock, struct sockaddr *uaddr,
int peer)
{
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);
DECLARE_SOCKADDR(struct sockaddr_in *, sin, uaddr);
sin->sin_family = AF_INET;
if (peer) {
if (!inet->inet_dport ||
(((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) &&
peer == 1))
return -ENOTCONN;
sin->sin_port = inet->inet_dport;
sin->sin_addr.s_addr = inet->inet_daddr;
BPF_CGROUP_RUN_SA_PROG_LOCK(sk, (struct sockaddr *)sin,
CGROUP_INET4_GETPEERNAME,
NULL);
} else {
__be32 addr = inet->inet_rcv_saddr;
if (!addr)
addr = inet->inet_saddr;
sin->sin_port = inet->inet_sport;
sin->sin_addr.s_addr = addr;
BPF_CGROUP_RUN_SA_PROG_LOCK(sk, (struct sockaddr *)sin,
CGROUP_INET4_GETSOCKNAME,
NULL);
}
memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
return sizeof(*sin);
}
EXPORT_SYMBOL(inet_getname);
peer가 0이면 source ip address와 source port를 uaddr에 넣어주는 것으로 보입니다.
peer가 1이면 destination ip address와 destination port, 즉 상대방의 ip / port를 uaddr에 넣어주는 것으로 보입니다. 단, TCP state를 한 번 체크하고 말이죠.
peer가 2이면 1처럼 상대방의 ip / port를 넣어주되, TCP state를 체크하지 않는 것으로 보입니다.
8-2) move_addr_to_user
// net/socket.c/move_addr_to_ser() :253
/**
* move_addr_to_user - copy an address to user space
* @kaddr: kernel space address
* @klen: length of address in kernel
* @uaddr: user space address
* @ulen: pointer to user length field
*
* The value pointed to by ulen on entry is the buffer length available.
* This is overwritten with the buffer space used. -EINVAL is returned
* if an overlong buffer is specified or a negative buffer size. -EFAULT
* is returned if either the buffer or the length field are not
* accessible.
* After copying the data up to the limit the user specifies, the true
* length of the data is written over the length limit the user
* specified. Zero is returned for a success.
*/
static int move_addr_to_user(struct sockaddr_storage *kaddr, int klen,
void __user *uaddr, int __user *ulen)
{
int err;
int len;
BUG_ON(klen > sizeof(struct sockaddr_storage));
err = get_user(len, ulen);
if (err)
return err;
if (len > klen)
len = klen;
if (len < 0)
return -EINVAL;
if (len) {
if (audit_sockaddr(klen, kaddr))
return -ENOMEM;
if (copy_to_user(uaddr, kaddr, len))
return -EFAULT;
}
/*
* "fromlen shall refer to the value before truncation.."
* 1003.1g
*/
return __put_user(klen, ulen);
}
user에서 kernel로 데이터를 옮길 때, move_addr_to_kernel을 썼었다면, 반대로도 user에게 데이터를 복사할 때 move_addr_to_user를 사용해야겠죠?
copy_to_user를 통해서 uaddr로 복사를 수행합니다.
9. __sys_accept4_file
// net/socket.c/__sys_accept4_file() :1816
newfile = do_accept(file, file_flags, upeer_sockaddr, upeer_addrlen,
flags);
if (IS_ERR(newfile)) {
put_unused_fd(newfd);
return PTR_ERR(newfile);
}
fd_install(newfd, newfile);
return newfd;
do_accept함수가 끝나면, struct file*의 newfile을 반환받습니다. 이 값이 에러가 아니라면 마지막으로 fd_install (newfd, newfile)을 수행하고 accept system call을 종료합니다.
최종 흐름도
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
Listen socket의 TCP SYN 처리 1 (0) | 2024.05.25 |
---|---|
ftrace 사용법 (0) | 2024.05.23 |
[Linux Kernel] Blocking I/O (0) | 2024.05.17 |
accept system call 2 (inet_csk_accept) (0) | 2024.05.15 |
accept system call 1 (__sys_accept4) (0) | 2024.05.14 |