티스토리 뷰
이전 글:
2024.05.30 - [개발/리눅스] - 커널 소스 코드 분석 - socket 1
커널 소스 코드 분석 - socket 1
우리가 쓰는 리눅스는 커널이라는 os로 관리하며, 이 커널에서 기본적인 함수나 매크로를 불러와서 사용한다.또, 커널은 c언어로 구성되어 있기에, 소스 코드를 분석하면 c의 숙련도와 리눅스의
lhs9602.tistory.com
__sys_socket_create
static struct socket *__sys_socket_create(int family, int type, int protocol)
{
struct socket *sock;
int retval;
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);
if ((type & ~SOCK_TYPE_MASK) & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return ERR_PTR(-EINVAL);
type &= SOCK_TYPE_MASK;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return ERR_PTR(retval);
return sock;
}
__sys_socket_create는 본격적으로 소켓을 생성하기 전 오류를 체크하는 함수이다.
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);
BUILD_BUG_ON은 컴파일 시에 컴파일 에러가 발생하는 지를 체크하는 매크로이다.
여기서는 소켓의 플래그 상수값이 올바르게 설정되었는지를 확인하고, 없다면 에러를 발생시킨다.
즉, 사용자가 소켓 플래그의 상수값을 건드렸는지를 확인하는 작업이다.
if ((type & ~SOCK_TYPE_MASK) & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return ERR_PTR(-EINVAL);
type &= SOCK_TYPE_MASK;
인자로 받은 type을 검증한다.
먼저 (type & ~SOCK_TYPE_MASK)에서 소켓 타입 비트를 제외한 나머지 비트를 추출한다.
type이 2(SOCK_STREAM 상수)이고, SOCK_TYPE_MASK는 0xF이라면 이진수로 각각 0010, 1111이다.
SOCK_TYPE_MASKdp ~로 반전시키면 0000, type과 &연산자를 할 경우 0000이 도출된다.
~(SOCK_CLOEXEC | SOCK_NONBLOCK)는 각각 십육 진수 02000000, 00004000이 들어가 있다.
#define SOCK_CLOEXEC O_CLOEXEC
#define SOCK_NONBLOCK O_NONBLOCK
#ifndef O_NONBLOCK
#define O_NONBLOCK 00004000
#endif
#ifndef O_CLOEXEC
#define O_CLOEXEC 02000000
#endif
이를 이진수로 변환시킨후 or연산 결과는 다음과 같다.
0x02000000 = 0000 0010 0000 0000 0000 0000 0000 0000
0x00004000 = 0000 0000 0000 0000 0100 0000 0000 0000
0x02000000 | 0x00004000= 0000 0010 0000 0000 0100 0000 0000 0000
이제 not 연산으로 반전시키면
~(0x02004000) = 1111 1101 1111 1111 1011 1111 1111 1111
이 된다.
마지막으로 구한 두 값에 &연산을 하면 다음과 같다.
1111 1101 1111 1111 1011 1111 1111 1111
&
0000 0000 0000 0000 0000 0000 0000 0000
=
0000 0000 0000 0000 0000 0000 0000 0000
즉 false가 되어서 ERR_PTR가 호출되지 않는다.
그 후 type 와 SOCK_TYPE_MASK이 &연산을 진행한다.
0010 & 1111 = 0010
굳이 이렇게하는 이유는 일관된 비트 마스크 유지를 위해, 불필요한 비트를 제거하고 소켓의 비트만 남기기 위해서다.
현재 커널에서 제공하는 소켓 type은 다음과 같다.
enum sock_type {
SOCK_DGRAM = 1,
SOCK_STREAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
개인적으로 확인해본 결과 값이 달라지는 것은 없는 것으로 보아, 이후 추가될 소켓 type을 위해 의도적으로 만든 것이 아닌가라는 생각도 된다.
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return ERR_PTR(retval);
return sock;
이후 sock_create에서 sock을 할당하고, 결괏값을 retval에 저장하며 에러 여부를 판단 후 반환한다.
sock_create
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
현재 실행 중인 프로세스의 네트워크 네임스페이스의 값을__sock_create에 넘겨주는 함수이다.
current는 현재 실행 중인 프로세스의 task_struct 포인터이고, nsproxy는 task_struct의 멤버로, 다양한 네임스페이스 포인터를 포함하는 구조체이다. net_ns는 nsproxy의 멤버로, 네트워크 네임스페이스를 가리키는 포인터.
주로 현재 보유하는 네트워크 디바이스나 전송 시 사용할 라우팅 테이블을 위해서 사용된다.
네트워크 네임스페이스?
- 하나의 호스트에서 여러 개의 격리된 네트워크 공간을 형성하는 기술.
- 각 네트워크 네임스페이스마다 네트워크 인터페이스, 라우팅 테이블, 포워딩 테이블을 가진다.
- 리눅스는 기본적으로 하나 이상의 네트워크 네임스페이스가 부팅 시 실행되고, ifconfig 명령어로 확인이 가능하다.
__sock_create
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;
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
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;
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE;
}
sock->type = type;
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
if (!try_module_get(pf->owner))
goto out_release;
rcu_read_unlock();
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
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;
}
EXPORT_SYMBOL(__sock_create);
family와 type의 인자를 체크하고 소켓 할당과 초기화를 진행하는 함수를 호출하는 함수이다.
해당 함수는 많이 생소한 구조를 가지고 있다.
가장 눈에 띄인은 것은 out_module_busy: 같은 문법인데, 이는 goto문을 사용하기 위한 라벨링이다.
goto로 해당 라벨을 입력하면 실행되는 코드가 해당 라벨로 이동한다.
c언어를 배울 때, goto문법은 쓰지말라는 말을 많이 듣는다. 그런데 커널에서는 의외로 자주 쓰이는 문법인 게 조금 놀랐다.
아마 속도나 효율성, 그리고 if문이 너무 중첩되는 것을 막기 위해서 사용하는 것이다.
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
두 인자의 값이 법위를 벗어나는 지를 확인하는 코드이다.
커널이 지원하는 값만 사용하도록 보장하기 위한 코드이다.
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET;
}
오래된 유형의 family사용을 알리는 코드이다.
소켓도 시간이 지남에 따라 발전하면서 더이상 지원하지 않거나 상수값이 변경되된 family가 존재한다.
만약 해당 family가 사용되던 때의 만든 코드를 사용할 경우, 에러문과 함께 family의 값을 변경시킨다.
int security_socket_create(int family, int type, int protocol, int kern)
{
return call_int_hook(socket_create, family, type, protocol, kern);
}
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
security_socket_create는 소켓 생성시 보안 정책에 따라 생성할 권한이 있는지 확인한다.
참고로 kern은 커널을 의미하는 값으로, 소켓 생성이 커널의 요청으로 진행한건지 사용자의 요청인지를 확인하는 값이다.
커널의 요청이면 1, 사용자의 요청이면 0이 들어간다.
#define call_int_hook(FUNC, ...) ({ \
int RC = LSM_RET_DEFAULT(FUNC); \
do { \
struct security_hook_list *P; \
\
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != LSM_RET_DEFAULT(FUNC)) \
break; \
} \
} while (0); \
RC; \
})
security_socket_create은 call_int_hook 매크로를 실행시키는 데, 내용은 복잡하지만 그냥 보안 정책들을 순회하면서 확인하는 것이다.
hlist_for_each_entry로 보안 훅 리스트를 순회하면서 각 훅을 호출하고, 반환 값을 확인합니다.
기본 반환 값이 아닌 값이 반환되면, 즉시 루프를 종료하고 그 값을 반환합니다.
그렇지 않으면 리스트의 모든 훅을 호출한 후 기본 반환 값을 반환합니다.
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE;
}
sock->type = type;
sock_alloc 함수에서 소켓을 생성하고, type값을 설정한다.
struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
inode = new_inode_pseudo(sock_mnt->mnt_sb);
if (!inode)
return NULL;
sock = SOCKET_I(inode);
inode->i_ino = get_next_ino(); // 새로운 inode 번호를 할당
inode->i_mode = S_IFSOCK | S_IRWXUGO; // 파일 모드를 설정
inode->i_uid = current_fsuid(); // 소유자 ID를 설정.
inode->i_gid = current_fsgid(); // 그룹 ID를 설정
inode->i_op = &sockfs_inode_ops; // inode 연산자를 설정
return sock;
}
sock_alloc은 sock과 inode을 생성하고, 서로 바인드시키는 함수이다.
소켓은 리눅스에서 파일로 취급하기에 vfs를 사용하고, inode를 필요로 한다.
new_inode_pseudo에서 vfs에 소켓에 대한 파일 시스템 추가 및 indoe 할당을 진행한다.
만약 inode를 할당할 수 없는 경우, 예를 들어 저장공간이 부족한 경우는 NULL을 반환한다.
SOCKET_I 매크로를 통해 inode와 sock을 바인드한다.
sock = SOCKET_I(inode);
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};
static inline struct socket *SOCKET_I(struct inode *inode)
{
return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
static_assert(__same_type(*(ptr), ((type *)0)->member) || \
__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
복잡하지만 간단히 설명하면, socket_alloc이라는 inode의 상위 구조체가 있는데, 해당 구조체에 있는 sock를 sock_alloc에서 생성한 sock에 할당한다.
이때 container_of라는 매크로가 사용되는데, 해당 매크로는 맴버의 상위 구조체를 찾는 기능을 한다.
즉, inode의 상위 구조체인 socket_alloc을 찾고, 해당 구조체의 맴버인 sock을 리턴하는 구조이다.
이렇게 하면 다음에 sock구조체만으로 연결된 inode을 찾을 수 있다.
사실 sock내부의 file이나 sock자체의 맴버로 inode를 넣으면 될 것 같은데 이렇게 하는 이유는 잘 모르겠다.
굳이 추즉한다면, sock의 크기가 너무 커지는 것을 방지하기 위함이 아닐까?
이후 inode에 파일 모드나 소유자 id등의 초기값을 할당하고, sock을 반환한다.
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
if (!try_module_get(pf->owner))
goto out_release;
rcu_read_unlock();
다시 __sock_create로 돌아온다.
코드 내용은 소켓 생성 전에 프로토콜 패밀리가 유효하고 사용 가능한지 확인하는 것이다.
또, pf에 인자로 넣은 family와 대응하는 net_families의 요소를 할당한다.
여기서는 rcu라는 특이한 문법이 보이는데, 이것은 커널에서 사용되는 동기화 기법이다.
리눅스에서 컴파일러 최적화에 의해서 실행 순서가 바뀔 수 있는데, rcu_read_lock과 rcu_read_unlock을 사용하면 해당 매크로 사이에 있는 코드는 실행 순서를 지키게된다.
이를 통해 쓰기와 읽기 작업이 안전하게 수행할 수 있다.
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
pf->create는 sock을 초기화 시키는 함수를 호출한다.
이때, create는 family값에 따라 다른 함수가 바인드되는데, ipv4는 inet_create가 실행된다.
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
참고로 함수 바인드는 다음과 같이 되어 있다.
inet_create는 다음 글에서 계속하겠다.
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
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;
마지막은 goto레이블에 따른 분기와 인자로 받은 res에 초기화된 sock을 할당하고 함수가 종료된다.
대부분은 에러 처리이지만, security_socket_post_create라는 함수가 눈에 띄는데, 해당 함수는 0을 그대로 반환하는 빕함수이다.
static inline int security_socket_post_create(struct socket *sock,
int family,
int type,
int protocol, int kern)
{
return 0;
}
아마 사용자가 직접 작성하도록 남긴 빈틈이다.
앞으로도 이런 함수가 자주 보일 것인데, 이후는 설명은 생략하고 무시하겠다.
'개발 > c 언어' 카테고리의 다른 글
커널 소스 코드 분석 - socket 5 (1) | 2024.06.13 |
---|---|
커널 소스 코드 분석 - socket 4 (1) | 2024.06.11 |
커널 소스 코드 분석 - socket 3 (1) | 2024.06.07 |
커널 소스 코드 분석 - socket 1 (0) | 2024.05.30 |