显然,课程的目的是将代码修改为
使用一个单独的线程,它接收调用sigwait()
或sigwaitinfo()
循环的信号。信号必须被阻塞(首先,并且一直,对于所有线程),或者操作是未指定的(或者信号被传递到另一个线程)。
这样就没有信号处理函数本身,这将仅限于异步信号安全函数。调用sigwait()
/的线程sigwaitinfo()
是一个完全正常的线程,并且不受任何与信号或信号处理程序相关的限制。
(还有其他接收信号的方法,例如使用设置全局标志的信号处理程序,并进行循环检查。其中大多数导致忙等待,运行无操作循环,无用地消耗 CPU 时间:a非常糟糕的解决方案。我在这里描述的方式不会浪费 CPU 时间:内核在调用sigwait()
/时会将线程置于睡眠状态sigwaitinfo()
,只有在信号到达时才会将其唤醒。如果你想限制睡眠的持续时间,你可以改为使用sigtimedwait()
。)
由于printf()
等人。不能保证是线程安全的,您可能应该使用 apthread_mutex_t
来保护输出到标准输出——换句话说,这样两个线程就不会尝试同时输出。
在 Linux 中这不是必需的,因为 GNU C printf()
(版本除外_unlocked()
)是线程安全的;对这些函数的每次调用都已经使用了一个内部互斥体。
请注意,C 库可能会缓存输出,因此要确保输出数据,您需要调用fflush(stdout);
.
如果您想以原子方式使用多个printf()
、fputs()
或类似调用,而其他线程无法在其间注入输出,则互斥锁特别有用。因此,建议使用互斥锁,即使在简单情况下在 Linux 上不需要它。(是的,您确实希望fflush()
在持有互斥锁的同时也这样做,尽管如果输出阻塞可能会导致互斥锁被持有很长时间。)
我个人会以完全不同的方式解决整个问题——我会write(STDERR_FILENO,)
在信号处理程序中使用输出到标准错误,并将主程序输出到标准输出;没有线程或任何特殊需要,只是信号处理程序中的一个简单的低级写循环。严格来说,我的程序的行为会有所不同,但对于最终用户来说,结果看起来非常相似。(除了可以将输出重定向到不同的终端窗口,并并排查看它们;或将它们重定向到辅助脚本/程序,这些脚本/程序将纳秒挂钟时间戳添加到每个输入行;以及在调查时有用的其他类似技巧事物。)
就个人而言,我发现了从原始问题到“正确解决方案”的跳跃——如果我所描述的确实是正确的解决方案;我确实认为这有点牵强。当 Saf 提到正确的解决方案应该使用 pthreads 时,我才意识到这种方法。
我希望您能从中找到信息,但不要剧透。
2013-03-13 编辑:
这是writefd()
我用来安全地将数据从信号处理程序写入描述符的函数。我还包括了包装函数wrout()
,wrerr()
您可以使用它们分别将字符串写入标准输出或标准错误。
#include <unistd.h>
#include <string.h>
#include <errno.h>
/**
* writefd() - A variant of write(2)
*
* This function returns 0 if the write was successful, and the nonzero
* errno code otherwise, with errno itself kept unchanged.
* This function is safe to use in a signal handler;
* it is async-signal-safe, and keeps errno unchanged.
*
* Interrupts due to signal delivery are ignored.
* This function does work with non-blocking sockets,
* but it does a very inefficient busy-wait loop to do so.
*/
int writefd(const int descriptor, const void *const data, const size_t size)
{
const char *head = (const char *)data;
const char *const tail = (const char *)data + size;
ssize_t bytes;
int saved_errno, retval;
/* File descriptor -1 is always invalid. */
if (descriptor == -1)
return EINVAL;
/* If there is nothing to write, return immediately. */
if (size == 0)
return 0;
/* Save errno, so that it can be restored later on.
* errno is a thread-local variable, meaning its value is
* local to each thread, and is accessible only from the same thread.
* If this function is called in an interrupt handler, this stores
* the value of errno for the thread that was interrupted by the
* signal delivery. If we restore the value before returning from
* this function, all changes this function may do to errno
* will be undetectable outside this function, due to thread-locality.
*/
saved_errno = errno;
while (head < tail) {
bytes = write(descriptor, head, (size_t)(tail - head));
if (bytes > (ssize_t)0) {
head += bytes;
} else
if (bytes != (ssize_t)-1) {
errno = saved_errno;
return EIO;
} else
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
/* EINTR, EAGAIN and EWOULDBLOCK cause the write to be
* immediately retried. Everything else is an error. */
retval = errno;
errno = saved_errno;
return retval;
}
}
errno = saved_errno;
return 0;
}
/**
* wrout() - An async-signal-safe alternative to fputs(string, stdout)
*
* This function will write the specified string to standard output,
* and return 0 if successful, or a nonzero errno error code otherwise.
* errno itself is kept unchanged.
*
* You should not mix output to stdout and this function,
* unless stdout is set to unbuffered.
*
* Unless standard output is a pipe and the string is at most PIPE_BUF
* bytes long (PIPE_BUF >= 512), the write is not atomic.
* This means that if you use this function in a signal handler,
* or in multiple threads, the writes may be interspersed with each other.
*/
int wrout(const char *const string)
{
if (string)
return writefd(STDOUT_FILENO, string, strlen(string));
else
return 0;
}
/**
* wrerr() - An async-signal-safe alternative to fputs(string, stderr)
*
* This function will write the specified string to standard error,
* and return 0 if successful, or a nonzero errno error code otherwise.
* errno itself is kept unchanged.
*
* You should not mix output to stderr and this function,
* unless stderr is set to unbuffered.
*
* Unless standard error is a pipe and the string is at most PIPE_BUF
* bytes long (PIPE_BUF >= 512), the write is not atomic.
* This means that if you use this function in a signal handler,
* or in multiple threads, the writes may be interspersed with each other.
*/
int wrerr(const char *const string)
{
if (string)
return writefd(STDERR_FILENO, string, strlen(string));
else
return 0;
}
如果文件描述符引用管道,writefd()
则可用于以PIPE_BUF
原子方式写入(至少 512)字节。 writefd()
也可以在 I/O 密集型应用程序中用于将信号(如果使用sigqueue()
、相关值、整数或指针引发)转换为套接字或管道输出(数据),从而更容易多路复用多个 I/O流和信号处理。变体(带有标记为 close-on-exec 的额外文件描述符)通常用于轻松检测子进程是执行了另一个进程还是失败了;否则很难检测到哪个进程——原始子进程或执行的进程——退出了。
在对这个答案的评论中,有一些讨论,以及修改errno
的事实是否使其不适合信号处理程序。write(2)
errno
首先,POSIX.1-2008(及更早版本)将异步信号安全函数定义为可以从信号处理程序安全调用的函数。2.4.3 信号动作一章包括此类功能的列表,包括write()
. 请注意,它还明确指出“获得 errno 值的操作和为 errno 分配值的操作应是异步信号安全的。”
这意味着 POSIX.1 打算write()
在信号处理程序中安全使用,并且errno
也可以对其进行操作以避免被中断的线程在errno
.
因为errno
是线程局部变量,所以每个线程都有自己的errno
. 传递信号时,它总是会中断进程中现有的线程之一。信号可以定向到特定线程,但通常内核决定哪个线程获得进程范围的信号;它因系统而异。如果只有一个线程,即初始线程或主线程,那么显然是被中断的线程。所有这一切意味着,如果信号处理程序保存errno
它最初看到的值,并在它返回之前将其恢复,则更改errno
在信号处理程序之外是不可见的。
不过,有一种方法可以检测到它,在 POSIX.1-2008 中也通过谨慎的措辞暗示了这一点:
从技术上讲,&errno
它几乎总是有效的(取决于应用的系统、编译器和标准),并产生int
保存当前线程错误代码的变量的地址。因此,另一个线程可以监视另一个线程的错误代码,是的,这个线程会在信号处理程序中看到对它的更改。但是,不能保证其他线程能够原子地访问错误代码(尽管它在许多架构上是原子的):这种“监视”无论如何都只会提供信息。
遗憾的是,几乎所有 C 中的信号处理程序示例都使用 stdio.hprintf()
等。不仅在很多层面上都是错误的——从非异步安全到缓存问题,可能是对FILE
字段的非原子访问,如果被中断的代码同时也在执行 I/O——而且是正确的解决方案在此编辑中使用unistd.h
与我的示例类似的方法同样简单。在信号处理程序中使用 stdio.h I/O 的基本原理似乎是“它通常有效”。我个人讨厌这一点,因为例如暴力也“通常有效”。我认为它愚蠢和/或懒惰。
我希望你发现这个信息。