Unix Domain Socket
Linux AF_UNIX(Unix Domain Socket) 소켓(Socket)의 커널 내부 구현을 심층 분석합니다. unix_sock 자료구조, 파일시스템(Filesystem) 소켓과 추상 네임스페이스(Namespace), 데이터 전송 경로, SCM_RIGHTS를 통한 파일 디스크립터(File Descriptor) 전달, SCM_CREDENTIALS 자격 증명, 가비지 컬렉션, socketpair IPC 패턴, 보안 모델, TCP 대비 성능 특성, 운영 디버깅(Debugging)까지 다룹니다.
핵심 요약
- AF_UNIX — 같은 호스트 내 프로세스(Process) 간 통신 전용 소켓 패밀리(네트워크 스택 미경유)
- SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET — 바이트 스트림, 데이터그램, 순서 보장(Ordering) 메시지 세 가지 타입 지원
- SCM_RIGHTS — 프로세스 간 파일 디스크립터 전달(ancillary data)
- SCM_CREDENTIALS — PID/UID/GID 자격 증명 전달 및 검증
- 추상 네임스페이스 — Linux 전용, 파일시스템 경로 없이 \0 접두사로 바인딩
- socketpair() — 연결된 소켓 쌍을 한 번에 생성하는 경량 IPC
단계별 이해
- 소켓 구조 이해
struct unix_sock과 sockaddr_un 구조를 먼저 파악합니다. - 생성-연결 경로
socket(), bind(), listen(), accept(), connect()의 커널 내부 호출 경로를 추적합니다. - 데이터 전송
sendmsg/recvmsg에서 sk_buff 없이 직접 복사가 이루어지는 경로를 확인합니다. - 보조 데이터
SCM_RIGHTS, SCM_CREDENTIALS로 파일 디스크립터와 자격 증명을 전달하는 메커니즘을 이해합니다. - 운영 디버깅
ss, /proc/net/unix, bpftrace로 소켓 상태를 모니터링합니다.
AF_UNIX 소켓 아키텍처
Unix Domain Socket(이하 UDS)은 net/unix/ 디렉터리에 구현되어 있으며, 네트워크 프로토콜 스택(IP, TCP/UDP)을 전혀 거치지 않습니다. 핵심 자료구조인 struct unix_sock은 struct sock을 내장(embed)하여 소켓 프레임워크와 통합됩니다.
/* include/net/af_unix.h */
struct unix_sock {
struct sock sk; /* 소켓 공통 구조체 (내장) */
struct unix_address *addr; /* 바인딩 주소 (sockaddr_un) */
struct path path; /* 파일시스템 경로 (바인딩된 경우) */
struct mutex iolock; /* I/O 직렬화 */
struct sock *peer; /* 연결된 상대 소켓 */
struct sock *listener; /* STREAM: 수신 대기 소켓 */
struct unix_vertex *vertex; /* GC 그래프 정점 */
spinlock_t lock;
unsigned long gc_flags; /* GC 상태 플래그 */
wait_queue_head_t peer_wait; /* DGRAM: 피어 대기 큐 */
struct scm_stat scm_stat; /* SCM 통계 */
struct sk_buff_head oob_skb; /* OOB 데이터 큐 */
};
/* include/uapi/linux/un.h */
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX (== AF_LOCAL) */
char sun_path[108]; /* 소켓 경로 (최대 108바이트) */
};
/* 바인딩 유형 3가지:
* 1. 파일시스템 소켓: sun_path = "/var/run/daemon.sock"
* → VFS에 S_IFSOCK 타입 inode 생성
* 2. 추상 네임스페이스: sun_path[0] = '\0', 이후 이름
* → 파일시스템에 흔적 없음 (Linux 전용)
* 3. 이름 없는 소켓: bind() 호출 안 함
* → socketpair(), connect() 전 클라이언트
*/
unlink() 전까지 경로에 남아 있으며, 파일 권한으로 접근을 제어합니다. 추상 네임스페이스 소켓은 모든 소켓이 닫히면 자동으로 사라지지만, 파일 권한이 없으므로 같은 네트워크 네임스페이스 내 모든 프로세스가 접근할 수 있습니다.
소스 파일 구조
UDS의 커널 구현은 net/unix/ 디렉터리에 집중되어 있습니다. 각 파일의 역할을 이해하면 커널 소스를 읽을 때 진입점(Entry Point)을 빠르게 찾을 수 있습니다.
| 파일 | 역할 | 주요 함수/구조체(Struct) |
|---|---|---|
net/unix/af_unix.c |
핵심 구현: 소켓 생성, 바인딩, 연결, 데이터 전송 | unix_create(), unix_bind(), unix_stream_sendmsg(), unix_dgram_sendmsg() |
net/unix/garbage.c |
가비지 컬렉션 (순환 참조 fd 회수) | unix_gc(), unix_inflight(), unix_notinflight() |
net/unix/scm.c |
보조 데이터 (ancillary data) 처리 | unix_attach_fds(), unix_detach_fds() |
net/unix/diag.c |
sock_diag 인터페이스 (ss 명령어 지원) |
unix_diag_handler, sk_diag_fill() |
net/unix/sysctl_net_unix.c |
sysctl 파라미터 등록 | net.unix.max_dgram_qlen |
include/net/af_unix.h |
struct unix_sock, 내부 API 선언 | unix_sock, unix_address, unix_peer() |
include/uapi/linux/un.h |
사용자 공간(User Space) API (sockaddr_un) | sockaddr_un |
/* net/unix/af_unix.c — 프로토콜 패밀리 등록 */
static const struct net_proto_family unix_family_ops = {
.family = PF_UNIX,
.create = unix_create,
.owner = THIS_MODULE,
};
/* 초기화: __init unix_net_init() 에서 호출
* → sock_register(&unix_family_ops)
* → socket(AF_UNIX, ...) 호출 시 unix_create()로 디스패치
*/
/* 해시 테이블 구조:
* net->unx.table.buckets[UNIX_HASH_SIZE]
* → 바인딩된 소켓을 이름(경로 또는 추상)으로 검색
* → unix_find_other()가 connect() 시 서버 소켓 조회에 사용
*
* 해시 함수:
* - 파일시스템 소켓: inode 번호 + device 번호 기반
* - 추상 네임스페이스: 이름 문자열 해시 (unix_abstract_hash())
*/
소스 읽기 팁: UDS 코드를 읽을 때는 af_unix.c의 unix_stream_ops / unix_dgram_ops / unix_seqpacket_ops 세 가지 proto_ops 구조체를 먼저 확인하세요. 각 소켓 타입의 시스콜 핸들러(Handler)가 여기에 매핑(Mapping)됩니다. 커널 소스 탐색에 대한 자세한 방법은 소스 코드 읽기 문서를 참고하세요.
소켓 생성과 바인딩
UDS는 socket(AF_UNIX, type, 0)으로 생성합니다. type은 세 가지 중 하나입니다.
| 타입 | 의미 | 커널 ops | 사용 예 |
|---|---|---|---|
| SOCK_STREAM | 연결 지향 바이트 스트림 | unix_stream_ops | D-Bus, systemd 소켓 활성화 |
| SOCK_DGRAM | 비연결 데이터그램 (메시지 경계 보존) | unix_dgram_ops | syslog, rsyslog |
| SOCK_SEQPACKET | 연결 지향 + 메시지 경계 보존 | unix_seqpacket_ops | Bluetooth L2CAP, 커스텀 IPC |
/* 소켓 생성 → 바인딩 → 연결의 커널 내부 경로 */
/* 1. socket(AF_UNIX, SOCK_STREAM, 0) 커널 진입 */
/* → __sys_socket()
* → sock_create() → __sock_create()
* → pf->create() == unix_create()
* → unix_create1() : unix_sock 할당 + 초기화
* → sock->ops = &unix_stream_ops (SOCK_STREAM인 경우)
*/
/* 2. bind(fd, &addr, sizeof(addr)) */
/* → __sys_bind()
* → sock->ops->bind() == unix_bind()
*
* 파일시스템 바인딩:
* → kern_path_create() : 경로에 소켓 파일 생성
* → init_special_inode(inode, S_IFSOCK | mode, 0)
* → 해시 테이블에 소켓 등록
*
* 추상 네임스페이스 바인딩:
* → unix_bind_abstract() : 해시 테이블에만 등록
* → 파일시스템 inode 생성 없음
*/
/* 3. listen(fd, backlog) — SOCK_STREAM/SEQPACKET만 */
/* → __sys_listen()
* → sock->ops->listen() == unix_listen()
* → sk->sk_state = TCP_LISTEN
* → sk->sk_max_ack_backlog = backlog
*/
/* 4. accept(fd, ...) */
/* → __sys_accept4()
* → sock->ops->accept() == unix_accept()
* → skb_dequeue(&sk->sk_receive_queue)
* → 큐에서 연결 요청 꺼내 새 소켓 쌍 생성
*/
/* 5. connect(fd, &addr, sizeof(addr)) */
/* → __sys_connect()
* → sock->ops->connect() == unix_stream_connect()
* → unix_find_other() : 이름으로 서버 소켓 검색
* → unix_peer(newsk) = other : 피어 설정
* → 서버의 sk_receive_queue에 연결 요청 skb 삽입
*/
소켓 활성화(Socket Activation): systemd는 .socket 유닛으로 UDS를 미리 바인딩하고, 첫 연결이 들어올 때 서비스를 시작합니다. 이 방식은 listen() 상태의 소켓 fd를 자식 프로세스에 상속시키는 UDS의 특성을 활용합니다.
데이터 전송 경로
UDS의 데이터 전송은 TCP/UDP와 달리 네트워크 프로토콜 스택을 완전히 우회합니다. IP 라우팅(Routing), 체크섬(Checksum), 분할/재조립이 모두 불필요하므로 단순한 커널 메모리 복사만으로 데이터가 전달됩니다.
/* SOCK_STREAM 전송 경로: unix_stream_sendmsg() */
/*
* 1. sendmsg(fd, &msg, flags)
* → sock_sendmsg() → sock->ops->sendmsg()
* → unix_stream_sendmsg()
*
* 2. unix_stream_sendmsg() 주요 로직:
* a) 피어 소켓 참조 획득: other = unix_peer(sk)
* b) 메모리 할당: alloc_skb_fclone() 또는 sock_alloc_send_pskb()
* c) 데이터 복사: skb_copy_datagram_from_iter()
* → 사용자 공간 → 커널 skb 데이터 영역으로 복사
* d) 피어 수신 큐에 삽입: skb_queue_tail(&other->sk_receive_queue, skb)
* e) 피어 깨우기: other->sk_data_ready(other)
*
* 핵심: sk_buff를 생성하지만, 네트워크 헤더(IP/TCP/UDP)를
* 추가하지 않습니다. 순수 데이터 컨테이너로만 사용.
*/
/* SOCK_DGRAM 전송 경로: unix_dgram_sendmsg() */
/*
* SOCK_STREAM과 유사하지만 차이점:
* - 연결 없는 상태에서도 sendto()로 목적지 지정 가능
* - 메시지 경계가 보존됨 (skb 하나 = 메시지 하나)
* - 수신 큐 가득 참 → -EAGAIN (비블로킹) 또는 대기 (블로킹)
* - 목적지 소켓이 없으면 -ECONNREFUSED
*/
/* 수신 경로: unix_stream_recvmsg() */
/*
* 1. recvmsg(fd, &msg, flags)
* → sock_recvmsg() → sock->ops->recvmsg()
* → unix_stream_recvmsg()
*
* 2. 주요 로직:
* a) sk_receive_queue에서 skb 획득
* b) skb_copy_datagram_msg() : 커널 → 사용자 공간 복사
* c) STREAM: 부분 읽기 가능 (skb에서 일부만 읽고 다음에 나머지)
* d) DGRAM/SEQPACKET: 메시지 전체를 한 번에 (MSG_TRUNC 처리)
*/
tcp_sendmsg()에서 데이터를 세그먼트로 분할하고, 혼잡 제어(Congestion Control) 윈도우를 확인하고, 재전송(Retransmission) 타이머(Timer)를 설정합니다. UDS는 이 모든 과정이 없으므로 같은 크기의 데이터를 전송할 때 CPU 사이클이 현저히 적습니다.
SCM_RIGHTS (파일 디스크립터 전달)
SCM_RIGHTS는 Unix Domain Socket의 가장 강력한 기능 중 하나로, 프로세스 간에 열린 파일 디스크립터를 전달할 수 있습니다. 이 메커니즘은 D-Bus, Wayland, systemd, 컨테이너(Container) 런타임 등에서 광범위하게 사용됩니다.
/* SCM_RIGHTS: 파일 디스크립터 전달 메커니즘 */
/* 송신측 (사용자 공간) */
struct msghdr msg = {};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int) * 3)]; /* 3개의 fd 전달 */
int fds[3] = { fd1, fd2, fd3 };
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
sendmsg(sock_fd, &msg, 0);
/* 커널 내부 경로:
*
* sendmsg() → unix_stream_sendmsg() / unix_dgram_sendmsg()
* → scm_send(msg, &scm)
* → __scm_send(msg, &scm)
* → SCM_RIGHTS 처리:
* → scm_fp_copy() : fd 번호 → struct file* 변환
* → fget_raw(fd) : fd 테이블에서 file 구조체 참조 획득
* → scm->fp->fp[i] = file : scm_fp_list에 저장
*
* → unix_scm_to_skb(&scm, skb)
* → UNIXCB(skb).fp = scm.fp : skb에 파일 포인터 목록 부착
* → unix_inflight(file) : in-flight 카운터 증가 (GC용)
*
* 수신측:
* recvmsg() → unix_stream_recvmsg()
* → scm_recv(msg, &scm)
* → SCM_RIGHTS 처리:
* → scm_detach_fds(msg, &scm)
* → 각 file에 대해 receive_fd() : 수신 프로세스의 새 fd 할당
* → unix_notinflight(file) : in-flight 카운터 감소
*/
recvmsg()로 수신하지 않으면 커널 메모리에 계속 남아있습니다. 이는 파일 디스크립터 누수와 메모리 누수를 유발할 수 있으며, 악의적인 프로세스가 대량의 fd를 in-flight 상태로 만들어 시스템 자원을 고갈시킬 수 있습니다. 이를 방지하기 위해 net.unix.max_dgram_qlen과 소켓 버퍼(Buffer) 크기를 적절히 설정해야 합니다.
SCM_CREDENTIALS (자격 증명 전달)
SCM_CREDENTIALS는 송신 프로세스의 PID, UID, GID를 수신 프로세스에 전달합니다. D-Bus가 클라이언트 인증에 이 메커니즘을 핵심적으로 사용합니다.
/* SCM_CREDENTIALS 자격 증명 전달 */
/* include/linux/socket.h */
struct ucred {
__u32 pid; /* 송신 프로세스 PID */
__u32 uid; /* 송신 프로세스 UID */
__u32 gid; /* 송신 프로세스 GID */
};
/* 수신측: SO_PASSCRED 활성화 필수 */
int optval = 1;
setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval));
/* 커널 내부 동작:
*
* 송신측:
* - 사용자가 SCM_CREDENTIALS cmsg를 첨부하면:
* → scm_send() → scm_check_creds(&scm->creds)
* → 커널이 실제 cred와 비교 검증
* → 루트(CAP_SYS_ADMIN)만 자신과 다른 PID/UID/GID 지정 가능
* → 일반 사용자가 다른 값 지정 시 -EPERM
*
* - 사용자가 SCM_CREDENTIALS를 첨부하지 않아도:
* → SO_PASSCRED 설정된 수신 소켓이면
* → 커널이 자동으로 송신자의 실제 PID/UID/GID를 채워 넣음
*
* 수신측:
* - recvmsg()에서 cmsg를 통해 struct ucred 수신
* - SO_PEERCRED로도 STREAM 연결의 피어 자격 증명 조회 가능:
* → getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len)
* → 이 값은 connect() 시점에 고정됨
*
* D-Bus 활용:
* - 클라이언트 연결 시 SO_PEERCRED로 UID 확인
* - 메시지별 인증이 필요하면 SCM_CREDENTIALS 사용
* - 정책 기반 접근 제어의 기초
*/
| 메커니즘 | 설정 | 시점 | 용도 |
|---|---|---|---|
| SO_PEERCRED | getsockopt() | connect() 시점 고정 | STREAM 연결의 피어 식별 |
| SO_PASSCRED | setsockopt() | 매 메시지 | 메시지별 송신자 인증 |
| SO_PEERSEC | getsockopt() | connect() 시점 | SELinux 보안 컨텍스트 조회 |
추상 네임스페이스
추상 네임스페이스(Abstract Namespace)는 Linux 전용 기능으로, 파일시스템에 소켓 파일을 생성하지 않고 커널 해시 테이블(Hash Table)에만 소켓 이름을 등록합니다. sun_path의 첫 번째 바이트가 \0(널 문자)이면 추상 네임스페이스로 인식됩니다.
/* 추상 네임스페이스 바인딩 예시 */
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0'; /* 추상 네임스페이스 표시 */
strncpy(addr.sun_path + 1, "my-service", sizeof(addr.sun_path) - 1);
socklen_t len = offsetof(struct sockaddr_un, sun_path)
+ 1 + strlen("my-service");
bind(fd, (struct sockaddr *)&addr, len);
/* 커널 내부: unix_bind_abstract()
* - unix_find_abstract() : 해시 테이블에서 중복 검사
* - __unix_insert_socket() : 해시 테이블에 삽입
* - VFS 경로(path) 설정 없음
* - 파일 권한 검사 없음 → 같은 네트워크 네임스페이스 내 모든 프로세스 접근 가능
*/
| 속성 | 파일시스템 소켓 | 추상 네임스페이스 |
|---|---|---|
| 접근 제어(Access Control) | 파일 권한(chmod/chown) | 네트워크 네임스페이스로만 격리(Isolation) |
| 수명 | unlink() 전까지 유지 | 모든 fd 닫히면 자동 제거 |
| 이식성 | 모든 Unix 계열 OS | Linux 전용 |
| 경로 충돌 | stale 소켓 파일 문제 | 충돌 없음 (자동 정리) |
| 컨테이너 | mount 네임스페이스로 격리 | network 네임스페이스로 격리 |
가비지 컬렉션 (GC)
SCM_RIGHTS로 전달된 파일 디스크립터가 순환 참조를 형성하면 일반 참조 카운팅으로는 회수할 수 없습니다. 이를 해결하기 위해 커널은 unix_gc()를 통한 전용 가비지 컬렉터를 구현합니다.
/* 순환 참조 시나리오:
*
* 소켓 A의 수신 큐에 소켓 B의 fd가 있고,
* 소켓 B의 수신 큐에 소켓 A의 fd가 있으면:
* → A의 참조 카운트: 사용자 fd(1) + B의 inflight(1) = 2
* → B의 참조 카운트: 사용자 fd(1) + A의 inflight(1) = 2
* → 사용자가 A, B 모두 close() → 참조 카운트가 1로 남음
* → 일반 해제 불가 → GC 필요
*/
/* net/unix/garbage.c — unix_gc() 알고리즘 */
/*
* 1. 후보 수집 (Candidate Collection)
* - gc_inflight_list에 있는 모든 소켓 수집
* - 조건: inflight 카운터 > 0 (in-flight fd가 있는 소켓)
*
* 2. 내부 참조 제거 (Internal Reference Decrement)
* - 각 후보 소켓의 수신 큐를 스캔
* - 큐에 있는 file이 다른 후보 소켓을 참조하면
* → 해당 소켓의 "외부 참조 카운트"를 감소
*
* 3. 도달 가능성 검사 (Reachability Check)
* - 외부 참조 카운트 > 0인 소켓: 도달 가능 → 보존
* - 외부 참조 카운트 == 0인 소켓: 도달 불가 → 회수 대상
* - 도달 가능 소켓에서 참조하는 소켓도 재귀적으로 보존
*
* 4. 회수 (Sweep)
* - 도달 불가능 소켓의 수신 큐에서 skb 제거
* - in-flight fd의 fput() 호출 → file 참조 감소
* - 순환 참조 해소 → 소켓과 파일 모두 해제
*
* 트리거 조건:
* - unix_tot_inflight > UNIX_INFLIGHT_TRIGGER_GC
* - 또는 close() 시 inflight 감소 후 GC 필요 판단
*/
/* 커널 6.x GC 리팩터링: SCC(Strongly Connected Components) 기반
*
* 기존 GC (커널 ~5.x):
* - 단순 마크-앤-스윕 방식
* - gc_inflight_list 전체 순회 → O(N) 잠금 구간
* - 큰 inflight 목록에서 성능 저하
*
* 새 GC (커널 6.x+, Kuniyuki Iwashima 패치):
* - unix_vertex / unix_edge 그래프 구조 도입
* - Tarjan 알고리즘으로 SCC(강결합 컴포넌트) 탐색
* - 순환 참조 그룹만 정확히 식별하여 회수
* - 정점/간선 수에 비례하는 복잡도
*
* 관련 구조체:
*/
struct unix_vertex {
struct list_head edges; /* 이 소켓에서 나가는 간선 목록 */
struct list_head entry; /* SCC 탐색용 리스트 */
struct list_head scc_entry; /* SCC 내 소켓 목록 */
unsigned long index; /* Tarjan 알고리즘 인덱스 */
unsigned long lowlink; /* Tarjan lowlink 값 */
bool on_stack;
};
struct unix_edge {
struct unix_sock *predecessor; /* fd를 보낸 소켓 */
struct unix_sock *successor; /* fd를 받은 소켓 */
struct list_head vertex_entry; /* vertex->edges 리스트 */
};
GC 모니터링: /proc/net/unix에서 Inode 컬럼이 0인 항목은 바인딩되지 않은 소켓이며, GC 대상이 될 수 있습니다. 대량의 in-flight fd가 있으면 dmesg에 GC: too many inflight fds 경고가 나타날 수 있습니다.
bpftrace -e 'kprobe:unix_gc { @gc_count = count(); }'로 GC 호출 빈도를 모니터링하고, 지나치게 잦다면 fd 전달 패턴을 최적화해야 합니다. 자세한 BPF 추적 기법은 BPF/eBPF/XDP 문서를 참고하세요.
socketpair와 IPC 패턴
socketpair()은 연결된 소켓 쌍을 한 번의 시스콜로 생성합니다. pipe()의 양방향 대안으로, fork() 전에 생성하여 부모-자식 프로세스 간 통신에 사용하는 것이 대표적 패턴입니다.
/* socketpair() 시스콜 */
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv);
/* sv[0] ↔ sv[1] 양방향 연결 완료 */
/* 커널 경로:
* __sys_socketpair()
* → sock_create() × 2 : 소켓 쌍 생성
* → type->socketpair() == unix_socketpair()
* → unix_peer(ska) = skb : A의 피어를 B로
* → unix_peer(skb) = ska : B의 피어를 A로
* → ska->sk_state = TCP_ESTABLISHED
* → skb->sk_state = TCP_ESTABLISHED
* → fd_install() × 2 : fd 테이블에 등록
*/
/* 일반적인 IPC 패턴: fork() + socketpair() */
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv);
pid_t pid = fork();
if (pid == 0) {
/* 자식 프로세스 */
close(sv[0]);
/* sv[1]로 부모와 양방향 통신 */
write(sv[1], "hello", 5);
close(sv[1]);
} else {
/* 부모 프로세스 */
close(sv[1]);
char buf[16];
read(sv[0], buf, sizeof(buf));
close(sv[0]);
}
| 특성 | pipe() | socketpair(AF_UNIX, SOCK_STREAM) | socketpair(AF_UNIX, SOCK_DGRAM) |
|---|---|---|---|
| 방향 | 단방향 | 양방향 | 양방향 |
| 메시지 경계 | 없음 (바이트 스트림) | 없음 (바이트 스트림) | 보존 |
| fd 전달 | 불가 | SCM_RIGHTS | SCM_RIGHTS |
| 자격 증명 | 불가 | SCM_CREDENTIALS | SCM_CREDENTIALS |
| 버퍼 | 커널 파이프 버퍼 (64KB 기본) | 소켓 버퍼 (SO_SNDBUF/SO_RCVBUF) | 소켓 버퍼 |
실전 사용 사례
Unix Domain Socket은 Linux 시스템에서 가장 널리 사용되는 IPC 메커니즘 중 하나입니다. 주요 시스템 소프트웨어가 UDS를 어떻게 활용하는지 이해하면 커널 구현의 설계 의도를 더 깊이 파악할 수 있습니다.
| 소프트웨어 | 소켓 경로 | 타입 | 활용하는 UDS 기능 |
|---|---|---|---|
| systemd | /run/systemd/private, /run/systemd/journal/socket |
STREAM, DGRAM | 소켓 활성화, fd 상속, SCM_RIGHTS로 소켓 fd 전달 |
| D-Bus | /var/run/dbus/system_bus_socket |
STREAM | SO_PEERCRED (클라이언트 UID 인증), SCM_RIGHTS (fd passing) |
| Docker/containerd | /var/run/docker.sock, /run/containerd/containerd.sock |
STREAM | HTTP-over-UDS (REST API), 파일 권한 기반 접근 제어 |
| Wayland | $XDG_RUNTIME_DIR/wayland-0 |
STREAM | SCM_RIGHTS로 DMA-BUF fd 전달 (GPU 버퍼 공유) |
| X11 | /tmp/.X11-unix/X0 |
STREAM | 로컬 디스플레이 연결 (TCP 대비 빠른 렌더링) |
| SSH Agent | $SSH_AUTH_SOCK |
STREAM | 키 서명 요청/응답 (인증 에이전트 프로토콜) |
| MySQL/PostgreSQL | /var/run/mysqld/mysqld.sock, /var/run/postgresql/.s.PGSQL.5432 |
STREAM | 로컬 DB 연결 (TCP 루프백 대비 ~30% 빠름) |
| rsyslog/syslog-ng | /dev/log |
DGRAM | 시스템 로그 수집 (메시지 경계 보존) |
| nginx | 구성에 따라 지정 | STREAM | 리버스 프록시 업스트림 연결 (FastCGI, PHP-FPM 등) |
/* 실전 패턴 1: systemd 소켓 활성화
*
* systemd가 .socket 유닛으로 소켓을 미리 생성:
* 1. socket(AF_UNIX, SOCK_STREAM, 0) → bind() → listen()
* 2. 첫 연결이 오면 fork()/exec()으로 서비스 시작
* 3. 서비스 프로세스는 sd_listen_fds()로 상속받은 fd 획득
*
* 이 방식의 장점:
* - 서비스 미실행 시에도 클라이언트 연결 대기 가능
* - 서비스 재시작 중에도 연결 요청 보존 (listen backlog)
* - 여러 서비스를 병렬로 시작 가능 (의존성 소켓만 미리 생성)
*/
/* 실전 패턴 2: 컨테이너 런타임 fd 전달
*
* containerd가 shim 프로세스에 컨테이너 stdio fd를 전달:
* 1. containerd가 PTY 또는 pipe fd를 생성
* 2. SCM_RIGHTS로 shim에게 fd 전달
* 3. shim이 fd를 컨테이너 프로세스에 dup2()
*
* → fork()/exec() 체인을 넘어 fd를 전달하는 핵심 메커니즘
*/
/* 실전 패턴 3: Wayland DMA-BUF 공유
*
* Wayland 클라이언트가 GPU 버퍼를 컴포지터와 공유:
* 1. 클라이언트: DRM ioctl로 DMA-BUF fd 획득
* 2. 클라이언트 → 컴포지터: SCM_RIGHTS로 DMA-BUF fd 전달
* 3. 컴포지터: 같은 GPU 메모리를 직접 참조 (zero-copy 렌더링)
*
* → 대용량 이미지 데이터를 복사 없이 프로세스 간 공유
*/
curl --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json처럼 호출하며, TCP 포트를 열지 않아 보안성이 높고 파일 권한으로 접근 제어가 간단합니다.
흐름 제어(Flow Control)와 백프레셔
UDS는 TCP의 윈도우 기반 흐름 제어 대신 소켓 버퍼 크기를 기반으로 한 단순한 흐름 제어를 사용합니다. 송신자가 수신자의 처리 속도보다 빠르게 데이터를 보내면, 소켓 버퍼가 가득 차면서 자연스러운 백프레셔(backpressure)가 발생합니다.
/* SOCK_STREAM 흐름 제어 메커니즘 */
/*
* 송신측: unix_stream_sendmsg()
* 1. sock_alloc_send_pskb(sk, ...) 호출
* → sk->sk_sndbuf 확인 (기본: net.core.wmem_default)
* → sk_stream_memory_free(sk) == false이면:
* → 블로킹 모드: sk_stream_wait_memory(sk)로 대기
* → 비블로킹 모드: -EAGAIN 반환
*
* 수신측 제한:
* - 피어 소켓의 sk_rmem_alloc이 sk_rcvbuf 초과 시
* → 추가 skb 삽입 시 송신측이 대기
*
* 핵심 차이: TCP는 수신 윈도우(rwnd)를 ACK에 실어 보내고
* 혼잡 윈도우(cwnd)도 관리하지만, UDS는 단순히
* 커널 메모리 할당 성공/실패로만 흐름을 제어합니다.
*/
/* SOCK_DGRAM 흐름 제어 */
/*
* 데이터그램은 메시지 단위:
* - 수신 큐 길이가 net.unix.max_dgram_qlen 초과 시:
* → 블로킹: peer_wait 큐에서 대기
* → 비블로킹: -EAGAIN
*
* - 메시지 하나가 SO_SNDBUF보다 크면: -EMSGSIZE
*
* syslog 과부하 시나리오:
* /dev/log (SOCK_DGRAM)에 대량 로그 발생
* → rsyslog가 처리 못 하면 큐 가득 참
* → 로그 송신 프로세스가 블로킹 또는 메시지 유실
* → 해결: net.unix.max_dgram_qlen 증가 (기본 10 → 512+)
*/
/* SOCK_SEQPACKET 특수 동작 */
/*
* STREAM과 DGRAM의 혼합:
* - STREAM처럼 연결 지향 (connect/accept 필요)
* - DGRAM처럼 메시지 경계 보존
* - 추가 보장: 메시지 순서 보장 + 신뢰성
*
* recv()에서 버퍼보다 큰 메시지:
* - STREAM: 부분 읽기 (나머지는 다음 recv)
* - DGRAM: 초과분 버림 (MSG_TRUNC로 감지)
* - SEQPACKET: 초과분 버림 (MSG_TRUNC), 순서 보장
*
* 사용 예: Bluetooth L2CAP, SCTP-like IPC 구현
*/
| sysctl 파라미터 | 기본값 | 설명 | 영향 |
|---|---|---|---|
net.unix.max_dgram_qlen |
10 | DGRAM 소켓의 최대 수신 큐 길이 | syslog 등 DGRAM 소켓 과부하 방지 |
net.core.wmem_default |
212992 | 송신 버퍼 기본 크기 (바이트) | 모든 소켓 (UDS 포함) 송신 버퍼 |
net.core.rmem_default |
212992 | 수신 버퍼 기본 크기 (바이트) | 모든 소켓 (UDS 포함) 수신 버퍼 |
net.core.wmem_max |
212992 | SO_SNDBUF 최대값 | setsockopt()로 설정 가능한 상한 |
net.core.rmem_max |
212992 | SO_RCVBUF 최대값 | setsockopt()로 설정 가능한 상한 |
net.unix.max_dgram_qlen의 기본값 10은 매우 작습니다. 시스템 로그(/dev/log)에 대량의 메시지가 발생하면 큐가 쉽게 가득 찰 수 있습니다. 프로덕션 환경에서는 512 이상으로 설정하는 것을 권장하며, systemd-journald가 처리하는 경우에도 이 값을 확인해야 합니다.
OOB (Out-of-Band) 데이터
커널 5.15부터 Unix Domain Socket의 SOCK_STREAM 타입에서 OOB(Out-of-Band) 데이터 전송을 지원합니다. TCP의 URG(urgent) 데이터와 유사한 개념으로, 일반 데이터 스트림과 별도로 긴급 데이터를 전달합니다.
/* OOB 데이터 전송/수신 (커널 5.15+) */
/* 송신측 */
char urgent = '!';
send(fd, &urgent, 1, MSG_OOB);
/* 수신측: SIGURG 시그널 수신 또는 poll()에서 POLLPRI */
char oob;
recv(fd, &oob, 1, MSG_OOB);
/* 커널 내부:
* 송신: unix_stream_sendmsg()
* → MSG_OOB 플래그 감지
* → skb를 oob_skb 큐에 저장 (일반 recv_queue와 분리)
* → 피어에게 SIGURG 시그널 전달 또는 sk_error_report() 호출
*
* 수신: unix_stream_recvmsg()
* → MSG_OOB 플래그 시 oob_skb에서 직접 읽기
* → SO_OOBINLINE 설정 시 일반 스트림에 인라인으로 포함
*
* 용도:
* - PostgreSQL 백엔드 취소 (cancel) 요청
* - 긴급 제어 메시지 전달
* - TCP 소켓에서 UDS로 마이그레이션할 때 호환성 유지
*/
보안 모델
UDS의 보안은 파일시스템 권한, LSM 훅, 네임스페이스 격리의 세 가지 계층으로 구성됩니다.
/* 1. 파일시스템 권한 (파일시스템 바인딩 소켓) */
/*
* - 소켓 파일의 owner/group/mode 적용
* - connect() 시 커널이 inode_permission() 검사
* → 쓰기(write) 권한 필요
* - 예: chmod 0770 /var/run/daemon.sock
* → 소유자와 그룹만 연결 가능
*
* 주의: 추상 네임스페이스는 파일 권한 없음!
*/
/* 2. LSM (Linux Security Module) 훅 */
/*
* SELinux:
* - unix_stream_connect 훅: 연결 시 검사
* - unix_may_send 훅: 데이터 전송 시 검사
* - 정책 예시:
* allow client_t server_t : unix_stream_socket connectto;
*
* AppArmor:
* - unix 규칙으로 소켓 접근 제어
* - 예: unix (send receive connect) type=stream peer=(label=server),
*
* Smack:
* - 소켓 파일에 Smack 레이블 적용
*/
/* 3. 네임스페이스 격리 */
/*
* - 파일시스템 소켓: mount namespace로 격리
* → 다른 mount namespace에서는 경로가 보이지 않음
* - 추상 네임스페이스: network namespace로 격리
* → 다른 network namespace에서는 이름이 보이지 않음
* - PID namespace: SCM_CREDENTIALS의 PID가 번역됨
* → 상대 프로세스의 namespace 내 PID로 전달
*/
/var/run/docker.sock), 컨테이너가 Docker API에 접근하여 사실상 호스트 루트 권한을 획득할 수 있습니다. 프로덕션에서는 이런 마운트를 피하거나, rootless 컨테이너와 SELinux/AppArmor 정책으로 보호해야 합니다.
성능 특성
UDS는 동일 호스트 IPC에서 TCP 루프백 대비 상당한 성능 이점을 제공합니다.
| 항목 | UDS (AF_UNIX) | TCP 루프백 (127.0.0.1) |
|---|---|---|
| 프로토콜 스택 | 완전 우회 | IP + TCP 전체 경로 |
| 체크섬 | 없음 | TCP/IP 체크섬 계산 |
| 혼잡 제어 | 없음 | CUBIC/BBR 동작 |
| ACK 처리 | 없음 | TCP ACK, 지연(Latency) ACK |
| 네트워크 필터 | Netfilter 미경유 | Netfilter/conntrack 경유 |
| 레이턴시 | 약 2-5 us (일반적) | 약 10-30 us (일반적) |
| 처리량(Throughput) | 메모리 복사 대역폭(Bandwidth)에 의존 | 프로토콜 오버헤드로 낮음 |
# 간단한 레이턴시 비교 (socat 활용)
# UDS
$ socat UNIX-LISTEN:/tmp/bench.sock,fork EXEC:/bin/cat &
$ time for i in $(seq 1000); do
echo "test" | socat - UNIX-CONNECT:/tmp/bench.sock
done
# TCP loopback
$ socat TCP-LISTEN:9999,fork EXEC:/bin/cat &
$ time for i in $(seq 1000); do
echo "test" | socat - TCP:127.0.0.1:9999
done
/* MSG_ZEROCOPY (커널 5.x+) */
/*
* UDS는 커널 6.2+에서 MSG_ZEROCOPY를 지원합니다.
* 대용량 전송 시 사용자 공간 → 커널 복사를 생략하여 성능 향상.
*
* 활성화:
* setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));
* sendmsg(fd, &msg, MSG_ZEROCOPY);
*
* 주의사항:
* - 소량 데이터에서는 오히려 오버헤드 (페이지 핀닝 비용)
* - 일반적으로 32KB 이상 전송에서 이점
* - 수신측은 변경 없이 동작
*/
/* io_uring + UDS (커널 5.6+) */
/*
* io_uring은 UDS에 대한 비동기 I/O를 지원:
* - IORING_OP_SENDMSG / IORING_OP_RECVMSG
* - 시스콜 오버헤드 제거 (submission queue로 배치 처리)
* - 폴링 모드에서 추가 성능 향상
*
* 적합한 시나리오:
* - 많은 UDS 연결을 처리하는 서비스 (프록시, 브로커)
* - D-Bus 대체 IPC 구현
*/
성능 튜닝 요약: UDS 소켓 버퍼 크기(SO_SNDBUF, SO_RCVBUF)를 조정하면 대용량 전송 성능이 개선됩니다. 시스템 전역 기본값은 net.core.wmem_default/net.core.rmem_default이며, 최대값은 net.core.wmem_max/net.core.rmem_max로 제한됩니다.
디버깅
UDS 문제를 진단하는 데 사용하는 주요 도구와 인터페이스입니다.
# ss -x : Unix Domain Socket 상태 조회 (가장 추천)
$ ss -xlnp
# Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# u_str LISTEN 0 128 /var/run/dbus/system_bus_socket 0 * 0 users:(("dbus-daemon",pid=...))
# -x : Unix 소켓만 표시
# -l : 리스닝 소켓
# -n : 숫자 표시
# -p : 프로세스 정보
# 연결된 소켓과 피어 조회
$ ss -xp | grep docker.sock
# /proc/net/unix : 커널 소켓 테이블 직접 조회
$ cat /proc/net/unix
# Num RefCount Protocol Flags Type St Inode Path
# 0000... 00000002 00000000 00010000 0001 01 12345 /var/run/daemon.sock
#
# Flags: 00010000 = ACC (accepting connections)
# Type: 0001 = SOCK_STREAM, 0002 = SOCK_DGRAM, 0005 = SOCK_SEQPACKET
# St: 01 = UNCONNECTED, 03 = CONNECTED, 02 = CONNECTING
# strace로 UDS 시스콜 추적
$ strace -e trace=network -f -p 1234
# sendmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="...", iov_len=256}],
# msg_iovlen=1, msg_control=[{cmsg_len=20, cmsg_level=SOL_SOCKET,
# cmsg_type=SCM_RIGHTS, cmsg_data=[7]}], msg_controllen=24, msg_flags=0}, 0) = 256
# bpftrace로 unix_stream_sendmsg 추적
$ bpftrace -e '
kprobe:unix_stream_sendmsg {
@bytes[comm] = hist(arg2);
}
interval:s:10 { exit(); }
'
# lsof로 특정 소켓 파일 사용 프로세스 확인
$ lsof /var/run/docker.sock
# inode 번호로 소켓 피어 찾기 (커널 4.2+)
$ ss -xp -e | grep "ino:12345"
- EADDRINUSE — 이전 실행에서 남은 소켓 파일.
unlink()하거나SO_REUSEADDR사용하되, 다른 프로세스가 사용 중인지 먼저 확인. - ECONNREFUSED — 서버가 listen 상태가 아니거나, 추상 네임스페이스의 경우 서버가 종료됨.
- ENOENT — 소켓 파일 경로가 존재하지 않음. 디렉터리 존재 여부와 권한 확인.
- EACCES — 소켓 파일 또는 경로 디렉터리의 권한 부족.
- fd 누수 — SCM_RIGHTS로 전달된 fd를 recvmsg()로 수신하지 않으면 커널에 누적.
/proc/net/unix에서 RefCount 모니터링.
# 고급 디버깅: 소켓 피어 매칭 (커널 4.2+)
# ss -xpe 출력에서 ino(inode 번호)를 이용해 연결된 쌍을 찾기
$ ss -xpe | awk '
/^u_str/ {
# 로컬 inode와 피어 inode 추출
for(i=1; i<=NF; i++) {
if($i ~ /ino:/) local_ino = $i
if($i ~ /peer:/) peer_ino = $i
}
print $0
}
'
# bpftrace: SCM_RIGHTS 전달 추적
$ bpftrace -e '
kprobe:unix_attach_fds {
printf("pid=%d comm=%s attaching fds\n", pid, comm);
}
kprobe:unix_detach_fds {
printf("pid=%d comm=%s detaching fds\n", pid, comm);
}
'
# bpftrace: 소켓 연결 추적
$ bpftrace -e '
kprobe:unix_stream_connect {
printf("pid=%d comm=%s connecting\n", pid, comm);
}
kretprobe:unix_stream_connect {
printf("pid=%d result=%d\n", pid, retval);
}
'
# /proc/<pid>/fd에서 소켓 파일 확인
$ ls -la /proc/1234/fd | grep socket
# lrwx------ 1 root root 64 ... 5 -> socket:[12345]
# 12345가 소켓 inode 번호 → /proc/net/unix에서 매칭
# systemd 서비스의 소켓 상태 확인
$ systemctl status dbus.socket
$ systemctl list-sockets --all
sock_diag 인터페이스: ss 명령은 내부적으로 Netlink 기반의 sock_diag 인터페이스를 사용합니다. net/unix/diag.c의 unix_diag_handler가 UDS 소켓 정보를 제공하며, UDIAG_SHOW_NAME, UDIAG_SHOW_PEER, UDIAG_SHOW_RQLEN 등의 속성으로 상세 정보를 조회할 수 있습니다.
소켓 조회 내부 구현
connect()에서 서버 소켓을 찾는 과정은 UDS 아키텍처의 핵심입니다. 파일시스템 소켓과 추상 네임스페이스 소켓은 조회 경로가 완전히 다릅니다.
/* unix_find_other() — 서버 소켓 조회 (net/unix/af_unix.c) */
/*
* 파일시스템 소켓 조회 경로:
* 1. kern_path(sunname, LOOKUP_FOLLOW, &path)
* → VFS 경로 탐색 (symlink 추적)
* → 실패 시 -ENOENT 반환
*
* 2. d_backing_inode(path.dentry) → inode 획득
* → S_ISSOCK(inode->i_mode) 검사
* → 소켓 파일이 아니면 -ECONNREFUSED
*
* 3. unix_find_socket_byinode(inode)
* → inode 번호로 해시 테이블 검색
* → net->unx.table.buckets[hash] 순회
* → 일치하는 unix_sock 반환
*
* 추상 네임스페이스 조회 경로:
* 1. unix_find_socket_byname(net, sunname, len, hash)
* → 이름 문자열의 해시값으로 버킷 결정
* → 버킷 내 리스트 순회하며 이름 비교
* → VFS를 전혀 거치지 않음 → 더 빠름
*
* 성능 차이:
* - 파일시스템: VFS 경로 탐색 + inode 조회 → 수 us
* - 추상 네임스페이스: 해시 테이블 직접 조회 → 수백 ns
* - 고빈도 연결이 필요한 서비스는 추상 네임스페이스가 유리
*/
/* unix_stream_connect()의 연결 설정 핵심 경로 */
static int unix_stream_connect(struct socket *sock,
struct sockaddr *uaddr,
int addr_len, int flags) {
/* 1. 새 소켓 할당 (클라이언트측 엔드포인트) */
newsk = unix_create1(net, NULL, 0, sock->type);
/* 2. 서버 소켓 검색 */
other = unix_find_other(net, sunaddr, addr_len, sock->type);
/* 3. 서버가 TCP_LISTEN 상태인지 확인 */
if (other->sk_state != TCP_LISTEN)
return -ECONNREFUSED;
/* 4. backlog 초과 확인 */
if (sk_acceptq_is_full(other))
return -EAGAIN;
/* 5. 피어 포인터 상호 설정 */
unix_peer(newsk) = other; /* 클라이언트 → 서버 */
unix_peer(sk) = newsk; /* 서버측 accept용 */
/* 6. 서버 accept 큐에 연결 요청 삽입 */
skb_queue_tail(&other->sk_receive_queue, skb);
other->sk_data_ready(other); /* accept() 깨우기 */
}
VFS 통합: UDS의 파일시스템 소켓은 sockfs 의사 파일시스템 위에 존재하지만, 바인딩 시 실제 파일시스템(ext4, tmpfs 등)에 S_IFSOCK 타입의 특수 파일을 생성합니다. VFS 계층과의 관계를 더 깊이 이해하려면 VFS 문서를 참고하세요.
listen/accept 내부 메커니즘
SOCK_STREAM과 SOCK_SEQPACKET에서 서버는 listen()으로 연결 수신 모드로 전환하고, accept()로 연결을 수락합니다. 이 과정의 내부 메커니즘을 상세히 살펴봅니다.
/* listen()과 accept()의 커널 내부 */
/* unix_listen() */
static int unix_listen(struct socket *sock, int backlog) {
/* 전제 조건:
* - SOCK_STREAM 또는 SOCK_SEQPACKET만 가능
* - bind()가 이미 호출되어 주소가 설정되어 있어야 함
* - 현재 상태가 TCP_CLOSE여야 함 (아직 연결/리스닝 아님)
*/
sk->sk_state = TCP_LISTEN;
sk->sk_max_ack_backlog = backlog;
/* backlog: accept() 전에 대기할 수 있는 최대 연결 수
* 초과 시 connect()가 -EAGAIN 반환 (비블로킹)
* 또는 대기 (블로킹 + backlog 여유 생길 때까지)
*/
}
/* unix_accept() */
static int unix_accept(struct socket *sock,
struct socket *newsock,
int flags, bool kern) {
/* 1. sk_receive_queue에서 연결 요청 skb 대기/획득
* → connect()가 삽입한 skb
* → skb에 새 소켓(connect측이 생성한 newsk)이 부착됨
*
* 2. skb에서 새 소켓 추출
* → tsk = skb->sk (connect가 unix_create1()로 만든 소켓)
*
* 3. newsock에 새 소켓 연결
* → newsock->sk = tsk
* → tsk->sk_socket = newsock
* → tsk->sk_state = TCP_ESTABLISHED
*
* 핵심 포인트:
* - TCP와 달리 3-way handshake 없음
* - connect() 시점에 이미 소켓 쌍이 생성됨
* - accept()는 단순히 대기 큐에서 꺼내는 작업
* - 따라서 TCP 대비 연결 설정이 훨씬 빠름
*/
}
TCP_LISTEN, TCP_ESTABLISHED, TCP_CLOSE 등의 상수를 그대로 사용하는 것이 혼란스러울 수 있습니다. 이는 소켓 계층의 공통 상태 머신을 재활용(Recycling)하는 것이며, 실제 TCP 프로토콜과는 관련이 없습니다. include/net/tcp_states.h에 정의된 이 상수들은 모든 연결 지향 소켓에서 공유됩니다.
unix_sock 구조체
struct unix_sock은 AF_UNIX 소켓의 모든 상태를 관리하는 핵심 자료구조입니다. struct sock을 첫 번째 멤버로 내장(embed)하여 소켓 프레임워크의 공통 인터페이스와 호환되면서, UDS 전용 필드를 추가합니다. 관련 보조 구조체인 unix_address와 unix_skb_parms의 내부 필드까지 분석합니다.
/* unix_sock 내부 필드 상세 분석 */
/* 1. unix_address: 참조 카운트 기반 공유 */
struct unix_address {
refcount_t refcnt; /* fork/dup으로 소켓 공유 시 증가 */
int len; /* sizeof(sa_family) + 실제 경로 길이 */
struct sockaddr_un name[]; /* 가변 길이 배열 (C99 flexible array) */
};
/* unix_bind()에서 kmalloc(sizeof(*addr) + sizeof(sun) + 1)로 할당
* → 소켓이 close()될 때 unix_release_sock()에서 refcount 감소
* → refcount == 0이면 kfree()
*
* 주의: 추상 네임스페이스는 name->sun_path에 '\0' 포함
* → len 필드가 실제 이름 길이를 결정 (strlen 사용 불가)
*/
/* 2. scm_stat: SCM 보조 데이터 통계 */
struct scm_stat {
atomic_t nr_fds; /* 수신 큐에 대기 중인 fd 총 수 */
};
/* recvmsg()에서 SCM_RIGHTS fd를 수신할 때마다 감소
* → FIONREAD ioctl에서 보고하는 대기 데이터 크기에 영향
* → 대량 fd 전달 시 메모리 사용량 추적에 활용
*/
/* 3. unix_skb_parms (UNIXCB 매크로): skb 메타데이터 */
struct unix_skb_parms {
struct pid *pid; /* struct pid (PID namespace 인식) */
kuid_t uid;
kgid_t gid;
struct scm_fp_list *fp; /* 전달할 file* 목록 */
u32 consumed; /* STREAM: 이미 읽은 바이트 수 */
};
/* skb->cb[] (48바이트 제어 블록)에 저장
* → UNIXCB(skb) 매크로로 접근:
* #define UNIXCB(skb) (*(struct unix_skb_parms *)&(skb)->cb)
*
* consumed 필드는 SOCK_STREAM의 부분 읽기를 지원:
* → 100바이트 skb에서 50바이트만 읽으면 consumed=50
* → 다음 recvmsg()에서 나머지 50바이트부터 읽기 시작
*/
/* 4. gc_flags 비트 필드 */
/*
* UNIX_GC_CANDIDATE (1 << 0):
* → GC 후보로 등록됨 (inflight fd 존재)
*
* UNIX_GC_MAYBE_CYCLE (1 << 1):
* → 순환 참조 가능성 감지 (SCC 탐색 대상)
*
* 새 GC (6.x):
* → gc_flags 대신 vertex->index, vertex->lowlink 사용
* → Tarjan 알고리즘의 스택/방문 상태 관리
*/
unix_sk() 매크로(Macro)는 container_of(sk, struct unix_sock, sk)로 구현됩니다. struct sock 포인터에서 이를 내장하고 있는 struct unix_sock 포인터를 역참조(Dereference)하는 리눅스 커널의 표준 패턴입니다. 이 덕분에 소켓 프레임워크의 공통 코드는 struct sock *만 다루고, AF_UNIX 전용 코드에서만 unix_sk()로 확장 필드에 접근합니다.
소켓 생명주기 상세
UDS 소켓의 전체 생명주기를 생성부터 종료까지 각 단계의 커널 코드 경로와 상태 전이를 추적합니다. 특히 connect()에서 발생하는 소켓 쌍 생성 과정과 shutdown/close의 차이를 상세히 분석합니다.
/* 소켓 종료 경로 상세: shutdown() vs close() */
/* shutdown(fd, how):
* → unix_shutdown()
*
* SHUT_RD (0):
* → sk->sk_shutdown |= RCV_SHUTDOWN
* → 수신 큐의 모든 skb 폐기 (unix_release_sock와 다름)
* → 이후 recv()는 0 반환 (EOF)
*
* SHUT_WR (1):
* → sk->sk_shutdown |= SEND_SHUTDOWN
* → peer->sk_shutdown |= RCV_SHUTDOWN
* → peer의 sk_data_ready() 호출 (EOF 알림)
* → 이후 send()는 -EPIPE + SIGPIPE
*
* SHUT_RDWR (2):
* → RCV_SHUTDOWN | SEND_SHUTDOWN 모두 설정
* → peer에게도 양방향 종료 알림
*
* 핵심: shutdown()은 소켓을 닫지 않음 — fd는 유효
* → 이후 상태 확인용 getsockopt() 등은 계속 가능
*/
/* close(fd) — unix_release_sock() */
/*
* 1. 해시 테이블에서 소켓 제거
* → __unix_remove_socket()
*
* 2. 바인딩된 경로 해제
* → path_put(&u->path) (파일시스템 소켓)
* → unix_table_double_lock() → 해시 제거 (추상 NS)
*
* 3. 피어 소켓 분리
* → unix_peer(sk) = NULL
* → 피어에게 EPOLLHUP 이벤트 전달
* → SOCK_DGRAM: 피어의 peer도 NULL로
*
* 4. 수신 큐 정리
* → skb_queue_purge(&sk->sk_receive_queue)
* → 각 skb의 SCM_RIGHTS fd → unix_notinflight() + fput()
*
* 5. sock_put(sk) → refcount 감소
* → 0이면 sk_free() → unix_sock_destructor()
* → kfree(u->addr) 등 최종 정리
*
* DGRAM 특수 사항:
* → 연결된 피어가 close()하면 SOCK_DGRAM 송신측은
* 다음 sendmsg()에서 -ECONNREFUSED 수신
* → peer_wait에서 대기 중인 쓰레드 깨우기
*/
shutdown(SHUT_WR)은 피어에게 정상 EOF를 전달하면서 fd는 유지합니다. close()는 fd를 즉시 해제하므로, 수신 큐에 아직 읽지 않은 데이터가 있으면 유실됩니다. 특히 SCM_RIGHTS로 전달된 in-flight fd는 close() 시 fput()이 호출되어 예상치 못한 파일 해제가 발생할 수 있습니다. 안전한 종료를 위해 shutdown(SHUT_WR) → 피어의 EOF 확인 → close() 순서를 권장합니다.
SCM_RIGHTS FD 전달 메커니즘
SCM_RIGHTS의 내부 동작을 scm_fp_list 구조체 관리, fget/fput 참조 카운팅 전이, LSM 보안 훅 호출 지점까지 상세히 추적합니다.
/* SCM_RIGHTS: 참조 카운트 전이 추적 */
/* scm_fp_copy() 상세 — net/core/scm.c */
static int scm_fp_copy(struct cmsghdr *cmsg,
struct scm_fp_list **fplp) {
/* fd 배열에서 각 fd를 struct file*로 변환:
*
* for (i = 0; i < num_fds; i++) {
* file = fget_raw(fd); // file->f_count++ (atomic_inc)
* if (!file) return -EBADF;
*
* // LSM 검사: 이 파일을 다른 프로세스에 전달 가능한가?
* err = security_file_receive(file);
* if (err) { fput(file); return err; }
*
* fpl->fp[fpl->count++] = file;
* }
*
* 최대 전달 가능 fd 수: SCM_MAX_FD = 253
* → CMSG_SPACE 한계 + 커널 메모리 보호
*/
}
/* unix_inflight() / unix_notinflight() — net/unix/scm.c */
void unix_inflight(struct user_struct *user,
struct file *fp) {
/* 1. fp가 AF_UNIX 소켓이면:
* → unix_sock의 inflight 카운터 증가
* → gc_inflight_list에 추가 (GC 추적 대상)
*
* 2. unix_tot_inflight 전역 카운터 증가
* → UNIX_INFLIGHT_TRIGGER_GC 초과 시 GC 스케줄
*
* 핵심: AF_UNIX 소켓 fd를 다른 AF_UNIX 소켓으로 전달하면
* 순환 참조 가능성 → GC 추적 필수
* 일반 파일(regular file) fd는 순환 참조 불가능 → 추적 불필요
*/
}
/* receive_fd_replace() — 수신측 fd 할당 (fs/file.c) */
/*
* 1. __alloc_fd() : 수신 프로세스의 fd 테이블에서 빈 슬롯 할당
* 2. fd_install(new_fd, file) : 슬롯에 file 구조체 등록
* 3. MSG_CMSG_CLOEXEC 플래그:
* → 설정 시 O_CLOEXEC 적용 (exec() 시 자동 close)
* → 미설정 시 exec()에 fd 상속 → 보안 위험!
* → 모범 사례: 항상 MSG_CMSG_CLOEXEC 사용
*/
recvmsg()에서 SCM_RIGHTS fd를 수신할 때 반드시 MSG_CMSG_CLOEXEC 플래그를 사용하세요. 이 플래그 없이 수신하면 exec() 시 fd가 자식 프로세스에 상속되어, 의도하지 않은 파일 접근 권한이 전파될 수 있습니다. 컨테이너 런타임이나 권한 분리 데몬에서는 특히 중요합니다.
SCM_CREDENTIALS
SCM_CREDENTIALS와 SO_PEERCRED의 내부 동작을 비교하고, PID namespace 환경에서의 동작 차이, SO_PEERPIDFD (커널 6.5+) 등 최신 기능까지 분석합니다.
/* SCM_CREDENTIALS: 커널 내부 검증 경로 */
/* scm_check_creds() — net/core/scm.c */
static int scm_check_creds(struct ucred *creds) {
/* 송신자가 제공한 ucred 검증:
*
* PID 검증:
* if (creds->pid != task_tgid_vnr(current))
* → PID가 다르면 CAP_SYS_ADMIN 필요
* → PID namespace 내 가상 PID(vnr) 기준
*
* UID 검증:
* if (!uid_eq(creds->uid, current_uid()) &&
* !uid_eq(creds->uid, current_euid()) &&
* !uid_eq(creds->uid, current_suid()))
* → uid/euid/suid 중 하나와도 불일치 → CAP_SETUID 필요
*
* GID 검증:
* if (!gid_eq(creds->gid, current_gid()) &&
* !gid_eq(creds->gid, current_egid()) &&
* !gid_eq(creds->gid, current_sgid()))
* → gid/egid/sgid 중 하나와도 불일치 → CAP_SETGID 필요
*
* 핵심: 일반 사용자는 자신의 실제 자격 증명만 전달 가능
* → 커널이 검증하므로 위조 불가능
*/
}
/* SO_PASSCRED 자동 채움: 사용자가 cmsg를 안 보내도 동작 */
/*
* unix_scm_to_skb()에서:
* UNIXCB(skb).pid = get_pid(scm->pid);
* UNIXCB(skb).uid = scm->creds.uid;
* UNIXCB(skb).gid = scm->creds.gid;
*
* scm->pid는 scm_send()에서 자동 설정:
* scm->pid = get_pid(task_tgid(current));
* scm->creds.uid = current_uid();
* scm->creds.gid = current_gid();
*
* → 수신측이 SO_PASSCRED 설정했으면 recvmsg()에서 자동 수신
* → 송신측이 명시적으로 cmsg를 보내지 않아도 됨
*/
/* SO_PEERCRED vs SCM_CREDENTIALS vs SO_PEERPIDFD 비교 */
/*
* SO_PEERCRED (getsockopt):
* - SOCK_STREAM/SEQPACKET에서만 사용 가능
* - connect() 시점의 자격 증명이 고정됨
* - 이후 setuid/setgid 변경이 반영되지 않음
* - 구현: unix_stream_connect()에서 sk->sk_peer_pid/cred 설정
*
* SCM_CREDENTIALS (sendmsg/recvmsg):
* - 모든 소켓 타입에서 사용 가능 (STREAM/DGRAM/SEQPACKET)
* - 매 메시지마다 현재 시점의 자격 증명 전달
* - SO_PASSCRED 설정 필요 (수신측)
*
* SO_PEERPIDFD (커널 6.5+, getsockopt):
* - SO_PEERCRED의 PID 대신 pidfd 반환
* - PID 재사용 경쟁 조건 방지 (TOCTOU 문제 해결)
* - pidfd로 프로세스 존재 여부를 안전하게 확인
* - 구현: 피어의 struct pid에서 pidfd_create()
*
* SO_PEERSEC (getsockopt):
* - LSM 보안 레이블 문자열 반환
* - SELinux: "system_u:system_r:httpd_t:s0" 형태
* - AppArmor: "/usr/sbin/sshd" 형태
* - security_socket_getpeersec_stream() LSM 훅 호출
*/
| 메커니즘 | 타입 | 시점 | PID NS 인식 | TOCTOU 안전 | 커널 버전 |
|---|---|---|---|---|---|
| SO_PEERCRED | getsockopt | connect() 시 고정 | 아니오 (init_pid_ns PID) | 아니오 | 2.x+ |
| SCM_CREDENTIALS | cmsg | 매 메시지 | 예 (가상 PID) | 아니오 | 2.x+ |
| SO_PEERPIDFD | getsockopt | connect() 시 고정 | 예 (pidfd) | 예 | 6.5+ |
| SO_PEERSEC | getsockopt | connect() 시 | N/A | N/A | 2.6.17+ |
SO_PEERCRED가 반환하는 PID는 정수값이므로, 피어 프로세스가 종료된 후 같은 PID가 다른 프로세스에 재할당될 수 있습니다(TOCTOU 경쟁). SO_PEERPIDFD는 이 문제를 해결합니다. pidfd는 프로세스의 struct pid에 대한 참조를 유지하므로, 프로세스가 종료되더라도 PID가 재사용되지 않습니다. systemd 255+ 등 최신 서비스 관리자에서 활용이 시작되고 있습니다.
추상 네임스페이스 vs 파일시스템 바인딩
두 바인딩 방식의 보안 모델, 접근 제어, 자동 정리 동작, 컨테이너 환경에서의 격리 차이를 커널 코드 수준에서 비교 분석합니다.
/* 바인딩 경로 비교: 커널 코드 추적 */
/* unix_bind() — 분기점 */
static int unix_bind(struct socket *sock,
struct sockaddr *uaddr, int addr_len) {
if (sunaddr->sun_path[0])
err = unix_bind_bsd(sk, sunaddr, addr_len); /* 파일시스템 */
else
err = unix_bind_abstract(sk, sunaddr, addr_len); /* 추상 NS */
}
/* unix_bind_bsd() 핵심 경로:
* 1. user_path_create() → 부모 디렉터리의 dentry 획득
* → 부모 디렉터리에 쓰기 권한 검사 (inode_permission)
* → 디렉터리 락 획득 (inode_lock)
*
* 2. vfs_mknod(dir, dentry, mode|S_IFSOCK, 0)
* → 파일시스템에 소켓 파일 inode 생성
* → security_inode_mknod() LSM 훅 호출
* → SELinux: 파일 전이 레이블 적용 가능
*
* 3. __unix_set_addr_hash()
* → inode 번호 기반 해시 계산
* → unix_table에 소켓 등록
*
* 4. d_instantiate(dentry, inode)
* → VFS 캐시에 등록 → connect()에서 경로 조회 가능
*
* 연결 시 접근 제어:
* unix_find_other() → kern_path() → inode_permission(MAY_WRITE)
* → 소켓 파일에 대한 쓰기 권한이 없으면 -EACCES
*/
/* unix_bind_abstract() 핵심 경로:
* 1. 해시 테이블에서 이름 중복 검사
* → unix_find_abstract(net, sunaddr, addr_len)
* → 중복 시 -EADDRINUSE
*
* 2. __unix_insert_socket()
* → 해시 테이블에 직접 삽입
* → VFS 호출 없음
* → 파일 권한 설정 없음
*
* 연결 시 접근 제어:
* unix_find_other() → unix_find_socket_byname()
* → 이름 매칭만 수행 → 파일 권한 검사 없음!
* → LSM만이 유일한 보안 경계:
* security_unix_stream_connect()
* security_unix_may_send()
*/
--net=host를 사용하면 호스트의 추상 소켓에 접근 가능하므로, D-Bus 시스템 버스(Bus)나 다른 서비스의 추상 소켓이 노출됩니다. 보안이 중요한 환경에서는 파일시스템 소켓을 사용하거나, SELinux/AppArmor 정책으로 추상 소켓 접근을 제한해야 합니다.
GC (Garbage Collection)
커널 6.x에서 도입된 SCC(강결합 컴포넌트) 기반 새 GC 알고리즘의 상세 동작을 분석합니다. Tarjan 알고리즘 적용, 그래프 구조, 성능 특성, 그리고 대규모 in-flight fd 환경에서의 동작을 추적합니다.
/* SCC GC 상세 구현 (커널 6.x+) */
/* 그래프 구성: unix_edge 생성 시점 */
/*
* SCM_RIGHTS로 AF_UNIX 소켓 fd를 전달할 때:
* unix_inflight() → unix_add_edges()
* → predecessor: fd를 보낸 소켓
* → successor: fd가 참조하는 소켓 (전달된 fd의 소켓)
* → edge를 predecessor의 vertex->edges에 추가
*
* recvmsg()로 수신하거나 close()로 폐기할 때:
* unix_notinflight() → unix_del_edges()
* → edge 제거
* → vertex->edges가 빈 리스트면 vertex도 해제
*/
/* Tarjan 알고리즘 적용 */
static void __unix_walk_scc(struct unix_vertex *vertex) {
/* 표준 Tarjan SCC 알고리즘:
*
* vertex->index = vertex->lowlink = scc_index++;
* vertex->on_stack = true;
* push(stack, vertex);
*
* for each edge in vertex->edges:
* successor = edge->successor->vertex;
* if (successor->index == -1):
* // 미방문 → 재귀 DFS
* __unix_walk_scc(successor);
* vertex->lowlink = min(vertex->lowlink, successor->lowlink);
* else if (successor->on_stack):
* // 스택에 있음 → 같은 SCC
* vertex->lowlink = min(vertex->lowlink, successor->index);
*
* if (vertex->lowlink == vertex->index):
* // SCC 루트 발견 → 스택에서 SCC 멤버 추출
* do:
* w = pop(stack);
* w->on_stack = false;
* add_to_scc(vertex->scc_entry, w);
* while (w != vertex);
*/
}
/* SCC 도달 가능성 판정 */
/*
* 각 SCC에 대해:
* 1. SCC 내 소켓의 총 참조 카운트 합산
* 2. SCC 내부 간선으로 인한 참조 감산
* 3. 잔여 참조 > 0 → SCC 전체 보존 (외부에서 도달 가능)
* 4. 잔여 참조 == 0 → SCC 전체 회수
*
* 보존된 SCC에서 참조하는 다른 SCC도 재귀적으로 보존
* → BFS/DFS로 도달 가능 SCC 전파
*/
/* 성능 영향 분석 */
/*
* GC 트리거: unix_gc() 호출 시점
* - close() 시 inflight 감소 후 wait_for_unix_gc() 호출
* - unix_tot_inflight가 감소할 때
* - work queue에서 비동기 실행 (gc_work)
*
* 잠금:
* - unix_gc_lock: GC 실행 중 다른 GC 방지
* - 개별 소켓 잠금은 최소화 (SCC 단위 처리)
*
* 벤치마크 (Kuniyuki Iwashima 패치 기준):
* - 10,000개 순환 참조 소켓 회수:
* 기존: ~200ms → 새 GC: ~5ms (40x 개선)
* - 100,000개 inflight fd:
* 기존: O(N^2) → 새 GC: O(N) 선형 시간
*
* Wayland/컨테이너 환경:
* - DMA-BUF fd 대량 전달 시 GC 부하 현저히 감소
* - Kubernetes pod 수십~수백 개 환경에서도 GC 지연 최소화
*/
SOCK_SEQPACKET
SOCK_SEQPACKET은 SOCK_STREAM의 연결 지향성과 SOCK_DGRAM의 메시지 경계 보존을 결합합니다. AF_UNIX에서의 내부 구현과 실전 활용 패턴을 분석합니다.
/* SOCK_SEQPACKET 내부 구현 분석 */
/* unix_seqpacket_ops — proto_ops 매핑 */
static const struct proto_ops unix_seqpacket_ops = {
.family = PF_UNIX,
.bind = unix_bind,
.connect = unix_stream_connect, /* STREAM과 동일! */
.accept = unix_accept, /* STREAM과 동일 */
.listen = unix_listen, /* STREAM과 동일 */
.sendmsg = unix_seqpacket_sendmsg, /* 고유 전송 함수 */
.recvmsg = unix_seqpacket_recvmsg, /* 고유 수신 함수 */
/* ... */
};
/* unix_seqpacket_sendmsg() */
/*
* unix_dgram_sendmsg()를 호출하되:
* - 연결 상태(TCP_ESTABLISHED) 확인
* - 미연결 시 -ENOTCONN 반환
* - sendto()로 주소 지정 불가 (DGRAM과 차이)
*
* 메시지 경계:
* - 하나의 sendmsg() = 하나의 skb = 하나의 메시지
* - 수신측에서 정확히 하나의 메시지 단위로 읽음
* - MSG_EOR (End of Record) 플래그 자동 설정
*/
/* unix_seqpacket_recvmsg() */
/*
* unix_dgram_recvmsg()를 호출하되:
* - 메시지 전체를 한 번에 읽음
* - 버퍼보다 큰 메시지: 초과분 버림 + MSG_TRUNC 설정
* - STREAM처럼 부분 읽기 없음
*
* STREAM과의 핵심 차이:
* send("Hello", 5); send("World", 5);
*
* STREAM recv(buf, 10): "HelloWorld" (합쳐짐)
* SEQPACKET recv(buf, 10): "Hello" (첫 메시지만)
* SEQPACKET recv(buf, 10): "World" (두 번째 메시지)
*/
성능 최적화
UDS의 성능을 극대화하기 위한 splice/sendfile, io_uring 통합, MSG_ZEROCOPY, 버퍼 크기 튜닝 기법을 상세히 다룹니다.
/* splice/sendfile을 통한 zero-copy 전송 */
/*
* splice(file_fd, NULL, unix_fd, NULL, len, SPLICE_F_MOVE);
*
* 커널 경로:
* → do_splice() → splice_file_to_pipe() → pipe → splice_pipe_to_sock()
* → unix_stream_splice_read() (수신측)
*
* 핵심: 사용자 공간 버퍼를 경유하지 않음
* → 파일 서빙 시나리오(nginx → PHP-FPM 등)에서 성능 향상
*
* 제한사항:
* - SCM_RIGHTS/CREDENTIALS와 함께 사용 불가
* - SOCK_STREAM에서만 지원
* - SOCK_DGRAM/SEQPACKET에는 splice 미지원
*/
/* io_uring UDS 활용 패턴 */
/*
* 지원 오퍼레이션 (커널 5.6+):
* IORING_OP_SENDMSG — sendmsg() 비동기 처리
* IORING_OP_RECVMSG — recvmsg() 비동기 처리
* IORING_OP_CONNECT — connect() 비동기 (5.5+)
* IORING_OP_ACCEPT — accept() 비동기 (5.5+)
* IORING_OP_SEND — send() 비동기 (5.6+)
* IORING_OP_RECV — recv() 비동기 (5.6+)
*
* 최적 활용:
* 1. SQPOLL 모드: io_uring_params.flags |= IORING_SETUP_SQPOLL
* → 커널 쓰레드가 SQ 폴링 → 시스콜 0회
* → 레이턴시 최소화 (CPU 사용 증가 트레이드오프)
*
* 2. 멀티샷 recv: IORING_RECV_MULTISHOT (6.0+)
* → 하나의 SQE로 여러 메시지 수신
* → CQE에 메시지별 결과 반환
*
* 3. 고정 버퍼: IORING_REGISTER_BUFFERS
* → 사전 등록된 버퍼로 매핑 오버헤드 제거
*
* 벤치마크 (대략적 수치):
* epoll + sendmsg/recvmsg: ~200K msg/s
* io_uring (기본): ~350K msg/s
* io_uring (SQPOLL): ~500K msg/s
*/
/* 버퍼 크기 튜닝 권장 */
/*
* 대용량 전송 (로그 수집, DB 덤프 등):
* setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &(int){1048576}, 4);
* setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &(int){1048576}, 4);
* → 시스템 상한: net.core.wmem_max / rmem_max 확인
*
* 저레이턴시 (RPC, D-Bus 대체):
* → 작은 버퍼 + io_uring SQPOLL
* → cork/uncork 활용:
* setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &(int){16384}, 4);
* → 작은 메시지는 빠르게, Nagle 없음
*
* DGRAM 큐 튜닝:
* sysctl -w net.unix.max_dgram_qlen=512
* → 기본 10은 syslog 과부하 시 메시지 유실 원인
*/
컨테이너에서의 AF_UNIX
컨테이너 환경(Docker, Kubernetes)에서 AF_UNIX 소켓의 네임스페이스 격리, 소켓 전달 패턴, systemd 소켓 활성화와의 통합을 분석합니다.
/* 컨테이너 환경 AF_UNIX 실전 패턴 */
/* 패턴 1: Kubernetes 사이드카 소켓 공유 */
/*
* Pod 내 컨테이너 간 UDS 통신:
*
* apiVersion: v1
* kind: Pod
* spec:
* volumes:
* - name: shared-sock
* emptyDir: {} # tmpfs → 빠르고 자동 정리
* containers:
* - name: app
* volumeMounts:
* - name: shared-sock
* mountPath: /run/shared
* # app이 /run/shared/app.sock에 바인딩
*
* - name: envoy-sidecar
* volumeMounts:
* - name: shared-sock
* mountPath: /run/shared
* # envoy가 /run/shared/app.sock에 connect
*
* → 같은 mount namespace가 아니지만 볼륨 공유로 소켓 접근
* → TCP 루프백 대비 30-50% 레이턴시 감소
*/
/* 패턴 2: Docker 소켓 보안 위험 완화 */
/*
* 위험: -v /var/run/docker.sock:/var/run/docker.sock
* → 컨테이너가 Docker API에 풀 액세스
* → docker run --privileged ... 실행 가능
* → 사실상 호스트 root 권한 획득
*
* 완화 방법:
* 1. 소켓 프록시 (docker-socket-proxy):
* → 읽기 전용 API만 허용하는 프록시 소켓 노출
*
* 2. rootless Docker:
* → 소켓이 사용자 네임스페이스 내에 생성
* → 호스트 root 접근 불가
*
* 3. SELinux/AppArmor 정책:
* → 컨테이너의 소켓 접근 범위 제한
* → container_t가 docker_var_run_t에 connectto만 허용
*/
/* 패턴 3: systemd 소켓 활성화 + 컨테이너 */
/*
* systemd-nspawn / Podman + systemd:
*
* [Socket]
* ListenStream=/run/myapp.sock
* SocketMode=0660
* SocketGroup=myapp
*
* [Service]
* ExecStart=/usr/bin/myapp
* # myapp은 sd_listen_fds()로 fd 획득
* # LISTEN_FDS=1, LISTEN_PID= 환경변수 확인
*
* 컨테이너 내 동작:
* - 컨테이너의 PID 1이 systemd면 소켓 유닛 사용 가능
* - Podman --systemd=true로 systemd 통합
* - 소켓 fd는 exec() 체인에서 상속
*
* 장점:
* - 서비스 미실행 시에도 소켓 바인딩 유지
* - 서비스 재시작 중 연결 보존 (listen backlog)
* - 지연 시작으로 리소스 절약
*/
/var/run/docker.sock을 컨테이너에 마운트하는 것은 CI/CD 파이프라인(Pipeline)에서 흔히 사용되지만, 컨테이너 탈출(container escape)의 주요 벡터입니다. 대안으로 Docker-in-Docker(DinD), Kaniko, Buildah 등 소켓 마운트가 필요 없는 도구를 사용하거나, 읽기 전용(Read-Only) 소켓 프록시를 사이에 배치하세요.
디버깅
ss -x, /proc/net/unix, ftrace, BPF 추적을 활용한 고급 디버깅 기법과 실전 트러블슈팅 패턴을 다룹니다.
# ===== 고급 디버깅 기법 =====
# 1. ss -x: 소켓 상태 상세 분석
$ ss -xape
# 출력 필드 해석:
# Netid: u_str(STREAM), u_dgr(DGRAM), u_seq(SEQPACKET)
# State: LISTEN, ESTAB, UNCONN
# Recv-Q: 수신 큐에 대기 중인 바이트 수 (LISTEN 시: 대기 연결 수)
# Send-Q: 송신 큐에 대기 중인 바이트 수 (LISTEN 시: backlog 크기)
# Local Address: 소켓 경로 또는 * (이름 없음)
# Peer Address: 연결된 피어의 inode 번호
# 2. 소켓 피어 매칭: inode 기반
$ ss -xpe | grep -E 'ESTAB|ino:'
# "ino:12345" = 로컬 소켓 inode
# "peer:67890" = 피어 소켓 inode
# → 양쪽을 매칭하면 연결된 쌍을 식별
# 3. /proc/net/unix 상세 분석
$ awk '
NR==1 { print; next }
{
flags = strtonum("0x" substr($4, 1))
type_str = ($5 == "0001") ? "STREAM" :
($5 == "0002") ? "DGRAM" :
($5 == "0005") ? "SEQPACKET" : $5
state_str = ($6 == "01") ? "UNCONNECTED" :
($6 == "02") ? "CONNECTING" :
($6 == "03") ? "CONNECTED" : $6
printf "%-20s Type=%-10s State=%-14s Inode=%-8s Ref=%s Path=%s\n",
$1, type_str, state_str, $7, $2, $8
}
' /proc/net/unix
# 4. ftrace로 소켓 함수 추적
$ echo 'unix_stream_connect' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo 'unix_bind' >> /sys/kernel/debug/tracing/set_ftrace_filter
$ echo 'unix_gc' >> /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 재현 후 ...
$ cat /sys/kernel/debug/tracing/trace
# 5. bpftrace: GC 성능 모니터링
$ bpftrace -e '
kprobe:unix_gc {
@gc_start[tid] = nsecs;
}
kretprobe:unix_gc /@gc_start[tid]/ {
@gc_duration_us = hist((nsecs - @gc_start[tid]) / 1000);
delete(@gc_start[tid]);
}
interval:s:30 { print(@gc_duration_us); }
'
# 6. bpftrace: SCM_RIGHTS fd 전달 실시간 추적
$ bpftrace -e '
kprobe:unix_attach_fds {
printf("[%s] pid=%d comm=%s: SCM_RIGHTS attach\n",
strftime("%H:%M:%S", nsecs), pid, comm);
}
kprobe:unix_detach_fds {
printf("[%s] pid=%d comm=%s: SCM_RIGHTS detach\n",
strftime("%H:%M:%S", nsecs), pid, comm);
}
kprobe:unix_inflight {
@inflight_by_comm[comm] = count();
}
'
# 7. bpftrace: 연결 지연 분석
$ bpftrace -e '
kprobe:unix_stream_connect {
@start[tid] = nsecs;
}
kretprobe:unix_stream_connect /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
if ($dur > 100) {
printf("slow connect: pid=%d comm=%s dur=%d us ret=%d\n",
pid, comm, $dur, retval);
}
@connect_us = hist($dur);
delete(@start[tid]);
}
'
# 8. perf로 UDS 시스콜 프로파일링
$ perf stat -e 'syscalls:sys_enter_sendmsg,syscalls:sys_enter_recvmsg' \
-p 1234 -- sleep 10
# 9. 소켓 파일 누수 검출
$ find /run /tmp /var/run -type s -printf '%T@ %p\n' 2>/dev/null | \
sort -n | while read ts path; do
if ! fuser "$path" 2>/dev/null; then
echo "STALE: $path"
fi
done
| 증상 | 원인 | 진단 도구 | 해결 |
|---|---|---|---|
| EADDRINUSE | stale 소켓 파일 잔존 | ls -la /path/to/sock, fuser |
unlink() 또는 bind 전 삭제 로직 |
| ECONNREFUSED | 서버 미시작/종료됨 | ss -xlnp, systemctl status |
서버 재시작(Reboot), listen 상태 확인 |
| fd 고갈 | SCM_RIGHTS fd 미수신 | /proc/pid/fd 개수, ulimit -n |
recvmsg()에서 fd 수신 확인, 불필요 시 close() |
| 메모리 증가 | 수신 큐 적체/in-flight fd | ss -xm (메모리 정보), bpftrace |
수신측 처리 속도 개선, 버퍼 크기 조정 |
| GC 지연 | 대량 순환 참조 fd | bpftrace kprobe:unix_gc |
fd 전달 패턴 개선, 커널 6.x+ GC 사용 |
| 권한 거부 | 소켓 파일/디렉터리 권한 | ls -la, namei -l |
chmod/chown, SELinux restorecon |
참고자료
- unix(7) man page
- cmsg(3) man page — Ancillary Data
- Linux Kernel Source: net/unix/
- Linux Kernel Source: include/net/af_unix.h
- LWN: Rethinking the Unix domain socket garbage collector
- socket(2) man page — AF_UNIX 도메인을 포함한 소켓 생성 시스템 콜 문서입니다
- socketpair(2) man page — 연결된 소켓 쌍 생성 시스템 콜 문서입니다
- cmsg(3) man page — 소켓 보조 데이터(Ancillary Data) 매크로 문서입니다
- sendmsg(2) man page — SCM_RIGHTS를 이용한 파일 디스크립터 전달에 사용되는 시스템 콜 문서입니다
- credentials(7) man page — SCM_CREDENTIALS를 통한 프로세스 자격 증명 전달 문서입니다
- kernel.org: AF_UNIX Documentation — 커널 공식 Unix 도메인 소켓 문서입니다
- 커널 소스: net/unix/af_unix.c — Unix 도메인 소켓 핵심 구현 소스 코드입니다
- LWN: Unix domain socket performance — Unix 도메인 소켓 성능 분석에 관한 LWN 문서입니다
관련 문서
Unix Domain Socket과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.