I've just implemented asynchronous file reading using GNU Asynchronous I/O and signals. I handle result using signals with callback handler (SIGUSR1 target):
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;
}
}
I wonder what could happen if someone sends SIGUSR1 to my process. Obviosly it won't have siginfo_t *si
populated with instance of my structure, therefore I read garbage, and in lucky situation the program would immediately end with segfault. In bad scenario some other data get corrupted and the error wouldn't be detected. How can I protect against that?
Complete source:
#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;
}
To focus on the stated question, I shall limit my suggestions to the signal handling aspect.
Consider using a realtime signal ( SIGRTMIN+0
to SIGRTMAX-0
, inclusive) instead of SIGUSR1
. Standard signals such as SIGUSR1
are not queued, so you may lose them (if you have one already pending when another same signal triggers), but realtime signals are queued, and much more reliable. See Real-time signals section in man 7 signal
for details.
Also consider saving errno
at the beginning of your signal handler, and restoring it before returning. Otherwise, it is possible that in some corner cases the signal delivery "corrupts" errno
(because your signal handler modifies it implicitly), which is very hard to debug -- simply put, in some cases the errno
you think was assigned due to a failed syscall, was actually reset by your signal handler.
(Language-lawyers might point out that accessing thread-local variables, errno
typically being one, is non-async-signal safe, at least in theory. In practice it is safe, especially if the thread-local variables have been accessed by the thread prior to the signal. For further details regarding glibc, see 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 , so if they are async-signal-safe, restoring errno
has to be too.)
As described in the man 7 signal
man page, and mentioned by Andrew Henle in a comment to the original question, only async-signal-safe functions are safe to use in a signal handler. Neither aio_read()
nor printf()
are async-signal-safe.
Note that read(2)
and write(2)
are async-signal-safe, and can be used with eg. an anonymous socket pair to transfer an information package (describing the event) to an event-processing thread, or to print (debugging) information to standard output or standard error ( STDOUT_FILENO
and STDERR_FILENO
descriptors, respectively).
If you absolutely need to use non-async-signal-safe functions, block those signals, and create a helper thread that uses 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 eg sem_post()
to wake up the event-processing thread. There are several design options here, and thus far even the oddball problems I've come across have always been solvable using a relatively straightforward approach.
As described in the man 2 sigaction
man page, you can examine si->code
to find out the reason for the signal; it will be SI_ASYNCIO
for AIO completions, POLL_IN
/ POLL_OUT
/ POLL_MSG
/ POLL_ERR
/ POLL_PRI
/ POLL_HUP
for SIGIO signals, SI_KERNEL
for other kernel-sent signals, and so on. If si->code
is SI_USER
or SI_QUEUE
, you can examine si->pid
to find out which process sent the signal.
It is also recommended to clear the entire struct sigaction
via eg memset(&sa, 0, sizeof sa);
prior to setting any of the fields. (This is because some of the fields may or may not be unions; clearing the entire structure to all zeros ensures "safe" values for the unused fields.)
Hmm, did I forget something? If you notice something I missed, please let me know in the comments, so I can fix this answer.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.