如何使 POSIX/Linux 信号处理安全?
How to make POSIX/Linux signal handling safe?
我刚刚使用 GNU 异步 I/O 和信号实现了异步文件读取。我使用带有回调处理程序(SIGUSR1 目标)的信号处理结果:
static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
struct aioRequest *request = si->si_value.sival_ptr;
int bytes_read = aio_return(request->aiocbp);
printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);
// continue reading if whole buffer was filled
if(bytes_read == BUF_SIZE) {
request->aiocbp->aio_offset += bytes_read;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
} else {
request->finished = 1;
}
}
我想知道如果有人向我的进程发送 SIGUSR1 会发生什么。显然它不会用我的结构实例填充 siginfo_t *si
,因此我读取了垃圾,在幸运的情况下,程序会立即以段错误结束。在糟糕的情况下,一些其他数据会损坏,并且不会检测到错误。我该如何防范?
完整来源:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <aio.h>
#include <signal.h>
#include <fcntl.h>
#define BUF_SIZE 4 /* Size of buffers for read operations */
#define IO_SIGNAL SIGUSR1 /* Signal used to notify I/O completion */
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
#define errMsg(msg) do { perror(msg); } while (0)
static volatile sig_atomic_t gotSIGQUIT = 0;
struct aioRequest {
int finished;
struct aiocb *aiocbp;
};
static void aioSigHandler(int sig, siginfo_t *si, void *ucontext);
static const char * aioStatusToString(int status);
static struct aioRequest * aioReadingStart(const char *filename);
static void quitHandler(int sig);
int
main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
exit(EXIT_FAILURE);
}
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sa.sa_handler = quitHandler;
if (sigaction(SIGQUIT, &sa, NULL) == -1)
errExit("sigaction");
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sa.sa_sigaction = aioSigHandler;
if (sigaction(IO_SIGNAL, &sa, NULL) == -1)
errExit("sigaction");
struct aioRequest *request = aioReadingStart(argv[1]);
while (1) {
sleep(3); /* Delay between each monitoring step */
if(request->finished) {
break;
}
int status = aio_error(request->aiocbp);
if (status != EINPROGRESS) {
printf("aio_error() for request (descriptor %d): ", request->aiocbp->aio_fildes);
printf("%s\n",aioStatusToString(status));
break;
}
if (gotSIGQUIT) {
/* On receipt of SIGQUIT, attempt to cancel I/O requests,
* and display status returned
* from the cancellation request */
printf("got SIGQUIT; canceling I/O request: ");
int s = aio_cancel(request->aiocbp->aio_fildes, request->aiocbp);
if (s == AIO_CANCELED)
printf("I/O canceled\n");
else if (s == AIO_NOTCANCELED)
printf("I/O not canceled\n");
else if (s == AIO_ALLDONE)
printf("I/O all done\n");
else
errMsg("aio_cancel");
gotSIGQUIT = 0;
}
}
printf("File reading completed\n");
exit(EXIT_SUCCESS);
}
static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
struct aioRequest *request = si->si_value.sival_ptr;
int bytes_read = aio_return(request->aiocbp);
printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);
// continue reading if whole buffer was filled
if(bytes_read == BUF_SIZE) {
request->aiocbp->aio_offset += bytes_read;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
} else {
request->finished = 1;
}
}
static
const char * aioStatusToString(int status) {
switch (status) {
case 0:
return "I/O succeeded\n";
case EINPROGRESS:
return "In progress\n";
case ECANCELED:
return "Canceled\n";
default:
errMsg("aio_error");
return 0;
}
}
static
struct aioRequest * aioReadingStart(const char *filename) {
struct aioRequest *request = malloc(sizeof(struct aioRequest));
struct aiocb *aiocbInstance = malloc(sizeof(struct aiocb));
if (request == NULL || aiocbInstance == NULL)
errExit("malloc");
request->finished = 0;
request->aiocbp = aiocbInstance;
request->aiocbp->aio_fildes = open(filename, O_RDONLY);
if (request->aiocbp->aio_fildes == -1)
errExit("open");
printf("opened %s on descriptor %d\n", filename,
request->aiocbp->aio_fildes);
request->aiocbp->aio_buf = malloc(BUF_SIZE);
if (request->aiocbp->aio_buf == NULL)
errExit("malloc");
request->aiocbp->aio_nbytes = BUF_SIZE;
request->aiocbp->aio_reqprio = 0;
request->aiocbp->aio_offset = 0;
request->aiocbp->aio_sigevent.sigev_notify = SIGEV_SIGNAL;
request->aiocbp->aio_sigevent.sigev_signo = IO_SIGNAL;
request->aiocbp->aio_sigevent.sigev_value.sival_ptr = request;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
return request;
}
static void
quitHandler(int sig) {
gotSIGQUIT = 1;
}
为了专注于所述问题,我将把我的建议限制在信号处理方面。
考虑使用实时信号(SIGRTMIN+0
到 SIGRTMAX-0
,包括在内)而不是 SIGUSR1
。 SIGUSR1
等标准信号未排队,因此您可能会丢失它们(如果在另一个相同信号触发时您已经有一个未决信号),但实时信号已排队,并且更可靠。有关详细信息,请参阅 man 7 signal
中的实时信号部分。
还可以考虑在信号处理程序的开头保存 errno
,并在返回前恢复它。否则,在某些极端情况下,信号传递可能 "corrupts" errno
(因为您的信号处理程序隐式修改了它),这很难调试 - 简单地说,在某些情况下 errno
你认为是由于系统调用失败而分配的,实际上是由你的信号处理程序重置的。
(语言律师可能会指出访问线程局部变量,errno
通常是一个,是非异步信号安全的,至少在理论上是这样。在实践中它是安全的,特别是如果线程局部变量在信号之前已被线程访问。有关 glibc 的更多详细信息,请参阅 this thread at the libc-alpha mailing list. Personally, I create my pthreads with smaller than default stacks (the default being way too large for typical worker threads), and ensure the thread function reads and writes thread-local variables as the first thing, avoiding any thread-local non-async-signal-safe issues in practice. This also applies to the main thread. In short, if the thread-local variables are known to be allocated and available prior to the signal delivery, their use is, in practice, async-signal-safe. Finally, async-signal-safe functions such as read()
and write()
do modify errno
internally, without any special handling,因此如果它们是异步信号安全的,则恢复 errno
也必须如此。)
如 man 7 signal
手册页中所述,以及 Andrew Henle 在对原始问题的评论中提到的那样,只有异步信号安全函数才能在信号处理程序中安全使用。 aio_read()
和 printf()
都不是异步信号安全的。
请注意 read(2)
and write(2)
是异步信号安全的,可以与例如一起使用。匿名套接字对,用于将信息包(描述事件)传输到事件处理线程,或将信息打印(调试)到标准输出或标准错误(分别为 STDOUT_FILENO
和 STDERR_FILENO
描述符) .
如果您绝对需要使用非异步信号安全函数,请阻止这些信号,并创建一个使用 sigwaitinfo()
to handle the signals. This won't necessarily work for thread-targeted signals on Linux, and I personally would use a signal handler, GCC atomic builtins (they're supported by most C compilers, fortunately) to maintain a queue of events, and e.g. sem_post()
唤醒事件处理线程的辅助线程。这里有几个设计选项,到目前为止,即使是我遇到的古怪问题也总是可以使用相对简单的方法解决。
如 man 2 sigaction
手册页所述,您可以检查 si->code
以找出信号的原因;对于 AIO 完成,它将是 SI_ASYNCIO
,对于 SIGIO 信号,它将是 POLL_IN
/POLL_OUT
/POLL_MSG
/POLL_ERR
/POLL_PRI
/POLL_HUP
, SI_KERNEL
用于其他内核发送的信号,依此类推。如果 si->code
是 SI_USER
或 SI_QUEUE
,您可以检查 si->pid
以找出哪个进程发送了信号。
还建议通过例如清除整个 struct sigaction
memset(&sa, 0, sizeof sa);
在设置任何字段之前。 (这是因为某些字段可能是也可能不是联合;将整个结构清除为全零可确保 "safe" 未使用字段的值。)
嗯,我是不是忘记了什么?如果您发现我遗漏了什么,请在评论中告诉我,以便我修复此答案。
我刚刚使用 GNU 异步 I/O 和信号实现了异步文件读取。我使用带有回调处理程序(SIGUSR1 目标)的信号处理结果:
static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
struct aioRequest *request = si->si_value.sival_ptr;
int bytes_read = aio_return(request->aiocbp);
printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);
// continue reading if whole buffer was filled
if(bytes_read == BUF_SIZE) {
request->aiocbp->aio_offset += bytes_read;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
} else {
request->finished = 1;
}
}
我想知道如果有人向我的进程发送 SIGUSR1 会发生什么。显然它不会用我的结构实例填充 siginfo_t *si
,因此我读取了垃圾,在幸运的情况下,程序会立即以段错误结束。在糟糕的情况下,一些其他数据会损坏,并且不会检测到错误。我该如何防范?
完整来源:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <aio.h>
#include <signal.h>
#include <fcntl.h>
#define BUF_SIZE 4 /* Size of buffers for read operations */
#define IO_SIGNAL SIGUSR1 /* Signal used to notify I/O completion */
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
#define errMsg(msg) do { perror(msg); } while (0)
static volatile sig_atomic_t gotSIGQUIT = 0;
struct aioRequest {
int finished;
struct aiocb *aiocbp;
};
static void aioSigHandler(int sig, siginfo_t *si, void *ucontext);
static const char * aioStatusToString(int status);
static struct aioRequest * aioReadingStart(const char *filename);
static void quitHandler(int sig);
int
main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
exit(EXIT_FAILURE);
}
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
sa.sa_handler = quitHandler;
if (sigaction(SIGQUIT, &sa, NULL) == -1)
errExit("sigaction");
sa.sa_flags = SA_RESTART | SA_SIGINFO;
sa.sa_sigaction = aioSigHandler;
if (sigaction(IO_SIGNAL, &sa, NULL) == -1)
errExit("sigaction");
struct aioRequest *request = aioReadingStart(argv[1]);
while (1) {
sleep(3); /* Delay between each monitoring step */
if(request->finished) {
break;
}
int status = aio_error(request->aiocbp);
if (status != EINPROGRESS) {
printf("aio_error() for request (descriptor %d): ", request->aiocbp->aio_fildes);
printf("%s\n",aioStatusToString(status));
break;
}
if (gotSIGQUIT) {
/* On receipt of SIGQUIT, attempt to cancel I/O requests,
* and display status returned
* from the cancellation request */
printf("got SIGQUIT; canceling I/O request: ");
int s = aio_cancel(request->aiocbp->aio_fildes, request->aiocbp);
if (s == AIO_CANCELED)
printf("I/O canceled\n");
else if (s == AIO_NOTCANCELED)
printf("I/O not canceled\n");
else if (s == AIO_ALLDONE)
printf("I/O all done\n");
else
errMsg("aio_cancel");
gotSIGQUIT = 0;
}
}
printf("File reading completed\n");
exit(EXIT_SUCCESS);
}
static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
struct aioRequest *request = si->si_value.sival_ptr;
int bytes_read = aio_return(request->aiocbp);
printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);
// continue reading if whole buffer was filled
if(bytes_read == BUF_SIZE) {
request->aiocbp->aio_offset += bytes_read;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
} else {
request->finished = 1;
}
}
static
const char * aioStatusToString(int status) {
switch (status) {
case 0:
return "I/O succeeded\n";
case EINPROGRESS:
return "In progress\n";
case ECANCELED:
return "Canceled\n";
default:
errMsg("aio_error");
return 0;
}
}
static
struct aioRequest * aioReadingStart(const char *filename) {
struct aioRequest *request = malloc(sizeof(struct aioRequest));
struct aiocb *aiocbInstance = malloc(sizeof(struct aiocb));
if (request == NULL || aiocbInstance == NULL)
errExit("malloc");
request->finished = 0;
request->aiocbp = aiocbInstance;
request->aiocbp->aio_fildes = open(filename, O_RDONLY);
if (request->aiocbp->aio_fildes == -1)
errExit("open");
printf("opened %s on descriptor %d\n", filename,
request->aiocbp->aio_fildes);
request->aiocbp->aio_buf = malloc(BUF_SIZE);
if (request->aiocbp->aio_buf == NULL)
errExit("malloc");
request->aiocbp->aio_nbytes = BUF_SIZE;
request->aiocbp->aio_reqprio = 0;
request->aiocbp->aio_offset = 0;
request->aiocbp->aio_sigevent.sigev_notify = SIGEV_SIGNAL;
request->aiocbp->aio_sigevent.sigev_signo = IO_SIGNAL;
request->aiocbp->aio_sigevent.sigev_value.sival_ptr = request;
if (aio_read(request->aiocbp) == -1)
errExit("aio_read");
return request;
}
static void
quitHandler(int sig) {
gotSIGQUIT = 1;
}
为了专注于所述问题,我将把我的建议限制在信号处理方面。
考虑使用实时信号(SIGRTMIN+0
到 SIGRTMAX-0
,包括在内)而不是 SIGUSR1
。 SIGUSR1
等标准信号未排队,因此您可能会丢失它们(如果在另一个相同信号触发时您已经有一个未决信号),但实时信号已排队,并且更可靠。有关详细信息,请参阅 man 7 signal
中的实时信号部分。
还可以考虑在信号处理程序的开头保存 errno
,并在返回前恢复它。否则,在某些极端情况下,信号传递可能 "corrupts" errno
(因为您的信号处理程序隐式修改了它),这很难调试 - 简单地说,在某些情况下 errno
你认为是由于系统调用失败而分配的,实际上是由你的信号处理程序重置的。
(语言律师可能会指出访问线程局部变量,errno
通常是一个,是非异步信号安全的,至少在理论上是这样。在实践中它是安全的,特别是如果线程局部变量在信号之前已被线程访问。有关 glibc 的更多详细信息,请参阅 this thread at the libc-alpha mailing list. Personally, I create my pthreads with smaller than default stacks (the default being way too large for typical worker threads), and ensure the thread function reads and writes thread-local variables as the first thing, avoiding any thread-local non-async-signal-safe issues in practice. This also applies to the main thread. In short, if the thread-local variables are known to be allocated and available prior to the signal delivery, their use is, in practice, async-signal-safe. Finally, async-signal-safe functions such as read()
and write()
do modify errno
internally, without any special handling,因此如果它们是异步信号安全的,则恢复 errno
也必须如此。)
如 man 7 signal
手册页中所述,以及 Andrew Henle 在对原始问题的评论中提到的那样,只有异步信号安全函数才能在信号处理程序中安全使用。 aio_read()
和 printf()
都不是异步信号安全的。
请注意 read(2)
and write(2)
是异步信号安全的,可以与例如一起使用。匿名套接字对,用于将信息包(描述事件)传输到事件处理线程,或将信息打印(调试)到标准输出或标准错误(分别为 STDOUT_FILENO
和 STDERR_FILENO
描述符) .
如果您绝对需要使用非异步信号安全函数,请阻止这些信号,并创建一个使用 sigwaitinfo()
to handle the signals. This won't necessarily work for thread-targeted signals on Linux, and I personally would use a signal handler, GCC atomic builtins (they're supported by most C compilers, fortunately) to maintain a queue of events, and e.g. sem_post()
唤醒事件处理线程的辅助线程。这里有几个设计选项,到目前为止,即使是我遇到的古怪问题也总是可以使用相对简单的方法解决。
如 man 2 sigaction
手册页所述,您可以检查 si->code
以找出信号的原因;对于 AIO 完成,它将是 SI_ASYNCIO
,对于 SIGIO 信号,它将是 POLL_IN
/POLL_OUT
/POLL_MSG
/POLL_ERR
/POLL_PRI
/POLL_HUP
, SI_KERNEL
用于其他内核发送的信号,依此类推。如果 si->code
是 SI_USER
或 SI_QUEUE
,您可以检查 si->pid
以找出哪个进程发送了信号。
还建议通过例如清除整个 struct sigaction
memset(&sa, 0, sizeof sa);
在设置任何字段之前。 (这是因为某些字段可能是也可能不是联合;将整个结构清除为全零可确保 "safe" 未使用字段的值。)
嗯,我是不是忘记了什么?如果您发现我遗漏了什么,请在评论中告诉我,以便我修复此答案。