Linux v6.18.37 · 개념과 코드 읽기

소켓 생성: 정수 핸들보다 먼저 필요한 프로토콜 객체

이 코드는 어떤 문제를 푸나요?

socket 시스템 호출은 단순히 숫자 하나를 발급하는 작업이 아닙니다. 주소 체계와 소켓 종류를 검사하고, 사용할 프로토콜 구현을 찾은 뒤 그 구현의 상태를 만들어야 합니다. __sock_create 전체는 공통 socket 객체와 프로토콜 객체를 연결하며 모듈과 보안 검사의 실패도 정리합니다. 파일 디스크립터 배정은 이 함수 바깥의 후속 단계입니다.

읽을 범위: v6.18.37 · net/socket.c · __sock_create 1535–1648행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

socket과 sock

struct socket은 파일 연산과 프로토콜 연산을 연결하는 공통 객체이고 sock->sk가 가리키는 struct sock은 프로토콜 측 상태입니다. 변수 이름 sock만 보고 구조체 종류를 혼동하지 않아야 합니다.

family·type·protocol

family는 주소 체계, type은 스트림·데이터그램 등의 사용 방식, protocol은 그 안에서 선택할 프로토콜입니다. protocol 0은 주소 체계 구현에 적절한 기본값 선택을 맡길 수 있습니다.

RCU와 모듈 참조

프로토콜 표를 안전하게 읽는 보호와 그 구현 코드가 사용 중에 제거되지 않게 하는 보호는 다릅니다. 표 조회에는 RCU를, 콜백 실행 수명에는 모듈 참조를 사용합니다.

LSM 보안 훅

생성 전후에 보안 정책이 해당 소켓을 허용하는지 검사하는 호출입니다. 입력 숫자의 범위 검사와 다른 단계입니다.

처음 읽을 때

IPv4 스트림 소켓을 만드는 예시를 생각하되 소켓 생성만으로 TCP 연결까지 끝났다고 보지 마십시오. 생성, bind, connect, 데이터 송수신은 별도 단계입니다.

더 깊이 살펴볼 때

pf->owner와 sock->ops->owner 참조의 역할을 나누어 추적하십시오. 생성 콜백을 실행하는 동안 필요한 참조와 소켓이 살아 있는 동안 필요한 참조가 바뀌며 오류 라벨은 어느 자원을 이미 확보했는지에 따라 달라집니다.

그림으로 보는 변화

소켓 생성: 정수 핸들보다 먼저 필요한 프로토콜 객체의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 요청 형식을 검사합니다

family·type 범위와 보안 정책을 확인하고 공통 socket 객체를 만듭니다.

화살표는 생성 단계의 순서이며 네트워크 패킷 발행이 아닙니다.

2. 프로토콜 구현을 연결합니다

family 표에서 구현을 찾고 모듈 참조를 확보한 뒤 create 콜백을 실행합니다.

화살표는 공통 객체에서 프로토콜 구현으로 이어지는 함수 호출과 참조 관계입니다.

3. 결과를 넘기거나 정리합니다

성공하면 socket을 결과 포인터에 넣고 실패하면 해당 단계까지 확보한 객체와 참조를 해제합니다.

되돌아가는 화살표는 실패 시 정리이며 상대 서버에 연결을 끊는 메시지가 아닙니다.

__sock_create를 한 줄씩 읽기

줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

int __sock_create(struct net *net, int family, int type, int protocol,
			 struct socket **res, int kern)
{
	int err;
	struct socket *sock;
	const struct net_proto_family *pf;

	/*
	 *      Check protocol is in range
	 */
	if (family < 0 || family >= NPROTO)
		return -EAFNOSUPPORT;
	if (type < 0 || type >= SOCK_MAX)
		return -EINVAL;

	/* Compatibility.

	   This uglymoron is moved from INET layer to here to avoid
	   deadlock in module load.
	 */
	if (family == PF_INET && type == SOCK_PACKET) {
		pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
			     current->comm);
		family = PF_PACKET;
	}

	err = security_socket_create(family, type, protocol, kern);
	if (err)
		return err;

	/*
	 *	Allocate the socket and allow the family to set things up. if
	 *	the protocol is 0, the family is instructed to select an appropriate
	 *	default.
	 */
	sock = sock_alloc();
	if (!sock) {
		net_warn_ratelimited("socket: no more sockets\n");
		return -ENFILE;	/* Not exactly a match, but its the
				   closest posix thing */
	}

	sock->type = type;

#ifdef CONFIG_MODULES
	/* Attempt to load a protocol module if the find failed.
	 *
	 * 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
	 * requested real, full-featured networking support upon configuration.
	 * Otherwise module support will break!
	 */
	if (rcu_access_pointer(net_families[family]) == NULL)
		request_module("net-pf-%d", family);
#endif

	rcu_read_lock();
	pf = rcu_dereference(net_families[family]);
	err = -EAFNOSUPPORT;
	if (!pf)
		goto out_release;

	/*
	 * We will call the ->create function, that possibly is in a loadable
	 * module, so we have to bump that loadable module refcnt first.
	 */
	if (!try_module_get(pf->owner))
		goto out_release;

	/* Now protected by module ref count */
	rcu_read_unlock();

	err = pf->create(net, sock, protocol, kern);
	if (err < 0) {
		/* ->create should release the allocated sock->sk object on error
		 * and make sure sock->sk is set to NULL to avoid use-after-free
		 */
		DEBUG_NET_WARN_ONCE(sock->sk,
				    "%ps must clear sock->sk on failure, family: %d, type: %d, protocol: %d\n",
				    pf->create, family, type, protocol);
		goto out_module_put;
	}

	/*
	 * Now to bump the refcnt of the [loadable] module that owns this
	 * socket at sock_release time we decrement its refcnt.
	 */
	if (!try_module_get(sock->ops->owner))
		goto out_module_busy;

	/*
	 * Now that we're done with the ->create function, the [loadable]
	 * module can have its refcnt decremented
	 */
	module_put(pf->owner);
	err = security_socket_post_create(sock, family, type, protocol, kern);
	if (err)
		goto out_sock_release;
	*res = sock;

	return 0;

out_module_busy:
	err = -EAFNOSUPPORT;
out_module_put:
	sock->ops = NULL;
	module_put(pf->owner);
out_sock_release:
	sock_release(sock);
	return err;

out_release:
	rcu_read_unlock();
	goto out_sock_release;
}
int __sock_create(struct net *net, int family, int type, int protocol,

네임스페이스와 주소 체계, 소켓 종류, 프로토콜 번호를 받습니다.

			 struct socket **res, int kern)

생성한 socket을 기록할 결과 포인터와 커널 내부용 생성인지 나타내는 kern 값을 받습니다.

	struct socket *sock;

새로 할당할 공통 struct socket의 포인터입니다. struct sock 포인터와 구분합니다.

	const struct net_proto_family *pf;

주소 체계에 등록된 create 함수와 모듈 정보를 가리킵니다.

	if (family < 0 || family >= NPROTO)

family 값이 프로토콜 표의 유효 범위 안에 있는지 확인합니다.

		return -EAFNOSUPPORT;

지원할 수 없는 주소 체계 범위를 오류로 돌려줍니다.

	if (type < 0 || type >= SOCK_MAX)

소켓 종류도 유효한 열거값 범위인지 검사합니다.

		return -EINVAL;

잘못된 종류를 -EINVAL로 반환합니다.

	if (family == PF_INET && type == SOCK_PACKET) {

오래된 PF_INET·SOCK_PACKET 조합의 호환 경로인지 확인합니다.

		pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",

구식 인터페이스를 사용한다는 메시지를 한 번 출력합니다.

			     current->comm);

메시지에 현재 태스크 이름을 넣습니다.

		family = PF_PACKET;

구식 요청을 패킷 소켓 주소 체계로 변환합니다.

	err = security_socket_create(family, type, protocol, kern);

보안 모듈에 이 종류의 소켓 생성을 허용하는지 묻습니다.

	if (err)

보안 검사 실패 여부를 확인합니다.

		return err;

아직 소켓을 할당하지 않았으므로 해당 오류만 반환합니다.

	sock = sock_alloc();

공통 socket 객체를 할당합니다.

		net_warn_ratelimited("socket: no more sockets\n");

반복 오류가 로그를 과도하게 채우지 않도록 빈도를 제한해 메시지를 출력합니다.

		return -ENFILE;	/* Not exactly a match, but its the

소켓 객체를 만들지 못한 오류를 -ENFILE로 반환합니다. 실제 디스크 파일 수를 센 결과라는 뜻은 아닙니다.

	sock->type = type;

생성한 socket에 요청한 종류를 저장합니다.

#ifdef CONFIG_MODULES

프로토콜 구현을 별도 커널 모듈로 가져올 수 있는 빌드인지 확인합니다. 켜져 있으면 아직 등록되지 않은 소켓 주소 계열을 request_module로 불러오려 시도하며, 꺼진 빌드는 그 자동 로드 경로를 포함하지 않습니다. 이 선택은 전처리 단계에서 이루어지며, CPU가 실행 중 이 줄의 조건을 검사하지 않습니다.

	if (rcu_access_pointer(net_families[family]) == NULL)

요청한 family 구현이 현재 등록되어 있지 않은지 가볍게 확인합니다.

		request_module("net-pf-%d", family);

해당 family의 프로토콜 모듈을 로드하도록 요청합니다. 로드 성공을 가정하지 않고 아래에서 다시 조회합니다.

#endif

모듈 자동 로드 조건부 코드가 끝납니다.

	rcu_read_lock();

등록된 프로토콜 포인터를 안전하게 읽도록 RCU 읽기 구간을 시작합니다.

	pf = rcu_dereference(net_families[family]);

family 인덱스로 실제 프로토콜 구현을 얻습니다.

	err = -EAFNOSUPPORT;

구현을 못 찾는 경로의 기본 오류를 설정합니다.

		goto out_release;

RCU 보호를 끝내고 socket을 해제하는 경로로 갑니다.

	if (!try_module_get(pf->owner))

create 함수가 실행되는 동안 구현 모듈이 제거되지 않도록 참조를 얻습니다.

		goto out_release;

제거 중인 모듈 등으로 참조를 얻지 못하면 생성하지 않고 정리합니다.

	rcu_read_unlock();

이제 모듈 참조로 구현 수명을 보호하므로 RCU 읽기 구간을 끝냅니다.

	err = pf->create(net, sock, protocol, kern);

주소 체계별 create 함수에 네임스페이스와 socket을 넘겨 프로토콜 상태를 만듭니다.

	if (err < 0) {

프로토콜 생성 실패를 검사합니다.

		DEBUG_NET_WARN_ONCE(sock->sk,

실패한 create 함수가 sock->sk를 정리했는지 디버그 경고로 확인합니다.

				    "%ps must clear sock->sk on failure, family: %d, type: %d, protocol: %d\n",

실패 시 포인터를 NULL로 해야 한다는 진단 문구입니다. 해제한 프로토콜 객체를 다시 사용하지 않도록 하는 규칙입니다.

				    pf->create, family, type, protocol);

문제를 일으킨 create 함수와 생성 인자들을 경고에 표시합니다.

		goto out_module_put;

생성용 모듈 참조와 공통 socket을 내려놓는 경로로 갑니다.

	if (!try_module_get(sock->ops->owner))

소켓 연산을 제공할 모듈을 소켓 수명 동안 유지하기 위한 참조를 얻습니다.

		goto out_module_busy;

연산 모듈을 유지할 수 없으면 생성 성공으로 넘기지 않고 정리합니다.

	module_put(pf->owner);

create 함수 실행이 끝났으므로 생성 단계에만 필요했던 모듈 참조를 내려놓습니다.

	err = security_socket_post_create(sock, family, type, protocol, kern);

만들어진 socket에 대해 보안 모듈의 후속 검사를 수행합니다.

	if (err)

생성 후 보안 검사에서 거부됐는지 확인합니다.

		goto out_sock_release;

이미 만들어진 소켓을 정상 해제 경로로 정리합니다.

	*res = sock;

모든 단계를 통과한 socket 포인터를 호출자의 결과 위치에 넣습니다.

	return 0;

생성 성공을 반환합니다. 아직 사용자 fd 번호를 반환하는 위치는 아닙니다.

out_module_busy:

소켓 연산 모듈을 유지할 수 없을 때 도달하는 오류 위치입니다.

	err = -EAFNOSUPPORT;

주소 체계 사용을 계속할 수 없다는 오류를 설정합니다.

out_module_put:

생성용 모듈 참조를 정리할 공통 위치입니다.

	sock->ops = NULL;

확보하지 못한 연산 모듈에 대해 잘못된 해제 콜백을 실행하지 않도록 연산 표를 비웁니다.

	module_put(pf->owner);

생성 콜백의 모듈 참조를 내려놓습니다.

out_sock_release:

공통 socket 자원을 해제하는 위치입니다.

	sock_release(sock);

현재 설정된 연산과 객체 상태에 맞춰 소켓을 정리합니다.

	return err;

실패 원인을 호출자에게 반환합니다.

out_release:

프로토콜 표를 조회하는 RCU 구간 안에서 실패했을 때 오는 위치입니다.

	rcu_read_unlock();

아직 유지한 RCU 읽기 보호를 끝냅니다.

	goto out_sock_release;

할당한 공통 socket을 해제하는 공통 경로로 이동합니다.

함께 생각해 볼 질문

이 함수가 반환한 socket 포인터가 사용자 fd인가요?

아닙니다. 커널 객체 포인터이며 파일 디스크립터를 만드는 후속 경로가 따로 있습니다.

소켓 생성 때 RCU 잠금을 끝까지 잡고 있으면 되지 않나요?

생성 콜백은 잠들 수 있고 실행 중 구현 모듈의 수명도 보장해야 합니다. 코드에서는 모듈 참조를 얻은 뒤 RCU 읽기 구간을 끝냅니다.

protocol이 0이면 사용할 프로토콜이 없다는 뜻인가요?

주소 체계의 create 구현이 적절한 기본 프로토콜을 선택하도록 요청할 수 있습니다.

출처와 읽은 범위

Linux stable v6.18.37 · net/socket.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑