Byeo

accept system call 1 (__sys_accept4) 본문

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

accept system call 1 (__sys_accept4)

BKlee 2024. 5. 14. 22:13
반응형

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

 

accept system call 0 - Intro

일반적인 BSD Socket API는 server가 socket을 생성하고 bind 하고 listen 하면, 그다음은 accept을 수행합니다. 이 accept은 client가 연결 요청을 시도 함으로 인해 listen queue에 차있는 established packet을 꺼내온

byeo.tistory.com

 

언제나 그렇듯, __sys_accept에서 시작합니다. 단, 함수 명의 끝에 4가 붙어있습니다.

0. Parameter

accpet의 parameter는 조금 특이합니다.

 

1. fd: 늘 그렇듯 socket fd를 첫 번째 인자로 받습니다.

2. *addrlen: 연결 수립 시, 상대방의 주소를 userspace에게 전달하기 위하여 struct sockaddr* 구조체를 하나 받습니다. 이 공간은 userspace에서 할당받은 공간이어야 하며 (구조체를 선언하여 &를 전달하는 방식 or malloc), 구조체는 kernel에 의해 값이 기록됩니다.

3. *addrlen: addrlen도 마찬가지로 kernel이 기록해줍니다.

4. flags: flag를 사용하지 않으려면 accept을 사용하면 됩니다. flag를 주고싶다면 accept4를 사용하면 됩니다.

 

1. sys_accept

// net/socket.c/SYSCALL_DEFINE(accept) :1855

SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
		int __user *, upeer_addrlen, int, flags)
{
	return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
}

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
		int __user *, upeer_addrlen)
{
	return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}

 

accept system call을 정의합니다. userspace에서는 accept, accept4를 모두 사용할 수 있습니다. 차이는 accept4는 standard가 아니며, flags라는 4번째 인자를 추가로 받습니다.

 

두 함수는 동일하게 __sys_accept4를 호출합니다. 단, 4번째 인자를 flags로 주냐, 0으로 주냐의 차이입니다.

 

2. __sys_accept4

// net/socket.c/__sys_accept4() :1826

/*
 *	For accept, we attempt to create a new socket, set up the link
 *	with the client, wake up the client, then return the new
 *	connected fd. We collect the address of the connector in kernel
 *	space and move it to user at the very end. This is unclean because
 *	we open the socket then return an error.
 *
 *	1003.1g adds the ability to recvmsg() to query connection pending
 *	status to recvmsg. We need to add that support in a way thats
 *	clean when we restructure accept also.
 */

int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
		  int __user *upeer_addrlen, int flags)
{
	int ret = -EBADF;
	struct fd f;

	f = fdget(fd);
	if (f.file) {
		ret = __sys_accept4_file(f.file, 0, upeer_sockaddr,
						upeer_addrlen, flags,
						rlimit(RLIMIT_NOFILE));
		fdput(f);
	}

	return ret;
}

 

 

struct socket* sock을 얻어오기 위해서 sockfd_lookup_light를 호출했던 bind, listen때와는 조금 다르게, __to_fd까지 처리하는 fdget을 얻어와 struct fd에 fd 관련 정보를 저장합니다.

 

// include/linux/file.h :36

struct fd {
	struct file *file;
	unsigned int flags;
};

 

__to_fd 함수는 다음과 같이 생겼습니다.

// include/linux/file.h/__to_fd() :58

static inline struct fd __to_fd(unsigned long v)
{
	return (struct fd){(struct file *)(v & ~3),v & 3};
}

 

그리고 f.file이 존재한다면 __sys_accept4_file을 실행하기 시작합니다.

 

3. __sys_accept4_file

// net/socket.c/__sys_accept4_file() :1798

int __sys_accept4_file(struct file *file, unsigned file_flags,
		       struct sockaddr __user *upeer_sockaddr,
		       int __user *upeer_addrlen, int flags,
		       unsigned long nofile)
{
	struct file *newfile;
	int newfd;

	if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
		return -EINVAL;

	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

	newfd = __get_unused_fd_flags(flags, nofile);
	if (unlikely(newfd < 0))
		return newfd;

	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;
}

 

__sys_accept4_file은 parameter를 여러 개 받습니다.

 

struct file *file: f.file

unsigned file_flags: 0

struct sockaddr __user *upeer_sockaddr: userspace의 sockaddr 구조체 포인터

int __user *upeer_addrlen: userspace의 addrlen integer 포인터

flags: accept system call을 호출 시 넘긴 flags

nofile: resource limit에 지정된 file 생성 limit 값

 

	if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
		return -EINVAL;

 

첫 if문은 flags가 SOCK_CLOEXEC 와 SOCK_NONBLOCK 외에 다른 bit가 있는지를 체크합니다. 즉, 이 두 flags 외에는 받지 않습니다.

 

	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

 

두 번째 if문은 SOCK_NONBLOCK과 O_NONBLOCK의 value가 다른 경우에 O_NONBLOCK 값으로 바꿔줍니다. linux kernel 5.18기준으로는 두 값은 같으므로 해당되지 않습니다.

// include/linux/net.h :79

#define SOCK_NONBLOCK	O_NONBLOCK

 

 

그 다음으로는 새로운 fd를 할당합니다. 이 함수는 socket system call에서 봤었죠. (https://byeo.tistory.com/entry/socket-system-call-4)

	newfd = __get_unused_fd_flags(flags, nofile);
	if (unlikely(newfd < 0))
		return newfd;

 

일단 blocking을 하기 전에 fd부터 먼저 할당을 해놓는 모습입니다. (kernel로부터 자원을 얻을 때에는 get, 반환할 때는 put)

 

그 다음 함수는 do_accept입니다.

 

4. do_accept

do_accept의 인자는 RLIMIT으로 걸려있던 nofile을 제외하고 전부 넘어갑니다. 참고로 새로 할당받았던 newfd는 넘어가지 않음에 유의하세요!

 

// net/socket.c/do_accept() :1739

struct file *do_accept(struct file *file, unsigned file_flags,
		       struct sockaddr __user *upeer_sockaddr,
		       int __user *upeer_addrlen, int flags)
{
	struct socket *sock, *newsock;
	struct file *newfile;
	int err, len;
	struct sockaddr_storage address;

	sock = sock_from_file(file);
	if (!sock)
		return ERR_PTR(-ENOTSOCK);

	newsock = sock_alloc();
	if (!newsock)
		return ERR_PTR(-ENFILE);

	newsock->type = sock->type;
	newsock->ops = sock->ops;

	/*
	 * We don't need try_module_get here, as the listening socket (sock)
	 * has the protocol module (sock->ops->owner) held.
	 */
	__module_get(newsock->ops->owner);

	newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
	if (IS_ERR(newfile))
		return newfile;

	err = security_socket_accept(sock, newsock);
	if (err)
		goto out_fd;

	err = sock->ops->accept(sock, newsock, sock->file->f_flags | file_flags,
					false);
	if (err < 0)
		goto out_fd;

	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;
out_fd:
	fput(newfile);
	return ERR_PTR(err);
}

 

1. 'sock_from_file'은 struct file에서 struct sock* sock을 가져오는 함수입니다. 우리는 socket system call을 호출할 때, file->private_data에 struct socket pointer를 보관한다는 것을 확인했었습니다.

 

2. 다음으로, 'sock_alloc()'을 호출합니다. 이는 새로운 socket fd를 제공하기 위하여 sock을 할당받습니다. alloc_inode를 통해서 inode 공간을 받고, inode에 관련 값들을 초기화 합니다.

 

3. 할당받은 struct socket에 type과 ops를 동일하게 대입합니다. type은 SOCK_STREAM, ops는 inet_stream_ops가 입력됩니다.

 

4. sock_alloc_file: socket 할당 때와 유사하게, 새로운 file을 할당합니다. sock->file과 file->private_data에 서로가 기록됩니다. 또한, struct file의 내용물이 sock_alloc_file 함수 내에서 초기화됩니다.

 

// net/socket.c/do_accept() :1773

	err = sock->ops->accept(sock, newsock, sock->file->f_flags | file_flags,
					false);

 

이제 sock에 연결된 accept함수를 호출합니다. ops는 inet_stream_ops고, 해당 구조체에서 accept은 inet_accept인 것을 확인할 수 있습니다.

 

현재까지 흐름도

반응형
Comments