Byeo

accept system call 3 (inet_accept) 본문

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

accept system call 3 (inet_accept)

BKlee 2024. 5. 19. 17:05
반응형

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

 

accept system call 2 (inet_csk_accept)

이전 포스트: https://byeo.tistory.com/entry/accept-system-call-1-sysaccept4 accept system call 1 (__sys_accept4)accept system call의 개요와 관련된 내용은 이전 포스트에 정리되어 있습니다.: https://byeo.tistory.com/entry/accept-

byeo.tistory.com

 

 

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을 종료합니다.

 

 

최종 흐름도

반응형
Comments