이 코드는 어떤 문제를 푸나요?
시그널은 번호 하나만 전달하는 일이 아닙니다. PID·UID를 담은 정보라면 필요할 때 받는 태스크의 namespace에 맞춰 신원을 변환합니다. 이 함수는 시그널 정보를 정리한 뒤 실제 큐 처리 함수로 넘깁니다. 여기서 사용자 핸들러를 바로 호출하는 것은 아닙니다.
읽을 범위: v6.18.37 · kernel/signal.c · send_signal_locked 1183–1217행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.
먼저 알아둘 개념
namespace
PID와 UID 같은 번호가 어느 범위에서 해석되는지 정합니다.
siginfo
시그널 원인과 발신 정보 등을 담는 기록입니다. 특별한 포인터 값으로 정보의 종류를 나타내기도 합니다.
처음 읽을 때
코드를 읽을 때 SEND_SIG_NOINFO·SEND_SIG_PRIV·PID/UID를 담은 siginfo·그 밖의 정보를 나눕니다. 그다음 어느 상태가 바뀌는지 각 줄에서 확인하세요.
더 깊이 살펴볼 때
이 경로의 호출 문맥, 잠금·인터럽트 상태, 오류 시 남는 자원을 함께 추적해 보세요. 호출된 함수가 수행하는 작업과 현재 함수가 직접 보장하는 범위를 구분하는 것이 중요합니다.
그림으로 보는 변화
이 도식은 Linux v6.18.37 send_signal_locked()의 if / else if 순서입니다. force는 무조건적인 전달 보장이 아니라 후속 무시 판단의 입력입니다.
info의 종류를 순서대로 검사SEND_SIG_NOINFO대상 PID namespace에서 발신 PID가 안 보이면 force 설정. si_uid/si_pid 필드 변환 없음.SEND_SIG_PRIV커널 요청으로 force = true. 신원 필드 변환 없음.has_si_pid_and_uid(info)user namespace가 다를 때만 si_uid 변환. 발신 PID가 안 보이면 si_pid = 0과 force = true; SI_KERNEL도 force에 반영.siginfo이 함수에서 UID/PID 변환 없이 force = false로 합류.__send_signal_locked(sig, info, t, type, force) 호출. 무시되거나 이미 pending인 신호라면 새 큐 항목을 만들지 않을 수 있습니다.| 첫 일치 경로 | 이 함수의 UID/PID 변경 | force 결과 |
|---|---|---|
SEND_SIG_NOINFO | 없음 | 발신 PID가 대상 PID namespace에서 보이지 않을 때 참 |
SEND_SIG_PRIV | 없음 | 참 |
PID·UID를 담은 siginfo | 다른 user namespace일 때 UID 변환; 불가시 발신 PID는 0 | SI_KERNEL 또는 불가시 발신 PID일 때 참 |
그 밖의 siginfo | 없음 | 거짓 |

1. 발신 정보 판별
정보 없음·커널 요청·PID/UID를 담은 정보·그 밖의 정보를 서로 배타적인 네 경로로 나눕니다.
분기는 정보 형식에 따른 선택입니다.
2. PID·UID 포함 정보만 조건부 변환
PID/UID를 담은 siginfo 경로에서만 user namespace 차이와 발신 PID 가시성에 따라 필드를 바꿉니다.
번호 변환은 대상 task_struct 교체가 아닙니다.
3. pending 처리 함수 호출
모든 경로가 __send_signal_locked()에 합류하지만 무시되거나 이미 pending이면 새 큐 항목은 생기지 않을 수 있습니다.
전달 요청과 실제 핸들러 실행 시점은 다릅니다.
send_signal_locked를 한 줄씩 읽기
줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.
int send_signal_locked(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type)
{
/* Should SIGKILL or SIGSTOP be received by a pid namespace init? */
bool force = false;
if (info == SEND_SIG_NOINFO) {
/* Force if sent from an ancestor pid namespace */
force = !task_pid_nr_ns(current, task_active_pid_ns(t));
} else if (info == SEND_SIG_PRIV) {
/* Don't ignore kernel generated signals */
force = true;
} else if (has_si_pid_and_uid(info)) {
/* SIGKILL and SIGSTOP is special or has ids */
struct user_namespace *t_user_ns;
rcu_read_lock();
t_user_ns = task_cred_xxx(t, user_ns);
if (current_user_ns() != t_user_ns) {
kuid_t uid = make_kuid(current_user_ns(), info->si_uid);
info->si_uid = from_kuid_munged(t_user_ns, uid);
}
rcu_read_unlock();
/* A kernel generated signal? */
force = (info->si_code == SI_KERNEL);
/* From an ancestor pid namespace? */
if (!task_pid_nr_ns(current, task_active_pid_ns(t))) {
info->si_pid = 0;
force = true;
}
}
return __send_signal_locked(sig, info, t, type, force);
}int send_signal_locked(int sig, struct kernel_siginfo *info,시그널 번호 sig와 정보 info를 받아 대상 t의 namespace에서 발신 정보가 올바르게 해석되도록 정리한 뒤 pending 처리 경로에 넘깁니다. 이름의 locked는 호출자가 시그널 상태를 보호하는 잠금 규칙을 지켜야 한다는 의미입니다.
struct task_struct *t, enum pid_type type)t는 신호를 받을 태스크이고 type은 개별 태스크인지 스레드 그룹 대상인지 등의 전달 범위를 구분합니다. 이 두 값도 지역변수가 아니라 함수 인자입니다. 포인터가 보관하는 것은 객체의 주소입니다. 이 선언만으로 대상 구조체나 문자열의 내용이 복사되지는 않습니다.
bool force = false;시그널을 강제로 처리해야 하는 특별한 조건이 아직 없다고 초기화합니다.
if (info == SEND_SIG_NOINFO) {일반 siginfo 객체 대신 정보 없음이라는 특별한 입력인지를 구분합니다.
force = !task_pid_nr_ns(current, task_active_pid_ns(t));발신 태스크가 대상 PID namespace에서 보이지 않는지 검사하여 강제 처리 조건을 정합니다.
} else if (info == SEND_SIG_PRIV) {커널이 지정하는 특별한 시그널 정보 유형으로 분기합니다.
force = true;이 경로의 커널 요청 또는 namespace 조건 때문에 일반 처리와 다른 force 표시를 설정합니다.
} else if (has_si_pid_and_uid(info)) {PID와 UID를 실제로 담는 siginfo 형식일 때만 신원 변환을 수행합니다.
struct user_namespace *t_user_ns;수신 태스크가 속한 user namespace를 담습니다. 발신 UID 숫자를 수신자에게 보이는 번호로 바꾸는 데 사용합니다. 포인터가 보관하는 것은 객체의 주소입니다. 이 선언만으로 대상 구조체나 문자열의 내용이 복사되지는 않습니다.
rcu_read_lock();참조하는 RCU 보호 객체의 수명이 읽기 도중 끝나지 않도록 읽기 구간을 시작합니다.
t_user_ns = task_cred_xxx(t, user_ns);대상 태스크의 자격 정보에서 user namespace를 읽습니다. 주변 RCU 구간과 함께 봐야 합니다.
if (current_user_ns() != t_user_ns) {발신 쪽과 수신 쪽 UID 번호 체계가 다른 경우에만 매핑을 변환합니다.
kuid_t uid = make_kuid(current_user_ns(), info->si_uid);현재 user namespace의 UID 숫자를 커널 내부 UID 표현으로 바꿉니다.
info->si_uid = from_kuid_munged(t_user_ns, uid);내부 UID를 받는 쪽 user namespace에서 보이는 번호로 바꿉니다. 매핑이 없을 때의 대체 표현도 처리합니다.
rcu_read_unlock();RCU 보호 읽기 구간을 끝냅니다. 잠든 독자를 기다리는 일반 mutex unlock과는 다릅니다.
force = (info->si_code == SI_KERNEL);명시적인 커널 기원 시그널인지 원인 코드를 검사합니다.
if (!task_pid_nr_ns(current, task_active_pid_ns(t))) {발신자가 대상의 PID namespace에서 표현되지 않는 경우를 처리합니다.
info->si_pid = 0;대상 namespace에서 나타낼 수 없는 발신 PID를 0으로 표기합니다.
force = true;이 경로의 커널 요청 또는 namespace 조건 때문에 일반 처리와 다른 force 표시를 설정합니다.
return __send_signal_locked(sig, info, t, type, force);정리된 시그널 정보와 force 여부를 실제 pending·대상 선택 경로로 넘기고 그 결과를 돌려줍니다.
함께 생각해 볼 질문
시그널 전송 즉시 대상 함수가 실행되나요?
아닙니다. 후속 처리에서 무시되거나 이미 pending이면 새 큐 항목 없이 끝날 수도 있습니다. 전달 대상 신호는 적절한 실행 문맥의 복귀 경로에서 처리됩니다.
UID 숫자를 그대로 복사하면 안 되나요?
서로 다른 user namespace에서는 같은 숫자가 다른 사용자를 뜻할 수 있습니다.
왜 RCU 읽기 구간이 있나요?
PID/UID를 담은 정보 경로에서 대상 자격 정보 포인터를 안전하게 읽기 위한 수명 보호가 필요하기 때문입니다.
출처와 읽은 범위
Linux stable v6.18.37 · kernel/signal.c
해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고
