Byeo

socket system call 4 (sock_map_fd) 본문

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

socket system call 4 (sock_map_fd)

BKlee 2024. 4. 14. 12:52
반응형

이전 포스트: https://byeo.tistory.com/entry/socket-system-call-3

 

socket system call 3 (tcp_v4_init_sock)

이전 포스트: https://byeo.tistory.com/entry/socket-system-call-2 socket system call 2 이전 포스트: https://byeo.tistory.com/entry/socket-system-call socket system call 1 int socket(int domain, int type, int protocol); socket 시스템 콜의 동

byeo.tistory.com

 

 

7. sock_map_fd

마지막으로 sock_map_fd 함수를 거칩니다. 이 함수는 기존에 tcp_ipv4_init_sock, inet_create를 모두 빠져나온 뒤, __sys_socket에서 fd값을 얻기 위해 실행하는 마지막 함수입니다.

 

// net/socket.c/__sys_socket() :1537

int __sys_socket(int family, int type, int protocol)
{
	int retval;
	struct socket *sock;
	int flags;

	/* Check the SOCK_* constants for consistency.  */
	BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
	BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

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

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

	retval = sock_create(family, type, protocol, &sock);
	if (retval < 0)
		return retval;

	return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

 

마지막에 sock_map_fd 함수 값을 return 하게 되어있네요.

 

sock_map_fd

// net/socket.c/sock_map_fd() :477

static int sock_map_fd(struct socket *sock, int flags)
{
	struct file *newfile;
	int fd = get_unused_fd_flags(flags);
	if (unlikely(fd < 0)) {
		sock_release(sock);
		return fd;
	}

	newfile = sock_alloc_file(sock, flags, NULL);
	if (!IS_ERR(newfile)) {
		fd_install(fd, newfile);
		return fd;
	}

	put_unused_fd(fd);
	return PTR_ERR(newfile);
}

 

sock_map_fd가 하는 역할은 크게 다음과 같습니다.

  • get_unused_fd_flags: 사용중이지 않은 fd 값을 조회
  • sock_alloc_file: file할당 및 socket과 연결
  • fd_install: file pointer를 fd array에 추가

 

1) get_unused_fd_flags

// fs/file.c/get_unused_fd_flags() :528

int __get_unused_fd_flags(unsigned flags, unsigned long nofile)
{
	return alloc_fd(0, nofile, flags);
}

int get_unused_fd_flags(unsigned flags)
{
	return __get_unused_fd_flags(flags, rlimit(RLIMIT_NOFILE));
}

 

get_unused_fd_flags는 __get_unused_fd_flags를 호출합니다. 여기서 인자로 rlimit(RLIMIT_NOFILE)을 넘겨주는데, 이는 다음 코드에서 알 수 있듯, 현재 task의 resource limit을 가져옵니다.

 

// include/linux/sched/signal.h/task_rlimit() :719

static inline unsigned long task_rlimit(const struct task_struct *task,
		unsigned int limit)
{
	return READ_ONCE(task->signal->rlim[limit].rlim_cur);
}

static inline unsigned long task_rlimit_max(const struct task_struct *task,
		unsigned int limit)
{
	return READ_ONCE(task->signal->rlim[limit].rlim_max);
}

 

 

alloc_fd

// fs/file.c/alloc_fd() :467

/*
 * allocate a file descriptor, mark it busy.
 */
static int alloc_fd(unsigned start, unsigned end, unsigned flags)
{
	struct files_struct *files = current->files;
	unsigned int fd;
	int error;
	struct fdtable *fdt;

	spin_lock(&files->file_lock);
repeat:
	fdt = files_fdtable(files);
	fd = start;
	if (fd < files->next_fd)
		fd = files->next_fd;

	if (fd < fdt->max_fds)
		fd = find_next_fd(fdt, fd);

	/*
	 * N.B. For clone tasks sharing a files structure, this test
	 * will limit the total number of files that can be opened.
	 */
	error = -EMFILE;
	if (fd >= end)
		goto out;

	error = expand_files(files, fd);
	if (error < 0)
		goto out;

	/*
	 * If we needed to expand the fs array we
	 * might have blocked - try again.
	 */
	if (error)
		goto repeat;

	if (start <= files->next_fd)
		files->next_fd = fd + 1;

	__set_open_fd(fd, fdt);
	if (flags & O_CLOEXEC)
		__set_close_on_exec(fd, fdt);
	else
		__clear_close_on_exec(fd, fdt);
	error = fd;
#if 1
	/* Sanity check */
	if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
		printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
		rcu_assign_pointer(fdt->fd[fd], NULL);
	}
#endif

out:
	spin_unlock(&files->file_lock);
	return error;
}

 

이 함수에 들어올 때, start는 0으로 주어집니다. 그리고 if 분기문을 2개 타면서 fd 값이 결정됩니다.

만약 빈 fd가 부족하여 새로 받을 수 없는 경우에는 expand_files를 실행하여 한 번 확장을 시도해봅니다.

 

한 번 궁금해서 몇 가지 변수들을 출력해 보았습니다.

// fs/file.c:486
printk("alloc_fd start: %d fd: %d files->next_fd: %d fdt->max_fds: %d", start, fd, files->next_fd, fdt->max_fds);

 

fd값은 3으로 결정이 된 것으로 보입니다.

 

// fs/file.c/alloc_fd() :506
	if (start <= files->next_fd)
		files->next_fd = fd + 1;

그 아래에서는 files->next_fdfd + 1을 대입해주고 있습니다. 

 

// fs/file.c/alloc_fd() :509

	__set_open_fd(fd, fdt);
	if (flags & O_CLOEXEC)
		__set_close_on_exec(fd, fdt);
	else
		__clear_close_on_exec(fd, fdt);

 

그리고 그 밑에서는 O_CLOEXEC flag set 여부에 따라서 몇 가지 조치를 더 취해주는 모습을 보입니다. __set_open_fd, __set_close_on_exec, __clear_close_on_exec 모두 함수 내부를 들어가 보면 특정 bit를 set 하는 함수를 호출합니다. 자세하게는 다루지 않으려고 합니다.

 

마지막으로 fd를 return 합니다. 오류가 발생했다면 errno를 return합니다.

 

2) sock_alloc_file

sock_map_fd에서 get_unused_fd_flags를 성공적으로 수행했다면, 이제 다음 코드를 실행합니다.

newfile = sock_alloc_file(sock, flags, NULL);

 

이 함수의 내부는 다음과 같습니다.

// net/socket.c/sock_alloc_file() :443

/**
 *	sock_alloc_file - Bind a &socket to a &file
 *	@sock: socket
 *	@flags: file status flags
 *	@dname: protocol name
 *
 *	Returns the &file bound with @sock, implicitly storing it
 *	in sock->file. If dname is %NULL, sets to "".
 *	On failure the return is a ERR pointer (see linux/err.h).
 *	This function uses GFP_KERNEL internally.
 */
 
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
	struct file *file;

	if (!dname)
		dname = sock->sk ? sock->sk->sk_prot_creator->name : "";

	file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
				O_RDWR | (flags & O_NONBLOCK),
				&socket_file_ops);
	if (IS_ERR(file)) {
		sock_release(sock);
		return file;
	}

	sock->file = file;
	file->private_data = sock;
	stream_open(SOCK_INODE(sock), file);
	return file;
}

 

먼저, dname을 설정합니다. sock->sk는 현재 있는 값이므로, sock->sk->sk_prot_creator->name을 대입할 것입니다.

 

sk_prot_creatorsk_alloc 당시에 answer_prot의 값으로 지정이 되어있습니다. 그리고 이 proto가 tcp_prot라고 가정하면, name은 다음에 의해 "TCP"가 됩니다.

// net/ipv4/tcp_ipv4.c:3051

struct proto tcp_prot = {
	.name			= "TCP",
    ...   
}

 

alloc_file_pseudo

	file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
				O_RDWR | (flags & O_NONBLOCK),
				&socket_file_ops);

 

dname을 지정했으면, 그다음 alloc_file_pseudo 함수를 실행합니다.

1번째 인자: SOCK_INODE(sock)는 포인터를 캐스팅한 뒤, 연관된 vfs_inode를 가져옵니다.

// include/net/sock.h
struct socket_alloc {
	struct socket socket;
	struct inode vfs_inode;
};

static inline struct inode *SOCK_INODE(struct socket *socket)
{
	return &container_of(socket, struct socket_alloc, socket)->vfs_inode;
}

// include/linux/kernel.h

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&	\
			 !__same_type(*(ptr), void),			\
			 "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

 

2번째 인자: filesystem과는 거리가 좀 멀어서 자세히는 알지는 못하지만, sock_mnt는 sock_init 당시에 다음과 같이 값이 지정되어 있습니다.

// net/socket.c:418
static struct vfsmount *sock_mnt __read_mostly;

// net/socket.c/sock_init() :3110
	sock_mnt = kern_mount(&sock_fs_type);
    
// net/socket.c:420
static struct file_system_type sock_fs_type = {
	.name =		"sockfs",
	.init_fs_context = sockfs_init_fs_context,
	.kill_sb =	kill_anon_super,
};

 

따라서 kern_mount 된 struct file_syste_type sock_fs_type을 2번째 인자로 넣어주는 것으로 보입니다.

 

3번째 인자: dname

4번째 인자: flags

5번째 인자: socket_file_ops 구조체를 넣어줍니다.

// net/socket.c:150

static const struct file_operations socket_file_ops = {
	.owner =	THIS_MODULE,
	.llseek =	no_llseek,
	.read_iter =	sock_read_iter,
	.write_iter =	sock_write_iter,
	.poll =		sock_poll,
	.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = compat_sock_ioctl,
#endif
	.mmap =		sock_mmap,
	.release =	sock_close,
	.fasync =	sock_fasync,
	.sendpage =	sock_sendpage,
	.splice_write = generic_splice_sendpage,
	.splice_read =	sock_splice_read,
	.show_fdinfo =	sock_show_fdinfo,
};

 

 

이제 이 인자들을 함께 아래의 alloc_file_pseudo 함수를 실행합니다.

// fs/file_table.c/alloc_file_pseudo() :214
struct file *alloc_file_pseudo(struct inode *inode, struct vfsmount *mnt,
				const char *name, int flags,
				const struct file_operations *fops)
{
	static const struct dentry_operations anon_ops = {
		.d_dname = simple_dname
	};
	struct qstr this = QSTR_INIT(name, strlen(name));
	struct path path;
	struct file *file;

	path.dentry = d_alloc_pseudo(mnt->mnt_sb, &this);
	if (!path.dentry)
		return ERR_PTR(-ENOMEM);
	if (!mnt->mnt_sb->s_d_op)
		d_set_d_op(path.dentry, &anon_ops);
	path.mnt = mntget(mnt);
	d_instantiate(path.dentry, inode);
	file = alloc_file(&path, flags, fops);
	if (IS_ERR(file)) {
		ihold(inode);
		path_put(&path);
	}
	return file;
}

 

역시 파일시스템의 향기가 너무 강하게 나서 대부분은 스킵을 하겠습니다. 나중에 언젠가 공부하게 된다면 깨달을지도... 대신에 alloc_file은 살펴보려고 합니다.

 

// fs/file_table.c/alloc_file():180

/**
 * alloc_file - allocate and initialize a 'struct file'
 *
 * @path: the (dentry, vfsmount) pair for the new file
 * @flags: O_... flags with which the new file will be opened
 * @fop: the 'struct file_operations' for the new file
 */
static struct file *alloc_file(const struct path *path, int flags,
		const struct file_operations *fop)
{
	struct file *file;

	file = alloc_empty_file(flags, current_cred());
	if (IS_ERR(file))
		return file;

	file->f_path = *path;
	file->f_inode = path->dentry->d_inode;
	file->f_mapping = path->dentry->d_inode->i_mapping;
	file->f_wb_err = filemap_sample_wb_err(file->f_mapping);
	file->f_sb_err = file_sample_sb_err(file);
	if ((file->f_mode & FMODE_READ) &&
	     likely(fop->read || fop->read_iter))
		file->f_mode |= FMODE_CAN_READ;
	if ((file->f_mode & FMODE_WRITE) &&
	     likely(fop->write || fop->write_iter))
		file->f_mode |= FMODE_CAN_WRITE;
	file->f_mode |= FMODE_OPENED;
	file->f_op = fop;
	if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
		i_readcount_inc(path->dentry->d_inode);
	return file;
}

 

alloc_empty_file 함수는 내부에서 다시 __alloc_file을 호출하며, 이 함수가 최종적으로 kmem_cache_zalloc을 호출하여 공간을 할당받습니다. 여기서 설정되는 변수 몇 가지가 보입니다.

 

// fs/file_table.c/__alloc_file() :96

static struct file *__alloc_file(int flags, const struct cred *cred)
{
	struct file *f;
	int error;

	f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
	if (unlikely(!f))
		return ERR_PTR(-ENOMEM);

	f->f_cred = get_cred(cred);
	error = security_file_alloc(f);
	if (unlikely(error)) {
		file_free_rcu(&f->f_u.fu_rcuhead);
		return ERR_PTR(error);
	}

	atomic_long_set(&f->f_count, 1);
	rwlock_init(&f->f_owner.lock);
	spin_lock_init(&f->f_lock);
	mutex_init(&f->f_pos_lock);
	f->f_flags = flags;
	f->f_mode = OPEN_FMODE(flags);
	/* f->f_version: 0 */

	return f;
}

 

 

함수들을 빠져나와서 sock_alloc_file로 돌아옵니다.

// net/socket.c/sock_alloc_file() :470

struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{

...

	sock->file = file;
	file->private_data = sock;
	stream_open(SOCK_INODE(sock), file);
    return file;
}

 

이제 sock과 file을 서로 연결시켜 줍니다.

 

stream_open

/*
 * stream_open is used by subsystems that want stream-like file descriptors.
 * Such file descriptors are not seekable and don't have notion of position
 * (file.f_pos is always 0 and ppos passed to .read()/.write() is always NULL).
 * Contrary to file descriptors of other regular files, .read() and .write()
 * can run simultaneously.
 *
 * stream_open never fails and is marked to return int so that it could be
 * directly used as file_operations.open .
 */
int stream_open(struct inode *inode, struct file *filp)
{
	filp->f_mode &= ~(FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE | FMODE_ATOMIC_POS);
	filp->f_mode |= FMODE_STREAM;
	return 0;
}

 

stream open을 설정합니다. 일반적인 파일과 같은 경우에는 fseek과 같은 함수를 통해서 cursor를 옮기며 데이터를 읽을 수 있습니다. 하지만 network는 읽고자 하는 데이터의 사이즈가 최대 얼마인지 정해져 있지도 않고, 언제 들어올지도 모르는 무한한 데이터이죠.

 

socket을 위한 file은 이러한 종류의 file임을 지정하는 함수로 보입니다. (inode는 왜 줬을까?)

 

3) fd_install

// net/socket.c/sock_map_fd() :486
static int sock_map_fd(struct socket *sock, int flags)

...

	newfile = sock_alloc_file(sock, flags, NULL);
	if (!IS_ERR(newfile)) {
		fd_install(fd, newfile);
		return fd;
	}

 

마지막으로 sock_alloc_file이 오류가 발생하지 않았다면, fd_install을 실행하고 fd를 return 합니다.

 

// fs/file.c/fd_install() :557
/*
 * Install a file pointer in the fd array.
 *
 * The VFS is full of places where we drop the files lock between
 * setting the open_fds bitmap and installing the file in the file
 * array.  At any such point, we are vulnerable to a dup2() race
 * installing a file in the array before us.  We need to detect this and
 * fput() the struct file we are about to overwrite in this case.
 *
 * It should never happen - if we allow dup2() do it, _really_ bad things
 * will follow.
 *
 * This consumes the "file" refcount, so callers should treat it
 * as if they had called fput(file).
 */

void fd_install(unsigned int fd, struct file *file)
{
	struct files_struct *files = current->files;
	struct fdtable *fdt;

	rcu_read_lock_sched();

	if (unlikely(files->resize_in_progress)) {
		rcu_read_unlock_sched();
		spin_lock(&files->file_lock);
		fdt = files_fdtable(files);
		BUG_ON(fdt->fd[fd] != NULL);
		rcu_assign_pointer(fdt->fd[fd], file);
		spin_unlock(&files->file_lock);
		return;
	}
	/* coupled with smp_wmb() in expand_fdtable() */
	smp_rmb();
	fdt = rcu_dereference_sched(files->fdt);
	BUG_ON(fdt->fd[fd] != NULL);
	rcu_assign_pointer(fdt->fd[fd], file);
	rcu_read_unlock_sched();
}

 

마지막에서 두 번째 줄이 가장 중요해 보입니다. rcu_assign_pointer(fdt->fd[fd], file);

커널 내부에서 관리하는 fdtable의 fd 배열에 socket을 위해서 할당됐었던 struct file * file을 넣어주는 것으로 보입니다.

 

최종 흐름도

 

socket system call 4 (sock_map_fd)

socket system call 분석해보기에서 마지막 포스트인 이 포스트는 파일시스템 관련 내용이 많아서 스킵한 부분이 좀 많습니다. 나중에 파일시스템까지 분석할 일이 생긴다면 더 잘 이해할 수 있을지도요..

 

 

반응형
Comments