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로 소켓 상태를 모니터링합니다.
Unix Socket이란?
Unix Domain Socket(이하 UDS)은 같은 리눅스 시스템에서 실행 중인 프로세스끼리 데이터를 주고받는 IPC(Inter-Process Communication) 메커니즘입니다. 네트워크 소켓(Socket)과 완전히 동일한 API(socket(), bind(), connect(), write(), read())를 사용하지만, 실제 네트워크 통신은 전혀 발생하지 않습니다. IP 주소·포트·라우팅이 불필요하며, 파일 경로(path)나 추상 이름(abstract name)을 "주소"로 사용합니다.
커널은 두 프로세스의 버퍼(Buffer)를 직접 연결하여 데이터를 복사합니다. 루프백(loopback, 127.0.0.1) TCP보다 약 2~5배 낮은 지연(latency)을 제공하며, 파일 디스크립터 전달(SCM_RIGHTS)과 발신자 신원 검증(SCM_CREDENTIALS)이라는 TCP에는 없는 강력한 기능도 갖추고 있습니다.
write(fd, data, len)으로 보낸 데이터가, 상대 프로세스의 read(fd, buf, len)에 곧바로 도착합니다. 데이터는 네트워크 카드(NIC)를 전혀 거치지 않고, 커널 메모리 내에서만 이동합니다.
언제 Unix Socket을 사용합니까?
- 같은 호스트 안에서만 통신할 때 — 다른 서버와 통신이 필요하면 TCP를 사용합니다.
- 파일 디스크립터 전달이 필요할 때 — SCM_RIGHTS(ancillary data)를 통해 열린 파일·소켓을 다른 프로세스로 건네줄 수 있습니다(TCP는 불가).
- 발신자 신원 검증이 필요할 때 — SO_PEERCRED 또는 SCM_CREDENTIALS로 상대 프로세스의 PID·UID·GID를 커널 수준에서 신뢰성 있게 확인합니다.
- 낮은 지연이 중요할 때 — loopback TCP 대비 더 낮은 지연을 제공합니다.
IPC 메커니즘 비교
| 메커니즘 | 방향 | 연결 대상 | 메시지 경계 | fd 전달 | 신원 검증 | 성능 |
|---|---|---|---|---|---|---|
| 익명 파이프 (pipe) | 단방향 | 부모↔자식만 | 바이트 스트림 | × | × | 빠름 |
| 명명 파이프 (FIFO) | 단방향 | 임의 프로세스 | 바이트 스트림 | × | × | 빠름 |
| UDS SOCK_STREAM | 양방향 | 임의 프로세스 | 바이트 스트림 | ○ | ○ | 매우 빠름 |
| UDS SOCK_DGRAM | 양방향 | 임의 프로세스 | 메시지 보존 | ○ | ○ | 매우 빠름 |
| 공유 메모리 (shmem) | 양방향 | 임의 프로세스 | 없음 (직접 접근) | × | × | 최고 속도 |
| TCP loopback (127.0.0.1) | 양방향 | 원격 포함 | 바이트 스트림 | × | × | 보통 |
SOCK_STREAM 서버–클라이언트 연결 흐름
기초 사용 예제
Unix Socket을 처음 사용한다면 다음 최소 예제로 시작하세요. SOCK_STREAM 기준으로 서버와 클라이언트가 메시지를 교환하는 완전한 C 코드입니다.
서버 코드 (server.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/demo.sock"
int main(void)
{
int sfd, cfd;
struct sockaddr_un addr;
char buf[256];
ssize_t n;
/* ① 소켓 생성 — AF_UNIX + SOCK_STREAM (TCP처럼 연결 지향) */
sfd = socket(AF_UNIX, SOCK_STREAM, 0);
/* ② 주소 구조체 초기화 */
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
/* 재시작 시 이전 소켓 파일 제거 */
unlink(SOCK_PATH);
/* ③ 소켓 파일을 파일시스템에 생성하고 바인딩 */
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
/* ④ 연결 요청 큐 활성화 (backlog = 최대 대기 연결 수) */
listen(sfd, 5);
printf("서버 대기 중: %s\n", SOCK_PATH);
/* ⑤ 클라이언트 연결 수락 — 연결이 올 때까지 블로킹 */
cfd = accept(sfd, NULL, NULL);
/* ⑥ 데이터 수신 후 응답 */
n = read(cfd, buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("수신: %s\n", buf);
write(cfd, "OK", 2);
/* ⑦ 정리 — 소켓 닫기 + 소켓 파일 삭제 */
close(cfd);
close(sfd);
unlink(SOCK_PATH);
return 0;
}
클라이언트 코드 (client.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_PATH "/tmp/demo.sock"
int main(void)
{
int fd;
struct sockaddr_un addr;
char buf[16];
ssize_t n;
/* ① 소켓 생성 */
fd = socket(AF_UNIX, SOCK_STREAM, 0);
/* ② 서버 주소 설정 */
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);
/* ③ 서버에 연결 — 서버가 accept()로 대기 중이어야 성공 */
connect(fd, (struct sockaddr *)&addr, sizeof(addr));
/* ④ 데이터 전송 */
write(fd, "Hello, Unix Socket!", 19);
/* ⑤ 서버 응답 수신 */
n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("응답: %s\n", buf); /* → "응답: OK" */
close(fd);
return 0;
}
빌드 및 실행
gcc -o server server.c
gcc -o client client.c
./server & # 서버를 백그라운드에서 실행
./client # 클라이언트 실행
# 출력:
# 서버 대기 중: /tmp/demo.sock
# 수신: Hello, Unix Socket!
# 응답: OK
각 시스템 콜의 역할
| 시스템 콜 | 서버 역할 | 클라이언트 역할 |
|---|---|---|
socket(AF_UNIX, SOCK_STREAM, 0) |
소켓 파일 디스크립터 생성 | 소켓 파일 디스크립터 생성 |
bind(fd, &addr, len) |
소켓 파일(/tmp/demo.sock)을 파일시스템에 생성하고 소켓과 연결 |
보통 불필요 (커널이 임시 이름 자동 할당) |
listen(fd, backlog) |
연결 요청 큐 활성화 — backlog는 큐에 쌓일 수 있는 최대 미처리 연결 수 | 해당 없음 |
accept(fd, NULL, NULL) |
큐에서 연결 하나를 꺼내 새 conn_fd 반환 (연결이 올 때까지 블로킹) | 해당 없음 |
connect(fd, &addr, len) |
해당 없음 | 서버 소켓에 연결 요청 전송 (서버가 listen 상태여야 성공) |
write() / send() |
데이터 전송 — 커널이 상대 프로세스의 수신 버퍼에 복사합니다 | |
read() / recv() |
데이터 수신 — 버퍼에 데이터가 없으면 블로킹됩니다 | |
close(fd) |
소켓 닫기, unlink()로 소켓 파일도 별도 삭제 필요 |
소켓 닫기 (연결 종료 신호 자동 전송) |
/tmp/demo.sock 파일은 파일시스템에 남습니다. 서버 시작 시 unlink(SOCK_PATH)로 기존 파일을 제거하고, 종료 시에도 삭제하는 습관이 중요합니다. 이 문제를 피하려면 추상 네임스페이스(abstract namespace)를 사용하면 됩니다 — 추상 네임스페이스 섹션을 참고하세요.
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 |
소켓 생성 → 바인딩 → 연결의 커널 내부 경로
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인 경우)
bind(fd, &addr, sizeof(addr))__sys_bind()→sock->ops->bind()==unix_bind()- 파일시스템 바인딩:
kern_path_create(): 경로에 소켓 파일 생성init_special_inode(inode, S_IFSOCK | mode, 0)- 해시 테이블(Hash Table)에 소켓 등록
- 추상 네임스페이스 바인딩:
unix_bind_abstract(): 해시 테이블에만 등록- 파일시스템 inode 생성 없음
listen(fd, backlog)— SOCK_STREAM/SEQPACKET만 해당합니다__sys_listen()→sock->ops->listen()==unix_listen()sk->sk_state=TCP_LISTENsk->sk_max_ack_backlog=backlog
accept(fd, ...)__sys_accept4()→sock->ops->accept()==unix_accept()skb_dequeue(&sk->sk_receive_queue)- 큐에서 연결 요청을 꺼내 새 소켓 쌍을 생성합니다
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()
sendmsg(fd, &msg, flags)→sock_sendmsg()→sock->ops->sendmsg()→unix_stream_sendmsg()unix_stream_sendmsg()주요 로직:- 피어 소켓 참조 획득:
other = unix_peer(sk) - 메모리 할당:
alloc_skb_fclone()또는sock_alloc_send_pskb() - 데이터 복사:
skb_copy_datagram_from_iter()— 사용자 공간에서 커널 skb 데이터 영역으로 복사합니다 - 피어 수신 큐에 삽입:
skb_queue_tail(&other->sk_receive_queue, skb) - 피어 깨우기(Wakeup):
other->sk_data_ready(other)
- 피어 소켓 참조 획득:
핵심: sk_buff를 생성하지만, 네트워크 헤더(IP/TCP/UDP)를 추가하지 않습니다. 순수 데이터 컨테이너(Container)로만 사용합니다.
SOCK_DGRAM 전송 경로: unix_dgram_sendmsg()
SOCK_STREAM과 유사하지만 다음과 같은 차이점이 있습니다.
- 연결 없는 상태에서도
sendto()로 목적지를 지정할 수 있습니다 - 메시지 경계가 보존됩니다 (skb 하나 = 메시지 하나)
- 수신 큐가 가득 차면
-EAGAIN(비블로킹) 또는 대기 (블로킹)합니다 - 목적지 소켓이 없으면
-ECONNREFUSED를 반환합니다
수신 경로: unix_stream_recvmsg()
recvmsg(fd, &msg, flags)→sock_recvmsg()→sock->ops->recvmsg()→unix_stream_recvmsg()- 주요 로직:
sk_receive_queue에서 skb를 획득합니다skb_copy_datagram_msg(): 커널에서 사용자 공간으로 복사합니다- STREAM: 부분 읽기가 가능합니다 (skb에서 일부만 읽고 다음에 나머지를 읽습니다)
- 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 전용 기능으로, 파일시스템에 소켓 파일을 생성하지 않고 커널 해시 테이블에만 소켓 이름을 등록합니다. 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의 참조 카운트(Reference Count): 사용자 fd(1) + B의 inflight(1) = 2
- B의 참조 카운트: 사용자 fd(1) + A의 inflight(1) = 2
- 사용자가 A, B 모두
close()→ 참조 카운트가 1로 남습니다 - 일반 해제가 불가능하므로 GC가 필요합니다
net/unix/garbage.c — unix_gc() 알고리즘
- 후보 수집(Candidate Collection):
gc_inflight_list에 있는 모든 소켓을 수집합니다. 조건은 inflight 카운터 > 0 (in-flight fd가 있는 소켓)입니다. - 내부 참조 제거(Internal Reference Decrement): 각 후보 소켓의 수신 큐를 스캔하고, 큐에 있는 file이 다른 후보 소켓을 참조하면 해당 소켓의 "외부 참조 카운트"를 감소시킵니다.
- 도달 가능성 검사(Reachability Check):
- 외부 참조 카운트 > 0인 소켓: 도달 가능 → 보존합니다
- 외부 참조 카운트 == 0인 소켓: 도달 불가 → 회수 대상입니다
- 도달 가능 소켓에서 참조하는 소켓도 재귀적으로 보존합니다
- 회수(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 패치(Patch)):
* - 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유닛으로 소켓을 미리 생성합니다.socket(AF_UNIX, SOCK_STREAM, 0)→bind()→listen()- 첫 연결이 오면
fork()/exec()으로 서비스를 시작합니다 - 서비스 프로세스는
sd_listen_fds()로 상속받은 fd를 획득합니다
이 방식의 장점은 다음과 같습니다.
- 서비스 미실행 시에도 클라이언트 연결 대기가 가능합니다
- 서비스 재시작(Reboot) 중에도 연결 요청이 보존됩니다 (listen backlog)
- 여러 서비스를 병렬로 시작할 수 있습니다 (의존성 소켓만 미리 생성)
- 실전 패턴 2: 컨테이너 런타임 fd 전달
-
containerd가 shim 프로세스에 컨테이너 stdio fd를 전달합니다.
- containerd가 PTY 또는 pipe fd를 생성합니다
- SCM_RIGHTS로 shim에게 fd를 전달합니다
- shim이 fd를 컨테이너 프로세스에
dup2()합니다
fork()/exec()체인을 넘어 fd를 전달하는 핵심 메커니즘입니다. - 실전 패턴 3: Wayland DMA-BUF 공유
-
Wayland 클라이언트가 GPU 버퍼를 컴포지터와 공유합니다.
- 클라이언트: DRM ioctl로 DMA-BUF fd를 획득합니다
- 클라이언트 → 컴포지터: SCM_RIGHTS로 DMA-BUF fd를 전달합니다
- 컴포지터: 같은 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()
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와 유사한 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 (TCP/UDP 지원; AF_UNIX 메인라인 미지원)
-
MSG_ZEROCOPY(SO_ZEROCOPY)는 TCP/UDP/VSOCK 소켓에서 지원되며, 대용량 전송 시 사용자 공간→커널 복사를 생략하여 성능이 향상됩니다. AF_UNIX는 메인라인 커널에서 미지원(공식 문서: docs.kernel.org/networking/msg_zerocopy.html)이며, 일부 배포판 전용 패치(Patch)에서만 제공됩니다.
활성화 방법 (TCP/UDP):
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)); sendmsg(fd, &msg, MSG_ZEROCOPY);주의사항:
- 소량 데이터에서는 오히려 오버헤드가 발생합니다 (페이지(Page) 핀닝 비용)
- 일반적으로 32KB 이상 전송에서 이점이 있습니다
- 수신측은 변경 없이 동작합니다
- io_uring + UDS (커널 5.6+)
-
io_uring은 UDS에 대한 비동기 I/O를 지원합니다.
IORING_OP_SENDMSG/IORING_OP_RECVMSG- 시스콜 오버헤드를 제거합니다 (submission queue로 배치 처리)
- 폴링(Polling) 모드에서 추가 성능이 향상됩니다
적합한 시나리오:
- 많은 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(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_SHUTDOWNpeer->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()
- 해시 테이블에서 소켓 제거:
__unix_remove_socket()을 호출합니다 - 바인딩된 경로 해제:
path_put(&u->path)(파일시스템 소켓)unix_table_double_lock()→ 해시(Hash) 제거 (추상 네임스페이스)
- 피어 소켓 분리:
unix_peer(sk)= NULL- 피어에게
EPOLLHUP이벤트를 전달합니다 - SOCK_DGRAM: 피어의 peer도 NULL로 설정합니다
- 수신 큐 정리:
skb_queue_purge(&sk->sk_receive_queue)- 각 skb의 SCM_RIGHTS fd에 대해
unix_notinflight()+fput()을 호출합니다
- 참조 카운트 감소:
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() 비동기 처리 | 5.6 |
IORING_OP_RECVMSG | recvmsg() 비동기 처리 | 5.6 |
IORING_OP_CONNECT | connect() 비동기 | 5.5 |
IORING_OP_ACCEPT | accept() 비동기 | 5.5 |
IORING_OP_SEND | send() 비동기 | 5.6 |
IORING_OP_RECV | recv() 비동기 | 5.6 |
최적 활용:
- SQPOLL 모드:
io_uring_params.flags |= IORING_SETUP_SQPOLL— 커널 쓰레드가 SQ를 폴링하여 시스콜 0회를 달성합니다. 지연를 최소화하지만 CPU 사용이 증가하는 트레이드오프가 있습니다. - 멀티샷 recv:
IORING_RECV_MULTISHOT(6.0+) — 하나의 SQE로 여러 메시지를 수신하며, CQE에 메시지별 결과를 반환합니다. - 고정 버퍼:
IORING_REGISTER_BUFFERS— 사전 등록된 버퍼로 매핑 오버헤드를 제거합니다.
벤치마크 (대략적 수치):
| 방식 | 처리량 |
|---|---|
| epoll + sendmsg/recvmsg | ~200K msg/s |
| io_uring (기본) | ~350K msg/s |
| io_uring (SQPOLL) | ~500K msg/s |
버퍼 크기 튜닝 권장
- 대용량 전송 (로그 수집, DB 덤프(Dump) 등)
-
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 권한을 획득할 수 있습니다.완화 방법:
- 소켓 프록시(docker-socket-proxy): 읽기 전용(Read-Only) API만 허용하는 프록시 소켓을 노출합니다
- rootless Docker: 소켓이 사용자 네임스페이스 내에 생성되어 호스트 root에 접근할 수 없습니다
- 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> 환경변수 확인
컨테이너 내 동작:
- 컨테이너의 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 등 소켓 마운트가 필요 없는 도구를 사용하거나, 읽기 전용 소켓 프록시를 사이에 배치하세요.
디버깅
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 |
서버 재시작, 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 |
최신 커널 동향 (2025~2026)
AF_UNIX는 성숙한 인터페이스지만 커널 6.x에서도 꾸준히 중요한 개선이 이어집니다. 특히 GC 재작성, pidfd 계열 확장, io_uring 통합이 최근 변화의 중심입니다.
AF_UNIX GC 전면 재작성 — 커널 6.10 (2024-07)
Kuniyuki Iwashima가 주도한 AF_UNIX GC 재작성이 커널 6.10에 정식 병합되었습니다. 기존의 마크-앤-스윕(mark-and-sweep) 기반 구현은 unix_gc_lock을 전역 잠금(Lock)으로 사용해 inflight fd가 많은 환경(Wayland 컴포지터, 컨테이너 런타임, systemd)에서 지연 스파이크를 유발했습니다. 새 구현은 Tarjan의 SCC(Strongly Connected Components) 알고리즘을 기반으로 순환 참조만 정확하게 식별하여 회수하며, 복잡도는 O(V + E)로 예측 가능해졌습니다.
- 전역 잠금(
unix_gc_lock) 대신 vertex 단위 세밀한 잠금 구조 - SCM_RIGHTS가 비정상적으로 많은 프로세스에서도 GC 지연 안정화
- 과거에 보고되었던 io_uring + SCM_RIGHTS 관련 use-after-free(CVE-2022-2602, 취약점(Vulnerability)) 재발 방지 설계 포함
- 선형
gc_inflight_list스캔 제거 — 대신 vertex 그래프만 탐색
모니터링 팁: 새 GC에서는 trace_unix_gc_* tracepoint가 추가되어 bpftrace -e 'tracepoint:af_unix:unix_gc_scc_found { @ = hist(args->scc_size); }'으로 SCC 크기 분포를 직접 관찰할 수 있습니다. 과거 kprobe:unix_gc 기반 추적은 관련 심볼 이름이 바뀌어 일부 스크립트 수정이 필요합니다.
pidfd 기반 peer 인증의 기본화
SO_PEERCRED와 SCM_CREDENTIALS가 갖던 PID 재사용 취약점은 커널 6.5에 병합된 SO_PEERPIDFD와 SCM_PIDFD로 해결되었습니다. 2025년에 들어 주요 배포판의 서비스 스택이 이 메커니즘을 기본 채택하기 시작했습니다.
- systemd 256 이후 — activation 소켓에서 피어의 pidfd를 획득하고
$LISTEN_PIDFDS환경 변수로 서비스 유닛에 전달하는 경로가 도입되었습니다. - Flatpak/Portal — 기존
SCM_CREDENTIALS기반 sandbox peer 검증을 SCM_PIDFD로 이관해, PID 재사용 레이스에 의한 샌드박스(Sandbox) 우회를 차단합니다. - pidfs 고유 ino (v6.9+) —
fstat(peer_pidfd)->st_ino가 프로세스 수명 동안 고유하므로, 연결 초기에 저장해 두면 이후 동일 peer 여부를 파일시스템 inode처럼 비교할 수 있습니다. - cgroup user.* xattr (v6.8+) — 서비스 매니저가
setxattr("user.role", ...)로 cgroup에 라벨을 붙이고, peer의 pidfd에서 cgroup 경로를 역추적(Backtrace)해 역할 기반 권한 검증을 수행합니다.
io_uring × AF_UNIX 통합 심화
io_uring은 AF_UNIX의 주 송수신 경로로 빠르게 자리잡고 있습니다. 커널 6.10 이후의 변화는 다음과 같습니다.
- Send zerocopy 최소 효율 구간 완화 (v6.10) — 기존 10 KB 이상에서만 이득이 있던
MSG_ZEROCOPY/IORING_OP_SEND_ZC가 3 KB 부근으로 크로스오버가 내려가, 일반적인 RPC 크기에서도 이득이 발생합니다. - IORING_RECVSEND_BUNDLE (v6.10) — 단일 SQE로 다수의 등록 버퍼를 연속 수신/송신할 수 있어, AF_UNIX 멀티플렉스 서버의 syscall overhead를 추가로 감소시킵니다.
- IORING_OP_EPOLL_WAIT (v6.12) — 레거시 epoll 기반 서버를 io_uring으로 하이브리드 마이그레이션하기 쉬워졌습니다.
- io_uring + SCM_RIGHTS 안정화 — 6.10 GC 재작성과 함께 io_uring context가 AF_UNIX inflight에 포함될 때 발생하던 참조 주기 문제가 근본적으로 제거되었습니다.
기타 최신 보강
- SOCK_SEQPACKET MSG_TRUNC 통계 보강 (v6.9) — 송신측이 경계 초과로 잘린 수신을 감지할 수 있도록
SIOCINQ에 드롭 카운터가 노출됩니다. - Abstract namespace per-netns 격리 강화 (v6.11) — 네트워크 네임스페이스 간 추상 소켓 이름 공간이 완전히 분리되어, 컨테이너에서
\0docker/...형태의 이름 충돌이 없어졌습니다. - SO_RCVMARK의 AF_UNIX 확장 (v6.12) — 외부 정책 엔진(Policy Engine)이 peer를 구분하기 위해 제공하던 skb mark가 UDS 수신에서도 사용 가능합니다.
- Documentation/networking/af_unix.rst 신설 (v6.11) — 오랫동안 문서가 분산되어 있던 AF_UNIX 설계가 공식 커널 문서로 통합되었습니다.
권장 조합(2026년 기준): 새 서비스는 AF_UNIX + SOCK_SEQPACKET + SCM_PIDFD로 peer 신원을 검증하고, io_uring + 등록 버퍼(IORING_REGISTER_BUFFERS)로 제로카피 송수신 루프를 구성하는 것이 성능/보안의 균형이 가장 좋습니다. 레거시 SCM_CREDENTIALS/SO_PEERCRED는 호환성 용도로만 유지하세요.
AF_UNIX coredump 소켓 — 커널 6.16 (2025-07)
커널 6.16에서 코어 덤프(Core Dump) 핸들러가 Unix 도메인 소켓 경로를 사용할 수 있도록 확장되었습니다. /proc/sys/kernel/core_pattern에 |@linuxafsk/… 형식의 추상 소켓 주소를 지정하면, 커널이 코어 덤프 데이터를 파이프 대신 AF_UNIX 스트림 소켓으로 전송합니다. 기존 |/usr/lib/systemd/systemd-coredump 파이프 방식과 달리, 수신 데몬이 SO_PEERPIDFD와 SCM_PIDFD로 덤프 전송 프로세스의 pidfd를 검증할 수 있어 coredump 데이터의 신뢰성이 강화됩니다. systemd-coredump 255 이상이 이 인터페이스를 우선적으로 사용합니다.
# v6.16+ coredump를 추상 소켓으로 전달
echo "|@linuxafsk/coredump %p %u %g %s %t %e" \
> /proc/sys/kernel/core_pattern
보안 이점: 파이프 방식은 coredump 핸들러가 PID를 신뢰해야 했지만, 소켓 방식에서는 getsockopt(fd, SOL_SOCKET, SO_PEERPIDFD, &pidfd, &len)로 커널이 보증하는 pidfd를 얻을 수 있어 race-free 검증이 가능합니다.
SCM_PIDFD에서 종료된 피어 pidfd 수신 — 커널 6.17 (2025-09)
커널 6.17 이전에는 SCM_PIDFD 제어 메시지를 수신할 때 송신 프로세스가 이미 종료(reap)되어 있으면 ESRCH 에러가 반환되었습니다. 커널 6.17부터 종료된 프로세스의 pidfd도 유효하게 전달되어, 수신측이 waitid(P_PIDFD, pidfd, &info, WEXITED)로 exit status를 정상적으로 수집할 수 있습니다. 프로세스 수퍼바이저(Supervisor)나 컨테이너 런타임에서 자식 프로세스 종료와 메시지 수신 사이의 race 조건을 처리하는 별도 코드를 제거할 수 있습니다.
/* v6.17+ SCM_PIDFD 수신 후 종료된 peer의 exit status 수집 */
siginfo_t info;
int ret = waitid(P_PIDFD, (id_t)peer_pidfd, &info,
WEXITED | WNOHANG);
if (ret == 0 && info.si_code == CLD_EXITED)
printf("peer exited with status %d\n", info.si_status);
참고자료
- 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 도메인을 포함한 소켓 생성 시스템 콜(System Call) 문서입니다
- 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 문서입니다
- LWN: The rewrite of the AF_UNIX garbage collector — 커널 6.10 SCC 기반 GC 재작성을 다룬 상세 분석입니다
- LWN: SCM_PIDFD and SO_PEERPIDFD — 커널 6.5 pidfd 기반 peer 인증 확장을 설명합니다
- swick's blog: SO_PEERPIDFD gets more useful — cgroup user.* xattr과 결합한 pidfd 기반 peer 신원 검증 흐름입니다
- 커널 공식 문서: AF_UNIX — v6.11부터 신설된 AF_UNIX 공식 문서입니다
- Oracle: Unix Garbage Collection and io_uring — io_uring과 SCM_RIGHTS 상호작용, CVE-2022-2602 배경을 설명합니다
관련 문서
Unix Domain Socket과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.