티스토리 뷰
우리가 쓰는 리눅스는 커널이라는 os로 관리하며, 이 커널에서 기본적인 함수나 매크로를 불러와서 사용한다.
또, 커널은 c언어로 구성되어 있기에, 소스 코드를 분석하면 c의 숙련도와 리눅스의 이해도를 높일 수 있다.
모든 부분을 전부 분석할 수는 없으므로, 필자가 가장 많이 쓰고 자신 있는 소켓 함수를 분석할 것이다.
커널 버전은 가장 최신인 6.10이고, ipv4환경에서 tcp 소켓 통신을 기준으로 분석하겠다.
우선 socket 함수부터 시작하겠다.
SYSCALL_DEFINE3
가장 먼저 볼 것은 리눅스에서 함수를 등록하는 매크로인 SYSCALL_DEFINEX이다.
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
이 매크로는 첫번째 인자의 값에 등록할 함수 명, 나머지는 등록한 함수에 사용할 인자들이 들어간다.
인자의 갯수에 따라 뒤에 X 대신 숫자가 붙고, 우리가 함수를 호출하면 가장 처음 실행되어 커널 내부 동작으로 넘겨주는 시스템 콜이다.
즉, 유저 스페이스와 커널 스페이스를 연결해 주는 인터페이스 역할을 한다고 볼 수 있다.
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
보면 인자가 3개인 socket 함수는 SYSCALL_DEFINE3을 사용하고, 첫 인자에 socket, 나머지에는 인자의 타입과 인자명이 들어있다. __sys_socket을 호출하는 것으로 역할을 마친다.
여담으로 실제로 동작하는 __SYSCALL_DEFINEx 내부 코드는 다음과 같다.
#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif
너무 복잡해서 간단히 설명하자면, GCC 컴파일러의 경고를 무시하고 오류를 확인한다.
그 후, 함수를 정의 및 시스템 호출에 등록하는 역할을 한다.
__sys_socket
int __sys_socket(int family, int type, int protocol)
{
struct socket *sock;
int flags;
sock = __sys_socket_create(family, type,
update_socket_protocol(family, type, protocol));
if (IS_ERR(sock))
return PTR_ERR(sock);
flags = type & ~SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
__sys_socket에서는 소켓을 생성하고, 그 소켓을 파일 디스크립터 테이블에 할당하는 것으로 나누어져 있다.
struct socket *sock;
int flags;
sock = __sys_socket_create(family, type,
update_socket_protocol(family, type, protocol));
if (IS_ERR(sock))
return PTR_ERR(sock);
소켓의 핵심 객체인 struct socket 을 선언하고, __sys_socket_create()을 호출하여 struct socket 할당 및 설정을 한다.
그 후 IS_ERR 매크로를 사용해 struct socket에 에러 여부를 확인하고, 에러가 발생시 에러 코드를 반환한다.
#define IS_ERR(ptr) ((unsigned long)(ptr) > (unsigned long)(-1000))
#define PTR_ERR(ptr) ((long)(ptr))
에러를 구분하는 법은 IS_ERR 매크로에서 인자의 포인터 값이 -MAX_ERRNO 이상인지 검사한다.
여기서 MAX_ERRNO는 시스템에서 정의된 최대 에러 코드 값이다.
그리고 ERR_PTR 매크로를 사용하여 정수형 에러 코드를 에러 포인터로 변환한다.
또, 특이한 점은 __sys_socket_create()에 update_socket_protocol라는 함수가 있는 것인데, 해당 함수의 내부는 다음과 같이 인자로 받은 protocol을 그대로 리턴한다.
__weak noinline int update_socket_protocol(int family, int type, int protocol)
{
return protocol;
}
이런 함수가 이후에도 종종 보이는 데, 이것은 사용자가 직접 함수를 정의하도록 의도적으로 만든 일종의 빈틈이다.
__weak 키워드를 함수 앞에 붙이면 약한 링크(Weak Linking)가 구현되어, 동일한 이름의 일반 선언 함수가 있다면 그 함수를 대신 호출한다.
즉, 일종의 오버라이딩(Overriding)을 c에서 구현한 것이다.
즉 만약 내가 다음과 같은 함수를 작성한다면, __weak 대신 아래의 함수가 대신 호출된다.
int update_socket_protocol(int family, int type, int protocol)
{
if (family == AF_INET && type == SOCK_STREAM) {
return IPPROTO_TCP;
}
return protocol;
}
참고로 noinline 키워드는 이 함수를 인라인 함수로 만들지 말라는 의미인데, 컴파일 최적화 기법에서 함수 호출 대신 본문을 그대로 넣는 경우가 있다.
이 경우 __weak를 사용한 오버라이딩(Overriding)이 제대로 작동이 되지 않기 때문에 같이 작성하는 것이다.
flags = type & ~SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
그 후, 플래그를 설정을 위해 type 인자에서 파일을 나타내는 비트만 남기고 나머지는 제거 후 flags에 저장한다.
SOCK_NONBLOCK은 소켓의 non-blocking 플래그 ,O_NONBLOCK은 fd 의 non-blocking 플래그이다,
non-blocking 파일이나 소켓 I/O이 호출 즉시 반환되어 프로세스가 대기 상태에 빠지지 않게 하는 옵션이다.
즉, accept나 recv같이 데이터를 받기 전까지는 멈춰서 대기하는 함수들을 더이상 대기하지 않게 한다.
소켓은 파일과 동일하게 관리되기에 소켓의 non-blocking 옵션이 비활성화라도, fd쪽이 활성화된다면 똑같이 적용받는다.
그리고 sock_map_fd()에 플래그 함께 struct socket을 인자로 주어서 소켓을 fd 테이블에 등록한다.
struct socket
다음 함수를 보기전 먼저 struct socket에 대해서 설명하겠다.
struct socket {
socket_state state;
short type;
unsigned long flags;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
struct socket_wq wq;
};
socket_state state | 현재 소켓의 상태 |
short type | 소켓의 유형. 예) SOCK_STREAM(TCP), SOCK_DGRAM(UDP) |
unsigned long flags | 소켓의 플래그. |
struct file *file | file 구조체 포인터. |
struct sock *sk | sock 구조체 포인터. 소켓 내부 상태와 동작 제어 |
const struct proto_ops *ops | 프로토콜 별 함수 포인터 모음. |
struct socket_wq wq | 소켓 대기 큐(wait queue) 관리 |
struct socket은 소켓의 핵심 구조체이며, 유저 스페이스와 커널 스페이스을 연결해주는 인터페이스의 역할을 수행한다.
소켓마다 하나의 struct socket이 존재하며, 소켓에 대한 정보를 관리한다.
socket_state
현재 소켓의 상태를 의미하며, 상태는 미리 정해놓은 enum의 값을 넣는다.
typedef enum {
SS_FREE = 0, /*소켓이 할당되지 않은 초기상태*/
SS_UNCONNECTED, /*어느 소켓과도 연결되지 않은 상태*/
SS_CONNECTING, /*연결 요청 중인 상태*/
SS_CONNECTED, /*다른 소켓과 연결된 상태*/
SS_DISCONNECTING /*연결 해제 중인 상태 */
} socket_state;
struct sock *sk
소켓 관련 구조체 세부 정보는 내부에 struct sock에서 관리한다.
주로 송수신 ip,port,ip 주소 체계, 소켓의 프로토콜 등 같은 커널 내부 동작 시 필요한 정보를 담고 있다.
struct sock {
struct sock_common __sk_common; // 공통 소켓 필드들
// 아래는 매크로 정의들로, __sk_common의 필드들을 접근하는 매크로들임
#define sk_node __sk_common.skc_node // 소켓의 노드
#define sk_nulls_node __sk_common.skc_nulls_node // 널 노드
#define sk_refcnt __sk_common.skc_refcnt // 참조 카운트
#define sk_tx_queue_mapping __sk_common.skc_tx_queue_mapping // 전송 큐 매핑
#ifdef CONFIG_SOCK_RX_QUEUE_MAPPING
#define sk_rx_queue_mapping __sk_common.skc_rx_queue_mapping // 수신 큐 매핑
#endif
#define sk_dontcopy_begin __sk_common.skc_dontcopy_begin // 복사 금지 시작
#define sk_dontcopy_end __sk_common.skc_dontcopy_end // 복사 금지 끝
#define sk_hash __sk_common.skc_hash // 해시값
#define sk_portpair __sk_common.skc_portpair // 포트 페어
#define sk_num __sk_common.skc_num // 소켓 번호
#define sk_dport __sk_common.skc_dport // 목적지 포트
#define sk_addrpair __sk_common.skc_addrpair // 주소 페어
#define sk_daddr __sk_common.skc_daddr // 목적지 주소
#define sk_rcv_saddr __sk_common.skc_rcv_saddr // 수신 주소
#define sk_family __sk_common.skc_family // 소켓 패밀리
#define sk_state __sk_common.skc_state // 소켓 상태
#define sk_reuse __sk_common.skc_reuse // 재사용 플래그
#define sk_reuseport __sk_common.skc_reuseport // 포트 재사용 플래그
#define sk_ipv6only __sk_common.skc_ipv6only // IPv6 전용 플래그
#define sk_net_refcnt __sk_common.skc_net_refcnt // 네트워크 참조 카운트
#define sk_bound_dev_if __sk_common.skc_bound_dev_if // 바운드 장치 인터페이스
#define sk_bind_node __sk_common.skc_bind_node // 바인드 노드
#define sk_prot __sk_common.skc_prot // 프로토콜 핸들러
#define sk_net __sk_common.skc_net // 네트워크 네임스페이스
#define sk_v6_daddr __sk_common.skc_v6_daddr // IPv6 목적지 주소
#define sk_v6_rcv_saddr __sk_common.skc_v6_rcv_saddr // IPv6 수신 주소
#define sk_cookie __sk_common.skc_cookie // 쿠키 값
#define sk_incoming_cpu __sk_common.skc_incoming_cpu // 수신 CPU
#define sk_flags __sk_common.skc_flags // 플래그들
#define sk_rxhash __sk_common.skc_rxhash // 수신 해시값
__cacheline_group_begin(sock_write_rx); // 캐시라인 그룹 시작 (쓰기 RX)
atomic_t sk_drops; // 드롭된 패킷 수
__s32 sk_peek_off; // 피크 오프셋
struct sk_buff_head sk_error_queue; // 에러 큐
struct sk_buff_head sk_receive_queue; // 수신 큐
struct {
atomic_t rmem_alloc; // 수신 메모리 할당
int len; // 길이
struct sk_buff *head; // 헤드 포인터
struct sk_buff *tail; // 테일 포인터
} sk_backlog; // 백로그
#define sk_rmem_alloc sk_backlog.rmem_alloc // 수신 메모리 할당 (매크로)
__cacheline_group_end(sock_write_rx); // 캐시라인 그룹 끝 (쓰기 RX)
__cacheline_group_begin(sock_read_rx); // 캐시라인 그룹 시작 (읽기 RX)
// 조기 디멀티플렉스 필드들
struct dst_entry __rcu *sk_rx_dst; // 수신 목적지
int sk_rx_dst_ifindex; // 수신 목적지 인터페이스 인덱스
u32 sk_rx_dst_cookie; // 수신 목적지 쿠키
#ifdef CONFIG_NET_RX_BUSY_POLL
unsigned int sk_ll_usec; // 로우 레이턴시 마이크로초
unsigned int sk_napi_id; // NAPI ID
u16 sk_busy_poll_budget; // 바쁜 폴링 예산
u8 sk_prefer_busy_poll; // 바쁜 폴링 선호 플래그
#endif
u8 sk_userlocks; // 사용자 잠금
int sk_rcvbuf; // 수신 버퍼 크기
struct sk_filter __rcu *sk_filter; // 소켓 필터
union {
struct socket_wq __rcu *sk_wq; // 소켓 대기열
// private:
struct socket_wq *sk_wq_raw; // 소켓 대기열 (raw)
// public:
};
void (*sk_data_ready)(struct sock *sk); // 데이터 준비 콜백
long sk_rcvtimeo; // 수신 타임아웃
int sk_rcvlowat; // 수신 저수준 한계
__cacheline_group_end(sock_read_rx); // 캐시라인 그룹 끝 (읽기 RX)
__cacheline_group_begin(sock_read_rxtx); // 캐시라인 그룹 시작 (읽기/쓰기 RXTX)
int sk_err; // 소켓 에러
struct socket *sk_socket; // 소켓 구조체
struct mem_cgroup *sk_memcg; // 메모리 cgroup
#ifdef CONFIG_XFRM
struct xfrm_policy __rcu *sk_policy[2]; // 전송 정책
#endif
__cacheline_group_end(sock_read_rxtx); // 캐시라인 그룹 끝 (읽기/쓰기 RXTX)
__cacheline_group_begin(sock_write_rxtx); // 캐시라인 그룹 시작 (쓰기 RXTX)
socket_lock_t sk_lock; // 소켓 잠금
u32 sk_reserved_mem; // 예약 메모리
int sk_forward_alloc; // 포워드 할당
u32 sk_tsflags; // 타임스탬프 플래그
__cacheline_group_end(sock_write_rxtx); // 캐시라인 그룹 끝 (쓰기 RXTX)
__cacheline_group_begin(sock_write_tx); // 캐시라인 그룹 시작 (쓰기 TX)
int sk_write_pending; // 쓰기 대기 중 플래그
atomic_t sk_omem_alloc; // 쓰기 메모리 할당
int sk_sndbuf; // 송신 버퍼 크기
int sk_wmem_queued; // 대기 중인 쓰기 메모리
refcount_t sk_wmem_alloc; // 쓰기 메모리 할당 참조 카운트
unsigned long sk_tsq_flags; // TSQ 플래그
union {
struct sk_buff *sk_send_head; // 송신 헤드
struct rb_root tcp_rtx_queue; // TCP 재전송 큐
};
struct sk_buff_head sk_write_queue; // 쓰기 큐
u32 sk_dst_pending_confirm; // 대기 중인 목적지 확인
u32 sk_pacing_status; // 페이싱 상태 (enum sk_pacing 참조)
struct page_frag sk_frag; // 페이지 프래그먼트
struct timer_list sk_timer; // 타이머
unsigned long sk_pacing_rate; // 페이싱 속도 (초당 바이트)
atomic_t sk_zckey; // 제로 카피 키
atomic_t sk_tskey; // 타임스탬프 키
__cacheline_group_end(sock_write_tx); // 캐시라인 그룹 끝 (쓰기 TX)
__cacheline_group_begin(sock_read_tx); // 캐시라인 그룹 시작 (읽기 TX)
unsigned long sk_max_pacing_rate; // 최대 페이싱 속도
long sk_sndtimeo; // 송신 타임아웃
u32 sk_priority; // 우선순위
u32 sk_mark; // 소켓 마크
struct dst_entry __rcu *sk_dst_cache; // 목적지 캐시
netdev_features_t sk_route_caps; // 라우트 기능
#ifdef CONFIG_SOCK_VALIDATE_XMIT
struct sk_buff* (*sk_validate_xmit_skb)(struct sock *sk,
struct net_device *dev,
struct sk_buff *skb); // 전송 패킷 검증
#endif
u16 sk_gso_type; // GSO 타입
u16 sk_gso_max_segs; // GSO 최대 세그먼트
unsigned int sk_gso_max_size; // GSO 최대 크기
gfp_t sk_allocation; // 할당 플래그
u32 sk_txhash; // 전송 해시값
u8 sk_pacing_shift; // 페이싱 시프트
bool sk_use_task_frag; // 태스크 프래그 사용 여부
__cacheline_group_end(sock_read_tx); // 캐시라인 그룹 끝 (읽기 TX)
// 비원자성 규칙으로 인해 모든 변경은 소켓 잠금으로 보호됨
u8 sk_gso_disabled : 1, // GSO 비활성화 플래그
sk_kern_sock : 1, // 커널 소켓 플래그
sk_no_check_tx : 1, // 송신 체크 비활성화 플래그
sk_no_check_rx : 1; // 수신 체크 비활성화 플래그
u8 sk_shutdown; // 종료 플래그
u16 sk_type; // 소켓 타입
u16 sk_protocol; // 소켓 프로토콜
unsigned long sk_lingertime; // 린거 타임 (종료 대기 시간)
struct proto *sk_prot_creator; // 프로토콜 생성자
rwlock_t sk_callback_lock; // 콜백 잠금
int sk_err_soft; // 소프트 에러
u32 sk_ack_backlog; // ACK 백로그
u32 sk_max_ack_backlog; // 최대 ACK 백로그
kuid_t sk_uid; // 소켓 소유자 UID
spinlock_t sk_peer_lock; // 피어 잠금
int sk_bind_phc; // 바인드 PHC
struct pid *sk_peer_pid; // 피어 PID
const struct cred *sk_peer_cred; // 피어 자격 증명
ktime_t sk_stamp; // 타임 스탬프
#if BITS_PER_LONG == 32
seqlock_t sk_stamp_seq; // 타임 스탬프 시퀀스 락
#endif
int sk_disconnects; // 연결 해제 수
u8 sk_txrehash; // 전송 해시 재생성 플래그
u8 sk_clockid; // 클락 ID
u8 sk_txtime_deadline_mode : 1, // TX 시간 제한 모드
sk_txtime_report_errors : 1, // TX 시간 오류 보고
sk_txtime_unused : 6; // TX 시간 미사용 비트
void *sk_user_data; // 사용자 데이터
#ifdef CONFIG_SECURITY
void *sk_security; // 보안 정보
#endif
struct sock_cgroup_data sk_cgrp_data; // 소켓 cgroup 데이터
void (*sk_state_change)(struct sock *sk); // 상태 변경 콜백
void (*sk_write_space)(struct sock *sk); // 쓰기 공간 콜백
void (*sk_error_report)(struct sock *sk); // 에러 보고 콜백
int (*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb); // 백로그 수신 핸들러
void (*sk_destruct)(struct sock *sk); // 소켓 파괴 콜백
struct sock_reuseport __rcu *sk_reuseport_cb; // 재사용 포트 콜백
#ifdef CONFIG_BPF_SYSCALL
struct bpf_local_storage __rcu *sk_bpf_storage; // BPF 로컬 저장소
#endif
struct rcu_head sk_rcu; // RCU 헤더
netns_tracker ns_tracker; // 네트워크 네임스페이스 추적기
};
내부에 네트워크 관련 필드는 struct sock_common에 따로 저장하는데, 이는 다른 소켓 구조체과 공유하는 필드들을 모아놓은 것이다.
실제 통신 연결 시 해당 구조체를 전송하여, 상대방에게 자신의 정보를 전달한다.
그외에도 송수신 큐, 쿠기, 오프셋 등의 값이 저장되지만 양이 많은 관계로 차후 자세히 설명하겠다.
이전에 설명한 파일 구조의 inode와 비슷한 역할을 수행한다.
실제로 struct sock과 struct socket을 따로 분리한 것은 file과 유사한 이유이다.
프로토콜이나 타입 등 소켓의 종류가 달라도 하나의 struct socket으로 표현하여 관리를 용이하게 하기 위함이다.
2024.05.28 - [개발/리눅스] - 리눅스 - 파일 시스템 VFS 2
리눅스 - 파일 시스템 VFS 2
이전글에서 이어서 진행하여 설명하겠습니다.2024.05.24 - [개발/리눅스] - 리눅스 - 파일 시스템 VFS 1(superblock, inode) 리눅스 - 파일 시스템 VFS 1(superblock,inode)2024.05.23 - [분류 전체보기] - 리눅스 - 모
lhs9602.tistory.com
struct proto_ops
소켓에서 제공하는 함수들의 포인터 테이블이다.
내부를 보면 bind나 connect같이 socket에서 함수 사용되는 함수들이 선언되어 있다.
struct proto_ops {
int family;
struct module *owner;
int (*release) (struct socket *sock);
int (*bind) (struct socket *sock,
struct sockaddr *myaddr,
int sockaddr_len);
int (*connect) (struct socket *sock,
struct sockaddr *vaddr,
int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1,
struct socket *sock2);
int (*accept) (struct socket *sock,
struct socket *newsock,
struct proto_accept_arg *arg);
int (*getname) (struct socket *sock,
struct sockaddr *addr,
int peer);
...
...
}
이렇게 포인터로 선언된 이유는 프로토콜이나 주소체계에 따라 바인드할 함수들이 다르기 때문이다.
예를 들어, tcp,ipv4에서 사용되는 함수들은 대체로 inet이라는 접두어가 붙는다. EX) inet_bind , inet_connect
이 함수들을 위의 테이블과 바인드하여, 코드 상에서 bind 같은 함수를 호출하면 알맞는 함수과 연결해준다.
마치 VFS에서 각 파일에 맞는 파일 시스템과 연결하는 것과 유사하다.
struct socket_wq wq
struct socket_wq {
wait_queue_head_t wait; // 대기 큐의 head
struct fasync_struc; // 비동기 IO를 관리
unsigned long flags; // 소켓 대기 큐와 관련된 다양한 플래그
struct rcu_head rcu; //RCU 헤더
}
소켓의 다양한 비동기 I/O 작업과 대기 큐를 처리하기 위한 구조체.
현재로써는 용도 파악이 어려워 나중에 계속.
listen()과 connect() 함수에서 사용되는 것으로 보인다
'개발 > c 언어' 카테고리의 다른 글
커널 소스 코드 분석 - socket 5 (1) | 2024.06.13 |
---|---|
커널 소스 코드 분석 - socket 4 (1) | 2024.06.11 |
커널 소스 코드 분석 - socket 3 (1) | 2024.06.07 |
커널 소스 코드 분석 - socket 2 (0) | 2024.06.03 |