제목: Self-pipe 기법을 이용한 Ada 시그널 핸들러 (코드 리뷰 부탁드립니다) 🚀
안녕하세요, Ada로 시스템 프로그래밍을 공부하고 있는 개발자입니다.
최근 유닉스(POSIX) 환경에서 시그널을 좀 더 안전하고 Ada스럽게 처리하는 라이브러리를 만들어보고 있습니다. 특히 시그널 핸들러의 비동기-안전(async-signal-safety) 문제를 해결하기 위해 self-pipe 기법을 적용해 보았는데, 다른 분들의 의견은 어떨지 궁금해서 코드를 공유하고 피드백을 요청합니다.
## 주요 설계
이 라이브러리의 핵심 설계는 다음과 같습니다.
- Self-Pipe 기법: 실제 시그널 핸들러에서는
write()
시스템 콜만으로 시그널 번호를 파이프에 쓰는 최소한의 작업만 수행합니다. 복잡한 로직은 모두 메인 이벤트 루프에서 파이프를 read()
하는 dispatch
프로시저로 옮겨, 비동기-안전 제약 조건에서 벗어나도록 설계했습니다. - 스레드-안전 핸들러 관리: 시그널 번호와 사용자 정의 핸들러를 매핑하는 자료구조를
protected object
로 감싸, 멀티스레드 환경에서도 안전하게 핸들러를 등록하고 호출할 수 있도록 했습니다. - 자동 자원 관리 (RAII):
Ada.Finalization
을 이용해 패키지 스코프가 종료될 때 생성된 파이프의 파일 디스크립터가 자동으로 close
되도록 구현하여 리소스 누수를 방지했습니다.
## 특징
- 비동기-안전(Async-Signal-Safe) 시그널 처리
- Ada의 강력한 타입을 활용한 안전한 API (
Action
레코드, Number
타입 등) - 스레드-안전(Thread-Safe) 핸들러 등록 및 관리
- 자동 자원 해제 (RAII)
## 고민되는 부분 및 질문
현재 dispatch
프로시저의 read
로직이 아직 미흡합니다. 지금 코드는 read
의 반환 값이 0 이하이면 무조건 루프를 빠져나가는데, 이렇게 되면 논블로킹(non-blocking) I/O에서 정상적으로 발생하는 EAGAIN
같은 상황에 제대로 대처하지 못합니다.
bytes_read < 0
일 때 errno
를 확인해서 EAGAIN
이나 EINTR
같은 경우를 구분하고, 실제 I/O 에러일 때는 예외를 던지는 식으로 개선해야 할 것 같은데, 이 부분에 대한 더 좋은 아이디어나 일반적인 처리 패턴이 있다면 조언 부탁드립니다!
## 전체 코드
-- clair-signal.adb
-- Copyright (c) 2025 Hodong Kim <hodong@nimfsoft.art>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
with Interfaces.C;
with System;
with Clair.Error;
with Ada.Containers.Hashed_Maps;
with Ada.Finalization;
with Ada.Unchecked_Conversion;
with unistd_h;
with signal_h;
with sys_signal_h; -- For the C sigaction type
with errno_h;
with sys_types_h;
with Clair.Config;
package body Clair.Signal is
use type Interfaces.C.Int;
use type Interfaces.C.long;
use type Interfaces.C.Unsigned;
use type Interfaces.C.Unsigned_Long;
use type Clair.File.Descriptor;
-- Internal state for the self-pipe
pipe_fds : aliased array (0 .. 1) of Clair.File.Descriptor := (-1, -1);
-- pipe() 함수에 전달할 포인터 타입을 정의합니다.
type C_Int_Access is access all Interfaces.C.int;
-- System.Address를 C_Int_Access 타입으로 변환하는 함수를 인스턴스화합니다.
function to_c_int_access is new Ada.Unchecked_Conversion
(source => System.Address,
target => C_Int_Access);
-- The actual signal handler that will be r e g i s t e r e d with the kernel
procedure clair_signal_handler
(signo : Interfaces.C.Int;
info : access sys_signal_h.siginfo_t;
context : System.Address);
pragma export (c, clair_signal_handler, "clair_signal_handler");
procedure clair_signal_handler
(signo : Interfaces.C.Int;
info : access sys_signal_h.siginfo_t;
context : System.Address)
is
signo_val : aliased Interfaces.C.Int := signo;
bytes_written : Interfaces.C.long;
begin
-- This is async-signal-safe
bytes_written :=
unistd_h.write
(Interfaces.C.Int (pipe_fds (1)),
signo_val'address,
sys_types_h.Size_t (Interfaces.C.Int'size / 8));
end clair_signal_handler;
-- Implementation of the raise(3) wrapper
procedure send (sig : Number) is
result : constant Interfaces.C.Int := signal_h.c_raise (Interfaces.C.Int (sig));
begin
if result /= 0 then
declare
error_code : constant Interfaces.C.Int := Clair.Error.get_errno;
error_msg : constant String :=
"raise(3) failed: " & "signal " & sig'image &
" (errno: " & error_code'image & ")";
begin
case error_code is
when errno_h.EINVAL => -- Invalid signal
raise Clair.Error.Invalid_Argument with
"Invalid signal specified. " & error_msg;
when errno_h.ESRCH => -- No such process
raise Clair.Error.No_Such_Process with "Process not found. " &
error_msg;
when errno_h.EPERM => -- Operation not permitted
raise Clair.Error.Permission_Denied with "Permission denied. " &
error_msg;
when others =>
declare
errno_text : constant String :=
Clair.Error.get_error_message (error_code);
begin
raise Clair.Error.Unknown_Error with errno_text & ". " &
error_msg;
end;
end case;
end;
end if;
end send;
-- 수동 해시 함수 정의
function hashfunc (key : Number) return Ada.Containers.Hash_Type is
begin
return Ada.Containers.Hash_Type (Interfaces.C.Int (key));
end hashfunc;
package Handler_Maps is new Ada.Containers.Hashed_Maps
(key_type => Number,
element_type => Handler_Access,
hash => hashfunc,
equivalent_keys => "=");
protected Handler_Registry is
procedure r e g i s t e r (sig : in Number; handler : in Handler_Access);
procedure call (sig : in Number);
private
handlers : Handler_Maps.Map;
end Handler_Registry;
protected body Handler_Registry is
procedure r e g i s t e r (sig : in Number; handler : in Handler_Access) is
begin
if handler = null then
handlers.delete (sig);
else
handlers.insert (sig, handler);
end if;
end r e g i s t e r;
procedure call (sig : in Number) is
-- 여기서 handler를 미리 선언할 필요가 없습니다.
begin
if handlers.contains (sig) then
-- declare 블록을 사용해 지역 상수를 선언과 동시에 초기화합니다.
declare
handler : constant Handler_Access := handlers.element (sig);
begin
if handler /= null then
handler.all (sig);
end if;
end;
end if;
end call;
end Handler_Registry;
-- Lifecycle management for automatic finalization
type Finalizer is new Ada.Finalization.Limited_Controlled with null record;
overriding
procedure finalize (object : in out Finalizer);
-- This object's declaration ensures finalize is called automatically
finalizer_instance : Finalizer;
overriding
procedure finalize (object : in out Finalizer) is
retval : Interfaces.C.Int;
begin
if pipe_fds (0) /= -1 then
retval := unistd_h.close (Interfaces.C.Int (pipe_fds (0)));
end if;
if pipe_fds (1) /= -1 then
retval := unistd_h.close (Interfaces.C.Int (pipe_fds (1)));
end if;
pragma unreferenced (retval);
end finalize;
function get_file_descriptor return Clair.File.Descriptor is
(Clair.File.Descriptor (pipe_fds (0)));
procedure set_action (sig : in Number; new_action : in Action) is
sa : aliased sys_signal_h.sigaction;
retval : Interfaces.C.Int;
begin
retval := signal_h.sigemptyset (sa.sa_mask'access);
if retval /= 0 then
if retval = errno_h.EINVAL then
raise Clair.Error.Invalid_Argument with "sigemptyset(3) failed";
else
declare
error_code : constant Interfaces.C.Int := Clair.Error.get_errno;
error_msg : constant String :=
"sigemptyset(3) failed (errno: " & error_code'image & ")";
begin
raise Program_Error with "sigemptyset(3) failed";
end;
end if;
end if;
case new_action.kind is
when Handle =>
sa.sa_flags := Interfaces.C.Int (
Interfaces.C.Unsigned (sys_signal_h.SA_RESTART) or
Interfaces.C.Unsigned (sys_signal_h.SA_SIGINFO)
);
sa.uu_sigaction_u.uu_sa_sigaction := clair_signal_handler'access;
Handler_Registry.r e g i s t e r (sig, new_action.handler);
when Default =>
sa.sa_flags := 0;
sa.uu_sigaction_u.uu_sa_handler := Clair.Config.SIG_DFL;
Handler_Registry.r e g i s t e r (sig, null);
when Ignore =>
sa.sa_flags := 0;
sa.uu_sigaction_u.uu_sa_handler := Clair.Config.SIG_IGN;
Handler_Registry.r e g i s t e r (sig, null);
end case;
if signal_h.sigaction2 (Interfaces.C.Int (sig), sa'access, null) /= 0
then
raise Program_Error with "sigaction system call failed";
end if;
end set_action;
procedure dispatch is
sig_num_c : aliased Interfaces.C.Int;
bytes_read : sys_types_h.ssize_t;
begin
loop
bytes_read := unistd_h.read (Interfaces.C.Int (pipe_fds (0)),
sig_num_c'address,
Interfaces.C.Int'size / 8);
if bytes_read > 0 then
Handler_Registry.call (Number (sig_num_c));
else
-- 읽을 데이터가 없거나(EAGAIN 등) 에러 발생 시 루프 종료
exit;
end if;
end loop;
end dispatch;
begin
if unistd_h.pipe (to_c_int_access (pipe_fds'address)) /= 0 then
raise Program_Error with "pipe() creation failed during initialization";
end if;
end Clair.Signal;
귀중한 시간 내어 읽어주셔서 감사하고, 어떤 피드백이든 환영입니다! 😊
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.