C언어에서 시그널과 비동기 I/O를 활용하는 실용 가이드

C언어에서 시그널과 비동기 I/O는 고성능 애플리케이션 개발에 필수적인 도구입니다. 시그널은 운영 체제와 프로세스 간의 비동기 통신을 가능하게 하며, 비동기 I/O는 입출력 작업 중 발생하는 지연을 최소화하여 프로그램의 처리 속도를 향상시킵니다. 본 기사에서는 두 기술의 기본 개념과 함께, 이를 결합하여 효율적이고 안정적인 프로그램을 개발하는 방법을 알아봅니다. 다양한 코드 예제와 응용 사례를 통해 실용적인 지식을 제공하며, 시그널과 비동기 I/O를 통해 복잡한 작업을 간소화하는 방법을 설명합니다.

목차

시그널과 비동기 I/O의 기본 개념


시그널과 비동기 I/O는 C언어에서 비동기 프로그래밍을 구현하기 위한 핵심 요소입니다.

시그널의 개념


시그널은 운영 체제에서 프로세스 간 또는 운영 체제와 프로세스 간의 비동기적 메시지를 전달하는 메커니즘입니다. 시그널은 특정 이벤트가 발생했음을 프로세스에 알리는 데 사용되며, 예를 들어 프로세스가 종료되거나 특정 리소스에 대한 접근이 허용되지 않을 때 시그널이 발생할 수 있습니다.

대표적인 시그널 종류

  • SIGINT: 사용자가 인터럽트를 발생시킬 때(예: Ctrl+C).
  • SIGTERM: 프로그램 종료 요청.
  • SIGCHLD: 자식 프로세스가 종료되었을 때.

비동기 I/O의 개념


비동기 I/O는 프로그램이 입출력 작업을 비차단 방식으로 처리하도록 설계된 메커니즘입니다. 이 방식에서는 I/O 작업이 완료될 때까지 대기하지 않고, 작업이 끝났다는 알림을 받을 때까지 다른 작업을 계속 진행할 수 있습니다.

비동기 I/O의 특징

  • 효율성: I/O 작업의 대기 시간을 줄여 처리 속도를 높입니다.
  • 비차단: 함수 호출이 즉시 반환되므로 멀티태스킹 환경에 적합합니다.
  • 적용 사례: 네트워크 서버, 파일 시스템, 데이터베이스 등에서 흔히 사용됩니다.

시그널과 비동기 I/O는 서로 보완적인 역할을 하며, 이들을 결합하여 비동기적 이벤트를 효율적으로 처리하는 프로그램을 설계할 수 있습니다.

시그널 처리의 원리와 메커니즘

시그널 처리의 작동 원리


시그널은 특정 이벤트가 발생했을 때 운영 체제가 프로세스에 전달하는 인터럽트입니다. 프로세스는 시그널 핸들러를 통해 이러한 시그널을 처리합니다. 시그널 핸들러는 사용자가 정의한 함수로, 시그널 발생 시 호출됩니다.

시그널 처리 흐름

  1. 이벤트 발생: 특정 조건에서 시그널이 생성됩니다.
  2. 시그널 전달: 운영 체제가 해당 시그널을 프로세스에 전달합니다.
  3. 시그널 처리: 프로세스는 등록된 핸들러를 호출하여 시그널을 처리합니다.

시그널 핸들러 설정


C언어에서는 signal() 함수 또는 sigaction() 함수를 사용하여 시그널 핸들러를 설정할 수 있습니다.

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void handle_signal(int sig) {
    printf("Received signal: %d\n", sig);
    exit(0);
}

int main() {
    signal(SIGINT, handle_signal); // SIGINT 핸들러 등록
    while (1) {
        printf("Running...\n");
        sleep(1);
    }
    return 0;
}

위 코드는 Ctrl+C로 SIGINT를 발생시키면 사용자 정의 핸들러가 호출되어 메시지를 출력하고 종료됩니다.

시그널 처리 중 주의 사항

  • 비동기 안전 함수 사용: 시그널 핸들러 내부에서는 비동기적으로 안전한 함수만 호출해야 합니다. 예를 들어, malloc()과 같은 함수는 호출하면 안 됩니다.
  • 재진입 문제 방지: 시그널 처리 중 동일한 시그널이 반복적으로 발생하지 않도록 주의해야 합니다.

시그널 처리는 비동기 이벤트를 처리하기 위한 중요한 기법이며, 이를 올바르게 이해하고 활용하면 프로그램의 안정성과 효율성을 크게 향상시킬 수 있습니다.

비동기 I/O의 구조와 사용 이유

비동기 I/O의 구조


비동기 I/O는 프로그램이 입출력 작업을 대기하지 않고, 다른 작업을 동시에 수행할 수 있도록 설계된 모델입니다. 운영 체제와 프로그램 간의 효율적인 데이터 전송을 위해 비동기 메커니즘이 사용됩니다.

비동기 I/O 처리 단계

  1. I/O 요청: 프로그램이 운영 체제에 특정 I/O 작업을 요청합니다.
  2. 비동기 작업 수행: 요청된 작업은 백그라운드에서 비차단 방식으로 실행됩니다.
  3. 알림 또는 폴링: 작업이 완료되면 콜백 함수 호출, 시그널 발생, 또는 폴링을 통해 완료 상태를 프로그램에 알립니다.

비동기 I/O 사용 이유

성능 향상

  • 지연 감소: I/O 작업이 끝날 때까지 대기하지 않으므로 CPU 자원을 효율적으로 활용할 수 있습니다.
  • 병렬 처리: 여러 작업을 동시에 실행하여 프로그램의 처리 속도를 높입니다.

비차단 프로세스 설계

  • 사용자 경험 개선: GUI 애플리케이션에서 UI를 차단하지 않고 데이터 처리를 백그라운드에서 수행할 수 있습니다.
  • 서버 성능 최적화: 네트워크 서버는 동시에 다수의 클라이언트 요청을 처리할 수 있습니다.

적용 사례

  • 네트워크 통신: 클라이언트-서버 모델에서 비동기 I/O는 다중 연결을 처리하는 데 유용합니다.
  • 파일 I/O: 대량의 파일 처리 작업에서 비동기 I/O를 통해 효율을 높일 수 있습니다.

비동기 I/O 구현 방식

POSIX AIO


POSIX 표준의 aio_read, aio_write와 같은 함수를 사용하여 비동기 파일 입출력을 수행할 수 있습니다.

#include <aio.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    struct aiocb cb;
    char buffer[100] = {0};

    int fd = open("example.txt", O_RDONLY);
    if (fd < 0) {
        perror("File open error");
        return 1;
    }

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = sizeof(buffer) - 1;
    cb.aio_offset = 0;

    aio_read(&cb);
    while (aio_error(&cb) == EINPROGRESS) {
        printf("Reading in progress...\n");
        sleep(1);
    }

    printf("Read completed: %s\n", buffer);
    close(fd);
    return 0;
}

위 코드는 aio_read를 사용하여 비동기적으로 파일을 읽고, 완료 시 데이터를 출력합니다.

비동기 I/O는 대규모 시스템에서 효율적이며, 이를 통해 프로그램의 성능과 반응성을 크게 향상시킬 수 있습니다.

시그널과 비동기 I/O의 결합

시그널과 비동기 I/O의 상호 보완


시그널과 비동기 I/O를 결합하면 효율적인 이벤트 기반 프로그래밍이 가능합니다. 시그널은 비동기적으로 발생하는 이벤트를 프로세스에 알리며, 비동기 I/O는 작업이 완료되었을 때 추가 작업을 수행할 수 있도록 설계됩니다.

결합의 이점

  1. 비동기 처리 강화: 시그널은 비동기 I/O의 상태를 알리는 데 사용됩니다.
  2. 리소스 활용 최적화: 시그널과 비동기 I/O를 통해 입출력 대기 시간을 최소화합니다.
  3. 복잡한 이벤트 처리 가능: 여러 I/O 작업과 시그널 이벤트를 조합하여 복잡한 로직을 효율적으로 구현할 수 있습니다.

시그널과 비동기 I/O를 결합한 구조

  1. 시그널 기반 알림: 비동기 I/O 작업 완료 시 시그널이 발생하도록 설정합니다.
  2. 시그널 핸들러: 발생한 시그널을 처리하고, 작업 완료 상태를 확인하거나 추가 작업을 수행합니다.

예제: 시그널과 비동기 I/O를 활용한 파일 읽기

#include <aio.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

volatile sig_atomic_t aio_completed = 0;

void signal_handler(int sig) {
    if (sig == SIGUSR1) {
        aio_completed = 1;
    }
}

int main() {
    struct aiocb cb;
    char buffer[100] = {0};

    int fd = open("example.txt", O_RDONLY);
    if (fd < 0) {
        perror("File open error");
        return 1;
    }

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = sizeof(buffer) - 1;
    cb.aio_offset = 0;

    // 시그널 핸들러 등록
    signal(SIGUSR1, signal_handler);
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGUSR1;

    // 비동기 읽기 요청
    aio_read(&cb);

    // 작업 완료 대기
    while (!aio_completed) {
        printf("Waiting for I/O to complete...\n");
        sleep(1);
    }

    printf("Read completed: %s\n", buffer);
    close(fd);
    return 0;
}

코드 설명

  • SIGUSR1 시그널은 비동기 읽기 작업이 완료되었을 때 발생합니다.
  • 시그널 핸들러는 aio_completed 플래그를 설정하여 작업 완료를 알립니다.
  • 메인 루프에서는 작업 완료를 대기하며 다른 작업을 수행할 수도 있습니다.

적용 사례

  • 네트워크 서버: 다중 클라이언트 요청을 처리할 때 I/O 작업과 이벤트를 병행 처리.
  • 데이터 스트리밍: 대량 데이터 전송 중 I/O와 이벤트 상태를 관리.

시그널과 비동기 I/O의 결합은 시스템 자원을 효율적으로 사용하며, 복잡한 비동기 이벤트 처리를 간결하게 구현하는 강력한 도구입니다.

시그널과 비동기 I/O를 활용한 이벤트 기반 프로그래밍

이벤트 기반 프로그래밍의 개념


이벤트 기반 프로그래밍은 특정 이벤트(시그널, I/O 작업 완료 등)가 발생할 때 이를 처리하는 방식으로 작동합니다. 이벤트 기반 설계는 효율적이며, 특히 다중 작업을 병행해야 하는 시스템에서 강력한 장점을 제공합니다.

이벤트 기반 프로그래밍의 구성 요소

  1. 이벤트 소스: 시그널, I/O 작업, 타이머 등 이벤트를 생성하는 요소.
  2. 이벤트 핸들러: 발생한 이벤트를 처리하는 함수 또는 메커니즘.
  3. 이벤트 루프: 이벤트를 감지하고 적절한 핸들러를 호출하는 구조.

시그널과 비동기 I/O를 활용한 이벤트 기반 설계

작동 흐름

  1. 비동기 I/O 작업을 시작하고 완료 시 알림을 받기 위해 시그널을 등록합니다.
  2. 메인 이벤트 루프에서 다른 작업을 수행하며 이벤트 발생을 감지합니다.
  3. 시그널 핸들러가 호출되어 작업 결과를 처리하거나 추가 작업을 수행합니다.

코드 예제: 이벤트 기반 네트워크 서버


아래는 시그널과 비동기 I/O를 사용하여 간단한 네트워크 서버를 구현한 예제입니다.

#include <aio.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

volatile sig_atomic_t aio_completed = 0;

void signal_handler(int sig) {
    if (sig == SIGUSR1) {
        aio_completed = 1;
    }
}

int main() {
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE] = {0};
    struct aiocb cb;

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("Socket creation error");
        return 1;
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        return 1;
    }

    if (listen(server_fd, 5) < 0) {
        perror("Listen failed");
        return 1;
    }

    printf("Server listening on port %d...\n", PORT);

    int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("Accept failed");
        return 1;
    }

    printf("Client connected.\n");

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = client_fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = BUFFER_SIZE - 1;
    cb.aio_offset = 0;

    signal(SIGUSR1, signal_handler);
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGUSR1;

    aio_read(&cb);

    while (!aio_completed) {
        printf("Waiting for client message...\n");
        sleep(1);
    }

    printf("Received message: %s\n", buffer);
    close(client_fd);
    close(server_fd);

    return 0;
}

코드 설명

  • 서버 구성: 소켓을 생성하고 클라이언트 연결을 수락합니다.
  • 비동기 읽기 설정: 클라이언트 메시지를 비동기적으로 읽고 완료 시 SIGUSR1 시그널로 알립니다.
  • 이벤트 루프: 메시지 수신 완료를 대기하면서 다른 작업을 처리할 수 있습니다.

이벤트 기반 설계의 장점

  • 효율성: CPU 자원을 효율적으로 사용하며, 대규모 클라이언트 요청을 처리할 수 있습니다.
  • 유연성: 다양한 이벤트 소스를 통합적으로 관리할 수 있습니다.
  • 응답성: 프로그램이 대기 상태 없이 사용자 요청에 신속하게 응답합니다.

이벤트 기반 프로그래밍은 비동기 I/O와 시그널의 조합을 통해 성능과 유지보수성을 모두 향상시킬 수 있는 강력한 설계 방식입니다.

오류 처리와 디버깅 전략

시그널과 비동기 I/O 사용 중 발생할 수 있는 오류

시그널 관련 오류

  • 시그널 중첩 문제: 동일한 시그널이 반복적으로 발생해 핸들러가 재진입하면 예상치 못한 동작이 발생할 수 있습니다.
  • 미등록 핸들러 호출: 시그널 핸들러를 등록하지 않거나 잘못 등록하면 프로그램이 비정상적으로 종료됩니다.
  • 비동기 안전 함수 사용 미흡: 시그널 핸들러에서 비동기적으로 안전하지 않은 함수를 호출하면 충돌 가능성이 높아집니다.

비동기 I/O 관련 오류

  • I/O 작업 중단: 작업 중 리소스가 해제되거나 파일 디스크립터가 닫히는 경우 작업이 중단됩니다.
  • 폴링 상태 확인 실패: I/O 작업 완료 여부를 제대로 확인하지 않으면 작업 결과가 유실될 수 있습니다.
  • 리소스 누수: 비동기 I/O 작업이 완료되지 않은 상태에서 리소스를 제대로 정리하지 않으면 시스템 메모리가 낭비됩니다.

오류 디버깅 전략

로그를 통한 문제 추적

  • 프로그램 실행 중 발생하는 시그널 및 비동기 I/O 상태를 상세히 기록합니다.
  • 각 작업의 시작, 진행, 완료 상태를 로그에 기록하여 오류 발생 시 원인을 추적할 수 있도록 합니다.
void log_event(const char *message) {
    FILE *log_file = fopen("event_log.txt", "a");
    if (log_file) {
        fprintf(log_file, "%s\n", message);
        fclose(log_file);
    }
}

디버깅 도구 활용

  • gdb: 시그널 발생 시 스택 추적 및 변수 값을 확인할 수 있습니다.
  • valgrind: 리소스 누수와 메모리 접근 문제를 디버깅하는 데 유용합니다.

시뮬레이션을 통한 테스트

  • 시그널 및 비동기 I/O 동작을 테스트하는 모의 환경을 만들어 경계 상황을 검증합니다.
  • 예를 들어, 시그널이 발생하지 않는 경우나 I/O 작업이 예상보다 오래 걸리는 상황을 테스트합니다.

비동기 안전 함수 사용

  • 시그널 핸들러 내에서는 write, _exit와 같은 비동기 안전 함수만 호출합니다.
  • 핸들러에서 복잡한 작업 대신 플래그를 설정하고, 메인 루프에서 이를 처리하도록 설계합니다.

예제: 안전한 시그널 처리

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t sig_received = 0;

void signal_handler(int sig) {
    sig_received = sig;
}

int main() {
    signal(SIGINT, signal_handler); // Ctrl+C 시그널 처리
    while (1) {
        if (sig_received) {
            printf("Signal %d received. Handling...\n", sig_received);
            sig_received = 0; // 플래그 초기화
        }
        sleep(1);
    }
    return 0;
}

오류 방지 팁

  1. 테스트 케이스 작성: 다양한 조건에서 시그널과 비동기 I/O 동작을 검증하는 테스트 케이스를 작성합니다.
  2. 리소스 정리: 작업이 완료되지 않은 경우에도 적절히 리소스를 정리하도록 설계합니다.
  3. 안전한 설정: 초기화되지 않은 상태에서 I/O 작업이나 시그널을 처리하지 않도록 주의합니다.

결론


효율적인 오류 처리와 디버깅 전략을 통해 시그널과 비동기 I/O를 사용하는 프로그램의 안정성과 신뢰성을 확보할 수 있습니다. 이러한 전략을 실천하면 복잡한 이벤트 기반 시스템에서도 예측 가능한 동작을 유지할 수 있습니다.

성능 최적화와 리소스 관리

시그널과 비동기 I/O에서의 성능 최적화

최소화된 시그널 핸들링

  • 시그널 핸들러에서는 최소한의 작업만 수행하고, 복잡한 처리는 메인 루프에서 진행합니다.
  • 시그널 핸들러는 작업 완료를 알리는 플래그 설정과 같은 가벼운 작업에 집중합니다.

비동기 작업 병렬화

  • 다중 비동기 I/O 작업을 병렬로 실행하여 시스템 리소스를 최대한 활용합니다.
  • 비차단 소켓과 비동기 파일 I/O를 결합해 효율적인 데이터 처리 구조를 설계합니다.

효율적인 이벤트 루프 설계

  • 이벤트 루프를 최적화하여 시그널 및 비동기 I/O 이벤트를 빠르게 처리합니다.
  • select(), poll(), epoll() 등 운영 체제 지원 이벤트 감지 메커니즘을 활용합니다.

리소스 관리 전략

적절한 리소스 정리

  • 비동기 I/O 작업이 완료되지 않은 상태에서 파일 디스크립터가 닫히는 것을 방지합니다.
  • 종료 시 모든 비동기 작업을 정리하고 사용한 리소스를 해제합니다.

리소스 누수 방지

  • 동적 메모리를 할당한 경우, 작업 완료 후 메모리를 즉시 해제합니다.
  • 네트워크 소켓, 파일 핸들 등 시스템 리소스를 사용한 후 반드시 닫습니다.

작업 타임아웃 설정

  • 비동기 작업이 과도하게 오래 걸리지 않도록 타임아웃을 설정합니다.
  • 작업이 제한 시간을 초과하면 자동으로 종료하거나 취소합니다.

코드 예제: 이벤트 루프에서 리소스 관리

#include <aio.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

volatile sig_atomic_t aio_completed = 0;

void signal_handler(int sig) {
    aio_completed = 1;
}

int main() {
    struct aiocb cb;
    char buffer[BUFFER_SIZE] = {0};

    int fd = open("example.txt", O_RDONLY);
    if (fd < 0) {
        perror("File open error");
        return 1;
    }

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = BUFFER_SIZE - 1;
    cb.aio_offset = 0;

    signal(SIGUSR1, signal_handler);
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGUSR1;

    aio_read(&cb);

    int timeout = 5; // 작업 타임아웃 설정 (5초)
    while (!aio_completed && timeout > 0) {
        printf("Waiting for I/O to complete...\n");
        sleep(1);
        timeout--;
    }

    if (timeout == 0) {
        printf("Operation timed out.\n");
    } else {
        printf("Read completed: %s\n", buffer);
    }

    close(fd);
    return 0;
}

코드 특징

  • 타임아웃을 설정하여 작업 완료 대기를 제한합니다.
  • 작업이 완료되지 않아도 파일 디스크립터를 닫아 리소스를 정리합니다.

성능 최적화의 이점

  • 자원 효율성: 시스템 메모리와 CPU 사용을 최소화하여 성능을 극대화합니다.
  • 안정성 향상: 작업 중단이나 리소스 누수로 인한 충돌을 방지합니다.
  • 확장성 확보: 복잡한 시스템에서도 높은 처리량을 유지합니다.

결론


성능 최적화와 리소스 관리는 고성능 프로그램 설계에서 핵심적인 요소입니다. 시그널과 비동기 I/O를 효율적으로 사용하여 시스템 리소스를 효과적으로 활용하고 안정적인 프로그램을 구현할 수 있습니다.

예제: 파일 I/O 및 네트워크 통신

파일 I/O에서의 시그널과 비동기 I/O 활용

비동기 파일 읽기


시그널을 통해 파일 읽기 작업 완료를 알리는 간단한 비동기 I/O 구현 예제입니다.

#include <aio.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 512

volatile sig_atomic_t aio_completed = 0;

void aio_signal_handler(int sig) {
    if (sig == SIGUSR1) {
        aio_completed = 1;
    }
}

int main() {
    struct aiocb cb;
    char buffer[BUFFER_SIZE] = {0};

    int fd = open("example.txt", O_RDONLY);
    if (fd < 0) {
        perror("File open error");
        return 1;
    }

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = BUFFER_SIZE - 1;
    cb.aio_offset = 0;

    signal(SIGUSR1, aio_signal_handler);
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGUSR1;

    aio_read(&cb);

    while (!aio_completed) {
        printf("Waiting for file read to complete...\n");
        sleep(1);
    }

    printf("File content: %s\n", buffer);
    close(fd);
    return 0;
}

코드 설명

  • 비동기 읽기 요청이 완료되면 SIGUSR1 시그널을 발생시킵니다.
  • 시그널 핸들러는 작업 완료 플래그를 설정하여 메인 루프에서 처리합니다.

네트워크 통신에서의 시그널과 비동기 I/O 활용

비동기 소켓 통신


다중 클라이언트와 통신하는 비동기 서버를 설계할 때, 시그널을 사용하여 I/O 작업 완료를 처리합니다.

#include <aio.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

volatile sig_atomic_t aio_completed = 0;

void aio_signal_handler(int sig) {
    if (sig == SIGUSR1) {
        aio_completed = 1;
    }
}

int main() {
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE] = {0};
    struct aiocb cb;

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("Socket creation error");
        return 1;
    }

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        return 1;
    }

    if (listen(server_fd, 5) < 0) {
        perror("Listen failed");
        return 1;
    }

    printf("Server listening on port %d...\n", PORT);

    int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("Accept failed");
        return 1;
    }

    printf("Client connected.\n");

    memset(&cb, 0, sizeof(cb));
    cb.aio_fildes = client_fd;
    cb.aio_buf = buffer;
    cb.aio_nbytes = BUFFER_SIZE - 1;
    cb.aio_offset = 0;

    signal(SIGUSR1, aio_signal_handler);
    cb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    cb.aio_sigevent.sigev_signo = SIGUSR1;

    aio_read(&cb);

    while (!aio_completed) {
        printf("Waiting for client message...\n");
        sleep(1);
    }

    printf("Received message: %s\n", buffer);
    close(client_fd);
    close(server_fd);

    return 0;
}

코드 설명

  • 서버는 클라이언트 메시지를 비동기적으로 읽고 완료 시 시그널을 통해 알림을 받습니다.
  • 메인 루프는 다른 클라이언트 요청 처리를 병행할 수 있습니다.

적용 사례와 이점

  • 파일 I/O: 대량 파일을 비동기로 읽거나 쓰는 데 유용합니다.
  • 네트워크 서버: 다중 클라이언트와 동시 통신 가능.
  • 리얼타임 애플리케이션: I/O 지연 없이 빠르게 데이터를 처리할 수 있습니다.

결론


파일 I/O와 네트워크 통신에 시그널과 비동기 I/O를 적용하면 성능을 향상시키고, 자원을 효율적으로 활용할 수 있습니다. 이러한 설계는 고성능 프로그램을 구현하는 데 필수적인 기법입니다.

요약


본 기사에서는 C언어에서 시그널과 비동기 I/O를 결합하여 고성능 프로그램을 설계하는 방법을 다뤘습니다. 시그널의 비동기적 이벤트 처리 능력과 비동기 I/O의 효율성을 활용하면 파일 I/O, 네트워크 통신, 이벤트 기반 프로그래밍에서 뛰어난 성능을 발휘할 수 있습니다. 다양한 코드 예제와 최적화 전략을 통해 안정적이고 확장 가능한 프로그램을 구현하는 방법을 제시했습니다. 이러한 기술을 적용함으로써 복잡한 작업도 간결하고 효율적으로 처리할 수 있습니다.

목차