Unix Domain Socket 심화

Linux AF_UNIX(Unix Domain Socket) 소켓의 커널 내부 구현을 심층 분석합니다. unix_sock 자료구조, 파일시스템 소켓과 추상 네임스페이스, 데이터 전송 경로, SCM_RIGHTS를 통한 파일 디스크립터 전달, SCM_CREDENTIALS 자격 증명, 가비지 컬렉션, socketpair IPC 패턴, 보안 모델, TCP 대비 성능 특성, 운영 디버깅까지 다룹니다.

전제 조건: IPC네트워크 스택 문서를 먼저 읽으세요. Unix Domain Socket은 네트워크 소켓 API를 사용하지만 실제로는 로컬 IPC 메커니즘이므로, 양쪽 개념을 모두 이해해야 합니다.
일상 비유: 이 주제는 같은 건물 내 우편함과 비슷합니다. TCP/UDP가 우체국을 거치는 외부 우편이라면, Unix Domain Socket은 같은 건물 내 사서함끼리 직접 주고받는 내부 우편입니다. 네트워크 프로토콜 오버헤드 없이 커널 메모리 복사만으로 빠르게 데이터를 전달합니다.

핵심 요약

  • AF_UNIX — 같은 호스트 내 프로세스 간 통신 전용 소켓 패밀리(네트워크 스택 미경유)
  • SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET — 바이트 스트림, 데이터그램, 순서 보장 메시지 세 가지 타입 지원
  • SCM_RIGHTS — 프로세스 간 파일 디스크립터 전달(ancillary data)
  • SCM_CREDENTIALS — PID/UID/GID 자격 증명 전달 및 검증
  • 추상 네임스페이스 — Linux 전용, 파일시스템 경로 없이 \0 접두사로 바인딩
  • socketpair() — 연결된 소켓 쌍을 한 번에 생성하는 경량 IPC

단계별 이해

  1. 소켓 구조 이해
    struct unix_sock과 sockaddr_un 구조를 먼저 파악합니다.
  2. 생성-연결 경로
    socket(), bind(), listen(), accept(), connect()의 커널 내부 호출 경로를 추적합니다.
  3. 데이터 전송
    sendmsg/recvmsg에서 sk_buff 없이 직접 복사가 이루어지는 경로를 확인합니다.
  4. 보조 데이터
    SCM_RIGHTS, SCM_CREDENTIALS로 파일 디스크립터와 자격 증명을 전달하는 메커니즘을 이해합니다.
  5. 운영 디버깅
    ss, /proc/net/unix, bpftrace로 소켓 상태를 모니터링합니다.

AF_UNIX 소켓 아키텍처

Unix Domain Socket(이하 UDS)은 net/unix/ 디렉터리에 구현되어 있으며, 네트워크 프로토콜 스택(IP, TCP/UDP)을 전혀 거치지 않습니다. 핵심 자료구조인 struct unix_sockstruct sock을 내장(embed)하여 소켓 프레임워크와 통합됩니다.

AF_UNIX 소켓 아키텍처 사용자 공간 (User Space) socket() / bind() / connect() / sendmsg() / recvmsg() / socketpair() 시스템 콜 인터페이스 (sys_socket / sys_bind / sys_sendmsg ...) 소켓 계층 (struct socket / struct sock) AF_UNIX 서브시스템 (net/unix/) struct unix_sock peer, path, recvq inflight, gc_list 데이터 전송 unix_stream_sendmsg() unix_dgram_sendmsg() 보조 데이터 SCM_RIGHTS (fd 전달) SCM_CREDENTIALS VFS (sockfs / tmpfs) inode, dentry, path 커널 메모리 복사 sk_buff 없는 직접 복사 unix_gc() 순환 참조 fd 감지/회수 AF_UNIX는 IP/TCP/UDP 스택을 완전히 우회 -- 커널 내부 메모리 복사만으로 데이터 전달
/* 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() 전 클라이언트
 */
파일시스템 소켓 vs 추상 네임스페이스: 파일시스템 소켓은 unlink() 전까지 경로에 남아 있으며, 파일 권한으로 접근을 제어합니다. 추상 네임스페이스 소켓은 모든 소켓이 닫히면 자동으로 사라지지만, 파일 권한이 없으므로 같은 네트워크 네임스페이스 내 모든 프로세스가 접근할 수 있습니다.

소켓 생성과 바인딩

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 라우팅, 체크섬, 분할/재조립이 모두 불필요하므로 단순한 커널 메모리 복사만으로 데이터가 전달됩니다.

/* 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와의 차이: TCP는 tcp_sendmsg()에서 데이터를 세그먼트로 분할하고, 혼잡 제어 윈도우를 확인하고, 재전송 타이머를 설정합니다. UDS는 이 모든 과정이 없으므로 같은 크기의 데이터를 전송할 때 CPU 사이클이 현저히 적습니다.

SCM_RIGHTS (파일 디스크립터 전달)

SCM_RIGHTS는 Unix Domain Socket의 가장 강력한 기능 중 하나로, 프로세스 간에 열린 파일 디스크립터를 전달할 수 있습니다. 이 메커니즘은 D-Bus, Wayland, systemd, 컨테이너 런타임 등에서 광범위하게 사용됩니다.

/* 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 카운터 감소
 */
보안 고려사항: SCM_RIGHTS로 전달받은 fd는 recvmsg()로 수신하지 않으면 커널 메모리에 계속 남아있습니다. 이는 파일 디스크립터 누수와 메모리 누수를 유발할 수 있으며, 악의적인 프로세스가 대량의 fd를 in-flight 상태로 만들어 시스템 자원을 고갈시킬 수 있습니다. 이를 방지하기 위해 net.unix.max_dgram_qlen과 소켓 버퍼 크기를 적절히 설정해야 합니다.

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 전용 기능으로, 파일시스템에 소켓 파일을 생성하지 않고 커널 해시 테이블에만 소켓 이름을 등록합니다. 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) 설정 없음
 * - 파일 권한 검사 없음 → 같은 네트워크 네임스페이스 내 모든 프로세스 접근 가능
 */
속성파일시스템 소켓추상 네임스페이스
접근 제어 파일 권한(chmod/chown) 네트워크 네임스페이스로만 격리
수명 unlink() 전까지 유지 모든 fd 닫히면 자동 제거
이식성 모든 Unix 계열 OS Linux 전용
경로 충돌 stale 소켓 파일 문제 충돌 없음 (자동 정리)
컨테이너 mount 네임스페이스로 격리 network 네임스페이스로 격리
이식성 주의: 추상 네임스페이스는 macOS, FreeBSD 등 다른 Unix 계열 OS에서 지원되지 않습니다. 크로스 플랫폼 소프트웨어에서는 파일시스템 소켓을 사용하거나, 런타임에 추상 네임스페이스 지원을 감지하는 코드가 필요합니다. Android는 Linux 기반이므로 추상 네임스페이스를 적극 활용합니다.

가비지 컬렉션 (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 필요 판단
 */
💡

GC 모니터링: /proc/net/unix에서 Inode 컬럼이 0인 항목은 바인딩되지 않은 소켓이며, GC 대상이 될 수 있습니다. 대량의 in-flight fd가 있으면 dmesgGC: too many inflight fds 경고가 나타날 수 있습니다.

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) 소켓 버퍼

보안 모델

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로 전달
 */
컨테이너 보안: Docker/Kubernetes에서 호스트의 UDS 소켓을 컨테이너에 마운트하면(예: Docker 소켓 /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, 지연 ACK
네트워크 필터 Netfilter 미경유 Netfilter/conntrack 경유
레이턴시 약 2-5 us (일반적) 약 10-30 us (일반적)
처리량 메모리 복사 대역폭에 의존 프로토콜 오버헤드로 낮음
# 간단한 레이턴시 비교 (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 모니터링.

Unix Domain Socket과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

참고자료