← System Programming DUJINLABS.COM

Syscall / ELF · Linux userspace / kernel ABI

errno, EINTR, 짧은 반환

오류 한 번을 재시도하는 규칙이 아니라, 작업이 얼마나 진행됐는지와 syscall restart 정책을 함께 판단하는 법을 다룹니다.

Series
03 / 37
Build
cc -std=c17 -Wall -Wextra -O2 eintr_read.c -o eintr_read
Run
./eintr_read
Kernel
Linux 6.18.37 LTS

EINTR이면 무조건 같은 syscall을 다시 호출해도 되는가?

syscall이 signal 때문에 끊겼다고 해서 항상 아무 일도 하지 않은 것은 아니다. read/write 계열은 일부 byte를 처리했다면 그 byte 수를 성공값으로 반환할 수 있고, 전혀 진행되지 않았을 때 EINTR을 돌려준다.

SA_RESTART는 일부 blocking syscall을 커널과 libc가 자동 재시작하게 하지만 모든 syscall에 적용되지 않는다. timeout이 있는 poll 계열이나 사용자 상태가 바뀌는 작업은 호출자가 남은 시간을 다시 계산해야 한다.

구조 그림

그림 1. signal이 I/O 진행량과 만나는 세 가지 경우
T0 read 시작 requested=128
T1 wait queue 처리 byte=0
T2 signal pending mask/action 검사
T3-A EINTR 진행 없음 · 재시도/중단 결정
T3-B short return 일부 진행 · byte 수 우선
T3-C restart SA_RESTART 조건 충족

같은 signal이라도 syscall이 아직 시작되지 않았는지, 일부 byte를 처리했는지, 자동 재시작 대상인지에 따라 관찰되는 반환값이 다르다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
blocking read 데이터를 기다림
signal handler 예약
restart rule SA_RESTART/errno
caller loop 진행량과 deadline 검사
result 완료 또는 명시적 중단

재시도 여부는 errno 하나가 아니라 이미 처리한 byte, 절대 deadline, 호출의 부작용을 함께 보고 정한다.

그림 3. 커널 내부에서 지나가는 주요 지점
read syscall wait queue 진입
signal_pending 깨울 이유 확인
-ERESTARTSYS 내부 restart code
arch return instruction 되감기/errno
userspace EINTR 또는 성공

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
fs/read_write.c vfs_read(), vfs_write() 짧은 반환과 file position 갱신
kernel/signal.c get_signal(), signal_setup_done() pending signal과 syscall restart 결정
arch/x86/entry/common.c syscall_exit_to_user_mode() userspace 복귀 전 signal 처리

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 eintr_read.c -o eintr_read
01#define _POSIX_C_SOURCE 200809L
02#include <errno.h>
03#include <signal.h>
04#include <stdio.h>
05#include <unistd.h>
06
07static volatile sig_atomic_t timed_out;
08
09static void on_alarm(int signo)
10{
11    (void)signo;
12    timed_out = 1;
13}
14
15int main(void)
16{
17    struct sigaction sa = { .sa_handler = on_alarm };
18    char buffer[128];
19
20    sigemptyset(&sa.sa_mask);
21    sigaction(SIGALRM, &sa, NULL);
22    alarm(3);
23
24    for (;;) {
25        ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
26        if (n > 0)
27            return write(STDOUT_FILENO, buffer, (size_t)n) < 0;
28        if (n == 0)
29            return 0;
30        if (errno == EINTR && !timed_out)
31            continue;
32        if (errno == EINTR && timed_out)
33            return 2;
34        perror("read");
35        return 1;
36    }
37}

코드 조각별 설명

실제 코드 7행volatile sig_atomic_t timed_out

signal handler와 main 사이에 공유할 수 있다고 C가 보장하는 최소 정수형이다. volatile은 compiler 접근 생략을 막지만 thread synchronization을 제공하지 않는다.

실제 코드 17행struct sigaction sa

SA_RESTART를 넣지 않아 read()가 EINTR로 돌아오는 상황을 일부러 관찰한다.

실제 코드 22행alarm(3)

3초 뒤 SIGALRM을 pending 상태로 만들며, blocking read의 wait를 깨운다.

실제 코드 28행if (n == 0)

EOF는 오류가 아니다. pipe나 파일의 writer가 모두 닫힌 정상 종료 조건이다.

실제 코드 32행errno == EINTR && timed_out

같은 EINTR이라도 timeout 정책에 따라 재시도하지 않고 상위 호출자에게 종료 이유를 전달한다.

세부 동작

01

커널의 restart code는 errno와 다르다

커널 내부의 -ERESTARTSYS, -ERESTARTNOHAND 같은 값은 사용자에게 그대로 보이는 errno가 아니다. signal disposition과 architecture return path가 syscall instruction을 다시 실행할지 EINTR로 바꿀지 판단한다.

strace에서 restart_syscall 또는 ERESTARTSYS 문구가 보이는 이유가 여기에 있다.

02

상대 timeout은 반복할수록 늘어날 수 있다

EINTR 뒤에 원래 timeout을 그대로 넣으면 signal이 잦을 때 총 대기 시간이 무한히 늘어난다. 시작 때 CLOCK_MONOTONIC 절대 deadline을 만들고 재호출 직전에 남은 시간을 계산해야 한다.

ppoll(), pselect()는 wait와 signal mask 변경을 하나의 kernel operation으로 묶어 wakeup race를 피한다.

03

부작용이 있는 호출은 재시도 전 상태를 확인한다

write, send, accept처럼 객체를 만들거나 데이터를 일부 소비할 수 있는 호출은 성공 byte나 새 fd가 이미 반환됐는지 먼저 본다.

close()가 EINTR를 반환했을 때 같은 fd 번호를 무조건 다시 닫는 코드는 위험하다. Linux에서는 fd slot이 일찍 해제되어 다른 thread가 재사용할 수 있다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
pending signal발생 시 thread/process signal queue에 기록되고 delivery 때 소비된다blocked mask, disposition
restart_block일부 syscall이 restart에 필요한 인자를 task에 보존한다restart_fn, remaining time
deadline사용자 코드가 시작 시 만들고 작업 완료 또는 timeout까지 유지한다CLOCK_MONOTONIC 기준

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
timeout이 예상보다 길다EINTR마다 상대 timeout을 초기화절대 deadline과 남은 시간 log
중복 요청 발생부작용이 끝난 syscall을 맹목 재시도반환값과 서버 request id 확인
read가 계속 EINTRSA_RESTART 미사용 또는 restart 불가 syscallsigaction flag와 strace 확인

직접 확인

  1. 입력하지 않고 기다려 SIGALRM 뒤 exit status 2가 되는지 확인한다.
  2. sa.sa_flags = SA_RESTART를 추가한 뒤 read가 계속 대기하는지 strace로 비교한다.
  3. SIGALRM을 100ms 주기로 보내는 별도 process를 두고 상대 timeout과 절대 deadline 구현의 총 시간을 비교한다.
실행./eintr_read
추적strace -e trace=read,rt_sigaction,alarm ./eintr_read

원문