티스토리 뷰

2024.06.11 - [개발/c 언어] - 커널 소스 코드 분석 - socket 4

 

커널 소스 코드 분석 - socket 4

2024.06.07 - [개발/c 언어] - 커널 소스 코드 분석 - socket 3 커널 소스 코드 분석 - socket 32024.06.03 - [개발/리눅스] - 커널 소스 코드 분석 - socket 2 커널 소스 코드 분석 - socket 2이전 글:2024.05.30 - [개발/

lhs9602.tistory.com

 

 

지금까지는 소켓의 구조체들을 할당 및 초기화 작업을 진행했다면, 이제부터는 만들어진 소켓을 fd테이블에 등록하는 동작을 실행한다.

int __sys_socket(int family, int type, int protocol)
{
	...
	return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

이전에 __sys_socket로 돌아와서, 생성된 sock을 sock_map_fd에서 fd 테이블에 등록하고 fd값을 반환한다.

 

 

sock_map_fd


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

먼저 file 구조체인 newfile을 선언하고, get_unused_fd_flags을 실행하여 비어있는 fd값을 할당한다.

만약 fd값이 음수이면 에러코드를 반환한 것이기에, sock_release에서 기존에 생성한 sock과 다른 구조체를 해제한다.

그 후 에러코드가 담긴 fd를 반환한다.

참고로 unlikely는 false 한 값을 받으면 true를 반환하는 매크로이다.

 

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

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

참고로 get_unused_fd_flags는 __get_unused_fd_flags를 거쳐서, alloc_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);
}

fd값이 에러 코드가 아니하면 sock_alloc_file으로 sock과 newfile을 바인딩한다.

newfile에 에러가 나지 않는다면 fd_install로 newfile을 fd 테이블에 할당하고, fd값을 반환한다.

 

만약 에러가 발생하면, fd를 해제하고, 에러를 반환한다.

 

alloc_fd


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

사용하지 않는 fd값을 할당하고, 반환하는 함수이다.

 

우선 current는 현재 실행 중인 프로세스를 관리하는 task_struct 구조체를 반환하는 매크로이다.

files_struct는 오픈된 파일들의 정보를 가진 fd테이블을 관리하는 구조체이다.

즉, current->files에 저장된 현재 실행 중인 프로세스의 fd테이블 정보를 할당하는 것이다.

 

struct fdtable *fdt는 files_struct의 하위 구조체로 오픈된 파일들의 포인터를 저장하는 fd테이블이다.

struct files_struct {
    atomic_t count;                       // 참조 카운트
    bool resize_in_progress;              // 파일 디스크립터 테이블 크기 조 여부
    wait_queue_head_t resize_wait;        // 크기 조정 대기 큐

    struct fdtable __rcu *fdt;            // 현재 파일 디스크립터 테이블에 대한 포인터
    struct fdtable fdtab;                 // 기본 파일 디스크립터 테이블

    spinlock_t file_lock ____cacheline_aligned_in_smp; // 파일 디스크립터 테이블에 대한 락
    unsigned int next_fd;                 // 다음에 사용할 파일 디스크립터

    unsigned long close_on_exec_init[1];  // close-on-exec 비트맵의 초기 값
    unsigned long open_fds_init[1];       // 열린 파일 디스크립터 비트맵의 초기 값
    unsigned long full_fds_bits_init[1];  // 전체 파일 디스크립터 비트맵의 초기 값

    struct file __rcu * fd_array[NR_OPEN_DEFAULT];  // 기본 파일 디스크립터 배열
};

struct fdtable {
    unsigned int max_fds;                 // 이 테이블에서 관리할 수 있는 최대 파일 디스크립터 수
    struct file __rcu **fd;               // 파일 디스크립터 포인터 배열
    unsigned long *close_on_exec;         // close-on-exec 비트맵
    unsigned long *open_fds;              // 열린 파일 디스크립터 비트맵
    unsigned long *full_fds_bits;         // 전체 파일 디스크립터 비트맵
    struct rcu_head rcu;                  // RCU(읽기-복사-업데이트) 헤더
};

이후 spin_lock으로 fd테이블에 lock을 거는데, 이는 동시에 파일을 오픈하여 fd값이 겹치지 않도록 동기화 문제를 해소하기 위함이다.

 

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

repeat 레이블이 있는데, 이는 goto문을 사용하여 fd값을 찾기 위함이다.

 

먼저 files_fdtable함수로 files의 fdtable을 멤버를 fdt에 할당한다.

그런 다음 인자로 받은 start에 담긴 0을 fd에 대입 한다.

 

그 후 fd값과 files->next_fd을 비교하는데, next_fd는 사용 가능한 fd값이 저장되어 있다.

즉, next_fd는 fd테이블에 파일이 할당 및 해제될 때마다, 다음에 쓸 fd값을 저장하는 것이다.

이때 next_fd보다 작다면, fd의 값을 files->next_fd을 대입한다.

 

참고로 오픈된 파일은 fd값을 3부터 할당하는데, 이는 0,1,2 값에는 이미 할당된 기본값이 존재하기 때문이다.

 

기본적으로 할당되는 파일 디스크립터
0 : 표준 입력(Standard Input) / STDIN_FILENO
1 : 표준 출력(Standard Output) / STDOUT_FILENO
2 : 표준 에러(Standard Error) / STDERR_FILENO

 

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

	error = -EMFILE;
	if (fd >= end)
		goto out;

fd와 fdt->max_fd를 비교하는데, max_fds는 fd테이블의 최댓값을 의미한다.

fd가 max_fd보다 작으면, fd 테이블가 전부 채워있지 않기에 할당이 가능하다.

그 후, find_next_fd에서 사용 가능한 fd을 찾아서 할당한다.

 

그러나 이미 이전 코드에서 next_fd로 사용가능한 fd를 찾아 할당했는데, 왜 이런 작업을 반복하는가?

그 이유는 next_fd에 있는 fd값이 사용 중일 수도 있기 때문이다.

 

이게 무슨 말이냐면, 이전에 다른 코드에서 파일을 fd테이블에 할당하려다 실패한 경우가 있을 수 있다.

그런 경우 next_fd에 사용 중인 파일이 할당되어 있을 수 있다.

그 외에 커널 오류나 next_fd값을 임의 조작하는 등의 원인으로 next_fd에 잘못된 값이 올 수 있다. 

따라서 find_next_fd로 next_fd값이 정말로 비어있는지 확인하고, 비어있지 않다면 다른 비어있는 fd값을 반환하는 것이다. 

 

만약 fd가 max_fd보다 크면, 더 이상 fd 테이블에 등록할 수 없기에 에러를 반환한다.

 

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

	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;

이후로는 fd값을 할당한 이후에 사후처리하는 과정이다.

먼저, expand_files로 fd테이블을 확장을 시도한다.

메모리를 효율적으로 관리하기 위해  fd테이블은 동적으로 크기가 변한다.

 

여기서 실패한다면 바로 에러를 반환하지만, 성공하면 repeat의 시작 부분으로 가서 반복한다.

성공할 때, 다시 돌아가는 이유는 확장 이후에 fd값을 다시 할당해야 할 수도 있기 때문이다.

 

이후 files->next_fd값에 fd+1을 한 값을 넣어서 다음 사용 가능한 fd를 지정한다.

fd테이블에 fd값이 오픈 상태로 변경하고, 플래그 설정을 진행 후 error에 fd값을 대입한다.

 

#if 1
	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;
}

rcu_access_pointer로 fd값이 정말 비었는지 마지막으로 확인한다.

그리고 락을 제거한 후, error에 담긴 fd값을 반환한다.

 

sock_alloc_file


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

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

이 함수는 fd테이블에 할당할 file구조체 생성하고 소켓에 할당하는 함수이다.

 

이전 글에서 설명한 대로 소켓이 직접적으로 fd테이블에 등록되는 게 아니라 file구조체를 경유해서 등록하는 것이다.

때문에 fd에 쓸 가짜 file 구조체를 생성하고 할당하는 것이다.

2024.05.28 - [개발/리눅스] - 리눅스 - 파일 시스템 VFS 2

 

리눅스 - 파일 시스템 VFS 2

이전글에서 이어서 진행하여 설명하겠습니다.2024.05.24 - [개발/리눅스] - 리눅스 - 파일 시스템 VFS 1(superblock, inode) 리눅스 - 파일 시스템 VFS 1(superblock,inode)2024.05.23 - [분류 전체보기] - 리눅스 - 모

lhs9602.tistory.com

 

 

alloc_file_pseudo로 file구조체를 생성하고, 초기값을 설정한다.

이때 이전에 생성한 inode를 sock으로 캐스팅하여, file 구조체에 할당한다.

 

만약 file 생성에 실패할 경우  sock_release로 sock을 해제한다.

성공한 경우에는 file->private_data에 sock을 할당한다. 

이후 소켓 함수에서는 fd 테이블에서 file을 거쳐서 private_data에 저장된 sock을 찾는 것이 기본 로직이다.

 

 

 

아래는 지금까지의 함수와 구조체를 도식화한 표이다.

 

 

 

후기


솔직히 말하면 socket 함수 하나에 이렇게 애먹을 줄은 몰랐다. 

사용할 때는 한 줄이면 되는 함수가 이 정도로 길고 복잡할 줄이야...........

최고의 c 오픈 소스는 커널이라는 생각으로 공부를 시작한 거지만, 역시 프로의 벽은 높은 것 같다.

 

찾아보니 수정, 배포, 공유를 추진하는 gnu선언이라는 것을 커널 개발자가 만들었다는 것을 알았다.

근데 커널 소스를 보면 볼수록, 악의적으로 코드를 이해하지 못하게 한 게 아닐까? 하는 생각이 절로 들었다.

나중에는 그게 다 이유가 있는 것을 알았고, 공부도 되었지만 초장기에는 욕이 절로 나왔다.

 

덕분에 거의 2주 동안 이것만 하느라 다른 일정이 많이 밀렸다.

사실 코드 자체를 읽는 시간보다는 잘못 이해하고 수정하는 시간 때문에 더 오래 걸린 것 같다.

커널에 대한 이해도를 높일 수 있는 기회였지만, 다른 소켓 함수들은 나중에 시간 날 때 해야 될 것 같다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함