Unix Domain Socket 심화
Linux AF_UNIX(Unix Domain Socket) 소켓의 커널 내부 구현을 심층 분석합니다. unix_sock 자료구조, 파일시스템 소켓과 추상 네임스페이스, 데이터 전송 경로, SCM_RIGHTS를 통한 파일 디스크립터 전달, SCM_CREDENTIALS 자격 증명, 가비지 컬렉션, socketpair IPC 패턴, 보안 모델, TCP 대비 성능 특성, 운영 디버깅까지 다룹니다.
핵심 요약
- AF_UNIX — 같은 호스트 내 프로세스 간 통신 전용 소켓 패밀리(네트워크 스택 미경유)
- SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET — 바이트 스트림, 데이터그램, 순서 보장 메시지 세 가지 타입 지원
- 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는 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_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 카운터 감소
*/
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 네임스페이스로 격리 |
가비지 컬렉션 (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가 있으면 dmesg에 GC: 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로 전달
*/
/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과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.