Unix Domain Socket

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

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

핵심 요약

  • AF_UNIX — 같은 호스트 내 프로세스(Process) 간 통신 전용 소켓 패밀리(네트워크 스택 미경유)
  • SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET — 바이트 스트림, 데이터그램, 순서 보장(Ordering) 메시지 세 가지 타입 지원
  • 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의 커널 구현은 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.cunix_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와의 차이: TCP는 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 카운터 감소
 */
보안 고려사항: SCM_RIGHTS로 전달받은 fd는 recvmsg()로 수신하지 않으면 커널 메모리에 계속 남아있습니다. 이는 파일 디스크립터 누수와 메모리 누수를 유발할 수 있으며, 악의적인 프로세스가 대량의 fd를 in-flight 상태로 만들어 시스템 자원을 고갈시킬 수 있습니다. 이를 방지하기 위해 net.unix.max_dgram_qlen과 소켓 버퍼(Buffer) 크기를 적절히 설정해야 합니다.
SCM_RIGHTS: 파일 디스크립터 전달 흐름 송신 프로세스 A fd 테이블 fd 3 fd 7 fd 5 (UDS) sendmsg(fd=5, SCM_RIGHTS, [fd3, fd7]) 수신 프로세스 B fd 테이블 fd 4 fd 6 (UDS) fd 8 ! recvmsg(fd=6) → 새 fd 8, 9 할당 커널 공간 1. scm_fp_copy() fget_raw(fd) → struct file* fd 번호를 file 구조체로 변환 2. unix_scm_to_skb() UNIXCB(skb).fp = scm.fp skb에 file* 목록 부착 3. unix_inflight() in-flight 카운터 증가 GC 추적 시작 4. skb_queue_tail(&peer->sk_receive_queue, skb) 피어 소켓 수신 큐에 삽입 (file* 포함) 5. receive_fd() → 수신 프로세스의 fd 테이블에 새 fd 할당 + unix_notinflight() 핵심: 동일 struct file*을 공유 -- fd 번호는 프로세스마다 다르지만 같은 파일 객체를 가리킴

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 네임스페이스로 격리
이식성 주의: 추상 네임스페이스는 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 필요 판단
 */
unix_gc() 순환 참조 감지와 회수 순환 참조 형성 소켓 A refcount = 2 user_fd(1) + inflight(1) recv_q: [B의 fd] 소켓 B refcount = 2 user_fd(1) + inflight(1) recv_q: [A의 fd] fd 전달 close(A_fd), close(B_fd) 이후: refcount = 1 (inflight만 남음) → 일반 해제 불가! unix_gc() 알고리즘 Phase 1: 후보 수집 gc_inflight_list 순회 inflight > 0인 소켓 → gc_candidates 리스트 Phase 2: 내부 참조 감산 각 후보의 recv_q 스캔 큐의 file이 다른 후보 참조 → 해당 소켓의 외부 참조 카운트 감소 Phase 3: 도달 가능성 판정 외부 ref > 0 → 도달 가능 (보존) 외부 ref == 0 → 도달 불가 (회수 대상) Phase 4: 회수 (Sweep) recv_q에서 skb 제거 → fput() 순환 참조 해소 → 소켓 + 파일 해제 GC 트리거 커널 6.x: SCC(강결합 컴포넌트) 기반 새 GC 알고리즘 도입 → O(vertices + edges) 복잡도, 확장성 개선
/* 커널 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가 있으면 dmesgGC: too many inflight fds 경고가 나타날 수 있습니다.

GC와 성능: SCM_RIGHTS를 대량으로 사용하는 애플리케이션(예: Wayland 컴포지터, 컨테이너 런타임)에서는 GC 빈도가 높아질 수 있습니다. 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 렌더링)
 *
 * → 대용량 이미지 데이터를 복사 없이 프로세스 간 공유
 */
HTTP-over-UDS 패턴: Docker, containerd, snapd 등은 TCP 대신 UDS 위에서 HTTP 프로토콜을 사용합니다. 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()로 설정 가능한 상한
max_dgram_qlen 주의: 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로 전달
 */
컨테이너 보안: Docker/Kubernetes에서 호스트의 UDS 소켓을 컨테이너에 마운트(Mount)하면(예: 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, 지연(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.cunix_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 상태 이름 재사용: UDS 코드에서 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_addressunix_skb_parms의 내부 필드까지 분석합니다.

unix_sock 구조체 레이아웃과 연관 구조체 struct unix_sock struct sock sk (내장) sk_receive_queue, sk_state, sk_sndbuf, sk_rmem_alloc, ... 소켓 공통 프레임워크 (net/core/sock.c) struct unix_address *addr 바인딩 주소 struct path path VFS 경로(dentry+vfsmount) struct mutex iolock sendmsg/recvmsg 직렬화 struct sock *peer 연결된 상대 소켓 struct sock *listener STREAM: 원본 리스닝 소켓 struct unix_vertex *vertex GC 그래프 정점 spinlock_t lock 필드 보호 스핀락 unsigned long gc_flags UNIX_GC_CANDIDATE 등 wait_queue_head_t peer_wait DGRAM 피어 대기 struct scm_stat scm_stat SCM 바이트/fd 누적 struct sk_buff_head oob_skb OOB 데이터 큐 (5.15+) struct list_head link 해시 테이블 연결 atomic_long_t inflight 전송 중 fd 수 struct unix_address refcount_t refcnt 참조 카운트 int len 주소 전체 길이 struct sockaddr_un name[0] 가변 길이 주소 sun_family(2B) + sun_path(최대 108B) path[0]=='\0' → 추상 NS / 그 외 → 파일시스템 UNIXCB (unix_skb_parms) skb->cb[]에 저장되는 AF_UNIX 메타데이터 struct pid *pid 송신자 PID 구조체 kuid_t uid / kgid_t gid 송신자 UID/GID struct scm_fp_list *fp SCM_RIGHTS fd 목록 u32 consumed STREAM 부분 읽기 오프셋 struct scm_fp_list short count 전달할 fd 개수 (max 253) short max 할당된 배열 크기 struct file *fp[SCM_MAX_FD] file 구조체 포인터 배열 unix_sock은 container_of(sk, struct unix_sock, sk)로 sock에서 역참조 — unix_sk() 매크로
/* 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 알고리즘의 스택/방문 상태 관리
 */
container_of 패턴: 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의 차이를 상세히 분석합니다.

SOCK_STREAM 연결 수립 과정 (상세) 서버 측 클라이언트 측 1. socket(AF_UNIX, SOCK_STREAM, 0) 2. bind("/tmp/srv.sock") → unix_bind() → mknod(S_IFSOCK) → 해시 테이블 등록 3. listen(fd, backlog=128) sk_state: TCP_CLOSE → TCP_LISTEN 1. socket(AF_UNIX, SOCK_STREAM, 0) 2. connect("/tmp/srv.sock") → unix_find_other() 서버 검색 → unix_create1() 새 소켓 할당 → skb에 새 소켓 부착 → 서버 큐 삽입 연결 요청 skb 4. accept() — 블로킹 대기 → sk_receive_queue에서 skb 디큐 → skb에서 새 소켓 추출 → 새 fd 할당 TCP_ESTABLISHED 상태 — 양방향 데이터 전송 가능 서버 accept_fd ↔ 클라이언트 connect_fd (peer 포인터 상호 참조) sendmsg() / recvmsg() — sk_receive_queue를 통한 데이터 교환 shutdown(fd, SHUT_WR) → sk_state = TCP_CLOSE, 피어에 EOF 전달 → peer->sk_shutdown |= RCV_SHUTDOWN close(fd) → unix_release_sock(): peer 분리, 큐 정리 → sock_put(): refcount 감소 → 0이면 해제 unix_release_sock(): 파일시스템 소켓이면 inode 참조 해제 → 해시 테이블에서 제거 → inflight fd 있으면 GC 트리거 가능 → sk_free()
/* 소켓 종료 경로 상세: 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 vs close 차이: 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: 참조 카운트 전이와 LSM 훅 송신 경로: sendmsg() → scm_send() scm_fp_copy() fget_raw(fd) 호출 file->f_count++ refcnt: 1 → 2 security_file_receive() LSM 훅: SELinux/AppArmor fd 전달 권한 검사 거부 시 → -EPERM unix_scm_to_skb() UNIXCB(skb).fp = scm.fp unix_inflight(fp->fp[i]) inflight++, GC 추적 시작 skb 큐 삽입 peer의 recv_q 에 삽입 완료 file ref: 2 전송 중 (In-Flight) 상태 송신측이 close(fd) → file refcnt 1로 감소 — 하지만 skb가 참조 유지 → 파일 해제 안 됨 수신측이 recvmsg() 호출 전까지 커널 메모리에 file* 보관 (fd 누수 위험 구간) 수신 경로: recvmsg() → scm_recv() scm_detach_fds() 각 file*에 대해: receive_fd_replace() → 새 fd 번호 할당 security_file_receive() 수신 프로세스의 보안 컨텍스트로 fd 수신 권한 검증 fd_install() 수신 프로세스의 fd 테이블에 등록 새 fd 번호 부여 unix_notinflight() inflight-- 감소 gc_inflight_list 에서 제거 가능 오류/누수 경로 MSG_PEEK로 수신 fd 복제 안 됨 — 재수신 시 fd 할당 수신 없이 close() → skb_queue_purge → fput() → GC 가능 MSG_CMSG_CLOEXEC 미설정 exec() 시 fd 상속 → 보안 위험
/* 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 사용
 */
MSG_CMSG_CLOEXEC 필수: recvmsg()에서 SCM_RIGHTS fd를 수신할 때 반드시 MSG_CMSG_CLOEXEC 플래그를 사용하세요. 이 플래그 없이 수신하면 exec() 시 fd가 자식 프로세스에 상속되어, 의도하지 않은 파일 접근 권한이 전파될 수 있습니다. 컨테이너 런타임이나 권한 분리 데몬에서는 특히 중요합니다.

SCM_CREDENTIALS

SCM_CREDENTIALSSO_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+
PID 재사용 문제와 SO_PEERPIDFD: SO_PEERCRED가 반환하는 PID는 정수값이므로, 피어 프로세스가 종료된 후 같은 PID가 다른 프로세스에 재할당될 수 있습니다(TOCTOU 경쟁). SO_PEERPIDFD는 이 문제를 해결합니다. pidfd는 프로세스의 struct pid에 대한 참조를 유지하므로, 프로세스가 종료되더라도 PID가 재사용되지 않습니다. systemd 255+ 등 최신 서비스 관리자에서 활용이 시작되고 있습니다.

추상 네임스페이스 vs 파일시스템 바인딩

두 바인딩 방식의 보안 모델, 접근 제어, 자동 정리 동작, 컨테이너 환경에서의 격리 차이를 커널 코드 수준에서 비교 분석합니다.

파일시스템 소켓 vs 추상 네임스페이스: 보안 모델 비교 파일시스템 소켓 sun_path = "/run/myservice.sock" 바인딩: unix_bind_bsd() kern_path_create() → VFS에 S_IFSOCK inode 생성 vfs_mknod() → 파일 시스템에 실제 파일 생성 접근 제어 (3단계) 1. 디렉터리 권한: /run/ 의 x 권한 필요 2. 소켓 파일 권한: w 권한 필요 (connect 시) 3. LSM: SELinux/AppArmor 정책 적용 수명 관리 명시적 unlink() 필요 — 프로세스 종료 시 파일 잔존(stale) 격리 mount namespace로 격리 — bind mount로 선택적 공유 가능 장점/단점 + 세밀한 파일 권한 제어 (chmod/chown) + 모든 Unix 계열 OS 호환 - stale 파일 정리 필요 (EADDRINUSE) - VFS 경로 탐색 오버헤드 추상 네임스페이스 (Linux 전용) sun_path[0] = '\0', "myservice" 바인딩: unix_bind_abstract() 해시 테이블에만 등록 — VFS 미사용 파일 시스템에 흔적 없음 접근 제어 (제한적) 1. 파일 권한 없음 — chmod/chown 불가 2. 같은 net NS 내 모든 프로세스 접근 가능 3. LSM만 유일한 세밀한 접근 제어 수단 수명 관리 자동 정리 — 모든 fd 닫히면 해시 테이블에서 자동 제거 격리 network namespace로 격리 — unshare(CLONE_NEWNET)로 분리 장점/단점 + stale 파일 문제 없음 (자동 정리) + VFS 미경유로 bind/connect 빠름 - 파일 권한 제어 불가 - Linux 전용 (macOS/BSD 미지원)
/* 바인딩 경로 비교: 커널 코드 추적 */

/* 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()
 */
추상 네임스페이스 보안 위험: 추상 네임스페이스 소켓은 같은 network namespace 내의 모든 프로세스가 이름만 알면 접근할 수 있습니다. Docker 컨테이너에서 --net=host를 사용하면 호스트의 추상 소켓에 접근 가능하므로, D-Bus 시스템 버스(Bus)나 다른 서비스의 추상 소켓이 노출됩니다. 보안이 중요한 환경에서는 파일시스템 소켓을 사용하거나, SELinux/AppArmor 정책으로 추상 소켓 접근을 제한해야 합니다.

GC (Garbage Collection)

커널 6.x에서 도입된 SCC(강결합 컴포넌트) 기반 새 GC 알고리즘의 상세 동작을 분석합니다. Tarjan 알고리즘 적용, 그래프 구조, 성능 특성, 그리고 대규모 in-flight fd 환경에서의 동작을 추적합니다.

SCC 기반 GC 알고리즘 (커널 6.x+) 1단계: 그래프 구성 (unix_vertex + unix_edge) A B C D E fd 외부 참조 SCC 1: {A, B} 순환 SCC 2: {C, D} 순환 E: 외부 참조 2단계: Tarjan 알고리즘 — SCC 탐색 DFS 순회 각 vertex에 index, lowlink 할당 스택에 push/pop으로 SCC 식별 O(V + E) 시간 복잡도 SCC 분류 lowlink == index → SCC 루트 스택에서 SCC 멤버 추출 scc_entry 리스트에 연결 도달 가능성 판정 SCC 외부에서 참조 있음? 예 → 보존 (E→C→D 보존) 아니오 → 회수 대상 3단계: 회수 — SCC {A, B} 외부 참조 없음 → 도달 불가능 A의 recv_q에서 B의 fd → fput(B의 file) B의 recv_q에서 A의 fd → fput(A의 file) → 양쪽 refcount 0 → sk_free() → 메모리 해제 순환 참조 해소 완료 3단계: 보존 — SCC {C, D} + E E가 C를 참조 → SCC {C,D} 도달 가능 C와 D는 순환 참조이지만 E의 외부 참조로 보존 E가 close()되면 → 다음 GC에서 {C,D}도 회수 정확한 도달 가능성 분석 → 오탐 방지 기존 GC vs SCC GC 비교 기존 GC (~5.x) 전체 inflight 목록 순회, 단순 마크-앤-스윕 O(N^2) 최악 — 대규모 inflight 시 잠금 경합 SCC GC (6.x+) Tarjan DFS, 그래프 정점/간선 기반 O(V + E) — 정확한 SCC 식별, 확장성 향상
/* 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_SEQPACKETSOCK_STREAM의 연결 지향성과 SOCK_DGRAM의 메시지 경계 보존을 결합합니다. AF_UNIX에서의 내부 구현과 실전 활용 패턴을 분석합니다.

데이터 전송 경로: STREAM vs DGRAM vs SEQPACKET SOCK_STREAM unix_stream_sendmsg() 대용량 → 여러 skb로 분할 메시지 경계 없음 (바이트 스트림) 연속 바이트 스트림 (경계 없음) unix_stream_recvmsg() 부분 읽기 가능 (consumed 추적) 특성: - 연결 필수 (connect/accept) - 순서 보장, 부분 읽기 OK 사용 예: D-Bus, SSH Agent, MySQL/PostgreSQL 로컬 연결 SOCK_DGRAM unix_dgram_sendmsg() 메시지 1개 = skb 1개 메시지 경계 보존 개별 메시지 (경계 보존) unix_dgram_recvmsg() 메시지 전체 읽기 (초과분 버림) 특성: - 비연결 (sendto로 목적지 지정) - 메시지 유실 가능 (큐 초과 시) 사용 예: syslog (/dev/log), rsyslog, journald 로그 수집 SOCK_SEQPACKET unix_seqpacket_sendmsg() 메시지 1개 = skb 1개 연결 상태에서만 전송 1 2 3 순서 보장 + 경계 보존 unix_seqpacket_recvmsg() 메시지 전체 읽기 + 신뢰성 보장 특성: - 연결 필수 (connect/accept) - 메시지 유실 없음, 순서 보장 사용 예: Bluetooth L2CAP, 커스텀 RPC, 메시지 기반 프로토콜 IPC
/* 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, 버퍼 크기 튜닝 기법을 상세히 다룹니다.

io_uring + AF_UNIX 비동기 I/O 통합 전통적 경로 (syscall 기반) sendmsg() 시스콜 커널 진입/복귀 오버헤드 메시지당 시스콜 1회 (컨텍스트 스위치) epoll_wait() → sendmsg() → recvmsg() → epoll_wait() ... 높은 시스콜 빈도 → 마이크로초 단위 오버헤드 누적 레이턴시: ~3-5 us/msg io_uring 경로 (배치 처리) SQ에 요청 적재 커널이 배치 처리 N개 요청을 1회 io_uring_enter()로 제출 IORING_OP_SENDMSG / IORING_OP_RECVMSG SQPOLL 모드: 시스콜 0회 (커널 쓰레드 폴링) 레이턴시: ~1-2 us/msg (SQPOLL) 성능 최적화 기법 비교 splice/sendfile (zero-copy) 파일 → UDS 직접 전송 사용자 공간 복사 제거 MSG_ZEROCOPY (6.2+) 페이지 핀닝으로 복사 회피 32KB+ 전송에서 이점 버퍼 크기 튜닝 SO_SNDBUF/SO_RCVBUF 증가 대용량 전송 처리량 개선 권장 조합: io_uring + 큰 버퍼 + MSG_ZEROCOPY (대용량) 또는 io_uring + SQPOLL (저레이턴시) splice: 파일 서빙 시 최적 / MSG_ZEROCOPY: 메모리 대 메모리 대용량 전송 / SQPOLL: 시스콜 제로 저레이턴시
/* 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 격리와 공유 호스트 (Host) 호스트 서비스 /var/run/docker.sock (FS 소켓) \0/org/freedesktop/DBus (추상 NS) 호스트 mount NS + net NS 컨테이너 A (격리됨) 별도 mount NS + net NS 파일시스템 소켓 격리 /run/app.sock → 컨테이너 자체 mount NS 호스트의 /var/run/*.sock 보이지 않음 추상 네임스페이스 격리 \0my-service → 컨테이너 자체 net NS 호스트의 추상 소켓 접근 불가 격리 무효화 시나리오 --net=host → 추상 NS 공유 (위험!) -v /var/run/docker.sock:... → FS 소켓 노출 --pid=host → SCM_CREDENTIALS PID 매핑 변경 --privileged → 모든 격리 약화 컨테이너 B (공유 패턴) 사이드카 패턴 / 소켓 공유 볼륨 마운트로 소켓 공유 emptyDir 볼륨에 소켓 생성 → 다른 컨테이너 접근 Envoy 사이드카 ↔ 앱 컨테이너 통신 systemd 소켓 활성화 ListenStream=/run/app.sock → 컨테이너 서비스 시작 sd_listen_fds()로 상속 fd 획득 컨테이너 내 SCM_CREDENTIALS 주의 PID namespace: 컨테이너 내부 PID로 번역됨 UID namespace: user namespace 매핑 적용 SO_PEERCRED: init_pid_ns PID → 컨테이너 내에서 무의미 가능 SO_PEERPIDFD (6.5+): pidfd 기반 → 네임스페이스 안전 -v 마운트 시 노출
/* 컨테이너 환경 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)
 * - 지연 시작으로 리소스 절약
 */
Docker 소켓 마운트 위험: /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 Domain Socket과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.