- Today
- Total
Byeo
socket system call 4 (sock_map_fd) 본문
이전 포스트: 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_fd에 fd + 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_creator는 sk_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 분석해보기에서 마지막 포스트인 이 포스트는 파일시스템 관련 내용이 많아서 스킵한 부분이 좀 많습니다. 나중에 파일시스템까지 분석할 일이 생긴다면 더 잘 이해할 수 있을지도요..
'프로그래밍 (Programming) > 네트워크 스택' 카테고리의 다른 글
bind system call 1 (__sys_bind) (0) | 2024.04.27 |
---|---|
BSD Socket API (0) | 2024.04.17 |
socket system call 3 (tcp_v4_init_sock) (0) | 2024.04.06 |
socket system call 2 (inet_create) (0) | 2024.03.31 |
socket system call 1 (syscall_socket ~ __sock_create) (0) | 2024.03.31 |