MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言信号处理函数的参数传递

2023-08-137.8k 阅读

信号处理函数基础概念

在Linux系统下,信号是进程间通信的一种机制,用于通知进程发生了某种特定事件。当一个信号被发送到进程时,进程可以选择忽略该信号、捕获并处理它或者采取默认行为。

信号处理函数是用来处理接收到信号的函数。在C语言中,我们可以使用 signal 函数或者 sigaction 函数来设置信号处理函数。signal 函数的原型如下:

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);

其中,signum 是要处理的信号编号,handler 可以是 SIG_IGN(忽略信号)、SIG_DFL(采取默认行为)或者一个自定义的信号处理函数指针。

sigaction 函数提供了更丰富的功能,其原型如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum 同样是信号编号,act 是一个指向 struct sigaction 的指针,用于设置新的信号处理动作,oldact 则用于保存旧的信号处理动作。

信号处理函数的参数传递基本原理

当一个信号被发送到进程并由信号处理函数处理时,信号处理函数需要获取关于信号的相关信息,这就涉及到参数传递。

以常见的 SIGINT(通常由 Ctrl+C 产生)信号为例,当进程接收到 SIGINT 信号时,系统会暂停当前进程的正常执行流程,转而执行 SIGINT 对应的信号处理函数。

在信号处理函数的定义中,其参数 int signum 就是用来标识接收到的信号类型。比如在处理 SIGINT 的信号处理函数中,signum 的值就是 SIGINT 的编号。这样,通过这个参数,信号处理函数可以知道是哪个信号触发了它,从而采取相应的处理逻辑。

传递附加信息

有时候,仅仅知道信号类型是不够的,我们可能还需要传递一些附加信息给信号处理函数。在Linux C语言中,可以使用 sigaction 函数的 sa_sigaction 成员来实现这一点。

struct sigaction 结构体定义如下:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

如果设置了 SA_SIGINFO 标志位,那么 sa_sigaction 成员将被使用,而不是 sa_handlersa_sigaction 函数的参数中,siginfo_t * 类型的参数可以提供更详细的信号信息。

siginfo_t 结构体包含了丰富的信息,例如发送信号的进程ID(si_pid)、发送信号的用户ID(si_uid)等。以下是 siginfo_t 结构体的部分常见成员:

typedef struct {
    int      si_signo;    /* Signal number */
    int      si_errno;    /* An errno value */
    int      si_code;     /* Signal code */
    int      si_trapno;   /* Trap number that caused
                               hardware-generated signal
                               (unused on most architectures) */
    pid_t    si_pid;      /* Sending process ID */
    uid_t    si_uid;      /* Real user ID of sending process */
    int      si_status;   /* Exit value or signal */
    clock_t  si_utime;    /* User time consumed */
    clock_t  si_stime;    /* System time consumed */
    sigval_t si_value;    /* Signal value */
    int      si_int;      /* POSIX.1b signal */
    void    *si_ptr;      /* POSIX.1b signal */
    int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
    int      si_timerid;  /* Timer ID; POSIX.1b timers */
    void    *si_addr;     /* Memory location which caused fault */
    long     si_band;     /* Band event */
    int      si_fd;       /* File descriptor */
    short    si_addr_lsb; /* Least significant bit of address */
    void    *si_call_addr;/* Address of system call instruction */
    int      si_syscall;  /* Number of attempted system call */
    unsigned int si_arch; /* Architecture of attempted system call */
} siginfo_t;

代码示例

下面通过具体的代码示例来展示信号处理函数的参数传递。

首先,使用 signal 函数来处理 SIGINT 信号,只获取信号类型:

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

void sigint_handler(int signum) {
    printf("Received SIGINT signal (signum = %d)\n", signum);
}

int main() {
    signal(SIGINT, sigint_handler);
    printf("Waiting for SIGINT (press Ctrl+C)\n");
    while(1) {
        sleep(1);
    }
    return 0;
}

在上述代码中,sigint_handler 函数的参数 signum 用于获取接收到的信号类型。当用户按下 Ctrl+C 时,进程接收到 SIGINT 信号,sigint_handler 函数被调用,输出接收到的信号编号。

接下来,使用 sigaction 函数并设置 SA_SIGINFO 标志位,获取更多信号信息:

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

void sigint_sigaction_handler(int signum, siginfo_t *info, void *context) {
    printf("Received SIGINT signal (signum = %d)\n", signum);
    printf("Sender process ID: %d\n", info->si_pid);
    printf("Sender user ID: %d\n", info->si_uid);
}

int main() {
    struct sigaction sa;
    sa.sa_sigaction = sigint_sigaction_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;

    sigaction(SIGINT, &sa, NULL);

    printf("Waiting for SIGINT (press Ctrl+C)\n");
    while(1) {
        sleep(1);
    }
    return 0;
}

在这段代码中,sigint_sigaction_handler 函数通过 siginfo_t * 类型的参数 info 获取了发送信号的进程ID和用户ID等更多信息。当接收到 SIGINT 信号时,不仅会输出信号编号,还会输出发送信号的进程ID和用户ID。

信号处理函数参数传递中的注意事项

  1. 可重入性:信号处理函数可能在进程执行的任何时刻被调用,因此它应该是可重入的。这意味着在信号处理函数中应该避免使用全局变量或者静态变量进行复杂的状态维护,因为这些变量可能在信号处理函数执行期间被其他部分的代码修改,导致不可预测的结果。
  2. 异步信号安全:信号处理函数只能调用异步信号安全的函数。异步信号安全的函数是指那些在信号处理函数中调用不会导致未定义行为的函数。例如,printf 函数不是异步信号安全的,在信号处理函数中调用 printf 可能会导致程序崩溃。常见的异步信号安全函数有 write_exit 等。
  3. 信号掩码:在信号处理函数执行期间,系统会自动将该信号添加到进程的信号掩码中,以防止该信号在处理过程中再次被接收。可以通过 sigaction 函数的 sa_mask 成员来设置在信号处理函数执行期间需要阻塞的其他信号。

利用信号处理函数参数传递实现进程间通信

通过信号处理函数的参数传递,我们可以在一定程度上实现进程间的通信。例如,一个进程可以通过发送带有特定信息的信号给另一个进程,接收信号的进程在其信号处理函数中解析这些信息。

假设我们有两个进程,父进程向子进程发送信号并传递一些数据。以下是示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void child_signal_handler(int signum, siginfo_t *info, void *context) {
    sigval_t value = info->si_value;
    int data = value.sival_int;
    printf("Child received signal %d with data: %d\n", signum, data);
}

int main() {
    struct sigaction sa;
    sa.sa_sigaction = child_signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;

    sigaction(SIGUSR1, &sa, NULL);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        while(1) {
            sleep(1);
        }
    } else {
        // 父进程
        sigval_t value;
        value.sival_int = 42;
        union sigval sv;
        sv.sival_int = 42;
        if (sigqueue(pid, SIGUSR1, sv) == -1) {
            perror("sigqueue");
            exit(EXIT_FAILURE);
        }
        wait(NULL);
    }
    return 0;
}

在上述代码中,父进程通过 sigqueue 函数向子进程发送 SIGUSR1 信号,并传递了一个整数值 42。子进程在其信号处理函数 child_signal_handler 中解析接收到的数据并输出。

信号处理函数参数传递在多线程环境中的应用

在多线程环境中,信号处理变得更加复杂。默认情况下,信号会被发送到整个进程,而不是特定的线程。然而,可以使用 pthread_sigmask 函数来控制线程对信号的屏蔽,并且可以使用 pthread_kill 函数向特定线程发送信号。

以下是一个简单的多线程信号处理示例:

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

void *thread_function(void *arg) {
    sigset_t *set = (sigset_t *)arg;
    int signum;
    while (1) {
        if (sigwait(set, &signum) == 0) {
            if (signum == SIGUSR1) {
                printf("Thread received SIGUSR1\n");
            }
        }
    }
    return NULL;
}

int main() {
    pthread_t thread;
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);

    if (pthread_sigmask(SIG_BLOCK, &set, NULL) != 0) {
        perror("pthread_sigmask");
        return 1;
    }

    if (pthread_create(&thread, NULL, thread_function, (void *)&set) != 0) {
        perror("pthread_create");
        return 1;
    }

    sleep(2);
    if (pthread_kill(thread, SIGUSR1) != 0) {
        perror("pthread_kill");
        return 1;
    }

    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
        return 1;
    }

    return 0;
}

在这个示例中,主线程创建了一个新线程,并将 SIGUSR1 信号添加到线程的信号掩码中。新线程通过 sigwait 函数等待 SIGUSR1 信号,主线程在延迟2秒后向新线程发送 SIGUSR1 信号。新线程接收到信号后,输出相应的信息。

信号处理函数参数传递与系统调用的交互

当进程在执行系统调用时接收到信号,系统调用的行为会受到影响。通常情况下,系统调用会被中断,并将控制权转移到信号处理函数。

例如,考虑一个使用 read 系统调用从文件描述符读取数据的进程。如果在 read 调用期间接收到信号,read 调用可能会被中断,返回 -1 并设置 errnoEINTR

以下是示例代码:

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

void sigint_handler(int signum) {
    printf("Received SIGINT\n");
}

int main() {
    signal(SIGINT, sigint_handler);
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    char buffer[1024];
    ssize_t bytes_read;
    while (1) {
        bytes_read = read(fd, buffer, sizeof(buffer));
        if (bytes_read == -1) {
            if (errno == EINTR) {
                printf("read interrupted by signal\n");
                continue;
            } else {
                perror("read");
                break;
            }
        } else if (bytes_read == 0) {
            break;
        }
        write(STDOUT_FILENO, buffer, bytes_read);
    }
    close(fd);
    return 0;
}

在上述代码中,当进程在执行 read 系统调用时,如果接收到 SIGINT 信号,read 调用会被中断,返回 -1errno 被设置为 EINTR。信号处理函数执行完毕后,read 调用可以继续执行(通过 continue 语句)。

信号处理函数参数传递与程序调试

在调试包含信号处理函数的程序时,了解信号处理函数的参数传递情况对于定位问题非常重要。调试工具如 gdb 可以帮助我们分析信号处理函数的执行过程。

例如,在 gdb 中,可以使用 handle 命令来设置对特定信号的处理方式,使用 catch signal 命令来捕获信号的发送。通过在信号处理函数中设置断点,可以查看参数的值以及函数的执行流程。

以下是使用 gdb 调试信号处理程序的简单步骤:

  1. 编译程序时加上 -g 选项以包含调试信息:gcc -g -o my_program my_program.c
  2. 启动 gdbgdb my_program
  3. 在信号处理函数处设置断点,例如 break sigint_handler
  4. 运行程序:run
  5. 发送信号(例如在另一个终端中对进程发送 SIGINT)。
  6. gdb 会停在信号处理函数的断点处,此时可以使用 print 命令查看参数的值,如 print signum 查看信号编号。

不同系统下信号处理函数参数传递的差异

虽然Linux系统遵循POSIX标准来处理信号,但不同的UNIX系统在信号处理函数参数传递方面可能存在一些细微差异。

例如,在一些老版本的UNIX系统中,siginfo_t 结构体的成员可能与Linux系统不完全相同,或者某些信号相关的功能实现方式略有不同。在编写跨平台的程序时,需要特别注意这些差异,通常可以通过条件编译(#ifdef)来处理不同系统下的代码。

以下是一个简单的跨平台处理信号的示例:

#ifdef _WIN32
#include <windows.h>
#include <stdio.h>
BOOL WINAPI ConsoleCtrlHandler(DWORD dwCtrlType) {
    switch (dwCtrlType) {
        case CTRL_C_EVENT:
            printf("Received Ctrl+C\n");
            return TRUE;
        default:
            return FALSE;
    }
}
int main() {
    SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);
    while (1) {
        Sleep(1000);
    }
    return 0;
}
#else
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum) {
    printf("Received SIGINT\n");
}
int main() {
    signal(SIGINT, sigint_handler);
    while (1) {
        sleep(1);
    }
    return 0;
}
#endif

在上述代码中,通过 #ifdef _WIN32 判断是否为Windows系统,如果是则使用Windows下的控制台控制处理函数,否则使用Linux下的信号处理函数。

总结

Linux C语言中信号处理函数的参数传递是一个重要的概念,它允许进程在接收到信号时获取详细的信息并做出相应的处理。通过合理地使用 signal 函数和 sigaction 函数,以及了解信号处理函数参数传递的原理和注意事项,我们可以编写出健壮、可靠的程序,实现进程间通信、多线程信号处理以及与系统调用的良好交互。同时,在跨平台开发中,要注意不同系统下信号处理的差异,以确保程序的兼容性。在调试包含信号处理的程序时,调试工具如 gdb 可以帮助我们更好地理解和分析信号处理函数的执行过程。通过深入理解信号处理函数的参数传递,我们能够在Linux环境下编写更高效、稳定的C语言程序。