프로세스 이름 표시, 부모가 죽었을 때 자식 정리, 권한 상승 차단, 코어 덤프 생성 여부처럼 프로세스 자체의 커널 레벨 동작을 세밀하게 제어하고 싶을 때가 있다. 리눅스는 이런 잡다한 제어를 위해 옵션마다 별도 시스템 콜을 만드는 대신 prctl() 하나에 PR_* 옵션 상수를 넘기는 방식을 쓴다. 이 글에서는 실무에서 자주 쓰는 옵션 네 가지를 실제 C 코드로 테스트한 결과와 함께 정리한다.
prctl 기본 형태
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5);
옵션에 따라 arg2~arg5의 의미가 완전히 달라진다. 자주 쓰는 옵션은 다음과 같다.
| 옵션 | 대상 | 효과 |
|---|---|---|
| PR_SET_NAME / PR_GET_NAME | 현재 스레드 | 16바이트(널 포함) 이름 설정/조회, ps·top에 표시됨 |
| PR_SET_PDEATHSIG | 현재 스레드 | 부모 프로세스 종료 시 받을 시그널 지정 |
| PR_SET_NO_NEW_PRIVS | 현재 스레드(비가역) | execve로 setuid/setgid·capability를 통한 권한 상승 차단 |
| PR_SET_DUMPABLE | 현재 프로세스 | 코어 덤프 생성 여부, /proc/pid/mem 접근 권한에 영향 |
스레드 이름 지정 (PR_SET_NAME)
스레드가 여러 개인 프로그램을 ps -T나 top으로 볼 때 전부 실행 파일 이름으로만 보이면 어떤 스레드가 뭘 하는지 구분하기 어렵다. PR_SET_NAME으로 현재 스레드에 이름을 붙일 수 있다.
#include <stdio.h>
#include <sys/prctl.h>
#include <unistd.h>
int main(void) {
char before[16] = {0};
prctl(PR_GET_NAME, before);
printf("before: %s\n", before);
prctl(PR_SET_NAME, "worker-1");
char after[16] = {0};
prctl(PR_GET_NAME, after);
printf("after: %s\n", after);
pause();
return 0;
}$ ./prctl_name &
before: prctl_name
after: worker-1
$ ps -p $! -o pid,comm
PID COMMAND
7192 worker-1
16바이트 제한(널 종료 포함 15자)이라 긴 이름은 잘린다. 이름은 스레드 단위 속성이라, 멀티스레드 프로그램이면 스레드마다 각자 prctl을 호출해야 한다.
부모 종료 시 시그널 받기 (PR_SET_PDEATHSIG)
부모가 자식을 fork만 해두고 wait이나 종료 처리를 안 하면, 부모가 먼저 죽었을 때 자식이 고아 프로세스로 계속 남는다. PR_SET_PDEATHSIG를 걸어두면 부모가 죽는 즉시 커널이 자식에게 지정한 시그널을 보내준다.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/prctl.h>
void handler(int sig) {
printf("child: received signal %d, exiting\n", sig);
_exit(0);
}
int main(void) {
pid_t pid = fork();
if (pid == 0) {
signal(SIGTERM, handler);
prctl(PR_SET_PDEATHSIG, SIGTERM);
printf("child: pid=%d, parent=%d, waiting...\n", getpid(), getppid());
pause();
return 0;
}
printf("parent: pid=%d, child=%d, exiting now\n", getpid(), pid);
return 0; /* wait/kill 없이 바로 종료 */
}$ ./prctl_pdeathsig
parent: pid=7233, child=7234, exiting now
child: pid=7234, parent=7233, waiting...
child: received signal 15, exiting
부모가 자식을 kill하지 않고 그냥 종료했는데도 자식이 SIGTERM(15)을 받고 스스로 정리된 것이 확인된다.
권한 상승 차단 (PR_SET_NO_NEW_PRIVS)
seccomp 필터나 샌드박스를 구현할 때, 이후 execve로 setuid/setgid 바이너리나 파일 capability를 통해 권한이 올라가는 걸 원천 차단하고 싶으면 PR_SET_NO_NEW_PRIVS를 켠다. 커널의 /proc/[pid]/status NoNewPrivs 필드에도 그대로 반영된다.
#include <stdio.h>
#include <unistd.h>
#include <sys/prctl.h>
int main(void) {
printf("before: PR_GET_NO_NEW_PRIVS = %d\n",
prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
printf("after: PR_GET_NO_NEW_PRIVS = %d, pid=%d\n",
prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0), getpid());
pause();
return 0;
}$ ./prctl_nnp &
before: PR_GET_NO_NEW_PRIVS = 0
after: PR_GET_NO_NEW_PRIVS = 1, pid=7304
$ grep NoNewPrivs /proc/7304/status
NoNewPrivs: 1
코어 덤프 생성 여부 제어 (PR_SET_DUMPABLE)
민감한 데이터를 다루는 프로세스가 크래시 났을 때 메모리 내용이 코어 파일로 디스크에 남는 걸 막고 싶다면 PR_SET_DUMPABLE을 0으로 설정한다. SIGSEGV를 직접 유발해서 코어 덤프 생성 여부를 비교했다.
#include <stdio.h>
#include <sys/prctl.h>
int main(int argc, char **argv) {
if (argc > 1 && argv[1][0] == '0') {
prctl(PR_SET_DUMPABLE, 0);
}
printf("dumpable=%d, crashing now\n", prctl(PR_GET_DUMPABLE));
int *p = NULL;
*p = 1; /* SIGSEGV */
return 0;
}$ ulimit -c unlimited
$ ./prctl_dumpable_crash 1 # dumpable=1 (기본값)
dumpable=1, crashing now
Segmentation fault (core dumped)
$ ./prctl_dumpable_crash 0 # PR_SET_DUMPABLE(0) 호출
dumpable=0, crashing now
Segmentation fault
같은 SIGSEGV인데도 dumpable=0일 때는 셸이 “core dumped” 표시를 하지 않는다 — 실제로 코어가 생성되지 않았다는 뜻이다.
주의사항
PR_SET_PDEATHSIG는 시그널을 보낸 스레드 기준으로 설정된다. 그 스레드가 아니라 다른 스레드가 먼저 죽는 경우, 또는 setuid 바이너리를execve한 경우처럼 예외적으로 리셋되는 케이스가 있으니 정확한 조건은 man 페이지를 확인한다.PR_SET_NO_NEW_PRIVS는 한 번 켜면 그 프로세스와 이후 자식들에게 계속 상속되며 되돌릴 수 없다.PR_SET_DUMPABLE은 setuid/setgid 실행 파일을execve하면 커널이 보안을 위해 자동으로 0으로 리셋한다. 직접 0으로 설정하지 않아도 이런 경우 코어 덤프가 안 남을 수 있다.
마무리
prctl은 옵션 하나하나가 사실상 별개의 기능이라 필요한 옵션만 그때그때 man 페이지에서 찾아 쓰게 된다. 그래도 이름 지정·부모 사망 처리·권한 상승 차단·코어 덤프 제어 네 가지는 데몬이나 워커 프로세스를 만들 때 반복적으로 마주치는 조합이라 기본으로 알아두면 좋다.