서비스 · 프로세스 관리 17 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

트래픽이 몰린 날 웹 서버 로그에 Too many open files가 쏟아지고, 누군가 서버에서 fork bomb을 실행해 SSH 접속조차 안 되는 상황. 두 장애 모두 프로세스 자원 한도(resource limit) 와 관련이 있다. 한도가 너무 낮으면 정상 서비스가 멈추고, 너무 높거나 없으면 한 사용자가 서버 전체를 마비시킬 수 있다.

이번 글에서는 ulimit으로 한도를 확인·변경하고, 한도를 넘겼을 때의 오류를 재현한다. 그리고 실무에서 가장 많이 헷갈리는 부분, limits.conf는 로그인 세션에만 적용되고 systemd 서비스에는 적용되지 않는다 는 점을 실측으로 확인한다.


2. 핵심 개념

2-1. 주요 자원 한도

ulimit 옵션limits.conf 항목systemd의미
-nnofileLimitNOFILE=열 수 있는 파일(fd) 수
-unprocLimitNPROC=사용자(UID)당 프로세스·스레드 수
-ccoreLimitCORE=코어 덤프 파일 크기
-sstackLimitSTACK=스택 크기
-tcpuLimitCPU=CPU 시간(초)
-vasLimitAS=가상 메모리 크기
——TasksMax=서비스 cgroup의 최대 태스크 수 (18편)

2-2. soft와 hard

구분의미일반 사용자
soft실제로 적용되는 값hard 이하에서 자유롭게 변경
hardsoft의 상한낮추기만 가능, 올리기는 root만

ulimit -n은 soft, ulimit -Hn은 hard를 본다. 실행 중인 프로세스의 한도는 /proc/PID/limits 또는 prlimit --pid PID로 확인하고, prlimit은 root가 실행 중인 프로세스의 한도를 바꾸는 데도 쓴다.

2-3. 적용 경로

경로적용 주체설정 파일
SSH·콘솔·su 로그인PAM pam_limits.so/etc/security/limits.conf, limits.d/*.conf
systemd 서비스systemdunit 파일의 Limit*= 또는 system.conf의 DefaultLimit*=
셸에서 실행한 명령부모 셸에서 상속ulimit 명령

3. 동작 원리

자원 한도(rlimit)는 어디서 정해지고 어떻게 물려받는가

open() 호출
  → 커널: 현재 열린 fd 수 >= RLIMIT_NOFILE(soft)?
       예 → 실패, errno = EMFILE ("Too many open files")
fork() 호출
  → 커널: 해당 UID 의 프로세스·스레드 수 >= RLIMIT_NPROC(soft)?
       예 → 실패, errno = EAGAIN ("Resource temporarily unavailable")

한도는 fork 시 자식에게 복사 된다. 그래서 로그인 셸의 한도는 그 셸에서 실행한 모든 명령에 적용되고, 서비스 메인 프로세스의 한도는 그 서비스의 모든 자식에게 적용된다.


4. 명령어 실습

# 1) 현재 한도
ulimit -a | grep -E 'open files|max user processes|core file|stack|cpu time'
ulimit -Sn; ulimit -Hn
grep -E 'Max (open files|processes|core)' /proc/$$/limits

# 2) nofile 초과 재현 (서브셸에서 soft 를 20 으로 낮춤)
bash -c 'ulimit -n 20; python3 -c "
fs=[]
try:
    while True: fs.append(open(\"/etc/hostname\"))
except OSError as e: print(len(fs), \"개 열고 실패:\", e)"'

# 3) 일반 사용자는 한 번 낮춘 한도를 다시 올릴 수 없다
bash -c 'ulimit -n 100; ulimit -n 5000'

# 4) nproc 초과 재현
ps -u analyst --no-headers | wc -l
bash -c 'ulimit -u 12; python3 -c "... fork 반복 ..."'

# 5) (root) limits.conf 는 로그인에, 서비스는 별도
echo 'analyst  soft  nofile  4096' > /etc/security/limits.d/90-analyst.conf
ssh analyst@localhost 'ulimit -Sn'              # 새 로그인 세션
prlimit --pid $(pgrep -xo sshd) --nofile         # 서비스 프로세스
systemctl show sshd crond -p Id,LimitNOFILE,LimitNOFILESoft,TasksMax

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 현재 셸의 자원 제한

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — soft 한도를 낮춰서 한도 초과 재현

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — limits.conf 는 PAM 로그인에, LimitNOFILE 은 서비스에

텍스트 원본(실제 출력):

[analyst@rocky9-lab ~]$ ulimit -a | grep -E 'open files|max user processes|core file|stack|cpu time'
core file size              (blocks, -c) unlimited
open files                          (-n) 1024
stack size                  (kbytes, -s) 8192
cpu time                   (seconds, -t) unlimited
max user processes                  (-u) 32058
[analyst@rocky9-lab ~]$ ulimit -Sn; ulimit -Hn
1024
20000
[analyst@rocky9-lab ~]$ grep -E 'Max (open files|processes|core)' /proc/$$/limits
Max core file size        unlimited            unlimited            bytes
Max processes             32058                32058                processes
Max open files            1024                 20000                files
[analyst@rocky9-lab ~]$ bash -c 'ulimit -n 20; python3 -c "
> fs=[]
> try:
>     while True: fs.append(open(\"/etc/hostname\"))
> except OSError as e: print(len(fs), \"개 열고 실패:\", e)"'
17 개 열고 실패: [Errno 24] Too many open files: '/etc/hostname'
[analyst@rocky9-lab ~]$ bash -c 'ulimit -n 100; ulimit -n 5000' ; echo "종료 코드=$?"
bash: line 1: ulimit: open files: cannot modify limit: Operation not permitted
종료 코드=1
[analyst@rocky9-lab ~]$ ps -u analyst --no-headers | wc -l
6
[analyst@rocky9-lab ~]$ bash -c 'ulimit -u 12; python3 -c "import os,time
> n=0
> try:
>     while True:
>         if os.fork()==0: time.sleep(3); os._exit(0)
>         n+=1
> except OSError as e: print(n, \"개 fork 후 실패:\", e)"'
7 개 fork 후 실패: [Errno 11] Resource temporarily unavailable
[analyst@rocky9-lab ~]$ pkill -u analyst -x sleep; true
[root@rocky9-lab ~]# echo 'analyst  soft  nofile  4096' > /etc/security/limits.d/90-analyst.conf; cat /etc/security/limits.d/90-analyst.conf
analyst  soft  nofile  4096
[root@rocky9-lab ~]# ssh -i /root/.ssh/lab_key -o LogLevel=ERROR analyst@localhost 'ulimit -Sn'
4096
[root@rocky9-lab ~]# prlimit --pid $(pgrep -xo sshd) --nofile
RESOURCE DESCRIPTION              SOFT  HARD UNITS
NOFILE   max number of open files 1024 20000 files
[root@rocky9-lab ~]# systemctl show sshd crond -p Id,LimitNOFILE,LimitNOFILESoft,TasksMax
TasksMax=51293
LimitNOFILE=524288
LimitNOFILESoft=1024
Id=sshd.service

TasksMax=51293
LimitNOFILE=524288
LimitNOFILESoft=1024
Id=crond.service
[root@rocky9-lab ~]# rm -f /etc/security/limits.d/90-analyst.conf

6. 결과 해석

관찰의미
open files 1024, hard 20000일반적인 soft 기본값 1024. 필요하면 사용자가 hard까지 올릴 수 있다
/proc/$$/limits의 Max open files 1024 20000ulimit과 같은 값을 커널에서 직접 읽었다
17 개 열고 실패: [Errno 24] Too many open files한도 20에서 이미 열려 있던 fd(0·1·2 등 3개)를 빼고 17개만 더 열 수 있었다. errno 24 = EMFILE
ulimit -n 5000 → cannot modify limit: Operation not permitted100으로 낮추면서 hard도 100으로 내려갔고, 일반 사용자는 hard를 다시 올릴 수 없다 (bash의 ulimit -n은 soft·hard를 함께 바꾼다)
analyst 프로세스 6개 + fork 7개에서 실패nproc은 해당 UID의 전체 프로세스 수 로 계산한다. 이미 있던 6개 + python 자신 + 자식 7개가 한도 12 근처에 도달했다
[Errno 11] Resource temporarily unavailableerrno 11 = EAGAIN. fork bomb이 이 한도에서 멈춘다
limits.d 추가 후 새 SSH 세션 ulimit -Sn → 4096PAM이 로그인 시 limits.conf를 적용했다
sshd 서비스 prlimit → 1024 / 20000같은 서버에서도 서비스 프로세스에는 limits.conf가 적용되지 않았다
systemctl show → LimitNOFILE=524288, LimitNOFILESoft=1024systemd가 서비스에 설정한 값. 서비스 한도는 unit 파일로 관리한다

실습 컨테이너는 Docker가 컨테이너 전체의 hard 한도를 20000으로 제한하고 있어, systemd 설정(hard 524288)보다 작은 값이 실제로 적용되었다. 일반 서버에서는 prlimit 결과가 systemctl show의 값과 일치한다. 설정값과 실제 적용값이 다를 수 있으므로 반드시 /proc/PID/limits로 확인 한다.


7. 보안 관점

주제내용
fork bomb 방어nproc(로그인)과 TasksMax=(서비스·사용자 slice)로 한 사용자가 PID를 고갈시키는 것을 막는다. 공격을 받아도 관리자 SSH 접속 여지가 남는다
코어 덤프core unlimited이면 크래시 시 프로세스 메모리 전체 가 파일로 저장된다. 비밀번호·키가 포함될 수 있어 운영 서버는 core 0 또는 systemd-coredump의 접근 통제를 쓴다
서비스 가용성웹·DB 서비스의 nofile이 낮으면 동시 접속 증가만으로 서비스 거부가 된다. DDoS 대응 점검 항목이다
권한 경계일반 사용자는 hard를 올릴 수 없다. 서비스 계정이 탈취되어도 한도 밖으로 자원을 쓰지 못한다

8. 보안관제 관점

[Detection]  웹 로그 급증: "accept4() failed (24: Too many open files)"
     ↓
[확인]       P=$(pgrep -xo nginx); grep 'open files' /proc/$P/limits
             ls /proc/$P/fd | wc -l                    ← 현재 사용량
             ss -tn state established '( sport = :443 )' | wc -l
     ↓
[판단]       정상 트래픽 증가인가? 특정 IP 대량 연결(Slowloris 등)인가?
     ↓
[Response]   공격이면 출발지 차단·연결 제한 / 정상이면 LimitNOFILE 상향 (drop-in, 28편)
점검 명령목적
grep -r . /etc/security/limits.conf /etc/security/limits.d/로그인 한도 정책
systemctl show SERVICE -p LimitNOFILE,LimitNPROC,TasksMax서비스 설정값
cat /proc/PID/limits실제 적용값
journalctl -g 'fork: retry\|Too many open files'한도 초과 흔적

9. 실무에서 자주 발생하는 실수

실수결과예방
서비스 한도를 limits.conf에 설정적용되지 않음 (실측)unit 파일 LimitNOFILE= (drop-in)
limits.conf 수정 후 기존 세션에서 확인값이 안 바뀜새 로그인 에서 적용된다
ulimit -n을 무심코 낮춤같은 셸에서 되돌릴 수 없음ulimit -Sn으로 soft만 변경
설정값만 보고 적용됐다고 판단상위 한도(컨테이너 등)에 막힐 수 있음/proc/PID/limits 확인
nproc을 프로세스 수로만 생각스레드도 포함된다멀티스레드 서비스 계정은 여유 있게 설정

10. 실습 체크리스트

[ ] ulimit -a, -Sn, -Hn 과 /proc/PID/limits 를 비교했다
[ ] nofile 을 낮춰 EMFILE(Too many open files)을 재현했다
[ ] 일반 사용자가 hard 를 올릴 수 없는 것을 확인했다
[ ] nproc 을 낮춰 fork 실패(EAGAIN)를 재현했다
[ ] limits.d 설정이 새 SSH 로그인에만 적용되는 것을 확인했다
[ ] 서비스 한도를 systemctl show 와 prlimit 로 확인했다

11. 핵심 정리

  • 자원 한도는 프로세스마다 soft·hard 두 값이며, fork로 자식에게 상속된다.
  • nofile 초과는 EMFILE(Too many open files), nproc 초과는 EAGAIN(fork 실패)이다.
  • 일반 사용자는 hard를 낮추기만 할 수 있다.
  • limits.conf는 PAM 로그인에만 적용된다. 서비스 한도는 unit 파일의 Limit*=로 설정한다.
  • 실제 적용값은 /proc/PID/limits로 확인한다.

12. 다음 편 예고

다음 글 「18. cgroup으로 자원 제어하기」 에서는 프로세스 하나가 아니라 서비스 전체 에 CPU·메모리·프로세스 수 제한을 거는 cgroup을 다룬다. systemd-run으로 CPU 20%, 프로세스 5개로 제한한 임시 서비스를 만들어 실제 제한이 동작하는 것을 확인한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글